Pintando en el Spectrum (6)

En la entrada anterior expliqué cómo crear un sprite, y puse una demostración que muestra cómo funciona, moviendo un sprite de un círculo en vertical. Y aunque en apariencia funciona bien cuando el fondo está vacío, si ponemos alguna imagen de fondo (en este ejemplo pongo «basura» copiada desde la zona de la ROM) y movemos el sprite por encima, vemos que hay un problema: aunque el sprite es un círculo, los límites del cuadrado que delimitan los caracteres son visibles.

Lo lógico sería que en las zonas externas del círculo pudiésemos ver el fondo, y que éste sólo estuviese tapado en donde está el círculo en sí.

Pues bien: este efecto es relativamente sencillo de conseguir, y para eso sólo necesitamos utilizar máscaras. Se trata de dividir el sprite en dos imágenes independientes: una contiene el sprite en sí, tal y como hasta ahora, y la otra es una máscara de transparencia, donde un bit puesto a 1 indica que ese pixel del sprite es transparente y se debe ver el fondo, y un bit a 0 especifica que el pixel correspondiente del sprite es opaco y debe pintarse.

En esta imagen vemos el sprite, la máscara, y cómo queda la transparencia al final:

Como vemos, en la máscara, la zona alrededor del círculo es de color negro (bit a 1), lo que significa que es transparente, mientras que la zona interna es blanca (bit a 0), lo que significa que es opaca. A la derecha podemos ver las zonas transparentes con un damero de ajedrez. Vemos que hemos dejado un margen de un píxel alrededor del círculo para que se vea mejor.

Todo esto está muy bien, pero ¿cómo hacemos para pintarlo? La clave está en utilizar operaciones lógicas: antes de copiar un byte del sprite en el buffer de la pantalla, tenemos que leer el byte que ya hay y hacer un AND lógico con la máscara. De esta manera, los bits del fondo que coincidan con un bit a 1 de la máscara se quedarán tal y como están, mientras que los bits que coincidan con un 0 en la máscara se pondrán a 0, «dejando un hueco» en donde podremos pintar luego el sprite. Por supuesto, para ello no podemos tampoco copiar directamente el byte del sprite, sino que tenemos que hacer un OR lógico de lo que haya en pantalla y el sprite, de manera que se mezclen.

Veámoslo de manera gráfica: supongamos que tenemos como fondo un damero de ajedrez, y queremos pintar encima nuestro sprite círculo. Primero aplicamos la máscara usando el operador AND entre cada byte de ella y el que hay en pantalla en la posición correspondiente, y almacenamos el resultado de nuevo en pantalla. Esto borrará sólo las partes que la máscara indica que son opacas, dejando intactas aquellas zonas marcadas como transparentes.

Hecho esto, copiamos los bytes del sprite en la misma zona pero utilizando esta vez la operación OR, de manera que «se mezcle» con lo que había. Dado que previamente habíamos «hecho un agujero» con la máscara, los píxeles del sprite se mezclarán exclusivamente donde nos interesa, dejando inalterados el resto de zonas.

Sin embargo, a la hora de implementarlo nos encontramos con el problema de que es un proceso muy ineficiente, pues tenemos que, literalmente, imprimir dos sprites realmente: primero la máscara, y luego los píxeles. Y por si fuera poco tenemos que hacer tres operaciones en memoria por cada sprite en lugar de dos, como hasta ahora: leer el dato de la pantalla, leer el byte del sprite (ya sea pixels o máscara), y escribir el nuevo valor.

La solución consiste en aplicar ambas operaciones a la vez. Para ello empezamos leyendo el primer byte de la zona de pantalla, hacemos AND con el primer byte de la máscara, luego OR con el primer byte de los píxeles, y finalmente escribimos el valor resultante en la memoria de pantalla. Hecho esto, incrementamos en uno los punteros de los píxeles y de la máscara, pasamos a la siguiente dirección de pantalla, y repetimos el proceso.

Este sistema permite aumentar mucho el rendimiento, pues no sólo reducimos los accesos a memoria a sólo dos tercios, sino que, además, se pueden compartir muchas operaciones comunes, como la inicialización de los bucles (e incluso la sobrecarga que supone ejecutar el bucle en sí). Sin embargo, todavía podemos optimizarlo un poquito más. Para entenderlo, pensemos en cómo almacenamos los píxeles y la máscara del sprite: la opción más inmediata es colocar la máscara justo a continuación de los píxeles, de manera que si conocemos la dirección inicial y el tamaño del sprite en caracteres, podemos obtener la dirección de la máscara simplemente con la operación MASCARA = PÍXELES + ANCHO * ALTO * 8. El resultado sería como tener un sprite del doble de tamaño, y almacenado así:

Es sencillo de describir, pero recordemos que el Z80 no tiene instrucción de multiplicación, lo que supone que hacer ese cálculo sea lento y ocupe bastante código. Además, nos obliga a tener tres punteros: uno para los píxeles, otro para la máscara, y otro para el buffer de pantalla.

Sin embargo, existe una manera de evitar todo esto, y es entrelazar los datos de la máscara y de los píxeles; esto es, almacenar en memoria un byte de máscara, un byte de píxeles, un byte de máscara, un byte de píxeles… así:

La ventaja de este sistema es que sólo necesitamos dos punteros: uno que apunte al sprite, y otro al buffer de pantalla. La mecánica es sencilla: leemos byte del puntero del buffer de pantalla y hacemos un AND con el byte del puntero del sprite. Incrementamos en uno el puntero del sprite y hacemos un OR del byte al que apunta con el resultado anterior. Hecho esto, incrementamos de nuevo el puntero del sprite y pasamos a la siguiente dirección de memoria del buffer de pantalla (según estemos todavía en el mismo scanline o tengamos que pasar al siguiente). Lo curioso es que pocos juegos utilizan este sistema, a pesar de sus claras ventajas. Un ejemplo de juego que lo usa es Knight Lore, uno de los primeros juegos en utilizar máscaras en sus sprites.

El código para hacer esto sería el siguiente:

pintar_sprite:
    push HL
    exx
    pop HL      ; metemos la dirección del sprite en HL'
    exx
    set 7, D    ; DE ya casi tiene la dirección, le falta el bit 7 de D
    ex de, hl   ; metemos la dirección de la pantalla en HL
    sla B
    sla B       ; rotamos tres veces B, que es igual que multiplicar por 8
    sla B       ; así tenemos en B la altura en scanlines
    ld A, 32
    sub C       ; calculamos cuanto tenemos que sumar para pasar al
    ld E, A     ; siguiente scanline, y lo almacenamos en DE
    ld D, 0
bucle:
    push BC     ; guardamos B (pues es el contador de scanlines) y C
    ld B, C     ; preparamos el bucle interno
bucle2:
    ld a, (hl)  ; leemos el byte actual de la pantalla
    exx
    and (hl)    ; aplicamos la máscara
    inc hl
    or (hl)     ; aplicamos los píxeles
    inc hl
    exx
    ld (hl), a  ; almacenamos el resultado en la pantalla
    inc hl      ; y pasamos a la siguiente posición de la pantalla
    djnz bucle2 ; terminamos el scanline
    add HL, DE  ; y pasamos al siguiente scanline
    pop BC      ; recuperamos el número de scanlines que nos quedan
    djnz bucle  ; y repetimos hasta hacer todos los scanlines

    call paint_screen ; llamamos a la función que vuelca el buffer
                      ; en la pantalla (si hemos pintado todos los
                      ; sprites)

y reemplazaría al código que escribimos en la entrada anterior. Existe un cambio extra a mayores: dado que ahora cada scanline ocupa el doble (pues contiene píxeles y máscara), en la parte donde comprobamos si el sprite está fuera de la pantalla por arriba tenemos que multiplicar por 16 en lugar de por 8, lo que se consigue añadiendo un add A, A extra.

Y este es el resultado: como se ve, es muchísimo mejor que el original, pues ya no hay ese cuadrado tan feo alrededor del sprite que hace que parezca pegado encima, sino que éste realmente aparece integrado con el fondo.

Como cabe suponer, mi editor de sprites ZXSpriter almacena los sprites precisamente en este formato. Pero no sólo eso, sino que si se incluyen atributos de color, puede también incluirlos entrelazados (aunque en este caso, es opcional). De esta manera, el sprite tendrá ocho scanlines con los bytes de máscara y píxeles entrelazados, tal y como hemos visto ya, y justo a continuación los bytes con los atributos de color para ese conjunto de scanlines; a continuación otros ocho scanlines, y otra vez los atributos de color para ese conjunto de scanlines. Y así sucesivamente. Esto permite pintar sprites de colores de manera óptima.

El código del ejemplo con máscaras está disponible aquí: ejemplo de sprites con máscaras. En él vuelvo a hacer uso de los registros alternativos (BC’, DE’ y HL’), y del hecho de que el registro A no se intercambia con A’ cuando se ejecuta la instrucción EXX, para tener en HL la dirección de la pantalla y en HL’ la del sprite.

Pintando en el Spectrum (5)

Editado: el código completo del ejemplo de esta entrada está disponible en un enlace al final.

Llega la hora de pintar cosas en pantalla. La idea consiste en modificar los valores de la memoria RAM de vídeo para que, cuando la ULA (que es el chip encargado de generar la imagen) lea esos bytes, éstos muestren la imagen que queremos. Hay dos maneras de hacerlo: vectorial, y por sprites. En el caso de gráficos vectoriales, se tiene almacenada en memoria una lista de segmentos y vértices que deben pintarse sobre la pantalla para generar la imagen. Son ideales para gráficos 3D, y son los utilizados en juegos como Elite o Driller, sin embargo, en una máquina de 8 bits son muy limitados, por lo que de momento lo dejaremos de lado. Es verdad que hace tiempo estuve trabajando en una rutina para pintar triángulos vectoriales a alta velocidad en Spectrum, por lo que puede que retome el tema algún día.

Los sprites, por su parte, son mucho más sencillos, pues consisten en tener los bytes necesarios para pintar algo en pantalla ya preparados en otra zona de memoria, y lo único que necesitaremos será copiarlos en la dirección adecuada de pantalla. Además, cambiando la dirección de memoria en la que empezamos a pintar podremos cambiar la posición, con lo que podemos colocarlos en cualquier punto de la pantalla o, incluso, moverlos y animarlos.

Para pintarlo en pantalla, primero necesitamos encontrar la dirección de memoria en la que irá el primer byte, que, como recordaremos de la entrada anterior, se podía hacer de manera sencilla:

Obtención de la dirección en pantalla a partir de las coordenadas de un carácter trabajando con el buffer lineal. En verde va la coordenada Y, y en azul la coordenada X. En magenta va el número de scanline dentro del carácter.

Así, si recordamos, la pantalla del Spectrum está dividida en una matriz de 32×24 caracteres, con ocho scanlines cada uno, y si utilizamos la rutina de copia de buffer que vimos en las tres primeras entradas, obtener la dirección de un carácter a partir de sus coordenadas X e Y es tan sencillo como multiplicar la Y por 256, sumarle la X, y ese valor sumarlo a la dirección inicial del buffer. Alguno dirá que las multiplicaciones en ensamblador son muy costosas, pero es aquí donde viene lo divertido: digamos que queremos tener en el par de registros HL la dirección inicial del carácter situado en las coordenadas X, Y. Pues sólo tenemos que copiar el valor de Y en el registro H, el de X en el registro L, y sumarle a HL la dirección base del buffer. Pero si hemos sido cuidadosos y hemos puesto el buffer en la dirección 0x8000, ni siquiera necesitaremos una suma, sino simplemente poner a 1 el último bit del registro H.

Por supuesto, esto nos da la dirección del primer scanline del carácter, pero movernos al siguiente scanline es tan sencillo como sumar 32 a la dirección. Esto nos permite pintar sprites del tamaño de un carácter en pantalla, pero ¿como hacemos para pintar sprites de mayor tamaño?

La solución más inmediata consiste en dividir el sprite en bloques de 8×8 píxeles, almacenar cada bloque por separado, y luego pintarlos uno a uno en las posiciones correspondientes. Así, si tuviésemos por ejemplo el siguiente sprite de 16×16 píxeles:

Lo que haríamos sería dividirlo en bloques de 8×8 píxeles, de manera que cada uno se podría pintar como un carácter en pantalla, y almacenaríamos los bytes en este orden en memoria:

Vemos que primero viene el primer byte del carácter superior izquierdo, luego el inmediatamente inferior, y así hasta hacer ocho bytes del primer carácter. A continuación vendrían los ocho bytes del carácter de la parte superior derecha, luego los de la parte inferior izquierda, y por último los de la parte inferior derecha.

Digamos que queremos pintarlo en las coordenadas X=5 e Y=12. Lo primero sería calcular la dirección de 5,12, la cual vamos a almacenarla en el par de registros DE (para tener la regla mnemotéctica de DE = DEstino), y eso lo haremos escribiendo 12 en D y 5 en E, y sumándole 128 a D para activar el bit 7 y que apunte al inicio de nuestro buffer. Ahora ya tenemos la dirección de memoria donde tenemos que empezar a copiar los datos. Digamos, además, que en HL tenemos la dirección de memoria del primer byte de nuestro sprite. Bien, ahora sólo tenemos que leer el byte de la dirección de memoria apuntada por HL, y escribirlo en la dirección de memoria apuntada por DE. El siguiente paso es saltar al siguiente byte, y para ello sólo tenemos que incrementar HL en 1, y sumar 32 a DE para pasar al siguiente scanline, con lo que ya podemos copiar el segundo byte. Este proceso lo repetimos ocho veces para copiar los ocho bytes del carácter.

Ahora que ya tenemos el primer carácter copiado, pasamos al siguiente. Para ello tenemos que calcular la dirección de las coordenadas 6,12 y meter el resultado en DE. HL no debemos tocarlo pues ya está apuntando al segundo carácter. Una vez hecho esto, copiamos los bytes igual que hicimos arriba y volvemos a repetirlo para las coordenadas 5, 13 y 6, 13.

El método es directo, pero tiene el problema de que no es demasiado eficiente: tenemos que mantener en algún lado las coordenadas del carácter actual, así como el ancho y el alto para saber cuando hemos terminado, y recalcular la dirección de memoria de cada carácter. Todo esto consume mucho tiempo de proceso, y por eso pocos juegos lo usan (uno que sí lo hace, por ejemplo, es The trapdoor). Es por esto que, normalmente, se prefiere almacenar los sprites en formato scanline. La diferencia está en que, en lugar de almacenarlo carácter a carácter, se almacena por filas completas. Así, el orden para scanline en el ejemplo anterior sería el siguiente:

En este caso, la manera de trabajar es ligeramente diferente: partimos una vez más de las coordenadas X e Y en las que queremos pintar nuestro sprite, y procedemos a calcular la dirección de memoria de dichas coordenadas. Ahora sólo necesitamos hacer dos bucles, uno que cuente de cero a ANCHO-1, que será el interno, y otro que cuente de 0 a (ALTO*8)-1. En el bucle interno leeremos un byte del sprite y lo copiaremos en la dirección de pantalla, tras lo cual incrementaremos ambas direcciones en 1 y repetiremos la operación tantas veces como ancho sea el sprite. Con eso ya hemos copiado un scanline. Ahora sólo tenemos que sumar a la dirección de destino, la que apunta al buffer de pantalla, 32 – ANCHO para pasar al siguiente scanline de la pantalla y volver a repetir la copia anterior, y así tantas veces como scanlines tenga el sprite, que será ALTO * 8. Además, el valor de 32 – ANCHO lo podemos tener almacenado en un registro, por lo que sólo lo tendremos que calcular una vez, fuera de los bucles. Un ejemplo de código sería éste:

; B contiene el alto, C el ancho, ambos en caractéres
; HL contiene la dirección del primer byte del sprite
; D contiene la coordenada Y, E la coordenada X
; Asumimos que el buffer lineal de pantalla está en 0x8000

pintar_sprite:
    set 7, D ; DE ya casi tiene la dirección, le falta el bit 7 de D
    sla B
    sla B ; rotamos tres veces B, que es igual que multiplicar por 8
    sla B ; así tenemos en B la altura en scanlines
    ld A, 32
    sub C     ; calculamos cuanto tenemos que sumar para pasar al
    ld IXl, A ; siguiente scanline (32-ANCHO), y lo guardamos en IXl
bucle:
    push BC ; guardamos B (pues es el contador de scanlines) y C
    ld B, 0 ; preparamos el bucle interno
    ldir ; copiamos C bytes (C contiene el ancho)
    ld C, IXl ; sumamos IXl a DE para pasar al siguiente scanline
    ex DE, HL ; funciona porque B vale cero después de LDIR
    add HL, BC
    ex DE, HL 
    pop BC ; recuperamos el número de scanlines que nos quedan
    djnz bucle2 ; y repetimos por cada scanline

    call paint_screen ; llamamos a la función que vuelca el buffer
                      ; en la pantalla (si hemos pintado todos los
                      ; sprites)

Con esta función podemos pintar fácilmente un sprite de cualquier tamaño en cualquier parte de la pantalla (a nivel de carácter). Sin embargo, hay una condición inexcusable: el sprite tiene que entrar completamente en la pantalla, no puede «estar medio fuera». Si no lo tenemos en cuenta ocurrirán «cosas raras». Así, si se sale por un lado aparecerá por el opuesto (parecido a Pacman), lo cual es erróneo pero no peligros; pero si se sale «por arriba» o «por abajo» escribiremos en zonas de memoria fuera del buffer de pantalla, por lo que lo más probable es que nuestro código se cuelgue. Afortunadamente este problema se puede solucionar.

En el caso vertical (que el sprite «se salga por arriba o por abajo») resolverlo es muy sencillo: supongamos que se sale «por abajo» porque la coordenada Y es tan grande que Y + ALTURA es mayor que la máxima coordenada «pintable» (24 en el caso del Spectrum). En ese caso simplemente tenemos que cambiar la altura del sprite (la que le pasamos a la función) por ALTURA – Y, con lo que el bucle externo pintará justo hasta el final de la pantalla, pero ni un solo byte más. Algo así:

y_no_negativa:
    ld A, D
    cp 24
    ret NC ; si está completamente fuera de la pantalla, no pintamos nada
    add A, B ; comprobamos Y + ALTURA
    cp 24
    ; si hay acarreo, el sprite está completamente dentro de la pantalla
    jr C, pintar_sprite ; lo pintamos normalmente
    ld A, 24
    sub D
    ld B, A ; sustituimos la altura del sprite por 24 - Y

pintar_sprite:
    ...

Y con esto, colocando este código justo antes de la función de pintar sprites, nuestro sprite ya se puede salir «por abajo» que no pasará nada. Para permitir que se pueda salir «por arriba» la solución es la misma, pero a mayores tenemos que cambiar la dirección de inicio del sprite, saltándonos tantos scanlines como se queden fuera. Dado que Y será negativa en este caso, el número de scanlines que nos tenemos que saltar es de (-Y * 8) (ojo al signo menos), y como cada scanline tiene tantos bytes como ancho sea el sprite, tenemos que hacer una multiplicación. Este será el código:

    bit 7, D ; comprueba si Y es negativa
    jr Z, y_no_negativa
    ld A, D
    add A, B; sumando Y al alto nos da la nueva altura (no olvidar que Y es negativo, por lo que realmente es una resta)
    ret Z
    ret NC ; si no hay acarreo, o es cero, significa que el sprite está completamente fuera de la pantalla (B < -Y)
    ld B, A ; sustituimos la altura
    push BC
    push DE
    ld A, C  ; necesitamos calcular (-Y) * ANCHO * 8 para saber
    add A, A ; cuantos bytes saltar
    add A, A
    add A, A ; multiplicamos ANCHO por 8
    ld E, A
    ld A, D
    neg ; A contiene -Y, que es el número de filas a saltar (en caracteres)
    ld B, A
    ld D, 0
bucle1:
    add HL, DE ; HL + 8 * ANCHO * (-Y)
    djnz bucle1
    pop DE
    pop BC
    ld D, 0 ; pintamos a partir de la coordenada 0
    jr pintar_sprite ; pintamos el sprite

y_no_negativa:

Así, colocando este código justo antes del anterior, ya podrá salirse también «por arriba» sin que pase nada.

Para tener en cuenta el que se salga «por los lados» el proceso es el mismo, pero con la diferencia de que hay que modificar la función de pintado para que, al terminar de pintar un scanline del sprite (que será más corto que el ancho real), se salte tantos bytes como haya de diferencia.

Así se ve este ejemplo corriendo en un emulador:

El código del ejemplo se puede descargar aquí: ejemplo de sprite.

En el próximo capítulo hablaré de máscaras y transparencia.

Pintando en el Spectrum (4)

Ha llegado el momento de empezar a pintar cosas en la pantalla. Obviamente lo que queremos pintar serán Sprites. Como explica la wikipedia, se trata de gráficos 2D que se integran con una escena de fondo. Como sabemos, el Spectrum no tiene soporte de sprites por hardware, lo que significa que nos toca a nosotros hacer todo el trabajo de pintarlos. Afortunadamente, en las entradas anteriores vimos cómo hacer una rutina que copie a la pantalla una imagen completa desde un buffer organizado de manera secuencial, y esto nos va a simplificar la tarea, como veremos.

Lo primero que necesitamos saber para pintar algo en pantalla es a partir de qué dirección de memoria tenemos que hacerlo. Si recordamos, la pantalla del Spectrum está formada por una matriz de 256 píxels de ancho por 192 de alto, y cada píxel puede tener dos colores, por lo que cada uno ocupa un bit. Esto significa que cada fila o scanline de la pantalla ocupa 32 bytes, y la parte de píxeles de la pantalla ocupa en total 6 144 bytes o 6 Kbytes exactos.

Sin embargo, justo a continuación viene una segunda zona denominada atributos, que define una matriz de 32 por 24 atributos y asigna un byte a cada uno. Este byte de atributos especifica qué colores tendrán un grupo de píxeles. En concreto, cada byte define dos colores para cada grupo de 8×8 píxeles de la pantalla: uno será el color que se mostrará cuando el bit correspondiente al píxel esté a cero, y el otro cuando esté a uno. Esta zona empieza en la dirección de memoria 22 528 y ocupa un total de 768 bytes. Este sistema permite al Spectrum mostrar hasta 16 colores simultáneos en pantalla pero consumiendo muy poca memoria.

Como ya vimos en la primera entrada, la organización de la pantalla es algo caótica. Veámoslo en un gráfico:

Vemos que los bytes situados en las direcciones de memoria en hexadecimal 0x4000, 0x4100, 0x4200, 0x4300, 0x4400, 0x4500, 0x4600 y 0x4700 se corresponden con el bloque de 8×8 píxeles superior izquierdo de la pantalla, y que los atributos de dicho bloque están almacenados en la dirección 0x5800, que especifica que los dos colores de ese grupo de píxeles son blanco y negro. El siguiente bloque de 8×8 píxeles está en las direcciones 0x4001, 0x4101, 0x4201, 0x4301, 0x4401, 0x4501, 0x4601 y 0x4701, y sus atributos, que indican que los dos colores mostrados serán celeste y rojo, están en la dirección 0x5801. Y así sucesivamente. Esta disposición puede parecer absurda, pero si nos fijamos, vemos que la dirección de memoria de un scanline de píxeles y la de los atributos que le corresponden tienen el mismo valor en los ocho bits inferiores. Gracias a esta característica, es posible leer los dos bytes necesarios para mostrar los ocho píxeles en sólo tres ciclos de reloj, en lugar de los cuatro que se necesitarían si ambas direcciones no tuviesen siempre un byte idéntico (lo que se denomina fast-page). Y si tenemos en cuenta que cada bloque de 8 píxeles tarda cuatro ciclos de reloj en ser pintado, salta a la vista de donde sale el ciclo de contención de memoria que vimos en el primer artículo: la circuitería «compacta» la lectura de dos grupos de 8 píxeles consecutivos, de manera que en lugar de bloquear al procesador durante tres ciclos y liberarlo uno, lo bloquea durante seis ciclos y lo libera durante dos consecutivos. Además, de esta manera se garantiza también que los siete bits bajos se recorren completos aproximadamente cada milisegundo, lo que es más que de sobra para garantizar el refresco de la RAM.

Obviamente este sistema se implementó porque permite abaratar y simplificar el hardware, pero tiene el inconveniente de que, a la hora de programar para él, es bastante engorroso calcular la dirección de memoria que se corresponde con cada coordenada. Así, si dividimos la pantalla en caracteres (bloques de 8×8 píxeles) y queremos encontrar las ocho direcciones de memoria de sus ocho bytes, la operación que hay que hacer es la siguiente (gráficamente):

En azul tenemos la coordenada X, que puede valer entre 0 y 31 para las 32 columnas del Spectrum. En verde y magenta tenemos la coordenada Y en líneas: en la parte verde estaría la coordenada Y en caracteres (de 0 a 23), y en magenta sería el scanline dentro de ese carácter, todo de arriba a abajo y de izquierda a derecha. A mayores es necesario poner los tres bits superiores a 010 para que apunte al bloque de memoria concreto. Como vemos, la cosa es bastante complicada y requiere rotaciones y máscaras. Sin embargo, gracias a las rutinas que vimos en las entradas anteriores esta complicación desaparece completamente. Si las utilizamos, la conversión es tan sencilla como:

Ponemos los tres bits superiores a 100 porque, para simplificar, colocamos el buffer en la dirección 0x8000 (32768). De esta manera, si ponemos la coordenada X en el byte inferior y la coordenada Y en la superior, sólo tendremos que poner a uno el bit 7 del byte inferior y ya tendremos la dirección de memoria del scanline superior del carácter con dichas coordenadas; para pasar al siguiente carácter sólo tenemos que sumar uno; para pasar al anterior, restar uno; para pasar al que está encima, restar 32, y para pasar al que está debajo, sumar 32. Tan simple como esto. ¿Y si tenemos una dirección de píxeles, cómo calculamos la de sus atributos? Pues sólo tenemos que hacer esto:

Sencillo ¿no? Aunque para los que no se quieran complicar, aquí está un trozo de código que lo hace:

; Asumimos que HL contiene la dirección de memoria
; de un byte de pantalla
    ld a, l
    and 0x1F
    ld l, a
    ld a, h
    rrca
    rrca
    rrca
    ld h, a
    and 0xE0
    or l
    ld l, a
    ld a, h
    and 3
    or 0x98
    ld h, a
; Ahora HL contiene la dirección de memoria del atributo
; que corresponde con el byte de pantalla inicial

Y como esta entrada quedó ya algo larga, seguiré en la siguiente.

Lanzado ZXSpriter

Editado: ya había un programa llamado ZXEditor, así que he renombrado mi editor a ZXSpriter

Después de mis tres entradas anteriores, llegó el momento de empezar a pintar cosas en el Spectrum, pero me encontré con que no había ninguna herramienta que me convenciese para hacer gráficos compatibles, así que me lié la manta a la cabeza y he escrito ZXSpriter. Se trata de un editor de Sprites que se adapta a mis necesidades, pues no sólo permite crear un número ilimitado de sprites y exportarlos a formato ensamblador, listos para integrarse con mi código, sino que permite que cada sprite sea de diferente tamaño, que tenga o no máscara de transparencia, o incluso datos de atributos de color.

Ahora que tengo este editor, en un par de días espero publicar una nueva entrada explicando cómo pintar sprites.

Pintando en el Spectrum (3)

El código anterior es funcional, pero tiene el problema de que ocupa 541 bytes (código más la tabla de direcciones). Teniendo en cuenta que la memoria de vídeo son casi siete kbytes, y otros siete más para el segundo buffer, tenemos que esos 541 bytes pueden ser un gasto excesivo de los 34,5 kbytes restantes de un Spectrum 48K (en un 128K tenemos doble página por hardware, con lo que no necesitamos nada de esto).

Para solucionarlo, en lugar de utilizar una tabla con todas las direcciones de memoria podemos utilizar una tabla sólo con las direcciones de inicio de cada bloque de caracteres (o sea, una de cada ocho filas de píxels). Esto es posible porque pasar de una fila a la siguiente en un bloque de caracteres es relativamente sencillo: sólo hay que incrementar el byte alto. Así, si tenemos en DE la dirección de un byte en una fila, sólo necesitamos hacer INC D para pasar a la siguiente fila (siempre que no sea la última fila de un carácter, claro).

Así que combinando todo lo anterior, podemos usar esta función:

    LD SP, tabla_direcciones
    LD HL, buffer
    LD BC, 0
    EXX
    LD HL, buffer + 6144
    LD DE, 0x5800 ; zona de atributos de color
    LD BC, 768 ; 32 columnas * 24 lineas
loop_l1:
    EXX
    INC B ; como BC es cero, esto es igual que LD BC, 256
          ; esto son 32 x 8 bytes en una fila de caracteres
    POP DE ; obtenemos la dirección inicial de la fila de caracteres
    LD IXh, E ; usamos una instrucción no oficial porque
              ; estamos justos de registros
    LD A, D
loop_l2:
    INC A ; preparamos ya la dirección de la siguiente fila
          ; hay que hacerlo antes de LDI porque INC modifica
          ; los flags
    LDI
    ... ; 32 LDIs en total
    LDI
    LD E, IXh ; recuperamos la posición inicial
    LD D, A ; y la siguiente fila
    JP PE, loop_l2
    EXX
    LDI
    ... ; 32 LDIs en total
    LDI
    JP PE, loop_l1

...
tabla_direcciones:
    DEFW 0x4000, 0x4020, 0x4040, 0x4060, 0x4080, 0x40A0, 0x40C0, 0x40E0
    DEFW 0x4800, 0x4820, 0x4840, 0x4860, 0x4880, 0x48A0, 0x48C0, 0x48E0
    DEFW 0x5000, 0x5020, 0x5040, 0x5060, 0x5080, 0x50A0, 0x50C0, 0x50E0

Y con esto lo tenemos ya, y altamente optimizada, pues en realidad esto ha sido el final de muchas iteraciones. Empecé utilizando DJNZ para las ocho iteraciones del bucle interno (loop_l2), y para conservar la dirección inicial de la fila de caracteres, lo que hacía era meter DE de nuevo en la pila con un PUSH, para sacarlo después de los LDIs y poder incrementar D para pasar a la siguiente fila. Esto era mucho más rápido que almacenarlo en alguna zona de la memoria (20 Tstados) o que decrementar dos veces el puntero de pila para que volviese a apuntar al valor inicial (12 Tstados).

La siguiente optimización que hice fue almacenar el valor de E en el registro A, de manera que después de los LDIs sólo tenía que hacer un LD E, A y ya tendría el valor original. Por desgracia esto CASI funcionaba, pues en las filas de caracteres 7, 15 y 23, al llegar al final de la línea se producía un desbordamiento y se sumaba uno a D, con lo que fallaba. Una solución sería reducir el número de columnas de 32 a 31, pero no era una solución muy elegante, así que al final decidí que era mejor almacenar D en A, y guardar E en una posición de memoria. Pero como no existe LD (nn), E ni la inversa, no podía hacerlo directamente… a menos que usase código automodificable. ¿Qué es esto? La instrucción LD E, 0 se codifica como 0x1E 0x00, siendo el segundo byte el valor a meter en el registro E. Así que lo que hacía era poner un LD E, 0 justo después de todos los LDIs, y después del primer POP DE almacenaba el valor de E justo en el segundo byte de la instrucción LD E, 0. De esta manera, cada vez que se hace una pasada en el bucle, el código ha cambiado. Aunque es más rápido, pues mientras que el par PUSH-POP son 22 Tstados en cada fila, con esto eran sólo 11 Tstados dentro de loop_l2, no me gusta nada usar código automodificable, así que me devané los sesos hasta que me acordé de que tenía cuatro registros extra de 8 bits: las mitades de IX e IY. Es cierto que son funcionalidades no documentadas, pero funcionan en todos los Z80. Así que la solución fue almacenar E en IXh (que consume 8 Tstados), y guardar D en A. Y además, el resultado es ligeramente más rápido también: aunque dentro del bucle pierdo ocho Tstados, pues LD E, 0 es 1 Tstado menos que LD E, IXh y el bucle se repite ocho veces, los compenso fuera, pues LD A, E son 4 Tstados y LD (nn), A son 13, 17 Tstados en total, mientras que LD IXh, E son sólo 8, con lo que, al final, usando IXh en lugar de código automodificable ahorro 1 Tstado por cada fila de caracteres (8 filas de píxels). Además, no hay que olvidar tampoco que esos 7 Tstados se convertirían en más siempre que hubiese contienda en la memoria, con lo que es un win-win.

Y con esto tenemos una función que ocupa 195 bytes en total (171 bytes más 24 de la tabla), a costa de ser un poco más lenta. ¿Cuanto más? El bucle interno son 4 + 512 + 8 + 4 + 10 = 538 Tstados, y hay que repetirlo 8 veces. Pero en caso de contienda, serán 544 Tstados, luego la duración será entre 4 304 y 4 352 Tstados. El bucle externo son 4 + 4 + 11 + 8 + 4 + 4 + 512 + 10 = 557 Tstados, pero con contienda serán 560 Tstados, y esto repetido 24 veces, una por cada fila de caracteres. Con esto tenemos que copiar una pantalla completa serán entre 116 664 y 117 888 Tstados. Aplicando la fórmula de la entrada anterior podemos aproximar a 117 597 Tstados, lo que es superior a los 112 800 Tstados de una pantalla completa. Si sólo hacemos 23 filas necesitamos 112 696 Tstados para copiar la pantalla frente a 111 008 Tstados disponibles antes de que nos alcance el haz. Pero si hacemos 22 filas, tardaremos 107 797 Tstados, frente a 109 216 Tstados que tarda el haz en alcanzar ese punto, por lo que con esta rutina, aunque ahorramos casi dos tercios de memoria, perdemos una fila respecto a la rutina anterior.

Sin embargo, no debemos olvidar que esto sólo significa que no podemos tener animaciones fluidas en las dos últimas filas de la pantalla, pero sí podemos tener gráficos estáticos o semi-estáticos como un marcador, un inventario… cosas que no cambien demasiado a menudo de manera que un artifact durante su modificación pase desapercibido.

Pintando en el Spectrum (2)

Esta mañana estaba revisando mi código y se me ocurrió una pequeña optimización. JP cc, nn (salto condicional) necesita 10 Tstados, mientras que DJNZ necesita 13 cuando no se cumple la condición, y 8 cuando sí se cumple. Dado que siete veces no se cumple pero una sí se cumple, si podemos sustituir el DJNZ por un JP pe, nn, ahorraremos 19 Tstados en cada fila. Sólo tenemos que cargar BC con 256, que es el número de transferencias que tenemos que hacer entre grupos de atributos. Pero además, dado que cada vez que terminamos una fila de caracteres BC valdrá cero, podemos simplemente incrementar B en uno, que es una operación más rápida que cargar un número, con lo que ahorraremos 3 Tstados más. Así quedaría el código:

    LD SP, tabla_direcciones
    LD HL, buffer
    LD BC, 0 ; mismo valor que si hubiésemos hecho una fila entera
    EXX ; cambiamos al juego de registros alternativo
    LD HL, buffer + 6144 ; apunta a los atributos de color del buffer
    LD DE, 22528 ;  zona de atributos de la pantalla
    LD BC, 768 ; tamaño de los atributos
 loop1:
     EXX ; volvemos al juego original con los datos de píxeles
     INC B ; como BC aquí vale cero, esto es igual que LD BC, 256
           ; pero más rápido
 loop2:
     POP DE
     LDI
     … ; 32 LDIs en total
     LDI
     JP PE, loop2
     EXX
     LDI
     … ; 32 LDIs en total
     LDI
     JP PE, loop1 ; no podemos usar DJNZ porque el salto es
                   ; de más de 128 bytes
     …
 tabla_direcciones:
     DEFW 0x4000, 0x4100, 0x4200, 0x4300, 0x4400, 0x4500, 0x4600, 0x4700
     DEFW 0x4020, 0x4120, 0x4220, 0x4320, 0x4420, 0x4520, 0x4620, 0x4720
     ; completar hasta las 192 líneas

Ahora el bucle interno dura 11 + 512 + 10 = 533 Tstados, aunque en las zonas con contienda serán 536, por lo que necesitaremos entre 4 264 y 4 288 Tstados por cada fila de caracteres. Sumando la parte de los atributos tenemos que serán 4 + 4 + 4 + 512 + 10 = 534 Tstados extra, que cuando haya contienda subirá a 536 Tstados. Y esto para cada una de las 24 filas de la pantalla, lo que nos da entre 115 152 y 115 776 Tstados, lo que significa que en el peor de los casos estamos igual, pero en el mejor ahorramos algo.

¿Pero realmente existe ese «mejor de los casos» si, al escribir en pantalla, siempre tenemos contienda? En realidad esto sólo es verdad a medias: sólo tenemos contienda cuando la ULA está leyendo de la memoria para pintar el paper, pero no cuando está pintando el borde. Teniendo en cuenta que de las 312 líneas de la pantalla, sólo 192 tienen contienda, y las 192 las recorremos dos veces (pues primero el haz va por delante nuestra, pero al llegar al final de la pantalla y volver al principio va por detrás hasta que nos alcanza) tenemos un total de 504 líneas, de las cuales 120 no tienen contienda. Eso significa que el tiempo real será, aproximadamente, 0,762 * tiempo_peor + 0,238 * tiempo mejor. Por tanto, en este caso, tenemos que tardaremos 115 627 Tstados, frente a los 115 764 Tstados del caso anterior. No es mucha diferencia, pero cualquier Tstado que ahorremos es tiempo que podemos emplear luego en generar el siguiente frame. Y teniendo en cuenta que antes de pintarlo tenemos que sincronizarnos con la pantalla, el pasarnos tan solo un Tstado puede hacer que tengamos que esperar al siguiente frame de la pantalla.

Pintando en el Spectrum (1)

Cuando tenía 12 años heredé el Sinclair ZX Spectrum de mi hermano. Era un ordenador que me fascinaba, y con él aprendí a programar, primero en BASIC, y luego directamente en Ensamblador. También aprendí rudimentos de electrónica digital, y gracias a ello construí varios circuitos que le acoplé, como un teclado nuevo, un puerto de E/S de 16 bits, y más.

Es una máquina a la que siempre le tuve mucho cariño, y por eso me lancé hace unos años a escribir mi propio emulador, FBZX, cuando los que había en aquel entonces no me acababan de convencer. Y recientemente, a raíz de varios canales de youtube de «nostalgia de 8 bits», me he puesto un poquito así y he decidido intentar programar algo. Al principio probé a usar el compilador de Z88dk para poder utilizar lenguaje C, hasta que vi la chapuza de código que genera (algo que no es culpa de los desarrolladores, sino de la propia arquitectura del Z80, que no es nada adecuada para C y, sencillamente, es imposible generar mejor código). Ante esto, decidí pasarme a Z80ASM y trabajar desde cero en ensamblador. A fin de cuentas, en una máquina de 8 bits cada bit cuenta, y el poder optimizar cada rutina hasta la última instrucción puede ser la diferencia entre conseguir o no conseguir algo concreto.

Y precisamente una de las cosas en donde la velocidad es crítica es a la hora de pintar en la pantalla, pues, por desgracia, el Spectrum original no tiene ninguna ayuda para esa tarea, si siquiera una doble página (a pesar de que sólo habría requerido añadir un único flip-flop a la ULA). El Spectrum 128K sí tiene dos páginas de vídeo, lo que permite que la ULA muestre una de ellas mientras el código genera el siguiente fotograma en la otra página, y cuando haya terminado, sólo tiene que esperar a que la ULA empiece a pintar un nuevo cuadro para cambiar la página activa, de manera que ahora se mostrará lo que haya en la segunda página y el programa podrá pintar el siguiente fotograma en la primera página.

Cuando se usa el sistema de doble página, las animaciones van fluidas y sin parpadeos ni deformaciones. Por desgracia, los modelos de 48K, al no tener esta capacidad, obligan al programador a sincronizarse con el haz de electrones de la pantalla para evitar que «le pille el rayo» en mitad de un acceso a la pantalla. Hay mucha literatura al respecto, así que no voy a entrar en explicar en qué consiste lo de «competir con el haz«. Sí voy a dar, sin embargo, algunas notas sobre cómo funciona la pantalla en el Spectrum. Para ello, veamos este dibujo de una televisión con la imagen generada por la circuitería del ordenador (la famosa ULA):

Una televisión con la imagen generada por un Spectrum.

En el dibujo vemos una televisión, y en la pantalla podemos ver las dos zonas en las que se divide la imagen generada por la ULA en el Spectrum: el border (en color verde en el dibujo) y el paper, o la zona de trabajo, en color blanco. También vemos esquematizado el recorrido del haz de electrones, de derecha a izquierda y de arriba a abajo, aunque exagerado. El border es una zona en la que sólo podemos definir qué color global queremos, pero no podemos pintar en ella. Sólo en el paper podemos pintar píxeles escribiendo en una zona de memoria, a partir de 0x4000 hasta 0x5800, en donde cada bit se corresponde con un pixel. Esta zona se divide en 256×192 pixels. A mayores, justo a continuación se encuentra la zona que almacena los atributos de color, que mide 768 bytes. Esta zona ocupa un byte para cada grupo de 8×8 píxels, y especifica qué color se usará para cuando el bit asociado a cada pixel está a 0 o a 1.Para más detalles, recomiendo leer la información de World of Spectrum sobre la memoria de vídeo. Edito: o bien la entrada número 4 de esta misma serie, donde entro en más profundidad en cómo es la distribución de la pantalla en el Spectrum. Siento no haber hecho las cosas en orden.

La ULA refresca la pantalla a una tasa de 50 veces por segundo. Además, para simplificar la circuitería, no utiliza entrelazado, sino que siempre pinta únicamente las líneas impares. Y dado que el reloj de la CPU va a 3,5 MHz, eso significa que desde que comienza a pintar la imagen, en la esquina superior izquierda del borde, hasta que ha terminado y empieza a pintar la siguiente, tenemos, en teoría, 3 500 000 / 50 = 70 000 ciclos de reloj o Tstados. Y como tenemos 312,5 líneas (recordemos que sólo utilizamos medio campo, por lo que es la mitad de 625), cada línea dura 224 Tstados. En la práctica no podemos tener media línea, por lo que, en realidad, cada frame dura 69 888 Tstados y tenemos un total de 312 líneas. Además, sabemos que cada vez que se empieza a pintar un frame, la ULA genera una interrupción, la cual podemos utilizar para sincronizarnos con la generación de la imagen. Por último, hay 64 líneas de border antes de que empiece a pintarse el paper.

Con todo esto ya podemos hacer un primer cálculo, que nos dice que si queremos estar seguros de que hemos pintado todo en pantalla antes de que el haz llegue al paper, tenemos que hacerlo en menos de 64 * 224 = 14 336 Tstados. Por desgracia esto es muy poco tiempo.

Una solución consiste en esperar a que la ULA haya terminado de pintar el paper y, entonces, pintar lo que necesitemos. En este caso tendremos 69 888 – (224 * 192) = 26 880 Tstados, que es el tiempo que tarda en pintar la parte inferior y la superior del borde. Ya es un poco más, pero todavía no llega para demasiado.

La solución que usan muchos juegos consiste en implementar una doble página por software. Para ello, pintan toda la imagen en una zona diferente de memoria, y cuando está lista, la copian de golpe en la memoria de pantalla. De esta manera la imagen que aparece siempre es definitiva y no hay parpadeos. El inconveniente es que consume casi siete Kbytes extra.

El problema es que, como de costumbre, la cosa no es tan sencilla. Veamos por qué. La primera idea, la más naive, sería utilizar la instrucción LDIR del Z80. Esta instrucción permite copiar un bloque de memoria a otra posición, y recibe tres parámetros: la dirección inicial del bloque (en el registro HL), la dirección de destino (en el registro DE), y el tamaño del bloque (en el registro BC), y hace todo el trabajo por nosotros: lee el byte contenido en la dirección de memoria apuntada por HL, lo escribe en la dirección de memoria apuntada por DE, incrementa en uno ambos registros, decrementa en uno el registro BC, y repite la operación hasta que este último valga cero. ¡Una bicoca! Así que lo único que tendríamos que hacer es esperar a la interrupción, cargar los valores en los registros, y ejecutar LDIR. ¿O no?

El problema es que cada iteración de LDIR consume 21 Tstados. Eso significa que una pantalla completa, que ocupa 6 912 bytes, tardará 145 152 Tstados en copiarse… ¡que es más del doble de lo que se tarda en generarse! Eso significa que el haz nos alcanzará como mínimo dos veces en cada volcado. Y sí, digo «como mínimo», porque por cuestiones de diseño, la pantalla no se almacena de manera secuencial en memoria, sino que primero viene la fila 0 de pixels, ocupando 32 bytes, luego la fila 8, luego la 16, 24, 32, 40, 48 y la 56, momento en que vuelve atrás hasta la fila 1, luego la 9, la 17… Eso hace que podamos cruzar el haz varias veces, y complica aún más la gestión de la pantalla.

Por si fuera poco, hay un segundo problema, y es que la ULA interfiere en el acceso a la RAM cuando está generando la imagen. Esto es debido a que en chips de RAM normales no es posible que dos sistemas lean o escriban a la vez, sino que se tienen que turnar. La ULA necesita leer dos bytes (uno con los pixels, y otro con los atributos de color) cada cuatro Tstados mientras está pintando el paper, y sólo deja la memoria libre cuando está pintando el borde. Para conseguir esto, lo que hace es monitorizar constantemente a la CPU, y si intenta acceder a la zona de vídeo mientras la ULA está pintando el paper, le detiene el reloj a la CPU hasta que termine.

Por suerte, gracias a un ingenioso diseño, la ULA sólo necesita tres Tstados para leer esos dos bytes, así que lo que hace es agrupar dos lecturas seguidas y dejar los dos Tstados restantes para la CPU. Esto significa que, si queremos que la escritura en la zona de vídeo no se vea penalizada por la ULA, tenemos que hacer accesos con un periodo que sea múltiplo de ocho Tstados. De esta manera, el primer acceso se verá retrasado entre uno y seis Tstados según donde caiga, pero el resto coincidirán exactamente en el siguiente hueco. Y este es otro de los problemas de LDIR: como cada ciclo dura 21 Tstados, que no es múltiplo de ocho, significa que, en la práctica, será como si la instrucción durase 24 Tstados (la primera hará la escritura en el primer hueco libre; la segunda lo hará en el segundo hueco, tres periodos después, con lo que ambas durarán 21 Tstados; la tercera, en cambio, se retrasará seis Tstados, la cuarta no sufrirá retraso, la quinta se retrasará seis Tstados… y así sucesivamente).

Sin embargo, si vemos el juego de instrucciones, comprobaremos que existe también una instrucción similar, LDI, que hace lo mismo excepto repetir la operación, y sólo consume 16 Tstados, que, además, es múltiplo de 8 y, por tanto, no sufriría penalización por parte de la ULA. ¿Qué pasaría si pusiésemos una laaaaaarga ristra de ellas, una detrás de otra? Pues que necesitaríamos 6 912 * 16 = 110 592 Tstados. Aún es más que el tiempo que se necesita para refrescar una pantalla, pero aún nos queda un as en la manga. ¿Qué ocurriría si esperamos a que el haz llegue hasta el principio del paper, y justo entonces empezamos a pintar? En ese caso, el haz iría por delante nuestra, y nunca lo adelantaríamos porque ya hemos visto que somos más lentos que él, y eso significa que cuando el haz haya pintado toda la pantalla y vuelto al punto de partida, ya llevaremos más de la mitad de la imagen pintada y, además, tendremos aún casi otro frame de tiempo antes de que nos alcance.

Veamos exactamente de cuanto tiempo disponemos. Tenemos los 69 888 Tstados que tarda en recorrer un frame, y ahora tenemos que sumarle el tiempo que tarda en llegar hasta el borde inferior derecho del paper. Esto será 224 Tstados_por_linea * 191 lineas_completas + 128 Tstados_de_la_ultima_linea = 42 912 Tstados. Esto significa que si utilizamos este truco, tendremos disponibles un total de 112 800 Tstados para copiar la pantalla antes de que nos alcance el haz, que es más de lo que tarda la ristra de LDIs. ¡Buena cosa!

Por desgracia, esta idea tiene dos problemas:

  • Para que funcione, tenemos que escribir los datos de manera lineal en la pantalla, de manera que nos mantengamos siempre por detrás del haz. Pero si usamos LDI directamente, se hará de manera escalonada por cómo está organizada la pantalla del Spectrum, y adelantaremos y retrasaremos al haz constantemente.
  • Cada instrucción LDI ocupa dos bytes, lo que significa que necesitaríamos una ristra de instrucciones que ocuparía el doble que el tamaño del bloque a copiar, lo que supone un desperdicio exagerado de memoria.

Así, dado que la única secuencia consecutiva que tenemos son los 32 bytes de cada línea, la solución es utilizar una secuencia de 32 instrucciones LDI, y modificar entre medias los registros para ir línea a línea. El problema es que cómo hacerlo con el mínimo de instrucciones posible, pues de nada sirve si perdemos por un lado lo que ahorramos por otro.

La primera solución consiste en tener una tabla con las direcciones de cada scanline, de manera que sólo tenemos que coger la dirección actual, ejecutar 32 instrucciones LDI, y repetir el bucle hasta que el flag de desbordamiento (overflow) se active después de la última instrucción LDI. El problema es que LDI utiliza los tres pares de registros principales, con lo que no nos queda nada para mantener un puntero a la lista de direcciones…

¿O sí que lo tenemos? Porque podemos simplemente cargar el puntero de pila SP con la dirección de la tabla, y leer los valores directamente con POP DE. Esta instrucción tiene la ventaja de que con sólo 11 Tstados nos carga un valor de 16 bits e incrementa el puntero de la tabla.

El resultado sería este código:

LD SP, tabla_direcciones
LD HL, buffer
LD BC, 6144 ; tamaño del buffer. De momento prescindimos del color
loop:
POP DE
LDI
LDI
… ; 32 instrucciones LDI en total
LDI
JP PE, loop

tabla_direcciones:
DEFW 0x4000, 0x4100, 0x4200, 0x4300, 0x4400, 0x4500, 0x4600, 0x4700
DEFW 0x4020, 0x4120, 0x4220, 0x4320, 0x4420, 0x4520, 0x4620, 0x4720
…. ; completar hasta las 192 líneas

Este código es muy rápido: dado que la instrucción JP consume 10 Tstados, tenemos que copiar cada fila consume un total de 533 Tstados; pero como tenemos que redondear a un múltiplo de 8 para tener en cuenta la contienda de memoria, se nos quedan en 536 Tstados; y como una pantalla completa son 192 líneas, al final gastamos entre 102 336 y 102 912 Tstados (no olvidemos que sólo hay contienda mientras se pinta el paper, nunca mientras se pinta el border, por lo que el valor final estará entre ambos), lo que está muy por debajo del límite de 112 800 Tstados. ¡Buena cosa! Por desgracia, nos falta por copiar los atributos de color, y el problema es que tenemos que copiar una fila de atributos por cada ocho de píxels, pues tenemos que mantenernos por detrás del haz catódico; no podemos copiar primero todos los píxels y luego todos los atributos. El problema es que ya estamos usando absolutamente todos los registros con la instrucción LDI, y ni siquiera tenemos la pila disponible para almacenar los valores entre uno y otro grupo.

¡¡¡Pero tenemos el juego de registros alternativo!!! El Z80 tiene su conjunto principal de registros, AF, BC, DE y HL, pero también tiene un segundo juego, AF’, BC’, DE’ y HL’, que podemos intercambiar con sólo dos instrucciones: EX AF, AF’ (que intercambia los valores de AF y AF’) y EXX (que intercambia los valores de BC, DE y HL con los de sus homólogos). Así que, dado que los atributos de color sí siguen un formato normal, podemos copiar ocho filas primero usando la tabla, cambiar al juego alternativo de registros, copiar una fila de atributos de color, volver a cambiar el juego, copiar otras ocho filas de pixeles, etc. El código quedaría así:

LD SP, tabla_direcciones
LD HL, buffer
EXX ; cambiamos al juego de registros alternativo
LD HL, buffer + 6144 ; apunta a los atributos de color del buffer
LD DE, 22528 ; zona de atributos de la pantalla
LD BC, 768 ; tamaño de los atributos
loop1:
EXX ; volvemos al juego original con los datos de píxeles
LD B, 9 ; las ocho filas necesitan 256 LDIs. Cada uno resta
; uno a BC; luego al final de ellos, C valdrá lo mismo
; pero B se habrá decrementado en una unidad. Así
; que tenemos que tenerlo en cuenta para DJNZ
loop2:
POP DE
LDI
… ; 32 LDIs en total
LDI
DJNZ loop2
EXX
LDI
… ; 32 LDIs en total
LDI
JP PE, loop1 ; no podemos usar DJNZ porque el salto es
; de más de 128 bytes

tabla_direcciones:
DEFW 0x4000, 0x4100, 0x4200, 0x4300, 0x4400, 0x4500, 0x4600, 0x4700
DEFW 0x4020, 0x4120, 0x4220, 0x4320, 0x4420, 0x4520, 0x4620, 0x4720
; completar hasta las 192 líneas

El bucle interno consume un total de 512 + 11 + 13 = 536 Tstados cuando B es distinto de cero (que, además, es múltiplo de 8), y 512 + 11 + 8 = 531 Tstados cuando B es cero. Por tanto, para ocho filas tenemos un total de 536 * 7 + 531 = 4 283 Tstados. Pero a esto hay que sumar la inicialización previa y la copia de los atributos. Antes tenemos, del EXX y el LD B, 9, un total de 4 + 7 = 13 Tstados, y después 4 + 512 + 10 = 526 Tstados; luego cada fila completa de 8 píxels de altura y con atributos necesita un total de 4 283 + 13 + 526 = 4 822 Tstados. Pero para que sea múltiplo de 8 hay que subir a 4 824 Tstados. Y como tenemos 24 filas, el total estará entre 115 728 y 115 776 Tstados.

Vaya, ahora nos hemos pasado. Una pena, porque eso significa que no podemos animar algo a pantalla completa sin que haya artifacts. Pero si copiamos sólo 23 filas sí nos da tiempo, pues ahí no necesitaríamos más de 110 952 Tstados. Por tanto, una solución consiste en obviar la última fila, lo cual no tiene por qué ser un problema, pues normalmente se suele dejar la parte baja de la pantalla para marcadores y otros elementos relativamente estáticos. Además, tenemos la ventaja de que el buffer de trabajo está en un formato secuencial, lo que simplifica pintar en él.

Esta rutina ya sería utilizable, pero tiene el inconveniente de necesitar una tabla de 384 bytes para las direcciones, que es mucho mayor que los 157 bytes de la rutina en sí. Sin embargo, si renunciamos a las dos últimas filas en lugar de sólo a la última, podemos reducir muchísimo la memoria consumida. Pero eso será en el próximo artículo.

A ritmo de conga (16)

Hace unos días me encontré con un problema raro: mi aspiradora no respondía a mis órdenes en la app. Pero lo mas raro era el estado de los botones: estaba cargando, pero el botón de «volver a casa» estaba activo, y el de «empezar a limpiar» estaba desactivado.

Un vistazo rápido a los logs me permitió descubrir que el estado actual de la aspiradora era «10», y antes había pasado por el «9» y por el «7». Esto era muy raro, pues los únicos estados que conocía hasta ahora iban desde el «1» (limpiando), hasta el «6» (en la base, batería cargada), tal y como comenté en la entrada «A ritmo de conga (6)».

Obviamente esto era muy raro, así que empecé a investigar revisando los logs, y descubrí qué ya me había ocurrido lo mismo en dos ocasiones más. Las preguntas obvias eran: ¿por qué? y ¿Qué significaban esos nuevos estados?

Entonces me fijé en un detalle raro: antes de pasar al modo «9», la aspiradora estaba limpiando (modo «1»), pero la batería estaba muy baja. De pronto se puso en modo «9», y unos segundos después pasó a modo «10», y entonces el nivel de la batería, de pronto, empezó a subir. Y cuando llegó al 100%, volvió a modo «1», o limpiar.

La respuesta era obvia: cuando la aspiradora está limpiando y ve que la batería está demasiado baja como para terminar el trabajo, ella sola se vuelve a la base (modo «9») para cargarla de nuevo (modo «10») y continuar la operación donde la dejó. El «7» todavía no se qué puede ser. A lo mejor tiene que ver con un par de veces que se me quedó atascada por culpa de un reflejo del sol (no se por qué, pero si da el sol en el parqué, el sensor antiescaleras se lía), pero tengo que hacer más pruebas para confirmarlo.

Así, la lista de estados posibles pasa a ser:

  • 1: limpiando
  • 2: detenida
  • 4: regresando a la base
  • 5: en la base, cargando la batería
  • 6: en la base, batería cargada
  • 7: ??????
  • 9: regresando a la base por batería baja
  • 10: en la base, cargando batería, para continuar un trabajo previo interrumpido por batería baja

A ritmo de conga (15)

Hoy hice algunos cambios extra en el repositorio de OpenDoñita. Para empezar, dado que han actualizado OSMC y ya trae Python 3.7, ahora ya es posible instalar mi servidor directamente en una Raspberry Pi con él. Así lo hice, así que aproveché para añadir un nuevo instalador que sea más sencillo. Ahora, al ejecutar install.sh, se bajarán los paquetes necesarios (tanto de python como del sistema operativo), se instalará todo en el sitio correspondiente, y se lanzarán los servicios necesarios.

Por otro lado, he escrito un nuevo programa para emparejar la aspiradora. Tiene esta pinta:

Gracias a ella, ahora es un poco más sencillo realizar el emparejado desde cualquier ordenador, con la única condición de que éste tenga WiFi.

Crust y protothreads

Hace tiempo había descubierto un curioso mecanismo denominado protothreads. Se trata de una manera de añadir capacidades asíncronas a C. El truco es bastante curioso, pues se basa en una característica poco conocida de C, que consiste en que un switch puede saltar a la mitad de un bucle, ya sea for, while o do…while. Esto es, este código es legal:

switch (a) {
default:
    while(1) {
        do something
        return;
case XXXX:
        do more things
    }
}

Esto funciona perfectamente en C, y además es perfectamente legal, gracias a la definición explícita de cómo funcionan los bucles en este lenguaje.

La idea detrás de esto es generar a mayores una serie de macros y definiciones que permitan embellecer el código, y hacer que se parezca más a programación asíncrona.

La cuestión es que llevo unas semanas trabajando en un proyectillo en C que utiliza eventos y programación asíncrona, y para simplificar y mejorar el código decidí implementar protothreads. Por supuesto, también utilizaba mi viejo analizador estático crust para analizar el código y detectar errores, y justo aquí me encontré con el problema: no era capaz de analizar correctamente estas estructuras.

Así que aquí tenía dos opciones: o pasar de crust, o actualizarlo para que lo soporte. Y obviamente me decidí por lo segundo.