diff --git a/lib/recomp-net b/lib/recomp-net index 79f84323..a1d77a32 160000 --- a/lib/recomp-net +++ b/lib/recomp-net @@ -1 +1 @@ -Subproject commit 79f84323eba5bbd855842020fe9861ce4fd2539d +Subproject commit a1d77a32dd4cbbfe95bd56d59febce11086f51a5 diff --git a/runtime/include/boot_state.h b/runtime/include/boot_state.h index fd288f46..5b66bdbd 100644 --- a/runtime/include/boot_state.h +++ b/runtime/include/boot_state.h @@ -91,6 +91,7 @@ enum { BS_SEC_DMA = 0x0C, /* DMA channels[7] + dpcr/dicr + async-transfer state */ BS_SEC_SIO = 0x0D, /* SIO regs + pad-config FSM + memcard FSM */ BS_SEC_DIRTY = 0x0E, /* dirty-RAM page bitmap (guest-written code pages) */ + BS_SEC_MDEC = 0x0F, /* MDEC command/FIFOs/quant/scale (FMV decode resume) */ }; /* Save a COMPLETE snapshot at game handoff. Returns 1 on success. */ @@ -107,6 +108,12 @@ int boot_state_load_buffer(const uint8_t* file, size_t file_len, uint32_t bios_checksum, uint32_t entry_pc, CPUState* cpu); +/* Header-only integrity check (no section inflate/apply). Returns 1 if this + * build can load the image; 0 and fills reason (when non-NULL) on reject. */ +int boot_state_check_buffer(const uint8_t* file, size_t file_len, + uint32_t bios_checksum, uint32_t entry_pc, + char* reason, size_t reason_cap); + /* Register a deferred capture: when boot_state_trigger_capture() fires (from * fntrace at game-start), serialize to path. One-shot. */ void boot_state_set_capture(const char* path, uint32_t bios_checksum, diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index c4718d3c..811b220e 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -56,6 +56,9 @@ void gpu_depth24_upload_span_reset(void); * Swap (keep prior frame) so stale trailing VRAM never flashes. One tick * per vblank; returns non-zero while the hold is still active after tick. */ int gpu_depth24_present_hold_tick(void); +/* After savestate restore: clear ephemeral hold so the restored depth24 + * frame can present immediately (span/prev_h come from the GPU snap). */ +void gpu_depth24_on_savestate_loaded(void); /* GP1(06h)/GP1(07h)/GP1(08h) fields for debug (gpu_state). */ void gpu_get_crtc_debug(uint32_t *x1, uint32_t *x2, uint32_t *y1, uint32_t *y2, uint32_t *hres1_out, uint32_t *hres2_out); diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index d29f997e..a12b653f 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -65,6 +65,12 @@ void gl_renderer_flush_cpu_uploads(void); * reloaded identical frame still reaches the window (double/triple buffer). */ void gl_renderer_invalidate_present(void); +/* After savestate restore: push the CPU VRAM mirror into the GL FBO. Needed + * when the load happened while GP1 depth24 was on — the normal path skips + * framebuffer-sized uploads, which also skipped the full-VRAM boot_state + * blit and left post-FMV menus without texture pages. */ +void gl_renderer_restage_vram_after_savestate(void); + /* Post-savestate freeze probe: skip/swap/dirty-mark counters (GL present path). * take() returns deltas since the previous take/reset. Safe no-ops when GL is * inactive. rect_dirty tests the current present-tile dirty bits. */ diff --git a/runtime/include/gpu_vk_renderer.h b/runtime/include/gpu_vk_renderer.h index fa12405e..47820fa8 100644 --- a/runtime/include/gpu_vk_renderer.h +++ b/runtime/include/gpu_vk_renderer.h @@ -45,6 +45,9 @@ void vk_renderer_present_blank(void); * server, 24-bit present). No-op when the Vulkan path is inactive. */ void vk_renderer_sync_cpu(void); +/* After savestate restore: push CPU VRAM into the Vulkan image (see GL twin). */ +void vk_renderer_restage_vram_after_savestate(void); + /* Set the present policy: 1=tear-free, 0=IMMEDIATE (lowest latency, may tear), * -1=MAILBOX. Tear-free prefers MAILBOX because the frontend already paces * frames; unsupported modes fall back to FIFO (always available). */ diff --git a/runtime/include/mdec.h b/runtime/include/mdec.h index 0db6e52f..edc83fbb 100644 --- a/runtime/include/mdec.h +++ b/runtime/include/mdec.h @@ -79,6 +79,11 @@ void mdec_debug_dma_in_end(uint32_t addr, uint32_t words); void mdec_debug_dma_out_start(uint32_t addr, uint32_t words); void mdec_debug_dma_out_end(uint32_t addr, uint32_t words); +/* boot_state / savestate: full MDEC FIFO + tables (required for FMV resume). */ +uint32_t mdec_snapshot_bytes(void); +void mdec_snapshot_write(uint8_t *p); +int mdec_snapshot_read(const uint8_t *p, uint32_t len); + #ifdef __cplusplus } #endif diff --git a/runtime/include/mod_runtime.h b/runtime/include/mod_runtime.h index b62056ea..a4f5a66a 100644 --- a/runtime/include/mod_runtime.h +++ b/runtime/include/mod_runtime.h @@ -18,6 +18,9 @@ bool mod_runtime_initialize(const std::filesystem::path& root, std::string* error = nullptr); bool mod_runtime_commit(const std::filesystem::path& disc_path = {}, std::string* error = nullptr); +/* Drop the in-session mod plan for a netplay launch without rewriting the + * user's persisted offline selection on disk. */ +bool mod_runtime_clear_for_netplay(std::string* error = nullptr); const std::string& mod_runtime_fingerprint(); const std::filesystem::path& mod_runtime_effective_disc_path(); diff --git a/runtime/include/psx_cycles.h b/runtime/include/psx_cycles.h index d7926aa7..529c5583 100644 --- a/runtime/include/psx_cycles.h +++ b/runtime/include/psx_cycles.h @@ -147,9 +147,17 @@ extern uint64_t g_idle_skip_cycles; extern uint32_t g_idle_skip_last_pc; extern uint32_t g_idle_skip_last_quantum; +/* Post-load probe cycle diagnostics (optional; main.cpp soft-load tooling). */ +extern int g_plp_cycle_diag; +extern uint64_t g_plp_adv_calls; +extern uint32_t g_plp_adv_max_chunk; +extern uint64_t g_plp_adv_sum; +extern uint64_t g_plp_svc_calls; + /* Save-state restore: re-anchor the deadline device model after psx_cycle_count - * is overwritten from a snapshot. */ -void psx_cycles_resync_after_restore(void); + * is overwritten from a snapshot. Pass the live CPU so GTE/muldiv completion + * stamps and load-absorb give-back are rewound with the guest clock. */ +void psx_cycles_resync_after_restore(struct CPUState *cpu); /* Soft rematch / session_reboot: zero the guest clock and deadline bookkeeping. * Soft-exit longjmps out of vblank (inside psx_devices_service_to_now) leave diff --git a/runtime/include/psx_netplay.h b/runtime/include/psx_netplay.h index 7188a0b3..c9632eea 100644 --- a/runtime/include/psx_netplay.h +++ b/runtime/include/psx_netplay.h @@ -103,9 +103,14 @@ int psx_netplay_is_host(void); int psx_netplay_request_save(int slot); int psx_netplay_request_load(int slot); -/* 1 while load probe/transfer/apply/ready owns the clock (no FPS / no present). */ +/* 1 while a save/load/memcard probe, chunk transfer, or post-load ready owns + * the clock (long admit timeout, no peer-silence kick, FPS suppressed). */ int psx_netplay_in_load_barrier(void); +/* 1 once after a staged netplay load apply failed (stale .pst / mismatch). + * Clears. Caller should soft-exit to lobby — do not keep waiting on the barrier. */ +int psx_netplay_consume_load_apply_failed(void); + /* Stage local pad for the current sim tick. Ignored once that tick is latched. */ void psx_netplay_stage_local(const PsxNetPad *pad); diff --git a/runtime/include/savestate.h b/runtime/include/savestate.h index f55309bd..b45d86ff 100644 --- a/runtime/include/savestate.h +++ b/runtime/include/savestate.h @@ -45,6 +45,11 @@ int savestate_write_slot(int slot, const void* data, size_t size); /* 1 if the slot file exists and is non-empty. */ int savestate_slot_exists(int slot); +/* 1 if the slot .pst header matches this build's integrity key (BIOS/entry/ + * codegen). 0 + optional reason when missing or stale — use before netplay + * load probe so incompatible saves never enter the post-load barrier. */ +int savestate_slot_compatible(int slot, char* reason, size_t reason_cap); + /* Stage a save/load of slot [0..SAVESTATE_SLOTS-1]. Executed at the next safe * boundary by savestate_poll (called every block from psx_check_interrupts). * Safe to call from the SDL key handler or a debug-server command. @@ -68,6 +73,10 @@ int savestate_pending(void); /* 1 once after a successful load restore (before scheduler longjmp). Clears. */ int savestate_take_load_completed(void); +/* 1 once after a staged load failed in savestate_poll (missing/mismatched). + * Clears. Netplay uses this to abort the load barrier instead of hanging. */ +int savestate_take_load_failed(void); + /* Frontend hook (main.cpp): restage VRAM present path after a successful load. */ void psx_frontend_on_savestate_loaded(void); diff --git a/runtime/include/sio.h b/runtime/include/sio.h index c36131dc..587f599d 100644 --- a/runtime/include/sio.h +++ b/runtime/include/sio.h @@ -79,6 +79,9 @@ int sio_get_multitap(void); /* phys_port: 0 = console Port 1, 1 = console Port 2. Default 0. */ void sio_set_multitap_port(int phys_port); int sio_get_multitap_port(void); +/* Tomba-only legacy pad config path ([controller] legacy_pad_config). */ +void sio_set_legacy_cfg(int enabled); +int sio_get_legacy_cfg(void); /* 1 when multitap is armed and `logical_slot` is a tap pad (not the lone * pad on the opposite console port). SCPH-1070 taps are treated as plain * digital controllers (0x41) — DualShock/analog on a tap is not reliable diff --git a/runtime/src/boot_state.c b/runtime/src/boot_state.c index 4f2a93a8..eeb7a1e9 100644 --- a/runtime/src/boot_state.c +++ b/runtime/src/boot_state.c @@ -69,6 +69,9 @@ extern int dma_snapshot_read(const uint8_t* p, uint32_t len); extern uint32_t sio_snapshot_bytes(void); extern void sio_snapshot_write(uint8_t* p); extern int sio_snapshot_read(const uint8_t* p, uint32_t len); +extern uint32_t mdec_snapshot_bytes(void); +extern void mdec_snapshot_write(uint8_t* p); +extern int mdec_snapshot_read(const uint8_t* p, uint32_t len); /* CPU regs wire: 32+3+32+32+32 LE u32 = 131 * 4 = 524 bytes (no padding). */ #define CPU_REGS_WIRE_BYTES (524u) @@ -208,7 +211,7 @@ int boot_state_save(const CPUState* cpu, uint32_t bios_checksum, h.codegen_hash = (uint32_t)PSX_OVERLAY_CODEGEN_HASH; h.abi_tag = (int32_t)PSX_OVERLAY_ABI_TAG; h.codegen_ver = (uint32_t)PSX_OVERLAY_CODEGEN_VER; - h.section_count = 14; + h.section_count = 15; int ok = write_header_le(f, &h); @@ -257,6 +260,7 @@ int boot_state_save(const CPUState* cpu, uint32_t bios_checksum, if (ok) ok = write_module_section(f, BS_SEC_CDROM, cdrom_snapshot_bytes, cdrom_snapshot_write); if (ok) ok = write_module_section(f, BS_SEC_DMA, dma_snapshot_bytes, dma_snapshot_write); if (ok) ok = write_module_section(f, BS_SEC_SIO, sio_snapshot_bytes, sio_snapshot_write); + if (ok) ok = write_module_section(f, BS_SEC_MDEC, mdec_snapshot_bytes, mdec_snapshot_write); if (ok) { uint32_t wc = dirty_ram_get_bitmap_word_count(); uint64_t nbytes = (uint64_t)wc * 4u; @@ -393,6 +397,8 @@ static int apply_section(uint32_t tag, const uint8_t* p, uint32_t len, return dma_snapshot_read(p, len); case BS_SEC_SIO: return sio_snapshot_read(p, len); + case BS_SEC_MDEC: + return mdec_snapshot_read(p, len); case BS_SEC_DIRTY: { uint32_t wc; uint32_t* words; @@ -417,18 +423,121 @@ static int apply_section(uint32_t tag, const uint8_t* p, uint32_t len, } } +static int boot_state_parse_header(const uint8_t* file, size_t file_len, + BootStateHeader* h_out) { + PstR hr; + if (!file || !h_out || file_len < BOOT_STATE_HEADER_WIRE_BYTES || + file_len > 64u * 1024u * 1024u) { + return 0; + } + pst_r_init(&hr, file, BOOT_STATE_HEADER_WIRE_BYTES); + memset(h_out, 0, sizeof(*h_out)); + if (!pst_r_u32(&hr, &h_out->magic) || + !pst_r_u32(&hr, &h_out->version) || + !pst_r_u32(&hr, &h_out->bios_checksum) || + !pst_r_u32(&hr, &h_out->entry_pc) || + !pst_r_u32(&hr, &h_out->codegen_hash) || + !pst_r_i32(&hr, &h_out->abi_tag) || + !pst_r_u32(&hr, &h_out->codegen_ver) || + !pst_r_u32(&hr, &h_out->section_count) || + !pst_r_u32(&hr, &h_out->reserved)) { + return 0; + } + return 1; +} + +static void boot_state_append_reason(char* reason, size_t reason_cap, + const char* part) { + size_t n; + if (!reason || reason_cap == 0 || !part || !part[0]) return; + n = strlen(reason); + if (n + 1 >= reason_cap) return; + if (n > 0) { + reason[n++] = ','; + reason[n] = '\0'; + if (n + 1 >= reason_cap) return; + } + snprintf(reason + n, reason_cap - n, "%s", part); +} + +int boot_state_check_buffer(const uint8_t* file, size_t file_len, + uint32_t bios_checksum, uint32_t entry_pc, + char* reason, size_t reason_cap) { + BootStateHeader h; + char part[96]; + + if (reason && reason_cap) + reason[0] = '\0'; + + if (!file || file_len < BOOT_STATE_HEADER_WIRE_BYTES) { + boot_state_append_reason(reason, reason_cap, "missing_or_truncated"); + return 0; + } + if (file_len > 64u * 1024u * 1024u) { + boot_state_append_reason(reason, reason_cap, "too_large"); + return 0; + } + if (!boot_state_parse_header(file, file_len, &h)) { + boot_state_append_reason(reason, reason_cap, "header_parse"); + return 0; + } + + if (h.magic != BOOT_STATE_MAGIC) { + snprintf(part, sizeof(part), "magic=%08X(want %08X)", + (unsigned)h.magic, (unsigned)BOOT_STATE_MAGIC); + boot_state_append_reason(reason, reason_cap, part); + } + if (h.version < BOOT_STATE_VERSION_MIN_READ || + h.version > BOOT_STATE_VERSION) { + snprintf(part, sizeof(part), "version=%u(want %u..%u)", + (unsigned)h.version, (unsigned)BOOT_STATE_VERSION_MIN_READ, + (unsigned)BOOT_STATE_VERSION); + boot_state_append_reason(reason, reason_cap, part); + } + if (h.bios_checksum != bios_checksum) { + snprintf(part, sizeof(part), "bios=%08X(want %08X)", + (unsigned)h.bios_checksum, (unsigned)bios_checksum); + boot_state_append_reason(reason, reason_cap, part); + } + if (h.entry_pc != entry_pc) { + snprintf(part, sizeof(part), "entry=%08X(want %08X)", + (unsigned)h.entry_pc, (unsigned)entry_pc); + boot_state_append_reason(reason, reason_cap, part); + } + if (h.codegen_hash != (uint32_t)PSX_OVERLAY_CODEGEN_HASH) { + snprintf(part, sizeof(part), "codegen_hash=%08X(want %08X)", + (unsigned)h.codegen_hash, + (unsigned)PSX_OVERLAY_CODEGEN_HASH); + boot_state_append_reason(reason, reason_cap, part); + } + if (h.abi_tag != (int32_t)PSX_OVERLAY_ABI_TAG) { + snprintf(part, sizeof(part), "abi_tag=%d(want %d)", + (int)h.abi_tag, (int)PSX_OVERLAY_ABI_TAG); + boot_state_append_reason(reason, reason_cap, part); + } + if (h.codegen_ver != (uint32_t)PSX_OVERLAY_CODEGEN_VER) { + snprintf(part, sizeof(part), "codegen_ver=%u(want %u)", + (unsigned)h.codegen_ver, (unsigned)PSX_OVERLAY_CODEGEN_VER); + boot_state_append_reason(reason, reason_cap, part); + } + + if (reason && reason_cap && reason[0]) + return 0; + return 1; +} + int boot_state_load_buffer(const uint8_t* file, size_t file_len, uint32_t bios_checksum, uint32_t entry_pc, CPUState* cpu) { const uint8_t* cur; const uint8_t* end; BootStateHeader h; - PstR hr; + char reject[256]; const uint32_t required = (1u< 64u * 1024u * 1024u) { + if (!boot_state_check_buffer(file, file_len, bios_checksum, entry_pc, + reject, sizeof(reject))) { + fprintf(stderr, "boot_state: reject — %s\n", + reject[0] ? reject : "unknown"); return 0; } - - /* Parse header from the in-memory image (one I/O, then CPU-side inflate). */ - pst_r_init(&hr, file, BOOT_STATE_HEADER_WIRE_BYTES); - memset(&h, 0, sizeof h); - if (!pst_r_u32(&hr, &h.magic) || - !pst_r_u32(&hr, &h.version) || - !pst_r_u32(&hr, &h.bios_checksum) || - !pst_r_u32(&hr, &h.entry_pc) || - !pst_r_u32(&hr, &h.codegen_hash) || - !pst_r_i32(&hr, &h.abi_tag) || - !pst_r_u32(&hr, &h.codegen_ver) || - !pst_r_u32(&hr, &h.section_count) || - !pst_r_u32(&hr, &h.reserved)) { + if (!boot_state_parse_header(file, file_len, &h)) return 0; - } - - if (h.magic != BOOT_STATE_MAGIC || - h.version < BOOT_STATE_VERSION_MIN_READ || - h.version > BOOT_STATE_VERSION || - h.bios_checksum != bios_checksum || - h.entry_pc != entry_pc || - h.codegen_hash != (uint32_t)PSX_OVERLAY_CODEGEN_HASH || - h.abi_tag != (int32_t)PSX_OVERLAY_ABI_TAG || - h.codegen_ver != (uint32_t)PSX_OVERLAY_CODEGEN_VER) { - return 0; - } cur = file + BOOT_STATE_HEADER_WIRE_BYTES; end = file + file_len; @@ -571,10 +658,16 @@ int boot_state_load(const char* path, uint32_t bios_checksum, const double t0 = boot_state_mono_ms(); double t_after_read; - if (!f) return 0; + if (!f) { + fprintf(stderr, "boot_state: reject — missing %s\n", + path ? path : "(null)"); + return 0; + } if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return 0; } sz = ftell(f); if (sz < (long)BOOT_STATE_HEADER_WIRE_BYTES || sz > 64L * 1024L * 1024L) { + fprintf(stderr, "boot_state: reject — bad size %ld for %s\n", + sz, path ? path : "(null)"); fclose(f); return 0; } diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 7dd6d347..b78d320c 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -2545,6 +2545,12 @@ int gpu_depth24_present_hold_tick(void) { return 1; } +void gpu_depth24_on_savestate_loaded(void) { + /* Hold skips Swap — after restore we want the restored VRAM visible now. + * Upload span / prev_h were restored from the GPU snap. */ + s_d24_present_hold = 0; +} + /* ---- Present-time screen-colour LUT (verified-enhancement, opt-in) ------- * * PRESENT-TIME ONLY. This sits on the 15-bit-scanout -> RGB888 conversion that @@ -4999,6 +5005,8 @@ static int gpu_snap_emit(PstW *w) { WH(vram_write_col); WH(vram_write_row); WU(vram_write_remaining); WI(vram_read_active); WH(vram_read_x); WH(vram_read_y); WH(vram_read_w); WH(vram_read_h); WH(vram_read_col); WH(vram_read_row); + /* Depth24 present helpers (MotK FMV) — must resume with upload span. */ + WU(s_d24_upload_x1); WI(s_d24_present_hold); WU(s_d24_prev_disp_h); #undef WU #undef WI #undef WH @@ -5031,6 +5039,7 @@ static int gpu_snap_parse(PstR *r) { RH(vram_write_col); RH(vram_write_row); RU(vram_write_remaining); RI(vram_read_active); RH(vram_read_x); RH(vram_read_y); RH(vram_read_w); RH(vram_read_h); RH(vram_read_col); RH(vram_read_row); + RU(s_d24_upload_x1); RI(s_d24_present_hold); RU(s_d24_prev_disp_h); #undef RU #undef RI #undef RH diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 077cc023..a780c4c0 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -69,12 +69,12 @@ #include "gpu_gl_renderer.h" #include "latency_ring.h" -#include "psx_sdl.h" -#if defined(PSX_SDL3) -#include -#else -#include -#endif +#include "psx_sdl.h" +#if defined(PSX_SDL3) +#include +#else +#include +#endif #include #include #include @@ -315,12 +315,12 @@ static GLint s_present_uTex = -1, s_present_uUvRect = -1; static GLuint s_interp_prog = 0, s_interp_tex[3]; static GLsync s_interp_fence[3]; static GLsync s_interp_draw_fence = NULL; -static GLint s_interp_uPrev = -1, s_interp_uCurr = -1; -static GLint s_interp_uAlpha = -1, s_interp_uUvRect = -1; -static GLint s_interp_uBlendMode = -1; -static int s_interp_enabled = 0, s_interp_valid = 0; -static int s_interp_suspended = 0; -static int s_interp_blend_mode = 0; +static GLint s_interp_uPrev = -1, s_interp_uCurr = -1; +static GLint s_interp_uAlpha = -1, s_interp_uUvRect = -1; +static GLint s_interp_uBlendMode = -1; +static int s_interp_enabled = 0, s_interp_valid = 0; +static int s_interp_suspended = 0; +static int s_interp_blend_mode = 0; static int s_interp_prev = 0, s_interp_cur = 0; static int s_interp_w = 0, s_interp_h = 0, s_interp_linear = 0; static int s_interp_force_4_3 = 0, s_interp_source_path = -1; @@ -748,21 +748,21 @@ static const char *PRESENT_FS = "#version 330\n" "in vec2 v_uv; uniform sampler2D u_tex; out vec4 frag;\n" "void main(){ frag = texture(u_tex, v_uv); }\n"; -static const char *INTERP_FS = - "#version 330\n" - "in vec2 v_uv; uniform sampler2D u_prev; uniform sampler2D u_curr;\n" - "uniform float u_alpha; uniform int u_blend_mode; out vec4 frag;\n" - "void main(){\n" - " vec4 prev=texture(u_prev,v_uv), curr=texture(u_curr,v_uv);\n" - " float alpha=u_alpha;\n" - " if(u_blend_mode==1){\n" - " vec3 d=abs(prev.rgb-curr.rgb);\n" - " float change=max(max(d.r,d.g),d.b);\n" - " float safe_blend=1.0-smoothstep(0.08,0.20,change);\n" - " alpha=mix(step(0.5,u_alpha),u_alpha,safe_blend);\n" - " }\n" - " frag=mix(prev,curr,alpha);\n" - "}\n"; +static const char *INTERP_FS = + "#version 330\n" + "in vec2 v_uv; uniform sampler2D u_prev; uniform sampler2D u_curr;\n" + "uniform float u_alpha; uniform int u_blend_mode; out vec4 frag;\n" + "void main(){\n" + " vec4 prev=texture(u_prev,v_uv), curr=texture(u_curr,v_uv);\n" + " float alpha=u_alpha;\n" + " if(u_blend_mode==1){\n" + " vec3 d=abs(prev.rgb-curr.rgb);\n" + " float change=max(max(d.r,d.g),d.b);\n" + " float safe_blend=1.0-smoothstep(0.08,0.20,change);\n" + " alpha=mix(step(0.5,u_alpha),u_alpha,safe_blend);\n" + " }\n" + " frag=mix(prev,curr,alpha);\n" + "}\n"; /* Geometry: position in VRAM pixels (draw offset already applied by gpu.c), * color rgb in 0..1, color a = mask bit (0/1). The clip transform is in @@ -2139,6 +2139,28 @@ static int depth24_is_fb_transfer(int w, int h) { return 0; } +/* Remember only the depth24 scanout band for clear-on-leave. Used after a + * full-VRAM savestate restage so texture pages stay in the FBO while the + * RGB888 movie band is still wiped when GP1 leaves 24-bit (avoids MotK + * rainbow/static from treating packed RGB as 1555). */ +static void depth24_mark_scanout_band(void) { + GpuDisplayInfo di; + int fb_w, fb_h, x0, y0, x1, y1; + if (!gpu_display_is_depth24()) return; + gpu_get_display_info(&di); + fb_w = (int)((di.width * 3u + 1u) / 2u); + fb_h = (int)di.height; + if (fb_w < 8) fb_w = 8; + if (fb_h < 1) fb_h = 1; + x0 = (int)(di.display_x & 1023u); + y0 = (int)(di.display_y & 511u); + x1 = x0 + fb_w - 1; + y1 = y0 + fb_h - 1; + if (x1 > VRAM_W - 1) x1 = VRAM_W - 1; + if (y1 > VRAM_H - 1) y1 = VRAM_H - 1; + rect_add(&s_d24_skip_fb, x0, y0, x1, y1); +} + static void depth24_clear_skipped_fb(void) { if (!s_raster_ok || !s_d24_skip_fb.set) return; flush_flat_batch(); @@ -2194,6 +2216,16 @@ static void glb_vram_transfer_in(int x,int y,int w,int h,const uint16_t *d){ sw_vram_transfer_in(x,y,w,h,d); depth24_upload_policy(); if (s_depth24_skip_up && depth24_is_fb_transfer(w, h)) { + /* Full-VRAM restore (boot_state): must stage into the FBO or every + * texture page outside the movie band is missing after FMV→menus. + * Only the scanout band is remembered for clear-on-leave. */ + if (w >= VRAM_W && h >= VRAM_H) { + up_add_transfer(x, y, w, h); + rect_clear(&s_d24_skip_fb); + depth24_mark_scanout_band(); + coh_record(GL_COH_UPLOAD, x, y, x + w - 1, y + h - 1); + return; + } int x0 = x & (VRAM_W - 1), y0 = y & (VRAM_H - 1); rect_add(&s_d24_skip_fb, x0, y0, x0 + w - 1, y0 + h - 1); coh_record(GL_COH_UPLOAD, x, y, x+w-1, y+h-1); @@ -2479,11 +2511,11 @@ int gl_renderer_init_context(SDL_Window *win) { s_present_uTex = p_glGetUniformLocation(s_present_prog, "u_tex"); s_present_uUvRect = p_glGetUniformLocation(s_present_prog, "u_uv_rect"); s_interp_uPrev = p_glGetUniformLocation(s_interp_prog, "u_prev"); - s_interp_uCurr = p_glGetUniformLocation(s_interp_prog, "u_curr"); - s_interp_uAlpha = p_glGetUniformLocation(s_interp_prog, "u_alpha"); - s_interp_uUvRect = p_glGetUniformLocation(s_interp_prog, "u_uv_rect"); - s_interp_uBlendMode = - p_glGetUniformLocation(s_interp_prog, "u_blend_mode"); + s_interp_uCurr = p_glGetUniformLocation(s_interp_prog, "u_curr"); + s_interp_uAlpha = p_glGetUniformLocation(s_interp_prog, "u_alpha"); + s_interp_uUvRect = p_glGetUniformLocation(s_interp_prog, "u_uv_rect"); + s_interp_uBlendMode = + p_glGetUniformLocation(s_interp_prog, "u_blend_mode"); glGenTextures(3, s_interp_tex); for (int i = 0; i < 3; i++) { glBindTexture(GL_TEXTURE_2D, s_interp_tex[i]); @@ -2656,6 +2688,22 @@ void gl_renderer_invalidate_present(void) { interp_reset_history(); } +void gl_renderer_restage_vram_after_savestate(void) { + if (!s_raster_ok || !s_vram) return; + /* Belt-and-suspenders after boot_state VRAM apply: force CPU mirror → FBO + * even if a depth24 skip swallowed the restore, then re-arm scanout-band + * clear so leaving FMV does not keep RGB888-as-1555 junk. */ + s_up_nrects = 0; + rect_clear(&s_d24_skip_fb); + s_depth24_skip_up = 0; + up_add_transfer(0, 0, VRAM_W, VRAM_H); + flush_cpu_upload(); + if (gpu_display_is_depth24()) { + s_depth24_skip_up = 1; + depth24_mark_scanout_band(); + } +} + void gl_renderer_present_probe_reset(void) { s_probe_skip = 0; s_probe_swap = 0; @@ -3290,13 +3338,13 @@ static void interp_reset_history(void) { if (s_interp_mutex) SDL_UnlockMutex(s_interp_mutex); } -void gl_renderer_set_interpolation(int enabled, double host_hz, double target_hz, - int blend_mode) { - double effective_hz = target_hz < 0.0 - ? -1.0 - : (target_hz >= 60.0 ? target_hz : host_hz); - int active = (enabled && - (effective_hz < 0.0 || effective_hz >= 50.0)) ? 1 : 0; +void gl_renderer_set_interpolation(int enabled, double host_hz, double target_hz, + int blend_mode) { + double effective_hz = target_hz < 0.0 + ? -1.0 + : (target_hz >= 60.0 ? target_hz : host_hz); + int active = (enabled && + (effective_hz < 0.0 || effective_hz >= 50.0)) ? 1 : 0; const char *diag = getenv("PSX_GL_INTERP_DIAG"); s_interp_diag = diag && diag[0] && diag[0] != '0'; if (active && !s_interp_ctx && s_ctx) { @@ -3324,20 +3372,20 @@ void gl_renderer_set_interpolation(int enabled, double host_hz, double target_hz if (s_interp_mutex) SDL_LockMutex(s_interp_mutex); if (active != s_interp_enabled) interp_reset_history_unlocked(); s_interp_enabled = active; - s_interp_host_hz = host_hz; - s_interp_target_hz = active ? effective_hz : 0.0; - s_interp_blend_mode = blend_mode == 1 ? 1 : 0; + s_interp_host_hz = host_hz; + s_interp_target_hz = active ? effective_hz : 0.0; + s_interp_blend_mode = blend_mode == 1 ? 1 : 0; if (s_interp_mutex) SDL_UnlockMutex(s_interp_mutex); - if (active && effective_hz < 0.0) - fprintf(stdout, "psxrecomp: GL frame interpolation enabled: uncapped " - "target on %.1f Hz display (%s blend)\n", host_hz, - s_interp_blend_mode ? "motion-adaptive" : "linear"); - else if (active) - fprintf(stdout, "psxrecomp: GL frame interpolation enabled: %.1f FPS " - "target on %.1f Hz display (%s blend)\n", effective_hz, host_hz, - s_interp_blend_mode ? "motion-adaptive" : "linear"); - else - fprintf(stdout, "psxrecomp: GL frame interpolation disabled (host %.1f Hz)\n", host_hz); + if (active && effective_hz < 0.0) + fprintf(stdout, "psxrecomp: GL frame interpolation enabled: uncapped " + "target on %.1f Hz display (%s blend)\n", host_hz, + s_interp_blend_mode ? "motion-adaptive" : "linear"); + else if (active) + fprintf(stdout, "psxrecomp: GL frame interpolation enabled: %.1f FPS " + "target on %.1f Hz display (%s blend)\n", effective_hz, host_hz, + s_interp_blend_mode ? "motion-adaptive" : "linear"); + else + fprintf(stdout, "psxrecomp: GL frame interpolation disabled (host %.1f Hz)\n", host_hz); } void gl_renderer_set_interpolation_suspended(int suspended) { @@ -3452,9 +3500,9 @@ static void interp_draw_quad(float alpha, int lx, int ly, int lw, int lh) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, s_interp_linear ? GL_LINEAR : GL_NEAREST); p_glUseProgram(s_interp_prog); p_glUniform1i(s_interp_uPrev, 0); - p_glUniform1i(s_interp_uCurr, 1); - p_glUniform1f(s_interp_uAlpha, alpha); - p_glUniform1i(s_interp_uBlendMode, s_interp_blend_mode); + p_glUniform1i(s_interp_uCurr, 1); + p_glUniform1f(s_interp_uAlpha, alpha); + p_glUniform1i(s_interp_uBlendMode, s_interp_blend_mode); p_glUniform4f(s_interp_uUvRect, 0.f, 0.f, 1.f, 1.f); p_glBindVertexArray(s_interp_thread_vao); glDrawArrays(GL_TRIANGLES, 0, 3); @@ -3498,46 +3546,46 @@ static int interp_present(void) { return 1; } -static int interp_thread_main(void *opaque) { - (void)opaque; - if (SDL_GL_MakeCurrent(s_win, s_interp_ctx) != 0) return -1; - SDL_GL_SetSwapInterval(0); /* host-period scheduler owns cadence */ +static int interp_thread_main(void *opaque) { + (void)opaque; + if (SDL_GL_MakeCurrent(s_win, s_interp_ctx) != 0) return -1; + SDL_GL_SetSwapInterval(0); /* host-period scheduler owns cadence */ p_glGenVertexArrays(1, &s_interp_thread_vao); - uint64_t freq = SDL_GetPerformanceFrequency(); - uint64_t deadline = SDL_GetPerformanceCounter(); - uint64_t diag_start = deadline, diag_swaps = 0, diag_captures = 0; - - while (SDL_AtomicGet(&s_interp_thread_run)) { - SDL_LockMutex(s_interp_mutex); - double hz = s_interp_target_hz; - SDL_UnlockMutex(s_interp_mutex); - int uncapped = hz < 0.0; - uint64_t now = SDL_GetPerformanceCounter(); - if (!uncapped) { - if (hz < 50.0) hz = 60.0; - uint64_t period = (uint64_t)((double)freq / hz); - if (!period) period = 1; - deadline += period; - if (now > deadline + period * 4u) deadline = now + period; - for (;;) { - now = SDL_GetPerformanceCounter(); - if (now >= deadline) break; - uint64_t remain = deadline - now; - uint32_t ms = (uint32_t)((remain * 1000u) / - (freq ? freq : 1u)); - if (ms > 1) SDL_Delay(ms - 1); - } - while (SDL_GetPerformanceCounter() < deadline) {} - now = SDL_GetPerformanceCounter(); - } else { - deadline = now; - } - - SDL_LockMutex(s_interp_mutex); - int presented = 0; - if (SDL_AtomicGet(&s_interp_thread_run) && s_interp_enabled) - presented = interp_present(); - if (s_interp_diag && now - diag_start >= freq * 5u) { + uint64_t freq = SDL_GetPerformanceFrequency(); + uint64_t deadline = SDL_GetPerformanceCounter(); + uint64_t diag_start = deadline, diag_swaps = 0, diag_captures = 0; + + while (SDL_AtomicGet(&s_interp_thread_run)) { + SDL_LockMutex(s_interp_mutex); + double hz = s_interp_target_hz; + SDL_UnlockMutex(s_interp_mutex); + int uncapped = hz < 0.0; + uint64_t now = SDL_GetPerformanceCounter(); + if (!uncapped) { + if (hz < 50.0) hz = 60.0; + uint64_t period = (uint64_t)((double)freq / hz); + if (!period) period = 1; + deadline += period; + if (now > deadline + period * 4u) deadline = now + period; + for (;;) { + now = SDL_GetPerformanceCounter(); + if (now >= deadline) break; + uint64_t remain = deadline - now; + uint32_t ms = (uint32_t)((remain * 1000u) / + (freq ? freq : 1u)); + if (ms > 1) SDL_Delay(ms - 1); + } + while (SDL_GetPerformanceCounter() < deadline) {} + now = SDL_GetPerformanceCounter(); + } else { + deadline = now; + } + + SDL_LockMutex(s_interp_mutex); + int presented = 0; + if (SDL_AtomicGet(&s_interp_thread_run) && s_interp_enabled) + presented = interp_present(); + if (s_interp_diag && now - diag_start >= freq * 5u) { double seconds = (double)(now - diag_start) / (double)freq; fprintf(stdout, "psxrecomp: GL interpolation cadence: " "%.2f captures/s, %.2f presents/s\n", @@ -3547,10 +3595,10 @@ static int interp_thread_main(void *opaque) { diag_start = now; diag_captures = s_interp_captures; diag_swaps = s_interp_swaps; - } - SDL_UnlockMutex(s_interp_mutex); - if (uncapped && !presented) SDL_Delay(1); - } + } + SDL_UnlockMutex(s_interp_mutex); + if (uncapped && !presented) SDL_Delay(1); + } p_glBindVertexArray(0); SDL_GL_MakeCurrent(s_win, NULL); return 0; diff --git a/runtime/src/gpu_vk_renderer.c b/runtime/src/gpu_vk_renderer.c index 5e9b4d6f..6ccc4bc7 100644 --- a/runtime/src/gpu_vk_renderer.c +++ b/runtime/src/gpu_vk_renderer.c @@ -39,6 +39,7 @@ int vk_renderer_present_wide(int a,int b,int c,int d){(void)a;(void)b;(void)c;( void vk_renderer_present_cpu(const uint32_t*p,int w,int h,int l,int f){(void)p;(void)w;(void)h;(void)l;(void)f;} void vk_renderer_present_blank(void){} void vk_renderer_sync_cpu(void){} +void vk_renderer_restage_vram_after_savestate(void){} void vk_renderer_set_present_mode(int m){(void)m;} int vk_perf_json(char *out,int cap,int count){(void)count; return cap>2?snprintf(out,cap,"[]"):0;} const GpuRenderBackend *vk_backend_get(void) { return 0; } @@ -2259,6 +2260,24 @@ static int depth24_is_fb_transfer(int w, int h) { return 0; } +static void depth24_mark_scanout_band(void) { + GpuDisplayInfo di; + int fb_w, fb_h, x0, y0, x1, y1; + if (!gpu_display_is_depth24()) return; + gpu_get_display_info(&di); + fb_w = (int)((di.width * 3u + 1u) / 2u); + fb_h = (int)di.height; + if (fb_w < 8) fb_w = 8; + if (fb_h < 1) fb_h = 1; + x0 = (int)(di.display_x & 1023u); + y0 = (int)(di.display_y & 511u); + x1 = x0 + fb_w - 1; + y1 = y0 + fb_h - 1; + if (x1 > VRAM_W - 1) x1 = VRAM_W - 1; + if (y1 > VRAM_H - 1) y1 = VRAM_H - 1; + rect_add(&s_d24_skip_fb, x0, y0, x1, y1); +} + static void depth24_clear_skipped_fb(void) { /* GL scissor-clears the skipped FB union. VK keeps texture uploads that * landed during depth24; the FB bands are overwritten by the next 15-bit @@ -2284,12 +2303,32 @@ static void vkb_vram_transfer_in(int x, int y, int w, int h, const uint16_t *dat if (!s_ctx_ok) return; depth24_upload_policy(); if (s_depth24_skip_up && depth24_is_fb_transfer(w, h)) { + /* Full-VRAM savestate restore: stage FBO; mark scanout band only. */ + if (w >= VRAM_W && h >= VRAM_H) { + up_add_transfer(x, y, w, h); + rect_clear(&s_d24_skip_fb); + depth24_mark_scanout_band(); + return; + } int x0 = x & (VRAM_W - 1), y0 = y & (VRAM_H - 1); rect_add(&s_d24_skip_fb, x0, y0, x0 + w - 1, y0 + h - 1); return; } up_add_transfer(x, y, w, h); /* exact touched rects, incl. per-pixel wrap */ } + +void vk_renderer_restage_vram_after_savestate(void) { + if (!s_ctx_ok || !s_vram) return; + s_up_nrects = 0; + rect_clear(&s_d24_skip_fb); + s_depth24_skip_up = 0; + up_add_transfer(0, 0, VRAM_W, VRAM_H); + flush_cpu_upload(); + if (gpu_display_is_depth24()) { + s_depth24_skip_up = 1; + depth24_mark_scanout_band(); + } +} static void vkb_vram_transfer_out(int x, int y, int w, int h, uint16_t *data) { ensure_cpu(); /* sync GPU-rendered content down to the CPU mirror first */ for (int row = 0; row < h; row++) diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index fa5d526a..79b05dc7 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -15,6 +15,7 @@ #include "text_xlate.h" #include "boot_state.h" #include "bios_hle.h" +#include "psx_bios_backend.h" #include "psx_cycles.h" #include "starvation_ring.h" #include "load_accel.h" @@ -67,7 +68,8 @@ extern "C" void psx_event_step_conservative_env_init(void); #include "launcher_profile.h" /* per-system variant profile (theme/caps bundle) */ #include "launcher_boot_timing.h" /* PSX_LAUNCHER_BOOT_TIMING stamps */ #endif -#include +#include "psx_sdl.h" +#include "psx_sdl_audio.h" #if defined(PSX_WEB) #include #endif @@ -110,8 +112,14 @@ extern "C" void psx_event_step_conservative_env_init(void); #endif #ifndef PSX_DEFAULT_BIOS_PATH +/* Compile-time fallback name only — not an implicit player choice. + * Runtime default with no explicit pick is bios/openbios.bin (see + * resolve_bios_for_runtime / docs/BIOS_SELECTION.md). */ #define PSX_DEFAULT_BIOS_PATH "bios/SCPH1001.BIN" #endif +#ifndef PSX_BUNDLED_BIOS_PATH +#define PSX_BUNDLED_BIOS_PATH "bios/openbios.bin" +#endif #ifndef PSX_DEFAULT_GAME_CONFIG_PATH #define PSX_DEFAULT_GAME_CONFIG_PATH "" #endif @@ -827,6 +835,19 @@ extern "C" void psx_frontend_on_savestate_loaded(void) { s_fps_last_frame = 0; /* Re-anchor guest-cycle→sample budgeting (pump clears queued PCM too). */ g_audio_cycle_resync = 1; + /* Depth24 FMV: drop ephemeral present hold/cutover so restored VRAM shows. + * Upload span came back with the GPU snap; treat MDEC as already active if + * depth24 is on so we don't full-black the first post-load presents. */ + gpu_depth24_on_savestate_loaded(); + s_d24_cutover_blank = 0; + s_d24_saw_gap = 0; + s_d24_prev_mdec = gpu_display_is_depth24() ? 1 : 0; + /* Depth24 load skipped framebuffer-sized CPU→GPU uploads — including the + * full-VRAM boot_state blit — so restage the mirror into the FBO/image + * before present. Otherwise post-FMV menus miss texture pages. Safe + * no-ops when that backend was never brought up. */ + gl_renderer_restage_vram_after_savestate(); + vk_renderer_restage_vram_after_savestate(); /* GL present-dirty early-out can skip SwapWindow when the restored frame * matches the last swap (typical on 2nd+ load of the same slot). Invalidate * tiles + force several swaps so the window actually updates. Safe no-op @@ -1379,57 +1400,133 @@ static std::filesystem::path normalize_disc_path_for_launch(const std::filesyste return p; } -static bool validate_bios_for_launch(const std::filesystem::path& path) { +/* BIOS selection state (docs/BIOS_SELECTION.md). s_openbios_allowed is the + * game's [runtime] openbios; s_bundled_bios_rel is where the shipped + * redistributable image lives relative to the executable. */ +static bool s_openbios_allowed = true; +static std::string s_bundled_bios_rel = PSX_BUNDLED_BIOS_PATH; + +/* Identity-match a file against a linked backend. Size+CRC must agree — + * bytes is a guaranteed wild jump. Identity therefore decides WHICH backend + * may run, not merely whether to warn. Null = no linked image matches. + */ +static const PsxBiosBackend* bios_backend_for_file(const std::filesystem::path& path, + uint32_t* out_crc, + uint64_t* out_size) { std::ifstream f(path, std::ios::binary | std::ios::ate); - if (!f.is_open()) return false; + if (!f.is_open()) return nullptr; const std::streamoff size = f.tellg(); - if (size != 512 * 1024) { - launcher_warning("BIOS Warning", - "The selected BIOS is not 512 KiB. Please select SCPH1001.BIN."); - return false; - } + if (out_size) *out_size = (uint64_t)size; + if (size <= 0) return nullptr; std::vector data((size_t)size); - if (!read_at(f, 0, data.data(), data.size())) return false; + if (!read_at(f, 0, data.data(), data.size())) return nullptr; const uint32_t crc = crc32_compute(data.data(), data.size()); - if (crc != 0x37157331u) { - char buf[256]; - std::snprintf(buf, sizeof(buf), - "The selected BIOS CRC32 is %08X, but this build was validated with SCPH1001.BIN CRC32 37157331.\n\n" - "The runtime will try it anyway, but boot may fail.", crc); - launcher_warning("BIOS Warning", buf); + if (out_crc) *out_crc = crc; + for (uint32_t i = 0; i < psx_bios_registry_count; i++) { + const PsxBiosBackend* b = psx_bios_registry[i]; + if (!b || !b->image) continue; + if ((uint64_t)size == (uint64_t)b->image->image_size && + crc == b->image->image_crc32) + return b; } - return true; + return nullptr; +} + +/* What a player may supply, for mismatch and picker copy. The bundled image is + * excluded: it is never something to go and find. */ +static std::string bios_accepted_images() { + std::string s; + for (uint32_t i = 0; i < psx_bios_registry_count; i++) { + const PsxBiosBackend* b = psx_bios_registry[i]; + if (!b || !b->image || b->image->image_bundled) continue; + if (!s.empty()) s += ", "; + s += b->image->image_id; + s += " (" + std::to_string(b->image->image_size / 1024u) + " KB)"; + } + return s.empty() ? std::string("(this build ships its own BIOS)") : s; +} + +/* Identity-gate a player-chosen BIOS and activate its backend on success. */ +static bool validate_bios_for_launch(const std::filesystem::path& path) { + uint32_t crc = 0; uint64_t size = 0; + const PsxBiosBackend* b = bios_backend_for_file(path, &crc, &size); + if (b) return psx_bios_activate(b) != 0; + + char buf[512]; + std::snprintf(buf, sizeof(buf), + "That BIOS (%llu bytes, CRC32 %08X) is not an image this build was " + "compiled from.\n\nThis build accepts: %s\n\nThe compiled-in code " + "would execute against mismatched data and crash.", + (unsigned long long)size, crc, bios_accepted_images().c_str()); + launcher_warning("BIOS Mismatch", buf); + return false; } static std::filesystem::path resolve_bios_path(const char* requested, const char* argv0); +/* Resolve which BIOS image file to load, and activate its backend. + * + * Policy (docs/BIOS_SELECTION.md): + * no explicit player choice -> bundled OpenBIOS, silently + * explicit choice, matching -> that image + * explicit choice, mismatched-> explained; falls back to OpenBIOS if allowed + * openbios disabled for this title -> a retail image is required + * + * "Explicit" means --bios or a remembered launcher/settings pick. Finding a + * file on disk deliberately does NOT count: discovery used to adopt whatever + * happened to sit near the executable, so two players with the same build + * could end up on different BIOSes. + */ static std::filesystem::path resolve_bios_for_runtime(const char* requested, - const char* argv0) { - std::filesystem::path resolved = resolve_bios_path(requested, argv0); - if (!resolved.empty() && std::filesystem::exists(resolved) && - validate_bios_for_launch(resolved)) { - return resolved; + const char* argv0, + bool requested_is_explicit) { + const bool openbios_allowed = s_openbios_allowed; + const PsxBiosBackend* bundled = psx_bios_bundled(); + + /* 1. An explicit choice: --bios, else a remembered pick. */ + std::filesystem::path chosen; + if (requested_is_explicit && requested && requested[0]) { + chosen = resolve_bios_path(requested, argv0); + } else { + std::filesystem::path cached = read_cached_path(argv0, "bios.cfg"); + if (!cached.empty() && std::filesystem::exists(cached)) chosen = cached; + } + if (!chosen.empty() && std::filesystem::exists(chosen)) { + if (validate_bios_for_launch(chosen)) return chosen; /* activates it */ + if (!(openbios_allowed && bundled)) return {}; } - std::filesystem::path cached = read_cached_path(argv0, "bios.cfg"); - if (!cached.empty() && std::filesystem::exists(cached) && - validate_bios_for_launch(cached)) { - return cached; + /* 2. No usable explicit choice -> bundled OpenBIOS, if this title allows it. */ + if (openbios_allowed && bundled) { + std::filesystem::path img = resolve_bios_path( + s_bundled_bios_rel.empty() ? nullptr : s_bundled_bios_rel.c_str(), argv0); + if (!img.empty() && std::filesystem::exists(img) && + bios_backend_for_file(img, nullptr, nullptr) == bundled && + psx_bios_activate(bundled)) { + return img; + } + launcher_warning("Bundled BIOS Missing", + std::string("This build ships its own BIOS (") + + bundled->image->image_id + "), but the bundled image is missing " + "or does not match.\n\nExpected next to the executable:\n" + + (s_bundled_bios_rel.empty() ? "(default path)" : s_bundled_bios_rel) + + "\n\nReinstall or rebuild."); + return {}; } - /* Interactive pick. Be explicit about WHICH of the two user-supplied - * files is being requested — first-time users see two pickers in a - * row and the difference between "BIOS" and "disc image" is not - * obvious to non-technical players. */ + /* 3. This title requires a retail BIOS: ask for one. */ + const std::string accepted = bios_accepted_images(); launcher_info((s_picker_game_name + " — PlayStation BIOS needed").c_str(), - s_picker_game_name + " does not include any Sony or game files.\n\n" + s_picker_game_name + " requires a PlayStation BIOS.\n\n" "Step 1 of 2 — PlayStation BIOS\n\n" - "In the next window, select your PlayStation BIOS dump. The file is " - "usually named SCPH1001.BIN and is exactly 512 KB. You must dump it " - "from your own console or otherwise legally obtain it.\n\n" + "In the next window, select your PlayStation BIOS dump. This build " + "requires the exact image it was compiled from: " + accepted + ". " + "Usually named SCPH1001.BIN and exactly 512 KB. Dump from your own " + "console or otherwise legally obtain it.\n\n" "(This is NOT the game disc — that is asked for next.)"); std::string bios_title = - s_picker_game_name + " — Step 1 of 2: select PlayStation BIOS (SCPH1001.BIN)"; + s_picker_game_name + " — Step 1 of 2: select PlayStation BIOS (" + + accepted + ")"; for (;;) { std::filesystem::path picked; if (!pick_runtime_file( @@ -1508,6 +1605,7 @@ static std::filesystem::path resolve_disc_for_runtime(const std::filesystem::pat static std::filesystem::path resolve_bios_path(const char* requested, const char* argv0) { namespace fs = std::filesystem; std::error_code ec; + if (!requested || !requested[0]) return {}; fs::path p(requested); if (fs::exists(p, ec)) { fs::path abs = fs::absolute(p, ec); @@ -1591,8 +1689,8 @@ static void shutdown_runtime(void) { overlay_capture_wait_pending(); overlay_capture_write_json(); if (sdl_audio_device) { - SDL_ClearQueuedAudio(sdl_audio_device); - SDL_CloseAudioDevice(sdl_audio_device); /* stops the pull callback */ + psx_sdl_audio_clear(sdl_audio_device); + psx_sdl_audio_close(sdl_audio_device); /* stops the pull callback */ sdl_audio_device = 0; } if (s_drc_ready) { rab_free(&s_drc); s_drc_ready = false; } @@ -1607,8 +1705,8 @@ static void teardown_game_session_keep_lobby(void) { psx_netplay_shutdown(); memcard_flush_all(); if (sdl_audio_device) { - SDL_ClearQueuedAudio(sdl_audio_device); - SDL_CloseAudioDevice(sdl_audio_device); + psx_sdl_audio_clear(sdl_audio_device); + psx_sdl_audio_close(sdl_audio_device); sdl_audio_device = 0; } if (s_drc_ready) { rab_free(&s_drc); s_drc_ready = false; } @@ -1668,7 +1766,7 @@ static void sdl_audio_pump(bool discard_output = false) { * means the device was silence-filling since the last pump = a gap. * Only meaningful after the first audio has been queued. */ const uint32_t max_queue_bytes = 44100u * bytes_per_frame / 5u; - queued = SDL_GetQueuedAudioSize(sdl_audio_device); + queued = psx_sdl_audio_queued_size(sdl_audio_device); if (queued == 0 && had_audio) { g_legacy_underruns++; audio_trace_event(AUDIO_EV_UNDERRUN, 0, 0); @@ -1717,7 +1815,7 @@ static void sdl_audio_pump(bool discard_output = false) { cycle_carry = 0; g_audio_cycle_resync = 0; if (legacy) - SDL_ClearQueuedAudio(sdl_audio_device); + psx_sdl_audio_clear(sdl_audio_device); else g_audio_unmute_resync = 1; /* skip mute-drain underrun reports */ return; @@ -1756,17 +1854,16 @@ static void sdl_audio_pump(bool discard_output = false) { if (legacy) { /* T3 tap: the exact post-fade bytes handed to the host audio queue. */ audio_trace_pcm(AUDIO_TAP_HOST, sdl_audio_buf, frames); - SDL_QueueAudio(sdl_audio_device, sdl_audio_buf, - (uint32_t)frames * bytes_per_frame); + psx_sdl_audio_queue(sdl_audio_device, sdl_audio_buf, (uint32_t)frames * bytes_per_frame); had_audio = 1; } else { /* Hand to the bridge (band-limited resample + DRC) instead of * SDL_QueueAudio. Lock guards the SPSC ring against the pull callback. * The T3 tap moves to sdl_drc_callback: what the device actually * receives is the bridge's device-rate output, not this buffer. */ - SDL_LockAudioDevice(sdl_audio_device); + psx_sdl_audio_lock(sdl_audio_device); rab_push(&s_drc, sdl_audio_buf, frames); - SDL_UnlockAudioDevice(sdl_audio_device); + psx_sdl_audio_unlock(sdl_audio_device); } } @@ -1793,7 +1890,7 @@ extern "C" int psx_audio_out_stats(double *fill_ms, uint64_t *underruns, *host_rate = g_audio_host_rate; if (*legacy || !s_drc_ready) { *fill_ms = sdl_audio_device - ? (double)SDL_GetQueuedAudioSize(sdl_audio_device) + ? (double)psx_sdl_audio_queued_size(sdl_audio_device) / (44100.0 * 4.0) * 1000.0 : 0.0; *underruns = g_legacy_underruns; @@ -2148,12 +2245,11 @@ static void sdl_audio_update(int hard_mute_active, int turbo_sink_active) { audio_trace_event(AUDIO_EV_MUTE, (uint32_t)tail, 0); if (audio_legacy_mode()) { audio_trace_pcm(AUDIO_TAP_HOST, sdl_audio_buf, tail); - SDL_QueueAudio(sdl_audio_device, sdl_audio_buf, - (uint32_t)tail * sizeof(int16_t) * 2u); + psx_sdl_audio_queue(sdl_audio_device, sdl_audio_buf, (uint32_t)tail * sizeof(int16_t) * 2u); } else if (s_drc_ready) { - SDL_LockAudioDevice(sdl_audio_device); + psx_sdl_audio_lock(sdl_audio_device); rab_push(&s_drc, sdl_audio_buf, tail); - SDL_UnlockAudioDevice(sdl_audio_device); + psx_sdl_audio_unlock(sdl_audio_device); } muted = 1; } @@ -3252,16 +3348,23 @@ static void netplay_barrier_admit(int override) { netplay_soft_exit("netplay_peer_disconnect"); if (psx_return_to_lobby_requested()) return; } + /* Staged .pst rejected (stale codegen / BIOS / missing) — do not wait + * out the 90s load barrier with stall=load_apply_done. */ + if (psx_netplay_consume_load_apply_failed()) { + netplay_soft_exit("netplay_load_failed"); + if (psx_return_to_lobby_requested()) return; + } /* Mutual INPUT/CONFIRM stall still refreshes last_peer_rx — detect * "no sim progress" separately (common rematch + TURN loss mode). - * Load probe/xfer/apply/ready uses a longer budget (TURN + 1.4MB). */ + * Save/load/memcard probe+chunk xfer uses a longer budget (TURN + + * ~1.4MB .pst). The old 20s admit timeout killed SAVE mid-transfer. */ if (psx_netplay_in_load_barrier() && now_ms - barrier_t0 >= 90000u) { char stall[96]; uint32_t sim = 0; int lead = 0; psx_netplay_admit_wait_info(stall, sizeof(stall), &sim, &lead); std::fprintf(stderr, - "psxrecomp: netplay load barrier timeout sim=%u " + "psxrecomp: netplay state barrier timeout sim=%u " "stall=%s lead=%d — returning to lobby\n", (unsigned)sim, stall[0] ? stall : "?", lead); netplay_soft_exit("netplay_load_stall"); @@ -3337,9 +3440,16 @@ static void netplay_barrier_admit(int override) { netplay_soft_exit("sdl_window_close"); if (psx_return_to_lobby_requested()) return; } - if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_ESCAPE) { - netplay_soft_exit("netplay_barrier_escape"); - if (psx_return_to_lobby_requested()) return; + if (ev.type == SDL_KEYDOWN) { +#if defined(PSX_SDL3) + const SDL_Keycode key = ev.key.key; +#else + const SDL_Keycode key = ev.key.keysym.sym; +#endif + if (key == SDLK_ESCAPE) { + netplay_soft_exit("netplay_barrier_escape"); + if (psx_return_to_lobby_requested()) return; + } } if (ev.type == SDL_CONTROLLERDEVICEADDED || ev.type == SDL_CONTROLLERDEVICEREMOVED) { @@ -3587,18 +3697,20 @@ static void depth24_cutover_tick(int depth24) { } /* Depth24 FMV: CRTC width stays full (e.g. MotK 512) while MDEC uploads may - * not cover the right side yet — cutover flashes a large colorful junk block - * when leftover VRAM is read as RGB888. Present width is never shrunk - * (cropping caused a flickering black pillar); black-fill uncovered columns. + * not cover the right side yet — leftover VRAM read as RGB888 flashes junk. + * Present width is never shrunk; black-fill only the trailing uncovered cols. * - * 0) Cutover hold: full-frame black for the first presents after an MDEC gap - * 1) Upload-span blank: [gpu_depth24_rgb_limit .. w) - * 2) Chroma fringe: if span reports full, still black the last ~8 cols when - * dense chroma junk is present (MotK crawl). Do NOT replicate the last - * good column — that stretched a tinted edge into an 8-wide streak. */ + * Match origin/master's span policy: when upload span is unknown (lim==0), + * only blank the last ~8 columns. Blanking [0..w) on lim==0 (an older tip + * path) turned MotK FMV into a permanent black screen after GP1(07h) hold + * reset the span — hold ticks skip Swap, then the next present saw lim=0 + * and wiped the whole frame every vblank. + * + * Optional short cutover blank (tip): full-frame black for 1–2 presents when + * MDEC returns after an idle gap, hiding one-frame transitional junk. */ static void depth24_fix_trailing_margin(uint32_t *buf, uint32_t w, uint32_t h, uint32_t display_x) { - if (!buf || w == 0u || h == 0u) return; + if (!buf || w < 8u || h == 0u) return; if (s_d24_cutover_blank > 0) { s_d24_cutover_blank--; @@ -3608,37 +3720,14 @@ static void depth24_fix_trailing_margin(uint32_t *buf, uint32_t w, uint32_t h, return; } - uint32_t good = gpu_depth24_rgb_limit(display_x, w); - if (good > w) good = w; - if (good < w) { - for (uint32_t y = 0; y < h; y++) { - for (uint32_t x = good; x < w; x++) - buf[y * w + x] = 0xFF000000u; - } - return; /* span blank already covered the junk region */ - } - - if (w < 24u) return; - const uint32_t margin = 8u; - const uint32_t edge = w - margin; - const uint32_t total = margin * h; - uint32_t junk_px = 0; - for (uint32_t x = edge; x < w; x++) { - for (uint32_t y = 0; y < h; y++) { - uint32_t p = buf[y * w + x]; - int r = (int)((p >> 16) & 255u); - int g = (int)((p >> 8) & 255u); - int b = (int)(p & 255u); - int m = (r + g + b) / 3; - int ch = (r > m ? r - m : m - r) + (g > m ? g - m : m - g) + - (b > m ? b - m : m - b); - if (ch > 40) junk_px++; - } - } - /* ~12% of margin texels — sparse stars stay; dense chroma fringe cleans. */ - if (total == 0u || junk_px * 100u < total * 12u) return; + /* Default: last 8 columns. If the upload span is known and ends earlier + * inside that margin, start blanking from the span edge instead. */ + uint32_t start = w - 8u; + uint32_t lim = gpu_depth24_rgb_limit(display_x, w); + if (lim > 0u && lim < w && lim < start) + start = lim; for (uint32_t y = 0; y < h; y++) { - for (uint32_t x = edge; x < w; x++) + for (uint32_t x = start; x < w; x++) buf[y * w + x] = 0xFF000000u; } } @@ -3766,23 +3855,33 @@ static void sdl_vblank_present(void) { } else if (ev.type == SDL_CONTROLLERDEVICEREMOVED) { bool ours = false; for (int s = 0; s < PSX_MAX_PLAYERS; s++) { +#if defined(PSX_SDL3) + if (ev.gdevice.which == g_players[s].instance) { ours = true; break; } +#else if (ev.cdevice.which == g_players[s].instance) { ours = true; break; } +#endif } if (ours) { close_controller(); refresh_player_devices(); } } else if (ev.type == SDL_KEYDOWN) { +#if defined(PSX_SDL3) + const SDL_Keymod mod = ev.key.mod; + const SDL_Keycode key = ev.key.key; +#else const Uint16 mod = ev.key.keysym.mod; - if (ev.key.keysym.sym == SDLK_ESCAPE && psx_netplay_active()) { + const SDL_Keycode key = ev.key.keysym.sym; +#endif + if (key == SDLK_ESCAPE && psx_netplay_active()) { netplay_soft_exit("netplay_escape"); return; } /* Save states: Shift+F1-F12 = save slot 0-11, F1-F12 = load. * (F11 is a save slot per the user's spec, so fullscreen is * Alt+Enter / Cmd+Ctrl+F only — no F11.) */ - if (ev.key.keysym.sym >= SDLK_F1 && ev.key.keysym.sym <= SDLK_F12) { - int slot = (int)(ev.key.keysym.sym - SDLK_F1); /* 0..11 */ + if (key >= SDLK_F1 && key <= SDLK_F12) { + int slot = (int)(key - SDLK_F1); /* 0..11 */ if (psx_netplay_active()) { /* Match host only — guest F-keys must not initiate. */ if (!psx_netplay_is_host()) { @@ -3798,7 +3897,7 @@ static void sdl_vblank_present(void) { savestate_request_load(slot); } } - else if (ev.key.keysym.sym == SDLK_c && (mod & KMOD_CTRL)) { + else if (key == SDLK_c && (mod & KMOD_CTRL)) { std::fprintf(stdout, "[DEBUG] Forzando reinserción de CD...\n"); debug_force_cd_reinsert(); } @@ -3810,8 +3909,8 @@ static void sdl_vblank_present(void) { * set in both SDL_WINDOW_FULLSCREEN and * SDL_WINDOW_FULLSCREEN_DESKTOP, so testing just that bit * detects "currently fullscreen, either mode". */ - else if ((ev.key.keysym.sym == SDLK_RETURN && (mod & KMOD_ALT)) || - (ev.key.keysym.sym == SDLK_f && (mod & (KMOD_GUI | KMOD_CTRL)))) { + else if ((key == SDLK_RETURN && (mod & KMOD_ALT)) || + (key == SDLK_f && (mod & (KMOD_GUI | KMOD_CTRL)))) { Uint32 is_fs = SDL_GetWindowFlags(sdl_window) & SDL_WINDOW_FULLSCREEN; if (is_fs) { @@ -4095,6 +4194,14 @@ static void sdl_vblank_present(void) { /* Mod hooks. Run after all normal input sampling. */ mod_call_frame_hooks(); + /* Depth24 GP1(07h) retarget (MotK intro→crawl): keep the prior Swap for a + * few vblanks so stale trailing VRAM never flashes on the right edge. + * Must tick every present — gpu.c arms s_d24_present_hold and also + * freezes upload-span tracking while hold > 0; without this call the hold + * sticks and depth24_fix_trailing_margin blanks the whole FMV forever. */ + if (gpu_depth24_present_hold_tick()) + return; + /* Engage widescreen at game entry: BIOS boot stays authentic 4:3. */ if (!g_ws_engaged) { extern int fntrace_is_game_started(void); @@ -4436,8 +4543,20 @@ namespace { const char* g_lnch_argv0 = nullptr; int ae_bios_verify(const char* bios_path, RecompLauncherCBiosVerify* out) { - if (!bios_path || !bios_path[0] || !out) return 0; + if (!out) return 0; std::memset(out, 0, sizeof(*out)); + /* Empty path = use bundled OpenBIOS when this title allows it. */ + if (!bios_path || !bios_path[0]) { + if (s_openbios_allowed && psx_bios_bundled()) { + out->ok = 1; + std::snprintf(out->detail, sizeof(out->detail), + "Using bundled OpenBIOS."); + return 1; + } + std::snprintf(out->detail, sizeof(out->detail), + "PlayStation BIOS required (SCPH1001.BIN)."); + return 1; + } std::ifstream f(bios_path, std::ios::binary | std::ios::ate); if (!f.is_open()) { std::snprintf(out->detail, sizeof(out->detail), "BIOS file not found."); @@ -6639,6 +6758,10 @@ int main(int argc, char** argv) { const char* game_config_path = nullptr; const char* disc_override_path = nullptr; bool bios_from_cli = false; /* CLI --bios/positional wins over settings.toml */ + /* Did the PLAYER choose this BIOS (CLI or settings), as opposed to it + * being the compile-time default? Only a real choice overrides the + * bundled OpenBIOS — see docs/BIOS_SELECTION.md. */ + bool bios_explicit = false; /* Launcher overrides (mirrors snesrecomp): --launcher forces the GUI back on * even when [launcher] skip_launcher = true is set; --no-launcher (and the * PSX_NO_LAUNCHER env) forces it off. --launcher wins if both are given. */ @@ -6679,6 +6802,7 @@ int main(int argc, char** argv) { if (std::strcmp(argv[i], "--bios") == 0 && i + 1 < argc) { bios_path = argv[++i]; bios_from_cli = true; + bios_explicit = true; } else if (std::strcmp(argv[i], "--game") == 0 && i + 1 < argc) { game_config_path = argv[++i]; } else if (std::strcmp(argv[i], "--disc") == 0 && i + 1 < argc) { @@ -6729,6 +6853,7 @@ int main(int argc, char** argv) { if (!bios_from_cli) { bios_path = argv[i]; bios_from_cli = true; + bios_explicit = true; } else { std::fprintf(stderr, "psxrecomp: ignoring unexpected positional argument after BIOS selection: %s\n", @@ -6832,6 +6957,7 @@ std::string player_device[PSX_MAX_PLAYERS]; bool deferred_overlay_cache = false; std::filesystem::path deferred_overlay_project_root; std::vector deferred_overlay_native_block; + uint32_t deferred_overlay_config_hash = 0; std::string deferred_overlay_backend; bool deferred_has_overlay_ac = false; std::string deferred_overlay_ac; @@ -7038,6 +7164,9 @@ std::string player_device[PSX_MAX_PLAYERS]; fast_boot = gc.runtime.fast_boot; bios_hle = gc.runtime.bios_hle; bios_hle_keep_intro = gc.runtime.bios_hle_keep_intro; + /* Developer compatibility finding, applied before BIOS selection. + * Not exposed to settings.toml on purpose — see BIOS_SELECTION.md. */ + s_openbios_allowed = gc.runtime.openbios; /* Let the dispatch layer distinguish "dirty because text was * loaded" from "diverged because runtime wrote different code over * the original EXE image". Packed/self-modifying games can rewrite @@ -7073,6 +7202,8 @@ std::string player_device[PSX_MAX_PLAYERS]; deferred_overlay_cache = true; deferred_overlay_project_root = gc.project_root; deferred_overlay_native_block = gc.runtime.overlay_native_block; + deferred_overlay_config_hash = + PSXRecompV4::overlay_codegen_config_hash(gc); deferred_overlay_backend = gc.runtime.overlay_backend; deferred_has_overlay_ac = gc.runtime.has_overlay_autocompile_cmd; deferred_overlay_ac = gc.runtime.overlay_autocompile_cmd; @@ -7166,9 +7297,10 @@ std::string player_device[PSX_MAX_PLAYERS]; g_video_aspect_den = us.aspect_den; } if (us.has_spu_hq) g_audio_spu_hq = us.spu_hq; - if (us.has_bios_path && !bios_from_cli) { + if (us.has_bios_path && !bios_from_cli && !us.bios_path.empty()) { settings_bios_storage = us.bios_path.string(); bios_path = settings_bios_storage.c_str(); + bios_explicit = true; } if (us.has_disc_path && !disc_override_path) resolved_disc = normalize_disc_path_for_launch(us.disc_path); @@ -7353,7 +7485,8 @@ std::string player_device[PSX_MAX_PLAYERS]; capture_persist_dir.empty() ? nullptr : capture_persist_dir.c_str(), game_id.c_str()); - overlay_loader_init(cache_dir.c_str(), game_id.c_str()); + overlay_loader_init(cache_dir.c_str(), game_id.c_str(), + deferred_overlay_config_hash); for (uint32_t addr : deferred_overlay_native_block) { overlay_loader_native_block_add(addr); } @@ -7499,7 +7632,10 @@ std::string player_device[PSX_MAX_PLAYERS]; seed.netplay_lobby_url = g_lnch_lobby_url; seed.has_netplay_lobby_url = true; } - if (bios_path && bios_path[0]) { seed.bios_path = bios_path; seed.has_bios_path = true; } + if (bios_explicit && bios_path && bios_path[0]) { + seed.bios_path = bios_path; + seed.has_bios_path = true; + } if (!resolved_disc.empty()) { seed.disc_path = resolved_disc; seed.has_disc_path = true; } seed.memcard_dir = memcard_dir; seed.has_memcard_dir = true; seed.memcard1_enabled = memcard1_enabled; seed.has_memcard1_enabled = true; @@ -7701,11 +7837,14 @@ std::string player_device[PSX_MAX_PLAYERS]; * the setup wizard inside recomp-ui (cross-platform file pickers). */ { bool bios_ok = false; + RecompLauncherCBiosVerify bv{}; if (ls.bios_path[0]) { - RecompLauncherCBiosVerify bv{}; if (ae_bios_verify(ls.bios_path, &bv) && bv.ok) bios_ok = true; else ls.bios_path[0] = '\0'; } + if (!bios_ok) { + if (ae_bios_verify("", &bv) && bv.ok) bios_ok = true; + } bool disc_ok = false; if (!rui_initial_disc.empty()) { std::error_code ec; @@ -7812,6 +7951,9 @@ std::string player_device[PSX_MAX_PLAYERS]; if (ls.bios_path[0]) { seed.bios_path = ls.bios_path; seed.has_bios_path = true; + } else { + seed.bios_path.clear(); + seed.has_bios_path = false; } /* Memory-card slots: enable flags + any Browse/New paths. */ seed.memcard1_enabled = ls.memcard_enabled[0] != 0; seed.has_memcard1_enabled = true; @@ -7901,7 +8043,13 @@ std::string player_device[PSX_MAX_PLAYERS]; if (seed.has_bios_path) { settings_bios_storage = seed.bios_path.string(); bios_path = settings_bios_storage.c_str(); + bios_explicit = true; write_cached_path(argv[0], "bios.cfg", seed.bios_path); + } else if (!bios_from_cli) { + /* Cleared to bundled OpenBIOS — drop any cached retail pick. */ + bios_explicit = false; + std::error_code ec; + std::filesystem::remove(sidecar_cfg_path(argv[0], "bios.cfg"), ec); } if (seed.has_disc_path) { seed.disc_path = normalize_disc_path_for_launch(seed.disc_path); @@ -7966,7 +8114,8 @@ std::string player_device[PSX_MAX_PLAYERS]; memcard2_path.clear(); } - std::filesystem::path resolved_bios = resolve_bios_for_runtime(bios_path, argv[0]); + std::filesystem::path resolved_bios = + resolve_bios_for_runtime(bios_path, argv[0], bios_explicit); if (resolved_bios.empty()) { std::fprintf(stderr, "psxrecomp: no BIOS selected; exiting.\n"); return 1; @@ -8219,18 +8368,17 @@ session_reboot: #ifndef PSX_SDL_NO_AUDIO audio_trace_init(); if (SDL_InitSubSystem(SDL_INIT_AUDIO) == 0) { - SDL_AudioSpec want; - SDL_AudioSpec have; - SDL_zero(want); + PsxSdlAudioSpec want = {}; + PsxSdlAudioSpec have = {}; want.freq = 44100; want.format = AUDIO_S16SYS; want.channels = 2; want.samples = 1024; const bool legacy = audio_legacy_mode(); + want.allow_frequency_change = legacy ? 0 : 1; if (!legacy) want.callback = sdl_drc_callback; /* pull model: bridge resamples + DRC */ - sdl_audio_device = SDL_OpenAudioDevice(NULL, 0, &want, &have, - legacy ? 0 : SDL_AUDIO_ALLOW_FREQUENCY_CHANGE); + sdl_audio_device = psx_sdl_audio_open(&want, &have); if (sdl_audio_device) { if (!legacy) { rab_config cfg; rab_config_defaults(&cfg); @@ -8241,7 +8389,7 @@ session_reboot: } g_audio_host_rate = have.freq; audio_trace_set_tap_rate(AUDIO_TAP_HOST, (uint32_t)have.freq); - SDL_PauseAudioDevice(sdl_audio_device, 0); + (void)psx_sdl_audio_resume(sdl_audio_device); } } #endif @@ -8314,7 +8462,8 @@ session_reboot: * undersized and the wide readback overflows it. */ g_video_scale = gr_scale(); gl_renderer_set_interpolation(g_frame_interpolation, g_host_refresh_hz, - (double)g_frame_interpolation_fps); + (double)g_frame_interpolation_fps, + /*blend_mode*/ 0); } /* Vulkan backend: create the instance/device/swapchain on the * SDL_WINDOW_VULKAN window. On failure, fall back to software (vkb_init diff --git a/runtime/src/mdec.c b/runtime/src/mdec.c index d9b7ae6e..64526476 100644 --- a/runtime/src/mdec.c +++ b/runtime/src/mdec.c @@ -1,4 +1,5 @@ #include "mdec.h" +#include "pst_wire.h" #include #include @@ -756,3 +757,140 @@ void mdec_debug_dma_out_end(uint32_t addr, uint32_t words) { (void)addr; trace_event(MDEC_EVT_DMA_OUT_END, words); } + +/* ---- boot_state snapshot (variable-length input/output FIFOs) ------------ */ +#define MDEC_SNAP_VER 1u +#define MDEC_SNAP_INPUT_MAX (4u * 1024u * 1024u) /* halfwords */ +#define MDEC_SNAP_OUTPUT_MAX (8u * 1024u * 1024u) /* bytes */ + +static uint32_t mdec_snap_fixed_bytes(void) { + /* ver + scalars + tables + counts + last_color_age */ + return 4u + /* ver */ + 4u * 14u + /* u32 scalars */ + 1u * 8u + /* u8 flags */ + 64u + 64u + /* y/uv quant */ + 64u * 2u + /* scale i16 */ + 4u + 4u + /* input_count, output_size */ + 8u; /* last_color_age */ +} + +uint32_t mdec_snapshot_bytes(void) { + uint64_t n = (uint64_t)mdec_snap_fixed_bytes() + + (uint64_t)mdec.input_count * 2u + + (uint64_t)mdec.output_size; + if (n > 0xffffffffu) return 0; + return (uint32_t)n; +} + +void mdec_snapshot_write(uint8_t *p) { + PstW w; + uint32_t n = mdec_snapshot_bytes(); + uint64_t age; + if (!p || n == 0) return; + pst_w_init(&w, p, n); + (void)pst_w_u32(&w, MDEC_SNAP_VER); + (void)pst_w_u32(&w, mdec.command); + (void)pst_w_u32(&w, mdec.expected_halfwords); + (void)pst_w_u32(&w, mdec.last_status); + (void)pst_w_u32(&w, mdec.decode_macroblocks); + (void)pst_w_u32(&w, mdec.decode_blocks); + (void)pst_w_u32(&w, mdec.decode_stop_reason); + (void)pst_w_u32(&w, mdec.decode_input_pos); + (void)pst_w_u32(&w, mdec.decode_input_end); + (void)pst_w_u32(&w, mdec.dma_in_words); + (void)pst_w_u32(&w, mdec.dma_out_words); + (void)pst_w_u32(&w, mdec.dma_read_underflows); + (void)pst_w_u32(&w, mdec.output_pos); + (void)pst_w_u32(&w, 0u); /* reserved */ + (void)pst_w_u32(&w, 0u); /* reserved */ + (void)pst_w_u8(&w, mdec.output_bit15); + (void)pst_w_u8(&w, mdec.output_signed); + (void)pst_w_u8(&w, mdec.output_depth); + (void)pst_w_u8(&w, mdec.current_block); + (void)pst_w_u8(&w, mdec.busy); + (void)pst_w_u8(&w, mdec.input_full); + (void)pst_w_u8(&w, mdec.enable_dma_in); + (void)pst_w_u8(&w, mdec.enable_dma_out); + (void)pst_w_bytes(&w, mdec.y_quant, 64u); + (void)pst_w_bytes(&w, mdec.uv_quant, 64u); + for (int i = 0; i < 64; i++) + (void)pst_w_i16(&w, mdec.scale[i]); + (void)pst_w_u32(&w, mdec.input_count); + (void)pst_w_u32(&w, mdec.output_size); + if (s_frame_count >= mdec_last_color_decode_frame) + age = s_frame_count - mdec_last_color_decode_frame; + else + age = 1000ull; + (void)pst_w_u64(&w, age); + for (uint32_t i = 0; i < mdec.input_count; i++) + (void)pst_w_u16(&w, mdec.input ? mdec.input[i] : 0u); + if (mdec.output_size && mdec.output) + (void)pst_w_bytes(&w, mdec.output, mdec.output_size); +} + +int mdec_snapshot_read(const uint8_t *p, uint32_t len) { + PstR r; + uint32_t ver = 0, input_count = 0, output_size = 0, reserved; + uint64_t age = 1000ull; + int16_t s16; + if (!p || len < mdec_snap_fixed_bytes()) return 0; + pst_r_init(&r, p, len); + if (!pst_r_u32(&r, &ver) || ver != MDEC_SNAP_VER) return 0; + if (!pst_r_u32(&r, &mdec.command) || + !pst_r_u32(&r, &mdec.expected_halfwords) || + !pst_r_u32(&r, &mdec.last_status) || + !pst_r_u32(&r, &mdec.decode_macroblocks) || + !pst_r_u32(&r, &mdec.decode_blocks) || + !pst_r_u32(&r, &mdec.decode_stop_reason) || + !pst_r_u32(&r, &mdec.decode_input_pos) || + !pst_r_u32(&r, &mdec.decode_input_end) || + !pst_r_u32(&r, &mdec.dma_in_words) || + !pst_r_u32(&r, &mdec.dma_out_words) || + !pst_r_u32(&r, &mdec.dma_read_underflows) || + !pst_r_u32(&r, &mdec.output_pos) || + !pst_r_u32(&r, &reserved) || + !pst_r_u32(&r, &reserved)) + return 0; + if (!pst_r_u8(&r, &mdec.output_bit15) || + !pst_r_u8(&r, &mdec.output_signed) || + !pst_r_u8(&r, &mdec.output_depth) || + !pst_r_u8(&r, &mdec.current_block) || + !pst_r_u8(&r, &mdec.busy) || + !pst_r_u8(&r, &mdec.input_full) || + !pst_r_u8(&r, &mdec.enable_dma_in) || + !pst_r_u8(&r, &mdec.enable_dma_out)) + return 0; + if (!pst_r_bytes(&r, mdec.y_quant, 64u) || + !pst_r_bytes(&r, mdec.uv_quant, 64u)) + return 0; + for (int i = 0; i < 64; i++) { + if (!pst_r_i16(&r, &s16)) return 0; + mdec.scale[i] = s16; + } + if (!pst_r_u32(&r, &input_count) || !pst_r_u32(&r, &output_size) || + !pst_r_u64(&r, &age)) + return 0; + if (input_count > MDEC_SNAP_INPUT_MAX || output_size > MDEC_SNAP_OUTPUT_MAX) + return 0; + if (mdec.output_pos > output_size) return 0; + if ((size_t)(r.end - r.p) < + (size_t)input_count * 2u + (size_t)output_size) + return 0; + if (!ensure_input_capacity(input_count ? input_count : 1u)) return 0; + if (!ensure_output_capacity(output_size ? output_size : 1u)) return 0; + mdec.input_count = input_count; + mdec.output_size = output_size; + for (uint32_t i = 0; i < input_count; i++) { + uint16_t hw; + if (!pst_r_u16(&r, &hw)) return 0; + mdec.input[i] = hw; + } + if (output_size && !pst_r_bytes(&r, mdec.output, output_size)) + return 0; + if (age > 100000ull) age = 100000ull; + if (age >= s_frame_count) + mdec_last_color_decode_frame = 0; + else + mdec_last_color_decode_frame = s_frame_count - age; + return 1; +} diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index 0adc09ad..5df9a0ec 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -763,6 +763,17 @@ int provider_commit(void*, const char* image_path) { return 1; } +int provider_commit_netplay(void*, const char* image_path) { + (void)image_path; + std::string error; + if (!mod_runtime_clear_for_netplay(&error)) { + set_error(error); + return 0; + } + state().error.clear(); + return 1; +} + const char* provider_error(void*) { return state().error.c_str(); } @@ -790,6 +801,9 @@ RecompLauncherCModProvider provider = { provider_feature_set_option, provider_diagnostic_count, provider_diagnostic_get, + nullptr, /* archive_extension — PSX defaults */ + nullptr, /* archive_description */ + provider_commit_netplay, }; #endif @@ -836,6 +850,28 @@ bool mod_runtime_initialize(const std::filesystem::path& root, return true; } +bool mod_runtime_clear_for_netplay(std::string* error) { + RuntimeMods& s = state(); + if (!s.initialized) { + if (error) error->clear(); + return true; + } + s.plan = {}; + s.validation = {}; + s.raw_disc_index.clear(); + s.user_disc_index.clear(); + s.raw_overlay_index.clear(); + s.user_overlay_index.clear(); + s.effective_disc_path.clear(); + s.main_applied = false; + s.disc_enabled = false; + s.disc_guard_failed = false; + s.error.clear(); + if (error) error->clear(); + std::fprintf(stdout, "psxrecomp: mods cleared for netplay (vanilla session)\n"); + return true; +} + bool mod_runtime_commit(const std::filesystem::path& disc_path, std::string* error) { RuntimeMods& s = state(); if (!s.initialized) return true; diff --git a/runtime/src/psx_netplay.c b/runtime/src/psx_netplay.c index 51ab51e4..f2910cc8 100644 --- a/runtime/src/psx_netplay.c +++ b/runtime/src/psx_netplay.c @@ -196,6 +196,7 @@ int psx_netplay_is_host(void) { return 0; } int psx_netplay_request_save(int slot) { (void)slot; return 0; } int psx_netplay_request_load(int slot) { (void)slot; return 0; } int psx_netplay_in_load_barrier(void) { return 0; } +int psx_netplay_consume_load_apply_failed(void) { return 0; } void psx_netplay_pump(void) {} int psx_netplay_poll_admit(void) { return 1; } void psx_netplay_finish_frame(void) {} @@ -260,9 +261,12 @@ typedef struct { int mc_sync_done; int mc_sync_sent; int local_save_staged; + int local_save_acked; /* guest: coord reply already sent */ + uint32_t save_target_tick; /* both peers save during this sim_tick */ int load_applied_local; int load_ready_replied; /* READY exchanged; synced; stay LOAD_READY until admit */ int load_sync_done; /* hard_resync+prime once at mutual ready */ + int load_apply_failed; /* sticky: staged apply rejected — soft-exit */ /* Transport / ICE / diag (MotK online path). */ int use_ice; int ice_has_turn; @@ -322,6 +326,7 @@ static void np_enter_load_ready(int slot); static void np_commit_load_sync(void); static void np_begin_load_apply(int slot); static void np_starv_reset(void); +static void np_maybe_stage_target_save(void); static int np_file_crc(const uint8_t *data, size_t size, uint32_t *crc_out) { @@ -582,24 +587,30 @@ static void np_guest_handle_probe(void) } if (size == 0) { - /* SAVE coordinate local write (admit is not stalled for size==0). */ - if (!g_np.local_save_staged) { - if (savestate_request_save_protocol((int)slot)) { - g_np.local_save_staged = 1; - printf("psxrecomp: netplay guest save slot=%u — writing sandbox…\n", - (unsigned)slot); - fflush(stdout); - } else { - (void)rnet_session_state_probe_reply(g_np.session, 0); - return; - } + /* SAVE coord: crc carries the shared target sim_tick. Both peers + * stage the write when sim reaches that tick (see + * np_maybe_stage_target_save) so CRCs match and skip transfer. */ + if (g_np.xfer != NP_XFER_SAVE_COORD) { + g_np.xfer = NP_XFER_SAVE_COORD; + g_np.xfer_slot = (int)slot; + g_np.save_target_tick = crc; + g_np.local_save_staged = 0; + g_np.local_save_acked = 0; + printf("psxrecomp: netplay guest save slot=%u — armed target " + "sim=%u\n", + (unsigned)slot, (unsigned)crc); + fflush(stdout); } if (savestate_pending()) return; - if (!savestate_slot_exists((int)slot)) return; - g_np.local_save_staged = 0; - (void)rnet_session_state_probe_reply(g_np.session, 1); - printf("psxrecomp: netplay guest save slot=%u — local write done\n", (unsigned)slot); - fflush(stdout); + if (!g_np.local_save_staged || !savestate_slot_exists((int)slot)) return; + if (!g_np.local_save_acked) { + g_np.local_save_acked = 1; + (void)rnet_session_state_probe_reply(g_np.session, 1); + printf("psxrecomp: netplay guest save slot=%u — local write done " + "@ target sim (frozen until hash probe)\n", + (unsigned)slot); + fflush(stdout); + } return; } @@ -613,18 +624,53 @@ static void np_guest_handle_probe(void) { uint32_t local_sz = 0, local_crc = 0; + char reason[192]; match = np_slot_crc((int)slot, &local_sz, &local_crc) && local_sz == size && local_crc == crc; + /* CRC match of a stale .pst (wrong codegen) is not loadable — ask the + * host to transfer. Host also refuses probe start if its own slot is + * stale, so this mainly covers guest-sandbox drift. */ + if (match && op == RNET_STATE_OP_LOAD && + !savestate_slot_compatible((int)slot, reason, sizeof(reason))) { + printf("psxrecomp: netplay guest load slot=%u — hash matched but " + "unloadable (%s); requesting transfer\n", + (unsigned)slot, reason[0] ? reason : "incompatible"); + fflush(stdout); + match = 0; + } (void)rnet_session_state_probe_reply(g_np.session, match); - if (match && op == RNET_STATE_OP_LOAD) { - if (g_np.xfer != NP_XFER_LOAD_APPLYING && g_np.xfer != NP_XFER_LOAD_READY) { - (void)savestate_request_load_protocol((int)slot); - np_begin_load_apply((int)slot); - printf("psxrecomp: netplay guest load slot=%u — hashes match, applying…\n", + if (op == RNET_STATE_OP_SAVE) { + if (match) { + g_np.xfer = NP_XFER_NONE; + printf("psxrecomp: netplay guest save slot=%u — hashes match, " + "skip transfer\n", (unsigned)slot); fflush(stdout); } else { - /* Retransmit of hash probe — already staging/applying. */ + /* Host will chunk the authoritative .pst — stay parked. */ + g_np.xfer = NP_XFER_SAVE_SEND; + g_np.xfer_slot = (int)slot; + } + } else if (op == RNET_STATE_OP_LOAD) { + if (match) { + if (g_np.xfer != NP_XFER_LOAD_APPLYING && + g_np.xfer != NP_XFER_LOAD_READY) { + (void)savestate_request_load_protocol((int)slot); + np_begin_load_apply((int)slot); + printf("psxrecomp: netplay guest load slot=%u — hashes match, " + "applying…\n", + (unsigned)slot); + fflush(stdout); + } + } else { + /* Must mark LOAD_SEND or guest keeps the 20s admit timeout and + * BYEs the host mid-TURN transfer. */ + g_np.xfer = NP_XFER_LOAD_SEND; + g_np.xfer_slot = (int)slot; + printf("psxrecomp: netplay guest load slot=%u — hash miss, " + "waiting for transfer…\n", + (unsigned)slot); + fflush(stdout); } } } @@ -665,8 +711,11 @@ static void np_host_drive_xfer(void) return; case NP_XFER_SAVE_COORD: + /* Host + guest both stage at save_target_tick; wait for local write + * and guest ACK before hashing. */ if (savestate_pending()) return; - if (!savestate_slot_exists(g_np.xfer_slot)) return; + if (!g_np.local_save_staged || !savestate_slot_exists(g_np.xfer_slot)) + return; if (!rnet_session_state_probe_take_reply(g_np.session, &match)) return; rnet_session_state_probe_finish(g_np.session); @@ -874,6 +923,23 @@ static void np_drive_load_barrier(void) return; if (savestate_pending()) return; + if (savestate_take_load_failed()) { + /* Stale/mismatched .pst: do not sit in load_apply_done forever. */ + printf("psxrecomp: netplay load slot=%d — apply failed " + "(incompatible or missing .pst) — aborting barrier\n", + g_np.xfer_slot); + fflush(stdout); + if (g_np.session) + rnet_session_state_finish(g_np.session, 0); + g_np.xfer = NP_XFER_NONE; + g_np.load_applied_local = 0; + g_np.load_ready_replied = 0; + g_np.load_sync_done = 0; + g_np.load_apply_failed = 1; + if (g_np.session) + rnet_session_set_input_send_suppress(g_np.session, 0); + return; + } if (!g_np.load_applied_local && !savestate_take_load_completed()) return; @@ -1622,6 +1688,9 @@ int psx_netplay_is_host(void) int psx_netplay_request_save(int slot) { + uint32_t sim; + uint32_t delay; + uint32_t target; if (!psx_netplay_active() || !rnet_session_is_running(g_np.session)) return 0; if (g_np.local_slot != 0) @@ -1631,17 +1700,24 @@ int psx_netplay_request_save(int slot) if (slot < 0) slot = 0; if (slot >= SAVESTATE_SLOTS) slot = SAVESTATE_SLOTS - 1; - if (!savestate_request_save_protocol(slot)) - return 1; - /* Coord probe (size=0) does not stall admit — both peers must keep - * running until savestate_poll writes the .pst, then hash-probe stalls. - * STATE_* rides the same UDP/relay path as inputs (LAN hub / server - * input relay fan-out). */ - if (rnet_session_state_probe(g_np.session, RNET_STATE_OP_SAVE, (rnet_u8)slot, 0, 0) != 0) + /* Agree a future sim_tick so TURN/coord latency cannot make the host + * write tick T while the guest still writes T+k (CRC miss → transfer). + * crc field of size==0 probe carries the target tick. */ + sim = rnet_session_sim_tick(g_np.session); + delay = (uint32_t)psx_netplay_input_delay(); + if (delay < 1u) delay = 1u; + target = sim + delay + 2u; + if (rnet_session_state_probe(g_np.session, RNET_STATE_OP_SAVE, (rnet_u8)slot, 0, + target) != 0) return 1; g_np.xfer = NP_XFER_SAVE_COORD; g_np.xfer_slot = slot; - printf("psxrecomp: netplay save slot=%d — coordinating local writes…\n", slot); + g_np.save_target_tick = target; + g_np.local_save_staged = 0; + g_np.local_save_acked = 0; + printf("psxrecomp: netplay save slot=%d — coordinating local writes " + "(target sim=%u, now=%u)…\n", + slot, (unsigned)target, (unsigned)sim); fflush(stdout); return 1; } @@ -1649,6 +1725,7 @@ int psx_netplay_request_save(int slot) int psx_netplay_request_load(int slot) { uint32_t size = 0, crc = 0; + char reason[192]; if (!psx_netplay_active() || !rnet_session_is_running(g_np.session)) return 0; if (g_np.local_slot != 0) @@ -1657,6 +1734,13 @@ int psx_netplay_request_load(int slot) return 1; if (slot < 0) slot = 0; if (slot >= SAVESTATE_SLOTS) slot = SAVESTATE_SLOTS - 1; + if (!savestate_slot_compatible(slot, reason, sizeof(reason))) { + printf("psxrecomp: netplay load slot=%d refused — %s " + "(resave with this build: Shift+F%d)\n", + slot, reason[0] ? reason : "incompatible", slot + 1); + fflush(stdout); + return 1; + } if (!np_slot_crc(slot, &size, &crc)) return 1; if (rnet_session_state_probe(g_np.session, RNET_STATE_OP_LOAD, (rnet_u8)slot, size, crc) != 0) @@ -1664,6 +1748,7 @@ int psx_netplay_request_load(int slot) g_np.xfer = NP_XFER_LOAD_PROBE; g_np.xfer_slot = slot; g_np.load_applied_local = 0; + g_np.load_apply_failed = 0; printf("psxrecomp: netplay load slot=%d — hash probe (%u bytes)\n", slot, (unsigned)size); fflush(stdout); return 1; @@ -1673,12 +1758,16 @@ int psx_netplay_in_load_barrier(void) { if (!psx_netplay_active()) return 0; - /* Probe/SEND too: large ICE/TURN transfers can exceed the normal admit - * stall timeout, and FPS/present must stay frozen until mutual ready. */ - return (g_np.xfer == NP_XFER_LOAD_PROBE || g_np.xfer == NP_XFER_LOAD_SEND || - g_np.xfer == NP_XFER_LOAD_APPLYING || g_np.xfer == NP_XFER_LOAD_READY) - ? 1 - : 0; + /* Any save/load/memcard sync phase — TURN chunk xfers of ~1.4MB need the + * 90s budget (20s admit stall was killing SAVE mid-transfer). */ + return (g_np.xfer != NP_XFER_NONE) ? 1 : 0; +} + +int psx_netplay_consume_load_apply_failed(void) +{ + int v = g_np.load_apply_failed; + g_np.load_apply_failed = 0; + return v; } @@ -1936,6 +2025,26 @@ void psx_netplay_diag_tick(void) } } +/* Stage the coord save once sim_tick reaches the agreed target. */ +static void np_maybe_stage_target_save(void) +{ + uint32_t sim; + if (g_np.xfer != NP_XFER_SAVE_COORD || g_np.local_save_staged) + return; + if (!g_np.session || !rnet_session_is_running(g_np.session)) + return; + sim = rnet_session_sim_tick(g_np.session); + if (sim < g_np.save_target_tick) + return; + if (!savestate_request_save_protocol(g_np.xfer_slot)) + return; + g_np.local_save_staged = 1; + printf("psxrecomp: netplay %s save slot=%d — staging @ sim=%u (target=%u)\n", + g_np.local_slot == 0 ? "host" : "guest", g_np.xfer_slot, + (unsigned)sim, (unsigned)g_np.save_target_tick); + fflush(stdout); +} + static void np_pump_session(void) { #if defined(PSX_HAS_LOBBY_CLIENT) @@ -1945,6 +2054,7 @@ static void np_pump_session(void) drain_lobby_signals(); rnet_session_pump(g_np.session); np_guest_handle_probe(); + np_maybe_stage_target_save(); np_apply_ready_state(); np_drive_load_barrier(); np_host_drive_xfer(); @@ -2038,11 +2148,29 @@ int psx_netplay_poll_admit(void) exit_need = np_starv_env_int("PSX_NET_STARVATION_EXIT_FRAMES", PSX_STARVATION_EXIT_DEFAULT); - /* Probe/SEND: state_xfer stalls are expected — do not latch starvation. */ + /* SAVE coord: run admit until both reach save_target_tick and flush the + * staged write. After the local .pst exists, freeze (host also freezes + * unless the guest tip is behind and still needs catch-up admits). */ + if (g_np.xfer == NP_XFER_SAVE_COORD) { + g_starv.enter_run = 0; + g_starv.exit_run = 0; + g_starv.latched = 0; + g_starv.just_cleared = 0; + np_maybe_stage_target_save(); + if (!g_np.local_save_staged || savestate_pending()) + return np_try_admit_gameplay(); + if (g_np.local_slot != 0) + return 0; /* guest saved — wait for hash probe */ + /* Host saved: freeze for same-tick match. If guest is still behind + * the target, keep admitting so it can catch up and write. */ + if (psx_netplay_remote_lead() < 0) + return np_try_admit_gameplay(); + return 0; + } + if (g_np.xfer == NP_XFER_LOAD_PROBE || g_np.xfer == NP_XFER_LOAD_SEND || g_np.xfer == NP_XFER_SAVE_PROBE || g_np.xfer == NP_XFER_SAVE_SEND || - g_np.xfer == NP_XFER_SAVE_COORD || g_np.xfer == NP_XFER_MC_PROBE || - g_np.xfer == NP_XFER_MC_SEND) { + g_np.xfer == NP_XFER_MC_PROBE || g_np.xfer == NP_XFER_MC_SEND) { g_starv.enter_run = 0; g_starv.exit_run = 0; g_starv.latched = 0; @@ -2209,6 +2337,29 @@ void psx_netplay_admit_wait_info(char *stall_out, size_t stall_cap, /* LOAD_READY never calls try_admit, so last_stall stays "ok" — surface * the app barrier phase (+ transfer progress) instead. */ switch (g_np.xfer) { + case NP_XFER_SAVE_COORD: + snprintf(phase, sizeof(phase), "save_coord"); + break; + case NP_XFER_SAVE_PROBE: + snprintf(phase, sizeof(phase), "save_probe"); + break; + case NP_XFER_SAVE_SEND: + if (st.state_bytes_total > 0) + snprintf(phase, sizeof(phase), "save_xfer_%u/%u", + (unsigned)st.state_bytes_acked, (unsigned)st.state_bytes_total); + else + snprintf(phase, sizeof(phase), "save_xfer"); + break; + case NP_XFER_MC_PROBE: + snprintf(phase, sizeof(phase), "mc_probe"); + break; + case NP_XFER_MC_SEND: + if (st.state_bytes_total > 0) + snprintf(phase, sizeof(phase), "mc_xfer_%u/%u", + (unsigned)st.state_bytes_acked, (unsigned)st.state_bytes_total); + else + snprintf(phase, sizeof(phase), "mc_xfer"); + break; case NP_XFER_LOAD_PROBE: snprintf(phase, sizeof(phase), "load_probe"); break; diff --git a/runtime/src/savestate.c b/runtime/src/savestate.c index 667eddee..0f00f63a 100644 --- a/runtime/src/savestate.c +++ b/runtime/src/savestate.c @@ -45,6 +45,7 @@ static int s_configured = 0; static int s_save_pending = -1; /* slot, or -1 */ static int s_load_pending = -1; static int s_load_completed = 0; +static int s_load_failed = 0; static uint8_t *s_load_blob = NULL; /* optional in-memory .pst for netplay */ static size_t s_load_blob_len = 0; @@ -137,6 +138,28 @@ int savestate_slot_exists(int slot) { return sz > 0; } +int savestate_slot_compatible(int slot, char* reason, size_t reason_cap) { + uint8_t* data = NULL; + size_t size = 0; + int ok; + if (reason && reason_cap) + reason[0] = '\0'; + if (!s_configured) { + if (reason && reason_cap) + snprintf(reason, reason_cap, "not_configured"); + return 0; + } + if (!savestate_read_slot(slot, &data, &size) || !data) { + if (reason && reason_cap) + snprintf(reason, reason_cap, "missing"); + return 0; + } + ok = boot_state_check_buffer(data, size, s_bios_checksum, s_entry_pc, + reason, reason_cap); + free(data); + return ok; +} + int savestate_read_slot(int slot, uint8_t** data_out, size_t* size_out) { char path[600]; FILE* f; @@ -232,6 +255,8 @@ static int request_load_inner(int slot) { "PSX_HLE_SCHEDULER=0 run cannot load states.\n"); return 0; } + s_load_failed = 0; + s_load_completed = 0; s_load_pending = slot; return 1; } @@ -278,6 +303,8 @@ int savestate_request_load_blob_protocol(const void* data, size_t size) { clear_load_blob(); s_load_blob = copy; s_load_blob_len = size; + s_load_failed = 0; + s_load_completed = 0; s_load_pending = 0; /* non-negative: poll will prefer the blob */ return 1; } @@ -292,6 +319,12 @@ int savestate_take_load_completed(void) { return v; } +int savestate_take_load_failed(void) { + int v = s_load_failed; + s_load_failed = 0; + return v; +} + void savestate_poll(CPUState* cpu, uint32_t resume_pc) { if (s_save_pending < 0 && s_load_pending < 0) return; /* hot path: nothing staged */ @@ -328,16 +361,19 @@ void savestate_poll(CPUState* cpu, uint32_t resume_pc) { fprintf(stderr, "savestate: LOAD FAILED blob (%zu bytes, entry=%08X)\n", blob_len, (unsigned)s_entry_pc); + s_load_failed = 1; } } else if (savestate_slot_path(slot, path, sizeof(path))) { loaded = boot_state_load(path, s_bios_checksum, s_entry_pc, cpu); if (!loaded) { fprintf(stderr, - "savestate: LOAD FAILED slot %d (missing/mismatched) %s\n", + "savestate: LOAD FAILED slot %d %s\n", slot, path); + s_load_failed = 1; } } else { fprintf(stderr, "savestate: LOAD FAILED slot %d (no path)\n", slot); + s_load_failed = 1; } if (loaded) { t_after_boot = savestate_mono_ms();