|
| 1 | +# hosttest — Linux host harness for the InfoNES core |
| 2 | + |
| 3 | +Runs the unmodified `infones/` emulator core headless on a Linux PC, dumping |
| 4 | +frames as images. Useful for debugging core behavior (PPU rendering, CPU |
| 5 | +timing, mapper state) with fast iteration and full instrumentation — no Pico |
| 6 | +flashing, no serial console. Only hardware-specific issues (HSTX/DVI output, |
| 7 | +SD card, PSRAM latency, audio sinks) still need the real device. |
| 8 | + |
| 9 | +The harness builds against the **RP2350 + framebuffer** configuration: |
| 10 | +`PICO_RP2350=1`, `FRAMEBUFFERISPOSSIBLE=1`, `isPsramEnabled()=true`. That |
| 11 | +unlocks MMC5, VRC7 CHR-RAM, the Famicom Disk System (FDS), and the |
| 12 | +320×240 full-frame rendering path the device uses on RP2350. |
| 13 | + |
| 14 | +## Layout |
| 15 | + |
| 16 | +| File | Purpose | |
| 17 | +|---|---| |
| 18 | +| `host_main.cpp` | main loop, InfoNES_* callbacks, 256×240 PPM dumper, env-var input injection, minimal iNES parser, inline CRC32, NES palette | |
| 19 | +| `stubs.cpp` | Frens helper subset (f_malloc/f_free/isPsramEnabled/...) the core links against, audio callback no-ops, host-stdio-backed FatFs (so FDS BIOS loads), `settings` instance | |
| 20 | +| `shim/pico.h` | empty `__not_in_flash_func` / `__not_in_flash` placement macros | |
| 21 | +| `shim/pico/time.h` | host `time_us_32` from `clock_gettime` | |
| 22 | +| `shim/ff.h` | minimal FatFs surface — backed by stdio in `stubs.cpp` | |
| 23 | +| `shim/FrensHelpers.h` | minimal `Frens::*` declarations the core needs; replaces the real (heavy) `pico_shared/FrensHelpers.h` | |
| 24 | +| `shim/settings.h` | minimal `settings` struct used by `FDS_AutoInsertEnabled` | |
| 25 | +| `ppm2png.py` | PPM → PNG converter, Python stdlib only (no PIL/ImageMagick needed) | |
| 26 | + |
| 27 | +## Build |
| 28 | + |
| 29 | +From the repo root: |
| 30 | + |
| 31 | +```sh |
| 32 | +g++ -O1 -g -fsanitize=address -std=gnu++17 \ |
| 33 | + -DPICO_RP2350=1 -DNDEBUG -DPICO_NO_HARDWARE=1 \ |
| 34 | + -I hosttest/shim -I infones -I pico_lib -I pico_shared \ |
| 35 | + -o hosttest/nes_host \ |
| 36 | + hosttest/host_main.cpp hosttest/stubs.cpp \ |
| 37 | + infones/InfoNES.cpp infones/K6502.cpp infones/InfoNES_Mapper.cpp \ |
| 38 | + infones/InfoNES_pAPU.cpp infones/InfoNES_Region.cpp \ |
| 39 | + infones/InfoNES_NSF.cpp infones/InfoNES_FDS.cpp |
| 40 | +``` |
| 41 | + |
| 42 | +- AddressSanitizer is intentional: it doubles as a memory-bug detector for |
| 43 | + the core. Drop `-fsanitize=address` for faster runs. |
| 44 | +- `-DPICO_RP2350=1` enables the MMC5 / VRC7 CHR-RAM / FDS code paths. |
| 45 | +- `-DNDEBUG` collapses `util/work_meter.h` to empty inlines. |
| 46 | + |
| 47 | +## Run |
| 48 | + |
| 49 | +```sh |
| 50 | +./hosttest/nes_host <rom.nes|rom.fds> <total-frames> <dump-every-N> [outdir] |
| 51 | + |
| 52 | +# examples |
| 53 | +./hosttest/nes_host "Super Mario Bros.nes" 600 60 hosttest/out |
| 54 | +./hosttest/nes_host "Akumajou Densetsu (J).nes" 800 100 hosttest/out # MMC5 |
| 55 | +./hosttest/nes_host "Zelda no Densetsu (J).fds" 600 60 hosttest/out # FDS |
| 56 | +python3 hosttest/ppm2png.py hosttest/out/frame_00200.ppm # -> .png next to it |
| 57 | +``` |
| 58 | + |
| 59 | +A `.fds` extension auto-routes to `fdsParse()` instead of the iNES path. |
| 60 | +Frames are written as `outdir/frame_NNNNN.ppm`, 256×240, RGB. |
| 61 | + |
| 62 | +## FDS BIOS |
| 63 | + |
| 64 | +FDS games need an 8 KB Famicom Disk System BIOS. Put it at: |
| 65 | + |
| 66 | +``` |
| 67 | +$NES_FAT_ROOT/bios/fds-bios.rom (default $NES_FAT_ROOT = ".") |
| 68 | +``` |
| 69 | + |
| 70 | +i.e. `./bios/fds-bios.rom` if you run the harness from the repo root. Sidecar |
| 71 | +save files (`*.SAV`) are written under `$NES_FAT_ROOT/saves/`. |
| 72 | + |
| 73 | +## Environment variables |
| 74 | + |
| 75 | +| Variable | Effect | |
| 76 | +|---|---| |
| 77 | +| `NES_PRESS_START=<frame>` | hold START for 10 frames starting there (gets past title screens) | |
| 78 | +| `NES_PRESS_KEYS=<f>:<hex>[,<f>:<hex>...]` | hold the given button mask 10 frames at each frame | |
| 79 | +| `NES_HOLD_A=<frame>` | autofire button A (4 frames on / 4 off) from that frame on | |
| 80 | +| `NES_REGION=ntsc\|pal\|dendy` | override `InfoNES_DetectRegion` (CRC lookup still runs, but result is overridden) | |
| 81 | +| `NES_DUMP_REGS=1` | print PPU R0..R7, scanline, PAD1 latch, mapper every 100 frames | |
| 82 | +| `NES_DUMP_VRAM=1` | write `ppuram.bin` (16 KB) and `sprram.bin` (256 B) to outdir at exit | |
| 83 | +| `NES_FDS_DISK_SIDE=<N>` | (FDS only) call `fdsRequestSwap(N)` once at startup | |
| 84 | +| `NES_FAT_ROOT=<dir>` | root directory for FatFs paths; default `.` | |
| 85 | + |
| 86 | +Button mask (per joypad, hex): |
| 87 | + |
| 88 | +| Bit | Value | Button | |
| 89 | +|---|---|---| |
| 90 | +| 0 | 0x01 | A | |
| 91 | +| 1 | 0x02 | B | |
| 92 | +| 2 | 0x04 | SELECT | |
| 93 | +| 3 | 0x08 | START | |
| 94 | +| 4 | 0x10 | UP | |
| 95 | +| 5 | 0x20 | DOWN | |
| 96 | +| 6 | 0x40 | LEFT | |
| 97 | +| 7 | 0x80 | RIGHT | |
| 98 | + |
| 99 | +Example: `NES_PRESS_KEYS=120:08,200:11` taps START at frame 120, then holds |
| 100 | +UP+A at frame 200 for 10 frames each. |
| 101 | + |
| 102 | +## Caveats |
| 103 | + |
| 104 | +- Host runs are fully deterministic: no PSRAM latency, no input-timing |
| 105 | + variation. A bug that is *intermittent* on the device usually shows up |
| 106 | + here as its always-broken variant. |
| 107 | +- Audio is stubbed entirely (`InfoNES_SoundOutput` is a sink). |
| 108 | +- NSF files aren't auto-detected (no `.nsf` dispatch in the harness). |
| 109 | +- The harness does NOT load NVRAM; cartridge save RAM starts empty every run. |
| 110 | +- `isPsramEnabled()` always returns true, so FDS multi-side games keep all |
| 111 | + sides in host RAM (matches the device's PSRAM build behavior). |
| 112 | +- Region detection runs the real MesenDB CRC lookup, so games with PAL/Dendy |
| 113 | + entries pick the right timing automatically; `NES_REGION=…` forces it. |
0 commit comments