Skip to content

Commit 519bdd5

Browse files
doublegateclaude
andauthored
feat(ppu): Option 2 octal-latch scaffold behind mc-ppu-bus-addr-hybrid (attempt, does not yet pass) (#234)
* feat(ppu): Option 2 octal-latch scaffold behind mc-ppu-bus-addr-hybrid (attempt; does not yet pass) The v2.0.1 "Option 2" attempt from ADR 0030 — Mesen2's persistent-PPU-bus-address model — to pass the two new AccuracyCoin PPU tests ("Hybrid Addresses" $0492, "ALE + Read" $0491). Landed as a flag-gated scaffold: it does NOT yet pass the two tests, but the shipped build is byte-identical and it is the shared `bus_addr` prerequisite for the Option 1 (2-cycle-ALE) effort that follows. Implementation (entirely `#[cfg(feature = "mc-ppu-bus-addr-hybrid")]`-gated, a new default-off feature threaded rustynes-ppu -> rustynes-core -> rustynes-test-harness): - `bus_addr: u16` on `Ppu`, fed by every `read_vram` (Mesen2's `_ppuBusAddress`; its low 8 bits are the octal-latch value). Not snapshotted (self-heals next fetch). - `hybrid_pending: Option<u16>`, set by the `$2006` case-6 render-time hook to `(v & 0x3F00) | (bus_addr & 0xFF)` and consumed once by the next `fetch_nt`. Outcome (measured): - Flag OFF (shipped): AccuracyCoin 139/141, `cargo test -p rustynes-test-harness --release --features test-roms` = 0 FAILED across all suites, no_std clean, clippy 0/0, fmt OK. Byte-identical to main by construction (all state is cfg-gated out). - Flag ON: clippy 0/0, but AccuracyCoin is STILL 139/141 — both new tests fail. Why it does not converge (confirms ADR 0030): the `$2006` hybrid hook fires, but `bus_addr`'s low byte at the write instant is not the tuned `$19` the test's 504-cycle clockslide targets. RustyNES drives each fetch address at whole-dot granularity in a single `read_vram`, so the "last driven address" when the CPU `$2006` write interleaves is not reliably the `$2C19` NT fetch — it may be the group's AT/pattern fetch or land a dot off. Faithfully reproducing the sub-cycle octal latch needs the per-cycle 2-cycle-ALE bus model (Option 1); the delayed-`v` refinement corrects only the `0x41F` scroll bits, not the full low byte. Kept (not reverted) as the documented Option-2 attempt + the `bus_addr` foundation Option 1 builds on. See ADR 0030 for the full analysis and the two-track plan. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(ppu): adopt #234 review — accurate feature comment + hybrid_pending snapshot note Three bot threads on the Option-2 scaffold, all doc-only: - rustynes-ppu/Cargo.toml (gemini + Copilot): the feature comment claimed the bus_addr/octal_latch tracking is 'always-on and inert', but in this PR ALL of it (the bus_addr tracking AND the corruption hooks) is #[cfg(feature=…)]-gated, so the default build compiles none of it. Corrected the comment to say exactly that. - rustynes-ppu/src/ppu.rs hybrid_pending (gemini HIGH): documented the known flag-on-only limitation — hybrid_pending is transient one-shot state (not self-healing like bus_addr) and is deliberately not snapshotted, so a save-state captured in its narrow window under the experimental feature would drop it on restore. Harmless in the shipped build (feature off ⇒ field compiled out); must be resolved when the Option 1 (2-cycle-ALE) work promotes this path. Tracked in ADR 0030. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b39889c commit 519bdd5

4 files changed

Lines changed: 69 additions & 0 deletions

File tree

‎crates/rustynes-core/Cargo.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ ppu-state-trace = ["std", "rustynes-ppu/ppu-state-trace"]
4444
cpu-boot-trace = ["std"]
4545
# v2.0 R-1 core C-1 diagnostic — forward to rustynes-apu. Default-off.
4646
mc-r1-dmc-abort-probe = ["rustynes-apu/mc-r1-dmc-abort-probe"]
47+
# v2.0.1 (ADR 0030) — EXPERIMENTAL persistent-PPU-bus-address model (Option 2). Forward
48+
# to rustynes-ppu. Default-off; the shipped build is byte-identical without it.
49+
mc-ppu-bus-addr-hybrid = ["rustynes-ppu/mc-ppu-bus-addr-hybrid"]
4750
# v2.0 R1c-1 diagnostic — per-instruction (PC, cpu_cycle) trace. Forward to rustynes-cpu.
4851
cpu-instr-cycle-trace = ["rustynes-cpu/cpu-instr-cycle-trace"]
4952
# v1.1.0 beta.2 (Workstream C) — debugger devtools hooks: PC/exec breakpoints (and,

‎crates/rustynes-ppu/Cargo.toml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ hd-pack = []
4343
# are reached only through the gated post-frame poke path (never the run loop),
4444
# so determinism + the AccuracyCoin contract are unaffected.
4545
debug-hooks = []
46+
# v2.0.1 (ADR 0030) — EXPERIMENTAL persistent-PPU-bus-address model (Option 2 /
47+
# Mesen2's `_ppuBusAddress` + 3-dot-delayed `v`), the attempt to pass the two
48+
# newest AccuracyCoin PPU tests ("Hybrid Addresses" $0492, "ALE + Read" $0491).
49+
# Off by default so the SHIPPED build is byte-identical and stays at its measured
50+
# AccuracyCoin value: EVERYTHING this feature adds — the `bus_addr` address
51+
# tracking AND the behaviour-changing `$2006` hybrid-fetch / `$2007` ALE+Read
52+
# corruption — is `#[cfg(feature = "mc-ppu-bus-addr-hybrid")]`-gated, so the
53+
# default build compiles NONE of it (the fields do not even exist). See ADR 0030.
54+
mc-ppu-bus-addr-hybrid = []
4655

4756
[dependencies]
4857
bitflags.workspace = true

‎crates/rustynes-ppu/src/ppu.rs‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,34 @@ pub struct Ppu {
283283
/// v3 tail (refreshed every rendered scanline).
284284
pub(crate) ppudata_spr0_nt_addr: u16,
285285

286+
// === v2.0.1 (ADR 0030) persistent PPU bus-address model (Option 2) ===
287+
// ENTIRELY `mc-ppu-bus-addr-hybrid`-gated — these fields, their init, and
288+
// every read/write of them are compiled out of the default build, so the
289+
// shipped binary is byte-identical (the feature is off by design; ADR 0030).
290+
/// The last 14-bit address the PPU drove onto its multiplexed AD/A pins —
291+
/// Mesen2's `_ppuBusAddress`. Fed by every `read_vram`. Its low 8 bits are
292+
/// the external octal-latch value the hybrid-address corruption reads. Not
293+
/// snapshotted (it self-heals on the next fetch).
294+
#[cfg(feature = "mc-ppu-bus-addr-hybrid")]
295+
pub(crate) bus_addr: u16,
296+
/// A one-shot override address for the next background nametable fetch, set
297+
/// when a `$2006` second write lands during a rendering fetch (the
298+
/// hybrid-address case).
299+
///
300+
/// KNOWN LIMITATION (flag-on only): unlike `bus_addr`, this is NOT
301+
/// self-healing — it is transient one-shot state that lives from the `$2006`
302+
/// write until the next background nametable fetch (a few PPU dots), and it
303+
/// is deliberately NOT serialized in the snapshot (the scaffold avoids a
304+
/// format-version bump for an experiment that does not yet pass). So a
305+
/// save-state captured inside that narrow window, under this experimental
306+
/// feature, would drop the pending override on restore. This is harmless in
307+
/// the SHIPPED build (the feature is off, so the field is compiled out
308+
/// entirely) and must be resolved — by snapshotting it or making the fetch
309+
/// model self-consistent — when the Option 1 (2-cycle-ALE) work promotes
310+
/// this path. Tracked in ADR 0030.
311+
#[cfg(feature = "mc-ppu-bus-addr-hybrid")]
312+
pub(crate) hybrid_pending: Option<u16>,
313+
286314
// === Internal scroll/address registers (loopy v/t/x/w) ===
287315
/// 15-bit "current VRAM address".
288316
pub(crate) v: u16,
@@ -745,6 +773,10 @@ impl Ppu {
745773
spr_fetch_lo_raw: [0; 8],
746774
spr_fetch_hi_raw: [0; 8],
747775
ppudata_spr0_nt_addr: 0x2000,
776+
#[cfg(feature = "mc-ppu-bus-addr-hybrid")]
777+
bus_addr: 0,
778+
#[cfg(feature = "mc-ppu-bus-addr-hybrid")]
779+
hybrid_pending: None,
748780
v: 0,
749781
t: 0,
750782
x: 0,
@@ -1810,6 +1842,16 @@ impl Ppu {
18101842
self.t = (self.t & 0xFF00) | u16::from(value);
18111843
self.v = self.t;
18121844
self.w = false;
1845+
// v2.0.1 (ADR 0030) — "Hybrid Addresses": a `$2006` second
1846+
// write during a rendering fetch composes the in-flight
1847+
// nametable fetch from {new v high 6}:{octal-latch low 8}
1848+
// (e.g. v -> $2F00 with the latch holding $19 from the prior
1849+
// $2C19 fetch => $2F19). The next `fetch_nt` consumes this.
1850+
// Flag-gated so the shipped build is byte-identical.
1851+
#[cfg(feature = "mc-ppu-bus-addr-hybrid")]
1852+
if self.mask.rendering_enabled() && self.is_render_scanline() {
1853+
self.hybrid_pending = Some((self.v & 0x3F00) | (self.bus_addr & 0xFF));
1854+
}
18131855
// PPUADDR write can flip A12.
18141856
self.observe_a12(bus);
18151857
} else {
@@ -1882,6 +1924,13 @@ impl Ppu {
18821924
#[allow(clippy::needless_pass_by_ref_mut)]
18831925
fn read_vram<B: PpuBus>(&mut self, bus: &mut B, addr: u16) -> u8 {
18841926
let a = addr & 0x3FFF;
1927+
// v2.0.1 (ADR 0030) — latch the address just driven onto the multiplexed
1928+
// PPU bus (Mesen2's `_ppuBusAddress`); the octal latch is its low byte.
1929+
// Entirely feature-gated (compiled out of the shipped build).
1930+
#[cfg(feature = "mc-ppu-bus-addr-hybrid")]
1931+
{
1932+
self.bus_addr = a;
1933+
}
18851934
let val = if a < 0x2000 {
18861935
bus.ppu_read(a)
18871936
} else {
@@ -2433,6 +2482,11 @@ impl Ppu {
24332482
} else {
24342483
0x2000 | (self.v & 0x0FFF)
24352484
};
2485+
// v2.0.1 (ADR 0030) — a pending `$2006` hybrid address overrides this
2486+
// fetch's nametable address exactly once. Flag-gated + `take()`s so the
2487+
// shipped build is byte-identical (the field is always `None` there).
2488+
#[cfg(feature = "mc-ppu-bus-addr-hybrid")]
2489+
let nt_addr = self.hybrid_pending.take().unwrap_or(nt_addr);
24362490
self.nt_latch = self.read_vram(bus, nt_addr);
24372491
// Latch any per-tile extended-attribute info (MMC5 ExGrafix). Skip
24382492
// when split is active: the alt region uses standard 4-bit AT

‎crates/rustynes-test-harness/Cargo.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ ppu-state-trace = ["rustynes-core/ppu-state-trace"]
4141
hd-pack = ["rustynes-core/hd-pack"]
4242
# v2.0 R-1 core C-1 diagnostic — forward to rustynes-core. Default-off.
4343
mc-r1-dmc-abort-probe = ["rustynes-core/mc-r1-dmc-abort-probe"]
44+
# v2.0.1 (ADR 0030) — EXPERIMENTAL persistent-PPU-bus-address model (Option 2). Forward
45+
# to rustynes-core so the AccuracyCoin battery can run with the flag on.
46+
mc-ppu-bus-addr-hybrid = ["rustynes-core/mc-ppu-bus-addr-hybrid"]
4447
# v2.0 R1c-1 diagnostic — per-instruction (PC, cpu_cycle) trace. Forward to rustynes-core.
4548
cpu-instr-cycle-trace = ["rustynes-core/cpu-instr-cycle-trace"]
4649
# Forward to `rustynes-core/cpu-boot-trace`. Off by default; enabled only by

0 commit comments

Comments
 (0)