From c2cf5ffd039b77cf9587809f2e3c5b0eef551d50 Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:18:31 -0700 Subject: [PATCH] Render-pass worker: run concurrently with emulation The worker only ran inside the emulation thread's pacing idle window, so heavy scenes (short windows) lost their in-between frames. It now runs as soon as a job is queued, on its own core, bounded by the job's own presentation period. Shared state that made concurrent runs crash or race, now per thread: - render_pass_mmio_read_allowed could longjmp to the emulation thread's synchronous-pass jump buffer from the worker (instant crash). The abort state (s_abort_jmp, s_abort_armed, s_pass_cpu, s_device_read_fault, s_attempt) is thread-local, and a private execution aborts its own job through g_psx_execution_abort. - The VRAM dirty mask: marks from the worker's private snapshot are dropped (gpu_vram_dirty_set_private_thread). Plan changes already quiesce (join) the worker before the hook table is rebuilt. R4 release, RTX 4080 8K, driven lap, DXGI on-screen count: 4/4 runs stable (was 1 crash in 2); 50-60 distinct/s on straights. Co-Authored-By: Claude Opus 5.5 --- runtime/include/gpu_vram_dirty.h | 4 ++++ runtime/src/gpu_vram_dirty.c | 10 ++++++++-- runtime/src/render_pass.c | 14 +++++++++----- runtime/src/render_pass_worker.cpp | 15 ++++++++------- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/runtime/include/gpu_vram_dirty.h b/runtime/include/gpu_vram_dirty.h index d34b15b5..242abb8a 100644 --- a/runtime/include/gpu_vram_dirty.h +++ b/runtime/include/gpu_vram_dirty.h @@ -39,6 +39,10 @@ uint32_t gpu_vram_dirty_row_count(void); /* 512-bit mask: bit y set ⇒ scanline y dirty. Non-owning. */ const uint64_t *gpu_vram_dirty_mask(void); +/* The calling thread runs a private execution (render-pass worker): its marks + * are dropped. */ +void gpu_vram_dirty_set_private_thread(int on); + /* 1 if PSX_NET_VRAM_DIRTY_VERIFY is set (non-zero). */ int gpu_vram_dirty_verify_enabled(void); diff --git a/runtime/src/gpu_vram_dirty.c b/runtime/src/gpu_vram_dirty.c index f843ef3d..8d27bd9f 100644 --- a/runtime/src/gpu_vram_dirty.c +++ b/runtime/src/gpu_vram_dirty.c @@ -1,4 +1,5 @@ #include "gpu_vram_dirty.h" +#include "psx_thread_local.h" #include #include @@ -8,6 +9,10 @@ int g_psx_vram_dirty_tracking = 0; /* All-dirty until the first successful incremental checkpoint (while on). */ static uint64_t s_mask[GPU_VRAM_DIRTY_H / 64u]; static int s_all = 1; +/* Set on the render-pass worker: its VRAM writes land in a private snapshot + * and must not mark (or race on) the live machine's mask. */ +static PSX_THREAD_LOCAL int s_private; +void gpu_vram_dirty_set_private_thread(int on) { s_private = on ? 1 : 0; } static int s_verify = -1; /* -1 = unread env */ static void ensure_verify_env(void) @@ -40,6 +45,7 @@ int gpu_vram_dirty_tracking(void) void gpu_vram_dirty_mark_row_impl(uint32_t y) { + if (s_private) return; y &= (GPU_VRAM_DIRTY_H - 1u); s_mask[y >> 6] |= (uint64_t)1u << (y & 63u); } @@ -47,7 +53,7 @@ void gpu_vram_dirty_mark_row_impl(uint32_t y) void gpu_vram_dirty_mark_rows(uint32_t y0, uint32_t h) { uint32_t i; - if (!g_psx_vram_dirty_tracking || h == 0u) + if (!g_psx_vram_dirty_tracking || h == 0u || s_private) return; if (h >= GPU_VRAM_DIRTY_H) { gpu_vram_dirty_mark_all(); @@ -71,7 +77,7 @@ void gpu_vram_dirty_mark_rect(int x, int y, int w, int h) void gpu_vram_dirty_mark_all(void) { - if (!g_psx_vram_dirty_tracking) + if (!g_psx_vram_dirty_tracking || s_private) return; memset(s_mask, 0xff, sizeof s_mask); s_all = 1; diff --git a/runtime/src/render_pass.c b/runtime/src/render_pass.c index f9d03e83..855b03ab 100644 --- a/runtime/src/render_pass.c +++ b/runtime/src/render_pass.c @@ -19,6 +19,7 @@ #include "render_pass.h" #include "render_pass_worker.h" +#include "psx_execution.h" #include #include @@ -267,11 +268,11 @@ static RenderPassCheckpoint s_ck; * a recorded pass must also roll back gpu.c's public copy. */ static uint16_t s_pass_cpu_vram[1024u * 512u]; static PsxCycleFreeze s_freeze; -static jmp_buf s_abort_jmp; -static volatile int s_abort_armed; -static const CPUState* s_pass_cpu; +static PSX_THREAD_LOCAL jmp_buf s_abort_jmp; /* per thread: the render-pass worker runs alongside */ +static PSX_THREAD_LOCAL volatile int s_abort_armed; +static PSX_THREAD_LOCAL const CPUState* s_pass_cpu; static int s_nesting; -static int s_device_read_fault; +static PSX_THREAD_LOCAL int s_device_read_fault; static RenderPassStats s_stats; static int s_verify = -1; @@ -283,7 +284,7 @@ static uint32_t s_plan_period = 2; static uint64_t s_plan_serial, s_restored_plan; static uint64_t s_restored_cycle, s_restored_stores; static int s_restored_valid; -static RenderPassFailure s_attempt; +static PSX_THREAD_LOCAL RenderPassFailure s_attempt; static const char *s_checkpoint_failure; static RenderStereoStats s_stereo; static int s_pair_busy; @@ -646,6 +647,9 @@ int render_pass_mmio_read_allowed(uint32_t phys, uint32_t width) { (phys >= 0x1F801820u && phys <= 0x1F801827u) || /* MDEC */ (phys >= 0x1F801C00u && phys <= 0x1F801FFFu))) /* SPU */ return 1; + /* A private execution (the render-pass worker) aborts its own job; it + * must never touch the synchronous pass's state or jump buffer. */ + if (g_psx_execution_abort) g_psx_execution_abort("device_read", phys); s_device_read_fault = 1; s_stats.device_reads++; s_stats.last_device_read = phys; diff --git a/runtime/src/render_pass_worker.cpp b/runtime/src/render_pass_worker.cpp index 75ea7c40..66fab603 100644 --- a/runtime/src/render_pass_worker.cpp +++ b/runtime/src/render_pass_worker.cpp @@ -2,6 +2,7 @@ * values, the worker owns guest execution, and only the render owner consumes * the sealed GPU list. Neither owner waits for an extra frame. */ #include "render_pass_worker.h" +#include "gpu_vram_dirty.h" #include "host_span_ring.h" #include "mod_plugins.h" #include "mod_execution.h" @@ -180,6 +181,7 @@ void run() { * normal lost those slices to desktop work and the extra came in late. */ SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL); #endif + gpu_vram_dirty_set_private_thread(1); /* Allocation and page commitment belong to the optional worker. The source * drops extras until both complete private contexts are ready. */ std::unique_ptr prepared[2]; @@ -200,19 +202,18 @@ void run() { { std::unique_lock lock(W.mutex); W.wake.wait(lock, [] { - return W.stop || (W.idle_until > W.failed_window && - (W.jobs[0]->queued.load() || W.jobs[1]->queued.load())); + return W.stop || W.jobs[0]->queued.load() || W.jobs[1]->queued.load(); }); if (W.stop) return; - const uint64_t now = host_clock_us(); - if (now + W.estimated_us + 500u >= W.idle_until) { - W.failed_window = W.idle_until; continue; - } - execution_deadline = W.idle_until - 500u; for (auto &slot : W.jobs) if (slot->queued.load(std::memory_order_acquire) && (!job || slot->serial < job->serial)) job = slot.get(); if (job) job->queued.store(false); + /* Runs at once on its own core, concurrently with the emulation + * thread (its execution is private: memory, GPU/GTE/PGXP + * snapshots, thread-local abort and dirty state). It has until + * its own presentation period ends; a later image is dropped. */ + if (job) execution_deadline = host_clock_us() + 16683u * (job->period ? job->period : 2u); } if (!job) continue; if (!job->consumer_done.load(std::memory_order_acquire)) {