Objetivo
¿Quieres saber cómo ver imágenes en MSX? ¿Necesitas cargar un dibujo como pantalla inicial de tu juego? ¿O tal vez una pantalla de algún nivel? Puedes lograr esto siguiendo este tutorial. Veremos cómo podemos transformar una imagen generada en otra plataforma utilizando un programa de dibujo y mostrarla en MSX. En este tutorial utilizaremos MSX-DOS y los modos de pantalla MSX en screen 5 o 7 (como se les llama en BASIC y generalmente se conocen, pero en el libro MSX-Technical book los llaman Graphic 4 y Graphic 6).
Cómo funciona el vídeo en MSX
La parte responsable de generar imágenes en MSX es el VDP (Video Display Processor). En la primera generación, se utiliza el TMS9918 con 16K de memoria; en el MSX2, se cambia al Yamaha V9938 con 128K, ofreciendo más colores y desplazamiento vertical; y finalmente, el MSX2+ y Turbo-R tienen una versión mejorada, el V9958, que añade el desplazamiento horizontal. Después de las versiones oficiales, apareció el V9990 (que algunos dicen que debería haberse incluido en el MSX Turbo-R), ofreciendo muchos más colores, 512K de RAM y desplazamiento multicapa.
Dependiendo del modo de pantalla que se esté utilizando, el VDP interpreta los datos almacenados en su memoria (a menudo denominada VRAM) como colores de píxel o colores de patrón.
En este capítulo, nos centraremos en la carga del screen 5 o 7, que utilizan la VRAM a nivel de píxel. En estos modos de pantalla, se pueden mostrar simultáneamente 16 colores diferentes de una paleta de 512 colores posibles. Los datos almacenados en la memoria representan el índice de color de cada píxel. La siguiente imagen presenta un diagrama basado en un gráfico del Manual Técnico MSX2:
La parte izquierda corresponde a la memoria de video (VRAM), y la parte derecha muestra su correspondencia con la imagen generada en la pantalla. Así, el primer byte de la pantalla contiene información sobre los píxeles (0,0) y (1,0). Para indexar 16 colores, solo necesitamos 4 bits. La parte alta del byte representa el píxel más a la izquierda, y la parte baja representa el píxel más a la derecha, tal como indican las flechas. En total, podemos mostrar 256 * 212 píxeles. Para pintarlos, necesitamos 256 * 212 / 2 = 27136 bytes (el diagrama muestra hasta el 27135 ya que se cuenta a partir de 0), ya que cada byte contiene dos píxeles.
Los índices de colores se definen utilizando el registro #16 y se almacenan en la memoria en las direcciones 0x7680 a 0x76AF para el screen 5, y entre las posiciones 0xFA80 y 0xFAAF para el screen 7.
Afortunadamente, Fusion-C tiene un comando para definir la paleta, lo cual es muy útil y simplifica este proceso. El comando es:
void SetPalette((Palette *) mypalette)
donde el parámetro es un array con todos los colores; o podemos utilizar la función
void SetColorPalette(char ColorNumber, char Red, char Green, char Blue)
que nos permite modificar un color en lugar de toda la paleta.
¿Cómo indicamos qué archivo del disco debe cargarse?
En la sección anterior, hemos visto cómo funciona la memoria de video. Ahora, exploraremos cómo leemos bytes de un archivo que se encuentra en el disco. Esta tarea la maneja MSX-DOS. MSX-DOS es una adaptación de CP/M para MSX, y requiere su propia ROM que proporciona las funciones BDOS. El sistema MSX debe tener al menos 64 KB de memoria. Para leer archivos, MSX-DOS define el Bloque de Control de Archivo (FCB), que es una estructura de memoria que contiene varios campos que MSX-DOS utiliza para manipular las pistas del disco y localizar los bytes que componen el archivo designado. Fusion-C ya define el FCB, y para leer archivos, solo necesitamos especificar el nombre del archivo.
¿Cómo convertimos una imagen de una PC a bytes que puedan ser leídos por el MSX?
Existe una aplicación multiplataforma llamada MSX Viewer que permite cargar una imagen desde un PC y convertirla en diferentes formatos para el MSX, dependiendo del modo de pantalla que se quiera utilizar. También permite la conversión inversa, donde se puede cargar una imagen en formato MSX y guardarla en un formato común moderno, como BMP. Además, personalmente uso un script en Python que convierte una imagen de PC al formato de pantalla 5 o pantalla 7. El script no recorta la imagen, solo convierte los colores, por lo que la imagen original debe tener la resolución adecuada. Este script utiliza la biblioteca OpenCV y el algoritmo K-Means para reducir la imagen a 16 colores. Puedes encontrar este script en el siguiente repositorio de GitLab.
Código para cargar una imagen en la VRAM
Estoy cansado de tanta teoría. ¿Podemos ver el código de una vez? ¡Por supuesto, aquí está! Este código se puede encontrar en el GitLab de MoltSXalats.
Las líneas 5-7 contienen los "includes" que usaremos en este archivo. Las líneas siguientes hasta la 14 son las variables que utilizaremos. Cuando se compila con sdcc (al menos hasta la versión 4.1), es mejor usar variables globales en lugar de locales, ya que utiliza menos líneas de código. Pude verificar esto con el juego "Bricks". El texto del scroll en la pantalla inicial, cuando definí los arreglos de caracteres dentro de la función para pintar el texto, el tiempo de compilación y el tamaño del archivo resultante aumentaron enormemente. Luego, los eliminé y los convertí en globales, y todo volvió a la normalidad. No dudes en probarlo si tiene un momento.
Bueno, volviendo al código en cuestión, tenemos la variable "file", que es de tipo FCB y coincide con la estructura del "File Control Block" que explicamos anteriormente. Esto servirá para almacenar la información del archivo para DOS. A continuación, definimos un buffer, que se llenará con los datos leídos del disco para transferirlos de la RAM a la VRAM. Lo definimos con un tamaño de 20 líneas del screen 5: cada línea consta de 256 píxeles, cada píxel tiene 4 bits y 8 bits conforman un byte; por lo que tenemos 20*256*4/8 = 2560 bytes (un "char" es equivalente a un byte). Elegí este número porque era el que se usaba en el ejemplo de Fusion-C "LoadScreen5Image.c". Sin embargo, no realicé una evaluación comparativa con otras cantidades para ver cuál es más rápido. Luego, tenemos la variable "mypalette", que será responsable de almacenar los 16 colores y tiene la estructura requerida por la función "SetPalette" de Fusion-C.
Desde las líneas 16 hasta la 34, tenemos la función "FT_SetName", que llena los campos del FCB con el nombre del archivo a procesar. Luego, desde las líneas 36 hasta 57, tenemos el manejo de errores para DOS con "FT_errorhandler".
Y finalmente llegamos a la función "FT_LoadSc5Image", que se encarga de leer desde el disco y transferir los datos a la VRAM. Toma como parámetros el nombre del archivo que queremos cargar, la coordenada vertical desde la cual queremos comenzar, el buffer donde almacenaremos los datos, el ancho de línea (que será 256 para pantalla 5 o 512 para pantalla 7) y el tamaño del buffer anterior.
Definimos la variable "rd", que contendrá los bytes leídos desde el disco, e inicializamos su valor con un valor distinto de cero. Colocamos el nombre del archivo en el FCB e intentamos acceder a él usando la función "fcb_open" de Fusion-C. Si devuelve un error, procesamos este mensaje y finalizamos.
En la línea 71, leemos los primeros 7 bytes, pero no los utilizamos para nada, ya que tanto la imagen binaria obtenida con el script como la obtenida con el MSX Viewer contienen estos bytes que no son información de píxeles de la imagen.
Las líneas 75-80 contienen el bucle que leerá desde el disco y transferirá los datos a la VRAM. Este bucle continuará hasta que ya no leamos más bytes desde el disco (rd). Ahora, en la línea 77, efectivamente leemos tantos bytes como el tamaño del buffer. Si el tamaño del archivo no es un múltiplo del tamaño del buffer, la última lectura devolverá un valor menor a rd, y la siguiente lectura será 0, finalizando el bucle. Una vez que tenemos los bytes en el buffer, la función "HMMC" transferirá todos los bytes desde la RAM a la VRAM. Primero indicamos dónde se encuentran estos bytes, luego la coordenada X de origen (que siempre será 0 ya que hemos hecho múltiplos de la línea), la coordenada Y que variará de 20 en 20 (que es el tamaño de la línea que leemos) y finalmente pasamos el ancho y la altura del bloque de la imagen a copiar, en este caso, los 256 para screen 5 y las 20 líneas de altura.
La siguiente función, "LoadPalette", se encarga de pasar la paleta de colores al VDP. Opera de manera similar a la carga de la imagen, pero aquí en lugar de transferir de la RAM a la VRAM, cargamos un vector RAM y llamamos a la función "LoadPalette" de Fusion-C, que llena los registros del VDP con los valores RGB correspondientes. Comenzamos indicándole al DOS el nombre del archivo que queremos cargar, manejamos la detección de errores, omitimos los primeros 7 bytes que no proporcionan información relevante y luego cargamos los 24 bytes con la información RGB de la paleta en un array.
¿Has dicho 24? ¡Imposible! Tienes razón, con 16 colores por 3 componentes de color (RGB), efectivamente son 48 bytes, no 24. Sin embargo, el script de Python comprime esta información, ya que cada componente puede tomar valores del 0 al 7, utilizando solo 3 bits. Así que cada byte contiene la definición de dos componentes de color. Por lo tanto, si tenemos la definición de 48 componentes de color, necesitamos 24 bytes.
Las líneas 98 a 100 manejan la conversión de estos 24 bytes a 48 bytes y luego lo convierten al formato requerido por Fusion-C en las líneas 102 a 107. Se llama a la función "SetPalette" para escribir los registros del VDP con las definiciones de color.
La función "FT_LoadPalette_MSXViewer" se utiliza para cargar la paleta obtenida con el MSX Viewer. Al igual que todas las funciones que leen datos del disco, define el nombre del archivo y lo abre para lectura. A diferencia de la función anterior, aquí necesitamos omitir más bytes porque el MSX Viewer genera dos archivos: uno con los índices de color de píxeles (igual que el obtenido con el script), y el otro es una paleta binaria para ejecutar una vez que se carga la imagen. Después de estudiar el archivo generado, nos damos cuenta de que la información de la paleta comienza en el byte 0x30 (48 en decimal) y no está comprimida. Por lo tanto, el bucle de adaptación es mucho más sencillo (líneas 145-148). Finalmente, llamamos a la función "SetPalette" para guardar la paleta en el VDP.
Antes de la función main, hay otra función encargada de llamar a las imágenes. Si la imagen se hubiera creado con el script de Python, necesitaríamos usar la función "FT_LoadPalette" con los mismos parámetros que la función "FT_LoadPalette_MSXViewer" en la línea 158. La línea 159 no es necesaria.
Y ahora tenemos la función principal que selecciona el modo de pantalla, llama a la función para cargar las imágenes, espera una tecla y luego finaliza volviendo a la pantalla de edición de texto (screen 0), restaurando la paleta a los colores originales y restableciendo los valores de los registros con "Exit(0)".
Comentarios finales
Páginas de VRAM
Cuando ejecutas el programa, notarás que la imagen se carga de manera intermitente. Esto se debe al proceso de cargarla en memoria y luego mostrarla. Para evitar esto, puedes cargar la imagen en una página oculta de la VRAM, indicando que debe comenzar en y=256 en lugar de y=0.
Pero, ¿qué son las páginas de VRAM? Los píxeles de la imagen que se mostrará en el MSX son 256 * 212. Cada píxel se almacena en 4 bits (1/2 byte), por lo que tenemos 256 * 212 / 2 = 27136 = 0x6A00 bytes. El VDP del MSX2 tiene 128 KB (131072 = 0x20000 bytes), que pueden alojar 4 páginas en memoria (27136 * 4 = 108544) < 131072. Una vez que tienes una página llena con los valores de los píxeles, puedes indicarle al MSX que muestre esa página. En BASIC, usarías el comando "set page", pero ahora en C, tenemos la función "SetDisplayPage".
El siguiente diagrama muestra un esquema de las páginas y las direcciones de VRAM utilizadas por el VDP (extraído de la wiki de msx.org wiki):
Otro lugar donde está muy bien explicado es en el manual de Fusion-C. También muestra el área en la página 0 donde se almacena la información de los sprites y los datos de la paleta. Puedes verificar que las otras páginas tienen más bytes de los necesarios para mostrar píxeles. Estos píxeles adicionales permanecen ocultos y se pueden utilizar para almacenar imágenes de la animación del personaje principal, letras para crear letreros y más.
Haz clic aquí para ver el ejemplo en funcionamiento.
Comments