El taller de Juanan: 0x02 Scroll
27/10/2024Scroll 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