{"id":2876,"date":"2021-02-06T20:49:03","date_gmt":"2021-02-06T20:49:03","guid":{"rendered":"http:\/\/blog.rastersoft.com\/?p=2876"},"modified":"2023-12-25T19:47:53","modified_gmt":"2023-12-25T19:47:53","slug":"pintando-en-el-spectrum-10","status":"publish","type":"post","link":"https:\/\/blog.rastersoft.com\/?p=2876","title":{"rendered":"Pintando en el Spectrum (10)"},"content":{"rendered":"\n<p>Llevo varios d\u00edas implementando el moverme por el mapa del juego con libertad, y cada vez me daba m\u00e1s rabia ver las cuatro filas inferiores desaprovechadas. No pod\u00eda evitar preguntarme si realmente no era posible conseguir hacer scroll a todo color a pantalla completa. El problema es que para ello, es imprescindible poder copiar todo el buffer intermedio (donde vamos pintando \u00abcon calma\u00bb las cosas) en la pantalla antes de que nos alcance el haz. Si recordamos los c\u00e1lculos de los primeros cap\u00edtulos, la cosa estaba bastante justa, as\u00ed que al final decid\u00ed utilizar la rutina con la tabla de 192 entradas, la que me permite usar hasta 23 filas sin <em>tearing<\/em>, pero pintando las 24. Por que se viese un poco abajo de todo, tampoco ser\u00eda un desastre.<\/p>\n\n\n\n<p>Sin embargo el efecto era bastante exagerado, lo que no me gustaba. Pero a la vez yo se que mi emulador FBZX, el que uso para las pruebas, no es completamente preciso, a pesar de todos mis intentos, as\u00ed que decid\u00ed pedir a algunos compa\u00f1eros de <a rel=\"noreferrer noopener\" href=\"https:\/\/www.zonadepruebas.com\/index.php\" target=\"_blank\">Zona de pruebas<\/a> que me hiciesen el favor de probar el c\u00f3digo en un Spectrum real. Y lo sorprendente es que la \u00faltima l\u00ednea se ve\u00eda perfecta.<\/p>\n\n\n\n<p>Demasiado perfecta.<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"174\" style=\"aspect-ratio: 232 \/ 174;\" width=\"232\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/02\/sospechoso.mp4\"><\/video><\/figure>\n\n\n\n<p>Era muy raro&#8230; TEN\u00cdA que notarse algo en la \u00faltima fila, pero no&#8230; era perfecta. \u00bfQu\u00e9 estaba pasando?<\/p>\n\n\n\n<p>Me recomendaron entonces que probase con <a rel=\"noreferrer noopener\" href=\"http:\/\/www.habisoft.com\/espectrum\/ES.htm\" target=\"_blank\">Es.pectrum<\/a>, un emulador muy preciso que, adem\u00e1s, tiene un depurador y es capaz de mostrar por donde va el barrido, paso a paso. Aunque es para Windows, funciona en wine, as\u00ed que le di un tiento&#8230; y me qued\u00e9 alucinado de la maravilla de depurador.<\/p>\n\n\n\n<p>R\u00e1pidamente cargu\u00e9 mi c\u00f3digo y lo prob\u00e9, y efectivamente, se ve\u00eda maravillosamente bien, as\u00ed que fui al depurador, puse un punto de ruptura al comienzo de los LDIs de copia del buffer, lo lanc\u00e9, fui viendo c\u00f3mo se pintaba la pantalla y&#8230; \u00a1\u00a1\u00a1El volcado necesit\u00f3 dos barridos y medio de pantalla, cuando deber\u00eda haber sido bastante menos de dos!!!<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"134\" style=\"aspect-ratio: 200 \/ 134;\" width=\"200\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2020\/06\/say-what-gone-with-the-wind.mp4\"><\/video><\/figure>\n\n\n\n<p>Aquello no ten\u00eda ning\u00fan sentido: ni a\u00fan suponiendo que todas las filas sufriesen contienda pod\u00eda ocurrir aquello, era demasiado tiempo de m\u00e1s.<\/p>\n\n\n\n<p>Decid\u00ed poner un punto de ruptura en cada uno de los LDIs de una fila, a ver qu\u00e9 sacaba en claro, y comenc\u00e9 a ejecutar. Y entonces me encontr\u00e9 con algo rar\u00edsimo: cada LDI tardaba 24 Testados en ejecutarse. \u00bfPor qu\u00e9, si se supone que dura 16? Podr\u00eda tener sentido que el primer LDI de los 32 tardase m\u00e1s porque sufriese contienda, pero el resto ten\u00edan que encajar exactamente en cada hueco que dejaba la ULA, pues 16 es m\u00faltiplo de 8.<\/p>\n\n\n\n<p>Decid\u00ed buscar en la <a rel=\"noreferrer noopener\" href=\"https:\/\/sinclair.wiki.zxnet.co.uk\/wiki\/Contended_memory\" target=\"_blank\">documentaci\u00f3n online sobre la contienda<\/a> si hab\u00eda algo que afectase a LDI, y me encontr\u00e9 con esto:<\/p>\n\n\n\n<p class=\"has-text-align-center\">pc:4,pc+1:4,hl:3,de:3,<strong>de<\/strong>:1 \u00d72<\/p>\n\n\n\n<p>No entend\u00eda qu\u00e9 significaba, as\u00ed que le di vueltas y vueltas hasta que, de pronto, ca\u00ed: LDI es una instrucci\u00f3n de dos bytes (prefijo + c\u00f3digo de instrucci\u00f3n). Los prefijos, al igual que los c\u00f3digos de instrucci\u00f3n, necesitan 4 ciclos de reloj para leerse desde la memoria, de ah\u00ed <strong>pc:4, pc+1:4<\/strong>: el bus de direcciones contiene la direcci\u00f3n del contador de programa, PC, durante cuatro ciclos de reloj (para leer el prefijo), y luego, durante otros cuatro ciclos, el valor PC+1, cuando lee la instrucci\u00f3n en s\u00ed. Luego se lee el byte a copiar, y como es una lectura de un dato, s\u00f3lo necesita tres ciclos, de ah\u00ed <strong>hl:3<\/strong>: el valor de HL aparece en el bus de direcciones durante tres ciclos de reloj. A continuaci\u00f3n se escribe el byte en el destino en la direcci\u00f3n contenida en el par de registros DE, lo que tambi\u00e9n consume tres ciclos de reloj, <strong>de:3<\/strong>. Y ahora viene la clave: 4+4+3+3=14 ciclos de reloj. Pero la instrucci\u00f3n consume 16. \u00bfQu\u00e9 pasa con esos dos ciclos extra? Pues son los necesarios para incrementar HL y DE y decrementar BC. La cuesti\u00f3n, y esta es la clave, es que durante ese tiempo el bus de direcciones conserva el valor de DE, y si recordamos, DE apunta a una direcci\u00f3n de memoria de pantalla, por lo que se ver\u00e1 afectado por la contienda.<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"162\" style=\"aspect-ratio: 288 \/ 162;\" width=\"288\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2020\/06\/mecagoenmiputavida.mp4\"><\/video><\/figure>\n\n\n\n<p>El motivo de que DE contin\u00fae en el bus de direcciones es que, dado que este bus es s\u00f3lo de salida, no es necesario ponerlo en tercer estado, por lo que cada vez que se pone un valor, \u00e9ste permanece, y solo cambia cuando es sobreescrito por un nuevo valor.<\/p>\n\n\n\n<p>\u00bfY por qu\u00e9 el mero hecho de que haya una direcci\u00f3n de pantalla en el bus es suficiente para disparar el mecanismo de contienda? Para saberlo, vamos a explicar c\u00f3mo funciona dicho mecanismo.<\/p>\n\n\n\n<p>Recordemos que el objetivo original del Spectrum es que fuese barato. Ese era EL factor clave que condicionaba absolutamente todo el dise\u00f1o, por lo que la ULA no pod\u00eda contener circuitos muy complejos sino que ten\u00eda que ser lo m\u00e1s simple posible (y as\u00ed poder utilizar el modelo m\u00e1s barato posible de los que ofertaba Ferranti).<\/p>\n\n\n\n<p>Fij\u00e9monos primero en c\u00f3mo es un acceso a memoria del Z80 si queremos leer una instrucci\u00f3n:<\/p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-large\"><a href=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/02\/Captura-de-pantalla-de-2021-02-05-23-39-00.png\" rel=\"lightbox-0\"><img loading=\"lazy\" decoding=\"async\" width=\"780\" height=\"550\" src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/02\/Captura-de-pantalla-de-2021-02-05-23-39-00.png\" alt=\"\" class=\"wp-image-2884\" srcset=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/02\/Captura-de-pantalla-de-2021-02-05-23-39-00.png 780w, https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/02\/Captura-de-pantalla-de-2021-02-05-23-39-00-300x212.png 300w, https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/02\/Captura-de-pantalla-de-2021-02-05-23-39-00-768x542.png 768w\" sizes=\"auto, (max-width: 780px) 100vw, 780px\" \/><\/a><\/figure><\/div>\n\n\n\n<p>Compar\u00e9moslo ahora con el acceso a RAM para leer o escribir un byte \u00aben general\u00bb (por ejemplo cuando leemos un dato inmediato, o un dato de la RAM&#8230; cualquier cosa que no sea el bytecode de una instrucci\u00f3n):<\/p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-large\"><a href=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/02\/Captura-de-pantalla-de-2021-02-05-23-39-13.png\" rel=\"lightbox-1\"><img loading=\"lazy\" decoding=\"async\" width=\"802\" height=\"439\" src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/02\/Captura-de-pantalla-de-2021-02-05-23-39-13.png\" alt=\"\" class=\"wp-image-2885\" srcset=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/02\/Captura-de-pantalla-de-2021-02-05-23-39-13.png 802w, https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/02\/Captura-de-pantalla-de-2021-02-05-23-39-13-300x164.png 300w, https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/02\/Captura-de-pantalla-de-2021-02-05-23-39-13-768x420.png 768w\" sizes=\"auto, (max-width: 802px) 100vw, 802px\" \/><\/a><\/figure><\/div>\n\n\n\n<p>Vemos que en ambos casos, el Z80 comienza poniendo en el bus de direcciones la direcci\u00f3n de memoria a la que quiere acceder, muy poquito despu\u00e9s del flanco de subida del primer ciclo de reloj, y no es hasta inmediatamente despu\u00e9s del flanco de bajada de ese mismo ciclo que el procesador pone a cero la l\u00ednea MREQ para indicar que va a realizar un acceso a memoria, para as\u00ed dar tiempo a que las tensiones en el bus de direcciones se estabilicen y evitar transitorios. Adem\u00e1s, vemos que la \u00fanica diferencia pr\u00e1ctica entre un acceso a memoria \u00abnormal\u00bb o uno para \u00abinstrucci\u00f3n\u00bb es que en el primer caso el dato se lee durante el flanco de bajada situado a la mitad del tercer ciclo, mientras que el segundo se lee durante el flanco de subida situado entre el segundo y tercer ciclo.<\/p>\n\n\n\n<p>\u00bfY c\u00f3mo aprovecha esto la ULA para realizar la contenci\u00f3n? Pues b\u00e1sicamente lo que hace es vigilar constantemente el bus de direcciones, de manera que si durante la parte alta de un ciclo de reloj el bit A15 es cero y el bit A14 es uno, considera que se va a realizar un acceso a memoria de v\u00eddeo y activa el mecanismo de contenci\u00f3n. Si estamos en alguno de los dos ciclos de reloj en los que el acceso est\u00e1 permitido, entonces no pasa nada y el procesador accede normalmente, accediendo al dato entre un ciclo y medio y dos ciclos m\u00e1s tarde; pero si estamos en alguno de los seis ciclos en los que la ULA est\u00e1 accediendo a memoria, entonces \u00e9sta bloquea el reloj del Z80, manteni\u00e9ndolo a nivel alto hasta alcanzar el punto en el que el acceso est\u00e1 permitido. Entonces liberar\u00e1 el reloj y el Z80 continuar\u00e1 como si no hubiese pasado nada, bajando entonces la se\u00f1al MREQ (Memory REQest) para indicar que es un acceso a memoria. Con esto ya tenemos resuelto el problema de detectar cuando el procesador va a intentar leer de, o escribir en, la memoria de v\u00eddeo.<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"186\" style=\"aspect-ratio: 320 \/ 186;\" width=\"320\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2020\/06\/tadah_harry_potter.mp4\"><\/video><\/figure>\n\n\n\n<p>Sin embargo, queda un segundo problema: la direcci\u00f3n de memoria permanece en el bus durante los tres ciclos completos de acceso a la RAM si estamos leyendo un dato, o durante los dos ciclos completos de acceso para leer una instrucci\u00f3n. \u00bfPor qu\u00e9 entonces no se activa el bloqueo durante el tercer ciclo, cuando ya han pasado los dos huecos libres y la ULA va a acceder para leer los datos de v\u00eddeo? Pues porque el mecanismo de contienda se desactiva cuando la l\u00ednea MREQ est\u00e1 baja. De esta manera, cualquier acceso que comience en los dos ciclos libres que deja la ULA tendr\u00e1n la l\u00ednea MREQ baja durante el resto del tiempo, garantizando que la ULA no bloquear\u00e1 al procesador hasta que termine el acceso y la l\u00ednea MREQ vuelva a estado alto (obviamente esto significa tambi\u00e9n que los dos ciclos \u00ablibres\u00bb est\u00e1n situados dos ciclos de reloj ANTES de que la ULA termine de leer datos, para que cuando el procesador vaya a leer o escribir realmente el dato, lo haga en el momento en el que la memoria est\u00e1 realmente libre).<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"216\" style=\"aspect-ratio: 288 \/ 216;\" width=\"288\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2020\/06\/phew.mp4\"><\/video><\/figure>\n\n\n\n<p>Y ya con todo esto podemos entender qu\u00e9 ocurre con LDI: cuando va a realizar la escritura del dato que ley\u00f3 antes, el Z80 pone la direcci\u00f3n de destino (contenida en el registro DE) en el bus de datos. En ese momento la ULA congelar\u00e1, si procede, el reloj del Z80 hasta llegar al primer ciclo libre en el que pueda acceder, momento en el cual el Z80 continuar\u00e1 su ejecuci\u00f3n y pondr\u00e1 a cero la l\u00ednea MREQ, activ\u00e1ndola, y durante los dos ciclos siguientes leer\u00e1 el dato. Ahora estamos dos ciclos y medio m\u00e1s tarde que antes (recordad que ya se consumi\u00f3 medio ciclo al comprobar si la direcci\u00f3n es del bloque de pantalla), por lo que ya volvemos a estar en zona de contienda, pero no importa porque MREQ a\u00fan est\u00e1 a nivel bajo, por lo que la ULA no va a bloquear el reloj.<\/p>\n\n\n\n<p>Ah, pero en cuanto termina ese acceso, el Z80 desactiva MREQ poni\u00e9ndolo a nivel alto, pero el bus de direcciones conserva la \u00faltima direcci\u00f3n puesta en \u00e9l, con lo que, de pronto, la ULA se encuentra de nuevo con que en el bus est\u00e1 una direcci\u00f3n pertenece al bloque de pantalla (pues estamos escribiendo en pantalla) y con MREQ desactivado, por lo que asume que la CPU va a realizar un nuevo acceso a memoria de pantalla y bloquear\u00e1 el reloj hasta el siguiente ciclo libre, cinco ciclos m\u00e1s adelante, cuando en realidad no son m\u00e1s que dos ciclos <em>extra<\/em> de la instrucci\u00f3n actual y la siguiente lectura o escritura no comenzar\u00e1 realmente hasta dos ciclos m\u00e1s tarde. Cuando llega el primer ciclo libre, el Z80 puede ejecutar dos ciclos, justo los dos ciclos que faltaban por terminar. \u00bfResultado? La CPU estuvo bloqueada cinco ciclos de m\u00e1s. Ahora el procesador leer\u00e1 el siguiente LDI (8 ciclos, con lo que volvemos a estar justo al principio de un grupo de seis ciclos de contienda), pero no se bloquear\u00e1 el reloj porque la instrucci\u00f3n est\u00e1 en la memoria alta; lee el dato a copiar, tambi\u00e9n desde la memoria alta (tres ciclos m\u00e1s, quedan tres para terminar el bloque de ciclos de contienda actual) e intentar\u00e1 escribir el dato en la memoria de v\u00eddeo&#8230; con lo que la ULA bloquear\u00e1 el Z80 durante esos tres ciclos que faltan.  Conclusi\u00f3n: cinco ciclos de retardo de la instrucci\u00f3n anterior, m\u00e1s estos tres, suman justo los ocho ciclos extra. Caso cerrado.<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"180\" style=\"aspect-ratio: 320 \/ 180;\" width=\"320\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2020\/06\/tepille.mp4\"><\/video><\/figure>\n\n\n\n<p>El problema, claro, es que por culpa de esto la copia tarda demasiado, y ya s\u00ed que no nos da tiempo a volcar toda la pantalla sin que nos pille el haz. As\u00ed que la pregunta es \u00bfqu\u00e9 podemos hacer?<\/p>\n\n\n\n<p>La soluci\u00f3n est\u00e1 en utilizar PUSH y POP, las instrucciones de acceso a la pila. Estas instrucciones son muy r\u00e1pidas, 11 y 10 ciclos de reloj respectivamente; pero adem\u00e1s, transfieren DOS bytes en una sola instrucci\u00f3n, lo que es muy interesante. Y adem\u00e1s, si revisamos en la tabla de ciclos, vemos que PUSH tiene:<\/p>\n\n\n\n<p class=\"has-text-align-center\">pc:4,<strong>ir<\/strong>:1,sp-1:3,sp-2:3<\/p>\n\n\n\n<p>y POP tiene:<\/p>\n\n\n\n<p class=\"has-text-align-center\">pc:4,sp:3,sp+1:3<\/p>\n\n\n\n<p>En otras palabras: POP no tiene ning\u00fan ciclo extra, y PUSH tiene uno, pero al estar situado justo despu\u00e9s de la lectura del c\u00f3digo de la instrucci\u00f3n, lo que hay en el bus es el contenido de los registros I y R, por lo que tampoco nos afecta (ese es el dato que pone el Z80 en el bus durante el refresco de memoria). Bueno, en realidad s\u00ed nos afectar\u00e1 un poco: tras la primera escritura, quedar\u00e1n cinco ciclos hasta que acabe la contienda y se pueda escribir el segundo dato. Tras grabarlo nos quedar\u00e1n de nuevo cinco ciclos hasta el siguiente, pero&#8230; eso son justo los cinco ciclos que necesitamos para leer el c\u00f3digo del siguiente PUSH m\u00e1s el ciclo extra, por lo que no tendremos contienda en el primer byte del siguiente PUSH, pero s\u00ed en el segundo. Por tanto, necesitaremos 16 ciclos para grabar dos bytes, y ya contando con la contienda, lo que hace que esta instrucci\u00f3n parezca m\u00e1s del doble de r\u00e1pida que LDI. \u00bfMola o no mola?<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"178\" style=\"aspect-ratio: 320 \/ 178;\" width=\"320\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/02\/sorpresa.mp4\"><\/video><\/figure>\n\n\n\n<p>Claro, ahora viene el problema: cada PUSH necesita un POP para leer antes la memoria, lo que significa que, en realidad, ser\u00e1n 26 ciclos para grabar dos bytes, o sea, trece ciclos por byte (no olvidemos que leeremos de un buffer que estar\u00e1 fuera de la memoria de pantalla, por lo que POP no sufrir\u00e1 contienda). Que sigue estando muy bien, eso s\u00ed.<\/p>\n\n\n\n<p>O no&#8230; porque mientras que POP lee en el sentido que nos gusta (\u00abhacia arriba\u00bb), PUSH escribe al rev\u00e9s (\u00abhacia abajo\u00bb), lo que significa que tenemos que poner el puntero de escritura \u00abpor delante\u00bb y escribir \u00abhacia atr\u00e1s\u00bb. Esto ya empieza a complicarse.<\/p>\n\n\n\n<p>Encima, PUSH y POP comparten el puntero de escritura (el registro de 16 bits SP), lo que significa que tras cada POP tenemos que cambiar su valor antes de hacer el PUSH, lo que lleva mucho tiempo (la instrucci\u00f3n m\u00e1s r\u00e1pida es LD SP, HL, que son 6 ciclos de reloj&#8230; pero s\u00f3lo nos servir\u00eda para una de las dos, pues la otra tendr\u00eda que almacenar el valor actual en IX o IY, con lo que ser\u00edan ya 10 ciclos de reloj).<\/p>\n\n\n\n<p>Parece que la cosa no tiene buena pinta, pero en realidad a\u00fan no estamos acabados: en lugar de hacer un \u00fanico POP seguido de un \u00fanico PUSH, podemos hacer varios POPs seguidos, cambiar SP, hacer el mismo n\u00famero de PUSH, volver a cambiar SP, hacer m\u00e1s POPs seguidos&#8230; y as\u00ed sucesivamente.<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"206\" style=\"aspect-ratio: 288 \/ 206;\" width=\"288\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2020\/07\/calculating.mp4\"><\/video><\/figure>\n\n\n\n<p>Este es el bloque en el que he basado la soluci\u00f3n, el cual utiliza HL para contener la direcci\u00f3n de origen de un bloque de 32 bytes (una <em>scanline<\/em>), y HL&#8217; para la direcci\u00f3n de destino (en realidad HL&#8217; debe contener la direcci\u00f3n de destino + 16, debido a que PUSH escribe en sentido inverso):<\/p>\n\n\n\n<pre class=\"wp-block-preformatted mycode\">; Copia 32 bytes desde HL hasta HL'-16\n\n    ld sp, hl   ; SP apunta al origen de los datos\n    ld a, 16    ; lo incrementamos para que ya apunte a los\n    add a, l    ; siguientes 16 bytes\n    ld l, a\n    pop af      ; leemos todos los datos que podamos en los\n    pop bc      ; pares de registros de que disponemos\n    pop de\n    pop ix\n    exx         ; tambi\u00e9n en el set alternativo, y los\n    ex af, af'  ; registros \u00edndice\n    pop af\n    pop bc\n    pop de\n    pop iy\n    ld sp, hl  ; cargamos la direcci\u00f3n de destino (HL', pues estamos\n    push iy    ; en el juego alternativo)\n    push de    ; escribimos los datos en orden inverso\n    push bc\n    push af\n    ld de, 16\n    add hl, de ; aprovechamos que DE ya est\u00e1 libre para incrementar\n    exx        ; el puntero de destino hasta los siguientes 16 bytes\n    ex af, af' ; y copiamos el resto de los registros que quedaban\n    push ix\n    push de\n    push bc\n    push af    ; ya hemos copiado 16 bytes, media scanline\n\n    ld sp, hl  ; repetimos el proceso para copiar los otros 16 bytes\n    ld a, 16   ; Ser\u00e1 casi igual, s\u00f3lo hay una \u00fanica diferencia...\n    add a, l\n    ld l, a\n    pop af\n    pop bc\n    pop de\n    pop ix\n    exx\n    ex af, af'\n    pop af\n    pop bc\n    pop de\n    pop iy\n    ld sp, hl\n    push iy\n    push de\n    push bc\n    push af\n    ld de, 240  ; al incrementar el puntero de destino, lo hacemos\n    add hl, de  ; sumando 240, para que junto con los 16 que ya\n    exx         ; copiamos en el bloque anterior, sume 256, que es\n    ex af, af'  ; justo lo que hay que saltar para pasar al siguiente\n    push ix     ; scanline de un caracter\n    push de\n    push bc\n    push af\n\n<\/pre>\n\n\n\n<p>Este bloque consume en total 490 Testados en el caso mejor (sin contienda) y 565 Testados en el peor (con contienda), pero copia 32 bytes, lo que nos da un tiempo entre 15,3125 y 17,65625 Testados por byte, lo que no est\u00e1 nada mal. Obviamente, una vez que a\u00f1adimos c\u00f3digo para leer la direcci\u00f3n de destino y el bucle, la cosa empeora un poco, pero no demasiado.<\/p>\n\n\n\n<p>Vemos que el puntero de destino siempre lo incremento con una suma de 16 bits, pero el de origen lo hago con una de 8 bits, incrementando s\u00f3lo la parte baja de cada vez. Esto lo hago as\u00ed porque esa operaci\u00f3n completa de ocho bits (cargar A, sumarle L y copiar A en L) consume seis ciclos de reloj menos que la operaci\u00f3n de suma completa de 16 bits (cargar DE y sumar a HL), pero tambi\u00e9n significa que cada ocho bloques, tenemos que incrementar manualmente en uno el registro H para que el valor sea correcto, aunque esto lo podemos hacer con un simple INC HL.<\/p>\n\n\n\n<p>Tambi\u00e9n significa que, sin m\u00e1s cambios, s\u00f3lo nos sirve para copiar las ocho <em>scanlines<\/em> de una fila de caracteres, pero no puede saltar de un car\u00e1cter al siguiente, sino que cada ocho repeticiones tendremos que recargar \u00aba mano\u00bb (en mi caso, desde una tabla) la direcci\u00f3n de la <em>scanline<\/em> siguiente.<\/p>\n\n\n\n<p>Al principio, para no consumir demasiada memoria, decid\u00ed meter el bloque en un bucle de ocho repeticiones, pero por desgracia tardaba demasiado, as\u00ed que prob\u00e9 a repetir tres veces el c\u00f3digo dentro del bucle, ejecutarlo dos veces, y poner sendas copias extra fuera, de manera que sumasen las ocho copias para un car\u00e1cter, tras lo cual se leer\u00eda la siguiente direcci\u00f3n de destino desde una tabla. El resultado era bueno: casi pod\u00eda pintar 22 caracteres en pantalla sin que hubiese <em>tearing<\/em>. Lo malo es que, realmente, s\u00ed hab\u00eda tiempo de sobra para las 22 filas con sus atributos de color, pero por desgracia, cuando terminaba de copiar los atributos de la \u00faltima fila, el haz ya hab\u00eda empezado a pintar las primeras <em>scanlines<\/em> de dicha fila, y obviamente hab\u00eda cogido los atributos incorrectos.<\/p>\n\n\n\n<p>Prob\u00e9 a cambiar el orden, copiando primero los atributos y luego los p\u00edxeles, de manera que cuando el haz llegase a la primera <em>scanline<\/em> de la fila 22 los atributos y los p\u00edxeles ya estuviesen all\u00ed, aunque los p\u00edxeles de la \u00faltima fila a\u00fan no. Y funcion\u00f3&#8230; en parte, porque entonces ahora el problema era el inverso: las primeras ocho filas ve\u00edan sus atributos de color cambiados antes de que el haz ya hubiese pasado, con lo que aparec\u00edan con el color futuro.<\/p>\n\n\n\n<p>Ante esto se me ocurri\u00f3 la soluci\u00f3n: copiar primero la mitad de los <em>scanlines<\/em> de p\u00edxeles, de manera que nunca me pueda adelantar al rato cat\u00f3dico, copiar entonces los atributos de color, aprovechando que el haz estar\u00e1 pintando en borde inferior, y finalmente seguir copiando las <em>scanlines<\/em> restantes desde el buffer. Es m\u00e1s: haciendo pruebas, la soluci\u00f3n \u00f3ptima es copiar siete filas, luego los atributos, y terminar de nuevo con el resto de las filas. Sin embargo, como la pila no est\u00e1 disponible, no puedo hacer una llamada con <em>call<\/em> y luego volver con <em>ret<\/em>, as\u00ed que tuve que hacer un truco con c\u00f3digo automodificable, y almacenar las direcciones de retorno en mitad de la lista de direcciones de cada fila.<\/p>\n\n\n\n<p>El resultado era casi perfecto, porque por desgracia las 22 l\u00edneas me sab\u00edan a poco: <em>sobraba <\/em>espacio en blanco y no me gustaba nada, as\u00ed que decid\u00ed intentar llegar hasta las 23 l\u00edneas de caracteres. Para ello <em>desenrroll\u00e9<\/em> completamente el bucle anterior, poniendo ocho copias de \u00e9l seguidas, una detr\u00e1s de la otra, con lo que me ahorraba el tiempo de ejecutar el bucle. Con eso consegu\u00ed algo m\u00e1s de 22 filas y media. Eso significa que habr\u00e1 un poco de <em>tearing<\/em> en las dos\/tres \u00faltimas l\u00edneas del \u00faltimo car\u00e1cter, algo que puedo decir que no se nota nada.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">El bus flotante en el +2A y +3<\/h2>\n\n\n\n<p>Para finalizar, a\u00f1ado un detalle importante: en la entrada 7 coment\u00e9 c\u00f3mo sincronizarnos con el haz f\u00e1cilmente aprovechando el bus flotante del Spectrum y saber cuando est\u00e1 la ULA leyendo la zona del PAPER. Coment\u00e9 tambi\u00e9n que en los modelos +2A y +3 esto no funcionaba si no se hac\u00eda una peque\u00f1a modificaci\u00f3n a nivel <em>hardware<\/em>. Sin embargo resulta que s\u00ed hay una manera de conseguir que funcione sin cambio alguno, lo que pasa es que est\u00e1 <em>ligeramente escondida<\/em>. Resulta que <a rel=\"noreferrer noopener\" href=\"https:\/\/worldofspectrum.org\/forums\/discussion\/51886\/spectrum-2a-floating-bus-port\" target=\"_blank\">el bus flotante del +2A\/+3 s\u00ed funciona si el puerto es de la forma 0000 xxxx xxxx xx01<\/a>, tal y como descubri\u00f3 Chernandezba (aunque s\u00f3lo si estamos en modo 128K; en modo 48K no funciona). He incluido este cambio en mi c\u00f3digo, usando el puerto 0x0FFD, y ahora ya funciona en cualquier Spectrum (con la excepci\u00f3n del Inves, claro).<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Llevo varios d\u00edas implementando el moverme por el mapa del juego con libertad, y cada vez me daba m\u00e1s rabia ver las cuatro filas inferiores desaprovechadas. No pod\u00eda evitar preguntarme si realmente no era posible conseguir hacer scroll a todo color a pantalla completa. El problema es que para ello, es imprescindible poder copiar todo &hellip; <a href=\"https:\/\/blog.rastersoft.com\/?p=2876\" class=\"more-link\">Seguir leyendo <span class=\"screen-reader-text\">Pintando en el Spectrum (10)<\/span> <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5,17,7],"tags":[20],"class_list":["post-2876","post","type-post","status-publish","format-standard","hentry","category-programacion","category-retrocomputacion","category-tutoriales","tag-pintando-en-el-spectrum"],"_links":{"self":[{"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts\/2876","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2876"}],"version-history":[{"count":10,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts\/2876\/revisions"}],"predecessor-version":[{"id":2891,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts\/2876\/revisions\/2891"}],"wp:attachment":[{"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2876"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2876"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2876"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}