Tutorial de optimización de cintas y ultracargas

antoniovillena

Tutorial de optimización de cintas y ultracargas

Mensajepor antoniovillena » 01 May 2014 22:42

Lección 1

En esta lección tan sólo voy a generar la cinta original del Manic Miner, para luego ir optimizando a medida que avanzamos en el tutorial.

Para seguirlo hay que instalarse los siguientes programas:

Examinemos el fuente que genera la cinta de Manic Miner y expliquemos:

Código: Seleccionar todo

join
ZMakeBas -r -o loader.bin loader.bas
SjAsmPlus manic.asm
GenTape manic.tap                       ^
    basic ManicMiner  10  loader.bin  ^
    hdata mmm       5900  screen.bin  ^
    hdata mm1       8000  manic.bin


La primera línea es para generar la pantalla de carga, es un programa ad-hoc hecho para esta carga en cuestión que es bastante peculiar. Sólo usa la parte de los atributos, en concreto el tercio central (256 bytes). A parte todos los atributos tienen el flash (y el brillo) activado, por lo que va conmutando entre los distintos colores. La gracia está en que se codifica una pantalla para la tinta y otra para el fondo, por lo que resulta una animación con el texto MANIC y MINER sin gastar ciclos de CPU. Partiendo de screen1.png y screen2.png se genera screen.bin (los 256 bytes).

La siguiente línea genera el binario que corresponde al fichero BASIC loader.bas, que se trata del cargador. El cargador no tiene mucha miga:

Código: Seleccionar todo

  10 CLEAR 30000
  20 PAPER 0: INK 0: CLS : LOAD ""CODE : LOAD ""CODE
  30 RANDOMIZE USR 33792


Baja RAMTOP a 30000 para ubicar el juego, pone el fondo negro para que los tercios superior e inferior no desentonen con la pantalla de carga, carga dos bloques de datos y salta al juego (dirección 33792).

Prosigamos con el archivo bat. La siguiente línea ensambla el código fuente, generando manic.bin, que ocupa 32768 bytes y parte de la posición 32768 (ocupa toda la RAM alta).

Por último tenemos el generador de cinta (GenTape), que crea los 3 siguientes bloques (6 si contamos las cabeceras):
  1. Bloque BASIC cargador, que hemos explicado antes, con el nombre ManicMiner y que se autoejecuta en la línea 10.
  2. Bloque de datos con la pantalla de carga, llamado mmm que ocupa 256 bytes y ubicado en la dirección hexadecimal $5900 (atributos del tercio central de la pantalla).
  3. Bloque de datos con el juego, llamado mm1, que tiene una longitud de $8000 en hexadecimal (nótese que la longitud nunca se especifica, viene determinada por el tamaño del archivo) y comienza también en $8000.

Eso es todo, una vez ejecutamos el make.bat ya tenemos listo nuestro manic.tap, exactamente igual que la cinta original. En la siguiente lección pondremos una pantalla de carga más normalita y trataremos de crear nuestro propio cargador en código máquina.

Pincha aquí para bajar el archivo de la lección

antoniovillena

Re: Tutorial de optimización de cintas y ultracargas

Mensajepor antoniovillena » 01 May 2014 22:42

Lección 2

Partimos del tutorial anterior. Este es el archivo make.bat que lo generaba:

Código: Seleccionar todo

zmakebas -r -o loader.bin loader.bas
SjAsmPlus manic.asm
GenTape manic.tap                       ^
    basic ManicMiner  10  loader.bin  ^
    hdata mmm       5900  screen.bin  ^
    hdata mm1       8000  manic.bin


La primera optimización que vamos a hacer es transformar el cargador, pasándolo de BASIC en código máquina. ¿Por qué? Pues muy sencillo, un cargador código máquina nos permite cargar bloques sin cabecera, por tanto reducimos el tiempo de carga.

¿Qué es esto de la cabecera? Es un bloque muy corto que sólo contiene información sobre el nombre del programa, la dirección de comienzo, la longitud, etc... cosas que podemos evitar si hacemos una llamada a la rutina de carga desde ensamblador. El único bloque que tendrá cabecera es el primero, obvio porque lo cargamos desde BASIC con LOAD"". Este sería el aspecto del nuevo make.bat:

Código: Seleccionar todo

SjAsmPlus loader.asm
SjAsmPlus manic.asm
GenTape manic.tap                       ^
    basic ManicMiner  10  loader.bin  ^
     data                   screen.bin  ^
     data                   manic.bin


Como véis, la única diferencia es que el archivo loader.bin lo ensamblamos desde loader.asm (antes lo obteníamos de loader.bas) y los bloques de pantalla y juego pasan de ser hdata (bloques de datos con cabecera) a data (bloques de dato sin cabecera). Han perdido los 2 primeros parámetros (nombre y dirección de carga), quedándose sólo con el nombre del binario a cargar. Por supuesto debemos incluir dicha dirección en el cargador. Desde Basic no hacía falta (se cargaban con LOAD""CODE) porque esta información la obtenían de la cabecera.

Recuerdo el antiguo archivo BASIC, el cual tenemos que pasar a ensamblador:

Código: Seleccionar todo

  10 CLEAR 30000
  20 PAPER 0: INK 0: CLS : LOAD ""CODE : LOAD ""CODE
  30 RANDOMIZE USR 33792


Para no meternos en harina tan pronto, hagamos un resumen de lo que tiene que hacer nuestro cargador, facilito los valores hexadecimales con el signo dolar delante:

  1. Poner la pila a 30000 ($7530)
  2. Poner toda la pantalla negra
  3. Cargar un bloque desde cinta desde la posición 22784 ($5900) con una longitud de 256 ($100) bytes. Esto sería la pantalla de carga.
  4. Cargar el siguiente bloque (juego) desde posición 32768 ($8000) con longitud 32768 ($8000), es decir, hasta el final de la RAM.
  5. Saltar a la dirección 33792 ($8400)

Bueno, antes de nada supongo que tenéis una mínima base de ensamblador. Si no es así, no os asustéis, vamos a usar muy pocas instrucciones, tan sólo hay que repetir los pasos con distintos valores.

Lo primero es saber dónde se encuentra la rutina de carga y qué parámetros necesita. Dicha rutina se llama LD_BYTES, y a parte de para cargar bloques también se usa para verificarlos. También hay que saber otra cosa, para diferenciar bloques de datos de bloques de cabecera se usa el byte de FLAG, que básicamente es el primer byte del bloque pero que no forma parte del bloque en sí (no se carga en memoria), es simplemente para comprobar que lo que el tipo de bloque que estamos leyendo es correcto, y en caso contrario mostrar un mensaje de error.

Bueno pues el byte de FLAG para un bloque de cabecera es 0 ($00), mientras que el byte de FLAG para un bloque de datos es 255 ($FF), así de sencillo. Como vamos a llamar a la rutina de carga desde ensamblador, en realidad podemos codificar el FLAG que queramos, pero no vamos a complicarnos, usaremos siempre 255 ($FF) para bloques de datos, que es el tipo de bloque que genera GenTape cuando especificamos "data".

La rutina LD_BYTES se encuentra en la dirección 1366 ($0556), y recibe los siguientes parámetros:
  • Flag carry. A 1 si vamos a cargar datos, a 0 si vamos a verificar. Como vamos a cargar siempre ponemos un 1.
  • Registro A. Byte FLAG que esperamos para el siguiente bloque. Como es un bloque de tipo datos cargamos 255 ($FF).
  • Registro IX. Dirección de comienzo del bloque.
  • Registro DE. Longitud del bloque.

Parece complicado, pero en realidad el código ensamblador para generar esto es muy sencillo y siempre el mismo. Por ejemplo si queremos cargar desde $4000 un bloque con longitud $1B00, lo que vendría a ser una pantalla (a partir de ahora por comodidad sólo pondré valores en hexadecimal).

Código: Seleccionar todo

        scf
        ld      a, $ff
        ld      ix, $4000
        ld      de, $1b00
        call    $0556


¿Veis que sencillo? La primera instrucción pone el carry a 1, las 3 siguientes cargan registros y la última hace la llamada a la rutina de carga. Lo siguiente es una pequeña mejora, aprovechando que carry está a 1, podemos cargar el registro A a $ff con otra instrucción más corta (1 byte en lugar de 2).

Código: Seleccionar todo

        scf
        sbc     a, a
        ld      ix, $4000
        ld      de, $1b00
        call    $0556


Ya sabemos hacer lo importante del cargador. El resto en realidad es muy sencillo. Lo primero es poner la pila a $7530, para que no haya posibilidad de cuelgue si cargamos algo por encima (recordad que el juego estará ubicado entre $8000 y $FFFF. Esto se hace con una única instrucción:

Código: Seleccionar todo

        ld      sp, $7530


Lo siguiente es poner la pantalla negra. Esto se hace normalmente con la instrucción LDIR, no es lo más rápido pero sí lo más sencillo. Como sólo vamos a usar atributos en nuestra pantalla de carga y la zona bitmap que nos interesa (la central) ya está inicializada a $00, pues nos basta con poner a cero toda la zona de atributos. Al poner en negro tanto la tinta como el fondo, se borrarán todos los mensajes de cabecera (el Program: ManicMiner).

Código: Seleccionar todo

        ld      hl, $5800
        ld      de, $5801
        ld      bc, $2ff
        ld      (hl), l
        ldir


Por último sólo tenemos que saltar a la dirección que comienza el juego ($8400), usando la instrucción JP.

Código: Seleccionar todo

        jp      $8400


Todo el cargador junto sería así:

Código: Seleccionar todo

        ld      sp, $7530
        ld      hl, $5800
        ld      de, $5801
        ld      bc, $2ff
        ld      (hl), l
        ldir
        scf
        sbc     a, a
        ld      ix, $5900
        ld      de, $0100
        call    $0556
        scf
        sbc     a, a
        ld      ix, $8000
        ld      de, $8000
        call    $0556
        jp      $8400


No es muy complicado, ¿verdad?. Pues bueno, ahora la clave está en cómo pasarlo a binario y sobre todo en cómo incluirlo en el primer bloque BASIC. Hay muchos mecanismos para incrustar y ejecutar código máquina desde BASIC, el que os voy a mostrar es el más corto de todos, se basa en un OVER USR 23755 ($5ccb) truncado, que es esta línea:

Código: Seleccionar todo

        db      $de, $c0, $37, $0e, $8f, $39, $96 ;OVER USR 7 ($5ccb)


Primero ejecutamos cuatro bytes de ensamblador (si no nos llega rellenamos con NOP), luego iría esta línea db, y finalmente el resto del código. Dado que la primera instrucción está en $5ccb debemos poner el ORG $5ccb antes de nada y las directivas que use nuestro ensamblador para generar el loader.bin, que en nuestro caso es: output loader.bin. En resumen, el archivo loader.asm en su totalidad es:

Código: Seleccionar todo

        output  loader.bin
        org     $5ccb
        ld      sp, $7530
        di
        db      $de, $c0, $37, $0e, $8f, $39, $96 ;OVER USR 7 ($5ccb)
        ld      hl, $5800
        ld      de, $5801
        ld      bc, $2ff
        ld      (hl), l
        ldir
        scf
        sbc     a, a
        ld      ix, $5900
        ld      de, $0100
        call    $0556
        scf
        sbc     a, a
        ld      ix, $8000
        ld      de, $8000
        call    $0556
        jp      $8400


Por último, dado que la pantalla de este juego es un poco peculiar, he creado una pantalla de carga alternativa a tamaño completo (6912 bytes), para que podáis aplicar el cargador a vuestros juegos. Las modificaciones están en make2.bat y en loader2.asm en el mismo zip de la lección.

Bueno, eso es todo por hoy. Espero que os haya sido útil la lección. En la siguiente no tengo muy claro lo que hacer, no sé si meter cosas turbo o explicar la compresión, ya veremos.

Pincha aquí para bajar el archivo de la lección

antoniovillena

Re: Tutorial de optimización de cintas y ultracargas

Mensajepor antoniovillena » 02 May 2014 00:06

Lección 2 y media

Antes de meterme en la lección 3 os voy a mostrar esta miniguía para analizar los TAPs/TZXs. La mejor herramienta para esto, desde mi punto de vista es Tapir. Es muy sencilla y se pueden hacer muchas cosas. Tiene cosas interesantes como un visor de código BASIC, de código máquina, de pantallas.

Lo más útil para mí es que se pueden extraer bloques binarios. Aunque bueno, la idea es editar archivos TZX copiando y pegando bloques entre el panel Left y Right. No guarda en formato TAP pero sí los reconoce y los lee. Suficiente para nuestro análisis.

Otra herramienta muy útil es un editor hexadecimal. Nos servirá para comprender el formato TAP. También se puede emplear para hacer pokes en los propios TAPs, aunque luego habría que corregir el checksum con Tapir. Yo uso HxD como editor, no es nada del otro mundo pero es open source y hace lo que necesito.

Os muestro 2 pantallazos y os lo explico. Lo suyo es que vayáis experimentando con estas utilidades hasta conseguir cierto manejo.

En este primer pantallazo muestro en Tapir el TAP de la lección 1, con el código BASIC abierto en ventana. Además he cargado detrás el mismo archivo con el editor hexadecimal, y he marcado los 3 primeros bloques (el primero en rojo, el segundo en azul y el tercero en verde). Cada bloque se compone de 4 segmentos:
  1. Longitud del bloque, 2 bytes. Si es cabecera siempre es 19 ($13 $00).
  2. Byte FLAG, 1 byte. $00 para cabecera, $FF para datos.
  3. Datos. Es el segmento grande del bloque.
  4. Checksum, 1 bytes. Es la función XOR de todos los datos del bloque, incluyendo el byte FLAG. Sirve para detectar errores de carga en cinta.

Imagen

En este segundo pantallazo cargo los dos TAPs de la segunda lección, uno en cada panel. Dejo abierta la ventana que corresponde a la pantalla de presentación del segundo TAP.

Imagen

antoniovillena

Re: Tutorial de optimización de cintas y ultracargas

Mensajepor antoniovillena » 04 May 2014 11:05

Lección 3

En esta tercera lección vamos a introducir un poco de teoría. Como ya no vamos a modificar más el manic.asm, partimos del manic.bin de 32768 bytes y comentamos la línea del make.bat que ensambla dicho archivo. Además, en esta lección tampoco vamos a tocar el loader.bin, así que ni siquiera necesitamos el ensamblador:

Código: Seleccionar todo

rem SjAsmPlus loader.asm
rem SjAsmPlus manic.asm
GenTape                     manic.tzx   ^
    basic ManicMiner  10  loader.bin  ^
     data                   manic.scr   ^
     data                   manic.bin


Habréis observado un manic.tzx en lugar del anterior manic.tap. Esto es así porque en esta lección vamos a manejarnos a bajo nivel y el formato TAP no nos vale. Los bloques que ya hemos visto: "basic", "hdata" y "data" son los de más alto nivel, es decir, generan cargas estándar con tiempos estándar. Con estos 3 bloques podemos generar tanto TAPs, como TZXs, como WAVs tan sólo cambiando el nombre del archivo.

Lo que vamos a hacer en un primer momento es generar un archivo TZX equivalente pero con otros componetes de más bajo nivel (que no permiten crear TAPs), para luego hacer ciertos ajustes que acorten el tiempo de carga. Pero antes de introducir estos nuevos bloques, un poco de teoría.

Lo primero de todo es distinguir bloques lógicos de bloques físicos. Un bloque "basic" es un bloque lógico compuesto por 2 bloques físicos (cabecera y datos). Lo mismo pasa con el bloque "hdata". Sin embargo el bloque lógico "data" contiene un único bloque físico de datos. Esto se supone que lo sabíamos de anteriores lecciones pero nunca viene mal darle un repaso.

Un bloque físico contiene 4 partes diferenciadas:
  • Tono guía. En inglés se suele usar la palabra "pilot" en lugar de "leader tone". Es un tono grave de 808Hz que avisa cuando va a comenzar un nuevo bloque. En carga estándar viene indicado por las bandas rojo y cian. Tiene distinta duración dependiendo de si el bloque físico es de cabecera o de datos: si es de cabecera dura unos 5 segundos; si es de datos, 2 segundos. Durante el primer segundo la ROM suele esperar sin hacer nada, aunque esto también lo hace si hay cualquier ruído en la cinta. Luego se asegura de que haya 512 pulsos guía después (unos 320ms) para empezar a detectar el sincronismo. La duración de un pulso (media onda) del tono guía es de 2168 ciclos de reloj. El reloj base que se toma es de 3.5Mhz (modelos 48K), siendo 285.7 nanosegundos la duración de un ciclo.
  • Sincronismo. En inglés "sync". Son dos pulsos cortos de duración 667 y 735 ciclos respectivamente. Sirven para indicar que ya se ha acabado el tono guía y que lo que viene después son los datos. El primero es un pelín más corto por retardos de instrucciones (en la rutina SAVE). La razón de que sean dos es para equilibrar ondas asimétricas. Si el azimut está muy alto (o muy bajo) no resultaría raro tener un tono guía de 1500 ciclos alternado con otro de 2836 ciclos. No habría ningún problema porque la suma es 2*2168 ciclos y se detectan de 2 en 2 (o en onda completa).
  • Datos. Aquí se codifican bit a bit todos los datos del bloque, incluido el byte flag de comienzo y el byte checksum del final. Se envian primero los bits de mayor peso y cuando llegamos al bit 0 pasamos al siguiente byte. En carga estándar se señaliza con bandas azules y amarillas en el borde. El tipo de modulación que se emplea es muy sencilla, se llama FSK, y se trata de codificar el bit 0 con dos pulsos seguidos de 855 ciclos cada uno, y el bit 1 con dos pulsos del doble de duración (1710 ciclos).
  • Pausa. Es un momento de silencio que se incluye al final de cada bloque (o al comienzo según se mire) para dar tiempo al Z80 a que haga lo que tenga que hacer, por ejemplo descomprimir una pantalla o mostrar una pequeña animación. También para que sea más fácil reconocerlos por el oído, si estamos buscando un bloque en concreto. Su duración difiere si el bloque físico es de datos o de cabecera. Tras un bloque de cabecera se inserta una pausa de 1 segundo; si es de datos son 2. La razón: tras una cabecera siempre se ejecuta el mismo código de ROM de duración fija (y pequeña). Con un segundo hay de sobra para el reconocimiento acústico.

Vista ya la teoría entremos en materia. Con GenTape podemos generar bloques físicos con los parámetros de tiempo que queramos, o incluso a más bajo nivel cualquier parte de un bloque físico (las que acabo de explicar) por separado.

Para generar bloques enteros tenemos el comando "turbo", mientras que para generar partes por separado tenemos los comandos "pilot", "pulse", "pure" y "pause" respectivamente. Como tenemos 2 bloque físicos (pantalla de carga y juego) voy a codificar el primer bloque con "turbo", mientras que en el segundo bloque emplearemos todo lo demás.

Una cosa muy importante. Tanto en "turbo" como en "pure" estamos trabajando a bajo nivel, y a diferencia de los tipos anteriores ("basic", "hdata", "data") no se insertan ni el byte flag al comienzo ni el checksum al final. Resumiendo, hay que coger un editor hexadecimal y añadir manualmente esos 2 bytes a los binarios. Para saber cuál es el checksum necesario puedes probar primero con un bloque "data" y ver cuál es el valor correcto (con Tapir o con un editor hexadecimal). Esto lo indico en el binario (que he añadido flag y checksum) añadiendo _flag_chk al final del nombre. Así, a "manic_flag_chk.scr" le he insertado los bytes $FF y $FF al principio y al final, mientras que en "manic_flag_chk.bin" han sido $FF y $19.

Siguiendo el orden que se muestra en la ayuda...

Código: Seleccionar todo

| turbo <pilot_ts> <syn1_ts> <syn2_ts> <zero_ts> <one_ts>
                       <pilot_ms> <pause_ms> <input_file>


...voy introduciendo los datos estándar, que son los mismos que he explicado antes. Al tener el tipo "turbo" una gran cantidad de parámetros yo suelo partir la línea en dos para no hacerme un lío, pero esto ya depende de lo que cada uno considere:

Código: Seleccionar todo

    turbo 2168   667   735                      ^
      855 1710  1980  2000  manic_flag_chk.scr  ^


Con esto ya tenemos un bloque físico idéntico al anterior, pero escrito a bajo nivel. Recuerdo que el bloque anterior se codificaba así:

Código: Seleccionar todo

     data                   manic.scr   ^


Esta es la pantalla de carga. Nos queda el último bloque, el que contiene el juego. Como he avanzado antes aquí vamos a emplear todos los subloques existentes, excepto el "pause", ya que el bloque "pure" te permite añadir una pausa al final.

Veamos primero la ayuda de los tipos que vamos a usar:

Código: Seleccionar todo

| pilot <pilot_ts> <pilot_ms>
| pulse <M> <pulse1_ts> <pulse2_ts> .. <pulseM_ts>
|  pure <zero_ts> <one_ts> <pause_ms> <input_file>


Recuerdo que el bloque a generar es equivalente a este:

Código: Seleccionar todo

     data                   manic.bin


Pues bien, siguiendo los valores de ciclos y duración que he explicado en la teoría, para generar el último bloque lo hacemos de la siguiente manera:

Código: Seleccionar todo

    pilot 2168  1980                            ^
    pulse    2   667   735                      ^
     pure  855  1710  2000  manic_flag_chk.bin


Pues ya está, ya hemos completado el archivo bat. En este caso lo llamaremos make2.bat para diferenciarlo del make.bat en alto nivel.

make2.bat

Código: Seleccionar todo

rem SjAsmPlus loader.asm
rem SjAsmPlus manic.asm
GenTape                     manic2.tzx          ^
    basic ManicMiner  10  loader.bin          ^
    turbo 2168   667   735                      ^
      855 1710  1980  2000  manic_flag_chk.scr  ^
    pilot 2168  1980                            ^
    pulse    2   667   735                      ^
     pure  855  1710  2000  manic_flag_chk.bin


Llegados aquí ya estamos listos para empezar a tunear la carga. Mi idea es acortar la duración lo máximo que pueda sin perder fiabilidad de carga (o en todo caso que se pierda muy poca). Para ello copiamos el make2.bat en un make3.bat y vamos cambiando los valores numéricos. Os recomiendo que probéis vosotros mismos: cambiáis un valor, generáis el tzx y veis si carga bien. Repetís el proceso hasta que estéis satisfechos. No se trata de conseguir grandes mejoras, tened en cuenta que seguimos con carga estándar, no hemos modificado el programa cargador. Para que os hagáis una idea, desde manic2.tzx hasta manic3.tzx hemos pasado de una duración de 3:25 a otra de 2:41.

Código: Seleccionar todo

rem SjAsmPlus loader.asm
rem SjAsmPlus manic.asm
GenTape                     manic3.tzx          ^
    basic ManicMiner  10  loader.bin          ^
    turbo 2168   667   735                      ^
      600 1600  1500     0  manic_flag_chk.scr  ^
    pilot 2168  1500                            ^
    pulse    2   667   735                      ^
     pure  600  1600     0  manic_flag_chk.bin


Dejo como ejercicio convertir a bajo nivel el tipo "basic" y recortar los tiempos de la misma forma que hemos hecho en los dos últimos bloques. Ojo, es un bloque lógico con 2 bloques físicos, os recomiendo que uséis Tapir para dumpear los binarios (tanto la cabecera como los datos).

En la siguiente lección vamos a dar un paso más, que es modificar el cargador para tener distintos colores de borde y una carga más rápida. La carga estándar es de 1500bps, con el cargador turbo de la siguiente lección (repito basado en la misma modulación que la carga estándar) espero poder alcanzar los 3000bps, que era lo habitual en las cargas turbo de la época. No se recomienda mayor velocidad con este tipo de modulación ya que bajaría drásticamente la fiabilidad.

Nos vemos en la siguiente lección.

Pincha aquí para bajar el archivo de la lección

Avatar de Usuario
wilco2009
Mensajes: 2141
Registrado: 07 Ene 2013 16:48
Ubicación: Valencia
Agradecido : 202 veces
Agradecimiento recibido: 384 veces

Re: Tutorial de optimización de cintas y ultracargas

Mensajepor wilco2009 » 04 May 2014 13:06

Estupendo post!!
Lástima que el día no tenga 30 horas para poder dedicarle a cosas como esta.
"Nada viaja a mayor velocidad que luz con la posible excepción de las malas noticias las cuales obedecen a sus propias leyes."

Douglas Adams. Guía de autoestopista galáctico.

Avatar de Usuario
ron
Mensajes: 21856
Registrado: 28 Oct 2010 14:20
Ubicación: retrocrypta
Agradecido : 3862 veces
Agradecimiento recibido: 4753 veces

Re: Tutorial de optimización de cintas y ultracargas

Mensajepor ron » 04 May 2014 21:28

wilco2009 escribió:Estupendo post!!
Lástima que el día no tenga 30 horas para poder dedicarle a cosas como esta.


Lo mismo digo, es fabuloso, lo que pasa que no quería estropear el hilo de AntonioVillena, pero si que lo vengo siguiendo. Lo voy a pasar a tema FIJO porque no merece menos y si es menester movemos estos mensajes en caso que AntonioVillena añada más lecciones.

Muchísimas gracias.

antoniovillena

Re: Tutorial de optimización de cintas y ultracargas

Mensajepor antoniovillena » 04 May 2014 22:07

No me importa que se rompa el hilo, las lecciones están marcadas en negrita y bien grandes. Si tenéis cualquier duda o problema técnico intentando seguir el curso decidlo por aquí. Todas las dudas y comentarios que pongáis en el hilo también forman parte del tutorial.

dancresp
Mensajes: 6225
Registrado: 13 Nov 2010 02:08
Ubicación: Barcelona
Agradecido : 665 veces
Agradecimiento recibido: 1017 veces

Re: Tutorial de optimización de cintas y ultracargas

Mensajepor dancresp » 05 May 2014 16:22

Pues yo, al igual que el hilo que denuncia que chema no se está quieto, lo he copiado a un WORD y de allí a un PDF.
En este caso, el PDF cuando se acabe el tutorial.

Esta información vale la pena tenerla con un aspecto impecable.

Gracias por el tutorial.
Buscando la IP de la W.O.P.R. he encontrado mi índice

antoniovillena

Re: Tutorial de optimización de cintas y ultracargas

Mensajepor antoniovillena » 05 May 2014 16:28

dancresp escribió:Pues yo, al igual que el hilo que denuncia que chema no se está quieto, lo he copiado a un WORD y de allí a un PDF.
En este caso, el PDF cuando se acabe el tutorial.

Esta información vale la pena tenerla con un aspecto impecable.

Gracias por el tutorial.


Gracias a tí por pasarlo a un formato más amigable. Voy a intentar acabarlo rápido, creo que da para 7 u 8 lecciones.

dancresp
Mensajes: 6225
Registrado: 13 Nov 2010 02:08
Ubicación: Barcelona
Agradecido : 665 veces
Agradecimiento recibido: 1017 veces

Re: Tutorial de optimización de cintas y ultracargas

Mensajepor dancresp » 05 May 2014 16:35

Pues como ya hice con el artículo del "Chess 1K", subiré el PDF cuando lo acabes.
Buscando la IP de la W.O.P.R. he encontrado mi índice

antoniovillena

Re: Tutorial de optimización de cintas y ultracargas

Mensajepor antoniovillena » 06 May 2014 09:38

Lección 3 y media

Esta minilección tiene 2 objetivos. Primero muestro la solución del ejercicio anterior. Segundo, os enseño cómo automatizar un proceso que es repetitivo.

Vamos a lo primero. Para simplificar vamos a pasar a "turbo" el último bloque, así nos queda el archivo BAT más corto y homogéneo. Los dos últimos bloques quedarían así:

Código: Seleccionar todo

    turbo 2168   667   735                  ^
      600 1600  1500     0  manic.scr.fck   ^
    turbo 2168   667   735                  ^
      600 1600  1500     0  manic.bin.fck


Ahora tenemos que pasar a turbo el primer bloque:

Código: Seleccionar todo

    basic ManicMiner  10  loader.bin          ^


Como hemos dicho antes este bloque lógico se corresponde con 2 bloques físicos (cabecera y datos). Además, en la cabecera hay información (longitud) que depende de los datos, por lo que no podemos generar dichos bloques de forma independiente. La solución, ensamblar en loader.asm la información de la cabecera, de tal forma que se generen 2 archivos: el antiguo loader.bin con los datos, y un nuevo archivo llamado header.bin con la cabecera. Este sería el contenido del nuevo loader.asm:

Código: Seleccionar todo

; 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     $5ccb
ini     ld      sp, $7530
        di
        db      $de, $c0, $37, $0e, $8f, $39, $96 ;OVER USR 7 ($5ccb)
        ld      hl, $5800
        ld      de, $5801
        ld      bc, $2ff
        ld      (hl), l
        ldir
        scf
        sbc     a, a
        ld      ix, $4000
        ld      de, $1b00
        call    $0556
        scf
        sbc     a, a
        ld      ix, $8000
        ld      de, $8000
        call    $0556
        jp      $8400
fin


Pues bien, ya tenemos los binarios de los 2 primeros bloques físicos (header.bin y loader.bin). El siguiente paso es añadirle el byte flag y el byte checksum. Esto antes lo hacíamos de forma manual con un editor hexadecimal. A medida que creemos varios TZXs con esta técnica, el proceso resulta tedioso y aburrido. La solución, hacer un programa que se encargue de esto por nosotros. Lo he llamado FlagCheck y está incluído en el fichero de la lección (tanto el código fuente en C como el ejecutable). Básicamente añade el flag que le especificamos (ó $FF si no lo hacemos) al principio y el checksum calculado del archivo al final. También he cambiado la nomeclatura: ahora en lugar de nombrearchivo_flag_chk.bin pasan a llamarse nombrearchivo.bin.fck, que es más sencillo.

Mostramos pues el make.bat solución:

Código: Seleccionar todo

SjAsmPlus loader.asm
rem SjAsmPlus manic.asm
FlagCheck header.bin 0
FlagCheck loader.bin
FlagCheck manic.scr
FlagCheck manic.bin
GenTape                     manic.tzx       ^
    turbo 2168   667   735                  ^
      600 1600  1500     0  header.bin.fck  ^
    turbo 2168   667   735                  ^
      600 1600  1500     0  loader.bin.fck  ^
    turbo 2168   667   735                  ^
      600 1600  1500     0  manic.scr.fck   ^
    turbo 2168   667   735                  ^
      600 1600  1500     0  manic.bin.fck


Pincha aquí para bajar el archivo de la lección

antoniovillena

Re: Tutorial de optimización de cintas y ultracargas

Mensajepor antoniovillena » 07 May 2014 11:24

Lección 4

Primero quiero mostraros un gráfico para que entendáis en qué punto estamos y hacia donde queremos llegar. Es una representación en la que cada columna equivale a una lección. Ojo, los tiempos no están a escala.

Imagen

La primera columna es la carga original del juego que mostramos en la lección 1, es decir son 6 bloques físicos (3 lógicos) con carga estándar.

La segunda columna es cuando en la lección 2 conseguimos reducir las cabeceras de los 2 últimos bloques lógicos (pantalla y juego), pero manteniendo la carga estándar.

En la tercera columna (tercera lección) quitamos las pausas entre bloques, recortamos los tiempos de tono guía y ligeramente los pulsos que codifican los ceros y los unos, pero manteniéndonos en carga estándar.

En esta cuarta lección lo que pretendemos es modificar el cargador de la ROM (LD-BYTES) para acelerar la carga (y de paso mostrar otros colores de borde), evidentemente sólo en los 2 últimos bloques que es cuando podemos tener disponible el nuevo cargador.

Como no podemos modificar la ROM, lo que hacemos es ubicar el nuevo cargador (LD-BYTES) en RAM. Desgraciadamente no nos vale cualquier parte de RAM, tiene que ser memoria no contenida o memoria alta (rango $8000-$FFFF), porque de lo contrario se producirían retardos en las instrucciones que producirían errores de carga.

Antes de nada os voy a mostrar el cargador de la ROM:

Código: Seleccionar todo

;; LD-BYTES
L0556:  INC     D               ; reset the zero flag without disturbing carry.
        EX      AF,AF          ; preserve entry flags.
        DEC     D               ; restore high byte of length.

        DI                      ; disable interrupts

        LD      A,$0F           ; make the border white and mic off.
        OUT     ($FE),A         ; output to port.

        LD      HL,L053F        ; Address: SA/LD-RET
        PUSH    HL              ; is saved on stack as terminating routine.

;   the reading of the EAR bit (D6) will always be preceded by a test of the
;   space key (D0), so store the initial post-test state.

        IN      A,($FE)         ; read the ear state - bit 6.
        RRA                     ; rotate to bit 5.
        AND     $20             ; isolate this bit.
        OR      $02             ; combine with red border colour.
        LD      C,A             ; and store initial state long-term in C.
        CP      A               ; set the zero flag.

;

;; LD-BREAK
L056B:  RET     NZ              ; return if at any time space is pressed.

;; LD-START
L056C:  CALL    L05E7           ; routine LD-EDGE-1
        JR      NC,L056B        ; back to LD-BREAK with time out and no
                                ; edge present on tape.

;   but continue when a transition is found on tape.

        LD      HL,$0415        ; set up 16-bit outer loop counter for
                                ; approx 1 second delay.

;; LD-WAIT
L0574:  DJNZ    L0574           ; self loop to LD-WAIT (for 256 times)

        DEC     HL              ; decrease outer loop counter.
        LD      A,H             ; test for
        OR      L               ; zero.
        JR      NZ,L0574        ; back to LD-WAIT, if not zero, with zero in B.

;   continue after delay with H holding zero and B also.
;   sample 256 edges to check that we are in the middle of a lead-in section.

        CALL    L05E3           ; routine LD-EDGE-2
        JR      NC,L056B        ; back to LD-BREAK
                                ; if no edges at all.

;; LD-LEADER
L0580:  LD      B,$9C           ; set timing value.
        CALL    L05E3           ; routine LD-EDGE-2
        JR      NC,L056B        ; back to LD-BREAK if time-out

        LD      A,$C6           ; two edges must be spaced apart.
        CP      B               ; compare
        JR      NC,L056C        ; back to LD-START if too close together for a
                                ; lead-in.

        INC     H               ; proceed to test 256 edged sample.
        JR      NZ,L0580        ; back to LD-LEADER while more to do.

;   sample indicates we are in the middle of a two or five second lead-in.
;   Now test every edge looking for the terminal sync signal.

;; LD-SYNC
L058F:  LD      B,$C9           ; initial timing value in B.
        CALL    L05E7           ; routine LD-EDGE-1
        JR      NC,L056B        ; back to LD-BREAK with time-out.

        LD      A,B             ; fetch augmented timing value from B.
        CP      $D4             ; compare
        JR      NC,L058F        ; back to LD-SYNC if gap too big, that is,
                                ; a normal lead-in edge gap.

;   but a short gap will be the sync pulse.
;   in which case another edge should appear before B rises to $FF

        CALL    L05E7           ; routine LD-EDGE-1
        RET     NC              ; return with time-out.

; proceed when the sync at the end of the lead-in is found.
; We are about to load data so change the border colours.

        LD      A,C             ; fetch long-term mask from C
        XOR     $03             ; and make blue/yellow.

        LD      C,A             ; store the new long-term byte.

        LD      H,$00           ; set up parity byte as zero.
        LD      B,$B0           ; timing.
        JR      L05C8           ; forward to LD-MARKER
                                ; the loop mid entry point with the alternate
                                ; zero flag reset to indicate first byte
                                ; is discarded.

; --------------
;   the loading loop loads each byte and is entered at the mid point.

;; LD-LOOP
L05A9:  EX      AF,AF          ; restore entry flags and type in A.
        JR      NZ,L05B3        ; forward to LD-FLAG if awaiting initial flag
                                ; which is to be discarded.

        JR      NC,L05BD        ; forward to LD-VERIFY if not to be loaded.

        LD      (IX+$00),L      ; place loaded byte at memory location.
        JR      L05C2           ; forward to LD-NEXT

; ---

;; LD-FLAG
L05B3:  RL      C               ; preserve carry (verify) flag in long-term
                                ; state byte. Bit 7 can be lost.

        XOR     L               ; compare type in A with first byte in L.
        RET     NZ              ; return if no match e.g. CODE vs. DATA.

;   continue when data type matches.

        LD      A,C             ; fetch byte with stored carry
        RRA                     ; rotate it to carry flag again
        LD      C,A             ; restore long-term port state.

        INC     DE              ; increment length ??
        JR      L05C4           ; forward to LD-DEC.
                                ; but why not to location after ?

; ---
;   for verification the byte read from tape is compared with that in memory.

;; LD-VERIFY
L05BD:  LD      A,(IX+$00)      ; fetch byte from memory.
        XOR     L               ; compare with that on tape
        RET     NZ              ; return if not zero.

;; LD-NEXT
L05C2:  INC     IX              ; increment byte pointer.

;; LD-DEC
L05C4:  DEC     DE              ; decrement length.
        EX      AF,AF          ; store the flags.
        LD      B,$B2           ; timing.

;   when starting to read 8 bits the receiving byte is marked with bit at right.
;   when this is rotated out again then 8 bits have been read.

;; LD-MARKER
L05C8:  LD      L,$01           ; initialize as %00000001

;; LD-8-BITS
L05CA:  CALL    L05E3           ; routine LD-EDGE-2 increments B relative to
                                ; gap between 2 edges.
        RET     NC              ; return with time-out.

        LD      A,$CB           ; the comparison byte.
        CP      B               ; compare to incremented value of B.
                                ; if B is higher then bit on tape was set.
                                ; if <= then bit on tape is reset.

        RL      L               ; rotate the carry bit into L.

        LD      B,$B0           ; reset the B timer byte.
        JP      NC,L05CA        ; JUMP back to LD-8-BITS

;   when carry set then marker bit has been passed out and byte is complete.

        LD      A,H             ; fetch the running parity byte.
        XOR     L               ; include the new byte.
        LD      H,A             ; and store back in parity register.

        LD      A,D             ; check length of
        OR      E               ; expected bytes.
        JR      NZ,L05A9        ; back to LD-LOOP
                                ; while there are more.

;   when all bytes loaded then parity byte should be zero.

        LD      A,H             ; fetch parity byte.
        CP      $01             ; set carry if zero.
        RET                     ; return
                                ; in no carry then error as checksum disagrees.

; -------------------------
; Check signal being loaded
; -------------------------
;   An edge is a transition from one mic state to another.
;   More specifically a change in bit 6 of value input from port $FE.
;   Graphically it is a change of border colour, say, blue to yellow.
;   The first entry point looks for two adjacent edges. The second entry point
;   is used to find a single edge.
;   The B register holds a count, up to 256, within which the edge (or edges)
;   must be found. The gap between two edges will be more for a 1 than a 0
;   so the value of B denotes the state of the bit (two edges) read from tape.

; ->

;; LD-EDGE-2
L05E3:  CALL    L05E7           ; call routine LD-EDGE-1 below.
        RET     NC              ; return if space pressed or time-out.
                                ; else continue and look for another adjacent
                                ; edge which together represent a bit on the
                                ; tape.

; ->
;   this entry point is used to find a single edge from above but also
;   when detecting a read-in signal on the tape.

;; LD-EDGE-1
L05E7:  LD      A,$16           ; a delay value of twenty two.

;; LD-DELAY
L05E9:  DEC     A               ; decrement counter
        JR      NZ,L05E9        ; loop back to LD-DELAY 22 times.

        AND      A              ; clear carry.

;; LD-SAMPLE
L05ED:  INC     B               ; increment the time-out counter.
        RET     Z               ; return with failure when $FF passed.

        LD      A,$7F           ; prepare to read keyboard and EAR port
        IN      A,($FE)         ; row $7FFE. bit 6 is EAR, bit 0 is SPACE key.
        RRA                     ; test outer key the space. (bit 6 moves to 5)
        RET     NC              ; return if space pressed.  >>>

        XOR     C               ; compare with initial long-term state.
        AND     $20             ; isolate bit 5
        JR      Z,L05ED         ; back to LD-SAMPLE if no edge.

;   but an edge, a transition of the EAR bit, has been found so switch the
;   long-term comparison byte containing both border colour and EAR bit.

        LD      A,C             ; fetch comparison value.
        CPL                     ; switch the bits
        LD      C,A             ; and put back in C for long-term.

        AND     $07             ; isolate new colour bits.
        OR      $08             ; set bit 3 - MIC off.
        OUT     ($FE),A         ; send to port to effect the change of colour.

        SCF                     ; set carry flag signaling edge found within
                                ; time allowed.
        RET                     ; return.


No me voy a poner a explicar cómo funciona, supongo que tenéis unos conocimientos mínimos de ensamblador y de inglés para leer los comentarios. Entender dicho código es recomendable, aunque sólo es necesario conocerlo si lo que queréis es escribir vuestro propio cargador.

En lugar de ir a la solución exacta como en otras lecciones, voy a hacerlo equivocándome varias veces a conciencia para que sea más instructivo. Pues bien, lo primero que se me ocurre es copiar el código del cargador en loader.asm a ver qué pasa:

Código: Seleccionar todo

        org     xxxx
ini     ld      sp, $7530
        di
        db      $de, $c0, $37, $0e, $8f, $39, $96
        ld      hl, yyyy
        ld      de, zzzz
        ld      bc, fin-L0556
        ldir
        ld      hl, $5800
        ld      de, $5801
        ld      bc, $2ff
        ld      (hl), l
        ldir
        scf
        sbc     a, a
        ld      ix, $4000
        ld      de, $1b00
        call    L0556
        scf
        sbc     a, a
        ld      ix, $8000
        ld      de, $8000
        call    L0556
        jp      $8400

;; LD-BYTES
L0556:  INC     D               ; reset the zero flag without disturbing carry.
        EX      AF,AF          ; preserve entry flags.
        DEC     D               ; restore high byte of length.
        ...
fin


He puesto sólo las 3 primeras instrucciones de la rutina de carga en ROM (LD-BYTES) para no ser muy repetitivo. Tenemos el mismo "loader.asm" de antes pero con este añadido:

Código: Seleccionar todo

        ld      hl, yyyy
        ld      de, zzzz
        ld      bc, fin-L0556
        ldir


Que se encargaría de mover a memoria alta el cargador (LD-BYTES). También he cambiado los valores de los CALLs para llamar a nuestra rutina y no a de la ROM original. De momento no sé qué poner en los valores xxxx, yyyy, zzzz. Es complicado porque sé que la etiqueta "ini" comienza en $5CCB, pero si pongo un "ORG $5CCB" como antes tenía, voy a tener mal calculadas las direcciones a partir de la etiqueta L0556. Al hacer una prueba de ensamblado con valor 0, compruebo que la longitud del cargador (desde L0556 hasta fin) es de $AF bytes. Si quiero ubicarlo en la zona más alta de memoria, tendría que ser en $FF51 ($10000-$AF).

Siempre es mala idea poner valores absolutos, ya que si modifico la longitud del cargador tengo que recalcularlo todo de nuevo. Por eso hay que pensarse muy bien estos valores. Como en el código que hay desde ini hasta L0556 no hay ninguna etiqueta, prefiero fijar el ORG a un valor que favorezca lo que hay a continuación de L0556. Los valores adecuados son:

Código: Seleccionar todo

        org     $10000-fin+ini
ini     ld      sp, $7530
        di
        db      $de, $c0, $37, $0e, $8f, $39, $96
        ld      hl, $5ccb+L0556-ini
        ld      de, L0556
        ld      bc, fin-L0556
        ldir
        ld      hl, $5800
        ld      de, $5801
        ...


Pues bien, ensamblo y pruebo el tzx. Tras cargar la pantalla se produce un cuelgue. Activo el depurador de mi emulador (Spectaculator) para ver qué es lo que pasa, y detecto esta instrucción errónea en FF59:

Código: Seleccionar todo

FF57    D3 FE       OUT     ($FE),A
FF59    21 00 00    LD      HL,$0000
FF5C    E5          PUSH    HL


Me doy cuenta de que el fallo está en que la etiqueta L053F no existe. Normal, he extraído la rutina de un código fuente que contiene toda la ROM y ese código no lo hemos copiado. Tampoco hace falta, es la rutina de retorno que no tenemos intención de modificar, tan sólo necesitamos saber cuál es su dirección. Por suerte las etiquetas están compuestas por la letra L seguida de la dirección hexadecimal que le corresponde, con lo que cambiando la L por el signo $ se soluciona el problema:

Código: Seleccionar todo

        LD      HL,$053F        ; Address: SA/LD-RET


Ejecutamos make.bat de nuevo y cargamos el TZX. Aparentemente carga bien el juego. Pero hay algo que no me concuerda. Si el cargador estaba en $FF51 y el último bloque empieza en $8000 y acaba en $FFFF, en algún momento se ha debido de machacar código que estaba en ejecución. Lo que suele suceder en estos casos es un cuelgue o un reseteo. Pero en pocas ocasiones (como es el caso) desafortunadamente no ocurre nada, por lo que el fallo seguramente se produzca al llegar al último nivel del juego. Por eso nunca está de más hacer un dump de memoria bajo emulador para comprobar que el juego se ha cargado correctamente.

Comprobemos qué ocurre en Spectaculator. Primero ponemos un punto de ruptura en $5CCB, luego cargamos el TZX y escribimos LOAD"" pulsando Enter. Tras la carga del bloque Basic salta el depurador en $5CCB. Nos vamos un poco más abajo y ponemos otro punto de ruptura donde está el asterisco:

Código: Seleccionar todo

  5CF9    37          SCF
  5CFA    9F          SBC     A,A
  5CFB    DD 21 00 80 LD      IX,$8000
  5CFF    11 00 80    LD      DE,$8000
  5D02    CD 51 FF    CALL    $FF51
* 5D05    C3 00 84    JP      $8400


Vemos que los registros DE y IX valen $005B y $FFA5 respectivamente. Estos deberían ser 0 en ambos casos (DE siempre vale cero tras la rutina de carga, puesto que en ella se va decrementando hasta alcanzar dicho valor). Pero no, la carga se ha cortado antes. Veamos qúe pasa en $FFA5:

Código: Seleccionar todo

FFA2  07      RLCA
FFA3  80      ADD     A,B
FFA4  07      RLCA
FFA5  20 07   JR      NZ,$FFAE
FFA7  30 0F   JR      NC,$FFB8


Lo que me temía, los datos del juego (concretamente del último nivel) han machacado el código del cargador hasta $FFA4, y por alguna razón se ha retornado la carga sin errores. No me detengo a analizar la traza exacta, pero evidentemente algo ha ido mal.

El problema es que el juego ocupa precisamente toda la memoria alta, que es donde obligatoriamente tiene que ir el cargador. La solución es cargar el juego un poco más abajo y moverlo a posteriori. Nada que no podamos hacer con la socorrida instrucción LDIR.

Código: Seleccionar todo

        scf
        sbc     a, a
        ld      ix, $8000-fin+L0556
        ld      de, $8000-fin+L0556
        call    L0556
        ld      hl, $8000-fin+L0556
        ld      de, $8000
        ld      bc, $8000
        ldir
        jp      $8400


Generamos de nuevo manic.tzx y cargamos. Ups, un reseteo. Como siempre depuramos a ver qué ha pasado. Vemos que toda la memoria alta ($8000-$FFFF) está a $00. Típico error de principiante: hemos movido un bloque con LDIR cuando teníamos que haber empleado LDDR. Cuando el registro destino (DE) tiene un valor mayor que el origen (HL) y hay solapamiento, el bloque no se mueve bien cuando lo hacemos con LDIR sino que se machaca a sí mismo repitiendo el mismo patrón cada DE-HL bytes.

Corregimos el error. Mucho cuidado que los límites de LDDR suelen confundir bastante.

Código: Seleccionar todo

        scf
        sbc     a, a
        ld      ix, $8000-fin+L0556
        ld      de, $8000-fin+L0556
        call    L0556
        ld      hl, $ffff-fin+L0556
        ld      de, $ffff
        ld      bc, $8000
        lddr
        jp      $8400


Probamos de nuevo y vemos que ahora sí funciona todo correctamente. Llegados a este punto hemos conseguido hacer lo mismo que hacía el cargador de la ROM pero con nuestro propio código, básicamente el mismo pero ensamblado en otro lugar. Antes de ponernos a tunear la velocidad veamos que ocurre si comentamos esta línea:

Código: Seleccionar todo

        IN      A,($FE)         ; read the ear state - bit 6.
        RRA                     ; rotate to bit 5.
        AND     $20             ; isolate this bit.
;        OR      $02             ; combine with red border colour.
        LD      C,A             ; and store initial state long-term in C.
        CP      A               ; set the zero flag.


Por arte de magia los colores de carga ahora son otros. No me voy a detener en detalles de cómo cambiarlos, sólo decir que si queréis usar una gama distinta a los pares complementarios (rojo-cian, amarillo-azul, verde-magenta y blanco-negro) tenéis que currároslo un poquito.

¿Os acordáis lo que os conté hace poco de que era mala idea poner valores absolutos? Pues echadle un vistazo al código máquina: ahora el cargador comienza en $FF53 y tiene longitud $AD. Como hemos hecho las cosas "bien", no tenemos que hacer ningún cambio de valores. Repasamos lo que hemos hecho hasta ahora, copiamos el make.bat en make2.bat (el loader.asm en loader2.asm) y procedemos con la segunda parte de la lección.

En esta segunda parte trataremos de acelerar la carga de los 2 últimos bloques. Para ello tengo que explicar un poco la parte de ensamblador que convierte cambios del puerto EAR en bits que luego se convierten en bytes y se guardan en memoria. La parte clave está en este trozo de código:

Código: Seleccionar todo

;; LD-MARKER
L05C8:  LD      L,$01           ; initialize as %00000001

;; LD-8-BITS
L05CA:  CALL    L05E3           ; routine LD-EDGE-2 increments B relative to
                                ; gap between 2 edges.
        RET     NC              ; return with time-out.

        LD      A,$CB           ; the comparison byte.
        CP      B               ; compare to incremented value of B.
                                ; if B is higher then bit on tape was set.
                                ; if <= then bit on tape is reset.

        RL      L               ; rotate the carry bit into L.

        LD      B,$B0           ; reset the B timer byte.
        JP      NC,L05CA        ; JUMP back to LD-8-BITS


Se trata de un bucle cerrado de 7 instrucciones que comienza en L05CA. En dicho bucle se van metiendo bits por la derecha del registro L hasta completar 8, en cuyo caso se ejecuta otro código que no he mostrado aquí pero que básicamente mueve L a (IX), incrementa IX, decrementa DE y xorea el byte leído (registro L) con el registro H para llevar la cuenta del checksum. Al final de todo esto volvemos a L05C8 donde se inicializa L a $01, que en binario es %00000001. El bit que está a 1 se llama bit marcador, y sirve para que cuando rotemos 8 veces con la instrucción RL L, nos avise por el flag carry de que hemos leído 8 bits, de esta forma no necesitamos ningún contador adicional.

¿Cómo se lee un bit del puerto EAR? Pues la primera instrucción es un salto a L05E3, también llamada rutina LD-EDGE-2 que muestro a continuación. Observa que el valor de B antes de llamar a esta rutina es $B0.

Código: Seleccionar todo

;; LD-EDGE-2
L05E3:  CALL    L05E7           ; call routine LD-EDGE-1 below.
        RET     NC              ; return if space pressed or time-out.
                                ; else continue and look for another adjacent
                                ; edge which together represent a bit on the
                                ; tape.

; ->
;   this entry point is used to find a single edge from above but also
;   when detecting a read-in signal on the tape.

;; LD-EDGE-1
L05E7:  LD      A,$16           ; a delay value of twenty two.

;; LD-DELAY
L05E9:  DEC     A               ; decrement counter
        JR      NZ,L05E9        ; loop back to LD-DELAY 22 times.

        AND      A              ; clear carry.

;; LD-SAMPLE
L05ED:  INC     B               ; increment the time-out counter.
        RET     Z               ; return with failure when $FF passed.

        LD      A,$7F           ; prepare to read keyboard and EAR port
        IN      A,($FE)         ; row $7FFE. bit 6 is EAR, bit 0 is SPACE key.
        RRA                     ; test outer key the space. (bit 6 moves to 5)
        RET     NC              ; return if space pressed.  >>>

        XOR     C               ; compare with initial long-term state.
        AND     $20             ; isolate bit 5
        JR      Z,L05ED         ; back to LD-SAMPLE if no edge.

;   but an edge, a transition of the EAR bit, has been found so switch the
;   long-term comparison byte containing both border colour and EAR bit.

        LD      A,C             ; fetch comparison value.
        CPL                     ; switch the bits
        LD      C,A             ; and put back in C for long-term.

        AND     $07             ; isolate new colour bits.
        OR      $08             ; set bit 3 - MIC off.
        OUT     ($FE),A         ; send to port to effect the change of colour.

        SCF                     ; set carry flag signaling edge found within
                                ; time allowed.
        RET                     ; return.


LD-EDGE-2 (L05E3) llama 2 veces a la rutina LD-EDGE-1 (L05E7), que a su vez detecta un cambio de nivel (detección de flanco, de ahí el nombre) en el puerto EAR (cambio de cero a uno o de uno a cero, da igual). Antes de leer del puerto se introduce un pequeño retardo de 22*16 ciclos (LD-DELAY) y luego se va sampleando el puerto EAR en iteraciones de 59 ciclos cada una, en las que incrementamos B y detectamos si se ha pulsado la tecla Espacio. Tanto si pulsamos espacio como si el contador B llega a $00 (estamos incrementando, harían falta $100-$B0= $50, en decimal 80 iteraciones) interrumpimos la carga con el correspondiente error.

En resumen, B tendrá un valor entre $B2 y $FF (si todo ha ido bien) tras la llamada a LD-EDGE-2 dependiendo de si los dos pulsos codifican un "0" o un "1". Por eso lo que hay después es una comparación con un valor umbral ($CB), y el resultado de dicha comparación es lo que va a parar al flag carry, que se meterá por la derecha del registro L (en RL L).

Ahora vamos a hacer una serie de cálculos. Primero vamos a ver cuántos ciclos son necesarios para que se desborde el registro B (llegue a $00) en la llamada LD-EDGE-2. El conteo es tedioso, aunque no muy complicado, si quieres seguirlo aquí tienes el desglose, siendo un total de 5598 ciclos. Recordemos que el "1" se codificaba con dos pulsos de 1710 ciclos cada uno, en total 3420 ciclos. Si leemos un pulso de más de 5598 ciclos (el 64% más del tiempo esperado) se produce un error.

Por otro lado y debido al pequeño retardo que hay al comienzo, el valor mínimo de B (que es $B2) no se corresponde con 0 ciclos, sino que lo hace con un conteo de 996 ciclos. Teniendo en cuenta que un "0" dura 855*2= 1710 ciclos, esto sería el 58% del tiempo esperado, aunque en este caso no se produce error directamente si hay menos de 996 ciclos, simplemente se añaden los ciclos que falten para completar los 996 al siguiente pulso.

Pues bien, si tenemos que con $B2 son 996 ciclos y con $00 son 5598, la fórmula que me devuelve el número de ciclos que duran los 2 pulsos leídos es:

Código: Seleccionar todo

Ciclos= 996+(regB-178)*59


Si tenemos en cuenta la variabilidad del registro A (en rutina estándar vale 22) que introduce el retardo, la fórmula más genérica sería:

Código: Seleccionar todo

Ciclos= 292+32*regA+(regB-178)*59


Lo siguiente a calcular es a cuántos ciclos equivale el valor umbral $CB (203 en decimal) usando dicha fórmula:

Código: Seleccionar todo

Ciclos= 292+32*22+(203-178)*59= 2471.


Dos pulsos que duren menos de 2471 ciclos son un "0", si duran más son un "1". Si normalizamos a 2471 tenemos 0.692 (1710/2471) y 1.384 (3420/2471) respectivamente, con lo que comprobamos que el umbral escogido por la rutina de carga es más o menos simétrico.

Bueno pues ahora queremos acelerar la carga a un valor de entre 1500bps y 3000bps (más sería arriesgado). Cojamos números sencillos y así agilizamos las cuentas: 1000 ciclos para el doble pulso del cero y 2000 ciclos para el del uno, que serían 2565bps. Para hacerlo todo simétrico y no pillarnos los dedos mantenemos el 58% del "0" como retardo mínimo. En la teoría basta con comprobar que 996<1000 sin cambiar el retardo; en la práctica los pulsos pueden ser muy asimétricos y tendríamos problemas. Retardo mínimo= 0.58*1000= 580 ciclos. En esta ecuación:

Código: Seleccionar todo

580= 292+32*regA


Despejamos regA y nos sale 9. Ahora veamos el umbral a aplicar:

Código: Seleccionar todo

0.692= 1000/Umbral


Despejando me salen 1445 ciclos. Pasamos estos ciclos a byte a comparar de B despejando de aquí:

Código: Seleccionar todo

292+32*9+(regB-178)*59= 1445


Nos salen 193, que en hexadecimal es $C1. Pues ya está, hacemos los siguientes cambios en el código:

Para el retardo:

Código: Seleccionar todo

;; LD-EDGE-1
L05E7:  LD      A,$09           ; a delay value of nine.


Para el umbral:

Código: Seleccionar todo

        RET     NC              ; return with time-out.
        LD      A,$C1           ; the comparison byte.
        CP      B               ; compare to incremented value of B.


Y por último cambiamos el make2.bat con los nuevos parámetros que hemos introducido en los dos últimos bloques:

Código: Seleccionar todo

SjAsmPlus loader2.asm
rem SjAsmPlus manic.asm
FlagCheck header2.bin 0
FlagCheck loader2.bin
FlagCheck manic.scr
FlagCheck manic.bin
GenTape                     manic2.tzx      ^
    turbo 2168   667   735                  ^
      600 1600  1500     0  header2.bin.fck ^
    turbo 2168   667   735                  ^
      600 1600  1500     0  loader2.bin.fck ^
    turbo 2168   667   735                  ^
      500 1000  1500     0  manic.scr.fck   ^
    turbo 2168   667   735                  ^
      500 1000  1500     0  manic.bin.fck


Ya casi hemos acabado. Lo siento, sé que esta lección es muy pesada y queréis acabar ya. El último cambio que voy a hacer en realidad es una optimización, vamos que es un complemento de la lección totalmente superfluo pero me parece interesante mostrarlo y no quiero hacer una lección 4 y media.

¿Os acordáis cuando movíamos todo el bloque del juego (32768 bytes) a su sitio porque era necesario ubicar el cargador en memoria alta? Pues bien, mover tal cantidad de bytes con un LDDR es muy lento, son 32768*21= 688128 ciclos, casi 200ms. Pensaréis que 200ms no es nada pero creedme, es un montón. Tal vez en esta situación sea plausible dejarlo, pero en cualquier otra circustancia puede ralentizar el desarrollo del juego, así que os diré como reducirlo a casi cero.

Tenemos 32768 bytes que parten de un archivo binario, que hay que desplazar 173 bytes hacia adelante. ¿Por qué no transformamos el binario de tal forma que haya que mover la mínima cantidad de bytes? Os dejo que penséis un poco y en breve muestro la solución.

...

...

...

Pues sí, se trata de poner los 173 últimos bytes del fichero al principio (o los 32595 bytes primeros al final). De esta forma bastaría con machacar el cargador con los primeros 173 bytes del archivo transformado para tener el juego correctamente cargado en memoria. Pues bien, el fichero se puede "transformar" haciendo un sencillo copy/paste con el editor hexadecimal. Pero como hemos dicho muchas veces, este tipo de soluciones a la larga no nos gustan porque suponen un trabajo extra cada vez que cambiemos la longitud del cargador. Por suerte tengo la utilidad hecha desde antes, no la he tenido que hacer ad-hoc como FlagCheck.c. Se llama fcut e incluyo sólo el ejecutable, el código fuente y el funcionamiento lo podéis ver aquí.

Vayamos al lío. Primero renombramos make2.bat y loader2.asm a make3.bat y loader3.asm respectivamente y hacemos los cambios allí.

En loader3.asm:

Código: Seleccionar todo

        scf
        sbc     a, a
        ld      ix, $8000-fin+L0556
        ld      de, $8000-fin+L0556
        call    L0556
        ld      hl, $8000-fin+L0556
        ld      de, $10000-fin+L0556
        ld      bc, fin-L0556
        ldir
        jp      $8400


En make3.bat:

Código: Seleccionar todo

SjAsmPlus loader3.asm
rem SjAsmPlus manic.asm
fcut manic.bin -AD  AD  manic.cut1
fcut manic.bin  00 -AD  manic.cut2
copy /b   manic.cut1  ^
        + manic.cut2  ^
      manic.new
FlagCheck header3.bin 0
FlagCheck loader3.bin
FlagCheck manic.scr
FlagCheck manic.new
GenTape                     manic3.tzx      ^
    turbo 2168   667   735                  ^
      600 1600  1500     0  header3.bin.fck ^
    turbo 2168   667   735                  ^
      600 1600  1500     0  loader3.bin.fck ^
    turbo 2168   667   735                  ^
      500 1000  1500     0  manic.scr.fck   ^
    turbo 2168   667   735                  ^
      500 1000  1500     0  manic.new.fck


Ya sí, ya hemos acabado. En la próxima lección explicaremos cómo funciona la compresión y cómo aplicarla para que nuestros bloques ocupen menos.

Pincha aquí para bajar el archivo de la lección

antoniovillena

Re: Tutorial de optimización de cintas y ultracargas

Mensajepor antoniovillena » 08 May 2014 21:20

Lección 5

Bienvenidos a la lección 5. Antes de meterme de lleno en la compresión quiero arreglar un poco el código fuente de la última lección. El último make (make3.bat) no está automatizado del todo, hay unos parámetros en la llamada a fcut que requieren ser corregidos manualmente cada vez que cambie la longitud del cargador. Primero separo la rutina LD-BYTES en un archivo aparte (ldbytes.asm), luego creo ldbytes_size.asm para que me genere un binario con sólo la rutina LD-BYTES con la única intención de calcular su longitud. Estas líneas del archivo make.bat leen la longitud del archivo y la pasan a hexadecimal para no tener que modificar a mano los parámetros de fcut:

Código: Seleccionar todo

for %%A in (ldbytes.bin) do set _fileSize=%%~zA
set /a "_fshex1=%_fileSize%>>4&15"
set /a "_fshex2=%_fileSize%&15"
set _map=0123456789ABCDEF
set _res=!_map:~%_fshex1%,1!!_map:~%_fshex2%,1!


También he optimizado código en ldbytes.asm, puesto que no necesitamos la parte de verificación (sólo cargamos) y el byte flag siempre vale $FF. Quitando el retardo de un segundo tras la detección del tono guía puedo reducir de 1500ms a 500ms la duración de dichos tonos en los dos últimos bloques, acortando en 2 segundos la duración total.

Debido a un bug en SjAsmPlus aparecen unos mensajes de error "Bytes lost" tras el ensamblado de los archivos de la lección anterior. En realidad ensambla bien pero dificulta la lectura de errores reales. Para evitar dichos mensajes trunco a 16 bits los valores de las líneas que dan error con este define al comienzo:

Código: Seleccionar todo

        define  tr $ffff &


Empleado en las líneas que dan error:

Código: Seleccionar todo

        ld      de, tr loader
        call    tr loader
L056C:  CALL    tr L05E7        ; routine LD-EDGE-1
        CALL    tr L05E3        ; routine LD-EDGE-2


Por último y para no liarnos con tanto archivo, al final del make.bat borro todos los archivos que han sobrado.

Código: Seleccionar todo

del *.fck *.cut header.bin loader.bin ldbytes.bin manic.new


Ya está, ya tenemos un make.bat limpio de donde partir, ahora renombramos a make2.bat (y loader2.asm) y comenzamos la lección.

Nuestro objetivo es reducir el tamaño de los bloques usando compresión para que éstos tarden menos en cargar, y una vez estén cargados los descomprimimos en memoria, generando el mismo mapa de memoria que habríamos tenido de haber cargado los bloques en crudo. Compresores hay muchos y que usan diferentes algoritmos. Los únicos viables para máquinas antiguas son los basados en Lempel Ziv, en concreto LZ77, debido a que son los más rápidos en descomprimir y no necesitan RAM adicional.

A grandes rasgos lo que hace el compresor es quitar la redundancia de los archivos, codificando los bloques repetidos por referencias. Por ejemplo si tenemos un archivo de texto de una sola línea con el contenido "hola hola", el primer "hola" lo codificará con 4 bytes; sin embargo para el segundo "hola" ya no hace falta, basta con que especifiquemos "copiar los 4 bytes que hay desplazándonos hacia atrás 5 bytes".

Pues bien, para ZX Spectrum hay muchos compresores basados en LZ77, nosotros vamos a usar dos: zx7/zx7b y Exomizer/Exoopt. La primera opción descomprime muy rápido, tiene un descompresor muy pequeño y un ratio de descompresión normal. La segunda opción es más lenta, el descompresor es más grande (incluso necesita espacio auxiliar para una tabla) pero tiene la ventaja de que comprime mejor.

De forma genérica el funcionamiento es el siguiente. Primero comprimimos un archivo que se corresponda con un bloque de carga, por ejemplo manic.scr. Para ello usamos el compresor, que es un archivo ejecutable (por ejemplo compress.exe) al que llamamos con una serie de parámetros, entre ellos el archivo de entrada.

Código: Seleccionar todo

C:/miruta> compress manic.scr manic.scr.comp


Tras la ejecución obtenemos el archivo comprimido manic.scr.comp, que evidentemente tendrá una longitud menor a la de su homólogo sin comprimir (manic.scr).

El siguiente paso sería descomprimir. Hay 2 formas de hacerlo: hacia adelante o hacia atrás, según el orden en que se van leyendo los datos del stream comprimido y se van escribiendo los datos descomprimidos. Descomprimir es tan sencillo como hacer una llamada a una rutina, llamémosla descomp, con 2 parámetros de entrada: en uno le indicamos dónde está el stream comprimido, en el otro le decimos dónde queremos descomprimir. Normalmente se usan los registros HL y DE como parámetros para estos menesteres. El "dónde" sería apuntar al primer byte del bloque si comprimimos hacia adelante, o el último byte del mismo si lo hacemos hacia atrás.

Imagínate que ya hemos cargado en $8000 el archivo manic.scr.comp, y que ocupa $1000 bytes. Si queremos descomprimir en memoria de video, la descompresión hacia adelante sería así:

Código: Seleccionar todo

        ld      hl, $8000
        ld      de, $4000
        call    descomp


Y si es hacia atrás lo hacemos de la siguiente forma:

Código: Seleccionar todo

        ld      hl, $8FFF
        ld      de, $5AFF
        call    descomp


Ahora voy a explicar un concepto importante, que es el posible solapamiento entre el bloque comprimido y descomprido. Y es que podemos ir machacando el bloque comprimido a medida que descomprimamos, siempre que al acabar la descompresión dejemos unos pocos bytes sin machacar. A estos pocos bytes es a lo que llamamos "safety offset" o desplazamiento de seguridad. Si dejamos menos bytes de margen de los que nos indica el "safety offset" con toda seguridad se va a producir un cuelgue. Por experiencia este valor suele ser de 2 ó 3 bytes, así que para no preocuparos podéis probar distintos valores hasta el cuelgue, o bien poner por ejemplo 4 bytes que seguro que no se va a colgar. En algunos compresores como exomizer te indican este valor tras la compresión.

Vamos a ver cómo sería descomprimir solapando al máximo los bloques para ocupar la mínima cantidad de RAM. Supongamos que el bloque descomprimido va desde $8000 hasta $bfff (en total 16K) y el bloque comprimido tiene un ratio de 0.5, vamos que ocupa la mitad ($2000 ú 8K). Por otro lado vamos a usar un safety offset de 4 bytes para no complicarnos.

Si comprimimos hacia adelante haríamos los siguientes cálculos. El bloque descomprimido acaba en $BFFF, por lo que el comprimido podría acabar 4 bytes después, serían los bytes que no se machaquen tras la descompresión. El bloque comprimido acabaría en $C003, y como tiene una longitud de $2000, empezaría en $A004. Ojo, estoy incluyendo los bytes de los límites, por eso al hacer diferencia hay que sumar uno para que coincida con la longitud.

Código: Seleccionar todo

        ld      hl, $A004
        ld      de, $8000
        call    descomp


Hacemos lo mismo si la descompresión fuese hacia atrás. El bloque acabaría (de descomprimirse) en $8000, por tanto el comprimido puede acabar en $7FFC dejando 4 bytes de safety offset. Si acaba en $7FFC debe empezar en $1FFF+$7FFC= $9FFB.

Código: Seleccionar todo

        ld      hl, $9FFB
        ld      de, $BFFF
        call    descomp


En resumidas cuentas, en nuestro juego vamos a aplicar descompresión dos veces, una en la pantalla de carga donde no queremos solapamiento (de lo contrario se vería basura en pantalla) y otra en el bloque del juego donde solaparemos al máximo. Prácticamente en todos los juegos ocurre que si sumamos la longitud del juego comprimido con la del descomprimido nos sale una cifra mayor de 48K, por lo que sin solapamiento sería imposible.

El primer algoritmo que voy a mostrar es el más sencillo, el ZX7 de Einar Saukas. Para no ralentizar la descompresión es necesario ubicar el descompresor en una zona no contenida, así que lo pondré justo debajo de ldbytes.asm. Ahora en ldbytes_descom.asm (donde cuento los bytes que necesito rotar el binario) añado a lo anterior la rutina descompresora y los 4 bytes del safety offset.

ldbytes_descom.asm

Código: Seleccionar todo

        define  tr $ffff &
        output  ldbytes_descom.bin
        defs    4               ; safety offset=4
        include dzx7_standard.asm
        include ldbytes.asm


En el make2.bat genero el manic.new modificado:

Código: Seleccionar todo

for %%A in (ldbytes_descom.bin) do set _fileSize=%%~zA
set /a "_fshex1=%_fileSize%>>4&15"
set /a "_fshex2=%_fileSize%&15"
set _map=0123456789ABCDEF
set _res=!_map:~%_fshex1%,1!!_map:~%_fshex2%,1!

fcut manic.bin -%_res% %_res% manic1.cut
fcut manic.bin  0     -%_res% manic2.cut
copy /b   manic1.cut  ^
        + manic2.cut  ^
      manic.new


Y procedo a comprimir los binarios y a generar los archivos con el flag y el checksum añadidos:

Código: Seleccionar todo

FlagCheck header.bin 0
FlagCheck loader2.bin
zx7 manic.scr
zx7 manic.new
FlagCheck manic.scr.zx7
FlagCheck manic.new.zx7


Evidentemente en la llamada a GenTape indico los archivos comprimidos para los 2 últimos bloques:

Código: Seleccionar todo

GenTape                     manic2.tzx        ^
    turbo 2168   667   735                    ^
      600 1600  1500     0  header.bin.fck    ^
    turbo 2168   667   735                    ^
      600 1600  1500     0  loader2.bin.fck   ^
    turbo 2168   667   735                    ^
      500 1000   500     0  manic.scr.zx7.fck ^
    turbo 2168   667   735                    ^
      500 1000   500     0  manic.new.zx7.fck


Esto sería el make2.bat. Ahora mostremos cómo modificar el loader2.asm. Lo primero de todo es hacer una primera ejecución del make2.bat para ver cuánto ocupan los comprimidos y hacer una tabla como ésta, en la que muestro la longitud de los bloques comprimidos y descomprimidos (tanto en decimal como en hexadecimal):

Código: Seleccionar todo

         | Descomprimido      Comprimido
---------|------------------------------
Pantalla |  6912 - $1B00     274 - $0112
Juego    | 32768 - $8000   13263 - $33CF


El primer bloque (pantalla) es muy sencillo, puesto que no hay solapamiento:

Código: Seleccionar todo

        ld      ix, $8000
        ld      de, $0112
        call    tr loader
        di
        ld      hl, $8000
        ld      de, $4000
        call    tr dzx7b


Nótese que he añadido un "di" después de la llamada al cargador que no había antes, pues me acabo de percatar que la rutina de la ROM con la que acaba nuestro cargador SA/LD-RET (L053F) introduce un EI al final. No nos interesa tener habilitadas las interrupciones durante todo el cargador, sobre todo descomprimiendo ya que es un poco más lento y podría provocarnos un cuelgue.

Prosigo. Aquí viene la parte complicada, que es la carga y la descompresión del bloque Juego:

Código: Seleccionar todo

        ld      ix, $10000-fin+dzx7b-$33cf
        ld      de, $33cf
        call    tr loader
        di
        ld      hl, $10000-fin+dzx7b-$33cf
        ld      de, $8000-fin+dzx7b-4
        call    tr dzx7b


¿Por qué es complicada? Porque al haber solapamiento, todos los cálculos se hacen en funcion de la longitud del archivo comprimido ($33cf) y de los 4 bytes del safety offset. Cargo el bloque de tal forma que éste acabe justo antes de donde comienza el código del descompresor ($FF31) para luego descomprimir acabando 4 bytes antes ($FF2D).

La última parte del loader2.asm es esta:

Código: Seleccionar todo

        ld      hl, $8000-fin+dzx7-4
        ld      de, $10000-fin+dzx7-4
        ld      bc, fin-dzx7+4
        ldir
        jp      $8400

dzx7    include dzx7_standard.asm
loader  include ldbytes.asm
fin


Donde machaco tanto el descompresor como el cargador (ya no lo vamos a utilizar más) con la parte de juego que corresponda. Como he mencionado antes, incluyo el fuente del descompresor justo antes del cargador.

¿Os ha resultado difícil de seguir? No os preocupéis, es normal. La aritmética de las etiquetas es así de compleja, más aún si añadimos la longitud del bloque comprimido. Todavía nos falta automatizar el proceso para no tener que hacer nada en caso de que cambien las longitudes. Pero antes quiero mostraros en make3.bat y loader3.asm cómo sería si empleamos la variante hacia atrás de zx7, es una que modifiqué partiendo del mismo algoritmo, llamada zx7b.

Tabla de longitudes:

Código: Seleccionar todo

         | Descomprimido      Comprimido
---------|------------------------------
Pantalla |  6912 - $1B00     287 - $011F
Juego    | 32768 - $8000   13292 - $33EC


make3.bat:

Código: Seleccionar todo

SETLOCAL EnableDelayedExpansion
SjAsmPlus loader3.asm
SjAsmPlus ldbytes_descom3.asm
rem SjAsmPlus manic.asm

for %%A in (ldbytes_descom3.bin) do set _fileSize=%%~zA
set /a "_fshex1=%_fileSize%>>4&15"
set /a "_fshex2=%_fileSize%&15"
set _map=0123456789ABCDEF
set _res=!_map:~%_fshex1%,1!!_map:~%_fshex2%,1!

fcut manic.bin -%_res% %_res% manic1.cut
fcut manic.bin  0     -%_res% manic2.cut
copy /b   manic1.cut  ^
        + manic2.cut  ^
      manic3.new

FlagCheck header.bin 0
FlagCheck loader3.bin
zx7b manic.scr manic.scr.zx7b
zx7b manic3.new manic3.new.zx7b
FlagCheck manic.scr.zx7b
FlagCheck manic3.new.zx7b

GenTape                     manic3.tzx          ^
    turbo 2168   667   735                      ^
      600 1600  1500     0  header.bin.fck      ^
    turbo 2168   667   735                      ^
      600 1600  1500     0  loader3.bin.fck     ^
    turbo 2168   667   735                      ^
      500 1000   500     0  manic.scr.zx7b.fck  ^
    turbo 2168   667   735                      ^
      500 1000   500     0  manic3.new.zx7b.fck

del *.fck *.cut header.bin loader3.bin ldbytes_descom.bin manic3.new manic.scr.zx7b manic3.new.zx7b
ENDLOCAL


loader3.asm

Código: Seleccionar todo

        define  tr $ffff &

; 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  loader3.bin
        org     $10000-fin+ini
ini     ld      sp, $7530
        di
        db      $de, $c0, $37, $0e, $8f, $39, $96
        ld      hl, $5ccb+dzx7b-ini
        ld      de, tr dzx7b
        ld      bc, fin-dzx7b
        ldir

        ld      ix, $8000
        ld      de, $011f
        call    tr loader
        di
        ld      hl, $811e
        ld      de, $5aff
        call    tr dzx7b

        ld      ix, $8000-fin+dzx7b-4
        ld      de, $33ec
        call    tr loader
        di
        ld      hl, $8000-fin+dzx7b+$33ec-4-1
        ld      de, $10000-fin+dzx7b-1
        call    tr dzx7b

        ld      hl, $8000-fin+dzx7b
        ld      de, $10000-fin+dzx7b
        ld      bc, fin-dzx7b
        ldir
        jp      $8400

dzx7b   include dzx7b_fast.asm
loader  include ldbytes.asm
fin


La descompresión hacia atrás tiene una ligera ventaja en cuanto a velocidad sobre la compresión hacia adelante, os recomiendo usarla siempre que podáis. Como veis, el cambio ha sido sencillo.

Lo que vamos a hacer ahora es automatizar el cálculo de longitudes, así ya no necesitaremos la tabla de longitudes y podremos modificar la pantalla y el juego tranquilamente. Lo de siempre, copiamos a make4.bat y loader4.asm, y hacemos los siguientes cambios:

Tras comprimir el archivo, calculo su longitud y lo meto en una variable. Luego introduzco esa variable en una línea de un archivo llamado define.asm redirigiendo la salida estándar. Hago lo mismo en los 2 archivos a comprimir:

Código: Seleccionar todo

zx7b manic.scr manic.scr.zx7b
for %%A in (manic.scr.zx7b) do set _fileSize=%%~zA
echo  define  scrsize %_fileSize% >  define.asm

zx7b manic4.new manic4.new.zx7b
for %%A in (manic4.new.zx7b) do set _fileSize=%%~zA
echo  define  binsize %_fileSize% >> define.asm


De esta forma el archivo define.asm que genero tendrá este aspecto:

Código: Seleccionar todo

 define  scrsize 287
 define  binsize 13292


Finalmente incluyo el define.asm al comienzo de loader4.asm y sustituyo los valores numéricos de las longitudes por estas constantes:

Código: Seleccionar todo

        include define.asm
        ...

        ld      ix, $8000
        ld      de, scrsize
        call    tr loader
        di
        ld      hl, $8000+scrsize-1
        ld      de, $5aff
        call    tr dzx7b

        ld      ix, $8000-fin+dzx7b-4
        ld      de, binsize
        call    tr loader
        di
        ld      hl, $8000-fin+dzx7b+binsize-4-1
        ld      de, $10000-fin+dzx7b-1
        call    tr dzx7b


Ya está, ya hemos acabado con los compresores sencillos (zx7 y zx7b). Ahora vamos a dar un paso más. Copiamos a make5.bat y loader5.asm y peleémonos con Exomizer/Exoopt.

La compresión se hace en 2 etapas, primero comprimimos por separado los archivos con exomizer, y luego procesamos conjuntamente los archivos intermedios con exoopt para generar los archivos definitivos. En exoopt decidimos el sentido y la velocidad de la descompresión, nosotros nos hemos decantado por descompresión hacia atrás y velocidad 1, con lo que el código a introducir es b1. Si fuese compresión hacia adelante y velocidad 3 sería f3.

Código: Seleccionar todo

set /a _offset= 0x10000-%_fileSize%
call :dec2hex %_offset%
exomizer raw manic.scr -b -r -c -o manic.scr.exo
exomizer raw manic5.new -b -r -c -o manic5.new.exo
exoopt b1 %_res% manic.scr.exo manic5.new.exo


Calculamos la dirección de la tabla que va a usar el descompresor, un buffer de 156 bytes que hemos ubicado justo debajo del compresor. Luego pasamos esa dirección (_offset) a hexadecimal (_res). Lo siguiente es comprimir los 2 bloques, usando los comandos "-b -r" que indican que la compresión es hacia atrás y "-c" que evita que aparezcan literales (son una característica que mejora la compresión en algunas ocasiones). Por último hacemos la llamada a exoopt, que toma como entrada los archivos intermedios *.exo y genera los archivos finales *.exo.opt. Además de estos archivos también genera el descompresor en d.asm:

Código: Seleccionar todo

; b1 [f0..87] nolit= 148 bytes
        ld      iy, 65136
        ld      a, 128
        ld      b, 52
        push    de
        cp      a
exinit: ld      c, 16
        ...


En la primera línea vemos un comentario que nos dice el tipo de descompresor (b1), el rango que usa el byte bajo de la dirección de la tabla de entre 2 posibles ($46 se encuentra entre $f0 y $87), el hecho de que no procesa literales y la longitud del descompresor (148 bytes).

Como en el caso anterior (make4.bat), calculamos las longitudes de los comprimidos y las metemos en el archivo define.asm:

Código: Seleccionar todo

for %%A in (manic.scr.exo.opt) do set _fileSize=%%~zA
echo  define  scrsize %_fileSize% >  define.asm
for %%A in (manic5.new.exo.opt) do set _fileSize=%%~zA
echo  define  binsize %_fileSize% >> define.asm


Y ya todo lo demás es igual que antes, sólo que teniendo en cuenta la tabla de 156 bytes. En el archivo ldbytes_descom5.asm se hace así:

Código: Seleccionar todo

        define  tr $ffff &
        output  ldbytes_descom5.bin
        defs    156             ; exomizer table
        include d.asm
        include ldbytes.asm


Y en el archivo loader5.asm lo hacemos de la siguiente manera:

Código: Seleccionar todo

        ld      ix, $8000-fin+deexo-156-4
        ld      de, binsize
        call    tr loader
        di
        ld      hl, $8000-fin+deexo-156+binsize-4-1
        ld      de, $10000-fin+deexo-156-1
        call    tr deexo

        ld      hl, $8000-fin+deexo-156
        ld      de, $10000-fin+deexo-156
        ld      bc, fin-deexo+156
        ldir
        jp      $8400

deexo   include d.asm
loader  include ldbytes.asm
fin


El paso de decimal a hexadecimal que calcula el archivo .bat ahora (también lo hicimos en make4.bat) lo tenemos implementado como una especie de procedimiento:

Código: Seleccionar todo

:dec2hex
set /a "_fsh1=%1>>12&15"
set /a "_fsh2=%1>>8&15"
set /a "_fsh3=%1>>4&15"
set /a "_fsh4=%1&15"
set _map=0123456789ABCDEF
set _res=!_map:~%_fsh1%,1!!_map:~%_fsh2%,1!!_map:~%_fsh3%,1!!_map:~%_fsh4%,1!
goto :eof


En el cual el valor que pasemos como parámetro lo pasará a hexadecimal en la variable _res.

Ahora sí que sí, ya hemos acabado la lección. A modo de resumencillo y para que no os hagáis un lío con los archivos, tenemos:
  • make.bat. No usamos compresión, es el mismo ejemplo de la última lección pero con algunas mejoras.
  • make2.bat. Compresión ZX7, calculando longitudes manualmente.
  • make3.bat. Compresión ZX7B, calculando longitudes manualmente.
  • make4.bat. Compresión ZX7B, proceso automatizado.
  • make5.bat. Compresión Exomizer/Exoopt, proceso automatizado.

La siguiente lección será más bien a modo de repaso, explicaré para qué sirve el filtro RCS y aplicaremos compresión en otras situaciones: en carga estándar, aplicándolo a un cargador Basic y cómo convertir a cartucho (16K) un juego de 48K.

Pincha aquí para bajar el archivo de la lección

antoniovillena

Re: Tutorial de optimización de cintas y ultracargas

Mensajepor antoniovillena » 10 May 2014 22:00

Lección 6

Hagamos un sumario de lo que vamos a ver:
  • make.bat. Carga turbo comprimida más sencilla.
  • make2.bat. Carga estándar comprimida en 3 bloques.
  • make3.bat. Carga estándar comprimida con cargador Basic.
  • make4.bat. Filtro RCS. Cargamos pantalla aplicando filtro inverso.
  • make5.bat. Compresión en cartucho Interface II (16K).

make.bat. Carga turbo comprimida más sencilla.

En todos los ejemplos anteriores la memoria alta siempre estaba ocupada por el juego ($8000-$FFFF). Por suerte no siempre es así, y en esos casos nos ahorramos la transformación (rotación) del archivo y el tener que restaurar la zona que ocupa el cargador/descompresor. Ni siquiera hace falta elegir otro juego, los primeros 512 bytes ($8000-$81FF) en realidad no necesitan ser inicializados, el juego los usa como zona de variables y él mismo se encarga de inicializarlas.

Con este sencillo corte ya tenemos en manic.cut el binario que nos interesa ($8200-$FFFF):

Código: Seleccionar todo

fcut manic.bin 200 -200 manic.cut


Un inconveniente de los descompresores hacia adelante es que no permiten descomprimir con solapamiento hasta el tope alto de la memoria ($FFFF), así que para no complicarnos después moviendo bloques voy a usar el compresor zx7b en toda la lección. Las siguientes líneas comprimen nuestros 2 bloques de siempre y obtienen la longitud del comprimido en define.asm:

Código: Seleccionar todo

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


Ensamblamos y calculamos/insertamos checksums:

Código: Seleccionar todo

SjAsmPlus loader.asm
FlagCheck header.bin 0
FlagCheck loader.bin
FlagCheck manic.scr.zx7b
FlagCheck manic.cut.zx7b


Y finalmente generamos la cinta, borrando después todos los archivos intermedios:

Código: Seleccionar todo

GenTape                     manic.tzx           ^
    turbo 2168   667   735                      ^
      600 1600  1500     0  header.bin.fck      ^
    turbo 2168   667   735                      ^
      600 1600  1500     0  loader.bin.fck      ^
    turbo 2168   667   735                      ^
      500 1000   500     0  manic.scr.zx7b.fck  ^
    turbo 2168   667   735                      ^
      500 1000   500     0  manic.cut.zx7b.fck

del *.fck *.zx7b header.bin loader.bin manic.cut define.asm


El cargador (loader.asm) en este caso queda muy sencillo:

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, $7530
        di
        db      $de, $c0, $37, $0e, $8f, $39, $96
        ld      hl, $5ccb+dzx7b-ini
        ld      de, $8000
        ld      bc, fin-dzx7b
        ldir

        ld      ix, $8100
        ld      de, scrsize
        call    tr loader
        di
        ld      hl, $8100+scrsize-1
        ld      de, $5aff
        call    tr dzx7b

        ld      ix, $8200-4
        ld      de, binsize
        call    tr loader
        di
        ld      hl, $8200+binsize-4-1
        ld      de, $ffff
        call    tr dzx7b

        jp      $8400

dzx7b   include dzx7b_fast.asm
loader  include ldbytes.asm
fin


make2.bat. Carga estándar comprimida en 3 bloques.

Vamos a ir retrocediendo lecciones, aplicando compresión en cada una de ellas. Lo siguiente es carga estándar con cargador en código máquina. Como voy a tener la misma velocidad de carga tanto en el cargador como en la pantalla de carga, y ésta no la vamos a poder ver al vuelo (está comprimida), no tiene ningún sentido meter cargador y pantalla de carga en bloques separados.

Así que de lo que se trata es de unificarlos en un sólo bloque y en lugar de 4 bloques físicos tendremos 3. Cuando incrustamos un binario en ensamblador se simplifica el archivo bat, porque no hace falta averiguar la longitud del comprimido ni pasarla al archivo define.asm. Así que nuestro make2.bat comienza así:

Código: Seleccionar todo

fcut manic.bin 200 -200 manic.cut
zx7b manic.scr manic.scr.zx7b
zx7b manic.cut manic.cut.zx7b
for %%A in (manic.cut.zx7b) do set _fileSize=%%~zA
echo  define  binsize %_fileSize% > define.asm


Hemos recortado, comprimido y calculado la longitud del juego en define.asm. Ya sólo nos falta ensamblar y generar la cinta, para posteriormente borrar los archivos sobrantes:

Código: Seleccionar todo

SjAsmPlus loader2.asm

GenTape                     manic2.tap      ^
    basic ManicMiner   0  loader2.bin     ^
     data                   manic.cut.zx7b

del *.zx7b loader2.bin manic.cut define.asm


Como veis, el script (archivo make2.bat) ha quedado bastante sencillote. Veamos ahora el cargador:

Código: Seleccionar todo

        ld      hl, $5ccb+dzx7b-ini
        ld      de, $8000
        ld      bc, fin-dzx7b
        ldir

        ld      hl, $5ccb+endscr-ini-1
        ld      de, $5aff
        call    tr dzx7b


Movemos la rutina descompresora a $8000 (para que descomprima más rápido sin contención), y luego descomprimimos la pantalla directamente en memoria de video. Como véis, no hemos tenido que cargar ningún bloque porque el archivo comprimido está en el propio cargador (más abajo). Deberíamos haber apuntado HL a endscr-1, pero como estamos antes de la etiqueta dzx7b hacemos el ajuste $5ccb-$ini.

Continuamos:

Código: Seleccionar todo

        scf
        sbc     a, a
        ld      ix, $8200-4
        ld      de, binsize
        call    $0556
        di
        ld      hl, $8200+binsize-4-1
        ld      de, $ffff
        call    tr dzx7b

        jp      $8400

        incbin  manic.scr.zx7b
endscr

dzx7b   include dzx7b_fast.asm
fin


Como podemos observar, el bloque del juego sí que lo cargamos como siempre, luego lo descomprimimos y saltamos a $8400. Lo que viene después es el stream comprimido de la pantalla. La etiqueta endscr está después porque usamos un descompresor hacia atrás, con lo que tenemos que apuntar un byte por debajo de dicha etiqueta. Por último tenemos el include del descompresor (que al comienzo lo hemos movido a $8000).

Por cierto no lo he dicho pero siempre estoy generando cintas con el formato más sencillo posible. En este caso al ser carga estándar uso el formato TAP, pero también podría haber creado un TZX o un WAV sin más que cambiar la extensión del archivo en la llamada:

Código: Seleccionar todo

GenTape                     manic2.tzx      ^
    basic ManicMiner   0  loader2.bin     ^
     data                   manic.cut.zx7b

ó bien

GenTape                     manic2.wav      ^
    basic ManicMiner   0  loader2.bin     ^
     data                   manic.cut.zx7b


make3.bat. Carga estándar comprimida con cargador Basic.

Soy un poco reacio a los cargadores Basic porque pierdes versatilidad y te obligan a meter bloques de cabecera innecesarios en un cargador código máquina. Pero bueno, para demostrar que es posible usar el compresor también en Basic voy a desarrollar el siguiente ejemplo.

He modificado el conversor de BAS a binario (ZMakeBas) para permitir introducir por separado los 2 valores (el que se muestra y el que se almacena en punto flotante) y así optimizar un poco más, el código fuente está aquí.

Lo primero de todo, para no llenar el cargador de POKEs y hacerlo poco legible, a cada bloque voy a adjuntar (al comienzo) un segmento de 9 bytes que contiene 3 instrucciones en código máquina con 3 parámetros:

Código: Seleccionar todo

        ld      hl, 0
        ld      de, 0
        jp      0


Estos serían los parámetros "último byte del stream comprimido" en HL, "último byte del buffer a descomprimir" en DE y dirección del descompresor. Como el descompresor va a estar siempre en $8000 y el código máquina de esto es muy sencillo, pues ni siquiera lo vamos a ensamblar. He creado dicho archivo (target.bin) en el editor hexadecimal, tan sólo haría falta parchear los 2 valores de 16 bits en las posiciones 1 y 4. Por simplicidad uso la herramienta fpoke, que aplica "pokes" a los archivos.

Otra cosilla, he sacado fuera del bat la función dec2hex para no tener que escribirla siempre, el archivo dec2hex.bat es éste:

Código: Seleccionar todo

@echo off
set /a "_fsh1=%1>>12&15"
set /a "_fsh2=%1>>8&15"
set /a "_fsh3=%1>>4&15"
set /a "_fsh4=%1&15"
set _map=0123456789ABCDEF
set _res=!_map:~%_fsh1%,1!!_map:~%_fsh2%,1!!_map:~%_fsh3%,1!!_map:~%_fsh4%,1!
@echo on


Comencemos a mostrar nuestro make3.bat:

Código: Seleccionar todo

SETLOCAL EnableDelayedExpansion
rem SjAsmPlus manic.asm
SjAsmPlus dzx7b_8000.asm
fcut manic.bin 200 -200 manic.cut


El "SETLOCAL EnableDelayedExpansion" es para que las variables que definamos sólo duren mientras se ejecuta el bat, y EnableDelayedExpansion para que la función dec2hex no nos dé problemas. Lo siguiente es el ensamblado (comentado) de manic.asm, que como no ha cambiado desde la primera lección no lo incluyo y sólo tengo en cuenta el manic.bin. Luego tenemos el ensamblado de dzx7b_8000.asm, que no es más que generar el código máquina correspondiente al descompresor dzx7b_fast.asm, pero ensamblado en la dirección $8000 (puedes abrir el archivo dzx7b_8000.asm y comprobarlo). Por último está el recorte de manic.bin ($8000-$FFFF) a manic.cut ($8200-$FFFF) explicado anteriormente.

Lo siguiente es una secuencia medianamente larga de instrucciones para generar el bloque pantalla:

Código: Seleccionar todo

zx7b manic.scr manic.scr.zx7b
copy /b target.bin + manic.scr.zx7b   manic.scr.block
for %%A in (manic.scr.block) do set _fileSize=%%~zA
set /a _hl= 0x8043+%_fileSize%-1
call dec2hex %_hl%
fpoke manic.scr.block 1 %_res%
fpoke manic.scr.block 4 5aff


Comprimimos, concatenamos el segmento de 9 bytes al comienzo, calculamos el tamaño del bloque (incluídos los 9 bytes) en _fileSize y calculamos en _hl el desplazamiento apropiado. Recuerda que no estamos ensamblando nada, toda la aritmética que empleemos tendrá que ser a base de script.

Pasamos a hexadecimal (en _res) la variable antes calculada en _hl y pokeamos el bloque justo en el segmento donde se corresponde el "ld hl,". Luego hacemos lo mismo con DE, pero en este caso el valor es fijo ($5aff) que apunta al último byte de la memoria de video (vamos a descomprimir directamente en pantalla).

Lo siguiente es exactamente la misma secuencia antes explicada pero aplicándola al bloque Juego en lugar de al bloque Pantalla.

Código: Seleccionar todo

zx7b manic.cut manic.cut.zx7b
copy /b target.bin + manic.cut.zx7b   manic.cut.block
for %%A in (manic.cut.block) do set _fileSize=%%~zA
set /a _de= 0x81f0+%_fileSize%-1
call dec2hex %_de%
fpoke manic.cut.block 1 %_res%
fpoke manic.cut.block 4 ffff


Ahora bien, con los mismos bloques voy a generar 2 versiones: una de 4 bloques lógicos (8 físicos) con Basic normal, y otra de 3 bloques (6 físicos) con Basic ultraoptimizado. Veamos cómo se genera la primera versión, manic3a.tap.

Código: Seleccionar todo

ZMakeBas -r -o loader3a.bin loader3a.bas
GenTape                     manic3a.tap     ^
    basic ManicMiner  10  loader3a.bin    ^
    hdata ZX7B      8000  dzx7b_8000.bin  ^
    hdata screen    8043  manic.scr.block ^
    hdata game      81f0  manic.cut.block


Primero paso a binario el archivo loader3a.bas y lo coloco en el primer bloque basic. Como segundo bloque tengo el descompresor, ubicado en $8000. Luego tenemos el bloque Pantalla (justo después del descompresor) y finalmente el bloque Juego. Recuerda que para descomprimir estos bloques necesitamos ejecutar los 9 bytes de instrucciones que hay al comienzo, justo después de cargar cada bloque.

Antes de acabar con el make3.bat veamos el contenido del cargador Basic:

Código: Seleccionar todo

  10 CLEAR 30000
  20 POKE 23740, 23: LOAD ""CODE: LOAD ""CODE: RANDOMIZE USR 32835
  30 LOAD ""CODE: RANDOMIZE USR 33264: RANDOMIZE USR 33792


El POKE inhabilita la impresión en pantalla. Lo hacemos para no ensuciar la pantalla de carga con los típicos "Bytes: nombre". Luego tenemos dos LOAD""CODE seguidos que cargan el descompresor y la pantalla comprimida respectivamente. Justo después de cargar la pantalla ejecutamos en la misma dirección de comienzo $8043 (32835) para proceder a su descompresión. Como descomprimimos en memoria de video se mostrará directamente en pantalla.

En la linea 30 tenemos la carga (LOAD""CODE) y la ejecución (RANDOMIZE USR 33264) del bloque Juego, que estaba comprimido en la dirección $81f0 (33264). Hemos escogido para su ubicación 16 bytes debajo de $8200 en lugar de los 4 típicos del safety offset porque hay que tener en cuenta los 9 bytes que "target.bin" nos desplaza el stream comprimido (los otros 3 bytes son por redondear).

Ya con el juego descomprimido en $8200 sólo nos queda saltar a la primera instrucción del mismo, que se encuentra en $8400 (33792), para lo cual usamos el último RANDOMIZE USR.

Veamos ahora la segunda versión, manic3b.tap. Observamos que el bloque descompresor acaba justo donde empieza el bloque Pantalla ($8043), por lo que sería buena idea juntarlos y reducimos así el número de bloques a cargar.

Código: Seleccionar todo

copy /b dzx7b_8000.bin + manic.scr.block   zx7bscr.block

ZMakeBas -r -o loader3b.bin loader3b.bas
GenTape                     manic3b.tap     ^
    basic ManicMiner   0  loader3b.bin    ^
    hdata ZX7Bscr   8000  zx7bscr.block   ^
    hdata game      81f0  manic.cut.block

del *.zx7b *.block dzx7b_8000.bin loader3a.bin loader3b.bin manic.cut


Con el copy /b hacemos la concatenación de los dos bloques en un nuevo bloque llamado zx7bscr.block, para justo después generar el archivo de 3 bloques lógicos (6 físicos) con el GenTape. Por último borramos los archivos intermedios.

En teoría la única modificación que habría que hacerle al cargador Basic sería quitar uno de los dos LOAD""CODE que hay en la línea 20. Pero nosotros vamos a ir un poco más allá, vamos a reducir de 104 bytes a 64 la longitud del cargador tan sólo codificando las constantes de forma más eficiente (en cuanto a tamaño). Éste sería el archivo loader3b.bas:

Código: Seleccionar todo

0 CLEAR VAL"3e4":
  POKE 0|23740, EXP PI:
  LOAD ""CODE: RANDOMIZE USR 0|32835:
  LOAD ""CODE: RANDOMIZE USR 0|33264:
  RANDOMIZE USR 0|33792


Para ahorrar bytes lo ponemos todo en una única línea separando las sentencias con dos puntos, como no hay ningún GOTO ni ningún GOSUB podemos hacer esto sin problemas. El es para continuar la línea con nuestro editor y poder verlo en varias líneas, pero en realidad es una línea muy larga de sentencias separadas por dos puntos. Luego tratamos de optimizar el CLEAR 30000. Cada constante en Basic está formada por su representación en ASCII más 6 bytes más. De estos 6 bytes el primero es el separador ($0e) y los otros 5 son la representación del número en punto flotante. Pues bien, el número 30000 está formado por 5 dígitos, por tanto son 5 bytes de texto ASCII, más 6 bytes que hacen un total de 11 bytes.

Lo primero que se nos ocurre es usar la notación científica para expresar el mismo valor, con lo que la parte ASCII constaría de solo 3 caracteres (3e4), dando un total de 9 bytes. Hay una forma de evitar la representación en punto flotante, que es pasar el número a cadena usando la función VAL para luego convertirlo a número. Entre el VAL y las dos comillas se comen 3 bytes, más otros 3 de la representación ASCII hacen un total de 6 bytes. En resumen, con VAL"3e4" hemos reducido la constante de 11 a 6 bytes (mejora de 5 bytes).

Con el segundo truco tratamos de engañar al intérprete, pasándole un valor en punto flotante distinto al del ASCII. Si siempre enviamos un 0 (valdría cualquier otro dígito) en la parte ASCII entonces podemos codificar cualquier entero de 16 bits con 1+6= 7 bytes. Esto es lo que hacemos en la dirección del POKE y en los RANDOMIZE USR, lo único que hay que hacer es separar los dos valores con |.

El tercer y último truco es codificar el 23 (que ocupaba originalmente 8 bytes) empleando una fórmula de tal forma que sólo necesitemos dos bytes. Esta fórmula es EXP PI, que da 23.1406... pero al truncar tenemos 23, la constante que necesitamos. Con este truco se pueden codificar otras constantes como el 0 (NOT PI), el 1 (SGN PI), el -1 (COS PI) o el 3 (PI).

make4.bat. Filtro RCS. Cargamos pantalla aplicando filtro inverso.

Un filtro es una transformación reversible de un archivo manteniendo el mismo número de bytes. De por sí el filtro no mejora el tamaño del archivo, pero al combinarlo con un compresor hacemos que se pueda comprimir aún más. La explicación de porqué funciona el filtro es que en el formato de video del ZX Spectrum las líneas no están ordenadas una después de la otra, sino que tienen siguen un patrón peculiar. Es el mismo patrón que vemos al cargar una pantalla de presentación en crudo desde la cinta. Con el filtro RCS cambiamos este orden, no sigue el orden línea a línea, sino que codifica una celda byte a byte y después la celda de la derecha. De esta forma los elementos gráficos que se repiten cerca se corresponden a bytes que también están juntos, por lo que el compresor tiene que codificar distancias menores, ahorrando tamaño en la representación.

Pues bien, la metodología es aplicar primero el filtro al archivo de imagen y luego comprimir. En el cargador hacemos lo contrario, que es descomprimir y luego aplicar el filtro inverso. Para el ejemplo hemos escogido la pantalla de carga del Daley Thompsons Decathlon, que ocupa más y por tanto le sacamos más provecho al filtro. Por cierto el filtro RCS es del mismo autor que el compresor ZX7, Einar Saukas, y se puede bajar desde aquí.

En el zip de WOS tenemos la utilidad para filtrar llamada rcs.exe, aunque nosotros vamos a emplear otra herramienta, Png2Rcs.exe, que convierte desde una imagen PNG y aplica el filtro simultáneamente. Así que lo que nos interesa del paquete de WOS es el filtro inverso, codificado en ensamblador del Z80.

En el código fuente se nos muestran 4 alternativas: una que necesita un buffer, otra que hace la compresión en pantalla y las 2 últimas que integran el filtro inverso al descompresor ZX7. Nosotros vamos a usar el código de la primera opción, la del buffer, ya que es la más rápida y la que menos tamaño ocupa (27 bytes).

Este sería el make4.bat:

Código: Seleccionar todo

Png2Rcs DaleyThompsons.png DaleyThompsons.rcs
zx7b DaleyThompsons.rcs DaleyThompsons.rcs.zx7b
for %%A in (DaleyThompsons.rcs.zx7b) do set _fileSize=%%~zA
echo  define  scrsize %_fileSize% >  define.asm
SjAsmPlus loader4.asm

GenTape                     manic4.tap      ^
    basic DaleyThomp   0  loader4.bin     ^
     data                   DaleyThompsons.rcs.zx7b

del *.zx7b *.rcs loader4.bin define.asm


Como veis es bastante sencillo, no hay nada nuevo que no hayamos explicado antes. El cargador (loader4.asm) sería lo mismo de siempre: cargamos el bloque y lo descomprimimos en un buffer ($A000-$BAFF)

Código: Seleccionar todo

        define  tr $ffff &
        include define.asm

        output  loader4.bin
        org     $8000-dzx7b+ini
ini     ld      sp, $7530
        di
        db      $de, $c0, $37, $0e, $8f, $39, $96
        ld      hl, $5ccb+dzx7b-ini
        ld      de, $8000
        ld      bc, fin-dzx7b
        ldir

        scf
        sbc     a, a
        ld      ix, $8100
        ld      de, scrsize
        call    $0556
        di
        ld      hl, $8100+scrsize-1
        ld      de, $baff
        call    tr dzx7b


Luego aplicamos el filtro inverso RCS, pasando de este buffer a la memoria de video con los bytes ya desfiltrados.

Código: Seleccionar todo

        ld      hl, $a000
        ld      bc, $4000       ; filtro RCS inverso (jamorski)
        ld      a, b
init    xor     c
        and     $f8
        xor     c
        ld      d, a
        xor     b
        xor     c
        rlca
        rlca
        ld      e, a
init2   inc     bc
        ldi
        inc     bc
        ld      a, b
        cp      $58
        jr      c, init
        sub     $5b
        jr      nz, init2


Para acabar con un bucle infinito (HALT) y el include del descompresor (que movimos a $8000 inicialmente para que la descompresión fuese más rápida).

Código: Seleccionar todo

        halt

dzx7b   include dzx7b_fast.asm
fin


Ya está. Para los interesados en este filtro aconsejo que se lean el hilo original de WOS, está en inglés. Es muy recomendable emplearlo siempre que tengamos que comprimir una pantalla completa o tercios de la misma por separado. En este ejemplo hemos comprimido la pantalla en 2947 bytes, que habrían sido 3657 de no haber aplicado el filtro. Si a los 710 bytes de diferencia le restamos los 27 bytes de código que ocupa el filtro inverso, tenemos una mejora de 683 bytes.

make5.bat. Compresión en cartucho Interface II (16K).

Acabamos la lección con algo que se sale de la temática del tutorial pero que he decidido incluir porque alguno me lo ha pedido. Es aplicar compresión a un juego de 48K de tal forma que este quepa en la ROM de un cartucho para Interface II, que ocupa 16K. Para los que lo desconozcan, al intruducir un cartucho en el interface estamos sustituyendo la ROM original del ZX Spectrum por el contenido de la ROM que hay en el cartucho.

Como consecuencia de esto, los juegos de cartucho no pueden acceder a la ROM, ni a sus funciones ni al juego de caracteres. Esto es un impedimento si queremos convertir juegos que dependen de la misma. Aquellos juegos que empleen Basic en gran parte del mismo (o en su totalidad) serían muy complicados de convertir. El caso que nos ocupa (Manic Miner) es sencillo puesto que sólo hace uso del juego de caracteres, ubicado en la zona $3D00-$3FFF de la ROM (la parte más alta de la misma).

Lo nuevo aquí es que no habrá que crear ninguna cinta, el archivo loader5.asm (lo he llamado así para localizarlo mejor pero en realidad no se trata de un cargador) es el juego en sí mismo que ensamblará directamente en una ROM de 16K (manic5.rom). El make5.bat es tan sencillo como esto:

Código: Seleccionar todo

rem SjAsmPlus manic.asm
fcut manic.bin 200 -200 manic.cut
zx7b manic.scr manic.scr.zx7b
zx7b manic.cut manic.cut.zx7b
SjAsmPlus loader5.asm
del *.zx7b manic.cut


O sea que ensamblamos loader5.asm proveyendo previamente los archivos comprimidos manic.scr.zx7b y manic.cut.zx7b y borrando los temporales después. Ahora toca explicar el loader5.asm, aunque tampoco es muy complicado puesto que no hay que cargar nada de cinta ni calcular longitudes, sólo descomprimir y listo. Comenzamos deshabilitando interrupciones y descomprimiendo la pantalla en memoria de video:

Código: Seleccionar todo

        di
        ld      sp, $7530
        ld      de, $5aff
        ld      hl, endscr-1
        call    dzx7


Como el fondo es negro, ponemos un borde del mismo color para que estén a juego. Esto en un cargador de cinta no tenía sentido puesto que el color del borde cambiaba constantemente.

Código: Seleccionar todo

        xor     a
        out     ($fe), a


Por norma general los cartuchos muestran la pantalla de carga (en este caso sería más bien de presentación) hasta que pulsemos una tecla para comenzar el juego. La forma más sencilla de hacer una pausa/detección de pulsación es la siguiente:

Código: Seleccionar todo

init    in      a, ($fe)
        or      $e0
        inc     a
        jr      z, init


Hay otros métodos más o menos iguales de sencillos, pero requieren acceso a la ROM del Spectrum, cosa que no tenemos. Ahora descomprimimos el juego en las posiciones $8200-$FFFF:

Código: Seleccionar todo

        ld      de, $ffff
        ld      hl, endman-1
        call    dzx7


Y saltamos a la primera instrucción del mismo:

Código: Seleccionar todo

        jp      $8400


Lo que viene a continuación son una serie de includes e incbins:

Código: Seleccionar todo

dzx7b   include dzx7b_fast.asm

        incbin  manic.scr.zx7b
endscr
        incbin  manic.cut.zx7b
endman
        BLOCK   $3d00-$
        include charset.asm


El primero es el descompresor. Nótese que no lo hemos movido a $8000 o a otra zona de memoria alta para que descomprima más rápido. Lo dejamos donde está porque en la ROM tampoco hay contención, se ejecuta igual de rápido que en memoria alta, por tanto es absurdo moverlo de ahí.

Luego tenemos los archivos comprimidos manic.scr.zx7b y manic.cut.zx7b (Pantalla y Juego) con etiquetas apuntando al final de cada archivo, puesto que el algoritmo de descompresión es backwards (hacia atrás).

Por último tenemos una directiva (BLOCK $3d00-$) que nos rellena con ceros todo lo que hay a continuación hasta llegar a la posición $3D00, que es donde empieza el mapa de caracteres del ZX Spectrum. El include que viene después es el mismo juego de caracteres, representando en charset.asm cada uno de los caracteres que lo componen ordenados según la secuencia ASCII, donde podemos ver cada byte en binario (con ceros y unos) para que los caracteres resulten fáciles de reconocer, por tanto de modificar.

Tengo que decir que son pocos los juegos de 48K que comprimidos quepan en una ROM de 16K, por lo general éstos tienen a ocupar unos 20K comprimidos.

Ya se acaba la lección, dando por finiquitado el tema de la compresión. En las 2 últimas lecciones del tutorial (en total serán 8) me gustaría tratar las ultracargas de verdad y la desprotección de juegos. Con ultracargas de verdad (en contraposición con las cargas turbo) hago referencia a todas aquellas que emplean una modulación distinta a FSK y a la vez se consiguen velocidades de carga superiores a 8Kbps (al menos 5 veces más rápidas que la carga estándar).

Pincha aquí para bajar el archivo de la lección

antoniovillena

Re: Tutorial de optimización de cintas y ultracargas

Mensajepor antoniovillena » 01 Jun 2014 00:46

Lección 7

Aviso 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.

Imagen

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:

Código: Seleccionar todo

        in      l, (c)
        jp      (hl)


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:

Código: Seleccionar todo

bucle   in      f, (c)
        jp      pe, bucle


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

Código: Seleccionar todo

        inc     a
        in      l, (c)
        jp      (hl)


En método 2

Código: Seleccionar todo

bucle   inc     a
        in      f, (c)
        jp      pe, bucle


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.

Imagen

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:

Imagen

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 fija

Como 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   .14     defb    $ed, $70, $e8
        jr      ultra9

leelo   .14     defb    $ed, $70, $e0

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   .14     defb    $ed, $70, $e8
        jr      ultra9

leelo   .14     defb    $ed, $70, $e0

ultra9  pop     hl
        exx


Está así de mal porque no he encontrado la forma de hacer macros de 2 líneas, pero en realidad 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 fija

Tenemos 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 reubicable

El 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   .14     defb    $ed, $70, $e0

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   .14     defb    $ed, $70, $e0

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   .14     defb    $ed, $70, $e8
        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:

Código: Seleccionar todo

lowhi   ld      a, $-$b7-$36*a4d & $80|$67+3*sf48+table&$7f


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ás

El 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.

Pincha aquí para bajar el archivo de la lección


Volver a “Software Spectrum”

¿Quién está conectado?

Usuarios navegando por este Foro: No hay usuarios registrados visitando el Foro y 2 invitados