diff --git a/CHANGELOG.md b/CHANGELOG.md index 60315656..a414b03b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,25 +11,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **A headless ares host: builds and links, does not yet run — and that changes the status of the - project's most-cited blocker.** Several findings are stuck at 2-versus-1 with no tiebreaker, - because this project's provenance rule counts ares and bsnes as *one* reference: `A2.10`, and the - OBJ-interlace field parity where RustySNES's `row + field` and `$213F` bit 7 are both ares'. Both - name the same missing thing — ares actually running the cart — and the recorded reason it had not - happened was that ares would have to be built and had no headless mode. - - The first half of that is now known to be cheap. `-DARES_CORES=sfc` configures and builds the SFC - core standalone in a couple of minutes; `ares::Platform` is a small interface whose methods all - have no-op defaults, so a headless host is ~120 lines; it links; and the cart's results block is - reachable by construction at `ares::SuperFamicom::cpu.wram[0xF000 + n]`. What remains is a crash - during setup — a bounded debugging job, not a feasibility question. - - Committed under `scripts/accuracysnes/ares_host/` **labelled as incomplete**, with the build - recipe and the two traps that each cost a round: `hiro` is not optional for a headless host - (`mia/mia.hpp` includes it and its generated resource header does not exist until hiro is built - once), and `nall/main.hpp` must be included by the host translation unit or the link fails with a - bare `undefined reference to 'main'` out of `crt1.o`. The README names the three likely causes of - the crash in the order worth trying. +- **A headless ares host — and `A2.10` is settled.** Several findings sat at 2-versus-1 with no + tiebreaker, because this project's provenance rule counts ares and bsnes as *one* reference, so + "RustySNES and snes9x against Mesen2" is only 2-vs-1 if ares is not already on RustySNES's side — + and nobody could check. `scripts/accuracysnes/ares_host/` now can: + + ``` + magic ACSN / done a5 / count 338 / passed 299 / failed 5 / skipped 1 / golden 33 + ``` + + Reproducible across runs. **ares passes `A2.10`** ("PEI does not page-wrap", catalogue index 11, + status `$01`), so with RustySNES and snes9x it is **3 against 1 with Mesen2 the outlier**, and the + row comes off the "unexplained, needs a fourth opinion" list it has been on since the oracle fix. + + **Five rows where ares disagrees with the cart** — `C7.05`, `C7.10`, `E8.02`, `E3.06`, `F1.10` — + are recorded and **not** adjudicated. `F1.10` is suspect-of-the-host first: it is a + `PAD2_CONTRACT` row and this host's port detection is assumed rather than verified. Not wired into + `crossval.sh` for that reason — an `ARES_KNOWN_FAILURES` constant encoding unexamined + disagreements would be worse than no third reference. + + Three setup steps turned out to be mandatory and each cost a round, because all three fail as the + *same* segfault inside `System::load` with a backtrace pointing at memory setup rather than at + what is missing: `ares::Memory::FixedAllocator::get()` before anything touches a core (`Bus::reset` + allocates from it); `ares::SuperFamicom::option("Pixel Accuracy", "true")` before `load` + (`PPUBase::implementation` is null until `setAccurate` picks a PPU, and `Bus::reset` calls + `ppu.map()`); and `nall/main.hpp` included by the host translation unit, without which the *link* + fails with a bare `undefined reference to 'main'` from `crt1.o`. - **The H-IRQ comparator moves into the clock domain (`T-06-A`), and nothing below the long dots moves with it.** `HIRQ_TRIGGER_DELAY = 4` was a *dot-domain rounding* of ares' diff --git a/docs/accuracysnes-plan.md b/docs/accuracysnes-plan.md index e9f23489..dc3085b5 100644 --- a/docs/accuracysnes-plan.md +++ b/docs/accuracysnes-plan.md @@ -843,7 +843,7 @@ before writing the row. disagreement in the battery, and a row batch authored while one row is unexplained risks attributing a new failure to the wrong cause. -#### First arbitrated finding: `A2.10` — Mesen2 is the outlier, 2-vs-1 +#### First arbitrated finding: `A2.10` — Mesen2 is the outlier, and ares makes it 3-vs-1 The moment the oracle started working it produced a result the project has never been able to see. `crossval.sh` reports `Mesen2: 1 failing test(s)`, catalogue index **11 = cart `A2.10`, "PEI does not @@ -855,16 +855,22 @@ divergences. So this is **2-vs-1 with Mesen2 as the outlier**, which inverts the standing heuristic warns that *RustySNES failing alone* means a real bug, and that is not what this is. -**Do not yet record it as a known-good Mesen2 divergence.** That would need a third opinion — ares or -bsnes on the same row — and the heuristic's own caveat applies with force here: a harness bug -upstream of an implementation produces exactly this signature, and one already did once in this -project (the `$F8`/`$F9` retraction). The cheap next step is to run the row against ares or bsnes. If -they agree with RustySNES and snes9x, add a `MESEN2_KNOWN_FAILURES` entry mirroring the snes9x one, -with the rationale, so `crossval.sh` reports AGREEMENT instead of `DISAGREEMENT` and stops masking -future real divergences behind a known one. - -Until then `crossval.sh`'s `DISAGREEMENT` verdict is **correct and should stay** — one genuinely -unexplained row is exactly what it exists to report. +**SETTLED 2026-08-01 — ares passes it, so this is 3-vs-1.** The third opinion the paragraph below +asked for now exists: `scripts/accuracysnes/ares_host/` runs the battery under ares +(`magic ACSN`, `done a5`, 338 tests, reproducible across runs) and reports catalogue index 11 as +status `$01` — **pass**. With RustySNES and snes9x that is three implementations against Mesen2, and +the caveat that held this open — a harness bug upstream of an implementation produces the same +signature, as the `$F8`/`$F9` retraction proved — is answered: the fourth host is independent of all +three and agrees. + +`A2.10` can therefore become a `MESEN2_KNOWN_FAILURES` entry whenever the count is next revised. It +is not being added in the same change that established it, because `MESEN2_KNOWN_FAILURES` currently +reads 2 and matches; changing the constant and the reasoning at once would leave nothing to check +the change against. + +*The original reasoning, kept because it is why the row waited:* recording it as a known-good Mesen2 +divergence without a third opinion would have been exactly the mistake the `$F8`/`$F9` retraction +already cost this project once. #### The Mesen oracle — the "14 of 335" reading does not reproduce diff --git a/scripts/accuracysnes/ares_host/README.md b/scripts/accuracysnes/ares_host/README.md index e49ff7d4..7668b631 100644 --- a/scripts/accuracysnes/ares_host/README.md +++ b/scripts/accuracysnes/ares_host/README.md @@ -1,66 +1,84 @@ -# A headless ares host for AccuracySNES — builds and links, does **not** yet run +# A headless ares host for AccuracySNES — the third opinion -**Status: incomplete infrastructure, committed deliberately.** `build.sh` produces a linked binary; -running it against the cart dumps core during setup. What is finished is the part that was recorded -as the blocker, and what remains is a bounded debugging job rather than a feasibility question. +**Working.** It runs the battery and reports the results block: -## Why this exists +``` +$ REF_PROJ=$PWD/ref-proj bash scripts/accuracysnes/ares_host/build.sh +$ /tmp/ares_host tests/roms/AccuracySNES/build/accuracysnes.sfc 900 +ACCURACYSNES-BEGIN +magic ACSN +done a5 +count 338 +passed 299 +failed 5 +skipped 1 +golden 33 +status 0 01 +... +``` -Several findings are stuck at **2 versus 1** with no way to break the tie, because this project's -own provenance rule counts ares and bsnes as **one** reference: +Reproducible: two runs give the identical tally. -- **`A2.10`** ("PEI does not page-wrap") — Mesen2 fails it, RustySNES and snes9x pass. Recorded as - *not* settled precisely because a harness bug upstream of every implementation produces the same - signature, and one did once (the `$F8`/`$F9` retraction). -- **OBJ/screen interlace field parity** — RustySNES draws one field's source rows, snes9x and - Mesen2 the other. RustySNES's `row + field` and its `$213F` bit 7 are both *ares'*, so this is the - bsnes/ares lineage against the other two rather than RustySNES alone. +## Why it exists -Both name the same missing thing: **ares actually running the cart.** The recorded reason it had not -happened was that ares would have to be built and had no headless mode. The first half of that is -now known to be cheap; only the second half is real. +Several findings sat at **2 versus 1** with no way to break the tie, because this project's +provenance rule counts ares and bsnes as **one** reference — so "RustySNES and snes9x against +Mesen2" is only 2-vs-1 if ares is not already on RustySNES's side, and nobody could check. -## What is established +**The first thing it settled: `A2.10` ("PEI does not page-wrap").** ares **passes** it (catalogue +index 11, status `$01`). With RustySNES and snes9x also passing, that is **3 against 1 with Mesen2 +the outlier**, and the row comes off the "unexplained, needs a fourth opinion" list. -| | | -|---|---| -| ares' SFC core builds standalone | **yes** — `-DARES_CORES=sfc`, ~76 targets, a couple of minutes | -| a headless host compiles against it | **yes** — `ares::Platform` is a small interface whose methods all have no-op defaults | -| it links | **yes** — see `build.sh` for the library set and the two non-obvious traps | -| the results block is reachable | **yes, by construction** — `ares::SuperFamicom::cpu.wram[0xF000 + n]` is the cart's `$7E:F000` | -| it runs the cart | **no** — dumps core during setup | +## The five rows ares disagrees with the cart about -Two traps `build.sh` records because each cost a round: +New information, and **not yet adjudicated** — the cart, snes9x and Mesen2 all pass these: -- **`hiro` is not optional for a headless host.** `mia/mia.hpp` includes it, and its generated - `resource/resource.hpp` does not exist until hiro has been built once. -- **`nall/main.hpp` must be included by the host translation unit.** It emits `::main` only when - `NALL_MAIN_IMPL` is undefined, and nall's own `main.cpp.o` defines that. Omit the include and the - link fails with a bare `undefined reference to 'main'` out of `crt1.o`, which reads like a missing - object file rather than a missing shim. +| idx | row | code | +|---:|---|---:| +| 116 | `C7.05` | 1 | +| 120 | `C7.10` | 1 | +| 265 | `E8.02` | 3 | +| 276 | `E3.06` | 2 | +| 288 | `F1.10` | 2 | -## What is left +**Treat `F1.10` as suspect-of-this-host first.** It is a `PAD2_CONTRACT` row, and this host's port +detection (`port->name().find("2")` on the button's grandparent) is *assumed* to work, not verified. +Check that before concluding anything about ares. -The crash is in setup, before any frame runs. The likely candidates, in order: +The standing heuristic — three implementations agreeing usually means a broken test, one disagreeing +usually means a real bug — cuts an unfamiliar way here: it is **ares** alone, on rows the other three +pass. -1. `mia::System::create("Super Famicom")->load()` needs a system pak that `mia` looks for under the - home location — `setHomeLocation` here points at `~/.local/share/ares/`, which may not exist. - desktop-ui populates it on first run. -2. The `Cartridge Slot` port is allocated with no argument; desktop-ui passes the medium and checks - the returned node. -3. Controller ports are allocated by name `"Gamepad"`; the actual node name should be confirmed - against `ares/sfc/controller/controller.cpp` rather than assumed. +## Three setup steps that are not optional, each of which cost a debugging round -Run it under a debugger and start at (1) — a null pak is the failure that would reach furthest -before dying. +All three fail as a **segfault inside `System::load`**, with a backtrace pointing at memory setup +rather than at what is actually missing. -## Usage, once it works +1. **`ares::Memory::FixedAllocator::get()` before anything touches a core.** `Bus::reset()` + allocates its page tables from that bump allocator. desktop-ui does this on its first line. +2. **`ares::SuperFamicom::option("Pixel Accuracy", "true")` before `load`.** `PPUBase::implementation` + is null until `setAccurate` picks one of the two PPUs, and `Bus::reset()` calls `ppu.map()` → + `implementation->map()`. `"true"` selects the **accurate** PPU, the only one worth + cross-validating against. +3. **`nall/main.hpp` included by this translation unit.** It emits `::main` only when + `NALL_MAIN_IMPL` is undefined, and nall's own `main.cpp.o` defines that. Omit it and the *link* + fails with a bare `undefined reference to 'main'` from `crt1.o`, which reads like a missing object + file rather than a missing shim. -```bash -REF_PROJ=$PWD/ref-proj bash scripts/accuracysnes/ares_host/build.sh -/tmp/ares_host tests/roms/AccuracySNES/build/accuracysnes.sfc 900 -``` +Plus one build fact: **`hiro` is not optional even headless** — `mia/mia.hpp` includes it, and its +generated `resource/resource.hpp` does not exist until `ninja hiro` has run once. + +`ptrace` is denied in this sandbox, so gdb cannot attach. The host installs its own `SIGSEGV` +handler and prints a backtrace; resolve the frames with `addr2line -Cfe /tmp/ares_host 0x…`. + +## Results-block offsets + +From `asm/runtime.inc`, and easy to get wrong by one field: `R_COUNT` is `+$06`, `R_PASSED` is +`+$0A`. Reading the latter as the former reported "count 299" for a 338-test battery, which looks +like a truncated run rather than a misread field. + +## Not yet wired into `crossval.sh` -Output is the same `magic` / `done` / `count` / `status N XX` shape the snes9x libretro host emits, -so `crossval.sh` can consume it as a third reference with a `ARES_KNOWN_FAILURES` constant beside -the existing two. +Deliberately. Adding a third reference means an `ARES_KNOWN_FAILURES` constant, and that constant +must not be written until the five rows above are adjudicated — a known-failure count that encodes +unexamined disagreements is worse than no third reference at all. diff --git a/scripts/accuracysnes/ares_host/ares_host.cpp b/scripts/accuracysnes/ares_host/ares_host.cpp index aac6f214..723707d9 100644 --- a/scripts/accuracysnes/ares_host/ares_host.cpp +++ b/scripts/accuracysnes/ares_host/ares_host.cpp @@ -9,6 +9,25 @@ #include #include #include +#include +#include +#include + +namespace { + +// A crash here is a setup mistake in this file, not an ares bug, and this sandbox cannot run a +// debugger (`ptrace` is denied), so the backtrace has to come from inside the process. Build with +// `-rdynamic` — `build.sh` does — or the frames come back as bare addresses that `addr2line -Cfe` +// still resolves. +auto crashHandler(int sig) -> void { + void* frames[40]; + int n = backtrace(frames, 40); + fprintf(stderr, "\nares_host: signal %d — backtrace follows\n", sig); + backtrace_symbols_fd(frames, n, 2); + _exit(9); +} + +} // namespace namespace { @@ -78,9 +97,17 @@ auto main(Arguments arguments) -> void { printf("usage: ares_host \n"); exit(2); } + bool verbose = (bool)arguments.take("--verbose"); string rom = arguments[0]; u32 budget = (u32)toNatural(arguments[1]); + // MUST come before anything touches a core. `Bus::reset()` allocates its page tables from this + // bump allocator, and without the first `get()` to construct it the very first load segfaults + // inside `System::load` with no hint that memory setup was the problem. desktop-ui does the same + // thing on its first line for the same reason. + signal(SIGSEGV, crashHandler); + signal(SIGABRT, crashHandler); + ares::Memory::FixedAllocator::get(); ares::platform = &platform_; mia::setHomeLocation([]() -> string { return {Path::userData(), "ares/"}; }); @@ -95,11 +122,21 @@ auto main(Arguments arguments) -> void { exit(3); } + if(verbose) fprintf(stderr, "ares_host: loaded the game and system paks\n"); + + // MUST come before `load`. `PPUBase::implementation` is null until `setAccurate` picks one of the + // two PPU implementations, and `Bus::reset()` calls `ppu.map()` -> `implementation->map()` during + // `System::load`. Without this the first load segfaults in `Bus::reset` with a backtrace that + // points at memory setup rather than at an unselected PPU. desktop-ui passes its own setting here. + // "true" selects the ACCURATE PPU, which is the only one worth cross-validating against. + ares::SuperFamicom::option("Pixel Accuracy", "true"); + ares::Node::System root; if(!ares::SuperFamicom::load(root, "[Nintendo] Super Famicom (NTSC)")) { fprintf(stderr, "ares_host: ares::SuperFamicom::load failed\n"); exit(3); } + if(verbose) fprintf(stderr, "ares_host: ares::SuperFamicom::load returned\n"); if(auto port = root->find("Cartridge Slot")) { port->allocate(); port->connect(); @@ -110,18 +147,28 @@ auto main(Arguments arguments) -> void { port->connect(); } } + if(verbose) fprintf(stderr, "ares_host: ports connected\n"); root->power(); + if(verbose) fprintf(stderr, "ares_host: powered on\n"); while(platform_.frames < budget) root->run(); // The results block, straight out of WRAM. $7E:F000 is WRAM offset $F000. const auto& wram = ares::SuperFamicom::cpu.wram; const u32 RESULTS = 0xF000; + // Offsets are `asm/runtime.inc`'s, and they are easy to get wrong by one field: R_COUNT is +$06 + // and R_PASSED is +$0A. Reading the latter as the former reported "count 299" for a 338-test + // battery, which looks like a truncated run rather than a misread field. + auto rd16 = [&](u32 off) -> u32 { return wram[RESULTS + off] | (wram[RESULTS + off + 1] << 8); }; + u32 count = rd16(0x06); printf("ACCURACYSNES-BEGIN\n"); printf("magic %c%c%c%c\n", wram[RESULTS], wram[RESULTS + 1], wram[RESULTS + 2], wram[RESULTS + 3]); printf("done %02x\n", wram[RESULTS + 0x08]); - u32 count = wram[RESULTS + 0x0A] | (wram[RESULTS + 0x0B] << 8); printf("count %u\n", count); + printf("passed %u\n", rd16(0x0A)); + printf("failed %u\n", rd16(0x0C)); + printf("skipped %u\n", rd16(0x0E)); + printf("golden %u\n", rd16(0x10)); for(u32 i = 0; i < count && i < 512; i++) { printf("status %u %02x\n", i, wram[RESULTS + 0x20 + i]); } diff --git a/scripts/accuracysnes/ares_host/build.sh b/scripts/accuracysnes/ares_host/build.sh index 5294094e..a5cf7c68 100755 --- a/scripts/accuracysnes/ares_host/build.sh +++ b/scripts/accuracysnes/ares_host/build.sh @@ -28,14 +28,14 @@ ninja -C "$BUILD" \ # `NALL_MAIN_IMPL` is undefined, and nall's own `main.cpp.o` defines that. Without the include the # link fails with a bare `undefined reference to 'main'` from crt1.o, which reads like a missing # object rather than a missing shim. -g++ -std=c++2b -O2 -c "$HERE/ares_host.cpp" -o "$BUILD/ares_host.o" \ +g++ -std=c++2b -O2 -g -rdynamic -c "$HERE/ares_host.cpp" -o "$BUILD/ares_host.o" \ -I"$ARES" -I"$ARES/ares" -I"$ARES/nall" -I"$ARES/libco" -I"$ARES/thirdparty" \ -I"$BUILD" -I"$BUILD/ares" \ $(pkg-config --cflags gtk+-3.0) -DHIRO_GTK=3 # The library set and order are cribbed from desktop-ui's own link line in `build.ninja`; ares is # listed twice there and needs to be here too. -g++ -O2 -o "$OUT" "$BUILD/ares_host.o" \ +g++ -O2 -g -rdynamic -o "$OUT" "$BUILD/ares_host.o" \ "$BUILD/nall/nall/CMakeFiles/nall.dir/main.cpp.o" \ "$BUILD/nall/nall/CMakeFiles/nall.dir/nall.cpp.o" \ "$BUILD/nall/nall/CMakeFiles/nall.dir/sljitAllocator.cpp.o" \