⚠️ Clean-Room / Educativo

Este proyecto es educativo y Open Source. No se copia código de otros emuladores. Implementación basada únicamente en documentación técnica y tests permitidas.

Step 0260: MBC1 ROM Banking

Fecha: 2025-12-23 Step ID: 0260 Estado: Draft

Resumen

Este Step implementa soporte básico de MBC1 (Memory Bank Controller 1) en la MMU de C++ para permitir que los juegos grandes (>32KB) accedan a sus bancos de ROM. El diagnóstico del Step 0259 confirmó que Pokémon Red estaba escribiendo ceros en VRAM porque intentaba leer gráficos de bancos ROM no mapeados. Con MBC1 implementado, los juegos pueden seleccionar bancos de ROM y leer los datos correctos.

Problema Resuelto: Pokémon Red (1024KB ROM) intentaba copiar gráficos desde el banco 2, 3, etc., pero nuestra MMU solo tenía mapeado el banco 0. El juego leía ceros o basura, y copiaba esos ceros a la VRAM, resultando en una pantalla verde. Con MBC1, el juego puede seleccionar el banco correcto y leer los datos gráficos reales.

Concepto de Hardware

MBC1 (Memory Bank Controller 1)

Los cartuchos de Game Boy pueden tener diferentes tamaños de ROM:

  • ROM ONLY (32KB): Cabe entero en el espacio de direcciones `0x0000-0x7FFF`. No necesita MBC.
  • MBC1 (>32KB): Usa un Memory Bank Controller para intercambiar bancos de ROM. El espacio `0x0000-0x3FFF` siempre mapea al Banco 0 (fijo), pero el espacio `0x4000-0x7FFF` puede mapear a diferentes bancos (1, 2, 3, etc.) escribiendo en registros especiales del MBC.

MBC1 Banking Control

El MBC1 controla el cambio de bancos mediante escrituras en el rango de ROM (que normalmente es de solo lectura):

  • 0x2000-0x3FFF: Selección de banco ROM. El valor escrito (bits 0-4) selecciona el banco que aparecerá en `0x4000-0x7FFF`. Nota: El banco 0 se trata como banco 1.
  • 0x0000-0x1FFF: Habilitación/deshabilitación de RAM externa (ignorado en esta implementación básica).

Mapeo de Memoria MBC1

  • 0x0000-0x3FFF: Siempre mapea al Banco 0 (fijo). No se puede cambiar.
  • 0x4000-0x7FFF: Mapea al banco seleccionado mediante escritura en `0x2000-0x3FFF`. Cada banco es de 16KB (0x4000 bytes).

Fuente: Pan Docs - "MBC1", "Memory Bank Controllers", "Cartridge Types"

Implementación

1. Modificación en `MMU.hpp`

Se añadieron dos miembros privados para soportar MBC1:

/**
 * --- Step 0260: MBC1 ROM BANKING ---
 * Almacena el cartucho ROM completo (puede ser >32KB).
 * Se usa para acceder a bancos de ROM conmutables.
 */
std::vector<uint8_t> rom_data_;

/**
 * --- Step 0260: MBC1 ROM BANKING ---
 * Banco de ROM actualmente seleccionado para el rango 0x4000-0x7FFF.
 * Inicializado a 1 (el banco 0 está siempre mapeado en 0x0000-0x3FFF).
 * Nota: En MBC1, el banco 0 se trata como banco 1 cuando se selecciona.
 */
uint8_t current_rom_bank_;

2. Modificación en `MMU.cpp` (Constructor)

Se inicializa `current_rom_bank_ = 1` en el constructor:

MMU::MMU() : memory_(MEMORY_SIZE, 0), ppu_(nullptr), timer_(nullptr), 
             joypad_(nullptr), debug_current_pc(0), current_rom_bank_(1) {

3. Modificación en `MMU.cpp` (Método `load_rom`)

Se modificó para cargar toda la ROM en `rom_data_` en lugar de solo 32KB:

void MMU::load_rom(const uint8_t* data, size_t size) {
    // Redimensionar rom_data_ y copiar toda la ROM
    rom_data_.resize(size);
    std::memcpy(rom_data_.data(), data, size);
    
    // También copiar el banco 0 (primeros 16KB) a memory_ para compatibilidad
    size_t bank0_size = (size > 0x4000) ? 0x4000 : size;
    std::memcpy(memory_.data(), data, bank0_size);
    
    // Inicializar el banco actual a 1
    current_rom_bank_ = 1;
    
    printf("[MBC1] ROM loaded: %zu bytes (%zu banks)\n", size, size / 0x4000);
}

4. Modificación en `MMU.cpp` (Método `read`)

Se añadió lógica para leer del banco correcto según la dirección:

// --- Step 0260: MBC1 ROM BANKING ---
// Si hay datos ROM cargados, usar banking
if (!rom_data_.empty()) {
    if (addr >= 0x0000 && addr <= 0x3FFF) {
        // Banco 0 fijo: leer desde el principio de la ROM
        if (addr < rom_data_.size()) {
            return rom_data_[addr];
        }
        return 0x00;  // Fuera de rango
    } else if (addr >= 0x4000 && addr <= 0x7FFF) {
        // Banco conmutable: calcular offset
        // Offset = (banco * 0x4000) + (addr - 0x4000)
        size_t bank_offset = static_cast<size_t>(current_rom_bank_) * 0x4000;
        size_t rom_addr = bank_offset + (addr - 0x4000);
        
        if (rom_addr < rom_data_.size()) {
            return rom_data_[rom_addr];
        }
        return 0x00;  // Fuera de rango
    }
}

5. Modificación en `MMU.cpp` (Método `write`)

Se añadió lógica para interceptar escrituras en `0x2000-0x3FFF` y cambiar el banco ROM:

// --- Step 0260: MBC1 ROM BANK SELECTION ---
// Interceptar escrituras en 0x2000-0x3FFF para cambiar el banco ROM
if (addr >= 0x2000 && addr <= 0x3FFF) {
    // Selección de banco ROM (bits 0-4 del valor escrito)
    uint8_t bank = value & 0x1F;  // Máscara para bits 0-4
    
    // En MBC1, el banco 0 se trata como banco 1
    if (bank == 0) {
        bank = 1;
    }
    
    // Validar que el banco no exceda el tamaño de la ROM
    size_t max_banks = rom_data_.size() / 0x4000;
    if (max_banks > 0 && bank >= max_banks) {
        bank = max_banks - 1;  // Limitar al último banco disponible
    }
    
    current_rom_bank_ = bank;
    
    // Log de diagnóstico (solo primeras 10 veces)
    static int bank_change_counter = 0;
    if (bank_change_counter < 10) {
        printf("[MBC1] PC:%04X -> ROM Bank changed to %d (max: %zu)\n", 
               debug_current_pc, current_rom_bank_, max_banks);
        bank_change_counter++;
    }
}

Decisiones de Diseño

  • Almacenamiento completo de ROM: Se almacena toda la ROM en `rom_data_` para permitir acceso a cualquier banco, no solo los primeros 32KB.
  • Compatibilidad con código existente: El banco 0 también se copia a `memory_[0x0000-0x3FFF]` para mantener compatibilidad con código que accede directamente a `memory_`.
  • Validación de bancos: Se valida que el banco seleccionado no exceda el tamaño de la ROM para evitar accesos fuera de rango.
  • Log limitado: El log de cambio de bancos se limita a las primeras 10 veces para no saturar la salida.

Archivos Afectados

  • src/core/cpp/MMU.hpp - Añadidos miembros `rom_data_` y `current_rom_bank_` para soportar MBC1 (Step 0260).
  • src/core/cpp/MMU.cpp - Modificado constructor, `load_rom()`, `read()` y `write()` para implementar MBC1 básico (Step 0260).

Tests y Verificación

Validación de MBC1:

  1. Recompilación: Ejecutar .\rebuild_cpp.ps1 para recompilar la extensión C++.
  2. Ejecución: Ejecutar python main.py roms/pkmn.gb (Pokémon Red es ideal porque tiene 1024KB de ROM y necesita MBC1).
  3. Observación del Log: Buscar en el log:
    • [MBC1] ROM loaded: X bytes (Y banks) - Confirma que la ROM se cargó correctamente.
    • [MBC1] PC:XXXX -> ROM Bank changed to N - Confirma que el juego está cambiando bancos.
    • [VRAM] PC:XXXX -> Write VRAM [XXXX] = XX - Los valores deberían ser distintos de `00` ahora.
  4. Observación Visual: Si MBC1 funciona correctamente, deberías ver gráficos en pantalla (con la paleta de debug activa).

Comando de Prueba:

.\rebuild_cpp.ps1
python main.py roms/pkmn.gb

Resultado Esperado:

  • El log debe mostrar que la ROM se cargó con múltiples bancos.
  • El log debe mostrar cambios de banco cuando el juego intenta acceder a diferentes partes de la ROM.
  • Los logs de VRAM deben mostrar valores distintos de `00` (datos gráficos reales).
  • Si todo funciona, deberías ver la intro de Pokémon en pantalla.

Validación de Módulo Compilado C++: El código se ejecuta en C++ compilado, por lo que es necesario recompilar la extensión antes de ejecutar. MBC1 se ejecuta en tiempo real durante la emulación, permitiendo que los juegos accedan a bancos de ROM correctos.

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • MBC1 Banking: Los cartuchos grandes (>32KB) usan MBC1 para intercambiar bancos de ROM. El espacio `0x0000-0x3FFF` siempre mapea al Banco 0 (fijo), pero el espacio `0x4000-0x7FFF` puede mapear a diferentes bancos escribiendo en `0x2000-0x3FFF`.
  • Problema de VRAM Vacía: Si el juego intenta leer gráficos del banco 2, 3, etc., pero solo se cargó el banco 0, leerá basura o ceros. La CPU copia esos "ceros" a la VRAM, resultando en una pantalla verde.
  • Solución MBC1: Al implementar MBC1, el juego puede seleccionar el banco correcto y leer los datos gráficos reales. Esto permite que los juegos grandes carguen sus gráficos correctamente.
  • Almacenamiento de ROM: Para soportar MBC1, necesitamos almacenar toda la ROM (no solo 32KB) en un vector separado (`rom_data_`) y calcular el offset correcto según el banco seleccionado.

Lo que Falta Confirmar

  • Funcionamiento Real: Ejecutar el diagnóstico con Pokémon Red y verificar que los gráficos aparecen en pantalla. Si los gráficos aparecen, confirmamos que MBC1 funciona correctamente.
  • Valores en VRAM: Verificar que los logs de VRAM muestran valores distintos de `00` (datos gráficos reales) después de implementar MBC1.
  • RAM Banking: MBC1 también soporta RAM externa (para guardar partidas), pero por ahora solo implementamos ROM banking. La RAM banking se puede añadir más adelante si es necesario.

Hipótesis y Suposiciones

Hipótesis Principal: Pokémon Red estaba escribiendo ceros en VRAM porque intentaba leer gráficos de bancos ROM no mapeados. Con MBC1 implementado, el juego puede seleccionar el banco correcto y leer los datos gráficos reales, permitiendo que los gráficos aparezcan en pantalla.

Suposición: Asumimos que la implementación básica de MBC1 (solo ROM banking, sin RAM banking) es suficiente para que los juegos grandes carguen sus gráficos. Si hay problemas, podemos añadir soporte para RAM banking más adelante.

Próximos Pasos

  • [ ] Recompilar: .\rebuild_cpp.ps1
  • [ ] Ejecutar: python main.py roms/pkmn.gb (Pokémon Red es ideal porque tiene 1024KB de ROM).
  • [ ] Observar los logs:
    • ¿Aparece `[MBC1] ROM loaded: X bytes (Y banks)`? Confirma que la ROM se cargó correctamente.
    • ¿Aparece `[MBC1] PC:XXXX -> ROM Bank changed to N`? Confirma que el juego está cambiando bancos.
    • ¿Los logs `[VRAM]` muestran valores distintos de `00`? Confirma que la CPU está copiando datos gráficos reales.
  • [ ] Observación Visual: Si MBC1 funciona correctamente, deberías ver gráficos en pantalla (con la paleta de debug activa). Si ves la intro de Pokémon, ¡MBC1 funciona!
  • [ ] Si hay problemas:
    • Verificar que el banco seleccionado no excede el tamaño de la ROM.
    • Verificar que el cálculo del offset del banco es correcto.
    • Verificar que el juego está escribiendo en `0x2000-0x3FFF` para cambiar bancos.