ProgramaciónTaller de Juan AntonioZX Spectrum

El taller de Juanan: 0x02 Scroll

27/10/2024

Scroll circular (marquesina)

Hola de nuevo.

En esta entrega del Taller de Juanan seguimos con rutinas en ensamblador para usar desde Basic. Os voy a mostrar una rutina para hacer scroll a modo del que se hace en Camelot Warriors.

Os mostraré una rutina para hacer scroll píxel a píxel, otra para hacerlo carácter a carácter y terminaré con una que incluye las dos anteriores para así poder hacer scroll en dos planos.

La última rutina va a ocupar 140 bytes y es compatible con 16K. La rutina es reubicable y la podréis cargar en la posición que queráis.

Scroll píxel a píxel

El código de esta rutina es considerablemente largo, por esa razón lo voy a mostrar por bloques.

LINE_INI: equ 0x5cb0 ; Dirección línea inicio
LINE_END: equ 0x5cb1 ; Dirección línea fin

Estas dos direcciones de memoria las conocemos de LoadScreen. Son dos direcciones de las variables de sistema que no se usan y es donde el programa BASIC va a indicar a la rutina las líneas de inicio y fin del scroll.

; Cálculo líneas totales
Lines:
ld   a, (LINE_INI)    ; A = línea inicio
and  0x1f             ; A = línea
ld   b, a             ; B = línea
ld   a, (LINE_END)    ; A = línea fin
and  0x1f             ; A = línea
sub  b                ; A = A - B, total líneas
inc  a                ; A = A + 1, total líneas > 0
ld   b, a             ; B = total líneas

Este bloque es común a todas las rutinas. Cargo la línea de inicio en el registro A, con AND 0x1F me quedo sólo con la línea y cargo el valor en el registro B. Cargo la línea de fin en A, me quedo sólo con la línea, con SUB B resto B a A (A = A – B), incremento A para que no sea cero y cargo el valor en B para luego usarlo en el bucle de líneas sobre las que se hace el scroll.

Con AND aplico una máscara para quedarme sólo con los valores de los bits del 0 al 4, los bits en los que está la línea. Más adelante, para distinguir si quiero hacer scroll al pixel o al carácter, sumaré 128 a la línea de inicio.

El funcionamiento de AND es el siguiente:

Bit Byte 1 Bit Byte 2 Resultado
0 0 0
1 0 0
0 1 0
1 1 1

AND compara los bits uno a uno, y el resultado solo es 1 cuando los dos bits valen 1. Al aplicar AND 0x1F a un valor, los bits del 5 al 7 se pondrán a 0 y los bits del 0 al 4 conservarán el valor que tienen.

La razón de incrementar A (INC A) tras restarle B es para que el contador de líneas sobre las que hacer el scroll nunca sea 0. Si es 0 en realidad tendría 256 iteraciones, se haría la primera iteración, restaría uno a A resultando 255 y se repetiría otras 255 veces hasta que A llegase a 0.

Una vez que tengo las líneas sobre las que voy a hacer scroll, tengo que calcular el scanline (línea horizontal de un píxel) en el que tengo que empezar.

Las direcciones de memoria de la pantalla (área de píxeles) tienen la siguiente composición:

010T TSSS LLLC CCCC

Dónde T = tercio, S = scanline, L = línea y C = columna.

La pantalla está dividida en tres tercios (del 0 al 2), cada uno de ellos tiene ocho líneas (del 0 al 7) y cada línea tiene ocho scanlines (del 0 al 7). La pantalla tiene 32 columnas (del 0 al 31).

Con el tercio y la línea obtenemos la línea que se pasa desde BASIC, por ejemplo:

Línea BASIC Binario Tercio Línea
03 0000 0011 00 (0) 011 (3)
10 0000 1010 01 (1) 010 (2)
16 0001 0000 10 (2) 000 (0)

La línea 3 serían 0 líneas del tercio 0, más 3 líneas. La línea 10 serían las 8 líneas del tercio 0, más dos líneas. La línea 16 serían 8 líneas del tercio 0, más 8 líneas del tercio 1, más 0 líneas del tercio 2.

Cuando indico una línea desde BASIC, su composición en binario es 000T TLLL. El siguiente bloque traslada esta composición a la composición de la pantalla; calculo el scanline en el que empieza el scroll.

; Cálculo primer scanline
FisrtScanLine:
ld   a, (LINE_INI)    ; A = línea fin
and  0x18             ; A = tercio
or   0x40             ; A = 010TT000
ld   h, a             ; A = 010TT000
ld   a, (LINE_INI)    ; A = línea inicio
and  0x07             ; A = línea
rrca
rrca
rrca                  ; A = LLL00000
ld l, a               ; L = LLL00000, HL = 010TT000 LLL0000

Cargo la línea de inicio en A, me quedo con la parte del tercio con AND 0x18, le añado la parte inicial de la dirección 010 con OR 0x40 y cargo el valor en H. Vuelvo a cargar la línea de inicio en A, me quedo con la parte de la línea con AND 0x07, paso el valor a los bits del 5 al 7 con tres RRCA y cargo en valor en L. Al finalizar la rutina, HL tiene la posición de pantalla en la que empieza el scroll en formato 010T TSSS LLLC CCCC.

Si la línea de inicio es 11 (0000 1011), la rutina hace lo siguiente:

Instrucción Operación Resultado
LD A, (LINE_INI) A = 0000 1011
AND 0x18 0000 1011 AND 0001 1000 A = 0000 1000
OR 0x40 0000 1000 OR    0100 0000 A = 0100 1000
LD H, A H = 0100 1000 (010T TSSS)
LD A, (LINE_INI) A = 0000 1011
AND 0x07 0000 1011 AND 0000 0111 A = 0000 0011
RRCA A = 1000 0001 Carry = 1
RRCA A = 1100 0000 Carry = 1
RRCA A = 0110 0000 Carry = 0
LD L, A L  = 0110 0000 (LLLC CCCC)

Al igual que AND, OR compara los bits uno a uno, pero el resultado solo es 0 si el valor de los dos bits es 0.

Bit Byte 1 Bit Byte 2 Resultado
0 0 0
1 0 1
0 1 1
1 1 1

RRCA hace una rotación circular del valor de A, desplaza todos los bit a la derecha, y el valor del bit 0 lo pone en el bit 7 y en el acarreo.

0000 0011 RRCA 1000 0001 Carry = 1

En esta parte el bit de acarreo no lo utilizo, pero más adelante, para hacer el scroll píxel a píxel, es fundamental.

El bit de acarreo  se encuentra en el registro F (flags). En este registro hay varios indicadores (bits) que cambian según qué operaciones se realicen. Puedes ver cómo afectan las instrucciones al registro F en esta página: https://clrhome.org/table/.

La composición del registro F es la siguiente:

Bit 7 6 5 4 3 2 1 0
S Z H P/V N C
  • C (acarreo): se pone a 1 si el resultado de la operación anterior necesita un bit extra para representarse (me llevo una). Ese bit, flag de acarreo, es el bit extra que se necesita.
  • N (resta): se pone a 1 si la última operación fue una resta.
  • P/V (paridad/desbordamiento): en operaciones que modifican el bit de paridad, se pone a 1 cuando el número de bits a 1 es par. En operaciones que modifican el bit de desbordamiento, se pone a 1 cuando el resultado de la operación necesita más de 8 bits para representarse.
  • Bit 3: no se usa.
  • H (acarreo BCD): se pone a 1 cuando en operaciones BCD existe un acarreo del bit 3 al 4.
  • Bit 5: no se usa.
  • Z (cero): se pone a 1 si el resultado de la operación anterior es 0. Muy útil en bucles.
  • S (signo): se pone a 1 si el resultado de la operación en complemento a dos es negativo.

Ya lo tengo todo listo para implementar el scroll píxel a píxel, para lo cual vamos a recorrer las 32 columnas de cada scanline, los 8 scanlines de cada línea y las líneas entre la línea de inicio y la de fin. En HL tengo la dirección del primer scanline y en B el número de líneas sobre las que hacer el scroll.

; Scroll
ScrollPx:
push hl         ; Preserva HL
ld   e, 0x08    ; E = 8 scanlines por línea
scrollPx_scan:
ld   c, 0x20    ; C = 32 columnas
ld   a, l       ; A = línea (LLL0000)
or   0x1f       ; A = línea y columna 31 (LLL11111)
ld   l, a       ; L = línea y columna 31 (LLL11111)

El scroll lo hago para que parezca que avanzo hacia la derecha, desplazando la pantalla hacia la izquierda.

Preservo HL poniendo su valor en la pila con PUSH HL y cargo en E el número de scanlines. En C cargo el número de columnas, en A cargo la línea, pongo la columna a 31  con OR 0x1F y cargo el valor en L. Como desplazo la pantalla hacia atrás, el scroll lo hago de la columna 31 a la 0.

scrollPx_col:
rl   (hl)                 ; Rota dirección HL
dec  l                    ; L-= 1, columna anterior
dec  c                    ; C-= 1
jr   nz, scrollPx_col     ; C <> 0, salta
inc  l                    ; L+= 1, columna posterior
jr   nc, scrollPx_scanCon ; RL (HL) no acarreo, salta
ld   a, l                 ; A = línea
or   0x1f                 ; A = línea y columna 31 (LLL11111)
ld   l, a                 ; L = línea y columna 31 (LLL11111)
set  0x00, (hl)           ; Activa último bit scanline

Roto los bits de la dirección de memoria a la que apunta HL hacia la izquierda con RL (HL), decremento L para que apunte a la columna anterior, decremento C para descontar uno del contador de columnas, y si C no ha llegado a 0 sigo en bucle con JR NZ, Scroll_R_col.

Cuando C llegue a 0, incremento L para apuntar a la columna 0, y si la última rotación no ha activado el acarreo salto a Scroll_R_scanCon  con JR NC, Scroll_R_scanCon. Dado que DEC e INC no afectan al acarreo, la última instrucción que lo afecta es RL (HL).

Si el acarreo está activo significa que al rotar la columna 0, el bit 7 estaba a uno y este pasó al acarreo. Para no perder este bit, cargo L en A, pongo la columna a 31 y vuelvo a cargar el valor en L para que apunte a esa columna. Por último, pongo a 1 el bit 0 de la columna 31  con SET 0x00, (HL).

Viendo cómo funciona RL se ve lo importancia del acarreo. Supón que roto dos bytes de la pantalla.

Instrucción Acarreo Byte 2 Acarreo Byte 1
0 0001 0001 0 1000 1111
RL Byte 1 0 0001 0001 1 0001 1110
RL Byte 2 0 0010 0011 0 0001 1110

Una vez realizadas las dos rotaciones, el bit 7 que salió del byte 1 entró en el bit 0 del byte 2.

Al rotar la columna 0, si el bit 7 estaba a uno tengo que activar el bit 0 de la columna 31, de lo contrario no estaría haciendo el scroll circular que pretendo hacer.

Una vez que he desplazado todas las columnas, paso al siguiente scanline y sigo hasta que haya desplazado los ocho.

scrollPx_scanCon:
inc  h                   ; H+= 1, siguiente scanline
dec  e                   ; E-= 1
jr   nz, scrollPx_scan   ; E <> 0, salta

Incremento H para que apunte al siguiente scanline, decremento E para descontar uno al contador de scanlines, y si E no ha llegado a 0 salto a desplazar el scanline siguiente; recordad que en H tengo el tercio y el scanline (010T TSSS).

Una vez desplazados los 8 scanlines de la línea, he de pasar a la línea siguiente.

pop  hl                  ; Recupera HL
ld   a, l                ; A = línea
add  a, 0x20             ; A+= 1 línea
ld   l, a                ; L = nueva línea
jr   nc, scrollPx_end    ; A + 0x20 no acarreo, salta
ld   a, h                ; A = 010TT000
add  a, 0x08             ; A+= 1 tercio
ld   h, a                ; H = nuevo tercio

Recupero el valor original de tercio y línea con POP HL, cargo L en A, le sumo una línea con ADD A, 0x20, cargo el valor en L y si no ha habido acarreo salto al fin de la rutina con JR NC, Scroll_R_end.

Si se activó el acarreo, hemos pasado a la línea 0 del tercio siguiente. Cargo H en A, sumo un tercio con ADD A, 0x08 y vuelvo a cargar el valor en H.

¿Cómo se activa el acarreo? Mejor lo muestro con un ejemplo.

Instrucción Valor de A Resultado
1110 0000
ADD A, 0x20 A = 1110 0000 + 0010 0000 A = 0000 0000 Carry = 1

Ahora ya podéis ver porqué se suma 0x20 para sumar una línea; es el mismo motivo de sumar 0x08 para sumar un tercio.

Instrucción Valor de A Resultado
0001 0000
ADD A, 0x08 A = 0000 1000 + 0000 1000 A = 0001 0000

Ya sólo queda el final de la rutina.

scrollPx_end:
djnz ScrollPx ; B-= 1, B <> 0 , salta
ret ; Vuelve a BASIC

Compruebo si ya he desplazado todas las líneas y si no es así empiezo con el desplazamiento de la siguiente con DJNZ Scroll_R. Una vez que he terminado de desplazar todo, vuelvo al BASIC  con RET.

He dejado para el final explicar lo que hace JR. JR es como GO TO, pero solo puede saltar a 127 o -128 bytes de distancia. Este tipo de salto hace que las rutinas sean reubicables, pues no salta a una dirección absoluta de la memoria, salta a una posición relativa desde la dirección en la que está el JR.

En el código, por ejemplo, tras DEC E pongo JR NZ, …, esto sería como IF E – 1 <> 0 THEN GO TO …

DJNZ hace lo mismo, pero en una sola instrucción, ocupa menos bytes y usa como contador del bucle el registro B. DJNZ sería como:

DEC B
JR NZ, …

La rutina de scroll píxel a píxel está lista, ahora la voy a usar desde un programa BASIC.

  1 DATA 0,3,15,63,127,123,125,62
  2 DATA 0,128,193,243,255,255,187,253
  3 DATA 48,120,254,255,249,246,239,255
  4 DATA 0,24,124,252,254,254,124,120
  5 DATA 31,7,3,1,0,0,0,0
  6 DATA 254,255,247,231,3,1,0,0
  7 DATA 255,47,223,255,249,240,224,0
  8 DATA 224,240,248,240,192,0,0,0
  9 DATA 63,127,239,247,238,127,63,0
 10 DATA 255,255,190,119,251,255,255,0
 11 DATA 248,252,238,222,190,252,248,0
 20 DATA 58,176,92,230,31,71,58,177
 21 DATA 92,230,31,144,60,71,58,176
 22 DATA 92,230,24,246,64,103,58,176
 23 DATA 92,230,7,15,15,15,111,229
 24 DATA 30,8,14,32,125,246,31,111
 25 DATA 203,22,45,13,32,250,44,48
 26 DATA 6,125,246,31,111,203,198,36
 27 DATA 29,32,231,225,125,198,32,111
 28 DATA 48,4,124,198,8,103,16,215,201
 40 BORDER 0: PAPER 0: INK 7: CLS : PRINT FLASH 1;AT 10,8;"CARGANDO DATOS"
 50 FOR i=0 TO 87: READ a: POKE USR "A"+i,a: NEXT i
 60 FOR i=0 TO 72: READ a: POKE 32000+i,a: NEXT i
100 BORDER 1: PAPER 5: INK 7: CLS
110 PRINT AT 1,5; CHR$(144) + CHR$(145) + CHR$(146) + CHR$(147): 
    PRINT AT 2,5; CHR$(148) + CHR$(149) + CHR$(150) + CHR$(151)
120 PRINT AT 2,15; CHR$(144) + CHR$(145) + CHR$(146) + CHR$(147):
    PRINT AT 3,15; CHR$(148) + CHR$(149) + CHR$(150) + CHR$(151)
130 PRINT AT 1,25; CHR$(144) + CHR$(145) + CHR$(146) + CHR$(147):
    PRINT AT 2,25; CHR$(148) + CHR$(149) + CHR$(150) + CHR$(151)
140 INK 1: PRINT AT 5,0; TAB 9; CHR$(144) + CHR$(145) + CHR$(146)
    + CHR$(147); TAB 32: PRINT AT 6,0; TAB 9; CHR$(148) + CHR$(149)
    + CHR$(150) + CHR$(151); TAB 32
150 INK 4: PRINT AT 9,0; CHR$(152) + CHR$(153) + CHR$(154); TAB 32:
    PRINT AT 10,0; TAB 15; CHR$(152) + CHR$(153) + CHR$(154); TAB 32
160 INK 2:PAPER 6:FOR f=18 TO 20 STEP 2: PRINT AT f,0; CHR$(152)
    + CHR$(153) + CHR$(153) + CHR$(154);:FOR c=0 TO 6: PRINT CHR$(152)
    + CHR$(153) + CHR$(153) + CHR$(154);: NEXT c: NEXT f
170 FOR f=19 TO 21 STEP 2: PRINT AT f,0; CHR$(153) + CHR$(154)
    + CHR$(152) + CHR$(153);: FOR c=0 TO 6: PRINT CHR$(153) + CHR$(154)
    + CHR$(152) + CHR$(153);: NEXT c: NEXT f
180 INK 1: PAPER 5: PRINT AT 13,5; "Play On Retro Magazine":
    PRINT AT 15,7; "Pulse P para scroll"
200 LET k$=INKEY$: IF k$<>"P" AND k$<>"p" THEN GO TO 200
210 POKE 23728,1: POKE 23729,3: RANDOMIZE USR 32000
220 POKE 23728,5: POKE 23729,6: RANDOMIZE USR 32000
230 POKE 23728,9: POKE 23729,10: RANDOMIZE USR 32000
240 POKE 23728,18: POKE 23729,21: RANDOMIZE USR 32000
250 GO TO 200

De la línea 1 a la 11 está la definición de los gráficos. De la línea 20 a la 28 está la rutina de scroll.

De la línea 40 a la 60 cargo los gráficos (línea 50) y la rutina de scroll (línea 60); la rutina la cargo en la dirección 32000 para que sea compatible con el modelo 16K. Si cargáis la rutina a partir de la dirección 32768 la rutina será más rápida, pero no será compatible con el modelo 16K.

De la línea 100 a la 180 lleno la pantalla. En la 200 espero a que se pulse la tecla P y de la línea 210 a la 240 hago scroll de un píxel de la línea 1 a la 3, de la 5 a la 6, de la 9 a la 10 y de la 18 a la 21 respectivamente.

En la línea 250 salto a la 200 para seguir en bucle hasta que se presione BREAK.

Esta rutina hace scroll píxel a píxel de los píxeles de la pantalla, pero no de los atributos (colores), cada atributo de color afecta a 64 píxeles (1 carácter). Por ese motivo en las líneas 140 y 150 uso TAB para rellenar toda la línea con las tintas 1 y 4 respectivamente. Si quitáis los TAB, al desplazarse la nube azul y las plataformas verdes por la pantalla cambiarán a color blanco hasta que vuelvan a su posición inicial.

Scroll carácter a carácter

Esta rutina hace scroll tanto de los píxeles como de los atributos de color, por lo que en el programa Basic prescindiré de TAB.

Los dos primeros bloques son iguales que en la anterior (Lines y FirstScanLine), sólo cambia la parte que hace el scroll. El scroll carácter a carácter es más rápido porque en lugar de desplazar un bit de cada byte en cada operación, desplaza el byte entero y usa LDIR.

El código que muestro a continuación va después del cálculo del scanline de inicio, dónde en la rutina de scroll píxel a píxel está la etiqueta Scroll_Px.

; Scroll
Scroll_Chr:
push bc                 ; Preserva BC
push hl                 ; Perserva HL
ld   e, 0x08            ; E = 8 scanlines por línea

El inicio es muy parecido al de Scroll_Px, pero en este caso preservo también el valor de BC en la pila; en B tengo el número de líneas sobre las que se hace scroll y necesito BC para indicar a LDIR el número de bytes a mover. Preservo en la pila HL y cargo en E el número de scanlines.

scrollChr_scan:
push hl                 ; Perserva HL
push de                 ; Perserva DE
ld   a, (hl)            ; A = valor dirección HL
ld   d, h               ; D = H
ld   e, l               ; E = L
inc  l                  ; L+= 1, siguiente columa
ld   bc, 0x1f           ; B = 31 columnas
ldir                    ; Desplaza bytes
dec  hl                 ; HL-= 1, columna 31
ld   (hl), a            ; Columna 31 = valor anterior columna 0
pop  de                 ; Recupera DE 
pop  hl                 ; Recupera HL
inc  h                  ; H+= 1, scanline siguiente
dec  e                  ; E-=1
jr   nz, scrollChr_scan ; E <> 0, salta

Dado que voy a usar LDIR para desplazar los bytes, y LDIR necesita en HL la dirección del byte de origen y en DE la del byte de destino, preservo ambos registros en la pila. Cargo en A el valor de la primera columna, cargo el valor de HL en DE con LD D, H y LD E, L, incremento L para que apunte a la siguiente columna y cargo el número de bytes a copiar en BC (31 columnas). Desplazo todos los bytes hacia la izquierda  con LDIR, decremento HL para que apunte a la columna 31 y cargo el valor de A (valor que había en la primera columna) en la columna 31 con LD A, (HL).

En la rutina de scroll pixel a pixel, para pasar de una columna a otra incremento L, por eso una vez recorridas la 32 columnas decremento L para que apunte a la columna 31. En esta rutina decremento HL porque LDIR incrementa HL y no sólo L.

Recupero los valores de DE y HL que contienen el número de scanlines y la dirección de la primera columna del scanline que se ha desplazado con POP, apunto H al siguiente scanline con INC H, resto uno del contador de scanlines con DEC E, y repito la operación hasta que E sea 0 y haya desplazado los 8 scanlines con JR NZ, Scroll_Chr_scan.

LDIR copia el valor que hay en la dirección de memoria a la que apunta HL en la dirección de memoria a la que apunta DE, incrementa HL, incrementa DE, decrementa BC y repite hasta que BC valga 0:

Operación Byte 1 Byte 2 Byte 3 Byte 4 Byte 5 Byte 6 Byte 7 Byte 8 Byte 9
0x11 0x22 0x33 0x44 0x55 0x66 0x77 0x88 0x99
LDIR 0x22 0x33 0x44 0x55 0x66 0x77 0x88 0x99 0x99

Una vez que desplazo los 8 scanlines de la línea, desplazo los atributos. El área de atributos está a continuación del área de píxeles y empieza por 0x58. Cada atributo afecta a 64 píxeles (8×8), un carácter.

scrollChr_attr:
pop  hl                 ; Recupera HL
push hl                 ; Preserva HL
ld   a, h               ; A = H, 010TTSSS
and  0x18               ; A = 000TTSSS
rra
rra
rra                     ; A = 000000TT
or   0x58               ; A = 010110TT
ld   h, a               ; H = A
ld   d, h               ; D = H, HL = dirección atributo
ld   e, l               ; E = L
ld   bc, 0x1f           ; BC = 31 columnas
ld   a, (hl)            ; A = atributo columna 1
inc  l                  ; L+= 1, columna siguiente
ldir                    ; Desplaza atributos
dec  hl                 ; HL-= 1, columna 31
ld   (hl), a            ; Columna 31 = atributos borde

Recupera de la pila el valor de HL que apunta a la columna 0 de la línea que acabamos de desplazar y volvemos a preservarlo en la pila.

Las direcciones de memoria del área de atributos se componen de la siguiente manera:

0101 10TT LLLC CCCC

La parte baja de la dirección (línea y columna) me sirve como está en el área de píxeles, solo tengo que transformar la parte alta de la dirección.

Cargo H en A y me quedo con el tercio con AND 0x18. Con tres rotaciones a la derecha coloco los bits del tercio en los bits 0 y 1, y con OR 0x58 añado la parte fija de la dirección y cargo el valor en H. Con esto HL apunta al atributo de la columna 0 de la línea.

Cargo el valor de HL en DE, en BC el número de columnas, en A el atributo de la columna 0, apunto L a la columna 1 y desplazo los atributos de la línea con LDIR.

Con DEC HL apunto HL a la columna 31 y le pongo el atributo que tenía la columna 0.

Una vez que he desplazado los atributos de la línea paso a la siguiente, siendo esta parte idéntica a la de la rutina de scroll píxel a píxel:

scrollChr_NextLine:
pop  hl                 ; Recupera HL
ld   a, l               ; A = línea
add  a, 0x20            ; A+= 1 línea
ld   l, a               ; L = nueva línea
jr   nc, scrollChr_end  ; A + 0x20 no acarreo, salta
ld   a, h               ; A = tercio
add  a, 0x08            ; A+= 1 tercio
ld   h, a               ; H = nuevo tercio

Por último, igual que en la rutina anterior, compruebo si ya he desplazado todas las líneas y de no ser así desplazo la siguiente. Al finalizar vuelvo al BASIC.

scrollChr_end:
pop  bc                 ; Recupera BC
djnz ScrollChr          ; B-= 1, B <> 0, salta
ret                     ; Vuelve a BASIC

La diferencia con lo que hice en la rutina anterior está en que recupero de la pila el valor de BC para recuperar el contador de líneas.

Para usar esta rutina parto desde el programa BASIC de la rutina píxel a píxel y modifico lo necesario.

Las líneas de la 20 a la 28 las sustituyo por las siguientes, los bytes de la rutina de scroll carácter a carácter:

 10 DATA 255,255,190,119,251,255,255,0
 11 DATA 248,252,238,222,190,252,248,0
 20 DATA 58,176,92,230,31,71,58,177
 21 DATA 92,230,31,144,60,71,58,176
 22 DATA 92,230,24,246,64,103,58,176
 23 DATA 92,230,7,15,15,15,111,197
 24 DATA 229,30,8,229,213,126,84,93
 25 DATA 44,1,31,0,237,176,43,119
 26 DATA 209,225,36,29,32,237,225,229
 27 DATA 124,230,24,31,31,31,246,88
 28 DATA 103,84,93,1,31,0,126,44
 29 DATA 237,176,43,119,225,125,198,32
 30 DATA 111,48,4,124,198,8,103,193
 31 DATA 16,197,201

En la línea 60, FOR I=0 TO 72… sustituyo 72 por 90.

Como comenté anteriormente, voy a prescindir de usar TAB. Modifico las líneas 140 y 150 incluso pintando más plataformas y de distinto color:

140 INK 1: PRINT AT 5,10; CHR$(144)+CHR$(145)+CHR$(146)+CHR$(147):
    PRINT AT 6,10; CHR$(148)+CHR$(149)+CHR$(150)+CHR$(151)
150 PRINT AT 9,0;INK 3;CHR$(152)+CHR$(153)+CHR$(154):
    PRINT AT 10,10;INK 2;CHR$(152)+CHR$(153)+CHR$(154):
    PRINT AT 9,20;INK 1;CHR$(152)+CHR$(153)+CHR$(154)

Con esto ya está listo el programa BASIC para ver cómo queda el scroll carácter a carácter.

Dos planos de scroll

Y ahora lo mejor. He aquí  una rutina que hace scroll píxel a píxel y carácter a carácter dependiendo de los argumentos que se pasen. ¿Y qué es lo mejor? Que ya tengo todo el código necesario a excepción de tres líneas, por lo que básicamente voy a hacer un copiar/pegar de las rutinas anteriores.

Las tres líneas nuevas las añado justo donde acaba FirstScanLine:

ld   a, (LINE_INI)      ; A = línea inicio
and  0x80               ; A >= 128
jr   nz, ScrollChr      ; Sí, salta

Cargo en A la línea de inicio, le aplico una máscara con  AND 128, 1000 0000 en binario, y si el resultado no es cero salto al scroll carácter a carácter. Si el resultado es cero se ejecuta el scroll píxel a píxel. Al inicio indiqué que sumaría 128 para indicar si quiero hacer scroll carácter a carácter.

A continuación, el código ensamblador de la rutina para que lo podáis repasar.

LINE_INI: equ 0x5cb0 ; Dirección línea inicio
LINE_END: equ 0x5cb1 ; Dirección línea fin

Lines:
ld   a, (LINE_INI) ; A = línea inicio
and  0x1f ; A = línea
ld   b, a ; B = línea
ld   a, (LINE_END) ; A = línea fin
and  0x1f ; A = línea
sub  b ; A = A - B, total líneas
inc  a ; A = A + 1, total líneas > 0
ld   b, a ; B = total líneas

FisrtScanLine:
ld   a, (LINE_INI) ; A = línea fin
and  0x18 ; A = tercio
or   0x40 ; A = 010TT000
ld   h, a ; A = 010TT000
ld   a, (LINE_INI) ; A = línea inicio
and  0x07 ; A = línea
rrca
rrca
rrca ; A = LLL00000
ld l, a ; L = LLL00000, HL = 010TT000 LLL0000

ld   a, (LINE_INI) ; A = línea inicio
and  0x80 ; A >= 128
jr   nz, ScrollChr ; Sí, salta

; Scroll
ScrollPx:
push hl                   ; Preserva HL
ld   e, 0x08              ; E = 8 scanlines por línea
scrollPx_scan:
ld   c, 0x20              ; C = 32 columnas
ld   a, l                 ; A = línea (LLL0000)
or   0x1f                 ; A = línea y columna 31 (LLL11111)
ld   l, a                 ; L = línea y columna 31 (LLL11111)
scrollPx_col:
rl   (hl)                 ; Rota dirección HL
dec  l                    ; L-= 1, columna anterior
dec  c                    ; C-= 1
jr   nz, scrollPx_col     ; C <> 0, salta
inc  l                    ; L+= 1, columna posterior
jr   nc, scrollPx_scanCon ; RL (HL) no acarreo, salta
ld   a, l                 ; A = línea
or   0x1f                 ; A = línea y columna 31 (LLL11111)
ld   l, a                 ; L = línea y columna 31 (LLL11111)
set  0x00, (hl)           ; Activa último bit scanline
scrollPx_scanCon:
inc  h                    ; H+= 1, siguiente scanline
dec  e                    ; E-= 1
jr   nz, scrollPx_scan    ; E <> 0, salta
scrollPx_NextLine:
pop  hl                   ; Recupera HL
ld   a, l                 ; A = línea
add  a, 0x20              ; A+= 1 línea
ld   l, a                 ; L = nueva línea
jr   nc, scrollPx_end     ; A + 0x20 no acarreo, salta
ld   a, h                 ; A = 010TT000
add  a, 0x08              ; A+= 1 tercio
ld   h, a                 ; H = nuevo tercio
scrollPx_end:
djnz ScrollPx             ; B-= 1, B <> 0 , salta
ret                       ; Vuelve a BASIC

ScrollChr:
push bc                   ; Preserva BC
push hl                   ; Perserva HL
ld   e, 0x08              ; E = 8 scanlines por línea
scrollChr_scan:
push hl                   ; Perserva HL
push de                   ; Perserva DE
ld   a, (hl)              ; A = valor dirección HL
ld   d, h                 ; D = H
ld   e, l                 ; E = L
inc  l                    ; L+= 1, siguiente columa
ld   bc, 0x1f             ; B = 31 columnas
ldir                      ; Desplaza bytes
dec  hl                   ; HL-= 1, columna 31
ld   (hl), a              ; Columna 31 = valor anterior columna 0
pop  de                   ; Recupera DE 
pop  hl                   ; Recupera HL
inc  h                    ; H+= 1, scanline siguiente
dec  e                    ; E-=1
jr   nz, scrollChr_scan   ; E <> 0, salta
scrollChr_attr:
pop  hl                   ; Recupera HL
push hl                   ; Preserva HL
ld   a, h                 ; A = H, 010TTSSS
and  0x18                 ; A = 000TTSSS
rra
rra
rra                       ; A = 000000TT
or   0x58                 ; A = 010110TT
ld   h, a                 ; H = A
ld   d, h                 ; D = H, HL = dirección atributo
ld   e, l                 ; E = L
ld   bc, 0x1f             ; BC = 31 columnas
ld   a, (hl)              ; A = atributo columna 1
inc  l                    ; L+= 1, columna siguiente
ldir                      ; Desplaza atributos
dec  hl                   ; HL-= 1, columna 31
ld   (hl), a              ; Columna 31 = atributos borde
scrollChr_NextLine:
pop  hl                   ; Recupera HL
ld   a, l                 ; A = línea
add  a, 0x20              ; A+= 1 línea
ld   l, a                 ; L = nueva línea
jr   nc, scrollChr_end    ; A + 0x20 no acarreo, salta
ld   a, h                 ; A = tercio
add  a, 0x08              ; A+= 1 tercio
ld   h, a                 ; H = nuevo tercio
scrollChr_end:
pop  bc                   ; Recupera BC
djnz ScrollChr            ; B-= 1, B <> 0, salta
ret                       ; Vuelve a BASIC

Para el programa BASIC tomo como base el del scroll píxel a píxel. Las líneas de la 20 a la 28 las sustituyo por los bytes de la nueva rutina:

20 DATA 58,176,92,230,31,71,58,177
21 DATA 92,230,31,144,60,71,58,176
22 DATA 92,230,24,246,64,103,58,176
23 DATA 92,230,7,15,15,15,111,58
24 DATA 176,92,230,128,32,42,229,30
25 DATA 8,14,32,125,246,31,111,203
26 DATA 22,45,13,32,250,44,48,6
27 DATA 125,246,31,111,203,198,36,29
28 DATA 32,231,225,125,198,32,111,48
29 DATA 4,124,198,8,103,16,215,201
30 DATA 197,229,30,8,229,213,126,84
31 DATA 93,44,1,31,0,237,176,43
32 DATA 119,209,225,36,29,32,237,225
33 DATA 229,124,230,24,31,31,31,246
34 DATA 88,103,84,93,1,31,0,126
35 DATA 44,237,176,43,119,225,125,198
36 DATA 32,111,48,4,124,198,8,103
37 DATA 193,16,197,201

En la línea 60 FOR I=0 TO 72 el valor después de TO debe ser 139.

En la línea 150 dibujo la nube sin rellenar, quedando así:

140 INK 1: PRINT AT 5,10; CHR$(144)+CHR$(145)+CHR$(146)+CHR$(147): 
    PRINT AT 6,10; CHR$(148)+CHR$(149)+CHR$(150)+CHR$(151)

En la línea 220, el primer POKE lo dejo como POKE 23728,5+128. En la línea 240 el primer POKE lo dejo como POKE 23728,18+128.

Con estas modificaciones hago scroll píxel a píxel en las nubes superiores y las plataformas intermedias, mientras que en la nube inferior y el suelo hago scroll al carácter, teniendo así dos planos de scroll.

La rutina de scroll píxel a píxel ocupa 73 bytes, la de carácter a carácter 91 bytes y la de dos planos 140 bytes.

Recodad que al cargarlas en la dirección 32000 es un 25% más lenta que si se carga de la dirección 32768 en adelante. Por el contrario, si la cargáis a partir de la dirección 32768 dejará de ser compatible con 16K.

Para cargar la rutina en otra dirección, en el POKE de la línea 60 tenéis que cambiar 32000 por la dirección deseada, y no olvidéis poner esa nueva dirección en todos los RANDOMIZE USR.

Podéis ver el resultado final en este vídeo.

Todo el código fuente y los programas resultantes los podéis descargar desde aquí.

BASICEnsambladorZX Spectrum

Deja una respuesta