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

Filter by extension

Filter by extension

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

### Added

- **`E3.06` rewritten to poll and accumulate — and ares agrees with it exactly.** The row read
`TnOUT` once at the end of the interval, and `TnOUT` is a four-bit read-and-clear counter, so the
useful range was `8..15` with the wrap one tick above. That is not a band that can be widened; it
is the instrument's ceiling. It now polls timer 2 every ~32 SPC cycles — one timer-2 period — so
every read returns 0, 1 or 2 and the running sum cannot lose a tick however long the interval gets.

It also asserts the **ratio** rather than two absolute counts: `|T2 − 8·T0| ≤ 6`, computed on the
65816. Absolute counts pin the poll loop's own cycle cost, which differs between cores for reasons
that have nothing to do with the 64 kHz stage — and that is precisely what made the old row fail on
ares while ares' own `Timer<128>`/`Timer<128>`/`Timer<16>` declarations were an exactly correct
8:1.

RustySNES and ares now report the **identical** 6 and 46. `ARES_KNOWN_FAILURES` drops 5 → 4. **A
ceiling in the instrument reads exactly like a defect in the thing measured**, which is the
reusable half.

- **RETRACTION: `F1.10` is not "1-vs-3 with RustySNES alone", and its Mesen2 verdict is
phase-fragile.** #306 published that reading from a measurement taken at the time. Then rewriting
`E3.06` — an **unrelated APU row** — made Mesen2 **pass** `F1.10`. Three identical runs before,
three identical runs after. The rewrite changed one uploaded program's length, which moved the
cart's execution phase, which moved when `F1.10` samples `$4212` relative to the vblank edge.

So Mesen2's verdict on that row encodes where the cart happens to be, not only what Mesen2 models.
snes9x and ares fail it stably; Mesen2 flips. `MESEN2_KNOWN_FAILURES` drops 2 → 1 (`F1.03` only),
and the comment says not to restore an `F1.10` entry — either pin the row's sampling point, or
accept that Mesen2's verdict on it carries no information.

**This is the same trap for the third time in one session**, after `E8.01`'s two rejected drafts
and the scene field gate: a verdict that encodes an uncontrolled phase looks stable until something
unrelated moves.

- **ares is wired into `crossval.sh` as a third reference.** `cross-validation: 3 reference(s) agree
with the cart`, with `ARES_KNOWN_FAILURES=5` carrying the same per-row rationale the other two
constants do: `C7.05`/`C7.10`/`F1.10` are rows snes9x already fails (ares corroborating it, not
Expand Down
71 changes: 42 additions & 29 deletions scripts/accuracysnes/crossval.sh
Original file line number Diff line number Diff line change
Expand Up @@ -191,26 +191,31 @@ SNES9X_KNOWN_FAILURES=14
# | containing no Start and the cart would never leave its menu. Covered by the
# | in-repo harness and snes9x, which do drive both ports.
#
# F1.10 code 2 | NOT the port-2 limitation, though this comment said so until it was measured.
# | `f1_require_contract` reads $4016 only, and code 2 means "$4212 read busy at
# | the very start of the vblank line". Mesen2 fails it for the same reason snes9x
# | and ares do -- the automatic read modelled as starting at the vblank edge
# | rather than a few dozen cycles into the line.
# `F1.10` USED to be a second entry here. It is not any more, and the reason is worth more than the
# entry was.
#
# **So `F1.10` is 1-vs-3, with RustySNES passing ALONE.** snes9x, Mesen2 and ares all fail it. The
# row is Documented -- nocash fullsnes puts the read's start at ~dot 32.5-95.5 of the first vblank
# line -- and RustySNES passes only because of a deliberate auto-read-start-timing fix. A first-party
# accuracy cart being right where three references are wrong is the point of having one, but 1-vs-3
# on a scored row is stated here rather than left to be mistaken for consensus. If that citation
# ever turns out to be misread, this row is where it will show.
# **The Mesen2 runner can under-report under load.** One run during this investigation returned 1
# where every other returned 2, while four other `dotnet` processes were live. `--timeout=60` is a
# wall-clock bound, so a loaded machine can cut the battery short and report a SMALLER failing count
# — which reads as "things improved", the most dangerous direction for a gate to be wrong in. Five
# consecutive runs on an idle machine gave 2 every time, and `mesen_failing_set_probe.lua` gave the
# identical SET four times. If this gate ever reports fewer failures than expected, re-run it idle
# before believing it.
MESEN2_KNOWN_FAILURES=2
# It was first attributed to the port-2 limitation, which is wrong: `f1_require_contract` reads
# $4016 only, and code 2 means "$4212 read busy at the very start of the vblank line". Measured
# 2026-08-01, Mesen2 DID fail it, alongside snes9x and ares -- which read as "RustySNES passes
# alone", and was published that way.
#
# **Then rewriting `E3.06` -- an unrelated APU row -- made Mesen2 PASS `F1.10`.** Three identical
# runs before, three identical runs after. The rewrite changed one uploaded program's length, which
# moved the cart's execution phase, which moved when `F1.10` samples `$4212` relative to the vblank
# edge.
#
# So `F1.10`'s Mesen2 verdict is **phase-fragile**: it encodes where the cart happens to be, not only
# what Mesen2 models. That is the same trap already recorded for `E8.01` and for the scene field
# gate, in a third place. snes9x and ares fail it stably; Mesen2 flips. **Do not restore a
# known-failure entry for it** -- either pin the row's sampling point, or accept that Mesen2's
# verdict on it carries no information.
#
# **The Mesen2 runner can also under-report under load.** One run returned a smaller count than every
# other while four other `dotnet` processes were live. `--timeout=60` is a wall-clock bound, so a
# loaded machine cuts the battery short and reports FEWER failures -- which reads as "things
# improved", the most dangerous direction for a gate to be wrong in. Re-run idle before believing a
# drop, and use `mesen_failing_set_probe.lua` to read the SET rather than trusting the count.
MESEN2_KNOWN_FAILURES=1

# 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
Expand All @@ -236,22 +241,30 @@ MESEN2_PAL_KNOWN_FAILURES=1
# F1.10 code 2 | The auto-read start race. snes9x, Mesen2 AND ares all fail it — see the F1.10
# | entry under MESEN2_KNOWN_FAILURES for why that leaves RustySNES passing alone.
#
# The other two share ONE cause, and it is a bug in ares rather than a disagreement:
# The fifth is a bug in ares rather than a disagreement:
#
# E3.06 code 2 | ares' `$F1` (CONTROL) handler NEGATES the timer-2 counter reset:
# E8.02 code 3 | if(timer0.enable.raise(data.bit(0))) { timer0.stage2 = 0; ... }
# E8.02 code 3 | ares' `$F1` (CONTROL) handler NEGATES the timer-2 counter reset:
# | if(timer0.enable.raise(data.bit(0))) { timer0.stage2 = 0; ... }
# | if(timer1.enable.raise(data.bit(1))) { timer1.stage2 = 0; ... }
# | if(!timer2.enable.raise(data.bit(2))) { timer2.stage2 = 0; ... }
# | ^ negated, and only here (ares/sfc/smp/io.cpp)
# | `raise()` is true on a 0->1 transition, so timers 0 and 1 reset ON a raise --
# | the documented behaviour -- while timer 2 resets on anything EXCEPT one. Both
# | rows enable timer 2 via `$F1`, run an interval, then write `$F1` again to STOP
# | it, and that stopping write has already zeroed T2OUT. Both read 0 where
# | RustySNES reads 10 and ~7. ares is internally inconsistent here, which is the
# | strongest evidence available that it is a stray `!`: its own timers 0 and 1 do
# | the un-negated version, and RustySNES, snes9x and Mesen2 all treat the three
# | identically. The first bug this cart has found in a REFERENCE emulator.
ARES_KNOWN_FAILURES=5
# | The row enables timer 2 via `$F1`, runs an interval, then writes `$F1` again to
# | STOP it, and that stopping write has already zeroed T2OUT -- so it reads 0.
# | ares is internally inconsistent here, which is the strongest evidence available
# | that it is a stray `!`: its own timers 0 and 1 do the un-negated version, and
# | RustySNES, snes9x and Mesen2 all treat the three identically. The first bug this
# | cart has found in a REFERENCE emulator.
#
# **`E3.06` used to be a sixth entry and is not any more, and it is worth saying why.** It failed on
# ares for a reason that turned out to be the CART's: it read `TnOUT` once at the end of the
# interval, and `TnOUT` is a four-bit read-and-clear counter, so the useful range was 8..15 with the
# wrap one tick above. Rewritten to POLL and accumulate -- and to assert the ratio rather than two
# absolute counts -- RustySNES and ares now report the identical 6 and 46. The row proves ares'
# timer 2 does run at 64 kHz; the old instrument could not see it. A ceiling in the instrument reads
# exactly like a defect in the thing measured.
ARES_KNOWN_FAILURES=4

# Where the built host lives. Not built by this script: it is a C++ link against ares' static libs
# and takes minutes, so it is opt-in via `scripts/accuracysnes/ares_host/build.sh` and this block
Expand Down
4 changes: 2 additions & 2 deletions tests/roms/AccuracySNES/ERROR_CODES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2640,8 +2640,8 @@ Provenance: **Documented** (SNESdev Wiki, SPC700 timers; fullsnes). Kind: scored

| Code | Byte | Meaning |
|---|---|---|
| 1 | `$02` | 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 |
| 2 | `$04` | 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. NOTE: this row cannot distinguish that from timer 2 having WRAPPED past 16, so read slot 267 before concluding anything; ares fails here with a recorded 0 |
| 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 |

### E3.08 — TEST bit 0 halts timers

Expand Down
51 changes: 29 additions & 22 deletions tests/roms/AccuracySNES/asm/tests_group_a.s
Original file line number Diff line number Diff line change
Expand Up @@ -22926,7 +22926,7 @@ CATALOG_IMPL = 1
rep #$30
.a16
.i16
lda #48
lda #69
sta f:V_APU_LEN
lda #$0200
sta f:V_APU_DEST ; APU RAM $0200: clear of the zero page and the stack
Expand Down Expand Up @@ -22973,11 +22973,6 @@ 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
; Record both counts before asserting on either. `TnOUT` is FOUR BITS, so timer 2's band
; below ends one tick short of its wrap -- and a core whose SMP runs slightly fast crosses
; it and reads a small number, which is indistinguishable from 'timer 2 is not running at
; 64 kHz' unless the raw counts are kept. ares fails this row, and without these the failure
; cannot be told from a real divergence.
rep #$30
.a16
.i16
Expand All @@ -22987,31 +22982,41 @@ CATALOG_IMPL = 1
sta f:$7EE414
lda f:$7E0102
and #$00FF
; record slot 267: E3.06 timer 2 ticks over the SAME interval (TnOUT wraps at 16)
; record slot 267: E3.06 timer 2 ticks, ACCUMULATED across polls (no 4-bit ceiling)
sta f:$7EE416
; Timer 0 first: one tick, maybe two. Zero would make the ratio below unmeasurable.
; Timer 0 first: the interval has to contain a couple of its ticks for a ratio to exist.
lda f:$7E0101
and #$00FF
cmp #$0001
bcs :+
jmp @fail1
:
cmp #$0004
cmp #$0007
bcc :+
jmp @fail1
:
; Timer 2 over the SAME interval: eight times the rate, so eight or more ticks. The band is
; 8..15 and `TnOUT` wraps at 16, which leaves NO headroom above -- and that is structural,
; not a choice: timer 0 must tick at least once for the ratio to mean anything, and one
; timer-0 period IS eight timer-2 periods, so timer 2 can never be below 8 and the wrap is
; only a factor of two away. Measured: RustySNES 10, ares 0 -- and 0 is what 16 reads as.
; The row, as a RATIO rather than two absolute counts. Asserting absolute numbers pins the
; poll loop's own cycle cost, which differs between cores for reasons that have nothing to
; do with the 64 kHz stage -- that is what made the previous version fail on ares while its
; `Timer<128>/Timer<128>/Timer<16>` declarations were an exactly correct 8:1.
lda f:$7E0101
and #$00FF
asl a
asl a
asl a ; timer 0 ticks x 8 = what timer 2 should have counted
sta f:$7E01F0
lda f:$7E0102
and #$00FF
cmp #$0008
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
and #$00FF
cmp #$0000
bcs :+
jmp @fail2
:
cmp #$0010
cmp #$000D
bcc :+
jmp @fail2
:
Expand All @@ -23029,14 +23034,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 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
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. NOTE: this row cannot distinguish that from timer 2 having WRAPPED past 16, so read slot 267 before concluding anything; ares fails here with a recorded 0
; 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
sep #$20
.a8
lda #$04
Expand Down Expand Up @@ -34143,10 +34148,12 @@ apu_prog_112:
.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_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
.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
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
Expand Down
Binary file modified tests/roms/AccuracySNES/build/accuracysnes-pal.sfc
Binary file not shown.
Binary file modified tests/roms/AccuracySNES/build/accuracysnes.sfc
Binary file not shown.
Loading
Loading