rollback FMV patches and overlay

This commit is contained in:
Alex Vanderveen
2026-08-06 00:52:14 -04:00
parent c8f668cc9e
commit 652be7ba02
29 changed files with 1617 additions and 220 deletions
+4
View File
@@ -147,6 +147,10 @@ host present paths in the profile.
| `PSXRECOMP_PROJECT_ROOT` / game-specific | project root |
| `PSXRECOMP_BUILD_DIR` / game-specific | cmake build dir |
| `PSXRECOMP_FORCE_SETUP` / game-specific | force setup wizard |
Project-root discovery order: env → walk **cwd** → walk **exe dir**
(`$APPIMAGE` parent or `/proc/self/exe` / `GetModuleFileName`). GUI launches
that leave cwd as `$HOME` still find a setup zip next to the binary.
| `PSXRECOMP_GAME` | path to `psxrecomp-game` binary |
| `PSX_HOST_MUTE=1` | discard host SDL audio (SPU still runs; set by PGO train) |
| `PSX_HEADLESS=1` | no SDL window (set by PGO train when `hide_video`) |
+160 -28
View File
@@ -24,7 +24,7 @@ episode. Menu releases rewind (FMV soft-promote release-only only). Host
| Keep delay-sync `RNetSession` working behind a flag | Lobby / ICE / save-xfer stay useful; Disable Rollback opts out |
| Predicted rows promote only via `hash_confirm` (or host protect) | Library invariant; NULL `hash_confirm_promote` = always rewind |
| Digests must be bit-identical across peers for the same sealed inputs | Otherwise every invent becomes an episode storm |
| **Same bit-identical binary on both peers** | Mixed `build-release` vs packaged `motk-*` can match live digests for hundreds of ticks then fork GPRs/timers in Replay (pin zlib skew is the symptom) |
| **Same game version + guest semantics** | Prefer matching commits/deps. **Per-peer PGO is OK** (Settings → Optimize FMV Playback). Mixed trees/zlib still fork — size alone is not proof of match |
| Snapshots must restore at the exact sim tick requested | Ring load is the only rewind mechanism |
| Single-thread ownership of session + rings | Same as delay-sync |
@@ -914,30 +914,22 @@ Adaptive mid-match delay bumps are always on (no lobby disable).
comparison value never compared across differently-built peers, and
both peers already have to run the identical binary.
**Tier 2 — dirty-region digest (planned, not started).** Convert
"CRC the whole buffer" into "CRC only what changed since the last
digest." **VRAM is the tractable target**: audited every write to
the 1 MiB `vram[]` array in `gpu.c` and found only **3 call sites**
in the whole GPU emulation (CPU→VRAM transfer ~L2071, pixel
plot/fill/line/poly ~L2930, depth24/16bpp write ~L4744) — a small,
fully auditable surface where each site could mark a per-scanline
or per-tile "touched" bitmap, and `netplay_av_digest()` would CRC
only touched regions since the last checkpoint. This is also the
cheaper alternative flagged above as the unblocker for "one-sided
episodes" (exact per-tick VRAM state without a permanent per-frame
CRC tax). **RAM is not a good near-term target the same way**: guest
stores happen from essentially every recompiled `sw`/`sh`/`sb` site
across the whole program — thousands of call sites, not 3 — so
reliable RAM dirty-tracking means threading write-tracking through
the recompiler/interpreter's store path itself, a much larger
change. (Note: `dirty_ram_interp.h`'s existing per-page dirty
bitmap is an unrelated subsystem — self-modifying-code detection for
the static recompiler, CLAUDE.md Rule 18 — not a digest optimization
and not reusable for this without its own risk analysis.) **Risk to
respect:** a missed write site doesn't crash, it silently produces a
digest that doesn't reflect true state — masking a real divergence
or manufacturing a phantom one. Should ship with a "shadow-verify
against a full recompute" mode during bring-up, not on faith.
**Tier 2 — dirty-region digest (tracking landed for snaps §96;
digest fold still planned).** Convert "CRC the whole buffer" into
"CRC only what changed since the last digest." **VRAM tracking is
in:** `gpu_vram_dirty.*` marks per-scanline dirtiness from
`gpu.c` (raster_pixel / GP0 A0) and `gpu_sw_renderer.c` (put_*/
fill/copy/transfer). Raw ring snaps already consume this (§96).
Next: `netplay_av_digest()` CRC only touched rows since the last
checkpoint (unblocker for one-sided episodes). **RAM is not a good
near-term target the same way**: guest stores happen from essentially
every recompiled `sw`/`sh`/`sb` site — thousands of call sites, not a
handful — so reliable RAM dirty-tracking means threading write-tracking
through the recompiler/interpreter store path. (Note:
`dirty_ram_interp.h`'s per-page bitmap is SMC detection — CLAUDE.md
Rule 18 — not a digest optimization.) **Risk:** a missed write site
silently produces a wrong digest. Shadow-verify:
`PSX_NET_VRAM_DIRTY_VERIFY=1`.
**Tier 3 — Merkle/hash-tree digest (planned, longer-horizon idea).**
Replace the flat CRC with fixed-size blocks (e.g. 4 KiB) each hashed,
@@ -979,8 +971,8 @@ Adaptive mid-match delay bumps are always on (no lobby disable).
4. ~~Episode resim wiring~~
5. ~~Lobby flag + UI~~
6. ~~Rate-limit live snaps (`PSX_NET_SNAP_INTERVAL`) + safe episode resume~~
7. ~~Thinner snap / FMV policy~~ — interval **16**, settle invent ok, digest
fold (skip-VRAM on live snaps unsafe for rewind; optional later)
7. ~~Thinner snap / FMV policy~~ — interval **16**, settle invent ok; §96
dirty-VRAM mirror + FMV media snap interval 4 (skip-VRAM still unsafe)
8. **Dual-instance soak** — prove Done-when items
---
@@ -4438,7 +4430,8 @@ FMV savestate completeness is not the bug — cross-peer determinism / policy is
raises fork_cap so the doomed load is not reused.
3. **MAX unmatched RELEASE:** if lockstep hits MAX with `streak < CONFIRM`,
arm `g_fmv_unmatched_desync` + storm cooldown — invent/begin stay held
until cores rematch CONFIRM ticks (or session reset). Log
until cores rematch CONFIRM ticks, invent-hold expires (~600 ticks /
§94), netplay SAVE completes (§94), or session reset. Log
`rb DESYNC — FMV lockstep MAX unmatched`.
**Re-soak watch:**
@@ -4454,3 +4447,142 @@ FMV savestate completeness is not the bug — cross-peer determinism / policy is
**Still open (P1/P2):** mid-session host state-transfer on storm; CD/MDEC
cycle determinism for matched-baseline sim 184.
## 94. Netplay SAVE under FMV DESYNC invent-hold — tip-hole deadlock (2026-08-05)
**Soak (rb-diag1):** after FMV `DESYNC — MAX unmatched` at sim=1044 (streak=0,
never rematched), fight ran wire-only (~60 fps, lead≈-1). Host Shift+F2 SAVE
at sim=2598 completed (`hashes match, skip transfer`), then admit froze at
sim=2608 `stall=fmv_settle lead=-1` for 20s → `admit stall timeout` → lobby.
Guest tip stuck at 2607. Save itself was fine; resume needed gap1 invent and
could not get it.
**Cause:** `g_fmv_unmatched_desync` keeps `lockstep_no_invent` true forever when
soft-fork digests never hit CONFIRM. Scheduler mis-tagged that stall as
`fmv_settle` (media inactive). SAVE coord freezes both peers → tip hole →
neither invents → mutual wait → watchdog.
**Fixes (§94):**
1. **Stall tag:** `fmv_desync_hold` when DESYNC invent-hold is the gate
(`psx_netplay_rb_fmv_desync_hold`); keep `fmv_media` / `fmv_settle`.
2. **Invent-hold expire:** `RB_FMV_DESYNC_INVENT_EXPIRE` (600 ticks) after arm —
clear invent-hold if rematch never comes. Log
`FMV DESYNC invent-hold cleared (invent-hold expired…)`.
3. **Save complete clear:** host/guest after SAVE hash-match or transfer done
call `psx_netplay_rb_clear_fmv_desync_hold("netplay save complete")` so tip
invent can refill immediately.
**Re-soak watch:** SAVE mid-fight after post-FMV DESYNC does not stall-timeout;
expect invent-hold cleared (expire or save) and gap1 invent resumes. Stall
lines should say `fmv_desync_hold` not `fmv_settle` while the hold is live.
## 95. Netplay LOAD — host hung `wait_confirm`, guest-only apply (2026-08-05)
**Soak (rb-diag1/2 session 19):** SAVE transfer OK. Host Shift+F1 LOAD hash-match
→ guest `LOADED` + `waiting for host`; host stuck
`load_applying+wait_confirm` ~14s (never `LOADED`) → peer disconnect / desync
(guest on .pst, host still live timeline).
**Cause:** `LOAD_APPLYING` used delay-sync `rnet_session_try_admit` (INPUT_CONFIRM).
Rollback live does not confirm. Faster peer applies → `LOAD_READY` freezes admit
→ slower peer never gets confirm for the next tick → `savestate_poll` never runs.
**Fixes (§95):**
1. **Rollback load-barrier admit:** while `LOAD_APPLYING` + `savestate_pending`
(and when exiting `LOAD_READY` after mutual sync), use tip + hold-last invent
with **no** confirm (`np_try_admit_load_barrier_rb`). Always invent missing
remotes — peer may already be frozen in LOAD_READY.
2. **Clear DESYNC invent-hold** at `np_commit_load_sync` (hard resync).
**Re-soak watch:** both peers log `LOADED` / `applied, waiting…` then
`peer ready, resuming lockstep`. No host-only `load_applying+wait_confirm`
stall after hash-match apply.
## 96. FMV snap cost — dirty VRAM mirror + media interval (2026-08-05)
**Goal:** keep invent/episode policy for FMV media (§50/§93) but make the
rollback snap machinery cheaper while the movie runs.
**What landed:**
1. **Per-scanline VRAM dirty tracking** (`gpu_vram_dirty.*`) on every CPU-VRAM
write path (gpu.c A0/raster + sw fill/copy/put/transfer). **Gated:**
`gpu_vram_dirty_set_tracking(1)` only in `psx_netplay_rb_start`; off in
`shutdown`. Offline / delay-sync: `g_psx_vram_dirty_tracking==0` so
`mark_row` is an inlined no-op (zero gameplay impact).
2. **Incremental raw snap VRAM** (`boot_state_save_buffer_raw` + tracking on):
persistent 1 MiB mirror; only dirty scanlines are copied from live VRAM
before emitting a still-complete `BS_SEC_VRAM` (loads stay independent /
no delta chain). Offline raw/zlib saves keep full `gr_vram_transfer_out`.
3. **FMV media snap interval** (default **4**, `PSX_NET_FMV_SNAP_INTERVAL`):
during media, live snaps every N ticks. Settle + post-FMV lockstep remain
every-tick dense (cutover near-tip load). Episodes into media still refused.
4. **Telemetry:** `rb snap tele` every 64 saves — `last/avg ms`, `dirty_rows`,
`incr%%`. Bring-up verify: `PSX_NET_VRAM_DIRTY_VERIFY=1` memcmp vs full
transfer_out.
**Not changed (still accept):** no invent through media; SW@1× CPU-auth VRAM;
soft-fork / DESYNC invent-hold after unmatched FMV.
**Re-soak watch:** during FMV media, `rb snap tele` shows `incr=1` with
`dirty_rows` ≪ 512 (typical depth24 band), lower `avg ms` than pre-change,
and media snap cadence ~interval 4. Settle/lockstep still dense. Optional
verify run once with `PSX_NET_VRAM_DIRTY_VERIFY=1` (no VERIFY FAIL lines).
## 97. FMV media keyframe episodes — invent into media (2026-08-05)
**Goal:** abandon blanket “no invent / no episode into FMV media” (§93) by
healing stream soft-forks the same way SAVE/LOAD heals state: **host seals a
raw snap keyframe at `load_tick` before Replay**.
**Default ON** (`PSX_NET_FMV_MEDIA_KF=0` restores §93 refuse + invent-off).
**What landed:**
1. **Invent during live media** when MEDIA_KF on (settle + DESYNC hold still
block invent). Media snap interval defaults to **1** (every tick) so
`load_tick` hits a ring snap.
2. **Begin/follow into media-range** allowed when a snap exists (initiator) or
peer flags `RNET_RB_SYNC_FLAG_MEDIA_KF` (follower may lack the snap).
3. **Host keyframe transfer** (`RNET_STATE_OP_RB_KF`): probe CRC of raw snap →
skip xfer on match; else chunked transfer. Both peers install into pin+ring
(`rb MEDIA-KF …`). No LIGHT_TIP into media KF episodes.
4. **Apply waits** for `g_media_kf_ready`; prefers pin; **skips**
`episode load into FMV media` abort for KF episodes.
5. Restore epilogue unchanged (`cdrom_resync_*`, depth24 cutover, XA FIFO).
**Re-soak watch:**
- Mid-FMV: invent can fire; logs `rb begin … media_kf=1` /
`MEDIA-KF probe` / `hash match` or `transfer start` / `installed`.
- No resim-diverge storm on first media episode; baseline digests match after KF.
- `PSX_NET_FMV_MEDIA_KF=0` restores prior refuse / invent-off behavior.
## 98. FMV admit/FPS — freeze D + timesync during media (2026-08-05)
**Diag (post-§97):** snaps were already cheap (`rb snap tele avg≈0.7 ms`,
`incr=1`, `dirty_rows≈128`) but FMV sat ~43–55 fps. Ceiling was
`guest≈13–16 ms/f` + `admit≈4–8 ms/f` (timesync debt + D ratcheted to 6–7
under invent-through-media). `present_gap_p95≈80–100 ms` was mostly the
hardcoded present-1/4 skip (~67 ms+ between Swaps), not snap cost.
**What landed:**
1. **No timesync debt during FMV media** — `np_timesync_note_late` skips;
`np_timesync_throttle` clears debt and never stalls admit while media.
2. **Freeze adaptive D during media** — both `np_adapt_delay_on_pcap_enter`
and `np_auto_delay_tick` no-op while `psx_netplay_rb_fmv_media_active()`.
3. **No LAN gap1 micro-grace during media** — invent immediately when the
MEDIA_KF path allows invent (avoid admit wait for a peer that is also
inventing through the movie).
4. **Media snap interval default 2** with MEDIA_KF (was 1). Near-tip loads
still work; halves snap tax vs every-tick. Override
`PSX_NET_FMV_SNAP_INTERVAL`.
5. **Present every 2nd depth24 vblank** during hot MDEC (was every 4th).
Override `PSX_NET_FMV_PRESENT_DIV` (`1`=every frame, `4`=legacy).
**Re-soak watch:** during FMV media, `admit=` near ~0–2 ms/f (not 5–11),
`D` stable (no mid-movie 6→7), `present_gap_p95` roughly half of §97,
`rb snap tele` cadence ~interval 2, sim FPS closer to guest ceiling.
+182 -42
View File
@@ -702,26 +702,12 @@ static int find_cmake(char* out, size_t cap) {
#endif
}
static int discover_project_root(char* out, size_t cap) {
const char* env_name =
cfg_or(g_cfg->project_root_env, "PSXRECOMP_PROJECT_ROOT");
const char* env = getenv(env_name);
if (env && env[0] && looks_like_project_root(env)) {
snprintf(out, cap, "%s", env);
return 1;
}
char start[1024];
#if defined(_WIN32)
if (!GetCurrentDirectoryA((DWORD)sizeof(start), start))
start[0] = '\0';
#else
if (!getcwd(start, sizeof(start)))
start[0] = '\0';
#endif
/* Walk start and up to 10 parents for game.toml + psxrecomp_cli.py. */
static int walk_up_for_project_root(const char* start, char* out, size_t cap) {
char cur[1024];
snprintf(cur, sizeof(cur), "%s", start[0] ? start : ".");
if (!start || !start[0])
return 0;
snprintf(cur, sizeof(cur), "%s", start);
for (int i = 0; i < 10; ++i) {
if (looks_like_project_root(cur)) {
snprintf(out, cap, "%s", cur);
@@ -737,6 +723,71 @@ static int discover_project_root(char* out, size_t cap) {
return 0;
}
/* Directory containing this process's executable (not cwd). Dolphin / .desktop
* launches leave cwd as $HOME — VIDEO/PGO/setup must still find the zip tree.
* Linux: $APPIMAGE parent, else /proc/self/exe. Windows: GetModuleFileName. */
static int resolve_host_exe_dir(char* out, size_t cap) {
char exe[1100];
if (!out || cap < 2)
return 0;
out[0] = '\0';
exe[0] = '\0';
#if defined(_WIN32)
{
DWORD n = GetModuleFileNameA(NULL, exe, (DWORD)sizeof(exe));
if (n == 0 || n >= (DWORD)sizeof(exe))
return 0;
}
#else
{
const char* appimg = getenv("APPIMAGE");
if (appimg && appimg[0] && path_is_file(appimg)) {
snprintf(exe, sizeof(exe), "%s", appimg);
} else {
char* rp = realpath("/proc/self/exe", NULL);
if (!rp)
return 0;
snprintf(exe, sizeof(exe), "%s", rp);
free(rp);
}
}
#endif
if (!exe[0])
return 0;
return dirname_copy(out, cap, exe);
}
static int discover_project_root(char* out, size_t cap) {
const char* env_name =
cfg_or(g_cfg->project_root_env, "PSXRECOMP_PROJECT_ROOT");
const char* env = getenv(env_name);
if (env && env[0] && looks_like_project_root(env)) {
snprintf(out, cap, "%s", env);
return 1;
}
/* 1) cwd (terminal launches from the unzip / repo root). */
char start[1024];
#if defined(_WIN32)
if (!GetCurrentDirectoryA((DWORD)sizeof(start), start))
start[0] = '\0';
#else
if (!getcwd(start, sizeof(start)))
start[0] = '\0';
#endif
if (start[0] && walk_up_for_project_root(start, out, cap))
return 1;
/* 2) exe dir (GUI double-click: cwd is often $HOME / Desktop). */
char exe_dir[1024];
if (resolve_host_exe_dir(exe_dir, sizeof(exe_dir)) &&
walk_up_for_project_root(exe_dir, out, cap))
return 1;
return 0;
}
static int resolve_build_paths(void) {
const char* env_name =
cfg_or(g_cfg->build_dir_env, "PSXRECOMP_BUILD_DIR");
@@ -2112,7 +2163,12 @@ static void bat_write_set(FILE* f, const char* name, const char* value) {
fprintf(f, "\"\r\n");
}
static int write_windows_deferred_rebuild_helper(char* err_msg, size_t err_cap) {
/* force_pgo: Settings Optimize FMV (instrument → train → use). Else setup
* rebuild with --no-pgo. disc_path required when force_pgo. */
static int write_windows_deferred_rebuild_helper(int force_pgo,
const char* disc_path,
char* err_msg,
size_t err_cap) {
if (!join_path(g_helper_path, sizeof(g_helper_path), g_build_dir,
"recomp_deferred_rebuild.cmd")) {
snprintf(err_msg, err_cap, "Failed to form helper path.");
@@ -2135,7 +2191,8 @@ static int write_windows_deferred_rebuild_helper(char* err_msg, size_t err_cap)
(unsigned long)GetCurrentProcessId());
fprintf(f, "@echo off\r\n");
fprintf(f, "setlocal EnableExtensions\r\n");
fprintf(f, "title %s - rebuilding\r\n", g_display);
fprintf(f, "title %s - %s\r\n", g_display,
force_pgo ? "PGO optimize" : "rebuilding");
bat_write_set(f, "PARENT_PID", pid_buf);
bat_write_set(f, "PYTHON", g_python);
bat_write_set(f, "CLI", g_cli_path);
@@ -2146,6 +2203,8 @@ static int write_windows_deferred_rebuild_helper(char* err_msg, size_t err_cap)
bat_write_set(f, "EXE_BASE", g_exe_basename);
bat_write_set(f, "EXE", g_exe_path);
bat_write_set(f, "DISPLAY", g_display);
if (force_pgo && disc_path && disc_path[0])
bat_write_set(f, "DISC", disc_path);
if (g_toolchain_bin[0])
bat_write_set(f, "TC_BIN", g_toolchain_bin);
fprintf(f,
@@ -2167,12 +2226,23 @@ static int write_windows_deferred_rebuild_helper(char* err_msg, size_t err_cap)
" echo RETCOMM_TOOLCHAIN_DIR / pass --toolchain-zip on rebuild.\r\n"
" pause\r\n"
" exit /b 1\r\n"
")\r\n"
"echo Building...\r\n"
"\"%%PYTHON%%\" \"%%CLI%%\" rebuild --project-root \"%%ROOT%%\" "
"--config \"%%CONFIG%%\" --build-dir \"%%BUILD_DIR%%\" "
"--target \"%%TARGET%%\" --exe-basename \"%%EXE_BASE%%\" "
"--no-pgo --prune-after build-intermediates\r\n"
")\r\n");
if (force_pgo) {
fprintf(f,
"echo PGO optimize (instrument + train + rebuild)...\r\n"
"\"%%PYTHON%%\" \"%%CLI%%\" rebuild --project-root \"%%ROOT%%\" "
"--config \"%%CONFIG%%\" --build-dir \"%%BUILD_DIR%%\" "
"--target \"%%TARGET%%\" --exe-basename \"%%EXE_BASE%%\" "
"--disc \"%%DISC%%\" --force-pgo --pgo-video\r\n");
} else {
fprintf(f,
"echo Building...\r\n"
"\"%%PYTHON%%\" \"%%CLI%%\" rebuild --project-root \"%%ROOT%%\" "
"--config \"%%CONFIG%%\" --build-dir \"%%BUILD_DIR%%\" "
"--target \"%%TARGET%%\" --exe-basename \"%%EXE_BASE%%\" "
"--no-pgo --prune-after build-intermediates\r\n");
}
fprintf(f,
"if errorlevel 1 (\r\n"
" echo.\r\n"
" echo Build failed. Fix the errors above, then rebuild manually.\r\n"
@@ -2187,41 +2257,60 @@ static int write_windows_deferred_rebuild_helper(char* err_msg, size_t err_cap)
}
#endif
static int host_rebuild_game(const char* disc_path, char* out_exe_path,
size_t out_cap, char* err_msg, size_t err_cap,
RecompLauncherCPrepareProgressFn on_progress,
void* progress_ctx) {
static int host_rebuild_game_ex(const char* disc_path, int force_pgo,
char* out_exe_path, size_t out_cap,
char* err_msg, size_t err_cap,
RecompLauncherCPrepareProgressFn on_progress,
void* progress_ctx) {
g_relaunch_is_helper = 0;
if (!g_ready || !g_build_dir[0]) {
snprintf(err_msg, err_cap, "CMake build environment is not available.");
return 0;
}
if (force_pgo) {
if (psxrecomp_codegen_host_sources_missing(g_cfg)) {
snprintf(err_msg, err_cap,
"Generated game C is missing — finish Generate & rebuild "
"in the setup wizard first.");
return 0;
}
if (!disc_path || !disc_path[0] || !path_is_file(disc_path)) {
snprintf(err_msg, err_cap,
"Select a playable disc image before optimizing FMV.");
return 0;
}
}
if (!host_ensure_toolchain(on_progress, progress_ctx, err_msg, err_cap))
return 0;
#if defined(_WIN32)
(void)disc_path;
activate_toolchain_path();
if (on_progress)
on_progress(progress_ctx, 0.4f,
"Scheduling Windows rebuild after exit…");
if (!write_windows_deferred_rebuild_helper(err_msg, err_cap))
force_pgo ? "Scheduling Windows PGO optimize after exit…"
: "Scheduling Windows rebuild after exit…");
if (!write_windows_deferred_rebuild_helper(force_pgo, disc_path, err_msg,
err_cap))
return 0;
g_relaunch_is_helper = 1;
snprintf(out_exe_path, out_cap, "%s", g_helper_path);
if (on_progress)
on_progress(progress_ctx, 1.0f,
"Exiting so Windows can rebuild safely…");
force_pgo
? "Exiting so Windows can run PGO train + rebuild…"
: "Exiting so Windows can rebuild safely…");
return 1;
#else
if (on_progress)
on_progress(progress_ctx, 0.05f, "Starting rebuild (cmake)…");
on_progress(progress_ctx, 0.05f,
force_pgo ? "Starting FMV PGO optimize…"
: "Starting rebuild (cmake)…");
activate_toolchain_path();
char disc_arg_storage[1100];
char* argv[36];
char* argv[40];
int argc = 0;
argv[argc++] = g_python;
argv[argc++] = g_cli_path;
@@ -2241,14 +2330,20 @@ static int host_rebuild_game(const char* disc_path, char* out_exe_path,
argv[argc++] = "--disc";
argv[argc++] = disc_arg_storage;
}
argv[argc++] = "--no-pgo";
argv[argc++] = "--prune-after";
argv[argc++] = "build-intermediates";
if (force_pgo) {
argv[argc++] = "--force-pgo";
argv[argc++] = "--pgo-video";
} else {
argv[argc++] = "--no-pgo";
argv[argc++] = "--prune-after";
argv[argc++] = "build-intermediates";
}
argv[argc++] = "--json-progress";
argv[argc] = NULL;
if (!run_cli_posix(argv, on_progress, progress_ctx, err_msg, err_cap,
"psxrecomp rebuild"))
force_pgo ? "psxrecomp PGO optimize"
: "psxrecomp rebuild"))
return 0;
if (!path_is_file(g_exe_path)) {
snprintf(err_msg, err_cap, "Build succeeded but binary missing: %s",
@@ -2257,11 +2352,43 @@ static int host_rebuild_game(const char* disc_path, char* out_exe_path,
}
snprintf(out_exe_path, out_cap, "%s", g_exe_path);
if (on_progress)
on_progress(progress_ctx, 1.0f, "Build complete");
on_progress(progress_ctx, 1.0f,
force_pgo ? "FMV optimize complete" : "Build complete");
return 1;
#endif
}
static int host_rebuild_game(const char* disc_path, char* out_exe_path,
size_t out_cap, char* err_msg, size_t err_cap,
RecompLauncherCPrepareProgressFn on_progress,
void* progress_ctx) {
return host_rebuild_game_ex(disc_path, 0, out_exe_path, out_cap, err_msg,
err_cap, on_progress, progress_ctx);
}
static int host_pgo_optimize(const char* disc_path, char* out_exe_path,
size_t out_cap, char* err_msg, size_t err_cap,
RecompLauncherCPrepareProgressFn on_progress,
void* progress_ctx) {
return host_rebuild_game_ex(disc_path, 1, out_exe_path, out_cap, err_msg,
err_cap, on_progress, progress_ctx);
}
/* Settings → VIDEO → Apply FMV Timing Opt: regenerate C (picks up
* load_charge_batch) then rebuild without PGO training. */
static int host_fmv_timing_optimize(const char* disc_path, char* out_exe_path,
size_t out_cap, char* err_msg,
size_t err_cap,
RecompLauncherCPrepareProgressFn on_progress,
void* progress_ctx) {
char gen_out[512];
if (!host_prepare_generate(disc_path, gen_out, sizeof(gen_out), err_msg,
err_cap, on_progress, progress_ctx))
return 0;
return host_rebuild_game_ex(disc_path, 0, out_exe_path, out_cap, err_msg,
err_cap, on_progress, progress_ctx);
}
void psxrecomp_codegen_host_relaunch_or_exit(const char* disc_path) {
char exe[512];
const char* near_exe;
@@ -2415,6 +2542,19 @@ void psxrecomp_codegen_host_apply(RecompLauncherCGameInfo* gi,
gi->setup_needs_toolchain = 1;
gi->toolchain_is_ready = host_toolchain_is_ready;
gi->ensure_toolchain_with_progress = host_ensure_toolchain_with_progress;
/* Settings → SYSTEM: PGO on existing generated C (no wizard). */
gi->pgo_optimize_with_progress = host_pgo_optimize;
#if defined(_WIN32)
gi->pgo_busy_status = "Scheduling FMV PGO optimize…";
gi->pgo_success_status =
"Exiting for Windows PGO train + rebuild…";
#else
gi->pgo_busy_status =
"Optimizing FMV (instrument → train → rebuild)…";
gi->pgo_success_status = "FMV optimize complete — restarting…";
#endif
/* FMV timing (emitter load_charge_batch) generate+rebuild: host kept
* but not exposed in Settings for now. */
} else {
gi->prepare_disc_label = "Generate sources…";
gi->prepare_disc_note =
+5
View File
@@ -67,6 +67,11 @@ struct CodeGenConfig {
// addresses (MotK VLC leaves, etc.). Host locality hint only.
std::set<uint32_t> hot_funcs;
// [recompiler] load_charge_batch(_funcs): install a function-local
// cycle accumulator so load charges skip deadline probes until IRQ/MMIO
// / function exit. Guest totals at those barriers are unchanged.
std::set<uint32_t> load_charge_batch_funcs;
// [load_accel.vsync_query] verified PsyQ VSync functions whose mode=-1
// path may bypass its unused GPUSTAT/Timer1 reads. The map value is the
// guest RAM VBlank counter returned by that query path. Empty = inert.
+11 -2
View File
@@ -2415,8 +2415,17 @@ GeneratedFunction CodeGenerator::generate_function(
<< config_.indent
<< "__attribute__((cleanup(psx_cyc_bb_defer_cleanup))) "
"int _psx_cyc_bb_guard = 1;\n"
<< config_.indent << "psx_cyc_bb_defer_begin();\n"
<< "#endif\n";
<< config_.indent << "psx_cyc_bb_defer_begin();\n";
// Declare local-acc AFTER bb_guard so cleanup runs local_end first
// (publish), then bb_defer_end (flush). Gated VLC leaves only.
if (config_.load_charge_batch_funcs.count(func.start_addr)) {
body_ss << config_.indent << "uint32_t _psx_lc_acc = 0;\n"
<< config_.indent
<< "__attribute__((cleanup(psx_cyc_local_cleanup))) "
"uint32_t *_psx_lc_guard = &_psx_lc_acc;\n"
<< config_.indent << "psx_cyc_local_begin(&_psx_lc_acc);\n";
}
body_ss << "#endif\n";
// CPS entry-switch: when the unified flat trampoline dispatches a
// continuation address (a callee published cpu->pc = $ra back to us), route
+25
View File
@@ -169,6 +169,15 @@ uint32_t overlay_codegen_config_hash(const GameConfig& c) {
h.u32(patch.expected);
h.u32(patch.replacement);
}
h.tag("load_charge_batch");
h.u32(c.load_charge_batch ? 1u : 0u);
{
std::vector<uint32_t> lcb = c.load_charge_batch_funcs;
std::sort(lcb.begin(), lcb.end());
h.u32((uint32_t)lcb.size());
for (uint32_t pc : lcb) h.u32(pc);
}
return h.value;
}
@@ -1266,6 +1275,20 @@ GameConfig load_game_config(const fs::path& config_path_in) {
for (const auto& a : arr)
hot_funcs.push_back(parse_hex(a, "recompiler.hot_funcs"));
}
// Optional emitter-level load-charge batching for VLC leaves.
bool load_charge_batch = false;
std::vector<uint32_t> load_charge_batch_funcs;
if (recomp.contains("load_charge_batch"))
load_charge_batch = toml::find<bool>(recomp, "load_charge_batch");
if (recomp.contains("load_charge_batch_funcs")) {
const auto& arr =
toml::find<std::vector<std::string>>(recomp, "load_charge_batch_funcs");
for (const auto& a : arr)
load_charge_batch_funcs.push_back(
parse_hex(a, "recompiler.load_charge_batch_funcs"));
}
if (load_charge_batch && load_charge_batch_funcs.empty())
load_charge_batch_funcs = hot_funcs;
uint32_t vsync_query_func = 0;
uint32_t vsync_counter_addr = 0;
uint32_t vsync_gpustat_ptr_addr = 0;
@@ -1858,6 +1881,8 @@ GameConfig load_game_config(const fs::path& config_path_in) {
/*ws_auto_ui_squash*/ ws_auto_ui_squash,
/*data_shard_funcs*/ data_shard_funcs,
/*hot_funcs*/ hot_funcs,
/*load_charge_batch*/ load_charge_batch,
/*load_charge_batch_funcs*/ load_charge_batch_funcs,
/*vsync_query_func*/ vsync_query_func,
/*vsync_counter_addr*/ vsync_counter_addr,
/*vsync_gpustat_ptr_addr*/ vsync_gpustat_ptr_addr,
+6
View File
@@ -654,6 +654,12 @@ struct GameConfig {
// on their generated C bodies (profile/host locality; no guest semantics).
std::vector<uint32_t> hot_funcs;
// [recompiler] load_charge_batch: when true, emit function-local cycle
// accumulators for load_charge_batch_funcs (or hot_funcs if that list is
// empty). Requires regen; guest totals at IRQ/MMIO barriers unchanged.
bool load_charge_batch = false;
std::vector<uint32_t> load_charge_batch_funcs;
// [load_accel.vsync_query] opt-in for a byte-verified PsyQ VSync(mode)
// implementation. mode=-1 returns vsync_counter_addr while bypassing two
// unused MMIO reads; every other mode executes the original function.
+6
View File
@@ -163,6 +163,7 @@ int main(int argc, char** argv) {
std::set<uint32_t> ws_tag_funcs; // [widescreen] sprite_tag_funcs
std::set<uint32_t> ds_funcs; // [data_shards] funcs
std::set<uint32_t> hot_funcs; // [recompiler] hot_funcs
std::set<uint32_t> load_charge_batch_funcs; // [recompiler] load_charge_batch*
std::map<uint32_t, std::array<uint32_t, 4>> vsync_query_hle_funcs;
std::set<uint32_t> ws_cull_bias, ws_cull_range, ws_cull_a1; // [widescreen.cull]
std::set<uint32_t> ws_cull_screen_x; // [widescreen.cull] screen_x_sites
@@ -211,6 +212,10 @@ int main(int argc, char** argv) {
cfg.ws_sprite_tag_funcs.end());
ds_funcs.insert(cfg.data_shard_funcs.begin(), cfg.data_shard_funcs.end());
hot_funcs.insert(cfg.hot_funcs.begin(), cfg.hot_funcs.end());
if (cfg.load_charge_batch) {
load_charge_batch_funcs.insert(cfg.load_charge_batch_funcs.begin(),
cfg.load_charge_batch_funcs.end());
}
if (cfg.vsync_query_func)
vsync_query_hle_funcs[cfg.vsync_query_func] = {
cfg.vsync_counter_addr, cfg.vsync_gpustat_ptr_addr,
@@ -1131,6 +1136,7 @@ int main(int argc, char** argv) {
codegen_config.ws_sprite_tag_funcs = ws_tag_funcs;
codegen_config.data_shard_funcs = ds_funcs;
codegen_config.hot_funcs = hot_funcs;
codegen_config.load_charge_batch_funcs = load_charge_batch_funcs;
codegen_config.vsync_query_hle_funcs = vsync_query_hle_funcs;
codegen_config.ws_bg2d_init_func = ws_bg2d_init_func;
codegen_config.ws_cull_bias_sites = ws_cull_bias;
+8
View File
@@ -118,6 +118,14 @@ int boot_state_save_buffer_raw(const CPUState* cpu, uint32_t bios_checksum,
uint32_t entry_pc, uint8_t** out_data,
size_t* out_len);
/* §96 telemetry: after the latest save, how many VRAM scanlines were dirty
* and whether the incremental mirror path patched (vs full memcpy). */
uint32_t boot_state_last_vram_dirty_rows(void);
int boot_state_last_vram_incremental(void);
/* Drop the §96 VRAM mirror (RB shutdown / before re-enable). */
void boot_state_vram_mirror_reset(void);
/* Load + validate (integrity key) + restore the full machine. On any mismatch
* or incompleteness returns 0 (caller then boots normally and recaptures). */
int boot_state_load(const char* path, uint32_t bios_checksum,
+40
View File
@@ -50,6 +50,8 @@ enum { PSX_CYC_BATCH_SOFT = 64u };
static inline void psx_cyc_bb_defer_begin(void) { }
static inline void psx_cyc_bb_defer_end(void) { }
static inline void psx_cyc_bb_defer_flush(void) { overlay_flush_cycles(); }
static inline void psx_cyc_local_begin(uint32_t *acc) { (void)acc; }
static inline void psx_cyc_local_end(void) { }
#else
static inline void psx_cyc_bb_defer_begin(void) { g_psx_cyc_bb_defer++; }
static inline void psx_cyc_bb_defer_end(void) {
@@ -57,6 +59,23 @@ static inline void psx_cyc_bb_defer_end(void) {
if (g_psx_cyc_bb_defer == 0) psx_cyc_batch_flush();
}
static inline void psx_cyc_bb_defer_flush(void) { psx_cyc_batch_flush(); }
/* Install a function-local charge sink (MotK VLC leaves). Soft-cap publish
* at 1<<28 so uint32 cannot wrap; IRQ/MMIO still go through batch_flush. */
static inline void psx_cyc_local_begin(uint32_t *acc) {
#if !defined(PSX_COSIM)
if (acc) *acc = 0;
g_psx_cyc_local_acc = acc;
#else
(void)acc;
#endif
}
static inline void psx_cyc_local_end(void) {
#if !defined(PSX_COSIM)
psx_cyc_local_publish();
g_psx_cyc_local_acc = NULL;
#endif
}
#endif
#if defined(__GNUC__) || defined(__clang__)
@@ -64,6 +83,10 @@ static inline void psx_cyc_bb_defer_cleanup(int *guard) {
(void)guard;
psx_cyc_bb_defer_end();
}
static inline void psx_cyc_local_cleanup(uint32_t **guard) {
(void)guard;
psx_cyc_local_end();
}
#endif
static inline void psx_cyc_charge(uint32_t cycles) {
@@ -84,6 +107,23 @@ static inline void psx_cyc_charge(uint32_t cycles) {
return;
}
#if !defined(PSX_COSIM)
/* Emitter-gated VLC path: stack local, no deadline probe. */
if (g_psx_cyc_local_acc) {
uint32_t prior = *g_psx_cyc_local_acc;
if (cycles <= UINT32_MAX - prior && (prior + cycles) < (1u << 28)) {
*g_psx_cyc_local_acc = prior + cycles;
return;
}
*g_psx_cyc_local_acc = 0;
{
uint32_t *saved = g_psx_cyc_local_acc;
g_psx_cyc_local_acc = NULL;
if (prior) psx_advance_cycles(prior);
g_psx_cyc_local_acc = saved;
}
*g_psx_cyc_local_acc = cycles;
return;
}
{
uint32_t prior = g_psx_cyc_batch;
uint32_t sum = prior + cycles;
+23
View File
@@ -1,6 +1,7 @@
#ifndef PSXRECOMP_PSX_CYCLES_H
#define PSXRECOMP_PSX_CYCLES_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
@@ -60,6 +61,11 @@ extern uint32_t g_psx_cyc_batch_limit;
* block. Interrupt/MMIO edges still publish the accumulated guest cycles. */
extern int g_psx_cyc_bb_defer;
/* Emitter-level VLC load-charge batching: when non-NULL, psx_cyc_charge
* accumulates into *g_psx_cyc_local_acc instead of g_psx_cyc_batch. Publish
* via psx_cyc_local_publish / psx_cyc_batch_flush before IRQ/MMIO barriers. */
extern uint32_t *g_psx_cyc_local_acc;
/* Advance guest time. Overlay DLLs forward this through their callback shim;
* normal runtime/generated code keeps the common production path inlined. */
#if defined(PSX_OVERLAY_DLL_BUILD)
@@ -121,10 +127,27 @@ static inline void psx_advance_cycles(uint32_t cycles) {
* their pending total in the callback shim rather than these host globals. */
#if defined(PSX_OVERLAY_DLL_BUILD)
void overlay_flush_cycles(void);
static inline void psx_cyc_local_publish(void) { }
static inline void psx_cyc_batch_flush(void) { overlay_flush_cycles(); }
#else
/* Publish function-local charges into the normal batch/advance path while
* keeping the local pointer installed (nested charges resume locally). */
static inline void psx_cyc_local_publish(void) {
#if !defined(PSX_COSIM)
uint32_t *acc = g_psx_cyc_local_acc;
if (!acc) return;
uint32_t v = *acc;
if (!v) return;
*acc = 0;
g_psx_cyc_local_acc = NULL;
psx_advance_cycles(v);
g_psx_cyc_local_acc = acc;
#endif
}
static inline void psx_cyc_batch_flush(void) {
#if !defined(PSX_COSIM)
psx_cyc_local_publish();
uint32_t b = g_psx_cyc_batch;
if (!b) return;
g_psx_cyc_batch = 0;
+17 -2
View File
@@ -6,6 +6,7 @@
* Delay-sync path never calls these.
*/
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
@@ -80,13 +81,27 @@ int psx_netplay_rb_fmv_media_active(void);
/* 1 during FMV media + short settle (§26) — admit waits for remote wire.
* Post-FMV digest lockstep no longer blocks invent. Ticks FMV→settle.
* Also 1 while §93 MAX-unmatched DESYNC hold is armed. */
* Also 1 while §93 MAX-unmatched DESYNC invent-hold is armed (expires). */
int psx_netplay_rb_lockstep_no_invent(void);
/* §93/§94: 1 while MAX-unmatched DESYNC invent-hold is armed (not media/settle).
* Stall tag should say fmv_desync_hold, not fmv_settle. */
int psx_netplay_rb_fmv_desync_hold(void);
/* §94: drop invent-hold after a successful netplay SAVE (tip hole refill) or
* when the hold expires. Episode media-range refuse is unchanged. */
void psx_netplay_rb_clear_fmv_desync_hold(const char *why);
/* §93: 1 if tick sits in the last FMV media bout / settle tail (or DESYNC
* hold). Begin/follow/hc-fork must not open episodes that load there. */
* hold). Begin/follow/hc-fork must not open episodes that load there.
* §97 MEDIA_KF (default on): returns 0 for media-range — episodes allowed. */
int psx_netplay_rb_fmv_episode_unsafe(uint32_t tick);
/* §97: guest probe reply — 1 if local snap at episode load matches size/crc. */
int psx_netplay_rb_media_kf_probe_match(uint32_t size, uint32_t crc);
/* §97: apply RB_KF state blob (or host finish). Returns 1 if handled. */
int psx_netplay_rb_media_kf_on_ready(const void *data, size_t size);
/* Mid-guest resim pump: abort if Replay has made no finish_frame progress.
* Full rb_pump stays admit/present-edge only (host-asymmetric). */
void psx_netplay_rb_poll_replay_stall(void);
+4
View File
@@ -82,6 +82,10 @@ void psx_frontend_on_savestate_loaded(void);
/* Rollback snap apply: depth24 hold clear + restage without FMV cutover thrash. */
void psx_frontend_on_rb_snap_loaded(void);
/* Frontend hook (main.cpp): host OSD toast after a user save/load settles.
* is_load: 0 = save, 1 = load. slot is 0-based (F1 = 0). ok: 1 on success. */
void psx_frontend_on_savestate_notify(int is_load, int slot, int ok);
/* Called every block from psx_check_interrupts (in_exception == 0). If a save is
* pending, serialize with cpu->pc = resume_pc; if a load is pending, restore and
* longjmp to the scheduler (never returns in that case). Near-free when idle. */
+2
View File
@@ -185,6 +185,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/ws_ui_group.c
${PSXRECOMP_ROOT}/runtime/src/ws_aspect_cone_math.c
${PSXRECOMP_ROOT}/runtime/src/gpu_sw_renderer.c
${PSXRECOMP_ROOT}/runtime/src/gpu_vram_dirty.c
${PSXRECOMP_ROOT}/runtime/src/gpu_render.c
${PSXRECOMP_ROOT}/runtime/src/gpu_gl_renderer.c
${PSXRECOMP_ROOT}/runtime/src/gpu_vk_renderer.c
@@ -215,6 +216,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/bios_hle.c
${PSXRECOMP_ROOT}/runtime/src/bios_hle_plan.c
${PSXRECOMP_ROOT}/runtime/src/savestate.c
${PSXRECOMP_ROOT}/runtime/src/host_osd.c
${PSXRECOMP_ROOT}/runtime/src/cosim_state.c
${PSXRECOMP_ROOT}/runtime/src/cosim.c
${PSXRECOMP_ROOT}/runtime/src/traps.c
+164 -19
View File
@@ -1,7 +1,9 @@
#include "boot_state.h"
#include "overlay_api.h" /* PSX_OVERLAY_CODEGEN_HASH / _ABI_TAG / _CODEGEN_VER */
#include "dirty_ram_interp.h"
#include "gpu.h" /* gpu_get_vram — CPU-auth mirror under dual-raster */
#include "gpu_render.h" /* gr_vram_transfer_in / gr_vram_transfer_out */
#include "gpu_vram_dirty.h"
#include "cpu_state.h" /* gte_canonicalize_cpu_state after CPU wire restore */
#include "interrupts.h"
#include "psx_cycles.h"
@@ -86,6 +88,103 @@ static char s_capture_path[512];
static uint32_t s_capture_checksum;
static uint32_t s_capture_entry_pc;
/* §96: persistent VRAM mirror for raw ring snaps — patch dirty scanlines only. */
static uint16_t s_vram_mirror[VRAM_W * VRAM_H];
static int s_vram_mirror_valid;
static uint32_t s_last_vram_dirty_rows;
static int s_last_vram_incremental;
uint32_t boot_state_last_vram_dirty_rows(void)
{
return s_last_vram_dirty_rows;
}
int boot_state_last_vram_incremental(void)
{
return s_last_vram_incremental;
}
void boot_state_vram_mirror_reset(void)
{
s_vram_mirror_valid = 0;
s_last_vram_dirty_rows = VRAM_H;
s_last_vram_incremental = 0;
}
/* Build s_vram_mirror from live CPU VRAM using dirty rows when possible.
* Always emits a full 1 MiB BS_SEC_VRAM (loads stay independent).
* Only used while gpu_vram_dirty_tracking() (rollback netplay). */
static int sync_vram_mirror_for_save(void)
{
const uint16_t *live = gpu_get_vram();
uint32_t dirty_n;
uint32_t y;
if (!gpu_vram_dirty_tracking()) {
/* Should not be called offline — full refresh fallback. */
if (live)
memcpy(s_vram_mirror, live, VRAM_SIZE);
else
gr_vram_transfer_out(0, 0, VRAM_W, VRAM_H, s_vram_mirror);
s_vram_mirror_valid = 1;
s_last_vram_dirty_rows = VRAM_H;
s_last_vram_incremental = 0;
return 1;
}
dirty_n = gpu_vram_dirty_row_count();
s_last_vram_dirty_rows = dirty_n;
if (!live) {
gr_vram_transfer_out(0, 0, VRAM_W, VRAM_H, s_vram_mirror);
s_vram_mirror_valid = 1;
s_last_vram_dirty_rows = VRAM_H;
s_last_vram_incremental = 0;
gpu_vram_dirty_clear();
return 1;
}
if (!s_vram_mirror_valid || dirty_n >= VRAM_H) {
memcpy(s_vram_mirror, live, VRAM_SIZE);
s_vram_mirror_valid = 1;
s_last_vram_incremental = 0;
} else if (dirty_n == 0u) {
/* Mirror already matches live. */
s_last_vram_incremental = 1;
} else {
const uint64_t *mask = gpu_vram_dirty_mask();
for (y = 0; y < VRAM_H; y++) {
if (mask[y >> 6] & ((uint64_t)1u << (y & 63u))) {
memcpy(s_vram_mirror + (size_t)y * VRAM_W,
live + (size_t)y * VRAM_W,
(size_t)VRAM_W * sizeof(uint16_t));
}
}
s_last_vram_incremental = 1;
}
if (gpu_vram_dirty_verify_enabled()) {
uint16_t *full = (uint16_t *)malloc(VRAM_SIZE);
if (full) {
gr_vram_transfer_out(0, 0, VRAM_W, VRAM_H, full);
if (memcmp(full, s_vram_mirror, VRAM_SIZE) != 0) {
fprintf(stderr,
"psxrecomp: VRAM dirty VERIFY FAIL dirty_rows=%u "
"incr=%d — forcing full mirror\n",
(unsigned)dirty_n, s_last_vram_incremental);
fflush(stderr);
memcpy(s_vram_mirror, full, VRAM_SIZE);
s_last_vram_incremental = 0;
s_last_vram_dirty_rows = VRAM_H;
}
free(full);
}
}
gpu_vram_dirty_clear();
return 1;
}
/* File or growable memory sink — both save paths share one serializer. */
typedef struct BsOut {
FILE* f; /* non-NULL => write to file */
@@ -227,6 +326,38 @@ static int write_timer_section(BsOut* o) {
/* ============================ SAVE ============================ */
/* Classic full VRAM section (offline / zlib / tracking off). */
static int write_vram_section_full(BsOut *o)
{
uint16_t *vbuf = (uint16_t *)malloc(VRAM_SIZE);
int ok;
if (!vbuf)
return 0;
gr_vram_transfer_out(0, 0, VRAM_W, VRAM_H, vbuf);
s_last_vram_dirty_rows = VRAM_H;
s_last_vram_incremental = 0;
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
ok = write_section(o, BS_SEC_VRAM, vbuf, VRAM_SIZE);
#else
{
uint8_t *wire = (uint8_t *)malloc(VRAM_SIZE);
if (!wire) {
free(vbuf);
return 0;
}
{
PstW w;
pst_w_init(&w, wire, VRAM_SIZE);
ok = pst_w_pod(&w, vbuf, VRAM_SIZE, 2) &&
write_section(o, BS_SEC_VRAM, wire, VRAM_SIZE);
}
free(wire);
}
#endif
free(vbuf);
return ok;
}
static int boot_state_save_to(BsOut* o, const CPUState* cpu,
uint32_t bios_checksum, uint32_t entry_pc) {
BootStateHeader h;
@@ -269,29 +400,29 @@ static int boot_state_save_to(BsOut* o, const CPUState* cpu,
}
if (ok) ok = write_module_section(o, BS_SEC_GPU, gpu_snapshot_bytes, gpu_snapshot_write);
if (ok) {
uint16_t* vbuf = (uint16_t*)malloc(VRAM_SIZE);
if (!vbuf) ok = 0;
else {
gr_vram_transfer_out(0, 0, VRAM_W, VRAM_H, vbuf);
/* VRAM is uint16 LE guest pixels — emit as LE u16 stream.
* pst_w_pod is a memcpy on LE hosts (see pst_wire.h); skip the
* extra wire alloc there (netplay ring snaps every few ticks). */
/* §96 incremental mirror only while RB dirty-tracking is on.
* Offline / delay-sync / zlib disk: classic full transfer_out. */
if (o->no_zlib && gpu_vram_dirty_tracking()) {
ok = sync_vram_mirror_for_save();
if (ok) {
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
ok = write_section(o, BS_SEC_VRAM, vbuf, VRAM_SIZE);
ok = write_section(o, BS_SEC_VRAM, s_vram_mirror, VRAM_SIZE);
#else
{
uint8_t* wire = (uint8_t*)malloc(VRAM_SIZE);
if (!wire) ok = 0;
else {
PstW w;
pst_w_init(&w, wire, VRAM_SIZE);
ok = pst_w_pod(&w, vbuf, VRAM_SIZE, 2) &&
write_section(o, BS_SEC_VRAM, wire, VRAM_SIZE);
free(wire);
{
uint8_t* wire = (uint8_t*)malloc(VRAM_SIZE);
if (!wire) ok = 0;
else {
PstW w;
pst_w_init(&w, wire, VRAM_SIZE);
ok = pst_w_pod(&w, s_vram_mirror, VRAM_SIZE, 2) &&
write_section(o, BS_SEC_VRAM, wire, VRAM_SIZE);
free(wire);
}
}
}
#endif
free(vbuf);
}
} else {
ok = write_vram_section_full(o);
}
}
if (ok) ok = write_module_section(o, BS_SEC_SPU, spu_snapshot_bytes, spu_snapshot_write);
@@ -472,6 +603,13 @@ static int apply_section(uint32_t tag, const uint8_t* p, uint32_t len,
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
/* Wire == host layout: upload straight from the section buffer. */
gr_vram_transfer_in(0, 0, VRAM_W, VRAM_H, (const uint16_t*)p);
if (gpu_vram_dirty_tracking()) {
memcpy(s_vram_mirror, p, VRAM_SIZE);
s_vram_mirror_valid = 1;
gpu_vram_dirty_clear();
} else {
s_vram_mirror_valid = 0;
}
return 1;
#else
{
@@ -485,6 +623,13 @@ static int apply_section(uint32_t tag, const uint8_t* p, uint32_t len,
return 0;
}
gr_vram_transfer_in(0, 0, VRAM_W, VRAM_H, vbuf);
if (gpu_vram_dirty_tracking()) {
memcpy(s_vram_mirror, vbuf, VRAM_SIZE);
s_vram_mirror_valid = 1;
gpu_vram_dirty_clear();
} else {
s_vram_mirror_valid = 0;
}
free(vbuf);
return 1;
}
+4
View File
@@ -13,6 +13,7 @@
#include "gpu.h"
#include "gpu_primitive_reject.h"
#include "gpu_sw_renderer.h"
#include "gpu_vram_dirty.h"
#include "gpu_render.h"
#include "text_xlate.h"
#include "crash_trace.h"
@@ -2258,6 +2259,7 @@ uint64_t g_pollhack_vblank_count = 0; /* instrumentation: poll-fallback VBlank
static void gpu_reset_state(int clear_vram) {
if (clear_vram) {
memset(vram, 0, sizeof(vram));
gpu_vram_dirty_mark_all();
}
gr_init(vram);
@@ -2928,6 +2930,7 @@ static void raster_pixel(int32_t x, int32_t y, uint16_t color) {
uint32_t idx = vy * 1024 + vx;
if (check_mask_bit && (vram[idx] & 0x8000u)) return;
vram[idx] = color | (set_mask_bit ? 0x8000u : 0u);
gpu_vram_dirty_mark_row(vy);
}
/* Inclusive draw-area reject (same predicate as raster_pixel / hardware clip).
@@ -4742,6 +4745,7 @@ static void gpu_write_gp0_body(uint32_t val) {
* reads/savestates. The renderer mirror is committed in bulk when
* the GP0 payload completes. */
vram[(uint32_t)wy * 1024u + wx] = pixel;
gpu_vram_dirty_mark_row((uint32_t)wy);
next_pixel:
if (++vram_write_col == vram_write_w) {
+90 -8
View File
@@ -67,6 +67,7 @@
#include "gpu_render.h"
#include "gpu_sw_renderer.h"
#include "gpu_gl_renderer.h"
#include "host_osd.h"
#include "latency_ring.h"
#include "psx_sdl.h"
@@ -312,7 +313,10 @@ static int s_req_scale = 1; /* requested before context init */
static GLuint s_present_tex = 0; /* CPU-readout present path (24bpp) */
static int s_present_w = 0, s_present_h = 0;
static GLuint s_osd_tex = 0;
static int s_osd_tw = 0, s_osd_th = 0;
static GLuint s_present_prog = 0, s_present_vao = 0;
static void gl_swap_with_osd(void);
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];
@@ -2716,6 +2720,9 @@ void gl_renderer_shutdown(void) {
* (rematch 24-bit FMV → black picture, audio still runs). */
s_present_w = 0;
s_present_h = 0;
s_osd_tex = 0;
s_osd_tw = 0;
s_osd_th = 0;
s_depth24_skip_up = 0;
rect_clear(&s_d24_skip_fb);
hold_invalidate();
@@ -2792,7 +2799,7 @@ void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linea
pres_record(GL_PRES_CPU, 0, 0, src_w, src_h, lx, ly, lw, lh);
hold_capture_drawable();
latency_ring_mark(LAT_SWAP_BEGIN);
SDL_GL_SwapWindow(s_win);
gl_swap_with_osd();
latency_ring_mark(LAT_SWAP_END);
s_probe_swap++;
present_force_consumed();
@@ -2808,7 +2815,7 @@ void gl_renderer_present_blank(void) {
pres_record(GL_PRES_BLANK, 0, 0, 0, 0, 0, 0, ww, wh);
hold_capture_drawable();
latency_ring_mark(LAT_SWAP_BEGIN);
SDL_GL_SwapWindow(s_win);
gl_swap_with_osd();
latency_ring_mark(LAT_SWAP_END);
s_probe_swap++;
present_force_consumed();
@@ -3695,7 +3702,7 @@ static int interp_present(void) {
glFlush();
pres_record(GL_PRES_INTERP, 0, 0, s_interp_w, s_interp_h,
lx, ly, lw, lh);
SDL_GL_SwapWindow(s_win);
gl_swap_with_osd();
s_interp_swaps++;
return 1;
}
@@ -3758,6 +3765,79 @@ static int interp_thread_main(void *opaque) {
return 0;
}
/* Draw one host OSD ARGB image into the default framebuffer at (vx,vy)
* in top-left window coordinates (y down). */
static void gl_draw_osd_image(const uint32_t *px, int ow, int oh,
int vx, int vy, int ww, int wh) {
if (!px || ow <= 0 || oh <= 0 || !s_present_prog || ww <= 0 || wh <= 0)
return;
if (!s_osd_tex) glGenTextures(1, &s_osd_tex);
p_glActiveTexture(PSXGL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, s_osd_tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
if (s_osd_tw != ow || s_osd_th != oh) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, ow, oh, 0,
GL_BGRA, GL_UNSIGNED_BYTE, px);
s_osd_tw = ow;
s_osd_th = oh;
} else {
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, ow, oh,
GL_BGRA, GL_UNSIGNED_BYTE, px);
}
int dw = ow, dh = oh;
if (vx + dw > ww) dw = ww - vx;
if (vy + dh > wh) dh = wh - vy;
if (dw < 1 || dh < 1) return;
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_STENCIL_TEST);
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
/* GL viewport origin is bottom-left. */
glViewport(vx, wh - vy - dh, dw, dh);
p_glUseProgram(s_present_prog);
p_glUniform1i(s_present_uTex, 0);
/* Cancel PRESENT_VS V-flip — CPU row 0 is the top of the image. */
p_glUniform4f(s_present_uUvRect, 0.f, 1.f, 1.f, 0.f);
{
GLuint vao = s_present_vao;
if (s_interp_ctx && SDL_GL_GetCurrentContext() == s_interp_ctx &&
s_interp_thread_vao)
vao = s_interp_thread_vao;
p_glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
p_glBindVertexArray(0);
}
p_glUseProgram(0);
glDisable(GL_BLEND);
}
/* Composite host toast + volume bar into the default framebuffer, then swap. */
static void gl_swap_with_osd(void) {
if (s_present_prog && s_ctx) {
int ww = 0, wh = 0;
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
if (ww > 0 && wh > 0) {
const uint32_t *px = NULL;
int ow = 0, oh = 0;
const int margin = 8;
if (host_osd_image(&px, &ow, &oh) && px)
gl_draw_osd_image(px, ow, oh, margin, margin, ww, wh);
if (host_osd_volume_image(&px, &ow, &oh) && px) {
int vx = (ww > ow + margin) ? (ww - ow - margin) : margin;
int vy = (wh > oh) ? ((wh - oh) / 2) : margin;
gl_draw_osd_image(px, ow, oh, vx, vy, ww, wh);
}
}
}
host_osd_present_done();
SDL_GL_SwapWindow(s_win);
}
static void present_target_quad(GLuint tex, int tex_w, int tex_h,
int x, int y, int w, int h, int linear,
int lx, int ly, int lw, int lh, int v_flip) {
@@ -3847,7 +3927,7 @@ int gl_renderer_present_hold_last(void) {
hold_capture_drawable();
}
latency_ring_mark(LAT_SWAP_BEGIN);
SDL_GL_SwapWindow(s_win);
gl_swap_with_osd();
latency_ring_mark(LAT_SWAP_END);
s_probe_swap++;
return 1;
@@ -3863,7 +3943,8 @@ void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear,
s_last_present_path == GL_PRES_VRAM &&
s_last_dx == disp_x && s_last_dy == disp_y &&
s_last_dw == w && s_last_dh == h &&
!present_dirty_test(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1)) {
!present_dirty_test(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1) &&
!host_osd_needs_present()) {
s_probe_skip++;
gl_perf_present_enter();
gl_perf_present_exit(0);
@@ -3904,7 +3985,7 @@ void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear,
pres_record(GL_PRES_VRAM, disp_x, disp_y, w, h, lx, ly, lw, lh);
hold_capture_drawable();
latency_ring_mark(LAT_SWAP_BEGIN);
SDL_GL_SwapWindow(s_win);
gl_swap_with_osd();
latency_ring_mark(LAT_SWAP_END);
s_probe_swap++;
gl_perf_present_exit(0);
@@ -3969,7 +4050,8 @@ int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear)
s_last_present_path == GL_PRES_WIDE &&
s_last_dx == disp_x && s_last_dy == disp_y &&
s_last_dw == g_wide_w && s_last_dh == disp_h &&
!present_dirty_test(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1)) {
!present_dirty_test(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1) &&
!host_osd_needs_present()) {
s_probe_skip++;
gl_perf_present_enter();
gl_perf_present_exit(1);
@@ -4004,7 +4086,7 @@ int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear)
pres_record(GL_PRES_WIDE, disp_x, disp_y, g_wide_w, disp_h, lx, ly, lw, lh);
hold_capture_drawable();
latency_ring_mark(LAT_SWAP_BEGIN);
SDL_GL_SwapWindow(s_win);
gl_swap_with_osd();
latency_ring_mark(LAT_SWAP_END);
s_probe_swap++;
gl_perf_present_exit(1);
+12
View File
@@ -28,6 +28,7 @@
*/
#include "gpu_sw_renderer.h"
#include "gpu_vram_dirty.h"
#include "gpu_sw_edges.h"
#include <string.h>
#include <stdlib.h>
@@ -274,6 +275,9 @@ static inline void put_opaque(const RTarget *t, int x, int y, uint16_t color) {
if (g_mask_set_bit) color |= 0x8000;
t->buf[idx] = color;
/* Canonical VRAM only (hi-res / wide mirrors are present-side). */
if (t->buf == g_vram)
gpu_vram_dirty_mark_row((uint32_t)y);
}
/* Write a textured pixel — semi-trans only if texel bit 15 is set */
@@ -316,6 +320,8 @@ static inline void put_textured(const RTarget *t, int x, int y, uint16_t texel,
if (g_mask_set_bit) color |= 0x8000;
t->buf[idx] = color;
if (t->buf == g_vram)
gpu_vram_dirty_mark_row((uint32_t)y);
}
/* ------------------------------------------------------------------ */
@@ -626,6 +632,7 @@ void sw_fill_rect(int x, int y, int w, int h, uint16_t color) {
g_vram[py * VRAM_WIDTH + px] = color;
}
}
gpu_vram_dirty_mark_rect(x0, y0, w, h);
if (g_hr) hr_fill_block(x, y, w, h, color);
}
@@ -691,6 +698,7 @@ void sw_copy_rect(int src_x, int src_y, int dst_x, int dst_y, int w, int h) {
}
}
}
gpu_vram_dirty_mark_row((uint32_t)dy);
}
if (hr_rows) free(hr_rows);
@@ -1514,6 +1522,7 @@ void sw_vram_write(int x, int y, uint16_t pixel) {
x &= (VRAM_WIDTH - 1);
y &= (VRAM_HEIGHT - 1);
g_vram[y * VRAM_WIDTH + x] = pixel;
gpu_vram_dirty_mark_row((uint32_t)y);
if (g_hr) {
int s = g_scale;
@@ -1580,6 +1589,7 @@ void sw_vram_transfer_in(int x, int y, int w, int h, const uint16_t *data) {
* does s² stores per guest texel — multi-second hitches on 2×/4×. */
if (x == 0 && y == 0 && w == VRAM_WIDTH && h == VRAM_HEIGHT) {
memcpy(g_vram, data, (size_t)VRAM_WIDTH * (size_t)VRAM_HEIGHT * sizeof(uint16_t));
gpu_vram_dirty_mark_all();
if (g_hr) hr_rebuild_from_vram();
return;
}
@@ -1593,6 +1603,7 @@ void sw_vram_transfer_in(int x, int y, int w, int h, const uint16_t *data) {
data + (size_t)row * (size_t)w,
(size_t)w * sizeof(uint16_t));
}
gpu_vram_dirty_mark_rect(x, y, w, h);
return;
}
@@ -1614,6 +1625,7 @@ void sw_vram_transfer_in(int x, int y, int w, int h, const uint16_t *data) {
}
}
}
gpu_vram_dirty_mark_row((uint32_t)py);
}
}
+109 -16
View File
@@ -21,6 +21,7 @@
#include "gpu_render.h"
#include "gpu_vk_renderer.h"
#include "gpu_sw_renderer.h"
#include "host_osd.h"
#include "crash_trace.h"
#include "gpu_vk_upload.h"
@@ -1394,11 +1395,13 @@ int vk_renderer_init_context(SDL_Window *win) {
}
static void cpres_cache_free(void); /* fwd: CPU-present resource cache */
static void osd_staging_free(void);
void vk_renderer_shutdown(void) {
if (!s_dev) return;
p_vkDeviceWaitIdle(s_dev);
vk_gpu_sync_internal(); /* reclaim deferred staging before tearing down */
cpres_cache_free(); /* FMV CPU-present cached image + staging */
osd_staging_free(); /* host toast OSD staging */
for (int i = 0; i < STAGING_CACHE_MAX; ++i) {
if (s_staging_cache[i].buf)
staging_destroy(s_staging_cache[i].buf, s_staging_cache[i].mem);
@@ -1491,6 +1494,108 @@ static int acquire_present(VkImage *out_sc, VkCommandBuffer *out_cb,
return 1;
}
/* Copy host toast into the swapchain (must still be TRANSFER_DST). Opaque
* overwrite — host_osd bakes an opaque panel so no blend pass is needed. */
static VkBuffer s_osd_buf = VK_NULL_HANDLE;
static VkDeviceMemory s_osd_mem = VK_NULL_HANDLE;
static void *s_osd_map = NULL;
static VkDeviceSize s_osd_cap = 0;
static void osd_staging_free(void) {
if (s_osd_buf) {
staging_destroy(s_osd_buf, s_osd_mem);
s_osd_buf = VK_NULL_HANDLE;
s_osd_mem = VK_NULL_HANDLE;
}
s_osd_map = NULL;
s_osd_cap = 0;
}
static void vk_osd_copy_rect(VkCommandBuffer cb, VkImage sc,
const uint32_t *px, int w, int h,
int x, int y, VkDeviceSize buf_off) {
if (!px || w <= 0 || h <= 0) return;
int dw = w, dh = h;
if (x + dw > (int)s_sc_extent.width) dw = (int)s_sc_extent.width - x;
if (y + dh > (int)s_sc_extent.height) dh = (int)s_sc_extent.height - y;
if (dw < 1 || dh < 1) return;
memcpy((uint8_t *)s_osd_map + (size_t)buf_off, px,
(size_t)w * (size_t)h * 4u);
VkBufferImageCopy bc = {0};
bc.bufferOffset = buf_off;
bc.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
bc.imageSubresource.layerCount = 1;
bc.imageOffset.x = x;
bc.imageOffset.y = y;
bc.imageExtent.width = (uint32_t)dw;
bc.imageExtent.height = (uint32_t)dh;
bc.imageExtent.depth = 1;
bc.bufferRowLength = (uint32_t)w;
bc.bufferImageHeight = (uint32_t)h;
p_vkCmdCopyBufferToImage(cb, s_osd_buf, sc,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &bc);
}
static void vk_osd_blit(VkCommandBuffer cb, VkImage sc) {
const uint32_t *text_px = NULL, *vol_px = NULL;
int tw = 0, th = 0, vw = 0, vh = 0;
const int have_text = host_osd_image(&text_px, &tw, &th) && text_px;
const int have_vol = host_osd_volume_image(&vol_px, &vw, &vh) && vol_px;
if (!have_text && !have_vol) {
host_osd_present_done();
return;
}
/* Only BGRA swapchains match ARGB8888 LE packing used by host_osd. */
if (s_sc_format != VK_FORMAT_B8G8R8A8_UNORM &&
s_sc_format != VK_FORMAT_B8G8R8A8_SRGB) {
host_osd_present_done();
return;
}
VkDeviceSize text_bytes =
have_text ? (VkDeviceSize)tw * (VkDeviceSize)th * 4u : 0;
VkDeviceSize vol_bytes =
have_vol ? (VkDeviceSize)vw * (VkDeviceSize)vh * 4u : 0;
VkDeviceSize bytes = text_bytes + vol_bytes;
if (bytes > s_osd_cap) {
p_vkQueueWaitIdle(s_queue);
osd_staging_free();
if (!make_staging(bytes, &s_osd_buf, &s_osd_mem, &s_osd_map)) {
host_osd_present_done();
return;
}
s_osd_cap = bytes;
}
const int margin = 8;
VkDeviceSize off = 0;
if (have_text) {
vk_osd_copy_rect(cb, sc, text_px, tw, th, margin, margin, off);
off += text_bytes;
}
if (have_vol) {
int x = ((int)s_sc_extent.width > vw + margin)
? ((int)s_sc_extent.width - vw - margin)
: margin;
int y = ((int)s_sc_extent.height > vh)
? (((int)s_sc_extent.height - vh) / 2)
: margin;
vk_osd_copy_rect(cb, sc, vol_px, vw, vh, x, y, off);
}
host_osd_present_done();
}
static void submit_present(VkCommandBuffer cb, uint32_t img_idx, uint32_t fr);
static void finish_present(VkCommandBuffer cb, VkImage sc,
uint32_t img_idx, uint32_t fr) {
vk_osd_blit(cb, sc);
img_barrier(cb, sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
VK_ACCESS_TRANSFER_WRITE_BIT, 0,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);
submit_present(cb, img_idx, fr);
}
static void submit_present(VkCommandBuffer cb, uint32_t img_idx, uint32_t fr) {
p_vkEndCommandBuffer(cb);
/* Present command buffers write the acquired swapchain image with
@@ -1562,10 +1667,7 @@ int vk_renderer_present_vram(int disp_x, int disp_y, int w, int h,
sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, &blit, linear ? VK_FILTER_LINEAR : VK_FILTER_NEAREST);
img_barrier(cb, sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
VK_ACCESS_TRANSFER_WRITE_BIT, 0,
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);
submit_present(cb, idx, fr);
finish_present(cb, sc, idx, fr);
perf_snapshot_present();
return 1;
}
@@ -1581,10 +1683,7 @@ void vk_renderer_present_blank(void) {
VkClearColorValue black = {{0,0,0,1}};
VkImageSubresourceRange rng = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
p_vkCmdClearColorImage(cb, sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &rng);
img_barrier(cb, sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
VK_ACCESS_TRANSFER_WRITE_BIT, 0,
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);
submit_present(cb, idx, fr);
finish_present(cb, sc, idx, fr);
perf_snapshot_present();
}
@@ -1677,10 +1776,7 @@ void vk_renderer_present_cpu(const uint32_t *pixels, int src_w, int src_h,
blit.dstOffsets[0] = dst[0]; blit.dstOffsets[1] = dst[1];
p_vkCmdBlitImage(cb, img, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, &blit, linear ? VK_FILTER_LINEAR : VK_FILTER_NEAREST);
img_barrier(cb, sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
VK_ACCESS_TRANSFER_WRITE_BIT, 0,
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);
submit_present(cb, idx, fr);
finish_present(cb, sc, idx, fr);
/* Cached resources are rewritten next frame: wait for this frame's copy +
* blit to complete first (FMV cadence is 15-24 fps; one idle-wait per
@@ -1738,10 +1834,7 @@ int vk_renderer_present_wide(int disp_x, int disp_y, int disp_h, int linear) {
1, &blit, linear ? VK_FILTER_LINEAR : VK_FILTER_NEAREST);
img_to(cb, s_wide_img[i], &s_wide_layout[i], VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
img_barrier(cb, sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
VK_ACCESS_TRANSFER_WRITE_BIT, 0,
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);
submit_present(cb, idx, fr);
finish_present(cb, sc, idx, fr);
perf_snapshot_present();
return 1;
}
+67 -12
View File
@@ -20,6 +20,7 @@
#include "starvation_ring.h"
#include "load_accel.h"
#include "savestate.h"
#include "host_osd.h"
#include "overlay_capture.h"
#include "overlay_loader.h"
#include "autocompile.h"
@@ -843,6 +844,23 @@ static void post_load_probe_on_vblank(int turbo_active, int present_reached) {
/* Called from savestate_poll after a successful restore (before scheduler
* longjmp). Clears present latches and forces the next vblank to show the
* restored VRAM — including a blank if display was disabled in the snapshot. */
extern "C" void psx_frontend_on_savestate_notify(int is_load, int slot, int ok) {
char buf[64];
const int disp = slot + 1; /* F1 = slot 1 */
if (is_load) {
if (ok)
snprintf(buf, sizeof(buf), "Loaded slot %d", disp);
else
snprintf(buf, sizeof(buf), "Load failed slot %d", disp);
} else {
if (ok)
snprintf(buf, sizeof(buf), "Saved slot %d", disp);
else
snprintf(buf, sizeof(buf), "Save failed slot %d", disp);
}
host_osd_push(buf, 2000);
}
extern "C" void psx_frontend_on_savestate_loaded(void) {
s_disabled_frame_presented = false;
s_force_present_after_load = true;
@@ -2084,6 +2102,15 @@ static void sdl_audio_pump(bool discard_output = false) {
sdl_audio_gain_ramp(sdl_audio_buf, ramp, g0, g1);
sdl_audio_fadein_left -= ramp;
}
/* Host master volume (launcher / numpad +/-). Applied after fade so mute
* edges stay continuous and volume steps take effect immediately. */
{
const int vol = host_volume_get();
if (vol < 100) {
const float g = (float)vol / 100.0f;
sdl_audio_gain_ramp(sdl_audio_buf, frames, g, g);
}
}
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);
@@ -2474,8 +2501,9 @@ static void sdl_audio_update(int hard_mute_active, int turbo_sink_active) {
if (tail > buf_cap) tail = buf_cap;
/* An unmute ramp may still be in progress; start the down-ramp
* from its current gain so the edge stays continuous. */
const float g0 = 1.0f - (float)sdl_audio_fadein_left
/ (float)sdl_audio_fade_samples;
const float vol = (float)host_volume_get() / 100.0f;
const float g0 = (1.0f - (float)sdl_audio_fadein_left
/ (float)sdl_audio_fade_samples) * vol;
sdl_audio_fadein_left = 0;
spu_render(sdl_audio_buf, tail);
sdl_audio_gain_ramp(sdl_audio_buf, tail, g0, 0.0f);
@@ -4088,6 +4116,7 @@ static void netplay_hold_last_present_tick(void) {
SDL_SetRenderDrawColor(sdl_renderer, 0, 0, 0, 255);
SDL_RenderClear(sdl_renderer);
SDL_RenderCopy(sdl_renderer, sdl_texture, &s_sw_hold_src, &s_sw_hold_dst);
host_osd_draw_sdl(sdl_renderer);
SDL_RenderPresent(sdl_renderer);
did = 1;
}
@@ -4534,6 +4563,13 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
else if (key == SDLK_c && (mod & KMOD_CTRL)) {
std::fprintf(stdout, "[DEBUG] Forzando reinserción de CD...\n");
debug_force_cd_reinsert();
host_osd_push("CD reinsert", 1500);
}
/* Host volume: numpad + / - (5% steps). Shows right-side bar. */
else if (key == SDLK_KP_PLUS) {
host_volume_adjust(+5);
} else if (key == SDLK_KP_MINUS) {
host_volume_adjust(-5);
}
/* Fullscreen toggle: Alt+Enter or Cmd/Ctrl+F. Toggles between
* windowed and the CONFIGURED tri-state mode (g_fullscreen: 1
@@ -4549,6 +4585,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
SDL_WINDOW_FULLSCREEN;
if (is_fs) {
SDL_SetWindowFullscreen(sdl_window, 0);
host_osd_push("Windowed", 1500);
} else {
/* If the configured mode is "off", the hotkey still
* needs a mode to switch INTO — default to borderless,
@@ -4556,6 +4593,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
Uint32 target = psx_fullscreen_flag_for_mode(g_fullscreen);
if (target == 0) target = SDL_WINDOW_FULLSCREEN_DESKTOP;
SDL_SetWindowFullscreen(sdl_window, target);
host_osd_push("Fullscreen", 1500);
}
}
}
@@ -4790,19 +4828,32 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
}
}
/* Netplay FMV: present 1 of every 4 depth24 vblanks while MDEC is hot.
* SW RGB888 scanout dominates guest ms; admit + wall pace still run every
* tick in the epilogue. Do NOT skip pace — MotK ran ~90fps when decode was
* fast and pace was skipped here. During MDEC-idle cutover (FMV1→FMV2)
* present every frame so the next stream can handshake (1/4 skip left the
* gap looking frozen with cheap guest + TURN admit wait). */
/* Netplay FMV: present 1 of every N depth24 vblanks while MDEC is hot.
* §98: default N=2 (was 4). 1/4 made present_gap_p95≈80–100 ms look like
* hitches; SW scanout is batched so 1/2 is affordable. Admit + wall pace
* still run every tick. During MDEC-idle cutover present every frame.
* Override: PSX_NET_FMV_PRESENT_DIV (1=every frame, 2=default, 4=legacy). */
if (psx_netplay_active() && gpu_display_is_depth24() &&
mdec_recently_active(8)) {
if (s_netplay_depth24_present_skip > 0) {
static int s_fmv_present_div = -1;
int div;
if (s_fmv_present_div < 0) {
const char *e = getenv("PSX_NET_FMV_PRESENT_DIV");
unsigned v = 2u;
if (e && e[0] && sscanf(e, "%u", &v) == 1 && v >= 1u && v <= 8u)
s_fmv_present_div = (int)v;
else
s_fmv_present_div = 2;
}
div = s_fmv_present_div;
if (div <= 1) {
s_netplay_depth24_present_skip = 0;
} else if (s_netplay_depth24_present_skip > 0) {
s_netplay_depth24_present_skip--;
return ep;
} else {
s_netplay_depth24_present_skip = div - 1;
}
s_netplay_depth24_present_skip = 3; /* skip next three presents */
} else {
s_netplay_depth24_present_skip = 0;
}
@@ -4927,6 +4978,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
if (sdl_renderer) {
SDL_SetRenderDrawColor(sdl_renderer, 0, 0, 0, 255);
SDL_RenderClear(sdl_renderer);
host_osd_draw_sdl(sdl_renderer);
SDL_RenderPresent(sdl_renderer);
}
}
@@ -5233,6 +5285,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
SDL_SetRenderDrawColor(sdl_renderer, 0, 0, 0, 255);
SDL_RenderClear(sdl_renderer);
SDL_RenderCopy(sdl_renderer, sdl_texture, &src, &dst);
host_osd_draw_sdl(sdl_renderer);
/* §33: remember active rect for resim hold-last (not full 640x512). */
s_sw_hold_src = src;
s_sw_hold_dst = dst;
@@ -8986,7 +9039,7 @@ std::string player_device[PSX_MAX_PLAYERS];
ls.widescreen_hud = ls.widescreen;
ls.enable_audio = 1;
ls.audio_freq = 44100;
ls.volume = 100;
ls.volume = host_volume_get();
{
const int n = std::min(PSX_MAX_PLAYERS, RECOMP_LAUNCHER_MAX_PLAYERS);
for (int i = 0; i < n; ++i) {
@@ -9278,6 +9331,7 @@ std::string player_device[PSX_MAX_PLAYERS];
seed.spu_hq = ls.spu_hq != 0; seed.has_spu_hq = true;
seed.auto_skip_fmv = ls.auto_skip_fmv != 0; seed.has_auto_skip_fmv = true;
seed.turbo_loads = ls.turbo_loads != 0; seed.has_turbo_loads = true;
host_volume_set(ls.volume);
if (ls.bios_path[0]) {
std::filesystem::path resolved =
resolve_bios_path(ls.bios_path, argv[0]);
@@ -10406,7 +10460,7 @@ soft_return_lobby:
ls.fullscreen = g_fullscreen ? 1 : 0;
ls.enable_audio = 1;
ls.audio_freq = 44100;
ls.volume = 100;
ls.volume = host_volume_get();
ls.window_width = g_video_win_w;
ls.renderer = g_video_renderer;
ls.supersampling = g_video_scale;
@@ -10499,6 +10553,7 @@ soft_return_lobby:
#endif
if (rui_rc == 0) {
host_volume_set(ls.volume);
if (rui_out_disc[0]) {
resolved_disc = normalize_disc_path_for_launch(rui_out_disc);
disc_path_str = resolved_disc.string();
+50 -1
View File
@@ -1,8 +1,29 @@
#include "netplay_snap_ring.h"
#include "boot_state.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(_WIN32)
# include <windows.h>
#else
# include <time.h>
#endif
static double snap_mono_ms(void)
{
#if defined(_WIN32)
static LARGE_INTEGER freq;
LARGE_INTEGER c;
if (!freq.QuadPart) QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&c);
return (double)c.QuadPart * 1000.0 / (double)freq.QuadPart;
#else
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1.0e6;
#endif
}
typedef struct NetplaySnapSlot {
int valid;
@@ -160,11 +181,39 @@ int netplay_snap_ring_save(NetplaySnapRing* r, uint32_t tick,
uint32_t entry_pc) {
uint8_t* data = NULL;
size_t len = 0;
double t0, dt;
static uint32_t s_tele_n;
static double s_tele_ms_sum;
static uint64_t s_tele_bytes_sum;
static uint64_t s_tele_dirty_sum;
static uint32_t s_tele_incr_n;
if (!r || !cpu) return 0;
/* Raw (no zlib): compress2 on RAM+VRAM+SPU dominated live FPS. Load
* accepts uncompressed v4 sections. */
* accepts uncompressed v4 sections. §96: dirty-scanline VRAM mirror. */
t0 = snap_mono_ms();
if (!boot_state_save_buffer_raw(cpu, bios_checksum, entry_pc, &data, &len))
return 0;
dt = snap_mono_ms() - t0;
s_tele_n++;
s_tele_ms_sum += dt;
s_tele_bytes_sum += (uint64_t)len;
s_tele_dirty_sum += (uint64_t)boot_state_last_vram_dirty_rows();
if (boot_state_last_vram_incremental())
s_tele_incr_n++;
/* Heartbeat every 64 saves — visible during FMV dense / media windows. */
if ((s_tele_n & 63u) == 0u) {
fprintf(stderr,
"psxrecomp: rb snap tele n=%u last=%.2fms avg=%.2fms "
"bytes=%zu avg_bytes=%.0f dirty_rows=%u incr=%d "
"(avg_dirty=%.1f incr%%=%.0f)\n",
(unsigned)s_tele_n, dt, s_tele_ms_sum / (double)s_tele_n,
len, (double)s_tele_bytes_sum / (double)s_tele_n,
(unsigned)boot_state_last_vram_dirty_rows(),
boot_state_last_vram_incremental(),
(double)s_tele_dirty_sum / (double)s_tele_n,
100.0 * (double)s_tele_incr_n / (double)s_tele_n);
fflush(stderr);
}
if (!netplay_snap_ring_store(r, tick, data, len))
return 0; /* store frees data on hard failure */
return 1;
+8 -1
View File
@@ -24,6 +24,7 @@ uint64_t psx_cycle_count = 0;
uint32_t g_psx_cyc_batch = 0;
uint32_t g_psx_cyc_batch_limit = 0;
int g_psx_cyc_bb_defer = 0;
uint32_t *g_psx_cyc_local_acc = NULL;
static int s_cycle_replay_active = 0;
static uint64_t s_cycle_replay_live = 0;
@@ -331,7 +332,9 @@ void psx_advance_cycles_slow(uint32_t cycles) {
}
uint64_t psx_get_cycle_count(void) {
return psx_cycle_count + (uint64_t)g_psx_cyc_batch;
uint64_t n = psx_cycle_count + (uint64_t)g_psx_cyc_batch;
if (g_psx_cyc_local_acc) n += (uint64_t)(*g_psx_cyc_local_acc);
return n;
}
/* ===== Idle-loop cycle skip (wait-loop elision, 2026-07-06) ==================
@@ -554,6 +557,8 @@ void psx_idle_note_check(CPUState *cpu, uint32_t check_pc) {
* would try to replay a bogus gap. Re-anchor devices at the restored cycle and
* force a fresh deadline on the next charge. */
void psx_cycles_resync_after_restore(CPUState *cpu) {
if (g_psx_cyc_local_acc) *g_psx_cyc_local_acc = 0;
g_psx_cyc_local_acc = NULL;
g_psx_cyc_batch = 0;
g_psx_cyc_batch_limit = 0;
g_psx_cyc_bb_defer = 0;
@@ -593,6 +598,8 @@ void psx_cycles_resync_after_restore(CPUState *cpu) {
}
void psx_cycles_reset_for_boot(void) {
if (g_psx_cyc_local_acc) *g_psx_cyc_local_acc = 0;
g_psx_cyc_local_acc = NULL;
g_psx_cyc_batch = 0;
g_psx_cyc_batch_limit = 0;
g_psx_cyc_bb_defer = 0;
+118 -9
View File
@@ -858,6 +858,7 @@ 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 void np_note_save_complete(void);
static int np_file_crc(const uint8_t *data, size_t size, uint32_t *crc_out)
{
@@ -1015,6 +1016,14 @@ static void np_apply_ready_state(void)
return;
}
/* §97: FMV media keyframe — install into RB pin/ring (no disk slot). */
if (op == RNET_STATE_OP_RB_KF) {
(void)psx_netplay_rb_media_kf_on_ready(data, size);
rnet_session_state_finish(g_np.session, 0);
g_np.xfer = NP_XFER_NONE;
return;
}
if (op == RNET_STATE_OP_SAVE) {
if (g_np.local_slot != 0) {
if (!savestate_write_slot((int)slot, data, size)) {
@@ -1047,6 +1056,7 @@ static void np_apply_ready_state(void)
}
rnet_session_state_finish(g_np.session, 0);
g_np.xfer = NP_XFER_NONE;
np_note_save_complete();
return;
}
@@ -1153,6 +1163,16 @@ static void np_guest_handle_probe(void)
return;
}
/* §97: rollback FMV media keyframe — CRC of raw snap at episode load. */
if (op == RNET_STATE_OP_RB_KF) {
match = psx_netplay_rb_media_kf_probe_match(size, crc);
(void)rnet_session_state_probe_reply(g_np.session, match);
printf("psxrecomp: netplay MEDIA-KF probe — %s (size=%u crc=%08x)\n",
match ? "match" : "miss", (unsigned)size, (unsigned)crc);
fflush(stdout);
return;
}
{
uint32_t local_sz = 0, local_crc = 0;
char reason[192];
@@ -1177,6 +1197,7 @@ static void np_guest_handle_probe(void)
"skip transfer\n",
(unsigned)slot);
fflush(stdout);
np_note_save_complete();
} else {
/* Host will chunk the authoritative .pst — stay parked. */
g_np.xfer = NP_XFER_SAVE_SEND;
@@ -1276,6 +1297,7 @@ static void np_host_drive_xfer(void)
g_np.xfer_slot);
fflush(stdout);
g_np.xfer = NP_XFER_NONE;
np_note_save_complete();
return;
}
if (!savestate_read_slot(g_np.xfer_slot, &buf, &n) || !buf ||
@@ -1333,6 +1355,7 @@ static void np_host_drive_xfer(void)
case NP_XFER_LOAD_SEND:
/* apply_ready runs first and clears take_ready (LOAD → LOAD_APPLYING). */
if (rnet_session_state_take_ready(g_np.session, NULL, NULL, NULL, NULL)) {
int was_save = (g_np.xfer == NP_XFER_SAVE_SEND);
if (g_np.xfer == NP_XFER_MC_SEND)
g_np.mc_sync_done = 1;
rnet_session_state_finish(g_np.session, 0);
@@ -1340,6 +1363,8 @@ static void np_host_drive_xfer(void)
np_begin_load_apply(g_np.xfer_slot);
} else {
g_np.xfer = NP_XFER_NONE;
if (was_save)
np_note_save_complete();
}
}
return;
@@ -1431,6 +1456,9 @@ static void np_commit_load_sync(void)
g_np.needs_advance = 0;
g_np.latched_for_tick = 0;
/* staged_valid left set by prime — tip must match delay-prefix hold. */
/* §95: load is a hard resync — drop leftover FMV DESYNC invent-hold. */
if (g_np.rollback)
psx_netplay_rb_clear_fmv_desync_hold("netplay load sync");
}
static void np_enter_load_ready(int slot)
@@ -3421,7 +3449,7 @@ int psx_netplay_start(const PsxNetplayConfig *cfg)
#endif
fprintf(stderr,
"psxrecomp: rb binary path=%s size=%lld "
"(peers must match bit-identical)\n",
"(same game version + digests; per-peer PGO OK)\n",
exe, sz);
fflush(stderr);
}
@@ -3943,6 +3971,16 @@ static void np_maybe_stage_target_save(void)
fflush(stdout);
}
/* §94: SAVE freeze opens a tip hole; invent-hold from FMV MAX-unmatched then
* deadlocks admit (stall looked like fmv_settle). Clear invent-hold so gap1
* invent can refill before the 20s watchdog. */
static void np_note_save_complete(void)
{
if (!g_np.rollback)
return;
psx_netplay_rb_clear_fmv_desync_hold("netplay save complete");
}
static void np_pump_session(void)
{
#if defined(PSX_HAS_LOBBY_CLIENT)
@@ -4008,6 +4046,66 @@ static int np_try_admit_gameplay(void)
return 0;
}
/*
* §95: rollback LOAD barrier admit — tip + hold-last invent, no INPUT_CONFIRM.
* Delay-sync try_admit wait_confirm hung the slower peer after the faster one
* applied and froze (guest LOADED, host stuck load_applying+wait_confirm).
* Load will hard_resync at mutual ready; pads here only need guest cycles for
* savestate_poll / one resume tick.
*/
static int np_try_admit_load_barrier_rb(void)
{
rnet_u32 sim = rnet_session_sim_tick(g_np.session);
rnet_u32 wire;
RNetInputSample sample;
RNetRbFrame row;
PsxNetPad pad;
int slot;
np_sched_set_admit_stall("load_barrier_rb");
if (!rnet_session_prepare_local_tip(g_np.session, sim)) {
np_sched_set_admit_stall("load_barrier_prepare_tip");
force_session_pads_connected(g_np.slot_count);
return 0;
}
wire = np_sched_wire_for_sim(sim);
for (slot = 0; slot < g_np.slot_count; ++slot) {
if (slot == g_np.local_slot) {
if (rnet_session_peek_input(g_np.session, slot, wire, &sample)) {
decode_pad(&sample, &pad);
netplay_ih_pad_to_frame(&pad, sim, 0, &row);
(void)netplay_ih_put(&g_np.ih, slot, &row);
} else if (g_np.staged_valid) {
netplay_ih_pad_to_frame(&g_np.staged, sim, 0, &row);
(void)netplay_ih_put(&g_np.ih, slot, &row);
} else {
memset(&pad, 0, sizeof(pad));
pad.buttons = 0xFFFFu;
pad.lx = pad.ly = pad.rx = pad.ry = 0x80u;
pad.analog = 0;
pad.connected = 1;
netplay_ih_pad_to_frame(&pad, sim, 0, &row);
(void)netplay_ih_put(&g_np.ih, slot, &row);
}
continue;
}
if (rnet_session_peek_remote_input(g_np.session, slot, wire, &sample)) {
decode_pad(&sample, &pad);
netplay_ih_pad_to_frame(&pad, sim, 0, &row);
(void)netplay_ih_put(&g_np.ih, slot, &row);
} else {
/* Never stall — peer may already be frozen in LOAD_READY. */
(void)netplay_ih_invent_hold_last(&g_np.ih, slot, sim, &row);
}
}
np_publish_hist_sio(sim);
g_np.needs_advance = 1;
np_sched_set_admit_stall("");
return 1;
}
int psx_netplay_poll_admit(void)
{
rnet_u32 sim;
@@ -4036,31 +4134,42 @@ int psx_netplay_poll_admit(void)
return 0;
/* Staged load must run guest cycles — bypass starvation latch. ICE xfer
* often leaves lead=D-1 and would otherwise block try_admit forever. */
* often leaves lead=D-1 and would otherwise block try_admit forever.
* §95: rollback must not use delay-sync confirm here. */
if (g_np.xfer == NP_XFER_LOAD_APPLYING && savestate_pending()) {
if (g_np.needs_advance)
return 1;
if (g_np.rollback)
return np_try_admit_load_barrier_rb();
return np_try_admit_gameplay();
}
/* Both peers: after mutual ready + sync, stay in LOAD_READY until try_admit
* succeeds (fresh tip exchange + INPUT_CONFIRM). Dropping the barrier early
* on the host let it spin on confirm with FPS/present already "live". */
/* Both peers: after mutual ready + sync, stay in LOAD_READY until admit
* succeeds. Dropping the barrier early on the host let it spin on confirm
* with FPS/present already "live". §95: rollback exits via tip invent. */
if (g_np.xfer == NP_XFER_LOAD_READY) {
if (g_np.load_sync_done && g_np.load_ready_replied && !g_np.needs_advance) {
sim = rnet_session_sim_tick(g_np.session);
if (rnet_session_try_admit(g_np.session, sim)) {
int admitted;
if (g_np.rollback)
admitted = np_try_admit_load_barrier_rb();
else {
sim = rnet_session_sim_tick(g_np.session);
admitted = rnet_session_try_admit(g_np.session, sim);
if (admitted)
g_np.needs_advance = 1;
else
force_session_pads_connected(g_np.slot_count);
}
if (admitted) {
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.needs_advance = 1;
printf("psxrecomp: netplay load slot=%d — peer ready, resuming lockstep\n",
g_np.xfer_slot);
fflush(stdout);
return 1;
}
force_session_pads_connected(g_np.slot_count);
}
return 0;
}
+444 -77
View File
@@ -34,7 +34,21 @@ int psx_netplay_rb_rewind_suppressed(void) { return 0; }
int psx_netplay_rb_fmv_defer_rewind(void) { return 0; }
int psx_netplay_rb_fmv_media_active(void) { return 0; }
int psx_netplay_rb_lockstep_no_invent(void) { return 0; }
int psx_netplay_rb_fmv_desync_hold(void) { return 0; }
void psx_netplay_rb_clear_fmv_desync_hold(const char *why) { (void)why; }
int psx_netplay_rb_fmv_episode_unsafe(uint32_t tick) { (void)tick; return 0; }
int psx_netplay_rb_media_kf_probe_match(uint32_t size, uint32_t crc)
{
(void)size;
(void)crc;
return 0;
}
int psx_netplay_rb_media_kf_on_ready(const void *data, size_t size)
{
(void)data;
(void)size;
return 0;
}
void psx_netplay_rb_poll_replay_stall(void) {}
int psx_netplay_rb_take_promote_sweep(void) { return 0; }
int psx_netplay_rb_fmv_unlock_grace_active(void) { return 0; }
@@ -102,6 +116,7 @@ uint32_t psx_netplay_rb_rtt_estimate_ms(void) { return 0; }
#include "cpu_state.h"
#include "crc32.h"
#include "gpu.h"
#include "gpu_vram_dirty.h"
#include "interrupts.h"
#include "mdec.h"
#include "psx_cycles.h"
@@ -175,6 +190,29 @@ static uint32_t snap_interval(void)
}
return iv;
}
static int rb_media_kf_enabled(void); /* §97 — defined near episode wire clear */
/* §96/§98: FMV media snap interval. Default 4; MEDIA_KF uses 2 (near-tip
* loads without every-tick 0.7 ms snap tax). Override: PSX_NET_FMV_SNAP_INTERVAL. */
static uint32_t fmv_media_snap_interval(void)
{
static int latched;
static uint32_t iv = 4u;
if (!latched) {
const char *e = getenv("PSX_NET_FMV_SNAP_INTERVAL");
if (e && e[0]) {
unsigned v = 0;
if (sscanf(e, "%u", &v) == 1 && v >= 1u && v <= 16u)
iv = v;
} else if (rb_media_kf_enabled()) {
iv = 2u;
}
latched = 1;
}
return iv;
}
/* §58 dense tip snaps: live saves every tick inside a sliding window so a
* near-tip mispredict loads at (or within a tick of) the mismatch instead of
* up to snap_interval-1 ticks below it (§57 soak: depth_avg 14–41 with
@@ -348,6 +386,14 @@ static size_t g_pin_size;
static uint32_t g_pin_tick;
static int g_pin_valid;
/* §97: FMV media keyframe episode — host forces identical snap at load_tick
* before Replay (probe CRC, transfer on miss). Default ON; set
* PSX_NET_FMV_MEDIA_KF=0 to restore §93 refuse / invent-off during media. */
static int g_media_kf_episode;
static int g_media_kf_ready; /* pin holds authoritative KF for load */
static int g_media_kf_host_armed; /* host started probe/send for this episode */
static int g_media_kf_sending; /* host transfer in flight */
/* §67 resim audit digest cache (fin state per replayed tick) — declared here
* so pin_baseline_from_cpu can compare pin digest vs audit fin (§78 PIN-SKEW
* diag). See log_resim_tick_audit for the cache contract. */
@@ -430,6 +476,11 @@ static uint32_t g_last_begin_mismatch = 0xffffffffu;
#define RB_FMV_LOCKSTEP_MIN 180u
#define RB_FMV_LOCKSTEP_MAX 300u
#define RB_FMV_LOCKSTEP_CONFIRM 16u
/* §94: MAX-unmatched DESYNC invent-hold must not last forever. Soft-forked
* digests never rematch CONFIRM ticks — invent stays off, then a SAVE/hitch
* tip hole deadlocks admit (soak: stall=fmv_settle → lobby). Expire invent
* hold after this many sim ticks; episode media-range refuse is separate. */
#define RB_FMV_DESYNC_INVENT_EXPIRE 600u
/* After cores match (or MAX): keep invent off this many more ticks so admit
* waits for wire. Prevents hold-last sticky Up (pub=ffef vs wire=ffff) from
* inventing at unlock+1 → FIRST CORE DIVERGE / tip episode. Matches dense
@@ -545,6 +596,7 @@ static uint32_t g_fmv_media_hi;
/* §93: FMV lockstep hit MAX with streak < CONFIRM — cores never agreed.
* Keep invent/begin off until cores rematch CONFIRM ticks or session reset. */
static int g_fmv_unmatched_desync;
static uint32_t g_fmv_unmatched_desync_arm_sim;
/* §93: consecutive resim-core aborts on the same diverge sim. */
static uint32_t g_resim_diverge_tick;
static uint32_t g_resim_diverge_streak;
@@ -614,6 +666,7 @@ static void clear_tip_hold_rereplay_pending(void);
static int flush_tip_hold_deferred_rereplay(void);
static void arm_rewind_cooldown_ticks(uint32_t sim, uint32_t ticks, const char *why);
static void clear_rewind_cooldown(const char *why);
static void rb_clear_fmv_desync_hold(const char *why);
static void tip_hold_try_finalize(void);
static void schedule_episode_rereplay(uint32_t prefer_plus_one);
static void ownership_on_post_match(uint32_t tip);
@@ -830,13 +883,13 @@ static void rb_fmv_update_lockstep_gate(uint32_t sim)
if (g_fmv_lockstep_released) {
if (g_fmv_unmatched_desync &&
g_fmv_core_match_streak >= RB_FMV_LOCKSTEP_CONFIRM) {
fprintf(stderr,
"psxrecomp: rb FMV DESYNC cleared — cores rematched "
"streak=%u sim=%u\n",
(unsigned)g_fmv_core_match_streak, (unsigned)sim);
fflush(stderr);
g_fmv_unmatched_desync = 0;
clear_rewind_cooldown("FMV cores rematched after MAX unmatched");
{
char why[96];
snprintf(why, sizeof(why),
"cores rematched streak=%u sim=%u",
(unsigned)g_fmv_core_match_streak, (unsigned)sim);
rb_clear_fmv_desync_hold(why);
}
}
return;
}
@@ -868,15 +921,18 @@ static void rb_fmv_update_lockstep_gate(uint32_t sim)
if (dense > g_fmv_lockstep_until)
g_fmv_lockstep_until = dense;
g_fmv_unmatched_desync = 1;
g_fmv_unmatched_desync_arm_sim = sim;
arm_rewind_cooldown_ticks(sim, RB_FORK_STORM_COOLDOWN_TICKS,
"FMV lockstep MAX unmatched "
"(cores never confirmed)");
fprintf(stderr,
"psxrecomp: rb DESYNC — FMV lockstep MAX unmatched "
"sim=%u streak=%u cap=%u — invent/begin held until "
"cores rematch (confirm=%u)\n",
"cores rematch (confirm=%u) or invent-hold expires "
"(+%u ticks) / netplay save\n",
(unsigned)sim, (unsigned)g_fmv_core_match_streak,
(unsigned)cap, (unsigned)RB_FMV_LOCKSTEP_CONFIRM);
(unsigned)cap, (unsigned)RB_FMV_LOCKSTEP_CONFIRM,
(unsigned)RB_FMV_DESYNC_INVENT_EXPIRE);
fflush(stderr);
}
fprintf(stderr,
@@ -1031,34 +1087,57 @@ static int rb_fmv_tick_unsafe_for_episode(uint32_t tick)
return 0;
}
/* Defer rewind during movie + short settle. */
/* §94: drop MAX-unmatched invent-hold (rematch, save complete, or expire). */
static void rb_clear_fmv_desync_hold(const char *why)
{
if (!g_fmv_unmatched_desync)
return;
g_fmv_unmatched_desync = 0;
g_fmv_unmatched_desync_arm_sim = 0;
fprintf(stderr,
"psxrecomp: rb FMV DESYNC invent-hold cleared (%s)\n",
why ? why : "?");
fflush(stderr);
clear_rewind_cooldown(why ? why : "FMV DESYNC invent-hold cleared");
}
/* Expire invent-hold when soft-fork digests never rematch CONFIRM ticks. */
static void rb_fmv_desync_invent_maybe_expire(uint32_t sim)
{
if (!g_fmv_unmatched_desync || g_fmv_unmatched_desync_arm_sim == 0u)
return;
if (sim < g_fmv_unmatched_desync_arm_sim + RB_FMV_DESYNC_INVENT_EXPIRE)
return;
rb_clear_fmv_desync_hold("invent-hold expired (no rematch)");
}
/* Defer rewind during movie + short settle.
* §97: with MEDIA_KF, allow rewind/episodes into live media (keyframe
* heals stream forks); settle + DESYNC hold still defer. */
static int rb_in_fmv_defer_rewind_window(void)
{
RNetSession *s = sess();
uint32_t sim = s ? rnet_session_sim_tick(s) : 0u;
rb_fmv_tick_settle();
rb_fmv_desync_invent_maybe_expire(sim);
if (rb_fmv_media_active())
return 1;
return rb_media_kf_enabled() ? 0 : 1;
if (g_fmv_unmatched_desync)
return 1;
return sim < g_fmv_settle_until;
}
/* Admit no-invent gate (§26): media + short settle only.
* Pre-§26 also blocked invent through g_fmv_lockstep_until (MIN 180 +
* UNLOCK_GRACE 64 ≈ 4s of title-menu delay-sync). On TURN/CGNAT that
* re-serialized presentation to ~packet rate (~30 fps) even with
* PSX_RB_GAP1_INVENT — invent count was 0 from FMV-on through invent_at.
* Digest-gated lockstep (g_fmv_lockstep_until) still runs for dense snaps
* / RELEASE logs; it no longer stalls admit.
* §93: also hold invent after MAX-unmatched DESYNC until cores rematch. */
* §97: MEDIA_KF allows invent during live media (episodes open with host
* keyframe). Settle + DESYNC invent-hold unchanged. */
static int rb_in_fmv_lockstep_window(void)
{
RNetSession *s = sess();
uint32_t sim = s ? rnet_session_sim_tick(s) : 0u;
rb_fmv_tick_settle();
rb_fmv_desync_invent_maybe_expire(sim);
if (rb_fmv_media_active())
return 1;
return rb_media_kf_enabled() ? 0 : 1;
if (g_fmv_unmatched_desync)
return 1;
return sim < g_fmv_settle_until;
@@ -1378,6 +1457,143 @@ static void clear_episode_wire_state(void)
g_seal_wait_logged = 0;
g_last_begin_rexmit_ms = 0;
g_begin_rexmit_logged = 0;
g_media_kf_episode = 0;
g_media_kf_ready = 0;
g_media_kf_host_armed = 0;
g_media_kf_sending = 0;
}
/* §97: default ON — invent/episodes into FMV media with host keyframe. */
static int rb_media_kf_enabled(void)
{
static int latched = -1;
if (latched < 0) {
const char *e = getenv("PSX_NET_FMV_MEDIA_KF");
latched = (e && e[0] == '0') ? 0 : 1;
}
return latched;
}
static void clear_baseline_pin(void);
static int rb_install_media_kf_bytes(uint32_t tick, const uint8_t *data, size_t size)
{
uint8_t *ring_copy;
if (!data || !size)
return 0;
clear_baseline_pin();
g_pin_data = (uint8_t *)malloc(size);
if (!g_pin_data)
return 0;
memcpy(g_pin_data, data, size);
g_pin_size = size;
g_pin_tick = tick;
g_pin_valid = 1;
/* Ring store takes ownership — keep a separate copy from the pin. */
if (g_snaps) {
ring_copy = (uint8_t *)malloc(size);
if (ring_copy) {
memcpy(ring_copy, data, size);
if (!netplay_snap_ring_store(g_snaps, tick, ring_copy, size))
; /* store frees ring_copy on hard failure */
}
}
g_media_kf_ready = 1;
return 1;
}
static int rb_media_kf_seal_local(uint32_t tick)
{
size_t sz = 0;
const uint8_t *p;
if (!g_snaps || !netplay_snap_ring_has(g_snaps, tick))
return 0;
p = netplay_snap_ring_peek(g_snaps, tick, &sz);
if (!p || !sz)
return 0;
return rb_install_media_kf_bytes(tick, p, sz);
}
static void rb_media_kf_host_drive(void)
{
RNetSession *s = sess();
uint32_t load;
size_t sz = 0;
const uint8_t *p;
int match = 0;
uint32_t crc;
int local_slot;
if (!g_media_kf_episode || !g_rb || !s || !rnet_rb_is_active(g_rb))
return;
if (g_media_kf_ready)
return;
local_slot = g_b.local_slot ? *g_b.local_slot : 0;
if (local_slot != 0)
return; /* host-only transfer */
if (rnet_session_state_busy(s) && !g_media_kf_sending)
return; /* SAVE/LOAD in flight — wait */
load = rnet_rb_get_load_tick(g_rb);
p = g_snaps ? netplay_snap_ring_peek(g_snaps, load, &sz) : NULL;
if (!p || !sz) {
static uint32_t s_miss_log;
if (s_miss_log != load) {
fprintf(stderr,
"psxrecomp: rb MEDIA-KF host — no snap at load=%u\n",
(unsigned)load);
fflush(stderr);
s_miss_log = load;
}
return;
}
if (!g_media_kf_host_armed) {
crc = rnet_checksum(p, sz);
if (rnet_session_state_probe(s, RNET_STATE_OP_RB_KF, 0, (rnet_u32)sz,
crc) != 0)
return;
g_media_kf_host_armed = 1;
fprintf(stderr,
"psxrecomp: rb MEDIA-KF probe load=%u bytes=%zu crc=%08x\n",
(unsigned)load, sz, (unsigned)crc);
fflush(stderr);
return;
}
if (g_media_kf_sending) {
/* Wait for take_ready on host side (sender also gets ready). */
return;
}
if (rnet_session_state_probe_take_reply(s, &match)) {
rnet_session_state_probe_finish(s);
if (match) {
if (rb_media_kf_seal_local(load)) {
fprintf(stderr,
"psxrecomp: rb MEDIA-KF hash match load=%u — local "
"pin sealed\n",
(unsigned)load);
fflush(stderr);
}
return;
}
/* Miss: send host snap bytes. */
if (rnet_session_state_begin(s, RNET_STATE_OP_RB_KF, 0, p, sz) != 0) {
fprintf(stderr,
"psxrecomp: rb MEDIA-KF begin FAILED load=%u\n",
(unsigned)load);
fflush(stderr);
g_media_kf_host_armed = 0;
return;
}
g_media_kf_sending = 1;
(void)rb_media_kf_seal_local(load);
fprintf(stderr,
"psxrecomp: rb MEDIA-KF transfer start load=%u bytes=%zu\n",
(unsigned)load, sz);
fflush(stderr);
}
}
static void clear_baseline_pin(void)
@@ -5134,10 +5350,16 @@ void psx_netplay_rb_start(void)
if (!g_bound || !s)
return;
/* §96: dirty-VRAM tracking only while RB netplay is live. */
gpu_vram_dirty_set_tracking(1);
boot_state_vram_mirror_reset();
g_snaps = netplay_snap_ring_create(NETPLAY_SNAP_RING_DEFAULT_DEPTH);
tip_dense_reset();
if (!g_snaps) {
fprintf(stderr, "psxrecomp: rb snap ring create FAILED — rewind disabled\n");
gpu_vram_dirty_set_tracking(0);
boot_state_vram_mirror_reset();
return;
}
delay = g_b.input_delay ? *g_b.input_delay : 2;
@@ -5245,6 +5467,8 @@ void psx_netplay_rb_shutdown(void)
netplay_snap_ring_destroy(g_snaps);
g_snaps = NULL;
}
gpu_vram_dirty_set_tracking(0);
boot_state_vram_mirror_reset();
clear_baseline_pin();
g_pending_save_valid = 0;
g_pending_load_valid = 0;
@@ -5266,6 +5490,7 @@ void psx_netplay_rb_shutdown(void)
g_fmv_media_lo = 0;
g_fmv_media_hi = 0;
g_fmv_unmatched_desync = 0;
g_fmv_unmatched_desync_arm_sim = 0;
g_resim_diverge_tick = 0;
g_resim_diverge_streak = 0;
g_bl_mismatch_streak = 0;
@@ -5311,15 +5536,13 @@ void psx_netplay_rb_shutdown(void)
g_rb_rtt_ema_ms = 0;
}
/* Dense tip snaps: media/settle, plus the digest lockstep/+tip window even
* after invent is allowed again (§26) — first Cross/Start after FMV still
* wants a near-tip load. */
/* Dense tip snaps: settle + digest lockstep/+tip (§26) — first Cross/Start
* after FMV still wants a near-tip load. Media uses fmv_media_snap_interval
* instead (§96); episodes into media remain refused. */
static int rb_fmv_dense_snap_window(void)
{
RNetSession *s = sess();
uint32_t sim = s ? rnet_session_sim_tick(s) : 0u;
if (rb_fmv_media_active())
return 1;
if (sim < g_fmv_settle_until)
return 1;
if (g_fmv_media_end_sim &&
@@ -5341,9 +5564,9 @@ void psx_netplay_rb_request_snap(uint32_t tick)
return;
/* Resim must keep a snap at every replayed tick (post-verify + next
* correction). Live path rate-limits — full boot_state snaps dominate FPS.
* Exception: media + post-FMV lockstep/+tip keep dense snaps so the first
* invent-miss loads near the mismatch (954→944 was a ~10-tick hitch;
* with tip snaps + shared frontier → load≈953). */
* Exception: post-FMV settle/lockstep/+tip keep every-tick snaps so the
* first invent-miss loads near the mismatch. FMV media (§96) uses a
* coarser interval — episodes into media are refused anyway. */
if (!(g_rb && rnet_rb_is_resimulating(g_rb))) {
/* §75: sealed Replay snaps are authoritative — Live invent must not
* poison them for tip-extend rereplay. */
@@ -5354,6 +5577,12 @@ void psx_netplay_rb_request_snap(uint32_t tick)
* first_bad-1 still in the ring after a 16–30 tick REMOTE-HELD walk. */
if (g_rb && rnet_rb_is_tip_holding(g_rb) && tip_dense_window() > 0u) {
tip_dense_push(tick);
} else if (rb_fmv_media_active()) {
iv = fmv_media_snap_interval();
if (iv > 1u && (tick % iv) != 0u)
return;
if (tick >= g_fmv_dense_through)
g_fmv_dense_through = tick;
} else if (!rb_fmv_dense_snap_window()) {
iv = snap_interval();
if (iv > 1u && (tick % iv) != 0u) {
@@ -5482,11 +5711,15 @@ static int try_apply_pending_load(CPUState *cpu_in)
int loaded = 0;
int loaded_from_pin = 0;
uint32_t loaded_tick = g_pending_load_tick;
/* §97: wait for host MEDIA-KF pin before applying into FMV media. */
if (g_media_kf_episode && !g_media_kf_ready)
return 0;
/* Prefer pin when it matches the load tick: live realign and §76
* tip-hold flush (sealed tip at tip-hold entry — ring may still be
* invent-poisoned). */
* invent-poisoned). §97 MEDIA-KF always prefers the sealed pin. */
if (g_pin_valid && g_pin_tick == loaded_tick && g_pin_data &&
g_pin_size && (live_realign || g_tip_extend_rereplay)) {
g_pin_size &&
(live_realign || g_tip_extend_rereplay || g_media_kf_episode)) {
loaded = boot_state_load_buffer(g_pin_data, g_pin_size,
*g_b.bios_checksum, *g_b.entry_pc,
cpu_in);
@@ -5664,21 +5897,22 @@ static int try_apply_pending_load(CPUState *cpu_in)
(unsigned)loaded_tick, (unsigned)pc);
fflush(stderr);
/* §93: Live media detector can be cold while the load snap is mid-
* FMV (soak: begin@184 load=176 → flush media=1). Expand the media
* range and abort before Replay poisons both peers. */
* FMV. Expand the media range. §97 MEDIA-KF episodes continue into
* Replay with the host-sealed keyframe (no abort). */
if (rb_fmv_media_active()) {
if (!g_was_in_fmv) {
g_was_in_fmv = 1;
fprintf(stderr,
"psxrecomp: rb FMV rewind-defer ON "
"(depth24/mdec; no invent)\n");
"(depth24/mdec; media_kf=%d)\n",
g_media_kf_episode);
fflush(stderr);
}
if (g_fmv_media_lo == 0u || loaded_tick < g_fmv_media_lo)
g_fmv_media_lo = loaded_tick;
if (loaded_tick >= g_fmv_media_hi)
g_fmv_media_hi = loaded_tick;
if (!g_ownership_skip_snap) {
if (!g_media_kf_episode && !g_ownership_skip_snap) {
char why[96];
snprintf(why, sizeof(why),
"episode load into FMV media (load=%u)",
@@ -5748,6 +5982,9 @@ void psx_netplay_rb_flush_resume(void)
{
extern int g_psx_cyc_bb_defer;
extern uint32_t g_psx_cyc_batch;
extern uint32_t *g_psx_cyc_local_acc;
if (g_psx_cyc_local_acc) *g_psx_cyc_local_acc = 0;
g_psx_cyc_local_acc = NULL;
g_psx_cyc_bb_defer = 0;
g_psx_cyc_batch = 0;
}
@@ -5850,17 +6087,81 @@ int psx_netplay_rb_fmv_media_active(void)
int psx_netplay_rb_lockstep_no_invent(void)
{
/* Media + settle only (§26). Post-FMV title menus invent+resim.
* §93: also hold after MAX-unmatched DESYNC until cores rematch. */
/* Media + settle (§26). Post-FMV title menus invent+resim.
* §93/§94: also hold after MAX-unmatched DESYNC until rematch,
* invent-hold expire, or netplay save complete. */
return rb_in_fmv_lockstep_window();
}
int psx_netplay_rb_fmv_desync_hold(void)
{
RNetSession *s = sess();
uint32_t sim = s ? rnet_session_sim_tick(s) : 0u;
rb_fmv_desync_invent_maybe_expire(sim);
return g_fmv_unmatched_desync ? 1 : 0;
}
void psx_netplay_rb_clear_fmv_desync_hold(const char *why)
{
rb_clear_fmv_desync_hold(why);
}
int psx_netplay_rb_fmv_episode_unsafe(uint32_t tick)
{
(void)rb_in_fmv_defer_rewind_window();
/* §97: MEDIA_KF allows episodes into media-range (begin arms keyframe).
* DESYNC invent-hold still blocks. */
if (rb_media_kf_enabled() && !g_fmv_unmatched_desync)
return 0;
return rb_fmv_tick_unsafe_for_episode(tick);
}
int psx_netplay_rb_media_kf_probe_match(uint32_t size, uint32_t crc)
{
uint32_t load;
size_t sz = 0;
const uint8_t *p;
if (!g_media_kf_episode || !g_rb || !g_snaps)
return 0;
load = rnet_rb_get_load_tick(g_rb);
p = netplay_snap_ring_peek(g_snaps, load, &sz);
if (!p || sz != (size_t)size)
return 0;
if (rnet_checksum(p, sz) != crc)
return 0;
/* Seal local pin so apply can proceed without waiting for a no-op xfer. */
(void)rb_media_kf_seal_local(load);
return 1;
}
int psx_netplay_rb_media_kf_on_ready(const void *data, size_t size)
{
uint32_t load;
if (!g_media_kf_episode || !g_rb)
return 0;
load = rnet_rb_get_load_tick(g_rb);
if (!g_media_kf_ready) {
if (data && size > 0) {
if (!rb_install_media_kf_bytes(load, (const uint8_t *)data, size)) {
fprintf(stderr,
"psxrecomp: rb MEDIA-KF install FAILED load=%u "
"bytes=%zu\n",
(unsigned)load, size);
fflush(stderr);
return 1;
}
fprintf(stderr,
"psxrecomp: rb MEDIA-KF installed load=%u bytes=%zu\n",
(unsigned)load, size);
fflush(stderr);
} else {
(void)rb_media_kf_seal_local(load);
}
}
g_media_kf_sending = 0;
return 1;
}
void psx_netplay_rb_poll_replay_stall(void)
{
uint64_t now;
@@ -6264,9 +6565,8 @@ int psx_netplay_rb_begin_rewind(uint32_t mismatch_tick, int slot)
}
return 0;
}
/* §93: refuse when the *mismatch* sits in the last media bout — Live tip
* may already be past FMV while hc-fork still targets an in-media tick. */
if (rb_fmv_tick_unsafe_for_episode(mismatch_tick)) {
/* §93 refuse / §97 MEDIA_KF: mismatch in prior media bout. */
if (rb_fmv_tick_unsafe_for_episode(mismatch_tick) && !rb_media_kf_enabled()) {
static uint32_t s_media_mm_refuse_sim;
if (s_media_mm_refuse_sim != sim) {
fprintf(stderr,
@@ -6386,17 +6686,30 @@ int psx_netplay_rb_begin_rewind(uint32_t mismatch_tick, int slot)
s_refuse_last_mismatch = 0xffffffffu;
}
/* §93: chosen load may sit inside a prior FMV bout even when Live tip and
* mismatch are past settle (soak: load=176 / 736 into media). */
if (rb_fmv_tick_unsafe_for_episode(load)) {
fprintf(stderr,
"psxrecomp: rb begin REFUSED mismatch=%u load=%u — FMV "
"media-range load (lo=%u hi=%u settle_until=%u)\n",
(unsigned)mismatch_tick, (unsigned)load,
(unsigned)g_fmv_media_lo, (unsigned)g_fmv_media_hi,
(unsigned)g_fmv_settle_until);
fflush(stderr);
return 0;
/* §93 refuse / §97 MEDIA_KF path for loads inside a prior FMV bout. */
{
int media_load = rb_fmv_tick_unsafe_for_episode(load) ||
rb_fmv_tick_unsafe_for_episode(mismatch_tick);
if (media_load && !rb_media_kf_enabled()) {
fprintf(stderr,
"psxrecomp: rb begin REFUSED mismatch=%u load=%u — FMV "
"media-range load (lo=%u hi=%u settle_until=%u)\n",
(unsigned)mismatch_tick, (unsigned)load,
(unsigned)g_fmv_media_lo, (unsigned)g_fmv_media_hi,
(unsigned)g_fmv_settle_until);
fflush(stderr);
return 0;
}
if (media_load && rb_media_kf_enabled()) {
if (!g_snaps || !netplay_snap_ring_has(g_snaps, load)) {
fprintf(stderr,
"psxrecomp: rb begin REFUSED mismatch=%u load=%u — "
"MEDIA-KF needs snap at load\n",
(unsigned)mismatch_tick, (unsigned)load);
fflush(stderr);
return 0;
}
}
}
target = rnet_rb_suggest_target(g_rb, mismatch_tick, sim);
@@ -6435,7 +6748,7 @@ int psx_netplay_rb_begin_rewind(uint32_t mismatch_tick, int slot)
/* Prefer MotK agreed tip when library watermark not yet set. Depth
* ceiling comes from the session's configured light_tip_max_depth
* (== RB_MOTK_TIP_RUNWAY), not the library default — see
* psx_netplay_rb_start(). §93: never light-tip into/near FMV media. */
* psx_netplay_rb_start(). §93/§97: never light-tip into/near FMV media. */
if (g_agreed_valid &&
!rb_fmv_tick_unsafe_for_episode(load) &&
!rb_fmv_tick_unsafe_for_episode(target) &&
@@ -6447,6 +6760,15 @@ int psx_netplay_rb_begin_rewind(uint32_t mismatch_tick, int slot)
rnet_rb_begin_episode(g_rb, &corr);
psx_netplay_timesync_on_episode_boundary();
clear_episode_wire_state();
/* §97: arm MEDIA_KF after wire clear (clear resets the flag). */
if (rb_media_kf_enabled() &&
(rb_fmv_tick_unsafe_for_episode(load) ||
rb_fmv_tick_unsafe_for_episode(mismatch_tick))) {
g_media_kf_episode = 1;
g_media_kf_ready = 0;
g_media_kf_host_armed = 0;
g_media_kf_sending = 0;
}
g_episode_snap_applied = 0;
g_pending_resume_valid = 0;
g_needs_advance = 0;
@@ -6463,6 +6785,8 @@ int psx_netplay_rb_begin_rewind(uint32_t mismatch_tick, int slot)
rnet_u8 wire_flags = (rnet_u8)(
(rnet_rb_get_corr_flags(g_rb) & RNET_RB_CORR_LIGHT_TIP)
? RNET_RB_SYNC_FLAG_LIGHT_TIP : 0u);
if (g_media_kf_episode)
wire_flags = (rnet_u8)(wire_flags | RNET_RB_SYNC_FLAG_MEDIA_KF);
(void)rnet_session_send_rb_sync(s, corr.epoch_id, corr.mismatch_tick, corr.load_tick,
corr.target_tick, (rnet_u8)(slot < 0 ? 0 : slot),
RNET_RB_SYNC_OP_BEGIN, wire_flags);
@@ -6470,10 +6794,10 @@ int psx_netplay_rb_begin_rewind(uint32_t mismatch_tick, int slot)
export_local_seals();
fprintf(stderr,
"psxrecomp: rb begin epoch=%u mismatch=%u load=%u target=%u slot=%d "
"light=%u (snaps=%u %u..%u local_slot=%d)\n",
"light=%u media_kf=%d (snaps=%u %u..%u local_slot=%d)\n",
(unsigned)corr.epoch_id, (unsigned)mismatch_tick, (unsigned)load, (unsigned)target,
slot, (unsigned)rnet_rb_recommend_light_tip(g_rb), (unsigned)snap_n,
(unsigned)snap_lo, (unsigned)snap_hi,
slot, (unsigned)rnet_rb_recommend_light_tip(g_rb), g_media_kf_episode,
(unsigned)snap_n, (unsigned)snap_lo, (unsigned)snap_hi,
g_b.local_slot ? *g_b.local_slot : -1);
fflush(stderr);
@@ -6917,35 +7241,65 @@ static void begin_follower(uint32_t epoch, uint32_t mismatch, uint32_t load, uin
g_follow_nack_sends = 1;
return;
}
/* §93: refuse follow when load/mismatch sits in the last FMV media bout. */
if (rb_fmv_tick_unsafe_for_episode(load) ||
rb_fmv_tick_unsafe_for_episode(mismatch)) {
fprintf(stderr,
"psxrecomp: rb follow REFUSED epoch=%u load=%u mismatch=%u — "
"FMV media-range (lo=%u hi=%u settle_until=%u)\n",
(unsigned)epoch, (unsigned)load, (unsigned)mismatch,
(unsigned)g_fmv_media_lo, (unsigned)g_fmv_media_hi,
(unsigned)g_fmv_settle_until);
fflush(stderr);
g_follow_nack_pending = 1;
g_follow_nack_epoch = epoch;
g_follow_nack_mismatch = mismatch;
g_follow_nack_load = load;
g_follow_nack_target = target;
g_follow_nack_slot = slot;
g_follow_nack_sends = 0;
send_follow_nack(epoch, mismatch, load, target, slot, 1);
g_follow_nack_sends = 1;
return;
/* §93 refuse / §97 MEDIA_KF follow into prior FMV bout. */
{
int media_ep = (wire_flags & RNET_RB_SYNC_FLAG_MEDIA_KF) ||
rb_fmv_tick_unsafe_for_episode(load) ||
rb_fmv_tick_unsafe_for_episode(mismatch);
if (media_ep && !rb_media_kf_enabled() &&
!(wire_flags & RNET_RB_SYNC_FLAG_MEDIA_KF)) {
fprintf(stderr,
"psxrecomp: rb follow REFUSED epoch=%u load=%u mismatch=%u — "
"FMV media-range (lo=%u hi=%u settle_until=%u)\n",
(unsigned)epoch, (unsigned)load, (unsigned)mismatch,
(unsigned)g_fmv_media_lo, (unsigned)g_fmv_media_hi,
(unsigned)g_fmv_settle_until);
fflush(stderr);
g_follow_nack_pending = 1;
g_follow_nack_epoch = epoch;
g_follow_nack_mismatch = mismatch;
g_follow_nack_load = load;
g_follow_nack_target = target;
g_follow_nack_slot = slot;
g_follow_nack_sends = 0;
send_follow_nack(epoch, mismatch, load, target, slot, 1);
g_follow_nack_sends = 1;
return;
}
if (media_ep && rb_media_kf_enabled()) {
/* Follower may lack the snap — host will transfer the keyframe. */
;
} else if (media_ep) {
fprintf(stderr,
"psxrecomp: rb follow REFUSED epoch=%u — MEDIA-KF disabled "
"but peer requested media episode\n",
(unsigned)epoch);
fflush(stderr);
g_follow_nack_pending = 1;
g_follow_nack_epoch = epoch;
g_follow_nack_mismatch = mismatch;
g_follow_nack_load = load;
g_follow_nack_target = target;
g_follow_nack_slot = slot;
g_follow_nack_sends = 0;
send_follow_nack(epoch, mismatch, load, target, slot, 1);
g_follow_nack_sends = 1;
return;
}
}
snap_n = g_snaps ? netplay_snap_ring_count(g_snaps) : 0u;
/* §60: ownership-chain continue uses skip-snap at the verified tip — the
* initiator does not require a ring snap there; neither may the follower,
* or we NACK a legitimate chain BEGIN and storm. */
* or we NACK a legitimate chain BEGIN and storm.
* §97 MEDIA_KF: missing snap is OK — host transfers the keyframe. */
if (!g_snaps || !netplay_snap_ring_has(g_snaps, load)) {
int skip_ok = (g_ownership_skip_snap && g_agreed_valid &&
load == g_agreed_through);
if (!skip_ok) {
int media_kf_ok = rb_media_kf_enabled() &&
((wire_flags & RNET_RB_SYNC_FLAG_MEDIA_KF) ||
rb_fmv_tick_unsafe_for_episode(load) ||
rb_fmv_tick_unsafe_for_episode(mismatch));
if (!skip_ok && !media_kf_ok) {
fprintf(stderr,
"psxrecomp: rb follow REFUSED epoch=%u load=%u — snap missing "
"(ring count=%u newest=%u)\n",
@@ -7041,6 +7395,15 @@ static void begin_follower(uint32_t epoch, uint32_t mismatch, uint32_t load, uin
rnet_rb_begin_episode(g_rb, &corr);
psx_netplay_timesync_on_episode_boundary();
clear_episode_wire_state();
if (rb_media_kf_enabled() &&
((wire_flags & RNET_RB_SYNC_FLAG_MEDIA_KF) ||
rb_fmv_tick_unsafe_for_episode(load) ||
rb_fmv_tick_unsafe_for_episode(mismatch))) {
g_media_kf_episode = 1;
g_media_kf_ready = 0;
g_media_kf_host_armed = 0;
g_media_kf_sending = 0;
}
g_episode_snap_applied = 0;
g_pending_resume_valid = 0;
g_needs_advance = 0;
@@ -7057,9 +7420,10 @@ static void begin_follower(uint32_t epoch, uint32_t mismatch, uint32_t load, uin
export_local_seals();
fprintf(stderr,
"psxrecomp: rb follow epoch=%u mismatch=%u load=%u target=%u "
"(snaps=%u local_slot=%d)\n",
"media_kf=%d (snaps=%u local_slot=%d)\n",
(unsigned)epoch, (unsigned)mismatch, (unsigned)load, (unsigned)target,
(unsigned)snap_n, g_b.local_slot ? *g_b.local_slot : -1);
g_media_kf_episode, (unsigned)snap_n,
g_b.local_slot ? *g_b.local_slot : -1);
fflush(stderr);
/* Initiator's export_local_seals() burst can have beaten this follower's
* own delayed SYNC through the relay — drain any stash for this epoch
@@ -7086,6 +7450,9 @@ void psx_netplay_rb_pump(void)
if (!g_rb || !s)
return;
/* §97: host probe/transfer media keyframe while awaiting baseline. */
rb_media_kf_host_drive();
/* §40: drain RESOLVED before SYNC so follow BEGIN in the same poll
* sees an updated frontier (max(agreed, resolved_through)). */
{
+28 -2
View File
@@ -591,6 +591,12 @@ static void np_timesync_note_late(uint32_t age)
g_ts_note_late_suppressed_rb++;
return;
}
/* §98: FMV media invent/mispredict noise must not pile timesync debt —
* each 6 ms/tick shave showed up as admit=5–11 ms/f in FMV soaks. */
if (psx_netplay_rb_fmv_media_active()) {
g_ts_note_late_suppressed_rb++;
return;
}
now = sched_mono_ms();
if (g_ts_off_until_ms != 0u && (int32_t)(now - g_ts_off_until_ms) < 0) {
g_ts_note_late_suppressed_off++;
@@ -715,6 +721,14 @@ static int np_timesync_throttle(uint32_t wire)
np_timesync_check_enabled();
if (!g_ts_enabled)
return 0;
/* §98: drain media-era debt without stalling admit (FMV guest is already
* the ceiling; do not stack timesync shaves on top). */
if (psx_netplay_rb_fmv_media_active()) {
g_ts_debt_ms = 0u;
s_stall_until = 0u;
s_logged = 0;
return 0;
}
now = sched_mono_ms();
if (wire != s_last_wire) {
if (s_last_wire != 0u && wire == s_last_wire + 1u) {
@@ -1052,6 +1066,10 @@ static void np_adapt_delay_on_pcap_enter(uint32_t now)
/* Host / sim-authority only — guests receive DELAY_SYNC. */
if (sched_local_slot() != 0)
return;
/* §98: FMV media invent storms must not ratchet D (soak: D=6→7 while
* admit climbed to 5–11 ms/f). Keep D stable through the movie. */
if (psx_netplay_rb_fmv_media_active())
return;
if (g_pcap_window_t0_ms == 0u ||
(uint32_t)(now - g_pcap_window_t0_ms) > RB_ADAPT_WINDOW_MS) {
@@ -1195,6 +1213,11 @@ static void np_auto_delay_tick(uint32_t now)
g_ad_late_sum_ms = 0;
g_ad_late_max_ms = 0;
/* §98: freeze D through FMV media — discard invent-storm samples so the
* first post-media eval does not ratchet from movie miss_rate. */
if (psx_netplay_rb_fmv_media_active())
return;
d = sched_delay();
if (ticks < 120u) {
s_agree_streak = 0u; /* <~2 s of live samples — episode/FMV window */
@@ -1513,8 +1536,10 @@ int np_sched_on_remote_miss(int slot, uint32_t sim, uint32_t wire,
* Start). Invent idle opened tip episodes that hung. Tip-ahead with a
* missing consumption wire is a ring hole — log before stalling. */
if (psx_netplay_rb_lockstep_no_invent()) {
/* §94: DESYNC invent-hold is not settle — mis-tag hid the real gate. */
const char *why = psx_netplay_rb_fmv_media_active() ? "fmv_media"
: "fmv_settle";
: (psx_netplay_rb_fmv_desync_hold() ? "fmv_desync_hold"
: "fmv_settle");
if (st->highest_remote_wire > wire)
np_diag_wire_hole(slot, sim, wire, st, why);
else
@@ -1646,7 +1671,7 @@ int np_sched_on_remote_miss(int slot, uint32_t sim, uint32_t wire,
* next tip is due by cadence. Expiry still invents
* (np_gap1_note_expire_invent), so WAN behavior degrades to
* §29 with a bounded 6ms tax at worst. */
if (case_a) {
if (case_a && !psx_netplay_rb_fmv_media_active()) {
uint32_t rtt_raw = 0u;
int lan;
int tip_due;
@@ -1660,6 +1685,7 @@ int np_sched_on_remote_miss(int slot, uint32_t sim, uint32_t wire,
gap1_cap = RB_GAP1_LAN_GRACE_CAP_MS;
}
}
/* §98: during FMV media invent immediately (no LAN micro-grace). */
}
if (gap1_cap != 0u) {
if (np_invent_grace_stall_ex(slot, wire, gap1_cap, 0)) {
+7
View File
@@ -340,6 +340,9 @@ void savestate_poll(CPUState* cpu, uint32_t resume_pc) {
int ok = boot_state_save(&snap, s_bios_checksum, s_entry_pc, path);
fprintf(stderr, "savestate: %s slot %d @ pc=0x%08X -> %s\n",
ok ? "SAVED" : "SAVE FAILED", slot, (unsigned)resume_pc, path);
psx_frontend_on_savestate_notify(0, slot, ok);
} else {
psx_frontend_on_savestate_notify(0, slot, 0);
}
}
@@ -362,6 +365,7 @@ void savestate_poll(CPUState* cpu, uint32_t resume_pc) {
"savestate: LOAD FAILED blob (%zu bytes, entry=%08X)\n",
blob_len, (unsigned)s_entry_pc);
s_load_failed = 1;
psx_frontend_on_savestate_notify(1, slot, 0);
}
} else if (savestate_slot_path(slot, path, sizeof(path))) {
loaded = boot_state_load(path, s_bios_checksum, s_entry_pc, cpu);
@@ -370,10 +374,12 @@ void savestate_poll(CPUState* cpu, uint32_t resume_pc) {
"savestate: LOAD FAILED slot %d %s\n",
slot, path);
s_load_failed = 1;
psx_frontend_on_savestate_notify(1, slot, 0);
}
} else {
fprintf(stderr, "savestate: LOAD FAILED slot %d (no path)\n", slot);
s_load_failed = 1;
psx_frontend_on_savestate_notify(1, slot, 0);
}
if (loaded) {
t_after_boot = savestate_mono_ms();
@@ -400,6 +406,7 @@ void savestate_poll(CPUState* cpu, uint32_t resume_pc) {
t_after_frontend - t_after_boot,
t_after_frontend - t_load0,
path[0] ? "" : " [blob]");
psx_frontend_on_savestate_notify(1, slot, 1);
/* Unwind to the scheduler and re-dispatch the restored PC. Never
* returns; abandons the suspended CPS frames on the current stack. */
psx_scheduler_resume_at(cpu->pc);
+3
View File
@@ -816,6 +816,9 @@ void psx_scheduler_run(CPUState* cpu)
{
extern int g_psx_cyc_bb_defer;
extern uint32_t g_psx_cyc_batch;
extern uint32_t *g_psx_cyc_local_acc;
if (g_psx_cyc_local_acc) *g_psx_cyc_local_acc = 0;
g_psx_cyc_local_acc = NULL;
g_psx_cyc_bb_defer = 0;
g_psx_cyc_batch = 0;
}
+19
View File
@@ -8,6 +8,7 @@ uint64_t psx_next_service_cycle = 0;
uint32_t g_psx_cyc_batch = 0;
uint32_t g_psx_cyc_batch_limit = 0;
int g_psx_cyc_bb_defer = 0;
uint32_t *g_psx_cyc_local_acc = 0;
int psx_in_device_service = 0;
int g_event_step_conservative = 0;
int g_ls_replay_active = 0;
@@ -33,6 +34,7 @@ static void reset_clock(uint64_t deadline) {
g_psx_cyc_batch = 0;
g_psx_cyc_batch_limit = 0;
g_psx_cyc_bb_defer = 0;
g_psx_cyc_local_acc = 0;
service_count = 0;
}
@@ -73,5 +75,22 @@ int main(void) {
psx_cyc_charge(1u);
assert(psx_cycle_count == 1u);
assert(service_count == 1);
/* Emitter-level local accumulator: charges stay off the published clock
* until local_publish / batch_flush; guest total at the barrier matches. */
reset_clock(1000u);
{
uint32_t acc = 0;
psx_cyc_local_begin(&acc);
for (int i = 0; i < 100; i++) psx_cyc_charge(5u);
assert(psx_cycle_count == 0u);
assert(g_psx_cyc_batch == 0u);
assert(acc == 500u);
psx_cyc_bb_defer_flush();
assert(psx_cycle_count == 500u);
assert(acc == 0u);
psx_cyc_local_end();
assert(g_psx_cyc_local_acc == 0);
}
return 0;
}