Skip to main content

Virtual Boy: full speed, then VBoy Color's colours

Goal, in the user's words (2026-10-11): the Virtual Boy at full speed, then in colour like VBoy Color, without disturbing the other emulators.

Done means: 50 fps, every frame drawn, with VBoy Color's Auto colours (colour packs) on — the user tries it only then. The colour mode's per-pixel work is part of the speed budget from the start, not an afterthought: every phase leaves room for it on core 1.

Full speed for this console is 50 frames a second, every one drawn: the Virtual Boy's display refreshes at 50.27 Hz, so 60 would run its games 20 % too fast. The budget is therefore 20 ms a frame.

Related pages: Arcade 60 fps plan (the same kind of work on the 68000, and why its targets were not reached), JIT plan, More systems.

Where we are (2026-10-11)​

Fork branch vb-color (pjcau/retro-go): libretro Beetle VB (Mednafen's emulation, the core VBoy Color is built on) inside retro-extra, tab "Nintendo Virtual Boy" next to the NES, with VBoy Color's per-shade "Multicolor" palettes (pixel-exact on the board) and the display filter off.

Measured on the board, Wario Land's first cave (save slot 0)ms a framefps
Play build10010
CPU only, the VIP drawing nothing (VBNODRAW=1)6715
Needed2050

So the V810 path is two thirds of a frame and the VIP (drawing) one third, both on core 0; core 1 is idle (load 100 / 10 %). No ESP32 Virtual Boy port exists to borrow from; the one fast emulator on a CPU of this class, Red Viper (3DS, ARM11 268 MHz), gets there with a V810 → ARM recompiler. That is the reference for phase 3.

Already done and measured: crash fixes (own task, sysmon), right eye not drawn (−0 fps: the VIP was not the bottleneck), V810 hot path in IRAM (−0 fps, the work RAM had stayed in PSRAM), memory rule enforced (below).

The rule: the other emulators do not notice the Virtual Boy​

retro-extra also runs Neo Geo Pocket, Atari 2600 and arcade3d. A VB change is accepted only if, with no VB game running:

  • static RAM of retro-extra stays within ~2 KB internal / 0 KB PSRAM of fork master (xtensa-esp32s3-elf-size -A retro-extra.elf: .dram0.*, .ext_ram.bss); every VB buffer is allocated when a VB game starts;
  • IRAM grows only by measured hot code (today +8.3 KB);
  • the board's NGP gate passes: Metal Slug 1st Mission, same scene, the per-second FPS / BUSY lines of the first 30 s identical to master's, and the boot "Available memory" line as on master. Not drawn/s. The board found (2026-10-11) that every build, master included, has one slow second at ~32.4 s (FPS 57-58); whether it trips rg_system's auto-frameskip from 0 to 1 is a knife-edge, and once at 1 it never comes back (Auto only lowers above 1). Drawn/s then reads 44 or 30 for identical code. That ratchet is a pre-existing NGP behaviour, not a VB cost; it is reported to the user separately, not changed here.

Builds go Mac → board with bin-relay (scripts/bin_relay.py): the Mac builds every variant once, the board fetches the exact bytes.

Phases​

Each phase ends with a board measurement on the same save state and the NGP gate. A phase that does not move the number is reverted, not kept "in case".

Phase 0 — memory placement (closed, no gain)​

bd1a092d (hot path in IRAM) and da29fe7a (VIP buffers to PSRAM to free internal RAM for the 64 KB work RAM): 10 / 15 fps unchanged. While a VB game runs the internal RAM is fragmented — 61 KB free but the largest block 31 KB — so the work RAM cannot get 64 KB in one piece and stays in PSRAM. Splitting it in two 32 KB halves would need the V810's fast memory map to work in 32 KB pages; not worth it before phase 1 says the interpreter waits on data at all. Kept: the hot path in IRAM (harmless, NGP gate passed).

Phase 1 — know the guest (1–2 days)​

Before any big work, measure what the game asks of the V810:

  1. Guest instructions per frame and host cycles per guest instruction (a counter in Run_Fast, printed per second). At 20 MHz the V810 can run ~400k instructions a frame; how many does Wario Land really execute?
  2. Guest PC histogram (as gbahost's PCHIST): VB games wait for the VIP's frame interrupt in short loops. If a large share of instructions is such a loop, idle-loop skipping (gpSP's idle_loop_target_pc, per game) removes it cheaply — on GBA games it removed up to 46 % of the work.
  3. A PC harness vbhost (as test/gbahost in xtensa-68000-dynarec): the same core built natively, run on the user's ROMs on the board PC, with per-frame picture/audio hashes. Every later phase is checked there first (same hashes = same emulation).

Decision gate: if idle loops are ≥ 40 % of the instructions, phase 2 (idle skip + VIP on core 1) may reach 50 fps on its own; otherwise phase 3 is needed.

Phase 1 — result (2026-10-11)​

VBGUEST=1 on the cave scene: ~180k guest instructions a frame, never a HALT, and 73 % of them in two loops — disassembled, both poll a VIP status register (bits 2-3) through a pointer, one with no side effect, one (and its twin) counting its turns in work RAM. The gate is met: idle skip first.

Phase 2 — use both cores (2–4 days)​

Idle skip done (56af3e81, board-proven): per-game loop heads by ROM CRC32; the CPU skips whole turns to the next event and adds them to the loop's counter. Wario Land: cave 10 → 15 fps, in play 10 → ~23 fps (20-27), ~300k of the ~400k cycles a frame skipped, the game plays correctly (walk, jumps, attacks, room changes, a life lost), NGP gate unchanged. Next: the new CPU/VIP split decides the VIP on core 1.

  • Idle-loop skip for the measured games (Wario Land first).
  • The VIP on core 1. The VIP draws 8-line blocks while the CPU runs; on core 1 it runs in parallel, synchronised at the frame's end (the VB double buffers its framebuffers). A frame then costs max(CPU, VIP) instead of the sum. Validated on vbhost by identical frame hashes, then on the board.

Expected: frame = CPU path only, i.e. ≤ 67 ms → 15 fps today, more after the idle skip. Not 50 without phase 3 unless the idle loops are most of it.

Phase 2 — results so far (2026-10-11, board-measured, Wario Land)​

Each step kept only if faster AND correct (webcam, screenshot, input) AND the NGP gate passed; the play build on the board is always the last kept one.

Build (vb-color)WhatPlay route fps (median / max)Core 0 / 1 load
startBeetle VB as ported10 / 12100 / 10
56af3e81idle skip (3 VIP-wait loops)23 / 27100 / 10
49f52249VIP on core 1, from a per-frame snapshot25 / 31100 / 88
197fe268single-pass output (blocks straight to RGB565)36 / 4699 / 100
588f82e34th idle loop (keypad read wait)36 / 4799 / 100
c7f8c455two-pixel conversion, tiles in internal RAM36 / 4985 / 100
d317f16aDrawBG skips empty / unmasks full tile rows40 / 5188 / 94
0380d43ftwo snapshots taken by core 041 / 5390 / 94
3161f6cbrow hashes from the shades (rg_surface_t.row_hash)42 / 5188 / 69

Tried and reverted: VIP_Write sync without snapshot (no gain), IRAM placement (no gain), work RAM in internal RAM (fragmented: largest block 31 KB), no per-line hash (slower: play screens are mostly still).

Now core 0 (the V810 interpreter) is the limit; core 1 has ~30 % left, which phase 4's colours need. Measured for phase 3: 40-58k guest instructions a frame, 9-20 KB of distinct V810 code a second (all ROM) — small enough for the instruction cache, as the GBA's and unlike the 68000's.

Phase 3 — a V810 recompiler (weeks)​

The only route to 50 fps if phase 1 shows real work, not idle. Same shape as the project's two existing Xtensa recompilers (68000 for mame-go, ARM for gpSP; repo xtensa-68000-dynarec), design from Red Viper:

  1. block translation of the integer ISA (ALU, loads/stores with fast paths for WRAM and ROM, branches), the rest (FPU via softfloat, bit-string ops, system registers) as calls to the interpreter's handlers;
  2. cycle accounting per block and interrupt checks at block ends (the VIP and timer events stay as they are);
  3. translation cache in internal RAM or executable PSRAM, allocated only while a VB game runs (the rule);
  4. lockstep check on vbhost: recompiler against interpreter, register file and memory compared after every block, then frame hashes;
  5. board measurement on the save state; the interpreter stays as fallback (per-game switch, as gbsp's "Fast CPU").

Honest expectation: the 68000 recompiler gave large gains but did not reach 60 drawn frames on the heaviest arcade games. The V810 is a simpler, RISC-like CPU at 20 MHz, so the odds are better, but the number is decided by measurement, not promised.

Phase 3 — results so far (2026-10-11, board-measured, Wario Land cave route ×3)​

The recompiler is v810jit (repo xtensa-68000-dynarec, branch v810jit), under Beetle's interpreter: a block runs only when no event can fall inside it and no interrupt is pending, so it is bit-exact — in QEMU (zvb-beetle/test/vbjit-test) the reference route and 6 000 frames of random play give the interpreter's video and audio hashes, window by window. The game option Fast CPU switches it; off, the old interpreter loop runs.

Build (vb-color)WhatFast CPU OnOff
3161f6cbinterpreter only (reference)—42 / 38.8 / 51
d00adba3every block translated at sight, code in PSRAM24 / 25.7 / 4337
d589cb8atranslated when hot; Off = old loop30 / 30.5 / 4640
5e2336d2+ blocks chained through slots32 / 33.3 / 50—
e66030e6+ lazy flags, cold sections34 / 34.3 / 52—
640b1c08code in internal RAM, but the cache got 8 KB31 / 30.1 / 4440
127b7011cache taken before the game's buffers: 32 KB36.5 / 33.5 / 52—
8f51408964 KB cache; blocks go on after a handler call; one-compare idle check41 / 36.2 / 53—
c540ad34+ block lookups from an 8 KB internal-RAM table43 / 38 / 52—
56326cd7+ the timer and the VIP schedule only observable events45 / 39 / 53 (core 0 55 %, core 1 100 %)45 / 39.5 / 54 (core 0 97 %)

(median / mean / max fps; Off is 2 fps under 3161f6cb on every recompiler build — code placement, not yet understood.)

What the measurements taught, in order:

  • Translated code in PSRAM loses to the interpreter. It misses the 32 KB instruction cache, which core 1's drawing shares; the interpreter is small and stays in it. Every PSRAM-code round gained 2–4 fps, never more.
  • Executable internal RAM exists only with memprot off (retro-extra/sdkconfig.vbjit, retro-extra only; NGP gate passes), and only before the game's buffers take it: 134 KB free then (largest block 104), 10 KB after. The cache is now allocated first; tile snapshot 1 goes to PSRAM, where 3161f6cb already had it.
  • The hot set in play is bigger than the route profile says. QEMU: 75 % of the guest work is 21 KB of translated code; on the board a 32 KB cache was renewed at every 5 s window. Next: 64 KB (8f514089).
  • Block exits cost. QEMU counts per frame: 3 500 block entries, 8 000 lookups, 2 000 handler calls (VIP writes mostly), 60 % of the instructions translated. A handler call used to leave the block; now the block goes on unless the handler raised an interrupt or moved the next event (df0b4118: entries 1 800, lookups 6 000).
  • Events were the structural brake. Beetle's VIP stopped at every display column (259 cycles) and the timer at every 20 µs tick: ~1 500 event-handler calls a frame, and ~1 350 blocks a frame refused because an event fell inside them — most of the interpreted share. f0ad094f: the timer schedules the next observable tick (the counter reaching zero; reads and writes run the ticks due first, as before), and with the single-pass output the VIP batches the columns between drawing blocks and region ends (the classic path is untouched, hashes unchanged). QEMU: held blocks 1 354 → 43 a frame, block entries 1 800 → 270, 70–79 % of the instructions translated; 6 000 random frames interpreter = recompiler.
  • The interpreter's own dispatch is ~20 % of core 0 (profile d317f16a: fetch, decode, computed goto, and the four idle-loop compares at every instruction, ~10 % alone — now one range test).

50 fps with VBoy Color's colours — reached 2026-10-12 (92d3189e, fork master)​

Board, Wario Land cave route ×3, Fast CPU On, Colours "Auto (VBoy Color)" with Wario's colour pack, median / mean / max:

BuildWhatAuto + packRedloads c0/c1
5165b9aapacks, first version35.5 / 32.7 / 534455 / 100
b97cc5d9cell bitmap, slot tables internal39 / 35.4 / 534557 / 100
4835e07bv810jit: no IRAM code, no static tables (NGP memory back to 182/278)38.542.567 / 99
e75430f0tile snapshots before the small tables; pack rows in 2 word loads40 / 36.1 / 524459 / 100
1191b8d5both snapshots internal (32 KB cache) — nothing394363 / 100
aed6780cdisplay scaler 4→5 (VB 384→480), shared code42 / 37.1 / 5247.570 / 100
097ad057the display task on core 0 for the VB47 / 39 / 5348100 / 87
a096bea548 KB code cache again48 / 38.8 / 5349.5100 / 90
92d3189ecold blocks counted inline, 2048-entry lookup, no emission into a full cache50 / 42.2 / 535092 / 90

NGP gate on every one of them (timing ±1, "Available memory" 182/278 since 4835e07b). What decided the last 10 fps was measuring who waits for whom (VBPROF "VBWAIT" lines: core 0 blocked 3.5–7 ms a frame on core 1's job queue while core 1 also ran the display): the display task moved to core 0, then core 0's own overheads (lookups from flash, cold-block counting) went. Placement experiments (snapshot 1 in internal RAM) gave nothing, again. The mean stays ~42 because of the route's slow moments (the left/right reversal): the next phase is headroom — the emulation able to run at ~60 fps-equivalent unthrottled, so that 50 holds everywhere (the VB itself runs at 50.27 Hz: 60 shown frames would be a 20 % faster game).

Headroom: the slow moments (2026-10-12 afternoon, fork master 7e012952)​

With the median at 50, the board measured where it was NOT: Wario's opening scene (the resume0 state is that scene, not a room) at 19-23 fps, its heavy part at 15-17, and ~2 s at 10 fps at every fade to black. What found them, in order: per-frame "VBSLOW" lines (the fades: all core 0, core 1 idle), a settings bisect (not the pack, not the recompiler, not the scaler), a profile of the slow frames only (VBPROF=slow: all in the interpreter's loop), and the board's own save state on the Mac (sent over the LAN, never git) - the Mac's earlier state was a room, so it had never seen either.

BuildWhatroute Auto + pack median / meanopening scene
92d3189e—50 / 42.219-23, 15-17 heavy
90ce3fadsprites per block, pack variants hashed, NGP memory50 / 40.622-24, 15-17
7520622apack row unrolled50 / 40.724-26, 15-17
7e012952the opening's wait loop in the idle table (8 slots); the recompiler refills a full cache at once50 / 47.223-26, heavy part gone

The opening's heavy part: 81 % of its instructions in a 3-instruction wait for status bits 2-3 at 0xFFFC294E (vbhost PC histogram) - the idle table was full. The fades: real work after the A press (~250k instructions a frame for ~30 frames), which the recompiler could not translate because its full cache renewed only at the end of a 250-frame window - 25 s at 10 fps. Left: the opening scene before its heavy part, 23-26 fps, core 1 bound (DrawBG).

Idle loops found automatically, in every game (2026-10-12 evening, fork master 0eb6ab66)​

All 13 games on the card measured on 7e012952 first: every one boots, but only Wario had idle-loop entries; 10 of 13 were core-0 bound, real gameplay 19-43 fps (mean 28.6). The per-game table is gone: the core finds the wait loops itself (libretro.cpp: vb_idle_probe, vb_idle_analyze).

  • Trigger: the PC the CPU is at when an event comes, when it is the same at two events in a row (< 1 analysis a frame).
  • A loop qualifies when one turn - followed through jal/jr/jmp [r31] - only reads memory that changes at events (not the timer's counter), carries no register from turn to turn, branches on flags it set, and stores nothing but the same value every turn or one work-RAM turn counter (ld.w; add 1; st.w, tested by nothing).
  • A skip comes only after a real turn: back at the loop's start from inside it, no event since, two turns of the same length; XPSTTS's SBOUT time caps it.
  • Exactness: vbhost, 12 games x 1500 frames with input: the video hash with the skip equals the one without in every game; Wario's route gives the old table's skip (294015 cycles/frame) and its exact hashes.

Board, 579712cd (vbgames-5797-2026-10-12.txt; input-part median, 7e01 in brackets): 3-D Tetris 28.5 [23], Galactic Pinball 41 [38], Mario Clash 50 [21], Nester's 40 [37], Space Invaders 47 [43], Space Squash 50 [44], Teleroboxer 50 [19], V-Tetris 38 [38], Waterworld 49 [48], Jack Bros. 28 [28] (core 1), Vertical Force J/U 24 [24] (no wait loop: the CPU is busy all frame), Wario route 50 / 47.0 [50 / 47.2]. NGP 182/278 PASS, 0 serial drops.

Two board-only lessons: the detector's tables static (+208 B) cost NGP 1 KB (182/277) and made the USB serial drop 66-76 times a run - every VB table goes on the heap (2 KB, only while a VB game runs). And a skip measured on a re-entry or after an event shifted 3-D Tetris and Waterworld (vbhost hash from frame 802 / 871): hence the real-turn rule.

Green screens and slow levels (2026-10-12 night, fork master 163b8b1a)​

The user saw Wario's title and level 1 turn green and slow. Two causes, both older than the idle work:

  • The classic path's frame (a few frames after the CPU touches the framebuffer) is little-endian RGB565 but was sent marked big-endian since 09cb1f6c: byte-swapped, Arcade's black-blue 0x050A24 showed as olive green. The format is now set per frame.
  • Wario dereferences null pointers into the framebuffer (title: a read of 00000 per frame; level 1: st.w r6, 0[r8] with r8 = 0, four writes a frame). Each access switched to the classic path for 4 frames and back with a full redraw: colours flickering, 21-31 fps. Up to 32 accesses a frame no longer switch (CPU-drawing games and full clears still do).

Board 163b8b1a: title in colour at 43-51 fps; the green room's colours right, no classic frames; NGP 182/278 PASS. That room is still 26-38 fps (Red 37-44): it draws 7 full-width background worlds (~690 rows against the cave's ~480), core 1 at 91 % in vb_vip, every frame a full redraw - the drawing itself is the next lever (occlusion of covered layers), the same one Wario needs from 53 to 60 unthrottled.

Fork master ccb8eb8e (stray framebuffer accesses no longer wait for core 1): the room at 37 fps Auto + pack, 47 Red; route 50 / 47; now core-1 bound (vb_vip 100 %).

Front-to-back drawing tried and dropped (branch vb-autoidle @ d8a23376, not merged). Worlds drawn nearest first with a per-row coverage mask, so covered background spans skip their map reads. The picture is exact (51 video hashes equal, no wrong pixels on the board) but the board is slower everywhere drawing-bound: route Auto + pack 44 vs 50, Red 48 vs 50; green room 36 vs 37 (Auto + pack), 41 vs 47 (Red); Galactic Pinball 32 vs 41, V-Tetris 31 vs 38. The mask reads and writes per span and per pixel cost more than the background writes they save. Occlusion, if retried, has to be decided per world (a whole world hidden behind an opaque full-screen one), never per row.

Both cores draw (fork master 52f2ffc3, 2026-10-13). When the job queue is full, core 0 draws the oldest queued block itself instead of waiting (only blocks of the copy core 1 has in view; the sprite list is per core; a frame is done when all its blocks are, in any order). Board, against ccb8eb8e: green room Auto + pack 47 / 46.3 (37), Red 50 / 50.1 (46.5); route 50 / 47.5 both modes; Speed max 54 / 53.4; opening 29-34 (23-26); Galactic Pinball 49 (41), V-Tetris 50 (38); NGP PASS; no picture errors. Profile: Auto + pack still core-1 bound (queue wait 8 ms), Red capped at 50. Then a7d883cc: the pack's variant flags per tile found at load (a scan of 164 variants in PSRAM per hashed tile was 7-10 % of core 0), and no tile hashing while the pack is not drawn. Green room 48 / 48.0 Auto + pack; Speed max 55 / 55 Auto + pack, 57 Red; 1x route 50 / 48.0; colours come back right after Auto -> Red -> Auto; NGP PASS. Then 16cae1d4: VRAM snapshot copies by written 64-byte line instead of 1 KB page (the ROM memcpy was ~7 % of core 0 at Speed max; Wario's route copies 5-6 KB a frame instead of 10-13). Speed max 55-56 / 55.0-55.3 Auto + pack, 57.1 Red; picture check clean; NGP PASS. (Its first build c4b55ddd hung at the first frame: __builtin_ctz(0) is not 32 on the S3.) The display task on core 1 (36671134) was worse with the pack (55 -> 54.3) and stays on core 0.

Where this leaves 50 fps (2026-10-11, late): with the events fix both the recompiler (core 0 at 55 %) and the interpreter (97 %) reach 45, so core 1 is the cap — the VIP's drawing plus the display. The recompiler's gain is now headroom on core 0. Core 1's profile (d317): DrawBG 28 %, the display scaler 18 %, the row hash 16 % (since avoided), the RGB565 conversion 12 % (since skipped for unchanged rows), idle 16 %. Next levers, in order: tile snapshot 0 back in internal RAM (e5586bf2, 48 KB cache), the display scaling (a settings experiment first), then DrawBG's map reads.

Phase 4 — VBoy Color's colours (after speed)​

Started 2026-10-11 (6c9db730): Auto mode. VBoy Color's rule — every background layer coloured by its depth in the drawing order, sprites by their palette, the three shades a ramp's dark/light/lightest dimmed with the game's brightness — as 8 colour classes in the byte the drawing already writes per pixel (class << 2 | shade) and a 4 KB two-pixel output table: no per-pixel tags (VBoy Color tags 64 bits a pixel and re-hashes 2 048 tiles a pass — not for this chip), the same drawing and output cost. Options → Colours → "Auto (VBoy Color)", the default. Checked by eye on the cave (vbhost FAST=1 AUTO=1 DUMPAT=…).

Colour packs (6dc76eb5–b97cc5d9, 2026-10-11/12). VBoy Color's hand-painted packs (colorpacks/*.vbcp, colours only, no game graphics) converted by tools/vbcp_convert.py to the console's .vbc: the pack's colours quantized to a 224-entry palette, one byte per tile pixel (Wario Land: 6 116 tiles + 1 089 map-cell variants, 552 KB in PSRAM). Loaded from the SD next to the ROM (<rom>.vbc; the board converts them itself — they are not in git). Core 0 hashes the character-RAM pages that changed when it takes a frame's snapshot and fills a per-slot table; the draw loops read one table per tile row; a painted pixel's byte is 32 + its colour, unpainted pixels keep the Auto colours; the output table dims the pack with the game's brightness. Variants: map cell (one bit per cell says whether to look), palette, layer. Not yet: fills, context groups (markers), right-eye packs.

Board, 5165b9aa (Fast CPU On, route ×3, median / mean / max, loads c0/c1):

Coloursfpsloads
Red, no pack44 / 38.9 / 5364 / 99 %
Arcade + pack36 / 32.8 / 5355 / 100 %
Auto + pack35.5 / 32.7 / 5355 / 100 %

Auto costs nothing over a palette (by design); the pack cost core 1 8.5 fps — a hash lookup per painted tile row (every tile, since Wario has cell variants) and the slot table in PSRAM — fixed in b97cc5d9 (cell bitmap, slot tables in internal RAM), measured next.

Two more things the board found on the way: a game's CPU write to the framebuffer (Wario, once, at a room change) switched the core to the classic output for the whole session — no colours, ~10 fps less, seen by the user as "red and black" — now it lasts 4 frames (27f28afb), and Auto keeps the Arcade palette for that path (5165b9aa); and rg_display byte-swapped every pixel of every line because the frame was little-endian — the VIP now writes the panel's big-endian order straight from its colour tables (09cb1f6c).

What the user saw in VBoy Color's screenshots is its Auto mode with colour packs: every tile has hand-painted colours (Wario Land's pack exists, 15 others too), and what is drawn without tiles is coloured by depth.

  1. Port vbgo_tiletrack (records, per drawn pixel, the tile and palette that drew it) into the VIP's drawing — on core 1 with the VIP (phase 2).
  2. Port TileColorPack / ColorPackRenderer: the pack (.vbcp, 0.3–4 MB) stays on the SD card, loaded into PSRAM when its game starts; the per-pixel tag buffer (~170–340 KB) in PSRAM, only while a VB game runs. The rule holds: nothing static.
  3. Flash: retro-extra has ~130 KB of its partition left. If the colour code does not fit, the VB moves to the shared SD-app partition (sdapp), copied from the card when started, rather than taking flash from another app.
  4. Menu: Colours → Auto (pack) / the 8 Multicolor palettes / Red.
  5. Licence: VBoy Color is GPL-3; retro-go is GPL-2-or-later, so the combined firmware is distributed under GPL-3.

Resume checklist​

2026-10-13 - current. Fork master 16cae1d4 (both cores draw, cheaper pack hashing, VRAM copies by line), board on 16cae1d4-vbjit. Paused by the user after febbf72c (fork branch vb-draw2, not merged: a write of the value already there marks nothing to copy; Mac hashes equal, copies ~25 lines a frame instead of 70-100). On the board only its quick check ran (Wario resume0 plays, opening 30-35): the board may still carry febbf72c-vbjit. To resume: its full test (Speed max route, green room, NGP, sprite/scroll picture check, profile with VBROMCALLER), merge if clean; then the job queue without critical sections (~4.6 % of core 0 in vPortExitCritical, board ceca25d9). Next toward 60 unthrottled (Speed max 55 Auto + pack, 57 Red): core 1 still bound in Auto + pack (queue wait 8 ms a frame) - core 0 could take more than the oldest job; then the recompiler for Vertical Force / Waterworld.

2026-10-12 night. Fork master ccb8eb8e, board on ccb8eb8e-vbjit. Front-to-back drawing (d8a23376) was slower on the board and is not merged (see "Green screens and slow levels").

2026-10-12 evening. Fork master 0eb6ab66 (automatic idle loops), board on 579712cd-vbjit. Next, toward 60 fps unthrottled at 1x: (1) Vertical Force (24, core 0 busy all frame) and Waterworld: recompiler speed, profile them; (2) core 1 (VIP drawing) for Wario 53 -> 60 unthrottled, Jack Bros. and Nester's (core 1 bound); (3) real play routes per game (the survey's input reaches play in 6 games only). Tests: vbhost with the 12 ROMs in test-roms/vb/all/ (git-ignored, LAN copy); every change must keep each game's video hash equal with and without the skip.

Closed 2026-10-12 ~01:00 (history below).

State: fork branch vb-color @ b97cc5d9 (worktree ~/.cache/rg-vb), submodule xtensa-68000-dynarec branch v810jit @ 8d5ffbb; the board has b97cc5d9 installed (Fast CPU On, Colours Auto, Wario pack on the SD). Nothing is merged to fork master yet (merge when 50 fps with colours holds and the NGP gate passes on the final build).

Numbers (Wario cave route, median / mean / max, Fast CPU On): 56326cd7 no colours 45 / 39 / 53 (core 0 55 %, core 1 100 %); 5165b9aa Red 44, Auto + pack 35.5 (core 1 100 %); scaling off +1.5 only. b97cc5d9 (pack cheaper, big-endian output, temporary classic switch): look in scripts/mamebench/results/ first — the board was measuring it at close (route ×3 Auto + pack and Red), plus the NGP gate and the profile if it got to them.

Next, in order:

  1. Read b97c's numbers. If Auto + pack is still well under Red, the pack path on core 1 is next: the per-pixel output conversion (4 lookups a word instead of 2 — a hybrid pair table), then DrawBG's map reads.
  2. Core 1 without the pack is at 100 % at 45 fps: profile b97cc5d9-vbprof (VBCORE1 split vb_vip / rg_display), then tile snapshot 0 back into internal RAM (free 16 KB: the slot tables could be 2 B) and the display send.
  3. The Off path's 2 fps under the interpreter build (placement) is still unexplained; low priority.
  4. Packs: fills, context groups (markers), right-eye packs — only after 50 fps.
  5. Then: merge vb-color → fork master, pointer in this repo, NGP gate on the merged build.

Tools: vbhost (FAST=1 AUTO=1 PACK=wario.vbc DUMPAT=n DUMPFILE=x.ppm) to look at any frame on the Mac; vbjit-test in QEMU (VBFAST=1 VBJIT=1 VBIRAM=1, reference 27dafbfd / beccd497) for interpreter = recompiler; scratchpad/build_round.sh (recreate: build play + VBPROF, stage both with bin_relay.py, keep the VBPROF ELF under ~/.cache/bin-relay/elf/).