parallel-rdp: stop using the C++ runtime

parallel-RDP and its frontend glue no longer call operator new or
delete, register a destructor with __cxa_atexit, or carry type_info.

- Objects live on the C heap. Util::create and Util::destroy do
  malloc/free with the constructor and destructor run in place. A type
  aligned past 8 bytes comes from memalign. heap_new aborts on out of
  memory, as operator new does, and is out of line so a call site stays
  one call. A destructor that does work runs out of line once per type,
  as a deleting destructor does.
- Virtual interfaces become structs of a function pointer and a user
  pointer: the logging hook, Vulkan::DeviceFactory,
  DebugChannelInterface and the renderer's ValidationInterface. Renderer
  and VideoInterface keep their debug channel as a member, so they have
  no vtable left. Crash reports go through one cold, out-of-line
  report_rdp_crash.
- rdp.cpp keeps its static timestamp and image handles in
  Util::HeldHandle, which has no destructor. They were already dropped
  by hand while the device is up, and now nothing runs at exit, after
  the device is gone. Assigning a handle drops the old reference before
  storing the new one, so a temporary handed in is known empty when it
  dies.
- free_allocators stays out of line and drops the slot stores that
  num_allocators already makes dead.
- parallel.cpp and parallel_hle.cpp build without exceptions, as the
  rest of the renderer does. Nothing there throws, and sanitizer builds
  otherwise give them landing pads.

GCC 13 x86-64 -O2 against master, parallel-RDP objects and rdp.o:
- 95091 -> 95024 instructions, text -351 bytes, data -372 bytes.
- Hot paths: load_tile -7 and validate_draw_state -30 instructions (its
  crash report moved out of line), complete_frame -9, deinit -12.
  begin_frame and the other per-frame handle code are unchanged.

tools/ci/check_cxx_runtime.py, run in the parallel-RDP CI job, fails if
any object under mupen64plus-video-paraLLEl references the runtime
again, or defines a vtable or type_info. It finds nine such objects on
master.
This commit is contained in:
libretroadmin committed 2026-11-02 18:17:01 +01:00
1 parent 080fef16de
commit fca2a07806
26 files changed
+365 -120

No files matched your search

+1
View File
@@ -163,6 +163,7 @@ test-parallel-rdp-linux-x64:
- make -j$(nproc) platform=unix WITH_DYNAREC=$WITH_DYNAREC HAVE_THR_AL=$HAVE_THR_AL HAVE_PARALLEL=$HAVE_PARALLEL HAVE_PARALLEL_RSP=$HAVE_PARALLEL_RSP
- make platform=unix HAVE_PARALLEL=$HAVE_PARALLEL lrhost_vk
- python3 tools/rdp_no_storage_8bit_bank.py --check
- python3 tools/ci/check_cxx_runtime.py
- python3 tools/gen_upscaler_shaders.py --check
- python3 tools/ci/parallel_rdp_regress.py --core ./parallel_n64_libretro.so --host ./lrhost_vk --out parallel-rdp-artifacts
- python3 tools/ci/parallel_rdp_syncval.py --core ./parallel_n64_libretro.so --host ./lrhost_vk
+2 -1
View File
@@ -650,7 +650,8 @@ ifeq ($(HAVE_PARALLEL),1)
INCFLAGS += $(PARALLEL_RDP_INCLUDE_DIRS)
SOURCES_CXX += $(PARALLEL_RDP_SOURCES_CXX) $(VIDEODIR_PARALLEL)/parallel.cpp $(VIDEODIR_PARALLEL)/rdp.cpp $(VIDEODIR_PARALLEL)/parallel_hle.cpp
# Nothing there throws, catches or uses the standard library
PARALLEL_RDP_NOEXCEPT_OBJECTS := $(PARALLEL_RDP_SOURCES_CXX:.cpp=.o) $(VIDEODIR_PARALLEL)/rdp.o
PARALLEL_RDP_NOEXCEPT_OBJECTS := $(PARALLEL_RDP_SOURCES_CXX:.cpp=.o) $(VIDEODIR_PARALLEL)/rdp.o \
$(VIDEODIR_PARALLEL)/parallel.o $(VIDEODIR_PARALLEL)/parallel_hle.o
SOURCES_C += $(PARALLEL_RDP_SOURCES_C)
LDFLAGS += $(PARALLEL_RDP_LDFLAGS)
HAVE_RDP_EMIT := 1
@@ -60,7 +60,7 @@ static ShaderBank *create_shader_bank(Vulkan::Device &device, Vulkan::ResourceLa
const VideoInterface &vi, const Renderer &renderer)
{
ShaderDefineResolver resolver = { &vi, &renderer };
return new ShaderBank(device, layout, resolver);
return Util::create<ShaderBank>(device, layout, resolver);
}
namespace
@@ -96,11 +96,11 @@ CommandProcessor::CommandProcessor(Vulkan::Device &device_, void *rdram_ptr,
if (const char *env = getenv("PARALLEL_RDP_DUMP_PATH"))
{
dump_writer = new RDPDumpWriter;
dump_writer = Util::create<RDPDumpWriter>();
if (!dump_writer->init(env, rdram_size, hidden_rdram_size))
{
LOGE("Failed to init RDP dump: %s.\n", env);
delete dump_writer;
Util::destroy(dump_writer);
dump_writer = nullptr;
}
else
@@ -209,7 +209,7 @@ CommandProcessor::CommandProcessor(Vulkan::Device &device_, void *rdram_ptr,
// Here, where ShaderBank is complete.
CommandProcessor::ShaderBankOwner::~ShaderBankOwner()
{
delete bank;
Util::destroy(bank);
}
CommandProcessor::~CommandProcessor()
@@ -220,7 +220,7 @@ CommandProcessor::~CommandProcessor()
// destroyed before the ring would otherwise be.
ring.teardown_thread();
rdp_upscaler_free(upscaler);
delete dump_writer;
Util::destroy(dump_writer);
}
void CommandProcessor::begin_frame_context()
@@ -1486,7 +1486,7 @@ CommandProcessor::CoherencyNodePool::~CoherencyNodePool()
auto *node = static_cast<CoherencyNode *>(lists[i]);
lists[i] = node->next;
node->~CoherencyNode();
::operator delete(node);
free(node);
}
}
@@ -1648,7 +1648,7 @@ static CoherencyNode *take_coherency_node(mpsc_stack_node_t **spare, mpsc_stack_
unsigned capacity = 8;
while (capacity < n)
capacity <<= 1;
void *mem = ::operator new(sizeof(CoherencyNode) + capacity * sizeof(CoherencyCopy));
void *mem = Util::heap_new(sizeof(CoherencyNode) + capacity * sizeof(CoherencyCopy));
auto *node = UTIL_PLACEMENT_NEW(mem) CoherencyNode;
node->capacity = capacity;
return node;
@@ -1587,32 +1587,41 @@ void Renderer::fixup_triangle_setup(TriangleSetup &setup) const
setup.flags |= TRIANGLE_SETUP_FILL_COPY_RASTER_BIT;
}
// Out of line and cold: draws and loads pay only the test of validation_iface.
#if defined(__GNUC__) || defined(__clang__)
__attribute__((noinline, cold))
#endif
void Renderer::report_rdp_crash(ValidationError err, const char *msg) const
{
validation_iface->report_rdp_crash(validation_iface->user, err, msg);
}
void Renderer::validate_draw_state() const
{
if ((stream.static_raster_state.flags & RASTERIZATION_FILL_BIT) != 0)
{
if (fb.fmt == FBFormat::I4)
{
validation_iface->report_rdp_crash(ValidationError::Fill4bpp,
report_rdp_crash(ValidationError::Fill4bpp,
"Attempted to use Fill mode on 4bpp surface.");
}
if ((stream.depth_blend_state.flags & DEPTH_BLEND_DEPTH_TEST_BIT) != 0)
{
validation_iface->report_rdp_crash(ValidationError::FillDepthTest,
report_rdp_crash(ValidationError::FillDepthTest,
"Attempted to use Fill mode with depth test.");
}
if ((stream.depth_blend_state.flags & DEPTH_BLEND_IMAGE_READ_ENABLE_BIT) != 0)
{
validation_iface->report_rdp_crash(ValidationError::FillImageReadEnable,
report_rdp_crash(ValidationError::FillImageReadEnable,
"Attempted to use Fill mode with image read enable.");
}
if ((stream.depth_blend_state.flags & DEPTH_BLEND_DEPTH_UPDATE_BIT) != 0 &&
!constants.use_prim_depth)
{
validation_iface->report_rdp_crash(ValidationError::FillDepthWrite,
report_rdp_crash(ValidationError::FillDepthWrite,
"Attempted to use Fill mode with depth write enabled.");
}
}
@@ -1620,7 +1629,7 @@ void Renderer::validate_draw_state() const
{
if (fb.fmt == FBFormat::RGBA8888)
{
validation_iface->report_rdp_crash(ValidationError::Copy32bpp,
report_rdp_crash(ValidationError::Copy32bpp,
"Attempted to use Copy mode on 32bpp surface.");
}
}
@@ -1962,7 +1971,7 @@ void Renderer::submit_rasterization(Vulkan::CommandBuffer &cmd, Vulkan::Buffer &
cmd.extract_pipeline_state(compile);
if (pending_async_pipeline_insert(compile.hash))
{
auto *node = new PipelineNode;
auto *node = Util::create<PipelineNode>();
node->compile = rdp_move(compile);
node->kind = PIPELINE_RECORD_COMPUTE;
node->ahead = 0;
@@ -2341,7 +2350,7 @@ void Renderer::submit_render_pass(Vulkan::CommandBuffer &cmd)
render_pass_start = cmd.write_timestamp(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
if (debug_channel)
cmd.begin_debug_channel(this, "Debug", 16 * 1024 * 1024);
cmd.begin_debug_channel(&debug_channel_iface, "Debug", 16 * 1024 * 1024);
// Here we run 3 dispatches in parallel. Span setup and TMEM instances are low occupancy kind of jobs, but the binning
// pass should dominate here unless the workload is trivial.
@@ -3276,7 +3285,7 @@ void Renderer::load_tile(uint32_t tile, const LoadTileInfo &info)
{
if ((info.thi >> 2) > (info.tlo >> 2))
{
validation_iface->report_rdp_crash(ValidationError::InvalidMultilineLoadTlut,
report_rdp_crash(ValidationError::InvalidMultilineLoadTlut,
"Attempting to load multiple lines in TLUT.");
}
}
@@ -3413,7 +3422,7 @@ void Renderer::load_tile_iteration(uint32_t tile, const LoadTileInfo &info, uint
{
LOGE("4-bit VRAM pointer crashes the RDP.\n");
if (validation_iface)
validation_iface->report_rdp_crash(ValidationError::LoadTile4bpp, "4-bit VRAM pointer crashes the RDP.");
report_rdp_crash(ValidationError::LoadTile4bpp, "4-bit VRAM pointer crashes the RDP.");
return;
}
@@ -3773,7 +3782,7 @@ void Renderer::pipeline_work(void *user, mpsc_stack_node_t *item)
if (node->urgent)
retro_atomic_fetch_add_int(&renderer->pipeline_ahead_urgent_done, 1);
if (!node->ahead)
delete node;
Util::destroy(node);
}
void Renderer::build_pipelines_ahead(const pipeline_record_t *records, size_t count)
@@ -87,18 +87,17 @@ enum class ValidationError
Copy32bpp
};
class ValidationInterface
struct ValidationInterface
{
public:
virtual ~ValidationInterface() = default;
// Validation errors may be called from a thread as errors are encountered.
// Reports situations that would cause fatal error on a real RDP.
// We only opt to report these situations rather than deliberately crashing the renderer.
// Handling crashes is only relevant during development of N64 homebrew.
virtual void report_rdp_crash(ValidationError err, const char *msg) = 0;
void (*report_rdp_crash)(void *user, ValidationError err, const char *msg);
void *user;
};
class Renderer : public Vulkan::DebugChannelInterface
class Renderer
{
public:
explicit Renderer(CommandProcessor &processor);
@@ -176,6 +175,7 @@ private:
Vulkan::Device *device = nullptr;
Vulkan::Buffer *rdram = nullptr;
ValidationInterface *validation_iface = nullptr;
void report_rdp_crash(ValidationError err, const char *msg) const;
Vulkan::BufferHandle upscaling_reference_rdram;
Vulkan::BufferHandle upscaling_multisampled_rdram;
@@ -388,7 +388,13 @@ private:
void message(const char *tag, uint32_t code,
uint32_t x, uint32_t y, uint32_t z,
uint32_t num_words, const Vulkan::DebugChannelInterface::Word *words) override;
uint32_t num_words, const Vulkan::DebugChannelInterface::Word *words);
static void debug_channel_message(void *user, const char *tag, uint32_t code,
uint32_t x, uint32_t y, uint32_t z,
uint32_t num_words, const Vulkan::DebugChannelInterface::Word *words)
{
static_cast<Renderer *>(user)->message(tag, code, x, y, z, num_words, words);
}
bool can_support_minimum_subgroup_size(unsigned size) const;
bool supports_subgroup_size_control(uint32_t minimum_size, uint32_t maximum_size) const;
@@ -455,5 +461,8 @@ private:
retro_atomic_int_t active_submissions;
void enqueue_fence_wait(Vulkan::Fence fence);
uint64_t last_submit_ns = 0;
// last: it is read only with a debug channel on
Vulkan::DebugChannelInterface debug_channel_iface = { debug_channel_message, this };
};
}
@@ -1519,7 +1519,7 @@ Vulkan::ImageHandle VideoInterface::scanout(VkImageLayout target_layout, const S
auto cmd = device->request_command_buffer();
if (debug_channel)
cmd->begin_debug_channel(this, "VI", 32 * 1024 * 1024);
cmd->begin_debug_channel(&debug_channel_iface, "VI", 32 * 1024 * 1024);
// In the first pass, we need to read from VRAM and apply the fetch filter.
// This is either the AA filter if coverage < 7, or the dither reconstruction filter if coverage == 7 and enabled.
@@ -103,7 +103,7 @@ struct VIScanoutBuffer
class Renderer;
class VideoInterface : public Vulkan::DebugChannelInterface
class VideoInterface
{
public:
void set_device(Vulkan::Device *device);
@@ -180,7 +180,13 @@ private:
void message(const char *tag, uint32_t code,
uint32_t x, uint32_t y, uint32_t z,
uint32_t num_words, const Vulkan::DebugChannelInterface::Word *words) override;
uint32_t num_words, const Vulkan::DebugChannelInterface::Word *words);
static void debug_channel_message(void *user, const char *tag, uint32_t code,
uint32_t x, uint32_t y, uint32_t z,
uint32_t num_words, const Vulkan::DebugChannelInterface::Word *words)
{
static_cast<VideoInterface *>(user)->message(tag, code, x, y, z, num_words, words);
}
// Intermediate images, reused instead of created every frame.
mutable VIImagePool image_pool;
@@ -309,5 +315,8 @@ private:
unsigned scaling_factor, bool field_select,
const ScanoutOptions &options) const;
static bool need_fetch_bug_emulation(const Registers &reg, unsigned scaling_factor);
// last: it is read only with a debug channel on
Vulkan::DebugChannelInterface debug_channel_iface = { debug_channel_message, this };
};
}
@@ -56,6 +56,23 @@ void *memalign_alloc(size_t boundary, size_t size)
return (void *) addr;
#endif
}
void *heap_new(size_t size)
{
void *ptr = malloc(size);
if (!ptr)
abort();
return ptr;
}
void *heap_new_aligned(size_t size, size_t align)
{
void *ptr = memalign_alloc(align, size);
if (!ptr)
abort();
return ptr;
}
void memalign_free(void *ptr)
{
#if defined(_WIN32)
@@ -32,31 +32,4 @@ void memalign_free(void *ptr);
struct AlignedDeleter { void operator()(void *ptr) { memalign_free(ptr); }};
template <typename T>
struct AlignedAllocation
{
static void *operator new(size_t size)
{
void *ret = ::Util::memalign_alloc(alignof(T), size);
if (!ret) abort();
return ret;
}
static void *operator new[](size_t size)
{
void *ret = ::Util::memalign_alloc(alignof(T), size);
if (!ret) abort();
return ret;
}
static void operator delete(void *ptr)
{
return ::Util::memalign_free(ptr);
}
static void operator delete[](void *ptr)
{
return ::Util::memalign_free(ptr);
}
};
}
@@ -295,6 +295,58 @@ private:
T *data = nullptr;
};
// A reference held without a destructor, for objects of static storage:
// they are dropped by hand while the device is up, rather than by a
// destructor registered to run at exit, after it is gone. Zero is empty.
template <typename T>
struct HeldHandle
{
T *data;
// The reference goes first, and the slot is written after it,
// so a temporary handed in is known empty when it dies.
static void drop(T *object)
{
IntrusivePtr<T> last(object);
}
void reset()
{
drop(data);
data = nullptr;
}
HeldHandle &operator=(const IntrusivePtr<T> &handle)
{
IntrusivePtr<T> keep(handle);
drop(data);
data = keep.release();
return *this;
}
HeldHandle &operator=(IntrusivePtr<T> &&handle)
{
drop(data);
data = handle.release();
return *this;
}
explicit operator bool() const
{
return data != nullptr;
}
// a handle of its own to it
operator IntrusivePtr<T>() const
{
IntrusivePtr<T> handle;
IntrusivePtr<T> borrowed(data);
handle = borrowed;
borrowed.release();
return handle;
}
};
template <typename T, typename Deleter, typename ReferenceOps>
IntrusivePtr<T> IntrusivePtrEnabled<T, Deleter, ReferenceOps>::reference_from_this()
{
@@ -308,13 +360,13 @@ using DerivedIntrusivePtrType = IntrusivePtr<Derived>;
template <typename T, typename... P>
DerivedIntrusivePtrType<T> make_handle(P &&... p)
{
return DerivedIntrusivePtrType<T>(new T(Util::forward<P>(p)...));
return DerivedIntrusivePtrType<T>(Util::create_init<T>(Util::forward<P>(p)...));
}
template <typename Base, typename Derived, typename... P>
typename Base::IntrusivePtrType make_derived_handle(P &&... p)
{
return typename Base::IntrusivePtrType(new Derived(Util::forward<P>(p)...));
return typename Base::IntrusivePtrType(Util::create_init<Derived>(Util::forward<P>(p)...));
}
template <typename T>
@@ -509,7 +509,7 @@ public:
T *to_delete = read_only.insert_yield(to_move);
if (to_delete)
object_pool.free(to_delete);
delete l;
Util::destroy(l);
l = next;
}
}
@@ -548,7 +548,7 @@ public:
{
Link *next = static_cast<Link *>(retro_atomic_load_relaxed_ptr(&l->next));
object_pool.free(l->value);
delete l;
Util::destroy(l);
l = next;
}
}
@@ -569,7 +569,7 @@ public:
{
static_cast<IntrusiveHashMapEnabled<T> *>(value)->set_hash(hash);
Link *link = new Link;
Link *link = Util::create<Link>();
link->hash = hash;
link->value = value;
@@ -584,7 +584,7 @@ public:
{
if (l->hash == hash)
{
delete link;
Util::destroy(link);
object_pool.free(value);
return l->value;
}
@@ -41,7 +41,7 @@ static bool log_helper(const char *tag, const char *fmt, va_list va)
namespace Util
{
static thread_local LoggingInterface *logging_iface;
static thread_local LoggingInterface logging_iface;
bool interface_log(const char *tag, const char *fmt, ...)
{
@@ -55,7 +55,7 @@ bool interface_log(const char *tag, const char *fmt, ...)
#ifdef __LIBRETRO__
bool ret = log_helper(tag, fmt, va);
#else
bool ret = logging_iface->log(tag, fmt, va);
bool ret = logging_iface(tag, fmt, va);
#endif
va_end(va);
return ret;
@@ -28,12 +28,8 @@
namespace Util
{
class LoggingInterface
{
public:
virtual ~LoggingInterface() = default;
virtual bool log(const char *tag, const char *fmt, va_list va) = 0;
};
// Where a thread's log lines go, when not to the libretro log
typedef bool (*LoggingInterface)(const char *tag, const char *fmt, va_list va);
bool interface_log(const char *tag, const char *fmt, ...);
}
@@ -47,7 +47,7 @@ public:
UTIL_PLACEMENT_NEW(ptr) T(Util::forward<P>(p)...);
return ptr;
#else
return new T(Util::forward<P>(p)...);
return Util::create_init<T>(Util::forward<P>(p)...);
#endif
}
@@ -57,7 +57,7 @@ public:
ptr->~T();
vacants[num_vacants++] = ptr;
#else
delete ptr;
Util::destroy(ptr);
#endif
}
@@ -179,7 +179,7 @@ public:
return nullptr;
return ObjectPool<T>::construct_at(slot(idx), Util::forward<P>(p)...);
#else
return ObjectPool<T>::construct_at(::operator new(sizeof(T)), Util::forward<P>(p)...);
return ObjectPool<T>::construct_at(Util::heap_alloc<T>(sizeof(T)), Util::forward<P>(p)...);
#endif
}
@@ -190,7 +190,7 @@ public:
push_chain(index_of(ptr), index_of(ptr));
#else
ObjectPool<T>::destroy(ptr);
::operator delete(ptr);
Util::heap_free<T>(ptr);
#endif
}
@@ -22,6 +22,7 @@
#include "logging.hpp"
#include "timeline_trace_file.hpp"
#include "util_lite.hpp"
#include "thread_name.hpp"
#include "timer.hpp"
#include <string.h>
@@ -44,7 +45,7 @@ void TimelineTraceFile::Event::set_tid(const char *tid_)
TimelineTraceFile::Event *TimelineTraceFile::allocate_event()
{
auto *e = new Event;
auto *e = Util::create<Event>();
e->next = nullptr;
e->desc[0] = '\0';
e->tid[0] = '\0';
@@ -59,7 +60,7 @@ void TimelineTraceFile::submit_event(Event *e)
if (worker)
rdp_worker_push(worker, e);
else
delete e;
Util::destroy(e);
}
void TimelineTraceFile::write_event(void *self_, mpsc_stack_node_t *item)
@@ -78,7 +79,7 @@ void TimelineTraceFile::write_event(void *self_, mpsc_stack_node_t *item)
e->desc, e->tid, e->pid, end_us);
}
delete e;
Util::destroy(e);
}
static void name_io_thread(void *)
@@ -28,6 +28,7 @@
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#if defined(_MSC_VER) && !defined(__clang__)
#include <intrin.h>
#endif
@@ -88,12 +89,16 @@ static inline void copy_string(char *dst, size_t size, const char *src)
dst[i] = '\0';
}
/* below: delete on the C heap */
template <typename T>
static inline void destroy(T *t);
template <typename T>
struct DefaultDelete
{
void operator()(T *t) const
{
delete t;
destroy(t);
}
};
@@ -138,6 +143,123 @@ inline void operator delete(void *, Util::PlacementTag, void *) noexcept
#define UTIL_PLACEMENT_NEW(p) new (::Util::PlacementTag(), (p))
/* Objects on the C heap: malloc and free, with the constructor and the
destructor run in place, so nothing calls the C++ runtime's operator
new or delete. A type aligned past 8 bytes, which malloc does not
promise everywhere (32-bit MSVC), comes from memalign. */
namespace Util
{
void *memalign_alloc(size_t boundary, size_t size);
void memalign_free(void *ptr);
/* Clang and MSVC name it as the standard trait does; GCC up to 13 has
only the older builtin, which Clang now warns of. */
#if defined(__clang__) || defined(_MSC_VER)
#define UTIL_IS_TRIVIALLY_DESTRUCTIBLE(T) __is_trivially_destructible(T)
#else
#define UTIL_IS_TRIVIALLY_DESTRUCTIBLE(T) __has_trivial_destructor(T)
#endif
/* What a new-expression's operator new does: never NULL, out of memory
ends the program. Out of line, as operator new is, so a call site is a
call and nothing else. */
#if defined(__GNUC__) || defined(__clang__)
#define UTIL_HEAP_NEW_ATTRS __attribute__((malloc, returns_nonnull, alloc_size(1)))
#else
#define UTIL_HEAP_NEW_ATTRS
#endif
UTIL_HEAP_NEW_ATTRS void *heap_new(size_t size);
/* for types aligned past what malloc promises */
UTIL_HEAP_NEW_ATTRS void *heap_new_aligned(size_t size, size_t align);
template <typename T>
static inline void *heap_alloc(size_t size)
{
return alignof(T) > 8 ? heap_new_aligned(size, alignof(T)) : heap_new(size);
}
template <typename T>
static inline void heap_free(void *p)
{
if (alignof(T) > 8)
memalign_free(p);
else
free(p);
}
/* new T(p...) */
template <typename T, typename P0, typename... P>
static inline T *create(P0 &&p0, P &&... p)
{
void *mem = heap_alloc<T>(sizeof(T));
return UTIL_PLACEMENT_NEW(mem) T(forward<P0>(p0), forward<P>(p)...);
}
/* new T(p...) for any number of arguments: with none, T() (zeroed where T
has no constructor of its own), as a template forwarding a pack spells it */
template <typename T, typename... P>
static inline T *create_init(P &&... p)
{
void *mem = heap_alloc<T>(sizeof(T));
return UTIL_PLACEMENT_NEW(mem) T(forward<P>(p)...);
}
/* new T: default-initialized, as a new-expression without () leaves it */
template <typename T>
static inline T *create()
{
void *mem = heap_alloc<T>(sizeof(T));
return UTIL_PLACEMENT_NEW(mem) T;
}
/* delete t: a destructor that does anything runs out of line, once per
type, as a deleting destructor does, rather than at every call */
template <typename T>
#if defined(__GNUC__) || defined(__clang__)
__attribute__((noinline))
#elif defined(_MSC_VER)
__declspec(noinline)
#endif
void destroy_object(T *t)
{
t->~T();
heap_free<T>(t);
}
template <typename T>
static inline void destroy(T *t)
{
if (!t)
return;
if (UTIL_IS_TRIVIALLY_DESTRUCTIBLE(T))
heap_free<T>(t);
else
destroy_object(t);
}
/* new T[n] (default-initialized), and delete[] */
template <typename T>
static inline T *create_array(size_t n)
{
size_t i;
T *a = static_cast<T *>(heap_alloc<T>(n * sizeof(T)));
for (i = 0; i < n; i++)
UTIL_PLACEMENT_NEW(&a[i]) T;
return a;
}
template <typename T>
static inline void destroy_array(T *a, size_t n)
{
if (a)
{
while (n--)
a[n].~T();
heap_free<T>(a);
}
}
}
/* Builtins of GCC, Clang and MSVC alike */
#define UTIL_UNDERLYING_TYPE(T) __underlying_type(T)
#define UTIL_IS_BASE_OF(B, D) __is_base_of(B, D)
@@ -145,6 +267,7 @@ inline void operator delete(void *, Util::PlacementTag, void *) noexcept
/* Out-of-line slow paths, and the index of the highest set bit */
#if defined(_MSC_VER)
#define UTIL_NOINLINE_COLD __declspec(noinline)
#define UTIL_NOINLINE __declspec(noinline)
static inline unsigned util_bsr32(uint32_t v)
{
unsigned long r;
@@ -153,6 +276,7 @@ static inline unsigned util_bsr32(uint32_t v)
}
#else
#define UTIL_NOINLINE_COLD __attribute__((noinline, cold))
#define UTIL_NOINLINE __attribute__((noinline))
static inline unsigned util_bsr32(uint32_t v)
{
return 31u - (unsigned)__builtin_clz(v);
@@ -209,7 +333,7 @@ public:
{
unsigned i;
for (i = 0; i < count; i++)
delete items[i];
destroy(items[i]);
count = 0;
}
@@ -35,7 +35,7 @@
namespace Vulkan
{
class DebugChannelInterface;
struct DebugChannelInterface;
enum CommandBufferDirtyBits
{
@@ -1079,7 +1079,7 @@ bool Context::create_device(VkPhysicalDevice gpu_, VkSurfaceKHR surface, const c
if (device_factory)
{
device = device_factory->create_device(gpu, &device_info);
device = device_factory->create_device(device_factory->user, gpu, &device_info);
if (device == VK_NULL_HANDLE)
return false;
}
@@ -134,8 +134,8 @@ struct QueueInfo
// libretro frontend that adds what it needs to the create info.
struct DeviceFactory
{
virtual ~DeviceFactory() = default;
virtual VkDevice create_device(VkPhysicalDevice gpu, const VkDeviceCreateInfo *info) = 0;
VkDevice (*create_device)(void *user, VkPhysicalDevice gpu, const VkDeviceCreateInfo *info);
void *user;
};
class Context
@@ -39,7 +39,7 @@ DescriptorSetAllocator::DescriptorSetAllocator(Hash hash, Device *device_, const
if (!push)
{
num_per_thread = device_->num_thread_indices;
per_thread = new PerThread[num_per_thread];
per_thread = Util::create_array<PerThread>(num_per_thread);
}
VkDescriptorSetLayoutCreateInfo info = { VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO };
@@ -230,7 +230,7 @@ DescriptorSetAllocator::~DescriptorSetAllocator()
if (set_layout != VK_NULL_HANDLE)
table.vkDestroyDescriptorSetLayout(device->get_device(), set_layout, nullptr);
clear();
delete[] per_thread;
Util::destroy_array(per_thread, num_per_thread);
}
}
@@ -1879,7 +1879,7 @@ void Device::init_frame_contexts(unsigned count)
count = MAX_DEFERRED_FRAMES;
for (unsigned i = 0; i < count; i++)
{
per_frame.push_back(new PerFrame(this, i));
per_frame.push_back(Util::create<PerFrame>(this, i));
}
if (frame_context_index >= per_frame.size())
frame_context_index = 0;
@@ -4498,7 +4498,7 @@ void Device::parse_debug_channel(const PerFrame::DebugChannel &channel)
while (size != 0 && words[0].u32 >= 5 && words[0].u32 <= size)
{
channel.iface->message(channel.tag, words[1].u32,
channel.iface->message(channel.iface->user, channel.tag, words[1].u32,
words[2].u32, words[3].u32, words[4].u32,
words[0].u32 - 5, &words[5]);
size -= words[0].u32;
@@ -113,17 +113,18 @@ struct HandlePool
VulkanObjectPool<DeferredInterval> deferred_intervals;
};
class DebugChannelInterface
// Where a debug channel's messages go: a function and what it is called on
struct DebugChannelInterface
{
public:
union Word
{
uint32_t u32;
int32_t s32;
float f32;
};
virtual void message(const char *tag, uint32_t code, uint32_t x, uint32_t y, uint32_t z,
uint32_t word_count, const Word *words) = 0;
void (*message)(void *user, const char *tag, uint32_t code, uint32_t x, uint32_t y, uint32_t z,
uint32_t word_count, const Word *words);
void *user;
};
namespace Helper
@@ -275,7 +275,7 @@ void DeviceAllocator::init(Device *device_)
heaps.resize(mem_props.memoryHeapCount);
for (unsigned i = 0; i < mem_props.memoryTypeCount; i++)
{
allocators[i] = new Allocator(object_pool);
allocators[i] = Util::create<Allocator>(object_pool);
allocators[i]->set_global_allocator(this, i);
}
num_allocators = mem_props.memoryTypeCount;
@@ -404,8 +404,7 @@ void DeviceAllocator::free_allocators()
{
for (unsigned i = 0; i < num_allocators; i++)
{
delete allocators[i];
allocators[i] = nullptr;
Util::destroy_object(allocators[i]);
}
num_allocators = 0;
}
@@ -310,7 +310,7 @@ private:
Util::ObjectPool<MiniHeap> object_pool;
Allocator *allocators[VK_MAX_MEMORY_TYPES] = {};
unsigned num_allocators = 0;
void free_allocators();
UTIL_NOINLINE void free_allocators();
Device *device = nullptr;
const VolkDeviceTable *table = nullptr;
VkPhysicalDeviceMemoryProperties mem_props;
+32 -27
View File
@@ -41,7 +41,8 @@ static Util::TimelineTraceFile *timeline_trace_file;
static CommandProcessor *frontend;
static Device *device;
static Context *context;
static QueryPoolHandle begin_ts, end_ts;
/* (held without destructors: dropped by hand while the device is up) */
static Util::HeldHandle<Vulkan::QueryPoolResult> begin_ts, end_ts;
/* Set true only when init() fully succeeds (including the device meeting
* parallel-RDP's feature requirements). The frame-driving entry points
@@ -54,7 +55,7 @@ static bool device_supported = false;
/* Per sync index; the frontend's index mask is 32 bits */
#define RDP_MAX_SYNC_FRAMES 32
static retro_vulkan_image retro_images[RDP_MAX_SYNC_FRAMES];
static ImageHandle retro_image_handles[RDP_MAX_SYNC_FRAMES];
static Util::HeldHandle<Vulkan::Image> retro_image_handles[RDP_MAX_SYNC_FRAMES];
static unsigned num_retro_images;
/* RDRAM as the command processor imports it */
@@ -70,7 +71,7 @@ static const struct retro_hw_render_upscaler_interface *upscaler_iface;
static struct retro_hw_upscaler_request upscaler_request;
static struct retro_hw_upscaler_inputs upscaler_inputs;
/* Per sync index: motion vectors, depth, then the translucency mask */
static ImageHandle upscaler_handles[3 * RDP_MAX_SYNC_FRAMES];
static Util::HeldHandle<Vulkan::Image> upscaler_handles[3 * RDP_MAX_SYNC_FRAMES];
static bool upscaler_on;
/* The frames released past n, as a shrinking vector did. */
@@ -276,7 +277,7 @@ void process_commands()
*GET_GFX_INFO(DPC_CURRENT_REG) = *GET_GFX_INFO(DPC_END_REG);
}
static QueryPoolHandle refresh_begin_ts;
static Util::HeldHandle<Vulkan::QueryPoolResult> refresh_begin_ts;
void profile_refresh_begin()
{
@@ -436,14 +437,14 @@ static bool create_processor(unsigned factor)
}
/* (the old one goes first: both at once may not fit) */
delete frontend;
frontend = new CommandProcessor(*device, reinterpret_cast<void *>(rdram_base),
Util::destroy(frontend);
frontend = Util::create<CommandProcessor>(*device, reinterpret_cast<void *>(rdram_base),
rdram_offset, 8 * 1024 * 1024, 4 * 1024 * 1024, flags);
if (!frontend->device_is_supported())
{
log_cb(RETRO_LOG_ERROR, "This device probably does not support 16-bit storage. Make sure you're using up-to-date drivers!\n");
delete frontend;
Util::destroy(frontend);
frontend = nullptr;
return false;
}
@@ -662,8 +663,8 @@ bool init()
set_num_sync_frames(num_frames);
delete device;
device = new Device;
Util::destroy(device);
device = Util::create<Device>();
device->set_context(*context);
pipeline_files_load();
device->init_frame_contexts(num_sync_frames);
@@ -708,15 +709,15 @@ void deinit()
set_num_sync_frames(0);
upscaler_iface = nullptr;
upscaler_on = false;
delete frontend;
Util::destroy(frontend);
frontend = nullptr;
if (device)
pipeline_files_save();
delete device;
Util::destroy(device);
device = nullptr;
delete context;
Util::destroy(context);
context = nullptr;
delete timeline_trace_file;
Util::destroy(timeline_trace_file);
timeline_trace_file = nullptr;
}
@@ -925,17 +926,18 @@ namespace RDP
{
// vkCreateDevice through the frontend (negotiation v2), which adds the
// extensions and features it wants, e.g. for its low-latency modes.
struct FrontendDeviceFactory : Vulkan::DeviceFactory
struct FrontendDeviceFactory
{
retro_vulkan_create_device_wrapper_t wrapper;
void *opaque;
VkDevice create_device(VkPhysicalDevice gpu, const VkDeviceCreateInfo *info) override
{
return wrapper(gpu, opaque, info);
}
};
static VkDevice frontend_create_device(void *user, VkPhysicalDevice gpu, const VkDeviceCreateInfo *info)
{
auto *f = static_cast<FrontendDeviceFactory *>(user);
return f->wrapper(gpu, f->opaque, info);
}
static bool create_device(struct retro_vulkan_context *frontend_context, VkInstance instance, VkPhysicalDevice gpu,
VkSurfaceKHR surface, PFN_vkGetInstanceProcAddr get_instance_proc_addr,
const char **required_device_extensions, unsigned num_required_device_extensions,
@@ -944,15 +946,15 @@ static bool create_device(struct retro_vulkan_context *frontend_context, VkInsta
if (!Vulkan::Context::init_loader(get_instance_proc_addr))
return false;
delete ::RDP::context;
::RDP::context = new Vulkan::Context;
Util::destroy(::RDP::context);
::RDP::context = Util::create<Vulkan::Context>();
::Vulkan::Context::SystemHandles handles;
if (const char *env = getenv("PARALLEL_RDP_TIMELINE_TRACE"))
{
delete ::RDP::timeline_trace_file;
::RDP::timeline_trace_file = new Util::TimelineTraceFile(env);
Util::destroy(::RDP::timeline_trace_file);
::RDP::timeline_trace_file = Util::create<Util::TimelineTraceFile>(env);
handles.timeline_trace_file = ::RDP::timeline_trace_file;
}
@@ -965,7 +967,7 @@ static bool create_device(struct retro_vulkan_context *frontend_context, VkInsta
::RDP::context->set_device_factory(nullptr);
if (!ok)
{
delete ::RDP::context;
Util::destroy(::RDP::context);
::RDP::context = nullptr;
return false;
}
@@ -999,9 +1001,12 @@ bool parallel_create_device2(struct retro_vulkan_context *frontend_context, VkIn
retro_vulkan_create_device_wrapper_t create_device_wrapper, void *opaque)
{
static const VkPhysicalDeviceFeatures no_features = {};
::RDP::FrontendDeviceFactory factory;
factory.wrapper = create_device_wrapper;
factory.opaque = opaque;
::RDP::FrontendDeviceFactory frontend;
Vulkan::DeviceFactory factory;
frontend.wrapper = create_device_wrapper;
frontend.opaque = opaque;
factory.create_device = ::RDP::frontend_create_device;
factory.user = &frontend;
return ::RDP::create_device(frontend_context, instance, gpu, surface, get_instance_proc_addr,
nullptr, 0, &no_features, &factory);
}
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""parallel-RDP and its frontend glue must not use the C++ runtime.
They allocate with malloc and free and construct in place (Util::create,
Util::destroy), call through structs of function pointers instead of
virtual interfaces, and hold their static handles without destructors.
So none of their objects references operator new or delete, the ABI
support library (__cxa_*, the type_info vtables), a personality routine
or the standard library, and none defines a vtable, type_info or type
name. Any of those coming back, in a header used by them as much as in
their own code, fails this, in the objects of a finished build.
check_cxx_runtime.py [dir ...] (run after the build, from the repo root)
"""
import os, re, subprocess, sys
DIRS = sys.argv[1:] or ["mupen64plus-video-paraLLEl"]
RUNTIME = re.compile(r"^(_Znw|_Zna|_Zdl|_Zda|__cxa_|_ZTI|_ZTS|_ZTV|__gxx_personality|_ZSt|_ZNSt)")
def main():
objects = 0
found = []
for d in DIRS:
for root, _, files in os.walk(d):
for f in sorted(files):
if not f.endswith(".o"):
continue
path = os.path.join(root, f)
objects += 1
out = subprocess.run(["nm", path], stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, text=True).stdout
names = sorted(set(line.split()[-1] for line in out.splitlines()
if line.split() and RUNTIME.match(line.split()[-1])))
if names:
found.append("%s: %s" % (path, " ".join(names)))
if not objects:
print("check_cxx_runtime: no objects; build first")
return 1
for line in found:
print("C++ runtime in %s" % line)
print("check_cxx_runtime: %s" % ("ok (%d objects)" % objects if not found
else "%d objects use it" % len(found)))
return 1 if found else 0
if __name__ == "__main__":
sys.exit(main())