no mods in MP, minor patches

This commit is contained in:
Alex Vanderveen
2026-07-29 02:40:50 -04:00
parent 070fc805c4
commit 92efb5c4fd
20 changed files with 1037 additions and 286 deletions
+7
View File
@@ -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,
+3
View File
@@ -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);
+6
View File
@@ -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. */
+3
View File
@@ -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). */
+5
View File
@@ -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
+3
View File
@@ -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();
+10 -2
View File
@@ -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
+6 -1
View File
@@ -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);
+9
View File
@@ -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);
+3
View File
@@ -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
+124 -31
View File
@@ -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<<BS_SEC_CPU)|(1u<<BS_SEC_RAM)|(1u<<BS_SEC_SPAD)|(1u<<BS_SEC_IRQ)|
(1u<<BS_SEC_TIMER)|(1u<<BS_SEC_CLOCK)|(1u<<BS_SEC_GPU)|(1u<<BS_SEC_VRAM)|
(1u<<BS_SEC_SPU)|(1u<<BS_SEC_SPURAM)|(1u<<BS_SEC_CDROM)|(1u<<BS_SEC_DMA)|
(1u<<BS_SEC_SIO)|(1u<<BS_SEC_DIRTY);
(1u<<BS_SEC_SIO)|(1u<<BS_SEC_MDEC)|(1u<<BS_SEC_DIRTY);
uint32_t seen = 0;
int ok = 1;
const double t0 = boot_state_mono_ms();
@@ -438,36 +547,14 @@ int boot_state_load_buffer(const uint8_t* file, size_t file_len,
double apply_spuram_ms = 0.0;
double apply_other_ms = 0.0;
if (!file || file_len < BOOT_STATE_HEADER_WIRE_BYTES ||
file_len > 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;
}
+9
View File
@@ -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
+146 -98
View File
@@ -69,12 +69,12 @@
#include "gpu_gl_renderer.h"
#include "latency_ring.h"
#include "psx_sdl.h"
#if defined(PSX_SDL3)
#include <SDL3/SDL_opengl.h>
#else
#include <SDL_opengl.h>
#endif
#include "psx_sdl.h"
#if defined(PSX_SDL3)
#include <SDL3/SDL_opengl.h>
#else
#include <SDL_opengl.h>
#endif
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
@@ -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;
+39
View File
@@ -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++)
+261 -112
View File
@@ -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 <SDL.h>
#include "psx_sdl.h"
#include "psx_sdl_audio.h"
#if defined(PSX_WEB)
#include <emscripten/emscripten.h>
#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<uint8_t> 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<uint32_t> 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
+138
View File
@@ -1,4 +1,5 @@
#include "mdec.h"
#include "pst_wire.h"
#include <stdint.h>
#include <stdlib.h>
@@ -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;
}
+36
View File
@@ -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;
+191 -40
View File
@@ -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;
+37 -1
View File
@@ -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();