Un diario de desarrollo donde registro mi progreso a medida que desarrollo un emulador como hobby y como proyecto puramente académico
22 marzo, 2015
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.
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.
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.
06 marzo, 2015
Blitting
En la próxima sesión de live coding voy a ocuparme de construir la pantalla del emulador. Para ello va a ser necesario manipular pixels en SDL. En SDL2, la forma preferente de gestionar una ventana es con aceleración de hardware. Esto implica que las texturas se ubiquen en GPU y sea la GPU quien dibuje, no la CPU. Esto es un bache a la hora de manipular los píxeles, que es exactamente lo que necesita nuestro emulador, ya que a las texturas no les gusta que las manipulen directamente como podríamos hacer con una Surface en SDL1.
En SDL 2 tenemos texturas de tipo streaming. Estas texturas están pensadas para ser usadas en situaciones donde pueda cambiar la textura frecuentemente, incluso cada fotograma. Por medio de la función SDL_LockTexture luego puedo bloquear la textura. Todo lo que hago es pedirle a SDL_LockTexture que me pase la textura a una SDL_Surface y luego manipulo los pixels de esa textura. Cuando he terminado, invoco a SDL_UnlockTexture para desbloquearla. Este proceso se repite 60 veces por segundo. El resultado es este:
Estoy limitando la ejecución a 60 segundos por medio de SDL_WaitEventTimeout. Podría usar SDL_PollEvent pero he comprobado que así el consumo de CPU se dispara al 100% ya que PollEvent no es bloqueante, como sí es SDL_WaitEventTimeout, lo que reduce mucho el consumo de CPU, algo muy importante en ordenadores portátiles y móviles.
El código, un poco sucio, se puede ver aquí, https://gist.github.com/danirod/4008bf7aab2f8b2466d5.
En SDL 2 tenemos texturas de tipo streaming. Estas texturas están pensadas para ser usadas en situaciones donde pueda cambiar la textura frecuentemente, incluso cada fotograma. Por medio de la función SDL_LockTexture luego puedo bloquear la textura. Todo lo que hago es pedirle a SDL_LockTexture que me pase la textura a una SDL_Surface y luego manipulo los pixels de esa textura. Cuando he terminado, invoco a SDL_UnlockTexture para desbloquearla. Este proceso se repite 60 veces por segundo. El resultado es este:
Estoy limitando la ejecución a 60 segundos por medio de SDL_WaitEventTimeout. Podría usar SDL_PollEvent pero he comprobado que así el consumo de CPU se dispara al 100% ya que PollEvent no es bloqueante, como sí es SDL_WaitEventTimeout, lo que reduce mucho el consumo de CPU, algo muy importante en ordenadores portátiles y móviles.
El código, un poco sucio, se puede ver aquí, https://gist.github.com/danirod/4008bf7aab2f8b2466d5.
02 marzo, 2015
Sobre máscaras de bit y operadores binarios
Me parecía oportuno hacer un inciso técnico sobre algo que llevo varios días mostrando en los streamings de live coding que estoy haciendo estos días: las máscaras de bit y los desplazamientos.
Una de las construcciones que más abundan el código fuente de mi emulador del CHIP-8, el cual estoy alojando en GitHub bajo una licencia GPL, es el aplicar desplazamiento y una máscara de bits sobre un resultado, por ejemplo:
mem.pc = (mem.pc + 2) & 0xFFF;
uint8_t n = opcode & 0xF;
uint8_t x = (opcode >> 8) & 0xF;
Asumiré que quien esté leyendo esto conoce más o menos cómo trabajar con bits. En cualquier caso como referencia quedan los dos episodios (el primero y el segundo) que dediqué a bits en mi videotutorial de C que grabé hace un par de años.
En el caso de las dos primeras operaciones las máscaras de bit las uso para limitar el número de bits que me quedo de la variable. Por ejemplo, en el primer caso, mem.pc es una variable de 16 bits. Pero puesto que la CHIP-8 sólo puede direccionar a 12 bits, debo asegurarme que no se guarda un valor de más de 12 bits. Puesto que 0xFFF son 12 unos, sólo los 12 bits menos significativos del contador de programa se guardarán. Los cuatro bits más significativos se perderán porque al hacer el AND, en la máscara están en 0.
Por ejemplo, supongamos que incremento un contador de programa que inicialmente vale 0x0FFE. Al sumarle 2, mi contador de programa pasa a valer 0x1000. Al hacer 0x1000 & 0x0FFF, puesto que & es un operador que hace el AND bit a bit, el resultado que obtengo es 0x000. ¿Otro ejemplo? Veamos: (0x0FFF + 2) & 0x0FFF = 0x1001 & 0x0FFF = 0x0001.
En el segundo ejemplo estoy haciendo lo mismo. La única diferencia es que la máscara de bit es 0xF, por lo que me quedo con los últimos 4 bits menos significativos del opcode. Por lo que supongamos que el opcode que he leído es 0xABCD. Al hacerle el AND, 0xABCD & 0x000F = 0x000D.
El tercer caso utiliza además desplazamientos. Lo hago porque en ese caso quiero quedarme con un valor que no está en los bits menos significativos. Lo que hago es desplazarlo a la derecha, para que sí esté en los bits menos significativos, y ya entonces le aplico la máscara. Por ejemplo, supongamos que el opcode es 0xABCD. Pues (0xABCD >> 8) & 0x000F = 0x00AB & 0x000F = 0x000B.
Una forma alternativa de hacer esta última operación aritmética hubiese sido al revés: en vez de desplazar y luego aplicar AND, primero aplicar el AND y luego desplazar a la derecha. En ese caso lo que tendría que hacer es aplicarle una máscara binaria que sólo deje activos los bits que me interesa. Por ejemplo, la tercera operación es reescribible como:
uint8_t x = (opcode & 0xF00) >> 8
En este caso la máscara de bit dejará activos los bits 8 a 11 del opcode. Luego al desplazar a la derecha esos valores pasarán a estar en las posiciones menos significativas. Por ejemplo, si el opcode es 0xABCD, lo que se hará es (0xABCD & 0x0F00) >> 8 = 0x0B00 >> 8 = 0x000B.
Y esta es más o menos la esencia que hay detrás de mis operadores de bit. ¿Por qué hacerlo así y no de otro modo? Por ejemplo, en vez de hacer esa máscara de bit al contador de programa podría haber hecho simplemente:
mem.pc = mem.pc + 2;
if (mem.pc > 0xFFF) {
mem.pc = mem.pc - 0xFFF;
}
Es claramente más simple y más fácil de comprender por alguien que no tenga mucha experiencia con máscaras de bit. Pero por otro lado es mucho más lento. Las operaciones de bit las puede hacer el procesador en una única instrucción. Las sumas, las restas y los condicionales suelen necesitar más instrucciones máquina en nuestro ordenador. Para un emulador muy simple como el de un CHIP-8 igual no hace falta ser tan exquisito con las optimizaciones porque la máquina emulada es lenta y nuestro ordenador físico tiene varios núcleos, pero cuando emulamos arquitecturas más grandes y complejas, puede significar bastante.
Una de las construcciones que más abundan el código fuente de mi emulador del CHIP-8, el cual estoy alojando en GitHub bajo una licencia GPL, es el aplicar desplazamiento y una máscara de bits sobre un resultado, por ejemplo:
mem.pc = (mem.pc + 2) & 0xFFF;
uint8_t n = opcode & 0xF;
uint8_t x = (opcode >> 8) & 0xF;
Asumiré que quien esté leyendo esto conoce más o menos cómo trabajar con bits. En cualquier caso como referencia quedan los dos episodios (el primero y el segundo) que dediqué a bits en mi videotutorial de C que grabé hace un par de años.
En el caso de las dos primeras operaciones las máscaras de bit las uso para limitar el número de bits que me quedo de la variable. Por ejemplo, en el primer caso, mem.pc es una variable de 16 bits. Pero puesto que la CHIP-8 sólo puede direccionar a 12 bits, debo asegurarme que no se guarda un valor de más de 12 bits. Puesto que 0xFFF son 12 unos, sólo los 12 bits menos significativos del contador de programa se guardarán. Los cuatro bits más significativos se perderán porque al hacer el AND, en la máscara están en 0.
Por ejemplo, supongamos que incremento un contador de programa que inicialmente vale 0x0FFE. Al sumarle 2, mi contador de programa pasa a valer 0x1000. Al hacer 0x1000 & 0x0FFF, puesto que & es un operador que hace el AND bit a bit, el resultado que obtengo es 0x000. ¿Otro ejemplo? Veamos: (0x0FFF + 2) & 0x0FFF = 0x1001 & 0x0FFF = 0x0001.
En el segundo ejemplo estoy haciendo lo mismo. La única diferencia es que la máscara de bit es 0xF, por lo que me quedo con los últimos 4 bits menos significativos del opcode. Por lo que supongamos que el opcode que he leído es 0xABCD. Al hacerle el AND, 0xABCD & 0x000F = 0x000D.
El tercer caso utiliza además desplazamientos. Lo hago porque en ese caso quiero quedarme con un valor que no está en los bits menos significativos. Lo que hago es desplazarlo a la derecha, para que sí esté en los bits menos significativos, y ya entonces le aplico la máscara. Por ejemplo, supongamos que el opcode es 0xABCD. Pues (0xABCD >> 8) & 0x000F = 0x00AB & 0x000F = 0x000B.
Una forma alternativa de hacer esta última operación aritmética hubiese sido al revés: en vez de desplazar y luego aplicar AND, primero aplicar el AND y luego desplazar a la derecha. En ese caso lo que tendría que hacer es aplicarle una máscara binaria que sólo deje activos los bits que me interesa. Por ejemplo, la tercera operación es reescribible como:
uint8_t x = (opcode & 0xF00) >> 8
En este caso la máscara de bit dejará activos los bits 8 a 11 del opcode. Luego al desplazar a la derecha esos valores pasarán a estar en las posiciones menos significativas. Por ejemplo, si el opcode es 0xABCD, lo que se hará es (0xABCD & 0x0F00) >> 8 = 0x0B00 >> 8 = 0x000B.
Y esta es más o menos la esencia que hay detrás de mis operadores de bit. ¿Por qué hacerlo así y no de otro modo? Por ejemplo, en vez de hacer esa máscara de bit al contador de programa podría haber hecho simplemente:
mem.pc = mem.pc + 2;
if (mem.pc > 0xFFF) {
mem.pc = mem.pc - 0xFFF;
}
Es claramente más simple y más fácil de comprender por alguien que no tenga mucha experiencia con máscaras de bit. Pero por otro lado es mucho más lento. Las operaciones de bit las puede hacer el procesador en una única instrucción. Las sumas, las restas y los condicionales suelen necesitar más instrucciones máquina en nuestro ordenador. Para un emulador muy simple como el de un CHIP-8 igual no hace falta ser tan exquisito con las optimizaciones porque la máquina emulada es lenta y nuestro ordenador físico tiene varios núcleos, pero cuando emulamos arquitecturas más grandes y complejas, puede significar bastante.
Suscribirse a:
Entradas (Atom)

