From 37eb1f528b7b9bf4a36e651044a23b59ec1e7164 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Fri, 21 Aug 2026 13:54:16 -0400 Subject: [PATCH] test(cosim): pin the CpuBootTrace wire layout to a literal The testbench in the sibling repository now writes this format, so cpu_boot_trace_diff -- already written, already tested, and already used against a third-party reference emulator -- reads a device-under-test's output with no modification at all. Demonstrated rather than asserted: a C++-written 256-record trace loads and reports "All 256 aligned records match", and a corrupted register is located at cyc=307 PC=$C064 A=$64 against A=$9B. The pin exists so the two implementations are anchored to the SAME hardcoded bytes rather than to each other. Anchoring them to each other has a specific failure: they can agree on something wrong, and neither test notices. Anchoring both to a literal means a drift on either side reddens that side's own test, at the moment the drift happens -- rather than at co-simulation time, where a format difference is indistinguishable from a DUT defect and would be debugged as one. It also pins the constants the C++ header duplicates: the magic, the schema version, HEADER_SIZE and RECORD_SIZE. Those are four more values that live in two repositories with nothing mechanical connecting them. scanline = -1 in the pinned record is deliberate, not an arbitrary value. The pre-render line is negative, and a writer that clamped or saturated instead of writing two's complement would pass every test that only ever used a positive scanline -- the C++ side carries a matching check that a POSITIVE scanline encodes differently, so neither case is proving something both branches would satisfy. Demonstrated to fail: changing the two scanline bytes in the expectation reddens the test. Gates. Test-only in the excluded crate; the emulation core is untouched, so AccuracyCoin 141/141 and nestest 0-diff hold by construction. fmt, clippy -D warnings and rustdoc -D warnings on the excluded crate, markdownlint on the changed document. 4 excluded-crate suites / 43 passed / 0 failed (was 42) --- CHANGELOG.md | 17 ++++++++++ crates/rustynes-cosim/src/lib.rs | 54 ++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69b64af..863530ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -152,6 +152,23 @@ cycle-accurate core later replaced. because a positive control alone is satisfiable by a comparison that always agrees. +- **The `CpuBootTrace` wire layout is pinned to a literal, on both sides of the + co-simulation boundary.** The testbench writes this format so + `cpu_boot_trace_diff` — already written, already tested, and already used + against a third-party reference emulator — reads a device-under-test's output + with **no modification at all**. Demonstrated rather than asserted: a + C++-written 256-record trace loads and reports "All 256 aligned records match", + and a corrupted register is located at `cyc=307 PC=$C064 A=$64` vs `A=$9B`. + + Both sides are anchored to the **same hardcoded bytes** rather than to each + other, so a drift on either fails its own test instead of the two quietly + agreeing on something wrong — or disagreeing at co-simulation time, where a + format difference is indistinguishable from a DUT defect. + + The pinned record uses **`scanline = -1`** deliberately: the pre-render line is + negative, and a writer that clamped or saturated rather than writing two's + complement would pass every test that only ever used a positive scanline. + ### Fixed - **The excluded crate's lockfile was silently gitignored, so CI re-resolved it diff --git a/crates/rustynes-cosim/src/lib.rs b/crates/rustynes-cosim/src/lib.rs index 9860c684..4e5b1329 100644 --- a/crates/rustynes-cosim/src/lib.rs +++ b/crates/rustynes-cosim/src/lib.rs @@ -799,6 +799,60 @@ mod tests { assert!(!a.checkpoints.expect("no overflow").is_empty()); } + /// The `CpuBootTrace` wire format, pinned to a literal. + /// + /// The C++ testbench writes this format so `cpu_boot_trace_diff` reads a + /// DUT's output unmodified. Both sides are anchored to the **same + /// hardcoded bytes** rather than to each other -- so if either + /// implementation drifts, its own test fails, instead of the two agreeing + /// on something wrong or disagreeing at co-simulation time where it would + /// look like a DUT defect. + #[test] + fn the_boot_trace_record_layout_is_pinned() { + use rustynes_core::cpu_boot_trace::{ + BINARY_MAGIC, CPU_BOOT_TRACE_SCHEMA_VERSION, CpuBootRecord, HEADER_SIZE, RECORD_SIZE, + }; + assert_eq!(BINARY_MAGIC, b"RUSTYNES_CPU"); + assert_eq!(CPU_BOOT_TRACE_SCHEMA_VERSION, 1); + assert_eq!(HEADER_SIZE, 16); + assert_eq!(RECORD_SIZE, 32); + + let r = CpuBootRecord { + cycle: 0x0102_0304_0506_0708, + frame: 0x1122_3344, + // Negative on purpose: the pre-render line is -1, and a writer that + // clamped or saturated instead of writing two's complement would + // pass every test using a positive scanline. + scanline: -1, + dot: 0x0155, + pc: 0xC5F5, + a: 0x11, + x: 0x22, + y: 0x33, + p: 0x24, + s: 0xFD, + opcode: 0x4C, + op1: 0xF5, + op2: 0xC5, + flags: 0x01, + }; + assert_eq!( + r.to_bytes(), + [ + 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, // cycle + 0x44, 0x33, 0x22, 0x11, // frame + 0xFF, 0xFF, // scanline = -1 + 0x55, 0x01, // dot + 0xF5, 0xC5, // pc + 0x11, 0x22, 0x33, 0x24, 0xFD, // a x y p s + 0x4C, 0xF5, 0xC5, // opcode op1 op2 + 0x01, // flags + 0x00, 0x00, 0x00, 0x00, 0x00, // pad to 32 + ], + "boot-trace record layout changed; the C++ writer must change with it" + ); + } + /// The power-on latch, pinned rather than worked around silently. /// /// If this test ever fails it means the core stopped swallowing the first