jevsnes.git / apps / native / CLAUDE.md
CLAUDE.mdpreviewCLAUDE.mdsource136 lines · 8.7 KB · raw
1@README.md
2
3- Run it inside the devshell. The binary links `libasound.so.2` and dlopens
4  wayland, xkbcommon and vulkan by bare name, and buck2 writes no runpath, so
5  outside `nix develop` it dies before `main` (`../../flake.nix` carries the
6  library path and the reason).
7- To see the window, call the MCP `window` tool; to see the game, `frame`; to
8  know what it is doing, `state`. Do not start a second instance to screenshot
9  it - the user sees every window, and the port is already taken.
10- The UI thread owns the console. MCP handlers send a `Request` down a channel
11  and the UI answers at a frame boundary; never reach for the console from the
12  server thread.
13- **A tool that changes anything goes in the `driving` router, never
14  `watching`.** `../../packages/mcp/src/lib.rs` (moved out of this crate
15  2026-09-20 - see that crate's own CLAUDE.md for why) has two
16  `#[tool_router]` blocks; which block a tool is in IS whether it exists with
17  dev mode off. The end state is a client with dev mode off while the bot
18  plays, so a mutating tool in `watching` is a hole in that, and nothing
19  else checks.
20- **Pick up a new build with the `restart` tool, not by killing the window.**
21  Killing it skips `on_exit`, so up to 30 seconds of game are lost, and the
22  user watches the window vanish for longer. And never `pkill -f native`: the
23  pattern matches the shell running it (2026-09-20, exit 144). `pgrep -x
24  native`.
25- **Turn dev mode off again when done debugging.** It is also off after any
26  restart.
27- **Nothing but MCP may reach stdout in `mcp` mode.** `proxy.rs` and anything
28  it calls use `eprintln!`. One stray `println!` corrupts the client's stream,
29  and the symptom is the client dropping the server with a parse error.
30- **Test `native mcp` with `/tmp`-style scripted stdio, not by restarting the
31  harness.** Pipe JSON-RPC lines in, read lines out; the case that matters is
32  a window `restart` underneath it, which must produce `tools/list_changed`
33  twice (gone, back). It once produced none: rmcp's client reconnects silently
34  and the window restores the session, so the proxy could not tell
35  (2026-09-20). `/alive` exists for exactly this; do not remove it as unused.
36- **Both MCP lifecycles are served and both matter.** Sessions (before
37  2026-07-28) are told of a tool-list change on their stream; sessionless
38  clients by `subscriptions/listen`. Claude Code 2.1.278 was seen on the
39  sessionless path (its calls succeeded with no session in the store,
40  2026-09-20). Test a change to either with curl: the new protocol needs
41  `MCP-Protocol-Version` and `Mcp-Method` headers and the three
42  `io.modelcontextprotocol/*` keys in `params._meta`.
43- Every tool call goes through `Activity` first. A new tool that skips it is
44  invisible in the panel.
45- Draw panels beside the picture, never over it.
46- **The bot is `zbanks::Bot`, one per process (its state is C globals), and
47  every emulated frame goes through `Bot::tick`.** Never set the console's
48  buttons directly for a frame: the human pad (keyboard + MCP holds) is the
49  bot's INPUT, and what it returns is what the console holds. That is
50  upstream's Snes9x contract; `../../research/zbanks-alttp.md` has it.
51- **Nothing may write to stdout in window mode except the bot.** Before the
52  bot starts, `stdout_to` makes fd 1 a pipe whose reader thread is the only
53  writer of `run/zbanks-window/bot.log`, rotated at `BOT_LOG_CAP` (50 MB,
54  `BOT_LOG_KEEP_FILES` = 3). App messages are `eprintln!`, which is
55  `run/native.log` when started as the README says. Do not go back to
56  truncating `bot.log` in place: the user rejected that, and a truncate
57  under the C's open fd is the race the pipe exists to avoid.
58- **The C is not hot-patchable.** `hot_patch` reaches `logic_impl`/`ui_impl`
59  and `packages/panels`; anything under `third-party/c` or `packages/zbanks`
60  needs a build and `restart`, and a restart starts a bot that remembers
61  nothing (its map is C globals, not part of a snapshot).
62- **The console boots the PATCHED image** (`zbanks::rando::patch_rom`), so a
63  snapshot taken by some other tool from the vanilla image is refused by its
64  cartridge CRC. Make states for this window from a patched image
65  (`apps/zbanks` patches before it boots).
66- **Adding a field to `App` needs `restart`, not `hot_patch`.**
67- **Only `Playback::Live` may call `bot.tick`.** `Paused`/`Playing` drive
68  `app.console` straight from the log (`replay::format::apply_wram_diff`,
69  `zbanks::apply_pad`) - the bot's own C state is left exactly where it was,
70  which is what lets returning to `Live` at the tip just resume ticking it
71  with no reconstruction (proven byte-identical by `apps/replay-probe`).
72  Calling `bot.tick` from `Paused`/`Playing` would silently desync the bot's
73  understanding of the game from whatever frame the picture is showing.
74- **"Run the bot from here" is a restart (`--branch-from`), never an
75  in-process rebuild.** `zbanks::Bot::start` panics on a second call in one
76  process (`packages/zbanks/CLAUDE.md`), so reconstructing the bot at a past
77  frame cannot happen alongside the LIVE bot already running here - the new
78  process replays the whole history through its own fresh bot
79  (`replay::rebuild::Rebuilder`) before going live as a new branch. Do not
80  try to short-circuit this by driving a second `Bot` in this process.
81- **A rebuild reinstalls the real Jev chooser when it finishes**
82  (`logic_impl`'s `progress.finished` branch) - the catch-up itself runs
83  under a `ReplayChooser` that only ever replays logged picks. Forgetting
84  this leaves Jev silently disabled after every "run the bot from here",
85  even with `--jev` set.
86
87## Hot patching
88
89`logic`/`ui` (the `eframe::App` trait methods) are thin shims: the real
90per-frame work is `logic_impl`/`ui_impl`, bare `#[inline(never)] fn`s taking
91`&mut App` (plus `&egui::Context`/`&mut egui::Ui`), called through
92`subsecond::HotFn::current(f).call((self, x))` rather than the `subsecond::call`
93sugar (which only takes a zero-argument closure - these take arguments).
94`research/subsecond-patch-build.md` §6-§8 and `apps/hotdemo/CLAUDE.md` are the
95background; this is the same mechanism against the real window.
96
97- **A new patch point must be a bare `fn` item, passed by name, never a
98  closure.** A closure capturing even one pointer-sized value silently takes
99  subsecond's "treat these bytes as a function pointer" branch instead of the
100  real redirect (§8.2.1) - the patch "applies" and does nothing, with no
101  error anywhere. `logic`/`ui` each carry a `debug_assert_eq!(size_of_val(&f),
102  0)` right before the call for exactly this reason; keep that assert on any
103  new patch point.
104- **`-Copt-level=3` inlines a small, single-call-site function into its
105  caller unless `#[inline(never)]` says not to** (§8.2.2). Without it there is
106  no addressable symbol left for a patch to jump to.
107- **`App` itself is never patched - only what runs against it.** State on
108  `app`/`self` survives a patch because the value already exists in the
109  running process; a `static` recompiled as part of the tip crate does not
110  (`apps/hotdemo`'s `TICKS` resets on every patch for this reason - same
111  limitation, not a bug to chase).
112- **A struct-layout change needs `restart`, not `hot_patch`.** Subsecond has
113  no safety net for it (research doc §5); a patch that changes the size or
114  field order of `App` or anything reachable across the patch boundary is
115  undefined behaviour, not a loud failure.
116- **A genuinely NEW thread-local can still fail loudly.** `tools/hotpatch`
117  copies a base binary's own thread-locals into the patch (its `.tdata`);
118  something that did not exist as a thread-local in the currently-running
119  binary at all has nothing to copy from and `tools/hotpatch` says so and
120  exits rather than emitting nonsense - rebuild without introducing it (or
121  restart, which relinks the whole binary and starts fresh).
122- **`mcp::Request::HotPatch` is handled on the UI thread, at a frame
123  boundary, like every other request** - `answer()` in `app.rs` (not
124  `main.rs`, since the 2026-09-20 split - see `app.rs`'s own doc comment) is
125  the only place `subsecond::apply_patch` is called from; never call it from
126  the MCP server thread.
127- **`patch_info` and `hot_patch` exist for `tools/hotpatch/patch.sh`, not for
128  a human to type by hand** - see that tool's own README/CLAUDE.md for the
129  MCP session dance (`initialize` -> `Mcp-Session-Id` -> tools/call) and for
130  what buck2 output shapes it depends on.
131- **`main.rs` is a three-line dispatcher; `app.rs` is its own `rust_library`
132  (`//apps/native:app`).** This is what lets `tools/hotpatch/patch.sh` get a
133  panels/app edit's fresh objects without ever relinking the executable -
134  see `app.rs`'s doc comment and `../../tools/hotpatch/CLAUDE.md`. Adding a
135  file to the window's own code means adding it to `app`'s `srcs` in `BUCK`,
136  not to `native`'s.