06 junio, 2015

Sobre las pruebas unitarias del Z80

El emulador Z80 tiene pruebas unitarias desde el primer momento. La intención de las pruebas unitarias de mi emulador es verificar que la implementación de los opcodes es correcta y libre de bugs. Para ello el emulador se está desarrollando mediante metodología TDD. Cuando quiero implementar un opcode sigo el siguiente proceso:

  1. Examino la documentación del Z80 para comprender qué debe hacer el opcode.
  2. Escribo la prueba unitaria correspondiente.
  3. Compruebo que la prueba falla.
  4. Implemento el opcode.
  5. Compruebo que la prueba se pasa.
Sin embargo, me dejo en el tintero lo más importante: ¿cómo haces pruebas unitarias de un emulador? ¿Pruebas la función de cada opcode? Claramente no, en tanto que los opcodes los estoy implementando como funciones static.

El secreto se encuentra en considerar a la CPU como una máquina que cambia estados. Una CPU con un estado de entrada E ejecuta un opcode y genera un estado de salida S. Por ejemplo, si ejecutamos el opcode INC B, con un estado de entrada en el que B = 0x04, el estado de salida deberá cumplir que B = 0x05. Similarmente, otros opcodes manipulan registros, memoria y flags, pero siempre de una forma predecible.

Ese es precisamente el fundamento que siguen estas pruebas unitarias. Como los opcodes documentados son predecibles, todo lo que hace una prueba unitaria del Z80 es:
  1. Instanciar una CPU.
  2. Establecer el estado de entrada E.
  3. Cargar en la memoria de la CPU el código máquina a probar y ejecutarlo.
  4. Verificar el estado de salida S.
El estado de salida S se verifica del siguiente modo:
  • Un registro que no sea modificado por el opcode, deberá tener el mismo valor en E y en S.
  • Un registro que sea modificado por el opcode, deberá tener un valor predecible en S siempre que conozcamos E. Es el ejemplo que he puesto antes: si ejecuto el opcode INC B y en mi estado de entrada E el registro B vale 0x04, en S ese registro deberá valer 0x05, porque ha tenido que ser incrementado.
Pongo un ejemplo sacado del código fuente. Cada sección la he coloreado de un color.

START_TEST(inc_hl_test)
{    
    // Instancio la CPU.
    struct cpu_t* cpu = setup_cpu();

    // Cargo el código máquina del opcode a probar.
    cpu->mem[0] = 0x23;
    
    // Establezco el estado inicial.
    cpu->tstates = 1000;
    REG_HL(*cpu) = 0x1234;

    // Ejecuto el código máquina.
    execute_opcode(cpu);
    
    // Verifico el estado final.
    ck_assert(REG_HL(*cpu) == 0x1235);
    ck_assert(cpu->tstates == 1006);
}
END_TEST

Las partes más importantes son la verde y la fucsia.
  • En verde establezco el estado de entrada E. Los valores son arbitrarios.
  • En morado verifico que el estado de salida S se ha modificado de acuerdo a como debe ser. Por ejemplo, si estoy probando el opcode INC HL, debe verificarse que si HL_E = 0x1234, entonces HL_S = 0x1235. En cualquier otro caso, el opcode no se ha ejecutado correctamente. Además, lo mismo con los T-states. Si inicialmente el contador era 1000 y este opcode consume 6 T-states, necesariamente al final el contador debe valer 1000 + 6 = 1006. Cualquier otro valor es un error.

Ya sé que no está completa la prueba, porque no verifico que los opcodes que no deban cambiar no cambien. Esto es algo que me ha quedado por verificar.

11 marzo, 2015

El parpadeo

Ayer, ejecutando mi emulador, además de encontrar unos cuantos bugs tontos que debería haber visto antes de hacer el lanzamiento de la 0.1.0 en vez de lanzar a ciegas (los cuales por cierto ya están corregidos y en la rama de la 0.1, la 0.1.1 probablemente saldrá en menos de 24 horas), observé que la pantalla parpadeaba muy a menudo, especialmente en el PONG.

Al principio pensé que sería un bug, y de hecho creé un issue marcador en GitHub. Sin embargo, al rato lo tuve que cancelar ya que un compañero que también había probado el emulador y también había visto el parpadeo me recordó el motivo por el que parpadea, el cual es lógico.

Simplemente: del mismo modo que la ROM enciende los píxeles también los debe apagar. Por ejemplo, presumiblemente el PONG (no he mirado el código máquina) está usando una instrucción de dibujado para apagar los píxeles y acto seguido una instrucción de dibujado para encender los píxeles, en vez de usar una instrucción que apague y encienda de golpe los píxeles necesarios para que parezca que se está moviendo.

Probablemente eventualmente encuentre una mejor forma de renderizar la pantalla que provoque menos parpadeos, pero por el momento he marcado el bug como sin solución ya que técnicamente no es un problema mío.

09 marzo, 2015

CHIP-8 en Windows

En la sesión de live coding de ayer quedé un poco en ridículo al ver que nada de lo que tenía planeado funcionaba. La pantalla se veía en negro, y no entendía realmente por qué. Durante el directo eché la culpa a un montón de factores: era el Windows, luego era el VirtualBox, luego era el OpenGL, luego era el Windows otra vez.

Esto está compilado hace 5 minutos:




¿Cuál era el problema? Ni SDL, ni OpenGL, ni siquiera VirtualBox.

La respuesta: la ROM. En Windows no se estaba cargando. No ha sido fácil encontrar este bug y ha requerido una intensa sesión de depuración hasta ver que la memoria de la máquina estaba completamente vacía. Finalmente, después de buscar en Google, he encontrado una explicación coherente a por qué en Windows no se está cargando la ROM: fread/ftell apparently broken under Windows, works fine under Linux. La respuesta de esta pregunta de Stack Overflow dice que debería abrir el archivo en modo "rb", no en modo "r".

Según el manual de fopen(3), en sistemas compatibles con POSIX, el "b" es ignorado ya que el fopen funciona tal cual. Por eso en Linux y en Mac la llamada a fopen funciona correctamente. Sin embargo, en otros sistemas operativos (clara indirecta a Windows), esto puede ser distinto y puede que sí que sea necesario. Y, efectivamente, parece que sí que lo era.

Así que... nada. Voy a compilar una versión inestable y la voy a subir al repositorio. Y, supongo que en el próximo directo intentaré repetir lo que quería hacer ayer. Aún no he comprobado si VirtualBox será capaz de mostrarlo en cualquier caso. EDIT: He sido capaz de usarlo en Linux sobre VirtualBox, así que asumo que en Windows sobre VirtualBox funcionará igual.