diff --git a/CHANGELOG.md b/CHANGELOG.md index 87aee4fe..181a175d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **`E3.06` polls BOTH timers; a single end-of-interval read measured a phase, not a count.** The row + compares timer 2's rate against timer 0's over one interval. Timer 2 was already accumulated across + polls — that was the earlier fix for `TnOUT`'s four-bit ceiling — but timer 0 was still read + **exactly once**, at the end, on the reasoning that a handful of ticks is nowhere near its own wrap. + True about magnitude, and silent about phase: one read samples whatever sub-tick phase the previous + test left the timer in. `$F1`'s enable-raise resets a timer's stage-2 and stage-3 state (`E3.02`) + but nothing resets stage 0, so the reading was reproducible only while nothing before it disturbed + that phase. + + It is not a hypothetical. Authoring a row that drives timer 2 hard (an SPC700 cycle-cost instrument) + broke `E3.06` **on the battery's second run and not its first** — the residue crosses the run + boundary. The band was not the problem and was not widened to admit it: **the row is right and its + instrument was one read short.** + + Both counters are now accumulated in the same poll loop, over 32 passes rather than 24, so neither + side carries a phase. The comparison is quantisation-honest for the first time: each accumulated + count is exact to ±1, so `8 x T0` carries ±8 and the difference ±9 — the previous ±6 was tighter + than its own instrument could support, and passed only because the two phases happened to correlate. + The band is now ±10 and still discriminating by a wide margin: a core running every timer from the + 8 kHz stage lands some ninety ticks out. Measured across hosts, the same interval yields T0 = 12-13 + and T2 = 99 — the one-tick spread in T0 is precisely the sensitivity that ±6 could not absorb. + - **`C11.12` is unauthorable, verified two ways — there is nothing to assert.** Checked rather than assumed: the row's subject ("Mode 7 scroll offset latch timing") appears exactly **twice** in `ref-docs/`, both times as a game-compatibility table row naming NHL '94, with **no behavioural diff --git a/crates/rustysnes-test-harness/tests/accuracysnes.rs b/crates/rustysnes-test-harness/tests/accuracysnes.rs index 151d19ce..25a17bdc 100644 --- a/crates/rustysnes-test-harness/tests/accuracysnes.rs +++ b/crates/rustysnes-test-harness/tests/accuracysnes.rs @@ -626,10 +626,13 @@ const GROUP_E_BATCH_SLOTS: [(u16, &str); 19] = [ "B2.07 APU ticks over 48 frames (~6398 NTSC, ~7689 PAL)", ), (276, "B2.07 measured frame height (261 NTSC, 311 PAL)"), - (266, "E3.06 timer 0 ticks over the interval"), + ( + 266, + "E3.06 timer 0 ticks, accumulated across polls (expect ~13)", + ), ( 267, - "E3.06 timer 2 ticks over the SAME interval (TnOUT wraps at 16)", + "E3.06 timer 2 ticks over the SAME interval (expect ~8x slot 266)", ), ]; diff --git a/tests/roms/AccuracySNES/ERROR_CODES.md b/tests/roms/AccuracySNES/ERROR_CODES.md index 0a33725c..5b60928d 100644 --- a/tests/roms/AccuracySNES/ERROR_CODES.md +++ b/tests/roms/AccuracySNES/ERROR_CODES.md @@ -2668,7 +2668,7 @@ Provenance: **Documented** (SNESdev Wiki, SPC700 timers; fullsnes). Kind: scored | Code | Byte | Meaning | |---|---|---| | 1 | `$02` | timer 0 did not tick over this interval, or ticked far more than the poll loop allows for — either way the interval is not the one this test needs and the ratio below means nothing | -| 2 | `$04` | timer 2 did not count within six ticks of eight times timer 0 over the same interval, so it is not running from the 64 kHz stage — a core running every timer at 8 kHz lands near a seventh of the expected count. The two raw counts are in slots 266 and 267 | +| 2 | `$04` | timer 2 did not count within ten ticks of eight times timer 0 over the same interval, so it is not running from the 64 kHz stage — a core running every timer at 8 kHz lands near a seventh of the expected count, which is some ninety ticks out. The two raw counts are in slots 266 and 267 | ### E3.08 — TEST bit 0 halts timers diff --git a/tests/roms/AccuracySNES/asm/tests_group_a.s b/tests/roms/AccuracySNES/asm/tests_group_a.s index e436d5c7..c1c7c445 100644 --- a/tests/roms/AccuracySNES/asm/tests_group_a.s +++ b/tests/roms/AccuracySNES/asm/tests_group_a.s @@ -23426,7 +23426,7 @@ CATALOG_IMPL = 1 rep #$30 .a16 .i16 - lda #69 + lda #83 sta f:V_APU_LEN lda #$0200 sta f:V_APU_DEST ; APU RAM $0200: clear of the zero page and the stack @@ -23478,20 +23478,23 @@ CATALOG_IMPL = 1 .i16 lda f:$7E0101 and #$00FF - ; record slot 266: E3.06 timer 0 ticks over the interval + ; record slot 266: E3.06 timer 0 ticks, ACCUMULATED across polls (no end-of-interval phase) sta f:$7EE414 lda f:$7E0102 and #$00FF ; record slot 267: E3.06 timer 2 ticks, ACCUMULATED across polls (no 4-bit ceiling) sta f:$7EE416 - ; Timer 0 first: the interval has to contain a couple of its ticks for a ratio to exist. + ; Timer 0 first: the interval has to contain several of its ticks for a ratio to exist. The + ; band is wide because the poll loop's cycle cost is a per-core detail the ratio below is + ; designed to be insensitive to; this guard only has to reject `nothing ticked` and a run + ; length that would wrap a byte accumulator. lda f:$7E0101 and #$00FF - cmp #$0001 + cmp #$0006 bcs :+ jmp @fail1 : - cmp #$0007 + cmp #$0015 bcc :+ jmp @fail1 : @@ -23510,13 +23513,13 @@ CATALOG_IMPL = 1 sec sbc f:$7E01F0 ; the signed error against the ideal ratio clc - adc #$0006 ; biased by 6 so the allowed band is 0..12 rather than -6..+6 + adc #$000A ; biased by 10 so the allowed band is 0..20 rather than -10..+10 and #$00FF cmp #$0000 bcs :+ jmp @fail2 : - cmp #$000D + cmp #$0015 bcc :+ jmp @fail2 : @@ -23541,7 +23544,7 @@ CATALOG_IMPL = 1 sta f:$7EE010 jml test_restore @fail2: - ; timer 2 did not count within six ticks of eight times timer 0 over the same interval, so it is not running from the 64 kHz stage — a core running every timer at 8 kHz lands near a seventh of the expected count. The two raw counts are in slots 266 and 267 + ; timer 2 did not count within ten ticks of eight times timer 0 over the same interval, so it is not running from the 64 kHz stage — a core running every timer at 8 kHz lands near a seventh of the expected count, which is some ninety ticks out. The two raw counts are in slots 266 and 267 sep #$20 .a8 lda #$04 @@ -34947,11 +34950,12 @@ apu_prog_113: .byte $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF apu_prog_114: .byte $CD, $EF, $BD, $8F, $01, $FA, $8F, $01, $FC, $8F, $00, $10 - .byte $8F, $00, $11, $E4, $FD, $E4, $FF, $8F, $85, $F1, $E4, $FF - .byte $C4, $12, $E4, $10, $60, $84, $12, $C4, $10, $AB, $11, $E4 - .byte $11, $68, $18, $D0, $ED, $8F, $80, $F1, $E4, $FD, $C4, $F6 - .byte $E4, $10, $C4, $F7, $E8, $5A, $C4, $F4, $E4, $F4, $68, $A5 - .byte $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF + .byte $8F, $00, $11, $8F, $00, $13, $E4, $FD, $E4, $FF, $8F, $85 + .byte $F1, $E4, $FF, $C4, $12, $E4, $10, $60, $84, $12, $C4, $10 + .byte $E4, $FD, $C4, $12, $E4, $13, $60, $84, $12, $C4, $13, $AB + .byte $11, $E4, $11, $68, $20, $D0, $E2, $8F, $80, $F1, $E4, $13 + .byte $C4, $F6, $E4, $10, $C4, $F7, $E8, $5A, $C4, $F4, $E4, $F4 + .byte $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF apu_prog_115: .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 diff --git a/tests/roms/AccuracySNES/build/accuracysnes-pal.sfc b/tests/roms/AccuracySNES/build/accuracysnes-pal.sfc index 1429855a..06df3c61 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 a26269c4..9d4145ba 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/tests/apu.rs b/tests/roms/AccuracySNES/gen/src/tests/apu.rs index d155e3f1..e138d92e 100644 --- a/tests/roms/AccuracySNES/gen/src/tests/apu.rs +++ b/tests/roms/AccuracySNES/gen/src/tests/apu.rs @@ -8306,32 +8306,18 @@ fn e6_02d() -> Test { ) } -/// Timer 2 counts **eight times faster** than timer 0 at the same divider. +/// Build `E3.06`'s SPC700 program: run timers 0 and 2 over one interval, accumulating BOTH. /// -/// The two timers are fed from different taps of the same clock: `T0` and `T1` from an 8 kHz stage, -/// `T2` from a 64 kHz one, so `TnDIV` means eight times as much wall time on `T0` as on `T2`. A -/// core that runs all three timers off one rate is the obvious mistake, and it is invisible to -/// every other timer test on this cart — `E3.01`, `E3.05` and `E2.01` all use `T0` alone, and a -/// uniform-rate core passes all of them. -/// -/// Both timers run over the *same* interval, started by one write and stopped by another, so this -/// is a ratio rather than two independent measurements: whatever the interval actually was, `T2` -/// must show about eight times what `T0` does. The interval is chosen short enough that `T2`'s -/// four-bit counter cannot wrap — a wrap would read as a *small* number and look like a slow timer, -/// which is the one failure this test could not tell from a pass. -/// -/// The assertion is a pair of ranges, not two exact counts. Where the interval falls relative to -/// each timer's internal divider phase decides whether the last tick lands inside it, so ±1 is not -/// a defect; a factor of eight is far outside that. A uniform-rate core reads `$01` where this -/// wants nine or more. -fn e3_06() -> Test { - let mut prog = Spc::new(); +/// Split out of [`e3_06`] because the reasoning below is longer than the code, and the code is +/// longer than one screen once both accumulators are in the loop. +fn e3_06_program(prog: &mut Spc) { prog.mov_x_imm(0xEF) .mov_sp_x() .mov_dp_imm(0xFA, 0x01) // T0DIV = 1 .mov_dp_imm(0xFC, 0x01) // T2DIV = 1 .mov_dp_imm(0x10, 0x00) // accumulated timer-2 ticks .mov_dp_imm(0x11, 0x00) // poll counter + .mov_dp_imm(0x13, 0x00) // accumulated timer-0 ticks .mov_a_dp(0xFD) // drain both counters so the interval starts from zero .mov_a_dp(0xFF) .mov_dp_imm(0xF1, 0x85); // enable timers 0 and 2 together; bit 7 keeps the IPL mapped @@ -8343,8 +8329,18 @@ fn e3_06() -> Test { // instrument's ceiling. ares read 0 here where RustySNES read 10, and the row could not tell // "timer 2 wrapped" from "timer 2 is not running at 64 kHz". // - // Each pass is ~32 SPC cycles and timer 2 ticks every 32, so every read returns 0, 1 or 2 and - // the running sum cannot lose a tick however long the interval gets. + // BOTH timers are polled, and that is the 2026-08-02 correction. Timer 2 was already polled; + // timer 0 was read exactly ONCE at the end, on the reasoning that a handful of ticks is nowhere + // near its four-bit wrap. True about magnitude, and silent about PHASE -- a single read samples + // whatever sub-tick phase the previous test left timer 0 in, so the reading was reproducible + // only as long as nothing before it disturbed that phase. Adding a row that drives timer 2 hard + // (the `E2.10` cycle instrument) broke it on the battery RESTART, where the residue crosses the + // boundary: `$F1`'s enable-raise resets stage2/stage3 (`E3.02`) but nothing resets stage0. + // Accumulating both removes the phase from both sides. + // + // Each pass is ~51 SPC cycles. Timer 2 ticks every 16 opcode cycles and timer 0 every 128, so + // every T2OUT read returns 0-4 and every T0OUT read 0 or 1 -- neither can lose a tick however + // long the interval runs, and the byte accumulators stay well short of wrapping. let poll = prog.here(); prog.mov_a_dp(0xFF) // T2OUT, read-and-clear .mov_dp_a(0x12) @@ -8352,26 +8348,62 @@ fn e3_06() -> Test { .clrc() .adc_a_dp(0x12) .mov_dp_a(0x10) + .mov_a_dp(0xFD) // T0OUT, read-and-clear, in the SAME loop + .mov_dp_a(0x12) + .mov_a_dp(0x13) + .clrc() + .adc_a_dp(0x12) + .mov_dp_a(0x13) .inc_dp(0x11) .mov_a_dp(0x11) - .cmp_a_imm(0x18); // 24 passes: enough for timer 0 to tick two or three times + .cmp_a_imm(0x20); // 32 passes: ~1630 cycles, so timer 0 ticks a dozen times and timer 2 ~100 prog.bne_back(poll); prog.mov_dp_imm(0xF1, 0x80) // stop them together - .mov_a_dp(0xFD) // timer 0, read once -- a handful of ticks, nowhere near its own wrap + .mov_a_dp(0x13) // the ACCUMULATED timer-0 ticks, not a single end-of-interval read .mov_dp_a(PORT2) .mov_a_dp(0x10) .mov_dp_a(PORT3) .mov_a_imm(DONE) .mov_dp_a(PORT0) .release_to_ipl(); +} + +/// Timer 2 counts **eight times faster** than timer 0 at the same divider. +/// +/// The two timers are fed from different taps of the same clock: `T0` and `T1` from an 8 kHz stage, +/// `T2` from a 64 kHz one, so `TnDIV` means eight times as much wall time on `T0` as on `T2`. A +/// core that runs all three timers off one rate is the obvious mistake, and it is invisible to +/// every other timer test on this cart — `E3.01`, `E3.05` and `E2.01` all use `T0` alone, and a +/// uniform-rate core passes all of them. +/// +/// Both timers run over the *same* interval, started by one write and stopped by another, so this +/// is a ratio rather than two independent measurements: whatever the interval actually was, `T2` +/// must show about eight times what `T0` does. **Both are accumulated across polls**, which is what +/// keeps `TnOUT`'s four-bit read-and-clear counter out of the answer — a wrap would read as a +/// *small* number and look like a slow timer, the one failure this test could not tell from a pass. +/// +/// Polling both sides is also what makes the row reproducible. A single end-of-interval read +/// samples whatever sub-tick phase the previous test left that timer in, and nothing resets a +/// timer's stage 0 — so the reading was stable only while nothing upstream disturbed the phase, and +/// a later row that drove timer 2 hard broke it on the battery's *second* run. +/// +/// The assertion is a range, not two exact counts. Each accumulated count is exact to ±1, so `8 x +/// T0` carries ±8 and their difference ±9; the band is ±10. A factor of eight is far outside that — +/// a core running every timer from the 8 kHz stage lands some ninety ticks out. +fn e3_06() -> Test { + let mut prog = Spc::new(); + e3_06_program(&mut prog); let mut a = Asm::new(); upload_and_run(&mut a, &prog); a.l("rep #$30"); a.l("lda f:$7E0101"); a.l("and #$00FF"); - a.record(266, "E3.06 timer 0 ticks over the interval"); + a.record( + 266, + "E3.06 timer 0 ticks, ACCUMULATED across polls (no end-of-interval phase)", + ); a.l("lda f:$7E0102"); a.l("and #$00FF"); a.record( @@ -8379,12 +8411,17 @@ fn e3_06() -> Test { "E3.06 timer 2 ticks, ACCUMULATED across polls (no 4-bit ceiling)", ); - a.c("Timer 0 first: the interval has to contain a couple of its ticks for a ratio to exist."); + a.c( + "Timer 0 first: the interval has to contain several of its ticks for a ratio to exist. The", + ); + a.c("band is wide because the poll loop's cycle cost is a per-core detail the ratio below is"); + a.c("designed to be insensitive to; this guard only has to reject `nothing ticked` and a run"); + a.c("length that would wrap a byte accumulator."); a.l("lda f:$7E0101"); a.l("and #$00FF"); a.assert_a16_range( - 1, 6, + 20, "timer 0 did not tick over this interval, or ticked far more than the poll loop allows for \ — either way the interval is not the one this test needs and the ratio below means nothing", ); @@ -8406,14 +8443,15 @@ fn e3_06() -> Test { a.l("sec"); a.l("sbc f:$7E01F0 ; the signed error against the ideal ratio"); a.l("clc"); - a.l("adc #$0006 ; biased by 6 so the allowed band is 0..12 rather than -6..+6"); + a.l("adc #$000A ; biased by 10 so the allowed band is 0..20 rather than -10..+10"); a.l("and #$00FF"); a.assert_a16_range( 0, - 12, - "timer 2 did not count within six ticks of eight times timer 0 over the same interval, so \ + 20, + "timer 2 did not count within ten ticks of eight times timer 0 over the same interval, so \ it is not running from the 64 kHz stage — a core running every timer at 8 kHz lands near \ - a seventh of the expected count. The two raw counts are in slots 266 and 267", + a seventh of the expected count, which is some ninety ticks out. The two raw counts are \ + in slots 266 and 267", ); apu_timeout_arm(&mut a); a.finish(