Skip to content

Commit ecbe10f

Browse files
doublegateclaude
andcommitted
test(accuracysnes): harden A3.03's stack; the oracle hypothesis was wrong
A3.03 hand-loads S and then runs assertions that need the stack, so on a core confining the stack differently the divergence lands in the return address rather than the verdict. The caller's S is now stashed and restored through long addressing before any assertion branches, and Y is parked through A because STY/LDY have no long form. That was also my hypothesis for the Mesen oracle hang, and it is WRONG: MesenCE still stops after 14 of 335 status bytes with the hardening in. The hardening is kept on its own merits -- the fragility is real regardless of host -- but it is not the fix and the docs no longer claim a root cause. Next, in order: verify the status array is written in catalogue order at all, since the whole index-to-test mapping rests on that and it has not been checked; then, if it is, find what A3.03 does that MesenCE does not return from, the stack no longer being a candidate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent aaf9f3e commit ecbe10f

6 files changed

Lines changed: 70 additions & 10 deletions

File tree

‎CHANGELOG.md‎

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

1212
### Added
1313

14-
- **AccuracySNES: the Mesen oracle's root cause — the battery hangs at `A3.03`.** Under MesenCE it
15-
completes **14 of 335** tests and stops; snes9x runs all 335. Index 14 is `A3.03` "PLD escapes,
16-
PLY not".
14+
- **AccuracySNES: the Mesen oracle's stopping point, and a hardened `A3.03`.** Under MesenCE the
15+
battery writes **14 of 335** status bytes and stops; snes9x runs all 335. By catalogue order index
16+
14 is `A3.03` "PLD escapes, PLY not" — but hardening its stack handling did **not** move the
17+
number, so that hypothesis is wrong and the root cause is still open.
1718

1819
This corrects two earlier conclusions. It is **not** the frame budget (4000 fails as 900 did), and
1920
it is **not** the Lua-to-emulator memory bridge — the bridge works, and is how the 14 was counted,
2021
by scanning the per-test status array at timeout. The magic never reading `ACSN` was a symptom of
2122
the battery never getting far enough to write it.
2223

23-
`A3.03` sets `S = $01FF`, enters emulation and executes `PLD`, asserting it escapes page 1. The
24-
test's own control flow then rides on that stack, so a core confining the stack differently
25-
produces a wrong *return address* rather than a wrong verdict, and the cart runs away instead of
26-
scoring. Fix direction recorded in `docs/accuracysnes-plan.md`: rows that hand-load `S` must
27-
restore a known-good stack before anything that returns.
24+
`A3.03` is hardened regardless, on its own merits: it hand-loads `S` and then runs assertions that
25+
need the stack, so on a core confining the stack differently the divergence would land in the
26+
return address rather than the verdict. The caller's `S` is now stashed and restored through long
27+
addressing before any assertion branches. Generalised in the plan — rows that hand-load `S` must
28+
restore a known-good stack before anything that returns — but it is not the oracle's fix.
2829

2930
- **AccuracySNES: the Mesen2 oracle diagnosed, and a stale frame budget aligned.** Three `v1.28.0`
3031
items ended at "no oracle can arbitrate", so the headless runner was investigated rather than

‎docs/accuracysnes-plan.md‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,8 +480,21 @@ not *impossible*.
480480

481481
### The Mesen oracle — the battery hangs at `A3.03` (`v1.29.0`)
482482

483-
**Root cause found.** Under MesenCE the battery completes **14 of 335** tests and stops; snes9x runs
484-
all 335. Index 14 is **`A3.03` "PLD escapes, PLY not"**.
483+
**Stopping point found; root cause NOT yet.** Under MesenCE the battery writes **14 of 335** status
484+
bytes and stops; snes9x runs all 335. By catalogue order index 14 is `A3.03` "PLD escapes, PLY not",
485+
which made its stack handling the obvious suspect.
486+
487+
**That hypothesis was tested and is wrong.** `A3.03` hand-loads `S` and then runs assertions that
488+
need the stack, so a core confining the stack differently would corrupt the return path rather than
489+
the verdict. It has been hardened accordingly — the caller's `S` is stashed and restored through long
490+
addressing before any assertion branches — and MesenCE still stops at **14**. The hardening is kept
491+
on its own merits (a row that hand-loads `S` and then asserts is fragile whatever the host does), but
492+
it is **not** the fix and must not be recorded as one.
493+
494+
What remains unknown is what actually stalls at or after index 14. Two things to check next, in
495+
order: whether the status array is written in catalogue order at all — the whole index-to-test
496+
mapping rests on that and it has not been verified — and, if it is, what `A3.03` does that MesenCE
497+
does not return from, since the stack is no longer a candidate.
485498

486499
Two earlier conclusions were wrong and are corrected here:
487500

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,9 +715,18 @@ CATALOG_IMPL = 1
715715
.i16
716716
; E=1, S=$01FF. PLD (new) pulls from $0200/$0201; PLY (old) pulls from $0100.
717717
; This pair is the cleanest test of the emulation-mode stack-confinement rule.
718+
;
719+
; BOTH halves hand-load S and then run an assertion, and an assertion needs the stack. On a
720+
; core that confines the stack differently the divergence lands in the RETURN ADDRESS, not
721+
; in the verdict, so the cart runs away instead of scoring -- and every later test is lost
722+
; with it. That is exactly what MesenCE did: the battery stopped here, 14 of 335 in, and the
723+
; host saw only an unattributed timeout. So the caller's S is stashed first and restored
724+
; through long addressing (no stack) the instant the measured value is safely in WRAM.
718725
rep #$30
719726
.a16
720727
.i16
728+
tsc
729+
sta f:$7E00C0 ; the caller's stack pointer, to be restored before any jsr/rts
721730
sep #$20
722731
.a8
723732
lda #$AB
@@ -745,11 +754,18 @@ CATALOG_IMPL = 1
745754
.a16
746755
.i16
747756
tdc
757+
sta f:$7E00C2 ; park the reading BEFORE touching the stack
758+
lda f:$7E00C0
759+
tcs ; stack restored: the assertion below can safely branch
760+
lda f:$7E00C2
748761
cmp #$CDAB
749762
beq :+
750763
jmp @fail1
751764
:
752765
; --- PLY: must NOT escape ---
766+
rep #$30
767+
.a16
768+
.i16
753769
lda #$0000
754770
tcd
755771
lda #$01FF
@@ -766,6 +782,13 @@ CATALOG_IMPL = 1
766782
rep #$30
767783
.a16
768784
.i16
785+
; STY/LDY have no long-addressing form, so the parking goes through A.
786+
tya
787+
sta f:$7E00C4 ; park Y before the stack is touched
788+
lda f:$7E00C0
789+
tcs ; and restore the caller's stack
790+
lda f:$7E00C4
791+
tay
769792
cpy #$005E
770793
beq :+
771794
jmp @fail2
0 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.

‎tests/roms/AccuracySNES/gen/src/tests/cpu.rs‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,7 +749,18 @@ fn a3_03() -> Test {
749749
let mut a = Asm::new();
750750
a.c("E=1, S=$01FF. PLD (new) pulls from $0200/$0201; PLY (old) pulls from $0100.");
751751
a.c("This pair is the cleanest test of the emulation-mode stack-confinement rule.");
752+
a.c("");
753+
a.c(
754+
"BOTH halves hand-load S and then run an assertion, and an assertion needs the stack. On a",
755+
);
756+
a.c("core that confines the stack differently the divergence lands in the RETURN ADDRESS, not");
757+
a.c("in the verdict, so the cart runs away instead of scoring -- and every later test is lost");
758+
a.c("with it. That is exactly what MesenCE did: the battery stopped here, 14 of 335 in, and the");
759+
a.c("host saw only an unattributed timeout. So the caller's S is stashed first and restored");
760+
a.c("through long addressing (no stack) the instant the measured value is safely in WRAM.");
752761
a.l("rep #$30");
762+
a.l("tsc");
763+
a.l("sta f:$7E00C0 ; the caller's stack pointer, to be restored before any jsr/rts");
753764
a.l("sep #$20");
754765
a.l("lda #$AB");
755766
a.l("sta f:$7E0200");
@@ -766,11 +777,16 @@ fn a3_03() -> Test {
766777
a.enter_native();
767778
a.l("rep #$30");
768779
a.l("tdc");
780+
a.l("sta f:$7E00C2 ; park the reading BEFORE touching the stack");
781+
a.l("lda f:$7E00C0");
782+
a.l("tcs ; stack restored: the assertion below can safely branch");
783+
a.l("lda f:$7E00C2");
769784
a.assert_a16(
770785
0xCDAB,
771786
"PLD did not pull from $0200/$0201 (it must escape page 1)",
772787
);
773788
a.c("--- PLY: must NOT escape ---");
789+
a.l("rep #$30");
774790
a.l("lda #$0000");
775791
a.l("tcd");
776792
a.l("lda #$01FF");
@@ -779,6 +795,13 @@ fn a3_03() -> Test {
779795
a.l("ply ; 8-bit index in emulation");
780796
a.enter_native();
781797
a.l("rep #$30");
798+
a.c("STY/LDY have no long-addressing form, so the parking goes through A.");
799+
a.l("tya");
800+
a.l("sta f:$7E00C4 ; park Y before the stack is touched");
801+
a.l("lda f:$7E00C0");
802+
a.l("tcs ; and restore the caller's stack");
803+
a.l("lda f:$7E00C4");
804+
a.l("tay");
782805
a.assert_y16(
783806
0x005E,
784807
"PLY escaped page 1 (old instructions must wrap to $0100)",

0 commit comments

Comments
 (0)