From af780621690d6791b71c3bd5de87c8f58722ba9e Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Sun, 2 Aug 2026 06:16:34 -0400 Subject: [PATCH 1/3] =?UTF-8?q?feat(accuracysnes):=20E2.10=20=E2=80=94=20t?= =?UTF-8?q?he=20full=20256-opcode=20SPC700=20cycle=20sweep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Coverage 359 -> 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, leaving 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 fullsnes's own rules and the build fails if they do not tile the map. The operand KIND is recorded at construction rather than derived from the opcode byte -- 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, the vectored calls whose vectors are in the IPL ROM, the returns, and SLEEP/STOP, for which fullsnes itself gives the cycle count as `?`. 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. This also lands E2.10i, the smaller instrument the sweep grew out of: a NOP/XCN/MUL YA differential on a different shape (eight copies, thirty-two iterations) that reports its three raw tick totals rather than a verdict. It stays as a golden because those totals cross-validate directly, where the sweep's answer is a pass/fail count. Verified by injection twice. One extra idle cycle in XCN -> exactly one disagreement, first-bad $9F, the opcode broken. One idle cycle removed from the taken relative branch -> 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 at $0900 and the 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. Battery 310/310. Three references agree: snes9x, Mesen2 and ares all report 231 measured and zero disagreements, with their known-failure counts unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 51 ++ .../tests/accuracysnes.rs | 8 +- docs/accuracysnes-coverage.md | 5 +- tests/roms/AccuracySNES/ERROR_CODES.md | 15 + tests/roms/AccuracySNES/SOURCE_CATALOG.tsv | 122 +-- tests/roms/AccuracySNES/asm/tests_group_a.s | 513 +++++++++++- .../AccuracySNES/build/accuracysnes-pal.sfc | Bin 262144 -> 262144 bytes .../roms/AccuracySNES/build/accuracysnes.sfc | Bin 262144 -> 262144 bytes tests/roms/AccuracySNES/gen/src/dossier.rs | 12 + tests/roms/AccuracySNES/gen/src/main.rs | 1 + tests/roms/AccuracySNES/gen/src/spc.rs | 144 ++++ .../roms/AccuracySNES/gen/src/spc_opcodes.rs | 739 ++++++++++++++++++ tests/roms/AccuracySNES/gen/src/tests/apu.rs | 213 ++++- .../AccuracySNES/gen/src/tests/apu_sweep.rs | 632 +++++++++++++++ tests/roms/AccuracySNES/gen/src/tests/mod.rs | 2 + 15 files changed, 2356 insertions(+), 101 deletions(-) create mode 100644 tests/roms/AccuracySNES/gen/src/spc_opcodes.rs create mode 100644 tests/roms/AccuracySNES/gen/src/tests/apu_sweep.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 181a175d..a5bf5d75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/crates/rustysnes-test-harness/tests/accuracysnes.rs b/crates/rustysnes-test-harness/tests/accuracysnes.rs index 25a17bdc..a6c616c4 100644 --- a/crates/rustysnes-test-harness/tests/accuracysnes.rs +++ b/crates/rustysnes-test-harness/tests/accuracysnes.rs @@ -596,7 +596,7 @@ const A5_08_SLOTS: [(u8, &str); 7] = [ ]; /// The measurement slots the `v1.29.0` Group E batch records. -const GROUP_E_BATCH_SLOTS: [(u16, &str); 19] = [ +const GROUP_E_BATCH_SLOTS: [(u16, &str); 25] = [ (256, "E8.01 KON sweep key-on mask (bit 7 = voice 0)"), ( 257, @@ -626,6 +626,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)", diff --git a/docs/accuracysnes-coverage.md b/docs/accuracysnes-coverage.md index 36672034..8da9f4f0 100644 --- a/docs/accuracysnes-coverage.md +++ b/docs/accuracysnes-coverage.md @@ -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 | — | @@ -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. @@ -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 diff --git a/tests/roms/AccuracySNES/ERROR_CODES.md b/tests/roms/AccuracySNES/ERROR_CODES.md index 5b60928d..32097a68 100644 --- a/tests/roms/AccuracySNES/ERROR_CODES.md +++ b/tests/roms/AccuracySNES/ERROR_CODES.md @@ -2697,6 +2697,12 @@ 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. + ### 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. @@ -2708,6 +2714,15 @@ Provenance: **Documented** (fullsnes and anomie's DSP doc [ERRATA]: the noise ou | 3 | `$06` | one step of the noise register did not flip the output's sign, so the register's top bit is not the output's sign bit: the core is emitting the 15-bit value directly, which is the DC-heavy noise the errata's highpass remark exists to exclude | | 4 | `$08` | the noise output turned positive after one step but is not the seed shifted right once or twice, so the output is some other function of the register than its top fifteen bits | +### 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 | + ## Group F ### F1.01 — Manual pad read order diff --git a/tests/roms/AccuracySNES/SOURCE_CATALOG.tsv b/tests/roms/AccuracySNES/SOURCE_CATALOG.tsv index 7febf06f..6f09af26 100644 --- a/tests/roms/AccuracySNES/SOURCE_CATALOG.tsv +++ b/tests/roms/AccuracySNES/SOURCE_CATALOG.tsv @@ -283,63 +283,65 @@ 280 E3.09 E Waits: CPU 10, timer 8 Documented ares and bsnes sfc/smp/timing.cpp, identically: cycleWaitStates {2,4,10,20} against timerWaitStates {2,4,8,16}, with the comment that the timers are not affected by the 8/16 divider glitch Scored 0x7EF138 E3.09 281 B2.07 B Frame rate vs APU clock Documented fullsnes and the SNESdev Wiki: NTSC 60.0988 Hz, PAL 50.00698 Hz; the APU's 24.576 MHz crystal is region-independent (ares apuFrequency, snes9x's two APU ratios) Scored 0x7EF139 B2.07,B2.08 282 E3.13 E Regs shadow into RAM Documented fullsnes and the SNESdev Wiki: writes to $00F0-$00FF reach the underlying APU RAM as well as the register, which the S-DSP can then read as sample data Scored 0x7EF13A E3.13 -283 E9.02 E Noise output is bipolar 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 Scored 0x7EF13B E9.02 -284 F1.01 F Manual pad read order Documented fullsnes and the SNESdev Wiki controller protocol: the shift register presents B, Y, Select, Start, Up, Down, Left, Right, A, X, L, R and then four zero bits Scored 0x7EF13C F1.01 -285 F1.02 F Pad reads 17+ are 1 Documented SNESdev Wiki, controller protocol; fullsnes Scored 0x7EF13D F1.02 -286 F1.03 F Shared $4016 latch Documented fullsnes and the SNESdev Wiki controller protocol: bit 0 of $4016 is the shared latch line that parallel-loads both controller ports' shift registers Scored 0x7EF13E F1.03 -287 F1.04 F $4016 bits 7-2 open bus Corroborated RustySNES, snes9x and Mesen2 all return $41 for the absolute read and $01 for the long one -- identical bytes, so bits 7-2 follow the CPU bus in all three Scored 0x7EF13F F1.04 -288 F1.07 F Auto-read needs $4200.0 Documented fullsnes and the SNESdev Wiki: bit 0 of $4200 arms the automatic joypad read, and with it clear $4218-$421F are not written Scored 0x7EF140 F1.07 -289 F1.05 F Pad signature is 0000 Documented fullsnes and the SNESdev Wiki: bits 3-0 of the auto-read result identify the device, and a standard controller reports 0000 Scored 0x7EF141 F1.05 -290 F1.06 F First bit clocked is B Documented fullsnes and the SNESdev Wiki: the auto-read result holds the sixteen shifted bits in clock order, most significant first, so B is bit 15 Scored 0x7EF142 F1.06 -291 F1.08 F Auto-read start dot Documented fullsnes: the automatic joypad read begins ~dot 32.5-95.5 of the first vblank line (RustySNES delays it 256 master clocks ~ dot 64), not at the vblank edge Golden 0x7EF143 F1.08 -292 F1.09 F Auto-read duration Documented fullsnes: the automatic joypad read is busy for exactly 4224 master cycles (~3.097 scanlines); the poll count is a CPU-cycle-proportional proxy for it Golden 0x7EF144 F1.09 -293 F1.10 F Auto-read start race Documented fullsnes: the automatic joypad read begins ~dot 32.5-95.5 of the first vblank line, not at the vblank edge, so $4212 bit 0 reads not-busy for that window and a $4212 poll at NMI entry sees the read not yet started Scored 0x7EF145 F1.10 -294 F1.11 F Latch corrupts auto-read Documented fullsnes and the SNESdev Wiki: while $4016 bit 0 is high the shift registers reload continuously rather than shifting, so an automatic read taken across it returns the same bit in every position Scored 0x7EF146 F1.11 -295 F1.12 F Auto-read result timing Contested F1.12 says results are valid by V = $E3, which does not reconcile with F1.09's 4224-cycle duration and F1.08's start window; no source says which to believe, and the cores split on whether the result appears at once or progressively Golden 0x7EF147 F1.12 -296 F1.14 F $4213 reads $4201 back Documented fullsnes: RDIO reads the WRIO output pins, which are open-collector, so with nothing driving them low the value read is the value written Scored 0x7EF148 F1.14 -297 G1.02 G Reset: $4210/$4211 clear Documented SNESdev Wiki, power-on state; fullsnes Scored 0x7EF149 G1.02 -298 G1.04 G Reset: emulation mode Documented SNESdev Wiki, power-on state; WDC 65C816 datasheet, XCE Scored 0x7EF14A G1.04 -299 G1.05 G Power-on PPU registers Documented the dossier marks the PPU power-on state indeterminate ('no boot ROM; most PPU registers start unknown') and says to report it, never assert; the readable registers ($2134-$2136, $213E/$213F) are reported and the only scored check is the self-guard that the power-on capture actually ran Scored 0x7EF14B G1.05 -300 G1.08 G Write-only read: openbus Documented SNESdev Wiki, open bus; fullsnes, memory map notes Scored 0x7EF14C G1.08 -301 G1.10 G Checksum XOR complement Documented SNESdev Wiki, cartridge header; fullsnes Scored 0x7EF14D G1.10 -302 G1.11 G Checksum over the image Documented SNESdev Wiki, cartridge header checksum; fullsnes Scored 0x7EF14E G1.11 -303 G1.12 G LoROM header location Documented SNESdev Wiki, cartridge header; fullsnes Scored 0x7EF14F G1.12 -304 G1.14 G LoROM bank decode Documented SNESdev Wiki, memory map; fullsnes Scored 0x7EF150 G1.14 -305 G1.19 G Power-on $4201/timers Documented fullsnes and the SNESdev Wiki power-on table: $4201 = $FF, HTIME and VTIME = $1FF; the other registers in the row are observed by B5.05 and B1.01 Scored 0x7EF151 G1.01 -306 G1.20 G Power-on indeterminate Contested the dossier marks the whole row [UNDEFINED] and says to report it and never assert it; half the registers it names are write-only and cannot be reported at all Golden 0x7EF152 G1.03 -307 G1.07 G WRAM power-on fill Contested the dossier marks the row [UNDEFINED] and asks for a golden vector by name: no canonical WRAM fill exists and real consoles disagree Golden 0x7EF153 G1.07 -308 A5.S01 A Sweep: CLC Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF154 A5.01-08 -309 A5.S02 A Sweep: SEC Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF155 A5.01-08 -310 A5.S03 A Sweep: CLV Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF156 A5.01-08 -311 A5.S04 A Sweep: INX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF157 A5.01-08 -312 A5.S05 A Sweep: DEX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF158 A5.01-08 -313 A5.S06 A Sweep: TAX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF159 A5.01-08 -314 A5.S07 A Sweep: TXY Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15A A5.01-08 -315 A5.S08 A Sweep: ASL A Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15B A5.01-08 -316 A5.S09 A Sweep: XBA Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15C A5.01-08 -317 A5.S10 A Sweep: TCD Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15D A5.01-08 -318 A5.S11 A Sweep: LDA #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15E A5.01-08 -319 A5.S12 A Sweep: LDX #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15F A5.01-08 -320 A5.S13 A Sweep: CMP #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF160 A5.01-08 -321 A5.S14 A Sweep: BIT #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF161 A5.01-08 -322 A5.S15 A Sweep: REP #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF162 A5.01-08 -323 A5.S16 A Sweep: SEP #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF163 A5.01-08 -324 A5.S17 A Sweep: WDM Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF164 A5.01-08 -325 A5.S18 A Sweep: PHA+PLA Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF165 A5.01-08 -326 A5.S19 A Sweep: PHP+PLP Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF166 A5.01-08 -327 A5.S20 A Sweep: PHB+PLB Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF167 A5.01-08 -328 A5.S21 A Sweep: PHD+PLD Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF168 A5.01-08 -329 A5.S22 A Sweep: PHX+PLX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF169 A5.01-08 -330 A5.S23 A Sweep: LDA dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16A A5.01-08 -331 A5.S24 A Sweep: LDA abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16B A5.01-08 -332 A5.S25 A Sweep: LDA long Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16C A5.01-08 -333 A5.S26 A Sweep: STA dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16D A5.01-08 -334 A5.S27 A Sweep: STA abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16E A5.01-08 -335 A5.S28 A Sweep: LDA dp,X Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16F A5.01-08 -336 A5.S29 A Sweep: LDA abs,X Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF170 A5.01-08 -337 A5.S30 A Sweep: INC dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF171 A5.01-08 -338 A5.S31 A Sweep: INC abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF172 A5.01-08 -339 A5.S32 A Sweep: ADC dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF173 A5.01-08 -340 A5.S33 A Sweep: CMP abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF174 A5.01-08 -341 A5.S34 A Sweep: BVS untaken Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF175 A5.01-08 -342 A5.S35 A Sweep: BRL flat 4 Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF176 A5.16 +283 E2.10i E SPC cycle instrument 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 Golden 0x7EF13B - +284 E9.02 E Noise output is bipolar 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 Scored 0x7EF13C E9.02 +285 E2.10 E 256-opcode cycle sweep Documented fullsnes, SNES APU SPC700 CPU instruction set Scored 0x7EF13D E2.10 +286 F1.01 F Manual pad read order Documented fullsnes and the SNESdev Wiki controller protocol: the shift register presents B, Y, Select, Start, Up, Down, Left, Right, A, X, L, R and then four zero bits Scored 0x7EF13E F1.01 +287 F1.02 F Pad reads 17+ are 1 Documented SNESdev Wiki, controller protocol; fullsnes Scored 0x7EF13F F1.02 +288 F1.03 F Shared $4016 latch Documented fullsnes and the SNESdev Wiki controller protocol: bit 0 of $4016 is the shared latch line that parallel-loads both controller ports' shift registers Scored 0x7EF140 F1.03 +289 F1.04 F $4016 bits 7-2 open bus Corroborated RustySNES, snes9x and Mesen2 all return $41 for the absolute read and $01 for the long one -- identical bytes, so bits 7-2 follow the CPU bus in all three Scored 0x7EF141 F1.04 +290 F1.07 F Auto-read needs $4200.0 Documented fullsnes and the SNESdev Wiki: bit 0 of $4200 arms the automatic joypad read, and with it clear $4218-$421F are not written Scored 0x7EF142 F1.07 +291 F1.05 F Pad signature is 0000 Documented fullsnes and the SNESdev Wiki: bits 3-0 of the auto-read result identify the device, and a standard controller reports 0000 Scored 0x7EF143 F1.05 +292 F1.06 F First bit clocked is B Documented fullsnes and the SNESdev Wiki: the auto-read result holds the sixteen shifted bits in clock order, most significant first, so B is bit 15 Scored 0x7EF144 F1.06 +293 F1.08 F Auto-read start dot Documented fullsnes: the automatic joypad read begins ~dot 32.5-95.5 of the first vblank line (RustySNES delays it 256 master clocks ~ dot 64), not at the vblank edge Golden 0x7EF145 F1.08 +294 F1.09 F Auto-read duration Documented fullsnes: the automatic joypad read is busy for exactly 4224 master cycles (~3.097 scanlines); the poll count is a CPU-cycle-proportional proxy for it Golden 0x7EF146 F1.09 +295 F1.10 F Auto-read start race Documented fullsnes: the automatic joypad read begins ~dot 32.5-95.5 of the first vblank line, not at the vblank edge, so $4212 bit 0 reads not-busy for that window and a $4212 poll at NMI entry sees the read not yet started Scored 0x7EF147 F1.10 +296 F1.11 F Latch corrupts auto-read Documented fullsnes and the SNESdev Wiki: while $4016 bit 0 is high the shift registers reload continuously rather than shifting, so an automatic read taken across it returns the same bit in every position Scored 0x7EF148 F1.11 +297 F1.12 F Auto-read result timing Contested F1.12 says results are valid by V = $E3, which does not reconcile with F1.09's 4224-cycle duration and F1.08's start window; no source says which to believe, and the cores split on whether the result appears at once or progressively Golden 0x7EF149 F1.12 +298 F1.14 F $4213 reads $4201 back Documented fullsnes: RDIO reads the WRIO output pins, which are open-collector, so with nothing driving them low the value read is the value written Scored 0x7EF14A F1.14 +299 G1.02 G Reset: $4210/$4211 clear Documented SNESdev Wiki, power-on state; fullsnes Scored 0x7EF14B G1.02 +300 G1.04 G Reset: emulation mode Documented SNESdev Wiki, power-on state; WDC 65C816 datasheet, XCE Scored 0x7EF14C G1.04 +301 G1.05 G Power-on PPU registers Documented the dossier marks the PPU power-on state indeterminate ('no boot ROM; most PPU registers start unknown') and says to report it, never assert; the readable registers ($2134-$2136, $213E/$213F) are reported and the only scored check is the self-guard that the power-on capture actually ran Scored 0x7EF14D G1.05 +302 G1.08 G Write-only read: openbus Documented SNESdev Wiki, open bus; fullsnes, memory map notes Scored 0x7EF14E G1.08 +303 G1.10 G Checksum XOR complement Documented SNESdev Wiki, cartridge header; fullsnes Scored 0x7EF14F G1.10 +304 G1.11 G Checksum over the image Documented SNESdev Wiki, cartridge header checksum; fullsnes Scored 0x7EF150 G1.11 +305 G1.12 G LoROM header location Documented SNESdev Wiki, cartridge header; fullsnes Scored 0x7EF151 G1.12 +306 G1.14 G LoROM bank decode Documented SNESdev Wiki, memory map; fullsnes Scored 0x7EF152 G1.14 +307 G1.19 G Power-on $4201/timers Documented fullsnes and the SNESdev Wiki power-on table: $4201 = $FF, HTIME and VTIME = $1FF; the other registers in the row are observed by B5.05 and B1.01 Scored 0x7EF153 G1.01 +308 G1.20 G Power-on indeterminate Contested the dossier marks the whole row [UNDEFINED] and says to report it and never assert it; half the registers it names are write-only and cannot be reported at all Golden 0x7EF154 G1.03 +309 G1.07 G WRAM power-on fill Contested the dossier marks the row [UNDEFINED] and asks for a golden vector by name: no canonical WRAM fill exists and real consoles disagree Golden 0x7EF155 G1.07 +310 A5.S01 A Sweep: CLC Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF156 A5.01-08 +311 A5.S02 A Sweep: SEC Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF157 A5.01-08 +312 A5.S03 A Sweep: CLV Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF158 A5.01-08 +313 A5.S04 A Sweep: INX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF159 A5.01-08 +314 A5.S05 A Sweep: DEX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15A A5.01-08 +315 A5.S06 A Sweep: TAX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15B A5.01-08 +316 A5.S07 A Sweep: TXY Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15C A5.01-08 +317 A5.S08 A Sweep: ASL A Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15D A5.01-08 +318 A5.S09 A Sweep: XBA Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15E A5.01-08 +319 A5.S10 A Sweep: TCD Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15F A5.01-08 +320 A5.S11 A Sweep: LDA #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF160 A5.01-08 +321 A5.S12 A Sweep: LDX #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF161 A5.01-08 +322 A5.S13 A Sweep: CMP #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF162 A5.01-08 +323 A5.S14 A Sweep: BIT #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF163 A5.01-08 +324 A5.S15 A Sweep: REP #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF164 A5.01-08 +325 A5.S16 A Sweep: SEP #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF165 A5.01-08 +326 A5.S17 A Sweep: WDM Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF166 A5.01-08 +327 A5.S18 A Sweep: PHA+PLA Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF167 A5.01-08 +328 A5.S19 A Sweep: PHP+PLP Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF168 A5.01-08 +329 A5.S20 A Sweep: PHB+PLB Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF169 A5.01-08 +330 A5.S21 A Sweep: PHD+PLD Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16A A5.01-08 +331 A5.S22 A Sweep: PHX+PLX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16B A5.01-08 +332 A5.S23 A Sweep: LDA dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16C A5.01-08 +333 A5.S24 A Sweep: LDA abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16D A5.01-08 +334 A5.S25 A Sweep: LDA long Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16E A5.01-08 +335 A5.S26 A Sweep: STA dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16F A5.01-08 +336 A5.S27 A Sweep: STA abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF170 A5.01-08 +337 A5.S28 A Sweep: LDA dp,X Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF171 A5.01-08 +338 A5.S29 A Sweep: LDA abs,X Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF172 A5.01-08 +339 A5.S30 A Sweep: INC dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF173 A5.01-08 +340 A5.S31 A Sweep: INC abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF174 A5.01-08 +341 A5.S32 A Sweep: ADC dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF175 A5.01-08 +342 A5.S33 A Sweep: CMP abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF176 A5.01-08 +343 A5.S34 A Sweep: BVS untaken Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF177 A5.01-08 +344 A5.S35 A Sweep: BRL flat 4 Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF178 A5.16 diff --git a/tests/roms/AccuracySNES/asm/tests_group_a.s b/tests/roms/AccuracySNES/asm/tests_group_a.s index c1c7c445..c8f87d2a 100644 --- a/tests/roms/AccuracySNES/asm/tests_group_a.s +++ b/tests/roms/AccuracySNES/asm/tests_group_a.s @@ -23938,6 +23938,103 @@ CATALOG_IMPL = 1 jml test_restore .endproc +; 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) +.proc test_e2_10i + .a16 + .i16 + rep #$30 + .a16 + .i16 + phk + plb + ; Point apu_upload at this test's own program image, which lives in another bank. + lda #.loword(apu_prog_119) + sta f:V_APU_SRC + sep #$20 + .a8 + lda #^apu_prog_119 + sta f:V_APU_BANK + rep #$30 + .a16 + .i16 + lda #146 + sta f:V_APU_LEN + lda #$0200 + sta f:V_APU_DEST ; APU RAM $0200: clear of the zero page and the stack + lda #$0200 + sta f:V_APU_ENTRY + jsl apu_upload_far + ; Clear the CPU-side port 0 before the program can look at it. The previous test left the + ; release byte there, and a program whose release loop sees it immediately jumps back to + ; the IPL before the cart has read a thing — which reads as a wrong answer, not a race. + sep #$20 + .a8 + lda #$00 + sta APUIO0 + ; Wait for the program's done marker, but not forever: an APU that never boots would + ; otherwise hang the whole battery and report nothing about any other test. + rep #$30 + .a16 + .i16 + ldx #$0000 +@wait: + sep #$20 + .a8 + lda APUIO0 + cmp #$5A + beq @ran + rep #$30 + .a16 + .i16 + inx + cpx #$8000 + bne @wait + jmp @timeout +@ran: + ; Copy the answers out BEFORE releasing the program: once it jumps to the IPL, the boot ROM + ; overwrites ports 0 and 1 with its $AA/$BB announcement. + sep #$20 + .a8 + lda APUIO1 + sta f:$7E0100 + lda APUIO2 + sta f:$7E0101 + lda APUIO3 + sta f:$7E0102 + ; Release: the program hands the APU back to the IPL so the NEXT test can upload at all. + lda #$A5 + sta APUIO0 + rep #$30 + .a16 + .i16 + lda f:$7E0100 + and #$00FF + ; record slot 277: E2.10 instrument: 8xNOP x32, timer-2 ticks (the baseline) + sta f:$7EE42A + lda f:$7E0101 + and #$00FF + ; record slot 278: E2.10 instrument: 8xXCN x32 (expect baseline + 48) + sta f:$7EE42C + lda f:$7E0102 + and #$00FF + ; record slot 279: E2.10 instrument: 8xMUL YA x32 (expect baseline + 112) + sta f:$7EE42E + bra @pass +@timeout: + sep #$20 + .a8 + lda #$FF + sta f:V_TEST_RESULT ; SKIP: the APU never published a done marker + jml test_restore +@pass: + sep #$20 + .a8 + lda #$01 + sta f:$7EE010 + jml test_restore +.endproc + ; 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) .proc test_e9_02 @@ -23966,11 +24063,11 @@ CATALOG_IMPL = 1 phk plb ; Point apu_upload at this test's own program image, which lives in another bank. - lda #.loword(apu_prog_119) + lda #.loword(apu_prog_120) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_119 + lda #^apu_prog_120 sta f:V_APU_BANK rep #$30 .a16 @@ -24117,6 +24214,156 @@ CATALOG_IMPL = 1 jml test_restore .endproc +; E2.10 — 256-opcode cycle sweep +; provenance: Documented (fullsnes, SNES APU SPC700 CPU instruction set) +.proc test_e2_10 + .a16 + .i16 + rep #$30 + .a16 + .i16 + phk + plb + ; Point apu_upload at this test's own program image, which lives in another bank. + lda #.loword(apu_prog_121) + sta f:V_APU_SRC + sep #$20 + .a8 + lda #^apu_prog_121 + sta f:V_APU_BANK + rep #$30 + .a16 + .i16 + lda #1905 + sta f:V_APU_LEN + lda #$0200 + sta f:V_APU_DEST ; APU RAM $0200: clear of the zero page and the stack + lda #$0200 + sta f:V_APU_ENTRY + jsl apu_upload_far + ; Clear the CPU-side port 0 before the program can look at it. The previous test left the + ; release byte there, and a program whose release loop sees it immediately jumps back to + ; the IPL before the cart has read a thing — which reads as a wrong answer, not a race. + sep #$20 + .a8 + lda #$00 + sta APUIO0 + ; Wait for the done marker, but not forever — see this proc's doc comment for why the + ; shared wait is far too short for this one program. + sep #$20 + .a8 + lda #$00 + sta f:$7E01F8 ; the outer pass counter, in the same scratch page E3.06 uses + rep #$30 + .a16 + .i16 + ldx #$0000 +@wait: + sep #$20 + .a8 + lda APUIO0 + cmp #$5A + beq @ran + rep #$30 + .a16 + .i16 + inx + bne @wait + ; X wrapped: one full pass of 65536. Count it, and give up after sixteen. + sep #$20 + .a8 + lda f:$7E01F8 + inc a + sta f:$7E01F8 + cmp #$10 + rep #$30 + .a16 + .i16 + bne @wait + jmp @timeout +@ran: + ; Copy the answers out BEFORE releasing the program: once it jumps to the IPL, the boot ROM + ; overwrites ports 0 and 1 with its $AA/$BB announcement. + sep #$20 + .a8 + lda APUIO1 + sta f:$7E0100 + lda APUIO2 + sta f:$7E0101 + lda APUIO3 + sta f:$7E0102 + ; Release: the program hands the APU back to the IPL so the NEXT test can upload at all. + lda #$A5 + sta APUIO0 + rep #$30 + .a16 + .i16 + lda f:$7E0100 + and #$00FF + ; record slot 280: E2.10 opcodes disagreeing with fullsnes (expect 0) + sta f:$7EE430 + lda f:$7E0101 + and #$00FF + ; record slot 281: E2.10 opcodes measured (expect 231 = 256 - 25) + sta f:$7EE432 + lda f:$7E0102 + and #$00FF + ; record slot 282: E2.10 first disagreeing opcode ($FF = none) + sta f:$7EE434 + ; Liveness first. A driver that fell over after one opcode would report no disagreements, + ; and a `0` in slot 280 would then read as a pass — so the count of opcodes actually + ; measured is asserted before anything is concluded from the count of failures. + lda f:$7E0101 + and #$00FF + cmp #$00E7 + bcs :+ + jmp @fail1 + : + cmp #$00E8 + bcc :+ + jmp @fail1 + : + ; The row itself: every measured opcode's timing agrees with the cycle count fullsnes + ; documents for it, to within half a cycle. + lda f:$7E0100 + and #$00FF + cmp #$0000 + bcs :+ + jmp @fail2 + : + cmp #$0001 + bcc :+ + jmp @fail2 + : + bra @pass +@timeout: + sep #$20 + .a8 + lda #$FF + sta f:V_TEST_RESULT ; SKIP: the APU never published a done marker + jml test_restore +@pass: + sep #$20 + .a8 + lda #$01 + sta f:$7EE010 + jml test_restore +@fail1: + ; 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 + sep #$20 + .a8 + lda #$02 + sta f:$7EE010 + jml test_restore +@fail2: + ; 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 + sep #$20 + .a8 + lda #$04 + sta f:$7EE010 + jml test_restore +.endproc + .segment "TESTSC" ; C1.01 — OAM word write/read @@ -35010,6 +35257,20 @@ apu_prog_118: .byte $C4, $F4, $E4, $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1 .byte $5F, $C0, $FF apu_prog_119: + .byte $CD, $EF, $BD, $8F, $01, $FC, $8F, $84, $F1, $8F, $00, $10 + .byte $8F, $00, $11, $E4, $FF, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $E4, $FF, $C4, $12, $E4, $10, $60, $84, $12, $C4, $10 + .byte $AB, $11, $E4, $11, $68, $20, $D0, $E5, $E4, $10, $C4, $F5 + .byte $8F, $00, $10, $8F, $00, $11, $E4, $FF, $9F, $9F, $9F, $9F + .byte $9F, $9F, $9F, $9F, $E4, $FF, $C4, $12, $E4, $10, $60, $84 + .byte $12, $C4, $10, $AB, $11, $E4, $11, $68, $20, $D0, $E5, $E4 + .byte $10, $C4, $F6, $8F, $00, $10, $8F, $00, $11, $E4, $FF, $CF + .byte $CF, $CF, $CF, $CF, $CF, $CF, $CF, $E4, $FF, $C4, $12, $E4 + .byte $10, $60, $84, $12, $C4, $10, $AB, $11, $E4, $11, $68, $20 + .byte $D0, $E5, $E4, $10, $C4, $F7, $8F, $80, $F1, $E8, $5A, $C4 + .byte $F4, $E4, $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F + .byte $C0, $FF +apu_prog_120: .byte $5F, $0C, $02, $83, $79, $79, $79, $79, $79, $79, $79, $79 .byte $CD, $EF, $BD, $E8, $03, $C5, $00, $01, $E8, $02, $C5, $01 .byte $01, $E8, $03, $C5, $02, $01, $E8, $02, $C5, $03, $01, $E8 @@ -35036,6 +35297,166 @@ apu_prog_119: .byte $F3, $C4, $F6, $E8, $6C, $C4, $F2, $E8, $E0, $C4, $F3, $E8 .byte $6C, $C4, $F2, $E8, $20, $C4, $F3, $E8, $5A, $C4, $F4, $E4 .byte $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF +apu_prog_121: + .byte $5F, $1C, $08, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $01, $00, $02, $03, $02, $03, $01, $02 + .byte $02, $03, $03, $02, $03, $01, $03, $00, $02, $00, $02, $03 + .byte $02, $03, $03, $02, $03, $01, $02, $02, $01, $01, $03, $00 + .byte $01, $00, $02, $03, $02, $03, $01, $02, $02, $03, $03, $02 + .byte $03, $01, $03, $02, $12, $00, $02, $03, $02, $03, $03, $02 + .byte $03, $01, $02, $02, $01, $01, $02, $00, $01, $00, $02, $03 + .byte $02, $03, $01, $02, $02, $03, $03, $02, $03, $01, $03, $00 + .byte $62, $00, $02, $03, $02, $03, $03, $02, $03, $01, $02, $02 + .byte $01, $01, $03, $00, $01, $00, $02, $03, $02, $03, $01, $02 + .byte $02, $03, $03, $02, $03, $01, $03, $00, $52, $00, $02, $03 + .byte $02, $03, $03, $02, $03, $01, $02, $02, $01, $01, $02, $00 + .byte $01, $00, $02, $03, $02, $03, $01, $02, $02, $03, $03, $02 + .byte $03, $02, $01, $03, $42, $00, $02, $03, $02, $03, $03, $02 + .byte $03, $01, $02, $02, $01, $01, $01, $01, $01, $00, $02, $03 + .byte $02, $03, $01, $02, $02, $03, $03, $02, $03, $02, $01, $01 + .byte $32, $00, $02, $03, $02, $03, $03, $02, $03, $01, $02, $02 + .byte $01, $01, $01, $01, $01, $00, $02, $03, $02, $03, $01, $02 + .byte $02, $03, $03, $02, $03, $02, $01, $01, $02, $00, $02, $03 + .byte $02, $03, $03, $02, $02, $02, $02, $02, $01, $01, $03, $01 + .byte $01, $00, $02, $03, $02, $03, $01, $02, $02, $03, $03, $02 + .byte $03, $01, $01, $00, $22, $00, $02, $03, $02, $03, $03, $02 + .byte $02, $02, $03, $02, $01, $01, $02, $00, $00, $01, $02, $03 + .byte $04, $05, $06, $07, $08, $09, $0A, $0B, $0C, $0D, $0E, $0F + .byte $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $1A, $1B + .byte $1C, $1D, $1E, $1F, $20, $21, $22, $23, $24, $25, $26, $27 + .byte $28, $29, $2A, $2B, $2C, $2D, $2E, $2F, $30, $31, $32, $33 + .byte $34, $35, $36, $37, $38, $39, $3A, $3B, $3C, $3D, $3E, $3F + .byte $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $4A, $4B + .byte $4C, $4D, $4E, $4F, $50, $51, $52, $53, $54, $55, $56, $57 + .byte $58, $59, $5A, $5B, $5C, $5D, $5E, $5F, $60, $61, $62, $63 + .byte $64, $65, $66, $67, $68, $69, $6A, $6B, $6C, $6D, $6E, $6F + .byte $70, $71, $72, $73, $74, $75, $76, $77, $78, $79, $7A, $7B + .byte $7C, $7D, $7E, $7F, $80, $81, $82, $83, $84, $85, $86, $87 + .byte $88, $89, $8A, $8B, $8C, $8D, $8E, $8F, $90, $91, $92, $93 + .byte $94, $95, $96, $97, $98, $99, $9A, $9B, $9C, $9D, $9E, $9F + .byte $A0, $A1, $A2, $A3, $A4, $A5, $A6, $A7, $A8, $A9, $AA, $AB + .byte $AC, $AD, $AE, $AF, $B0, $B1, $B2, $B3, $B4, $B5, $B6, $B7 + .byte $B8, $B9, $BA, $BB, $BC, $BD, $BE, $BF, $C0, $C1, $C2, $C3 + .byte $C4, $C5, $C6, $C7, $C8, $C9, $CA, $CB, $CC, $CD, $CE, $CF + .byte $D0, $D1, $D2, $D3, $D4, $D5, $D6, $D7, $D8, $D9, $DA, $DB + .byte $DC, $DD, $DE, $DF, $E0, $E1, $E2, $E3, $E4, $E5, $E6, $E7 + .byte $E8, $E9, $EA, $EB, $EC, $ED, $EE, $EF, $F0, $F1, $F2, $F3 + .byte $F4, $F5, $F6, $F7, $F8, $F9, $FA, $FB, $FC, $FD, $FE, $FF + .byte $00, $00, $30, $32, $30, $00, $00, $00, $00, $31, $00, $30 + .byte $00, $00, $00, $00, $00, $00, $30, $30, $00, $00, $00, $30 + .byte $00, $00, $30, $00, $00, $00, $00, $00, $00, $00, $30, $32 + .byte $30, $00, $00, $00, $00, $31, $00, $30, $00, $00, $30, $00 + .byte $00, $00, $30, $30, $00, $00, $00, $30, $00, $00, $30, $00 + .byte $00, $00, $30, $00, $00, $00, $30, $32, $30, $00, $00, $00 + .byte $00, $31, $00, $30, $00, $00, $00, $00, $00, $00, $30, $30 + .byte $00, $00, $00, $30, $00, $00, $30, $00, $00, $00, $00, $00 + .byte $00, $00, $30, $32, $30, $00, $00, $00, $00, $31, $00, $30 + .byte $00, $00, $30, $00, $00, $00, $30, $30, $00, $00, $00, $30 + .byte $00, $00, $30, $00, $00, $00, $30, $00, $00, $00, $30, $32 + .byte $30, $00, $00, $00, $00, $31, $00, $30, $00, $00, $00, $00 + .byte $00, $00, $30, $30, $00, $00, $00, $30, $00, $00, $30, $00 + .byte $00, $00, $00, $00, $00, $00, $30, $32, $30, $00, $00, $00 + .byte $00, $31, $00, $30, $00, $00, $00, $00, $00, $00, $30, $30 + .byte $00, $00, $00, $30, $00, $00, $30, $00, $00, $00, $00, $00 + .byte $00, $00, $30, $32, $30, $00, $00, $00, $00, $00, $00, $30 + .byte $00, $00, $00, $00, $00, $00, $30, $30, $00, $00, $00, $30 + .byte $30, $00, $30, $00, $00, $00, $00, $00, $00, $00, $30, $32 + .byte $30, $00, $00, $00, $00, $00, $00, $30, $00, $00, $00, $00 + .byte $00, $00, $30, $30, $00, $00, $00, $30, $30, $00, $31, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $0E, $00, $00 + .byte $00, $30, $0E, $00, $0E, $00, $0E, $00, $00, $00, $00, $00 + .byte $00, $0E, $0E, $00, $30, $00, $00, $00, $00, $00, $0E, $0E + .byte $00, $00, $00, $00, $00, $0E, $00, $00, $00, $30, $0E, $00 + .byte $0E, $00, $00, $00, $00, $00, $00, $00, $00, $0E, $0E, $00 + .byte $30, $00, $00, $00, $00, $00, $00, $0E, $00, $00, $00, $00 + .byte $00, $0E, $00, $00, $00, $30, $0E, $00, $0E, $00, $0E, $00 + .byte $00, $00, $00, $00, $00, $0E, $0E, $00, $30, $00, $00, $00 + .byte $00, $00, $0E, $0E, $00, $00, $00, $00, $00, $0E, $00, $00 + .byte $00, $30, $0E, $00, $0E, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $0E, $0E, $00, $30, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $0E, $00, $00, $00, $30, $0E, $00 + .byte $0E, $00, $00, $30, $00, $00, $00, $00, $00, $0E, $0E, $00 + .byte $30, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $0E, $00, $00, $00, $30, $0E, $00, $0E, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $0E, $0E, $00, $30, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $0E, $00, $00 + .byte $00, $0E, $0E, $00, $0E, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $0E, $0E, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $0E, $00, $00, $00, $0E, $0E, $00 + .byte $0E, $00, $00, $00, $00, $00, $00, $00, $00, $0E, $0E, $00 + .byte $00, $00, $30, $00, $00, $00, $00, $00, $02, $08, $04, $07 + .byte $03, $04, $03, $06, $02, $06, $05, $04, $05, $04, $06, $08 + .byte $04, $08, $04, $07, $04, $05, $05, $06, $05, $05, $06, $05 + .byte $02, $02, $04, $06, $02, $08, $04, $07, $03, $04, $03, $06 + .byte $02, $06, $05, $04, $05, $04, $07, $04, $04, $08, $04, $07 + .byte $04, $05, $05, $06, $05, $05, $06, $05, $02, $02, $03, $08 + .byte $02, $08, $04, $07, $03, $04, $03, $06, $02, $06, $04, $04 + .byte $05, $04, $06, $06, $04, $08, $04, $07, $04, $05, $05, $06 + .byte $05, $05, $04, $05, $02, $02, $04, $03, $02, $08, $04, $07 + .byte $03, $04, $03, $06, $02, $06, $04, $04, $05, $04, $07, $05 + .byte $04, $08, $04, $07, $04, $05, $05, $06, $05, $05, $05, $05 + .byte $02, $02, $03, $06, $02, $08, $04, $07, $03, $04, $03, $06 + .byte $02, $06, $05, $04, $05, $02, $04, $05, $04, $08, $04, $07 + .byte $04, $05, $05, $06, $05, $05, $05, $05, $02, $02, $0C, $05 + .byte $03, $08, $04, $07, $03, $04, $03, $06, $02, $06, $04, $04 + .byte $05, $02, $04, $04, $04, $08, $04, $07, $04, $05, $05, $06 + .byte $05, $05, $05, $05, $02, $02, $03, $04, $03, $08, $04, $07 + .byte $04, $05, $04, $07, $02, $05, $06, $04, $05, $02, $04, $09 + .byte $04, $08, $04, $07, $05, $06, $06, $07, $04, $05, $05, $05 + .byte $02, $02, $08, $03, $02, $08, $04, $07, $03, $04, $03, $06 + .byte $02, $04, $05, $03, $04, $03, $04, $00, $04, $08, $04, $07 + .byte $04, $05, $05, $06, $03, $04, $05, $04, $02, $02, $06, $00 + .byte $E8, $01, $E8, $01, $E8, $80, $E8, $80, $E8, $00, $E8, $00 + .byte $E8, $FF, $68, $00, $E8, $00, $68, $FF, $E8, $7F, $88, $7F + .byte $E8, $00, $88, $00, $CD, $EF, $BD, $8F, $01, $FC, $8F, $84 + .byte $F1, $8F, $00, $07, $8F, $00, $08, $8F, $FF, $09, $8F, $00 + .byte $00, $F8, $00, $F5, $00, $03, $C4, $06, $28, $0F, $C4, $01 + .byte $D0, $03, $5F, $01, $09, $E4, $06, $9F, $28, $0F, $C4, $02 + .byte $8F, $00, $05, $E4, $02, $1C, $1C, $5D, $8D, $00, $F5, $00 + .byte $08, $C4, $0C, $3D, $7D, $C4, $06, $F8, $05, $E4, $0C, $D5 + .byte $00, $0D, $AB, $05, $F8, $06, $FC, $AD, $04, $D0, $E7, $8D + .byte $00, $F8, $00, $F5, $00, $04, $C4, $0C, $F5, $00, $05, $C4 + .byte $0D, $F5, $00, $06, $C4, $0E, $F8, $05, $E4, $0C, $D5, $00 + .byte $0D, $3D, $E4, $0D, $D5, $00, $0D, $3D, $E4, $0E, $D5, $00 + .byte $0D, $E4, $05, $60, $84, $01, $C4, $05, $FC, $AD, $06, $D0 + .byte $D0, $F8, $05, $E8, $20, $D5, $00, $0D, $F8, $05, $E8, $CD + .byte $3D, $D5, $00, $0D, $F8, $05, $E8, $ED, $3D, $3D, $D5, $00 + .byte $0D, $F8, $05, $E8, $BD, $3D, $3D, $3D, $D5, $00, $0D, $F8 + .byte $05, $E8, $6F, $3D, $3D, $3D, $3D, $D5, $00, $0D, $8F, $00 + .byte $03, $8F, $00, $04, $E4, $FF, $CD, $EF, $BD, $CD, $30, $8D + .byte $30, $8F, $00, $30, $8F, $0E, $31, $8F, $FF, $32, $3F, $00 + .byte $0D, $E4, $FF, $C4, $06, $E4, $04, $60, $84, $06, $C4, $04 + .byte $AB, $03, $E4, $03, $68, $10, $D0, $DA, $F8, $00, $E4, $04 + .byte $D5, $00, $0C, $AB, $07, $AB, $00, $E4, $00, $68, $00, $F0 + .byte $03, $5F, $31, $08, $CD, $00, $F5, $00, $0C, $C4, $0A, $8F + .byte $00, $00, $F8, $00, $F5, $00, $03, $28, $0F, $F0, $2A, $F5 + .byte $00, $07, $80, $A8, $02, $FD, $E8, $06, $CF, $60, $84, $0A + .byte $C4, $06, $F8, $00, $F5, $00, $0C, $80, $A4, $06, $60, $88 + .byte $03, $68, $07, $90, $0C, $AB, $08, $E4, $09, $68, $FF, $D0 + .byte $04, $E4, $00, $C4, $09, $AB, $00, $E4, $00, $68, $00, $D0 + .byte $C5, $8F, $80, $F1, $E4, $08, $C4, $F5, $E4, $07, $C4, $F6 + .byte $E4, $09, $C4, $F7, $E8, $5A, $C4, $F4, $E4, $F4, $68, $A5 + .byte $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF .segment "CATALOG" .export _test_count .export _test_entries @@ -35043,7 +35464,7 @@ apu_prog_119: .export _test_flags _test_count: - .word 343 + .word 345 ; Entry points, 24-bit: test bodies no longer all live in bank $00. _test_entries: @@ -35330,7 +35751,9 @@ _test_entries: .faraddr test_e3_09 .faraddr test_b2_07 .faraddr test_e3_13 + .faraddr test_e2_10i .faraddr test_e9_02 + .faraddr test_e2_10 .faraddr test_f1_01 .faraddr test_f1_02 .faraddr test_f1_03 @@ -35676,7 +36099,9 @@ _test_flags: .byte $01 ; E3.09 .byte $01 ; B2.07 .byte $01 ; E3.13 + .byte $02 ; E2.10i .byte $01 ; E9.02 + .byte $01 ; E2.10 .byte $01 ; F1.01 .byte $01 ; F1.02 .byte $01 ; F1.03 @@ -36022,7 +36447,9 @@ _test_names: .addr @n_e3_09 .addr @n_b2_07 .addr @n_e3_13 + .addr @n_e2_10i .addr @n_e9_02 + .addr @n_e2_10 .addr @n_f1_01 .addr @n_f1_02 .addr @n_f1_03 @@ -36931,9 +37358,15 @@ _test_names: @n_e3_13: .byte 20 .byte "Regs shadow into RAM" +@n_e2_10i: + .byte 20 + .byte "SPC cycle instrument" @n_e9_02: .byte 23 .byte "Noise output is bipolar" +@n_e2_10: + .byte 22 + .byte "256-opcode cycle sweep" @n_f1_01: .byte 21 .byte "Manual pad read order" @@ -37120,7 +37553,7 @@ _test_names: .export _page_tests _page_count: - .word 51 + .word 52 _page_names: .addr @pn_0 @@ -37174,6 +37607,7 @@ _page_names: .addr @pn_48 .addr @pn_49 .addr @pn_50 + .addr @pn_51 @pn_0: .byte 18 .byte "65816: XCE & FLAGS" @@ -37286,45 +37720,48 @@ _page_names: .byte 20 .byte "APU: SPC700 TIMING 2" @pn_37: - .byte 17 - .byte "APU: SPC700 FLAGS" + .byte 19 + .byte "APU: SPC700 FLAGS 1" @pn_38: + .byte 19 + .byte "APU: SPC700 FLAGS 2" +@pn_39: .byte 15 .byte "APU: DSP VOICES" -@pn_39: +@pn_40: .byte 13 .byte "APU: DSP ECHO" -@pn_40: +@pn_41: .byte 20 .byte "APU: DSP REGISTERS 1" -@pn_41: +@pn_42: .byte 20 .byte "APU: DSP REGISTERS 2" -@pn_42: +@pn_43: .byte 13 .byte "APU: TIMERS 1" -@pn_43: +@pn_44: .byte 13 .byte "APU: TIMERS 2" -@pn_44: +@pn_45: .byte 19 .byte "APU: DSP ENVELOPE 1" -@pn_45: +@pn_46: .byte 19 .byte "APU: DSP ENVELOPE 2" -@pn_46: +@pn_47: .byte 15 .byte "APU: DSP MIXING" -@pn_47: +@pn_48: .byte 20 .byte "INPUT: CONTROLLERS 1" -@pn_48: +@pn_49: .byte 20 .byte "INPUT: CONTROLLERS 2" -@pn_49: +@pn_50: .byte 16 .byte "POWER-ON STATE 1" -@pn_50: +@pn_51: .byte 16 .byte "POWER-ON STATE 2" @@ -37366,7 +37803,8 @@ _page_len: .byte 4 .byte 10 .byte 4 - .byte 9 + .byte 10 + .byte 1 .byte 6 .byte 2 .byte 10 @@ -37420,19 +37858,20 @@ _page_off: .word 237 .word 247 .word 251 - .word 260 - .word 266 + .word 261 + .word 262 .word 268 - .word 278 - .word 282 - .word 292 - .word 295 - .word 305 - .word 312 - .word 319 - .word 329 - .word 332 - .word 342 + .word 270 + .word 280 + .word 284 + .word 294 + .word 297 + .word 307 + .word 314 + .word 321 + .word 331 + .word 334 + .word 344 _page_tests: .word 0 @@ -37486,8 +37925,6 @@ _page_tests: .word 70 .word 71 .word 72 - .word 308 - .word 309 .word 310 .word 311 .word 312 @@ -37521,6 +37958,8 @@ _page_tests: .word 340 .word 341 .word 342 + .word 343 + .word 344 .word 30 .word 31 .word 32 @@ -37695,6 +38134,8 @@ _page_tests: .word 225 .word 226 .word 227 + .word 283 + .word 285 .word 195 .word 196 .word 274 @@ -37716,7 +38157,7 @@ _page_tests: .word 246 .word 247 .word 248 - .word 283 + .word 284 .word 206 .word 207 .word 208 @@ -37754,8 +38195,6 @@ _page_tests: .word 268 .word 269 .word 270 - .word 284 - .word 285 .word 286 .word 287 .word 288 @@ -37778,3 +38217,5 @@ _page_tests: .word 305 .word 306 .word 307 + .word 308 + .word 309 diff --git a/tests/roms/AccuracySNES/build/accuracysnes-pal.sfc b/tests/roms/AccuracySNES/build/accuracysnes-pal.sfc index 06df3c612b8ca721aca434dfe34e6fef76e79fdd..3ace4f66afeb1d5cab4bbc8376d0169ff6a7583f 100644 GIT binary patch delta 3413 zcmbVO3s_Xu7Cw8QGiL@Ih5_G-Ia(qh+CZXdAz?o5O*FD7iM-;2gcMQG(o$wh%z2-A zpLxHS8B|hKGR*Q?G^X4l#}rY^0vwfISL}w7SLN;*1`zA(e)sM@v-eu-U;kSF`sU1h zBNTFlLhk;J*Q{Z_0j!#9%+uVMT?93) zrMw2Vl<#Td9lNAC$(jVVXntYwKveC}2oivoYvUsG0B2G)v482sdcGub&#}qXwL1n4 z!%+zf=M`{C!;IdJY-w~nIuAyVXQQGcO0br4l)6RJ5-k`Is9nQN2VVj@28g1fdwaH&a?J(Ce@ZKWP)K{M?pDPBu?J%6{F zz7Yx1VAeVM02joMObV3O08pOTjZw+SggxezKsmh}f;jqE4j2g^Zlf>efVuFAW_sN7 zATw=Aa}5T{Pa&Eg&sxx=&?r(?qWj z2tn>40CRB0s&i1(T$h8xMnKObU?iQyfKvq{M2Vsd(KDg~(H4k77&`D@l-KNM4W>O3Eb~$sWnalCzTUCAc(B%9qZP zE|lg;*GUCZmDD4BS9(HvNqSvM${vuV$)1wEB-7|V zY&1jMV+Eiaar-3B9=;|W>MMHYU*`L zO?jygsMFMUR5vwDnXH_x%u%jURwzx%1Im-iZA1RE?@O zRi`RS{h)f9`e}8(TJWk`p?0g^SD#W}Ro_y_Y9?wP)nsd4)@;*I8n@;{&6k>X%^eL} zJ3;%ncCmJ~cB@vS-KG6ddscf@`dUwP1h~Z<>^Xw5}if2Uw2%0QTKxm(+|~8 z(r4%w>x=Z|dX3(zKdL{Yzox&dkEIjoOgfh?p?A_20ey&Wq_5C->7j-ML%QL4L$P7I zL1(Bod}L@cd~djA;20k=K4x5KTxHy9)Eajg4;w!-eq+38WSe-VX{LFmT+;6`v92+$$*y$QLf2APv1^-4TqF|oXRnklPhOcW>w}@7FBMpl-3Pm!SvyChCG}7ByZ)Gm1_%lMeE8omToF8ShsQb zqLQMr0$$0cjq5jU$~p`k?Cby3Zek zCt$+h%1g~gty+Q4!I#CcG%>JGnu24JO1g3lMVm)?DNJiPWexNdFE`>x#sg1E`Ig(rEk7%X>Ge4xN`Mc`*+{} z@Z(Sayxwu+=YRdudGpt<-){X|aQn_(^f>ZU`N2mr1q)rs58mh?PE=UPh>scIh@i|y69EdZCfjG|U$B7`H?8k{9A0LP_8DU(28Zb}-YJ9H%h-&l+2;yu8-iM%uxQ+(#xv`+y%`1|kBa9odk=7y%d;w^WW`mXDpj z%|~`I0_-L%s+!S7-rh_2ugODP!pH6uU<_YWCr0`>;hgk96v|8waL^fG186fg2 zNFPy+2k=)4x&Dh?Sb%84*wxHxEPxfFf9K0mSdB}IpsWH-sC;v-m!(WxB@T7^#4S|wU4nrdywjxv@??a=AiRHq;M<<33lo;maT zbk%BwTCHerl_!NClx0GXV|rkp;<0i978-Y9_57}I0+rten~28nJ)n;GCH$)5ORo$o z3Mze2P}z1D?$Q&z5!oP}coG4F@Q!hjasb}zNQqhn#$IoewXYfI$<;-*bp_fw4!x8h z4OOsXHv>Hp9&;eHBQfcr`Dx6g=ul}=lO(7NMbn93jOu?zN26jcNUL+naA`%N$rt}T z_8&$_y}-B_`$`Dok63dE1BrWQtL%DL_JWR}YOtTuC4gke{v)-o29!b80CiFU(y5^u zkX(2&;FAF56#$gqN-Mqy_(Dsjx=-?0TG1Ei$$c)LN%|C$c4*K*Z zg3F8cUqGR>Oi>NgV0kBimP!4qrO-Eknka)JsQfT!rYc&KswvQvX;x~sY7S}`%?ZtU z&46Y|6Q&)novtm`F4fj+KhUCDr?ykus|{+OXyv*TU4gD#w_3MdcUZ^iI(5Ce+q$PZ z1u_N6M~ab3q!wvG4k1?L6XbK`Dsm4QMdHv*bRJrT)}bGw7%HM?(5q+={S%E==_l%E z=u7o$^gH!>y;I+z@6`|LhxIbU1VfHtfuY8*#jwvn8jc#e4L1yr400?L%g0KwRoFJ{ zV~oLk*jLyf_B$4Z!*~(C7+;I;#tpa!KZ{?(AK=gM1Y@Rgjn-7~=bGy0MJYarghRAUwOlFhw$x5=GRFXL9CeM(U$vfl|GSZS}DX=WEtg&pj z9JVl)la`B?A1uQbIh9HkQcI`})E){~QAen5>U(OKilkHMJo+tqJ-v(8(hmAFx{tn1 z57Ce{$vVSYY+Ye(wrZ@b^_aEG`knPxtAvSXvY6S-5@s#a!XS*moMFCZZZX45q-~Pz zHCu&kgKeixW8-Y6Y*%bQ+eU2)b{ac}t!C@ly)3~VWlyu0Sk+JLQ#Q&D+jHy-?JMk! z_WgFkF51u9`|S_yQZ9+h;TCesxh>ot&cKOW7k7hu$jKbZjsnMrn3OBUCwGGWpTh?#)(`itcOjRJcv}^TA?J0cxn-8PF!# zOi&1@I~mYI|I|!qHJ}b;|62e0=FEH} z5O4$n&cV&(h{$px0d_>yajQ8UWuqu`v~dOLCnD8S@@rsIuVcOjEb42_vz+KX1U0>- ztQxkI?Q7$nxTHSKnhdt7e`aw(WX;ep5`Y(LV#9L*dvX=r5}Vpe&&qqmqybd(El-GI}KhvGj>-FcLo0MqkVZ^Wfu6^tfk1 zM(VPrY7CT}K{Owpxu&UyFQ~^l@XU=(b-ZA?dJc9F&)nWrpMfF$+neZ>L%?jcHJLMH z@7l}vY1+i3qX8K65R!>eT>!~+Vptn@|KsE8pYIr1$7}cf)W)qwz^DLP&#B}2S~GK- zdnQ=G8<-$vV1jA=(fVrvA*Idr?ExXB%>nIb+NV=3G+$s+&) zkaGmUY@D&?0u(mYX5+8{(6a~_PA4+pH2w%-f-qh9lrUeoO{frhgvW$F;Z@-+VYFzX z=n>H(QJ$zov`eHHy(RikbYAp>2ouMMW#*xR#XUa3>tK?hdGPy%uD{qirmj5b`p%SRsR2H?CdX-X9 z9_l^nEcGqbO$}2dDds4$73&n`3ZvqX;n#EKy37F6A+0gR)K8 zsf<)TsG6=?qRLb8UslOgF4eoLGpehqTdElKB=sZeEcJ`(9coJLQopbMT-~m|qmIx_ z)I6qHs#&Yqu2F0DXx`VH*Id>7s$px#X_K@wvAI!5LS30ot@G%P>(1$}>F(-c=ma{0&Y_Fx-L#ocAE6uQEA(A@s6Jkwrhisnq~EF6 z>Z|l0=o|Il>2K-Th6#p84T}tG4BHJF!ydy?!zYHX4L1!DMy_$XalSFfxX#Eo(#8YE zlg10icH>>+P}5Y?Jkv5$vFQzy#-H53mHV=G>qvQ zE^_a3o81T8$J}S#e)n(gNY7YL zvgZlUQqM}yI?oP|!c*Zn>S^$_dOAIf%E6WKm1&g=D|0G~DqpWuR(dMmtNgU~(+A-x z7(X~8D|;3f>7T*lapz}dEO?rm{3o%LT6Y|b1TY-XHIrb@Xa*ob1Ym;^AQr@d1n@AJ z1v0=wuox_(C&t6Iv?3nP!Jh-mYR|^Q<$xFs#sGS1B1}ZTfH{e9A-yLNCgIaSYVEm1 zc;T6M1klpTZ@7M(0=KF18x+Q&uxNib7F_;rZ;YRlS03Z#R7Lxv3%I^3evY@ZFU=NQ zf1Y-;FYQcl{dw9i6rB=*52o#0{4sh-6n>OeuEOWyD`HvdXu%ymz#tBB03jZ@a18eN zy&-`7`*0W&g2Vf9Y*h&U2R^nm1c&_40ubPHLJ|N75dZ;5`7e0i01z!SKqw!1C?|-Kl|LWoab|2 zSpJU{c`H|~UX%ag+JeHD)~zqvP`q(dN$KV-FTb*N+pDi_=kF-v3toR?XZfx-cMC;g ziBu-16iSs^qt(%RgVAKRSZ#KPv%=-}RPNckuj;M+2dWPqI$ZPik#}m3zI*Jw_mBVc z#0MYNedIm)@u}1G|N7+9hR@FU&VGLGeB*_t<}WUO`PHSbziDZ0yX?Pm^;-M4-+lkX zkN>{japR}|{M>o-m#$xL{l>q2=Pr62c`3ZWBbki#T*wdH=pA%J@E+(MyvO{1`TX8K zAq@gb{z|}K;Qfkw`MmoF_yf*Q4pi%1Y;^J1SYY=oHakd1Mqu^hJs$G=@F1Zd{~!2) z`TJLYfq}k#A3#se0NzU(5D@gyg<`?>J$5e;JOjXeIO^Zq05DYXYf%VqsGf4F1CmGCs-+eE13w<|l2)h7`u|!`Rs6G6b`{5otTT zWEaC9(TGJ>F}ldx`w8DQSr3=+Ms)HqhBvYkBfackPMSXwWv2Pr==3x4%7K@0dq3&( zburqxD1@$twBGy2w4iZ3tPc&i?Rm>{Pj}@SQ=PQy~4M=^2H@t7m_^6J=PK+to1MjqxXY$I! z(NKX5fs4!&;ss1aL>0>)R?zJu{Fpbaw~p^5CAi^dc{}|~?@fOgy$RxjYa3VL@!H=3 DHic1| delta 1354 zcmY+EYfuw+6vgx3gzzXzU=UD1wmy)DR31XD)&~N%7A$m7)T&ica1a%$_-L(3eAOho zo84ry+0ACN$!?NO#Rpi5(WxB@T7^#4T1&K4G}YRU9c3()+M(02X`O!Pmpk{Id*;mV z)75HKTCJ+RMVSJIWIvQ1TL0X+#LC7l-&291j z#{RzC^5_3!CXy-VMz@6`wOPxMMdilM+zZdh&DZa8A#4V{Kw!)?P; zg9@2~^T17^m2!hGI5U>-6n$z*amSxUZ5zDph?8M2+cNZulUC1sZJmR!ps z%PLE=<%or|v|D;D1C~b?h#E)1R5mrAs-)^EHAPS!>MV7Ix0*TP2=Wp)m%Ndk0ZHb+!^jNr}>e4%0)R~M~-8mV}+y9 zalk=3B*!^NzvH1p&L{CX{6c;?zlGn+8+nQE;&1Q|d4)6CS>Sx#xz4%EX>dB6ozBb7 zd(KhkSl5fLB3GqrlS}QQT_;=@T!XF=*BD{4P$-lOwZcw8D@Dz?duzQdUagn*wtBn0*S+_>PrZ>+k~B@qlS-r-sX5=rO6zNO#<@t(zHNHmQ2fnzX!dZDmv*8sbWpFyYsG|6_>e!IwpEw0-gW_U~ ztCr1%tE)<0&dq&(^pB)NV*rSx6&cXV@d^+HqCov}^TA?J0cz+y8PF!h zOi&2uI~mYI|I|!qHJ}e?L0KS~ZqI_s, } +/// A forward branch waiting for its destination, from [`Spc::branch_fwd`]. +/// +/// # Why forward branches need a mechanism at all +/// +/// [`Spc::bne_back`] computes its displacement as it is emitted, because the target already +/// exists. A forward branch cannot: the gap is not known until the code it jumps over has been +/// written. Rather than let a caller hand-count that gap — right until an instruction between the +/// two moves, the exact failure [`Spc::release_to_ipl`] documents — the emitter writes a +/// placeholder and [`Spc::patch_fwd`] fills it in from the two offsets. +#[derive(Debug)] +#[must_use = "a forward branch that is never patched branches to itself"] +pub struct Fwd(usize); + impl Spc { /// Start an empty program. #[must_use] @@ -182,6 +195,137 @@ impl Spc { entry } + /// Emit a forward branch with `opcode` and a placeholder displacement. See [`Fwd`]. + pub fn branch_fwd(&mut self, opcode: u8) -> Fwd { + self.push(&[opcode, 0x00]); + Fwd(self.bytes.len() - 1) + } + + /// `BNE` forward. See [`Fwd`]. + pub fn bne_fwd(&mut self) -> Fwd { + self.branch_fwd(0xD0) + } + + /// `BEQ` forward. See [`Fwd`]. + pub fn beq_fwd(&mut self) -> Fwd { + self.branch_fwd(0xF0) + } + + /// `BCC` forward. See [`Fwd`]. + pub fn bcc_fwd(&mut self) -> Fwd { + self.branch_fwd(0x90) + } + + /// Point a [`Fwd`] at the current offset. + /// + /// # Panics + /// + /// If the gap is beyond a branch's reach. A displacement that does not fit is a program that + /// silently jumps somewhere else, so it fails the build instead. + /// Taken by value, and clippy's `needless_pass_by_value` is allowed rather than obeyed: the + /// point of consuming the handle is that a forward branch cannot be patched twice, and a + /// `&Fwd` would give that back. + #[allow(clippy::needless_pass_by_value)] + pub fn patch_fwd(&mut self, fwd: Fwd) -> &mut Self { + let Fwd(at) = fwd; + let after = at + 1; + let rel = i64::try_from(self.bytes.len()).expect("offset fits i64") + - i64::try_from(after).expect("offset fits i64"); + let rel = i8::try_from(rel).expect("forward branch target is out of reach"); + self.bytes[at] = rel.to_le_bytes()[0]; + self + } + + /// `MOV Y,A` — `$FD`. + pub fn mov_y_a(&mut self) -> &mut Self { + self.push(&[0xFD]) + } + + /// `MOV X,dp` — `$F8`. + pub fn mov_x_dp(&mut self, dp: u8) -> &mut Self { + self.push(&[0xF8, dp]) + } + + /// `JMP !abs` with a placeholder target, for a forward jump past a branch's reach. + /// + /// A [`Fwd`] branch covers ±127 bytes; the opcode sweep has to jump over its whole per-opcode + /// body, which is several hundred. `base` is where the program will be uploaded, because a + /// `JMP` names an absolute address and the assembler only knows offsets — the two are the same + /// number plus that base, and every Group E program is uploaded to a fixed one. + pub fn jmp_fwd(&mut self) -> Fwd { + self.push(&[0x5F, 0x00, 0x00]); + Fwd(self.bytes.len() - 2) + } + + /// Point a [`Spc::jmp_fwd`] at the current offset, given the program's upload address. + /// Consumed for the same reason as [`Spc::patch_fwd`]. + #[allow(clippy::needless_pass_by_value)] + pub fn patch_jmp_fwd(&mut self, fwd: Fwd, base: u16) -> &mut Self { + let Fwd(at) = fwd; + let target = base + u16::try_from(self.bytes.len()).expect("a program is smaller than RAM"); + let [lo, hi] = target.to_le_bytes(); + self.bytes[at] = lo; + self.bytes[at + 1] = hi; + self + } + + /// `JMP !abs` back to an offset recorded by [`Spc::here`], given the program's upload address. + /// + /// The long-range counterpart of [`Spc::bne_back`], for a loop whose body is bigger than a + /// branch can span. See [`Spc::jmp_fwd`] on why the base has to be supplied. + pub fn jmp_back(&mut self, target: usize, base: u16) -> &mut Self { + let addr = base + u16::try_from(target).expect("a program is smaller than RAM"); + let [lo, hi] = addr.to_le_bytes(); + self.push(&[0x5F, lo, hi]) + } + + /// `MOV !abs+X,A` — `$D5`. The indexed absolute *write*, the counterpart of + /// [`Spc::mov_a_abs_x`], and how the opcode sweep lays a block down byte by byte. + pub fn mov_abs_x_a(&mut self, addr: u16) -> &mut Self { + let [lo, hi] = addr.to_le_bytes(); + self.push(&[0xD5, lo, hi]) + } + + /// `MOV X,A` — `$5D`. + pub fn mov_x_a(&mut self) -> &mut Self { + self.push(&[0x5D]) + } + + /// `MOV A,X` — `$7D`. + pub fn mov_a_x(&mut self) -> &mut Self { + self.push(&[0x7D]) + } + + /// `INC X` — `$3D`. + pub fn inc_x_reg(&mut self) -> &mut Self { + self.push(&[0x3D]) + } + + /// `INC Y` — `$FC`. + pub fn inc_y(&mut self) -> &mut Self { + self.push(&[0xFC]) + } + + /// `CMP Y,#imm` — `$AD`. + pub fn cmp_y_imm(&mut self, v: u8) -> &mut Self { + self.push(&[0xAD, v]) + } + + /// `AND A,#imm` — `$28`. + pub fn and_a_imm(&mut self, v: u8) -> &mut Self { + self.push(&[0x28, v]) + } + + /// `SBC A,#imm` — `$A8`. Borrow is `not C`, so a plain subtraction needs `SETC` first. + pub fn sbc_a_imm(&mut self, v: u8) -> &mut Self { + self.push(&[0xA8, v]) + } + + /// `SBC A,dp` — `$A4`. See [`Spc::sbc_a_imm`] on the borrow. + pub fn sbc_a_dp(&mut self, dp: u8) -> &mut Self { + self.push(&[0xA4, dp]) + } + /// `MOV A,!abs+X` — `$F5`. The indexed absolute read the IPL-ROM checksum walks with. pub fn mov_a_abs_x(&mut self, addr: u16) -> &mut Self { let [lo, hi] = addr.to_le_bytes(); diff --git a/tests/roms/AccuracySNES/gen/src/spc_opcodes.rs b/tests/roms/AccuracySNES/gen/src/spc_opcodes.rs new file mode 100644 index 00000000..3c82c67e --- /dev/null +++ b/tests/roms/AccuracySNES/gen/src/spc_opcodes.rs @@ -0,0 +1,739 @@ +//! The documented SPC700 opcode map — encoding, length, cycle count, and how each opcode can be +//! measured in place. +//! +//! # Where the numbers come from +//! +//! Every length and cycle count in this table is transcribed from **fullsnes**'s SPC700 instruction +//! set (`ref-docs/fullsnes/40-apu-dsp.md`, the Load/Store, ALU and Jump/Control sections). Nothing +//! here is read out of `crates/rustysnes-apu/`. That direction matters: `E2.10` is a *scored* row, +//! so its expectation has to come from a source outside the thing being tested, or the row is our +//! own arithmetic checked against itself — the `E9.11` failure mode the provenance tier exists to +//! prevent. +//! +//! # Why the table is built from rules rather than typed out +//! +//! The SPC700 map is regular, and fullsnes documents it *as* rules — `OR/AND/EOR/CMP/ADC/SBC` share +//! one operand column at `x + 04/05/06/…`, the shift and increment group shares another at +//! `x + 0B/0C/1B/1C`, and the bit ops are `b * 20 + 02/12/03/13`. Typing 256 lines out by hand +//! would introduce transcription errors the rules cannot have, and would hide the structure that +//! makes the map checkable. [`table`] asserts it filled all 256 slots exactly once, so a rule that +//! overlaps another or misses a slot fails the build rather than shipping a hole. +//! +//! # Straight-line measurability +//! +//! [`Measure`] records whether an opcode can be timed by executing copies of it back to back. Most +//! can. The ones that cannot are not a gap in the sweep — for them the question "how long does this +//! take in a straight line" has no answer, because they are the opcodes that end the straight line. +//! Each carries its reason. + +/// How an opcode can be measured by executing copies of it back to back. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Measure { + /// Executes and falls through to the next instruction. Timeable directly. + Straight, + /// A relative branch. Timeable with a displacement of zero — the taken path lands on the next + /// instruction, which is where a not-taken branch would have gone anyway, so a block of copies + /// runs straight through whichever way each one goes. The recorded cost is the **taken** one, + /// and [`Flag`] says which condition the arm has to arrange for that to be the path measured. + BranchTaken(Flag), + /// Not timeable in place, with the reason. These are the opcodes that leave the block: absolute + /// jumps and calls (whose operand would have to differ for every copy), the vectored calls + /// (whose vectors are in the IPL ROM), the returns (which need a stack the block did not push), + /// and the two that halt the processor outright. + NotStraightLine(&'static str), +} + +/// The processor-status condition an arm must arrange so its branch is taken. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Flag { + /// `N = 0`. + NegClear, + /// `N = 1`. + NegSet, + /// `V = 0`. + OvfClear, + /// `V = 1`. + OvfSet, + /// `C = 0`. + CarryClear, + /// `C = 1`. + CarrySet, + /// `Z = 0`. + ZeroClear, + /// `Z = 1`. + ZeroSet, + /// No flag needed — the branch is unconditional, or its condition is arranged by the driver's + /// register and memory setup rather than by a flag (`CBNE`, `DBNZ`). + None, +} + +/// One opcode's documented shape. +#[derive(Clone, Copy, Debug)] +pub struct Op { + /// The opcode byte. + pub code: u8, + /// Total instruction length in bytes, opcode included. + pub len: u8, + /// Documented cycle count. For a branch this is the **taken** cost, which is what + /// [`Measure::BranchTaken`] arranges to measure. + pub cycles: u8, + /// How it can be timed. + pub measure: Measure, + /// Mnemonic, for the generated failure text and the coverage report. + pub name: &'static str, + /// What its operand bytes mean. + /// + /// Recorded when the entry is built rather than derived from the opcode byte afterwards. The + /// map's low nibble *nearly* decides the addressing mode, and the first draft of this file + /// derived it that way — but "nearly" hides at least four traps: `$x9` is `cmd aa,bb` in the + /// ALU columns and an absolute or indexed `MOV` at `$C9`-`$F9`, and the `+X` variants sit one + /// `$10` bit away from their unindexed twins in three separate columns. A derived rule that is + /// wrong about one of them gives `MOV [aa+X],A` a pointer read out of uninitialised memory, + /// which is zero, which is the sweep driver's own variables. The construction rules already + /// know the answer; this field is them writing it down. + pub operands: Operands, +} + +/// Operand roles, so the sweep can fill each opcode's operand bytes with something safe. +/// +/// "Safe" means three things at once, and every one of them has a way to ruin a measurement: +/// the operand must not name an I/O register (`$F0-$FF` — reading `$FD-$FF` clears the very +/// counters the sweep is reading, and most stores do a dummy read of their destination, so even a +/// *write* to `$FF` would clear `T2OUT`), it must not name the code block itself (an opcode that +/// rewrites the block changes what the remaining copies are), and it must stay in range once the +/// index registers are added. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Operands { + /// No operand bytes. + None, + /// One direct-page address, used directly or as the low half of an indirect pointer + /// (`[aa]+Y` reads its pointer from `aa`, so it belongs here). + Dp, + /// A direct-page address that an index register is added to before use — `aa+X`, `aa+Y`, and + /// the `[aa+X]` indirect, whose *pointer* is at `aa+X`. + /// + /// It needs its own variant because the safe value is the opposite of [`Operands::Dp`]'s. A + /// plain operand names the window directly; an indexed one has to name `window - X`, so that + /// adding the index lands on the window rather than 48 bytes past it. Get this wrong on + /// `MOV [aa+X],A` and the store goes through a pointer read from uninitialised memory — which + /// is zero, which is the driver's own variables. + DpIndexed, + /// Two direct-page addresses, destination then source (`cmd aa,bb` is encoded `bb aa`). + DpDp, + /// An immediate byte. + Imm, + /// An immediate byte then a direct-page address (`cmd aa,#nn` is encoded `nn aa`). + ImmDp, + /// A 16-bit absolute address. + Abs, + /// A 13-bit absolute address with a 3-bit bit index in the top bits. + AbsBit, + /// A signed relative displacement. + Rel, + /// A direct-page address then a relative displacement. + DpRel, + /// A direct-page address that `X` is added to, then a relative displacement (`CBNE aa+X,rr`). + DpIndexedRel, +} + +/// The `x` bases of the six ALU operations, in fullsnes's `00+x` form. +const ALU: [(u8, &str); 6] = [ + (0x00, "OR"), + (0x20, "AND"), + (0x40, "EOR"), + (0x60, "CMP"), + (0x80, "ADC"), + (0xA0, "SBC"), +]; + +/// The ALU operand column: `(low nibble offset, length, cycles, operands, suffix)`. +const ALU_FORMS: [(u8, u8, u8, Operands, &str); 12] = [ + (0x04, 2, 3, Operands::Dp, "A,aa"), + (0x05, 3, 4, Operands::Abs, "A,!aaaa"), + (0x06, 1, 3, Operands::None, "A,(X)"), + (0x07, 2, 6, Operands::DpIndexed, "A,[aa+X]"), + (0x08, 2, 2, Operands::Imm, "A,#nn"), + (0x09, 3, 6, Operands::DpDp, "aa,bb"), + (0x14, 2, 4, Operands::DpIndexed, "A,aa+X"), + (0x15, 3, 5, Operands::Abs, "A,!aaaa+X"), + (0x16, 3, 5, Operands::Abs, "A,!aaaa+Y"), + (0x17, 2, 6, Operands::Dp, "A,[aa]+Y"), + (0x18, 3, 5, Operands::ImmDp, "aa,#nn"), + (0x19, 1, 5, Operands::None, "(X),(Y)"), +]; + +/// The `x` bases of the shift / increment group. +const SHIFT: [(u8, &str); 6] = [ + (0x00, "ASL"), + (0x20, "ROL"), + (0x40, "LSR"), + (0x60, "ROR"), + (0x80, "DEC"), + (0xA0, "INC"), +]; + +/// The shift / increment operand column: `(low nibble offset, length, cycles, operands, suffix)`. +const SHIFT_FORMS: [(u8, u8, u8, Operands, &str); 4] = [ + (0x0B, 2, 4, Operands::Dp, "aa"), + (0x0C, 3, 5, Operands::Abs, "!aaaa"), + (0x1B, 2, 5, Operands::DpIndexed, "aa+X"), + (0x1C, 1, 2, Operands::None, "A"), +]; + +/// Everything the two regular groups do not cover, spelled out. +/// +/// `(code, len, cycles, measure, name)`. Grouped in source order by fullsnes's own headings so a +/// reader can check a run of them against one block of the reference at a time. +const SINGLES: &[(u8, u8, u8, Measure, Operands, &str)] = &[ + // Register manipulation. + (0xE8, 2, 2, Measure::Straight, Operands::Imm, "MOV A,#nn"), + (0xCD, 2, 2, Measure::Straight, Operands::Imm, "MOV X,#nn"), + (0x8D, 2, 2, Measure::Straight, Operands::Imm, "MOV Y,#nn"), + (0x7D, 1, 2, Measure::Straight, Operands::None, "MOV A,X"), + (0x5D, 1, 2, Measure::Straight, Operands::None, "MOV X,A"), + (0xDD, 1, 2, Measure::Straight, Operands::None, "MOV A,Y"), + (0xFD, 1, 2, Measure::Straight, Operands::None, "MOV Y,A"), + (0x9D, 1, 2, Measure::Straight, Operands::None, "MOV X,SP"), + (0xBD, 1, 2, Measure::Straight, Operands::None, "MOV SP,X"), + // Memory load. + (0xE4, 2, 3, Measure::Straight, Operands::Dp, "MOV A,aa"), + ( + 0xF4, + 2, + 4, + Measure::Straight, + Operands::DpIndexed, + "MOV A,aa+X", + ), + (0xE5, 3, 4, Measure::Straight, Operands::Abs, "MOV A,!aaaa"), + ( + 0xF5, + 3, + 5, + Measure::Straight, + Operands::Abs, + "MOV A,!aaaa+X", + ), + ( + 0xF6, + 3, + 5, + Measure::Straight, + Operands::Abs, + "MOV A,!aaaa+Y", + ), + (0xE6, 1, 3, Measure::Straight, Operands::None, "MOV A,(X)"), + (0xBF, 1, 4, Measure::Straight, Operands::None, "MOV A,(X)+"), + (0xF7, 2, 6, Measure::Straight, Operands::Dp, "MOV A,[aa]+Y"), + ( + 0xE7, + 2, + 6, + Measure::Straight, + Operands::DpIndexed, + "MOV A,[aa+X]", + ), + (0xF8, 2, 3, Measure::Straight, Operands::Dp, "MOV X,aa"), + ( + 0xF9, + 2, + 4, + Measure::Straight, + Operands::DpIndexed, + "MOV X,aa+Y", + ), + (0xE9, 3, 4, Measure::Straight, Operands::Abs, "MOV X,!aaaa"), + (0xEB, 2, 3, Measure::Straight, Operands::Dp, "MOV Y,aa"), + ( + 0xFB, + 2, + 4, + Measure::Straight, + Operands::DpIndexed, + "MOV Y,aa+X", + ), + (0xEC, 3, 4, Measure::Straight, Operands::Abs, "MOV Y,!aaaa"), + (0xBA, 2, 5, Measure::Straight, Operands::Dp, "MOVW YA,aa"), + // Memory store. + (0x8F, 3, 5, Measure::Straight, Operands::ImmDp, "MOV aa,#nn"), + (0xFA, 3, 5, Measure::Straight, Operands::DpDp, "MOV aa,bb"), + (0xC4, 2, 4, Measure::Straight, Operands::Dp, "MOV aa,A"), + (0xD8, 2, 4, Measure::Straight, Operands::Dp, "MOV aa,X"), + (0xCB, 2, 4, Measure::Straight, Operands::Dp, "MOV aa,Y"), + ( + 0xD4, + 2, + 5, + Measure::Straight, + Operands::DpIndexed, + "MOV aa+X,A", + ), + ( + 0xDB, + 2, + 5, + Measure::Straight, + Operands::DpIndexed, + "MOV aa+X,Y", + ), + ( + 0xD9, + 2, + 5, + Measure::Straight, + Operands::DpIndexed, + "MOV aa+Y,X", + ), + (0xC5, 3, 5, Measure::Straight, Operands::Abs, "MOV !aaaa,A"), + (0xC9, 3, 5, Measure::Straight, Operands::Abs, "MOV !aaaa,X"), + (0xCC, 3, 5, Measure::Straight, Operands::Abs, "MOV !aaaa,Y"), + ( + 0xD5, + 3, + 6, + Measure::Straight, + Operands::Abs, + "MOV !aaaa+X,A", + ), + ( + 0xD6, + 3, + 6, + Measure::Straight, + Operands::Abs, + "MOV !aaaa+Y,A", + ), + (0xAF, 1, 4, Measure::Straight, Operands::None, "MOV (X)+,A"), + (0xC6, 1, 4, Measure::Straight, Operands::None, "MOV (X),A"), + (0xD7, 2, 7, Measure::Straight, Operands::Dp, "MOV [aa]+Y,A"), + ( + 0xC7, + 2, + 7, + Measure::Straight, + Operands::DpIndexed, + "MOV [aa+X],A", + ), + (0xDA, 2, 5, Measure::Straight, Operands::Dp, "MOVW aa,YA"), + // Push / pop. + (0x2D, 1, 4, Measure::Straight, Operands::None, "PUSH A"), + (0x4D, 1, 4, Measure::Straight, Operands::None, "PUSH X"), + (0x6D, 1, 4, Measure::Straight, Operands::None, "PUSH Y"), + (0x0D, 1, 4, Measure::Straight, Operands::None, "PUSH PSW"), + (0xAE, 1, 4, Measure::Straight, Operands::None, "POP A"), + (0xCE, 1, 4, Measure::Straight, Operands::None, "POP X"), + (0xEE, 1, 4, Measure::Straight, Operands::None, "POP Y"), + (0x8E, 1, 4, Measure::Straight, Operands::None, "POP PSW"), + // Compare with X / Y. + (0xC8, 2, 2, Measure::Straight, Operands::Imm, "CMP X,#nn"), + (0x3E, 2, 3, Measure::Straight, Operands::Dp, "CMP X,aa"), + (0x1E, 3, 4, Measure::Straight, Operands::Abs, "CMP X,!aaaa"), + (0xAD, 2, 2, Measure::Straight, Operands::Imm, "CMP Y,#nn"), + (0x7E, 2, 3, Measure::Straight, Operands::Dp, "CMP Y,aa"), + (0x5E, 3, 4, Measure::Straight, Operands::Abs, "CMP Y,!aaaa"), + // Increment / decrement of X and Y, which sit outside the shift group's own column. + (0x1D, 1, 2, Measure::Straight, Operands::None, "DEC X"), + (0xDC, 1, 2, Measure::Straight, Operands::None, "DEC Y"), + (0x3D, 1, 2, Measure::Straight, Operands::None, "INC X"), + (0xFC, 1, 2, Measure::Straight, Operands::None, "INC Y"), + // 16-bit ALU. + (0x7A, 2, 5, Measure::Straight, Operands::Dp, "ADDW YA,aa"), + (0x9A, 2, 5, Measure::Straight, Operands::Dp, "SUBW YA,aa"), + (0x5A, 2, 4, Measure::Straight, Operands::Dp, "CMPW YA,aa"), + (0x3A, 2, 6, Measure::Straight, Operands::Dp, "INCW aa"), + (0x1A, 2, 6, Measure::Straight, Operands::Dp, "DECW aa"), + (0x9E, 1, 12, Measure::Straight, Operands::None, "DIV YA,X"), + (0xCF, 1, 9, Measure::Straight, Operands::None, "MUL YA"), + // 1-bit ALU on the carry. + ( + 0xEA, + 3, + 5, + Measure::Straight, + Operands::AbsBit, + "NOT1 aaa.b", + ), + ( + 0xCA, + 3, + 6, + Measure::Straight, + Operands::AbsBit, + "MOV1 aaa.b,C", + ), + ( + 0xAA, + 3, + 4, + Measure::Straight, + Operands::AbsBit, + "MOV1 C,aaa.b", + ), + ( + 0x0A, + 3, + 5, + Measure::Straight, + Operands::AbsBit, + "OR1 C,aaa.b", + ), + ( + 0x2A, + 3, + 5, + Measure::Straight, + Operands::AbsBit, + "OR1 C,/aaa.b", + ), + ( + 0x4A, + 3, + 4, + Measure::Straight, + Operands::AbsBit, + "AND1 C,aaa.b", + ), + ( + 0x6A, + 3, + 4, + Measure::Straight, + Operands::AbsBit, + "AND1 C,/aaa.b", + ), + ( + 0x8A, + 3, + 5, + Measure::Straight, + Operands::AbsBit, + "EOR1 C,aaa.b", + ), + (0x60, 1, 2, Measure::Straight, Operands::None, "CLRC"), + (0x80, 1, 2, Measure::Straight, Operands::None, "SETC"), + (0xED, 1, 3, Measure::Straight, Operands::None, "NOTC"), + (0xE0, 1, 2, Measure::Straight, Operands::None, "CLRV"), + // Special ALU. + (0xDF, 1, 3, Measure::Straight, Operands::None, "DAA A"), + (0xBE, 1, 3, Measure::Straight, Operands::None, "DAS A"), + (0x9F, 1, 5, Measure::Straight, Operands::None, "XCN A"), + (0x4E, 3, 6, Measure::Straight, Operands::Abs, "TCLR1 !aaaa"), + (0x0E, 3, 6, Measure::Straight, Operands::Abs, "TSET1 !aaaa"), + // Conditional jumps. The cycle count is the taken one; see `Measure::BranchTaken`. + ( + 0x10, + 2, + 4, + Measure::BranchTaken(Flag::NegClear), + Operands::Rel, + "BPL rr", + ), + ( + 0x30, + 2, + 4, + Measure::BranchTaken(Flag::NegSet), + Operands::Rel, + "BMI rr", + ), + ( + 0x50, + 2, + 4, + Measure::BranchTaken(Flag::OvfClear), + Operands::Rel, + "BVC rr", + ), + ( + 0x70, + 2, + 4, + Measure::BranchTaken(Flag::OvfSet), + Operands::Rel, + "BVS rr", + ), + ( + 0x90, + 2, + 4, + Measure::BranchTaken(Flag::CarryClear), + Operands::Rel, + "BCC rr", + ), + ( + 0xB0, + 2, + 4, + Measure::BranchTaken(Flag::CarrySet), + Operands::Rel, + "BCS rr", + ), + ( + 0xD0, + 2, + 4, + Measure::BranchTaken(Flag::ZeroClear), + Operands::Rel, + "BNE rr", + ), + ( + 0xF0, + 2, + 4, + Measure::BranchTaken(Flag::ZeroSet), + Operands::Rel, + "BEQ rr", + ), + ( + 0x2E, + 3, + 7, + Measure::BranchTaken(Flag::None), + Operands::DpRel, + "CBNE aa,rr", + ), + ( + 0xDE, + 3, + 8, + Measure::BranchTaken(Flag::None), + Operands::DpIndexedRel, + "CBNE aa+X,rr", + ), + ( + 0xFE, + 2, + 6, + Measure::BranchTaken(Flag::None), + Operands::Rel, + "DBNZ Y,rr", + ), + ( + 0x6E, + 3, + 7, + Measure::BranchTaken(Flag::None), + Operands::DpRel, + "DBNZ aa,rr", + ), + ( + 0x2F, + 2, + 4, + Measure::BranchTaken(Flag::None), + Operands::Rel, + "BRA rr", + ), + // The opcodes that end the straight line. + (0x5F, 3, 3, JMP_ABS, Operands::Abs, "JMP !aaaa"), + (0x1F, 3, 6, JMP_ABS, Operands::Abs, "JMP [!aaaa+X]"), + (0x3F, 3, 8, CALL_ABS, Operands::Abs, "CALL !aaaa"), + (0x4F, 2, 6, IPL_VECTOR, Operands::Imm, "PCALL uu"), + (0x6F, 1, 5, NEEDS_STACK, Operands::None, "RET"), + (0x7F, 1, 6, NEEDS_STACK, Operands::None, "RET1"), + (0x0F, 1, 8, IPL_VECTOR, Operands::None, "BRK"), + // Wait / delay / control. + (0x00, 1, 2, Measure::Straight, Operands::None, "NOP"), + (0xEF, 1, 0, HALTS, Operands::None, "SLEEP"), + (0xFF, 1, 0, HALTS, Operands::None, "STOP"), + (0x20, 1, 2, Measure::Straight, Operands::None, "CLRP"), + (0x40, 1, 2, Measure::Straight, Operands::None, "SETP"), + (0xA0, 1, 3, Measure::Straight, Operands::None, "EI"), + (0xC0, 1, 3, Measure::Straight, Operands::None, "DI"), +]; + +/// An absolute jump's target is a fixed address, so eight copies cannot each reach the next. +const JMP_ABS: Measure = Measure::NotStraightLine( + "an absolute jump takes one fixed target, and every copy in the block sits at a different \ + address — a single encoding cannot make each copy fall into the next", +); + +/// A call has the jump's problem and pushes as well. +const CALL_ABS: Measure = Measure::NotStraightLine( + "a call has the absolute jump's fixed-target problem and pushes a return address the block \ + never pops", +); + +/// The vectored calls read their target out of the IPL ROM. +const IPL_VECTOR: Measure = Measure::NotStraightLine( + "the target comes from the IPL ROM at $FFC0-$FFFF, which every Group E program keeps mapped so \ + it can hand the APU back — the vector is not the cart's to point anywhere", +); + +/// Returns need an address the block did not push. +const NEEDS_STACK: Measure = Measure::NotStraightLine( + "a return pops an address the block never pushed; priming the stack for eight of them is a \ + different measurement with a different setup cost", +); + +/// `SLEEP` and `STOP` never come back. +const HALTS: Measure = Measure::NotStraightLine( + "halts the processor, and the SNES APU has no interrupt source to wake it — fullsnes gives its \ + cycle count as `?` for the same reason", +); + +/// The whole 256-entry map. +/// +/// # Panics +/// +/// Panics if the rules above leave a slot empty or fill one twice. That is a build-time gate on the +/// table's completeness: the regular groups and [`SINGLES`] are two independent descriptions of the +/// same map, and an overlap between them means one of the two is wrong about the opcode map's +/// shape. +#[must_use] +pub fn table() -> Vec { + let mut slots: Vec> = vec![None; 256]; + + let mut put = + |code: u8, len: u8, cycles: u8, measure: Measure, operands: Operands, name: String| { + let name: &'static str = Box::leak(name.into_boxed_str()); + assert!( + slots[code as usize].is_none(), + "opcode ${code:02X} is described twice — as {} and as {name}", + slots[code as usize].expect("checked").name + ); + slots[code as usize] = Some(Op { + code, + len, + cycles, + measure, + name, + operands, + }); + }; + + for (base, op) in ALU { + for (offset, len, cycles, operands, form) in ALU_FORMS { + put( + base + offset, + len, + cycles, + Measure::Straight, + operands, + format!("{op} {form}"), + ); + } + } + for (base, op) in SHIFT { + for (offset, len, cycles, operands, form) in SHIFT_FORMS { + put( + base + offset, + len, + cycles, + Measure::Straight, + operands, + format!("{op} {form}"), + ); + } + } + for bit in 0..8u8 { + put( + bit * 0x20 + 0x02, + 2, + 4, + Measure::Straight, + Operands::Dp, + format!("SET1 aa.{bit}"), + ); + put( + bit * 0x20 + 0x12, + 2, + 4, + Measure::Straight, + Operands::Dp, + format!("CLR1 aa.{bit}"), + ); + put( + bit * 0x20 + 0x03, + 3, + 7, + Measure::BranchTaken(Flag::None), + Operands::DpRel, + format!("BBS aa.{bit},rr"), + ); + put( + bit * 0x20 + 0x13, + 3, + 7, + Measure::BranchTaken(Flag::None), + Operands::DpRel, + format!("BBC aa.{bit},rr"), + ); + put( + bit * 0x10 + 0x01, + 1, + 8, + IPL_VECTOR, + Operands::None, + format!("TCALL {bit}"), + ); + put( + (bit + 8) * 0x10 + 0x01, + 1, + 8, + IPL_VECTOR, + Operands::None, + format!("TCALL {}", bit + 8), + ); + } + for &(code, len, cycles, measure, operands, name) in SINGLES { + put(code, len, cycles, measure, operands, name.to_owned()); + } + + slots + .into_iter() + .enumerate() + .map(|(code, op)| op.unwrap_or_else(|| panic!("opcode ${code:02X} has no description"))) + .collect() +} + +#[cfg(test)] +mod tests { + use super::{Measure, table}; + + /// Every slot filled exactly once — the assertion inside [`table`], reached as a test so the + /// failure names the opcode rather than arriving as a generator panic mid-build. + #[test] + fn the_map_is_complete_and_has_no_overlaps() { + let t = table(); + assert_eq!(t.len(), 256); + for (i, op) in t.iter().enumerate() { + assert_eq!(usize::from(op.code), i); + assert!( + (1..=3).contains(&op.len), + "${:02X} has length {}", + i, + op.len + ); + } + } + + /// The opcodes that cannot be timed in place are a short, named list. If this count moves, the + /// coverage report's account of what the sweep leaves out has to move with it. + #[test] + fn twenty_five_opcodes_are_not_straight_line() { + let t = table(); + let excluded: Vec<_> = t + .iter() + .filter(|op| matches!(op.measure, Measure::NotStraightLine(_))) + .map(|op| op.name) + .collect(); + assert_eq!( + excluded.len(), + 25, + "the non-straight-line set is {excluded:?}" + ); + } + + /// A branch's documented cost here is the taken one, which is what the sweep arranges to + /// measure. Every branch costs strictly more taken than the two-cycle fetch it would otherwise + /// be, so a zero would mean a transcription slip. + #[test] + fn every_branch_has_a_taken_cost() { + for op in table() { + if matches!(op.measure, Measure::BranchTaken(_)) { + assert!(op.cycles >= 4, "{} is {} cycles taken", op.name, op.cycles); + } + } + } +} diff --git a/tests/roms/AccuracySNES/gen/src/tests/apu.rs b/tests/roms/AccuracySNES/gen/src/tests/apu.rs index e138d92e..e5e83e9a 100644 --- a/tests/roms/AccuracySNES/gen/src/tests/apu.rs +++ b/tests/roms/AccuracySNES/gen/src/tests/apu.rs @@ -151,6 +151,11 @@ pub fn all() -> Vec { e3_09(), b2_07(), e3_13(), + // Late, and for the same class of reason `e9_02` is last: it drives timer 2 hard, and + // registered earlier it moved `E3.06`'s timer-ratio reading enough to fail that row's + // +/-6 band. Measured, not guessed -- E3.06 failed on code 1 with this row placed after + // `e3_09`. Nothing here depends on running late; it simply must not run before E3.06. + e2_10_instrument(), // LAST, and it has to be. Every other program leaves FLG's noise rate at zero, so the // noise LFSR never advances and `E9.01` reads the power-on seed. `E9.02` steps it and // nothing can put it back. Anything appended below this line runs after that has happened. @@ -168,7 +173,7 @@ pub fn all() -> Vec { /// caller's `.a8`/`.a16` directives come from its own `sep`/`rep` lines and a helper call is not /// one of those, so an undocumented width here would have the assembler and the CPU disagreeing /// about the size of the next immediate — and every instruction after it shifted. -fn upload_and_run(a: &mut Asm, prog: &Spc) { +pub fn upload_and_run(a: &mut Asm, prog: &Spc) { upload_and_run_tagged(a, prog, ""); } @@ -214,6 +219,62 @@ fn upload_only(a: &mut Asm, prog: &Spc) { a.l("sta APUIO0"); } +/// [`upload_and_run`] with a **much** longer bounded wait, for a program that takes tens of frames. +/// +/// The shared wait is `cpx #$8000` — some thirty thousand iterations, a fraction of one frame. That +/// is the right size for every other Group E program, which answers in well under a scanline, and +/// it is the reason a broken upload stands one test down instead of hanging the battery. +/// +/// `E2.10` is the exception and had to be measured to be believed: the 256-opcode sweep runs for +/// about **42 frames**, so the shared wait times out while the sweep is still on opcode `$0A`. The +/// symptom is indistinguishable from an APU that never booted — the row reported SKIP while the +/// SPC was working correctly and, checked directly in RAM, had already reached the right answer. +/// +/// This waits sixteen times through a full 16-bit counter, roughly two hundred frames' worth, so +/// the bound is still a bound and still far shorter than the battery's own frame budget. +pub fn upload_and_run_long(a: &mut Asm, prog: &Spc) { + upload_only(a, prog); + a.c("Wait for the done marker, but not forever — see this proc's doc comment for why the"); + a.c("shared wait is far too short for this one program."); + a.l("sep #$20"); + a.l("lda #$00"); + a.l("sta f:$7E01F8 ; the outer pass counter, in the same scratch page E3.06 uses"); + a.l("rep #$30"); + a.l("ldx #$0000"); + a.label("wait"); + a.l("sep #$20"); + a.l("lda APUIO0"); + a.l(&format!("cmp #${DONE:02X}")); + a.l("beq @ran"); + a.l("rep #$30"); + a.l("inx"); + a.l("bne @wait"); + a.c("X wrapped: one full pass of 65536. Count it, and give up after sixteen."); + a.l("sep #$20"); + a.l("lda f:$7E01F8"); + a.l("inc a"); + a.l("sta f:$7E01F8"); + a.l("cmp #$10"); + a.l("rep #$30"); + a.l("bne @wait"); + a.l("jmp @timeout"); + a.label("ran"); + a.c( + "Copy the answers out BEFORE releasing the program: once it jumps to the IPL, the boot ROM", + ); + a.c("overwrites ports 0 and 1 with its $AA/$BB announcement."); + a.l("sep #$20"); + a.l("lda APUIO1"); + a.l("sta f:$7E0100"); + a.l("lda APUIO2"); + a.l("sta f:$7E0101"); + a.l("lda APUIO3"); + a.l("sta f:$7E0102"); + a.c("Release: the program hands the APU back to the IPL so the NEXT test can upload at all."); + a.l(&format!("lda #${RELEASE:02X}")); + a.l("sta APUIO0"); +} + fn upload_and_run_tagged(a: &mut Asm, prog: &Spc, tag: &str) { // The image goes in the out-of-bank data segment, not inline in the test body: these are // several hundred bytes each and bank $00 is finite. `apu_upload` takes a 24-bit pointer @@ -395,7 +456,7 @@ fn apu_require_power_on(a: &mut Asm, why: &str) { /// Every test in this group needs it because `upload_and_run` branches to `@timeout` when the APU /// never answers, and that arm has to record SKIP and leave — a test whose APU did not boot has /// asserted nothing, and reporting a pass would be a lie about the only thing it was measuring. -fn apu_timeout_arm(a: &mut Asm) { +pub fn apu_timeout_arm(a: &mut Asm) { apu_timeout_arm_tagged(a, "timeout"); } @@ -9045,6 +9106,154 @@ fn b2_07() -> Test { ) } +/// The `E2.10` SPC700 cycle-sweep **instrument**, validated against three known opcodes. +/// +/// # Why this is a golden and does not claim `E2.10` +/// +/// `E2.10` is a *full 256-opcode* cycle sweep. This is the measuring apparatus it needs plus the +/// proof that the apparatus works, and nothing more — so it records values rather than scoring +/// them, and `dossier.rs::UNENUMERATED` says exactly that. Claiming `E2.10` from three opcodes +/// would be the honesty-gate violation the whole provenance tier exists to prevent. +/// +/// # The instrument +/// +/// Timer 2 at `T2DIV = 1` steps `T2OUT` every **32 SMP base clocks — 16 opcode cycles**. That is +/// far too coarse for one instruction, so the measurement is a differential over a repeated block: +/// +/// **That rate was MEASURED, and the first draft of this comment derived it wrongly.** `Timer<16>` +/// fires stage 1 every 16 base clocks, but `T2OUT` counts stage-2 increments, which happen on the +/// 1→0 transition — every *two* stage-1 toggles, so every 32 base clocks. Deriving "8 opcode +/// cycles" from the 16 predicted exactly double the real differences (96/224 against a measured +/// 48/112) and would have shipped a doc that disagreed with its own row's numbers. +/// +/// ```text +/// loop: +/// sum += T2OUT (read-and-clear) +/// until ITERATIONS +/// ``` +/// +/// One cycle of difference becomes `REPEATS * ITERATIONS / 16` = **16 ticks**, which is still +/// enormous against the +/-1 tick the counter quantises at. The poll's own cost sits in every arm and cancels +/// in the difference, exactly as `E3.06`'s ratio does. +/// +/// **The 4-bit ceiling decides `REPEATS`.** `T2OUT` is read-and-clear and four bits, so one +/// iteration must stay under 16 ticks = 128 cycles or the count silently wraps — the trap `E3.06` +/// was rewritten to escape. Eight copies of the slowest opcode measured here (`MUL YA`, 9 cycles) +/// plus the ~15-cycle poll is 87 cycles = ~5 ticks, with room to spare. An opcode above ~30 cycles +/// would need a smaller `REPEATS`, which is why the full sweep should not assume one constant +/// serves all 256. +/// +/// # The three validators +/// +/// Chosen because their costs are independently known from `spc700_exec.rs`'s dispatch — each +/// instruction's cycles are its `read`/`idle` calls plus the fetch — and because `XCN` is separately +/// pinned at 5 by `E1.14`: +/// +/// | opcode | cycles | expected difference from `NOP` | in ticks | +/// |---|---:|---:|---:| +/// | `NOP` | 2 | — (the baseline) | — | +/// | `XCN` | 5 | 3 | **48** | +/// | `MUL YA` | 9 | 7 | **112** | +/// +/// Measured: baseline **94**, `XCN` **142** (+48), `MUL YA` **206** (+112) — both exact. +/// +/// A core whose `XCN` were 4 or 6 would read 32 or 64 ticks, not 48. The apparatus is what is being +/// validated here; the opcodes are the ruler it is checked against. +fn e2_10_instrument() -> Test { + /// Copies of the opcode per iteration. Bounded by `T2OUT`'s four bits — see the doc comment. + const REPEATS: usize = 8; + /// Iterations. `REPEATS * ITERATIONS / 8` is the ticks-per-cycle-of-difference resolution. + const ITERATIONS: u8 = 32; + + let mut prog = Spc::new(); + prog.mov_x_imm(0xEF) + .mov_sp_x() + .mov_dp_imm(0xFC, 0x01) // T2DIV = 1: T2OUT steps every 16 opcode cycles (measured) + .mov_dp_imm(0xF1, 0x84); // enable timer 2; bit 7 keeps the IPL mapped + + e2_10_measure(&mut prog, REPEATS, ITERATIONS, PORT1, |p| { + p.nop(); + }); + e2_10_measure(&mut prog, REPEATS, ITERATIONS, PORT2, |p| { + p.xcn(); + }); + e2_10_measure(&mut prog, REPEATS, ITERATIONS, PORT3, |p| { + p.mul_ya(); + }); + + prog.mov_dp_imm(0xF1, 0x80) + .mov_a_imm(DONE) + .mov_dp_a(PORT0) + .release_to_ipl(); + + let mut a = Asm::new(); + upload_and_run(&mut a, &prog); + a.l("rep #$30"); + a.l("lda f:$7E0100"); + a.l("and #$00FF"); + a.record( + 277, + "E2.10 instrument: 8xNOP x32, timer-2 ticks (the baseline)", + ); + a.l("lda f:$7E0101"); + a.l("and #$00FF"); + a.record(278, "E2.10 instrument: 8xXCN x32 (expect baseline + 48)"); + a.l("lda f:$7E0102"); + a.l("and #$00FF"); + a.record( + 279, + "E2.10 instrument: 8xMUL YA x32 (expect baseline + 112)", + ); + apu_timeout_arm(&mut a); + a.finish( + "E2.10i", + 'E', + "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, + None, + ) +} + +/// Emit one arm of [`e2_10_instrument`]: `repeats` copies of `body`, `iterations` times, summing +/// `T2OUT` into `port`. +/// +/// The sum is 8-bit and the arms are sized so it cannot overflow: the slowest arm here is ~11 ticks +/// an iteration over 32 iterations, about 350 — which does NOT fit in eight bits, so the low byte is +/// what reaches the port and the caller compares differences rather than absolutes. Differences of +/// the low byte are still exact while the arms stay within 256 ticks of each other, which the +/// expected 96 and 224 both do. +fn e2_10_measure( + prog: &mut Spc, + repeats: usize, + iterations: u8, + port: u8, + body: impl Fn(&mut Spc), +) { + prog.mov_dp_imm(0x10, 0x00) // running sum (low byte) + .mov_dp_imm(0x11, 0x00) // iteration counter + .mov_a_dp(0xFF); // drain T2OUT so this arm counts only its own loop + let top = prog.here(); + for _ in 0..repeats { + body(prog); + } + prog.mov_a_dp(0xFF) // T2OUT, read-and-clear + .mov_dp_a(0x12) + .mov_a_dp(0x10) + .clrc() + .adc_a_dp(0x12) + .mov_dp_a(0x10) + .inc_dp(0x11) + .mov_a_dp(0x11) + .cmp_a_imm(iterations); + prog.bne_back(top); + prog.mov_a_dp(0x10).mov_dp_a(port); +} + /// Emit: set `$F0` to `test`, then accumulate timer-0 ticks over `polls` passes into `port`. /// /// The counter is drained before the loop so each phase starts from zero, and read-and-cleared diff --git a/tests/roms/AccuracySNES/gen/src/tests/apu_sweep.rs b/tests/roms/AccuracySNES/gen/src/tests/apu_sweep.rs new file mode 100644 index 00000000..d924a83d --- /dev/null +++ b/tests/roms/AccuracySNES/gen/src/tests/apu_sweep.rs @@ -0,0 +1,632 @@ +//! `E2.10` — the full 256-opcode SPC700 cycle sweep. +//! +//! # What the row claims +//! +//! For every opcode the SPC700 can execute in a straight line, the cart measures how long it takes +//! and compares that against the cycle count **fullsnes** documents for it. The comparison happens +//! on the SPC, against a table the generator built from the reference (`spc_opcodes.rs`); the cart +//! reports how many opcodes it measured and how many disagreed, and the host supplies no expected +//! values at all. A result therefore means the same thing on any emulator and on a flash cart, +//! which is the property the whole battery is built around. +//! +//! # How one opcode is timed +//! +//! Timer 2 at `T2DIV = 1` steps `T2OUT` once every 32 SMP base clocks — **16 opcode cycles**, a +//! figure that was measured rather than derived (the derivation from `Timer<16>` gives 8 and is +//! exactly half, because `T2OUT` counts stage-2 increments and those happen every *two* stage-1 +//! toggles). Sixteen cycles is far too coarse for one instruction, so the sweep never times one: +//! it times a block of six copies, sixteen times over, and reads the difference against the same +//! block built from `NOP`. +//! +//! ```text +//! iteration: reset X/Y/SP and the safe data window <- identical in every arm +//! CALL block <- identical in every arm +//! prologue 4 bytes, 4 cycles <- identical COST in every arm +//! opcode x6 <- the only thing that differs +//! epilogue 5 bytes, 11 cycles <- identical in every arm +//! sum += T2OUT <- identical in every arm +//! ``` +//! +//! Everything except the six copies is common, so it cancels in the difference, and one cycle of +//! difference becomes `6 * 16 / 16` = **6 ticks**. The sum across iterations is exact to ±1 (the +//! reads are read-and-clear, so they partition the interval), and the baseline carries its own ±1, +//! so the honest band is ±2. The row allows ±3, which is still half a cycle. +//! +//! # Why the block is built in RAM +//! +//! Six copies of each of 256 opcodes, plus a prologue and epilogue each, is some eight kilobytes of +//! straight-line code — more than the APU program budget and more than the ROM bank has. Instead +//! the driver assembles one 27-byte block in RAM per opcode from a table, and calls it. Four +//! **page-aligned** 256-byte tables (length-and-prologue, and the three instruction bytes) plus a +//! fifth of documented cycle counts make every lookup a single `MOV A,!table+X` with the opcode in +//! `X` — no pointer arithmetic anywhere in the driver, which is what keeps it short enough to +//! reason about. +//! +//! # The operands are chosen, and every one of the three rules has teeth +//! +//! An operand must not name an I/O register, must not name the block itself, and must stay in range +//! once an index register is added. The first is not a formality: most SPC700 stores perform a +//! dummy read of their destination, so a *write* to `$FF` would clear `T2OUT` — the sweep would be +//! erasing its own instrument. See [`crate::spc_opcodes::Operands`]. +//! +//! # Branches are measured taken, on purpose +//! +//! 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 copies runs straight through +//! whichever way each one goes, and the arm can arrange for the taken path — the more interesting +//! of the two, and the one whose cost differs between cores. The prologue sets whichever flag that +//! needs, in a fixed four-byte two-instruction shape so its cost is the same in every arm. +//! +//! # What it does not cover, and why that is not a gap +//! +//! Twenty-five opcodes end the straight line rather than continuing it: 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 live in the IPL ROM, which every Group E +//! program keeps mapped so it can hand the APU back), 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 this row asks has no answer. They are named in `spc_opcodes.rs`, counted by a +//! unit test there, and the count is asserted on-cart — a sweep that measured 230 or 232 would fail +//! rather than quietly cover a different set. + +use crate::dsl::{Asm, Kind, Provenance, Test}; +use crate::spc::{DONE, PORT1, PORT2, PORT3, Spc}; +use crate::spc_opcodes::{Flag, Measure, Op, Operands, table}; + +use super::apu::{apu_timeout_arm, upload_and_run_long}; + +/// Copies of the opcode per block. Six keeps the slowest opcode's iteration under `T2OUT`'s +/// four-bit ceiling: `DIV YA,X` at 12 cycles is 72 cycles of block against a ~87-cycle fixed cost, +/// which is ten ticks with six to spare. +const REPEATS: u8 = 6; + +/// Iterations per opcode. `REPEATS * ITERATIONS / 16` = **6 ticks per cycle of difference**, and +/// the largest total — `DIV YA,X` again — stays inside a byte. +const ITERATIONS: u8 = 16; + +/// Ticks per cycle of difference. Kept as a named constant because the on-cart arithmetic +/// multiplies by it and the doc comment above reasons about it; they must not drift apart. +const TICKS_PER_CYCLE: u8 = REPEATS * ITERATIONS / 16; + +/// How far a measurement may sit from its documented expectation. Two comes from quantisation — +/// ±1 on the arm and ±1 on the baseline — and the third is margin. One cycle is +/// [`TICKS_PER_CYCLE`] away, so the band cannot swallow a real error. +const BAND: u8 = 3; + +/// Opcodes the sweep measures: 256 less the ones that end the straight line. +const MEASURED: u8 = 231; + +/// Where every Group E program is uploaded, and therefore what an absolute address in this one +/// means. `upload_and_run` sets both the destination and the entry point to it. +const BASE: u16 = 0x0200; + +/// APU RAM the sweep uses beyond its uploaded image. +mod ram { + /// Per-opcode results, one byte each. + /// + /// Above the uploaded image, and [`super::e2_10`] asserts that at build time. The first draft + /// put this at `$0900` and the image reached `$0948`, so the sweep overwrote its own driver as + /// it recorded results — the battery reported the row as SKIP, because the code it was running + /// stopped being code. + pub const RESULTS: u16 = 0x0C00; + /// The block the driver assembles and calls. + pub const BLOCK: u16 = 0x0D00; + /// The window every memory operand points into. Far from the image, the block, the stack and + /// the zero page, with room above it for an index register to be added. + pub const WINDOW: u16 = 0x0E00; +} + +/// Direct-page variables. `$30` upward is the operand window's own page; `$00-$0F` is the driver. +mod dp { + /// The opcode being measured. + pub const OP: u8 = 0x00; + /// Its length in bytes. + pub const LEN: u8 = 0x01; + /// Its prologue index. + pub const PRO: u8 = 0x02; + /// Iteration counter. + pub const ITER: u8 = 0x03; + /// Accumulated ticks for this opcode. + pub const SUM: u8 = 0x04; + /// Where the next byte of the block goes, as an offset from [`super::ram::BLOCK`]. + pub const CURSOR: u8 = 0x05; + /// Scratch. + pub const TMP: u8 = 0x06; + /// How many opcodes were actually measured. + pub const COUNT: u8 = 0x07; + /// How many disagreed with their documented cycle count. + pub const BAD: u8 = 0x08; + /// The first that did, or `$FF`. + pub const FIRST_BAD: u8 = 0x09; + /// The `NOP` baseline. + pub const BASE: u8 = 0x0A; + /// The three instruction bytes, staged between the table read and the block write. + pub const BYTES: u8 = 0x0C; + + /// The low byte of the operand window pointer, and the plain direct-page operand. + pub const WINDOW_PTR: u8 = 0x30; + /// A direct-page byte holding `$FF`, so `BBS aa.b` has a set bit to branch on. + pub const ONES: u8 = 0x32; +} + +/// The prologues, indexed by [`Flag`]. Each is exactly two instructions, four bytes and four +/// cycles, so the choice costs nothing that could leak into a difference. +/// +/// `CLRV` would be the obvious way to clear `V`, but it is one byte and two cycles where `ADC` is +/// two and two — a uniform *shape* matters more here than a shorter one, because the shape is what +/// guarantees the cost is identical across arms. +const PROLOGUES: [[u8; 4]; 7] = [ + [0xE8, 0x01, 0xE8, 0x01], // MOV A,#$01 x2 — N=0, Z=0, A=1 (the default, and CBNE's A != [aa]) + [0xE8, 0x80, 0xE8, 0x80], // MOV A,#$80 x2 — N=1 + [0xE8, 0x00, 0xE8, 0x00], // MOV A,#$00 x2 — Z=1 + [0xE8, 0xFF, 0x68, 0x00], // MOV A,#$FF; CMP A,#$00 — C=1 + [0xE8, 0x00, 0x68, 0xFF], // MOV A,#$00; CMP A,#$FF — C=0 + [0xE8, 0x7F, 0x88, 0x7F], // MOV A,#$7F; ADC A,#$7F — V=1 whatever the carry in + [0xE8, 0x00, 0x88, 0x00], // MOV A,#$00; ADC A,#$00 — V=0 whatever the carry in +]; + +/// Pick the prologue that makes this opcode's branch taken. +const fn prologue_for(measure: Measure) -> u8 { + match measure { + Measure::BranchTaken(flag) => match flag { + Flag::NegSet => 1, + Flag::ZeroSet => 2, + Flag::CarrySet => 3, + Flag::CarryClear => 4, + Flag::OvfSet => 5, + Flag::OvfClear => 6, + // `N = 0`, `Z = 0` and the `CBNE`/`DBNZ` conditions are all what the default arranges. + Flag::NegClear | Flag::ZeroClear | Flag::None => 0, + }, + _ => 0, + } +} + +/// The epilogue every block ends with. +/// +/// It is not just a `RET`. The block may have pushed (`PUSH A` six times), popped, moved `SP` +/// outright (`MOV SP,X`), or set the direct-page flag (`SETP`, or a `POP PSW` that loaded garbage) +/// — and a `RET` under any of those returns somewhere else. `CLRP` puts the direct page back and +/// `MOV SP,#$ED` puts the stack pointer where `CALL` left it, both at a fixed cost. +/// +/// `$ED` is the driver's `$EF` less the two bytes `CALL` pushed. +const EPILOGUE: [u8; 5] = [ + 0x20, // CLRP + 0xCD, 0xED, // MOV X,#$ED + 0xBD, // MOV SP,X + 0x6F, // RET +]; + +/// Fill in an opcode's operand bytes with something safe to execute six times in a row. +/// +/// See the module docs on what "safe" has to mean. The choices below are the whole of it: +/// +/// | operand | value | why | +/// |---|---|---| +/// | direct page | `$30` | the window pointer's own page, clear of `$00-$0F` and of `$F0-$FF` | +/// | direct page, indexed | `$00` | `X` and `Y` are both `$30`, so `$00 + X` lands on the same byte | +/// | absolute | `$0C00` | far from the image, the block and the stack; `+ $30` stays inside it | +/// | bit address | `$0C00` bit 0 | the same window; the bit index rides in the top three bits | +/// | relative | `0` | the taken path lands on the next instruction — see the module docs | +/// +/// `BBS aa.b` is the one exception, and it earns it: with the window byte at zero every `BBS` would +/// find its bit clear and measure the *not-taken* cost while the table says taken. It points at +/// `$32` instead, which the driver keeps at `$FF`. +const fn operand_bytes(op: Op) -> [u8; 3] { + let [wlo, whi] = ram::WINDOW.to_le_bytes(); + let is_bbs = (op.code & 0x1F) == 0x03; + // `X` and `Y` both hold `WINDOW_PTR` while a block runs, so an indexed direct-page operand of + // zero is what lands on the window. Naming the window directly would land 48 bytes past it. + let indexed = dp::WINDOW_PTR.wrapping_sub(dp::WINDOW_PTR); // = 0, written so the reason shows + match op.operands { + Operands::None | Operands::Imm | Operands::Rel => [op.code, 0x00, 0x00], + Operands::Dp => [op.code, dp::WINDOW_PTR, 0x00], + // `CBNE aa+X,rr`'s displacement is zero like every other branch's, so it shares this arm. + Operands::DpIndexed | Operands::DpIndexedRel => [op.code, indexed, 0x00], + // `cmd aa,bb` encodes the SOURCE first. Both sit in the window's page. + Operands::DpDp => [op.code, dp::WINDOW_PTR + 1, dp::WINDOW_PTR], + // `cmd aa,#nn` encodes the immediate first. + Operands::ImmDp => [op.code, 0x00, dp::WINDOW_PTR], + // `AbsBit` shares this arm at **bit zero**: the bit index rides in the top three bits of + // the 16-bit operand, and any bit of a window byte answers the same question — so the sweep + // picks one rather than deriving one from the opcode, which for this column would be + // deriving it from nothing. + Operands::Abs | Operands::AbsBit => [op.code, wlo, whi], + Operands::DpRel => [ + op.code, + if is_bbs { dp::ONES } else { dp::WINDOW_PTR }, + 0x00, + ], + } +} + +/// The five page-aligned tables, laid out so a lookup is one `MOV A,!table+X`. +struct Tables { + /// Length in the low nibble, prologue index in the high one. Zero length means "not measured". + lenp: u16, + /// The opcode byte. + b0: u16, + /// Operand byte 1. + b1: u16, + /// Operand byte 2. + b2: u16, + /// The documented cycle count. + cyc: u16, + /// The seven prologues, 28 bytes, indexed by `prologue * 4 + i`. + prologues: u16, +} + +/// Build the data blob and return where each table landed. +/// +/// The blob is padded so the first table starts on a page boundary. Two hundred and fifty-odd bytes +/// of padding buys an indexed lookup with no pointer arithmetic in it — a trade worth making in a +/// bank with kilobytes free, and not one worth making if it were tight. +fn build_tables(prog: &mut Spc, base: u16) -> Tables { + let ops = table(); + let mut blob = Vec::new(); + + // `data_first` emits a three-byte `JMP` over the data, so the data starts at `base + 3`. + let data_start = base + 3; + let first_page = (data_start + 0xFF) & 0xFF00; + blob.resize(usize::from(first_page - data_start), 0x00); + + let mut lenp = Vec::with_capacity(256); + let mut b0 = Vec::with_capacity(256); + let mut b1 = Vec::with_capacity(256); + let mut b2 = Vec::with_capacity(256); + let mut cyc = Vec::with_capacity(256); + for op in &ops { + let skipped = matches!(op.measure, Measure::NotStraightLine(_)); + let bytes = operand_bytes(*op); + lenp.push(if skipped { + 0x00 + } else { + op.len | (prologue_for(op.measure) << 4) + }); + b0.push(bytes[0]); + b1.push(bytes[1]); + b2.push(bytes[2]); + cyc.push(op.cycles); + } + + let addr_lenp = first_page; + let addr_b0 = first_page + 0x100; + let addr_b1 = first_page + 0x200; + let addr_b2 = first_page + 0x300; + let addr_cyc = first_page + 0x400; + let addr_pro = first_page + 0x500; + blob.extend_from_slice(&lenp); + blob.extend_from_slice(&b0); + blob.extend_from_slice(&b1); + blob.extend_from_slice(&b2); + blob.extend_from_slice(&cyc); + for p in PROLOGUES { + blob.extend_from_slice(&p); + } + + let emitted = prog.data_first(base, &blob); + assert_eq!( + emitted, data_start, + "the table blob did not land where the layout assumed" + ); + + Tables { + lenp: addr_lenp, + b0: addr_b0, + b1: addr_b1, + b2: addr_b2, + cyc: addr_cyc, + prologues: addr_pro, + } +} + +/// Emit the sweep driver. +#[allow(clippy::too_many_lines)] +fn driver(prog: &mut Spc, t: &Tables, base: u16) { + let [wlo, whi] = ram::WINDOW.to_le_bytes(); + + prog.mov_x_imm(0xEF) + .mov_sp_x() + .mov_dp_imm(0xFC, 0x01) // T2DIV = 1 + .mov_dp_imm(0xF1, 0x84) // enable timer 2; bit 7 keeps the IPL mapped + .mov_dp_imm(dp::COUNT, 0x00) + .mov_dp_imm(dp::BAD, 0x00) + .mov_dp_imm(dp::FIRST_BAD, 0xFF) + .mov_dp_imm(dp::OP, 0x00); + + // ---- pass 1: measure every straight-line opcode ---- + let op_loop = prog.here(); + prog.mov_x_dp(dp::OP) + .mov_a_abs_x(t.lenp) + .mov_dp_a(dp::TMP) + .and_a_imm(0x0F) + .mov_dp_a(dp::LEN); + // A short branch cannot clear the whole per-opcode body, so the test is inverted: fall into + // the body when the length is non-zero, and take an absolute jump to the tail when it is not. + let measure_it = prog.bne_fwd(); + let skip = prog.jmp_fwd(); + prog.patch_fwd(measure_it); + prog.mov_a_dp(dp::TMP) + .xcn() + .and_a_imm(0x0F) + .mov_dp_a(dp::PRO); + + // ---- assemble the block: prologue, six copies, epilogue ---- + prog.mov_dp_imm(dp::CURSOR, 0x00); + // X walks the prologue table from `prologue * 4`; Y counts the four bytes. + prog.mov_a_dp(dp::PRO) + .asl_a() + .asl_a() + .mov_x_a() + .mov_y_imm(0x00); + let pro_loop = prog.here(); + prog.mov_a_abs_x(t.prologues) + .mov_dp_a(dp::BYTES) + .inc_x_reg(); + prog.mov_a_x().mov_dp_a(dp::TMP); // stash the prologue-table index + prog.mov_x_dp(dp::CURSOR) + .mov_a_dp(dp::BYTES) + .mov_abs_x_a(ram::BLOCK) + .inc_dp(dp::CURSOR); + prog.mov_x_dp(dp::TMP).inc_y().cmp_y_imm(0x04); + prog.bne_back(pro_loop); + + // Six copies. Three bytes are written every time and the cursor advances by the real length, + // so a one- or two-byte opcode's tail is simply overwritten by whatever comes next — and the + // epilogue, written last, overwrites the final tail. + prog.mov_y_imm(0x00); + let copy_loop = prog.here(); + prog.mov_x_dp(dp::OP) + .mov_a_abs_x(t.b0) + .mov_dp_a(dp::BYTES) + .mov_a_abs_x(t.b1) + .mov_dp_a(dp::BYTES + 1) + .mov_a_abs_x(t.b2) + .mov_dp_a(dp::BYTES + 2); + prog.mov_x_dp(dp::CURSOR) + .mov_a_dp(dp::BYTES) + .mov_abs_x_a(ram::BLOCK) + .inc_x_reg() + .mov_a_dp(dp::BYTES + 1) + .mov_abs_x_a(ram::BLOCK) + .inc_x_reg() + .mov_a_dp(dp::BYTES + 2) + .mov_abs_x_a(ram::BLOCK); + prog.mov_a_dp(dp::CURSOR) + .clrc() + .adc_a_dp(dp::LEN) + .mov_dp_a(dp::CURSOR); + prog.inc_y().cmp_y_imm(REPEATS); + prog.bne_back(copy_loop); + + for (i, byte) in EPILOGUE.iter().enumerate() { + prog.mov_x_dp(dp::CURSOR).mov_a_imm(*byte); + for _ in 0..i { + prog.inc_x_reg(); + } + prog.mov_abs_x_a(ram::BLOCK); + } + + // ---- measure it ---- + prog.mov_dp_imm(dp::ITER, 0x00) + .mov_dp_imm(dp::SUM, 0x00) + .mov_a_dp(0xFF); // drain T2OUT so the interval starts from zero + let iter_loop = prog.here(); + prog.mov_x_imm(0xEF) + .mov_sp_x() + .mov_x_imm(dp::WINDOW_PTR) + .mov_y_imm(dp::WINDOW_PTR) + .mov_dp_imm(dp::WINDOW_PTR, wlo) + .mov_dp_imm(dp::WINDOW_PTR + 1, whi) + .mov_dp_imm(dp::ONES, 0xFF); + prog.call_abs(ram::BLOCK); + prog.mov_a_dp(0xFF) + .mov_dp_a(dp::TMP) + .mov_a_dp(dp::SUM) + .clrc() + .adc_a_dp(dp::TMP) + .mov_dp_a(dp::SUM) + .inc_dp(dp::ITER) + .mov_a_dp(dp::ITER) + .cmp_a_imm(ITERATIONS); + prog.bne_back(iter_loop); + + prog.mov_x_dp(dp::OP) + .mov_a_dp(dp::SUM) + .mov_abs_x_a(ram::RESULTS) + .inc_dp(dp::COUNT); + + prog.patch_jmp_fwd(skip, base); + prog.inc_dp(dp::OP).mov_a_dp(dp::OP).cmp_a_imm(0x00); + // The body is far beyond a branch's reach in both directions, so the loop is a short + // conditional exit over a long absolute jump rather than a conditional jump back. + let swept = prog.beq_fwd(); + prog.jmp_back(op_loop, base); + prog.patch_fwd(swept); + + // ---- pass 2: compare every result against its documented cycle count ---- + // + // `NOP` is opcode $00 and costs two cycles, so its result IS the baseline the differences are + // taken from. Nothing here needs the baseline to be any particular value — only that the same + // fixed overhead sits in it as in every other arm, which is what the identical loop guarantees. + prog.mov_x_imm(0x00) + .mov_a_abs_x(ram::RESULTS) + .mov_dp_a(dp::BASE); + prog.mov_dp_imm(dp::OP, 0x00); + let chk_loop = prog.here(); + prog.mov_x_dp(dp::OP).mov_a_abs_x(t.lenp).and_a_imm(0x0F); + let chk_skip = prog.beq_fwd(); + + // expected = base + (cycles - 2) * TICKS_PER_CYCLE + prog.mov_a_abs_x(t.cyc).setc().sbc_a_imm(0x02); + prog.mov_y_a() + .mov_a_imm(TICKS_PER_CYCLE) + .mul_ya() + .clrc() + .adc_a_dp(dp::BASE) + .mov_dp_a(dp::TMP); + // delta = measured - expected, then biased by BAND so the whole allowed range is 0 ..= 2*BAND + prog.mov_x_dp(dp::OP) + .mov_a_abs_x(ram::RESULTS) + .setc() + .sbc_a_dp(dp::TMP) + .clrc() + .adc_a_imm(BAND) + .cmp_a_imm(BAND * 2 + 1); + let chk_ok = prog.bcc_fwd(); + prog.inc_dp(dp::BAD).mov_a_dp(dp::FIRST_BAD).cmp_a_imm(0xFF); + let already = prog.bne_fwd(); + prog.mov_a_dp(dp::OP).mov_dp_a(dp::FIRST_BAD); + prog.patch_fwd(already); + prog.patch_fwd(chk_ok); + prog.patch_fwd(chk_skip); + prog.inc_dp(dp::OP).mov_a_dp(dp::OP).cmp_a_imm(0x00); + prog.bne_back(chk_loop); + + prog.mov_dp_imm(0xF1, 0x80) // stop timer 2; the IPL stays mapped for the handover + .mov_a_dp(dp::BAD) + .mov_dp_a(PORT1) + .mov_a_dp(dp::COUNT) + .mov_dp_a(PORT2) + .mov_a_dp(dp::FIRST_BAD) + .mov_dp_a(PORT3) + .mov_a_imm(DONE) + .mov_dp_a(0xF4) + .release_to_ipl(); +} + +/// The `E2.10` row. +pub fn e2_10() -> Test { + let mut prog = Spc::new(); + let tables = build_tables(&mut prog, BASE); + driver(&mut prog, &tables, BASE); + let top = BASE + u16::try_from(prog.bytes().len()).expect("a program is smaller than RAM"); + assert!( + top <= ram::RESULTS, + "the sweep image reaches ${top:04X}, which is inside the result page at \ + ${:04X} — it would overwrite its own driver as it records", + ram::RESULTS + ); + + let mut a = Asm::new(); + upload_and_run_long(&mut a, &prog); + a.l("rep #$30"); + a.l("lda f:$7E0100"); + a.l("and #$00FF"); + a.record(280, "E2.10 opcodes disagreeing with fullsnes (expect 0)"); + a.l("lda f:$7E0101"); + a.l("and #$00FF"); + a.record(281, "E2.10 opcodes measured (expect 231 = 256 - 25)"); + a.l("lda f:$7E0102"); + a.l("and #$00FF"); + a.record(282, "E2.10 first disagreeing opcode ($FF = none)"); + + a.c("Liveness first. A driver that fell over after one opcode would report no disagreements,"); + a.c("and a `0` in slot 280 would then read as a pass — so the count of opcodes actually"); + a.c("measured is asserted before anything is concluded from the count of failures."); + a.l("lda f:$7E0101"); + a.l("and #$00FF"); + a.assert_a16_range( + u16::from(MEASURED), + u16::from(MEASURED), + "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", + ); + + a.c("The row itself: every measured opcode's timing agrees with the cycle count fullsnes"); + a.c("documents for it, to within half a cycle."); + a.l("lda f:$7E0100"); + a.l("and #$00FF"); + a.assert_a16_range( + 0, + 0, + "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", + ); + apu_timeout_arm(&mut a); + a.finish( + "E2.10", + 'E', + "256-opcode cycle sweep", + Provenance::Documented("fullsnes, SNES APU SPC700 CPU instruction set"), + Kind::Scored, + None, + ) +} + +#[cfg(test)] +mod tests { + use super::{MEASURED, PROLOGUES, REPEATS, TICKS_PER_CYCLE, build_tables, operand_bytes}; + use crate::spc::Spc; + use crate::spc_opcodes::{Measure, table}; + + /// No operand may name an I/O register. This is the rule with teeth: most SPC700 stores dummy- + /// read their destination, so an operand of `$FF` would clear `T2OUT` and the sweep would erase + /// its own instrument mid-measurement. + #[test] + fn no_operand_reaches_the_io_page() { + for op in table() { + if matches!(op.measure, Measure::NotStraightLine(_)) { + continue; + } + let b = operand_bytes(op); + for (i, byte) in b.iter().enumerate().take(usize::from(op.len)).skip(1) { + assert!( + *byte < 0xF0 || op.operands == crate::spc_opcodes::Operands::Imm, + "{} operand byte {i} is ${byte:02X}, which reaches the I/O page", + op.name + ); + } + } + } + + /// Every prologue is the same shape, because its cost has to cancel in the difference. + #[test] + fn every_prologue_is_four_bytes_of_two_two_cycle_instructions() { + for p in PROLOGUES { + assert_eq!(p.len(), 4); + // Both instructions are two-byte immediates: `MOV A,#nn` ($E8), `CMP A,#nn` ($68) or + // `ADC A,#nn` ($88). All three are two cycles. + assert!(matches!(p[0], 0xE8), "first opcode ${:02X}", p[0]); + assert!( + matches!(p[2], 0xE8 | 0x68 | 0x88), + "second opcode ${:02X}", + p[2] + ); + } + } + + /// The block has to fit in one page, because the driver addresses it with an 8-bit index. + #[test] + fn the_assembled_block_fits_the_indexed_write() { + let longest = 4 + usize::from(REPEATS) * 3 + 5; + assert!(longest <= 0xFF, "the longest block is {longest} bytes"); + } + + /// The tables land page-aligned, which is the whole reason a lookup is one instruction. + #[test] + fn the_tables_are_page_aligned() { + let mut prog = Spc::new(); + let t = build_tables(&mut prog, 0x0200); + for addr in [t.lenp, t.b0, t.b1, t.b2, t.cyc, t.prologues] { + assert_eq!(addr & 0xFF, 0, "table at ${addr:04X} is not page aligned"); + } + } + + /// The measured count the cart asserts has to be the one the table actually produces. + #[test] + fn the_asserted_measured_count_matches_the_table() { + let n = table() + .iter() + .filter(|op| !matches!(op.measure, Measure::NotStraightLine(_))) + .count(); + assert_eq!(n, usize::from(MEASURED)); + } + + /// The resolution the doc comment claims. + #[test] + fn one_cycle_of_difference_is_six_ticks() { + assert_eq!(TICKS_PER_CYCLE, 6); + } +} diff --git a/tests/roms/AccuracySNES/gen/src/tests/mod.rs b/tests/roms/AccuracySNES/gen/src/tests/mod.rs index 3b3de714..c99ccfef 100644 --- a/tests/roms/AccuracySNES/gen/src/tests/mod.rs +++ b/tests/roms/AccuracySNES/gen/src/tests/mod.rs @@ -8,6 +8,7 @@ //! each remaining block, and `to-dos/ROADMAP.md` carries the T-04-* ticket IDs. pub mod apu; +pub mod apu_sweep; pub mod bus; pub mod cart; pub mod cpu; @@ -27,6 +28,7 @@ pub fn all() -> Vec { v.extend(bus::all()); v.extend(dma::all()); v.extend(apu::all()); + v.push(apu_sweep::e2_10()); v.extend(input::all()); v.extend(cart::all()); v.extend(sweep::all()); From 64f14566074b0738c18da073df4a8b5f33fbec82 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Sun, 2 Aug 2026 07:48:01 -0400 Subject: [PATCH 2/3] fix(accuracysnes): resolve the E2.10 review findings All eight were real; the ROM image is byte-identical, so the battery and cross-validation results are unchanged. - inc_x_reg duplicated the existing inc_x. Two emitters for one opcode is the exact failure spc.rs's module doc guards against; removed and the callers moved to inc_x. - The operand doc table named $0C00 for the absolute operand and the bit address. WINDOW moved to $0E00 when the results/block overlap was fixed, and $0C00 is now the RESULTS page -- so the table pointed at the one address the rules exist to keep operands away from. It now links ram::WINDOW rather than restating a literal that can drift again. - Nothing pinned the MUL YA product below 256. The driver reads only A, the low byte; correct today only because DIV YA,X at 12 cycles scales to 60, and TICKS_PER_CYCLE is derived from two constants a change to the block shape would move. Crossing it would report an arithmetic artefact as a timing fault, which looks exactly like a real one. Now a unit test. - E2.10i's ITERATIONS doc said `REPEATS * ITERATIONS / 8` where T2OUT steps every 16 opcode cycles -- the same halving error the row's own doc comment already records having made once. - The Fwd must_use message said an unpatched branch "branches to itself". The placeholder displacement is zero, so it falls THROUGH -- the more dangerous and more silent of the two outcomes, which is the reason the message exists. - jmp_fwd's doc described a `base` parameter it does not take; that explanation belongs on patch_jmp_fwd, which does. Both it and jmp_back now carry the `# Panics` section their expect() calls imply. - The SINGLES tuple doc omitted the operands field it gained. A reader checking entries against fullsnes counts fields against that line. - The Group E slot list named v1.29.0 in a doc comment; it is version-agnostic now, having been wrong the first time the list grew. Also: docs/accuracysnes-plan.md still read 359/302. It is hand-maintained where the coverage report is generated, which is exactly the drift CLAUDE.md warns about; now 360/303. Co-Authored-By: Claude Opus 5 (1M context) --- .../tests/accuracysnes.rs | 7 +++- docs/accuracysnes-plan.md | 2 +- tests/roms/AccuracySNES/gen/src/spc.rs | 29 ++++++++------ .../roms/AccuracySNES/gen/src/spc_opcodes.rs | 6 ++- tests/roms/AccuracySNES/gen/src/tests/apu.rs | 4 +- .../AccuracySNES/gen/src/tests/apu_sweep.rs | 39 +++++++++++++++---- 6 files changed, 62 insertions(+), 25 deletions(-) diff --git a/crates/rustysnes-test-harness/tests/accuracysnes.rs b/crates/rustysnes-test-harness/tests/accuracysnes.rs index a6c616c4..fd128a2f 100644 --- a/crates/rustysnes-test-harness/tests/accuracysnes.rs +++ b/crates/rustysnes-test-harness/tests/accuracysnes.rs @@ -595,7 +595,10 @@ 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. +/// 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)"), ( @@ -653,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}"); diff --git a/docs/accuracysnes-plan.md b/docs/accuracysnes-plan.md index 022e557e..aa39f1e6 100644 --- a/docs/accuracysnes-plan.md +++ b/docs/accuracysnes-plan.md @@ -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. | diff --git a/tests/roms/AccuracySNES/gen/src/spc.rs b/tests/roms/AccuracySNES/gen/src/spc.rs index 8f03d45b..ed301896 100644 --- a/tests/roms/AccuracySNES/gen/src/spc.rs +++ b/tests/roms/AccuracySNES/gen/src/spc.rs @@ -36,7 +36,8 @@ pub struct Spc { /// two moves, the exact failure [`Spc::release_to_ipl`] documents — the emitter writes a /// placeholder and [`Spc::patch_fwd`] fills it in from the two offsets. #[derive(Debug)] -#[must_use = "a forward branch that is never patched branches to itself"] +#[must_use = "an unpatched forward branch keeps its placeholder displacement of 0 and silently \ + falls through to the next instruction"] pub struct Fwd(usize); impl Spc { @@ -249,16 +250,23 @@ impl Spc { /// `JMP !abs` with a placeholder target, for a forward jump past a branch's reach. /// /// A [`Fwd`] branch covers ±127 bytes; the opcode sweep has to jump over its whole per-opcode - /// body, which is several hundred. `base` is where the program will be uploaded, because a - /// `JMP` names an absolute address and the assembler only knows offsets — the two are the same - /// number plus that base, and every Group E program is uploaded to a fixed one. + /// body, which is several hundred. The destination is supplied later, to + /// [`Spc::patch_jmp_fwd`]. pub fn jmp_fwd(&mut self) -> Fwd { self.push(&[0x5F, 0x00, 0x00]); Fwd(self.bytes.len() - 2) } /// Point a [`Spc::jmp_fwd`] at the current offset, given the program's upload address. - /// Consumed for the same reason as [`Spc::patch_fwd`]. + /// + /// `base` is needed because a `JMP` names an absolute address and the assembler only knows + /// offsets — the two are the same number plus that base, and every Group E program is uploaded + /// to a fixed one. + /// + /// # Panics + /// + /// If the program has grown past 64 KiB, which is more than the APU has. Consumed for the same + /// reason as [`Spc::patch_fwd`]. #[allow(clippy::needless_pass_by_value)] pub fn patch_jmp_fwd(&mut self, fwd: Fwd, base: u16) -> &mut Self { let Fwd(at) = fwd; @@ -272,7 +280,11 @@ impl Spc { /// `JMP !abs` back to an offset recorded by [`Spc::here`], given the program's upload address. /// /// The long-range counterpart of [`Spc::bne_back`], for a loop whose body is bigger than a - /// branch can span. See [`Spc::jmp_fwd`] on why the base has to be supplied. + /// branch can span. See [`Spc::patch_jmp_fwd`] on why the base has to be supplied. + /// + /// # Panics + /// + /// If `target` is past 64 KiB, which is more than the APU has. pub fn jmp_back(&mut self, target: usize, base: u16) -> &mut Self { let addr = base + u16::try_from(target).expect("a program is smaller than RAM"); let [lo, hi] = addr.to_le_bytes(); @@ -296,11 +308,6 @@ impl Spc { self.push(&[0x7D]) } - /// `INC X` — `$3D`. - pub fn inc_x_reg(&mut self) -> &mut Self { - self.push(&[0x3D]) - } - /// `INC Y` — `$FC`. pub fn inc_y(&mut self) -> &mut Self { self.push(&[0xFC]) diff --git a/tests/roms/AccuracySNES/gen/src/spc_opcodes.rs b/tests/roms/AccuracySNES/gen/src/spc_opcodes.rs index 3c82c67e..06b2beb0 100644 --- a/tests/roms/AccuracySNES/gen/src/spc_opcodes.rs +++ b/tests/roms/AccuracySNES/gen/src/spc_opcodes.rs @@ -182,8 +182,10 @@ const SHIFT_FORMS: [(u8, u8, u8, Operands, &str); 4] = [ /// Everything the two regular groups do not cover, spelled out. /// -/// `(code, len, cycles, measure, name)`. Grouped in source order by fullsnes's own headings so a -/// reader can check a run of them against one block of the reference at a time. +/// `(code, len, cycles, measure, operands, name)`. Grouped in source order by fullsnes's own +/// headings so a +/// reader can check a run of them against one block of the reference at a time — which means +/// counting fields against that tuple, so it has to stay accurate. const SINGLES: &[(u8, u8, u8, Measure, Operands, &str)] = &[ // Register manipulation. (0xE8, 2, 2, Measure::Straight, Operands::Imm, "MOV A,#nn"), diff --git a/tests/roms/AccuracySNES/gen/src/tests/apu.rs b/tests/roms/AccuracySNES/gen/src/tests/apu.rs index e5e83e9a..a214d40f 100644 --- a/tests/roms/AccuracySNES/gen/src/tests/apu.rs +++ b/tests/roms/AccuracySNES/gen/src/tests/apu.rs @@ -9162,7 +9162,9 @@ fn b2_07() -> Test { fn e2_10_instrument() -> Test { /// Copies of the opcode per iteration. Bounded by `T2OUT`'s four bits — see the doc comment. const REPEATS: usize = 8; - /// Iterations. `REPEATS * ITERATIONS / 8` is the ticks-per-cycle-of-difference resolution. + /// Iterations. `REPEATS * ITERATIONS / 16` is the ticks-per-cycle-of-difference + /// resolution — 16, because `T2OUT` steps every **16** opcode cycles. The first draft of this + /// line said `/ 8`, which is the same halving error the doc comment above already records. const ITERATIONS: u8 = 32; let mut prog = Spc::new(); diff --git a/tests/roms/AccuracySNES/gen/src/tests/apu_sweep.rs b/tests/roms/AccuracySNES/gen/src/tests/apu_sweep.rs index d924a83d..532e4635 100644 --- a/tests/roms/AccuracySNES/gen/src/tests/apu_sweep.rs +++ b/tests/roms/AccuracySNES/gen/src/tests/apu_sweep.rs @@ -204,8 +204,8 @@ const EPILOGUE: [u8; 5] = [ /// |---|---|---| /// | direct page | `$30` | the window pointer's own page, clear of `$00-$0F` and of `$F0-$FF` | /// | direct page, indexed | `$00` | `X` and `Y` are both `$30`, so `$00 + X` lands on the same byte | -/// | absolute | `$0C00` | far from the image, the block and the stack; `+ $30` stays inside it | -/// | bit address | `$0C00` bit 0 | the same window; the bit index rides in the top three bits | +/// | absolute | [`ram::WINDOW`] | far from the image, the block and the stack; `+ $30` stays inside it | +/// | bit address | [`ram::WINDOW`] bit 0 | the same window; the bit index rides in the top three bits | /// | relative | `0` | the taken path lands on the next instruction — see the module docs | /// /// `BBS aa.b` is the one exception, and it earns it: with the window byte at zero every `BBS` would @@ -359,9 +359,7 @@ fn driver(prog: &mut Spc, t: &Tables, base: u16) { .mov_x_a() .mov_y_imm(0x00); let pro_loop = prog.here(); - prog.mov_a_abs_x(t.prologues) - .mov_dp_a(dp::BYTES) - .inc_x_reg(); + prog.mov_a_abs_x(t.prologues).mov_dp_a(dp::BYTES).inc_x(); prog.mov_a_x().mov_dp_a(dp::TMP); // stash the prologue-table index prog.mov_x_dp(dp::CURSOR) .mov_a_dp(dp::BYTES) @@ -385,10 +383,10 @@ fn driver(prog: &mut Spc, t: &Tables, base: u16) { prog.mov_x_dp(dp::CURSOR) .mov_a_dp(dp::BYTES) .mov_abs_x_a(ram::BLOCK) - .inc_x_reg() + .inc_x() .mov_a_dp(dp::BYTES + 1) .mov_abs_x_a(ram::BLOCK) - .inc_x_reg() + .inc_x() .mov_a_dp(dp::BYTES + 2) .mov_abs_x_a(ram::BLOCK); prog.mov_a_dp(dp::CURSOR) @@ -401,7 +399,7 @@ fn driver(prog: &mut Spc, t: &Tables, base: u16) { for (i, byte) in EPILOGUE.iter().enumerate() { prog.mov_x_dp(dp::CURSOR).mov_a_imm(*byte); for _ in 0..i { - prog.inc_x_reg(); + prog.inc_x(); } prog.mov_abs_x_a(ram::BLOCK); } @@ -629,4 +627,29 @@ mod tests { fn one_cycle_of_difference_is_six_ticks() { assert_eq!(TICKS_PER_CYCLE, 6); } + + /// The driver computes `expected = base + (cycles - 2) * TICKS_PER_CYCLE` with `MUL YA`, which + /// leaves the product's high byte in `Y` — and reads only `A`. + /// + /// That is correct today only because the slowest measured opcode is `DIV YA,X` at 12 cycles, + /// which scales to 60. Nothing else enforced it, and `TICKS_PER_CYCLE` is derived from two + /// constants that a future change to the block shape would move. Crossing 256 would make the + /// driver compute an expectation from the low byte alone and report an arithmetic artefact as a + /// timing fault — the worst kind of failure, because it looks exactly like a real one. + #[test] + fn every_expected_offset_fits_the_low_byte_of_mul_ya() { + for op in table() { + if matches!(op.measure, Measure::NotStraightLine(_)) { + continue; + } + let scaled = u16::from(op.cycles - 2) * u16::from(TICKS_PER_CYCLE); + assert!( + scaled < 256, + "{} is {} cycles, which scales to {scaled} — MUL YA would carry into Y and the \ + driver reads only A", + op.name, + op.cycles + ); + } + } } From 73b09fb4ff6d600c7d4cb51147007b16cfe9f415 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Sun, 2 Aug 2026 08:34:06 -0400 Subject: [PATCH 3/3] fix(accuracysnes): register E2.10 inside apu::all(), not appended in mod.rs Two ordering constraints apply to this row and neither is visible from mod.rs, which is where the registration first went: - it must not run BEFORE E3.06. It drives timer 2 for some forty frames, harder than anything else on the cart, and apu.rs already records that the far smaller E2.10i instrument moved E3.06's reading when it was registered earlier. - it must not run AFTER e9_02, which apu.rs marks as necessarily last: every other program leaves FLG's noise rate at zero, e9_02 steps the LFSR, and nothing can put it back. Appending after apu::all() satisfied neither by intent -- only by luck, since e2_10 happens not to read the noise LFSR. It now sits next to e2_10_instrument, inside the window both constraints define, with the reasoning at the call site where the next row appended will see it. Battery 310/310 and three references still agree after the reorder; the test order changes the image, so this was re-cross-validated rather than assumed. Co-Authored-By: Claude Opus 5 (1M context) --- tests/roms/AccuracySNES/ERROR_CODES.md | 18 +- tests/roms/AccuracySNES/SOURCE_CATALOG.tsv | 4 +- tests/roms/AccuracySNES/asm/tests_group_a.s | 314 +++++++++--------- .../AccuracySNES/build/accuracysnes-pal.sfc | Bin 262144 -> 262144 bytes .../roms/AccuracySNES/build/accuracysnes.sfc | Bin 262144 -> 262144 bytes tests/roms/AccuracySNES/gen/src/tests/apu.rs | 6 + tests/roms/AccuracySNES/gen/src/tests/mod.rs | 1 - 7 files changed, 174 insertions(+), 169 deletions(-) diff --git a/tests/roms/AccuracySNES/ERROR_CODES.md b/tests/roms/AccuracySNES/ERROR_CODES.md index 32097a68..12aa5eb2 100644 --- a/tests/roms/AccuracySNES/ERROR_CODES.md +++ b/tests/roms/AccuracySNES/ERROR_CODES.md @@ -2703,6 +2703,15 @@ Provenance: **Documented** (the apparatus for E2.10's 256-opcode sweep, validate 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. @@ -2714,15 +2723,6 @@ Provenance: **Documented** (fullsnes and anomie's DSP doc [ERRATA]: the noise ou | 3 | `$06` | one step of the noise register did not flip the output's sign, so the register's top bit is not the output's sign bit: the core is emitting the 15-bit value directly, which is the DC-heavy noise the errata's highpass remark exists to exclude | | 4 | `$08` | the noise output turned positive after one step but is not the seed shifted right once or twice, so the output is some other function of the register than its top fifteen bits | -### 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 | - ## Group F ### F1.01 — Manual pad read order diff --git a/tests/roms/AccuracySNES/SOURCE_CATALOG.tsv b/tests/roms/AccuracySNES/SOURCE_CATALOG.tsv index 6f09af26..dd586920 100644 --- a/tests/roms/AccuracySNES/SOURCE_CATALOG.tsv +++ b/tests/roms/AccuracySNES/SOURCE_CATALOG.tsv @@ -284,8 +284,8 @@ 281 B2.07 B Frame rate vs APU clock Documented fullsnes and the SNESdev Wiki: NTSC 60.0988 Hz, PAL 50.00698 Hz; the APU's 24.576 MHz crystal is region-independent (ares apuFrequency, snes9x's two APU ratios) Scored 0x7EF139 B2.07,B2.08 282 E3.13 E Regs shadow into RAM Documented fullsnes and the SNESdev Wiki: writes to $00F0-$00FF reach the underlying APU RAM as well as the register, which the S-DSP can then read as sample data Scored 0x7EF13A E3.13 283 E2.10i E SPC cycle instrument 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 Golden 0x7EF13B - -284 E9.02 E Noise output is bipolar 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 Scored 0x7EF13C E9.02 -285 E2.10 E 256-opcode cycle sweep Documented fullsnes, SNES APU SPC700 CPU instruction set Scored 0x7EF13D E2.10 +284 E2.10 E 256-opcode cycle sweep Documented fullsnes, SNES APU SPC700 CPU instruction set Scored 0x7EF13C E2.10 +285 E9.02 E Noise output is bipolar 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 Scored 0x7EF13D E9.02 286 F1.01 F Manual pad read order Documented fullsnes and the SNESdev Wiki controller protocol: the shift register presents B, Y, Select, Start, Up, Down, Left, Right, A, X, L, R and then four zero bits Scored 0x7EF13E F1.01 287 F1.02 F Pad reads 17+ are 1 Documented SNESdev Wiki, controller protocol; fullsnes Scored 0x7EF13F F1.02 288 F1.03 F Shared $4016 latch Documented fullsnes and the SNESdev Wiki controller protocol: bit 0 of $4016 is the shared latch line that parallel-loads both controller ports' shift registers Scored 0x7EF140 F1.03 diff --git a/tests/roms/AccuracySNES/asm/tests_group_a.s b/tests/roms/AccuracySNES/asm/tests_group_a.s index c8f87d2a..94b32192 100644 --- a/tests/roms/AccuracySNES/asm/tests_group_a.s +++ b/tests/roms/AccuracySNES/asm/tests_group_a.s @@ -24035,26 +24035,9 @@ CATALOG_IMPL = 1 jml test_restore .endproc -; 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) -.proc test_e9_02 - .a16 - .i16 - sep #$20 - .a8 - lda f:V_RESTARTED - beq :+ - ; SKIP: the sign flip is only predictable from the $4000 seed, which this row itself consumed - sep #$20 - .a8 - lda #VERDICT_SKIP - sta f:V_TEST_RESULT - jml test_restore - ; unreachable — restores the assembler's width belief only - .a16 - .i16 - : - rep #$30 +; E2.10 — 256-opcode cycle sweep +; provenance: Documented (fullsnes, SNES APU SPC700 CPU instruction set) +.proc test_e2_10 .a16 .i16 rep #$30 @@ -24072,7 +24055,7 @@ CATALOG_IMPL = 1 rep #$30 .a16 .i16 - lda #312 + lda #1905 sta f:V_APU_LEN lda #$0200 sta f:V_APU_DEST ; APU RAM $0200: clear of the zero page and the stack @@ -24086,8 +24069,12 @@ CATALOG_IMPL = 1 .a8 lda #$00 sta APUIO0 - ; Wait for the program's done marker, but not forever: an APU that never boots would - ; otherwise hang the whole battery and report nothing about any other test. + ; Wait for the done marker, but not forever — see this proc's doc comment for why the + ; shared wait is far too short for this one program. + sep #$20 + .a8 + lda #$00 + sta f:$7E01F8 ; the outer pass counter, in the same scratch page E3.06 uses rep #$30 .a16 .i16 @@ -24102,7 +24089,17 @@ CATALOG_IMPL = 1 .a16 .i16 inx - cpx #$8000 + bne @wait + ; X wrapped: one full pass of 65536. Count it, and give up after sixteen. + sep #$20 + .a8 + lda f:$7E01F8 + inc a + sta f:$7E01F8 + cmp #$10 + rep #$30 + .a16 + .i16 bne @wait jmp @timeout @ran: @@ -24124,52 +24121,40 @@ CATALOG_IMPL = 1 .i16 lda f:$7E0100 and #$00FF - ; record slot 258: E9.02 noise OUTX with the LFSR frozen at its seed - sta f:$7EE404 + ; record slot 280: E2.10 opcodes disagreeing with fullsnes (expect 0) + sta f:$7EE430 lda f:$7E0101 and #$00FF - ; record slot 259: E9.02 noise OUTX one LFSR step later - sta f:$7EE406 - ; Two guards before the row's own assertions. The first fixes the starting point: this is - ; E9.01's reading, and without it there is no known state to have stepped away from. - sep #$20 - .a8 - lda f:$7E0100 - and #$80 - cmp #$80 - beq :+ + ; record slot 281: E2.10 opcodes measured (expect 231 = 256 - 25) + sta f:$7EE432 + lda f:$7E0102 + and #$00FF + ; record slot 282: E2.10 first disagreeing opcode ($FF = none) + sta f:$7EE434 + ; Liveness first. A driver that fell over after one opcode would report no disagreements, + ; and a `0` in slot 280 would then read as a pass — so the count of opcodes actually + ; measured is asserted before anything is concluded from the count of failures. + lda f:$7E0101 + and #$00FF + cmp #$00E7 + bcs :+ jmp @fail1 : - ; The second: the register has to have actually moved. A 48-sample window cannot contain - ; zero multiples of 32, so this failing means the rate table or the counter is wrong, not - ; that the noise output is shaped wrongly. - lda f:$7E0100 - cmp f:$7E0101 - bne :+ - jmp @fail2 - : - ; The row. One step right must move bit 14 out of the sign position, so a full-scale - ; negative reading becomes positive. - lda f:$7E0101 - and #$80 - cmp #$00 - beq :+ - jmp @fail3 + cmp #$00E8 + bcc :+ + jmp @fail1 : - ; And at about half scale, or a quarter if the window caught two steps -- both are what a - ; right shift produces, and neither is what a core that merely re-centred the value gives. - rep #$30 - .a16 - .i16 - lda f:$7E0101 + ; The row itself: every measured opcode's timing agrees with the cycle count fullsnes + ; documents for it, to within half a cycle. + lda f:$7E0100 and #$00FF - cmp #$0010 + cmp #$0000 bcs :+ - jmp @fail4 + jmp @fail2 : - cmp #$0041 + cmp #$0001 bcc :+ - jmp @fail4 + jmp @fail2 : bra @pass @timeout: @@ -24185,38 +24170,41 @@ CATALOG_IMPL = 1 sta f:$7EE010 jml test_restore @fail1: - ; the frozen noise reading is not negative, so the LFSR was not at the $4000 seed and the step below has no known starting point -- E9.01 covers that reading itself + ; 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 sep #$20 .a8 lda #$02 sta f:$7EE010 jml test_restore @fail2: - ; enabling the noise clock for 48 output samples did not change the output at all, so the LFSR never stepped and the sign test below would be comparing the seed with itself + ; 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 sep #$20 .a8 lda #$04 sta f:$7EE010 jml test_restore -@fail3: - ; one step of the noise register did not flip the output's sign, so the register's top bit is not the output's sign bit: the core is emitting the 15-bit value directly, which is the DC-heavy noise the errata's highpass remark exists to exclude +.endproc + +; 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) +.proc test_e9_02 + .a16 + .i16 sep #$20 .a8 - lda #$06 - sta f:$7EE010 - jml test_restore -@fail4: - ; the noise output turned positive after one step but is not the seed shifted right once or twice, so the output is some other function of the register than its top fifteen bits + lda f:V_RESTARTED + beq :+ + ; SKIP: the sign flip is only predictable from the $4000 seed, which this row itself consumed sep #$20 .a8 - lda #$08 - sta f:$7EE010 + lda #VERDICT_SKIP + sta f:V_TEST_RESULT jml test_restore -.endproc - -; E2.10 — 256-opcode cycle sweep -; provenance: Documented (fullsnes, SNES APU SPC700 CPU instruction set) -.proc test_e2_10 + ; unreachable — restores the assembler's width belief only + .a16 + .i16 + : + rep #$30 .a16 .i16 rep #$30 @@ -24234,7 +24222,7 @@ CATALOG_IMPL = 1 rep #$30 .a16 .i16 - lda #1905 + lda #312 sta f:V_APU_LEN lda #$0200 sta f:V_APU_DEST ; APU RAM $0200: clear of the zero page and the stack @@ -24248,12 +24236,8 @@ CATALOG_IMPL = 1 .a8 lda #$00 sta APUIO0 - ; Wait for the done marker, but not forever — see this proc's doc comment for why the - ; shared wait is far too short for this one program. - sep #$20 - .a8 - lda #$00 - sta f:$7E01F8 ; the outer pass counter, in the same scratch page E3.06 uses + ; Wait for the program's done marker, but not forever: an APU that never boots would + ; otherwise hang the whole battery and report nothing about any other test. rep #$30 .a16 .i16 @@ -24268,17 +24252,7 @@ CATALOG_IMPL = 1 .a16 .i16 inx - bne @wait - ; X wrapped: one full pass of 65536. Count it, and give up after sixteen. - sep #$20 - .a8 - lda f:$7E01F8 - inc a - sta f:$7E01F8 - cmp #$10 - rep #$30 - .a16 - .i16 + cpx #$8000 bne @wait jmp @timeout @ran: @@ -24300,40 +24274,52 @@ CATALOG_IMPL = 1 .i16 lda f:$7E0100 and #$00FF - ; record slot 280: E2.10 opcodes disagreeing with fullsnes (expect 0) - sta f:$7EE430 - lda f:$7E0101 - and #$00FF - ; record slot 281: E2.10 opcodes measured (expect 231 = 256 - 25) - sta f:$7EE432 - lda f:$7E0102 - and #$00FF - ; record slot 282: E2.10 first disagreeing opcode ($FF = none) - sta f:$7EE434 - ; Liveness first. A driver that fell over after one opcode would report no disagreements, - ; and a `0` in slot 280 would then read as a pass — so the count of opcodes actually - ; measured is asserted before anything is concluded from the count of failures. + ; record slot 258: E9.02 noise OUTX with the LFSR frozen at its seed + sta f:$7EE404 lda f:$7E0101 and #$00FF - cmp #$00E7 - bcs :+ - jmp @fail1 - : - cmp #$00E8 - bcc :+ + ; record slot 259: E9.02 noise OUTX one LFSR step later + sta f:$7EE406 + ; Two guards before the row's own assertions. The first fixes the starting point: this is + ; E9.01's reading, and without it there is no known state to have stepped away from. + sep #$20 + .a8 + lda f:$7E0100 + and #$80 + cmp #$80 + beq :+ jmp @fail1 : - ; The row itself: every measured opcode's timing agrees with the cycle count fullsnes - ; documents for it, to within half a cycle. + ; The second: the register has to have actually moved. A 48-sample window cannot contain + ; zero multiples of 32, so this failing means the rate table or the counter is wrong, not + ; that the noise output is shaped wrongly. lda f:$7E0100 + cmp f:$7E0101 + bne :+ + jmp @fail2 + : + ; The row. One step right must move bit 14 out of the sign position, so a full-scale + ; negative reading becomes positive. + lda f:$7E0101 + and #$80 + cmp #$00 + beq :+ + jmp @fail3 + : + ; And at about half scale, or a quarter if the window caught two steps -- both are what a + ; right shift produces, and neither is what a core that merely re-centred the value gives. + rep #$30 + .a16 + .i16 + lda f:$7E0101 and #$00FF - cmp #$0000 + cmp #$0010 bcs :+ - jmp @fail2 + jmp @fail4 : - cmp #$0001 + cmp #$0041 bcc :+ - jmp @fail2 + jmp @fail4 : bra @pass @timeout: @@ -24349,19 +24335,33 @@ CATALOG_IMPL = 1 sta f:$7EE010 jml test_restore @fail1: - ; 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 + ; the frozen noise reading is not negative, so the LFSR was not at the $4000 seed and the step below has no known starting point -- E9.01 covers that reading itself sep #$20 .a8 lda #$02 sta f:$7EE010 jml test_restore @fail2: - ; 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 + ; enabling the noise clock for 48 output samples did not change the output at all, so the LFSR never stepped and the sign test below would be comparing the seed with itself sep #$20 .a8 lda #$04 sta f:$7EE010 jml test_restore +@fail3: + ; one step of the noise register did not flip the output's sign, so the register's top bit is not the output's sign bit: the core is emitting the 15-bit value directly, which is the DC-heavy noise the errata's highpass remark exists to exclude + sep #$20 + .a8 + lda #$06 + sta f:$7EE010 + jml test_restore +@fail4: + ; the noise output turned positive after one step but is not the seed shifted right once or twice, so the output is some other function of the register than its top fifteen bits + sep #$20 + .a8 + lda #$08 + sta f:$7EE010 + jml test_restore .endproc .segment "TESTSC" @@ -35271,33 +35271,6 @@ apu_prog_119: .byte $F4, $E4, $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F .byte $C0, $FF apu_prog_120: - .byte $5F, $0C, $02, $83, $79, $79, $79, $79, $79, $79, $79, $79 - .byte $CD, $EF, $BD, $E8, $03, $C5, $00, $01, $E8, $02, $C5, $01 - .byte $01, $E8, $03, $C5, $02, $01, $E8, $02, $C5, $03, $01, $E8 - .byte $00, $C5, $04, $01, $E8, $00, $C5, $05, $01, $E8, $00, $C5 - .byte $06, $01, $E8, $00, $C5, $07, $01, $E8, $6C, $C4, $F2, $E8 - .byte $E0, $C4, $F3, $E8, $6C, $C4, $F2, $E8, $20, $C4, $F3, $E8 - .byte $5C, $C4, $F2, $E8, $00, $C4, $F3, $E8, $3D, $C4, $F2, $E8 - .byte $01, $C4, $F3, $E8, $4D, $C4, $F2, $E8, $00, $C4, $F3, $E8 - .byte $2D, $C4, $F2, $E8, $00, $C4, $F3, $E8, $5D, $C4, $F2, $E8 - .byte $01, $C4, $F3, $E8, $0C, $C4, $F2, $E8, $7F, $C4, $F3, $E8 - .byte $1C, $C4, $F2, $E8, $7F, $C4, $F3, $E8, $00, $C4, $F2, $E8 - .byte $7F, $C4, $F3, $E8, $01, $C4, $F2, $E8, $7F, $C4, $F3, $E8 - .byte $02, $C4, $F2, $E8, $00, $C4, $F3, $E8, $03, $C4, $F2, $E8 - .byte $10, $C4, $F3, $E8, $04, $C4, $F2, $E8, $00, $C4, $F3, $E8 - .byte $06, $C4, $F2, $E8, $00, $C4, $F3, $E8, $07, $C4, $F2, $E8 - .byte $7F, $C4, $F3, $E8, $05, $C4, $F2, $E8, $00, $C4, $F3, $E8 - .byte $7C, $C4, $F2, $E8, $00, $C4, $F3, $E8, $4C, $C4, $F2, $E8 - .byte $01, $C4, $F3, $8D, $00, $FE, $FE, $E8, $4C, $C4, $F2, $E8 - .byte $00, $C4, $F3, $8D, $00, $FE, $FE, $8D, $00, $FE, $FE, $8D - .byte $00, $FE, $FE, $8D, $00, $FE, $FE, $8D, $00, $FE, $FE, $8D - .byte $00, $FE, $FE, $E8, $09, $C4, $F2, $E4, $F3, $C4, $F5, $E8 - .byte $6C, $C4, $F2, $E8, $33, $C4, $F3, $8D, $00, $FE, $FE, $E8 - .byte $6C, $C4, $F2, $E8, $20, $C4, $F3, $E8, $09, $C4, $F2, $E4 - .byte $F3, $C4, $F6, $E8, $6C, $C4, $F2, $E8, $E0, $C4, $F3, $E8 - .byte $6C, $C4, $F2, $E8, $20, $C4, $F3, $E8, $5A, $C4, $F4, $E4 - .byte $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF -apu_prog_121: .byte $5F, $1C, $08, $00, $00, $00, $00, $00, $00, $00, $00, $00 .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 @@ -35457,6 +35430,33 @@ apu_prog_121: .byte $C5, $8F, $80, $F1, $E4, $08, $C4, $F5, $E4, $07, $C4, $F6 .byte $E4, $09, $C4, $F7, $E8, $5A, $C4, $F4, $E4, $F4, $68, $A5 .byte $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF +apu_prog_121: + .byte $5F, $0C, $02, $83, $79, $79, $79, $79, $79, $79, $79, $79 + .byte $CD, $EF, $BD, $E8, $03, $C5, $00, $01, $E8, $02, $C5, $01 + .byte $01, $E8, $03, $C5, $02, $01, $E8, $02, $C5, $03, $01, $E8 + .byte $00, $C5, $04, $01, $E8, $00, $C5, $05, $01, $E8, $00, $C5 + .byte $06, $01, $E8, $00, $C5, $07, $01, $E8, $6C, $C4, $F2, $E8 + .byte $E0, $C4, $F3, $E8, $6C, $C4, $F2, $E8, $20, $C4, $F3, $E8 + .byte $5C, $C4, $F2, $E8, $00, $C4, $F3, $E8, $3D, $C4, $F2, $E8 + .byte $01, $C4, $F3, $E8, $4D, $C4, $F2, $E8, $00, $C4, $F3, $E8 + .byte $2D, $C4, $F2, $E8, $00, $C4, $F3, $E8, $5D, $C4, $F2, $E8 + .byte $01, $C4, $F3, $E8, $0C, $C4, $F2, $E8, $7F, $C4, $F3, $E8 + .byte $1C, $C4, $F2, $E8, $7F, $C4, $F3, $E8, $00, $C4, $F2, $E8 + .byte $7F, $C4, $F3, $E8, $01, $C4, $F2, $E8, $7F, $C4, $F3, $E8 + .byte $02, $C4, $F2, $E8, $00, $C4, $F3, $E8, $03, $C4, $F2, $E8 + .byte $10, $C4, $F3, $E8, $04, $C4, $F2, $E8, $00, $C4, $F3, $E8 + .byte $06, $C4, $F2, $E8, $00, $C4, $F3, $E8, $07, $C4, $F2, $E8 + .byte $7F, $C4, $F3, $E8, $05, $C4, $F2, $E8, $00, $C4, $F3, $E8 + .byte $7C, $C4, $F2, $E8, $00, $C4, $F3, $E8, $4C, $C4, $F2, $E8 + .byte $01, $C4, $F3, $8D, $00, $FE, $FE, $E8, $4C, $C4, $F2, $E8 + .byte $00, $C4, $F3, $8D, $00, $FE, $FE, $8D, $00, $FE, $FE, $8D + .byte $00, $FE, $FE, $8D, $00, $FE, $FE, $8D, $00, $FE, $FE, $8D + .byte $00, $FE, $FE, $E8, $09, $C4, $F2, $E4, $F3, $C4, $F5, $E8 + .byte $6C, $C4, $F2, $E8, $33, $C4, $F3, $8D, $00, $FE, $FE, $E8 + .byte $6C, $C4, $F2, $E8, $20, $C4, $F3, $E8, $09, $C4, $F2, $E4 + .byte $F3, $C4, $F6, $E8, $6C, $C4, $F2, $E8, $E0, $C4, $F3, $E8 + .byte $6C, $C4, $F2, $E8, $20, $C4, $F3, $E8, $5A, $C4, $F4, $E4 + .byte $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF .segment "CATALOG" .export _test_count .export _test_entries @@ -35752,8 +35752,8 @@ _test_entries: .faraddr test_b2_07 .faraddr test_e3_13 .faraddr test_e2_10i - .faraddr test_e9_02 .faraddr test_e2_10 + .faraddr test_e9_02 .faraddr test_f1_01 .faraddr test_f1_02 .faraddr test_f1_03 @@ -36100,8 +36100,8 @@ _test_flags: .byte $01 ; B2.07 .byte $01 ; E3.13 .byte $02 ; E2.10i - .byte $01 ; E9.02 .byte $01 ; E2.10 + .byte $01 ; E9.02 .byte $01 ; F1.01 .byte $01 ; F1.02 .byte $01 ; F1.03 @@ -36448,8 +36448,8 @@ _test_names: .addr @n_b2_07 .addr @n_e3_13 .addr @n_e2_10i - .addr @n_e9_02 .addr @n_e2_10 + .addr @n_e9_02 .addr @n_f1_01 .addr @n_f1_02 .addr @n_f1_03 @@ -37361,12 +37361,12 @@ _test_names: @n_e2_10i: .byte 20 .byte "SPC cycle instrument" -@n_e9_02: - .byte 23 - .byte "Noise output is bipolar" @n_e2_10: .byte 22 .byte "256-opcode cycle sweep" +@n_e9_02: + .byte 23 + .byte "Noise output is bipolar" @n_f1_01: .byte 21 .byte "Manual pad read order" @@ -38135,7 +38135,7 @@ _page_tests: .word 226 .word 227 .word 283 - .word 285 + .word 284 .word 195 .word 196 .word 274 @@ -38157,7 +38157,7 @@ _page_tests: .word 246 .word 247 .word 248 - .word 284 + .word 285 .word 206 .word 207 .word 208 diff --git a/tests/roms/AccuracySNES/build/accuracysnes-pal.sfc b/tests/roms/AccuracySNES/build/accuracysnes-pal.sfc index 3ace4f66afeb1d5cab4bbc8376d0169ff6a7583f..bd9621a30851b62ae52f1913eab25948348831e2 100644 GIT binary patch delta 281 zcmZo@5NK!+*l?$wc~x}i=KJ-n*QWRTFtRe~Kbqbj&KS*Petr5z0YW%nzYObGjWS9V=r#nV4dQHF2 z$jHi+{&;$J7-KY3^bLri@4^_>6pL@1d;wBfd;?^-CeQ!|kOB2KPBMao>u*d?WMVYh zeyoS7RDD|mV`MX9$Mz^@CN~zw#_bE)nNEmLZ;)q_5EnBtHPg*6NX}15RYBG@*giv^=`|Ol?DQ5bCT&KU_6=H0+c#)2H<(XPFk!Zy?$6J>WBQaVW|!^yNz8@p L+izqsYfJ?Ipig8t diff --git a/tests/roms/AccuracySNES/build/accuracysnes.sfc b/tests/roms/AccuracySNES/build/accuracysnes.sfc index 6218bfb6c6751650c04cd9d708e8329f4b25297a..a82a2c5a0fb24c3d877ec52d0a367c494dff733c 100644 GIT binary patch delta 281 zcmZo@5NK!+*l?$wd3AKi=KJ-n*QWRTFtRe~Kbqbj&KS*Petr5z0YW%nzYObGjWS9V=r#nV4dQHF2 z$jHi+{&;$J7-KY3^bLri@4^_>6pL@1d;wBfd;?^-CeQ!|kOB2KPBMao>u*d?WMVYh zeyoS7RDD|mV`MX9$Mz^@CN~zw#_bE)nNEmLZ;)q_5EnBtHPg*6NX}15RYBG@*giv^=`|Ol?DQ5bCT&KU_6=H0+c#)2H<(XPFk!Zy?$6J>WBQaVW|!^yNz8@p L+izqsYfJ?IpiyKv diff --git a/tests/roms/AccuracySNES/gen/src/tests/apu.rs b/tests/roms/AccuracySNES/gen/src/tests/apu.rs index a214d40f..2c7e575c 100644 --- a/tests/roms/AccuracySNES/gen/src/tests/apu.rs +++ b/tests/roms/AccuracySNES/gen/src/tests/apu.rs @@ -156,6 +156,12 @@ pub fn all() -> Vec { // +/-6 band. Measured, not guessed -- E3.06 failed on code 1 with this row placed after // `e3_09`. Nothing here depends on running late; it simply must not run before E3.06. e2_10_instrument(), + // Registered HERE rather than appended in `mod.rs`, which is where it first went. Both of + // the ordering constraints below apply to it and neither is visible from that file: it must + // not run before `E3.06` (it drives timer 2 for some forty frames, harder than anything + // else on the cart), and it must not run after `e9_02`. Appending it there satisfied + // neither by intent, only by luck. + super::apu_sweep::e2_10(), // LAST, and it has to be. Every other program leaves FLG's noise rate at zero, so the // noise LFSR never advances and `E9.01` reads the power-on seed. `E9.02` steps it and // nothing can put it back. Anything appended below this line runs after that has happened. diff --git a/tests/roms/AccuracySNES/gen/src/tests/mod.rs b/tests/roms/AccuracySNES/gen/src/tests/mod.rs index c99ccfef..06a00778 100644 --- a/tests/roms/AccuracySNES/gen/src/tests/mod.rs +++ b/tests/roms/AccuracySNES/gen/src/tests/mod.rs @@ -28,7 +28,6 @@ pub fn all() -> Vec { v.extend(bus::all()); v.extend(dma::all()); v.extend(apu::all()); - v.push(apu_sweep::e2_10()); v.extend(input::all()); v.extend(cart::all()); v.extend(sweep::all());