ZX-V Documentación English Pruébalo en el navegador

Gráficos

ZX-V no tiene hardware de sprites ni de tiles. En su lugar, la ROM trae un motor gráfico: rutinas que funcionan en el procesador principal y dibujan sprites, mapas de tiles, figuras y suelos con textura en la memoria de vídeo, muy deprisa. El hardware de vídeo pone lo que el software no puede hacer bien: dos páginas de pantalla, scroll por hardware, efectos por línea y remapeo de colores.

Esta página lo explica todo con programas completos. Cada función se describe en detalle en la referencia de la API. Para empezar con C en ZX-V, lee antes la guía del SDK.

Contenido

Cómo dibuja el motor

Incluye zxv_gfx.h. Todas las funciones de dibujo dibujan en el destino actual:

Hay unas reglas que valen para todo:

Las rutinas sencillas zxv_cls, zxv_plot y las de texto también dibujan en la página de dibujo, pero no en bitmaps.

La memoria de vídeo

Cada página es un bloque de memoria normal: la página 0 empieza en ZXV_VRAM0_BASE (0x2000_4000) y la 1 en ZXV_VRAM1_BASE (0x2001_C000). Los píxeles ocupan 4 bits, dos por byte, línea tras línea:

Modo Píxeles Bytes por línea
0 256 × 192 128
1 512 × 384 256

El píxel (x, y) está en el byte y × bytes_por_línea + x / 2: en los 4 bits bajos si x es par y en los altos si es impar. Vista en palabras de 32 bits, cada palabra tiene 8 píxeles seguidos, el píxel x en los bits 4·(x mod 8) a 4·(x mod 8)+3.

#include "zxv_api.h"

/* Pinta el píxel (x, y) de la página 0 en el modo 0: 128 bytes por línea,
 * dos píxeles por byte, el de la izquierda (x par) en los 4 bits bajos. */
static void pset(int x, int y, int c)
{
    volatile uint8_t *p = (volatile uint8_t *)(uintptr_t)ZXV_VRAM0_BASE + y * 128 + x / 2;
    if (x & 1)
        *p = (uint8_t)((*p & 0x0F) | c << 4);
    else
        *p = (uint8_t)((*p & 0xF0) | c);
}

int main(void)
{
    zxv_wait_vblank();                          /* acaba de empezar un frame... */
    while (*(volatile uint32_t *)(uintptr_t)ZXV_SV_SYSFLAGS & ZXV_SYSF_LINK_BUSY)
        ;                                       /* ...espera a que se haya enviado */
    for (int y = 0; y < 192; y++)
        for (int x = 0; x < 256; x++)
            pset(x, y, (x ^ y) & 15);           /* un dibujo con todos los colores */
    while (zxv_key_get() < 0)
        zxv_wait_vblank();
    return 0;
}

En el modo 0 solo se ven los primeros 24 KB de cada página de 96 KB; el resto es memoria libre para tu programa (salvo con los campos de scroll grandes de Scroll por hardware).

Si conoces el Spectrum: no hay atributos ni tercios: cada píxel tiene su propio color y las líneas van en orden.

Las páginas y el doble buffer

Al empezar cada frame (el VBLANK), el sistema toma la página visible y la envía a la pantalla, junto con los registros de vídeo. Esa copia se ve durante el frame siguiente. Dos consecuencias:

Para animar, lo limpio es el doble buffer: dibuja el frame siguiente en la página oculta, hazla visible y espera al VBLANK.

int visible = 0;
for (;;) {
    int hidden = 1 - visible;
    zxv_gfx_draw_page(hidden);          /* 1. dibuja en la página que nadie ve */
    draw_everything();
    zxv_set_page(hidden);               /* 2. será la visible desde el frame siguiente */
    zxv_wait_vblank();                  /* 3. espera a ese frame */
    visible = hidden;
}

El programa que mueve un sprite de la guía es un ejemplo completo. Antes de volver al BASIC, deja la página 0 visible y como página de dibujo (zxv_set_page(0); zxv_gfx_draw_page(0);).

zxv_set_page cambia el registro ZXV_V_PAGE. Todos los registros de vídeo funcionan igual: los escribes cuando quieras y se aplican en el siguiente VBLANK.

Dibujar en memoria

Un bitmap (zxv_bitmap) describe una imagen en memoria, con el mismo formato que la memoria de vídeo:

typedef struct {
    uint8_t *pix;           /* primer píxel, alineado a 4 bytes */
    uint16_t stride;        /* bytes por línea, múltiplo de 4 */
    uint16_t w, h;          /* tamaño en píxeles */
    uint16_t reserved;
} zxv_bitmap;

Puedes hacerlo destino y dibujar en él con todas las funciones del motor, y luego copiar trozos a la pantalla con zxv_gfx_blit o convertirlos en sprites. Es la forma fácil de hacer gráficos sin ficheros de imagen: Estela dibuja así sus rocas, minas, explosiones y jefes al arrancar. Declara los píxeles como un vector de uint32_t para que queden alineados:

static uint32_t pix[32 * 16 / 8];                   /* 32x16 píxeles: 16 bytes por línea */
zxv_bitmap b = { (uint8_t *)pix, 16, 32, 16, 0 };

zxv_gfx_blit(&b, sx, sy, w, h, x, y, opciones) copia el rectángulo (sx, sy, w, h) de un bitmap en (x, y) del destino. Las opciones pueden ponerle espejo (ZXV_FLIP_H, ZXV_FLIP_V) y saltarse el color 8 (ZXV_BLIT_TRANSPARENT).

Sprites

Un sprite es un trozo de un bitmap convertido con zxv_spr_prepare en un formato que se dibuja deprisa, con su máscara de transparencia ya calculada. Preparas cada sprite una vez y lo dibujas tantas veces como quieras:

#include "zxv_gfx.h"

static uint32_t sheet_pix[32 * 16 / 8];         /* una hoja de 32x16: dos dibujos de 16x16 */
static uint32_t face[(8 + 16 * 2 * 8) / 4];     /* 8 + h * ceil(w/8) * 8 bytes */
static uint32_t ball[(8 + 16 * 2 * 8) / 4];

static void make_sheet(void)
{
    zxv_bitmap b = { (uint8_t *)sheet_pix, 16, 32, 16, 0 };    /* 16 bytes por línea = 32 px */
    zxv_gfx_target(&b);
    zxv_gfx_rect(0, 0, 32, 16, ZXV_TRANSPARENT);
    zxv_gfx_circle(7, 7, 7, 6, 1);              /* dibujo 0: una cara mirando a la derecha */
    zxv_gfx_rect(9, 4, 2, 3, 0);
    zxv_gfx_line(8, 11, 12, 10, 2);
    zxv_gfx_circle(23, 7, 6, 10, 1);            /* dibujo 1: una pelota */
    zxv_gfx_target(0);
    zxv_spr_prepare(&b, 0, 0, 16, 16, face);
    zxv_spr_prepare(&b, 16, 0, 16, 16, ball);
}

int main(void)
{
    make_sheet();
    zxv_gfx_rect(0, 0, 256, 192, 1);
    zxv_spr_draw(face, 16, 16, 0, 0);                           /* tal cual */
    zxv_spr_draw(face, 40, 16, ZXV_FLIP_H, 0);                  /* con espejo */
    zxv_spr_draw(face, 64, 16, ZXV_FLIP_V, 0);                  /* boca abajo */

    static const uint8_t green[16] = { 0, 1, 2, 3, 4, 5, 12, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
    zxv_spr_draw(face, 88, 16, 0, green);                       /* el color 6 sale como 12 */

    zxv_spr_draw_scaled(face, 16, 48, 64, 64, 0, 0);            /* cuatro veces mayor */
    zxv_spr_draw_scaled(face, 96, 48, 8, 8, 0, 0);              /* a la mitad */

    /* Colisión píxel a píxel: los rectángulos se solapan en los dos casos,
     * pero solo en el primero se tocan píxeles opacos. */
    int a = zxv_spr_collide(face, 150, 60, ball, 158, 66);
    int b = zxv_spr_collide(face, 150, 100, ball, 163, 113);
    zxv_spr_draw(face, 150, 60, 0, 0);
    zxv_spr_draw(ball, 158, 66, 0, 0);
    zxv_spr_draw(face, 150, 100, 0, 0);
    zxv_spr_draw(ball, 163, 113, 0, 0);
    zxv_gfx_rect(190, 64, 8, 8, a ? 12 : 2);    /* verde = se tocan, rojo = no */
    zxv_gfx_rect(190, 108, 8, 8, b ? 12 : 2);

    while (zxv_key_get() < 0)
        zxv_wait_vblank();
    return 0;
}

Mapas de tiles

Un mapa de tiles (mosaico) construye una escena grande con piezas cuadradas pequeñas, los tiles, de 8 × 8 o 16 × 16 píxeles. El mapa es una cuadrícula de celdas; cada celda (16 bits) dice qué tile va ahí y cómo:

Bits Significado
0–11 (ZXV_CELL_TILE) Número de tile, de 0 a 4095
12 (ZXV_CELL_FLIP_H) Espejo horizontal
13 (ZXV_CELL_FLIP_V) Espejo vertical
14 (ZXV_CELL_TRANSP) El color 8 es transparente (si no, el tile es opaco y se dibuja más deprisa)
15 (ZXV_CELL_PRIO) Prioridad: el tile va delante de los sprites
typedef struct {
    const uint16_t *cells;  /* las celdas, fila a fila */
    uint16_t w, h;          /* tamaño en celdas */
    const uint8_t *tiles;   /* los gráficos de los tiles, seguidos (32 o 128 bytes cada uno) */
    uint8_t size;           /* 8 o 16 */
    uint8_t flags;          /* ZXV_MAP_WRAP: el mapa se repite en las dos direcciones */
} zxv_tilemap;

Un tile son simplemente sus píxeles en el formato de la memoria de vídeo, línea a línea: un tile de 8 × 8 ocupa 32 bytes (4 por línea) y uno de 16 × 16, 128 bytes. Por eso una columna de tiles de 8 × 8 dibujada en un bitmap de 8 píxeles de ancho (4 bytes por línea) son justo los datos de tiles que necesita el mapa.

#include "zxv_gfx.h"

#define MAP_W 64                                /* celdas: 64 x 24 de 8x8 píxeles */
#define MAP_H 24

static uint32_t tile_pix[3 * 8];                /* 3 tiles de 8x8: 32 bytes cada uno */
static uint16_t cells[MAP_W * MAP_H];
static const zxv_tilemap level = { cells, MAP_W, MAP_H, (const uint8_t *)tile_pix, 8, ZXV_MAP_WRAP };

enum { SKY, BRICK, GRASS };

static void make_level(void)
{
    zxv_bitmap b = { (uint8_t *)tile_pix, 4, 8, 3 * 8, 0 };     /* los tiles, uno debajo de otro */
    zxv_gfx_target(&b);
    zxv_gfx_rect(0, 0, 8, 8, 5);                                /* SKY: cian */
    zxv_gfx_rect(0, 8, 8, 8, 2);                                /* BRICK: rojo con argamasa */
    zxv_gfx_line(0, 11, 7, 11, 7);
    zxv_gfx_line(3, 8, 3, 11, 7);
    zxv_gfx_line(7, 12, 7, 15, 7);
    zxv_gfx_rect(0, 16, 8, 8, 4);                               /* GRASS: verde */
    zxv_gfx_line(0, 16, 7, 16, 12);
    zxv_gfx_target(0);

    for (int y = 0; y < MAP_H; y++)
        for (int x = 0; x < MAP_W; x++) {
            uint16_t t = SKY;
            if (y >= 20)
                t = BRICK;
            else if (y == 19)
                t = GRASS;
            else if (y == 12 && (x / 6) % 3 == 0)
                t = BRICK;                                      /* plataformas en el aire */
            cells[y * MAP_W + x] = t;
        }
}

int main(void)
{
    make_level();
    int scroll = 0, visible = 0;
    while (zxv_key_get() < 0) {
        int hidden = 1 - visible;
        zxv_gfx_draw_page(hidden);
        /* La ventana de 256x192 del mapa que empieza en el píxel (scroll, 0). */
        zxv_map_draw(&level, scroll, 0, 0, 0, 256, 192, ZXV_MAP_ALL);
        /* ¿Hay un ladrillo en el centro de la pantalla, en la fila de las plataformas? */
        uint16_t c = zxv_map_cell(&level, scroll + 128, 12 * 8 + 4);
        zxv_gfx_rect(124, 80, 8, 8, (c & ZXV_CELL_TILE) == BRICK ? 15 : 0);
        zxv_set_page(hidden);
        zxv_wait_vblank();
        visible = hidden;
        scroll++;                               /* el mapa se repite: no se acaba nunca */
    }
    zxv_set_page(0);
    zxv_gfx_draw_page(0);
    return 0;
}

Redibujar la pantalla entera de tiles en cada frame está bien en el modo 0 (cerca del 11 % del frame si el mapa no cae en tiles enteros). Para escenas grandes, y en el modo 1, es mejor dejar el scroll al hardware.

Scroll por hardware

Los registros ZXV_V_SCROLL_X y ZXV_V_SCROLL_Y (de 16 bits) mueven la imagen dentro de la página de forma circular: lo que sale por un lado entra por el otro. No cuestan nada: lo hace el hardware de vídeo.

En el modo 0, la página puede ser además más grande que la pantalla, y la pantalla pasa a ser una ventana que mueves sobre ella. El registro ZXV_V_VSIZE elige el tamaño de las dos páginas:

ZXV_V_VSIZE Página Bytes por línea Para
ZXV_VSIZE_SCREEN (0) 256 × 192 128 Lo normal
ZXV_VSIZE_2X2 (1) 512 × 384 256 Scroll en las dos direcciones
ZXV_VSIZE_WIDE (2) 1024 × 192 512 Scroll horizontal
ZXV_VSIZE_TALL (3) 256 × 768 128 Scroll vertical

Con una página grande se usan los 96 KB enteros, y el sistema tarda en enviarla lo mismo que en el modo 1 (ZXV_SYSF_LINK_BUSY dura hasta 6 ms). Las funciones de dibujo siguen al nuevo tamaño: tras cambiar ZXV_V_VSIZE, la página de dibujo mide 512 × 384, 1024 × 192, etc.

#include "zxv_gfx.h"

#define REG8(a)  (*(volatile uint8_t *)(uintptr_t)(a))
#define REG16(a) (*(volatile uint16_t *)(uintptr_t)(a))

int main(void)
{
    REG8(ZXV_V_VSIZE) = ZXV_VSIZE_2X2;          /* página de 512 x 384 en el modo 0 */
    zxv_gfx_draw_page(0);                       /* el motor sigue al nuevo tamaño */
    zxv_gfx_rect(0, 0, 512, 384, 1);
    for (int i = 0; i < 40; i++)                /* dibuja el campo entero una vez */
        zxv_gfx_circle((i * 97) % 512, (i * 61) % 384, 10 + i % 20, 2 + i % 13, 1);

    int x = 0, y = 0;
    while (zxv_key_get() < 0) {
        zxv_wait_vblank();
        x = (x + 1) % 512;                      /* la ventana da la vuelta al campo */
        y = (y + 1) % 384;
        REG16(ZXV_V_SCROLL_X) = (uint16_t)x;    /* sin redibujar: lo hace el vídeo */
        REG16(ZXV_V_SCROLL_Y) = (uint16_t)y;
    }
    REG16(ZXV_V_SCROLL_X) = 0;
    REG16(ZXV_V_SCROLL_Y) = 0;
    REG8(ZXV_V_VSIZE) = ZXV_VSIZE_SCREEN;
    return 0;
}

Para un nivel más largo que la página, usa el truco de las consolas: como el scroll es circular, dibuja solo la columna de tiles que va a aparecer, justo fuera de la ventana, y sigue avanzando. El ejemplo scroll hace exactamente eso en una página de 1024 × 192: cada columna nueva de 8 × 144 píxeles cuesta menos del 1 % de un frame.

Efectos por línea y remapeo de colores

La tabla por línea

La tabla por línea (ZXV_LINE_TABLE) deja que cada línea de la imagen tenga su propio color de borde, su tabla de colores, su scroll horizontal e incluso que elija qué línea de la página enseña. Con ella se hacen el parallax (cada franja del escenario se desplaza a su velocidad), las pantallas partidas, el agua, las carreteras y las rayas del borde, sin cambiar registros a mitad de frame.

Tiene una entrada de 8 bytes por cada línea del cuadro entero, borde incluido: 240 líneas en el modo 0 (las 24 primeras son el borde de arriba, así que la línea y de la pantalla es la entrada 24 + y) y 480 en el modo 1 (48 de borde arriba). En zxv.h, ZXV_FRAME_LINES(modo) y ZXV_BORDER_LINES(modo).

Desplazamiento Tamaño Campo Valor «como siempre»
ZXV_LT_BORDER (0) 8 bits Color del borde de esta línea 0xFF
ZXV_LT_REMAP (1) 8 bits Tabla de colores de esta línea (0–3) 0xFF
ZXV_LT_SCROLL_X (2) 16 bits Scroll horizontal de esta línea 0xFFFF
ZXV_LT_SRC_LINE (4) 16 bits Línea de la página que se ve aquí 0xFFFF
6 16 bits Reservado: escribe 0

La tabla se usa mientras esté a 1 el bit ZXV_VFLAG_LINETAB de ZXV_V_FLAGS. Como la página visible, se lee justo después de cada VBLANK: cámbiala cuando ZXV_SYSF_LINK_BUSY se ponga a 0.

El remapeo de colores

Hay cuatro tablas de remapeo de 16 bytes en ZXV_REMAP_TABLES. El registro ZXV_V_REMAP elige por cuál pasa la pantalla: un píxel de color v se ve del color tabla[v]. La tabla 0 se suele dejar como está (cada color como él mismo). Cambiar una tabla cambia los colores de toda la pantalla de golpe, sin tocar la memoria de vídeo: fundidos, destellos, ciclos de color.

Cada tabla tiene además su paleta: los 16 colores RGB565 que se ven para cada índice, en ZXV_PALETTE(t). Al empezar son los de siempre; cambiar uno da cualquier color (ZXV_RGB(r, g, b), de 0 a 255 por componente) a todo lo que está dibujado con él. Con la tabla por línea, cada franja de la pantalla puede tener sus 16 colores:

volatile uint16_t *pal1 = (volatile uint16_t *)(uintptr_t)ZXV_PALETTE(1);
pal1[2] = ZXV_RGB(255, 128, 0);                 /* el rojo de la tabla 1, naranja */

El índice 8 sigue siendo transparente para los sprites y los tiles aunque la paleta le dé otro color.

Este programa enseña las dos cosas: rayas en el borde con la tabla por línea, y seis franjas cuyos colores giran a través de la tabla 1:

#include "zxv_gfx.h"

#define REG8(a)  (*(volatile uint8_t *)(uintptr_t)(a))
#define REG32(a) (*(volatile uint32_t *)(uintptr_t)(a))

/* Tras un VBLANK el sistema todavía lee un momento la página visible, la
 * tabla por línea y las de remapeo: espera a que termine. */
static void wait_frame(void)
{
    zxv_wait_vblank();
    while (REG32(ZXV_SV_SYSFLAGS) & ZXV_SYSF_LINK_BUSY)
        ;
}

int main(void)
{
    volatile uint8_t *remap1 = (volatile uint8_t *)(uintptr_t)(ZXV_REMAP_TABLES + 16);
    volatile uint8_t *lines = (volatile uint8_t *)(uintptr_t)ZXV_LINE_TABLE;

    zxv_gfx_rect(0, 0, 256, 192, 0);
    for (int i = 0; i < 6; i++)                 /* seis franjas de los colores 1 a 6 */
        zxv_gfx_rect(0, 40 + i * 16, 256, 16, 1 + i);

    /* Tabla por línea: cada línea del cuadro de 240 (primero 24 de borde)
     * tiene su color de borde; el resto de la entrada es «como siempre». */
    for (int l = 0; l < ZXV_FRAME_LINES(0); l++) {
        volatile uint8_t *e = lines + l * ZXV_LINE_ENTRY_SIZE;
        e[ZXV_LT_BORDER] = (uint8_t)(9 + (l / 8) % 7);     /* rayas brillantes */
        e[ZXV_LT_REMAP] = 0xFF;
        e[ZXV_LT_SCROLL_X] = e[ZXV_LT_SCROLL_X + 1] = 0xFF;
        e[ZXV_LT_SRC_LINE] = e[ZXV_LT_SRC_LINE + 1] = 0xFF;
    }
    REG8(ZXV_V_FLAGS) = ZXV_VFLAG_LINETAB;
    REG8(ZXV_V_REMAP) = 1;                      /* la pantalla pasa por la tabla 1 */

    int frame = 0;
    while (zxv_key_get() < 0) {
        wait_frame();
        if (frame % 8 == 0)                     /* gira los colores 1..6: las franjas se mueven */
            for (int v = 1; v <= 6; v++)
                remap1[v] = (uint8_t)(1 + (v - 1 + frame / 8) % 6);
        frame++;
    }

    wait_frame();                               /* recoge antes de volver */
    REG8(ZXV_V_FLAGS) = 0;
    REG8(ZXV_V_REMAP) = 0;
    for (int v = 0; v < 16; v++)
        remap1[v] = (uint8_t)v;
    return 0;
}

El ejemplo scroll usa el campo de scroll de la tabla por línea para el parallax: las montañas de arriba van a la mitad de velocidad que el suelo.

Modo 7

El modo 7 (se llama así por el modo de la SNES que lo hizo famoso) dibuja una textura en cada línea con su propia posición y su paso: girada, ampliada o en perspectiva, como un suelo visto desde arriba.

#include "zxv_gfx.h"

static uint8_t texels[64 * 64];                 /* una textura de 64x64, un byte por texel */
static zxv_m7line rows[192];                    /* una transformación por línea de pantalla */

int main(void)
{
    for (int y = 0; y < 64; y++)                /* un tablero de ajedrez */
        for (int x = 0; x < 64; x++)
            texels[y * 64 + x] = ((x / 8) ^ (y / 8)) & 1 ? 12 : 4;
    const zxv_m7tex tex = { texels, 6, 1, 0, 0 };   /* 2^6 = 64, se repite */

    int visible = 0;
    for (int frame = 0; zxv_key_get() < 0; frame++) {
        int hidden = 1 - visible;
        zxv_gfx_draw_page(hidden);
        zxv_gfx_rect(0, 0, 256, 72, 9);         /* el cielo */
        zxv_m7cam cam = {
            .x = frame << 15, .y = 0,           /* coma fija 16.16: avanza por x */
            .height = 16 << 16,
            .angle = frame * 64,                /* 0x10000 es una vuelta entera */
            .focal = 128 << 16,
            .horizon = 64,                      /* línea de la pantalla del horizonte */
        };
        zxv_m7_camera(rows, 192, &cam);         /* las líneas 0..64 quedan vacías */
        zxv_m7_draw(&tex, rows + 72, 72, 120);  /* dibuja las líneas 72..191 */
        zxv_set_page(hidden);
        zxv_wait_vblank();
        visible = hidden;
    }
    zxv_set_page(0);
    zxv_gfx_draw_page(0);
    return 0;
}

El modo 7 cuesta unas 15 instrucciones por píxel: la pantalla entera en el modo 0 es un cuarto del frame, y en el modo 1 no cabe en un frame, así que dibuja solo el suelo. El ejemplo graphics pone objetos sobre el suelo con sprites escalados, proyectados con la misma cámara.

Imágenes desde ficheros PNG: png2gfx.py

Dibujar todo con código va bien para las figuras, pero para el pixel art de verdad querrás un programa de dibujo. sdk/tools/png2gfx.py convierte imágenes PNG en un fichero .gfx: sprites, tiles, niveles enteros (cortados en tiles, aprovechando los repetidos) y texturas del modo 7. Es el formato que carga el BASIC con LOAD "nombre" GRAPHICS, y tu programa en C también puede leerlo. Solo necesita Python 3.

python3 sdk/tools/png2gfx.py art.gfx --sprites 0 sheet.png 16,16 --level 0 8 level.png --wrap 0
python3 sdk/tools/png2gfx.py --list art.gfx

La primera línea corta sheet.png en sprites de 16 × 16 numerados desde el 0, y convierte level.png en el mapa 0 de tiles de 8 × 8, que se repite. La segunda enseña lo que hay en el fichero: sprite 0: 16 x 16, tiles de 8 x 8: 4, mapa 0: 8 x 4 celdas.... Habla en español si tu sistema está en español, y si no, en inglés.

Orden Efecto
--sprite N IMAGEN [X,Y,AN,AL] El sprite N es la imagen, o ese rectángulo de ella
--sprites N IMAGEN AN,AL Una hoja de sprites: los sprites N, N+1... de AN × AL, fila a fila
--tiles S N IMAGEN [X,Y,AN,AL] La imagen (o el rectángulo) cortada en tiles de S × S (8 o 16), numerados desde N
--level M S IMAGEN Un nivel entero: se corta en tiles de S × S aprovechando los repetidos (con --flip, también los espejados); el mapa M queda con su distribución. Las celdas de los tiles con píxeles transparentes llevan el bit de transparencia
--map M S FICHERO.csv El mapa M a partir de un CSV con el valor de cada celda
--texture N IMAGEN [C] La textura N (un cuadrado de lado potencia de 2, de 2 a 256); se repite o, con C, fuera es del color C
--wrap M El mapa M se repite
--screen N IMAGEN [B] La pantalla N (como las de SAVE "x" SCREEN$ y las pantallas de carga): de 256 × 192 (modo 0) o 512 × 384 (modo 1), o el cuadro entero (320 × 240 o 640 × 480) con el borde del color de su esquina; B, el color del borde. Con colores que no son los de siempre (16 como mucho), lleva su paleta
--flip --level aprovecha también los tiles espejados
--key RRGGBB Ese color es transparente (además de los píxeles con alfa menor que 128)

Cada píxel toma el color de ZX-V más cercano, con un aviso si no era exacto. Dibuja con los 15 colores exactos: cada componente a 00, D7 o FF (por ejemplo, D70000 es el rojo y FF0000 el rojo brillante). Las pantallas conservan sus colores: con colores que no son los de siempre (16 como mucho), la pantalla lleva su paleta.

Leer un fichero .gfx en C

Un fichero .gfx empieza con las 4 letras ZXVG, un byte de versión (1), un byte a cero y el número de objetos (16 bits). Cada objeto tiene una cabecera de 8 bytes (tipo, 0, número en 16 bits y longitud de sus datos en 32 bits) seguida de sus datos:

Tipo Objeto Datos
1 Sprite ancho y alto (16 bits cada uno), y sus píxeles: ⌈ancho / 2⌉ bytes por línea
2 Tiles de S × S (S es el número del objeto) cuántos (16 bits), 0 (16 bits) y los tiles (32 o 128 bytes cada uno)
3 Mapa ancho y alto (16 bits cada uno), lado del tile y repetición (bytes), 0 (16 bits) y las celdas (16 bits cada una)
4 Textura log2 del lado, repetición, color de fuera, 0 (bytes) y los texels (un byte cada uno)
5 Pantalla modo (0 o 1), color del borde, marcas (bit 0: lleva paleta) y 0 (bytes); si lleva paleta, sus 16 colores RGB565 (32 bytes); y los píxeles de la página, como en la VRAM (24.576 bytes en modo 0, 98.304 en modo 1)

Todos los números van con el byte bajo primero. Para leerlos, el SDK trae una pequeña biblioteca, zxv_gfxfile.h (con gfxfile.c). El fichero puede estar donde sea: leído de la tarjeta con zxv_file_load o dentro del programa. Como los objetos de dentro del fichero no van alineados, cada función copia el suyo en un buffer del programa, alineado como lo quiere el motor, y devuelve los bytes que necesita ese buffer (0 si el objeto no está; con el buffer a NULL, solo lo dice):

Función Lo que hace
zxv_gfxf_open(&f, datos, tamaño) 1 si es un .gfx bien formado (mira todos los objetos); deja f listo
zxv_gfxf_sprite(&f, n, &bitmap, buf, sitio) El sprite n como bitmap, para zxv_spr_prepare, zxv_gfx_blit o zxv_gfx_target
zxv_gfxf_tiles(&f, lado, buf, sitio, &cuántos) Los tiles de 8 o de 16
zxv_gfxf_map(&f, n, &mapa, tiles, buf, sitio) El mapa n: sus celdas en buf y, en el zxv_tilemap, los tiles que se le den
zxv_gfxf_texture(&f, n, &textura, buf, sitio) La textura n de modo 7
zxv_gfxf_screen(&f, n, &modo, &borde, buf, sitio) La pantalla n: sus píxeles en buf, como una página de la VRAM (por ejemplo, la que no se ve, para cambiar de página en el VBLANK)
zxv_gfxf_screen_palette(&f, n, pal) Los 16 colores RGB565 con que se veía la pantalla n, para ponerlos en una paleta (ZXV_PALETTE(t)): 1 si los lleva; si no, 0 y los de siempre
zxv_gfxf_find(&f, clase, n, &longitud) Los datos de un objeto tal cual están en el fichero (clase: ZXV_GFXF_SPRITE 1, ZXV_GFXF_TILES 2, ZXV_GFXF_MAP 3, ZXV_GFXF_TEXTURE 4, ZXV_GFXF_SCREEN 5)

Este programa carga art.gfx de la carpeta actual, prepara los dos sprites y el mapa (con tiles de 8), y los desplaza:

#include "zxv_gfxfile.h"

static uint8_t file[16 * 1024];                 /* el fichero .gfx entero */
static uint32_t pix[4096];                      /* un sprite, alineado, para prepararlo */
static uint32_t face[1024], ball[1024];         /* los sprites preparados */
static uint32_t tiles[64 * 32 / 4];             /* hasta 64 tiles de 8x8 */
static uint16_t cells[1024];

/* El sprite num del fichero, preparado en out. */
static int load_sprite(const zxv_gfxfile *f, int num, void *out, uint32_t room)
{
    zxv_bitmap b;
    uint32_t n = zxv_gfxf_sprite(f, num, &b, pix, sizeof(pix));
    if (!n || n > sizeof(pix) || zxv_spr_prepare(&b, 0, 0, b.w, b.h, NULL) > room)
        return 0;
    zxv_spr_prepare(&b, 0, 0, b.w, b.h, out);
    return 1;
}

int main(void)
{
    zxv_gfxfile f;
    zxv_tilemap level;
    int n = zxv_file_load("art.gfx", file, sizeof(file));     /* si cabe en 'file' */
    if (n < 0 || !zxv_gfxf_open(&f, file, (uint32_t)n) ||
        !load_sprite(&f, 0, face, sizeof(face)) || !load_sprite(&f, 1, ball, sizeof(ball)))
        zxv_report_error(0, "No se puede cargar art.gfx");  /* vuelve al BASIC con este informe */
    uint32_t t = zxv_gfxf_tiles(&f, 8, tiles, sizeof(tiles), NULL);
    uint32_t c = zxv_gfxf_map(&f, 0, &level, tiles, cells, sizeof(cells));
    if (!t || t > sizeof(tiles) || !c || c > sizeof(cells))
        zxv_report_error(0, "El mapa de art.gfx no cabe");
    int visible = 0;
    for (int frame = 0; zxv_key_get() < 0; frame++) {
        int hidden = 1 - visible;
        zxv_gfx_draw_page(hidden);
        zxv_map_draw(&level, frame, 0, 0, 0, 256, 192, ZXV_MAP_ALL);
        zxv_spr_draw(face, 60, 120, 0, NULL);
        zxv_spr_draw(ball, 100 + frame % 100, 60, 0, NULL);
        zxv_set_page(hidden);
        zxv_wait_vblank();
        visible = hidden;
    }
    zxv_set_page(0);
    zxv_gfx_draw_page(0);
    return 0;
}

Para compilarlo, añade $(SDK)/gfxfile.c a los ficheros del programa (en el Makefile de arriba, SOURCES := hello.c $(SDK)/gfxfile.c), y pon art.gfx en la misma carpeta de la tarjeta SD que el programa. Los juegos Route 128 y Estela de los ejemplos leen así sus gráficos: llevan su .gfx dentro del programa y, si en la tarjeta hay otro, usan ese.

El editor de gráficos para el PC: zxv-gfx

Para dibujar en el PC, los paquetes del emulador traen zxv-gfx, un editor de los mismos ficheros .gfx para Windows y Linux (en la AppImage, ./ZX-V-x86_64.AppImage zxv-gfx). Es software libre con la licencia MIT: su código está en sdk/tools/gfx, y se compila solo con Qt 6 (cmake -S sdk/tools/gfx -B build-gfx). Habla español o inglés, el del sistema (o el que se le pida: zxv-gfx --lang en).

zxv-gfx con el nivel del juego de plataformas de los ejemplos

El editor de gráficos de la máquina: gfxedit

Sin salir de la máquina: LOAD "ROM:gfxedit" (o el gfxedit.zxv de sdk/build) abre un editor de los mismos ficheros .gfx, que se usa con el ratón. Arriba hay una pestaña por cada clase de gráfico:

Pestaña Qué edita
Sprites Los 256 sprites, de 1 × 1 a 1024 × 1024 píxeles (el color 8 es el transparente)
Tiles 8, Tiles 16 Los dos juegos de tiles: el tile ampliado, el mismo repetido 3 × 3 para ver si casa con sus vecinos y debajo todo el juego, para elegir
Mapas Los 16 mapas: se pintan con el tile elegido y sus atributos (los dos espejos, el 8 transparente y delante), como con MAP POKE
Texturas Las 4 texturas del modo 7: su lado, si se repiten y el color de fuera

Las pantallas (las de SAVE "x" SCREEN$ y las pantallas de carga) no se editan aquí, sino en zxv-gfx: al cargar un fichero se las salta, y al grabarlo no las lleva.

Caben unos 112 KB de gráficos a la vez. Su código está en sdk/examples/gfxedit: store.c lee y escribe los .gfx y tu programa puede usarlo tal cual.

¿Cuánto cuesta?

A 60 frames por segundo el procesador tiene unos 2,4 millones de ciclos por frame. Estas cifras se midieron en la máquina real:

Operación Modo 0 (256 × 192) Modo 1 (512 × 384)
Borrar la página 1,2 % de un frame 2,7 %
Pantalla entera de tiles de 8 × 8, alineada 3,9 % 11,7 %
Pantalla entera de tiles de 8 × 8, desplazada cualquier número de píxeles 13,1 % 48 %
Pantalla entera de tiles de 16 × 16, alineada 2,2 % 6,4 %
100 sprites de 16 × 16 6,1 % 5,6 %
100 sprites de 16 × 16 escalados a 32 × 32 63 % 62 %
100 sprites de 16 × 16 escalados a 12 × 12 28 % 27 %
Modo 7, pantalla entera 31 % 121 %
100 triángulos de unos 470 píxeles 29 % 28 %
100 líneas al azar 14 % 25 %