fix crash path from bad splash screen bug, and optimize cross platform pacing + rollback

This commit is contained in:
Alex Vanderveen
2026-08-14 02:25:59 -04:00
parent 4274a76a19
commit d4ea4ade1f
13 changed files with 959 additions and 120 deletions
+143
View File
@@ -4863,6 +4863,9 @@ window. `media_kf=0` because load was past the media/settle unsafe range.
5. **On heal Live/tip-hold** — clear DESYNC invent-hold and RELEASE lockstep.
6. **Pad-mispredict** begins in the post-FMV window still SPAN (optionally with
MEDIA_KF via `rb_want_heal_kf`); only silent-fork recovery is apply-only.
7. **POST match → Live** — `ownership_on_post_match` treats heal like
peer-ahead light: no ownership SPAN hop (session 28: matched POST @826
then chain target=850 hung guest wait-FOLLOW → disconnect).
**Re-soak watch (Win↔Linux 2P through title FMV):**
@@ -4873,3 +4876,143 @@ window. `media_kf=0` because load was past the media/settle unsafe range.
- No `ABORT — resim core diverge` on that heal episode; no
`DESYNC — FMV lockstep MAX unmatched` from the same cutover.
- Linux↔Linux control still clean; 3P WIRE_HOLE is a separate bug class.
## 110. Post-FMV heal sticky + invent hold (session 3 loading hitch) (2026-08-13)
**Soak (TM4 Win↔CachyOS session 3):** §109 heal POST matched @2494 → Live →
`FMV lockstep RELEASE`. Loading-screen hitch kept HC forked at 2495; absurd
lead + invent catch-up; second `hc-fork` with `heal=0` tip-SPAN →
`resim core diverge @2495` → storm DESYNC.
**Cause:** `request_post_fmv_heal_kf` gated on pre-RELEASE lockstep only.
RELEASE dropped eligibility; invent free-ran through the soft fork.
**What landed (§110):**
1. **Heal sticky** (`RB_FMV_HEAL_STICKY_TICKS=300`) armed on heal Live/tip-hold
— `rb_post_fmv_heal_eligible` stays true through sticky (and DESYNC /
lockstep). Re-heal hc-fork keeps `media_kf=1 heal=1`.
2. **Invent hold** during sticky via `lockstep_no_invent` — no GAP1 invent
catch-up until sticky expires or cores rematch CONFIRM.
3. Early sticky clear when post-RELEASE HC streak hits CONFIRM.
**Re-soak watch:** after heal expect `heal sticky until sim=…`; on hitch
re-fork expect `heal KF requested … sticky_until=` then apply-only again —
not `begin … media_kf=0 heal=0` / resim storm. Digs rematch →
`heal sticky cleared (cores rematched…)`.
## 111. Post-FMV tip+1 ±1-cycle Win↔Linux (dirty entry poll) (2026-08-13)
**Soak (MotK Win↔CachyOS after §110b verify heal):** tip **870** matched
(`core=1d304d69`, MEDIA-KF CRC match, resume `pc=0x80076880`,
`i_stat=00000001`). Resim fin@871: Win `cyc=492226568` vs Cachy
`492226569` (±1); cpu/clk/tim/ram fork; hc-fork `no pad mispredict`.
**Cause:** post-FMV wait lives in a dirty-interp hole (not in static emit).
Dirty entry used a host-only `s_interp_entry_poll %% 64` stride to pump IRQs.
Peers that drifted through FMV entered the wait with opposite phases while
VBlank was already latched — one delivered immediately, the other after a
few wait-loop instructions (`exit_pc=0x800768C8`, `v0=1`). Same class as the
old CD54 hold gated on host `s_present_pending`.
**What landed (§111):**
1. **Deliverable → poll every dirty entry** (guest-deterministic). Keep the
%%64 throttle only when no IRQ is deliverable.
2. **`dirty_ram_irq_ambient_resync_after_restore`** — reset entry-poll stride
+ 4096-insn pump gap from `psx_cycles_resync_after_restore`.
3. **Second MotK wait pair** `0x800768C8` (hold A) ↔ `0x80076880` (canonical
B) for IRQ hold / present gate / snap resume canonicalize.
**Re-soak watch:** through title FMV → settle → tip+1 must keep matched
`audit fin` cyc (no ±1) and no `FIRST CORE DIVERGE` / heal-verify abort at
the first post-settle frame. Linux↔Linux control still clean.
## 112. Post-snap LEGACY_SENTINEL tip+1 (loading screen) (2026-08-13)
**Soak (MotK Win↔CachyOS after §111):** matched through settle@1354 and live
dig@1472; soft fork **sim=1480**. Tip **1479** matched (`core=f9e21305`,
resume `pc=0x800756e0`, `i_stat=00000001`). Heal verify fin@1480 cyc Δ4;
Win abort irqctx `same_thr=0 reason=3 exit_pc=0 epc=80000048` at arm cycle
(`PSX_EXC_ESCAPE_LEGACY_SENTINEL`).
**Cause:** `interrupts_resync_after_restore` zeros compiled/dirty resume
latches. Sticky VBlank then delivered on the first post-`flush_resume`
check with `take_pc=0` → sentinel EPC / cross-thread restore, while a peer
that latched the BB PC first took the real same-thread path.
**What landed (§112):**
1. **`psx_irq_arm_compiled_resume_pc`** — publish resume PC (+ last-check edge).
2. **`psx_scheduler_resume_at` + RESUME_CURRENT dispatch** arm before
`psx_dispatch`.
3. **Delivery fallback** — if latches still 0 under top-level resume, use
`cpu->pc` instead of the sentinel.
**Re-soak watch:** loading-screen tip+1 — no Win `irqctx … reason=3
epc=80000048` on heal abort; fin cyc match; no `FIRST CORE DIVERGE` at the
first post-settle loading frame.
## 113. Win↔UNIX post-FMV invent + ahead-tip pacing (2026-08-13)
**Soak:** after §111/§112 accuracy work, Win↔Cachy still showed chronic
cadence skew through loading: Win `phase ctrl lead≈+7..8` / `debt_ms=0`,
Cachy `GAP1_LEGACY` / `RUNWAY_EMPTY` invents. Soft fork @1480 sat **before**
`dense_lockstep min=1510` while invent had already unlocked at settle@1354.
**What landed (§113):**
1. **Invent hold through lockstep MIN** — `rb_in_fmv_lockstep_window` now
gates invent until `g_fmv_lockstep_until` (media_end+MIN), matching the
gate comment that already said invent stays off through MIN. Stall tag
`fmv_lockstep` after settle; settle log `invent_hold=` reports MIN.
2. **Ahead-of-tip timesync** — `np_timesync_note_ahead_skew`: when
`remote_lead < 0` for ~8 admits outside media/lockstep, add ~½-tick debt
so the inventing seat paces before GAP1 (BattleShip cross-OS pattern).
3. **Gap1 LAN grace cap 12→20 ms** — more room for a mid-flight tip row on
LAN Win↔UNIX before inventing.
**Re-soak watch:** `RBE_CROSS_OS_PACING_DIAG=1` — through FMV→loading,
`stall=fmv_lockstep` (not invent) until MIN; `debt_ms` rises on the
ahead-of-tip seat; fewer `GAP1_LEGACY` / `RUNWAY_EMPTY` before tip+1.
Accuracy (§112 arm) still required for matched fin cyc at tip+1.
## 114. Platform tip+1 apply-only heal + KF stream (2026-08-13)
**Problem:** matched-baseline tip+1 after post-FMV loading is Win↔Linux
platform nondeterminism (cpu/clk/tim/ram diverge with matched dirty). Verify
span past load always aborted; empty tip KF rematched tip and stormed.
**What landed (§114):**
1. Apply-only MEDIA_KF heal (`target=load`) — no tip+1 resim.
2. Heal Live → invent off + `g_post_fmv_platform_nondet` + initiator KF stream
every 16 sim ticks (dense snaps).
3. Heal loop CAP → same keep-live stream instead of hard DESYNC abort.
**Gap:** stream always rewound to HC's stuck tip+1 fork (`resolved+1`), so
`choose_load` re-pinned keep forever → loading-screen livelock (no DESYNC).
## 115. Platform KF stream escape / host-tip ride (2026-08-13)
**Soak (MotK Win↔CachyOS after §114):** `FIRST CORE DIVERGE sim=1112`, heal
pin `1111` / `pc=0x800768e8`, then hundreds of `§114 KF stream begin
fork=1112` + `heal KF CAP →1111` with `invent off until rematch`. No DESYNC;
loading never advanced. HC `peek_mismatch` stays at tip+1 while
`resolved_through=keep`.
**What landed (§115):**
1. **Host-tip ride** — when HC fork is the known platform tip+1 and
`sim > fork`, stream `begin_rewind(sim)` so MEDIA-KF pins near host tip.
2. **Accept on advance** — heal Live/tip-hold with `tip > keep` primes HC
past the soft fork, clears invent-hold/stream, sets
`g_platform_accepted_fork` (hc-fork / begin refuse re-heal).
3. **Stream CAP** (`RB_FMV_PLATFORM_KF_STREAM_LIMIT=6`) — same stale fork
without escape → force accept.
**Re-soak watch:** after first post-FMV tip+1 diverge, logs
`§115 KF stream ride host tip` and/or `§115 platform fork accepted`; sim
advances past keep; no endless `snap applied tick=<keep>`; loading completes.
+5
View File
@@ -52,6 +52,11 @@ void dirty_ram_ld_delay_flush(CPUState* cpu);
* on the next interp step (selfcheck MotK v0=countdown vs v0=1 forks). */
void dirty_ram_ld_delay_discard(void);
/* Re-anchor host-only dirty IRQ pump ambient (entry-poll stride + 4096-insn
* gap) after snap restore. Not in boot_state — peers that drifted through FMV
* otherwise entered the post-FMV dirty wait on opposite poll phases. */
void dirty_ram_irq_ambient_resync_after_restore(void);
/* Overlay-cache windows — the address ranges eligible for capture, offline
* recompilation, and per-entry-validated native execution (Rule 18 code that
* does not exist in any compile-time image):
+3
View File
@@ -102,6 +102,9 @@ uint32_t psx_compiled_irq_resume_pc(void);
/* Soft-return / netplay BYE: drop sticky resume latches so rematch dig0 snaps
* cannot inherit a prior-match game PC (see pick_snap_resume_pc). */
void psx_irq_clear_resume_latches(void);
/* Arm compiled IRQ resume before top-level flush_resume / savestate dispatch
* so a latched I_STAT cannot take LEGACY_SENTINEL with take_pc=0. */
void psx_irq_arm_compiled_resume_pc(uint32_t pc);
uint64_t psx_last_irq_check_cycle(void);
uint64_t psx_interrupt_total_checks(void);
uint32_t psx_interrupt_fast_maintenance(void);
+21 -4
View File
@@ -84,11 +84,15 @@ int psx_netplay_rb_fmv_defer_rewind(void);
/* 1 while depth24 or recent MDEC (not settle). */
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 invent-hold is armed (expires). */
/* 1 during FMV media + post-FMV lockstep MIN (§26/§113) — admit waits for
* remote wire through loading tip+1 (not settle-only). Ticks FMV→settle→MIN.
* Also 1 while §93 MAX-unmatched DESYNC invent-hold is armed (expires).
* §111: post-FMV heal sticky invent-hold only while remote_lead > 0. */
int psx_netplay_rb_lockstep_no_invent(void);
/* 1 while sim is still inside the short post-FMV settle tail (before MIN). */
int psx_netplay_rb_fmv_settle_active(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);
@@ -98,9 +102,22 @@ int psx_netplay_rb_fmv_desync_hold(void);
void psx_netplay_rb_clear_fmv_desync_hold(const char *why);
/* §109: next begin_rewind is an apply-only MEDIA_KF heal (target=load).
* Call from hc-fork / resim-diverge escalate before begin_rewind. */
* Call from hc-fork / resim-diverge escalate before begin_rewind.
* §114: tip+1 verify-span abandoned (platform nondet); heal Live arms
* invent-off + KF stream instead of lockstep RELEASE. */
void psx_netplay_rb_request_post_fmv_heal_kf(void);
/* §110: 1 while post-FMV lockstep, DESYNC hold, or heal sticky — hc-fork
* may open apply-only KF even when invent is held. */
int psx_netplay_rb_post_fmv_heal_eligible(void);
/* §111: 1 while heal sticky window is live (invent hold may be tip-lead only). */
int psx_netplay_rb_post_fmv_heal_sticky(void);
/* §115: 1 if this tip+1 fork was accepted (HC primed past it) — hc-fork /
* begin must not re-open apply-only heal for it. */
int psx_netplay_rb_platform_fork_accepted(uint32_t fork_tick);
/* §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.
* §97 MEDIA_KF (default on): returns 0 for media-range — episodes allowed. */
+38 -7
View File
@@ -54,6 +54,11 @@ uint64_t g_dirty_ram_native_handoffs = 0;
uint64_t g_dirty_pump_max_gap_insns = 0;
uint64_t g_dirty_pump_count = 0;
static uint64_t s_last_dirty_irq_pump_insns = 0;
/* Host-only stride for the no-pending IRQ entry throttle. NOT in the snap —
* peers that drifted apart through FMV entered the post-FMV dirty wait with
* different phases and forked tip+1 (Win↔Linux ±1 cyc / swapped cores). Reset
* on restore; when an IRQ is already deliverable, poll every entry instead. */
static uint32_t s_interp_entry_poll = 0;
/* EPC de-overload signal (Tomba 2 frame-1997 fix). Set to the committed guest PC
* immediately around a dirty-pump psx_check_interrupts() call, 0 otherwise. When
@@ -343,6 +348,13 @@ void dirty_ram_ld_delay_discard(void) {
s_ld_pend_val = 0;
}
void dirty_ram_irq_ambient_resync_after_restore(void) {
/* Re-anchor host-only IRQ pump ambient at the restored timeline so both
* peers take the first post-load dirty entry poll from the same phase. */
s_last_dirty_irq_pump_insns = g_dirty_ram_insns_run;
s_interp_entry_poll = 0;
}
uint32_t g_insn_gate_lo = 0; /* extra always-log phys range [lo,hi) */
uint32_t g_insn_gate_hi = 0; /* 0 = extra range disabled */
uint32_t g_insn_freeze_addr = 0; /* candidate phys entry to watch */
@@ -2767,20 +2779,39 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
* per-invocation `(insns_executed & 0xFFF)` gate below can fire — so a
* pending, IEc+IM2-enabled interrupt is never taken and the loop spins for
* seconds while the CD IRQ that would set its wait-flag is never serviced.
* Poll on a GLOBAL invocation counter so short-block loops still yield to
* interrupts (this is the interpreter analogue of a block-leader poll in
* static code). psx_check_interrupts runs the handler and returns with
* When an IRQ is already deliverable, poll EVERY entry (guest-deterministic;
* MotK post-FMV overlay wait @0x80076880 with latched I_STAT.VBlank forked
* Win↔Linux on a host-only %%64 stride). Otherwise throttle on the global
* invocation counter. psx_check_interrupts runs the handler and returns with
* registers restored, so continuing the loop afterward is safe. */
{
static uint32_t s_interp_entry_poll = 0;
static int s_entry_poll_enabled = -1; /* DIAGNOSTIC toggle (590c236 x kind-30 escape regression hunt) */
if (s_entry_poll_enabled < 0) {
const char* e = getenv("PSX_DIRTY_ENTRY_POLL");
s_entry_poll_enabled = (e && e[0] == '0') ? 0 : 1;
}
if (s_entry_poll_enabled && (++s_interp_entry_poll & 0x3Fu) == 0) {
cpu->pc = pc;
psx_check_interrupts(cpu);
if (s_entry_poll_enabled) {
extern uint32_t i_stat;
uint32_t sr = cpu->cop0[12];
int deliverable =
((i_stat & i_mask) != 0u) &&
!psx_get_in_exception() &&
(sr & 0x1u) != 0u &&
(sr & (1u << 10)) != 0u;
if (deliverable || (++s_interp_entry_poll & 0x3Fu) == 0) {
cpu->pc = pc;
s_last_dirty_irq_pump_insns = g_dirty_ram_insns_run;
psx_check_interrupts(cpu);
if (cpu->pc != 0u && !dirty_ram_same_pc(cpu->pc, pc)) {
/* Handler resumed elsewhere — surface to dispatch. */
g_dirty_ram_blocks_run++;
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
g_dirty_interp_chain_target = cpu->pc;
OV_FPLOG_RET1();
}
if (cpu->pc == 0u)
cpu->pc = pc;
}
}
}
for (int i = 0; i < MAX_INSNS_PER_DISPATCH; i++) {
+12 -11
View File
@@ -2595,11 +2595,10 @@ void gpu_vblank_flush_present(void) {
if (psx_netplay_active() && sio_hold_present_for_card())
return;
}
/* MotK menu wait (0x8006CD54↔0x8006CDA0): present ONLY at an explicit
* CDA0 edge. Never present on a CD54 edge (sticky CDA0 must not allow
* that — soak ep3 non-det fin@946). Non-wait edges (FMV / cutover) must
* present even if sticky still names the wait loop — treating sticky as
* in_wait blocked deferred present for the whole FMV1→FMV2 gap. */
/* MotK menu wait (0x8006CD54↔0x8006CDA0) and post-FMV overlay wait
* (0x800768C8↔0x80076880): present ONLY at an explicit B edge. Never
* present on an A edge (sticky B must not allow that). Non-wait edges
* (FMV / cutover) must present even if sticky still names the wait loop. */
{
extern int psx_netplay_active(void);
extern uint32_t psx_compiled_irq_resume_pc(void);
@@ -2608,18 +2607,20 @@ void gpu_vblank_flush_present(void) {
if (psx_netplay_active()) {
const uint32_t wait_a = 0x8006CD54u;
const uint32_t wait_b = 0x8006CDA0u;
const uint32_t wait2_a = 0x800768C8u;
const uint32_t wait2_b = 0x80076880u;
uint32_t pc = psx_compiled_irq_resume_pc();
uint32_t last = psx_last_irq_check_pc();
uint32_t sticky = psx_netplay_rb_sticky_bb_pc();
uint32_t edge = pc ? pc : last;
if (edge == wait_a)
if (edge == wait_a || edge == wait2_a)
return;
if (edge == wait_b || edge != 0u) {
/* CDA0, or non-wait (FMV/cutover) — present; ignore sticky */
} else if (sticky == wait_a) {
return; /* latch cleared, sticky CD54 — defer */
if (edge == wait_b || edge == wait2_b || edge != 0u) {
/* B edge, or non-wait (FMV/cutover) — present; ignore sticky */
} else if (sticky == wait_a || sticky == wait2_a) {
return; /* latch cleared, sticky A — defer */
}
/* sticky CDA0 or unrelated/0 — present */
/* sticky B or unrelated/0 — present */
}
}
s_flushing_present = 1;
+37 -10
View File
@@ -553,6 +553,18 @@ void psx_irq_clear_resume_latches(void)
s_last_interrupt_check_cycle = UINT64_MAX;
}
/* Publish the known resume PC before the first post-load / flush_resume
* dispatch. interrupts_resync zeros the latches; a sticky I_STAT.VBlank can
* then deliver with take_pc=0 → LEGACY_SENTINEL / same_thr=0 and fork peers
* (MotK loading-screen tip+1 @1480, Win irqctx reason=3 epc=sentinel). */
void psx_irq_arm_compiled_resume_pc(uint32_t pc)
{
if (pc == 0u || (pc & 3u) != 0u)
return;
s_compiled_interrupt_resume_pc = pc;
s_last_interrupt_check_pc = pc;
}
/* Deferred cooperative thread switch from nested exception delivery.
*
* A genuine in-exception ChangeThread (kind-30, escape site below) must be
@@ -1006,20 +1018,21 @@ void psx_check_interrupts(CPUState* cpu) {
#define COSIM_IRQ_NOTE(kind_) cosim_irq_note(cpu, (kind_), COSIM_IRQ_TAKE_PC(), g_dirty_safe_resume_pc, s_compiled_interrupt_resume_pc, cpu->cop0[COP0_SR])
#endif
/* MotK wait CD54 + VBlank-only I_STAT: never deliver at the CD54 edge in
* netplay — hold until CDA0. Delivering at CD54 leaves v0=slt(1) while a
* peer that hit CDA0 first delivers with countdown in v0 (soak: non-det
* fin@946/@902 CD54 vs CDA0, cyc ±1 VB, on identical baseline+pads).
* UNCONDITIONAL on the edge PC — the earlier gate also required
* gpu_vblank_present_pending(), but s_present_pending is host-only state
* (not in the snap), so replay delivery timing forked across peers.
* Edge PC + I_STAT are guest-deterministic; the wait ping-pong reaches
* CDA0 a few instructions later, so no starvation. */
/* MotK wait CD54 / post-FMV 768C8 + VBlank-only I_STAT: never deliver at
* the "A" edge in netplay — hold until the canonical B edge. Delivering at
* A leaves v0=slt(1) while a peer that hit B first delivers with countdown
* in v0 (soak: non-det fin@946/@902 CD54 vs CDA0; post-FMV tip+1 @871
* 768C8 vs 76880, cyc ±1). UNCONDITIONAL on the edge PC — the earlier gate
* also required gpu_vblank_present_pending(), but s_present_pending is
* host-only state (not in the snap), so replay delivery timing forked
* across peers. Edge PC + I_STAT are guest-deterministic; the wait
* ping-pong reaches B a few instructions later, so no starvation. */
if (!in_exception && psx_netplay_active()) {
const uint32_t wait_a = 0x8006CD54u;
const uint32_t wait2_a = 0x800768C8u;
uint32_t edge = s_last_interrupt_check_pc;
uint32_t pend = i_stat & i_mask;
if (edge == wait_a && pend != 0u &&
if ((edge == wait_a || edge == wait2_a) && pend != 0u &&
(pend & ~(1u << IRQ_VBLANK)) == 0u) {
PSX_CHECK_INTERRUPTS_RETURN();
}
@@ -1454,6 +1467,20 @@ irq_deliver_eval:
{
uint32_t real_pc = g_dirty_safe_resume_pc ? g_dirty_safe_resume_pc
: s_compiled_interrupt_resume_pc;
/* Top-level flush_resume / savestate: resync cleared the latches and
* the first IRQ may fire before any BB edge republishes them. Prefer
* cpu->pc (already set to the resume target) over the sentinel so
* EPC/saved_gpr stay on the real same-thread path. */
if (real_pc == 0u) {
extern int psx_scheduler_top_level_resume_active(void);
if (psx_scheduler_top_level_resume_active() &&
cpu->pc != 0u && (cpu->pc & 3u) == 0u) {
uint32_t phys = cpu->pc & 0x1FFFFFFFu;
if (phys < 0x00200000u ||
(phys >= 0x1FC00000u && phys < 0x1FC80000u))
real_pc = cpu->pc;
}
}
/* Accept the real resume PC from guest RAM (<2MB) OR the BIOS ROM
* window. The old RAM-only guard rejected ROM-space block leaders
* (e.g. OpenBIOS mcWaitForStatus spinning at 0xBFC076xx during a
+6
View File
@@ -595,6 +595,12 @@ void psx_cycles_resync_after_restore(CPUState *cpu) {
extern void dirty_ram_ld_delay_discard(void);
dirty_ram_ld_delay_discard();
}
/* Entry-poll %%64 stride + 4096-insn pump gap are host-only; reset so
* both peers take the first post-load dirty wait IRQ on the same phase. */
{
extern void dirty_ram_irq_ambient_resync_after_restore(void);
dirty_ram_irq_ambient_resync_after_restore();
}
}
void psx_cycles_reset_for_boot(void) {
+14 -5
View File
@@ -693,17 +693,22 @@ static void np_try_hc_fork_recovery(uint32_t fork_tick)
return;
if (g_np.local_slot != 0)
return; /* initiator side only */
/* §115: tip+1 soft fork already accepted — stay Live. */
if (psx_netplay_rb_platform_fork_accepted(fork_tick))
return;
/* §64: do not bookkeep persist while an episode / tip-hold / load is
* in flight. Previously fork_tick advances during tip-extend rereplay
* started the persist clock, so tip-extend abandon → Live opened a
* second hc-fork recovery with zero Live gap (soak: epoch 8 → 16,
* "persisted 39 ticks"). */
/* §109: allow hc-fork through DESYNC invent-hold when MEDIA_KF can heal
* (fmv_episode_unsafe is 0 with KF on). Keep blocking media+settle. */
/* §109/§110: allow hc-fork through DESYNC invent-hold or heal sticky when
* MEDIA_KF can heal. Keep blocking live media+settle invent-hold. */
if (psx_netplay_rb_active() || psx_netplay_rb_tip_holding() ||
psx_netplay_rb_load_pending() || psx_netplay_rb_rewind_suppressed() ||
(psx_netplay_rb_fmv_defer_rewind() && !psx_netplay_rb_fmv_desync_hold()) ||
(psx_netplay_rb_lockstep_no_invent() && !psx_netplay_rb_fmv_desync_hold()) ||
(psx_netplay_rb_fmv_defer_rewind() && !psx_netplay_rb_fmv_desync_hold() &&
!psx_netplay_rb_post_fmv_heal_eligible()) ||
(psx_netplay_rb_lockstep_no_invent() && !psx_netplay_rb_fmv_desync_hold() &&
!psx_netplay_rb_post_fmv_heal_eligible()) ||
psx_netplay_rb_fmv_episode_unsafe(fork_tick))
return;
sim = rnet_session_sim_tick(g_np.session);
@@ -729,7 +734,7 @@ static void np_try_hc_fork_recovery(uint32_t fork_tick)
(unsigned)fork_tick, (unsigned)(sim - s_fork_first_sim),
fork_cap ? "; fork_cap backoff" : "");
fflush(stderr);
/* §109: silent fork → apply-only host KF (tip-snap SPAN re-diverges). */
/* §109/§114: silent fork → apply-only MEDIA_KF heal (target=load). */
psx_netplay_rb_request_post_fmv_heal_kf();
(void)psx_netplay_rb_begin_rewind(fork_tick, remote);
}
@@ -4754,6 +4759,10 @@ int psx_netplay_catchup_budget(void)
return 0;
if (psx_start_bisect_no_catchup())
return 0;
/* §114: during post-FMV DESYNC / heal sticky, do not turbo catch-up —
* absurd lead after heal abort + catchup widens platform tip+1 skew. */
if (psx_netplay_rb_fmv_desync_hold() || psx_netplay_rb_post_fmv_heal_sticky())
return 0;
cap = np_starv_env_int("PSX_NET_CATCHUP_CAP", PSX_CATCHUP_CAP_DEFAULT);
if (cap <= 0 && g_starv.recovery_amount <= 0)
return 0;
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -95,7 +95,12 @@ static const char *np_gate_lockstep_stall_tag(void *ctx)
return "fmv_media";
if (psx_netplay_rb_fmv_desync_hold())
return "fmv_desync_hold";
return "fmv_settle";
if (psx_netplay_rb_post_fmv_heal_sticky())
return "heal_sticky";
/* §113: invent hold runs through lockstep MIN (loading), not settle-only. */
if (psx_netplay_rb_fmv_settle_active())
return "fmv_settle";
return "fmv_lockstep";
}
static uint32_t np_gate_episode_count(void *ctx)
+14
View File
@@ -732,6 +732,12 @@ void psx_scheduler_resume_at(uint32_t resume_pc)
/* longjmp abandons every mid-block CPS frame — sentinel RFE must not
* treat the new top-level dispatch as a continuable live chain. */
g_sched_top_level_resume = 1;
/* Publish before the longjmp lands in dispatch: sticky I_STAT can deliver
* on the first edge with take_pc=0 otherwise (LEGACY_SENTINEL fork). */
{
extern void psx_irq_arm_compiled_resume_pc(uint32_t pc);
psx_irq_arm_compiled_resume_pc(resume_pc);
}
longjmp(g_scheduler_jmpbuf, 1); /* unwind to psx_scheduler_run; never returns */
}
@@ -849,10 +855,12 @@ void psx_scheduler_run(CPUState* cpu)
}
uint32_t run_pc;
int from_top_resume = 0;
if (g_sched_escape.reason == PSX_RUN_RESUME_CURRENT &&
g_sched_escape.resume_pc != 0u) {
/* Same-thread RFE: GPRs already committed by the RFE; just re-dispatch. */
run_pc = g_sched_escape.resume_pc;
from_top_resume = 1;
} else {
uint32_t cur = psx_current_tcb_ptr(cpu);
if (psx_is_valid_tcb(cpu, cur)) {
@@ -872,6 +880,12 @@ void psx_scheduler_run(CPUState* cpu)
cpu->pc = run_pc;
g_sched_escape.reason = PSX_RUN_CONTINUE;
/* Re-arm on the dispatch that consumes RESUME_CURRENT (covers
* recover_null_pc continues that skip resume_at). */
if (from_top_resume) {
extern void psx_irq_arm_compiled_resume_pc(uint32_t pc);
psx_irq_arm_compiled_resume_pc(run_pc);
}
psx_dispatch(cpu, run_pc);
/* psx_dispatch returned with NO structured escape => the outermost
@@ -61,6 +61,7 @@ void starvation_ring_pc_sample(void) {}
int psx_netplay_active(void) { return 0; }
int psx_selfcheck_enabled(void) { return 0; }
void dirty_ram_ld_delay_discard(void) {}
void dirty_ram_irq_ambient_resync_after_restore(void) {}
int main(void) {
/* NULL cpu: this test pins the scheduler boundary, not the CPU-state