rollback + fmv + connectivity

This commit is contained in:
Alex Vanderveen
2026-08-06 20:34:50 -04:00
parent 36e0d92c8b
commit 4073e2311c
22 changed files with 1911 additions and 261 deletions
+3
View File
@@ -189,6 +189,9 @@ Use this before tagging a setup-host release that matches other titles
- [ ] `psxrecomp/` and `recomp-ui/` are root-level submodules on pinned commits
- [ ] CLI lives in the submodule (`psxrecomp/psxrecomp_cli.py`) — no sibling sdk
- [ ] `game.toml` has disc identity (`[prepare_disc]` hashes/sizes) and boot EXE
- [ ] Multi-track titles that ship netplay: `[netplay] require_cue` +
`required_tracks` (and optional `required_disc_fp`) so Track-01-only
dumps cannot go online against full Redump cues
- [ ] Seeds cover the boot path; `VERSION` matches the release you will tag
- [ ] Disc images, `generated/`, and `SCPH1001.BIN` are gitignored
- [ ] Setup-host CMake path builds with **no** game C and **no** BIOS backends
+216
View File
@@ -4586,3 +4586,219 @@ hardcoded present-1/4 skip (~67 ms+ between Swaps), not snap cost.
**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.
---
## 101. Zombie-load follow NACK (session-136 seal hang) (2026-08-06)
**Symptoms (session 136 soak):** guest `begin epoch=33 load=3184` while host
already had `agreed tip=3200` → host `follow REFUSED … zombie load` ×100 with
**no NACK** → guest sat in `SealInputs` until `RB_SEAL_TIMEOUT_MS` (4s) →
`episode ABORT` → hc-fork + ownership cascade; host FPS cliff `0.8` /
`admit≈1241 ms`, present_gap max ~3–4 s.
**Root cause:** §87 zombie-load / zombie-epoch refuse returned silently. Other
refuse paths (past frontier, missing snap, FMV) already armed `follow NACK`.
**What landed:**
1. **NACK on zombie load / zombie epoch** — `follow_refuse_nack` arms the
same follow-NACK (frontier = `follower_frontier_hint` / agreed tip) so the
initiator aborts Seal in ~1 RTT instead of 4s.
2. **Rate-limited refuse log** — one line per `(epoch, load)`, then
`+N similar` every 32; one NACK arm per unique refuse.
3. **NACK peer-ahead fast path** — when NACK `frontier >= load`, set
`g_peer_nack_floor`, `apply_peer_resolved(frontier)`, abort Seal, and
**keep-live** when the snap was never applied (no demote-to-`load-1`
rewind of good Live).
4. **Begin raise** — after `choose_load`, if `peer_resolved > load`, raise
to a mutual snap ≤ peer tip so we do not reopen a zombie load.
**Re-soak watch:** no `seal timeout (peer missing snap / NACK lost)` after
zombie refuse; expect `follow NACK … frontier=` then
`NACK keep-live peer-ahead` / `begin raise load` within tens of ms, not a
multi-second present hitch.
---
## 102. Peer-ahead NACK → one light tip (no ownership cascade) (2026-08-06)
**Symptoms (session 140 after §101):** zombie refuse NACKed cleanly, but host
still opened `hc-fork` → `begin load=6640 target=6660` → ownership hops
32/40 → host **22 fps / replay 96%** / present_gap max ~531 ms.
**Root cause:** keep-live left hc-fork recovery to open a deep SPAN; Replay
ownership then chained to drain confirmed backlog.
**What landed:**
1. **Drain RESOLVED before `choose_load`** — avoid opening a zombie load when
the peer tip is already queued on the socket.
2. **`psx_netplay_hc_fork_recovery_clear`** — peer-ahead NACK drops pending
hc-fork bookkeeping so it cannot open a SPAN recovery.
3. **Immediate light tip reopen** — raise agreed to peer tip, arm
`g_peer_ahead_force_load`, `begin` with target capped to `load+4`, force
light-tip class.
4. **No ownership chain** — `ownership_on_post_match` exits that episode to
Live (`peer-ahead light → Live`) instead of SPAN hops.
**Re-soak watch:** after zombie NACK expect
`peer-ahead light reopen` / `begin … light=1` / `peer-ahead light → Live`
with **no** `ownership chain tip=` hops and no `hc-fork recovery begin` on
that edge. Hitch should be a short light replay (depth ≤4), not a 96% window.
---
## 103. Past-frontier NACK deep demote → MEDIA-KF hang (2026-08-06)
**Symptoms (session 142):** post-FMV host `begin epoch=16 load=816 light=1
media_kf=0`; guest `follow REFUSED … past frontier=763` + NACK; host
`NACK realign … demote=763 live_was=827` (dropped 63 snaps) →
`FIRST CORE DIVERGE @764` → `begin … load=752 media_kf=1` (~3.7 MB transfer,
`stall=rb_baseline`) → after KF tip=776 ownership hop
`begin REFUSED … FMV lockstep` → tip-hold; host present_gap max ~3531 ms /
~15 fps, guest ~7.4 fps.
**Root cause:** past-frontier NACK treated a stale follower frontier like a
shared rewind tip. Live demote across the FMV bout forked cores and forced a
deep MEDIA-KF; re-armed settle then blocked the ownership catch-up hop.
**What landed:**
1. **Deep keep-live** — when snap was never applied and
`live − demote > RB_NACK_DEEP_REALIGN_MAX` (8): no Live realign; soft-demote
agreed to `load-1` only; clamp `peer_resolved` to the follower frontier.
2. **Sticky peer gate** — `g_past_frontier_sticky` prevents GATE_MS force-open
from reopening above that frontier until the follower advertises RESOLVED.
3. **Ownership through settle** — `begin` allows `g_ownership_chain` (and
active MEDIA_KF) through FMV lockstep so post-KF SPAN catch-up is not
tip-held.
**Re-soak watch:** after `follow REFUSED … past frontier` expect
`NACK keep-live deep frontier=…` (not `NACK realign … demote=` far below
live); no immediate `MEDIA-KF transfer` / `stall=rb_baseline` from that edge;
if a MEDIA-KF still completes, ownership should log `ownership chain tip=`
hops rather than `ownership chain FAILED … tip-hold`.
---
## 104. Fight ownership tax + guest invent (session 144) (2026-08-06)
**Symptoms (session 144 after §103):** no MEDIA-KF / deep demote / seal hang.
Median 60 fps. Remaining cost was fight-phase: many short ownership
`final`/`BUDGET` hops → replay windows 12–79% / present_gap max ~430 ms;
guest invent ~5× host (`GAP1_LEGACY` case_b + `RUNWAY_EMPTY`); auto-delay
`5→4` mid-fight; one ~3.1 s FMV present cliff (unchanged — not `present_div`).
**What landed:**
1. **Ownership floor tip+6** — `RB_OWNERSHIP_CHAIN_MIN_AHEAD` 4→6 so tip+4/+5
delay cushion exits Live instead of SPAN. Suppress after budget
24→48 ticks / 400→800 ms.
2. **Gap1 micro-grace for relay-LAN** — `RB_GAP1_LAN_RTT_MAX_MS` 12→48 so
`force_input_relay` RTT≈21–45 ms arms grace; cap 6→12 ms. Healthy-lead
case_b also waits (not invent-now on tip cadence blips).
3. **Runway grace** — `RB_INVENT_RUNWAY_GRACE_CAP_MS` 8→12.
4. **Auto-delay lower floor** — never shrink below D=5 mid-match
(`RB_AUTO_DELAY_LOWER_FLOOR`); tighter lower bar (miss<1‰ + lead≥3).
Force-TURN floor 6 unchanged.
5. **PIN-SKEW log rate-limit** — one line per tick (cosmetic; no control change).
**Left alone:** FMV `PSX_NET_FMV_PRESENT_DIV` default 2 (3.1 s cliff is a
single media stall, not 1/N cadence); HC-silent promote stay fail-closed on
digest proof; §101–103 NACK paths untouched.
**Re-soak watch:** fewer `ownership chain tip=` / `BUDGET` lines during
fight; guest `admit stats invent_gap1` closer to host; `gap1_grace` counts
rise; no `delay committed 5 → 4`; replay% spikes under button traffic
smaller (aim <30% windows, present_gap max <200 ms outside FMV).
---
## 105. Invent≠delay raise + gap1 wait for tip invent (session 149) (2026-08-06)
**Symptoms (session 149 after §104):** felt regressive. Ownership begins
dropped (good), but host invent exploded (`invent_gap1=2285`, all
`remote_lead=-1` / case_b), `gap1_grace=0` (healthy-lead arm dead), lobby
`D=4` then auto-delay `4→5→6→7` on invent-as-miss (`miss=993‰`,
`late_n≈1`, `lead_avg≈-2`). Ended in FMV DESYNC MAX unmatched.
**Root cause:** tip invent counted as auto-delay arrival miss → raising D
added lag without stopping invent. §104 grace required `lead≥D-1`, which
never holds on the invent path (`lead=-1`).
**What landed:**
1. **Undo invent miss** — invent clears the auto-delay miss sample
(`np_auto_delay_undo_invent_miss`); scorecard miss-at-need still counts.
2. **Raise needs lateness** — auto-delay bump only when `late_n>0` (waited-out
arrival), not invent-only miss storms.
3. **Gap1 grace for tip invent** — on gap=1, if RTT≤48 ms or tip-due, always
wait `min(½RTT+base, 12ms)` (no healthy-lead gate).
4. **Session D floor 5** — rollback clamps lobby seed `<5` → `5` at
`rnet_session_create` and in `np_sched_sync_delay_from_session`.
**Kept from §104:** ownership `min_ahead=6` / suppress 48/800; runway grace
12; PIN-SKEW rate-limit; auto-delay lower floor 5.
**Re-soak watch:** start log `session delay floor … → 5` if lobby was 4;
`gap1_grace` > 0 in admit stats; no `auto delay 5 → 6/7` driven by
`miss≈1000‰` with `late_n=0/1` and `lead_avg<0`; invent counts lower; D
stable near 5–6 unless real lateness / pcap.
---
## 106. No-ICE MotK → SFU dial / clear ice_p2p refuse (session 151) (2026-08-06)
**Symptoms (session 151):** host (`dist/motk`, ICE build) started SFU
(`force_input_relay=1`, peer `192.168.66.3:8777`). Guest Desktop pack
failed start `-4` with `hosted lobby requires ICE, but ICE is not available
in this build` while peer was already `netplay…:8777` — SFU endpoint present,
`force_input_relay` not applied / ICE required anyway.
**Root cause:** no-ICE guest + MotK room treated as hard ICE requirement even
when a usable SFU peer (or equal host/guest relay advertise) was available.
`ice_p2p` launch on no-ICE builds fell through to the same opaque `-4`.
**What landed:**
1. **`resolve_use_ice` no-ICE** — MotK + non-empty `peer_hostport` → LAN/SFU
dial (log note); MotK/`transport=1` with empty peer → clearer SFU/ICE error.
2. **Launch `ice_p2p` without ICE** — refuse with `last_error=ice_required`
(do not leave launch_pending for a doomed start).
3. **`ae_np_fill_launch`** — infer `force_input_relay=1` when
`host_endpoint==guest_endpoint` (SFU advertise) even if caps bit omitted.
**Re-soak watch:** no-ICE guest on SFU MotK logs `server input relay` or
`no-ICE build — MotK room dialing peer … as LAN/SFU` and connects; ice_p2p
without ICE shows `ice_required` in lobby UI, not start `-4`; both sides
share SFU when server picked `transport=sfu`.
---
## 107. Gap1 micro-grace on SFU/relay RTT (post-§105 soak) (2026-08-06)
**Symptoms (post-§105 soak, ~18k ticks, SFU `force_input_relay=1`):** resim
health good (mispredict host 8 / guest 17, freeze=0, MEDIA-KF sealed, D
ends 7 and moves on real `late_n`). Remaining cost: host tip invent storms
in fight windows (`GAP1_LEGACY` ~550–600 at `remote_lead=-1` while guest
~0–5); **1277/1305 host invents had `rtt>48`**. One auto-delay window
`miss=926‰` with `lead_avg≈-0.9` at rtt~75 (tip-park tax).
**Root cause:** §105 gap1 micro-grace only armed when `rtt_raw≤48` (LAN gate)
or tip-due. SFU/relay POST-RTT sat ~75–125 → `lan=false` → `gap1_cap=0` →
invent every gap=1 tick. §105 correctly stopped invent-as-miss from
ratcheting D; it did not stop the invents.
**What landed:**
1. **Always grace on gap=1** (non-FMV) — `min(½RTT+base, cap)` for tip invent.
2. **Split caps** — LAN / tip-due keep cap **12 ms**; relay (`rtt>48` and not
tip-due) cap **20 ms** so SFU gets a real wait without parking a full
frame on a due tip.
3. **FMV unchanged** — §98 invent-immediate during media.
**Re-soak watch:** host invents/1k drop hard in fight windows (aim ≪70);
`gap1_grace` rises vs invent_gap1; guest invent stays low; D still only
raises on real lateness (`late_n>0`); no feel of extra stall on LAN
(`rtt≤48` path unchanged at cap 12).
+24
View File
@@ -53,11 +53,35 @@ How the two configs relate:
```toml
[program] # in bios.toml; describes the BIOS
[game] # in game.toml; describes the game
[prepare_disc] # optional; data-track digests for prepare/verify
[netplay] # optional; TOC / cue policy for online
[recompiler]
[runtime]
[audit]
```
## Netplay disc mount (`[netplay]`)
Optional. Online play needs the same CD geometry on every peer — data-track
CRC/SHA alone cannot distinguish a Track-01-only dump from a full Redump
multi-track cue. The runtime mounts the resolved path, fingerprints the TOC
(`disc_fp`), and gates Host/Join on `netplay_ok`. Peers also exchange
`disc_fp` through the lobby (`disc_mismatch` on join).
| Field | Default | Description |
|---|---|---|
| `require_cue` | `false` | Require a `.cue` mount (reject bare `.bin` / cue→bin fallback) |
| `required_tracks` | `0` | Exact `iso_track_count` when > 0 (e.g. MotK Redump = `17`) |
| `required_leadout_lba` | unset | Exact lead-out LBA when set |
| `required_disc_fp` | `""` | Exact lowercase hex SHA-256 TOC fingerprint when non-empty |
Offline Play may still launch with a TOC warning; first-run setup Finish and
online Create/Join require `netplay_ok` (and online also a clean verify +
non-empty `disc_fp`). Mirror `required_tracks` in the RetComM catalog as
`rom_identity.track_counts` so the hub library scan rejects Track-01-only dumps.
Wizard / RetComM / catalog submission accept Redump `.cue` + sibling `.bin`
tracks only — not `.iso`/`.chd` (cannot reliably expand to multi-track).
## Program / game block
- **bios.toml** has a `[program]` block describing the BIOS ROM.
+29
View File
@@ -1128,6 +1128,30 @@ GameConfig load_game_config(const fs::path& config_path_in) {
for (char& c : disc_sha1) c = (char)std::tolower((unsigned char)c);
}
// [netplay] — mount / TOC policy for online (see DiscIdentity::disc_fp).
bool netplay_require_cue = false;
int netplay_required_tracks = 0;
bool has_netplay_required_leadout = false;
uint32_t netplay_required_leadout_lba = 0;
std::string netplay_required_disc_fp;
if (cfg.contains("netplay")) {
const toml::value& np = toml::find(cfg, "netplay");
if (np.contains("require_cue"))
netplay_require_cue = toml::find<bool>(np, "require_cue");
if (np.contains("required_tracks"))
netplay_required_tracks = toml::find<int>(np, "required_tracks");
if (np.contains("required_leadout_lba")) {
netplay_required_leadout_lba =
(uint32_t)toml::find<int64_t>(np, "required_leadout_lba");
has_netplay_required_leadout = true;
}
if (np.contains("required_disc_fp")) {
netplay_required_disc_fp = toml::find<std::string>(np, "required_disc_fp");
for (char& c : netplay_required_disc_fp)
c = (char)std::tolower((unsigned char)c);
}
}
// [recompiler]
if (!cfg.contains("recompiler")) {
throw std::runtime_error(
@@ -1866,6 +1890,11 @@ GameConfig load_game_config(const fs::path& config_path_in) {
/*has_disc_crc*/ has_disc_crc,
/*disc_crc*/ disc_crc,
/*disc_sha1*/ disc_sha1,
/*netplay_require_cue*/ netplay_require_cue,
/*netplay_required_tracks*/ netplay_required_tracks,
/*has_netplay_required_leadout*/ has_netplay_required_leadout,
/*netplay_required_leadout_lba*/ netplay_required_leadout_lba,
/*netplay_required_disc_fp*/ netplay_required_disc_fp,
/*seeds_path*/ seeds_path,
/*bios_thunks_path*/ bios_thunks_path,
/*bios_config_path*/ bios_config_path,
+10
View File
@@ -603,6 +603,16 @@ struct GameConfig {
uint32_t disc_crc = 0;
std::string disc_sha1;
// [netplay] disc mount policy (portable across psxrecomp titles).
// Data-track CRC proves "right game"; these prove "same CD geometry"
// (GetTN track count / cue layout) so peers cannot join with Track-01-only
// dumps vs full Redump multi-track cues. 0 / empty = do not check that field.
bool netplay_require_cue = false;
int netplay_required_tracks = 0;
bool has_netplay_required_leadout = false;
uint32_t netplay_required_leadout_lba = 0;
std::string netplay_required_disc_fp; // lowercase hex SHA-256
// [recompiler] block
std::filesystem::path seeds_path; // absolute path to seeds (text or json)
std::filesystem::path bios_thunks_path; // optional; empty if not set
+6 -1
View File
@@ -63,7 +63,8 @@ int cdrom_xa_stream_active(void);
* entry opened a tip episode into a matched black wait. */
int cdrom_fmv_stream_pending(void);
/* Re-arm host absolute CD deadlines from restored relative delays after
* boot_state / RB snap load. Does NOT clamp delays (unlike accelerate). */
* boot_state / RB snap load (read stream + pending/present dues from snap).
* Does NOT clamp delays (unlike accelerate). */
void cdrom_resync_deadlines_after_restore(void);
/* CD load-burst ring (always-on). One record per gap-separated run of
@@ -155,9 +156,13 @@ typedef struct CDROMDebugState {
uint8_t filter_channel;
uint8_t muted;
int read_delay;
/* Cycles until a held CD response is presented to INTC (0 = ready). */
int irq_present_delay;
int pending_pending;
int pending_delay;
int pending_phase;
/* Operating disc-speed divisor (1 during BIOS; game.toml post-entry). */
int speed_divisor;
uint32_t i_stat;
int last_sector_lba;
int last_sector_size;
+3
View File
@@ -125,6 +125,9 @@ uint32_t dirty_ram_get_bitmap(void);
uint32_t dirty_ram_get_bitmap_word(uint32_t word_index);
uint32_t dirty_ram_get_bitmap_word_count(void);
void dirty_ram_set_bitmap_words(const uint32_t* words, uint32_t count);
/* Rematch / session_reboot: wipe host dirty tracking so dig0 matches a cold
* process (memory_init clears RAM but used to leave these bitmaps sticky). */
void dirty_ram_reset_for_boot(void);
/* After bulk RAM restore (savestate): bump overlay page gens + lazy-miss epoch
* so native overlays re-hash against restored bytes; also drop sticky
* text_diverged/modified bitmaps (host-only — restored RAM may match ref). */
+34 -1
View File
@@ -5,6 +5,11 @@
// PlayStation boot serial (from SYSTEM.CNF), derives the region from the
// serial prefix, and optionally compares against an expected serial / CRC32.
//
// Also mounts the resolved path via ISOReader to capture TOC geometry
// (track count, lead-out, per-track LBAs) and a stable disc_fp fingerprint
// used by netplay peer matching — data-track CRC alone cannot catch a
// Track-01-only mount vs a full Redump multi-track cue.
//
// Used both by the runtime's launch-time disc check (runtime/src/main.cpp)
// and by the shared launcher's "Disc verified" badge, so
// the two never drift apart.
@@ -36,9 +41,31 @@ struct DiscIdentity {
bool expected_crc_given = false;
bool crc_matches = false;
// Mount / TOC (ISOReader on resolve_disc_path().mount).
bool from_cue = false; // mount path is a .cue
bool cue_fallback = false; // resolver fell back from a broken cue to a bin
bool upgraded_to_cue = false; // caller picked bin; mounted owning cue
bool toc_opened = false; // ISOReader::Open succeeded on mount
int track_count = 0; // >= 1 when toc_opened
uint32_t leadout_lba = 0; // iso sector count (GetTD track 0)
std::string disc_fp; // lowercase hex SHA-256 of canonical TOC
// Netplay gate (filled when NetplayDiscExpect is applied).
bool netplay_ok = true;
std::string netplay_detail; // why netplay_ok is false
std::string detail; // human-readable note (failure reason or extra info)
};
// Optional title policy from game.toml [netplay] (0 / empty = do not check).
struct NetplayDiscExpect {
bool require_cue = false;
int required_tracks = 0; // exact iso_track_count; 0 = skip
bool has_required_leadout = false;
uint32_t required_leadout_lba = 0;
std::string required_disc_fp; // exact fingerprint; empty = skip
};
// Identify and (optionally) verify a disc image.
// path : a .cue file or a raw .bin/.iso image
// expected_serial : the game id, e.g. "SCUS-94236" ("" skips the serial check)
@@ -47,9 +74,15 @@ struct DiscIdentity {
// compute_crc : compute the full-file CRC32 (streamed; can be slow on big
// images — callers typically only request it when there is an
// expected CRC to compare against)
// netplay_expect : optional TOC / cue policy; nullptr skips netplay_ok checks
// (disc_fp / track_count are still computed when mount opens)
DiscIdentity identify_disc(const std::filesystem::path& path,
const std::string& expected_serial,
uint32_t expected_crc, bool has_expected_crc,
bool compute_crc);
bool compute_crc,
const NetplayDiscExpect* netplay_expect = nullptr);
// Apply / re-apply netplay mount policy onto an already-identified disc.
void apply_netplay_disc_expect(DiscIdentity& id, const NetplayDiscExpect& expect);
} // namespace PSXRecompV4
+2
View File
@@ -24,6 +24,8 @@ int mdec_dma_read_ready(void);
* widescreen present pins such frames to native 4:3 (FMVs are authored 4:3
* and get no GTE squash to compensate the stretch). */
int mdec_recently_active(uint32_t within_frames);
/* Guest cycles since last colour decode; UINT64_MAX if never / reset. */
uint64_t mdec_color_age_cycles(void);
typedef struct MDECDebugState {
uint32_t command;
+10
View File
@@ -103,6 +103,16 @@ void psx_lobby_pump(void);
void psx_lobby_set_game_identity(const char *game_name, const char *game_version);
const char *psx_lobby_game_version(void);
/*
* TOC fingerprint (lowercase hex SHA-256 from DiscIdentity::disc_fp).
* Sent on create/join so the lobby server can reject mismatched mounts
* (e.g. Track-01-only vs full multi-track cue). Empty disables the check
* on the server for that peer (legacy clients); modern clients always set it
* after a successful disc verify.
*/
void psx_lobby_set_disc_fp(const char *disc_fp);
const char *psx_lobby_disc_fp(void);
/* Default max_slots for create (clamped 2..8, default 2). */
void psx_lobby_set_max_slots(int max_slots);
+16 -3
View File
@@ -52,7 +52,7 @@ typedef struct PsxNetplayConfig {
int input_delay;
/* Rollback invent runway (phase_lock / P). Clamped 2..16. Unused in delay-sync. */
int input_prediction;
int force_input_relay; /* 1 = lobby UDP SFU star (online default) */
int force_input_relay; /* 1 = lobby UDP SFU star (from launch) */
int force_turn; /* 1 = ICE relay-only (Force TURN for UDP) */
/* 0 = auto (MotK room → ICE, else LAN), 1 = force ICE, 2 = force LAN.
* Env PSX_NET_TRANSPORT=lan|ice overrides. */
@@ -93,8 +93,10 @@ int psx_netplay_start(const PsxNetplayConfig *cfg);
void psx_netplay_shutdown(void);
/*
* Guest only: after savestate_configure + memcard_init, redirect .pst/.mcd
* writes to saves/netplay/ so host sync never touches personal saves.
* After savestate_configure + memcard_init: refresh RB integrity keys from
* savestate (host + guest — start() ran before configure), then guest-only
* redirect .pst/.mcd writes to saves/netplay/ so host sync never touches
* personal saves.
*/
void psx_netplay_bind_guest_saves(void);
@@ -247,6 +249,17 @@ void psx_netplay_on_rb_snap_loaded(void);
/* §64: restart hc-fork persist/retry clocks after tip-extend abandon so a
* second recovery episode cannot open on the same Live tick. */
void psx_netplay_hc_fork_recovery_restart(void);
/* §102: drop pending hc-fork bookkeeping (peer-ahead NACK already scheduled
* a light tip reopen — do not open a deep SPAN recovery). */
void psx_netplay_hc_fork_recovery_clear(void);
/* §93 P1 bisect: PSX_RB_CD_BISECT=1 (optional =LO-HI, default 150-250) logs
* per-tick CD/MDEC/clk crumbs + CD cmd ISSUE/QUEUE/DONE timeline (cdrom.c;
* includes stat/resp FIFO + GetTN tracks / GetTD td_lba).
* Arm N more ticks after a media flush_resume. */
void psx_netplay_cd_bisect_arm(uint32_t from_sim, uint32_t ticks);
/* 1 when CD bisect env is on and sim is in the fixed or post-media arm window. */
int psx_netplay_cd_bisect_active(void);
/* Netplay GPU lock: CPU-authoritative VRAM (software raster) so snaps/digests
* are peer-identical. If OpenGL was requested, keep the GL context for
+3
View File
@@ -101,6 +101,9 @@ int psx_netplay_rb_fmv_episode_unsafe(uint32_t tick);
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);
/* §100: 1 while a MEDIA-KF episode is waiting on probe/transfer/pin (sim
* stalled — present should hold-last so the intro FMV does not freeze). */
int psx_netplay_rb_media_kf_busy(void);
/* Mid-guest resim pump: abort if Replay has made no finish_frame progress.
* Full rb_pump stays admit/present-edge only (host-asymmetric). */
+257 -91
View File
@@ -16,7 +16,9 @@
#include "spu.h"
#include "event_ring.h"
#include "audio_trace.h"
#include "interrupts.h"
#include "psx_cycles.h"
#include "psx_netplay.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
@@ -116,7 +118,9 @@ static uint32_t cdrom_intc_latched_generation;
* while staying well under the second-response delays (10k-30k), so command
* throughput is unaffected. */
#define CDROM_IRQ_PRESENT_DELAY 5000
static int cdrom_irq_present_delay;
/* Absolute guest cycle when an armed CD response may be presented to INTC.
* 0 = no presentation hold. Relative remaining is derived for snaps/digests. */
static uint64_t cdrom_irq_present_due;
/* Parameter FIFO */
#define PARAM_FIFO_SIZE 16
@@ -285,9 +289,10 @@ int cdrom_fmv_stream_pending(void)
{
if (xa_stream_active)
return 1;
/* mode 0x40 = XA-ADPCM enable; 0x08 = sector size / related FMV setup.
* Match warm-route consumer (mode & 0x48). */
if (reading && (mode_reg & 0x48u))
/* FMV/STR preamble: Form2 (0x20) often arms before XA-ADPCM (0x40) /
* filter (0x08). MotK intro SetMode(0xa2)+ReadN was invisible here, so
* invent ran through the first double-speed Form2 sector race. */
if (reading && (mode_reg & 0x68u))
return 1;
return 0;
}
@@ -298,6 +303,8 @@ void cdrom_resync_deadlines_after_restore(void)
s_cd_timing_next_due = psx_cycle_count + (uint64_t)read_delay;
else if (!reading)
s_cd_timing_next_due = 0;
/* pending.due_cyc / cdrom_irq_present_due rebuilt in snap_parse from
* relative remaining (same guest clock as the restore). */
}
/* Response-overwrite diagnostics consumed by debug_server.c. master's CD
@@ -524,9 +531,11 @@ static int apply_speed(int delay) {
static int apply_read_speed(int delay) {
/* A route is an explicit DATA-read allowlist, never a blanket drive-speed
* change. XA filter/ADPCM modes are rejected before the first streaming
* sector can set xa_stream_active; established XA remains authentic too. */
if (xa_stream_active || (mode_reg & 0x48u)) return delay;
* change. Keep FMV/STR authentic: XA-ADPCM (0x40), filter (0x08), and
* Form2/0x924 (0x20) — MotK sets mode 0xa2 (Form2+2×) before the XA bit,
* and disc_speed=4x was compressing that preamble into a one-sector race
* (soak §93 P1 sim 184: rd≈112896 vs delivered). */
if (xa_stream_active || (mode_reg & 0x68u)) return delay;
if (s_warm_route_active) return warm_route_period();
return apply_speed(delay);
}
@@ -611,15 +620,58 @@ static int seek_complete_delay_cycles(void) {
return apply_speed(base);
}
/* Pending command */
/* Pending second-response command. due_cyc is an absolute guest deadline —
* the drive clock never freezes while irq_flag is set (ack only gates
* presentation). Relative remaining is derived for snaps/digests so wire
* format stays peer-stable across hosts. */
typedef struct {
uint8_t cmd;
int pending;
int delay;
int phase;
uint64_t due_cyc;
} PendingCmd;
static PendingCmd pending;
static int cycles_until_due(uint64_t due_cyc) {
if (due_cyc == 0 || psx_cycle_count >= due_cyc) return 0;
uint64_t rem = due_cyc - psx_cycle_count;
return rem > 0x7FFFFFFFull ? 0x7FFFFFFF : (int)rem;
}
static int pending_rem_cycles(void) {
if (!pending.pending) return 0;
return cycles_until_due(pending.due_cyc);
}
static int irq_present_rem_cycles(void) {
return cycles_until_due(cdrom_irq_present_due);
}
static void pending_arm(uint8_t cmd, int latency_cycles, int phase) {
if (latency_cycles < 0) latency_cycles = 0;
pending.cmd = cmd;
pending.pending = 1;
pending.phase = phase;
pending.due_cyc = psx_cycle_count + (uint64_t)latency_cycles;
}
static void pending_set_remaining(int rem) {
if (!pending.pending) {
pending.due_cyc = 0;
return;
}
if (rem < 0) rem = 0;
pending.due_cyc = psx_cycle_count + (uint64_t)rem;
}
static void irq_present_set_remaining(int rem) {
if (rem <= 0) {
cdrom_irq_present_due = 0;
return;
}
cdrom_irq_present_due = psx_cycle_count + (uint64_t)rem;
}
typedef struct {
uint8_t cmd;
uint8_t params[PARAM_FIFO_SIZE];
@@ -661,7 +713,7 @@ static void trace_cdrom(uint8_t kind, uint32_t addr, uint32_t val, uint8_t width
e->sector_size = sector_size;
e->pending_cmd = pending.cmd;
e->pending_pending = (uint8_t)pending.pending;
e->pending_delay = pending.delay;
e->pending_delay = pending_rem_cycles();
e->reading = (uint8_t)reading;
e->read_cmd = read_cmd;
e->read_delay = read_delay;
@@ -796,8 +848,9 @@ static void set_irq(int type) {
cdrom_intc_request_latched = 0;
/* Arm the presentation latency: this response must NOT be presented to INTC
* synchronously inside the guest store that triggered it (see the
* CDROM_IRQ_PRESENT_DELAY note). It is presented at a later cdrom_advance. */
cdrom_irq_present_delay = CDROM_IRQ_PRESENT_DELAY;
* CDROM_IRQ_PRESENT_DELAY note). Absolute due — not a slice-relative
* countdown (see pending.due_cyc). */
cdrom_irq_present_due = psx_cycle_count + (uint64_t)CDROM_IRQ_PRESENT_DELAY;
trace_cdrom('I', 0, (uint32_t)type, 0);
/* DEQUEUE: CD response/data event fired (aux = CD irq type). */
event_ring_record_aux(EV_DEQ, (uint8_t)SRC_CD_IRQ, (uint32_t)type);
@@ -819,7 +872,8 @@ static void present_cdrom_irq(void) {
* keeps a freshly-issued response from being presented synchronously inside
* the guest's command/ack write — and thus from being lost to that ISR's
* own trailing INTC ack. */
if (cdrom_irq_present_delay > 0) return;
if (cdrom_irq_present_due != 0 && psx_cycle_count < cdrom_irq_present_due)
return;
if (cdrom_irq_mask_matches_reason(irq_enable, irq_flag) &&
!cdrom_intc_request_latched) {
psx_irq_raise(2, irq_flag); /* IRQ_CDROM; detail = CD response/IRQ type */
@@ -1403,6 +1457,26 @@ static void start_read_stream(uint8_t cmd) {
* content load that happens in OFF but not ON shows up as a missing
* SRC_CD_READ enqueue here. */
event_ring_record_aux(EV_ENQ, (uint8_t)SRC_CD_READ, (uint32_t)read_delay);
{
static int s_bisect = -1;
if (s_bisect < 0) {
const char *e = getenv("PSX_RB_CD_BISECT");
s_bisect = (e && e[0] && e[0] != '0') ? 1 : 0;
}
if (s_bisect) {
fprintf(stderr,
"psxrecomp: rb cd-bisect ReadN-arm cyc=%llu csv=%u "
"rd0=%d mode=%02x div=%d cmd=%02x lba=%d "
"seek=%02d:%02d:%02d irq_f=%02x present_d=%d\n",
(unsigned long long)psx_cycle_count,
(unsigned)interrupts_get_cycles_since_vblank(),
read_delay, (unsigned)mode_reg, g_disc_speed_divisor,
(unsigned)cmd, msf_to_lba(read_min, read_sec, read_sect),
read_min, read_sec, read_sect,
(unsigned)irq_flag, irq_present_rem_cycles());
fflush(stderr);
}
}
}
static void stop_read_stream(void) {
@@ -1561,6 +1635,82 @@ static int deliver_read_sector_without_irq(void) {
return delivered;
}
/* §93 P1: cmd timeline under PSX_RB_CD_BISECT (window via netplay).
* ISSUE/DONE include controller stat + response FIFO (GetTN last-track BCD,
* GetTD MSF) so peers can diff TOC replies after matched command issue. */
static void cd_bisect_cmd_log(const char *kind, uint8_t cmd,
const uint8_t *params, int nparam)
{
char pbuf[3 * PARAM_FIFO_SIZE + 4];
char rbuf[3 * RESPONSE_FIFO_SIZE + 4];
int i, o = 0;
int pend_rem;
int seek_lba;
int rn;
int tracks = 0;
int td_lba = -1;
if (!psx_netplay_cd_bisect_active())
return;
if (nparam < 0) nparam = 0;
if (nparam > PARAM_FIFO_SIZE) nparam = PARAM_FIFO_SIZE;
pbuf[0] = '-';
pbuf[1] = '\0';
if (params && nparam > 0) {
for (i = 0; i < nparam && o + 3 < (int)sizeof(pbuf); i++) {
if (i) pbuf[o++] = ',';
o += sprintf(pbuf + o, "%02x", (unsigned)params[i]);
}
pbuf[o] = '\0';
}
rn = response_count;
if (rn < 0) rn = 0;
if (rn > RESPONSE_FIFO_SIZE) rn = RESPONSE_FIFO_SIZE;
rbuf[0] = '-';
rbuf[1] = '\0';
o = 0;
if (rn > 0) {
for (i = 0; i < rn && o + 3 < (int)sizeof(rbuf); i++) {
if (i) rbuf[o++] = ',';
o += sprintf(rbuf + o, "%02x", (unsigned)response_fifo[i]);
}
rbuf[o] = '\0';
}
pend_rem = pending_rem_cycles();
seek_lba = msf_to_lba(seek_min, seek_sec, seek_sect);
/* TOC helpers: binary track count / resolved GetTD LBA (mount-side). */
if (cmd == 0x13u)
tracks = iso_handle ? iso_track_count(iso_handle) : 0;
else if (cmd == 0x14u && params && nparam >= 1) {
int raw = (int)params[0];
int track = bcd_to_bin(raw);
if (raw == 0xAA || track == 0) {
uint32_t sectors = iso_handle ? iso_sector_count(iso_handle) : 0u;
td_lba = sectors ? (int)sectors : 0;
} else {
td_lba = iso_handle ? (int)iso_track_start_lba(iso_handle, track) : 0;
}
}
fprintf(stderr,
"psxrecomp: rb cd-cmd %s sim=%u cyc=%llu csv=%u cmd=%02x "
"n=%d params=%s stat=%02x rn=%d resp=%s irq_f=%02x mode=%02x "
"reading=%d pend=%d/%d pend_cmd=%02x seek=%02d:%02d:%02d lba=%d "
"rMSF=%02d:%02d:%02d tracks=%d td_lba=%d resim=%d\n",
kind,
(unsigned)psx_netplay_sim_tick(),
(unsigned long long)psx_cycle_count,
(unsigned)interrupts_get_cycles_since_vblank(),
(unsigned)cmd, nparam, pbuf,
(unsigned)stat_reg, rn, rbuf,
(unsigned)irq_flag, (unsigned)mode_reg, reading ? 1 : 0,
pending.pending ? 1 : 0, pend_rem,
pending.pending ? (unsigned)pending.cmd : 0u,
seek_min, seek_sec, seek_sect, seek_lba,
read_min, read_sec, read_sect,
tracks, td_lba,
psx_netplay_is_resimulating() ? 1 : 0);
fflush(stderr);
}
static void try_execute_queued_command(void) {
if (!queued_cmd.pending || irq_flag != 0) return;
@@ -1595,6 +1745,7 @@ static void queue_or_exec_command(uint8_t cmd) {
param_count = 0;
trace_cdrom('Q', 0, cmd, 0);
record_command_history('Q', cmd, queued_cmd.params, queued_cmd.param_count);
cd_bisect_cmd_log("QUEUE", cmd, queued_cmd.params, queued_cmd.param_count);
}
/* Pause (0x09) completion latency — Beetle PS_CDC::Command_Pause.
@@ -1713,12 +1864,12 @@ static void exec_command(uint8_t cmd) {
}
response_push(stat_reg);
set_irq(CDIRQ_ACK);
pending.cmd = 0x07;
pending.pending = 1;
pending.delay = apply_speed(30000); /* motor spin-up */
s_cd_probe_motor_count++;
s_cd_probe_motor_cycles += (uint64_t)pending.delay;
pending.phase = 1;
{
int lat = apply_speed(30000); /* motor spin-up */
pending_arm(0x07, lat, 1);
s_cd_probe_motor_count++;
s_cd_probe_motor_cycles += (uint64_t)lat;
}
break;
case 0x08: /* Stop — stop the motor. Two-phase like Pause: INT3 (ACK) now
@@ -1738,12 +1889,12 @@ static void exec_command(uint8_t cmd) {
stat_reg &= ~(CDSTAT_READ | CDSTAT_PLAY | CDSTAT_SEEK);
response_push(stat_reg);
set_irq(CDIRQ_ACK);
pending.cmd = 0x08;
pending.pending = 1;
pending.delay = apply_speed(30000); /* motor spin-down */
s_cd_probe_stop_count++;
s_cd_probe_stop_cycles += (uint64_t)pending.delay;
pending.phase = 1;
{
int lat = apply_speed(30000); /* motor spin-down */
pending_arm(0x08, lat, 1);
s_cd_probe_stop_count++;
s_cd_probe_stop_cycles += (uint64_t)lat;
}
break;
case 0x09: { /* Pause */
@@ -1755,12 +1906,9 @@ static void exec_command(uint8_t cmd) {
stat_reg &= ~(CDSTAT_READ | CDSTAT_PLAY);
response_push(stat_reg);
set_irq(CDIRQ_ACK);
pending.cmd = 0x09;
pending.pending = 1;
pending.delay = complete_delay;
pending_arm(0x09, complete_delay, 1);
s_cd_probe_pause_count++;
s_cd_probe_pause_cycles += (uint64_t)complete_delay;
pending.phase = 1;
break;
}
@@ -1772,8 +1920,6 @@ static void exec_command(uint8_t cmd) {
stat_reg = has_disc() ? CDSTAT_MOTOR : CDSTAT_SHELL;
response_push(stat_reg);
set_irq(CDIRQ_ACK);
pending.cmd = 0x0A;
pending.pending = 1;
/* The old 1136000-cycle (34 ms) value was borrowed from Beetle's
* Command_RESET — a different command. Init(0x0A) on an already-
* spinning drive completes far faster on real hardware, and OpenBIOS
@@ -1786,8 +1932,7 @@ static void exec_command(uint8_t cmd) {
* two, not a measured constant) sits inside OpenBIOS's window and
* within real-hardware quick-init behavior; cross-check against
* Beetle's exact PS_CDC::Command_Init figure when refining. */
pending.delay = 131072;
pending.phase = 1;
pending_arm(0x0A, 131072, 1);
break;
case 0x0B: /* Mute */
@@ -1975,12 +2120,12 @@ static void exec_command(uint8_t cmd) {
stat_reg |= CDSTAT_SEEK;
response_push(stat_reg);
set_irq(CDIRQ_ACK);
pending.cmd = cmd; /* 0x15 or 0x16 — completed in process_pending */
pending.pending = 1;
pending.delay = seek_complete_delay_cycles();
s_cd_probe_seek_count++;
s_cd_probe_seek_cycles += (uint64_t)pending.delay;
pending.phase = 1;
{
int lat = seek_complete_delay_cycles();
pending_arm(cmd, lat, 1); /* 0x15/0x16 — completed in process_pending */
s_cd_probe_seek_count++;
s_cd_probe_seek_cycles += (uint64_t)lat;
}
break;
case 0x1A: /* GetID */
@@ -1989,12 +2134,9 @@ static void exec_command(uint8_t cmd) {
* or INT5 (ERROR) if no disc / lid open. */
response_push(stat_reg);
set_irq(CDIRQ_ACK);
pending.cmd = 0x1A;
pending.pending = 1;
/* Beetle PS_CDC::Command_ID: second response after 33868 cycles.
* Unscaled — same authentic-latency class as Pause/Init/ReadTOC. */
pending.delay = 33868;
pending.phase = 1;
pending_arm(0x1A, 33868, 1);
break;
case 0x1B: /* ReadS */
@@ -2011,13 +2153,10 @@ static void exec_command(uint8_t cmd) {
case 0x1E: /* ReadTOC */
response_push(stat_reg);
set_irq(CDIRQ_ACK);
pending.cmd = 0x1E;
pending.pending = 1;
/* Beetle PS_CDC::Command_ReadTOC: ~30M cycles (a near-second TOC
* re-scan; Beetle adds a seek term on top — we keep the dominant
* constant). Unscaled — authentic-latency class. */
pending.delay = 30000000;
pending.phase = 1;
pending_arm(0x1E, 30000000, 1);
break;
case 0x19: /* Test */
@@ -2054,24 +2193,27 @@ static void exec_command(uint8_t cmd) {
* Delayed responses (pending) fire from process_pending(). */
fire_cdrom_irq();
record_command_history('C', cmd, cmd_params, cmd_param_count);
cd_bisect_cmd_log("ISSUE", cmd, cmd_params, cmd_param_count);
}
static void process_pending(uint32_t cycles) {
uint8_t done_cmd;
(void)cycles;
if (!pending.pending) return;
if (cycles == 0) return;
/* Serialized responses: a delayed completion (e.g. SeekL / Init COMPLETE)
* cannot be presented while a prior response IRQ is still unacked. Freeze
* pending.delay while held so it does not accrue fake time debt and
* underflow once the guest finally acks (mirrors process_read_stream). */
/* Absolute drive deadline: due_cyc advances with guest time regardless of
* slice quantum or irq_flag. Ack only gates *presentation* — do not push
* due_cyc forward while the response FIFO is busy (old relative freeze
* under/over-counted mid-slice acks and forked Pause→Seek→ReadN). */
if (psx_cycle_count < pending.due_cyc) return;
if (irq_flag != 0) return;
pending.delay -= (int)cycles;
if (pending.delay > 0) return;
done_cmd = pending.cmd;
pending.pending = 0;
pending.due_cyc = 0;
response_clear();
switch (pending.cmd) {
switch (done_cmd) {
case 0x06: /* ReadN data ready */
case 0x1B: /* ReadS data ready */
if (reading) {
@@ -2150,6 +2292,7 @@ static void process_pending(uint32_t cycles) {
default:
break;
}
cd_bisect_cmd_log("DONE", done_cmd, NULL, 0);
}
/* Read-stream hold accounting (always-on). Before 2026-07-10 the stream
@@ -2273,7 +2416,7 @@ void cdrom_init(const char* cue_path) {
cdrom_intc_request_latched = 0;
cdrom_irq_generation = 0;
cdrom_intc_latched_generation = 0;
cdrom_irq_present_delay = 0;
cdrom_irq_present_due = 0;
param_count = 0;
response_read = 0;
response_count = 0;
@@ -2300,9 +2443,17 @@ void cdrom_init(const char* cue_path) {
cd_muted = 0;
xa_reset_decode();
spu_cd_audio_reset();
pending.pending = 0;
/* Rematch: pending.cmd/delay/phase and read MSF used to survive with
* pending.pending=0 / reading=0 and fork the CD digest at dig0. */
memset(&pending, 0, sizeof(pending));
memset(&queued_cmd, 0, sizeof(queued_cmd));
cdrom_clear_pending_dataready();
read_min = read_sec = read_sect = 0;
reading = 0;
read_cmd = 0;
read_delay = 0;
seek_min = seek_sec = seek_sect = 0;
s_setloc_lba = -1;
setloc_seek_far = 0;
s_warm_routes_count = 0;
s_warm_route_configured = 0;
@@ -2454,9 +2605,10 @@ void cdrom_write(uint32_t addr, uint32_t value) {
param_count = 0;
}
/* A fully-acked INT presents any pended data-ready first
* (Beetle CheckAIP on IRQ-register clear); a queued command
* then waits behind that INT1's own ack cycle. */
* (Beetle CheckAIP on IRQ-register clear); then a second-response
* that is already past due_cyc; a queued command waits behind. */
present_pending_dataready();
process_pending(0);
try_execute_queued_command();
}
break;
@@ -2475,14 +2627,23 @@ void cdrom_write(uint32_t addr, uint32_t value) {
uint32_t cdrom_cycles_to_irq(uint32_t i_mask) {
if (!(i_mask & (1u << 2))) return 0xFFFFFFFFu; /* IRQ_CDROM masked */
uint32_t best = 0xFFFFFFFFu;
/* Armed response awaiting presentation (will raise bit2 when delay hits 0). */
/* Armed response awaiting presentation (raises bit2 at present_due). */
if (cdrom_irq_mask_matches_reason(irq_enable, irq_flag)) {
uint32_t d = cdrom_irq_present_delay > 0 ? (uint32_t)cdrom_irq_present_delay : 0u;
int rem = irq_present_rem_cycles();
uint32_t d = rem > 0 ? (uint32_t)rem : 0u;
if (d < best) best = d;
}
/* Pending second response: fires set_irq in pending.delay cycles. */
if (pending.pending && pending.delay > 0 && (uint32_t)pending.delay < best)
best = (uint32_t)pending.delay;
/* Pending second response: slice to due_cyc while the drive clock runs.
* Once due, presentation waits on irq_flag clear (CPU ack) — do not
* advertise a deliverable CD IRQ until the FIFO is free. */
if (pending.pending) {
if (psx_cycle_count < pending.due_cyc) {
uint32_t d = (uint32_t)cycles_until_due(pending.due_cyc);
if (d < best) best = d;
} else if (irq_flag == 0 && 0u < best) {
best = 0u;
}
}
/* Active sector read: next data-ready in read_delay cycles. */
if (reading && !warm_route_consumer_blocked() &&
read_delay > 0 && (uint32_t)read_delay < best)
@@ -2491,21 +2652,13 @@ uint32_t cdrom_cycles_to_irq(uint32_t i_mask) {
}
void cdrom_advance(uint32_t cycles) {
/* Age the response presentation latency. Once it elapses, the following
* refresh_cdrom_irq_line() presents the held response to INTC — at this
* cdrom_advance boundary, not synchronously inside the guest store that
* armed it.
*
* Note: do NOT freeze CD during rollback Replay. MotK FMV skip resim
/* Presentation hold is absolute (cdrom_irq_present_due); refresh checks
* it. Note: do NOT freeze CD during rollback Replay. MotK FMV skip resim
* resumes into VLC/sector waits; frozen IRQs → no finish_frame hang.
* Menu CD asymmetry is contained by no-invent during media + core POST. */
if (cdrom_irq_present_delay > 0) {
cdrom_irq_present_delay -= (int)cycles;
if (cdrom_irq_present_delay < 0) cdrom_irq_present_delay = 0;
}
refresh_cdrom_irq_line();
try_execute_queued_command();
process_pending(cycles);
try_execute_queued_command();
process_read_stream(cycles);
process_cdda_stream(cycles);
refresh_cdrom_irq_line();
@@ -2594,13 +2747,15 @@ void cdrom_debug_snapshot(CDROMDebugState* out) {
out->filter_channel = filter_channel;
out->muted = cd_muted;
out->read_delay = read_delay;
out->irq_present_delay = irq_present_rem_cycles();
out->speed_divisor = g_disc_speed_divisor;
out->read_hold_cycles = s_read_hold_cycles;
out->read_hold_events = s_read_hold_events;
out->int1_pended = s_int1_pended;
out->int1_lost = s_int1_lost;
out->int1_pending_now = pending_dataready;
out->pending_pending = pending.pending;
out->pending_delay = pending.delay;
out->pending_delay = pending_rem_cycles();
out->pending_phase = pending.phase;
out->i_stat = i_stat;
out->last_sector_lba = last_sector_lba;
@@ -2694,13 +2849,16 @@ int cdrom_data_read_active(void) {
static int s_savestate_cd_boost_vblanks = 0;
static void cdrom_apply_savestate_delay_caps(int log_clamps) {
if (pending.pending && pending.delay > CDROM_SAVESTATE_DELAY_CAP) {
if (log_clamps)
fprintf(stderr,
"cdrom: savestate clamped pending cmd=0x%02X delay %d -> %d\n",
(unsigned)pending.cmd, pending.delay,
CDROM_SAVESTATE_DELAY_CAP);
pending.delay = CDROM_SAVESTATE_DELAY_CAP;
if (pending.pending && psx_cycle_count < pending.due_cyc) {
int rem = pending_rem_cycles();
if (rem > CDROM_SAVESTATE_DELAY_CAP) {
if (log_clamps)
fprintf(stderr,
"cdrom: savestate clamped pending cmd=0x%02X delay %d -> %d\n",
(unsigned)pending.cmd, rem,
CDROM_SAVESTATE_DELAY_CAP);
pending_set_remaining(CDROM_SAVESTATE_DELAY_CAP);
}
}
if (reading && !xa_stream_active &&
read_delay > CDROM_SAVESTATE_DELAY_CAP) {
@@ -2711,8 +2869,8 @@ static void cdrom_apply_savestate_delay_caps(int log_clamps) {
read_delay = CDROM_SAVESTATE_DELAY_CAP;
s_cd_timing_next_due = psx_cycle_count + (uint64_t)read_delay;
}
if (cdrom_irq_present_delay > CDROM_SAVESTATE_DELAY_CAP)
cdrom_irq_present_delay = CDROM_SAVESTATE_DELAY_CAP;
if (irq_present_rem_cycles() > CDROM_SAVESTATE_DELAY_CAP)
irq_present_set_remaining(CDROM_SAVESTATE_DELAY_CAP);
}
void cdrom_accelerate_after_savestate(void) {
@@ -2734,7 +2892,7 @@ void cdrom_savestate_boost_vblank(void) {
int cdrom_savestate_cd_wait_active(void) {
if (s_savestate_cd_boost_vblanks <= 0) return 0;
if (pending.pending && pending.delay > 0) return 1;
if (pending.pending && pending_rem_cycles() > 0) return 1;
if (reading && !xa_stream_active) return 1;
return 0;
}
@@ -2761,7 +2919,7 @@ static int cdrom_snap_emit(PstW *w) {
#define WB(a) do { if (!pst_w_bytes(w, (a), sizeof(a))) return 0; } while (0)
W8(index_reg); W8(stat_reg); W8(request_reg); W8(irq_enable); W8(irq_flag);
WI(cdrom_intc_request_latched); WU(cdrom_irq_generation);
WU(cdrom_intc_latched_generation); WI(cdrom_irq_present_delay);
WU(cdrom_intc_latched_generation); WI(irq_present_rem_cycles());
WB(param_fifo); WI(param_count);
WB(response_fifo); WI(response_read); WI(response_count);
WB(sector_buffer); WI(sector_read_pos); WI(sector_available); WI(sector_size);
@@ -2781,8 +2939,9 @@ static int cdrom_snap_emit(PstW *w) {
return 0;
W8(xa_stream_file); W8(xa_stream_channel); W8(xa_stream_coding); WI(xa_stream_active);
WI(g_disc_speed_divisor); WI(g_game_divisor); WI(g_instant_max_per_frame);
/* PendingCmd / QueuedCmd — no host padding on the wire */
W8(pending.cmd); WI(pending.pending); WI(pending.delay); WI(pending.phase);
/* PendingCmd / QueuedCmd — relative remaining on the wire (absolute due
* rebuilt on load from psx_cycle_count). */
W8(pending.cmd); WI(pending.pending); WI(pending_rem_cycles()); WI(pending.phase);
W8(queued_cmd.cmd); WB(queued_cmd.params); WI(queued_cmd.param_count); WI(queued_cmd.pending);
W8(pending_dataready); W8(pending_dataready_stat);
#undef W8
@@ -2798,6 +2957,8 @@ static int cdrom_snap_parse(PstR *r) {
int32_t i;
uint32_t u;
uint64_t u64;
int present_rem = 0;
int pending_rem = 0;
#define R8(f) do { if (!pst_r_u8(r, &b)) return 0; (f) = b; } while (0)
#define RI(f) do { if (!pst_r_i32(r, &i)) return 0; (f) = (int)i; } while (0)
#define RU(f) do { if (!pst_r_u32(r, &u)) return 0; (f) = u; } while (0)
@@ -2805,7 +2966,7 @@ static int cdrom_snap_parse(PstR *r) {
#define RB(a) do { if (!pst_r_bytes(r, (a), sizeof(a))) return 0; } while (0)
R8(index_reg); R8(stat_reg); R8(request_reg); R8(irq_enable); R8(irq_flag);
RI(cdrom_intc_request_latched); RU(cdrom_irq_generation);
RU(cdrom_intc_latched_generation); RI(cdrom_irq_present_delay);
RU(cdrom_intc_latched_generation); RI(present_rem);
RB(param_fifo); RI(param_count);
RB(response_fifo); RI(response_read); RI(response_count);
RB(sector_buffer); RI(sector_read_pos); RI(sector_available); RI(sector_size);
@@ -2823,7 +2984,7 @@ static int cdrom_snap_parse(PstR *r) {
return 0;
R8(xa_stream_file); R8(xa_stream_channel); R8(xa_stream_coding); RI(xa_stream_active);
RI(g_disc_speed_divisor); RI(g_game_divisor); RI(g_instant_max_per_frame);
R8(pending.cmd); RI(pending.pending); RI(pending.delay); RI(pending.phase);
R8(pending.cmd); RI(pending.pending); RI(pending_rem); RI(pending.phase);
R8(queued_cmd.cmd); RB(queued_cmd.params); RI(queued_cmd.param_count); RI(queued_cmd.pending);
R8(pending_dataready); R8(pending_dataready_stat);
#undef R8
@@ -2831,6 +2992,11 @@ static int cdrom_snap_parse(PstR *r) {
#undef RU
#undef R64
#undef RB
irq_present_set_remaining(present_rem);
if (pending.pending)
pending_set_remaining(pending_rem);
else
pending.due_cyc = 0;
return 1;
}
+106 -1
View File
@@ -8,9 +8,12 @@
#include "crc32.h"
#include "disc_path.h"
#include "iso_reader.h"
#include "psx_sha256.h"
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <sstream>
@@ -112,12 +115,103 @@ uint32_t crc32_stream(std::ifstream& f) {
return crc ^ 0xFFFFFFFFu;
}
std::string hex_sha256(const std::string& canonical) {
uint8_t dig[32];
psx_sha256_compute(reinterpret_cast<const uint8_t*>(canonical.data()),
canonical.size(), dig);
char hex[65];
for (int i = 0; i < 32; i++)
std::snprintf(hex + i * 2, 3, "%02x", dig[i]);
hex[64] = '\0';
return std::string(hex);
}
void fill_toc_from_mount(DiscIdentity& v, const DiscPathResolution& resolved) {
v.from_cue = resolved.from_cue;
v.cue_fallback = resolved.cue_fallback;
v.upgraded_to_cue = resolved.upgraded_to_cue;
PS1::ISOReader reader;
if (!reader.Open(resolved.mount.string())) {
v.toc_opened = false;
if (v.detail.empty())
v.detail = "Could not mount the disc image for TOC inspection "
"(missing track FILE or unreadable cue).";
return;
}
v.toc_opened = true;
v.track_count = reader.TrackCount();
if (v.track_count < 1) v.track_count = 1;
v.leadout_lba = reader.GetSectorCount();
// Canonical TOC string — peers with the same mount geometry share disc_fp.
// Version prefix lets us evolve the format without silent collisions.
std::ostringstream toc;
toc << "psxrecomp-toc-v1\n";
toc << "tracks=" << v.track_count << "\n";
toc << "leadout=" << v.leadout_lba << "\n";
for (int t = 1; t <= v.track_count; t++) {
toc << "t=" << t
<< ",a=" << (reader.TrackIsAudio(t) ? 1 : 0)
<< ",start=" << reader.TrackStartLBA(t)
<< ",pregap=" << reader.TrackPregapLBA(t) << "\n";
}
v.disc_fp = hex_sha256(toc.str());
}
} // namespace
void apply_netplay_disc_expect(DiscIdentity& id, const NetplayDiscExpect& expect) {
id.netplay_ok = true;
id.netplay_detail.clear();
if (!id.toc_opened) {
id.netplay_ok = false;
id.netplay_detail = "Disc TOC could not be opened for netplay.";
return;
}
if (expect.require_cue) {
if (id.cue_fallback || !id.from_cue) {
id.netplay_ok = false;
id.netplay_detail =
"Netplay requires the full .cue (with all track files), "
"not a bare .bin / incomplete dump.";
return;
}
}
if (expect.required_tracks > 0 && id.track_count != expect.required_tracks) {
id.netplay_ok = false;
char buf[160];
std::snprintf(buf, sizeof(buf),
"Disc has %d track(s); netplay requires %d "
"(use the supported multi-track .cue dump).",
id.track_count, expect.required_tracks);
id.netplay_detail = buf;
return;
}
if (expect.has_required_leadout && id.leadout_lba != expect.required_leadout_lba) {
id.netplay_ok = false;
char buf[160];
std::snprintf(buf, sizeof(buf),
"Disc lead-out LBA %u != required %u.",
(unsigned)id.leadout_lba,
(unsigned)expect.required_leadout_lba);
id.netplay_detail = buf;
return;
}
if (!expect.required_disc_fp.empty() && id.disc_fp != expect.required_disc_fp) {
id.netplay_ok = false;
id.netplay_detail = "Disc TOC fingerprint does not match this build's "
"required dump.";
return;
}
}
DiscIdentity identify_disc(const fs::path& path,
const std::string& expected_serial,
uint32_t expected_crc, bool has_expected_crc,
bool compute_crc) {
bool compute_crc,
const NetplayDiscExpect* netplay_expect) {
DiscIdentity v;
v.expected_serial_given = !expected_serial.empty();
v.expected_crc_given = has_expected_crc;
@@ -126,12 +220,18 @@ DiscIdentity identify_disc(const fs::path& path,
// the data track no matter which member of the dump the user handed us —
// the .cue and its .bin must produce the same verdict, serial and CRC32.
const DiscPathResolution resolved = resolve_disc_path(path);
fill_toc_from_mount(v, resolved);
const fs::path data_path = resolved.data;
std::ifstream f(data_path, std::ios::binary | std::ios::ate);
if (!f.is_open()) {
v.detail = resolved.note.empty()
? "Could not open the disc image or its CUE-referenced BIN file."
: resolved.note;
if (netplay_expect)
apply_netplay_disc_expect(v, *netplay_expect);
else if (!v.toc_opened)
v.netplay_ok = false;
return v;
}
v.opened = true;
@@ -151,6 +251,8 @@ DiscIdentity identify_disc(const fs::path& path,
v.detail = "No ISO9660 CD001 header was found at the standard PS1 disc locations.";
// Region may still be inferable from the expected serial for display.
v.region = region_from_serial(expected_serial);
if (netplay_expect)
apply_netplay_disc_expect(v, *netplay_expect);
return v;
}
@@ -200,6 +302,9 @@ DiscIdentity identify_disc(const fs::path& path,
if (has_expected_crc) v.crc_matches = (v.crc == expected_crc);
}
if (netplay_expect)
apply_netplay_disc_expect(v, *netplay_expect);
return v;
}
+16 -1
View File
@@ -2263,12 +2263,27 @@ static void gpu_reset_state(int clear_vram) {
}
gr_init(vram);
/* Reset GP0 state machine */
/* Reset GP0 state machine (+ leftover cmd/xfer crumbs that survive in
* gpu_snapshot and fork av digests on rematch). */
gp0_state = GP0_IDLE;
gp0_words_collected = 0;
gp0_words_needed = 0;
memset(gp0_cmd_buf, 0, sizeof(gp0_cmd_buf));
gp0_next_source_addr = 0xFFFFFFFFu;
gp0_cmd_source_addr = 0xFFFFFFFFu;
polyline_color = 0;
polyline_prev_x = polyline_prev_y = 0;
polyline_prev_c = 0;
polyline_semi_trans = 0;
polyline_has_prev = 0;
vram_write_x = vram_write_y = 0;
vram_write_w = vram_write_h = 0;
vram_write_col = vram_write_row = 0;
vram_write_remaining = 0;
vram_read_active = 0;
vram_read_x = vram_read_y = 0;
vram_read_w = vram_read_h = 0;
vram_read_col = vram_read_row = 0;
/* Reset all state to power-on defaults */
texpage_x = 0;
+105 -14
View File
@@ -3996,8 +3996,13 @@ static void netplay_barrier_admit(int override) {
if (psx_return_to_lobby_requested()) return;
/* §35: TipHold invent-cap stall freezes guest (no vblank present).
* Keep Swap alive on the last Live frame so SAFETY/held waits do not
* open a ~250ms present gap. */
if (psx_netplay_rb_tip_holding())
* open a ~250ms present gap.
* §100: MEDIA-KF probe/xfer (~3.7 MB) also parks sim in admit — hold
* last FMV frame so intro does not freeze for the transfer window. */
if (psx_netplay_rb_tip_holding() ||
psx_netplay_rb_media_kf_busy() ||
(psx_netplay_rb_fmv_media_active() && psx_netplay_rb_active() &&
!psx_netplay_rb_is_resimulating()))
netplay_hold_last_present_tick();
/* Wake on peer UDP (or 1ms timeout). SDL_Delay(1) under dual FMV load
* often stretches multi-ms and cut MotK netplay intro ~59→~36; Delay(0)
@@ -4925,6 +4930,14 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
uint32_t sim = psx_netplay_sim_tick();
int beyond_shown = sim != 0u && sim != 0xffffffffu &&
sim > s_netplay_present_sim_watermark;
/* §99: FMV media Replay is CD/MDEC-bound — skip dual-raster present
* entirely (hold-last). Live FMV present + catchup gate still apply
* once media ends; near-tip media loads are short enough that a frozen
* frame for a few resim ticks is preferable to 1–3s GL hitch. */
if (psx_netplay_rb_fmv_media_active()) {
netplay_hold_last_present_tick();
return ep;
}
if (!beyond_shown && !netplay_replay_catchup_should_live_present(rem)) {
netplay_hold_last_present_tick();
return ep;
@@ -5349,6 +5362,12 @@ static void sdl_vblank_present(void) {
latency_ring_mark(LAT_PACED);
}
/* game.toml [netplay] + last TOC fingerprint — shared by launcher verify and
* the pre-psx_netplay_start gate (works with or without RECOMP_LAUNCHER). */
static PSXRecompV4::NetplayDiscExpect g_netplay_disc_expect{};
static std::string g_session_disc_fp;
static bool g_session_netplay_disc_ok = false;
#if defined(RECOMP_LAUNCHER)
// Host verification/inspection callbacks for the shared recomp-ui launcher.
// The launcher re-runs these on every disc/memory-card change so the "Disc
@@ -5526,18 +5545,30 @@ namespace {
int ae_disc_verify(const char* disc_path, RecompLauncherCDiscVerify* out) {
if (!disc_path || !disc_path[0] || !out) return 0;
std::memset(out, 0, sizeof(*out));
PSXRecompV4::DiscIdentity id = PSXRecompV4::identify_disc(
disc_path, g_lnch_expected_serial, g_lnch_expected_crc,
g_lnch_has_crc, /*compute_crc*/ g_lnch_has_crc);
g_lnch_has_crc, /*compute_crc*/ g_lnch_has_crc,
&g_netplay_disc_expect);
const std::string& serial = !id.detected_serial.empty()
? id.detected_serial : g_lnch_expected_serial;
std::snprintf(out->serial, sizeof(out->serial), "%s", serial.c_str());
std::snprintf(out->region, sizeof(out->region), "%s", id.region.c_str());
out->iso_ok = id.has_header ? 1 : 0;
// Verdict shown by the launcher.
out->track_count = id.track_count;
out->netplay_ok = id.netplay_ok ? 1 : 0;
std::snprintf(out->disc_fp, sizeof(out->disc_fp), "%s", id.disc_fp.c_str());
std::snprintf(out->netplay_detail, sizeof(out->netplay_detail), "%s",
id.netplay_detail.c_str());
g_session_disc_fp = id.disc_fp;
g_session_netplay_disc_ok = id.netplay_ok && !id.disc_fp.empty();
psx_lobby_set_disc_fp(id.disc_fp.c_str());
// Verdict shown by the launcher. TOC / [netplay] failures are warn so
// offline Play still works; online is gated by netplay_ok + disc_fp.
if (!id.opened || !id.has_header) out->verdict = 3; // bad
else if (id.expected_serial_given && !id.serial_matches) out->verdict = 3; // wrong disc
else if (id.expected_crc_given && id.crc_computed && !id.crc_matches) out->verdict = 2; // warn
else if (!id.netplay_ok) out->verdict = 2; // TOC/cue
else out->verdict = 1; // ok
return 1;
}
@@ -5559,10 +5590,10 @@ namespace {
RecompLauncherCNetplayLaunch g_lnch_pending_direct_launch{};
int g_lnch_lobby_input_delay = 2;
int g_lnch_lobby_input_prediction = 4;
/* Online default: lobby UDP SFU star (server always opens relay on start). */
int g_lnch_force_input_relay = 1;
/* Default on: CGNAT-safe relay-only ICE (BattleShip-style online path). */
int g_lnch_force_turn = 1;
/* Offline default off — waiting-room ICE probes host/srflx so the lobby
* server can choose ice_p2p vs SFU. Force-relay / Force TURN still override. */
int g_lnch_force_input_relay = 0;
int g_lnch_force_turn = 0;
/* Lobby default on; host “Disable Rollback” clears this → delay_sync. */
int g_lnch_rollback = 1;
int g_lnch_host_max_slots = 2;
@@ -6843,6 +6874,7 @@ namespace {
int ae_np_connect(void*) {
psx_lobby_set_game_identity(g_lnch_netplay_game_name.c_str(), PSX_GAME_VERSION);
psx_lobby_set_disc_fp(g_session_disc_fp.c_str());
psx_lobby_set_max_slots(g_lnch_game_players);
return psx_lobby_connect(ae_np_default_url(nullptr));
}
@@ -8012,11 +8044,26 @@ namespace {
if (seated > out->max_slots) seated = out->max_slots;
out->player_count = seated;
}
/* Online WS lobbies always use lobby UDP SFU; LAN/direct stay local. */
/* SFU when launch rewrote caps.force_input_relay (relay_endpoint);
* ice_p2p / LAN leave it clear. Also detect equal host/guest
* endpoints (server SFU advertise) when the caps bit was omitted —
* session 151 Desktop guest dialed the relay hostname without
* force_input_relay and died in resolve_use_ice (-4). */
out->force_input_relay =
(g_lnch_hosting_lan || g_lnch_joined_lan) ? 0 : 1;
out->force_turn =
(caps && caps->valid && caps->force_turn) ? 1 : 0;
(g_lnch_hosting_lan || g_lnch_joined_lan)
? 0
: (caps->force_input_relay ? 1 : 0);
if (!out->force_input_relay && !g_lnch_hosting_lan &&
!g_lnch_joined_lan && ji->host_endpoint[0] &&
ji->guest_endpoint[0] &&
std::strcmp(ji->host_endpoint, ji->guest_endpoint) == 0) {
out->force_input_relay = 1;
std::fprintf(stderr,
"psxrecomp: launch force_input_relay inferred "
"(host_endpoint==guest_endpoint=%s)\n",
ji->host_endpoint);
}
out->force_turn = caps->force_turn ? 1 : 0;
out->rollback =
(caps && caps->valid && caps->rollback) ? 1 : 0;
return 1;
@@ -8311,6 +8358,13 @@ std::string player_device[PSX_MAX_PLAYERS];
g_offline_pad_count = PSX_MAX_PLAYERS;
game_has_disc_crc = gc.has_disc_crc;
game_disc_crc = gc.disc_crc;
g_netplay_disc_expect.require_cue = gc.netplay_require_cue;
g_netplay_disc_expect.required_tracks = gc.netplay_required_tracks;
g_netplay_disc_expect.has_required_leadout =
gc.has_netplay_required_leadout;
g_netplay_disc_expect.required_leadout_lba =
gc.netplay_required_leadout_lba;
g_netplay_disc_expect.required_disc_fp = gc.netplay_required_disc_fp;
if (!gc.discs.empty()) resolved_disc = gc.discs.front();
if (gc.runtime.has_memcard_dir) memcard_dir = gc.runtime.memcard_dir;
if (gc.runtime.has_window_title) window_title = gc.runtime.window_title;
@@ -10055,6 +10109,43 @@ session_reboot:
* Must start after SDL so local pad capture has devices; before the guest
* fiber so the first vblanks already pump HELLO/START. */
if (net_cfg.enabled) {
/* Refuse mismatched / incomplete mounts before opening transport.
* Launcher already gates create/join; this covers CLI --netplay and
* any path that skipped the UI verify. */
{
const std::filesystem::path disc_check = resolved_disc;
if (!disc_check.empty()) {
#if defined(RECOMP_LAUNCHER)
const std::string& expect_serial = g_lnch_expected_serial;
const uint32_t expect_crc = g_lnch_expected_crc;
const bool has_crc = g_lnch_has_crc;
#else
const std::string expect_serial = game_id;
const uint32_t expect_crc = game_disc_crc;
const bool has_crc = game_has_disc_crc;
#endif
PSXRecompV4::DiscIdentity nid = PSXRecompV4::identify_disc(
disc_check, expect_serial, expect_crc, has_crc,
/*compute_crc*/ false, &g_netplay_disc_expect);
g_session_disc_fp = nid.disc_fp;
g_session_netplay_disc_ok = nid.netplay_ok && !nid.disc_fp.empty();
psx_lobby_set_disc_fp(nid.disc_fp.c_str());
if (!g_session_netplay_disc_ok) {
std::fprintf(stderr,
"psxrecomp: netplay refused — disc mount not valid for "
"online (%s)\n",
nid.netplay_detail.empty()
? "TOC fingerprint missing or policy failed"
: nid.netplay_detail.c_str());
return 1;
}
} else if (!g_session_netplay_disc_ok || g_session_disc_fp.empty()) {
std::fprintf(stderr,
"psxrecomp: netplay refused — no verified disc TOC "
"fingerprint (mount the supported .cue dump)\n");
return 1;
}
}
/* Transport role and gameplay slot are independent. An empty peer
* means this process listens for the first peer, even when the lobby
* owner was reordered into gameplay slot 1 (Player 2). */
@@ -10182,8 +10273,8 @@ session_reboot:
}
}
}
/* Guest netplay: sandbox .pst/.mcd under saves/netplay/ after personal
* roots are known (must follow savestate_configure + memcard_init). */
/* Netplay: refresh RB bios/entry after savestate_configure (start() ran
* earlier with zeros); guest also sandboxes .pst/.mcd under saves/netplay/. */
psx_netplay_bind_guest_saves();
/* Let memory subsystem see SR for cache-isolation checks. */
+8
View File
@@ -782,6 +782,14 @@ int mdec_recently_active(uint32_t within_frames) {
return (psx_cycle_count - mdec_last_color_decode_cycle) <= window;
}
uint64_t mdec_color_age_cycles(void) {
if (mdec_last_color_decode_cycle == (uint64_t)0 - 1000u)
return (uint64_t)0 - 1u;
if (psx_cycle_count < mdec_last_color_decode_cycle)
return (uint64_t)0 - 1u;
return psx_cycle_count - mdec_last_color_decode_cycle;
}
void mdec_init(void) {
memset(&mdec, 0, sizeof(mdec));
memset(mdec_trace, 0, sizeof(mdec_trace));
+16
View File
@@ -609,6 +609,20 @@ void dirty_ram_set_bitmap_words(const uint32_t* words, uint32_t count) {
static uint32_t overlay_watch_bitmap[DIRTY_RAM_BITMAP_WORDS];
static uint32_t overlay_page_gen[DIRTY_RAM_PAGE_COUNT];
void dirty_ram_reset_for_boot(void) {
memset(dirty_ram_bitmap, 0, sizeof(dirty_ram_bitmap));
memset(text_modified_bitmap, 0, sizeof(text_modified_bitmap));
memset(text_diverged_bitmap, 0, sizeof(text_diverged_bitmap));
g_text_diverged_pages = 0;
memset(overlay_watch_bitmap, 0, sizeof(overlay_watch_bitmap));
memset(overlay_page_gen, 0, sizeof(overlay_page_gen));
memset(g_dirty_ram_exec_page_bitmap, 0, sizeof(g_dirty_ram_exec_page_bitmap));
memset(g_dirty_ram_exec_pc_bitmap, 0, sizeof(g_dirty_ram_exec_pc_bitmap));
memset(g_dirty_ram_dispatch_pc_bitmap, 0,
sizeof(g_dirty_ram_dispatch_pc_bitmap));
g_dirty_ram_code_gen++;
}
void overlay_watch_set_range(uint32_t phys, uint32_t len) {
if (len == 0 || phys >= RAM_SIZE) return;
uint32_t end = phys + len - 1u;
@@ -874,6 +888,8 @@ void memory_init(const char* bios_path) {
ram_size_reg = 0;
i_stat = 0;
i_mask = 0;
/* Host dirty/text/overlay bitmaps survive memset(ram) and fork dig0. */
dirty_ram_reset_for_boot();
FILE* f = fopen(bios_path, "rb");
if (!f) {
+92 -6
View File
@@ -25,6 +25,8 @@ int psx_lobby_list_count(void) { return 0; }
int psx_lobby_list_get(int index, PsxLobbyRow *out) { (void)index; (void)out; return 0; }
void psx_lobby_set_game_identity(const char *a, const char *b) { (void)a; (void)b; }
const char *psx_lobby_game_version(void) { return PSX_GAME_VERSION; }
void psx_lobby_set_disc_fp(const char *disc_fp) { (void)disc_fp; }
const char *psx_lobby_disc_fp(void) { return ""; }
void psx_lobby_set_max_slots(int max_slots) { (void)max_slots; }
int psx_lobby_create(const char *a, const char *b, const char *c, const char *d,
const char *e, const PsxLobbyMatchCaps *f)
@@ -147,6 +149,7 @@ typedef struct {
char my_bind[PSX_LOBBY_ENDPOINT_LEN];
char filter_game_name[PSX_LOBBY_NAME_LEN];
char filter_game_version[PSX_LOBBY_VERSION_LEN];
char disc_fp[65]; /* lowercase hex SHA-256 TOC fingerprint; "" = unset */
PsxLobbyJoinInfo join;
PsxLobbyMember members[PSX_LOBBY_MAX_MEMBERS];
int member_count;
@@ -223,6 +226,9 @@ static RNetIceRttProbe *g_ice_rtt;
static char g_ice_rtt_peer_id[PSX_LOBBY_ID_LEN];
static int g_ice_rtt_force_relay;
static uint64_t g_ice_rtt_last_log_ms;
/* Last path_report sent to the lobby server (direct|relay|fail). */
static char g_ice_path_reported[16];
static uint64_t g_ice_path_report_ms;
/* One-shot list latency: burst-ping LAN + public candidates after lobby_list. */
#define PSX_LOBBY_MAX_PROBE_PEND (PSX_LOBBY_MAX_LIST * (PSX_LOBBY_MAX_LAN_EPS + 1))
@@ -1840,12 +1846,17 @@ static void handle_server_json(const char *json)
if (!g_lc.match_caps.valid)
g_lc.match_caps.valid = 1;
g_lc.match_caps.force_input_relay = 1;
} else if (g_lc.match_caps.valid) {
/* ice_p2p / non-SFU launch — do not keep a stale force_input_relay. */
g_lc.match_caps.force_input_relay = 0;
}
fill_peer_bind_from_join();
parse_slots_array(json);
/* Guest must know host:port (or relay). Host may leave peer empty for
* accept-first / host-as-relay. Server relay: every peer dials relay. */
* accept-first / host-as-relay. Server relay: every peer dials relay.
* ice_p2p: MotK ICE signaling; LAN endpoints optional. */
{
char transport[24];
const int force_relay = using_server_input_relay(&g_lc.join);
const int seats = g_lc.join.player_count >= 2 ? g_lc.join.player_count
: g_lc.join.max_slots;
@@ -1853,6 +1864,12 @@ static void handle_server_json(const char *json)
(g_lc.is_host && seats >= 3 && !force_relay) ? 1 : 0;
const int peer_bad = !g_lc.join.peer_hostport[0] ||
endpoint_port_is_zero(g_lc.join.peer_hostport);
int ice_p2p = 0;
transport[0] = '\0';
json_get_str(json, "transport", transport, sizeof(transport));
if (strcmp(transport, "ice_p2p") == 0 ||
(!force_relay && seats == 2 && !relay_endpoint[0]))
ice_p2p = 1;
if (force_relay) {
if (peer_bad) {
strncpy(g_lc.join.last_error, "missing_endpoints",
@@ -1860,6 +1877,24 @@ static void handle_server_json(const char *json)
g_lc.launch_pending = 0;
return;
}
} else if (ice_p2p) {
#if !defined(RNET_ENABLE_ICE)
/* Session 151: no-ICE Desktop cannot run ice_p2p — refuse
* launch with a clear error instead of start failed (-4). */
fprintf(stderr,
"psx_lobby: launch transport=ice_p2p refused — this "
"build has no ICE (need SFU / Force Relay, or rebuild "
"with PSX_NET_ICE=ON)\n");
fflush(stderr);
strncpy(g_lc.join.last_error, "ice_required",
sizeof(g_lc.join.last_error) - 1);
g_lc.launch_pending = 0;
return;
#else
fprintf(stderr,
"psx_lobby: launch transport=ice_p2p (no SFU)\n");
fflush(stderr);
#endif
} else if (!g_lc.join.host_endpoint[0] ||
(g_lc.is_host && !host_hub && !g_lc.join.guest_endpoint[0]) ||
(!g_lc.is_host && peer_bad)) {
@@ -1928,7 +1963,8 @@ static void handle_server_json(const char *json)
strcmp(code, "already_in_lobby") == 0 ||
strcmp(code, "lobby_limit") == 0 ||
strcmp(code, "version_mismatch") == 0 ||
strcmp(code, "game_mismatch") == 0) {
strcmp(code, "game_mismatch") == 0 ||
strcmp(code, "disc_mismatch") == 0) {
g_lc.join.ok = 0;
}
return;
@@ -2193,6 +2229,8 @@ static void lobby_ice_rtt_close(void)
g_ice_rtt_peer_id[0] = '\0';
g_ice_rtt_force_relay = 0;
g_ice_rtt_last_log_ms = 0;
g_ice_path_reported[0] = '\0';
g_ice_path_report_ms = 0;
/* Keep accept=1 through launch / match so match ICE can queue offers.
* Waiting-room resume clears accept until the probe restarts. */
if (!g_lc.launch_pending && !g_lc.ice_rtt_suspended)
@@ -2366,6 +2404,7 @@ static void lobby_ice_rtt_tick(void)
rnet_ice_rtt_selected_path(g_ice_rtt, path, sizeof(path));
{
uint64_t now = lobby_mono_ms();
const char *report = NULL;
if (now - g_ice_rtt_last_log_ms >= 3000ull) {
g_ice_rtt_last_log_ms = now;
fprintf(stderr,
@@ -2373,6 +2412,27 @@ static void lobby_ice_rtt_tick(void)
rnet_ice_state_name(st), path, ms);
fflush(stderr);
}
/* Server picks SFU vs ICE P2P from these reports (2 seated only). */
if (strcmp(path, "host") == 0 || strcmp(path, "srflx") == 0 ||
strcmp(path, "prflx") == 0)
report = "direct";
else if (strcmp(path, "relay") == 0)
report = "relay";
else if (strcmp(path, "failed") == 0)
report = "fail";
if (report && g_lc.member_count == 2 &&
(strcmp(report, g_ice_path_reported) != 0 ||
now - g_ice_path_report_ms >= 10000ull)) {
char msg[96];
snprintf(msg, sizeof(msg),
"{\"op\":\"path_report\",\"path\":\"%s\"}", report);
queue_send(msg);
strncpy(g_ice_path_reported, report, sizeof(g_ice_path_reported) - 1);
g_ice_path_reported[sizeof(g_ice_path_reported) - 1] = '\0';
g_ice_path_report_ms = now ? now : 1ull;
fprintf(stderr, "psx_lobby: path_report %s\n", report);
fflush(stderr);
}
}
#endif /* RNET_ENABLE_ICE */
}
@@ -2498,6 +2558,29 @@ const char *psx_lobby_game_version(void)
return effective_game_version(NULL);
}
void psx_lobby_set_disc_fp(const char *disc_fp)
{
size_t i;
g_lc.disc_fp[0] = '\0';
if (!disc_fp || !disc_fp[0]) return;
/* Accept only 64 lowercase/uppercase hex chars; normalize to lower. */
for (i = 0; i < 64; i++) {
unsigned char c = (unsigned char)disc_fp[i];
if (!isxdigit(c)) return;
g_lc.disc_fp[i] = (char)tolower(c);
}
if (disc_fp[64] != '\0') {
g_lc.disc_fp[0] = '\0';
return;
}
g_lc.disc_fp[64] = '\0';
}
const char *psx_lobby_disc_fp(void)
{
return g_lc.disc_fp;
}
void psx_lobby_request_list(void)
{
g_list_rtt_on_next_list = 1;
@@ -2547,10 +2630,12 @@ int psx_lobby_create(const char *name, const char *game_name, const char *game_v
}
n = snprintf(msg, sizeof(msg),
"{\"op\":\"create\",\"name\":\"%s\",\"game_name\":\"%s\",\"game_version\":\"%s\","
"\"password\":\"%s\",\"max_slots\":%d,\"host_bind\":\"%s\",\"display_name\":\"%s\"%s}",
"\"password\":\"%s\",\"max_slots\":%d,\"host_bind\":\"%s\",\"display_name\":\"%s\","
"\"disc_fp\":\"%s\"%s}",
name && name[0] ? name : "Lobby", gn, gv,
password ? password : "", g_lobby_max_slots, g_lc.my_bind,
g_lc.display_name[0] ? g_lc.display_name : "Host", caps_json);
g_lc.display_name[0] ? g_lc.display_name : "Host",
g_lc.disc_fp, caps_json);
if (n < 0 || (size_t)n >= sizeof(msg)) return -1;
queue_send(msg);
flush_pending();
@@ -2572,10 +2657,11 @@ int psx_lobby_join(const char *lobby_id, const char *password, const char *guest
g_lc.join.last_error[0] = '\0';
snprintf(msg, sizeof(msg),
"{\"op\":\"join\",\"lobby_id\":\"%s\",\"password\":\"%s\",\"guest_bind\":\"%s\","
"\"display_name\":\"%s\",\"game_name\":\"%s\",\"game_version\":\"%s\"}",
"\"display_name\":\"%s\",\"game_name\":\"%s\",\"game_version\":\"%s\","
"\"disc_fp\":\"%s\"}",
lobby_id, password ? password : "", g_lc.my_bind,
g_lc.display_name[0] ? g_lc.display_name : "Guest",
gn, gv);
gn, gv, g_lc.disc_fp);
queue_send(msg);
flush_pending();
return 0;
+211 -10
View File
@@ -34,9 +34,11 @@
#include "psx_netplay_rb.h"
#include "psx_netplay_sched.h"
#include "cpu_state.h"
#include "cdrom.h"
#include "gpu.h"
#include "interrupts.h"
#include "mdec.h"
#include "psx_cycles.h"
#include "psx_scheduler.h"
#if defined(PSX_HAS_LOBBY_CLIENT)
#include "psx_lobby_client.h"
@@ -415,6 +417,13 @@ void psx_netplay_pad_trace_dev(int card, int fallback, int sdl_start,
}
void psx_netplay_on_rb_snap_loaded(void) {}
void psx_netplay_hc_fork_recovery_restart(void) {}
void psx_netplay_hc_fork_recovery_clear(void) {}
void psx_netplay_cd_bisect_arm(uint32_t from_sim, uint32_t ticks)
{
(void)from_sim;
(void)ticks;
}
int psx_netplay_cd_bisect_active(void) { return 0; }
int psx_netplay_needs_local_sample(void) { return 0; }
int psx_netplay_live_pad_buttons(uint16_t *out)
{
@@ -712,6 +721,166 @@ void psx_netplay_hc_fork_recovery_restart(void)
fflush(stderr);
}
void psx_netplay_hc_fork_recovery_clear(void)
{
s_fork_tick = 0xffffffffu;
s_fork_first_sim = 0u;
s_fork_last_attempt_sim = 0u;
}
/* §93 P1: per-tick CD/MDEC/clk crumbs across BIOS→FMV (~160–192) and after
* media realign. Enable with PSX_RB_CD_BISECT=1 or =LO-HI (default 150-250).
* PSX_RB_CD_BISECT_ARM=N extends N ticks past each media flush_resume arm. */
static int s_cd_bisect_enabled;
static int s_cd_bisect_inited;
static uint32_t s_cd_bisect_lo = 150u;
static uint32_t s_cd_bisect_hi = 250u;
static uint32_t s_cd_bisect_arm_until; /* exclusive sim; 0 = inactive */
static uint32_t s_cd_bisect_arm_span = 48u;
static uint32_t s_cd_bisect_last_tick = 0xffffffffu;
static void np_cd_bisect_init(void)
{
const char *e;
const char *dash;
unsigned lo = 0, hi = 0;
if (s_cd_bisect_inited)
return;
s_cd_bisect_inited = 1;
e = getenv("PSX_RB_CD_BISECT");
if (!e || !e[0] || e[0] == '0')
return;
s_cd_bisect_enabled = 1;
dash = strchr(e, '-');
if (dash && dash > e &&
sscanf(e, "%u-%u", &lo, &hi) == 2 && hi >= lo) {
s_cd_bisect_lo = lo;
s_cd_bisect_hi = hi;
}
e = getenv("PSX_RB_CD_BISECT_ARM");
if (e && e[0]) {
unsigned n = (unsigned)atoi(e);
if (n > 0u && n < 4096u)
s_cd_bisect_arm_span = n;
}
fprintf(stderr,
"psxrecomp: rb CD bisect ON sim=%u..%u +arm=%u after media "
"flush_resume (per-tick crumbs + cd-cmd ISSUE/QUEUE/DONE; "
"diff peer lines by cyc, ignore resim=1)\n",
(unsigned)s_cd_bisect_lo, (unsigned)s_cd_bisect_hi,
(unsigned)s_cd_bisect_arm_span);
fflush(stderr);
}
void psx_netplay_cd_bisect_arm(uint32_t from_sim, uint32_t ticks)
{
uint32_t span;
np_cd_bisect_init();
if (!s_cd_bisect_enabled)
return;
span = ticks ? ticks : s_cd_bisect_arm_span;
if (span == 0u)
span = 48u;
if (from_sim + span < from_sim)
s_cd_bisect_arm_until = 0xffffffffu;
else
s_cd_bisect_arm_until = from_sim + span;
fprintf(stderr,
"psxrecomp: rb CD bisect arm sim=%u..%u (media flush_resume)\n",
(unsigned)from_sim, (unsigned)(s_cd_bisect_arm_until - 1u));
fflush(stderr);
}
int psx_netplay_cd_bisect_active(void)
{
uint32_t sim;
np_cd_bisect_init();
if (!s_cd_bisect_enabled)
return 0;
sim = psx_netplay_sim_tick();
if (sim >= s_cd_bisect_lo && sim <= s_cd_bisect_hi)
return 1;
if (s_cd_bisect_arm_until != 0u && sim < s_cd_bisect_arm_until)
return 1;
return 0;
}
static void np_cd_bisect_tick(uint32_t tick)
{
CDROMDebugState cds;
MDECDebugState mds;
CPUState dig_cpu;
uint32_t csv;
uint32_t cd_crc;
uint32_t clk_h;
uint64_t mdec_age;
int in_fixed;
int in_arm;
np_cd_bisect_init();
if (!s_cd_bisect_enabled || !g_np.cpu)
return;
in_fixed = (tick >= s_cd_bisect_lo && tick <= s_cd_bisect_hi);
in_arm = (s_cd_bisect_arm_until != 0u && tick < s_cd_bisect_arm_until);
if (!in_fixed && !in_arm)
return;
if (s_cd_bisect_last_tick == tick)
return;
s_cd_bisect_last_tick = tick;
memset(&cds, 0, sizeof(cds));
memset(&mds, 0, sizeof(mds));
cdrom_debug_snapshot(&cds);
mdec_debug_get_state(&mds);
psx_netplay_rb_cpu_for_present_digest(&dig_cpu, g_np.cpu);
csv = interrupts_get_cycles_since_vblank();
cd_crc = netplay_cdrom_digest();
{
NetplayCoreParts parts;
netplay_core_digest_parts(&dig_cpu, &parts);
clk_h = parts.clock_irq;
}
mdec_age = mdec_color_age_cycles();
fprintf(stderr,
"psxrecomp: rb cd-bisect sim=%u cyc=%llu csv=%u pc=%08x "
"i_stat=%08x clk=%08x cd=%08x "
"rd=%d reading=%d present_d=%d pend=%d/%d pend_cmd=%02x div=%d "
"lba=%d rMSF=%02d:%02d:%02d mode=%02x "
"irq_f=%02x irq_e=%02x sec_av=%d sec_pos=%d "
"mdec_age=%llu mdec_busy=%u in=%u out=%u/%u depth24=%d "
"xa=%d fmv_pend=%d\n",
(unsigned)tick,
(unsigned long long)psx_cycle_count,
(unsigned)csv,
(unsigned)dig_cpu.pc,
(unsigned)cds.i_stat,
(unsigned)clk_h,
(unsigned)cd_crc,
cds.read_delay,
cds.reading,
cds.irq_present_delay,
cds.pending_pending,
cds.pending_delay,
(unsigned)cds.pending_cmd,
cds.speed_divisor,
cds.last_sector_lba,
cds.read_min, cds.read_sec, cds.read_sect,
(unsigned)cds.mode_reg,
(unsigned)cds.irq_flag,
(unsigned)cds.irq_enable,
cds.sector_available,
cds.sector_read_pos,
(unsigned long long)mdec_age,
(unsigned)mds.busy,
(unsigned)mds.input_count,
(unsigned)mds.output_pos,
(unsigned)mds.output_size,
gpu_display_is_depth24(),
cdrom_xa_stream_active(),
cdrom_fmv_stream_pending());
fflush(stderr);
}
static void np_check_core_diverge(void)
{
uint32_t tick = 0, local_d = 0, peer_d = 0;
@@ -2310,9 +2479,9 @@ static void np_rollback_reconcile_wire(void)
cooldown = psx_netplay_rb_rewind_suppressed();
fmv_defer = psx_netplay_rb_fmv_defer_rewind();
{
/* Media + short settle (§26): promote-only; admit waits for wire so
* FMV Start/skip never opens depth24 tip episodes. Post-FMV menus
* invent+resim (digest lockstep no longer gates admit). */
/* Media + short settle (§26/§100): promote-only; invent off during
* live FMV even with MEDIA_KF (avoids GAP1→3.7MB KF xfer). Post-FMV
* menus invent+resim. */
int fmv_lock = psx_netplay_rb_lockstep_no_invent();
no_resim = promote_sweep || cooldown || fmv_lock;
if (promote_sweep)
@@ -3114,13 +3283,24 @@ static int resolve_use_ice(const PsxNetplayConfig *cfg)
return 0;
#else
{
int online_requested = cfg->transport == 1 ||
(cfg->transport == 0 && in_motk_room);
if (online_requested) {
/* Session 151: Desktop guest (no ICE) got MotK launch with peer set
* to the SFU advertise host but force_input_relay unset → hard -4
* while host SFU-started. Dial a usable peer as LAN/SFU instead. */
if (in_motk_room && cfg->peer_hostport && cfg->peer_hostport[0]) {
fprintf(stderr,
"psx_netplay: hosted lobby requires ICE, but ICE is not "
"available in this build (configure with PSX_NET_ICE=ON / "
"RNET_ENABLE_ICE=ON)\n");
"psx_netplay: no-ICE build — MotK room dialing peer %s "
"as LAN/SFU (rebuild with PSX_NET_ICE=ON for ice_p2p)\n",
cfg->peer_hostport);
fflush(stderr);
return 0;
}
if (cfg->transport == 1 || in_motk_room) {
fprintf(stderr,
"psx_netplay: hosted lobby needs ICE or a SFU "
"relay_endpoint, but ICE is not in this build and peer "
"is empty (configure PSX_NET_ICE=ON / RNET_ENABLE_ICE=ON, "
"or Force Relay so launch includes relay_endpoint)\n");
fflush(stderr);
return -1;
}
}
@@ -3156,6 +3336,15 @@ int psx_netplay_start(const PsxNetplayConfig *cfg)
/* Max delay 20 matches RNET_MAX_BUNDLE 21 (neutral prefix + tip). */
rcfg.input_delay = (rnet_u8)(cfg->input_delay < 0 ? 0
: (cfg->input_delay > 20 ? 20 : cfg->input_delay));
/* §105: rollback floor D≥5 at session create (both peers). Lobby can
* still seed 2–4 for delay-sync probe; Play clamps here. */
if (cfg->rollback && rcfg.input_delay > 0u && rcfg.input_delay < 5u) {
fprintf(stderr,
"psxrecomp: rb session delay floor %u → 5 (rollback min)\n",
(unsigned)rcfg.input_delay);
fflush(stderr);
rcfg.input_delay = 5u;
}
rcfg.session_id = cfg->session_id ? cfg->session_id : 1u;
/* Host resolves auto (-1) before start; accept 0..PSX_MAX_PLAYERS-1. */
@@ -3522,7 +3711,16 @@ int psx_netplay_start(const PsxNetplayConfig *cfg)
void psx_netplay_bind_guest_saves(void)
{
if (!psx_netplay_active() || g_np.local_slot == 0 || g_np.guest_sandbox)
uint32_t bios = 0, entry = 0;
if (!psx_netplay_active())
return;
/* psx_netplay_start runs before savestate_configure, so g_np still held
* bios=0/entry=0. Refresh for both seats: host RB snaps (MEDIA-KF) must
* carry the real integrity key or the guest rejects the transfer. */
savestate_get_integrity(&bios, &entry);
g_np.bios_checksum = bios;
g_np.entry_pc = entry;
if (g_np.local_slot == 0 || g_np.guest_sandbox)
return;
np_enter_guest_sandbox();
}
@@ -4329,6 +4527,8 @@ void psx_netplay_finish_frame(void)
psx_netplay_rb_finish_frame();
/* Exchange cores during Replay so mid-resim forks abort before POST. */
np_emit_frame_commit(done);
/* Outside emit: Live FMV skips FRAME_COMMIT; bisect still needs crumbs. */
np_cd_bisect_tick(done);
np_drain_peer_frame_commits();
g_np.latched_for_tick = 0;
g_np.needs_advance = 0; /* live latch must not outlive a resim vblank */
@@ -4340,6 +4540,7 @@ void psx_netplay_finish_frame(void)
/* Digest the tick that just ran (sim_tick before advance). */
done = rnet_session_sim_tick(g_np.session);
np_emit_frame_commit(done);
np_cd_bisect_tick(done);
if (g_np.rollback) {
uint32_t resume_hint = 0;
/* TipHold Live: reconcile late wire so tip-extend can schedule rereplay. */
File diff suppressed because it is too large Load Diff
+81 -35
View File
@@ -26,6 +26,10 @@
#include "psx_netplay_rb.h"
#include "recomp_net/recomp_net.h"
/* §104/§105: auto-delay / lobby seed floors (shared with sync_delay). */
#define RB_FORCE_TURN_DELAY_FLOOR 6
#define RB_AUTO_DELAY_LOWER_FLOOR 5
/* ------------------------------------------------------------------ */
/* Bridge into psx_netplay.c state */
/* ------------------------------------------------------------------ */
@@ -114,6 +118,22 @@ void np_sched_sync_delay_from_session(void)
if (!s || !g_sb.input_delay)
return;
d = rnet_session_committed_delay(s);
/* §105: rollback never runs undersized D from lobby seed (session-149
* started D=4 → invent storm → auto-delay ratchet to 7). Floor matches
* auto-delay lower bound; Force-TURN keeps its higher floor via adapt. */
if (d >= 2u && d < (rnet_u8)RB_AUTO_DELAY_LOWER_FLOOR &&
np_sched_real_delay_enabled()) {
static int s_floor_log;
if (!s_floor_log) {
fprintf(stderr,
"psxrecomp: rb delay floor %u → %u (rollback min D; "
"lobby seed was undersized)\n",
(unsigned)d, (unsigned)RB_AUTO_DELAY_LOWER_FLOOR);
fflush(stderr);
s_floor_log = 1;
}
d = (rnet_u8)RB_AUTO_DELAY_LOWER_FLOOR;
}
if (d >= 2u && (int)d != *g_sb.input_delay) {
fprintf(stderr, "psxrecomp: rb delay committed %d → %u (session)\n",
*g_sb.input_delay, (unsigned)d);
@@ -335,20 +355,21 @@ static uint32_t g_gap1_expire_invent_streak;
#define RB_GAP1_SHRINK_HOLD_MS 1000u
/* §27: deep invent (pred_depth≥2) only after tip looks stale. */
#define RB_INVENT_DEPTH_STALE_FLOOR_MS 40u
#define RB_INVENT_RUNWAY_GRACE_CAP_MS 8u
/* §29: A/B classification — tip considered "advancing" if its last advance
* was within this multiple of its own arrival-period EMA (x2 fixed point). */
#define RB_TIP_FRESH_MULT_X2 3u
/* §43: LAN micro-grace before a gap=1 invent. 2026-08-02 soak #4: with
* rtt_raw 0–1ms both peers still ran ~100% of admissions 1 tick ahead of
* the confirmed wire — a sub-frame PHASE offset, not latency. §29's
* immediate-invent is right for WAN (a transit delay cannot be waited out)
* but on LAN the row is in flight and lands within a few ms. Under §44 real
* delay this only fires once the cushion is empty. */
#define RB_GAP1_LAN_RTT_MAX_MS 12u
#define RB_GAP1_LAN_GRACE_BASE_MS 3u
#define RB_GAP1_LAN_GRACE_CAP_MS 6u
/* §43/§104/§105/§107: micro-grace before a gap=1 invent. Session-149 invents
* were 100% remote_lead=-1. §105 waited only when RTT≤48 or tip-due (cap 12).
* Post-§105 soak: 1277/1305 host invents had rtt>48 (SFU ~75ms) → lan gate
* off → gap1_cap=0 → invent every tip tick. §107: always grace on gap=1
* (non-FMV), scale ½RTT+base, LAN cap 12 / relay cap 20. */
#define RB_GAP1_LAN_RTT_MAX_MS 48u
#define RB_GAP1_LAN_GRACE_BASE_MS 4u
#define RB_GAP1_LAN_GRACE_CAP_MS 12u
#define RB_GAP1_RELAY_GRACE_CAP_MS 20u
/* §104: runway (gap≥2) grace — was 8; guest RUNWAY_EMPTY invent was high. */
#define RB_INVENT_RUNWAY_GRACE_CAP_MS 12u
/* §28: tip arrival cadence — tracks how often the confirmed remote tip
* (highest_remote_wire) actually advances, independent of whether admit
@@ -954,17 +975,28 @@ static void np_scorecard_note_miss(rnet_u32 wire)
if (s_counted_wire != wire) {
s_counted_wire = wire;
g_sc_miss_at_need++;
/* §57: feed the delay controller and arm the lateness timer — if we
* end up waiting this miss out, note_remote_hit measures how late
* the row was; if we invent instead, the pending flag is cleared at
* the invent exit (lateness then shows up as a mispredict age, not
* an arrival stat). */
/* §57/§105: feed the delay controller and arm the lateness timer —
* if we wait this miss out, note_remote_hit measures how late the
* row was. If we invent instead, np_auto_delay_undo_invent_miss
* drops the controller sample (session-149: invent→miss 993‰
* ratcheted D 4→7 without real arrival lateness). */
g_ad_miss++;
g_ad_miss_pending = 1;
g_ad_miss_t0_ms = sched_mono_ms();
}
}
/* §105: invent is not an arrival miss — undo the controller sample so tip
* invent storms cannot raise D (mushy input with no cushion gain). */
static void np_auto_delay_undo_invent_miss(void)
{
if (g_ad_miss_pending) {
if (g_ad_miss > 0u)
g_ad_miss--;
g_ad_miss_pending = 0;
}
}
static void np_scorecard_sample(uint32_t now, rnet_u32 sim, rnet_u32 wire,
const RNetSessionStats *st)
{
@@ -1160,9 +1192,8 @@ static void np_pcap_freeze_exit(void)
#define RB_AUTO_DELAY_EVAL_MS 5000u
#define RB_AUTO_DELAY_AGREE 3u
#define RB_AUTO_DELAY_COOLDOWN_MS 30000u
/* Match launcher Force TURN Play floor — do not shrink below this mid-match
* when the session is relay-only (soak: auto delay 6→5 then invent/fork). */
#define RB_FORCE_TURN_DELAY_FLOOR 6
/* Delay floors: RB_FORCE_TURN_DELAY_FLOOR / RB_AUTO_DELAY_LOWER_FLOOR
* defined near top of file (§104/§105). */
static void np_auto_delay_tick(uint32_t now)
{
@@ -1240,7 +1271,10 @@ static void np_auto_delay_tick(uint32_t now)
return; /* host proposes; guests follow DELAY_SYNC */
miss_per_mille = (uint32_t)(((uint64_t)miss * 1000u) / ticks);
if (miss_per_mille > 20u) {
/* §105: raise only on waited-out arrival pressure (late_n>0). Invent-only
* miss storms (session-149 lead_avg≈-2, late_n=1, miss=993‰) must not
* ratchet D — raising delay does not stop tip invent. */
if (miss_per_mille > 20u && late_n > 0u) {
uint32_t late_avg = late_n ? (late_sum / late_n) : 0u;
int bump = (int)((late_avg + tick_ms - 1u) / tick_ms);
if (bump < 1)
@@ -1248,13 +1282,15 @@ static void np_auto_delay_tick(uint32_t now)
if (bump > 2)
bump = 2;
target = d + bump;
} else if (miss_per_mille < 2u && lead_avg_x16 >= 2 * 16) {
target = d - 1; /* ≥2 spare ticks sustained — cushion oversized */
} else if (miss_per_mille < 1u && lead_avg_x16 >= 3 * 16) {
/* §104: tighter lower bar (was miss<2‰ + lead≥2) — session-144
* shrank D 5→4 under healthy fight cushion and fueled invent. */
target = d - 1;
} else {
target = d;
}
{
int floor_d = 2;
int floor_d = RB_AUTO_DELAY_LOWER_FLOOR;
if (g_sb.force_turn)
floor_d = RB_FORCE_TURN_DELAY_FLOOR;
if (target < floor_d)
@@ -1666,26 +1702,35 @@ int np_sched_on_remote_miss(int slot, uint32_t sim, uint32_t wire,
else
g_admit_gap1_case_b++;
gap1_cap = 0u; /* §29 default: no wait — classify only */
/* §43: LAN micro-grace — wait a few ms for the in-flight row
* instead of inventing, when the link is provably fast or the
* 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 && !psx_netplay_rb_fmv_media_active()) {
/* §43/§105/§107: micro-grace for gap=1 tip invent (lead typically
* -1). Session-149: invents were 100% remote_lead=-1 / case_b —
* the §104 "healthy lead ≥ D-1" arm never fired. §105 waited
* only when RTT≤LAN gate or tip-due — SFU soaks (rtt~75) skipped
* grace and invented every tick. §107: always wait
* min(½RTT+base, lan_cap|relay_cap) outside FMV; tip_due still
* uses the short LAN cap so a due tip does not stall a full
* relay budget. Expiry still invents. */
if (!psx_netplay_rb_fmv_media_active()) {
uint32_t rtt_raw = 0u;
int lan;
int tip_due;
uint32_t cap;
(void)np_invent_rtt_ms(&rtt_raw);
lan = (rtt_raw <= RB_GAP1_LAN_RTT_MAX_MS);
tip_due = (tip_age != 0xffffffffu &&
tip_age + RB_GAP1_LAN_GRACE_CAP_MS >= period);
if (lan || tip_due) {
gap1_cap = rtt_raw / 2u + RB_GAP1_LAN_GRACE_BASE_MS;
if (gap1_cap > RB_GAP1_LAN_GRACE_CAP_MS)
gap1_cap = RB_GAP1_LAN_GRACE_CAP_MS;
}
/* Prefer trusted POST sample; synth/zero still get base. */
gap1_cap = rtt_raw / 2u + RB_GAP1_LAN_GRACE_BASE_MS;
if (lan || tip_due)
cap = RB_GAP1_LAN_GRACE_CAP_MS;
else
cap = RB_GAP1_RELAY_GRACE_CAP_MS;
if (gap1_cap > cap)
gap1_cap = cap;
if (gap1_cap < RB_GAP1_LAN_GRACE_BASE_MS)
gap1_cap = RB_GAP1_LAN_GRACE_BASE_MS;
}
/* §98: during FMV media invent immediately (no LAN micro-grace). */
/* §98: during FMV media invent immediately (no micro-grace). */
}
if (gap1_cap != 0u) {
if (np_invent_grace_stall_ex(slot, wire, gap1_cap, 0)) {
@@ -1736,7 +1781,8 @@ int np_sched_on_remote_miss(int slot, uint32_t sim, uint32_t wire,
np_admit_note_invent_gap(wire, st->highest_remote_wire);
np_admit_log_invent(sim, wire, st->highest_remote_wire,
st->remote_lead, invent_reason);
g_ad_miss_pending = 0; /* §57: invented — lateness timer no longer valid */
/* §105: invent is not arrival lateness — drop controller miss sample. */
np_auto_delay_undo_invent_miss();
if (reason_out)
*reason_out = invent_reason;
np_sched_clear_admit_stall(); /* inventing — not a barrier stall */