Run DSVita on the remote ARM test box
Prerequisite: .env in the repo root with DSVITA_PI_HOST (ssh, key auth). Optional:
DSVITA_PI_BIN (default ~/claude/dsvita/dsvita). Never use sudo on the box without asking.
Working directory & roms. The dev machine is x86 and can't run the armhf binary natively,
so the pi5 is where all roms/games run. Roms live on the box at ~/nds. Do every box-side
thing — deployed binaries, logs, traces, savestates — under a ~/claude/dsvita/ working
directory so you don't pollute the home dir; keep DSVITA_PI_BIN pointed into it.
Deploy
cargo build --profile release-debug --target thumbv7neon-unknown-linux-gnueabihf
ssh "$DSVITA_PI_HOST" 'mkdir -p ~/claude/dsvita'
scp target/thumbv7neon-unknown-linux-gnueabihf/release-debug/dsvita "$DSVITA_PI_HOST:~/claude/dsvita/dsvita"
When testing multiple build variants, give each binary a DISTINCT name on the box and
md5sum them locally first to prove they differ (a chained sed && cargo build can
silently skip the recompile — see DEVELOPMENT.md §4 pitfalls).
Run / observe / drive
| action | command |
|---|---|
| Kill + relaunch with args | tools/pi_run.sh '-e 2 -f 1 ~/nds/<rom>.nds' |
| Screenshot (wayland grim) | tools/pi_shot.sh <local.png> |
| Press a key | tools/pi_key.sh <xkb-key> [hold_ms] |
| Check alive | ssh $DSVITA_PI_HOST 'pgrep -x <binary-name>' |
Keyboard map: WASD = dpad, K = A, J = B, I = X, U = Y, B = Start, V = Select, 8/9 = L/R.
Debug command port (debug/release-debug builds) — the headless control channel; needs no
wayland virtual keyboard (so it works even where wtype/pi_key.sh is absent). Launch with
DSVITA_DBG_PORT=<port> and send newline-delimited commands to 127.0.0.1:<port> on the box,
e.g. printf 'buttons a\n' | nc -q0 127.0.0.1 5555:
press/release <btn>|buttons [<btn>...](exact held set) — btn:a b x y up down left right start select l r. A held button = held on the DS.touch <x> <y>(DS coords, x 0..256 y 0..192) |touch off— drives "touch to start" gates and touch menus (Zelda PH, HG title). No drag path, so rub/drag minigames can't be driven.framelimit <0..9>(0 = uncapped) |savestate(quick-save at vblank) |inst-log(arm a--inst-log-lazycapture) |quit.
Sequence timing in ONE ssh session (loop the nc sends with short sleeps); a command per ssh
round-trip is too slow and the game "heals" between them.
Input-free verify loop: the port's savestate (or the F11 key) quick-saves at vblank;
relaunch with -s <savestate> to resume — lets you A/B or re-test a scene deterministically
without re-driving inputs.
Rules that bite:
- ALWAYS launch with
LIBGL_ALWAYS_SOFTWARE=1— the box's GPU driver renders incorrectly (garbled 2D layers); software GL is the reference. Never diagnose rendering from a GPU-driver run. - ALWAYS pass a framelimit:
-f 1for interaction,-f 5/-f 9to fast-forward through boot/loading. Uncapped (-f 0) is uninteractable. pkill -f/pgrep -fover ssh match the remote shell's own command line and kill your session — use-x <exact-binary-name>and name binaries distinctly.- The box's
/tmpis a small tmpfs: redirect big logs (DEBUG_LOG stdout, traces) to the home directory, never/tmp. A full/tmpkills the cpu thread with a StorageFull panic while the UI keeps rendering — looks exactly like an emulation hang. - The box's V3D GL driver has a known cosmetic tile/glyph rendering offset; verify suspected rendering bugs against a pure-jit build or another renderer before blaming emulation.