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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
66 changes: 66 additions & 0 deletions scripts/accuracysnes/ares_host/README.md
Original file line number Diff line number Diff line change
@@ -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.
130 changes: 130 additions & 0 deletions scripts/accuracysnes/ares_host/ares_host.cpp
Original file line number Diff line number Diff line change
@@ -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 <ares/ares.hpp>
#include <sfc/sfc.hpp>
#include <mia/mia.hpp>
#include <nall/main.hpp>

#include <cstdio>
#include <cstring>
#include <cstdlib>

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<mia::Pak> system, game;
u32 frames = 0;

auto pak(ares::Node::Object node) -> std::shared_ptr<vfs::directory> 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<ares::Node::Input::Button>()) {
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 <rom.sfc> <frames>\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<ares::Node::Port>("Cartridge Slot")) {
port->allocate();
port->connect();
}
for(auto name : {"Controller Port 1", "Controller Port 2"}) {
if(auto port = root->find<ares::Node::Port>(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");
}
}
51 changes: 51 additions & 0 deletions scripts/accuracysnes/ares_host/build.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading