Lección 7Aviso de antemano, esta lección es muy avanzada. No recomiendo seguirla a no ser que tengáis un buen nivel de ensamblador del Z80 para comprenderla.
Un poco de historia.Las cargas turbo aparecieron relativamente pronto, sobre 1984. Usan la misma modulación (FSK) que la carga estándar, pero modificando los tiempos para conseguir una carga al doble de velocidad (3000 bps). El objetivo principal era dificultar la piratería de dos formas: encriptando los datos para que no los pueda leer ningún copiador, y poniendo al límite el ancho de banda de las cintas, evitando así su copiado en cassettes de doble pletina. Las cargas turbo más populares fueron Alkatraz y Speedlock.
Aparte de las protecciones comerciales también han habido intentos homebrew de acelerar la carga de juegos hasta 2005, pero siempre basándose en la misma modulación y el mismo código que la carga estándar. Como ejemplo de utilidad que permitía esto tenemos el
Taper (ojo es para MS-DOS) y una página conocida que empleaba dicha utilidad era la de
Digiexp. Estamos hablando del cuádruple de velocidad estándar (6000 bps) y tiempos de carga de minuto y pico.
¿Qué ocurrió a partir de 2005? Pues por un lado se emplearon métodos más rápidos para muestrear el puerto EAR y por otro lado aparecieron mejores modulaciones. La primera utilidad fue
Sna2Wav, escrita por un servidor, y la velocidad alcanzada era de 8 veces la velocidad estándar (12000 bps). Se puede descargar
aquí. Básicamente es la misma modulación que la carga estándar (FSK) pero empleando símbolos de 3 y 5 muestras para el 0 y el 1 (en carga estándar se codifican con 23 y 47 muestras respectivamente).
Luego apareció el famoso
k7zx de Francisco Villa (decicoder en los foros), mejorando tanto las rutinas de muestreo como desarrollando nuevas modulaciones. Esta utilidad evolucionó en el proyecto OTLA, en el que se portan las ultracargas a otras plataformas (ZX81, Amstrad CPC y MSX). Dichas utilidades explotaron al máximo todo lo que podían dar de sí las ultracargas. Se consiguió la máxima velocidad en un spectrum real (21333 bps) y se llegó a la conclusión de que el método más rápido (que a la vez ofrece fiabilidad) es el llamada "Shawings Raudo 2.25", que explicaré más adelante.
El único escollo que quedaba por resolver era convertir fácilmente un archivo TAP a ultracarga. Con los snapshots no había problema, pero tienen sus inconvenientes: requieren cargar 48K de datos por lo que siempre tardan más, es necesario generar un snapshot ya que los juegos normalmente se distribuyen en formato cinta (TAP ó TZX) y por último no siempre es imposible mostrar la pantalla de carga. Por esa razón desarrollé (con la ayuda de decicoder)
CargandoLeches, un proyecto que mediante reemplazo de ROM se pueden pasar juegos en formato TAP a ultracarga, empleando las mismas modulaciones "Shawings Raudo" y "Shawings Slow" del k7zx/OTLA. Tienen la ventaja de ser las más rápidas (en torno a 10-15 segundos para un juego de 48K) porque no es necesario que los primeros bloques tengan carga estándar. Aunque claro, hay que reemplazar la ROM (o disponer de un +2A) si quieres disfrutar de ellas.
Por último tenemos la utilidad GenTape, que permite todo tipo de ultracargas mediante plugins. Tan sólo hay que desarrollar un ejecutable que genere un bloque (en formato TZX y WAV) partiendo de un binario dado, que GenTape llama a dicho ejecutable varias veces y concatena los segmentos de audio al archivo principal. En esta lección mostraré cómo hacer esto, e introduciré un nuevo formato de ultracarga (basado en la modulación más rápida de CargandoLeches) que he escrito exclusivamente para este tutorial.
Antes de nada, ajusta el volumen.Para que las ultracargas funcionen es necesario que la señal esté lo más balanceada posible. Es decir, si le meto una onda cuadrada, los pulsos altos deben durar lo mismo que los pulsos bajos. Antiguamente esto se hacía variando el azimuth, alineando el cabezal lector con la cinta mediante el ajuste de un tornillo. Evidentemente no vamos a usar cintas, tienen un ancho de banda muy limitado que no permite superar la barrera de los 10 Kbps, pero el concepto es el mismo.
El Spectrum siempre ve una onda cuadrada por el puerto EAR, pero la señal de audio es muy parecida a la función seno. Para estar perfectamente balanceados necesitamos que el pulso de la señal cuadrada resultante (lo que ve el Spectrum) cambie cada vez que nuestro "seno" pase por cero, leyendo un "0" cuando la señal es negativa y un "1" cuando la misma es positiva. Se permite cierto margen de asimetría, pero en las ultracargas este margen es muy pequeño. En teoría nuestra rutina temporiza ciclos enteros (un ciclo son dos pulsos) por lo que daría igual que las 10 muestras de un hipotético ciclo duren 5/5, 4/6 ó 3/7. En la práctica el código se pasa una parte del tiempo muestreando y otra haciendo otras cosas, mientras más grande sea la proporción de "haciendo otras cosas", más sensible será nuestra rutina a fallar cuando la señal muestre asimetría. Esto es lo que pasa con las ultracargas, que seguro funcionan a 5/5, es posible que también a 4/6, pero a 3/7 dejan de funcionar, mientras que la carga estándar se lo traga todo (ojo estas cifras no son reales sino ejemplos ilustrativos).
Vamos a utilizar la utilidad LoadVerify (hay un TZX y un WAV en el fichero de la lección) para hacer el ajuste del volumen. Es muy importante usar el mismo reproductor tanto en el calibrado como en la carga final, no nos vale de nada cargar el LoadVerify.tzx con Tapir y luego reproducir el WAV con VLC. Se entiende que vamos a cargar el juego al mismo volumen con el que hemos conseguido el mejor calibrado, y el mejor calibrado es aquel en el cual el cuadradito rosa esté lo más centrado posible. La guía verde de arriba es orientativa, nada nos asegura que vaya a cargar si estamos dentro de la guía o que no vaya a hacerlo si estamos fuera. Evidentemente a mayor velocidad de carga, mejor calibración necesitaremos, más centrado debe estar el cuadradito rosa. En el ejemplo de esta lección cargaremos a 21333bps (la máxima conseguida en hardware real sin modificaciones) por lo que hay que centrar todo lo que podamos.
Muestro un pantallazo de lo que sería ideal. Nótese que una señal asimétrica cuando es pronunciada también se aprecia en las bandas del borde. Si las bandas de un color son notablemente más anchas que las de otro es porque la señal que lee el spectrum es muy asimétrica. Esto en tiempos del Spectrum era un síntoma claro de que hacía falta un ajuste de azimuth, y afortunadamente se podía calibrar sin ninguna utilidad específica, tan sólo observando las bandas rojas/cyan del tono guía en cualquier carga estándar.
Un poco de código antes de empezar.Hay 2 formas para muestrear el puerto EAR lo más rápidamente posible. La primera es la que se me ocurrió a mí para el Sna2Wav:
Tarda 16 ciclos, serían 12 de la instrucción IN, más 4 del JP (HL). El secreto está en ubicar distintos fragmentos en direcciones que acaben en $BF y $FF, que son los valores posibles que podemos leer del puerto EAR. Por ejemplo en $80BF tengo el bucle que me lee el nivel cero y en $80FF tengo el que me lee el nivel uno.
¿Por qué es importante que el bucle dure pocos ciclos? Pues porque mientras menos dure, más veces podemos muestrear la señal EAR y más exactos serán los valores a comparar. Para que te hagas una idea, una muestra a 48000 Hz dura unos 73 ciclos de CPU. En carga estándar el bucle que incrementa el registro B (lo vimos en la lección 4) tarda 59 ciclos en muestrear. Dependiendo de cuándo toca muestrear (esto no lo podemos controlar) podemos tener el mismo número de lecturas, por ejemplo 2, tanto en un pulso de una muestra (73 ciclos) como en uno de dos (146 ciclos).
Veamos ahora la segunda forma de muestrear, que descubrió decicoder y lo usó por primera vez en su k7zx:
Este bucle tarda 22 ciclos en completarse, 12 en el IN, más 10 en el JP. En este caso la ventaja es evidente, podemos ubicar la rutina donde queramos a costa de un muestreo 6 ciclos más lento. Lo primero es una instrucción no documentada (el registro F no existe), lo único que hace es actualizar los flags, el byte leído no se almacena en ningún registro. Ojo que hay otra instrucción, IN A,(N), de 11 ciclos que no actualiza los flags, y por tanto no nos vale. De hecho el único flag que nos interesa es P/V (paridad/overflow), que en este caso usamos como paridad. La paridad es el resultado de la función XOR de los 8 bits que componen el byte, y es equivalente a contar el número de unos y ver si la cuenta es par o impar. Ojo que en la última instrucción he puesto un JP PE (parity even o par), pero también podría ser un JP PO (parity odd o impar).
Si hacemos cuentas, con la primera rutina podemos detectar el nivel de una muestra a 48000 bps (73 ciclos) entre 4 y 5 veces (73/16= 4.56), mientras que con la segunda rutina sería entre 3 y 4 veces (73/22= 3.31). Evidentemente mientras más veces muestremos un pulso mejor, así podemos distinguirlo mejor de otro pulso de distinta duración.
Hasta ahora he indicado cómo se muestrea pero no como se contabiliza el número de muestras leídas. Este paso es imprescindible si queremos distinguir pulsos de distinta duración. Bueno pues también existen dos formas de contabilizar, una que emplea 4 ciclos adicionales y otra que emplea... 0 ciclos. ¿Cómorrrr? Sí, lo que estás oyendo, en breve explicaré lo que a simple vista parece una magufada. Primero la de 4 ciclos:
En método 1
En método 2
Esto evidentemente empeora el muestreo a 20 ciclos (método 1) y a 26 ciclos (método 2) respectivamente. Ahora toca explicar la magufada. ¿Cómo podemos saber cuántas veces se ha ejecutado el bucle sin meter ninguna instrucción que incremente un contador? Pues muy fácil, mediante el registro R del Z80. Es un registro exclusivamente pensado para simplificar la circuitería en las memorias dinámicas. El Z80 incrementa el registro R (los 7 bits menos significativos) tras cada ciclo de fetch, de tal forma que pueda hacer una falsa lectura (con el objetivo de refrescar) a cada una de las 128 filas que componen la matriz. De no hacer estas lecturas periódicas la RAM se corrompería.
Pues bien, en ambos casos lo que hay que hacer es leer el registro R antes y después y hacer la diferencia, muestro el método 2:
Código: Seleccionar todo
ld a, r
ld b, a
bucle in f, (c)
jp pe, bucle
ld a, r
sub b
En el bucle hay 2 intrucciones pero la primera es compuesta (el opcode ED tiene su propio ciclo de fetch), por lo que en cada pasada el registro R se incrementa en 3. Así que en este código, dependiendo del número de pasadas del bucle (lo llamaremos N) tendremos un incremento del registro R de N*3+2.
Lo que acabo de contar es el corazón de toda ultracarga, al fin y al cabo de lo que se trata es de leer el puerto EAR lo más rápidamente posible e interpretar los símbolos dependiendo de la duración de los pulsos. Todo lo demás es dependiente de la modulación: rellenar el byte de bits, escribirlo en memoria una vez esté relleno, incrementar puntero de memoria, calcular checksum, comprobar si hemos acabado la ultracarga, etc...
La modulación que vamos a emplear.Es la misma que "Shawings Raudo 2.25" en su k7zx, es una FSK con 2 bits por símbolo. Es decir que tenemos 4 símbolos distintos que codificar (00, 01, 10 y 11) a diferencia de la rutina de carga estándar donde teníamos sólo dos (0 y 1). El numerito que viene después indica la velocidad de carga. Viene a decir cúantas muestras hacen falta de media para codificar un bit (no un símbolo). A partir de este numerito podemos calcular la velocidad de carga haciendo una simple división. Así, a 44100 Hz tendremos 19600 bps, mientras que a 48000 Hz serían 21333 bps. Los valores reales, al igual que en la carga estándar aunque en menor medida, son ligeramente mejores. Esto se debe a que los ceros son más frecuentes que los unos (los 00s también) y al ser también los símbolos más cortos hacen que la media baje. O sea que normalmente una carga estándar puede ir a 1600/1700 bps y esta ultracarga por tanto entre 22000/23000 bps. Teniendo en cuenta ésto y la duración de los silencios y los tonos guía ya podemos calcular de forma aproximada cuánto va a tardar en cargar un juego.
Por poner un ejemplo, tenemos un juego con 7K de pantalla de carga y 30K de binario. Serían 4 bloques físicos (3 lógicos), y el cargador pongamos que ocupa 300 bytes.
Tonos guía: 5 segundos del primer bloque, y 2 segundos de cada uno de los otros tres suman 11.
Pausas: 1 segundo tras el primer bloque y 2 en cada uno de los otros 3 (descontando 2 del silencio final) nos dan un total de 5 segundos.
Datos: 300bytes * 8 bits-por-byte / 1600 bps hacen un total de 1.5 segundos para el cargador, haciendo las mismas cuentas en pantalla de carga (34.5s) y juego (150s) sale un total de 186 segundos.
En carga estándar tendríamos un juego de 202 segundos (3 minutos y 22 segundos).
Hagamos las mismas cuentas para una ultracarga, reduciendo pausas y tonos guía a la mitad aunque en realidad se reducen aún más:
Tonos guía: 5.5s
Pausas: 2.5s
Datos: 1.5s (cargador) + 2.4s (pantalla de carga) + 10.4s (juego)= 14.3
Total: 22.3s
La duración de la carga es más o menos 9 veces menor en la ultracarga. Si sólo cargáramos datos debería haber salido 21333/1500= 14.22. Por eso es muy importante optimizar también el tiempo de los silencios y tonos guías. En el ejemplo que estamos siguiendo en el tutorial partíamos de 3:25 y lo dejaremos en 9 segundos, vamos que la mejora es de casi 23. Ojo que en esto último hemos metido compresión de por medio, sólo quiero indicaros lo que se puede conseguir.
Ahora veamos el aspecto que tiene una señal en carga estándar. Son capturas del Audacity con leyendas sobreescritas.

Como podemos observar, tenemos un tren de pulsos equiespaciados a la izquierda (sería el tono guía) seguido de dos pulsos cortos de sincronismo (sync) y después tenemos los datos. En este ejemplo vemos 2 bytes con sus correspondientes bits, donde podemos distinguir los ciclos del símbolo 0 (dos pulsos cortos) de los ciclos del 1 (dos pulsos largos). Sabemos que es un bloque de datos porque el primer byte (de flag) es $FF, y salvo éste byte y el último (checksum) todos los demás se escriben en memoria.
Ahora le toca a una señal con modulación "Shawings Raudo 2.25". He omitido el tono guía y el checksum (en esta modulación se codifican de forma diferente y no existe byte de flag), yéndonos directamente a la zona de datos:

Vemos que los símbolos son de 2 bits (cada símbolo) y que cada byte se codifica con 4 símbolos. La duración de cada símbolo va desde 3 muestras (00) hasta 6 muestras (11) y al igual que en carga estándar cada símbolo está compuesto por 2 pulsos (pulso negativo+pulso positivo). En esta modulación en concreto el primer pulso es siempre más corto o igual que el segundo, esto es así porque la rama de código del primer pulso también es más corta, aunque hay otras modulaciones como CargandoLeches donde es justo al revés. Así las codificaciones exactas son: 00 (1+2=3), 01 (2+2=4), 10 (2+3=5) y 11 (3+3=6).
Ahora con estos 4 símbolos calculamos la media, sería: (3+4+5+6)/4 = 4.5 muestras. Es decir, cada símbolo (2 bits) dura de media 4.5 muestras, con lo que cada bit se codifica en una media de 2.25 muestras. Es por eso que la codificación se llama "Shawings Raudo 2.25". Existen otras variantes Shawings llamadas Slow en las que la distancia entre símbolos es de 2 muestras. Evidentemente esto es más lento pero a la vez es más inmune a errores de carga (decimos que la carga es más fiable). Por ejemplo la Shawings Slow más rápida es la que usa símbolos de 2,4,6 y 8 muestras respectivamente, también llamada "Shawings Slow 2.5". El conjunto de modulaciones que acepta CargandoLeches para "Shawings Raudo" es 1.75, 2.25, 2.75 y 3.25 (a 1.75 no funciona en máquinas reales) y para "Shawings Slow" es 2.5, 3, 3.5 y 4.
En nuestro ultracargador partimos del código "Shawings Raudo 2.25", al cual le haremos una serie de modificaciones para hacerlo más simple. El cambio más importante es el de la rutina muestreadora, usaremos una más lenta pero más sencilla de ubicar (algo parecido al método 2).
make.bat. Todo metido en el cargador, rutina en dirección fijaComo siempre, lo mejor es empezar haciendo algo lo más sencillo posible y que funcione, luego habrá tiempo de mejorarlo. No lo he dicho antes pero GenTape funciona con dos velocidades de muestreo (44100Hz y 48000Hz), y por defecto se trabaja a 44100. Y si queremos hacer las cosas bien tenemos que tener en cuenta ambas frecuencias a la hora de codificar el WAV, y que nuestra rutina cargadora se adapte a estos pequeños cambios (44100Hz serían muestras de 79 ciclos, a diferencia de los 73 a 48000Hz). También podemos trabajar sólo a una en concreto y mostrar error si se intenta usar la otra, aunque no es lo recomendable.
Veamos el archivo make.bat:
Código: Seleccionar todo
set _freq=44100
fcut manic.bin 200 -200 manic.cut
zx7b manic.scr manic.scr.zx7b
zx7b manic.cut manic.cut.zx7b
for %%A in (manic.scr.zx7b) do set _fileSize=%%~zA
echo define scrsize %_fileSize% > define.asm
for %%A in (manic.cut.zx7b) do set _fileSize=%%~zA
echo define binsize %_fileSize% >> define.asm
set /a _sf48=%_freq%/48000
echo define sf48 %_sf48% >> define.asm
SjAsmPlus loader.asm
FlagCheck header.bin 0
FlagCheck loader.bin
GenTape %_freq% manic.wav ^
turbo 2168 667 735 ^
600 1600 1500 0 header.bin.fck ^
turbo 2168 667 735 ^
600 1600 1500 0 loader.bin.fck ^
plug-ultra-3 100 500 manic.scr.zx7b ^
plug-ultra-3 100 500 manic.cut.zx7b
En la variable _freq decidimos cual es la frecuencia de muestreo a la que vamos a trabajar. Las 7 líneas siguientes ya las hemos visto antes, son para recortar, comprimir y escribir el tamaño de los comprimidos en define.asm. Luego tenemos otras 2 líneas adicionales que introducen una constante nueva sf48 en define.asm, que estará a "0" si trabajamos a 44100, o a "1" si lo hacemos a 48000 Hz. Esto es necesario porque en un momento dado necesitamos indicarle al ensamblador a qué frecuencia trabajamos y es más sencillo hacerlo con una variable booleana que con una numérica.
El siguiente bloque de 3 líneas también lo hemos visto antes, ensamblamos loader.asm (que genera header.bin y loader.bin) y calculamos los flags/checksums para usar bloques "turbo" en lugar de "data" (en los que podemos ajustar los tiempos a nuestro antojo).
Por último tenemos la llamada a GenTape. Como véis le hemos pasado la variable _freq para decirle a qué frecuencia de muestreo queremos que nos genere el WAV. En este caso generamos un WAV pero podríamos haber generado un TZX. Para ultracargas de este estilo recomiendo los WAVs, son más fiables. Los dos primeros bloques son tipo "turbo", donde tenemos carga estándar con parámetros ligeramente modificados. La novedad está en los 2 últimos bloques, donde se usa un tipo que no hemos visto antes llamado "plug-ultra-3".
Todo tipo que empieza por "plug-" lo que hace es invocar un plugin, que es un ejecutable externo cuyo nombre es lo que viene a continuación. Es decir por cada línea "plug-ultra-3" se hace una llamada al ejecutable "ultra.exe" con una serie de parámetros, algunos fijos y otros que introducimos en la propia línea. Como el número de parámetros puede variar para cada plugin, se lo indicamos con el "-3", así GenTape sabrá cuando acaba la línea (GenTape recibe todos los parámetros en una línea, los ^ no los recibe, son para mejorar la legibilidad). Los 3 parámetros que le enviamos son "100 500 manic.scr.zx7b" para la pantalla y "100 500 manic.cut.zx7b" para el juego.
Como curiosidad, escribamos "ultra" para invocar a "ultra.exe" en la línea de comandos:
Código: Seleccionar todo
ultra v0.03, an ultra load block generator by Antonio Villena, 31 May 2014
ultra <srate> <channel_type> <ofile> <pilot_ms> <pause_ms> <ifile>
<srate> Sample rate, 44100 or 48000. Default is 44100
<channel_type> Possible values are: mono (default), stereo or stereoinv
<ofile> Output file, between TZX or WAV file
<pilot_ms> Duration of pilot in milliseconds
<pause_ms> Duration of pause after block in milliseconds
<ifile> Hexadecimal string or filename as data origin of that block
Tenemos esta ayuda en pantalla que nos lo explica todo. Es más, seríamos capaces de poder generar la ultracarga sin la herramienta GenTape con otras herramientas y a base de editar WAVs. Como véis el ejecutable tiene 6 parámetros, mientras que la llamada al plugin sólo 3 (en concreto los 3 últimos). Internamente GenTape rellena los 3 primeros parámetros con lo que le hayamos indicado, o lo que tome por defecto si no le indicamos nada (mono y 44100), y como fichero de salida un archivo temporal que luego concatenará con el fichero que esté generando, para luego borrarlo.
Pues bien, como podemos deducir de todo esto, en ambos bloques de ultracarga queremos un tono guía de 100 milisegundos y una pausa de 500 milisegundos después de cada bloque.
Recomiendo que le echéis un vistazo al código fuente de ultra.exe (ultra.c), está en C y es más o menos sencillo de asimilar. No sé si lo he dicho pero si desarrolláis vuestra propia modulación es conveniente poder generar la ultracarga tanto en formato TZX como en WAV. Para ello tendréis que conocer las especificaciones de dichos formatos, aunque siguiendo el código de ultra.c no debería ser muy difícil.
Ahora veamos el código Z80 del cargador, loader.asm:
Código: Seleccionar todo
define tr $ffff &
include define.asm
; Bloque cabecera
output header.bin
db 0 ; tipo: 0=cabecera, 1=array numérico
; 2=array alfanumérico, 3=código máquina
db 'ManicMiner' ; Nombre del archivo (hasta 10 letras)
block 11-$, 32 ; Relleno el resto con espacios
dw fin-ini ; Longitud del bloque basic
dw 10 ; Autoejecución en línea 10
dw fin-ini ; Longitud del bloque basic
; Bloque datos (Basic con código máquina incrustado)
output loader.bin
org $8000-dzx7b+ini
ini ld sp, $8200-4
di
db $de, $c0, $37, $0e, $8f, $39, $96
ld hl, $5ccb+dzx7b-ini
ld de, $8000
ld bc, fin-dzx7b
ldir
ld hl, $8200
ld de, scrsize
call tr loader
ld hl, $8200+scrsize-1
ld de, $5aff
call tr dzx7b
ld hl, $8200-4
ld de, binsize
call tr loader
ld hl, $8200+binsize-4-1
ld de, $ffff
call tr dzx7b
jp $8400
dzx7b include dzx7b_fast.asm
loader include ldbytes.asm
fin
Como tenéis experiencia de lecciones anteriores no hace falta que explique lo que hace línea por línea. Tanto el descompresor (dzx7b_fast.asm) como la rutina cargadora (ldbytes.asm) van incluídos en el cargador (en el primer bloque, el bloque Basic) y por tanto son cargadas a velocidad estándar. En concreto el descompresor va desde $8000 hasta $8042 y la rutina cargadora desde $8043 hasta $8159. Si hacemos la suma de cargador+descompresor+rutina cargadora nos sale un total de 407 bytes. Puede parecer poco pero supone bastante tiempo de carga si lo hacemos a 1500 bps en lugar de a 21333.
Por otro lado no nos hemos preocupado de que la rutina cargadora se pueda reubicar, o sea funciona sólo si la colocamos en $8043 (o en cualquier dirección que acabe en $43). Estos pequeños inconvenientes los resolveremos en los proximos makeX.bat.
Por último voy a mostrar el contenido de ldbytes.asm:
Código: Seleccionar todo
ultra exx ; salvo de, en caso de volver al cargador estandar
ld c, 0
ultra1 defb $26
ultra2 jp nz, $053f ; return if at any time space is pressed.
ultra3 ld b, 0
call $05ed ; leo la duracion de un pulso (positivo o negativo)
jr nc, ultra2 ; si el pulso es muy largo retorno a bucle
ld a, b
add a, -16 ; si el contador esta entre 10 y 16 es el tono guia
rr h ; de las ultracargas, si los ultimos 8 pulsos
jr z, ultra1
add a, 6 ; son de tono guia h debe valer ff
jr c, ultra3
ld a, $d8 ; a' tiene que valer esto para entrar en raudo
ex af, af'
dec h
jr nz, ultra1 ; si detecto sincronismo sin 8 pulsos de tono guia retorno a bucle
call $05ed ; leo pulso negativo de sincronismo
inc h
ultra4 ld b, 0 ; 16 bytes
call $05ed ; esta rutina lee 2 pulsos e inicializa el contador de pulsos
call $05ed
ld a, b
cp 12
rl h
jr nc, ultra4
ld a, h
exx
ld c, a ; guardo checksum en c'
push hl ; pongo direccion de comienzo en pila
exx
pop de ; recupero en de la direccion de comienzo del bloque
inc c ; pongo en flag z el signo del pulso
ld bc, $effe ; este valor es el que necesita b para entrar en raudo
ld hl, leehi
jr z, ultra6
ld (lowh1+1), hl
ultra5 in f, (c)
jp pe, ultra5
jr ultra8 ; salto a raudo segun el signo del pulso en flag z
ultra6 ld (lowh0+1), hl
ultra7 in f, (c)
jp po, ultra7
add hl, hl
ultra8 ld h, table>>8
jr lowhi ; salto a raudo
lowh0 call leelo ;17 61
ex af, af' ;4
ld a, r ;9
ld l, a ;4
ld b, (hl) ;7
lowhi ld a, $0d+3*sf48 ;7
ld r, a ;9
ex af, af' ;4
lowh1 call leelo ;17 65/65
jr nc, lowh2 ;7/12
xor b ;4
xor $9c ;7
ld (de), a ;7
inc de ;6
ld a, $dc ;7
jp lowh0 ;10
lowh2 xor b ;4
add a, a ;4
add a, a ;4
out (c), b ;12
jr lowh0 ;12
leehi REPT 14
in f, (c)
ret pe
ENDM
jr ultra9
leelo REPT 14
in f, (c)
ret po
ENDM
ultra9 pop hl
exx ; ya se ha acabado la ultracarga (raudo)
dec de
ld b, e
inc b
inc d
ultraa xor (hl)
inc hl
djnz ultraa
dec d
jr nz, ultraa ; con JP ahorro algunos ciclos
xor c
ret z ; si no coincide el checksum salgo con carry desactivado
ei
rst $08 ; error-1
defb $1a ; error report: tape loading error
table .15 defb $ec
.12 defb $ed
.12 defb $ee
.13 defb $ef
Empiezo por lo más importante, la rutina muestreadora:
Código: Seleccionar todo
leehi REPT 14
in f, (c)
ret pe
ENDM
jr ultra9
leelo REPT 14
in f, (c)
ret po
ENDM
ultra9 pop hl
exx
Las directivas REPT repiten el código de su interior tantas veces como le hayamos indicado, en este caso, 14. El código sería equivalente a esto:
Código: Seleccionar todo
leehi in f, (c)
ret pe
in f, (c)
ret pe
... (14 veces en total)
in f, (c)
ret pe
jr ultra9
leelo in f, (c)
ret po
in f, (c)
ret po
... (14 veces en total)
in f, (c)
ret po
ultra9 pop hl
exx
Sería un bucle desenrollado que lee el puerto EAR cada 17 ciclos. Desgraciadamente al no ser múltiplo de 8 se ve mas afectada por la contención, y si nos pilla en mitad de una línea las lecturas de puerto tienden a 20 ciclos. Esto con una rutina de 16 ciclos no pasaba, si hay contención solo le afecta a la primera lectura (entre 0 y 6 ciclos de penalización), las demás se libran.
Otra ventaja de este método (lo llamaré método 3) es que es muy sencillo detectar el fin de la carga, como en ningún símbolo legal llegamos a hacer 14 lecturas (en uno de los pulsos) podemos indicar el fin de la carga con un símbolo especial más largo, y por tanto salimos por ultra9. El pop hl es para equilibrar la pila, ya que a leelo o leehi hemos entrado con un call.
Entendiendo esta rutina todo lo demás es coser y cantar. Desde ultra a ultra8 tenemos código de inicialización, que incluye detección de tono guía y lectura de byte de checksum. Lo que hay entre lowh0 y el final de lowh2 es el bucle principal, se estará ejecutando todo el rato que dure la ultracarga. Consta de 2 segmentos. Desde lowh0 hasta lowh1 leemos el primer pulso, contamos el número de lecturas a puerto extrayéndolo del registro R y miramos en una tabla a qué símbolo se corresponde tal número de lecturas (en el registro B). Finalmente inicializamos el registro R a un valor en el cual concuerden el número de las lecturas con el contenido de la tabla. En este caso dicho valor es $0d+3*sf48, lo que quiere decir que vale 13 a 44100Hz y 16 a 48000Hz. Al ser un valor fijo significa que la rutina sólo funciona en dicha ubicación: $8043.
Veamos ahora el segundo segmento del bucle principal, que va desde lowh1 hasta el último "jr lowh0". Este segmento se bifurca en 2 ramas: en la "no carry" introducimos el símbolo que acabamos de leer (está en los 2 bits menos significativos de B) en el registro A y luego rotamos dos bits a la izquierda, preparándolo para el siguiente símbolo. El último out (c), b muestra el borde, que será uno de 4 colores (de 4 a 7) dependiendo del símbolo que acabamos de leer. La otra rama, la rama "carry", ocurre cuando hemos leído los 4 símbolos del byte y procedemos a cargar el byte en memoria, incrementar el puntero DE e inicializar el registro A para el siguiente símbolo.
En total tenemos 61 ciclos del primer segmento y 65 en el segundo segmento (las 2 ramas están balanceadas), por esa razón procuramos que si los dos pulsos tienen distinta duración, que el segundo sea el más largo.
El resto del código (desde ultra9 hasta "defb $1a") lo que hace es comprobar que el checksum indicado al comienzo coincide con el del bloque que acabamos de leer y muestra error en caso de que no coincidan. Esto lo hacemos después, en diferido, porque nuestra rutina es muy crítica en tiempos. En realidad habría sido más sencillo llevar la cuenta del checksum en un registro (en la rutina estándar es el registro H) y xorearlo con cada byte que acabáramos de leer.
Ya sólo me queda por explicar la tabla. Los valores son $ec, $ed, $ee y $ef en lugar de algo más lógico como $00, $01, $02 y $03. Lo hacemos así porque este mismo byte es el que escribimos en el puerto $FE (por eso cambia el borde). Las restricciones vienen impuestas porque el bit 3 debe valer 0 y el bit 4, 1 para que las posteriores lecturas se hagan correctamente, digamos que el valor debe ser así %ABC01DXX. Los valores XX no dependen de nosotros sino del símbolo que acabamos de leer. Los otros (A,B,C y D) pueden valer lo que queramos, pero si lo ponemos a 1 mejor, así evitamos conflictos con el teclado.
Otra restricción que tiene la tabla es que no debe pasar el límite de media página (128 bytes) ya que el siguiente byte volvería al principio. Un ejemplo, tenemos una tabla que empieza en $8070 y acaba en $81A3. Muy mal, porque el registro R pasaría de $7F a $00 (el bit alto no cambia) y por tanto después de $807F se leería $8000. Esto segmentaría la tabla dejando un hueco inútil. En nuestro caso no hay problema porque estamos en una dirección fija, y la tabla se encuentra entre $8126 y $8159. Por último decir que sólo 1/3 de los valores de la tabla son los que realmente se usan porque en cada lectura incrementamos R en 3, y 3 es un número muy chungo de tratar para un procesador que no puede multiplicar ni dividir directamente, así que preferimos desperdiciar unos pocos bytes (en concreto 34 de los 52) y listo.
Bueno ya he explicado lo más gordo. A partir de ahora todo será mejorar la rutina que acabamos de crear, ya sea para hacerla más flexible o para optimizar el tiempo de carga. Ah bueno, se me olvidaba, hay una parte del código que es automodificable, en concreto de los dos "call leelo" uno de ellos se convierte en "call leehi" en función del nivel con el que trabajemos. A priori toda señal de audio es neutral, vamos que da igual el valor que le pongas a una muestra porque a la salida la puedes tener invertida o no (dependiendo de si el número de veces que la circuitería ha invertido la polaridad es par o impar). Así que nosotros debemos de tratar los dos casos posibles. Como todo ciclo está compuesto por dos pulsos, tendremos un primer caso en que el primer pulso tengamos nivel negativo (leemos un 0 del puerto EAR) y el segundo positivo (leemos 1), y un segundo caso con el comportamiento contrario (primero positivo y segundo negativo). Esto lo detectamos en el primer pulso de sincronismo. Lo mejor es generar un WAV con la señal invertida a uno dado y comprobar que los dos WAVs carguen correctamente.
make2.bat. Sacamos el descompresor del cargador, rutina en dirección fijaTenemos 3 bloques lógicos, el primero (2 bloques físicos) carga a 1500 bps y los otros dos a 21333 bps. ¿Cómo aceleramos la carga? Pues moviendo todo lo que podamos del primer bloque al segundo. ¿Y qué es lo único que no necesitamos del primer bloque en ese momento? Pues el descompresor, ya que lo necesitaremos por primera vez tras la carga del segundo bloque, para descomprimir la pantalla de carga.
Hay 2 formas de hacer esto:
- Ensamblamos sólo el descompresor en un archivo aparte (dzx7b_81b9.asm) como hicimos en el make3.bat de la lección anterior, y luego concatenamos el archivo resultante (usando "copy /b") con la pantalla comprimida para el segundo bloque.
- Creamos un tercer archivo binario de salida en loader2.asm, concretamente dzx7screen2.bin, que genere exactamente el mismo contenido que el punto anterior.
Ninguna solución es buena, pero la segunda me parece menos mala. Yo prefiero la convención de que cada archivo .asm genere un único archivo .bin con el mismo nombre, aunque en este caso merece la pena hacer una excepción, al menos desde mi punto de vista. Sería tan fácil como añadir estas líneas al final del archivo loader2.asm:
Código: Seleccionar todo
; Bloque datos (descompresor y pantalla de carga)
output dzx7screen2.bin
org $8200-67-4
dzx7b include dzx7b_fast.asm
defb 0, 0, 0, 0
incbin manic.scr.zx7b
scrend
Los 4 bytes que dejo a cero son el hueco que necesitamos (safety offset) para descomprimir ya que nuestro descompresor es hacia atrás y no queremos mover datos con LDIR después. El resto del código en loader2.asm apenas ha cambiado, hemos quitado el include dzx7b_fast.asm y movido el destino del primer bloque desde $8000 a $8043, de lo contrario no funcionaría el ultracargador, recordemos que es de ubicación fija.
El make2.bat se ha simplificado, estas 2 líneas ya no las necesitamos:
Código: Seleccionar todo
for %%A in (manic.scr.zx7b) do set _fileSize=%%~zA
echo define scrsize %_fileSize% > define.asm
Puesto que la longitud del comprimido se puede calcular fácilmente haciendo una resta entre etiquetas.
Como veis este cambio no tiene mucha chicha. Hemos adelgazado la carga estándar en 67 bytes, que en tiempo serían 67*8/1500= 357 ms. La mejora total al moverlo sería esta cifra menos lo que hemos engordado el segundo bloque 67*8/21333= 25 ms. La diferencia es de 332 ms, es decir, un tercio de segundo. Que sí, que es poco, pero lo importante es que vamos en buen camino, prosigamos con más optimizaciones.
make3.bat. Hacemos el ultracargador reubicableEl siguiente cambio es quizás el más interesante. Queremos reutilizar el mismo ultracargador en otros juegos y muy probablemente necesitemos ubicarlo en otra dirección, por ejemplo la parte más alta de RAM. El código tal cual lo tenemos no nos sirve.
Por supuesto el make3.bat y el loader3.asm es casi idéntico a los anteriores, por eso no los muestro. Lo único que en loader3.asm voy a ubicar el ultracargador en $8000 en lugar de en $8043, así de paso comprobamos que es reubicable.
Muestro aquí el contenido del nuevo ldbytes3.asm:
Código: Seleccionar todo
IF ($ & $7f) < $4d
define a4d 1
table .15 defb $ec
.12 defb $ed
.12 defb $ee
.13 defb $ef
leelo REPT 14
in f, (c)
ret po
ENDM
ultra9 pop hl
exx ; ya se ha acabado la ultracarga (raudo)
dec de
ld b, e
inc b
inc d
ultraa xor (hl)
inc hl
djnz ultraa
dec d
jr nz, ultraa ; con JP ahorro algunos ciclos
xor c
ret z ; si no coincide el checksum salgo con carry desactivado
ei
rst $08 ; error-1
defb $1a ; error report: tape loading error
ELSE
define a4d 0
leelo REPT 14
in f, (c)
ret po
ENDM
ultra9 pop hl
exx ; ya se ha acabado la ultracarga (raudo)
dec de
ld b, e
inc b
inc d
ultraa xor (hl)
inc hl
djnz ultraa
dec d
jr nz, ultraa ; con JP ahorro algunos ciclos
xor c
ret z ; si no coincide el checksum salgo con carry desactivado
ei
rst $08 ; error-1
defb $1a ; error report: tape loading error
table .15 defb $ec
.12 defb $ed
.12 defb $ee
.13 defb $ef
ENDIF
leehi REPT 14
in f, (c)
ret pe
ENDM
jr ultra9
ultra exx ; salvo de, en caso de volver al cargador estandar...
ld c, 0
ultra1 defb $26
ultra2 jp nz, $053f ; return if at any time space is pressed.
ultra3 ld b, 0
call $05ed ; leo la duracion de un pulso (positivo o negativo)
jr nc, ultra2 ; si el pulso es muy largo retorno a bucle
ld a, b
add a, -16 ; si el contador esta entre 10 y 16 es el tono guia
rr h ; de las ultracargas, si los ultimos 8 pulsos
jr z, ultra1
add a, 6 ; son de tono guia h debe valer ff
jr c, ultra3
ld a, $d8 ; a' tiene que valer esto para entrar en raudo
ex af, af'
dec h
jr nz, ultra1 ; si detecto sincronismo sin 8 pulsos de tono guia retorno a bucle
call $05ed ; leo pulso negativo de sincronismo
inc h
ultra4 ld b, 0 ; 16 bytes
call $05ed ; esta rutina lee 2 pulsos e inicializa el contador de pulsos
call $05ed
ld a, b
cp 12
rl h
jr nc, ultra4
ld a, h
exx
ld c, a ; guardo checksum en c'
push hl ; pongo direccion de comienzo en pila
exx
pop de ; recupero en de la direccion de comienzo del bloque
inc c ; pongo en flag z el signo del pulso
ld bc, $effe ; este valor es el que necesita b para entrar en raudo
ld hl, leehi
jr z, ultra6
ld (lowh1+1), hl
ultra5 in f, (c)
jp pe, ultra5
jr ultra8 ; salto a raudo segun el signo del pulso en flag z
ultra6 ld (lowh0+1), hl
ultra7 in f, (c)
jp po, ultra7
add hl, hl
ultra8 ld h, table>>8
jr lowhi ; salto a raudo
lowh0 call leelo ;17 61
ex af, af' ;4
ld a, r ;9
ld l, a ;4
ld b, (hl) ;7
lowhi ld a, $-$b7-$36*a4d & $80|$67+3*sf48+table&$7f
ld r, a ;9
ex af, af' ;4
lowh1 call leelo ;17 65/65
jr nc, lowh2 ;7/12
xor b ;4
xor $9c ;7
ld (de), a ;7
inc de ;6
ld a, $dc ;7
jp lowh0 ;10
lowh2 xor b ;4
add a, a ;4
add a, a ;4
out (c), b ;12
jr lowh0 ;12
Se trata del mismo código que antes pero cambiando el orden de las rutinas, tablas... y con una directiva de ensamblado condicional. Veamos cómo funciona. Lo que hay dentro de los dos casos en el IF es lo mismo pero en distinto orden. En el primer caso tenemos table/leelo/ultra9 y en el segundo tenemos leelo/ultra9/table. Lo que hacemos es usar una ordenación u otra dependiendo del rango de ($ & $7f). $ es una variable (o directiva o como quieras llamarlo) que nuestro ensamblador reemplazará por la dirección que toca ser ensamblada justo en el punto donde se invoca. Como está al comienzo de ldbytes3.asm y hemos dicho que queríamos ubicar el ultracargador en la dirección $8000, pues esta variable $ se sustituye por $8000. Y como $8000 & $007f es igual a $00, y $00 es menor de $4d pues se ensamblaría la primera parte del IF.
¿Por qué hacemos todo este lío? Pues por la sencilla razón de que no podemos partir la tabla en dos, los 9 bits más significativos de la dirección de cada elemento de la tabla deben permanecer inalterables. Si nos ubicamos en $8000, la tabla comienza en $8000 y acaba en $8033. Podemos ir desplándonos byte a byte $8001/$8034, $8002/$8035 hasta llegar a un punto en el que no podemos desplazar más ya que se partiría la tabla. Ese punto es $804d, si la tabla comienza aquí, acabaría hipotéticamente en $8080, pero es que el registro R nunca valdrá $80, pasará de $7f a $00, partiendo el ultimo byte en $8000. Tendríamos un segmento de 1 byte en $8000 y otro de 51 bytes en $804d-$807f. Esto no es aceptable.
Por eso al intentar ensamblar en $804d se toma la otra rama del IF. En la otra rama la tabla no está al comienzo, sino desplazada por leelo y ultra9 (60 bytes) y por tanto la tabla comenzaría en $8089 (acaba en $80bc). Para el resto del rango, $4d-$7f, no hay problemas de que se segmente la tabla, por tanto tan sólo necesitamos 2 casos en nuestro ensamblado condicional. Si por ejemplo queremos ubicar nuestro ultracargador en $807f (último byte del rango) vemos cómo la tabla comenzaría en $80bb y acabaría en $80ee, sin llegar al límite $8100 que es donde estaría el problema.
¿Con esto hemos acabado? Casi, todavía me falta por explicar una instrucción:
Este chorizaco de fórmula para inicializar A se debe a que hay que tener en cuenta distintos casos. Anteriormente sólo contábamos con sf48 porque table tenía una dirección fija y no había que lidiar con ensamblado condicional. Seguramente se pueda simplificar un poco la fórmula pero no quiero tocarla porque funciona en todas las posibles ubicaciones. Evidentemente estas cosas no salen así porque sí, vas depurando, añadiendo cosas y comprobando que funciona en todos los casos. Si te pones a pensarlo igual sale algo más sencillo, pero, ¿para qué perder el tiempo pensando si con el método de ensayo/error acabas antes?
make4.bat. Comprimiendo el ultracargador y moviendo al bloque 2 algunas cosillas másEl ultracargador actualmente ocupa 279 bytes, que junto a los 61 bytes del código restante hacen 340 bytes de carga estándar. El objetivo es comprimir esos 279 para que se queden en menos y mover parte de los 61 bytes restantes al bloque 2. ¿Cómo comprimimos algo que de por sí es muy pequeño?
Pues a mano, con un descompresor escrito ad-hoc, sólo nos preocupamos por comprimir las partes más redundantes, que son los 2 bloques de IN/RET PE e IN/RET PO, y por supuesto la tabla. El resto lo copiamos sin comprimir, al ser tan poco código cualquier intento de hacerlo sería contraproducente (el descompresor ocuparía más que los bytes que se compriman).
Veamos cómo quedaría el código.
Código: Seleccionar todo
ini ld de, location+fin-ultrac+154-1
di
db $de, $c0, $37, $0e, $8f, $39, $96
ld hl, fin-1 +$5ccb-ini
ld bc, fin-ultrac
ld a, $e8
ini2 lddr
ini3 ex de, hl
ld bc, $0e12
ini4 ld (hl), a
dec hl
ld (hl), $70
dec hl
ld (hl), $ed
dec hl
djnz ini4
ex de, hl
IF (location & $7f) < $4d
define a4d 1
define leelo ultrac-102
define table ultrac-154
xor $08
jp po, ini2 +$5ccb-ini
ld h, d
ld l, e
dec de
ld (hl), $ef
ini5 ld c, $0d
ini6 lddr
dec (hl)
add a, a
jr c, ini5
jp ini7
ELSE
define a4d 0
define leelo ultrac-154
define table ultrac-94
xor $07
jp pe, ini7
ini5 ld b, 13
ini6 ld (de), a
dec e
djnz ini6
dec a
cp $eb
jr nz, ini5
ld a, $e0
jr ini2
ENDIF
Como veis, tampoco es para tanto, todo es ponerse. Lo único a destacar aquí es que las dos ramas condicionales las he optimizado por separado, y el descompresor de tabla resultante es bastante distinto (uno rellena la tabla con LDIR y el otro con LD (DE),A y DJNZ) pese a que ambos hacen lo mismo. Haciendo cuentas el descompresor ocupa 56 bytes y el stream (realmente está sin comprimir, sólo lo movemos) 143 bytes, dándonos un total de 199 bytes.
Por otro lado tenemos las cadenas de llamadas al ultracargador/compresor. En loader3.asm tenía este aspecto:
Código: Seleccionar todo
ld hl, dzx7b
ld de, scrend-dzx7b
call tr ultra
;.................................
ld hl, scrend-1
ld de, $5aff
call tr dzx7b
ld hl, $8200-4
ld de, binsize
call tr ultra
ld hl, $8200+binsize-4-1
ld de, $ffff
call tr dzx7b
jp $8400
¿Realmente necesitamos tener todo esto en el primer bloque? Pues no, para cargar el primer bloque sólo hacen falta las 3 primeras instrucciones (hasta la línea de puntos). El resto lo podemos cargar posteriormente. ¿Y esto cómo se hace? Ubicando las 3 primeras instrucciones al final, justo antes de donde vamos a cargar el siguiente bloque. El código que hay debajo del "call tr ultra" se cargará en la propia llamada. Veamos el código:
Código: Seleccionar todo
include ldbytes4.asm
ini7 ld hl, fin
ld de, dzxend-fin
call tr ultra
fin
; Bloque datos (descompresor y pantalla de carga)
output dzx7screen4.bin
org location+154-ultrac+fin
ld hl, dzxend-1
ld de, $5aff
call tr dzx7b
ld hl, $8200-4
ld sp, hl
ld de, binsize
call tr ultra
ld hl, $8200+binsize-4-1
ld de, $ffff
call tr dzx7b
jp $8400
dzx7b include dzx7b_fast.asm
incbin manic.scr.zx7b
dzxend
Como véis, hemos movido 31 bytes del bloque 1 (carga estándar) al bloque 2 (ultracarga). Tras este pequeño fragmento de código, el bloque 2 contiene el descompresor y la pantalla de carga comprimida. No muestro el make4.bat porque es idéntico al make3.bat. Al ldbytes4.asm le podéis echar un vistazo, es el mismo código qu ldbytes3.asm pero sin las partes comprimidas table/leelo/leehi y sin ensamblado condicional.
Ya sólo me falta hacer cuentas de lo que hemos ganado. Antes teníamos 279+61= 340 bytes de carga estándar. Ahora tenemos 299+9= 208 bytes (un byte más en la otra rama condicional), con lo que la mejora total es de 132 bytes. Por otro lado el siguiente bloque (archivo dzx7screen3/4.bin) ha pasado de 358 bytes a 385, engordando 27 bytes.
La mejora de tiempo sería de: 132*8/1500-27*8/21333= 0.694s. Unas 7 décimas de segundo, no está nada mal.
make5.bat. Reducciones drásticas finales.Voy a acabar la lección reduciendo aún más el tiempo de carga, de 9.67s a 8.61s. Por dos frentes, recortando al máximo las pausas (silencios) y los tonos guía y dándole una última vuelta de tuerca a la cantida de bytes de carga estándar.
El make5.bat tiene este aspecto:
Código: Seleccionar todo
freverse fast5.bin fast5.bin.rev
GenTape %_freq% manic5.wav ^
turbo 2168 667 735 ^
600 1600 1500 0 header.bin.fck ^
turbo 2168 667 735 ^
600 1600 1500 0 loader5.bin.fck ^
plug-ultra-3 -1 0 fast5.bin.rev ^
plug-ultra-3 50 0 dzx7screen5.bin ^
plug-ultra-3 100 0 manic.cut.zx7b
Hay una llamada a freverse de un bloque nuevo (fast5.bin.rev) que parece ser que lo transforma de alguna manera (ya lo explicaremos) y en la llamada a GenTape podemos ver ese nuevo bloque de ultracarga. Las reducciones en los bloques existentes son quitar los silencios de 500ms y acortar el tono guía del primer bloque ultracarga de 100 a 50ms. Esto daría una mejora de 1050ms, pero como no estamos contabilizando el silencio del final, nos quedamos en 550ms.
Hacemos un "dir loader5.bin" y vemos que ocupa 71 bytes. ¿Cómorrrr? ¿Qué hemos pasado de 208 a 71 bytes de carga estándar? ¿Dónde está el truco?
El truco está en introducir un bloque ultracargador más. No será tan rápido como el anterior (21333 bps) pero será bastante más rápido que la carga estándar. En concreto vamos a usar "Shawings Slow 3.5", que tiene una velocidad de 13714 bps. La idea es un poco retorcida: cargar un cargador grande y rápido usando otro cargador más lento y pequeño (pero bastante más rápido que la carga estándar).
Tendremos en loader5.bin un cargador "Shawings Slow 3.5" lo más sencillo y optimizado posible, en concreto se ha quedado en 71 bytes (es una mejora de una antigua versión de 74 bytes que usé en CargandoLeches). Luego tendremos fast5.bin, que es el código de nuestro cargador de antes, es decir hemos movido aquí lo que antes estaba en el bloque anterior de carga estándar. La reducción de 208 a 197 bytes se debe exclusivamente a que hemos eliminado el snippet Basic de Paolo Ferraris. Y todo lo demás está igual que antes.
No me voy a poner a explicar el código de este ultracargador, al estar tan optimizado es bantante críptico para leer. De todas formas dejo aquí el código, pero aviso que sin un depurador y altas dosis de paciencia es difícil de entender:
Código: Seleccionar todo
org $8edd+load-load2-fast6+fast
load ld de, load1-167
di
db $d6, $c0, $31, $0e, $8f, $39, $96
ld h, b
ld bc, $e2
ld a, c
ldir
jp $8edd+load1-load2-fast6+fast
load1 dec c
dec c
load2 ld h, b
ld (load8), a
xor 8
ld (load4), a
ld (loada), a
load3 inc h
in f, (c)
load4 db 0
dw load3
add hl, hl
jr nc, load2
ld de, $000d+sf48*2
load5 add hl, hl ; 11 11
jr c, load6 ; 7 12
push hl ; 11
inc sp ; 6
ld h, c ; 4
load6 ld l, $c0 ; 7 7
load7 add hl, de
in f, (c)
load8 db 0
defw load7
load9 add hl, de
in f, (c)
loada db 0
defw load9
dec h ; 4 4
add hl, hl ; 11 11
jr c, load5 ; 12 12
loadb ; 73 57
A modo de apunte, este cargador va rellenando la memoria hacia atrás (usa la pila para rellenar), por esa razón necesito una utilidad que me invierta el fichero (freverse). Por otro lado no me he querido complicar mucho haciendo otro generador de ultracargas (ultra.exe) distinto que me cree el bloque "Shawings Slow 3.5". Lo que hago es parchear el mismo de tal forma que si recibe un -1 en el parámetro "duración del tono guía" cambiamos la tabla de símbolos, generando "Shawings Slow 3.5" en lugar de "Shawings Raudo 2.25".
Ya sólo nos falta hacer las cuentas. La reducción de carga estándar es de 138 bytes (208-70). Por otro lado ahora cargamos 197 bytes a 13714 bps que antes no estaban ahí. Entonces tenemos: 138*8/1500-197*8/13714= 736ms-115ms= 621ms.
Con esto y un bizcocho, nos vemos en la lección 8. Por cierto, será la última lección de este tutorial y trataré el tema de la desprotección de juegos.