#pragma once #include "cpu_state.h" #include "mod_plugins.h" #include "psx_cycle_freeze.h" #include "render_pass_projection.h" #include "pgxp.h" #include "mod_memory.h" #include #include extern "C" uint8_t* memory_get_ram_ptr(void); extern "C" uint8_t* memory_get_scratchpad_ptr(void); extern "C" uint32_t memory_get_ram_bytes(void); /* A draw section may calculate temporary matrices, consume random seeds or * update effect caches. Start every replay from the original section's RAM, * rather than applying those calculations twice to the completed frame. Only * restore inside the framework's machine/VRAM sandbox. */ class PSXDrawReplay { CPUState entry{}; std::vector ram; std::vector arenas; uint8_t scratch[1024]{}; uint32_t start = 0, stop = 0, previous_tick = 0; bool captured = false, ready = false, have_tick = false; bool projections = true; PSXProjectionHistory* history = nullptr; public: PSXProjectionStats stats{}; uint32_t ticks = 0; ~PSXDrawReplay() { psx_projection_destroy(history); } void invalidate() { captured = ready = have_tick = false; ticks = 0; psx_projection_invalidate(history); } // Titles with known actor/camera layouts can apply their motion sets after // restore, instead of matching final GTE projections. Both routes share the // same draw-entry snapshot and machine sandbox. void capture(CPUState* cpu, uint32_t first, uint32_t last, bool capture_projections = true) { if (g_psx_render_pass_active) return; if (captured) invalidate(); // previous drawing section never completed projections = capture_projections; if (projections && !history) history = psx_projection_create(65536); if ((projections && !history) || first >= last || ((first | last) & 3)) return; entry = *cpu; start = first; stop = last; ram.resize(memory_get_ram_bytes()); std::memcpy(ram.data(), memory_get_ram_ptr(), ram.size()); std::memcpy(scratch, memory_get_scratchpad_ptr(), sizeof scratch); arenas.resize(psx_mod_memory_snapshot_bytes()); if (!arenas.empty()) psx_mod_memory_snapshot_write(arenas.data()); captured = true; ready = false; if (projections) psx_projection_capture_begin(history); } bool prepare(uint32_t tick, bool motion_ready = false) { if (!captured) { invalidate(); return false; } captured = false; ticks = have_tick ? tick - previous_tick : 0; previous_tick = tick; have_tick = true; if (projections) { psx_projection_capture_end(history, ticks, 512.0); psx_projection_stats(history, &stats); } ready = ticks && ticks <= 8 && (projections ? stats.changed && !stats.overflow : motion_ready); return ready; } bool restore(CPUState* cpu, uint32_t alpha) { if (!ready || !g_psx_render_pass_active || ram.size() != memory_get_ram_bytes() || arenas.size() != psx_mod_memory_snapshot_bytes()) return false; // This reader journals the live arena before restoring draw entry. // OT prefix packets may live outside main RAM when distance is raised. if (!arenas.empty() && !psx_mod_memory_snapshot_read(arenas.data(), arenas.size())) return false; std::memcpy(memory_get_ram_ptr(), ram.data(), ram.size()); std::memcpy(memory_get_scratchpad_ptr(), scratch, sizeof scratch); // Raw RAM is now from section entry. Its current-frame precision // shadows are stale; the pass checkpoint restores them afterwards. pgxp_invalidate_all(); *cpu = entry; if (projections) psx_projection_replay_begin(history, alpha); return true; } bool draw(CPUState* cpu) { const bool reached = psx_mod_run_guest_span(cpu, start, stop) != 0; if (projections) { psx_projection_replay_end(history); psx_projection_stats(history, &stats); } return reached && (!projections || stats.replayed != 0); } static uint32_t call(CPUState* cpu, uint32_t function, uint32_t a0=0, uint32_t a1=0) { const CPUState saved = *cpu; cpu->pc = 0; cpu->gpr[4] = a0; cpu->gpr[5] = a1; psx_dispatch_call(cpu, function, saved.gpr[31]); const uint32_t result = cpu->gpr[2]; *cpu = saved; return result; } static void rate(uint32_t fps, uint32_t flip) { psx_mod_set_frame_interpolation_source(PSX_MOD_FRAME_SOURCE_FLIP); psx_mod_set_render_pass_flip(flip); psx_mod_set_frame_interpolation_blend(PSX_MOD_FRAME_INTERPOLATION_HOLD); psx_mod_set_frame_interpolation(fps); } };