Emulador de GB – 20: Bugs

Para probar el emulador obviamente hay que probar juegos, todos los que sea posible. Al implementar los componentes principales los juegos de a poco empiezan a funcionar, pero cada juego hace las cosas a su manera y algunos dependen de comportamientos muy específicos del hardware.

Cada tanto pruebo algunos juegos de GB y GBC que elegí medio al azar y anoto cuales tienen problemas, y cuando los hago funcionar agrego mas juegos a las pruebas. Los problemas pueden ser muy variados, pero un mismo problema puede afectar a varios juegos. Es importante probar los que ya funcionaban cada tanto para segurarse de que un fix no rompa otras cosas.

La idea de este post es hablar sobre algunos errores que arreglé últimamente. Algunos de estos problemas tienen la particularidad de que dependen de cosas que no están del todo documentadas, por esto la forma de arreglarlos puede que no sea la correcta.

Cómo dije antes, un error puede afectar a mas de un juego, así que aunque ponga capturas de uno en particular el fix puede servir para otros que no probé.

Turok – Rage Wars (HDMA1-5)

Este juego tenía problemas gráficos importantes y mas allá de que el juego avanza (se puede llegar hasta el momento de gameplay) era claramente injugable. Se veía así:

Pero tendría que verse así:

En todas las pantallas se dibuja el fondo hasta cierto punto, después de ahí se ve todo blanco. La flecha que se ve mas abajo es un sprite, por eso se ve bien.

Lo primero que hice fue mirar las ventanas de debug de Tiles y la del TileMap 0 que es el fondo en esas pantallas:

La información en los tiles está, pero el tilemap no está haciendo referencia a los tiles correctos, así que usé los logs del emulador para ver si encontraba algo y vi esto:

HDMA1 = d0
HDMA2 = 00
HDMA3 = 18
HDMA4 = 00
mode: 0
HDMA5 = 00
GDMA started <- Inicia una transferencia de 0x10 bytes
Transfering
Count: 10
Source: d000
Dest: 9800 <- [0x9800, 0x9C00] define los tiles que usa el TileMap 0
VRAM0[9800] = 01
...
VRAM0[980f] = 0f
Transfer finished
HDMA5 = 00
GDMA started <- Inicia otra transferencia de 0x10 bytes 
Transfering
Count: 10
Source: d000 <- Mismo origen
Dest: 9800 <- Mismo destino
VRAM0[9800] = 01
...
VRAM0[980f] = 0f
Transfer finished

Marqué en negrita las direcciones de origen y destino que usa DMA para copiar la información. En lugar de copiar 0x10 bytes de D000 a 9800 y después copiar 0x10 de D010 a 9810 (y así hasta completar la pantalla), está copiando siempre los bytes de las primeras 16 posiciones.

También se puede ver que HDMA1-4 (definen el origen y el destino) se setean una sola vez y después se inicia varias veces la transferencia escribiendo 00 en HDMA5. Esto es raro, la mayoría de los juegos que probé hasta ahora siempre escriben HDMA1-4 antes de cada escritura a HDMA5.

El problema en este caso es un comportamiento que no leí en ningún lado: HDMA1-4 se modifican mientras HDMA copia los bytes! De esta forma, al terminar la primer transferencia de 0x10 bytes, HDMA2 y HDMA4 van a incrementarse en 0x10, y cada 0x100 bytes también van a incrementarse HDMA1 y HDMA3. Por esto es posible copiar valores para toda la pantalla escribiendo varias veces seguidas en HDMA5 pero una sola vez en HDMA1-4.

Para arreglar esto sólo tuve que asegurarme de actualizar HDMA1-4 cada vez que termina una copia con la cantidad de bytes copiados.

3D Ultra Pinball, Little Mermaid (HDMA y HBlank)

Otros dos juegos que comparten código y un mismo bug. En este caso de vuelta era un problema gráfico que hacía que fueran injugables:

Y así es como tienen que verse:

Viendo los TileMaps se ve que la información no está bien, que puede ser por dos motivos: los TileMaps están mal creados o los Tiles están desordenados. Se me ocurrió revisar lo primero, así que busqué en los logs cuando se escribe la memoria desde 0x9800 y vi esto:

line: 29
mode: 2
mode: 3
mode: 0 : <- GPU en modo HBlank
HDMA4 = 00
HDMA3 = 08
HDMA2 = 00
HDMA1 = c8
HDMA started
HDMA5 = 80 ; 00 <- Configura HDMA para transferir 0x10 bytes en modo HBlank
line: 30 <- Nueva línea
mode: 2
mode: 3
mode: 0 : <- GPU en modo HBlank
Transfering
Count: 10
Source: c800
Dest: 8800
VRAM0[8800] = 00
....
VRAM0[880f] = 00
Transfer finished
HDMA4 = 10
HDMA3 = 08
HDMA2 = 10
HDMA1 = c8
HDMA started
HDMA5 = 80 ; 00 <- Configura de nuevo HDMA para transferir 0x10 bytes en modo HBlank
line: 31
mode: 2
mode: 3
mode: 0
Transfering
Count: 10
Source: c810
Dest: 8810
VRAM0[8810] = 00
....
VRAM0[881f] = 00
Transfer finished

HDMA tiene un modo que copia de a 16 bytes en cada HBlank a partir del momento en que se escribe HDMA5 y estos juegos lo usan, pero lo usan raro.

En primer lugar el modo HDMA está pensado para escribir una sola vez en HDMA5, y el hardware automáticamente va a copiar de a 0x10 bytes en cada HBlank, pero acá están usándolo para copiar de a 0x10 bytes y hacen una escritura nueva en cada línea. Normalmente el valor de HDMA5 debería haber sido 0xFF, y eso automáticamente hubiese copiado 0x800 bytes.

Pero ese uso raro no es el problema, el problema es que inician la transferencia cuando la GPU está en modo HBlank, algo que según la documentación no se recomienda.

Al leer eso asumí que ningún juego hacía eso, así que programé que después de escribir HDMA, cada vez que se inicie el modo HBlank se copien valores. Estos juegos no estaban siguiendo el consejo, así que se me ocurrió que capaz si se escribe HDMA5 durante un HBlank se hace una primera transferencia en ese momento. Implementé ese cambio y los 2 juegos se arreglaron!

El fix es muy simple, sólo agregué lo que está en negrita:

void GPU::Write(u8 value, u16 address) {
  ...
  case 0xFF55:
    dma.Write(value, address);
    if (mode == GPUMode::HBlank)
      dma.StepHDMA();
    break;
  ...
}

Lufia (MBCs)

Este juego directamente no iniciaba. Me gusta tratar de arreglar juegos con este problema porque es mucho mas fácil de reproducir y comparar con el debugger de BGB.

La verdad para este problema tuve que comparar paso a paso con BGB hasta que me di cuenta que estaba pasando, y es otro de esos comportamientos no documentados. Antes de explicar la solución voy a explicar el problema.

Ya conté en un post como los juegos pueden tener diferentes tamaños gracias a los distintos tipos de MBC que existen. La función del MBC es “mapear” una parte de todo el contenido del juego en una región de memoria, y a estas partes se las llama “bancos”. En el header del juego uno de los bytes determina cuantos bancos tiene el juego, porque para un mismo tipo de MBC algunos juegos pueden ser mucho mas chicos que otros.

Cuando se quiere seleccionar un banco en particular se escribe un número en cierta dirección de memoria, y ese número lo usa el MBC para elegir el banco. Este número es un byte, así que en teoría se puede escribir cualquier número de 0 a 255… ¿Pero qué pasa si un juego con 64 bancos escribe, por ejemplo, 250? No está documentado, pero además uno podría pensar… ¿Si un juego tiene 64 bancos, por qué escribiría un número de banco mayor? Los motivos la verdad no los conozco, pero resulta que hay juegos que lo hacen! Lufia es solo un caso, muchos otros hacen lo mismo.

La solución en este caso la tuve que deducir, y lo que entiendo es que si el RomBank seteado es mayor al disponible, se hace un “mod”. Es decir, si se escribe el valor “250” pero el juego sólo tiene 64 bancos, se selecciona el banco 58 (250 % 64).

Esto, por lo que pude ver con otros juegos que tenían problemas, es cierto para TODOS los MBCs.

Darkwing (LCDC)

En algunas transiciones entre menues/juego, la imagen se veía rara.

Me llamó la atención y probé en otros emuladores, y vi que en esos momentos lo que debería verse es la pantalla toda blanca. Ahí me acordé que en la documentación de la GPU aclaran que cuando la pantalla se apaga se ve todo blanco (no negro como en un monitor moderno), así que me imaginé que era eso.

El problema en mi emulador no es que eso funcionara mal, directamente es algo que había leído en la documentación pero que nunca implementé, así que lo implementé teniendo en cuenta esto:

  • Cuando el bit 7 LCDC pasa de 1 a 0 significa que la pantalla se apagó y tiene que verse toda blanca
  • En GB y en GBC en modo Non-CGB, cuando el bit 0 de LCDC es 0 el fondo no se tiene que dibujar, así que esa línea tiene que quedar blanca (después la ventana o los sprites pueden dibujarse encima del blanco)

Para implementar esto se me ocurrió que podía usar la estructura de PixelInfo que definí cuando implemente soporte para GBC. Esa estructura tiene información (paleta, si es bg o sprite, etc) que después se usa para determinar el color de cada pixel. Cuando la definí me sobró un bit, así que adapté un poco las cosas para que ese bit en 1 signifique que es blanco:

typedef union {
  u8 v;
  struct {
    u8 colorIndex : 2;   // Bits 0 - 1 colorIndex [0,3]
    u8 paletteIndex : 3; // Bits 2 - 4 paletteIndex [0,7] for CGB, [0,1] for sprites in GB, 0 for BG in GB
    bool isBG : 1;       // Bit 5      bg/win or sprite, 1 for BG, 0 for sprites
    bool bgPriority : 1; // Bit 6      BG priority (CGB only, used when rendering sprites)
    bool blank : 1;      // Bit 7      Set to 1 when LCD is off
  };
} PixelInfo;

Así, al apagar la GPU puedo poner toda la pantalla en blanco de esta forma:

void GPU::Write(u8 value, u16 address) {
  ...
  case 0xFF40: {
    bool wasOn = LCDC.displayOn;
    LCDC.v = value;
    if (LCDC.displayOn != wasOn) {
      if (LCDC.displayOn) {
        SetCurrentLine(0);
        mode = GPUMode::OAMAccess;
        modeCycles = 0;
      } else
        memset(screen, 0x80, LCDWidth * LCDHeight); // 0x80 = PixelInfo con el bit "blank" en 1
    }
    break;
  }
  ...
}

Y cuando voy a dibujar una línea, si el fondo está apagado, puedo poner los 160 pixeles en blanco de esta forma:

void GPU::DrawLine(u8 line) {
  bool drawBG = isCGB || LCDC.bgOn;
  ...
  if (drawBG)
    DrawBackground(line, drawWIN ? WX : LCDWidth + 8);
  else
    memset(screen + line*LCDWidth, 0x80, LCDWidth); // 0x80 = PixelInfo con el bit "blank" en 1
  ...
}

Por último tengo que tener en cuenta el bit en GetABGR

ABGR GPU::GetABGR(PixelInfo pixelInfo) {
  if (pixelInfo.blank)
    return { 0xFFFFFFFF }; // blanco
  ...
}

Shantae (8×16)

Este juego se veía muy mal:

Y debería verse así:

En este caso estuve un montón de tiempo debuggeando y encontré la solución. Pensé que era algo no documentado, pero después busqué información y lo encontré explícito en la documentación, así que no voy a contar todo lo que hice para llegar a la solución porque no tiene sentido. El problema está relacionado con como se dibujan los sprites de 8×16. Para este tipo de sprites se hace referencia a un solo tile, pero se dibujan 2 consecutivos de memoria uno abajo del otro. Si un sprite usa el tile 0, en la pantalla se dibujan el 0 y abajo el 1.

Lo que me faltaba emular fue que para sprites de 8×16 hay que ignorar el bit 0 del “TileIndex”. Es decir, si el TileIndex es 1 no hay que dibujar los tiles 1 y 2, hay que dibujar el 0 y el 1.

El cambio en el código es muy simple:

void GPU::DrawSprites(u8 line) {
  u8 spriteHeight = LCDC.spritesSize == 0 ? 8 : 16;
  ...
  u8 tileIndex = oam[spriteIndex + 2];
  if (spriteHeight == 16)
    tileIndex &= 0xFE;
  ...
}

X-Men: Mutant Academy, Tony Hawks (Serial Transfer)

Estos dos juegos no comparten el código, pero tenían un problema similar: se quedaban trabados.

En estos casos lo primero que hago es mirar los logs a ver que está pasando, en general el código está en un loop esperando que pase algo (una interrupción o que un valor cambie) y eso nunca pasa. En los logs del X-Men noté que estaba bloqueado en este loop:

LDH A,[0xFF02]    ; [0xFF02] = ff
ADD A,A
JR C,-5                 ; F = 30

Para entender el problema hay que entender ese código, a la izquierda están las instrucciones que se ejecutaron y a la derecha después de “;” un comentario que logueo según la instrucción para saber que valores tienen ciertos registros o direcciones de memoria.

  • La primer instrucción está cargando en el registro A el valor en la dirección de memoria 0xFF02, que en ese momento vale 0xFF, así que A pasa a valer 0xFF.
  • La segunda instrucción suma A con A, es decir 0xFF + 0xFF. El resultado es 0x1FE, por lo tanto A pasa a valer 0xFE y el bit de Carry en el registro de F (flags) se poné en 1.
  • La última instrucción es un salto relativo condicional, va a volver 5 “bytes” para atrás si el bit de Carry de F está en 1 y va a seguir si está en 0. Cómo en el paso 2 se puso en 1 ese bit, el flujo vuelve a la primer instrucción.

El valor final de A en el segundo paso realmente no importa, esa instrucción se usa a veces para saber si el bit 7 de un registro está prendido o no, que es lo que interesa en este caso. Si A valiera 0x80, ADD A,A daría 0x100, es decir, también se pone en 1 el bit de Carry. Puede ser confuso siendo que existe una instrucción específica para chequear si cierto bit de un registro vale 1 o 0 (BIT 7,A), pero cuando se programaba en hardware tan limitado era común encontrar este tipo de optimizaciones. Es una optimización porque la instrucción ADD A,A usa un byte en el programa y tarda un ciclo de procesador, mientras que BIT 7,A usa 2 bytes y tarda 2 ciclos.

El registro 0xFF02 es “Serial Transfer Control” y el bit 7 tiene dos funciones: iniciar la transferencia a otro GameBoy conectado por Link Cable y reportar el estado actual de la transferencia (1 = en progreso, 0 = terminada). En mi emulador la transferencia nunca termina, el bit 7 siempre está en 1. Para arreglar este problema hay que tener en cuenta que haya o no otro GameBoy conectado, la transferencia “se hace”, tiene que terminar, es decir que después de cierto tiempo el bit 7 de 0xFF02 tiene que pasar a 0. Yo nunca implementé esto porque supuse que si no elegía opciones específicas de link no se iba a usar, pero no… Así que emulé la transferencia por link cable y actualizo el componente junto con el resto después de cada instrucción, y esos juegos ya no se traban.

void SerialDataTransfer::Step(u8 cycles, bool isDoubleSpeedEnabled) {
  if (bitsTransfered == 8)
    return;

  elapsedCycles += (isDoubleSpeedEnabled ? cycles * 2 : cycles);
  while (elapsedCycles >= maxCycles) {
    bitsTransfered++;
    elapsedCycles -= maxCycles;

    if (bitsTransfered == 8) {
      SC &= ~0x80; // pongo el bit 7 en 0 después de "transferir" 8 bits
      return;
    }
  }
}

Algo a tener en cuenta, aunque no se esté transfiriendo nada realmente, es que el modo Double Speed de la CPU afecta el tiempo de transferencia por serial, por eso tengo que pasarle el dato.

VideoPlayer (VBlank, OAM y LCDStat)

Esto es algo que leí en un foro y lo implementé por las dudas: https://forums.nesdev.com/viewtopic.php?f=20&t=13727

Este VideoPlayer aparentemente lo usa y funciona en hardware real, así que es posible que algún juego oficial también lo necesite.

Ya hablé varias veces sobre los 4 modos por los que pasa la GPU y sobre las interrupciones así que no voy a explicarlos en detalle de nuevo, pero aparentemente esto es lo que pasa:

  1. Se dibuja la línea 143 pasando por los modos normales de OAM -> VRAM -> HBlank
  2. Empieza la línea 144 y se pasa a modo VBlank
  3. PERO si el bit de OAM de LCDStat está seteado, se genera una interrupción de LCDStat como si el modo OAM hubiese iniciado (que no corresponde a partir de la línea 144).

Esto hace que durante un frame hayan 145 interrupciones de LCDStats por OAM, pero sólo 144 por HBlank. VideoPlayer espera 145 LCDStats por frame, así que depende de este comportamiento. La discusión en el foro se pone medio confusa y el comportamiento es tan oscuro que no se llega a una conclusión unánime, pero aparentemente esta solución funciona y hasta tiene cierta lógica. Mi implementación pasa el test rom que compartieron en ese foro.

Sagaia, The Jetsons, Final Fantasy Legend (LY 153)

En el post anterior conté que hay un comportamiento raro durante la última línea de VBlank pero revisando mas juegos noté que la explicación anterior no era suficiente. O sea, funciona para el modo HiColor de los juegos que probé mientras escribía el post anterior, pero para otros no. En Sagaia no se dibujaba el fondo, en The Jetsons el fondo no scrolleaba, Final Fantasy Legend no iniciaba.

En línea generales lo que conté en el post anterior sigue pasando, durante VBlank la última línea, equivalente a LY 153, se comporta como una falsa línea 0. Esta línea puede generar una interrupción si LYC vale 0, y cuando termina VBlank la línea siguiente es de nuevo LY 0. De esta forma antes de la línea 0 de la pantalla el juego tiene 456 ciclos para preparar cosas.

Al arreglar estos otros juegos tuve que cambiar dos cosas:

  • Si LYC vale 153, TAMBIÉN se tiene que generar la interrupción de LCDStat, yo estaba ignorarndo la línea 153 totalmente. Es raro, parecería que la línea 153 dura solo algunos ciclos y después pasa a valer 0 (y hace posible lo que expliqué de HiColor). Esto arregla Sagaia y The Jetsons.
  • Cuando la línea falsa 0 pasa a la línea real 0, NO se genera una interrupción de LCDStat. Esto arregla Final Fantasy Legends.

Adapté el código para esto pero no lo quiero compartir porque al hacer esto al menos 1 juego con HiColor dejó de funcionar, el Alone In The Dark. Cuando tenga alguna solución que funcione para todos seguramente actualizo este post con el resultado final.

Flash, Seaside Volley, Samurai Shodown y mas

Estos juegos y varios mas tenían problemas para dibujar los sprites en ciertos casos:

En esas capturas:

  • Samurai Shodown: No se ve el personaje de la derecha
  • Seaside Volley: La pelota está incompleta y no se ve el cursor
  • The Flash: Flash desaparece cuando sube escaleras

El problema lo arreglé para Seaside Volley y cuando probé los otros juegos vi que se habían arreglado también. Lo primero que hice fue mirar la ventana de Sprites pero ahí también faltaban. Después revisé cuando el estado de la memoria OAM (que define los sprites), pero todo parecía estar correcto. Cómo estaba medio perdido implementé mas información de debug en la ventana de Sprites: Al clickear en la pantalla me fijo en la memoria OAM si hay algún sprite en esa posición e imprimo los atributos. Al hacer esto confirmé que los sprites estaban bien definidos, pero no se dibujaban, así que el problema tenía que estar en como los dibujaba.

Después de debugear el código encontré varios problemas, pero se pueden resumir todos en un mismo problema mas general: si un bit de los atributos dice “Sólo en CGB” o “Solo en GB y Non-CGB”, hay que respetarlo!

Hay algunos bits en los atributos de los sprites que en GB o en Non-CGB no se usan, y otros que en CGB no se usan. Cómo en muchos otros casos, asumí que si el bit no se usa no tenía que importarme, pero en realidad hay que ignorarlos explícitamente. Es importante al emular GB y GBC con el mismo código, porque los juegos funcionan asumiendo que el valor de ciertos bits no importa, por lo que pueden ponerlos en 0 o en 1 y eso no debería afectar el funcionamiento.

Voy a dar un ejemplo del emulador para que se entienda mejor el problema, considerando el bit 3 de los atributos de sprites que indica en que banco de VRAM están los datos del tile. Un juego de GB puede tener un 1 o un 0 en el bit 3 de sus atributos porque realmente no debería hacer diferencia, pero si uso el valor puedo terminar buscando el tile en el banco 1, cuando en GB ese banco no existe.

Entonces, cuando se van a leer los bytes de un tile en un juego de GB hay que usar el banco 0, independientemente del atributo. También hay que tener en cuenta que los juegos de GB funcionando en Non-CGB pueden configurar cualquier valor en los bits [0,2] que determinan la paleta de GBC, así que en estos casos hay que ignorar el valor, porque para este modo las paletas ya están definidas de cierta forma.

No voy a copiar el código porque es medio confuso (igual que la explicación, perdón!), pero se puede ver en el respositorio donde tengo todo el código disponible: https://github.com/DiegoSLTS/gb

Black Bass, Hammerin’ Harry, Koro Dice y otros (Timer)

Hace mucho conté como funciona el timer, y eso está basado en la documentación, pero en ese link hay otro link a algo que nunca implementé: el “Obscure behaviour” del Timer.

Ese link explica unos comportamientos “extraños” del timer que ocurren por como está construido físicamente. Hay gráficos de lo que sería el circuito, y la verdad es difícil de explicar y difícil de entender, sobre todo sin un buen conocimiento de electrónica. Después de leerlo varias veces y hacer pruebas logré entender e implementar todos los puntos del título “Relation between Timer and Divider register”, pero me faltó implementar lo de “Timer Overflow Behaviour”.

Lo que creo que es importante de esto es que tuve que reimplementar el Timer, realmente se hace mas simple tratar de emular el circuito que simular y el comportamento raro termina cumpliéndose automáticamente. Con esto me refiero a que, por ejemplo, en lugar de tener DIV como una variable de 8 bits y distintos contadores, tengo una sola variable de 16 bits como usa el hardware, incremento siempre esa variable y si quiero leer “DIV” simplemente me quedo con los 8 bits mas altos.

Como lo tuve que reimplementar tampoco tiene sentido mostrar las diferencias, realmente cambié toda la clase, así que recomiendo mirar el código directamente: https://github.com/DiegoSLTS/gb/blob/master/emulador/emulador/Timer.cpp

Emulando estos comportamientos varios juegos empezaron a funcionar, porque este comportamiento termina generando mas interrupciones que la documentación original.

Block Kuzushi, Fighting Simulator, In Your Face (SerialTransfer, otra vez)

Estos juegos también se trababan y estaba relacionado con otros problemas de Serial Transfer, que en realidad es todo consecuencia de que mi implementación de este componente nunca la hice completa, lo vengo emparchando con lo mínimo porque realmente es algo que nunca planeé implementar.

A la implementación que mostré antes le faltan 2 cosas:

  • Cuando no hay nada conectado y se inicia una transferencia, el byte “recibido” es 0xFF.
  • Al terminar una transferencia se genera una interrupción. Lo importante de este punto es que incluso si no hay nada conectado y se recibe 0xFF, la interrupción no se va a generar, pero el bit 7 de SC tiene que resetearse.

El código final es así:

void SerialDataTransfer::Step(u8 cycles, bool isDoubleSpeedEnabled) {
  if (bitsTransfered == 8)
    return;

  elapsedCycles += (isDoubleSpeedEnabled ? cycles * 2 : cycles);
  while (elapsedCycles >= maxCycles) {
    SB <<= 1;
    SB |= 1;
    bitsTransfered++;
    elapsedCycles -= maxCycles;

    if (bitsTransfered == 8) {
      SC &= ~0x80;
      if (SB != 0xFF)
        interruptService.SetInterruptFlag(InterruptFlag::Serial);
      return;
    }
  }
}

Harvest Moon y otros (MBC1)

Estos juegos tenían problemas diferentes, el Harvest Moon por ejemplo se trababa, pero otros mostraban cosas raras. El problema lo resolví comparando el juego corriendo en BGB y encontré que estaba relacionado con un bug en mi implementación de MBC1.

El MBC 1 tiene una región en la que se puede escribir un byte y según como esté configurado, ese byte define parte del número de banco de la memoria ROM o el banco de la memoria RAM. El banco completo de la memoria ROM está definido por la combinación de ese byte y otro que se escribe en otra región.

Mi código era algo así antes de arreglarlo:

void MBC1::Write(u8 value, u16 address) {
  if (address = 0x2000 && address = 0x4000 && address < 0x6000) {
    if (romRamSwitch == 0) {
      u8 highBits = (value & 0x03) < ramBanksCount)
        ramBank = ramBanksCount;
      ramBankOffset = 8 * 1024 * ramBank;
    }
  } else if (address >= 0x6000 && address = 0xA000 && address < 0xC000 && ramEnabled)
    ram[ramBankOffset + address - 0xA000] = value;

  if (romRamSwitch == 0)
    romBankOffset = 16 * 1024 * romBank; // Este era el problema
}

Por la forma en que implementé los MBC, no uso el número de banco cada vez que se va a leer un byte del juego, sino que precalculo un offset del array con el contenido total del juego, así me ahorro algunas multiplicaciones. El problema era que por mal interpretar el “romRamSwitch”, estaba pasando que el juego escribía valores para los bits mas bajos del banco de la memoria ROM con el switch configurado para que los bits mas altos se usen para el banco de la RAM, y yo estaba calculando el offset solo si el switch estaba configurado para ROM. Es decir, estaba ignorando los cambios en el banco de la ROM, ya que el offset seguía calculado para el valor anterior.

Y arreglado es precalcular le offset siempre que se cambie el banco, y quedó así:

void MBC1::Write(u8 value, u16 address) {
  if (address = 0x2000 && address < 0x4000) {
    u8 lowBits = value & 0x1F;
    if (lowBits == 0)
      lowBits = 1;
    romBank &= 0x60;
    romBank |= lowBits;
    romBank %= romBanksCount;
    romBankOffset = 16 * 1024 * romBank; // Esta es la solución
  } else if (address >= 0x4000 && address < 0x6000) {
    if (romRamSwitch == 0) {
      u8 highBits = (value & 0x03) << 5;
      romBank &= 0x1F;
      romBank |= highBits;
      romBank %= romBanksCount;
      romBankOffset = 16 * 1024 * romBank; // Esta es la solución
    } else if (ramEnabled) {
      ramBank = value & 0x03;
      if (ramBank > ramBanksCount)
        ramBank = ramBanksCount;
      ramBankOffset = 8 * 1024 * ramBank;
    }
  } else if (address >= 0x6000 && address = 0xA000 && address < 0xC000 && ramEnabled)
    ram[ramBankOffset + address - 0xA000] = value;
}

Conclusión

Cuando empiezan a aparecer estos errores espécificos de algunos juegos puede ser bastante difícil encontrar la causa, pero después el fix es relativamente simple. Se complica todavía mas si la solución es por algo que no está del todo documentado, así que es importante tener muchas herramientas para debugear y buscar en foros o incluso código de otros emuladores. También cada vez que se me ocurre algo que me puede ayudar lo implemento, y ya tengo varias cosas que me permiten debuggear algunas cosas sin recurrir a otros emuladores. Por ejemplo, hasta ahora el TileViewer solamente mostraba los tiles, pero agregué que al clickear sobre uno me imprima en consola información del tile (dirección, atributos, etc).

Por el momento voy a dejar el post acá, hay mas juegos que no funcionan pero todavía no pude arreglarlos así que es probable que haga otro post con mas bugs arreglados en el futuro.

Deja un comentario

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s

Crea tu sitio web con WordPress.com
Empieza ahora
A %d blogueros les gusta esto: