Skip to main content

PPU_SNAPSHOT_VERSION

Constant PPU_SNAPSHOT_VERSION 

Source
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-bit at_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-buffer rendering-time $2007 PPUDATA 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_remaining countdown for the in-flight PPU extra-scanlines overclock insertion. At the default extra_scanlines == 0 this is always 0, so the blob merely gains a zero u16 and restore is behaviourally identical to v3; a non-default countdown taken mid-insertion now round-trips instead of restoring as 0 (which desynced). v1/v2/v3 blobs upconvert with extra_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-CopyV copy_v_delay countdown. These fields carry a background fetch’s ALE→read state and the two modeled $2006/$2007 corruption 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 $2007 state machine and the overclock countdown. This is an ADDITIVE save-state format change (a .rns gains 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 .rns still 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-ahead snapshot/restore round-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 with spr_halted = [true; 8] (halted = power-on default) and all others at false/0. Not a load-break — a pre-v6 .rns still 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 - timestamp in CPU cycles) rather than the raw absolute value. The absolute timestamps reference the free-running dot_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 reconstructing now - age against the live counter on load makes a run-ahead / netplay snapshotrestore byte-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 like region / active_palette), so — matching the extra_scanlines precedent — 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 .rns still 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 pointer oam2_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 through sprite_eval_read_latch, and commits in-range sprites into secondary_oam at _sec_idx. secondary_oam itself 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 at run_ahead = 1) hit this every frame: the hidden frames advanced the FSM and the rollback left that advance behind. Measured effect on the AccuracyCoin battery: 141/141 headless but 138/141 through the desktop frontend, failing exactly Sprite Evaluation :: Arbitrary Sprite zero (error 2), Sprite Evaluation :: Misaligned OAM behavior (error 1), and PPU 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.cpp NesPpu<T>::Serialize) — independent confirmation that this is live state, not derived.

    v1..=7 blobs upconvert to the constructor defaults (0xFF for the two read latches, [0xFF; 32] for the parallel secondary OAM, 0/false elsewhere) — the at-rest state, and exactly what a pre-v8 restore left behind. NOTE that rustynes_core’s .rns container is version-EXACT per section, so an existing save state fails to load with a clear VersionMismatch rather than silently misreading (ADR 0028); the in-function upconvert path serves direct Ppu::restore callers.

    Also on restore (ALL versions): the scanline-classification cache (cached_visible / cached_pre_render / cached_render_line, keyed by flags_cached_scanline) is INVALIDATED rather than serialized. It is a pure function of scanline + region, both of which are serialized, so recomputing it is equivalent and cheaper than carrying derived bytes — the same choice Mesen2 makes in its if(!s.IsSaving()) post-load fixup block.