Source: https://wiki.speccy.org/cursos/ensamblador/gfx2_direccionamiento
Timestamp: 2020-04-05 10:20:30+00:00

Document:
cursos:ensamblador:gfx2_direccionamiento [El wiki de speccy.org]
gfx2_direccionamiento
cursos:ensamblador:gfx2_direccionamiento
Gráficos (y II): Cálculo de direcciones y coordenadas
Cálculo de posiciones de atributo
Cálculo de posiciones diferenciales de atributo
Cálculo de posiciones de caracteres
Cálculo de posiciones de caracteres por composición
Cálculo de posiciones diferenciales de carácter
Cálculo de posiciones de píxeles
Cálculo de posiciones de pixeles mediante composición
Obtención y uso de la Máscara de Pixel
La rutina de la ROM PIXEL-ADDRESS
Cálculo de posiciones de pixeles mediante tabla
Optimizando la lectura a través de tablas
Cálculos contra Tablas
Cálculo de posiciones de posiciones diferenciales de pixel
Rutinas cruzadas y de propósito general
En el anterior capítulo exploramos la organización interna de los 6912 bytes de la videomemoria del Spectrum, separándo esta videomemoria en un área de 6144 bytes de archivo de imagen comenzando en la dirección 16384 y otros 768 bytes de archivo de atributos comenzando en la dirección 22528.
La manipulación de dichas áreas de memoria nos permite el trazado de gráficos en pantalla y la manipulación de los colores que tienen dichos gráficos en el monitor. Esto es así porque este área de memoria es leída por el chip de la ULA 50 veces por segundo en sistemas de televisión PAL (Europa y Australia) y 60 veces por segundo en sistemas NTSC (América y Asia) para enviar la señal que el televisor convierte en una imagen para nuestros ojos.
Es necesario que la ULA refresque la pantalla de forma continuada y regular ya que debe reflejar los cambios que los programas hagan en la videomemoria, así cómo para refrescar el estado de los píxeles del monitor (necesario por el funcionamiento de la tecnología CRT).
Sabemos por el capítulo anterior que escribir en la videomemoria nos permite trazar gráficos en pantalla. Hasta ahora hemos visto efectos globales aplicados a toda la vram (borrados, fundidos, etc), pero nuestro interés principal será, seguramente, el trazar gráficos con precisión de bloque o de pixel en la pantalla.
Para poder realizar esta tarea necesitamos relacionar las posiciones de memoria de la videoram con las coordenadas (x,y) de pantalla cuya información gráfica representan. Necesitaremos pues programar rutinas de cálculo de direcciones en función de coordenadas de alta y baja resolución. Sabemos cómo dibujar, pero no cómo calcular la dirección de memoria donde hacerlo. Este es precisamente nuestro objetivo en esta sección.
Para empezar, estableceremos una terminología unánime a la que haremos referencia a lo largo de todo el capítulo, con las siguientes definiciones:
Resolución del área gráfica: El Spectrum dispone de una resolución de 256×192 píxeles cuyo estado se define en cada byte del área de imagen de la videomemoria.
Estado de un pixel: Cada byte del área de imagen contiene el estado de 8 píxeles, de tal forma que cada uno de los bits de dicho byte pueden estar a 1 (pixel encendido, se traza con el color de tinta) o a 0 (apagado, se traza con el color del papel).
Resolución del área de atributos: El Spectrum tiene una resolución de color de 32×24 puntos, que se mapean sobre la pantalla de forma que cada grupo de 8×8 píxeles del área gráfica tiene una correspondencia con un atributo del área de atributos.
Atributo: Un atributo define en los 8 bits de un byte el color de tinta y papel y el estado de brillo y parpadeo de un bloque concreto de la pantalla.
Bloque o carácter: Si dividimos la pantalla de 256×192 en 32×24 bloques de color, nos quedan bloques de 8×8 píxeles que mantienen el mismo atributo de pantalla. Estamos acostumbrados a trabajar con bloques ya que el intérprete BASIC del Spectrum utiliza la fuente de la ROM de 8×8 en una rejilla de bloques que coincide con la resolución de atributos. Podemos pensar en los bloques como “posiciones de carácter”.
Scanline: Un scanline es una línea normalmente horizontal de datos gráficos. Por ejemplo, el scanline 0 de pantalla es la línea gráfica que va desde (0,0) a (255,0), y que definen los 32 bytes de videomemoria que van desde 16384 hasta 16415. También se puede hablar del scanline de un sprite o de un carácter cuando nos referimos a una línea concreta de esa porción de gráfico.
Coordenadas (x,y): Se utiliza la nomenclatura (x,y) para definir la posición de un píxel en pantalla en función de su posición horizontal y vertical siendo (0,0) la esquina superior izquierda de la misma y (255,191) la esquina inferior derecha. Son, pues, “coordenadas en alta resolución”.
Coordenadas (c,f): Se utiliza la nomenclatura (c,f), de (columna,fila), para hacer referencia a la posición de un bloque 8×8 en pantalla en función de su posición horizontal y vertical siendo (0,0) la esquina superior izquierda y (31,23) la esquina inferior derecha. Se conocen como “coordenadas en baja resolución” o “coordenadas de bloque” o “de carácter”.
Conversión (c,f) a (x,y): Como cada bloque es de 8×8 píxeles, podemos convertir una coordenada en baja resolución a coordenadas de pixel como (x,y) = (8*c,8*f). Asímismo, (c,f) = (x/8,y/8).
Tercio de pantalla: El área gráfica del Spectrum se divide en 3 áreas de 2KB de videoram que almacenan la información de 256×64 píxeles. Estas áreas son comunmente denominadas “tercios”.
Offset o Desplazamiento: Llamaremos offset o desplazamiento a la cantidad de bytes que tenemos que avanzar desde una base (normalmente el inicio de la propia memoria o un punto de la misma) para llegar a una posición de memoria. Así, un offset de 32 bytes desde 16384 referenciará a los 8 píxeles desde (0,1) a (7,1). En las rutinas que veremos, el offset estará calculado con $0000 como la base, es decir, serán offsets absolutos (posiciones de memoria).
Con estas definiciones, podemos hacer las siguientes afirmaciones:
La pantalla del Spectrum tiene 192 scanlines horizontales de 256 píxeles cada uno.
La pantalla del Spectrum se divide en 32×24 bloques o posiciones de caracteres.
Un bloque o carácter tiene 8 scanlines de 8 píxeles cada uno (8×8).
Cada byte de la videoram almacena el estado de 8 píxeles, por lo que un bloque se almacena en 8×8/8 = 8 bytes.
Cada posición de bloque / carácter de la pantalla tiene asociado un atributo del área de atributos.
Cada uno de los 3 tercios de la pantalla tiene 8 líneas de 32 caracteres.
Para aprovechar la información que trataremos en este capítulo es imprescindible comprender a la perfección la organización interna de la videomemoria que se detalló en el anterior capítulo.
A modo de resumen, la estructura interna de estas 2 áreas de memoria es la siguiente:
El área de imagen se divide en 3 tercios de pantalla de 2KB de memoria cada uno, que van de $4000 a $47FF (tercio superior), de $4800 a $4FFF (tercio central) y de $5000 a $57FF (tercio inferior).
Cada uno de los tercios comprende 8 líneas de 32 bloques horizontales (256×64 píxeles). Dentro de cada uno de esos 2KB, tenemos, de forma lineal, 64 bloques de 32 bytes (256 píxeles) de información que representan cada scanline de esos 8 bloques.
Los primeros 32 bytes de dicho bloque contienen la información del scanline 0 del bloque 0. Avanzando de 32 en 32 bytes tenemos los datos del scanline 0 del bloque 1, el scanline 0 del bloque 2, el scanline 0 del bloque 3, etc, hasta que llegamos al scanline 7 del bloque 0. Los siguientes 32 bytes repiten el proceso pero con el scanline 1 de cada bloque.
Tras los últimos 32 bytes de un tercio, vienen los primeros 32 bytes del siguiente tercio, con la misma organización, pero afectando a otra porción de la pantalla.
Area de atributos:
El área de atributos se encuentra en memoria inmediatamente después del área de imagen, por lo que empieza en la posición de memoria 16384+6144 = 22528 ($5800).
Cada byte del área de atributos se denomina atributo y define el valor de color de tinta, papel, brillo y flash de un carácter / bloque de la pantalla. Esto implica que el área de atributos ocupa 32x24x1 = 768 bytes en memoria, por lo que empieza en 22528 ($5800) y acaba en 23295 ($5AFF).
Los diferentes bits de un atributo de carácter son: Bit 7 = FLASH, Bit 6 = BRIGHT, Bits 5-3 = PAPER, Bits 2-0 = INK.
Los valores de tinta y papel son un valor de 0-7 que junto al brillo como bit más significativo componen un índice (B-I-I-I) contra una paleta de colores interna definida en la ULA, donde el 0 es el color negro y el 15 el blanco de brillo máximo.
La organización interna del área de atributos es lineal: Los primeros 32 bytes desde $5800 se corresponden con los atributos de la primera fila de bloques de la pantalla. Los segundos 32 bytes, con la segunda fila, y así sucesivamente hasta los últimos 32 bytes que se corresponden con los atributos de la fila 23. La organización de la zona de atributos no se ve pues relacionada con los tercios de pantalla, tan sólo con la columna y fila (c,f) del bloque.
Nuestro capítulo de hoy tiene los siguientes objetivos prioritarios:
Cálculo de posiciones de atributo: Saber calcular la posición en memoria del atributo de una posición de carácter (c,f) o de un pixel (x,y).
Cálculo de posiciones de carácter (baja resolución): Saber calcular la posición en memoria en que comienzan los datos gráficos (pixel 0,0 del carácter) de un carácter o bloque de 8×8 píxeles referenciado como (c,f) o (x,y), asumiendo una resolución de 32×24 bloques en pantalla coincidiendo con las posiciones de carácter de texto estándar.
Cálculo de posiciones de pixel (alta resolución): Saber calcular la posición en memoria de un pixel referenciado por (x,y).
Cálculo de posiciones diferenciales: Dada una dirección de memoria de un atributo, carácter o pixel, ser capaz de modificar esta dirección para acceder a los elementos de la izquierda, derecha, arriba o abajo.
Utilizaremos las rutinas que veremos a continuación para el posicionamiento en pantalla de los elementos de nuestros juegos y programas. En los próximos capítulos trabajaremos ya con sprites en baja y alta resolución, fuentes de texto, mapeados por bloques, etc.
Durante el desarrollo de un programa gráfico o un juego necesitaremos (ya sea como funciones independientes o dentro de rutinas de sprites/gráficos más amplias) alguna de las siguientes rutinas:
Get_Attribute_Offset_LR(c,f) : Dadas las coordenadas en baja resolución (columna,fila) de un bloque / carácter, debe devolver la dirección de memoria del atributo de dicho bloque.
Get_Attribute_Offset_HR(x,y) : Dadas las coordenadas en alta resolución (x,y) contenida en un bloque / carácter, debe devolver la dirección de memoria del atributo de dicho bloque.
Get_Attribute_Coordinates_LR(offset): Dada una dirección de memoria dentro del área de atributos, debe devolver las coordenadas (c,f) en baja resolución del bloque al que está asociado.
Get_Attribute_Coordinates_HR(offset): Dada una dirección de memoria dentro del área de atributos, debe devolver las coordenadas (x,y) en alta resolución del pixel superior izquierdo del bloque al que está asociado.
Es importante comprobar antes de llamar a nuestras rutinas si estas modifican algún registro o flag que necesitemos preservar. Podemos modificar las rutinas para que realicen PUSH y POP de los registros necesarios o hacer nosotros estos PUSH/POP en la rutina llamadora.
Comencemos con las rutinas:
Get_Attribute_Offset
Una primera aproximación a la obtención de la dirección en memoria de un atributo concreto (columna,fila) podría ser la utilización de una tabla de 24 valores de 16 bits que alojara las direcciones de inicio en memoria de los atributos del primer carácter de cada fila.
De esta forma bastaría con utilizar el número de fila como índice en la tabla y sumar el número de columna para obtener la dirección de memoria de la celdilla de atributos de (c,f):
Dirección en Hexadecimal
0 $5800 22528 0101100000000000b
1 $5820 22560 0101100000100000b
2 $5840 22592 0101100001000000b
3 $5860 22624 0101100001100000b
4 $5880 22656 0101100010000000b
5 $58A0 22688 0101100010100000b
6 $58C0 22720 0101100011000000b
7 $58E0 22752 0101100011100000b
8 $5900 22784 0101100100000000b
9 $5920 22816 0101100100100000b
10 $5940 22848 0101100101000000b
11 $5960 22880 0101100101100000b
12 $5980 22912 0101100110000000b
13 $59A0 22944 0101100110100000b
14 $59C0 22976 0101100111000000b
15 $59E0 23008 0101100111100000b
16 $5A00 23040 0101101000000000b
17 $5A20 23072 0101101000100000b
18 $5A40 23104 0101101001000000b
19 $5A60 23136 0101101001100000b
20 $5A80 23168 0101101010000000b
21 $5AA0 23200 0101101010100000b
22 $5AC0 23232 0101101011000000b
23 $5AE0 23264 0101101011100000b
Direcciones del atributo en el carácter (0,f)
Así pues, podríamos tener una tabla de 16 bytes para indexarla con el número de fila, que permitiría calcular la dirección de memoria como:
dirección_atributo(c,f) = tabla_offsetY_LR[ f ] + c
No obstante, existe una opción mucho más aconsejable en el caso de los atributos como es el realizar el cálculo de la dirección destino en lugar de un lookup en una tabla.
Como ya vimos en el capítulo anterior, la dirección de un atributo concreto se puede calcular mediante la siguiente fórmula:
Direccion_Atributo(x_bloque,y_bloque) = 22528 + (f*32) + c
Desde el inicio del área de atributos, avanzamos 32 bytes por fila hasta posicionarnos en el bloque de 32 bytes que referencia a nuestro bloque, y sumamos el número de columna.
Implementando este cálculo en código máquina, obtendríamos la siguiente rutina (de la cual no haremos uso, ya que diseñaremos una versión mucho más óptima):
; Obtener la direccion de memoria del atributo del caracter
; (c,f) especificado mediante multiplicacion por 32.
; Entrada:   B = FILA,  C = COLUMNA
; Salida:    HL = Direccion del atributo
Get_Attribute_Offset_LR_SLOW:
; calcular dir_atributo como "inicio_attr + (32*f) + c"
LD L, B         ; HL = "fila"
ADD HL, HL      ; HL = HL*2
ADD HL, HL      ; HL = HL*4
ADD HL, HL      ; HL = HL*8
ADD HL, HL      ; HL = HL*16
ADD HL, HL      ; HL = HL*32
LD E, C         ; DE = "columna"
ADD HL, DE      ; HL = fila*32 + columna
LD DE, 22528    ; Direccion de inicio de atributos
ADD HL, DE      ; HL = 22528 + fila*32 + columna
El código que acabamos de ver es perfectamente funcional pero tiene ciertas desventajas:
Hace uso de prácticamente todo el juego de registros, DE incluído (lo que nos implicaría realizar PUSHes y POPs en nuestra rutina externa o dentro de la misma).
Tiene un coste de ejecución de 112 t-estados.
Veamos cómo podemos mejorar esta rutina: Si nos fijamos en la representación en binario de la anterior tabla de direcciones, veremos que todas ellas siguen un patrón común:
Los 6 bits más significativos de la dirección son 010110, que es la parte de la dirección que provoca que todas las posiciones estén entre $5800 y $5AFF.
Los bits 5, 6, 7 y 8 se corresponden con la fila que queremos consultar.
Los bits 0, 1, 2, 3 y 4 los utilizaremos para acceder a a la columna deseada. En la tabla anterior son siempre 0 porque estamos mostrando las direcciones de inicio de cada fila, es decir, de (0,f), por lo que estos bits 0-4 son 0.
La formación de la dirección destino queda pues así:
Cálculo de la dirección de atributo (c,f)
La rutina de cálculo de la dirección del atributo a partir de coordenadas de baja resolución se podría implementar, pues, de la siguiente forma:
; Get_Attribute_Offset_LR:
; (c,f) especificado. Por David Webb.
Get_Attribute_Offset_LR:
LD A, B            ; Ponemos en A la fila (000FFFFFb)
RRCA               ; Desplazamos A 3 veces (A=A>>3)
AND 3              ; A = A AND 00000011 = los 2 bits mas
; altos de FILA (000FFFFFb -> 000000FFb)
ADD A, $58         ; Ponemos los bits 15-10 como 010110b
LD H, A            ; Lo cargamos en el byte alto de HL
LD A, B            ; Recuperamos de nuevo en A la FILA
AND 7              ; Nos quedamos con los 3 bits que faltan
RRCA               ; Los rotamos para colocarlos en su
RRCA               ; ubicacion final (<<5 = >>3)
ADD A, C           ; Sumamos el numero de columna
LD L, A            ; Lo colocamos en L
RET                ; HL = 010110FFFFFCCCCCb
La rutina realiza operaciones de bits para ubicar los datos de FILA, COLUMNA y 010011b en las posiciones que requiere la dirección destino final. Aconsejamos al lector revisar el capítulo dedicado a Desplazamientos de memoria, bits y operaciones lógicas para recordar el efecto de los desplazamientos realizados con operaciones como RRCA, SRA, SLA, RLC, etc.
El coste de ejecución de esta rutina es de (RET aparte) 70 t-estados y no hace uso de DE, lo que es un ahorro sustancial tanto en tiempo de ejecución como en preservación de un registro muy utilizado.
La salida de esta rutina se puede utilizar directamente para almacenar en (HL) el atributo del caracter (c,f) cuya direccion hemos solicitado:
CALL Get_Attribute_Offset_LR
LD A, 85             ; Brillo + Magenta sobre Cyan
LD (HL), A           ; Establecemos el atributo de (12,10)
La rutina no hace ningún tipo de comprobación del rango de COLUMNA y FILA, por lo que si proporcionamos valores menores de cero o mayores de 31 o 23 respectivamente se devolverá una dirección de memoria fuera del área de atributos.
La versión para coordenadas en alta resolución de la anterior rutina (Get_Attribute_Offset_HR(x,y)) se implementa fácilmente mediante la conversión de las coordenadas (x,y) en coordenadas (c,f) dividiendo x e y entre 8 para obtener las coordenadas de baja resolución que corresponden al pixel que estamos considerando.
Para eso, las primeras líneas de la rutina deberían ser:
SRL B              ; B = B/8 -> Ahora B es FILA
SRL C              ; C = C/8 -> Ahora C es COLUMNA
Una vez obtenido (c,f), el desarrollo de la rutina es el mismo que en el caso de Get_Attribute_Offset_LR(c,f):
; Get_Attribute_Offset_HR:
; Obtener la direccion de memoria del atributo del caracter al
; que corresponde el pixel (x,y) especificado.
; Entrada:   B = Y,  C = X
Get_Attribute_Offset_HR:
Hemos utilizado las instrucciones de desplazamiento SRL sobre los registros B y C para dividir sus valores por 8 y convertir la dirección (x,y) en una dirección (c,f), pudiendo aplicar así el algoritmo de cálculo de dirección que ya conocemos.
Get_Attribute_Coordinates
La siguiente rutina nos proporciona, dada una dirección de memoria apuntada por HL y dentro de la zona de atributos, la posición (c,f) que corresponde a dicho carácter. Se basa en la descomposición de HL en los campos que componen la dirección del atributo:
; Get_Attribute_Coordinates_LR
; Obtener las coordenadas de caracter que se corresponden a
; una direccion de memoria de atributo.
; Entrada:    HL = Direccion del atributo
; Salida:     B = FILA,  C = COLUMNA
Get_Attribute_Coordinates_LR:
; Descomponemos HL = 010110FF FFFCCCCCb
LD A, H            ; A = 010110FFb
AND 3              ; A = bits 0, 1 de HL = 2 bits altos de F, CF=0
RLCA               ; Rotacion a izquierda 000000FFb -> 000FF000b
LD B, A            ; B = 000FF000b
AND 224            ; Nos quedamos con los 3 bits mas altos
RLCA              ; Rotacion a izquierda FFF00000b -> 00000FFFb
OR B               ; A = A + B = 000FFFFFb
LD B, A            ; B = FILA
AND 31             ; Nos quedamos con los 5 bits mas bajos
LD C, A            ; C = COLUMNA
De nuevo, el código no incluye ningún tipo de control sobre la dirección que se le proporciona, que podría estar fuera de la zona de atributos y le haría devolver valores en el rango 0-255 para B y para C que, obviamente, no corresponden con la dirección entrada en HL.
La rutina para trabajar con coordenadas en alta resolución (Get_Attribute_Coordinates_HR(x,y)) es esencialmente idéntica a su versión en baja resolución, salvo que finaliza multiplicando B y C por 8 (mediante instrucciones de desplazamiento a izquierda) para convertir las coordenadas (c,f) en (x,y). Los valores (x,y) resultantes se corresponderán con el pixel superior izquierdo del bloque apuntado por (c,f).
; Get_Attribute_Coordinates_HR
; Obtener las coordenadas de pixel que se corresponden a
; Salida:     B = y,  C = x
Get_Attribute_Coordinates_HR:
RLCA               ; Rotacion a izquierda FFF00000b -> 00000FFFb
SLA C              ; C = C*8
SLA B              ; B = B*8
Una vez calculada la posición de memoria de un atributo, puede interesarnos (por ejemplo, en una rutina de impresión de Sprites) el conocer la dirección de memoria del bloque inferior, superior, izquierdo o derecho sin necesidad de recalcular HL a partir de las coordenadas. Por ejemplo, esto sería útil para imprimir los atributos de un sprite de Ancho X Alto caracteres sin recalcular la dirección de memoria para cada atribuo.
Asumiendo que HL contiene una dirección de atributo válida y que tenemos verificado que nuestro sprite no tiene ninguno de sus caracteres fuera del área de pantalla, podemos modificar HL para movernos a cualquiera de los atributos de alrededor. Para eso aprovecharemos la linealidad del área de atributos incrementando o decrementando HL para movernos a izquierda o derecha y sumando o restando 32 a HL para bajar o subir una línea:
Atributo_derecha:
INC HL            ; HL = HL + 1
Atributo_izquierda:
DEC HL            ; HL = HL - 1
Atributo_abajo:
LD DE, 32
ADD HL, DE        ; HL = HL + 32
Atributo_arriba:
LD DE, -32
ADD HL, DE        ; HL = HL - 32
Si tenemos la necesidad de preservar el valor del registro DE y el utilizarlo para sumar o restar 32 nos supone hacer un PUSH y POP del mismo a la pila y queremos evitar esto, podemos sumar la parte baja y después incrementar la parte alta si ha habido acarreo:
Atributo_abajo_sin_usar_DE_2:
LD A, L           ; A = L
ADD 32            ; Sumamos A = A + 32 . El Carry Flag se ve afectado.
LD L, A           ; Guardamos en L (L = L+32)
JR NC, attrab_noinc
attrab_noinc:       ; Ahora HL = (H+CF)*256 + (L+32) = HL + 32
Atributo_arriba_sin_usar_DE:
SUB 32            ; Restamos A = A - 32 . El Carry Flag se ve afectado.
LD L, A           ; Guardamos en L (L = L-32)
JR NC, attrab_nodec
attrab_nodec:       ; Ahora HL = (H+CF)*256 + (L+32) = HL + 32
Nótese que, como nos apunta Jaime Tejedor en los foros de Speccy.org, el código con salto…
attrab_noinc:
… es más rápido que la combinación de ADD y ADC para sumar 32 al byte bajo de HL y 0 + Acarreo al byte alto de HL:
LD A, 0           ; Ponemos A a cero, no podemos usar un "XOR A"
; o un "OR A" porque afectariamos al Carry Flag.
ADC H             ; A = H + CarryFlag
LD H, A           ; H = H + CarryFlag
; Ahora HL = (H+CF)*256 + (L+32) = HL + 32
Este código no utiliza DE pero se apoya en el registro A para los cálculos. Si necesitamos preservar su valor, siempre podemos realizar un EX AF, AF antes y después de la ejecución de la rutina.
Nuestro siguiente objetivo es el de conocer el mecanismo para trabajar con gráficos de baja resolución o gráficos de bloque / carácter. Esto nos permitirá dibujar gráficos de 8×8 píxeles (o de múltiplos de ese tamaño) comenzando en posiciones de memoria de carácter, en nuestra pantalla de 32×24 bloques de baja resolución.
Para ello necesitamos calcular la dirección de inicio en videomemoria de la dirección de inicio del bloque.
Las rutinas que tenemos que implementar son:
Get_Char_Offset_LR(c,f) : Dadas las coordenadas en baja resolución (columna,fila) de un bloque / carácter, debe devolver la dirección de memoria de los 8 pixeles del scanline 0 de dicho bloque.
Get_Char_Offset_HR(x,y) : Dadas las coordenadas en alta resolución (x,y) de un bloque / carácter, debe devolver la dirección de memoria de los 8 pixeles del scanline 0 de dicho bloque.
Get_Char_Coordinates_LR(offset): Dada una dirección de memoria dentro del área de imagen, debe devolver las coordenadas (c,f) en baja resolución del bloque al que está asociada.
Get_Char_Coordinates_HR(offset): Dada una dirección de memoria dentro del área de imagen, debe devolver las coordenadas (x,y) en alta resolución del pixel superior izquierdo del bloque al que está asociada.
Nótese que podemos realizar las 2 primeras rutinas de forma que devuelvan el offset calculado bien en el registro DE o bien en el registro HL. Según utilicemos los registros en el código que llama a la rutina, puede sernos más conveniente recibir el valor en uno u otro registro. Si resulta necesario, podemos adaptar el código de las rutinas para que funcionen con uno u otro registro, o utilizar al final de la misma (o tras el CALL) un EX HL, DE que devuelva el resultado en el registro que más nos interese.
Utilizando técnicas de composición desde los bits de las coordenadas vamos a calcular la dirección de inicio de cada “primera línea” de fila de caracteres de la pantalla, es decir, el scanline 0 de cada fila de bloques en baja resolución. Conociendo la posición inicial de dicha línea podemos sumar el número de columna y posicionarnos en el inicio del carácter (c,f) deseado, para trazar en él texto o un sprite de 8×8 (o múltiplos).
Al igual que en el caso de las direcciones de atributo, es posible componer la dirección de memoria de este “pixel 0” del “scanline 0” de la fila f mediante descomposición de los bits de las coordenadas y su recomposición en una dirección en memoria.
Para encontrar la relación coordenadas/dirección comencemos viendo una tabla con las direcciones de pantalla buscadas ya precalculadas:
Direccion (0,f) (HEX)
Tercio (0-2)
Fila dentro del tercio
0 $4000 16384 0100000000000000b 0 (00b) 0
1 $4020 16416 0100000000100000b 0 (00b) 1
2 $4040 16448 0100000001000000b 0 (00b) 2
3 $4060 16480 0100000001100000b 0 (00b) 3
4 $4080 16512 0100000010000000b 0 (00b) 4
5 $40A0 16544 0100000010100000b 0 (00b) 5
6 $40C0 16576 0100000011000000b 0 (00b) 6
7 $40E0 16608 0100000011100000b 0 (00b) 7
8 $4800 18432 0100100000000000b 1 (01b) 0
9 $4820 18464 0100100000100000b 1 (01b) 1
10 $4840 18496 0100100001000000b 1 (01b) 2
11 $4860 18528 0100100001100000b 1 (01b) 3
12 $4880 18560 0100100010000000b 1 (01b) 4
13 $48A0 18592 0100100010100000b 1 (01b) 5
14 $48C0 18624 0100100011000000b 1 (01b) 6
15 $48E0 18656 0100100011100000b 1 (01b) 7
16 $5000 20480 0101000000000000b 2 (10b) 0
17 $5020 20512 0101000000100000b 2 (10b) 1
18 $5040 20544 0101000001000000b 2 (10b) 2
19 $5060 20576 0101000001100000b 2 (10b) 3
20 $5080 20608 0101000010000000b 2 (10b) 4
21 $50A0 20640 0101000010100000b 2 (10b) 5
22 $50C0 20672 0101000011000000b 2 (10b) 6
23 $50E0 20704 0101000011100000b 2 (10b) 7
Examinemos (y marquemos) los bits de la representación binaria de la dirección para una selección de elementos de la tabla:
Lo primero que puede llamarnos la atención es lo siguiente:
Hay una relación directa entre el byte alto de la dirección y el tercio en que está posicionada la línea. Esta relación está marcada por los bits 3 y 4 del byte superior:
Tercio superior (0, 00b) → Byte alto = $40 → Bits 3 y 4 = 00.
Tercio central (1, 01b) → Byte alto = $48 → Bits 3 y 4 = 01.
Tercio inferior (2, 10b) → Byte alto = $50 → Bits 3 y 4 = 10.
Conclusión: el número de tercio se corresponde con los 2 bits superiores de la coordenada Y, de tal forma que las fila (0,7) están en el tercio 00b, las filas 8-15 en el tercio 01b, y las 16-23 en el 10b.
Hay una relación directa entre el número de fila dentro de cada tercio (0-7) y los 3 bits superiores (5-7) del byte bajo de la dirección.
Los 3 bytes más significativos de la dirección son siempre 010b. Esta es la parte de la composición de la dirección que ubica el offset en memoria en el rango de direcciones del área de imagen de la videoram ($4000 a $57FF).
Los 5 bytes menos significativos de la dirección son siempre cero en la tabla. En realidad, representan a la columna (posición c de carácter dentro de los 32 bytes de datos horizontales) pero al estar calculando direcciones de inicio de línea (c = 0 = 00000b), en nuestro caso son siempre cero.
Así pues, podemos componer la dirección en memoria del pixel (0,0) de un carácter (0,f) de la pantalla como:
Cálculo de la dirección del scanline 0 de (0,f)
A partir del anterior diagrama, se desarrollan las siguientes subrutinas:
Get_Line_Offset
Esta rutina devuelve en HL la dirección de memoria de una fila en baja resolución. Esa dirección apunta a los 8 píxeles (0-7) del scanline 0 de la fila solicitada.
; Get_Line_Offset_LR(f)
; Obtener la direccion de memoria de inicio de la fila f.
; Entrada:   B = FILA
; Salida:   HL = Direccion de memoria del caracter (0,f)
Get_Line_Offset_LR:
LD A, B         ; A = B, para extraer los bits de tercio
AND $18         ; A = A AND 00011000b
; A = estado de bits de TERCIO desde FILA
ADD A, $40      ; Sumamos $40 (2 bits superiores = 010)
LD H, A         ; Ya tenemos la parte alta calculada
; H = 010TT000
LD A, B         ; Ahora calculamos la parte baja
AND 7           ; Nos quedamos con los bits más bajos de FILA
; que coinciden con FT (Fila dentro del tercio)
RRCA            ; Ahora A = 00000NNNb  (donde N=FT)
RRCA            ; Desplazamos A 3 veces
RRCA            ; A = NNN00000b
LD L, A         ; Lo cargamos en la parte baja de la direccion
RET             ; HL = 010TT000NNN00000b
Get_Char_Offset
Como ya sabemos, la posición horizontal de un pixel dentro de una fila sí que es lineal, a razón de 8 píxeles por columna, por lo que:
OFFSET(c,f) = Direccion_Inicio(f) + c
OFFSET(x,y) = Direccion_Inicio(y/8) + (x/8)
Así, una vez calculado el inicio de línea, basta sumar la columna para obtener la dirección de memoria del scanline 0 del carácter en baja resolución (c,f):
Cálculo de la dirección del scanline 0 de (c,f)
El código con la columna añadida quedaría así:
; Get_Char_Offset_LR(c,f)
; Obtener la direccion de memoria del caracter (c,f) indicado.
; Salida:   HL = Direccion de memoria del caracter (c,f)
Get_Char_Offset_LR:
RRCA            ; Ahora A = 00000NNNb     (N=FT)
RRCA            ; Desplazamos A 3 veces a la derecha
ADD A, C        ; Sumamos COLUMNA -> A = NNNCCCCCb
RET             ; HL = 010TT000NNNCCCCCb
Una rutina que deba trabajar con direcciones en alta resolución pero que devuelva el offset del inicio del bloque que contiene el punto (x,y) deberá dividir B y C entre 8 en el punto de entrada de la rutina:
; Get_Char_Offset_HR(x,y)
; Obtener la direccion de memoria del caracter que contiene
; el pixel (x,y) indicado.
; Salida:   HL = Direccion de memoria del caracter con (x,y)
Get_Char_Offset_HR:
SRL B           ; B = B/8 -> Ahora B es FILA
SRL C           ; C = C/8 -> Ahora C es COLUMNA
(...)           ; Resto de la rutina Get_Char_Offset_LR
Nuestra siguiente subrutina tiene como objetivo el calcular la posición (c,f) en baja resolución de un carácter dado un offset en memoria que almacene alguno de los 64 pixeles del mismo. Llamar a esta función con la dirección de cualquiera de las 8 líneas de un carácter devolvería el mismo par de coordenadas (c,f):
; Get_Char_Coordinates_LR(offset)
; Obtener las coordenadas (c,f) que corresponden a una
; direccion de memoria de imagen en baja resolucion.
; Entrada:   HL = Direccion de memoria del caracter (c,f)
; Salida:    B = FILA, C = COLUMNA
Get_Char_Coordinates_LR:
; HL = 010TT000 NNNCCCCCb ->
;    Fila = 000TTNNNb y Columna = 000CCCCCb
; Calculo de la fila:
LD A, H         ; A = H, para extraer los bits de tercio
AND $18         ; A = 000TT000b
LD B, A         ; B = A = 000TT000b
LD A, L         ; A = L, para extraer los bits de N (FT)
AND $E0         ; A = A AND 11100000b = NNN00000b
RLC A           ; Rotamos A 3 veces a la izquierda
RLC A           ; A = 00000NNNb
OR B            ; A = A OR B = 000TTNNNb
LD B, A         ; B = A = 000TTNNNb
; Calculo de la columna:
LD A, L         ; A = L, para extraer los bits de columna
AND $1F         ; Nos quedamos con los ultimos 5 bits de L
LD C, A         ; C = Columna
Adaptar esta rutina a alta resolución (Get_Char_Coordinates_HR(x,y)) implicaría el multiplicar las coordenadas X e Y por 8, añadiendo el siguiente código inmediatamente antes del RET:
Si no queremos tener una rutina específica para esta operación, podemos llamar a la rutina en baja resolución y realizar los desplazamientos (*8) a la salida de la misma.
Recorrer los 8 scanlines de un bloque
Dada en HL la dirección del primer scanline de un bloque, podemos avanzar a lo largo de los 7 scanlines del mismo bloque sumando “256” a dicha dirección. Como sumar 256 equivale a incrementar la parte alta de la dirección, podemos subir y bajar al scanline anterior y siguiente de los 8 que componen el carácter mediante simples DEC H e INC H:
Scanline_Arriba_HL:
DEC H            ; H = H - 1  (HL = HL-255)
Scanline_Abajo_HL:
INC H            ; H = H + 1  (HL = HL-255)
Este salto de 256 bytes será válido sólo dentro de los 8 scanlines de un mismo carácter.
Offset del carácter de la izquierda/derecha/arriba/abajo
Dentro de las rutinas de impresión de sprites de más de un carácter es probable que necesitemos movernos a los carácteres de alrededor de uno dado (normalmente hacia la derecha y hacia abajo).
Las siguientes rutinas no realizan control de la posición, por lo que moverse en una dirección cuando estamos en el límite del eje vertical u horizontal tendrá resultados diferentes de los esperados.
Moverse un carácter a derecha o izquierda es sencillo dada la disposición lineal de las filas de caracteres. Estando en el scanline 0 de un carácter, bastará con incrementar o decrementar la posición de memoria actual:
Caracter_Derecha_HL:
Caracter_Izquierda_HL:
Moverse un carácter arriba o abajo es más laborioso ya que tenemos que tener en cuenta los cambios de tercios. Para ello, basta con que recordemos la disposición de los bits de la dirección:
Dirección VRAM
Bits de Tercio
Bits de scanline
Bits de Carácter-Y
Bits de Columna
HL = 010 TT SSS NNN CCCCC
Así, para saltar al siguiente carácter tenemos que incrementar los 3 bits más altos de L (sumando 32). Esto provocará el avance de bloque en bloque, pero debemos tener en cuenta el momento en que realizamos un salto del bloque 7 al 8, y del 15 al 16, ya que entonces tenemos que cambiar de tercio y poner NNN a 0.
Podemos detectar fácilmente el paso de la fila 7 a la 8 y de la 15 a la 16 ya que en ambos casos la “Fila dentro del Tercio” (NNN, bits 7, 6 y 5 de la dirección) pasaría de 111b a 1000b, lo que provocaría que estos 3 bits se quedaran a 0 y se activara el bit de CARRY.
Es decir, cuando tenemos TT = 00b y NNN = 111b y queremos avanzar al siguiente scanline, sumamos 32 (00100000b) con lo que provocamos NNN = 000b y Carry=1. Teniendo la variable “Fila dentro del Tercio” a 1, basta con que incrementemos TT sumando 00001000b (8) a la parte alta, lo que sumaría 01b a los 2 bits de tercio TT:
LD A, L                     ; Cargamos A en L y le sumamos 32 para
ADD A, 32                   ; incrementar "Bloque dentro del tercio"
LD L, A                     ; L = A
JR NC, no_ajustar_H_abajob  ; Si esta suma produce acarreo, ajustar
LD A, H                     ; la parte alta sumando 8 a H (TT = TT + 1).
ADD A, 8                    ; Ahora NNN=000b y TT se ha incrementado.
LD H, A                     ; H = A
no_ajustar_H_abajob
; Ahora HL apunta al bloque de debajo.
El procedimiento para subir un carácter es similar:
LD A, L                       ; Cargamos L en A
AND 224                       ; A = A AND 11100000b
JR NZ, no_ajustar_h_arribab   ; Si no es cero, no retrocedemos tercio
LD A, H                       ; Si es cero, ajustamos tercio (-1)
SUB 8                         ; Decrementamos TT
no_ajustar_h_arribab:
LD A, L                       ; Decrementar NNN
LD L, A                       ; NNN = NNN-1
En forma de rutina:
Caracter_Abajo_HL:
RET NC                      ; Si esta suma no produce acarreo, fin
Caracter_Arriba_HL:
JR NZ, nofix_h_arribab        ; Si no es cero, no retrocedemos tercio
nofix_h_arribab:
Con estas 2 subrutinas podemos subir y bajar carácter a carácter sin tener que recalcular la dirección destino y haciendo uso sólo de A, H y L. Hay que tener en cuenta, no obstante, que se no comprueban los límites de la pantalla, por lo que no nos avisarán si pretendemos “subir” más arriba de la línea 0 o “bajar” más abajo de la 23.
Finalmente, en cuanto a coordenación, vamos a estudiar el cálculo de la dirección de memoria de un pixel (x,y) en alta resolución. La dirección en memoria obtenida tendrá la información gráfica de 8 píxeles (pues cada byte almacena el estado de 8 píxeles horizontales consecutivos). Debido a esto nuestra rutina no sólo deberá devolver el offset en memoria sino un valor de posición relativa 0-7 que nos permita alterar el pixel concreto solicitado.
Nuestra rutina de cálculo de offset puede ser implementada mediante 2 aproximaciones:
Mediante cálculo de la posición de memoria a partir de las coordenadas (x,y), utilizando operaciones de descomposición y rotación de bits, como ya hemos visto en los apartados anteriores.
Mediante una tabla precalculada de posiciones de memoria que almacene la dirección de inicio de cada línea de pantalla, a la cual sumaremos el número de columna (x/8), obteniendo así el offset de nuestro pixel.
Vamos a ver las 2 técnicas por separado con rutinas aplicadas a cada uno de los métodos. Cada sistema, como veremos, tiene sus ventajas e inconvenientes, resultando siempre ambos un balance entre el tiempo de ejecución de una rutina y la ocupación en bytes en memoria entre código y datos de la misma.
Get_Pixel_Offset(x,y) : Dadas las coordenadas en alta resolución (x,y) de un pixel, debe devolver la dirección de memoria que aloja el pixel y un indicador de la posición del pixel dentro de dicho byte (recordemos que cada dirección de memoria contiene los datos de 8 píxeles lineales consecutivos), utilizando descomposición y composición de bits.
Get_Pixel_Coordinates(offset): Dada una dirección de memoria dentro del área de imagen, debe devolver las coordenadas (x,y) del pixel al que está asociada.
Get_Pixel_Offset_LUT(x,y) : Dadas las coordenadas en alta resolución (x,y) de un pixel, debe devolver la dirección de memoria que aloja dicho pixel mediante la utilización de tablas de precálculo (Look Up Table, o LUT).
Hasta ahora hemos visto rutinas que nos proporcionan la posición en memoria de un bloque en baja resolución, pero en el caso que veremos ahora tenemos una coordenada Y que se mueve de 0 a 191, por lo que la posición en memoria puede corresponder a cualquiera de los 8 scanlines de un bloque dado. Además, la coordenada X tampoco es un carácter por lo que el pixel resultante es el estado de un bit concreto de la dirección obtenida.
Así pues, ¿cómo podemos calcular la dirección destino del pixel cuando tratamos con coordenadas en alta resolución? Recuperemos para ello parte de nuestra tabla de direcciones de memoria en baja resolución:
Línea LowRes
Línea HiRes
Fila en el tercio
0 0 $4000 16384 0100000000000000b 0 (00b) 0
1 8 $4020 16416 0100000000100000b 0 (00b) 1
2 16 $4040 16448 0100000001000000b 0 (00b) 2
3 24 $4060 16480 0100000001100000b 0 (00b) 3
Añadamos ahora las direcciones en alta resolución y veamos el estado de los diferentes bits de la coordenada Y y de la dirección de videomemoria que le corresponde:
Coord. F
Coord. Y (Binario)
Direccion (0,y) (HEX)
0 0 00000000b $4100 0100000000000000b 0 (00b) 0
0 1 00000001b $4200 0100000100000000b 0 (00b) 0
0 2 00000010b $4300 0100001000000000b 0 (00b) 0
0 3 00000011b $4400 0100001100000000b 0 (00b) 0
0 4 00000100b $4500 0100010000000000b 0 (00b) 0
0 5 00000101b $4600 0100010100000000b 0 (00b) 0
0 6 00000110b $4700 0100011000000000b 0 (00b) 0
0 7 00000111b $4800 0100011100000000b 0 (00b) 0
1 8 00001000b $4020 0100000000100000b 0 (00b) 1
1 9 00001001b $4120 0100000100100000b 0 (00b) 1
1 10 00001010b $4220 0100001000100000b 0 (00b) 1
1 11 00001011b $4320 0100001100100000b 0 (00b) 1
1 12 00001100b $4420 0100010000100000b 0 (00b) 1
Como puede verse, la diferencia entre la composición de baja resolución y la de alta resolución es la modificación de los 3 bits menos significativos de la parte alta de la dirección, que son un reflejo de los 3 bits bajos de la coordenada Y.
Si examinamos en binario la coordenada Y, vemos que ésta se puede descomponer en 2 bits de Tercio de Pantalla, 3 bits de Fila Dentro del Tercio (FT o N en los ejemplos) y 3 bits de Scanline Dentro Del Carácter (S):
Por otra parte, ya sabemos que C es X / 8, por lo que ya tenemos todos los componentes para realizar nuestra rutina de cálculo de dirección de memoria.
Así pues, la composición final de la dirección de memoria del pixel (x,y) se define de la siguiente forma:
Cálculo de la dirección del pixel (x,y)
No obstante, recordemos que esta dirección de memoria obtenida hace referencia a 8 píxeles, por lo que necesitamos obtener además la información del número de bit con el que se corresponde nuestro pixel, que podemos extraer del resto de la división entre 8 de la coordenada X (P = X AND 7).
La rutina resultante es similar a la vista en baja resolución con la descomposición de la coordenada Y en el “número de scanline” (0-7) y la “fila dentro del tercio (0-7)”:
; Get_Pixel_Offset_HR(x,y)
; Obtener la direccion de memoria del pixel (x,y).
;            A = Posicion del pixel (0-7) en el byte.
Get_Pixel_Offset_HR:
; Calculo de la parte alta de la direccion:
AND 7                       ; A = 00000SSSb
LD H, A                     ; Lo guardamos en H
LD A, B                     ; Recuperamos de nuevo Y
RRA                         ; Rotamos para asi obtener el tercio
AND 24                      ; con un AND 00011000b -> 000TT000b
OR H                        ; H = H OR A = 00000SSSb OR 000TT000b
OR 64                       ; Mezclamos H con 01000000b (vram)
LD H, A                     ; Establecemos el "H" definitivo
; Calculo de la parte baja de la direccion:
LD A, C                     ; A = coordenada X
RRA                         ; Rotamos para obtener CCCCCb
AND 31                      ; A = A AND 31 = 000CCCCCb
LD L, A                     ; L = 000CCCCCb
RLA                         ; Rotamos para obtener NNN
AND 224                     ; A = A AND 11100000b
OR L                        ; L = NNNCCCCC
LD L, A                     ; Establecemos el "L" definitivo
; Finalmente, calcular posicion relativa del pixel:
LD A, C                      ; Recuperamos la coordenada X
AND 7                        ; AND 00000111 para obtener pixel
; A = 00000PPP
Esta rutina de 118 t-estados nos devuelve el valor de la dirección calculado en HL y la posición relativa del pixel dentro del byte:
Posición del pixel
desde la izquierda +7 +6 +5 +4 +3 +2 +1 +0
dentro del byte (Bit) 0 1 2 3 4 5 6 7
Esta posición relativa del pixel nos sirve para 2 cosas:
Por una parte, cuando realicemos rutinas de impresión de sprites con movimiento pixel a pixel, este valor nos puede servir para tratar los sprites (rotarlos) de cara a su impresión en posiciones de byte.
Por otra parte, si necesitamos activar (PLOT, bit=1), desactivar (UNPLOT, b=0) o testear el estado del pixel (x,y), podremos utilizar este valor “posición del pixel” para generar una máscara de pixel.
Nuestra rutina de coordenación nos devuelve en HL la dirección de memoria que contiene el pixel (x,y) y en A la posición relativa del pixel dentro de dicha dirección.
Para poder modificar el pixel exacto al que hacen referencia la pareja de datos HL y A resulta necesario convertir A en una “máscara de pixel” que nos permita manipular la memoria con sencillas operaciones lógicas sin afectar al estado de los demás píxeles.
Esta “máscara de pixel” tiene activo el bit 8-pixel ya que el pixel 0 es el pixel de más a la izquierda de los 8, es decir, el bit 7 de la dirección:
Pixel 0 1 2 3 4 5 6 7
La máscara que debemos generar, en función del valor de A, es:
Máscara de pixel
0 10000000b
1 01000000b
2 00100000b
3 00010000b
4 00001000b
5 00000100b
6 00000010b
7 00000001b
La porción de código que hace esta conversión es la siguiente:
LD B, A         ; Cargamos A (posicion de pixel) en B
INC B           ; Incrementamos B (para pasadas del bucle)
XOR A           ; A = 0
SCF             ; Set Carry Flag (A=0, CF=1)
pix_rotate_bit:
RRA             ; Rotamos A a la derecha B veces
DJNZ pix_rotate_bit
La rutina pone A a cero y establece el Carry Flag a 1, por lo que la primera ejecución de RRA (que siempre se realizará) ubica el 1 del CF en el bit 7 de A. A continuación el DJNZ que se realiza “B” veces mueve ese bit a 1 a la derecha (también “B” veces) dejando A con el valor adecuado según la tabla que acabamos de ver.
En formato de rutina:
: Relative_to_Mask: Convierte una posicion de pixel
;                   relativa en una mascara de pixel.
; IN:  A = Valor relativo del pixel (0,7)
; OUT: A = Pixel Mask (128-1)
; CHANGES: B, F
Relative_to_Mask:
Mediante esa máscara podemos activar (PLOT), desactivar (UNPLOT) y testear (TEST) el estado del pixel en cuestión:
; Activar el pixel apuntado por HL usando la máscara A
Plot_Pixel_HL:
; Desactivar el pixel apuntado por HL usando la máscara A
Unplot_Pixel_HL:
; Testear el pixel apuntado por HL usando la máscara A
Test_Pixel_HL:
La anterior rutina de PLOT funciona realizando un OR entre la máscara de pixel y el estado de actual de la memoria, y luego escribiendo el resultado de dicho OR en la videoram. De esta forma, sólo alteramos el pixel sobre el que queremos escribir.
Explicándolo con un ejemplo, supongamos que queremos escribir en el pixel (3,0) de la pantalla y ya hay píxeles activos en (0,0) y (7,0):
Pixeles activos en (16384) = 10000001
Máscara de pixel           = 00010000
Si ejecutaramos un simple “LD (HL), A”, el resultado de la operación eliminaría los 2 píxeles activos que ya teníamos en memoria:
Máscara de pixel A         = 00010000
OPERACION (HL)=A           = LD (HL), A
Resultado en (16384)       = 00010000
Mediante el OR entre la máscara de pixel y la videomemoria conseguimos alterar el estado de (3,0) sin modificar los píxeles ya existentes:
OPERACION A = A OR (HL)    = OR (HL)
Resultado en A             = 10010001
Resultado en (16384)       = 10010001
Si en lugar de un OR hubieramos complementado A y hubieramos hecho un AND, habríamos puesto a 0 el bit (y por tanto el pixel):
OPERACION A = CPL(A)       = 11101111
OPERACION A = A OR (HL)    = AND (HL)
Resultado en A             = 10000001
Resultado en (16384)       = 10000001
El cálculo de memoria y la escritura de un pixel quedaría pues de la siguiente forma:
LD C, 127                   ; X = 127
LD B, 95                    ; Y = 95
CALL Get_Pixel_Offset_HR    ; Calculamos HL y A
OR (HL)                     ; OR de A y (HL)
LD (HL), A                  ; Activamos pixel
La primera pregunta que nos planteamos es, si es imprescindible disponer de una máscara de pixel para dibujar o borrar píxeles, ¿por qué no incluir este código de rotación de A directamente en la rutina de coordenación? La respuesta es, “depende de para qué vayamos a utilizar la rutina”.
Si la rutina va a ser utilizada principalmente para trazar píxeles, resultará conveniente incorporar al final de Get_Pixel_Offset_HR() el cálculo de la máscara, y devolver en A dicha máscara en lugar de la posición relativa del pixel.
Pero lo normal en el desarrollo de programas y juegos es que utilicemos la rutina de coordenación para obtener la posición inicial en la que comenzar a trazar sprites, bloques (del mapeado), fuentes de texto, marcadores. En ese caso es absurdo emplear “ciclos de reloj” adicionales para el cálculo de una máscara que sólo se utiliza en el trazado de puntos. En esas circunstancias resulta mucho más útil disponer de la posición relativa del pixel, para, como ya hemos comentado, conocer la cantidad de bits que necesitamos rotar estos datos gráficos antes de su trazado.
Por ese motivo, no hemos agregado esta pequeña porción de código a la rutina de Get_Pixel_Offset, siendo el programador quien debe decidir en qué formato quiere obtener la salida de la rutina.
Curiosamente, los usuarios de Spectrum tenemos disponible en la memoria ROM una rutina parecida, llamada PIXEL-ADDRESS (o PIXEL-ADD), utilizada por las rutinas POINT y PLOT de la ROM (y de BASIC). La rutina está ubicada en $22AA y su código es el siguiente:
; THE 'PIXEL ADDRESS' SUBROUTINE
; This subroutine is called by the POINT subroutine and by the PLOT
; command routine. Is is entered with the co-ordinates of a pixel in
; the BC register pair and returns with HL holding the address of the
; display file byte which contains that pixel and A pointing to the
; position of the pixel within the byte.
; IN: (C,B) = (X,Y)
; OUT: HL = address, A = pixel relative position in (HL)
$22AA PIXEL-ADD
LD    A,$AF               ; Test that the y co-ordinate (in
SUB   B                   ; B) is not greater than 175.
LD    B,A                 ; B now contains 175 minus y.
$22B1 PIXEL_ADDRESS_B:        ; Entramos aqui para saltarnos la limitacion
; hacia las 2 ultimas lineas de pantalla.
AND   A                   ; A holds b7b6b5b4b3b2b1b0,
RRA                       ; the bite of B. And now
; 0b7b6b5b4b3b2b1.
RRA                       ; Now 10b7b6b5b4b3b2.
RRA                       ; Now 010b7b6b5b4b3.
AND   $F8                 ; Finally 010b7b6b2b1b0, so that
XOR   B                   ; H becomes 64 + 8*INT (B/64) +
LD    H,A                 ; B (mod 8), the high byte of the
LD    A,C                 ; pixel address. C contains X.
RLCA                      ; A starts as c7c6c5c4c3c2c1c0.
RLCA                      ; And is now c2c1c0c7c6c5c4c3.
AND   $C7
XOR   B                   ; Now c2c1b5b4b3c5c4c3.
RLCA                      ; Finally b5b4b3c7c6c5c4c3, so
LD    L,A                 ; that L becomes 32*INT (B(mod
LD    A,C                 ; 64)/8) + INT(x/8), the low byte.
AND   $07                 ; A holds x(mod 8): so the pixel
RET                       ; is bit (A - 7) within the byte.
Esta rutina tiene una serie de ventajas: Entrando por $22B1 tenemos 23 instrucciones (107 t-estados) que realizan el cálculo de la dirección de memoria además de la posición del pixel dentro del byte al que apunta dicha dirección. La rutina está ubicada en ROM, por lo que ahorramos esta pequeña porción de espacio en nuestro programa. Además, no usa la pila, no usa registros adicionales a B, C, HL y A, y no altera los valores de B y C durante el cálculo.
Nótese que aunque la rutina está ubicada en $22AA y se entra con los valores (x,y) en C y B, el principio de la rutina está diseñado para evitar que PLOT y POINT puedan acceder a las 2 últimas filas (16 últimos píxeles) de la pantalla. Para saltarnos esta limitación entramos saltando con un CALL a $22B1 con la coordenada X en el registro C y la coordenada Y en los registros A y B:
LD A, (coord_x)
LD A, (coord_y)
CALL $22B1
De esta forma no sólo nos saltamos la limitación de acceso a las 2 últimas líneas de la pantalla sino que podemos especificar las coordenadas empezando (0,0) en la esquina superior izquierda, con el sistema tradicional de coordenadas, en contraposición al PLOT de BASIC (y de la ROM), donde se comienza a contar la altura como Y = 0 en la parte inferior de la pantalla (empezando en 191-16=175).
Veamos un ejemplo de uso de la rutina de coordenación de la ROM:
; Ejemplo de uso de pixel-address (ROM)
PIXEL_ADDRESS EQU $22B1
; Imprimimos un solo pixel en (0,0)
LD C, 0                ; X = 0
LD B, 0                ; Y = 0
LD A, B                ; A = Y = 0
CALL PIXEL_ADDRESS     ; HL = direccion (0,0)
LD A, 128              ; A = 10000000b (1 pixel).
LD (HL), A             ; Imprimimos el pixel
; Imprimimos 8 pixeles en (255,191)
LD C, 255              ; X = 255
LD B, 191              ; Y = 191
LD A, B                ; A = Y = 191
CALL PIXEL_ADDRESS
LD A, 255              ; A = 11111111b (8 pixeles)
; Imprimimos 4 pixeles en el centro de la pantalla
LD C, 127              ; X = 127
LD B, 95               ; Y = 95
LD A, B                ; A = Y = 95
LD A, 170              ; A = 10101010b (4 pixeles)
loop:                    ; Bucle para no volver a BASIC y que
jr loop                ; no se borren la 2 ultimas lineas
La ejecución del anterior programa nos dejará la siguiente información gráfica en pantalla:
Nótese que la rutina de la ROM nos devuelve en A la posición relativa del pixel cuyas coordenadas hemos proporcionado, por lo que podemos convertir A en una máscara de pixel a la salida de la rutina encapsulando PIXEL-ADDRESS en una rutina “propia” que haga ambas operaciones, a cambio de 2 instrucciones extras (un CALL y un RET adicionales):
; Rutina que encapsula a PIXEL_ADDRESS calculando pix-mask.
; OUT: HL = address, A = pixel mask
PIXEL_ADDRESS_MASK:
CALL PIXEL_ADDRESS   ; Llamamos a la rutina de la ROM
LD B, A              ; Cargamos A (posicion de pixel) en B
INC B                ; Incrementamos B (para pasadas del bucle)
XOR A                ; A = 0
SCF                  ; Set Carry Flag (A=0, CF=1)
RRA                  ; Rotamos A a la derecha B veces
Hasta ahora hemos visto cómo calcular la dirección de memoria de un pixel (x,y) mediante descomposición de las coordenadas y composición de la dirección destino utilizando operaciones lógicas y de desplazamiento.
La alternativa a este método es la utilización de una Look Up Table (LUT), una tabla de valores precalculados mediante la cual obtener la dirección destino dada una variable concreta.
En nuestro caso, crearíamos una Lookup Table (LUT) que se indexaría mediante la coordenada Y, de tal modo que la dirección destino de un pixel X,Y sería:
DIRECCION_DESTINO  = Tabla_Offsets_Linea[Y] + (X/8)
PIXEL_EN_DIRECCION = Resto(X/8) = X AND 7
La tabla de offsets de cada inicio de línea tendría 192 elementos de 2 bytes (tamaño de una dirección), por lo que ocuparía en memoria 384 bytes. A cambio de esta “elevada” ocupación en memoria, podemos obtener rutinas más rápidas que las de composición de las coordenadas.
A continuación se muestra la tabla de offsets precalculados:
Scanline_Offsets:
DW 16384, 16640, 16896, 17152, 17408, 17664, 17920, 18176
DW 16416, 16672, 16928, 17184, 17440, 17696, 17952, 18208
DW 16448, 16704, 16960, 17216, 17472, 17728, 17984, 18240
DW 16480, 16736, 16992, 17248, 17504, 17760, 18016, 18272
DW 16512, 16768, 17024, 17280, 17536, 17792, 18048, 18304
DW 16544, 16800, 17056, 17312, 17568, 17824, 18080, 18336
DW 16576, 16832, 17088, 17344, 17600, 17856, 18112, 18368
DW 16608, 16864, 17120, 17376, 17632, 17888, 18144, 18400
DW 18432, 18688, 18944, 19200, 19456, 19712, 19968, 20224
DW 18464, 18720, 18976, 19232, 19488, 19744, 20000, 20256
DW 18496, 18752, 19008, 19264, 19520, 19776, 20032, 20288
DW 18528, 18784, 19040, 19296, 19552, 19808, 20064, 20320
DW 18560, 18816, 19072, 19328, 19584, 19840, 20096, 20352
DW 18592, 18848, 19104, 19360, 19616, 19872, 20128, 20384
DW 18624, 18880, 19136, 19392, 19648, 19904, 20160, 20416
DW 18656, 18912, 19168, 19424, 19680, 19936, 20192, 20448
DW 20480, 20736, 20992, 21248, 21504, 21760, 22016, 22272
DW 20512, 20768, 21024, 21280, 21536, 21792, 22048, 22304
DW 20544, 20800, 21056, 21312, 21568, 21824, 22080, 22336
DW 20576, 20832, 21088, 21344, 21600, 21856, 22112, 22368
DW 20608, 20864, 21120, 21376, 21632, 21888, 22144, 22400
DW 20640, 20896, 21152, 21408, 21664, 21920, 22176, 22432
DW 20672, 20928, 21184, 21440, 21696, 21952, 22208, 22464
DW 20704, 20960, 21216, 21472, 21728, 21984, 22240, 22496
En hexadecimal (para ver la relación entre los aumentos de líneas y el de scanlines y bloques):
DW $4000, $4100, $4200, $4300, $4400, $4500, $4600, $4700
DW $4020, $4120, $4220, $4320, $4420, $4520, $4620, $4720
DW $4040, $4140, $4240, $4340, $4440, $4540, $4640, $4740
DW $4060, $4160, $4260, $4360, $4460, $4560, $4660, $4760
DW $4080, $4180, $4280, $4380, $4480, $4580, $4680, $4780
DW $40A0, $41A0, $42A0, $43A0, $44A0, $45A0, $46A0, $47A0
DW $40C0, $41C0, $42C0, $43C0, $44C0, $45C0, $46C0, $47C0
DW $40E0, $41E0, $42E0, $43E0, $44E0, $45E0, $46E0, $47E0
DW $4800, $4900, $4A00, $4B00, $4C00, $4D00, $4E00, $4F00
DW $4820, $4920, $4A20, $4B20, $4C20, $4D20, $4E20, $4F20
DW $4840, $4940, $4A40, $4B40, $4C40, $4D40, $4E40, $4F40
DW $4860, $4960, $4A60, $4B60, $4C60, $4D60, $4E60, $4F60
DW $4880, $4980, $4A80, $4B80, $4C80, $4D80, $4E80, $4F80
DW $48A0, $49A0, $4AA0, $4BA0, $4CA0, $4DA0, $4EA0, $4FA0
DW $48C0, $49C0, $4AC0, $4BC0, $4CC0, $4DC0, $4EC0, $4FC0
DW $48E0, $49E0, $4AE0, $4BE0, $4CE0, $4DE0, $4EE0, $4FE0
DW $5000, $5100, $5200, $5300, $5400, $5500, $5600, $5700
DW $5020, $5120, $5220, $5320, $5420, $5520, $5620, $5720
DW $5040, $5140, $5240, $5340, $5440, $5540, $5640, $5740
DW $5060, $5160, $5260, $5360, $5460, $5560, $5660, $5760
DW $5080, $5180, $5280, $5380, $5480, $5580, $5680, $5780
DW $50A0, $51A0, $52A0, $53A0, $54A0, $55A0, $56A0, $57A0
DW $50C0, $51C0, $52C0, $53C0, $54C0, $55C0, $56C0, $57C0
DW $50E0, $51E0, $52E0, $53E0, $54E0, $55E0, $56E0, $57E0
La tabla ha sido generada mediante el siguiente script en python:
$ cat specrows.py
print "Scanline_Offsets:"
for tercio in range(0,3):
for caracter in range(0,8):
print "  DW",
for scanline in range(0,8):
# Componer direccion como 010TTSSSNNN00000
base = 16384
direccion = base + (tercio * 2048)
direccion = direccion + (scanline * 256)
direccion = direccion + (caracter * 32)
print str(direccion),
if scanline!=7: print ",",
La tabla de valores DW estaría incorporada en nuestro programa y por tanto pasaría a formar parte del “binario final”, incluyendo en este aspecto la necesidad de carga desde cinta.
Si por algún motivo no queremos incluir la tabla en el listado, podemos generarla en el arranque de nuestro programa en alguna posición de memoria libre o designada a tal efecto mediante la siguiente rutina:
; Generar LookUp Table de scanlines en memoria.
; Rutina por Derek M. Smith (2005).
Scanline_Offsets EQU $F900
Generate_Scanline_Table:
LD HL, Scanline_Offsets
genscan_loop:
LD (HL), D           ; Guardamos en (HL) (tabla)
INC HL               ; el valor de DE (offset)
; Recorremos los scanlines y bloques en un bucle generando las
; sucesivas direccione en DE para almacenarlas en la tabla.
; Cuando se cambia de caracter, scanline o tercio, se ajusta:
JR NZ, genscan_nextline
JR C, genscan_nextline
genscan_nextline:
DJNZ genscan_loop
La anterior rutina ubicará en memoria una tabla con el mismo contenido que las ya vistas en formato DW.
Get_Pixel_Offset_LUT
Una vez tenemos generada la tabla (ya sea en memoria o pregenerada en el código de nuestro programa), podemos indexar dicha tabla mediante la coordenada Y y sumar la coordenada X convertida en columna para obtener la dirección de memoria del pixel solicitado, y la posición relativa del mismo en el byte:
; Get_Pixel_Offset_LUT_HR(x,y):
; Salida:   HL = Direccion de memoria del pixel (x,y)
;            A = Posicion relativa del pixel en el byte
Get_Pixel_Offset_LUT_HR:
LD DE, Scanline_Offsets   ; Direccion de nuestra LUT
LD L, B                   ; L = Y
ADD HL, HL                ; HL = HL * 2 = Y * 2
ADD HL, DE                ; HL = (Y*2) + ScanLine_Offset
; Ahora Offset = [HL]
LD A, (HL)                ; Cogemos el valor bajo de la direccion en A
LD H, (HL)                ; Cogemos el valor alto de la direccion en H
LD L, A                   ; HL es ahora Direccion(0,Y)
; Ahora sumamos la X, para lo cual calculamos CCCCC
LD A, C                   ; Calculamos columna
RRA                       ; A = A>>3 = ???CCCCCb
AND 31                    ; A = 000CCCCB
ADD A, L                  ; HL = HL + C
LD A, C                   ; Recuperamos la coordenada (X)
AND 7                     ; A = Posicion relativa del pixel
RET                       ; HL = direccion destino
Veamos un ejemplo de utilización de las anteriores rutinas:
; Ejemplo de uso de LUT
CALL Generate_Scanline_Table
LD B, 191
loop_draw:
PUSH BC                ; Preservamos B (por el bucle)
LD C, 127              ; X = 127, Y = B
CALL Get_Pixel_Offset_LUT_HR
DJNZ loop_draw
Y su salida en pantalla:
El coste de ejecución de la rutina Get_Pixel_Offset_LUT_HR es de 117 t-estados, demasiado elevada por culpa de las costosas (en términos temporales) instrucciones de 16 bits, sobre todo teniendo en cuenta que hemos empleado 384 bytes de memoria en nuestra tabla.
Una ingeniosa solución a este problema consiste en dividir la tabla de 192 direcciones de 16 bits en 2 tablas de 192 bytes cada una que almacenen la parte alta de la dirección en la primera de las tablas y la parte baja de la dirección en la segunda, de tal forma que:
H = Tabla_Parte_Alta[Y]
L = Tabla_Parte_Baja[Y]
Si realizamos esta división y colocamos en memoria las tablas de forma que estén alineadas en una dirección múltiplo de 256, el mecanismo de acceso a la tabla será mucho más rápido.
La ubicación de las tablas en memoria en una dirección X múltiplo de 256 tendría el siguiente aspecto:
Sea una Direccion_XX divisible por 256:
Direccion_XX hasta Direccion_XX+191
-> Partes bajas de las direcciones de pantalla
Direccion_XX+256 hasta Direccion_XX+447
-> Partes altas de las direcciones de pantalla.
El paso de una tabla a otra se realizará incrementando o decrementando la parte alta del registro de 16 bits (INC H o DEC H), gracias al hecho de que son 2 tablas múltiplos de 256 y consecutivas en memoria.
Veamos primero la rutina para generar la tabla separando las partes altas y bajas y alineando ambas a una dirección múltiplo de 256:
; Generar LookUp Table de scanlines en memoria en 2 tablas.
; En Scanline_Offsets (divisible por 256)
;    -> 192 bytes de las partes bajas de la direccion.
; En Scanline_Offsets + 256 (divisible por 256)
;    -> 192 bytes de las partes altas de la direccion.
Scanline_Offsets EQU 64000        ; Divisible por 256
Generate_Scanline_Table_Aligned:
LD (HL), E        ; Escribimos parte baja
INC H             ; Saltamos a tabla de partes altas
LD (HL), D        ; Escribimos parte alta
DEC H             ; Volvemos a tabla de partes bajas
INC L             ; Siguiente valor
En el ejemplo anterior, tendremos entre 64000 y 64191 los 192 valores bajos de la dirección, y entre 64256 y 64447 los 192 valores altos de la dirección. Entre ambas tablas hay un hueco de 256-192=64 bytes sin usar que debemos saltar para poder alinear la segunda tabla en un múltiplo de 256.
Estos 64 bytes no se utilizan en la rutina de generación ni (como veremos a continuación) en la de cálculo, por lo que podemos aprovecharlos para ubicar variables de nuestro programa, tablas temporales, etc, y así no desperdiciarlos.
Si necesitaramos reservar espacio en nuestro programa para después generar la tabla sobre él, podemos hacerlo mediante las directivas de preprocesado del ensamblador ORG (Origen) y DS (Define Space). Las siguientes líneas (ubicadas al final del fichero de código) reservan en nuestro programa un array de 448 bytes de longitud y tamaño cero alineado en una posición múltiplo de 256:
DS 448, 0
También podemos incluir las 2 tablas “inline” en nuestro programa, y además sin necesidad de conocer la dirección de memoria en que están (por ejemplo, embedidas dentro del código del programa en la siguiente dirección múltiplo de 256 disponible) aprovechando el soporte de macros del ensamblador PASMO:
; Macro de alineacion para PASMO
align   macro value
if $ mod value
ds value - ($ mod value)
LUT_Scanlines_LO:
DB $00, $00, $00, $00, $00, $00, $00, $00, $20, $20, $20, $20
DB $20, $20, $20, $20, $40, $40, $40, $40, $40, $40, $40, $40
DB $60, $60, $60, $60, $60, $60, $60, $60, $80, $80, $80, $80
DB $80, $80, $80, $80, $A0, $A0, $A0, $A0, $A0, $A0, $A0, $A0
DB $C0, $C0, $C0, $C0, $C0, $C0, $C0, $C0, $E0, $E0, $E0, $E0
DB $E0, $E0, $E0, $E0, $00, $00, $00, $00, $00, $00, $00, $00
DB $20, $20, $20, $20, $20, $20, $20, $20, $40, $40, $40, $40
DB $40, $40, $40, $40, $60, $60, $60, $60, $60, $60, $60, $60
DB $80, $80, $80, $80, $80, $80, $80, $80, $A0, $A0, $A0, $A0
DB $A0, $A0, $A0, $A0, $C0, $C0, $C0, $C0, $C0, $C0, $C0, $C0
DB $E0, $E0, $E0, $E0, $E0, $E0, $E0, $E0, $00, $00, $00, $00
DB $00, $00, $00, $00, $20, $20, $20, $20, $20, $20, $20, $20
DB $40, $40, $40, $40, $40, $40, $40, $40, $60, $60, $60, $60
DB $60, $60, $60, $60, $80, $80, $80, $80, $80, $80, $80, $80
DB $A0, $A0, $A0, $A0, $A0, $A0, $A0, $A0, $C0, $C0, $C0, $C0
DB $C0, $C0, $C0, $C0, $E0, $E0, $E0, $E0, $E0, $E0, $E0, $E0
Free_64_Bytes:
DB 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
LUT_Scanlines_HI:
DB $40, $41, $42, $43, $44, $45, $46, $47, $40, $41, $42, $43
DB $44, $45, $46, $47, $40, $41, $42, $43, $44, $45, $46, $47
DB $44, $45, $46, $47, $48, $49, $4A, $4B, $4C, $4D, $4E, $4F
DB $48, $49, $4A, $4B, $4C, $4D, $4E, $4F, $48, $49, $4A, $4B
DB $4C, $4D, $4E, $4F, $48, $49, $4A, $4B, $4C, $4D, $4E, $4F
DB $48, $49, $4A, $4B, $4C, $4D, $4E, $4F, $50, $51, $52, $53
DB $54, $55, $56, $57, $50, $51, $52, $53, $54, $55, $56, $57
DB $50, $51, $52, $53, $54, $55, $56, $57, $50, $51, $52, $53
Finalmente, veamos si ha merecido la pena el cambio a 2 tablas analizando la nueva rutina de cálculo de dirección:
; Get_Pixel_Offset_LUT_2(x,y):
Get_Pixel_Offset_LUT_2:
LD A, C                     ; Ponemos en A la X
RRA                         ; A = ???CCCCC
AND 31                      ; A = 000CCCCCb
LD L, B                     ; B = coordenada Y
LD H, Scanline_Offsets/256  ; Parte alta de la dir de tabla
ADD A, (HL)                 ; A = columna + tabla_baja[linea]
INC H                       ; saltamos a la siguiente tabla
LD H, (HL)                  ; cargamos en H la parte alta
LD L, A                     ; cargamos en L la parte baja
LD A, C                     ; Recuperamos la coordenada (X)
AND 7                       ; A = Posicion relativa del pixel
El coste de ejecución de esta rutina es de 77 t-estados, incluyendo el RET, la conversión de “X” en “Columna” y la obtención de la posición relativa del pixel.
Como casi siempre en código máquina, nos vemos forzados a elegir entre velocidad y tamaño: las rutinas basadas en tablas son generalmente más rápidas al evitar muchos cálculos, pero a cambio necesitamos ubicar en memoria dichas tablas. Por contra, las rutinas basadas en cálculos ocupan mucho menos tamaño en memoria pero requieren más tiempo de ejecución.
Debemos elegir uno u otro sistema en función de las necesidades y requerimientos de nuestro programa: si disponemos de poca memoria libre y el tiempo de cálculo individual es suficiente, optaremos por la rutina de composición. Si, por contra, la cantidad de memoria libre no es un problema y sí que lo es el tiempo de cálculo, usaremos las rutinas basadas en tablas.
Los programadores debemos muchas veces determinar si una rutina es crítica o no según la cantidad de veces que se ejecute en el “bucle principal” y el porcentaje de tiempo que su ejecución supone en el programa.
Por ejemplo, supongamos una rutina de impresión de sprites de 3×3 bloques: aunque el tiempo de dibujado de los sprites en sí de un juego sea crítico, el posicionado en pantalla para cada sprite sólo se realiza una vez (para su esquina superior izquierda) frente a toda la porción de código que debe imprimir los 9 caracteres (9*8 bytes en pantalla) más sus atributos, con sus correspondientes rotaciones si el movimiento es pixel a pixel. El movimiento entre los diferentes bloques del sprite se realiza normalmente de forma diferencial. Probablemente, invertir “tiempo” para optimizar o “memoria” para tener tablas de precalculo sea más aconsejable en el cuerpo de la rutina de sprites o en tablas de sprites pre-rotados que en la coordenación en sí misma.
La diferencia entre rutinas de tablas y de cálculos se resume en la siguiente tabla:
Bytes rutina
Bytes adicionales
Cálculo 118 t-estados 32 Ninguno 32
Tablas 77 t-estados 17 GetOffset + 32 GenLUT 448 (384 si aprovechamos
los 64 entre tablas) 443 - 487 bytes
Los cálculos de las posiciones de píxeles en alta resolución son “costosos” por lo que a la hora de dibujar sprites, líneas, círculos o cualquier otra primitiva gráfica, lo normal es realizar el cálculo de una posición inicial y moverse diferencialmente respecto a la misma.
Para eso se utilizan rutinas de posicionamiento diferencial como las que ya vimos en los atributos o en baja resolución que nos permitan movernos a cualquiera de los 8 píxeles de alrededor de la dirección HL y posición de pixel que estamos considerando.
Offset del pixel de la izquierda/derecha
En el caso de la coordenación por caracteres (baja resolución), nos bastaba con decrementar o incrementar HL para ir al carácter siguiente o anterior. En este caso, debemos tener en cuenta que cada byte contiene 8 píxeles, por lo que se hace necesaria una máscara de pixel para referenciar a uno u otro bit dentro del byte apuntado por HL.
Teniendo una máscara de pixel en A, las rutinas de cálculo del pixel a la izquierda y a la derecha del pixel actual se basarían en la ROTACIÓN de dicha máscara comprobando las situaciones especiales en las 2 situaciones especiales que se pueden presentar:
El pixel se encuentra en 10000000b y queremos acceder al pixel de la izquierda.
El pixel se encuentra en 00000001b y queremos acceder al pixel de la derecha.
En esos casos se podría utilizar el incremento y decremento de la posición de HL:
; HL = Direccion de pantalla base
; A = Mascara de pixeles
Pixel_Izquierda_HL_Mask:
RLC A            ; Rotamos A a la izquierda
RET NC           ; Si no se activa el carry flag, volvemos
DEC L            ; Si se activa, hemos pasado de 10000000b
RET              ; a 00000001b y ya podemos alterar HL
Pixel_Derecha_HL_Mask:
RRC A            ; Rotamos A a la derecha
INC L            ; Si se activa, hemos pasado de 00000001b
RET              ; a 10000000b y ya podemos alterar HL
Son apenas 4 instrucciones, lo que resulta en un cálculo significativamente más rápido que volver a llamar a la rutina de coordenación original.
Nótese cómo en lugar de utilizar DEC HL o INC HL (6 t-estados), realizamos un DEC L o INC L (4 t-estados), ya que dentro de un mismo scanline de pantalla no hay posibilidad de, moviendo a derecha o izquierda, variar el valor del byte alto de la dirección (siempre y cuando no excedamos los límites de la pantalla por la izquierda o por la derecha). De esta forma ahorramos 2 valiosos ciclos de reloj en una operación que suele realizarse en el bucle más interno de las rutinas de impresión de sprites.
Si en lugar de una máscara de pixel tenemos en A la posición relativa (0-7), podemos utilizar el siguiente código:
; A = Posicion relativa del pixel (0=Pixel de la izquierda)
Pixel_Derecha_HL_Rel:
INC A            ; Incrementamos A
AND 7            ; Si A=8 -> A=0
RET NZ           ; Si no es cero, hemos acabado
INC L            ; Si se activa, hemos pasado al byte
RET              ; siguiente -> alterar HL
Pixel_Izquierda_HL_Rel:
DEC A            ; Decrementamos A
RET P            ; Si no hay overflow (A de 0 a 255), fin
AND 7            ; 11111111b -> 00000111b
DEC L            ; Hemos pasado al byte siguiente ->
RET              ; alteramos HL
Recordemos que ninguna de estas rutinas contempla los líneas izquierdo y derecho de la pantalla.
Offset del pixel del scanline de arriba/abajo
Moverse un scanline arriba o abajo requiere código adicional, como ya vimos en el apartado de coordenadas de caracteres, para detectar tanto los cambios de tercios como los cambios de caracteres.
Primero debemos detectar si estamos en el último scanline del carácter, ya que avanzar 1 scanline implicaría poner SSS a 000 e incrementar NNN (carácter dentro del tercio). Al incrementar NNN (carácter dentro del tercio) tenemos que verificar también si NNN pasa de 111 a 1000 lo que supone un cambio de tercio:
El código para incrementar HL hacia el siguiente scanline horizontal detectando los saltos de tercio y de carácter sería el siguiente:
; Avanzamos HL 1 scanline:
INC H                       ; Incrementamos HL en 256 (siguiente scanline)
LD A, H                     ; Cargamos H en A
AND 7                       ; Si despues del INC H los 3 bits son 0,
; es porque era 111b y ahora 1000b.
JR NZ, nofix_abajop         ; Si no es cero, hemos acabado (solo INC H).
LD A, L                     ; Es cero, hemos pasado del scanline 7 de un
; caracter al 0 del siguiente: ajustar NNN
ADD A, 32                   ; Ajustamos NNN (caracter dentro de tercio += 1)
LD L, A                     ; Ahora hacemos la comprobacion de salto de tercio
JR C, nofix_abajop          ; Si esta suma produce acarreo, habria que ajustar
; tercio, pero ya lo hizo el INC H (111b -> 1000b)
LD A, H                     ; Si no produce acarreo, no hay que ajustar
SUB 8                       ; tercio, por lo que restamos el bit TT que sumo
LD H, A                     ; el INC H inicial.
nofix_abajop:
; HL contiene ahora la direccion del siguiente scanline
; ya sea del mismo caracter o el scanline 0 del siguiente.
Y para retroceder HL en 1 scanline, usando la misma técnica:
AND 7                       ; Comprobamos scanline
JR Z, Anterior_SL_DEC       ; Si es cero, hay salto de caracter
DEC H                       ; No es cero, basta HL = HL - 256
RET                         ; Hemos acabado (solo INC H).
Anterior_SL_DEC:               ; Hay que ir de caracter 000b a 111b
DEC H                       ; Decrementamos H
LD A, L                     ; Ajustamos NNN (caracter en tercio -=1)
RET C                       ; Si se produjo carry, no hay que ajustar
LD A, H                     ; Se produjo carry, ajustamos el tercio
ADD A, 8                    ; por el DEC H inicial.
Veamos este mismo código en forma de subrutina, aprovechando con RET la posibilidad de evitar los saltos hacia el final de las rutinas:
; Siguiente_Scanline_HL:
; Obtener la direccion de memoria del siguiente scanline dada
; en HL la direccion del scanline actual, teniendo en cuenta
; saltos de caracter y de tercio.
; Entrada:   HL = Direccion del scanline actual
; Salida:    HL = Direccion del siguiente scanline
Siguiente_Scanline_HL:
RET             ; Devolvemos en HL el valor ajustado
La rutina para retroceder al scanline superior es de similar factura, con una pequeña reorganización del código para evitar el salto con JR:
; Anterior_Scanline_HL:
; Obtener la direccion de memoria del anterior scanline dada
; Salida:    HL = Direccion del anterior scanline
Anterior_Scanline_HL:
Siguiente_Scanline_HL será especialmente útil en el desarrollo de rutinas de impresión de Sprites, aunque lo normal es que incluyamos el código “inline” dentro de dichas rutinas, para ahorrar los ciclos de reloj usandos en un CALL+RET.
En el libro “Lenguaje Máquina Avanzado para ZX Spectrum” de David Webb encontramos 3 rutinas útiles de propósito general para obtener el offset en el área de imagen dada una dirección de atributo o viceversa.
La primera subrutina obtiene la dirección del atributo que corresponde a una dirección de pantalla especificada:
; Attr_Offset_From_Image (DF-ATT):
; Entrada:  HL = Direccion de memoria de imagen.
; Salida:   DE = Direccion de atributo correspondiente a HL.
Attr_Offset_From_Image:
La segunda realiza el proceso inverso: obtiene la dirección del archivo de imagen dada una dirección de atributo. Esta dirección se corresponde con la dirección de los 8 píxeles del primer scanline del carácter.
; Image_Offset_From_Attr (ATT_DF):
; Entrada:  HL = Direccion de memoria de atributo.
; Salida:   DE = Direccion de imagen correspondiente a HL.
Image_Offset_From_Attr:
La tercera es una combinación de localización de offset de imagen, de atributo, y el valor del atributo:
; Get_Char_Data(c,f) (LOCATE):
; Entrada:  B = FILA, C = COLUMNA
; Salida:   DE = Direccion de atributo.
;           HL = Direccion de imagen.
;           A = Atributo de (C,F)
Get_Char_Data:
Llamando a la anterior rutina con unas coordenadas (c,f) en C y B obtenemos la dirección de memoria de imagen (HL) y de atributo (DE) de dicho carácter, así como el valor del atributo en sí mismo (A).
Ejemplo de uso de PIXEL-ADD (ROM)
Ejemplo de uso de Look Up Tables
Cálculo de direcciones de pantalla a partir de coordenadas (por Metalbrain).
Tiempos de ejecución y tamaños de las instrucciones del Z80 (ver z80time.txt).
cursos/ensamblador/gfx2_direccionamiento.txt
Última modificación: 16-11-2010 19:56

References: Resolución 
 resolución 

Resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución