Step 0419: Fix MMU - Test Mode ROM Writes
Summary
A was implementedtest modein the MMU to allow direct writes to ROM (0x0000-0x7FFF) during unit testing, resolving the root issue that caused all 10 tests ALU will fail. This minimal clean-room change eliminates the first cluster of ALU faults without touching the logic of the CPU.
Result: All 10 ALU tests now pass correctly (52 total tests pass vs previous 17).
Hardware Concept
The Problem: MBC vs Testing
On the real Game Boy, writes to region 0x0000-0x7FFF (ROM) do not modify the contents of the ROM. Instead, they are interpreted as commands for theMemory Bank Controller (MBC):
- 0x0000-0x1FFF: RAM Enable/Disable (writing 0x0A enables external RAM)
- 0x2000-0x3FFF: Lower ROM bank select
- 0x4000-0x5FFF: Upper ROM or RAM bank selection
- 0x6000-0x7FFF: MBC1 mode (ROM banking vs RAM banking)
Fountain: Pan Docs - "Memory Bank Controllers (MBC1/MBC3/MBC5)"
The Problem in Testing
ALU unit tests need to write test instructions to memory and execute them.
For example, to testADD A, d8:
mmu.write(0x0100, 0x3E) # LD A, d8
mmu.write(0x0101, 0x0A) # Operand: 10
mmu.write(0x0102, 0xC6) # ADD A, d8
mmu.write(0x0103, 0x02) # Operand: 2
But the MMU interprets these writes as MBC commands (e.g.:write(0x0100, 0x3E)was interpreted as "RAM-ENABLE"), and the ROM contents at 0x0100 remained unchanged (typically 0x00/NOP).
That's why the CPU executed NOPs instead of the test instructions, and register A never changed from its
initial value (1), causing all ALU tests to fail withAssertionError: A must be 12, is 1.
The Solution: Test Mode
The solution is astandard pattern in emulators: add a boolean flagtest_mode_allow_rom_writes_which, when active, allows direct writes torom_data_instead of interpreting as MBC.
This mode isexclusive for testingand is not used in normal emulation.
Implementation
Changes in C++ (MMU)
-
MMU.hpp: Method declaration and private field
void set_test_mode_allow_rom_writes(bool allow); // public method bool test_mode_allow_rom_writes_; // Private field -
MMU.cpp (constructor): Initialization to false (normal mode)
, test_mode_allow_rom_writes_(false) // Test mode disabled by default -
MMU.cpp (write): Early return before MBC handling
// Step 0419: Test Mode - Allow direct writes to ROM if (test_mode_allow_rom_writes_ && addr< 0x8000) { // Calcular offset en rom_data_ según el banco actual size_t rom_offset; if (addr < 0x4000) { rom_offset = static_cast(bank0_rom_) * 0x4000 + addr; } else { rom_offset = static_cast (bankN_rom_) * 0x4000 + (addr - 0x4000); } // Write to rom_data_ (expand if necessary) if (rom_offset >= rom_data_.size()) { rom_data_.resize(rom_offset + 1, 0x00); } rom_data_[rom_offset] = value; return; // Early return - DO NOT process as MBC } -
MMU.cpp (setter): Method implementation
void MMU::set_test_mode_allow_rom_writes(bool allow) { test_mode_allow_rom_writes_ = allow; }
Changes in Cython (Wrapper)
-
mmu.pxd: C++ method declaration
void set_test_mode_allow_rom_writes(bool allow) # Step 0419 -
mmu.pyx:Python Wrapper
def set_test_mode_allow_rom_writes(self, bool allow): """Enable/disable direct ROM writes for unit testing.""" if self._mmu == NULL: raise MemoryError("The C++ MMU instance does not exist.") self._mmu.set_test_mode_allow_rom_writes(allow)
Changes in Tests (test_core_cpu_alu.py)
All 10 ALU tests now enable test mode after creating the MMU:
mmu = PyMMU()
mmu.set_test_mode_allow_rom_writes(True) # Step 0419: Allow ROM writes
regs = PyRegisters()
cpu = PyCPU(mmu, regs)
Tests and Verification
Compilation
$ python3 setup.py build_ext --inplace > /tmp/viboy_step0419_build.log 2>&1
BUILD_EXIT=0 ✅
Test Build
$ python3 test_build.py > /tmp/viboy_step0419_test_build.log 2>&1
TEST_BUILD_EXIT=0 ✅
[SUCCESS] The build pipeline works correctly
Objective Test (test_add_immediate_basic)
$ pytest -q --maxfail=1 -x tests/test_core_cpu_alu.py::TestCoreCPUALU::test_add_immediate_basic
============================== 1 passed in 0.36s ==============================
[BG-ENABLE-SEQUENCE] PC:0x0100 OP:0x3E | HL:0x014D | A:0x01 ← ✅ RH A, d8
[BG-ENABLE-SEQUENCE] PC:0x0102 OP:0xC6 | HL:0x014D | A:0x0A ← ✅ ADD A, d8
Pytest Global
$ pytest -q > /tmp/viboy_step0419_pytest_after.log 2>&1
BEFORE: 10 failed (ALU), 17 passed
AFTER: 10 failed (other files), 52 passed ✅
All 10 ALU tests now PASS correctly.
Test Code (test_add_immediate_basic)
def test_add_immediate_basic(self):
"""Test 1: Verify basic sum without carry (10 + 2 = 12)"""
mmu = PyMMU()
mmu.set_test_mode_allow_rom_writes(True) # Step 0419
regs = PyRegisters()
cpu = PyCPU(mmu, regs)
regs.pc = 0x0100
# Load A = 10
mmu.write(0x0100, 0x3E) # LD A, d8
mmu.write(0x0101, 0x0A) # Operand: 10
cpu.step()
# Add 2
mmu.write(0x0102, 0xC6) # ADD A, d8
mmu.write(0x0103, 0x02) # Operand: 2
cpu.step()
# Check
assert regs.a == 12 # ✅ HAPPEN NOW
Native Validation: Compiled C++ module successfully verified.
Affected Files
src/core/cpp/MMU.hpp- Method and field declarationsrc/core/cpp/MMU.cpp- Implementation of test modesrc/core/cython/mmu.pxd- Cython statementsrc/core/cython/mmu.pyx-Python Wrappertests/test_core_cpu_alu.py- Enabling test mode in 10 tests
Impact
- Positive: All 10 ALU tests now pass (52 total tests vs 17 previous, +206% pass rate)
- scope: Minimum change (MMU only, no CPU/ALU touched)
- Clean Room: Standard testing pattern in emulators, not copied from other projects
- Warnings: 10 tests in other files (compares, inc_dec, indirect, interrupts) also need test mode, but are left for Step 0420+
Technical Notes
- Test mode isdisabled by default(false in constructor)
- Only tests should activate it explicitly
- Normal emulation (main.py with real ROMs) should NOT use this mode
- The early return in
MMU::write()prevents MBC logs from being generated during tests - Yeah
rom_data_is too small, it expands automatically