From 7c6e0c540285e8b58d59fb62549bc3d09f366b39 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Sat, 1 Aug 2026 04:18:19 -0400 Subject: [PATCH] docs(accuracysnes): record why E8.06 is not reachable from the cart Authored, injected and withdrawn the same day. The design was sound -- two KON writes 32 cycles apart, GAIN $DF so ENVX climbs 2 per sample, difference the two voices: 4 for the two-sample lead hardware gives, 2 for the one-sample lead a per-sample poll would give, with both hardware outcomes accepted so no uncontrolled phase decides the verdict. What killed it is the read window. Each voice's ENVX is latched at its own tick of the DSP's 32-tick schedule, so a reading taken between two voices' updates is one whole sample wrong -- exactly the size of the signal. Bracketing the voice-6 read with two voice-7 reads and requiring them to agree does not fit: three dsp_read_to calls span 39 cycles of a 32-cycle sample, and even landing each reading in its own register spans 21. Injecting the named bug trips the guard rather than the row's own code on both versions, so the scored assertion could not be demonstrated. The reusable finding: two voices' ENVX cannot be compared at single-sample resolution from the cart. The loop is already as tight as the instruction set allows, so a future row needs a different observable rather than a faster one. E8.06's swallow half is not separately assertable either -- at any gap both poll rates swallow or spare the first voice depending on phase. Withdrawal verified byte-clean: the ROM returned to checksum $DA9E, its value before the row was added. The MOV Y,dp / MOV X,dp emitters went with it, since this file carries only opcodes a committed test exercises. Co-Authored-By: Claude Opus 5 (1M context) --- docs/accuracysnes-plan.md | 41 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/accuracysnes-plan.md b/docs/accuracysnes-plan.md index 2ad21d91..680081ec 100644 --- a/docs/accuracysnes-plan.md +++ b/docs/accuracysnes-plan.md @@ -293,6 +293,47 @@ decoder that saturates instead of wrapping settles, and a settled decoder's read 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. +### `E8.06` — authored, injected, and withdrawn: two voices cannot be compared at one sample + +**Attempted 2026-08-01 and withdrawn the same day.** The row reads: *"collapse case `KON=$01` / +`KON=$02` → usually only voice 2, and if both, voice 1 is **2 samples ahead**, proving the 16 kHz +rate."* It is `E8.01`'s mechanism asked the other way round, and the setup was already built, so it +looked like the cheapest row left in Group E. + +**The design was sound.** Two `KON` writes exactly 32 SPC cycles apart, `GAIN = $DF` (mode 6, linear +increase at rate 31) so `ENVX` climbs 2 per output sample, then read both voices and difference +them: **4** for the two-sample lead hardware gives, **2** for the one-sample lead a per-sample poll +would give. Both hardware outcomes — swallowed, or leading by two — are accepted, because which one +occurs depends on the phase the cart does not control; what no phase can produce is a one-sample +lead. RustySNES reports the collapse case, `ENVX` 0 against voice 7's `$26`. + +**What killed it is the read window.** Each voice's `ENVX` is latched at its own tick of the DSP's +32-tick schedule, so the two registers do not update together and a reading taken between them is +one whole sample wrong — *exactly the size of the signal*. The fix is to bracket the voice-6 read +with two voice-7 reads and require them to agree, and the fix does not fit: + +| three-reading window | of a 32-cycle sample | +|---|---| +| `dsp_read_to` ×3 (the ordinary form) | **39 cycles** — a straddle is certain | +| register-landed reads (`MOV Y,dp` / `MOV X,dp`, values parked in ports afterwards) | **21 cycles** — a straddle is likely | + +Injecting the named bug (`KON` examined every sample) trips the **guard**, not the row's own code, +on both versions: the injected build's timing moves the update tick inside the window and voice 7 +reads `$24` then `$26`. Per the standing rule, a row whose named injection fires the guard rather +than the assertion has not been demonstrated, and shipping it would mean shipping a scored row whose +failure code cannot be produced on demand. + +**The finding, which is the reusable part:** *two voices' `ENVX` cannot be compared at +single-sample resolution from the cart.* The minimum three-read window is two-thirds of a sample and +the straddle error equals the signal. Any future row that needs to compare two voices' envelope +positions needs a different observable, not a tighter loop — the loop is already as tight as the +instruction set allows. `E8.06`'s swallow half is not separately assertable either: at any gap, both +poll rates swallow or spare voice 6 depending on phase, so the lead is what carries the claim. + +Withdrawal verified byte-clean: removing it returned the ROM to checksum `$DA9E`, the value before +it was added. The two `MOV Y,dp` / `MOV X,dp` emitters went with it — this file carries only +opcodes a committed test exercises, and an unexercised encoding is an unverified one. + ### An open question `B4.12` used to answer by accident Does the V-IRQ flag re-assert while `V == VTIME` still holds — is it a one-shot per frame, or a