Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **`E2.10` — the full 256-opcode SPC700 cycle sweep, and the coverage number moves to 360 of 443.**
The cart measures how long every opcode the SPC700 can execute in a straight line actually takes,
and compares it on-cart against the cycle count **fullsnes** documents for it. The host supplies
no expected values; it reads back three bytes — opcodes measured, opcodes disagreeing, and the
first one that did.

**How one opcode is timed.** `T2OUT` steps once every 16 opcode cycles, far too coarse for a
single instruction, so the sweep never times one. It times a block of six copies, sixteen times
over, against the same block built from `NOP`. Everything around the copies — a fixed four-byte
prologue, a fixed five-byte epilogue, the `CALL`, the register reset, the poll — is identical in
every arm and cancels in the difference, which leaves **six ticks per cycle** against a
quantisation of ±1 on each side.

**Branches are measured taken.** A relative branch with a displacement of *zero* lands on the
following instruction, which is where a not-taken branch would have gone anyway — so a block of
six runs straight through whichever way each one goes, and the arm can arrange the taken path.
The prologue that arranges it is two two-cycle immediates whatever flag it has to set, so
choosing one costs nothing that could leak into a difference.

**The table is built from rules, and the build fails if the rules do not tile the map.** fullsnes
documents the opcode map *as* rules (`OR/AND/EOR/CMP/ADC/SBC` share one operand column, the shift
and increment group another, the bit ops a third), and `spc_opcodes.rs` follows them; a slot filled
twice or left empty is a build failure, not a shipped hole. The operand kind is **recorded** at
construction rather than derived from the opcode byte afterwards — the first draft derived it from
the low nibble, which is nearly right and hides at least four traps, one of which gave
`MOV [aa+X],A` a pointer read from uninitialised memory, which is zero, which is the driver's own
variables.

**Twenty-five opcodes are excluded, by name and with reasons.** The absolute jumps and calls (one
encoding cannot make six copies at six addresses each fall into the next), the vectored calls
(`TCALL`, `PCALL`, `BRK` — their vectors are in the IPL ROM every Group E program keeps mapped),
the returns (they pop an address the block never pushed), and `SLEEP`/`STOP`, for which fullsnes
itself gives the cycle count as `?`. For those the question has no answer. The count of what *was*
measured is asserted on-cart at 231, so a sweep covering a different set fails rather than quietly
reporting no disagreements.

**Verified by injection, twice.** Adding one idle cycle to `XCN` produced exactly one disagreement,
first-bad `$9F` — the opcode broken. Removing one idle cycle from the taken relative branch
produced exactly nine, first-bad `$10` — the eight conditional branches plus `BRA`, which is also
the proof that the taken-path prologues really do take the branch in all eight conditions.

Two things had to be measured rather than reasoned about. The results page was first placed at
`$0900` and the uploaded image reached `$0948`, so the sweep overwrote its own driver as it
recorded — a build-time assertion now rejects that layout. And the shared bounded APU wait is a
fraction of a frame, right for every other Group E program and far too short for this one, which
runs about **42 frames**: the row reported SKIP while the SPC was working correctly and had
already reached the right answer in RAM.


### Fixed

- **`E3.06` polls BOTH timers; a single end-of-interval read measured a phase, not a count.** The row
Expand Down
15 changes: 12 additions & 3 deletions crates/rustysnes-test-harness/tests/accuracysnes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -595,8 +595,11 @@ const A5_08_SLOTS: [(u8, &str); 7] = [
(6, "8x (PHD+PLD) - 16 NOP (expect 76)"),
];

/// The measurement slots the `v1.29.0` Group E batch records.
const GROUP_E_BATCH_SLOTS: [(u16, &str); 19] = [
/// The Group E measurement slots reported for cross-emulator comparison.
///
/// Version-agnostic on purpose: this list grows whenever a Group E row records a slot, and naming
/// one release in the doc made it wrong the first time it did.
const GROUP_E_BATCH_SLOTS: [(u16, &str); 25] = [
(256, "E8.01 KON sweep key-on mask (bit 7 = voice 0)"),
(
257,
Expand Down Expand Up @@ -626,6 +629,12 @@ const GROUP_E_BATCH_SLOTS: [(u16, &str); 19] = [
"B2.07 APU ticks over 48 frames (~6398 NTSC, ~7689 PAL)",
),
(276, "B2.07 measured frame height (261 NTSC, 311 PAL)"),
(280, "E2.10 opcodes disagreeing with fullsnes (expect 0)"),
(281, "E2.10 opcodes measured (expect 231)"),
(282, "E2.10 first disagreeing opcode ($FF = none)"),
(277, "E2.10i 8xNOP x32 timer-2 ticks (baseline)"),
(278, "E2.10i 8xXCN x32 (expect baseline + 48)"),
(279, "E2.10i 8xMUL YA x32 (expect baseline + 112)"),
(
266,
"E3.06 timer 0 ticks, accumulated across polls (expect ~13)",
Expand All @@ -647,7 +656,7 @@ fn group_e_batch_measurements_are_reported() {
let report = run().expect("battery must run");
assert!(report.done, "battery did not finish");

let mut out = String::from("\n v1.29.0 Group E batch measurements:\n");
let mut out = String::from("\n Group E measurements:\n");
for (slot, what) in GROUP_E_BATCH_SLOTS {
let v = report.meas[slot as usize];
let _ = writeln!(out, " slot {slot} {v:5} (${v:04X}) {what}");
Expand Down
5 changes: 3 additions & 2 deletions docs/accuracysnes-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Every sub-group of Part V is enumerated, so this is a **complete** statement of
| `D2` | 14 | 6 | 0 | 0 | D2.01, D2.02, D2.08, D2.10, D2.11-14, D2.15, D2.16, D2.17 |
| `D3` | 2 | 0 | 0 | 0 | D3.01, D3.02 |
| `E1` | 15 | 14 | 0 | 0 | E1.11 |
| `E2` | 10 | 9 | 0 | 0 | E2.10 |
| `E2` | 10 | 10 | 0 | 0 | |
| `E3` | 14 | 12 | 0 | 0 | E3.07, E3.12 |
| `E4` | 11 | 7 | 0 | 0 | E4.05, E4.07, E4.09, E4.10 |
| `E5` | 13 | 13 | 0 | 0 | — |
Expand All @@ -52,7 +52,7 @@ Every sub-group of Part V is enumerated, so this is a **complete** statement of
| `F1` | 22 | 13 | 0 | 0 | F1.13, F1.15, F1.16, F1.17, F1.18, F1.19, F1.20, F1.21, F1.22 |
| `G1` | 18 | 15 | 0 | 2 | G1.13 |

**302 of 443** enumerated assertion rows covered by an on-cart test, plus **55** covered only by a rendered scene (`docs/adr/0013`) and **2** covered only by a host-side test — **359 of 443** in total.
**303 of 443** enumerated assertion rows covered by an on-cart test, plus **55** covered only by a rendered scene (`docs/adr/0013`) and **2** covered only by a host-side test — **360 of 443** in total.

The three columns are kept apart on purpose, in descending order of what the evidence is worth. An on-cart result means the same thing on any emulator and on real hardware; a rendered scene needs a host holding the golden; a **host-side** cover is this project testing its own code, which is the one thing AccuracySNES exists to stop being the only evidence. The host tier is admitted only where the cart *physically cannot* observe the assertion — the stimulus comes from outside the cartridge, or the subject is the loader rather than the machine — and every entry names the test and the reason (`dossier.rs::HOST_COVERED`). Adding the columns into one figure would quietly change what the number claims.

Expand Down Expand Up @@ -135,6 +135,7 @@ Declared in `gen/src/scenes.rs`. Each is reported by the host framebuffer oracle

## Tests with no enumerated assertion

- **`E2.10i`** — The apparatus for E2.10's 256-opcode cycle sweep, plus the three opcodes that prove it works (NOP = 2, XCN = 5, MUL YA = 9). It implements no enumerated assertion because E2.10 is a FULL 256-opcode sweep and three opcodes are not that -- claiming the row from a validation set is exactly the overclaim the provenance tier exists to stop. It is a golden so the measured tick totals cross-validate across emulators. E2.10 itself is now claimed by the full sweep in tests/apu_sweep.rs; this row stays because its three raw tick totals cross-validate directly, where the sweep's verdict is a pass/fail count
- **`B4.16`** — The before/after guard for T-06-A. It records where an H-IRQ fires at an HTIME below the long dots and one above, because nothing else covers raster-IRQ position -- so the dot-model change would pass its own acceptance criteria while shifting every H-IRQ. It implements no enumerated assertion of its own; B4.07 and B4.14 own the H-IRQ rows
- **`E3.11c`** — DSP global-register addressing. The companion to E3.11b: the global block is decoded from the same latch by a different part of the address, so a core that gets the voice registers right and aliases the globals passes one and fails the other
- **`E3.11b`** — DSP register addressing through the $F2/$F3 latch. Not an enumerated assertion of its own: it is the mechanism every other DSP assertion is reached through, so a core that mis-decodes it makes those tests meaningless rather than failing
Expand Down
2 changes: 1 addition & 1 deletion docs/accuracysnes-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ AccuracySNES closed ticket **T-04**. The follow-on tickets minted here are **T-0
| | |
|---|---|
| Tests | **343** (scoring + golden vectors + region SKIP per image) — *tests, not assertions; see the note below the table* |
| Assertion coverage | **359 of 443** dossier assertions — **302 on-cart** + **55 rendered scenes** + **2 host-side**, kept as separate columns in descending order of what the evidence is worth (`docs/accuracysnes-coverage.md`) |
| Assertion coverage | **360 of 443** dossier assertions — **303 on-cart** + **55 rendered scenes** + **2 host-side**, kept as separate columns in descending order of what the evidence is worth (`docs/accuracysnes-coverage.md`) |
| Rendered scenes | **55** declared, all blessed and matching on both scene hosts (`docs/adr/0013`); **55** dossier rows have a scene as their only cover. One uses the `hires-even` extraction (`C5.15`, Mode 5) |
| Pass rate | **100.00%** on-cart, floor enforced at 1.00 by `tests/accuracysnes.rs` |
| Cross-validated | **Three references** as of `v1.29.0`. Mesen2 agrees on every test but `F1.03`, which clocks both ports out of one latch and so needs the port-2 input its Lua runner cannot drive; snes9x has 14 recorded divergences and **ares** 3, each with a citation in `scripts/accuracysnes/crossval.sh`. A headless **MesenCE** is separately the per-dot compositor's blueprint + exact-frame oracle. All images. |
Expand Down
15 changes: 15 additions & 0 deletions tests/roms/AccuracySNES/ERROR_CODES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2697,6 +2697,21 @@ Provenance: **Documented** (fullsnes and the SNESdev Wiki: writes to $00F0-$00FF
| 1 | `$02` | the control voice did not read back as a large positive value, so the copy in ordinary RAM is not decoding either -- this is a broken setup, not a statement about the shadow |
| 2 | `$04` | a sample placed at $00F7 through the register block did not decode to what the identical copy in ordinary RAM decodes to, so those writes reached the registers only and the RAM underneath them never saw them |

### E2.10i — SPC cycle instrument

Provenance: **Documented** (the apparatus for E2.10's 256-opcode sweep, validated against NOP = 2, XCN = 5 (also E1.14) and MUL YA = 9 -- each independently derivable from spc700_exec.rs's read/idle sequence). Kind: golden vector, never scored.

No failure codes — this is a **golden vector**. It cannot fail: it records what it observed and is excluded from the pass rate. Where the observation fits in a byte it goes in the verdict as a variant code (`(variant << 1) | 1`); where it does not — a dot count, say — the verdict is a plain pass and the value goes to the measurement channel at `$7E:E200`, which the host harness reads and prints. See the test's entry in `SOURCE_CATALOG.tsv` for its provenance tier and the reason it records rather than asserts.

### E2.10 — 256-opcode cycle sweep

Provenance: **Documented** (fullsnes, SNES APU SPC700 CPU instruction set). Kind: scored.

| Code | Byte | Meaning |
|---|---|---|
| 1 | `$02` | the sweep did not measure 231 opcodes — it either stopped early or covered a different set than the 25 documented non-straight-line exclusions. Slot 281 holds the count it reached |
| 2 | `$04` | at least one opcode's measured timing disagrees with the cycle count fullsnes documents for it by more than half a cycle. Slot 280 holds how many, slot 282 the first — and one cycle is six ticks here, so a disagreement is a whole cycle or more, not rounding |

### E9.02 — Noise output is bipolar

Provenance: **Documented** (fullsnes and anomie's DSP doc [ERRATA]: the noise output is highpass-filtered as a consequence of the 15-bit shift register being interpreted as the top bits of a signed 16-bit sample). Kind: scored.
Expand Down
Loading
Loading