pub const PPU_SNAPSHOT_VERSION: u8 = 8;Expand description
Schema version for the PPU snapshot blob.
-
v1: 8-bit
at_shift_lo/at_shift_hi+ 1-bitat_feed_lo/at_feed_hi. -
v2: 16-bit
at_shift_lo/at_shift_hi(lockstep with the pattern shifters); the feed fields are gone. v1 blobs are still read. -
v3 (W3-Stage-4 promotion, 2026-06-10): appends the
mc-ppu-2007-render-bufferrendering-time$2007PPUDATA state-machine tail —render_data_bus,ppudata_sm_countdown,ppudata_v_inc_pending, the raw (pre-h-flip) sprite pattern fetch bytes, and the slot-0 garbage-NT ALE latch. Written unconditionally (zeros when the feature is off) so the layout is identical across feature builds; v1/v2 blobs upconvert with the tail at the inactive defaults (the state the old clear-on-restore assumption imposed). -
v4 (v1.7.0 F3, 2026-06-18): appends the
extra_lines_remainingcountdown for the in-flight PPU extra-scanlines overclock insertion. At the defaultextra_scanlines == 0this is always0, so the blob merely gains a zerou16and restore is behaviourally identical to v3; a non-default countdown taken mid-insertion now round-trips instead of restoring as0(which desynced). v1/v2/v3 blobs upconvert withextra_lines_remaining = 0(no insertion in flight). -
v5 (v2.0.3, ADR 0030): appends the 2-cycle-ALE fetch model’s in-flight multiplexed-bus / octal-latch state —
octal_latch,address_bus,ale_armed,pattern_latch_stale, and the delayed-CopyVcopy_v_delaycountdown. These fields carry a background fetch’s ALE→read state and the two modeled$2006/$2007corruption one-shots; a mid-render save/restore (netplay rollback checkpoints, TAS/save-states) that landed with any of them live restored them to the wrong value and desynced the re-simulated framebuffer — exactly the class of bug the v3/v4 tails already fixed for the$2007state machine and the overclock countdown. This is an ADDITIVE save-state format change (a.rnsgains the tail): v1..=4 blobs upconvert with all five at their inactive defaults (0/false), i.e. the “no fetch in flight” rest state, which is correct for any pre-v5 save taken at rest. Not a load-break — a pre-v5.rnsstill restores. -
v6 (v2.1.1, “Fathom”): appends the per-sprite shifter-halt flags (
spr_halted[8]), the 1-dot-delayed rendering gate (prev_rendering_enabled/rendering_enabled_delayed), and the OAM-corruption arming state (oam_corruption_pending,oam_corruption_index,oam_corruption_disabled,oam_corruption_disabled_instant). These fields were previously not serialized; a frontend run-aheadsnapshot/restoreround-trip landing with any of them live (mid-frame sprite-0 split, OAM corruption arming) would restore stale constructor values and corrupt rendering — the Wizards & Warriors half-blank playfield / stalled audio class. v1..=5 blobs upconvert withspr_halted = [true; 8](halted = power-on default) and all others atfalse/0. Not a load-break — a pre-v6.rnsstill restores. -
v7 (v2.1.4 F2.3): appends the optional OAM-decay model’s per-8-byte-row last-touch timestamps (
oam_decay_cycles[32]), stored as a relative age (now - timestampin CPU cycles) rather than the raw absolute value. The absolute timestamps reference the free-runningdot_counter, which is deliberately NOT part of the save-state (it is cosmetic / re-derived); a raw absolute value would be meaningless after a rollback/restore rebased that counter, and would desync the decay clock. Encoding the age and reconstructingnow - ageagainst the live counter on load makes a run-ahead / netplaysnapshot→restorebyte-identical to the forward run (the property the v3/v4/v5/v6 tails secured for the other mid-frame state). The enable flag itself is a frontend/config knob (re-applied on load likeregion/active_palette), so — matching theextra_scanlinesprecedent — it is NOT serialized; only the in-flight ages are. At the default (decay off) the ages are inert dead state, so the blob merely grows by 256 bytes and restore is behaviourally identical. v1..=6 blobs upconvert by stamping every row as freshly-touched at the live cycle (age 0), i.e. the rest state — correct for any pre-v7 save. Not a load-break — a pre-v7.rnsstill restores. -
v8 (run-ahead sprite-evaluation fix): appends the per-dot sprite-evaluation FSM (
sprite_eval_read_latch,_n,_m,_found,_sec_idx,_copying,_done,_overflow_search,_zero_found,_first_iter), the parallel OAM-data-bus model (oam_bus_copybuffer,oam_bus_secondary[32],oam_bus_addr_h,oam_bus_addr_l,oam_bus_secondary_addr,oam_bus_copy_done,oam_bus_sprite_in_range,oam_bus_overflow_counter), and the secondary-OAM clear-window write pointeroam2_addr. 50 bytes.These are the mid-scanline working registers of dots 65..=256: the eval pass walks primary OAM through
_n/_m, stages each byte throughsprite_eval_read_latch, and commits in-range sprites intosecondary_oamat_sec_idx.secondary_oamitself was already serialized — but the pointers and phase driving it were not, so a snapshot taken with an eval pass in flight restored a full secondary-OAM buffer alongside a power-on-default FSM. The frontend’s run-ahead (snapshot_core_into→ N frames →restore_quiet, every visible frame, and ON BY DEFAULT atrun_ahead = 1) hit this every frame: the hidden frames advanced the FSM and the rollback left that advance behind. Measured effect on theAccuracyCoinbattery: 141/141 headless but 138/141 through the desktop frontend, failing exactlySprite Evaluation :: Arbitrary Sprite zero(error 2),Sprite Evaluation :: Misaligned OAM behavior(error 1), andPPU Behavior :: Rendering Flag Behavior(error 2). Same bug class as the v6 tail (which closed the Wizards & Warriors half-blank playfield), a different uncovered field set.Mesen2 serializes the same set —
_spriteIndex,_sprite0Added,_sprite0Visible,_oamCopybuffer,_secondaryOamAddr,_spriteInRange,_oamCopyDone,_overflowBugCounter(Core/NES/NesPpu.cppNesPpu<T>::Serialize) — independent confirmation that this is live state, not derived.v1..=7 blobs upconvert to the constructor defaults (
0xFFfor the two read latches,[0xFF; 32]for the parallel secondary OAM,0/falseelsewhere) — the at-rest state, and exactly what a pre-v8 restore left behind. NOTE thatrustynes_core’s.rnscontainer is version-EXACT per section, so an existing save state fails to load with a clearVersionMismatchrather than silently misreading (ADR 0028); the in-function upconvert path serves directPpu::restorecallers.Also on restore (ALL versions): the scanline-classification cache (
cached_visible/cached_pre_render/cached_render_line, keyed byflags_cached_scanline) is INVALIDATED rather than serialized. It is a pure function ofscanline+region, both of which are serialized, so recomputing it is equivalent and cheaper than carrying derived bytes — the same choice Mesen2 makes in itsif(!s.IsSaving())post-load fixup block.