← Return to Index

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)

  1. 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
  2. MMU.cpp (constructor): Initialization to false (normal mode)
    , test_mode_allow_rom_writes_(false) // Test mode disabled by default
  3. 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
    }
  4. 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)

  1. mmu.pxd: C++ method declaration
    void set_test_mode_allow_rom_writes(bool allow) # Step 0419
  2. 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 declaration
  • src/core/cpp/MMU.cpp- Implementation of test mode
  • src/core/cython/mmu.pxd- Cython statement
  • src/core/cython/mmu.pyx-Python Wrapper
  • tests/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

  1. Test mode isdisabled by default(false in constructor)
  2. Only tests should activate it explicitly
  3. Normal emulation (main.py with real ROMs) should NOT use this mode
  4. The early return inMMU::write()prevents MBC logs from being generated during tests
  5. Yeahrom_data_is too small, it expands automatically