Entonces, a la hora de explicar la extraña organización de la memoria de video del Spectrum, me asaltó una duda.... ¿era ésta de verdad la mejor opción que tenía Altwasser?
La comparé con una supuesta organización lineal, y ahí creo que hay consenso: la organización original del Spectrum permite dibujar caracteres de forma muy rápida: de un scan al siguiente, cuando se pinta un carácter, sólo hay que incrementar el byte más significativo del registro que lleva la dirección de pantalla. Si la organización fuera lineal, habría que sumar una cantidad, no un incremento, y eso implica tener que usar sí o sí el acumulador, en cambio para incrementar un registro de 8 bits, el que sea, no hay que pasar por el acumulador.
Código: Seleccionar todo
;HL = address of first byte of character pattern
;DE = address of first scan of character in video memory
ld b,8
BucScan:
ld a,(hl)
ld (de),a
inc l
inc d ;move to next scan
djnz BucScan
Por otra parte, la rutina de la ROM que calcula la dirección de memoria que contiene a un determinado pixel de coordenadas C,B es tal que así:
Código: Seleccionar todo
;Taken from The Incomplete Spectrum ROM Assembly
;http://www.wearmouth.demon.co.uk/
;INPUT: C=coordinate X, B=coordinate Y.
;OUTPUT: HL=address of byte containing pixel
LD B,A
AND A
RRA
SCF
RRA
AND A
RRA
XOR B
AND $F8
XOR B
LD H,A
LD A,C
RLCA
RLCA
RLCA
XOR B
AND $C7
XOR B
RLCA
RLCA
LD L,A
Pero entonces me acordé de un experimento que hice con el Commodore 64:
El C64, para quien no lo sepa, le diré que tiene un modo de pantalla denominado "HiRes" y que es el que te da los 320x200 puntos, peeeeeeero con una limitación de 2 colores por cada bloque de 8x8 píxeles. ¿Os suena?
No se usa mucho en juegos, porque no permite el pintado rápido de tiles, ni en pixel art, por la limitación antes comentada, pero algunos artistas sí que usan este modo para sus creaciones, bien "a palo seco", o bien ayudándose dando más color en algunas zonas mediante el uso de sprites superpuestos al dibujo.
Lo curioso de este modo de pantalla es la organización de la zona de bitmaps (que como en el Spectrum, está separada de la zona de atributos). Me explico: si en el Spectrum hacemos el típico programa...
Código: Seleccionar todo
10 FOR n=16384 TO 22527: POKE n,255: NEXT n
Obtenemos el también típico patrón de rellenado de pantalla, que es el mismo que se ve cuando se carga una pantalla desde el cassette:
Si el Spectrum tuviera la organización de la memoria de bitmaps como la del C64, el programa anterior se vería así:
Curioso, ¿no? Parece, intuitivamente, que esta organización ayuda aún más que la anterior a pintar caracteres de forma rápida:
- Para pasar de un scan al siguiente, basta incrementar el registro (o su byte menos significativo)
Código: Seleccionar todo
;HL = address of first byte of character pattern
;DE = address of first scan of character in video memory
ld b,8
BucScan:
ld a,(hl)
ld (de),a
inc l
inc e ;Increment lo byte instead of hi byte
djnz BucScan
- Para pasar de un caracter al siguiente, incluso de una línea a la siguiente, no hay que hacer nada especial, ya que una vez pintado un carácter, el registro que apunta a la dirección de pantalla ya está apuntando al principio del siguiente carácter. Esto significa que para pintar cadenas de texto largas no hay que recalcular la dirección de pantalla, acelerándose por tanto esta operación.
Si la ROM se hubiera escrito atendiendo a esta disposición de la pantalla, incluso la rutina que hemos puesto antes, para calcular la dirección de memoria del byte que contiene un pixel de coordenadas C,B se hace más corta y rápida:
Código: Seleccionar todo
;INPUT: C=coordinate X, B=coordinate Y.
;OUTPUT: HL=address of byte containing pixel
ld a,b
and a
rra
scf
rra
and a
rra
ld h,a
ld a,c
and $F8
ld l,a
ld a,b
and $07
or l
ld l,a
Y dado que la ROM usa esta rutina no sólamente para dibujar un punto (PLOT) sino para DRAW (que llama a PLOT) y CIRCLE (que llama a DRAW), resulta que, desde BASIC, esta disposición daría lugar a rutinas algo más rápidas que las que tenemos.
Incluso la rutina que calcula la dirección de inicio de un carácter dada su fila y columna es más sencilla con esta organización:
Código: Seleccionar todo
;INPUT: H : valid character Y coordinate. L : valid character X coordinate
;OUTPUT: HL = address of character in screen
ld a,l
add a,a
add a,a
add a,a
ld l,a
ld a,h
or $40h
ld h,a
ret
Con todo esto, pensé que si Altwasser no usó esta disposición (que además parece más intuitiva que la que después se usó realmente) era porque en hardware resultaba costoso reordenar la pantalla así. Pues no (al menos, no para la zona de bitmaps). Si tenemos X,Y las coordenadas de un pixel (8 bits cada una):
Código: Seleccionar todo
Coordenada X: X7 X6 X5 X4 X3 X2 X1 X0
Coordenada Y: Y7 Y6 Y5 Y4 Y3 Y2 Y1 Y0
Dirección de memoria (organización original del ZX Spectrum)
Y7 Y6 Y2 Y1 Y0 Y5 Y4 Y3 X7 X6 X5 X4 X3
Dirección de memoria (organización estilo C64)
Y7 Y6 Y5 Y4 Y3 X7 X6 X5 X4 X3 Y2 Y1 Y0
Pero es que no acaba ahí la cosa: al usar esta disposición como dirección de fila/columna para direccionar la DRAM, resulta que las 128 filas las recorre en sólo 512us, cuando usando la organización del Spectrum, las recorre en 2048us (=2,048ms). Lo peor es que el tiempo máximo de refresco para las 128 filas es precisamente de 2ms, con lo que tenemos que la organización original del Spectrum está usando la DRAM al límite de su velocidad de refresco (llega a tardar un pelín más y habría serios problemas con la integridad de los datos en memoria)
O sea: resulta que la organización del C64 resulta mejor si tenemos en cuenta los objetivos que se marcaron los diseñadores de Sinclair, respecto de una pantalla orientada a mostrar caracteres de forma rápida (ese, si mal no recuerdo, era su objetivo), ya que:
- Las rutinas que manejan esta disposición requieren menos cálculos, o en el peor de los casos, el mismo número de cálculos que con la organización del ZX.
- En hardware resulta igual de sencillo implementar una u otra.
Al menos esto era lo que yo pensaba. Incluso imaginé que sería algún problema de patentes, o vaya usted a saber..... hasta que me dio por intentar incluir los atributos en esta ecuación.
Y ahí vi mi error: realmente esto no se puede implementar (con las restricciones del Z80, la DRAM, etc) en la ULA, al menos no de forma evidente. Y si al final se pudiera, seguramente sería a costa de usar más puertas lógicas que las que da la ULA original. Veamos:
La organización de bitmap + atributos es tal que a partir de una coordenada de un pixel (en alta resolución), la dirección de memoria que contiene dicho pixel, y la dirección del atributo que contiene su color se calcula así:
Código: Seleccionar todo
Coordenada X: X7 X6 X5 X4 X3 X2 X1 X0
Coordenada Y: Y7 Y6 Y5 Y4 Y3 Y2 Y1 Y0
Dirección de memoria de pixel (tal como la genera la ULA, 14 bits)
0 Y7 Y6 Y2 Y1 Y0 Y5 || Y4 Y3 X7 X6 X5 X4 X3
Dirección de memoria de atributo (tal como la genera la ULA, 14 bits)
1 1 0 Y7 Y6 Y5 || Y4 Y3 X7 X6 X5 X4 X3
La ULA no genera estas direcciones de golpe, sino que como tiene que comandar memorias dinámicas, y éstas usan la dirección multiplexada en fila y columna, primero la ULA manda la fila (7 bits) y luego la columna (los otros 7 bits)
La fila se corresponde con los 7 bits menos significativos de la dirección, y la columna, los 7 más significativos. En el diagrama anterior, he separado fila y columna para que se vea lo que quiero decir:
Y lo que quiero decir es que tanto para la dirección de bitmap como para la de atributo, el valor de la fila es el mismo. Esto no es ninguna coincidencia: es un requisito necesario para poder hacer en la DRAM una lectura en modo página: se deja la fila fijada, y se cambia la columna tantas veces como uno quiera. Así se pueden leer múltiples posiciones de memoria, dentro de la misma página de DRAM, a más velocidad que si fuera una lectura separada.
Y esto es lo que posibilita que la contienda en la memoria de video no sea tan brutal como hubiera sido si no llega a ser por esta posibilidad de la lectura en modo página.
Nótese, para lo que veremos después, que la dirección de fila tiene el valor Y4 Y3 X7 X6 X5 X4 X3 . Cada bit que se usa en la construcción de esta dirección mantiene su peso relativo a su peso original cuando se toman las coordenadas X,Y por separado: Y7 Y6 Y5 Y4 Y3 Y2 Y1 Y0 X7 X6 X5 X4 X3 X2 X1 X0.
Esto significa que si en la segunda secuencia, Y3 está por encima de cualquier Xk, en la primera también debe complirse. Esta propiedad es la que permite que se pueda construir una dirección de atributo que sea correlativa una de la siguiente. Si esto no se cumpliera, las direcciones de atributo no podrían ser correlativas, y tendrían que seguir algún tipo de patrón más o menos extraño.
Pues bien, esto es lo que ocurre si tomáramos el modelo de organización estilo C64: las direcciones de bitmap y atributo tendrían que ser tal que así:
Código: Seleccionar todo
Coordenada X: X7 X6 X5 X4 X3 X2 X1 X0
Coordenada Y: Y7 Y6 Y5 Y4 Y3 Y2 Y1 Y0
Dirección de memoria de pixel (tal como la generaría la ULA, 14 bits)
0 Y7 Y6 Y5 Y4 Y3 X7 || X6 X5 X4 X3 Y2 Y1 Y0
Dirección de memoria de atributo (tal como la generaría la ULA, 14 bits)
? ? ? ? ? ? ? || X6 X5 X4 X3 Y2 Y1 Y0
La pregunta es: ¿qué combinación de bits pongo en los 7 bits superiores para que la dirección total siga siendo correlativa? Pues resulta que con la combinación que hay en los 7 bits inferiores es imposible encontrar una, ya que hay bits dentro de esos 7 bits inferiores que no conservan su peso (orden) original: Y2,Y1 e Y0 por ejemplo van después que los bits X6 X5 X4 X3.
Bueno, pues parece que después de todo Altwasser dio con la organización de pantalla que mejor venía a sus intereses:
- Fácil de implementar en hardware
- Posibilita la impresión rápida de caracteres
- Posibilita la lectura en modo página de la DRAM
Así que ahí dejé mi pequeña investigación, no sin antes acordarme de que había visto en algún sitio, creo que la pantalla de carga del Fairlight, el 1 o el 2, que se cargaba la pantalla con un patrón de relleno similar a éste que he puesto, así que se me ha ocurrido, como tontería adicional, escribir un hack para la rutina de carga estándar del Spectrum, para que cargue una pantalla usando esta disposición. Creo que queda resultona, ¿no?
Podeis probarla (en un emulador desactivar el "flash load") reproduciendo este TAP (aunque sea un TAP no funcionará con DivIDE, sólo con entrada de audio "de verdad"):
http://www.zxprojects.com/atc/demo_c64_ ... _color.tap
(la pantalla es una de las pedazo de obras de arte de MAC)