mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
rollback FMV patches and overlay
This commit is contained in:
@@ -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
@@ -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
@@ -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 =
|
||||
|
||||
+1
-1
Submodule lib/recomp-net updated: 7f00475673...eebd43b5bf
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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)). */
|
||||
{
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user