Skip to content

Commit 08361ad

Browse files
doublegateclaude
andauthored
fix(accuracysnes): E3.06 polls and accumulates; retract F1.10's "1-vs-3" (#310)
E3.06 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 unrelated to the 64 kHz stage -- and that is exactly what made the old row fail on ares while ares' own Timer<128>/Timer<128>/Timer<16> declarations were a 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. RETRACTION: F1.10 is not "1-vs-3 with RustySNES passing 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. Third instance of the same trap in one session, after E8.01's two rejected drafts and the scene field gate. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent d75a8ed commit 08361ad

7 files changed

Lines changed: 158 additions & 79 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Added
1313

14+
- **`E3.06` rewritten to poll and accumulate — and ares agrees with it exactly.** The row read
15+
`TnOUT` once at the end of the interval, and `TnOUT` is a four-bit read-and-clear counter, so the
16+
useful range was `8..15` with the wrap one tick above. That is not a band that can be widened; it
17+
is the instrument's ceiling. It now polls timer 2 every ~32 SPC cycles — one timer-2 period — so
18+
every read returns 0, 1 or 2 and the running sum cannot lose a tick however long the interval gets.
19+
20+
It also asserts the **ratio** rather than two absolute counts: `|T2 − 8·T0| ≤ 6`, computed on the
21+
65816. Absolute counts pin the poll loop's own cycle cost, which differs between cores for reasons
22+
that have nothing to do with the 64 kHz stage — and that is precisely what made the old row fail on
23+
ares while ares' own `Timer<128>`/`Timer<128>`/`Timer<16>` declarations were an exactly correct
24+
8:1.
25+
26+
RustySNES and ares now report the **identical** 6 and 46. `ARES_KNOWN_FAILURES` drops 5 → 4. **A
27+
ceiling in the instrument reads exactly like a defect in the thing measured**, which is the
28+
reusable half.
29+
30+
- **RETRACTION: `F1.10` is not "1-vs-3 with RustySNES alone", and its Mesen2 verdict is
31+
phase-fragile.** #306 published that reading from a measurement taken at the time. Then rewriting
32+
`E3.06` — an **unrelated APU row** — made Mesen2 **pass** `F1.10`. Three identical runs before,
33+
three identical runs after. The rewrite changed one uploaded program's length, which moved the
34+
cart's execution phase, which moved when `F1.10` samples `$4212` relative to the vblank edge.
35+
36+
So Mesen2's verdict on that row encodes where the cart happens to be, not only what Mesen2 models.
37+
snes9x and ares fail it stably; Mesen2 flips. `MESEN2_KNOWN_FAILURES` drops 2 → 1 (`F1.03` only),
38+
and the comment says not to restore an `F1.10` entry — either pin the row's sampling point, or
39+
accept that Mesen2's verdict on it carries no information.
40+
41+
**This is the same trap for the third time in one session**, after `E8.01`'s two rejected drafts
42+
and the scene field gate: a verdict that encodes an uncontrolled phase looks stable until something
43+
unrelated moves.
44+
1445
- **ares is wired into `crossval.sh` as a third reference.** `cross-validation: 3 reference(s) agree
1546
with the cart`, with `ARES_KNOWN_FAILURES=5` carrying the same per-row rationale the other two
1647
constants do: `C7.05`/`C7.10`/`F1.10` are rows snes9x already fails (ares corroborating it, not

‎scripts/accuracysnes/crossval.sh‎

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

215220
# The PAL image's own count: only F1.03 fails there, so F1.10 passes on PAL and fails on NTSC under
216221
# the same port-2 limitation. That asymmetry has NOT been explained and is recorded as an open
@@ -236,22 +241,30 @@ MESEN2_PAL_KNOWN_FAILURES=1
236241
# F1.10 code 2 | The auto-read start race. snes9x, Mesen2 AND ares all fail it — see the F1.10
237242
# | entry under MESEN2_KNOWN_FAILURES for why that leaves RustySNES passing alone.
238243
#
239-
# The other two share ONE cause, and it is a bug in ares rather than a disagreement:
244+
# The fifth is a bug in ares rather than a disagreement:
240245
#
241-
# E3.06 code 2 | ares' `$F1` (CONTROL) handler NEGATES the timer-2 counter reset:
242-
# E8.02 code 3 | if(timer0.enable.raise(data.bit(0))) { timer0.stage2 = 0; ... }
246+
# E8.02 code 3 | ares' `$F1` (CONTROL) handler NEGATES the timer-2 counter reset:
247+
# | if(timer0.enable.raise(data.bit(0))) { timer0.stage2 = 0; ... }
243248
# | if(timer1.enable.raise(data.bit(1))) { timer1.stage2 = 0; ... }
244249
# | if(!timer2.enable.raise(data.bit(2))) { timer2.stage2 = 0; ... }
245250
# | ^ negated, and only here (ares/sfc/smp/io.cpp)
246251
# | `raise()` is true on a 0->1 transition, so timers 0 and 1 reset ON a raise --
247252
# | the documented behaviour -- while timer 2 resets on anything EXCEPT one. Both
248-
# | rows enable timer 2 via `$F1`, run an interval, then write `$F1` again to STOP
249-
# | it, and that stopping write has already zeroed T2OUT. Both read 0 where
250-
# | RustySNES reads 10 and ~7. ares is internally inconsistent here, which is the
251-
# | strongest evidence available that it is a stray `!`: its own timers 0 and 1 do
252-
# | the un-negated version, and RustySNES, snes9x and Mesen2 all treat the three
253-
# | identically. The first bug this cart has found in a REFERENCE emulator.
254-
ARES_KNOWN_FAILURES=5
253+
# | The row enables timer 2 via `$F1`, runs an interval, then writes `$F1` again to
254+
# | STOP it, and that stopping write has already zeroed T2OUT -- so it reads 0.
255+
# | ares is internally inconsistent here, which is the strongest evidence available
256+
# | that it is a stray `!`: its own timers 0 and 1 do the un-negated version, and
257+
# | RustySNES, snes9x and Mesen2 all treat the three identically. The first bug this
258+
# | cart has found in a REFERENCE emulator.
259+
#
260+
# **`E3.06` used to be a sixth entry and is not any more, and it is worth saying why.** It failed on
261+
# ares for a reason that turned out to be the CART's: it read `TnOUT` once at the end of the
262+
# interval, and `TnOUT` is a four-bit read-and-clear counter, so the useful range was 8..15 with the
263+
# wrap one tick above. Rewritten to POLL and accumulate -- and to assert the ratio rather than two
264+
# absolute counts -- RustySNES and ares now report the identical 6 and 46. The row proves ares'
265+
# timer 2 does run at 64 kHz; the old instrument could not see it. A ceiling in the instrument reads
266+
# exactly like a defect in the thing measured.
267+
ARES_KNOWN_FAILURES=4
255268

256269
# Where the built host lives. Not built by this script: it is a C++ link against ares' static libs
257270
# and takes minutes, so it is opt-in via `scripts/accuracysnes/ares_host/build.sh` and this block

‎tests/roms/AccuracySNES/ERROR_CODES.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2640,8 +2640,8 @@ Provenance: **Documented** (SNESdev Wiki, SPC700 timers; fullsnes). Kind: scored
26402640

26412641
| Code | Byte | Meaning |
26422642
|---|---|---|
2643-
| 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 |
2644-
| 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 |
2643+
| 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 |
2644+
| 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 |
26452645

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

‎tests/roms/AccuracySNES/asm/tests_group_a.s‎

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22926,7 +22926,7 @@ CATALOG_IMPL = 1
2292622926
rep #$30
2292722927
.a16
2292822928
.i16
22929-
lda #48
22929+
lda #69
2293022930
sta f:V_APU_LEN
2293122931
lda #$0200
2293222932
sta f:V_APU_DEST ; APU RAM $0200: clear of the zero page and the stack
@@ -22973,11 +22973,6 @@ CATALOG_IMPL = 1
2297322973
; Release: the program hands the APU back to the IPL so the NEXT test can upload at all.
2297422974
lda #$A5
2297522975
sta APUIO0
22976-
; Record both counts before asserting on either. `TnOUT` is FOUR BITS, so timer 2's band
22977-
; below ends one tick short of its wrap -- and a core whose SMP runs slightly fast crosses
22978-
; it and reads a small number, which is indistinguishable from 'timer 2 is not running at
22979-
; 64 kHz' unless the raw counts are kept. ares fails this row, and without these the failure
22980-
; cannot be told from a real divergence.
2298122976
rep #$30
2298222977
.a16
2298322978
.i16
@@ -22987,31 +22982,41 @@ CATALOG_IMPL = 1
2298722982
sta f:$7EE414
2298822983
lda f:$7E0102
2298922984
and #$00FF
22990-
; record slot 267: E3.06 timer 2 ticks over the SAME interval (TnOUT wraps at 16)
22985+
; record slot 267: E3.06 timer 2 ticks, ACCUMULATED across polls (no 4-bit ceiling)
2299122986
sta f:$7EE416
22992-
; Timer 0 first: one tick, maybe two. Zero would make the ratio below unmeasurable.
22987+
; Timer 0 first: the interval has to contain a couple of its ticks for a ratio to exist.
2299322988
lda f:$7E0101
2299422989
and #$00FF
2299522990
cmp #$0001
2299622991
bcs :+
2299722992
jmp @fail1
2299822993
:
22999-
cmp #$0004
22994+
cmp #$0007
2300022995
bcc :+
2300122996
jmp @fail1
2300222997
:
23003-
; Timer 2 over the SAME interval: eight times the rate, so eight or more ticks. The band is
23004-
; 8..15 and `TnOUT` wraps at 16, which leaves NO headroom above -- and that is structural,
23005-
; not a choice: timer 0 must tick at least once for the ratio to mean anything, and one
23006-
; timer-0 period IS eight timer-2 periods, so timer 2 can never be below 8 and the wrap is
23007-
; only a factor of two away. Measured: RustySNES 10, ares 0 -- and 0 is what 16 reads as.
22998+
; The row, as a RATIO rather than two absolute counts. Asserting absolute numbers pins the
22999+
; poll loop's own cycle cost, which differs between cores for reasons that have nothing to
23000+
; do with the 64 kHz stage -- that is what made the previous version fail on ares while its
23001+
; `Timer<128>/Timer<128>/Timer<16>` declarations were an exactly correct 8:1.
23002+
lda f:$7E0101
23003+
and #$00FF
23004+
asl a
23005+
asl a
23006+
asl a ; timer 0 ticks x 8 = what timer 2 should have counted
23007+
sta f:$7E01F0
2300823008
lda f:$7E0102
2300923009
and #$00FF
23010-
cmp #$0008
23010+
sec
23011+
sbc f:$7E01F0 ; the signed error against the ideal ratio
23012+
clc
23013+
adc #$0006 ; biased by 6 so the allowed band is 0..12 rather than -6..+6
23014+
and #$00FF
23015+
cmp #$0000
2301123016
bcs :+
2301223017
jmp @fail2
2301323018
:
23014-
cmp #$0010
23019+
cmp #$000D
2301523020
bcc :+
2301623021
jmp @fail2
2301723022
:
@@ -23029,14 +23034,14 @@ CATALOG_IMPL = 1
2302923034
sta f:$7EE010
2303023035
jml test_restore
2303123036
@fail1:
23032-
; 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
23037+
; 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
2303323038
sep #$20
2303423039
.a8
2303523040
lda #$02
2303623041
sta f:$7EE010
2303723042
jml test_restore
2303823043
@fail2:
23039-
; 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
23044+
; 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
2304023045
sep #$20
2304123046
.a8
2304223047
lda #$04
@@ -34143,10 +34148,12 @@ apu_prog_112:
3414334148
.byte $C4, $F2, $E4, $F3, $C4, $F7, $E8, $5A, $C4, $F4, $E4, $F4
3414434149
.byte $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF
3414534150
apu_prog_113:
34146-
.byte $CD, $EF, $BD, $8F, $01, $FA, $8F, $01, $FC, $E4, $FD, $E4
34147-
.byte $FF, $8F, $85, $F1, $8D, $18, $FE, $FE, $8F, $80, $F1, $E4
34148-
.byte $FD, $C4, $F6, $E4, $FF, $C4, $F7, $E8, $5A, $C4, $F4, $E4
34149-
.byte $F4, $68, $A5, $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF
34151+
.byte $CD, $EF, $BD, $8F, $01, $FA, $8F, $01, $FC, $8F, $00, $10
34152+
.byte $8F, $00, $11, $E4, $FD, $E4, $FF, $8F, $85, $F1, $E4, $FF
34153+
.byte $C4, $12, $E4, $10, $60, $84, $12, $C4, $10, $AB, $11, $E4
34154+
.byte $11, $68, $18, $D0, $ED, $8F, $80, $F1, $E4, $FD, $C4, $F6
34155+
.byte $E4, $10, $C4, $F7, $E8, $5A, $C4, $F4, $E4, $F4, $68, $A5
34156+
.byte $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF
3415034157
apu_prog_114:
3415134158
.byte $CD, $EF, $BD, $8F, $01, $FA, $8F, $0B, $F0, $8F, $81, $F1
3415234159
.byte $8D, $00, $FE, $FE, $8F, $80, $F1, $E4, $FD, $C4, $F6, $8F
0 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)