jevsnes.git / apps / native / README.md
README.mdpreviewREADME.mdsource223 lines · 12.1 KB · raw
1# Chapter 12: native, the window
2
3Everything so far comes together here: the SNES in a window, played by the
4zbanks/alttp C bot, with Jev at its goal choice if you ask, and panels around
5the picture saying what is going on. The picture is never drawn over, so
6nothing covers the game's own HUD.
7
8```bash
9nix develop -c buck2 run //apps/native:native -- \
10  "roms/Legend of Zelda, The - A Link to the Past (USA).sfc"
11```
12
13With `--jev` (or `ZB_JEV=1`) Jev makes the bot's goal choice (chapter 5),
14which needs the key in the window's environment:
15
16```bash
17op-env-run -- nix develop -c buck2 run //apps/native:native -- --jev \
18  "roms/Legend of Zelda, The - A Link to the Past (USA).sfc"
19```
20
21Without `--jev` nothing in the window asks Jev and the bot plays exactly as
22upstream's. With it and no key, it plays the same and the Jev panel says why
23Jev is off. Each question blocks the frame loop for its round trip (about
24150 ms measured), so the game pauses for a blink while Jev thinks.
25
26> **Aside: you can take the controller.** Every frame the window hands the
27> bot the human's pad (your keyboard, plus anything an MCP `press` holds)
28> and holds whatever pad the bot returns, exactly the hook upstream's
29> Snes9x used. The bot zeroes the pad and plays, except that **holding X
30> (the `S` key) makes it step aside and pass your pad through**: upstream's
31> own manual override (`alttp.c:57`). The Bot panel says when it has stepped
32> aside.
33
34| Key | Pad |
35| --- | --- |
36| Arrows | D-pad |
37| `X` / `Z` | A / B |
38| `S` / `A` | X / Y |
39| `Q` / `W` | L / R |
40| Enter / Backspace | Start / Select |
41| `-` / `=` | Playback speed down / up |
42
43## What happens before the window opens
44
451. The ROM is patched the way the Randomizer the bot was written for differs
46   from vanilla where the bot can tell (`zbanks::rando::patch_rom`).
472. The bot starts: `ap_init` caches its RAM pointers and loads the states
48   `home` and `hpegs` from `<rom>.states/`. Make `home` once with chapter
49   11's `--make-home home --states "roms/<rom>.states"`. Without it the bot
50   starts at power-on, where it presses nothing, and the log says so.
513. Unless `--fresh`, the machine then goes back to `resume`, the state the
52   last window kept, as a Snes9x user loading their own state after the bot
53   was up.
54
55The bot's printf trace goes to `run/zbanks-window/bot.log` (this process's
56stdout is a pipe that a thread copies into that file, rotated at 50 MB with
57three old files kept). Its debug files (`goals.txt`, `map`, `map.pbm`,
58`full_map.dot`, ...) go beside it. The window's own messages are stderr.
59
60## The panels
61
62- **Bot**: the bot's own line (`ap_info_string`, what upstream's Snes9x drew
63  over the picture), the frame, where it puts Link, its task list (current
64  first, green) and its goals with the score the planner last gave each
65  (dim: unsatisfiable or over the limit; green: complete), `assert_bp` traps
66  it has stepped over and reads of unmapped memory (red). A red "STALLED"
67  line when the bot has stopped playing, and under it what the bounded
68  recovery is doing about it: amber while it is still trying, red once it
69  has given up for good.
70- **MCP**: the server's address, a light (amber: listening; green: a client
71  has been heard from; red: the port would not bind, with the reason), and
72  a log of every tool call with its arguments and outcome.
73- **State**: the game in words, from chapter 3.
74- **Jev**: whether it is on; the shared spend guards in one line (questions
75  in the last minute against the bucket of 30, every process's; dollars in
76  the last hour; spent so far, with no cap); in amber, whatever is stopping
77  questions right now ("throttled: bucket empty, next question in 12s"
78  clears by itself, "out of credit (API)" does not); the totals; a usage
79  graph by game frame across the current recording, with a playhead that
80  follows the scrubber; and a newest-first history of every choice this
81  window made, plus the last window's (loaded from
82  `run/zbanks-window/jev.jsonl`). Click a row to see every option in the
83  words Jev was given, its probability, the pick (`>`) and upstream's own
84  (`u`), and for a choice that asked, the exact request, tokens, latency and
85  cost.
86- **Pad**: the pad the console held on the last frame (the bot's), lit; a
87  button an MCP `press` holds carries its confidence as a percentage.
88- **Scrubber**: directly under the picture: play/pause, skip to end, a speed
89  control (0.25x to 8x), "run the bot from here", and a timeline bar to click
90  or drag. Marks show where other branches forked off.
91
92Bot and MCP share the left column; State and Jev share the right. The layout
93is one function of the window's size (chapter 7).
94
95## The replay timeline
96
97Every emulated frame of live play is recorded (chapter 6) to
98`run/zbanks-window/replay/<recording-id>/`, and the scrubber turns that into
99VCR controls:
100
101- **Click or drag the bar** to jump to any recorded frame and pause there:
102  the nearest keyframe, then the log replayed forward, headless. The bot is
103  never asked anything while scrubbing.
104- **Play** replays the log from a paused frame; running off the end hands
105  back to the live bot at exactly the frame it had reached, byte for byte,
106  so nothing is reconstructed and nothing stutters.
107- **Speed** scales both replay and live play; sound mutes at anything but
108  1x, and the choice survives a restart.
109- **Run the bot from here** restarts the window with `--branch-from`, which
110  replays the whole history up to that frame through a *fresh* bot (a second
111  one cannot exist in this process) before going live as a new branch. The
112  scrubber shows "rebuilding bot: N/M frames" meanwhile. The old future is
113  kept.
114
115> **Try it.** Let the bot play for a minute, drag the scrubber back thirty
116> seconds, and press play. Then try "run the bot from here" and watch the
117> new branch's mark appear on the bar.
118
119## Restarting without losing the game
120
121A snapshot is the whole machine (1.3 MB), kept as `<rom>.states/<name>.state`
122beside the ROM (gitignored). The window keeps one for itself, `resume`: every
12330 seconds, on exit, and before a `restart`. It resumes from it unless given
124`--fresh`, and a `restart` drops `--fresh` from the arguments it re-runs
125with, so it carries on the game.
126
127The bot's state is *not* in a snapshot: it is the C bot's globals, and a
128restart starts a new bot. What it learned of the map is carried the way
129upstream carried it, as a map file: whenever the window keeps `resume` it
130also exports the bot's map to `run/zbanks-window/map_export.txt` (written to
131a `.part` name and renamed, so a killed window leaves the last whole one),
132and a window starting copies that to `map.19.txt`, which the new bot's first
133tick imports. Each goal's outcome is carried too (chapter 4's
134`goal_outcomes.jsonl`); the goal list itself is rebuilt from the map.
135
136A snapshot names the core version and the cartridge it was taken from, and
137is refused by any other; the window then plays from `home` and says why. The
138cartridge here is the ROM as patched for the bot, so snapshots of the
139unpatched image are refused.
140
141The cartridge save is read from and written to a `.sav` beside the ROM
142(gitignored), every few seconds and on exit. `--sound` turns audio on; it is
143off by default because under WSLg it cuts in and out (heard 2026-09-20).
144
145## MCP: the window answers questions
146
147The window serves MCP (chapter 8) over streamable HTTP at
148`http://127.0.0.1:7637/mcp`. A client does not connect to that. It starts
149`native mcp`, the same binary with no window, which serves MCP on stdio and
150passes everything on to whichever window is running; `../../.mcp.json`
151starts it through `../../tools/mcp/launch.sh` (chapter 25).
152
153| Always | What |
154| --- | --- |
155| `state` | The game state, as JSON. |
156| `bot` | What the bot says about itself, as JSON: info line, tasks, the first goals with their scores, goal count, where it puts Link, traps, stray reads, and the savestate calls `ap_init` made. |
157| `jev_history` | Jev's recent goal-choice history, newest first, as JSON: what the Jev panel's history shows, with the exact request sent when it asked. |
158| `frame` | The game picture as a PNG, at the console's resolution. |
159| `window` | The whole window as drawn, panels included. |
160| `read_wram` | Bytes of work RAM by SNES address, as hex. |
161| `states` | The names of the snapshots kept beside the ROM. |
162| `budget` | Jev's spend guards from the shared ledger: spent so far, with no cap; the question bucket (and seconds until it refills); the hour breaker (and seconds until it reopens); holds in flight; whether the vendor says the account is out of credit. Nothing in it needs clearing by hand. |
163| `dev_mode` | `on: true` adds the tools below; `false` takes them away. |
164| `patch_info` | This process's pid and `subsecond::aslr_reference()`, for `tools/hotpatch/patch.sh`. |
165
166| Dev mode only | What |
167| --- | --- |
168| `press` | Hold buttons for a number of emulated frames, then return the state. The buttons go to the bot as the human's pad, so they reach the game only while X is among them (the bot steps aside) or the bot is in a mode it does not play. Optional `confidence`, 0 to 1. |
169| `save_state` | Keep the machine exactly as it stands, under a name. |
170| `load_state` | Become the machine kept under a name. The bot carries on. |
171| `power_cycle` | Turn the SNES off and on again; the battery save is kept. |
172| `hot_patch` | Apply a `tools/hotpatch`-built patch (a path to its `patch-N.json`): no restart, the game keeps running. Rust host code only. |
173| `restart` | Re-run the binary this window was started from, to pick up a new build. |
174
175## Changing the code while it runs
176
177```bash
178nix develop
179tools/hotpatch/patch.sh //apps/native:native
180```
181
182rebuilds the window's own code (`//apps/native:app`: `src/app.rs`,
183`src/activity.rs`) and `packages/panels`, builds a patch from the fresh
184objects (chapter 27), and delivers it over MCP: dev mode on, `hot_patch`,
185dev mode off. The picture and the panels are redrawn on the very next frame,
186same process, same game. The executable itself is never rebuilt for this:
187`src/main.rs` is a three-line dispatcher with no patch points, so a panels
188edit never pays for relinking the whole eframe/wgpu binary.
189
190Most edits to `logic`/`ui`'s bodies and to `packages/panels` patch. Changing
191the *shape* of `App`, or of a struct crossing the patch boundary, does not,
192and needs a `restart`. The C bot is not hot-patchable at all: a change there
193is a `buck2 build` and a `restart`. The window closes and reopens about a
194second later on the same frame of the same game, and the client is still
195connected, because what it is connected to is `native mcp`.
196
197## For the people who maintain it
198
199Left running in the background, start it so that its end is written down
200whatever the cause (a window once vanished with no trace, 2026-09-21):
201
202```bash
203nohup nix develop -c sh -c 'buck2 run //apps/native:native -- \
204  "roms/Legend of Zelda, The - A Link to the Past (USA).sfc"; \
205  echo "native exited with status $? at $(date -Is)"' > run/native.log 2>&1 &
206```
207
208### In this folder
209
210| Path | What |
211| --- | --- |
212| [src/main.rs](src/main.rs) | The executable: `native mcp` goes to `mcp::proxy::run`, anything else to `app::window`. |
213| [src/app.rs](src/app.rs) | The window, as its own `rust_library` (`//apps/native:app`): arguments, the bot and its stall/recovery, Jev, sound, saves, `resume`, the map carry-over, the replay recorder and scrubber, `App::answer` for every MCP `Request`, the patch points `logic_impl`/`ui_impl`, and the bot.log rotation. |
214| [src/activity.rs](src/activity.rs) | The MCP panel: where the server listens, whether a client has been heard from, and every tool call that came in. |
215| [BUCK](BUCK) | `:app` (with the hot-patch flags), `:test` (the bot.log rotation tests), and `:native`, the executable, which keeps `-Csave-temps`/`-Clink-dead-code` for the "main" sentinel object a patch needs. |
216
217Measured 2026-09-20: the window opens as a native Wayland surface, wgpu runs
218on Vulkan (Dozen, under WSL), and the process sits at about half of one core.
219The hot-patch build flags cost nothing measurable (50.6% CPU before and after
220loading a build with them); the emulator core is in `packages/console`, which
221does not carry them.
222
223← Previous: [Chapter 11, zbanks/](../zbanks/) · Up: [apps](../) · Next: [Chapter 13, jevprobe/](../jevprobe/) →