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 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-22 12:18:31 -07:00
parent 8866c0375b
commit c2cf5ffd03
4 changed files with 29 additions and 14 deletions
+4
View File
@@ -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);
+8 -2
View File
@@ -1,4 +1,5 @@
#include "gpu_vram_dirty.h"
#include "psx_thread_local.h"
#include <stdlib.h>
#include <string.h>
@@ -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;
+9 -5
View File
@@ -19,6 +19,7 @@
#include "render_pass.h"
#include "render_pass_worker.h"
#include "psx_execution.h"
#include <setjmp.h>
#include <stdio.h>
@@ -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;
+8 -7
View File
@@ -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<Job> prepared[2];
@@ -200,19 +202,18 @@ void run() {
{
std::unique_lock<std::mutex> 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)) {