Step 0402 - Frontend Boot ROM Integration + Fileless Stub Mode
1. Hardware Concept
Boot ROM and Initialization Pipeline
TheBoot ROMof the Game Boy is a small internal ROM (256 bytes for DMG, 2304 bytes for CGB) which runs before the cartridge code. Its purpose is:
- Show the Nintendo logo: Logo scroll animation (license requirement)
- Validate the cartridge: Verify logo checksum in the ROM header
- Initialize hardware: Configure I/O registers (LCDC, BGP, CGB palettes, etc.)
- Transfer control: Write 0xFF50=1 to disable Boot ROM and jump to 0x0100
After Boot ROM, the hardware state is consistent and predictable. Games depend on this state for initialization. Without Boot ROM, the I/O registers are left at undefined or incorrect values, causing problems like BGP=0x00 (invalid palette, white screen).
Optional Boot ROM Support in the Emulator
To comply withClean Room compliance, the Boot ROM is NOT included in the repository. However, the emulator now supports:
- Real Boot ROM: User can provide their own Boot ROM (legally extracted from real hardware)
- stub mode: Minimal post-boot configuration without running proprietary binary (for validation)
- Skip-boot mode: Standard post-boot state (PC=0x0100, predefined registers)
Sources: Pan Docs - Power Up Sequence, Bread Docs - FF50 Register
2. Implementation
A. Frontend: CLI and Environment Variables
Modifications inmain.py
Two CLI flags were added:
parser.add_argument(
"--bootrom",
type=str,
default=None,
help="Path to Boot ROM file (DMG: 256 bytes, CGB: 2304 bytes)",
)
parser.add_argument(
"--bootrom-stub",
action="store_true",
help="Enable Boot ROM stub mode (no binary, only post-boot state)",
)
Priority logic:
- Yeah
--bootrom PATHis specified, load Boot ROM from file - If not, consult environment variable
VIBOY_BOOTROM - Yeah
--bootrom-stubis specified, activate stub mode - If nothing is specified, use skip-boot mode (default behavior)
Modifications insrc/viboy.py
The builder ofViboynow accepts optional parameters:
def __init__(
self,
rom_path: str | Path | None = None,
use_cpp_core: bool = True,
bootrom_bytes: bytes | None = None,
bootrom_stub: bool = False
) -> None:
#...
self._bootrom_bytes = bootrom_bytes
self._bootrom_stub = bootrom_stub
Inload_cartridge(), the Boot ROM or stub is applied:
if self._bootrom_bytes:
# Load real Boot ROM (user provided)
self._mmu.set_boot_rom(self._bootrom_bytes)
if self._mmu.is_boot_rom_enabled():
self._regs.pc = 0x0000 # Boot ROM active: PC starts at 0x0000
else:
self._initialize_post_boot_state()
elif self._bootrom_stub:
# Stub mode: activate stub without proprietary binary
self._mmu.enable_bootrom_stub(True, cgb_mode=False)
self._initialize_post_boot_state()
else:
# Skip-boot mode (default)
self._initialize_post_boot_state()
B. Backend: Stub Mode in C++
Implementation inMMU.cpp
The methodenable_bootrom_stub()applies a minimal post-boot state:
void MMU::enable_bootrom_stub(bool enable, bool cgb_mode) {
if (!enable) {
boot_rom_enabled_ = false;
boot_rom_.clear();
return;
}
// Set I/O registers to post-boot state (according to Pan Docs)
memory_[0xFF40] = 0x91; // LCDC: LCD ON, BG ON, Tilemap 0x9800
memory_[0xFF47] = 0xFC; // BGP: Standard palette (00=white, 11=black)
memory_[0xFF42] = 0x00; // SCY: Scroll Y = 0
memory_[0xFF43] = 0x00; // SCX: Scroll X = 0
memory_[0xFF48] = 0xFF; // OBP0: Sprite palette 0
memory_[0xFF49] = 0xFF; // OBP1: Sprite palette 1
memory_[0xFFFF] = 0x01; // IE: VBlank interrupt enabled
// Write 0xFF50 = 1 to simulate the Boot ROM finished
memory_[0xFF50] = 0x01;
boot_rom_enabled_ = false;
boot_rom_.clear();
}
Wrapper Cython (mmu.pyx)
def enable_bootrom_stub(self, bool enable, bool cgb_mode=False):
"""
Enables Boot ROM stub mode (no proprietary binary).
The stub does NOT emulate real boot instructions. Just force a minimal set
Documented post-boot status (Pan Docs) and flag boot_rom_enabled_=false
immediately.
"""
if self._mmu == NULL:
raise MemoryError("The C++ MMU instance does not exist.")
self._mmu.enable_bootrom_stub(enable, cgb_mode)
3. Tests and Verification
Compile Command
python3 setup.py build_ext --inplace
Result: ✅ Compilation successful (no errors)
Controlled Tests (30 seconds each)
Test 1: Tetris DX (skip-boot, baseline)
timeout 30s python3 main.py roms/tetris_dx.gbc > logs/step0402_skip_tetris_dx.log 2>&1
Results:
- ✅ No regressions
- Frame 720:
gameplay_state=YES - TileData: 23.0% (1416/6144 bytes)
- UniqueTiles: 256/256
- LCDC: changed to 0x81 in frame 677
- BGP: changed to 0xE4 on frame 711
Test 2: Zelda DX (skip-boot, baseline)
timeout 30s python3 main.py roms/Oro.gbc > logs/step0402_skip_zelda_dx.log 2>&1
Results:
- LCDC: 0xE3 from frame 0 (changed immediately)
- BGP: 0x00 from frame 0 (known issue: invalid palette)
- TileData: 0% (never loads tiles)
- TileMap: 100% (2048/2048 bytes, but all 0x00)
- UniqueTiles: 1/256 (only one tile ID)
gameplay_state=NO
Test 3: Zelda DX (stub)
timeout 30s python3 main.py --bootrom-stub roms/Oro.gbc > logs/step0402_stub_zelda_dx.log 2>&1
Results:
- ✅ Stub activated correctly:
[BOOTROM-STUB] Activating stub mode (DMG) - Stub configured: LCDC=0x91, BGP=0xFC, SCY=0, SCX=0, OBP0=0xFF, OBP1=0xFF, IE=0x01, FF50=0x01
- But: The game overwrites BGP to 0x00 immediately (frame 0)
- LCDC: changed to 0xE3 by the game (frame 0)
- Results identical to skip-boot: TileData 0%, gameplay_state=NO
Analysis of Results
Tetris DX: No regressions. Skip-boot mode (default) still works perfectly.
Zelda DX: The stub was successfully activated and configured I/O registers, but the gameoverwrites BGP to 0x00 immediately. This confirms the hypothesis of Step 0400:
Zelda DX/Pokemon Redexpect the actual Boot ROM to configure specific CGB palettes, not just the basic I/O registers. The stub configures basic DMG values (BGP=0xFC), but the code The game assumes that the Boot ROM has already configured CGB palettes and overwrites them with specific values (which are 0x00 when there is no valid data).
This means that the stubtechnically it works(post-boot state applies), but it is not enough for games that depend on the Boot ROM's advanced CGB configuration.
4. Conclusions and Next Steps
Step 0402 Achievements
- ✅ Official optional Boot ROM integration on frontend (CLI + env var)
- ✅ Stub mode implemented in C++ (no proprietary binary)
- ✅ No regressions in working ROMs (Tetris DX)
- ✅ Validated end-to-end wiring (frontend → backend → MMU)
- ✅ Prepared for real Boot ROM when the user provides it
Stub Limitations
The stubdoes not emulate the real Boot ROM. Just configure a minimal post-boot state (DMG). Games that rely on advanced CGB settings (CGB palettes, HDMA, etc.) will not work with the stub.
Next Steps
- Step 0403: Document how user can legally extract Boot ROM from real hardware
- Step 0404: Implement partial Boot ROM emulation (logo scroll, no proprietary code)
- Step 0405: Investigate specific CGB settings that Zelda DX/Pokemon Red expect
Lessons Learned
- Clean Room and pragmatism: We can support Boot ROM without violating clean room (user provides it)
- Stub vs real Boot: The stub is useful for validation, but does not replace the actual Boot ROM
- Game dependencies: Some games rely heavily on Boot ROM (more than expected)
5. Modified Files
Frontend
main.py: Added flags--bootromand--bootrom-stub, reading env varVIBOY_BOOTROMsrc/viboy.py: Builder updated to receivebootrom_bytesandbootrom_stub, application logic inload_cartridge()
Backend (C++)
src/core/cpp/MMU.hpp: Declaration ofenable_bootrom_stub()src/core/cpp/MMU.cpp: Implementation ofenable_bootrom_stub()
Cython Wrapper
src/core/cython/mmu.pxd: Declaration ofenable_bootrom_stub()src/core/cython/mmu.pyx:Python wrapperenable_bootrom_stub()
Documentation
docs/bitacora/entries/2026-01-01__0402__bootrom-cli-env-y-stub.html: This entrydocs/bitacora/index.html: Updated with link to this entrydocs/report_phase_2/part_00_steps_0370_0401.md: Updated with Step 0402
6. References
- Pan Docs - Power Up Sequence
- Bread Docs - FF50 Register
- Pan Docs - Boot ROM
- Step 0400 - Comparative Analysis: Tetris DX vs Zelda DX/Pokemon Red
- Step 0401 - Optional Boot ROM + Correct I/O Initialization