mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user