mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 18:25:35 -04:00
368 lines
16 KiB
C++
368 lines
16 KiB
C++
/* Double-buffered private guest draw execution. The emulation owner publishes
|
|
* 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 "host_span_ring.h"
|
|
#include "mod_plugins.h"
|
|
#include "mod_execution.h"
|
|
#include "mod_runtime.h"
|
|
#include "cpu_state.h"
|
|
#include "gpu.h"
|
|
#include "gpu_gl_renderer.h"
|
|
#include "gte_view.h"
|
|
#include "pgxp.h"
|
|
#include "psx_memory.h"
|
|
#include "psx_cycles.h"
|
|
#include "psx_cycle_freeze.h"
|
|
#include "psx_execution.h"
|
|
#include "psx_icache.h"
|
|
#include "psx_bios_backend.h"
|
|
#include "overlay_loader.h"
|
|
#include "render_thread.h"
|
|
#include "interrupts.h"
|
|
#include "timers.h"
|
|
#include <atomic>
|
|
#include <condition_variable>
|
|
#include <mutex>
|
|
#include <thread>
|
|
#include <memory>
|
|
#include <cstring>
|
|
#include <cstdlib>
|
|
#include <csetjmp>
|
|
#ifdef _WIN32
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
#include <windows.h>
|
|
#endif
|
|
extern "C" {
|
|
uint32_t dma_snapshot_bytes(void);
|
|
void dma_snapshot_write(uint8_t *);
|
|
int dma_snapshot_read(const uint8_t *, uint32_t);
|
|
int mod_runtime_execution_ready(void);
|
|
int psx_game_execution_lookup(uint32_t, PsxExecutionCode *);
|
|
int psx_netplay_active(void);
|
|
void psx_irq_set_cause_ptr(uint32_t *);
|
|
extern PSX_THREAD_LOCAL void (*g_overlay_flush_pending_cycles)(void);
|
|
extern PSX_THREAD_LOCAL int g_host_store_depth, g_dma_exec_depth, g_call_unit_depth;
|
|
extern PSX_THREAD_LOCAL uint32_t g_debug_last_store_pc;
|
|
}
|
|
namespace {
|
|
constexpr size_t PayloadCapacity = 64u * 1024u;
|
|
struct Job {
|
|
std::atomic<bool> queued{false}, worker_done{true}, consumer_done{true};
|
|
bool posted = true, ok = false;
|
|
uint64_t serial = 0, cycle = 0;
|
|
uint32_t period = 2, boundaries = 0, open = 0, vblank = 0, gcs[3]{}, icache[1024]{};
|
|
int icache_active = -1;
|
|
CPUState cpu{};
|
|
PSXModRenderPass pass{};
|
|
PSXModRenderPassFn draw = nullptr;
|
|
alignas(16) unsigned char payload[PayloadCapacity]{};
|
|
const PsxBiosBackend *bios = nullptr;
|
|
PsxMemoryBacking *memory = memory_backing_create();
|
|
PsxMemoryExecutionState *metadata = memory_execution_create();
|
|
PGXPExecutionSnapshot *pgxp = pgxp_execution_snapshot_create();
|
|
GTEExecutionSnapshot *gte = gte_execution_snapshot_create();
|
|
GpuExecutionSnapshot *gpu = gpu_execution_snapshot_create();
|
|
GlExecutionProducerState *gl = gl_renderer_execution_create();
|
|
OverlayExecutionSnapshot *overlays = overlay_execution_create();
|
|
ModExecutionSnapshot *mods = mod_execution_create();
|
|
RtCommandList *commands = rt_list_create(16u << 20);
|
|
uint32_t dma_bytes = dma_snapshot_bytes();
|
|
std::unique_ptr<uint8_t[]> dma;
|
|
uint16_t timer_counter[3]{}, timer_target[3]{};
|
|
uint32_t timer_mode[3]{}, timer_frac[3]{};
|
|
int32_t timer_irq[3]{};
|
|
Job() : dma(new (std::nothrow) uint8_t[dma_bytes]) {}
|
|
bool valid() const { return memory && metadata && pgxp && gte && gpu && gl &&
|
|
overlays && mods && commands && dma; }
|
|
~Job() {
|
|
memory_backing_destroy(memory); memory_execution_destroy(metadata);
|
|
pgxp_execution_snapshot_destroy(pgxp); gte_execution_snapshot_destroy(gte);
|
|
gpu_execution_snapshot_destroy(gpu); gl_renderer_execution_destroy(gl);
|
|
overlay_execution_destroy(overlays); mod_execution_destroy(mods);
|
|
rt_list_destroy(commands);
|
|
}
|
|
};
|
|
struct Worker {
|
|
std::unique_ptr<Job> jobs[2];
|
|
std::thread thread;
|
|
std::mutex mutex;
|
|
std::condition_variable wake;
|
|
bool stop = false;
|
|
std::atomic<bool> enabled{false}, ready{false};
|
|
unsigned next_slot = 0;
|
|
std::atomic<uint64_t> published{0}, completed{0}, late{0}, aborted{0}, refused{0}, misses{0}, calls{0};
|
|
std::atomic<uint32_t> address{0};
|
|
std::atomic<const char *> failure{nullptr};
|
|
uint64_t serial = 0;
|
|
uint64_t idle_until = 0, failed_window = 0, estimated_us = 3000;
|
|
};
|
|
Worker W;
|
|
thread_local std::jmp_buf landing;
|
|
thread_local bool armed, deadline_expired;
|
|
thread_local uint64_t execution_deadline;
|
|
thread_local unsigned deadline_checks;
|
|
void fault(const char *reason, uint32_t addr) {
|
|
W.failure.store(reason); W.address.store(addr);
|
|
if (!std::strcmp(reason, "dispatch_miss")) ++W.misses;
|
|
if (!armed) std::abort();
|
|
std::longjmp(landing, 1);
|
|
}
|
|
void overrun() { if (armed) fault("watchdog", 0); }
|
|
void capture_fault(const char *reason) { fault(reason, 0); }
|
|
int lookup(void *user, CPUState *cpu, uint32_t addr, PsxExecutionCode *code, void (**flush)(void)) {
|
|
Job *job = static_cast<Job *>(user);
|
|
/* Private dispatch supplies an inexpensive host-budget cancellation point.
|
|
* Source dispatch and the faithful cycle helpers do not pay for this. */
|
|
if (armed && (++deadline_checks % 32u) == 0 && host_clock_us() >= execution_deadline) {
|
|
deadline_expired = true; ++W.late; std::longjmp(landing, 1);
|
|
}
|
|
if (overlay_execution_lookup(job->overlays, addr, code, flush) ||
|
|
psx_game_execution_lookup(addr, code)) { ++W.calls; return 1; }
|
|
uint32_t phys = addr & 0x1FFFFFFFu;
|
|
if ((phys >= 0x1FC00000u || phys < 0x10000u) && job->bios && job->bios->execution_lookup) {
|
|
int result = job->bios->execution_lookup(cpu, addr, code);
|
|
if (result) ++W.calls;
|
|
return result;
|
|
}
|
|
return 0;
|
|
}
|
|
void execute(Job &job) {
|
|
job.ok = false;
|
|
memory_backing_bind(job.memory);
|
|
memory_execution_bind(job.metadata, &job.cpu.cop0[12]);
|
|
psx_irq_set_cause_ptr(&job.cpu.cop0[13]);
|
|
psx_cycle_count = job.cycle;
|
|
psx_guest_cycle_scale_restore(job.gcs);
|
|
interrupts_set_cycles_since_vblank(job.vblank);
|
|
std::memcpy(g_psx_icache_tv, job.icache, sizeof job.icache);
|
|
g_psx_icache_active = job.icache_active;
|
|
timers_set_snapshot(job.timer_counter, job.timer_mode, job.timer_target, job.timer_irq, job.timer_frac);
|
|
if (!dma_snapshot_read(job.dma.get(), job.dma_bytes) ||
|
|
!pgxp_execution_snapshot_bind(job.pgxp) || !gte_execution_snapshot_bind(job.gte) ||
|
|
!gpu_execution_snapshot_bind(job.gpu)) std::abort();
|
|
mod_execution_bind(job.mods);
|
|
ModFunctionEntryContext empty{};
|
|
mod_runtime_function_entry_context_restore(&empty);
|
|
g_host_store_depth = g_dma_exec_depth = g_call_unit_depth = 0;
|
|
g_debug_last_store_pc = 0;
|
|
psx_execution_begin(&job, lookup, fault);
|
|
overlay_execution_bind(job.overlays);
|
|
rt_capture_begin(job.commands, capture_fault);
|
|
gl_renderer_execution_bind(job.gl);
|
|
PsxCycleFreeze freeze{};
|
|
if (!psx_cycle_freeze_begin(&freeze, 8000000u, overrun)) std::abort();
|
|
if (setjmp(landing) == 0) {
|
|
armed = true;
|
|
gl_renderer_pass_begin(job.pass.x, job.pass.y, job.pass.w, job.pass.h,
|
|
job.open, job.period, 0);
|
|
job.ok = job.draw(&job.cpu, job.payload, job.pass.alpha_q16) != 0;
|
|
armed = false;
|
|
} else {
|
|
armed = false; if (!deadline_expired) ++W.aborted;
|
|
g_psx_cyc_local_acc = nullptr; g_psx_cyc_bb_defer = 0; g_psx_cyc_batch = 0;
|
|
}
|
|
psx_execution_end();
|
|
psx_cycle_freeze_end(&freeze);
|
|
gl_renderer_pass_end(job.ok ? job.pass.alpha_q16 : 0, job.ok);
|
|
rt_capture_end();
|
|
mod_runtime_function_entry_context_restore(&empty);
|
|
g_host_store_depth = g_dma_exec_depth = g_call_unit_depth = 0;
|
|
gpu_execution_snapshot_unbind(); gte_execution_snapshot_unbind();
|
|
pgxp_execution_snapshot_unbind(); memory_backing_bind(nullptr);
|
|
psx_irq_set_cause_ptr(nullptr);
|
|
}
|
|
void run() {
|
|
#ifdef _WIN32
|
|
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
|
|
#endif
|
|
/* 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];
|
|
for (auto &slot : prepared) {
|
|
slot.reset(new (std::nothrow) Job);
|
|
if (!slot || !slot->valid()) {
|
|
W.failure.store("snapshot_allocation"); return;
|
|
}
|
|
}
|
|
{
|
|
std::lock_guard<std::mutex> lock(W.mutex);
|
|
if (W.stop) return;
|
|
for (unsigned i = 0; i < 2; ++i) W.jobs[i] = std::move(prepared[i]);
|
|
W.ready.store(true, std::memory_order_release);
|
|
}
|
|
for (;;) {
|
|
Job *job = nullptr;
|
|
{
|
|
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()));
|
|
});
|
|
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);
|
|
}
|
|
if (!job) continue;
|
|
if (!job->consumer_done.load(std::memory_order_acquire)) {
|
|
const uint64_t began = host_clock_us();
|
|
deadline_expired = false; deadline_checks = 0;
|
|
execute(*job);
|
|
const uint64_t elapsed = host_clock_us() - began;
|
|
host_span_record(HSPAN_MOD_WORK, began, elapsed, 0x5257524Bu);
|
|
const uint64_t observed = deadline_expired ? elapsed + elapsed / 4u : elapsed;
|
|
const uint64_t smoothed = (W.estimated_us * 7u + observed) / 8u;
|
|
W.estimated_us = observed > smoothed ? observed : smoothed;
|
|
if (W.estimated_us > 12000u) W.estimated_us = 12000u;
|
|
}
|
|
job->worker_done.store(true, std::memory_order_release);
|
|
}
|
|
}
|
|
void consume(void *user) {
|
|
Job &job = *static_cast<Job *>(user);
|
|
/* Queue markers are VBlanks; one pending duplicate is still this game frame. */
|
|
if (!W.enabled.load() || rt_frames_ahead() >= static_cast<int>(job.period) ||
|
|
!job.worker_done.load(std::memory_order_acquire)) ++W.late;
|
|
else if (job.ok) {
|
|
if (gl_renderer_execution_consume(job.commands)) ++W.completed;
|
|
else ++W.refused;
|
|
}
|
|
job.consumer_done.store(true, std::memory_order_release);
|
|
}
|
|
bool start() {
|
|
if (W.thread.joinable()) return true;
|
|
static bool exit_registered = false;
|
|
if (!exit_registered) {
|
|
if (std::atexit(render_pass_worker_stop) != 0) {
|
|
for (auto &slot : W.jobs) slot.reset(); return false;
|
|
}
|
|
exit_registered = true;
|
|
}
|
|
mod_execution_seal();
|
|
W.stop = false; W.next_slot = 0;
|
|
W.idle_until = W.failed_window = 0; W.estimated_us = 3000;
|
|
W.ready.store(false);
|
|
W.thread = std::thread(run);
|
|
return true;
|
|
}
|
|
}
|
|
extern "C" int psx_mod_set_worker_render_passes(int enabled) {
|
|
if (!enabled) { render_pass_worker_stop(); return 1; }
|
|
W.enabled.store(true); return start() ? 1 : 0;
|
|
}
|
|
extern "C" int psx_mod_render_pass_async(CPUState *cpu, const PSXModRenderPass *pass,
|
|
PSXModRenderPassFn draw, const void *payload, size_t bytes) {
|
|
if (!W.enabled.load() || !cpu || !pass || pass->struct_size != sizeof(*pass) ||
|
|
!draw || (!payload && bytes) || bytes > PayloadCapacity || psx_netplay_active() ||
|
|
psx_mod_render_pass_status() != PSX_MOD_RENDER_PASS_READY || !mod_runtime_execution_ready()) {
|
|
++W.refused; return 0;
|
|
}
|
|
bool builtin = false;
|
|
for (uint32_t i = 0; i < psx_bios_builtin_registry_count; ++i)
|
|
if (psx_bios_builtin_registry[i] == psx_bios_active) builtin = true;
|
|
if (!builtin || !psx_bios_active->execution_lookup || !start() ||
|
|
!W.ready.load(std::memory_order_acquire)) { ++W.refused; return 0; }
|
|
Job *job = nullptr;
|
|
for (unsigned n = 0; n < 2; ++n) {
|
|
unsigned index = (W.next_slot + n) % 2;
|
|
auto &slot = W.jobs[index];
|
|
if (slot->worker_done.load(std::memory_order_acquire) &&
|
|
slot->consumer_done.load(std::memory_order_acquire)) {
|
|
job = slot.get(); W.next_slot = (index + 1) % 2; break;
|
|
}
|
|
}
|
|
if (!job) { ++W.late; return 0; }
|
|
if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles();
|
|
job->cpu = *cpu; job->pass = *pass; job->draw = draw; job->bios = psx_bios_active;
|
|
if (bytes) std::memcpy(job->payload, payload, bytes);
|
|
/* Existing host-span receipts separate publication from guest execution.
|
|
* Tags: RRAM, RPRE (precision), RGPU, RPUB (whole publication). */
|
|
struct PublicationSpan {
|
|
uint64_t start = host_span_begin();
|
|
~PublicationSpan() { host_span_end(HSPAN_MOD_WORK, start, 0x52505542u); }
|
|
} publication_span;
|
|
uint64_t stage = host_span_begin();
|
|
memory_backing_capture(job->memory); memory_execution_capture(job->metadata);
|
|
host_span_end(HSPAN_MOD_WORK, stage, 0x5252414Du);
|
|
stage = host_span_begin();
|
|
int precision_ok = pgxp_execution_snapshot_capture(job->pgxp) && gte_execution_snapshot_capture(job->gte);
|
|
host_span_end(HSPAN_MOD_WORK, stage, 0x52505245u);
|
|
if (!precision_ok) { ++W.refused; return 0; }
|
|
stage = host_span_begin();
|
|
int gpu_ok = gpu_execution_snapshot_capture(job->gpu) && gl_renderer_execution_capture(job->gl);
|
|
host_span_end(HSPAN_MOD_WORK, stage, 0x52475055u);
|
|
if (!gpu_ok || !overlay_execution_capture(job->overlays)) { ++W.refused; return 0; }
|
|
mod_execution_capture(job->mods);
|
|
dma_snapshot_write(job->dma.get());
|
|
timers_get_snapshot(job->timer_counter, job->timer_mode, job->timer_target, job->timer_irq, job->timer_frac);
|
|
job->cycle = psx_cycle_count;
|
|
job->vblank = interrupts_get_cycles_since_vblank();
|
|
psx_guest_cycle_scale_snapshot(job->gcs);
|
|
std::memcpy(job->icache, g_psx_icache_tv, sizeof job->icache);
|
|
job->icache_active = g_psx_icache_active;
|
|
job->open = render_pass_worker_take_plan(&job->period);
|
|
job->serial = ++W.serial; job->boundaries = 0; job->posted = false; job->ok = false;
|
|
job->worker_done.store(false); job->consumer_done.store(false);
|
|
{
|
|
std::lock_guard<std::mutex> lock(W.mutex);
|
|
job->queued.store(true, std::memory_order_release);
|
|
}
|
|
++W.published; W.wake.notify_one(); return 1;
|
|
}
|
|
extern "C" void render_pass_worker_idle_until(uint64_t deadline_us) {
|
|
if (!W.enabled.load() || !W.ready.load(std::memory_order_acquire)) return;
|
|
{
|
|
std::lock_guard<std::mutex> lock(W.mutex);
|
|
W.idle_until = deadline_us;
|
|
}
|
|
if (deadline_us) W.wake.notify_one();
|
|
}
|
|
extern "C" void render_pass_worker_flush(void) {
|
|
if (!W.enabled.load() || !W.ready.load(std::memory_order_acquire)) return;
|
|
/* The canonical presentation boundary is the extra's admission deadline.
|
|
* Process publication order even when the two reusable slots alternate. */
|
|
for (unsigned n = 0; n < 2; ++n) {
|
|
Job *next = nullptr;
|
|
for (auto &slot : W.jobs)
|
|
if (!slot->posted && !slot->consumer_done.load(std::memory_order_acquire) &&
|
|
(!next || slot->serial < next->serial)) next = slot.get();
|
|
if (!next) break;
|
|
/* Publication can precede the real present. An unfinished extra still
|
|
* has the duplicate VBlank before its next game-frame deadline. */
|
|
++next->boundaries;
|
|
if (!next->worker_done.load(std::memory_order_acquire) && next->boundaries < next->period)
|
|
break;
|
|
if (gl_renderer_execution_enqueue(consume, next)) next->posted = true;
|
|
else { next->posted = true; next->consumer_done.store(true, std::memory_order_release); ++W.late; }
|
|
}
|
|
}
|
|
extern "C" void render_pass_worker_stop(void) {
|
|
W.enabled.store(false);
|
|
if (!W.thread.joinable()) return;
|
|
{ std::lock_guard<std::mutex> lock(W.mutex); W.stop = true; }
|
|
W.wake.notify_one(); W.thread.join();
|
|
rt_drain();
|
|
W.ready.store(false, std::memory_order_release);
|
|
for (auto &slot : W.jobs) slot.reset();
|
|
}
|
|
extern "C" void render_pass_worker_stats(RenderPassWorkerStats *out) {
|
|
if (!out) return;
|
|
*out = {W.published.load(), W.completed.load(), W.late.load(), W.aborted.load(),
|
|
W.refused.load(), W.misses.load(), W.calls.load(), W.address.load(), W.failure.load()};
|
|
}
|
|
|
|
extern "C" void render_pass_worker_quiesce(void) {
|
|
bool enabled = W.enabled.load();
|
|
render_pass_worker_stop();
|
|
W.enabled.store(enabled);
|
|
}
|