jevsnes.git / apps / native

For agents, on top of README.md, which they read first.

  • Run it inside the devshell. The binary links libasound.so.2 and dlopens wayland, xkbcommon and vulkan by bare name, and buck2 writes no runpath, so outside nix develop it dies before main (../../flake.nix carries the library path and the reason).
  • To see the window, call the MCP window tool; to see the game, frame; to know what it is doing, state. Do not start a second instance to screenshot it - the user sees every window, and the port is already taken.
  • The UI thread owns the console. MCP handlers send a Request down a channel and the UI answers at a frame boundary; never reach for the console from the server thread.
  • A tool that changes anything goes in the driving router, never watching. ../../packages/mcp/src/lib.rs (moved out of this crate 2026-09-20 - see that crate's own CLAUDE.md for why) has two #[tool_router] blocks; which block a tool is in IS whether it exists with dev mode off. The end state is a client with dev mode off while the bot plays, so a mutating tool in watching is a hole in that, and nothing else checks.
  • Pick up a new build with the restart tool, not by killing the window. Killing it skips on_exit, so up to 30 seconds of game are lost, and the user watches the window vanish for longer. And never pkill -f native: the pattern matches the shell running it (2026-09-20, exit 144). pgrep -x native.
  • Turn dev mode off again when done debugging. It is also off after any restart.
  • Nothing but MCP may reach stdout in mcp mode. proxy.rs and anything it calls use eprintln!. One stray println! corrupts the client's stream, and the symptom is the client dropping the server with a parse error.
  • Test native mcp with /tmp-style scripted stdio, not by restarting the harness. Pipe JSON-RPC lines in, read lines out; the case that matters is a window restart underneath it, which must produce tools/list_changed twice (gone, back). It once produced none: rmcp's client reconnects silently and the window restores the session, so the proxy could not tell (2026-09-20). /alive exists for exactly this; do not remove it as unused.
  • Both MCP lifecycles are served and both matter. Sessions (before 2026-07-28) are told of a tool-list change on their stream; sessionless clients by subscriptions/listen. Claude Code 2.1.278 was seen on the sessionless path (its calls succeeded with no session in the store, 2026-09-20). Test a change to either with curl: the new protocol needs MCP-Protocol-Version and Mcp-Method headers and the three io.modelcontextprotocol/* keys in params._meta.
  • Every tool call goes through Activity first. A new tool that skips it is invisible in the panel.
  • Draw panels beside the picture, never over it.
  • The bot is zbanks::Bot, one per process (its state is C globals), and every emulated frame goes through Bot::tick. Never set the console's buttons directly for a frame: the human pad (keyboard + MCP holds) is the bot's INPUT, and what it returns is what the console holds. That is upstream's Snes9x contract; ../../research/zbanks-alttp.md has it.
  • Nothing may write to stdout in window mode except the bot. Before the bot starts, stdout_to makes fd 1 a pipe whose reader thread is the only writer of run/zbanks-window/bot.log, rotated at BOT_LOG_CAP (50 MB, BOT_LOG_KEEP_FILES = 3). App messages are eprintln!, which is run/native.log when started as the README says. Do not go back to truncating bot.log in place: the user rejected that, and a truncate under the C's open fd is the race the pipe exists to avoid.
  • The C is not hot-patchable. hot_patch reaches logic_impl/ui_impl and packages/panels; anything under third-party/c or packages/zbanks needs a build and restart, and a restart starts a bot that remembers nothing (its map is C globals, not part of a snapshot).
  • The console boots the PATCHED image (zbanks::rando::patch_rom), so a snapshot taken by some other tool from the vanilla image is refused by its cartridge CRC. Make states for this window from a patched image (apps/zbanks patches before it boots).
  • Adding a field to App needs restart, not hot_patch.
  • Only Playback::Live may call bot.tick. Paused/Playing drive app.console straight from the log (replay::format::apply_wram_diff, zbanks::apply_pad) - the bot's own C state is left exactly where it was, which is what lets returning to Live at the tip just resume ticking it with no reconstruction (proven byte-identical by apps/replay-probe). Calling bot.tick from Paused/Playing would silently desync the bot's understanding of the game from whatever frame the picture is showing.
  • "Run the bot from here" is a restart (--branch-from), never an in-process rebuild. zbanks::Bot::start panics on a second call in one process (packages/zbanks/CLAUDE.md), so reconstructing the bot at a past frame cannot happen alongside the LIVE bot already running here - the new process replays the whole history through its own fresh bot (replay::rebuild::Rebuilder) before going live as a new branch. Do not try to short-circuit this by driving a second Bot in this process.
  • A rebuild reinstalls the real Jev chooser when it finishes (logic_impl's progress.finished branch) - the catch-up itself runs under a ReplayChooser that only ever replays logged picks. Forgetting this leaves Jev silently disabled after every "run the bot from here", even with --jev set.

Hot patching

logic/ui (the eframe::App trait methods) are thin shims: the real per-frame work is logic_impl/ui_impl, bare #[inline(never)] fns taking &mut App (plus &egui::Context/&mut egui::Ui), called through subsecond::HotFn::current(f).call((self, x)) rather than the subsecond::call sugar (which only takes a zero-argument closure - these take arguments). research/subsecond-patch-build.md §6-§8 and apps/hotdemo/CLAUDE.md are the background; this is the same mechanism against the real window.

  • A new patch point must be a bare fn item, passed by name, never a closure. A closure capturing even one pointer-sized value silently takes subsecond's "treat these bytes as a function pointer" branch instead of the real redirect (§8.2.1) - the patch "applies" and does nothing, with no error anywhere. logic/ui each carry a debug_assert_eq!(size_of_val(&f), 0) right before the call for exactly this reason; keep that assert on any new patch point.
  • -Copt-level=3 inlines a small, single-call-site function into its caller unless #[inline(never)] says not to (§8.2.2). Without it there is no addressable symbol left for a patch to jump to.
  • App itself is never patched - only what runs against it. State on app/self survives a patch because the value already exists in the running process; a static recompiled as part of the tip crate does not (apps/hotdemo's TICKS resets on every patch for this reason - same limitation, not a bug to chase).
  • A struct-layout change needs restart, not hot_patch. Subsecond has no safety net for it (research doc §5); a patch that changes the size or field order of App or anything reachable across the patch boundary is undefined behaviour, not a loud failure.
  • A genuinely NEW thread-local can still fail loudly. tools/hotpatch copies a base binary's own thread-locals into the patch (its .tdata); something that did not exist as a thread-local in the currently-running binary at all has nothing to copy from and tools/hotpatch says so and exits rather than emitting nonsense - rebuild without introducing it (or restart, which relinks the whole binary and starts fresh).
  • mcp::Request::HotPatch is handled on the UI thread, at a frame boundary, like every other request - answer() in app.rs (not main.rs, since the 2026-09-20 split - see app.rs's own doc comment) is the only place subsecond::apply_patch is called from; never call it from the MCP server thread.
  • patch_info and hot_patch exist for tools/hotpatch/patch.sh, not for a human to type by hand - see that tool's own README/CLAUDE.md for the MCP session dance (initialize -> Mcp-Session-Id -> tools/call) and for what buck2 output shapes it depends on.
  • main.rs is a three-line dispatcher; app.rs is its own rust_library (//apps/native:app). This is what lets tools/hotpatch/patch.sh get a panels/app edit's fresh objects without ever relinking the executable - see app.rs's doc comment and ../../tools/hotpatch/CLAUDE.md. Adding a file to the window's own code means adding it to app's srcs in BUCK, not to native's.