diff --git a/CHANGELOG.md b/CHANGELOG.md index ba3fa9a3..6f5e8bd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **AccuracySNES: three Group E rows land — `E8.01`, `E9.02`, `E5.06`.** Coverage **347 → 350 of + 443** (297 on-cart + 53 scenes), battery **338 tests at 100%**, and both references agree: + `snes9x: OK (14 known divergence(s))`, `Mesen2: OK (2 known divergence(s))`, 53/53 scenes on each. + `MESEN2_KNOWN_FAILURES` came **down** from 3 to 2 — the one Mesen2 divergence that was ours is + gone. + + **`E8.01` (`KON` examined at 16 kHz) — the two earlier drafts were measuring their own timing.** + Both timed a *delay*: write `KON`, count until `ENVX` leaves zero. That delay is `(P − φ) mod P` — + a sawtooth in the write's phase against the DSP's examination grid — and two readings of a + sawtooth cannot recover its period. Worse, `φ` is set by where the IPL handshake left the SPC700, + which is paced by the S-CPU and therefore **differs between the NTSC and PAL images of the same + build**. The poll-count draft duly reported a difference of 2 on NTSC and 0 on PAL. Neither draft + was ever scored. + + What replaced it measures **writes the DSP never sees**: `KON` is a register, not a queue, so a + second write before the DSP next looks *cancels* the first and that voice is never keyed. Eight + writes 24 SPC cycles apart span 168 cycles, and a half-open interval of that length contains + `⌊168/P⌋` or `⌊168/P⌋ + 1` grid points **for every phase** — 3 or 4 voices started at 16 kHz, + 6 or 7 at 32 kHz, two ranges that cannot meet. RustySNES measures 3; Mesen2, which failed the + phase-fragile draft, passes this one. + + **`E9.02` (noise output is bipolar).** `E9.01` already pins the frozen LFSR seed; this row scores + the *transition*, because one step must turn a full-scale negative reading (`$81`) into a positive + one at about half scale (`$3F`, or `$1F` if the window caught two steps). A core emitting the + register's 15-bit value directly reads positive throughout — the DC-heavy noise the errata's + "highpass" remark exists to exclude. + + **`E5.06` (BRR wraps at 15 bits) — the first assertion was vacuous, and the injection proved it.** + "At least one of 64 `OUTX` readings is negative" looked airtight for a purely positive drive. + Injecting the named bug — clamping the store instead of letting it wrap — made the row **pass + harder**: 64 negatives out of 64, against the correct decoder's 32. A clamping decoder pins the + buffer at `+7FFEh`, and the gaussian interpolator's three-term partial sum (the truncation + `E5.13` is about) overflows a signed 16-bit intermediate from that constant, so every reading comes + back negative. *The interpolator, not the decoder, was supplying the sign.* The row now asserts + **bipolarity** — a saturating decoder settles, and a settled decoder's readings carry one sign — + and both saturating variants fail it. + - **AccuracySNES: the Mesen2 oracle is FIXED — one `emu.setInput` call too many.** The battery has never run under MesenCE; it now completes: `magic='ACSN'`, `R_DONE=$A5`, **335/335** status bytes written, and `crossval.sh` reports `Mesen2: 1 failing test(s)` where it previously timed out. @@ -610,6 +647,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **AccuracySNES: `E9.01` and `E9.02` stand down as SKIP on a menu restart.** `E9.02` steps the noise + LFSR by design and nothing can put it back: `FLG` bit 7 is the DSP's *soft* reset and does not + re-seed the shift register (checked against ares, which re-seeds only in `DSP::power`). A comment + in `voice_program` claimed the opposite and has been corrected. `E9.02` is registered last, both + rows use the same power-on gate `F1.07` uses for its unwritten `$4218`, and the harness's + restart-idempotence check moved from `first_run_passed - 1` to `- 3`. + - **Run-ahead stays opt-in, and the reason is now measured rather than assumed.** `docs/frontend.md` recorded the per-frame save-state allocation as *the* blocker on making run-ahead default-on. `v1.25.0` removed that allocation, so the question was re-measured. diff --git a/crates/rustysnes-test-harness/tests/accuracysnes.rs b/crates/rustysnes-test-harness/tests/accuracysnes.rs index c97bd440..2a950a3f 100644 --- a/crates/rustysnes-test-harness/tests/accuracysnes.rs +++ b/crates/rustysnes-test-harness/tests/accuracysnes.rs @@ -595,6 +595,51 @@ const A5_08_SLOTS: [(u8, &str); 7] = [ (6, "8x (PHD+PLD) - 16 NOP (expect 76)"), ]; +/// The measurement slots the `v1.29.0` Group E batch records. +const GROUP_E_BATCH_SLOTS: [(u16, &str); 7] = [ + (256, "E8.01 KON sweep key-on mask (bit 7 = voice 0)"), + ( + 257, + "E8.01 voices the sweep started (expect 3-4; 6-7 means a 32 kHz poll)", + ), + (258, "E9.02 noise OUTX at the seed (expect $81)"), + (259, "E9.02 noise OUTX one step on (expect $3F or $1F)"), + ( + 260, + "E5.06 negative OUTX readings (of 64; 0 means the decoder clamps)", + ), + (261, "E5.06 non-zero OUTX readings (of 64)"), + (262, "E5.06 the last of the 64 readings"), +]; + +/// Report what the Group E batch measured, for cross-emulator comparison. +/// +/// These three rows all score a *shape* — how many voices a burst of `KON` writes started, which +/// way a noise reading's sign went, how often a purely positive drive read back negative — and the +/// verdict byte carries none of the underlying numbers. A disagreement between references is far +/// easier to read here than through a failure code. +#[test] +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"); + 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}"); + } + println!("{out}"); + + // One crude check, in the same spirit as the timing channel's: an unwritten slot reads zero, + // and every one of these has a value the cart always writes. + let voices = report.meas[257]; + assert!( + (1..=8).contains(&voices), + "E8.01 recorded {voices} started voices, which is not a count of eight voices — the slot \ + was never written, or the mask reduction overflowed" + ); +} + /// Report the raw timing measurements, and sanity-check them against physics. /// /// This exists because a one-byte verdict cannot carry a dot count. Reporting a 32-`NOP` baseline @@ -1889,14 +1934,19 @@ fn the_dpad_navigates_the_pages() { DONE_MARK, "the restarted battery never finished" ); - // One fewer than a cold boot: F1.07 stands down as SKIP on a re-run because its phase A needs - // the power-on value of $4218, which a soft restart cannot reproduce (the previous run armed - // auto-read). Its verdict is $FF (skip), not a fail code. Relative to the first run's pass count - // rather than a literal, so adding tests does not break this. + // Three fewer than a cold boot, and every one of them a genuine power-on dependency that + // stands down as SKIP ($FF) rather than reporting a fail code: + // + // * F1.07 -- phase A needs the power-on value of $4218, and the previous run armed auto-read; + // * E9.01 and E9.02 -- both read the noise LFSR's power-on seed, and E9.02 steps the register + // away from it by design. FLG bit 7 does not re-seed it, so nothing can put it back. + // + // Relative to the first run's pass count rather than a literal, so adding tests does not break + // this. A change here means a NEW power-on dependency appeared, which is worth knowing about. assert_eq!( rd16(&sys, R_PASSED), - first_run_passed - 1, - "the restarted battery did not reproduce its result (minus the power-on-only F1.07)" + first_run_passed - 3, + "the restarted battery did not reproduce its result (minus the three power-on-only rows)" ); let f107_idx = catalog() .iter() diff --git a/docs/STATUS.md b/docs/STATUS.md index 5735771a..ab8d9a28 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -23,8 +23,8 @@ compositor (`docs/adr/0014`, T-CA-10) becomes the sole renderer — its compose/ per-dot against live registers (mid-line CGRAM/OAM/`INIDISP` at dot resolution; BG-fetch-ahead is Phase 4c, not yet landed) — and the first-party AccuracySNES cartridge matures into a usable instrument (AccuracyCoin-style paged menu + automatic skyline -results + per-test B-skip + a Select WRAM debug viewer) with its battery at **344 of 443** dossier -assertions (291 on-cart + 53 rendered scenes). +results + per-test B-skip + a Select WRAM debug viewer) with its battery at **350 of 443** dossier +assertions (297 on-cart + 53 rendered scenes). **`v1.22.0 "Horizon"` fixes the long-standing DSP-1 continuous-mode Mode-7 flat floor** (Pilotwings flight, SMK track): the shared NEC-DSP host-sync (`run_until_rqm`) stopped one host-write too early, so reading a host input word raised RQM before the firmware cleared DRC to 16-bit — mis-framing the @@ -234,7 +234,7 @@ tracked here, always current, reaffirmed every release: | Core/Curated coprocessors (oracle-gated) | ✅ **3 / 3, honesty gate green** | DSP-1 (4 commercial ROMs), Super FX/GSU (58 Krom ROMs + per-opcode suite), SA-1 (18 commercial carts) — `ORACLE_COPROCESSORS` | | BestEffort coprocessors, real-title validated | ✅ **10 / 11** | DSP-2, DSP-4, ST010, S-DD1, CX4, OBC1 boot a real commercial title to real gameplay content. **DSP-3 (SD Gundam GX) + ST011 (2-dan Morita Shougi)** are liveness/determinism-validated — detection + `host_accesses > 0` + bit-identical framebuffer (`dsp3_st011_oncart`). **ST018 (Nidan Morita Shogi 2) + S-RTC (Daikaijuu Monogatari II)** are detection + boot + determinism-validated against their real carts (`srtc_st018_oncart`), but their coprocessor's core function is **usage-gated** (the ST018 shogi AI runs only on the computer's move; the S-RTC clock is read at specific moments), so it is not exercised in a headless boot | | BestEffort coprocessors, no booting dump | ⚠️ **1 / 11** | SPC7110 — **two** local Tengai Makyou Zero dumps both fail to boot to content: the 7 MiB one is a fan-translation ROM hack needing a patch-only memory region no cartridge has (`docs/audit/spc7110-boot-crash-2026-07-08.md`); a 5 MiB dump (sha256 `8620203d…`) freezes at a near-blank screen with zero coprocessor activity, and does not match the documented-good original (`69d06a3f…`). The correct original-cartridge dump remains the ROM-sourcing gap (`docs/rom-test-corpus.md`) | -| AccuracySNES (first-party battery) | ✅ **344 / 443 assertions covered, 100% on-cart pass** | A 332-test self-scoring battery spanning Groups A-G. **344 of 443** dossier assertions covered: **291 on-cart** + **53 via rendered scenes** (`docs/adr/0013`), kept as separate columns on purpose (`docs/accuracysnes-coverage.md`, regenerated with the ROM so it cannot drift). Every scored row is inject-verified for non-vacuity and cross-validated headlessly against **Mesen2, snes9x, and MesenCE** (the last built this cycle as the per-dot compositor's blueprint and exact-frame oracle). Provenance gate green. Ships an AccuracyCoin-style on-cart UI as of `v1.21.0` (paged menu + automatic skyline results + per-test B-skip + a Select WRAM debug viewer). Remaining ~99 assertions are compositor-gated hi-res, second-image G-rows, treacherous CPU/dot-model timing, or provably uncoverable (`docs/accuracysnes-coverability-audit-2026-07-23.md`) | +| AccuracySNES (first-party battery) | ✅ **350 / 443 assertions covered, 100% on-cart pass** | A 338-test self-scoring battery spanning Groups A-G. **350 of 443** dossier assertions covered: **297 on-cart** + **53 via rendered scenes** (`docs/adr/0013`), kept as separate columns on purpose (`docs/accuracysnes-coverage.md`, regenerated with the ROM so it cannot drift). Every scored row is inject-verified for non-vacuity and cross-validated headlessly against **Mesen2, snes9x, and MesenCE** (the last built this cycle as the per-dot compositor's blueprint and exact-frame oracle). Provenance gate green. Ships an AccuracyCoin-style on-cart UI as of `v1.21.0` (paged menu + automatic skyline results + per-test B-skip + a Select WRAM debug viewer). Remaining ~93 assertions are compositor-gated hi-res, second-image G-rows, treacherous CPU/dot-model timing, or provably uncoverable — though "uncoverable" has twice proved too strong (`E5.06` and `B2.02`/`B2.03` all landed after being parked) (`docs/accuracysnes-coverability-audit-2026-07-23.md`) | | Determinism contract | ✅ **proven** | bit-identical framebuffer/audio across runs; save-state round-trip proven across all three board tiers (no-coprocessor, Curated, BestEffort) | **Named residuals, tracked not hidden:** the 65816 `e1.e` divergence (`docs/adr/0002`); diff --git a/docs/accuracysnes-coverage.md b/docs/accuracysnes-coverage.md index 91ed4b63..b457d042 100644 --- a/docs/accuracysnes-coverage.md +++ b/docs/accuracysnes-coverage.md @@ -43,16 +43,16 @@ Every sub-group of Part V is enumerated, so this is a **complete** statement of | `E2` | 10 | 9 | 0 | E2.10 | | `E3` | 14 | 10 | 0 | E3.07, E3.09, E3.12, E3.13 | | `E4` | 11 | 7 | 0 | E4.05, E4.07, E4.09, E4.10 | -| `E5` | 13 | 12 | 0 | E5.06 | +| `E5` | 13 | 13 | 0 | — | | `E6` | 11 | 3 | 0 | E6.01, E6.03, E6.04, E6.05, E6.06, E6.07, E6.08, E6.10 | | `E7` | 18 | 17 | 0 | E7.02 | -| `E8` | 11 | 6 | 0 | E8.01, E8.06, E8.08, E8.09, E8.11 | -| `E9` | 20 | 12 | 0 | E9.02, E9.07, E9.08, E9.09, E9.11, E9.14, E9.16, E9.20 | +| `E8` | 11 | 7 | 0 | E8.06, E8.08, E8.09, E8.11 | +| `E9` | 20 | 13 | 0 | E9.07, E9.08, E9.09, E9.11, E9.14, E9.16, E9.20 | | `E10` | 6 | 2 | 0 | E10.02, E10.03, E10.04, E10.06 | | `F1` | 22 | 13 | 0 | F1.13, F1.15, F1.16, F1.17, F1.18, F1.19, F1.20, F1.21, F1.22 | | `G1` | 18 | 15 | 0 | G1.06, G1.13, G1.18 | -**294 of 443** enumerated assertion rows covered by an on-cart test, plus **53** covered only by a rendered scene (`docs/adr/0013`) — **347 of 443** in total. +**297 of 443** enumerated assertion rows covered by an on-cart test, plus **53** covered only by a rendered scene (`docs/adr/0013`) — **350 of 443** in total. The two columns are kept apart on purpose. An on-cart result means the same thing on any emulator and on real hardware; a rendered scene needs a host holding the golden. Adding them into one figure would quietly change what the number claims. diff --git a/docs/accuracysnes-plan.md b/docs/accuracysnes-plan.md index 962fb5cb..3d95c9f9 100644 --- a/docs/accuracysnes-plan.md +++ b/docs/accuracysnes-plan.md @@ -13,11 +13,11 @@ AccuracySNES closed ticket **T-04**. The follow-on tickets minted here are **T-0 | | | |---|---| -| Tests | **332** (scoring + golden vectors + region SKIP per image) — *tests, not assertions; see the note below the table* | -| Assertion coverage | **344 of 443** dossier assertions — **291 on-cart** + **53 rendered scenes**, kept as separate columns (`docs/accuracysnes-coverage.md`) | +| Tests | **338** (scoring + golden vectors + region SKIP per image) — *tests, not assertions; see the note below the table* | +| Assertion coverage | **350 of 443** dossier assertions — **297 on-cart** + **53 rendered scenes**, kept as separate columns (`docs/accuracysnes-coverage.md`) | | Rendered scenes | **53**, all cross-validated (`docs/adr/0013`) | | Pass rate | **100.00%** on-cart, floor enforced at 1.00 by `tests/accuracysnes.rs` | -| Cross-validated | RustySNES and Mesen2 agree on every test; snes9x agrees on every test but a handful of recorded reference bugs with citations in `scripts/accuracysnes/crossval.sh`; a headless **MesenCE** (this cycle) is the per-dot compositor's blueprint + exact-frame oracle. All images. | +| Cross-validated | RustySNES and Mesen2 agree on every test but two `PAD2_CONTRACT` rows the Lua runner cannot drive; snes9x agrees on every test but a handful of recorded reference bugs with citations in `scripts/accuracysnes/crossval.sh`; a headless **MesenCE** (this cycle) is the per-dot compositor's blueprint + exact-frame oracle. All images. | | Groups shipped | **A** (65C816) · **B** (5A22) · **C** (PPU, on-cart and rendered) · **D** (DMA/HDMA) · **E** (SPC700 + S-DSP) · **F** (controller ports) · **G** (cartridge/memory map) — all seven, all partial | | On-cart UI | AccuracyCoin-style paged menu + automatic skyline results + per-test B-skip + a Select WRAM debug viewer (`v1.21.0`) | | Defects found in this emulator | **12+** — see §5 | @@ -269,11 +269,14 @@ cannot yet. Writing `assert ENVX == $40` with a citation that says `$3F` would b of test that records our own output and calls it a spec. Parked, not abandoned: the number above is the finding to start from. -### `E5.06` — attempted, and the attempt is the finding +### `E5.06` — the attempt was the finding, and the finding became the row -The fifteen-bit wrap (`+4000h..+7FFFh` becomes `-4000h..-1`, sign lost) looked reachable through -`VxOUTX`: drive filter 1 past the boundary with a constant input and read the sign. It is not, and -the reason generalises to every `OUTX` test. +**Now covered (2026-08-01)** — see the `v1.29.0` batch below. This section is the diagnosis that +made it reachable, and the rule it leaves behind still governs every `OUTX` test. + +The fifteen-bit wrap (`+4000h..+7FFFh` becomes `-4000h..-1`, sign lost) looked reachable through a +single `VxOUTX` read: drive filter 1 past the boundary with a constant input and read the sign. It +is not, and the reason generalises. The constant-input trick the other BRR tests rely on works because a non-overflowing filter converges on a *fixed point* — the output stops changing, so it does not matter which sample the @@ -282,9 +285,13 @@ through the whole range, and `VxOUTX` reports wherever it happens to be. The two emulators returned `$E1` and `$D0` from the same image; they agree only that it is negative, and that agreement is luck, not behaviour. -Reaching it needs a read phase-locked to the sample clock, which the cart cannot do through four -mailbox bytes. **The rule this leaves behind: an `OUTX` assertion is only valid where the output is -provably stationary.** Every committed one says so in its own comment. +**The rule this leaves behind: an `OUTX` assertion is only valid where the output is provably +stationary.** Every committed one says so in its own comment. + +What unblocked the row was turning that around: **the non-stationarity is the observable**. A +decoder that saturates instead of wrapping settles, and a settled decoder's readings all carry one +sign; only a value that wraps swings both ways. So the row samples `OUTX` 64 times and asserts that +both signs appear, which needs no phase-locking at all. ### An open question `B4.12` used to answer by accident @@ -655,32 +662,95 @@ Chosen 2026-08-01, now that both halves of the Mesen2 oracle arbitrate. Two corr | `E9.02` | noise output is **highpass-filtered** `[ERRATA]` | DSP-observable via OUTX on a NON voice; errata-flagged | | `E5.06` | BRR 15-bit wrap: clamp to 16 bits, then `+4000h..+7FFFh → -4000h..-1` | reuses the landed `E5` decoder scaffolding | -**`E8.01` design, worked out so the next session does not restart from the assertion text.** Follow -`e8_02`'s shape (`apu.rs:5298`), which already solves the hard parts — `e8_02_sounding_voice` builds -the voice, `e8_02_time_to_envx` counts timer-2 ticks to a non-zero `ENVX`, and `T2DIV = 2` gives -**exactly one tick per output sample**. +**All three landed 2026-08-01.** Battery 338 tests, 100% on-cart, and both references agree: +`snes9x: OK (14 known divergence(s))`, `Mesen2: OK (2 known divergence(s))`, 53/53 scenes on each. +`MESEN2_KNOWN_FAILURES` came **down** from 3 to 2 in the process, because the redesign below removed +the one Mesen2 divergence that was ours rather than the harness's. -The assertion is a **differential**, not an absolute: write `KON` twice, the second offset by **one -sample (32 SPC cycles)** from the first, and compare the two measured delays. +### `E8.01` — two rejected drafts, and what they were really measuring -- polled every **second** sample ⇒ the two delays differ by **1** (one write waits for the next - even poll, the other does not); -- polled every sample ⇒ they are **equal**. +The row asks how often the DSP looks at `KON`. Both first attempts measured a **delay**: write +`KON`, time how long until `ENVX` leaves zero. That cannot work, and no amount of resolution rescues +it. The delay is `(P − φ) mod P` — a sawtooth in `φ`, the write's phase against the DSP's +examination grid — and **two readings of a sawtooth cannot recover its period**. Shift the second +write by `Δ` and the difference takes one of two values depending on `φ`. -So the row asserts *"the two differ"*, which `assert_a16_range` expresses fine — and note this is one -of the cases the `never hand-write a verdict byte` rule exists for. +And `φ` is not ours to set. It is fixed by where the IPL handshake left the SPC700 against the DSP's +sample clock, which is paced by the S-CPU — so it **differs between the NTSC and PAL images of the +same build**. That is exactly what the drafts reported: -Two traps carried over from `e8_02`, both already paid for once: -- the voice must be **fully silent** before the second key-on. A full-scale envelope is `$7F0` and - release steps down 8/sample, so ~254 samples; `e8_02`'s first version polled while `ENVX` was still - `~$40` and measured a delay of one tick. Reuse its arming guard (`dsp_read_to(0x08, …)` must read - zero) rather than re-deriving it. -- `T2DIV = 1` is finer but puts the reading at 15 of `TnOUT`'s 16 values, close enough to the wrap - that the NTSC/PAL drift gate caught it on the PAL image alone. Keep `T2DIV = 2`. - -**Verify by injection at the named site**: force the KON poll to every sample in -`rustysnes-apu`'s DSP and confirm *this* row's code fires — per [[accuracysnes-attribution-trap]], a -row that passes without moving under its own named injection is measuring something else. +| draft | instrument | result | +|---|---|---| +| 1 | timer 2 at `T2DIV = 2`, one-sample offset | difference **0** — tick, sample and offset are all 32 SPC cycles and move together | +| 2 | poll-loop iteration count, half-sample offset | NTSC **2**, PAL **0**, same build | + +The second was published here as "better, not yet correct" with the bands left stale on purpose, so +both images failed loudly rather than being re-banded into agreement. Neither draft was ever scored. + +**What replaced them: writes the DSP never sees.** `KON` is a register, not a queue — a write +replaces all eight bits, so a second write before the DSP next looks **cancels** the first, and that +voice is never keyed at all. The number of key-ons the DSP acts on over a burst therefore *counts +the examinations that fell between the writes*. + +The sweep writes `$01, $02, $04 … $80` — one voice each, no bit repeated — **24 SPC cycles apart**, +so 168 cycles separate the first write from the last. Over a half-open interval of length `L`, a +grid of period `P` contributes `⌊L/P⌋` or `⌊L/P⌋ + 1` points **for every phase**; add one for the +final `$80`, still pending when the burst ends: + +| `KON` examined | period | examinations in 168 cycles | voices started | +|---|---|---:|---:| +| every second sample (hardware) | 64 cycles | 2 or 3 | **3 or 4** | +| every sample | 32 cycles | 5 or 6 | **6 or 7** | + +The ranges cannot meet and neither depends on `φ`. RustySNES measures **3** (mask `$25` — voices 2, +5 and 7), and the injection that forces a per-sample examination moves it out of the band and fails +the row's only code. Mesen2, which failed the phase-fragile draft, **passes this**. + +### `E9.02` — the step, not the state + +`E9.01` already pins the frozen seed at `$4000` and reads `$81` from it. Scoring that again would be +scoring one observation twice, so `E9.02` scores the **transition**: one step of the register turns a +full-scale negative reading into a positive one at about half the magnitude. + +| LFSR | `LFSR << 1` | `VxOUTX` | +|---|---|---:| +| `$4000` seed | `$8000` | `$81` | +| `$2000` | `$4000` | `$3F` | +| `$1000` | `$2000` | `$1F` | + +A core emitting the register's 15-bit value directly reads `$3F` then `$1F` — positive throughout, +which is the DC-heavy noise the errata's "highpass" remark exists to exclude. Measured: `$81` then +`$1F`, i.e. the 48-sample window caught two steps, and the band holds both outcomes on purpose. + +**The cost, stated rather than buried:** this is the only row in the battery that moves the noise +LFSR, and nothing can put it back — `FLG` bit 7 is the DSP's soft reset and does **not** re-seed the +shift register (checked against ares, which re-seeds only in `DSP::power`). A stale comment in +`voice_program` claimed otherwise and has been corrected. So `E9.02` is registered **last**, and both +it and `E9.01` stand down as SKIP on a menu restart, the same mechanism `F1.07` uses for its +power-on `$4218`. The harness's restart-idempotence check went from `first_run_passed - 1` to `- 3`. + +### `E5.06` — the first assertion was vacuous, and the injection is what proved it + +The recorded finding from the earlier attempt still stands: a wrapping filter's `OUTX` is a sawtooth, +so no single reading is valid, and **an `OUTX` assertion is only valid where the output is provably +stationary**. The new row samples `OUTX` 64 times instead. + +The first version of that assertion was "at least one reading is negative" — a purely positive drive +cannot produce one, so a negative reading proves the sign was dropped. **Injecting the named bug made +the row pass harder:** clamping the store to `-4000h..+3FFFh` gave **64** negative readings out of 64, +against the correct decoder's **32**. A clamping decoder pins the buffer at `+7FFEh`, and the gaussian +interpolator's three-term partial sum — the same truncation `E5.13` is about — overflows a signed +16-bit intermediate from that constant, so *every* reading comes back negative. The interpolator, not +the decoder, was supplying the sign. + +What survives is the **bipolarity**. A saturating decoder leaves the buffer constant, so whatever the +interpolator makes of it, it makes every sample and the readings carry a single sign; only a value +that genuinely wraps swings both ways. The row now asserts `1..=63` negatives out of 64, and **both** +saturating variants — clamp at 15 bits, and saturate at 16 after the shift — fail it. + +**The lesson, which is the reusable part:** "this drive cannot produce a negative sample" was an +argument about the *decoder*, and the observable was three stages downstream of it. An injection at +the named site is not optional even when the reasoning looks airtight — especially then. **Explicitly NOT first: `E1.11`** ("TSET1/TCLR1 read the target twice"). Inspected and set aside as a likely **vacuous** row: the second read is a dummy, and on a `$FD`-`$FF` timer-output target the diff --git a/docs/apu.md b/docs/apu.md index 693996b4..699bed25 100644 --- a/docs/apu.md +++ b/docs/apu.md @@ -230,6 +230,21 @@ ares 32-clock voice/echo/misc micro-sequence is reproduced per output sample. Va decode, Gaussian interpolation, envelope attack/release, KON setup, echo silence, mute) plus determinism checks; the assembled `Apu` is covered by `tests/dsp_unit.rs`. +**Two properties AccuracySNES now pins on-cart, both easy to get wrong:** + +- **`KON` is *examined* every second output sample (16 kHz), not every sample** — `clock.sample` + toggles once per 32-tick sample and only the true half latches `keylatch` into `_keyon`. Because a + `$4C` write replaces all eight bits, a second write inside that 64-cycle window **cancels** the + first and that voice is never keyed at all. `E8.01` scores it by counting how many of eight + closely-spaced writes survive; a per-sample examination lets six or seven through where hardware + lets three or four. Note that the *delay* from a `KON` write to the voice starting cannot measure + this — it is a sawtooth in an uncontrolled phase. See `docs/accuracysnes-plan.md`. +- **`FLG` bit 7 does not re-seed the noise LFSR.** It is the DSP's *soft* reset: every voice is + forced into release with a zero envelope, and the shift register is untouched. ares agrees — + `noise = {}` happens in `DSP::power`, a console reset, while its `$6C` write sets only + `mainvol.reset`. The consequence is cart-visible: `E9.01`/`E9.02` read the **power-on** seed + `$4000`, `E9.02` steps it, and neither can be re-run without a real power-on. + **`Apu` API (for the core to wire the bus ports + async resync):** - `Apu::new() -> Apu` — power-on; SMP boots from the IPL reset vector (`$FFC0`). diff --git a/scripts/accuracysnes/crossval.sh b/scripts/accuracysnes/crossval.sh index 5f8dc3d4..694b8d98 100755 --- a/scripts/accuracysnes/crossval.sh +++ b/scripts/accuracysnes/crossval.sh @@ -165,6 +165,36 @@ ran=0 # and the SNESdev Wiki (34-tile budget, raised by the fetch phase). Region-independent. SNES9X_KNOWN_FAILURES=14 +# --- Mesen2's own known divergences ------------------------------------------------------------- +# +# Same mechanism as SNES9X_KNOWN_FAILURES, and set only after the failing SET (not the count) was +# read at `R_DONE == $A5` and reproduced exactly across runs, both completing at frame 479. A count +# alone would have hidden the composition, which is the whole point of enumerating them: +# +# idx279 F1.03 code 2 | Group F input rows. mesen_crossval.lua makes exactly ONE emu.setInput +# idx286 F1.10 code 2 | call because in this build the port argument does not select a +# | controller -- 0/1/2 all land on controller 1 -- so a second call for +# | port 2 overwrites port 1 with a mask containing no Start and the cart +# | never leaves its menu. The cost is that PAD2_CONTRACT-dependent rows +# | cannot pass here. Covered by the in-repo harness and snes9x, which do +# | drive both ports. See that script's comment for the full account. +# +# E8.01 used to be a third entry here, and is not any more. Its two rejected drafts asserted +# something a phase the cart cannot control got to decide -- the delay from a KON write to the +# voice starting is a sawtooth in that phase -- so the row passed on the PAL image and failed on +# the NTSC one of the SAME build, which is the signature of a test measuring its own timing rather +# than the hardware's. The write-cancellation sweep that replaced it has an answer no phase can +# move, and Mesen2 now agrees with it. Kept as a note rather than deleted: "a reference disagrees" +# was the wrong conclusion, and the record of that is worth more than the line it saves. +MESEN2_KNOWN_FAILURES=2 + +# The PAL image's own count: only F1.03 fails there, so F1.10 passes on PAL and fails on NTSC under +# the same port-2 limitation. That asymmetry has NOT been explained and is recorded as an open +# question rather than dressed up -- it is the same shape as the region flip that turned out to be +# E8.01's whole problem, and it deserves the same suspicion. Set separately rather than reusing the +# NTSC constant because the two genuinely differ, and a shared constant would hide it. +MESEN2_PAL_KNOWN_FAILURES=1 + # --- snes9x, via the libretro host -------------------------------------------------------------- if [[ -f $SNES9X ]]; then cc -O2 -o "$HOST" scripts/accuracysnes/libretro_crossval.c -ldl || exit 1 @@ -199,7 +229,9 @@ if [[ -f $MESEN ]] && command -v dotnet >/dev/null; then 0) echo "Mesen2: OK (0 failing tests)" ;; 253) echo "Mesen2: results block never appeared (bad magic)" >&2; rc=1 ;; 254) echo "Mesen2: timed out before the battery finished" >&2; rc=1 ;; - *) echo "Mesen2: $code failing test(s)" >&2; rc=1 ;; + "$MESEN2_KNOWN_FAILURES") + echo "Mesen2: OK ($code known divergence(s) — see MESEN2_KNOWN_FAILURES above)" ;; + *) echo "Mesen2: $code failing test(s), expected $MESEN2_KNOWN_FAILURES" >&2; rc=1 ;; esac ran=$((ran + 1)) else @@ -234,6 +266,8 @@ if [[ -f $PAL_ROM ]]; then 0) echo "Mesen2 PAL: OK (0 failing tests)" ;; 253) echo "Mesen2 PAL: results block never appeared (bad magic)" >&2; rc=1 ;; 254) echo "Mesen2 PAL: timed out before the battery finished" >&2; rc=1 ;; + "$MESEN2_PAL_KNOWN_FAILURES") + echo "Mesen2 PAL: OK ($code known divergence(s))" ;; *) echo "Mesen2 PAL: $code failing test(s)" >&2; rc=1 ;; esac fi diff --git a/tests/roms/AccuracySNES/ERROR_CODES.md b/tests/roms/AccuracySNES/ERROR_CODES.md index 73b50055..e0bcb3d5 100644 --- a/tests/roms/AccuracySNES/ERROR_CODES.md +++ b/tests/roms/AccuracySNES/ERROR_CODES.md @@ -2417,6 +2417,15 @@ Provenance: **Documented** (fullsnes, S-DSP BRR filters; anomie's DSP doc). Kind |---|---|---| | 1 | `$02` | filter 1 did not settle well above its constant input — a single-digit reading is filter 0's answer, so the filter was not applied | +### E5.06 — BRR wraps at 15 bits + +Provenance: **Documented** (fullsnes and anomie's DSP doc: the decoded sample is clamped to 16 bits and then stored as 15, so +4000h..+7FFFh becomes -4000h..-1 and the sign is lost). Kind: scored. + +| Code | Byte | Meaning | +|---|---|---| +| 1 | `$02` | not one of the sixty-four OUTX readings was non-zero, so the voice never sounded and the sign test below is about silence rather than about the decoder | +| 2 | `$04` | sixty-four OUTX readings from a filter driven only by positive nibbles all carried the same sign, so the decoded sample settled instead of wrapping: the store is saturating where hardware drops the sign bit at fifteen bits | + ### E5.13 — Released voices decode Provenance: **Documented** (fullsnes and anomie's DSP doc: the BRR decoder advances on the pitch clock regardless of the envelope; voices never actually stop decoding). Kind: scored. @@ -2527,6 +2536,14 @@ Provenance: **Documented** (SNESdev Wiki, S-DSP envelopes; fullsnes; anomie's DS |---|---|---| | 1 | `$02` | the envelope was not zero well after key-off, so release did not run to silence | +### E8.01 — KON examined at 16 kHz + +Provenance: **Documented** (fullsnes and anomie's DSP doc [ERRATA]: the key-on/key-off registers are examined once every two output samples, giving a 16 kHz effective poll rate). Kind: scored. + +| Code | Byte | Meaning | +|---|---|---| +| 1 | `$02` | eight KON writes 24 SPC cycles apart started more than five voices, so more than four examinations fell inside the burst -- KON is being read every output sample rather than every second one. A count of zero or one instead means the sweep never ran | + ### E8.02 — Key-on takes 5 samples Provenance: **Documented** (fullsnes and anomie's DSP doc: KON is held for five output samples while the directory and the first BRR block are fetched, before the envelope starts). Kind: scored. @@ -2635,6 +2652,17 @@ Provenance: **Documented** (fullsnes, SPC700 TEST register; ares and bsnes smp/t | 1 | `$02` | timer 0 advanced while TEST bit 0 was set, so the halt bit is being modelled as ordinary storage rather than as a control | | 2 | `$04` | timer 0 did not advance with TEST back at its reset value, so the halted reading above says nothing about the halt bit | +### 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. + +| Code | Byte | Meaning | +|---|---|---| +| 1 | `$02` | 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 | +| 2 | `$04` | 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 | +| 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 | + ## 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 9f92c3a4..e716e9f9 100644 --- a/tests/roms/AccuracySNES/SOURCE_CATALOG.tsv +++ b/tests/roms/AccuracySNES/SOURCE_CATALOG.tsv @@ -250,88 +250,91 @@ 247 E5.03 E BRR sample arithmetic Documented fullsnes, S-DSP BRR; anomie's DSP doc Scored 0x7EF117 E5.03 248 E5.04 E Invalid shift collapses Documented fullsnes, S-DSP BRR; anomie's DSP doc Scored 0x7EF118 E5.04 249 E5.05 E BRR filter 1 Documented fullsnes, S-DSP BRR filters; anomie's DSP doc Scored 0x7EF119 E5.05 -250 E5.13 E Released voices decode Documented fullsnes and anomie's DSP doc: the BRR decoder advances on the pitch clock regardless of the envelope; voices never actually stop decoding Scored 0x7EF11A E5.13 -251 E7.01 E Rate 0 never fires Documented SNESdev Wiki, S-DSP envelopes; fullsnes; anomie's DSP doc Scored 0x7EF11B E7.01 -252 E7.13 E GAIN bent-increase Documented fullsnes and anomie's DSP doc: GAIN mode 7 increases +32 per sample below $600 and +8 above, comparing the internal envelope latch unsigned Scored 0x7EF11C E7.13 -253 E7.17 E Lin-decrease clamps 0 Documented fullsnes and anomie's DSP doc: GAIN linear-decrease subtracts $20 per tick and clamps the envelope to zero on underflow, comparing the internal envelope unsigned Scored 0x7EF11D E7.17 -254 E7.18 E ENVX is E>>4 Documented fullsnes and anomie's DSP doc: VxENVX = envelope >> 4, a seven-bit value with bit 7 always clear Scored 0x7EF11E E7.18 -255 E7.04 E Attack $F is instant Documented fullsnes and anomie's DSP doc: attack rate $F fires every sample with a step of +1024, rather than the +32 on a counter tick every other rate uses Scored 0x7EF11F E7.04 -256 E7.09 E Release rate is fixed Documented fullsnes and anomie's DSP doc: release steps -8 per sample regardless of ADSR, which is why a custom release has to be built from GAIN Scored 0x7EF120 E7.09 -257 E7.05 E Decay index d*2+16 Documented fullsnes and anomie's DSP doc: the decay phase indexes the counter table at d*2+16, stepping E -= 1 then E -= E>>8 Scored 0x7EF121 E7.05 -258 E7.06 E Sustain index verbatim Documented fullsnes and anomie's DSP doc: the sustain phase indexes the counter table with the ADSR2 rate field directly, unlike decay's d*2+16 Scored 0x7EF122 E7.06 -259 E7.07 E Sustain boundary Documented fullsnes and anomie's DSP doc: the decay phase ends when (E >> 8) equals the ADSR2 sustain level, giving a boundary of $100 per level Scored 0x7EF123 E7.07 -260 E7.03 E Attack index a*2+1 Documented fullsnes and anomie's DSP doc: the attack phase indexes the counter table at a*2+1 and steps the envelope by +32 Scored 0x7EF124 E7.03 -261 E7.12 E GAIN sustain boundary Contested the dossier records the GAIN-sourced boundary as [ERRATA]; RustySNES takes it from ADSR2 instead, which E7.07 asserts, so which behaviour is right is not settled here Golden 0x7EF125 E7.12 -262 E7.08 E Key-off releases to zero Documented SNESdev Wiki, S-DSP envelopes; fullsnes; anomie's DSP doc Scored 0x7EF126 E7.08 -263 E8.02 E Key-on takes 5 samples Documented fullsnes and anomie's DSP doc: KON is held for five output samples while the directory and the first BRR block are fetched, before the envelope starts Scored 0x7EF127 E8.02 -264 E8.03 E KON restarts a voice Documented fullsnes and anomie's DSP doc: KON re-enters the key-on sequence unconditionally, resetting the BRR pointer and zeroing the envelope, whether or not the voice was already playing Scored 0x7EF128 E8.03 -265 E8.07 E KOFF pulse collapses Contested KON/KOFF are sampled every second output sample, so whether a short pulse is seen depends on where the poll falls inside it -- which makes the outcome phase-dependent rather than fixed, the same hedge E8.05 and E8.06 carry as "usually" Golden 0x7EF129 E8.07 -266 E8.10 E KOFF+KON cuts faster Documented fullsnes and anomie's DSP doc: KON zeroes the envelope before its attack, and KOFF outranks KON, so the pair silences immediately where KOFF alone ramps Scored 0x7EF12A E8.10 -267 E7.11 E GAIN linear increase Documented SNESdev Wiki, S-DSP envelopes; fullsnes; anomie's DSP doc Scored 0x7EF12B E7.11 -268 E7.14 E GAIN decrease clamps Documented SNESdev Wiki, S-DSP envelopes; fullsnes; anomie's DSP doc Scored 0x7EF12C E7.14 -269 E7.15 E ENVX is E >> 4 Documented SNESdev Wiki, S-DSP envelopes; fullsnes; anomie's DSP doc Scored 0x7EF12D E7.15 -270 E6.02 E Pitch $1000 is 1:1 Documented fullsnes, S-DSP pitch; anomie's DSP doc Scored 0x7EF12E E6.02 -271 E6.02b E Pitch $1000 does finish Documented fullsnes, S-DSP pitch; anomie's DSP doc Scored 0x7EF12F E6.02 -272 E6.02c E Pitch $2000 is +1 octave Documented fullsnes, S-DSP pitch; anomie's DSP doc Scored 0x7EF130 E6.02 -273 E6.02d E Pitch $2000 upper bound Documented fullsnes, S-DSP pitch; anomie's DSP doc Scored 0x7EF131 E6.02 -274 E3.06 E T2 is eight times T0 Documented SNESdev Wiki, SPC700 timers; fullsnes Scored 0x7EF132 E3.06 -275 E3.08 E TEST bit 0 halts timers Documented fullsnes, SPC700 TEST register; ares and bsnes smp/timing Scored 0x7EF133 E3.08 -276 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 0x7EF134 F1.01 -277 F1.02 F Pad reads 17+ are 1 Documented SNESdev Wiki, controller protocol; fullsnes Scored 0x7EF135 F1.02 -278 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 0x7EF136 F1.03 -279 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 0x7EF137 F1.04 -280 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 0x7EF138 F1.07 -281 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 0x7EF139 F1.05 -282 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 0x7EF13A F1.06 -283 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 0x7EF13B F1.08 -284 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 0x7EF13C F1.09 -285 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 0x7EF13D F1.10 -286 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 0x7EF13E F1.11 -287 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 0x7EF13F F1.12 -288 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 0x7EF140 F1.14 -289 G1.02 G Reset: $4210/$4211 clear Documented SNESdev Wiki, power-on state; fullsnes Scored 0x7EF141 G1.02 -290 G1.04 G Reset: emulation mode Documented SNESdev Wiki, power-on state; WDC 65C816 datasheet, XCE Scored 0x7EF142 G1.04 -291 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 0x7EF143 G1.05 -292 G1.08 G Write-only read: openbus Documented SNESdev Wiki, open bus; fullsnes, memory map notes Scored 0x7EF144 G1.08 -293 G1.10 G Checksum XOR complement Documented SNESdev Wiki, cartridge header; fullsnes Scored 0x7EF145 G1.10 -294 G1.11 G Checksum over the image Documented SNESdev Wiki, cartridge header checksum; fullsnes Scored 0x7EF146 G1.11 -295 G1.12 G LoROM header location Documented SNESdev Wiki, cartridge header; fullsnes Scored 0x7EF147 G1.12 -296 G1.14 G LoROM bank decode Documented SNESdev Wiki, memory map; fullsnes Scored 0x7EF148 G1.14 -297 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 0x7EF149 G1.01 -298 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 0x7EF14A G1.03 -299 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 0x7EF14B G1.07 -300 A5.S01 A Sweep: CLC Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF14C A5.01-08 -301 A5.S02 A Sweep: SEC Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF14D A5.01-08 -302 A5.S03 A Sweep: CLV Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF14E A5.01-08 -303 A5.S04 A Sweep: INX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF14F A5.01-08 -304 A5.S05 A Sweep: DEX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF150 A5.01-08 -305 A5.S06 A Sweep: TAX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF151 A5.01-08 -306 A5.S07 A Sweep: TXY Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF152 A5.01-08 -307 A5.S08 A Sweep: ASL A Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF153 A5.01-08 -308 A5.S09 A Sweep: XBA Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF154 A5.01-08 -309 A5.S10 A Sweep: TCD Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF155 A5.01-08 -310 A5.S11 A Sweep: LDA #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF156 A5.01-08 -311 A5.S12 A Sweep: LDX #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF157 A5.01-08 -312 A5.S13 A Sweep: CMP #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF158 A5.01-08 -313 A5.S14 A Sweep: BIT #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF159 A5.01-08 -314 A5.S15 A Sweep: REP #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15A A5.01-08 -315 A5.S16 A Sweep: SEP #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15B A5.01-08 -316 A5.S17 A Sweep: WDM Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15C A5.01-08 -317 A5.S18 A Sweep: PHA+PLA Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15D A5.01-08 -318 A5.S19 A Sweep: PHP+PLP Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15E A5.01-08 -319 A5.S20 A Sweep: PHB+PLB Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15F A5.01-08 -320 A5.S21 A Sweep: PHD+PLD Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF160 A5.01-08 -321 A5.S22 A Sweep: PHX+PLX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF161 A5.01-08 -322 A5.S23 A Sweep: LDA dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF162 A5.01-08 -323 A5.S24 A Sweep: LDA abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF163 A5.01-08 -324 A5.S25 A Sweep: LDA long Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF164 A5.01-08 -325 A5.S26 A Sweep: STA dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF165 A5.01-08 -326 A5.S27 A Sweep: STA abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF166 A5.01-08 -327 A5.S28 A Sweep: LDA dp,X Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF167 A5.01-08 -328 A5.S29 A Sweep: LDA abs,X Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF168 A5.01-08 -329 A5.S30 A Sweep: INC dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF169 A5.01-08 -330 A5.S31 A Sweep: INC abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16A A5.01-08 -331 A5.S32 A Sweep: ADC dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16B A5.01-08 -332 A5.S33 A Sweep: CMP abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16C A5.01-08 -333 A5.S34 A Sweep: BVS untaken Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16D A5.01-08 -334 A5.S35 A Sweep: BRL flat 4 Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16E A5.16 +250 E5.06 E BRR wraps at 15 bits Documented fullsnes and anomie's DSP doc: the decoded sample is clamped to 16 bits and then stored as 15, so +4000h..+7FFFh becomes -4000h..-1 and the sign is lost Scored 0x7EF11A E5.06 +251 E5.13 E Released voices decode Documented fullsnes and anomie's DSP doc: the BRR decoder advances on the pitch clock regardless of the envelope; voices never actually stop decoding Scored 0x7EF11B E5.13 +252 E7.01 E Rate 0 never fires Documented SNESdev Wiki, S-DSP envelopes; fullsnes; anomie's DSP doc Scored 0x7EF11C E7.01 +253 E7.13 E GAIN bent-increase Documented fullsnes and anomie's DSP doc: GAIN mode 7 increases +32 per sample below $600 and +8 above, comparing the internal envelope latch unsigned Scored 0x7EF11D E7.13 +254 E7.17 E Lin-decrease clamps 0 Documented fullsnes and anomie's DSP doc: GAIN linear-decrease subtracts $20 per tick and clamps the envelope to zero on underflow, comparing the internal envelope unsigned Scored 0x7EF11E E7.17 +255 E7.18 E ENVX is E>>4 Documented fullsnes and anomie's DSP doc: VxENVX = envelope >> 4, a seven-bit value with bit 7 always clear Scored 0x7EF11F E7.18 +256 E7.04 E Attack $F is instant Documented fullsnes and anomie's DSP doc: attack rate $F fires every sample with a step of +1024, rather than the +32 on a counter tick every other rate uses Scored 0x7EF120 E7.04 +257 E7.09 E Release rate is fixed Documented fullsnes and anomie's DSP doc: release steps -8 per sample regardless of ADSR, which is why a custom release has to be built from GAIN Scored 0x7EF121 E7.09 +258 E7.05 E Decay index d*2+16 Documented fullsnes and anomie's DSP doc: the decay phase indexes the counter table at d*2+16, stepping E -= 1 then E -= E>>8 Scored 0x7EF122 E7.05 +259 E7.06 E Sustain index verbatim Documented fullsnes and anomie's DSP doc: the sustain phase indexes the counter table with the ADSR2 rate field directly, unlike decay's d*2+16 Scored 0x7EF123 E7.06 +260 E7.07 E Sustain boundary Documented fullsnes and anomie's DSP doc: the decay phase ends when (E >> 8) equals the ADSR2 sustain level, giving a boundary of $100 per level Scored 0x7EF124 E7.07 +261 E7.03 E Attack index a*2+1 Documented fullsnes and anomie's DSP doc: the attack phase indexes the counter table at a*2+1 and steps the envelope by +32 Scored 0x7EF125 E7.03 +262 E7.12 E GAIN sustain boundary Contested the dossier records the GAIN-sourced boundary as [ERRATA]; RustySNES takes it from ADSR2 instead, which E7.07 asserts, so which behaviour is right is not settled here Golden 0x7EF126 E7.12 +263 E7.08 E Key-off releases to zero Documented SNESdev Wiki, S-DSP envelopes; fullsnes; anomie's DSP doc Scored 0x7EF127 E7.08 +264 E8.01 E KON examined at 16 kHz Documented fullsnes and anomie's DSP doc [ERRATA]: the key-on/key-off registers are examined once every two output samples, giving a 16 kHz effective poll rate Scored 0x7EF128 E8.01 +265 E8.02 E Key-on takes 5 samples Documented fullsnes and anomie's DSP doc: KON is held for five output samples while the directory and the first BRR block are fetched, before the envelope starts Scored 0x7EF129 E8.02 +266 E8.03 E KON restarts a voice Documented fullsnes and anomie's DSP doc: KON re-enters the key-on sequence unconditionally, resetting the BRR pointer and zeroing the envelope, whether or not the voice was already playing Scored 0x7EF12A E8.03 +267 E8.07 E KOFF pulse collapses Contested KON/KOFF are sampled every second output sample, so whether a short pulse is seen depends on where the poll falls inside it -- which makes the outcome phase-dependent rather than fixed, the same hedge E8.05 and E8.06 carry as "usually" Golden 0x7EF12B E8.07 +268 E8.10 E KOFF+KON cuts faster Documented fullsnes and anomie's DSP doc: KON zeroes the envelope before its attack, and KOFF outranks KON, so the pair silences immediately where KOFF alone ramps Scored 0x7EF12C E8.10 +269 E7.11 E GAIN linear increase Documented SNESdev Wiki, S-DSP envelopes; fullsnes; anomie's DSP doc Scored 0x7EF12D E7.11 +270 E7.14 E GAIN decrease clamps Documented SNESdev Wiki, S-DSP envelopes; fullsnes; anomie's DSP doc Scored 0x7EF12E E7.14 +271 E7.15 E ENVX is E >> 4 Documented SNESdev Wiki, S-DSP envelopes; fullsnes; anomie's DSP doc Scored 0x7EF12F E7.15 +272 E6.02 E Pitch $1000 is 1:1 Documented fullsnes, S-DSP pitch; anomie's DSP doc Scored 0x7EF130 E6.02 +273 E6.02b E Pitch $1000 does finish Documented fullsnes, S-DSP pitch; anomie's DSP doc Scored 0x7EF131 E6.02 +274 E6.02c E Pitch $2000 is +1 octave Documented fullsnes, S-DSP pitch; anomie's DSP doc Scored 0x7EF132 E6.02 +275 E6.02d E Pitch $2000 upper bound Documented fullsnes, S-DSP pitch; anomie's DSP doc Scored 0x7EF133 E6.02 +276 E3.06 E T2 is eight times T0 Documented SNESdev Wiki, SPC700 timers; fullsnes Scored 0x7EF134 E3.06 +277 E3.08 E TEST bit 0 halts timers Documented fullsnes, SPC700 TEST register; ares and bsnes smp/timing Scored 0x7EF135 E3.08 +278 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 0x7EF136 E9.02 +279 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 0x7EF137 F1.01 +280 F1.02 F Pad reads 17+ are 1 Documented SNESdev Wiki, controller protocol; fullsnes Scored 0x7EF138 F1.02 +281 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 0x7EF139 F1.03 +282 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 0x7EF13A F1.04 +283 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 0x7EF13B F1.07 +284 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 0x7EF13C F1.05 +285 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 0x7EF13D F1.06 +286 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 0x7EF13E F1.08 +287 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 0x7EF13F F1.09 +288 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 0x7EF140 F1.10 +289 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 0x7EF141 F1.11 +290 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 0x7EF142 F1.12 +291 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 0x7EF143 F1.14 +292 G1.02 G Reset: $4210/$4211 clear Documented SNESdev Wiki, power-on state; fullsnes Scored 0x7EF144 G1.02 +293 G1.04 G Reset: emulation mode Documented SNESdev Wiki, power-on state; WDC 65C816 datasheet, XCE Scored 0x7EF145 G1.04 +294 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 0x7EF146 G1.05 +295 G1.08 G Write-only read: openbus Documented SNESdev Wiki, open bus; fullsnes, memory map notes Scored 0x7EF147 G1.08 +296 G1.10 G Checksum XOR complement Documented SNESdev Wiki, cartridge header; fullsnes Scored 0x7EF148 G1.10 +297 G1.11 G Checksum over the image Documented SNESdev Wiki, cartridge header checksum; fullsnes Scored 0x7EF149 G1.11 +298 G1.12 G LoROM header location Documented SNESdev Wiki, cartridge header; fullsnes Scored 0x7EF14A G1.12 +299 G1.14 G LoROM bank decode Documented SNESdev Wiki, memory map; fullsnes Scored 0x7EF14B G1.14 +300 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 0x7EF14C G1.01 +301 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 0x7EF14D G1.03 +302 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 0x7EF14E G1.07 +303 A5.S01 A Sweep: CLC Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF14F A5.01-08 +304 A5.S02 A Sweep: SEC Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF150 A5.01-08 +305 A5.S03 A Sweep: CLV Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF151 A5.01-08 +306 A5.S04 A Sweep: INX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF152 A5.01-08 +307 A5.S05 A Sweep: DEX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF153 A5.01-08 +308 A5.S06 A Sweep: TAX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF154 A5.01-08 +309 A5.S07 A Sweep: TXY Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF155 A5.01-08 +310 A5.S08 A Sweep: ASL A Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF156 A5.01-08 +311 A5.S09 A Sweep: XBA Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF157 A5.01-08 +312 A5.S10 A Sweep: TCD Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF158 A5.01-08 +313 A5.S11 A Sweep: LDA #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF159 A5.01-08 +314 A5.S12 A Sweep: LDX #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15A A5.01-08 +315 A5.S13 A Sweep: CMP #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15B A5.01-08 +316 A5.S14 A Sweep: BIT #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15C A5.01-08 +317 A5.S15 A Sweep: REP #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15D A5.01-08 +318 A5.S16 A Sweep: SEP #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15E A5.01-08 +319 A5.S17 A Sweep: WDM Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15F A5.01-08 +320 A5.S18 A Sweep: PHA+PLA Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF160 A5.01-08 +321 A5.S19 A Sweep: PHP+PLP Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF161 A5.01-08 +322 A5.S20 A Sweep: PHB+PLB Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF162 A5.01-08 +323 A5.S21 A Sweep: PHD+PLD Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF163 A5.01-08 +324 A5.S22 A Sweep: PHX+PLX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF164 A5.01-08 +325 A5.S23 A Sweep: LDA dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF165 A5.01-08 +326 A5.S24 A Sweep: LDA abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF166 A5.01-08 +327 A5.S25 A Sweep: LDA long Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF167 A5.01-08 +328 A5.S26 A Sweep: STA dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF168 A5.01-08 +329 A5.S27 A Sweep: STA abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF169 A5.01-08 +330 A5.S28 A Sweep: LDA dp,X Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16A A5.01-08 +331 A5.S29 A Sweep: LDA abs,X Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16B A5.01-08 +332 A5.S30 A Sweep: INC dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16C A5.01-08 +333 A5.S31 A Sweep: INC abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16D A5.01-08 +334 A5.S32 A Sweep: ADC dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16E A5.01-08 +335 A5.S33 A Sweep: CMP abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16F A5.01-08 +336 A5.S34 A Sweep: BVS untaken Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF170 A5.01-08 +337 A5.S35 A Sweep: BRL flat 4 Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF171 A5.16 diff --git a/tests/roms/AccuracySNES/asm/tests_group_a.s b/tests/roms/AccuracySNES/asm/tests_group_a.s index 8215df6c..c135f381 100644 --- a/tests/roms/AccuracySNES/asm/tests_group_a.s +++ b/tests/roms/AccuracySNES/asm/tests_group_a.s @@ -13971,6 +13971,23 @@ CATALOG_IMPL = 1 ; E9.01 — Noise LFSR seed ; provenance: Documented (fullsnes and anomie's DSP doc: the noise shift register resets to $4000, with taps bit0 XOR bit1 feeding bit 14) .proc test_e9_01 + .a16 + .i16 + sep #$20 + .a8 + lda f:V_RESTARTED + beq :+ + ; SKIP: the seed this reads is the power-on one, and E9.02 steps the register away from it + 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 .a16 .i16 rep #$30 @@ -19269,6 +19286,141 @@ CATALOG_IMPL = 1 jml test_restore .endproc +; E5.06 — BRR wraps at 15 bits +; provenance: Documented (fullsnes and anomie's DSP doc: the decoded sample is clamped to 16 bits and then stored as 15, so +4000h..+7FFFh becomes -4000h..-1 and the sign is lost) +.proc test_e5_06 + .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_78) + sta f:V_APU_SRC + sep #$20 + .a8 + lda #^apu_prog_78 + sta f:V_APU_BANK + rep #$30 + .a16 + .i16 + lda #307 + 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 260: E5.06 negative readings out of 64, driving filter 1 with +7 nibbles + sta f:$7EE408 + lda f:$7E0101 + and #$00FF + ; record slot 261: E5.06 non-zero readings out of 64 (the sounding guard) + sta f:$7EE40A + lda f:$7E0102 + and #$00FF + ; record slot 262: E5.06 the last of the 64 readings + sta f:$7EE40C + ; The guard: sixty-four silences would report zero negatives and look like a clean clamp. + lda f:$7E0101 + and #$00FF + cmp #$0001 + bcs :+ + jmp @fail1 + : + cmp #$0041 + bcc :+ + jmp @fail1 + : + ; The row. A decoder that saturates leaves the buffer constant, so its readings all carry + ; the same sign -- either all 64 negative or all 64 positive, depending on what the + ; interpolator makes of a pinned buffer. Only a value that wraps swings both ways. + lda f:$7E0100 + and #$00FF + cmp #$0001 + bcs :+ + jmp @fail2 + : + cmp #$0040 + 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: + ; not one of the sixty-four OUTX readings was non-zero, so the voice never sounded and the sign test below is about silence rather than about the decoder + sep #$20 + .a8 + lda #$02 + sta f:$7EE010 + jml test_restore +@fail2: + ; sixty-four OUTX readings from a filter driven only by positive nibbles all carried the same sign, so the decoded sample settled instead of wrapping: the store is saturating where hardware drops the sign bit at fifteen bits + sep #$20 + .a8 + lda #$04 + sta f:$7EE010 + jml test_restore +.endproc + ; E5.13 — Released voices decode ; provenance: Documented (fullsnes and anomie's DSP doc: the BRR decoder advances on the pitch clock regardless of the envelope; voices never actually stop decoding) .proc test_e5_13 @@ -19280,11 +19432,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_78) + lda #.loword(apu_prog_79) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_78 + lda #^apu_prog_79 sta f:V_APU_BANK rep #$30 .a16 @@ -19411,11 +19563,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_79) + lda #.loword(apu_prog_80) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_79 + lda #^apu_prog_80 sta f:V_APU_BANK rep #$30 .a16 @@ -19507,11 +19659,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_80) + lda #.loword(apu_prog_81) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_80 + lda #^apu_prog_81 sta f:V_APU_BANK rep #$30 .a16 @@ -19615,11 +19767,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_81) + lda #.loword(apu_prog_82) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_81 + lda #^apu_prog_82 sta f:V_APU_BANK rep #$30 .a16 @@ -19744,11 +19896,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_82) + lda #.loword(apu_prog_83) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_82 + lda #^apu_prog_83 sta f:V_APU_BANK rep #$30 .a16 @@ -19847,11 +19999,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_83) + lda #.loword(apu_prog_84) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_83 + lda #^apu_prog_84 sta f:V_APU_BANK rep #$30 .a16 @@ -19914,11 +20066,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_84) + lda #.loword(apu_prog_85) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_84 + lda #^apu_prog_85 sta f:V_APU_BANK rep #$30 .a16 @@ -20044,11 +20196,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_85) + lda #.loword(apu_prog_86) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_85 + lda #^apu_prog_86 sta f:V_APU_BANK rep #$30 .a16 @@ -20111,11 +20263,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_86) + lda #.loword(apu_prog_87) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_86 + lda #^apu_prog_87 sta f:V_APU_BANK rep #$30 .a16 @@ -20255,11 +20407,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_87) + lda #.loword(apu_prog_88) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_87 + lda #^apu_prog_88 sta f:V_APU_BANK rep #$30 .a16 @@ -20322,11 +20474,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_88) + lda #.loword(apu_prog_89) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_88 + lda #^apu_prog_89 sta f:V_APU_BANK rep #$30 .a16 @@ -20455,11 +20607,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_89) + lda #.loword(apu_prog_90) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_89 + lda #^apu_prog_90 sta f:V_APU_BANK rep #$30 .a16 @@ -20522,11 +20674,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_90) + lda #.loword(apu_prog_91) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_90 + lda #^apu_prog_91 sta f:V_APU_BANK rep #$30 .a16 @@ -20655,11 +20807,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_91) + lda #.loword(apu_prog_92) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_91 + lda #^apu_prog_92 sta f:V_APU_BANK rep #$30 .a16 @@ -20722,11 +20874,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_92) + lda #.loword(apu_prog_93) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_92 + lda #^apu_prog_93 sta f:V_APU_BANK rep #$30 .a16 @@ -20853,11 +21005,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_93) + lda #.loword(apu_prog_94) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_93 + lda #^apu_prog_94 sta f:V_APU_BANK rep #$30 .a16 @@ -20920,11 +21072,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_94) + lda #.loword(apu_prog_95) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_94 + lda #^apu_prog_95 sta f:V_APU_BANK rep #$30 .a16 @@ -21053,11 +21205,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_95) + lda #.loword(apu_prog_96) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_95 + lda #^apu_prog_96 sta f:V_APU_BANK rep #$30 .a16 @@ -21120,11 +21272,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_96) + lda #.loword(apu_prog_97) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_96 + lda #^apu_prog_97 sta f:V_APU_BANK rep #$30 .a16 @@ -21227,11 +21379,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_97) + lda #.loword(apu_prog_98) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_97 + lda #^apu_prog_98 sta f:V_APU_BANK rep #$30 .a16 @@ -21312,6 +21464,124 @@ CATALOG_IMPL = 1 jml test_restore .endproc +; E8.01 — KON examined at 16 kHz +; provenance: Documented (fullsnes and anomie's DSP doc [ERRATA]: the key-on/key-off registers are examined once every two output samples, giving a 16 kHz effective poll rate) +.proc test_e8_01 + .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_99) + sta f:V_APU_SRC + sep #$20 + .a8 + lda #^apu_prog_99 + sta f:V_APU_BANK + rep #$30 + .a16 + .i16 + lda #783 + 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 256: E8.01 which voices the KON sweep started (bit 7 = voice 0) + sta f:$7EE400 + ; Count the set bits: how many of the eight writes the DSP acted on. The mask itself is in + ; the measurement channel above, so a failure here can be read back to the exact pattern. + ldx #$0000 + ldy #$0008 +@e801bit: + lsr a + bcc @e801next + inx +@e801next: + dey + bne @e801bit + txa + ; record slot 257: E8.01 how many of the eight voices the sweep started + sta f:$7EE402 + cmp #$0002 + bcs :+ + jmp @fail1 + : + cmp #$0006 + bcc :+ + jmp @fail1 + : + 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: + ; eight KON writes 24 SPC cycles apart started more than five voices, so more than four examinations fell inside the burst -- KON is being read every output sample rather than every second one. A count of zero or one instead means the sweep never ran + sep #$20 + .a8 + lda #$02 + sta f:$7EE010 + jml test_restore +.endproc + ; E8.02 — Key-on takes 5 samples ; provenance: Documented (fullsnes and anomie's DSP doc: KON is held for five output samples while the directory and the first BRR block are fetched, before the envelope starts) .proc test_e8_02 @@ -21323,11 +21593,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_98) + lda #.loword(apu_prog_100) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_98 + lda #^apu_prog_100 sta f:V_APU_BANK rep #$30 .a16 @@ -21484,11 +21754,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_99) + lda #.loword(apu_prog_101) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_99 + lda #^apu_prog_101 sta f:V_APU_BANK rep #$30 .a16 @@ -21551,11 +21821,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_100) + lda #.loword(apu_prog_102) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_100 + lda #^apu_prog_102 sta f:V_APU_BANK rep #$30 .a16 @@ -21680,11 +21950,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_101) + lda #.loword(apu_prog_103) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_101 + lda #^apu_prog_103 sta f:V_APU_BANK rep #$30 .a16 @@ -21779,11 +22049,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_102) + lda #.loword(apu_prog_104) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_102 + lda #^apu_prog_104 sta f:V_APU_BANK rep #$30 .a16 @@ -21846,11 +22116,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_103) + lda #.loword(apu_prog_105) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_103 + lda #^apu_prog_105 sta f:V_APU_BANK rep #$30 .a16 @@ -21967,11 +22237,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_104) + lda #.loword(apu_prog_106) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_104 + lda #^apu_prog_106 sta f:V_APU_BANK rep #$30 .a16 @@ -22063,11 +22333,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_105) + lda #.loword(apu_prog_107) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_105 + lda #^apu_prog_107 sta f:V_APU_BANK rep #$30 .a16 @@ -22159,11 +22429,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_106) + lda #.loword(apu_prog_108) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_106 + lda #^apu_prog_108 sta f:V_APU_BANK rep #$30 .a16 @@ -22255,11 +22525,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_107) + lda #.loword(apu_prog_109) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_107 + lda #^apu_prog_109 sta f:V_APU_BANK rep #$30 .a16 @@ -22353,11 +22623,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_108) + lda #.loword(apu_prog_110) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_108 + lda #^apu_prog_110 sta f:V_APU_BANK rep #$30 .a16 @@ -22451,11 +22721,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_109) + lda #.loword(apu_prog_111) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_109 + lda #^apu_prog_111 sta f:V_APU_BANK rep #$30 .a16 @@ -22549,11 +22819,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_110) + lda #.loword(apu_prog_112) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_110 + lda #^apu_prog_112 sta f:V_APU_BANK rep #$30 .a16 @@ -22647,11 +22917,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_111) + lda #.loword(apu_prog_113) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_111 + lda #^apu_prog_113 sta f:V_APU_BANK rep #$30 .a16 @@ -22717,10 +22987,127 @@ CATALOG_IMPL = 1 bcc :+ jmp @fail1 : - ; Timer 2 over the SAME interval: eight times the rate, so eight or more ticks. + ; Timer 2 over the SAME interval: eight times the rate, so eight or more ticks. + lda f:$7E0102 + and #$00FF + cmp #$0008 + bcs :+ + jmp @fail2 + : + cmp #$0010 + 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: + ; timer 0 did not tick once over this interval, or ticked more than three times — either way the interval is not the one this test needs and the ratio below means nothing + sep #$20 + .a8 + lda #$02 + sta f:$7EE010 + jml test_restore +@fail2: + ; timer 2 did not count roughly eight times what timer 0 did over the same interval, so it is not running from the 64 kHz stage — a core reading $01 here runs every timer at 8 kHz + sep #$20 + .a8 + lda #$04 + sta f:$7EE010 + jml test_restore +.endproc + +; E3.08 — TEST bit 0 halts timers +; provenance: Documented (fullsnes, SPC700 TEST register; ares and bsnes smp/timing) +.proc test_e3_08 + .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_114) + sta f:V_APU_SRC + sep #$20 + .a8 + lda #^apu_prog_114 + sta f:V_APU_BANK + rep #$30 + .a16 + .i16 + lda #57 + 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 + ; Halted: enabled, at the fastest divider, over a delay that is several ticks long. + rep #$30 + .a16 + .i16 + lda f:$7E0101 + and #$00FF + cmp #$0000 + beq :+ + jmp @fail1 + : + ; Running: the control, and without it the reading above would pass on a dead timer. lda f:$7E0102 and #$00FF - cmp #$0008 + cmp #$0001 bcs :+ jmp @fail2 : @@ -22742,14 +23129,14 @@ CATALOG_IMPL = 1 sta f:$7EE010 jml test_restore @fail1: - ; timer 0 did not tick once over this interval, or ticked more than three times — either way the interval is not the one this test needs and the ratio below means nothing + ; timer 0 advanced while TEST bit 0 was set, so the halt bit is being modelled as ordinary storage rather than as a control sep #$20 .a8 lda #$02 sta f:$7EE010 jml test_restore @fail2: - ; timer 2 did not count roughly eight times what timer 0 did over the same interval, so it is not running from the 64 kHz stage — a core reading $01 here runs every timer at 8 kHz + ; timer 0 did not advance with TEST back at its reset value, so the halted reading above says nothing about the halt bit sep #$20 .a8 lda #$04 @@ -22757,9 +23144,26 @@ CATALOG_IMPL = 1 jml test_restore .endproc -; E3.08 — TEST bit 0 halts timers -; provenance: Documented (fullsnes, SPC700 TEST register; ares and bsnes smp/timing) -.proc test_e3_08 +; 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 .a16 .i16 rep #$30 @@ -22768,16 +23172,16 @@ CATALOG_IMPL = 1 phk plb ; Point apu_upload at this test's own program image, which lives in another bank. - lda #.loword(apu_prog_112) + lda #.loword(apu_prog_115) sta f:V_APU_SRC sep #$20 .a8 - lda #^apu_prog_112 + lda #^apu_prog_115 sta f:V_APU_BANK rep #$30 .a16 .i16 - lda #57 + 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 @@ -22824,26 +23228,57 @@ CATALOG_IMPL = 1 ; Release: the program hands the APU back to the IPL so the NEXT test can upload at all. lda #$A5 sta APUIO0 - ; Halted: enabled, at the fastest divider, over a delay that is several ticks long. rep #$30 .a16 .i16 + lda f:$7E0100 + and #$00FF + ; record slot 258: E9.02 noise OUTX with the LFSR frozen at its seed + sta f:$7EE404 lda f:$7E0101 and #$00FF - cmp #$0000 + ; 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 : - ; Running: the control, and without it the reading above would pass on a dead timer. - lda f:$7E0102 - and #$00FF - cmp #$0001 - bcs :+ + ; 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 #$0010 + bcs :+ + jmp @fail4 + : + cmp #$0041 bcc :+ - jmp @fail2 + jmp @fail4 : bra @pass @timeout: @@ -22859,19 +23294,33 @@ CATALOG_IMPL = 1 sta f:$7EE010 jml test_restore @fail1: - ; timer 0 advanced while TEST bit 0 was set, so the halt bit is being modelled as ordinary storage rather than as a control + ; 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: - ; timer 0 did not advance with TEST back at its reset value, so the halted reading above says nothing about the halt bit + ; 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" @@ -32646,6 +33095,33 @@ apu_prog_77: .byte $5A, $C4, $F4, $E4, $F4, $68, $A5, $D0, $FA, $E8, $80, $C4 .byte $F1, $5F, $C0, $FF apu_prog_78: + .byte $5F, $0C, $02, $C7, $77, $77, $77, $77, $77, $77, $77, $77 + .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 $20, $C4, $F3, $E8, $5C, $C4, $F2, $E8, $00, $C4, $F3, $E8 + .byte $3D, $C4, $F2, $E8, $00, $C4, $F3, $E8, $4D, $C4, $F2, $E8 + .byte $00, $C4, $F3, $E8, $2D, $C4, $F2, $E8, $00, $C4, $F3, $E8 + .byte $5D, $C4, $F2, $E8, $01, $C4, $F3, $E8, $0C, $C4, $F2, $E8 + .byte $7F, $C4, $F3, $E8, $1C, $C4, $F2, $E8, $7F, $C4, $F3, $E8 + .byte $00, $C4, $F2, $E8, $7F, $C4, $F3, $E8, $01, $C4, $F2, $E8 + .byte $7F, $C4, $F3, $E8, $02, $C4, $F2, $E8, $00, $C4, $F3, $E8 + .byte $03, $C4, $F2, $E8, $10, $C4, $F3, $E8, $04, $C4, $F2, $E8 + .byte $00, $C4, $F3, $E8, $06, $C4, $F2, $E8, $00, $C4, $F3, $E8 + .byte $07, $C4, $F2, $E8, $7F, $C4, $F3, $E8, $05, $C4, $F2, $E8 + .byte $00, $C4, $F3, $E8, $7C, $C4, $F2, $E8, $00, $C4, $F3, $E8 + .byte $4C, $C4, $F2, $E8, $01, $C4, $F3, $8D, $00, $FE, $FE, $E8 + .byte $4C, $C4, $F2, $E8, $00, $C4, $F3, $8D, $00, $FE, $FE, $8D + .byte $00, $FE, $FE, $8D, $00, $FE, $FE, $8D, $00, $FE, $FE, $8D + .byte $00, $FE, $FE, $8D, $00, $FE, $FE, $8F, $00, $12, $8F, $00 + .byte $13, $8F, $00, $14, $E8, $09, $C4, $F2, $E4, $F3, $C4, $15 + .byte $68, $80, $E4, $12, $88, $00, $C4, $12, $E4, $15, $68, $01 + .byte $E4, $13, $88, $00, $C4, $13, $AB, $14, $E4, $14, $68, $40 + .byte $D0, $E2, $E4, $12, $C4, $F5, $E4, $13, $C4, $F6, $E4, $15 + .byte $C4, $F7, $E8, $5A, $C4, $F4, $E4, $F4, $68, $A5, $D0, $FA + .byte $E8, $80, $C4, $F1, $5F, $C0, $FF +apu_prog_79: .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 @@ -32671,7 +33147,7 @@ apu_prog_78: .byte $00, $FE, $FE, $E8, $7C, $C4, $F2, $E4, $F3, $C4, $F6, $E8 .byte $5C, $C4, $F2, $E8, $00, $C4, $F3, $E8, $5A, $C4, $F4, $E4 .byte $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF -apu_prog_79: +apu_prog_80: .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 @@ -32695,7 +33171,7 @@ apu_prog_79: .byte $F3, $C4, $F6, $E8, $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8 .byte $5A, $C4, $F4, $E4, $F4, $68, $A5, $D0, $FA, $E8, $80, $C4 .byte $F1, $5F, $C0, $FF -apu_prog_80: +apu_prog_81: .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 @@ -32718,7 +33194,7 @@ apu_prog_80: .byte $F3, $C4, $F6, $E8, $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8 .byte $5A, $C4, $F4, $E4, $F4, $68, $A5, $D0, $FA, $E8, $80, $C4 .byte $F1, $5F, $C0, $FF -apu_prog_81: +apu_prog_82: .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 @@ -32739,7 +33215,7 @@ apu_prog_81: .byte $FF, $FE, $FE, $8D, $FF, $FE, $FE, $E8, $08, $C4, $F2, $E4 .byte $F3, $C4, $F6, $E8, $5A, $C4, $F4, $E4, $F4, $68, $A5, $D0 .byte $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF -apu_prog_82: +apu_prog_83: .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 @@ -32763,7 +33239,7 @@ apu_prog_82: .byte $F3, $C4, $F6, $E8, $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8 .byte $5A, $C4, $F4, $E4, $F4, $68, $A5, $D0, $FA, $E8, $80, $C4 .byte $F1, $5F, $C0, $FF -apu_prog_83: +apu_prog_84: .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 @@ -32785,7 +33261,7 @@ apu_prog_83: .byte $F3, $C4, $F5, $E8, $08, $C4, $F2, $E4, $F3, $C4, $F6, $E8 .byte $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8, $5A, $C4, $F4, $E4 .byte $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF -apu_prog_84: +apu_prog_85: .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 @@ -32807,7 +33283,7 @@ apu_prog_84: .byte $F3, $C4, $F5, $E8, $08, $C4, $F2, $E4, $F3, $C4, $F6, $E8 .byte $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8, $5A, $C4, $F4, $E4 .byte $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF -apu_prog_85: +apu_prog_86: .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 @@ -32832,7 +33308,7 @@ apu_prog_85: .byte $F3, $C4, $F6, $E8, $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8 .byte $5A, $C4, $F4, $E4, $F4, $68, $A5, $D0, $FA, $E8, $80, $C4 .byte $F1, $5F, $C0, $FF -apu_prog_86: +apu_prog_87: .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 @@ -32857,7 +33333,7 @@ apu_prog_86: .byte $F3, $C4, $F6, $E8, $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8 .byte $5A, $C4, $F4, $E4, $F4, $68, $A5, $D0, $FA, $E8, $80, $C4 .byte $F1, $5F, $C0, $FF -apu_prog_87: +apu_prog_88: .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 @@ -32887,7 +33363,7 @@ apu_prog_87: .byte $F3, $C4, $F5, $E8, $08, $C4, $F2, $E4, $F3, $C4, $F6, $E8 .byte $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8, $5A, $C4, $F4, $E4 .byte $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF -apu_prog_88: +apu_prog_89: .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 @@ -32917,7 +33393,7 @@ apu_prog_88: .byte $F3, $C4, $F5, $E8, $08, $C4, $F2, $E4, $F3, $C4, $F6, $E8 .byte $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8, $5A, $C4, $F4, $E4 .byte $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF -apu_prog_89: +apu_prog_90: .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 @@ -32947,7 +33423,7 @@ apu_prog_89: .byte $F3, $C4, $F5, $E8, $08, $C4, $F2, $E4, $F3, $C4, $F6, $E8 .byte $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8, $5A, $C4, $F4, $E4 .byte $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF -apu_prog_90: +apu_prog_91: .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 @@ -32977,7 +33453,7 @@ apu_prog_90: .byte $F3, $C4, $F5, $E8, $08, $C4, $F2, $E4, $F3, $C4, $F6, $E8 .byte $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8, $5A, $C4, $F4, $E4 .byte $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF -apu_prog_91: +apu_prog_92: .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 @@ -33007,7 +33483,7 @@ apu_prog_91: .byte $F3, $C4, $F5, $E8, $08, $C4, $F2, $E4, $F3, $C4, $F6, $E8 .byte $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8, $5A, $C4, $F4, $E4 .byte $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF -apu_prog_92: +apu_prog_93: .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 @@ -33037,7 +33513,7 @@ apu_prog_92: .byte $F3, $C4, $F5, $E8, $08, $C4, $F2, $E4, $F3, $C4, $F6, $E8 .byte $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8, $5A, $C4, $F4, $E4 .byte $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF -apu_prog_93: +apu_prog_94: .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 @@ -33061,7 +33537,7 @@ apu_prog_93: .byte $F3, $C4, $F6, $E8, $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8 .byte $5A, $C4, $F4, $E4, $F4, $68, $A5, $D0, $FA, $E8, $80, $C4 .byte $F1, $5F, $C0, $FF -apu_prog_94: +apu_prog_95: .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 @@ -33085,7 +33561,7 @@ apu_prog_94: .byte $F3, $C4, $F6, $E8, $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8 .byte $5A, $C4, $F4, $E4, $F4, $68, $A5, $D0, $FA, $E8, $80, $C4 .byte $F1, $5F, $C0, $FF -apu_prog_95: +apu_prog_96: .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 @@ -33115,7 +33591,7 @@ apu_prog_95: .byte $F3, $C4, $F5, $E8, $08, $C4, $F2, $E4, $F3, $C4, $F6, $E8 .byte $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8, $5A, $C4, $F4, $E4 .byte $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF -apu_prog_96: +apu_prog_97: .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 @@ -33145,7 +33621,7 @@ apu_prog_96: .byte $F3, $C4, $F5, $E8, $08, $C4, $F2, $E4, $F3, $C4, $F6, $E8 .byte $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8, $5A, $C4, $F4, $E4 .byte $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF -apu_prog_97: +apu_prog_98: .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 @@ -33173,7 +33649,74 @@ apu_prog_97: .byte $F3, $C4, $F5, $E8, $08, $C4, $F2, $E4, $F3, $C4, $F6, $E8 .byte $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8, $5A, $C4, $F4, $E4 .byte $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF -apu_prog_98: +apu_prog_99: + .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 $6C, $C4, $F2, $E8, $20, $C4, $F3, $E8, $3D, $C4, $F2, $E8 + .byte $00, $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, $02, $C4, $F2, $E8 + .byte $00, $C4, $F3, $E8, $03, $C4, $F2, $E8, $10, $C4, $F3, $E8 + .byte $04, $C4, $F2, $E8, $00, $C4, $F3, $E8, $06, $C4, $F2, $E8 + .byte $00, $C4, $F3, $E8, $07, $C4, $F2, $E8, $7F, $C4, $F3, $E8 + .byte $05, $C4, $F2, $E8, $00, $C4, $F3, $E8, $12, $C4, $F2, $E8 + .byte $00, $C4, $F3, $E8, $13, $C4, $F2, $E8, $10, $C4, $F3, $E8 + .byte $14, $C4, $F2, $E8, $00, $C4, $F3, $E8, $16, $C4, $F2, $E8 + .byte $00, $C4, $F3, $E8, $17, $C4, $F2, $E8, $7F, $C4, $F3, $E8 + .byte $15, $C4, $F2, $E8, $00, $C4, $F3, $E8, $22, $C4, $F2, $E8 + .byte $00, $C4, $F3, $E8, $23, $C4, $F2, $E8, $10, $C4, $F3, $E8 + .byte $24, $C4, $F2, $E8, $00, $C4, $F3, $E8, $26, $C4, $F2, $E8 + .byte $00, $C4, $F3, $E8, $27, $C4, $F2, $E8, $7F, $C4, $F3, $E8 + .byte $25, $C4, $F2, $E8, $00, $C4, $F3, $E8, $32, $C4, $F2, $E8 + .byte $00, $C4, $F3, $E8, $33, $C4, $F2, $E8, $10, $C4, $F3, $E8 + .byte $34, $C4, $F2, $E8, $00, $C4, $F3, $E8, $36, $C4, $F2, $E8 + .byte $00, $C4, $F3, $E8, $37, $C4, $F2, $E8, $7F, $C4, $F3, $E8 + .byte $35, $C4, $F2, $E8, $00, $C4, $F3, $E8, $42, $C4, $F2, $E8 + .byte $00, $C4, $F3, $E8, $43, $C4, $F2, $E8, $10, $C4, $F3, $E8 + .byte $44, $C4, $F2, $E8, $00, $C4, $F3, $E8, $46, $C4, $F2, $E8 + .byte $00, $C4, $F3, $E8, $47, $C4, $F2, $E8, $7F, $C4, $F3, $E8 + .byte $45, $C4, $F2, $E8, $00, $C4, $F3, $E8, $52, $C4, $F2, $E8 + .byte $00, $C4, $F3, $E8, $53, $C4, $F2, $E8, $10, $C4, $F3, $E8 + .byte $54, $C4, $F2, $E8, $00, $C4, $F3, $E8, $56, $C4, $F2, $E8 + .byte $00, $C4, $F3, $E8, $57, $C4, $F2, $E8, $7F, $C4, $F3, $E8 + .byte $55, $C4, $F2, $E8, $00, $C4, $F3, $E8, $62, $C4, $F2, $E8 + .byte $00, $C4, $F3, $E8, $63, $C4, $F2, $E8, $10, $C4, $F3, $E8 + .byte $64, $C4, $F2, $E8, $00, $C4, $F3, $E8, $66, $C4, $F2, $E8 + .byte $00, $C4, $F3, $E8, $67, $C4, $F2, $E8, $7F, $C4, $F3, $E8 + .byte $65, $C4, $F2, $E8, $00, $C4, $F3, $E8, $72, $C4, $F2, $E8 + .byte $00, $C4, $F3, $E8, $73, $C4, $F2, $E8, $10, $C4, $F3, $E8 + .byte $74, $C4, $F2, $E8, $00, $C4, $F3, $E8, $76, $C4, $F2, $E8 + .byte $00, $C4, $F3, $E8, $77, $C4, $F2, $E8, $7F, $C4, $F3, $E8 + .byte $75, $C4, $F2, $E8, $00, $C4, $F3, $E8, $4C, $C4, $F2, $E8 + .byte $00, $C4, $F3, $E8, $5C, $C4, $F2, $E8, $FF, $C4, $F3, $8D + .byte $00, $FE, $FE, $8D, $00, $FE, $FE, $8D, $00, $FE, $FE, $8D + .byte $00, $FE, $FE, $8D, $00, $FE, $FE, $8D, $00, $FE, $FE, $E8 + .byte $5C, $C4, $F2, $E8, $00, $C4, $F3, $E8, $4C, $C4, $F2, $E8 + .byte $01, $C4, $F3, $00, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $E8, $02, $C4, $F3, $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $E8, $04, $C4, $F3, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $E8, $08, $C4, $F3, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $E8, $10, $C4, $F3, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $E8, $20, $C4, $F3, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $E8, $40, $C4, $F3, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $E8, $80, $C4, $F3, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $8D, $00, $FE, $FE, $E8 + .byte $4C, $C4, $F2, $E8, $00, $C4, $F3, $8D, $20, $FE, $FE, $8F + .byte $00, $13, $E8, $08, $C4, $F2, $E4, $F3, $68, $01, $E4, $13 + .byte $84, $13, $C4, $13, $E8, $18, $C4, $F2, $E4, $F3, $68, $01 + .byte $E4, $13, $84, $13, $C4, $13, $E8, $28, $C4, $F2, $E4, $F3 + .byte $68, $01, $E4, $13, $84, $13, $C4, $13, $E8, $38, $C4, $F2 + .byte $E4, $F3, $68, $01, $E4, $13, $84, $13, $C4, $13, $E8, $48 + .byte $C4, $F2, $E4, $F3, $68, $01, $E4, $13, $84, $13, $C4, $13 + .byte $E8, $58, $C4, $F2, $E4, $F3, $68, $01, $E4, $13, $84, $13 + .byte $C4, $13, $E8, $68, $C4, $F2, $E4, $F3, $68, $01, $E4, $13 + .byte $84, $13, $C4, $13, $E8, $78, $C4, $F2, $E4, $F3, $68, $01 + .byte $E4, $13, $84, $13, $C4, $13, $E4, $13, $C4, $F5, $E8, $5A + .byte $C4, $F4, $E4, $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1 + .byte $5F, $C0, $FF +apu_prog_100: .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 @@ -33201,7 +33744,7 @@ apu_prog_98: .byte $80, $F1, $E4, $FF, $C4, $F6, $E8, $4C, $C4, $F2, $E8, $00 .byte $C4, $F3, $E8, $5A, $C4, $F4, $E4, $F4, $68, $A5, $D0, $FA .byte $E8, $80, $C4, $F1, $5F, $C0, $FF -apu_prog_99: +apu_prog_101: .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 @@ -33232,7 +33775,7 @@ apu_prog_99: .byte $F3, $C4, $F6, $E8, $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8 .byte $5A, $C4, $F4, $E4, $F4, $68, $A5, $D0, $FA, $E8, $80, $C4 .byte $F1, $5F, $C0, $FF -apu_prog_100: +apu_prog_102: .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 @@ -33263,7 +33806,7 @@ apu_prog_100: .byte $F3, $C4, $F5, $E8, $08, $C4, $F2, $E4, $F3, $C4, $F6, $E8 .byte $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8, $5A, $C4, $F4, $E4 .byte $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF -apu_prog_101: +apu_prog_103: .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 @@ -33292,7 +33835,7 @@ apu_prog_101: .byte $F3, $C4, $F6, $E8, $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8 .byte $5A, $C4, $F4, $E4, $F4, $68, $A5, $D0, $FA, $E8, $80, $C4 .byte $F1, $5F, $C0, $FF -apu_prog_102: +apu_prog_104: .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 @@ -33317,7 +33860,7 @@ apu_prog_102: .byte $F3, $C4, $F6, $E8, $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8 .byte $5A, $C4, $F4, $E4, $F4, $68, $A5, $D0, $FA, $E8, $80, $C4 .byte $F1, $5F, $C0, $FF -apu_prog_103: +apu_prog_105: .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 @@ -33343,7 +33886,7 @@ apu_prog_103: .byte $08, $C4, $F2, $E4, $F3, $C4, $F6, $E8, $09, $C4, $F2, $E4 .byte $F3, $C4, $F7, $E8, $5A, $C4, $F4, $E4, $F4, $68, $A5, $D0 .byte $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF -apu_prog_104: +apu_prog_106: .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 @@ -33367,7 +33910,7 @@ apu_prog_104: .byte $F3, $C4, $F6, $E8, $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8 .byte $5A, $C4, $F4, $E4, $F4, $68, $A5, $D0, $FA, $E8, $80, $C4 .byte $F1, $5F, $C0, $FF -apu_prog_105: +apu_prog_107: .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 @@ -33391,7 +33934,7 @@ apu_prog_105: .byte $F3, $C4, $F6, $E8, $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8 .byte $5A, $C4, $F4, $E4, $F4, $68, $A5, $D0, $FA, $E8, $80, $C4 .byte $F1, $5F, $C0, $FF -apu_prog_106: +apu_prog_108: .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 @@ -33415,7 +33958,7 @@ apu_prog_106: .byte $F3, $C4, $F6, $E8, $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8 .byte $5A, $C4, $F4, $E4, $F4, $68, $A5, $D0, $FA, $E8, $80, $C4 .byte $F1, $5F, $C0, $FF -apu_prog_107: +apu_prog_109: .byte $5F, $DB, $02, $80, $77, $77, $77, $77, $77, $77, $77, $77 .byte $80, $77, $77, $77, $77, $77, $77, $77, $77, $80, $77, $77 .byte $77, $77, $77, $77, $77, $77, $80, $77, $77, $77, $77, $77 @@ -33456,7 +33999,7 @@ apu_prog_107: .byte $C4, $F5, $E8, $08, $C4, $F2, $E4, $F3, $C4, $F6, $E8, $09 .byte $C4, $F2, $E4, $F3, $C4, $F7, $E8, $5A, $C4, $F4, $E4, $F4 .byte $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF -apu_prog_108: +apu_prog_110: .byte $5F, $DB, $02, $80, $77, $77, $77, $77, $77, $77, $77, $77 .byte $80, $77, $77, $77, $77, $77, $77, $77, $77, $80, $77, $77 .byte $77, $77, $77, $77, $77, $77, $80, $77, $77, $77, $77, $77 @@ -33501,7 +34044,7 @@ apu_prog_108: .byte $C4, $F6, $E8, $09, $C4, $F2, $E4, $F3, $C4, $F7, $E8, $5A .byte $C4, $F4, $E4, $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1 .byte $5F, $C0, $FF -apu_prog_109: +apu_prog_111: .byte $5F, $DB, $02, $80, $77, $77, $77, $77, $77, $77, $77, $77 .byte $80, $77, $77, $77, $77, $77, $77, $77, $77, $80, $77, $77 .byte $77, $77, $77, $77, $77, $77, $80, $77, $77, $77, $77, $77 @@ -33542,7 +34085,7 @@ apu_prog_109: .byte $C4, $F5, $E8, $08, $C4, $F2, $E4, $F3, $C4, $F6, $E8, $09 .byte $C4, $F2, $E4, $F3, $C4, $F7, $E8, $5A, $C4, $F4, $E4, $F4 .byte $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF -apu_prog_110: +apu_prog_112: .byte $5F, $DB, $02, $80, $77, $77, $77, $77, $77, $77, $77, $77 .byte $80, $77, $77, $77, $77, $77, $77, $77, $77, $80, $77, $77 .byte $77, $77, $77, $77, $77, $77, $80, $77, $77, $77, $77, $77 @@ -33582,17 +34125,44 @@ apu_prog_110: .byte $C4, $F5, $E8, $08, $C4, $F2, $E4, $F3, $C4, $F6, $E8, $09 .byte $C4, $F2, $E4, $F3, $C4, $F7, $E8, $5A, $C4, $F4, $E4, $F4 .byte $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF -apu_prog_111: +apu_prog_113: .byte $CD, $EF, $BD, $8F, $01, $FA, $8F, $01, $FC, $E4, $FD, $E4 .byte $FF, $8F, $85, $F1, $8D, $18, $FE, $FE, $8F, $80, $F1, $E4 .byte $FD, $C4, $F6, $E4, $FF, $C4, $F7, $E8, $5A, $C4, $F4, $E4 .byte $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF -apu_prog_112: +apu_prog_114: .byte $CD, $EF, $BD, $8F, $01, $FA, $8F, $0B, $F0, $8F, $81, $F1 .byte $8D, $00, $FE, $FE, $8F, $80, $F1, $E4, $FD, $C4, $F6, $8F .byte $0A, $F0, $8F, $81, $F1, $8D, $00, $FE, $FE, $8F, $80, $F1 .byte $E4, $FD, $C4, $F7, $E8, $5A, $C4, $F4, $E4, $F4, $68, $A5 .byte $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF +apu_prog_115: + .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 @@ -33600,7 +34170,7 @@ apu_prog_112: .export _test_flags _test_count: - .word 335 + .word 338 ; Entry points, 24-bit: test bodies no longer all live in bank $00. _test_entries: @@ -33854,6 +34424,7 @@ _test_entries: .faraddr test_e5_03 .faraddr test_e5_04 .faraddr test_e5_05 + .faraddr test_e5_06 .faraddr test_e5_13 .faraddr test_e7_01 .faraddr test_e7_13 @@ -33867,6 +34438,7 @@ _test_entries: .faraddr test_e7_03 .faraddr test_e7_12 .faraddr test_e7_08 + .faraddr test_e8_01 .faraddr test_e8_02 .faraddr test_e8_03 .faraddr test_e8_07 @@ -33880,6 +34452,7 @@ _test_entries: .faraddr test_e6_02d .faraddr test_e3_06 .faraddr test_e3_08 + .faraddr test_e9_02 .faraddr test_f1_01 .faraddr test_f1_02 .faraddr test_f1_03 @@ -34192,6 +34765,7 @@ _test_flags: .byte $01 ; E5.03 .byte $01 ; E5.04 .byte $01 ; E5.05 + .byte $01 ; E5.06 .byte $01 ; E5.13 .byte $01 ; E7.01 .byte $01 ; E7.13 @@ -34205,6 +34779,7 @@ _test_flags: .byte $01 ; E7.03 .byte $02 ; E7.12 .byte $01 ; E7.08 + .byte $01 ; E8.01 .byte $01 ; E8.02 .byte $01 ; E8.03 .byte $02 ; E8.07 @@ -34218,6 +34793,7 @@ _test_flags: .byte $01 ; E6.02d .byte $01 ; E3.06 .byte $01 ; E3.08 + .byte $01 ; E9.02 .byte $01 ; F1.01 .byte $01 ; F1.02 .byte $01 ; F1.03 @@ -34530,6 +35106,7 @@ _test_names: .addr @n_e5_03 .addr @n_e5_04 .addr @n_e5_05 + .addr @n_e5_06 .addr @n_e5_13 .addr @n_e7_01 .addr @n_e7_13 @@ -34543,6 +35120,7 @@ _test_names: .addr @n_e7_03 .addr @n_e7_12 .addr @n_e7_08 + .addr @n_e8_01 .addr @n_e8_02 .addr @n_e8_03 .addr @n_e8_07 @@ -34556,6 +35134,7 @@ _test_names: .addr @n_e6_02d .addr @n_e3_06 .addr @n_e3_08 + .addr @n_e9_02 .addr @n_f1_01 .addr @n_f1_02 .addr @n_f1_03 @@ -35365,6 +35944,9 @@ _test_names: @n_e5_05: .byte 12 .byte "BRR filter 1" +@n_e5_06: + .byte 20 + .byte "BRR wraps at 15 bits" @n_e5_13: .byte 22 .byte "Released voices decode" @@ -35404,6 +35986,9 @@ _test_names: @n_e7_08: .byte 24 .byte "Key-off releases to zero" +@n_e8_01: + .byte 22 + .byte "KON examined at 16 kHz" @n_e8_02: .byte 22 .byte "Key-on takes 5 samples" @@ -35443,6 +36028,9 @@ _test_names: @n_e3_08: .byte 23 .byte "TEST bit 0 halts timers" +@n_e9_02: + .byte 23 + .byte "Noise output is bipolar" @n_f1_01: .byte 21 .byte "Manual pad read order" @@ -35879,12 +36467,12 @@ _page_len: .byte 6 .byte 2 .byte 10 - .byte 2 + .byte 3 .byte 10 - .byte 2 + .byte 3 .byte 10 .byte 7 - .byte 6 + .byte 7 .byte 10 .byte 3 .byte 10 @@ -35933,15 +36521,15 @@ _page_off: .word 262 .word 264 .word 274 - .word 276 - .word 286 - .word 288 - .word 298 - .word 305 - .word 311 - .word 321 + .word 277 + .word 287 + .word 290 + .word 300 + .word 307 + .word 314 .word 324 - .word 334 + .word 327 + .word 337 _page_tests: .word 0 @@ -35994,9 +36582,6 @@ _page_tests: .word 69 .word 70 .word 71 - .word 300 - .word 301 - .word 302 .word 303 .word 304 .word 305 @@ -36029,6 +36614,9 @@ _page_tests: .word 332 .word 333 .word 334 + .word 335 + .word 336 + .word 337 .word 30 .word 31 .word 32 @@ -36189,8 +36777,8 @@ _page_tests: .word 217 .word 218 .word 219 - .word 274 - .word 275 + .word 276 + .word 277 .word 192 .word 193 .word 199 @@ -36202,10 +36790,10 @@ _page_tests: .word 226 .word 194 .word 195 - .word 270 - .word 271 .word 272 .word 273 + .word 274 + .word 275 .word 196 .word 197 .word 202 @@ -36220,6 +36808,7 @@ _page_tests: .word 244 .word 245 .word 246 + .word 278 .word 205 .word 206 .word 207 @@ -36232,9 +36821,9 @@ _page_tests: .word 248 .word 249 .word 250 + .word 251 .word 211 .word 235 - .word 251 .word 252 .word 253 .word 254 @@ -36246,18 +36835,17 @@ _page_tests: .word 260 .word 261 .word 262 - .word 267 - .word 268 + .word 263 .word 269 + .word 270 + .word 271 .word 236 .word 237 - .word 263 .word 264 .word 265 .word 266 - .word 276 - .word 277 - .word 278 + .word 267 + .word 268 .word 279 .word 280 .word 281 @@ -36279,3 +36867,6 @@ _page_tests: .word 297 .word 298 .word 299 + .word 300 + .word 301 + .word 302 diff --git a/tests/roms/AccuracySNES/build/accuracysnes-pal.sfc b/tests/roms/AccuracySNES/build/accuracysnes-pal.sfc index 1c4c68e6..d971aa52 100644 Binary files a/tests/roms/AccuracySNES/build/accuracysnes-pal.sfc and b/tests/roms/AccuracySNES/build/accuracysnes-pal.sfc differ diff --git a/tests/roms/AccuracySNES/build/accuracysnes.sfc b/tests/roms/AccuracySNES/build/accuracysnes.sfc index 67252897..d4c27f52 100644 Binary files a/tests/roms/AccuracySNES/build/accuracysnes.sfc and b/tests/roms/AccuracySNES/build/accuracysnes.sfc differ diff --git a/tests/roms/AccuracySNES/gen/src/dossier.rs b/tests/roms/AccuracySNES/gen/src/dossier.rs index 672dd386..6b34cfe6 100644 --- a/tests/roms/AccuracySNES/gen/src/dossier.rs +++ b/tests/roms/AccuracySNES/gen/src/dossier.rs @@ -192,6 +192,7 @@ pub const MAP: &[(&str, &[&str])] = &[ ("G1.07", &["G1.07"]), ("E9.03", &["E9.03"]), ("E9.01", &["E9.01"]), + ("E9.02", &["E9.02"]), ("E7.04", &["E7.04"]), ("E7.13", &["E7.13"]), ("E7.17", &["E7.17"]), @@ -225,6 +226,7 @@ pub const MAP: &[(&str, &[&str])] = &[ ("F1.12", &["F1.12"]), ("C11.07", &["C11.07"]), ("C11.08", &["C11.08"]), + ("E8.01", &["E8.01"]), ("E8.02", &["E8.02"]), ("B2.01", &["B2.01"]), ("E9.05", &["E9.05"]), @@ -321,6 +323,7 @@ pub const MAP: &[(&str, &[&str])] = &[ ("E5.03", &["E5.03"]), ("E5.04", &["E5.04"]), ("E5.05", &["E5.05"]), + ("E5.06", &["E5.06"]), ("E5.13", &["E5.13"]), ("E7.14", &["E7.14"]), ("E5.07", &["E5.07"]), diff --git a/tests/roms/AccuracySNES/gen/src/tests/apu.rs b/tests/roms/AccuracySNES/gen/src/tests/apu.rs index d1a90ce7..fbc0b98b 100644 --- a/tests/roms/AccuracySNES/gen/src/tests/apu.rs +++ b/tests/roms/AccuracySNES/gen/src/tests/apu.rs @@ -37,6 +37,10 @@ fn next_prog_id() -> usize { } /// Every Group E test, in menu order. +// One line per registered test, and the ORDER is load-bearing in three places (see the comments +// inside): splitting it to satisfy the length lint would put those constraints on two sides of a +// function boundary, which is exactly where an ordering rule stops being visible. +#[allow(clippy::too_many_lines)] #[must_use] pub fn all() -> Vec { vec![ @@ -114,6 +118,7 @@ pub fn all() -> Vec { e5_03(), e5_04(), e5_05(), + e5_06(), e5_13(), e7_01(), e7_13(), @@ -127,6 +132,7 @@ pub fn all() -> Vec { e7_03(), e7_12(), e7_08(), + e8_01(), e8_02(), e8_03(), e8_07(), @@ -140,6 +146,10 @@ pub fn all() -> Vec { e6_02d(), e3_06(), e3_08(), + // 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. + e9_02(), ] } @@ -318,6 +328,26 @@ fn upload_2block_and_run(a: &mut Asm, data: &[u8], dest_a: u16, prog: &Spc, dest a.l("sta APUIO0"); } +/// Emit: stand down as SKIP unless this is a cold boot. +/// +/// The noise LFSR is seeded once at power-on and there is no way to put it back — `FLG` bit 7 is +/// the DSP's soft reset and it does **not** touch the shift register (checked against ares' +/// `DSP::power`, which re-seeds only on a console reset). Until `E9.02` existed no program in the +/// battery advanced it, so the seed was readable at any point; `E9.02` advances it by design, and +/// a menu restart re-runs the battery without a power-on. So the two rows that need the seed stand +/// down on a restart rather than reporting a failure that only means "this was not a cold boot" — +/// the same mechanism, and the same reasoning, as `F1.07`'s power-on `$4218`. +/// +/// **Leaves `A`/`X`/`Y` 16-bit**, matching what `upload_and_run` expects on entry. +fn apu_require_power_on(a: &mut Asm, why: &str) { + a.l("sep #$20"); + a.l("lda f:V_RESTARTED"); + a.l("beq :+"); + a.skip(why); + a.l(":"); + a.l("rep #$30"); +} + /// Emit the shared tail: jump past the timeout arm, then land where `finish`'s pass stub follows. /// /// Every test in this group needs it because `upload_and_run` branches to `@timeout` when the APU @@ -1498,12 +1528,15 @@ fn e5_01() -> Test { /// bits clear the register is seeded and then never advances, and the voice's output is a direct /// function of the seed for as long as anyone cares to look. /// -/// Every program in this group leaves `FLG` at `$20`, so the noise rate is zero throughout the -/// battery and the LFSR never advances *anywhere*. What this test reads is therefore the -/// **power-on** seed. The `flg_reset` in its configuration is harmless and correct in intent, but -/// it does no work here: RustySNES's `$6C` write sets the mute and reset flags without touching the -/// shift register, so whether a soft reset re-seeds it is a separate question this test does not -/// answer. +/// Every program in this group leaves `FLG` at `$20` except one, so the noise rate is zero almost +/// everywhere and the LFSR does not advance. What this test reads is therefore the **power-on** +/// seed. The `flg_reset` in its configuration is harmless but does no work: the `$6C` write sets +/// the mute and reset flags without touching the shift register — checked against ares, which +/// re-seeds only in `DSP::power`. +/// +/// The one exception is `E9.02`, which steps the register on purpose and is registered last for +/// that reason. Nothing can put it back, so on a menu restart this row stands down as SKIP; see +/// [`apu_require_power_on`]. /// /// The arithmetic checks out exactly: `$4000` shifted left one is `$8000`, scaled by the direct /// gain's `$7F0` envelope gives `$8100`, and `VxOUTX` is that value's top byte — `$81`, which is @@ -1531,6 +1564,10 @@ fn e9_01() -> Test { ); let mut a = Asm::new(); + apu_require_power_on( + &mut a, + "the seed this reads is the power-on one, and E9.02 steps the register away from it", + ); upload_and_run(&mut a, &prog); a.l("rep #$30"); a.l("lda f:$7E0102"); @@ -1569,6 +1606,145 @@ fn e9_01() -> Test { ) } +/// `E9.02` — the noise output is the 15-bit LFSR placed in the **top** 15 bits of a signed word, +/// which is what makes it highpass rather than a DC-heavy hiss `[ERRATA]`. +/// +/// # The claim, and the wrong implementation it excludes +/// +/// The shift register is 15 bits, so its value is always in `$0000`-`$7FFF` — a *unipolar* number. +/// The DSP does not use it that way: the output is `LFSR << 1` reinterpreted as signed 16-bit, so +/// **LFSR bit 14 becomes the sign bit**. That single shift is the whole of the errata's "highpass" +/// remark: a core that emitted the register's value directly would produce noise with an enormous +/// positive DC offset, audible as a click on every key-on and a bias on every mix. +/// +/// # A step, not a state +/// +/// `E9.01` already reads the frozen seed and pins it at `$4000`. Repeating that here would score +/// the same observation twice, so this row scores the **transition**: one step of the register must +/// turn a full-scale *negative* reading into a *positive* one at about half the magnitude. +/// +/// | LFSR | as `LFSR << 1` | `VxOUTX` | +/// |---|---|---:| +/// | `$4000` (the seed) | `$8000`, full-scale negative | `$81` | +/// | `$2000` (one step) | `$4000`, half-scale positive | `$3F` | +/// | `$1000` (two steps) | `$2000` | `$1F` | +/// +/// A unipolar core reads `$3F` then `$1F` — positive throughout, no sign change. A core that +/// centred the register by subtracting `$4000` reads ~`$00` then *negative*. Only the shift gives +/// negative-then-positive. +/// +/// # Stepping the register exactly, without knowing the phase +/// +/// The rate comes from `FLG` bits 0-4 through the envelope counter table, and rate 0 never fires +/// (`E7.01`) — which is why the seed is observable at all. Rate 19 fires every **32 output +/// samples**, and the register is enabled for one `delay` block: 1536 SPC cycles, **48 samples**. +/// A half-open window of 48 samples contains one or two multiples of 32 whatever the phase, so the +/// step count is 1 or 2 and never 0. **Both outcomes are in the asserted band** (`$3F` and `$1F`), +/// which is the point: this row must not repeat the mistake the two rejected `E8.01` drafts made of +/// asserting something a phase the cart cannot control gets to decide. +/// +/// # It must run last, and this is the only row in the battery with that property +/// +/// Every other program leaves `FLG` at `$20`, so the LFSR never advances anywhere and `E9.01` reads +/// the **power-on** seed. This row advances it, and nothing can put it back — RustySNES's `$6C` +/// write sets the reset and mute flags without touching the shift register. The soft reset at the +/// end is a courtesy to cores that *do* re-seed on it, not something this row relies on. It is +/// registered last for that reason; see the note at the registration site. +fn e9_02() -> Test { + let mut p = voice_setup( + &looping_sample(), + Voice { + non: 0x01, // noise mode: the voice's output is the LFSR, not the sample + flg_reset: true, // seed it; FLG's noise rate is still 0, so it does not advance + settle: 6, + ..Voice::direct_gain() + }, + ); + + dsp_read_to(&mut p, 0x09, PORT1); // OUTX with the register still at its seed + + // One step, or two. Rate 19 (`$13`) fires every 32 output samples; one delay block is 48 of + // them, and a half-open 48-sample window holds one or two multiples of 32 whatever the phase. + dsp_write(&mut p, 0x6C, 0x20 | 0x13); + p.delay(0x00); + dsp_write(&mut p, 0x6C, 0x20); // frozen again, so the read below is of a settled value + dsp_read_to(&mut p, 0x09, PORT2); + + // Courtesy only -- see the doc comment. A core that re-seeds on FLG.7 gets its LFSR back. + dsp_write(&mut p, 0x6C, 0xE0); + dsp_write(&mut p, 0x6C, 0x20); + p.mov_a_imm(DONE).mov_dp_a(PORT0).release_to_ipl(); + + let mut a = Asm::new(); + apu_require_power_on( + &mut a, + "the sign flip is only predictable from the $4000 seed, which this row itself consumed", + ); + upload_and_run(&mut a, &p); + a.l("rep #$30"); + a.l("lda f:$7E0100"); + a.l("and #$00FF"); + a.record(258, "E9.02 noise OUTX with the LFSR frozen at its seed"); + a.l("lda f:$7E0101"); + a.l("and #$00FF"); + a.record(259, "E9.02 noise OUTX one LFSR step later"); + + a.c("Two guards before the row's own assertions. The first fixes the starting point: this is"); + a.c("E9.01's reading, and without it there is no known state to have stepped away from."); + a.l("sep #$20"); + a.l("lda f:$7E0100"); + a.l("and #$80"); + a.assert_a8( + 0x80, + "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", + ); + a.c("The second: the register has to have actually moved. A 48-sample window cannot contain"); + a.c("zero multiples of 32, so this failing means the rate table or the counter is wrong, not"); + a.c("that the noise output is shaped wrongly."); + a.l("lda f:$7E0100"); + a.l("cmp f:$7E0101"); + a.fail_if_eq( + "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", + ); + + a.c("The row. One step right must move bit 14 out of the sign position, so a full-scale"); + a.c("negative reading becomes positive."); + a.l("lda f:$7E0101"); + a.l("and #$80"); + a.assert_a8( + 0x00, + "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", + ); + a.c("And at about half scale, or a quarter if the window caught two steps -- both are what a"); + a.c("right shift produces, and neither is what a core that merely re-centred the value gives."); + a.l("rep #$30"); + a.l("lda f:$7E0101"); + a.l("and #$00FF"); + a.assert_a16_range( + 0x10, + 0x40, + "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", + ); + apu_timeout_arm(&mut a); + a.finish( + "E9.02", + 'E', + "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, + None, + ) +} + /// GAIN bent-increase climbs at two slopes, breaking at `$600`, and it reads the *clipped* envelope. /// /// GAIN mode 7 is a two-slope linear increase: the envelope climbs `+32` per sample until it reaches @@ -3439,6 +3615,26 @@ impl Voice { } fn voice_program(sample: &[u8], v: Voice) -> Spc { + let mut p = voice_setup(sample, v); + dsp_read_to(&mut p, 0x7C, PORT1); // ENDX + dsp_read_to(&mut p, 0x08, PORT2); // voice 0 ENVX + dsp_read_to(&mut p, 0x09, PORT3); // voice 0 OUTX + p.mov_a_imm(DONE).mov_dp_a(PORT0).release_to_ipl(); + p +} + +/// [`voice_program`] without the reporting tail, for a test that measures something else. +/// +/// The three fixed reads above are the right shape for a row whose answer is one register at one +/// moment, which is most of them. `E5.06` and `E9.02` are not: one needs `VxOUTX` sampled *many* +/// times because the value it is about does not hold still, and the other needs two readings with a +/// register write between them. Both want everything up to the key-on and nothing after it, so the +/// split is here rather than duplicated — a second copy of this setup is a second place for the +/// FLG/NON/ADSR-ordering decisions above to drift. +/// +/// The caller owns what follows: it must write its own results to the ports and end with +/// [`Spc::release_to_ipl`], or the next test's upload has no IPL to handshake with. +fn voice_setup(sample: &[u8], v: Voice) -> Spc { let mut p = Spc::new(); let addr = p.data_first(IMAGE_BASE, sample); p.mov_x_imm(0xEF).mov_sp_x(); @@ -3467,8 +3663,13 @@ fn voice_program(sample: &[u8], v: Voice) -> Spc { // power-on value has set. Noise, echo and pitch modulation are cleared explicitly rather than // assumed, since a previous test's program shares the same DSP. if v.flg_reset { - // Bit 7 is the DSP's soft reset, and it re-seeds the noise LFSR to $4000. Written with - // mute and echo-write-disable so nothing is audible while the reset is asserted. + // Bit 7 is the DSP's soft reset: every voice is forced into release with a zero envelope. + // Written with mute and echo-write-disable so nothing is audible while it is asserted. + // + // It does NOT re-seed the noise LFSR, which an earlier version of this comment claimed. + // Checked against ares, where the shift register is re-seeded in `DSP::power` — a console + // reset — and the `$6C` write only sets `mainvol.reset`. `E9.01`/`E9.02` are built on the + // register being seeded once at power-on and never restored; see `apu_require_power_on`. dsp_write(&mut p, 0x6C, 0xE0); // FLG: reset } dsp_write(&mut p, 0x6C, 0x20); // FLG @@ -3517,10 +3718,6 @@ fn voice_program(sample: &[u8], v: Voice) -> Spc { dsp_write(&mut p, reg, val); } - dsp_read_to(&mut p, 0x7C, PORT1); // ENDX - dsp_read_to(&mut p, 0x08, PORT2); // voice 0 ENVX - dsp_read_to(&mut p, 0x09, PORT3); // voice 0 OUTX - p.mov_a_imm(DONE).mov_dp_a(PORT0).release_to_ipl(); p } @@ -5256,6 +5453,341 @@ fn e5_10() -> Test { ) } +/// `E5.06` — a decoded BRR sample is clamped to 16 bits and then **wraps** at 15, losing its sign. +/// +/// `+4000h`-`+7FFFh` becomes `-4000h`-`-1`, and `-8000h`-`-4001h` becomes `0`-`3FFFh`. In code the +/// whole of it is `let stored = (sclamp16(s) << 1) as i16` — the clamp is at sixteen bits, the +/// store is at fifteen, and the bit that falls off is the one carrying the sign. +/// +/// # The first attempt failed, and the reason is the design of this one +/// +/// The obvious test reads `VxOUTX` once after driving a filter past the boundary. It does not work, +/// and the recorded finding is worth more than the attempt: the constant-input trick every other +/// BRR row relies on works because a non-overflowing filter converges on a **fixed point**, so the +/// reading does not depend on which sample the cart caught. Wrapping destroys exactly that. The +/// output becomes a sawtooth over the whole range, and two reference emulators returned `$E1` and +/// `$D0` from the same image — agreeing only that it was negative, which was luck rather than +/// behaviour. **An `OUTX` assertion is only valid where the output is provably stationary.** +/// +/// # So the non-stationarity is the observable +/// +/// The drive here is *purely positive*: filter 1, scale 12, every nibble `+7`. Filter 1 is +/// `s + p₁·15/16`, so a constant input `s` converges on `16s` — at scale 12 that is `14336 · 16`, +/// far above the boundary. Under **either** hypothesis the input never once goes negative: +/// +/// | if the store | the sample sequence | `VxOUTX` over 64 readings | +/// |---|---|---| +/// | clamps instead | pins at the ceiling and stays there | **one sign**, whatever that sign is | +/// | wraps at 15 bits (hardware) | sawtooths through the whole range | **both signs** | +/// +/// So the assertion is that **both signs occur**, and the count of negative readings is recorded +/// rather than pinned. +/// +/// # The obvious assertion was vacuous, and the injection is what showed it +/// +/// The first version asserted only "at least one reading is negative". Injecting the named bug — +/// clamping the store to `-4000h..+3FFFh` instead of letting it wrap — made the row **pass harder**: +/// 64 negative readings out of 64, against the correct decoder's 32. The reason is worth writing +/// down, because it invalidates the intuition that a positive-only drive cannot read back negative: +/// a clamping decoder pins the buffer at `+7FFEh`, and the gaussian interpolator's *three-term +/// partial sum* — the same truncation `E5.13` is about — overflows a signed 16-bit intermediate +/// from that constant, so every reading comes back negative. The interpolator, not the decoder, +/// was supplying the sign. +/// +/// What survives that is the bipolarity. A clamping decoder produces a constant buffer, so whatever +/// the interpolator does with it, it does every sample and the reading has a single sign; only a +/// value that genuinely wraps swings both ways. Measured: **32 of 64** negative on the correct +/// decoder, **64 of 64** with the clamp injected. +/// +/// # Why the loop is 45 cycles and not tighter +/// +/// One pass is a little over an output sample (45 SPC cycles against 32), and deliberately not a +/// whole number of them: a loop that took exactly one sample would step in lockstep with the +/// sawtooth and could sample the same point of it 64 times. The ratio here is 1.41, so 64 readings +/// spread across about 90 output samples at a phase that keeps moving. +fn e5_06() -> Test { + let mut p = voice_setup( + &filtered_sample(12, 1, 7), + Voice { + settle: 6, + ..Voice::direct_gain() + }, + ); + + // $12 counts negative readings, $13 non-zero ones, $14 is the pass counter, $15 stashes the + // reading so the second comparison sees it again. The address latch is loaded once outside the + // loop, so each pass is one `MOV A,dp` against $F3. + p.mov_dp_imm(0x12, 0x00); + p.mov_dp_imm(0x13, 0x00); + p.mov_dp_imm(0x14, 0x00); + p.mov_a_imm(0x09).mov_dp_a(0xF2); // voice 0 OUTX + let pass = p.here(); + p.mov_a_dp(0xF3); + p.mov_dp_a(0x15); + p.cmp_a_imm(0x80); // carry set for a reading with its top bit set + p.mov_a_dp(0x12); + p.adc_a_imm(0x00); + p.mov_dp_a(0x12); + p.mov_a_dp(0x15); + p.cmp_a_imm(0x01); // carry set for any non-zero reading + p.mov_a_dp(0x13); + p.adc_a_imm(0x00); + p.mov_dp_a(0x13); + p.inc_dp(0x14); + p.mov_a_dp(0x14); + p.cmp_a_imm(0x40); + p.bne_back(pass); + p.mov_a_dp(0x12).mov_dp_a(PORT1); + p.mov_a_dp(0x13).mov_dp_a(PORT2); + p.mov_a_dp(0x15).mov_dp_a(PORT3); // the last reading, for a failure to be readable + p.mov_a_imm(DONE).mov_dp_a(PORT0).release_to_ipl(); + + let mut a = Asm::new(); + upload_and_run(&mut a, &p); + a.l("rep #$30"); + a.l("lda f:$7E0100"); + a.l("and #$00FF"); + a.record( + 260, + "E5.06 negative readings out of 64, driving filter 1 with +7 nibbles", + ); + a.l("lda f:$7E0101"); + a.l("and #$00FF"); + a.record( + 261, + "E5.06 non-zero readings out of 64 (the sounding guard)", + ); + a.l("lda f:$7E0102"); + a.l("and #$00FF"); + a.record(262, "E5.06 the last of the 64 readings"); + + a.c("The guard: sixty-four silences would report zero negatives and look like a clean clamp."); + a.l("lda f:$7E0101"); + a.l("and #$00FF"); + a.assert_a16_range( + 1, + 0x40, + "not one of the sixty-four OUTX readings was non-zero, so the voice never sounded and the \ + sign test below is about silence rather than about the decoder", + ); + a.c("The row. A decoder that saturates leaves the buffer constant, so its readings all carry"); + a.c("the same sign -- either all 64 negative or all 64 positive, depending on what the"); + a.c("interpolator makes of a pinned buffer. Only a value that wraps swings both ways."); + a.l("lda f:$7E0100"); + a.l("and #$00FF"); + a.assert_a16_range( + 1, + 0x3F, + "sixty-four OUTX readings from a filter driven only by positive nibbles all carried the \ + same sign, so the decoded sample settled instead of wrapping: the store is saturating \ + where hardware drops the sign bit at fifteen bits", + ); + apu_timeout_arm(&mut a); + a.finish( + "E5.06", + 'E', + "BRR wraps at 15 bits", + Provenance::Documented( + "fullsnes and anomie's DSP doc: the decoded sample is clamped to 16 bits and then \ + stored as 15, so +4000h..+7FFFh becomes -4000h..-1 and the sign is lost", + ), + Kind::Scored, + None, + ) +} + +/// `E8.01` — `KON` is examined once every **second** output sample, i.e. at 16 kHz. +/// +/// # Two designs were wrong before this one, and for the same reason +/// +/// The obvious instrument is a delay: write `KON`, count how long until `ENVX` leaves zero. It does +/// not work, and no amount of resolution rescues it. The delay from a write to the voice starting +/// is `(P - φ) mod P`, where `P` is the poll period and `φ` is the write's phase against the DSP's +/// examination grid — a **sawtooth**. Two readings of a sawtooth cannot recover its period: shift +/// the second write by `Δ` and the difference takes one of two values depending on `φ`, and `φ` is +/// set by where the IPL handshake left the SPC700 against the DSP's sample clock, which is paced by +/// the S-CPU and therefore **different on the NTSC and PAL images of this very cartridge**. +/// +/// That is exactly what the two rejected drafts measured. A timer-2 differential reported a +/// difference of zero (at `T2DIV = 2` the tick, the sample and a one-sample offset are all 32 SPC +/// cycles and move together); a finer poll-counting differential reported **2 on the NTSC image and +/// 0 on the PAL image of the same build**, which is the sawtooth answering the question it was +/// actually asked. Neither draft was scored. Both are recorded in `docs/accuracysnes-plan.md`. +/// +/// # What this measures instead: writes the DSP never sees +/// +/// `KON` is a register, not a queue. A write **replaces** all eight bits, so a second write before +/// the DSP next looks at the register **cancels** the first one — that voice is never keyed at all. +/// So the number of key-ons the DSP acts on over a burst of writes counts the examinations that +/// fell between them, and *that* count is what the poll period decides. +/// +/// The sweep writes `$01, $02, $04 … $80` — one voice per write, no bit ever repeated — **24 SPC +/// cycles apart**, so 168 cycles separate the first write from the last. Over a half-open interval +/// of length `L`, a grid of period `P` contributes either `⌊L/P⌋` or `⌊L/P⌋ + 1` points, **for every +/// phase**. Add one for the final `$80`, which is still pending when the burst ends and is keyed at +/// the next examination whatever the phase: +/// +/// | `KON` examined | period | examinations in 168 cycles | voices started | +/// |---|---|---:|---:| +/// | every second sample (hardware) | 64 cycles | 2 or 3 | **3 or 4** | +/// | every sample | 32 cycles | 5 or 6 | **6 or 7** | +/// +/// The two ranges cannot meet, and neither depends on `φ`. Consecutive examinations are further +/// apart than consecutive writes under either period (64 and 32 both exceed 24), so each one keys a +/// *different* voice and the count is a count of distinct voices. The assertion allows `2..=5`: one +/// unit of slack on each side of the derived range for the exact-coincidence edges, and still a +/// clear unit below the six a per-sample poll cannot go under. +/// +/// # Why the blocks are padded to a round 24 cycles +/// +/// `MOV A,#imm` is 2 cycles and `MOV dp,A` is 4, so a bare write pair is 6; nine `NOP`s bring each +/// block to 24. The DSP address latch is loaded **once**, before the burst, so every block is the +/// same two instructions and the spacing is a property of the emitted code rather than of which +/// register is being written. The spacing does not have to be exactly 24 for the row to hold — at +/// ±2 cycles the two ranges are still `3..=4` and `5..=7` — but it does have to be *known*, which +/// is why it is padded rather than left as whatever the encoding happened to cost. +/// +/// # The eight voices are the instrument, so all eight are set up +/// +/// Every voice gets the same looping sample and the same direct-gain envelope, and every voice is +/// keyed **off** and left to run down to silence first: a voice already sounding would be counted +/// whether or not the sweep started it. `ENVX` is read for each afterwards and reduced on-cart to a +/// bitmask, so the failure report names *which* voices ran rather than only how many. +fn e8_01() -> Test { + let mut p = e8_01_eight_silent_voices(); + e8_01_sweep(&mut p); + + // Let the last write's key-on land, then clear KON. A core that re-keys a voice for as long as + // the bit stays set would hold voice 7's envelope at zero and undercount by one -- the same + // trap the module comment above records for `E5.07`. + p.delay(0x00); + dsp_write(&mut p, 0x4C, 0x00); + p.delay(0x20); + + // Reduce the eight envelopes to one byte, most significant bit = voice 0. `CMP A,#$01` sets + // carry for any non-zero reading, and `ADC A,dp` against the accumulator itself is `mask * 2 + + // carry` -- a shift-in without an `ROL` the emitter does not have. + p.mov_dp_imm(0x13, 0x00); + for v in 0..8u8 { + p.mov_a_imm((v << 4) | 0x08).mov_dp_a(0xF2); // VxENVX + p.mov_a_dp(0xF3); + p.cmp_a_imm(0x01); + p.mov_a_dp(0x13); + p.adc_a_dp(0x13); + p.mov_dp_a(0x13); + } + p.mov_a_dp(0x13).mov_dp_a(PORT1); + p.mov_a_imm(DONE).mov_dp_a(PORT0).release_to_ipl(); + + let mut a = Asm::new(); + upload_and_run(&mut a, &p); + a.l("rep #$30"); + a.l("lda f:$7E0100"); + a.l("and #$00FF"); + a.record( + 256, + "E8.01 which voices the KON sweep started (bit 7 = voice 0)", + ); + + a.c("Count the set bits: how many of the eight writes the DSP acted on. The mask itself is in"); + a.c("the measurement channel above, so a failure here can be read back to the exact pattern."); + a.l("ldx #$0000"); + a.l("ldy #$0008"); + a.label("e801bit"); + a.l("lsr a"); + a.l("bcc @e801next"); + a.l("inx"); + a.label("e801next"); + a.l("dey"); + a.l("bne @e801bit"); + a.l("txa"); + a.record(257, "E8.01 how many of the eight voices the sweep started"); + a.assert_a16_range( + 2, + 5, + "eight KON writes 24 SPC cycles apart started more than five voices, so more than four \ + examinations fell inside the burst -- KON is being read every output sample rather than \ + every second one. A count of zero or one instead means the sweep never ran", + ); + apu_timeout_arm(&mut a); + a.finish( + "E8.01", + 'E', + "KON examined at 16 kHz", + Provenance::Documented( + "fullsnes and anomie's DSP doc [ERRATA]: the key-on/key-off registers are examined \ + once every two output samples, giving a 16 kHz effective poll rate", + ), + Kind::Scored, + None, + ) +} + +/// Emit: eight identically-configured voices, all released and silent. +/// +/// Only the registers that decide whether `ENVX` can leave zero are written per voice — `GAIN` +/// direct at full scale with `ADSR1` bit 7 clear, and `SRCN`/`PITCH` pointing at the shared looping +/// sample. Leaving any of them to their power-on value would make the row depend on what the +/// previous test left in voice 0's registers, which is the one thing a program sharing a DSP with +/// forty others must not do. +fn e8_01_eight_silent_voices() -> Spc { + let mut p = Spc::new(); + let addr = p.data_first(IMAGE_BASE, &looping_sample()); + p.mov_x_imm(0xEF).mov_sp_x(); + + // One directory entry, shared: SRCN 0 for every voice. Start and loop address are the same + // block, so the code-3 header repeats it forever and no voice ever runs out. + let dir = u16::from(DIR_PAGE) << 8; + let [lo, hi] = addr.to_le_bytes(); + p.mov_a_imm(lo).mov_abs_a(dir); + p.mov_a_imm(hi).mov_abs_a(dir + 1); + p.mov_a_imm(lo).mov_abs_a(dir + 2); + p.mov_a_imm(hi).mov_abs_a(dir + 3); + + dsp_write(&mut p, 0x6C, 0x20); // FLG: running, unmuted, echo writes off + dsp_write(&mut p, 0x3D, 0x00); // NON + dsp_write(&mut p, 0x4D, 0x00); // EON + dsp_write(&mut p, 0x2D, 0x00); // PMON + dsp_write(&mut p, 0x5D, DIR_PAGE); // DIR + dsp_write(&mut p, 0x0C, 0x7F); // MVOLL + dsp_write(&mut p, 0x1C, 0x7F); // MVOLR + for v in 0..8u8 { + let b = v << 4; + dsp_write(&mut p, b | 0x02, 0x00); // PITCH low + dsp_write(&mut p, b | 0x03, 0x10); // PITCH high: one sample per output sample + dsp_write(&mut p, b | 0x04, 0x00); // SRCN + dsp_write(&mut p, b | 0x06, 0x00); // ADSR2 + dsp_write(&mut p, b | 0x07, 0x7F); // GAIN: direct, full scale + dsp_write(&mut p, b | 0x05, 0x00); // ADSR1 bit 7 clear, so GAIN governs + } + + // Silence first, and prove it by construction rather than by assumption: release steps the + // envelope down 8 a sample from at most $7F0, so ~254 samples, and one delay block is ~48. + dsp_write(&mut p, 0x4C, 0x00); // KON: nothing pending + dsp_write(&mut p, 0x5C, 0xFF); // KOF: all eight + for _ in 0..6 { + p.delay(0x00); + } + dsp_write(&mut p, 0x5C, 0x00); // and released, so the sweep's key-ons are not undone + p +} + +/// Emit: eight `KON` writes, one bit each, exactly 24 SPC cycles apart. +/// +/// The address latch is loaded once outside the burst so each block is `MOV A,#imm` (2 cycles) + +/// `MOV dp,A` (4) + nine `NOP`s (18). The write itself lands on the store's final cycle, so the +/// interval between consecutive writes is the block length and nothing else. +fn e8_01_sweep(p: &mut Spc) { + p.mov_a_imm(0x4C).mov_dp_a(0xF2); + for v in 0..8u8 { + p.mov_a_imm(1 << v); + p.mov_dp_a(0xF3); + for _ in 0..9 { + p.nop(); + } + } +} + /// Key-on takes five output samples to reach the envelope generator. /// /// A `KON` write does not start a voice immediately. The DSP holds it for five output samples — diff --git a/to-dos/ROADMAP.md b/to-dos/ROADMAP.md index 96a13352..0480cf0a 100644 --- a/to-dos/ROADMAP.md +++ b/to-dos/ROADMAP.md @@ -445,7 +445,7 @@ constraints live in **`docs/accuracysnes-plan.md`**; this list is the citable ID | **T-04-B** | Group B — 5A22 bus, clock, timing. **Started:** access speed, `RDNMI` mechanics, IRQ timers, frame geometry, multiply/divide + power-on shipped (14 tests, 2 emulator defects found). Left: `B2` scanline geometry, `B3` DRAM refresh — **unblocked**, write as golden vectors per the `D3` precedent (see the plan §4) — and the rest of `B4` | ~16 | nothing | | **T-04-C** | The rest of register-observable Group C — `C1.07`/`C1.08`, the 9/10-bit `VMAIN` rotations, CGRAM-during-render, `C7.04`–`C7.09`, `C9.05`, `C11.07`/`C11.08` | ~20 | nothing | | **T-04-D** | Group D — DMA / HDMA. **Started:** 15 tests — GP-DMA (`D1.01`-`D1.07`, `D1.09`/`D1.15`, `D1.10`) and HDMA (`D2.03`-`D2.06`). Found two defects: the unmodelled `$43xB` scratch latch and a WRAM->`$2180` transfer that wrote when hardware does not. **The blocker below is over-broad — re-scoped 2026-07-21.** Most uncovered rows carry specific, actionable content and need no new research: `D1.08` (invalid A-bus addresses, `[ERRATA]`, with the address list enumerated), `D1.13` (DMA reads update open bus, writes never do), `D1.14` (`$2180` B->A asymmetry, quoted verbatim with its +4 clocks), `D2.07` (HDMA preempts GP-DMA, which pauses and resumes), `D2.08` (`$420C` mid-frame starts the channel next line). **Genuinely thin and still needing sources:** `D1.11` (power-on state — names a game, not a mechanism), `D1.12` (CPU timing before DMA start), `D2.01` (init at "H~6", an approximation no test can assert). | ~20 | **partial** — only `D1.11`/`D1.12`/`D2.01` are actually source-blocked; the rest are writable now | -| **T-04-E** | Group E — APU (SPC700 + S-DSP). **Unblocked and started:** `apu_upload` implements the IPL boot handshake and `gen/src/spc.rs` assembles SPC700 programs, so the cart can upload code, run it and read results through the four ports. `E1.01` landed and is cross-validated | ~75 | an **on-cart APU harness** (IPL upload, results back through `$2140`–`$2143`) | +| **T-04-E** | Group E — APU (SPC700 + S-DSP). **Unblocked and started:** `apu_upload` implements the IPL boot handshake and `gen/src/spc.rs` assembles SPC700 programs, so the cart can upload code, run it and read results through the four ports. `E1.01` landed and is cross-validated. **2026-08-01:** `E8.01`, `E9.02` and `E5.06` land (PR #298), taking the group to **95 of 129** enumerated assertions; `E8.01`'s two rejected drafts are recorded in `docs/accuracysnes-plan.md` as the reference case for why a *delay* cannot measure a poll period | ~34 | an **on-cart APU harness** (IPL upload, results back through `$2140`–`$2143`) | | **T-04-F** | Group F — input | ~22 | a decision on the **on-cart / host-driven split**: a cart cannot press its own buttons | | **T-04-G** | Group G — power-on / reset / cartridge, mostly golden vectors | ~18 | ~~boot-path ordering~~ **UNBLOCKED** — `capture_power_on` samples before `init_registers` into a documented capture block; `B5.05` is the first consumer | | **T-04-I** | The 256-opcode cycle sweep (`A5.01`–`A5.08`) | 1 mechanism | ~~an external timing table~~ **ORACLE ESTABLISHED** — `docs/accuracysnes-timing-oracle.md`. The 5A22 is a stock WDC core plus a wait-state generator, so WDC Table 5-7 (VDA/VPA cycle classification) + the SNES speed map is emulator-independent and sufficient. Remaining: safe-operand table + sandbox. `STP` excluded — it halts until reset |