diff --git a/CHANGELOG.md b/CHANGELOG.md index 7590ee85..60315656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,26 @@ 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. + - **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' `hcounter(10) == (HTIME+1)<<2` — exact only while every dot is four clocks, which stopped being diff --git a/scripts/accuracysnes/ares_host/README.md b/scripts/accuracysnes/ares_host/README.md new file mode 100644 index 00000000..e49ff7d4 --- /dev/null +++ b/scripts/accuracysnes/ares_host/README.md @@ -0,0 +1,66 @@ +# A headless ares host for AccuracySNES — builds and links, does **not** yet run + +**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. + +## Why this exists + +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: + +- **`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. + +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. + +## What is established + +| | | +|---|---| +| 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 | + +Two traps `build.sh` records because each cost a round: + +- **`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. + +## What is left + +The crash is in setup, before any frame runs. The likely candidates, in order: + +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. + +Run it under a debugger and start at (1) — a null pak is the failure that would reach furthest +before dying. + +## Usage, once it works + +```bash +REF_PROJ=$PWD/ref-proj bash scripts/accuracysnes/ares_host/build.sh +/tmp/ares_host tests/roms/AccuracySNES/build/accuracysnes.sfc 900 +``` + +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. diff --git a/scripts/accuracysnes/ares_host/ares_host.cpp b/scripts/accuracysnes/ares_host/ares_host.cpp new file mode 100644 index 00000000..aac6f214 --- /dev/null +++ b/scripts/accuracysnes/ares_host/ares_host.cpp @@ -0,0 +1,130 @@ +// Headless ares host for the AccuracySNES cartridge — the third opinion the project has been +// blocked on. Reads the cart's results block out of WRAM and prints it in the same shape the +// snes9x libretro host does, so `crossval.sh` can consume it. +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +// The controller mask every AccuracySNES runner holds for the whole run (`asm/runtime.inc`). +constexpr u16 PAD_CONTRACT = 0x9050; // B + Start + X + R on controller 1 +constexpr u16 PAD2_CONTRACT = 0x60A0; // Y + Select + A + L on controller 2 + +// $4218 bit order, which is what the contract masks are expressed in. +auto held(u16 mask, const string& name) -> bool { + if(name == "B") return mask & 0x8000; + if(name == "Y") return mask & 0x4000; + if(name == "Select") return mask & 0x2000; + if(name == "Start") return mask & 0x1000; + if(name == "Up") return mask & 0x0800; + if(name == "Down") return mask & 0x0400; + if(name == "Left") return mask & 0x0200; + if(name == "Right") return mask & 0x0100; + if(name == "A") return mask & 0x0080; + if(name == "X") return mask & 0x0040; + if(name == "L") return mask & 0x0020; + if(name == "R") return mask & 0x0010; + return false; +} + +struct Headless : ares::Platform { + std::shared_ptr system, game; + u32 frames = 0; + + auto pak(ares::Node::Object node) -> std::shared_ptr override { + if(node->name() == "Super Famicom") return system->pak; + if(node->name() == "Super Famicom Cartridge") return game->pak; + return {}; + } + + auto video(ares::Node::Video::Screen, const u32*, u32, u32, u32) -> void override { + frames++; + } + + auto input(ares::Node::Input::Input input) -> void override { + // Which port a button belongs to is read from its ancestry, the same way desktop-ui does it. + if(auto button = input->cast()) { + u16 mask = PAD_CONTRACT; + auto wp = button->parent(); + if(!wp.expired()) { + if(auto device = wp.lock()) { + auto wpp = device->parent(); + if(!wpp.expired()) { + if(auto port = wpp.lock()) { + if(port->name().find("2")) mask = PAD2_CONTRACT; + } + } + } + } + button->setValue(held(mask, button->name())); + } + } +}; + +Headless platform_; + +} // namespace + +// nall owns `main` and calls this. Exit codes go through `exit()` rather than a return value. +namespace nall { +auto main(Arguments arguments) -> void { + if(arguments.size() < 2) { + printf("usage: ares_host \n"); + exit(2); + } + string rom = arguments[0]; + u32 budget = (u32)toNatural(arguments[1]); + + ares::platform = &platform_; + mia::setHomeLocation([]() -> string { return {Path::userData(), "ares/"}; }); + + platform_.game = mia::Medium::create("Super Famicom"); + if(platform_.game->load(rom) != successful) { + fprintf(stderr, "ares_host: could not load %s\n", (const char*)rom); + exit(3); + } + platform_.system = mia::System::create("Super Famicom"); + if(platform_.system->load() != successful) { + fprintf(stderr, "ares_host: could not load the Super Famicom system pak\n"); + exit(3); + } + + 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(auto port = root->find("Cartridge Slot")) { + port->allocate(); + port->connect(); + } + for(auto name : {"Controller Port 1", "Controller Port 2"}) { + if(auto port = root->find(name)) { + port->allocate("Gamepad"); + port->connect(); + } + } + root->power(); + + 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; + 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); + for(u32 i = 0; i < count && i < 512; i++) { + printf("status %u %02x\n", i, wram[RESULTS + 0x20 + i]); + } + printf("ACCURACYSNES-END\n"); +} +} diff --git a/scripts/accuracysnes/ares_host/build.sh b/scripts/accuracysnes/ares_host/build.sh new file mode 100755 index 00000000..5294094e --- /dev/null +++ b/scripts/accuracysnes/ares_host/build.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Build the headless ares host. See README.md — this does NOT yet run the cart successfully. +# +# The recipe is the deliverable: every step below was established by doing it, and the previous +# recorded state of this blocker was "ares would need building", which turned out to be wrong. +set -euo pipefail + +REF_PROJ=${REF_PROJ:-$PWD/ref-proj} +ARES=$REF_PROJ/ares +BUILD=${BUILD:-${TMPDIR:-/tmp}/ares-sfc-build} +OUT=${OUT:-${TMPDIR:-/tmp}/ares_host} +HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +[[ -d $ARES ]] || { echo "no ares clone at $ARES — set REF_PROJ" >&2; exit 1; } + +# Only the sfc core. The default core list builds a dozen consoles and is pure wall-clock here. +cmake -S "$ARES" -B "$BUILD" -G Ninja -DARES_CORES=sfc -DCMAKE_BUILD_TYPE=Release + +# `hiro` is not optional even for a headless host: `mia/mia.hpp` includes it, and its generated +# `resource/resource.hpp` only exists after hiro has been built once. +ninja -C "$BUILD" ares mia hiro ruby +ninja -C "$BUILD" \ + nall/nall/CMakeFiles/nall.dir/main.cpp.o \ + nall/nall/CMakeFiles/nall.dir/nall.cpp.o \ + nall/nall/CMakeFiles/nall.dir/sljitAllocator.cpp.o + +# `nall/main.hpp` must be included by THIS translation unit: it emits `::main` only when +# `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" \ + -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" \ + "$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" \ + "$BUILD/ruby/libruby.a" "$BUILD/hiro/libhiro.a" "$BUILD/ares/libares.a" "$BUILD/mia/libmia.a" \ + "$BUILD/thirdparty/chdr-static.a" \ + -lXrandr -lXrender -lXext -lX11 -lGLX -lOpenGL -lSDL3 -lrashader \ + $(pkg-config --libs gtk+-3.0) -lz \ + "$BUILD/ares/libares.a" "$BUILD/libco/liblibco.a" "$BUILD/thirdparty/tzxfile.a" \ + "$BUILD/thirdparty/chdr-static.a" \ + "$BUILD/thirdparty/libchdr/deps/miniz-3.1.1/miniz.a" \ + "$BUILD/thirdparty/libchdr/deps/zstd-1.5.7/zstd.a" "$BUILD/thirdparty/sljit.a" + +echo "built $OUT"