Files
Sprinter-SDCC/libbgi/include/gfx.h
T
snark13 3f45430454 libbgi: vflip-блиты для row-major картинок (шаг I.2 плана L9-INVERT)
gfx_blit_noclip_vflip / gfx_blit_part_noclip_vflip / gfx_blit_part_vflip.
Ассемблера не потребовалось: у row-major вертикальное зеркало — это порядок
строк, а он задаётся ЗНАКОМ src-страйда, который _bgi_blit_rows_raw и так
принимает знаковым и патчит SMC.  Даём адрес последней строки и -stride.

Клип у part_vflip свой: обрезка сверху экрана съедает НИЖНИЕ строки
источника, и первая строка обязана считаться от высоты ДО клипа (поймано
тестом — сначала формула брала уже укороченную).

tests/pageflip расширен до 8 проверок, все PASS в MAME.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 17:35:50 +03:00

297 lines
22 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* gfx.h — Sprinter graphics: mode-agnostic API (BGI_GFX).
*
* Рисование (putpixel/line/bar/circle/…) вынесено в BGI — см.
* <graphics.h> и driver-библиотеки bgi256.lib / bgi16.lib (выбор режима
* линковкой: sprinter-cc --gfx 256 | --gfx 16). Здесь остались только
* функции БЕЗ BGI-аналога: вход/выход в режим, страницы, банк, vsync,
* палитра, шрифт. Они mode-agnostic и живут в libbgi/common/ (один
* исходник, .rel в обеих driver-библиотеках).
*
* Адресация (для не-BGI программ, использующих эти примитивы напрямую:
* пиксель (x,y) — CPU 0xC000 + (x или x/2) с Port_Y (0x89) = y;
* gfx-код мапит 16 КБ VRAM-страницу в W3 при записи.
* Для double-buffering страница 1 начинается на 320 байт дальше (0xC140).
*
* Палитра: BIOS $A4 (RST 8); 4 байта на запись — B, G, R, pad.
*/
#ifndef GFX_H
#define GFX_H
#include <stdint.h>
/* ESTEX SETVMOD codes — same values as the SETVMOD `A` register. */
#define GFX_MODE_TEXT_40x32 0x02
#define GFX_MODE_TEXT_80x32 0x03
#define GFX_MODE_320x256x256 0x81
#define GFX_MODE_640x256x16 0x82
/* Pixel dimensions of mode 0x81 (320×256, 256 colours). */
#define GFX_WIDTH 320
#define GFX_HEIGHT 256
/* Pixel dimensions of mode 0x82 (640×256, 16 colours). Each byte at
* 0xC000+x_byte holds two pixels: high nibble = LEFT (even-x), low
* nibble = RIGHT (odd-x) — see memory/sprinter_graphics.md. */
#define GFX_WIDTH_16 640
#define GFX_HEIGHT_16 256
#define GFX_COLORS_16 16
/* ---- Page selection (double-buffering) and bank control ---------- */
/* Внутреннее состояние (хранилище: libbgi/common/_gfx_state.c) — НЕ
* трогать напрямую, только через функции ниже. */
extern uint8_t _gfx_visible_page, _gfx_draw_page, _gfx_bank;
void gfx_set_visible_page(uint8_t page); /* 0 or 1 */
inline uint8_t gfx_get_visible_page(void) { return _gfx_visible_page; }
void gfx_set_draw_page(uint8_t page); /* 0 or 1 */
inline uint8_t gfx_get_draw_page(void) { return _gfx_draw_page; }
/* Видеобанк W3 (0x50..0x5F): биты 2/3 номера страницы — аппаратные
* подрежимы ЗАПИСИ (Иван Мак §4.3; docs/sprite-api-design.md §1),
* действуют на ВСЕ примитивы рисования до следующего gfx_set_bank.
* Биты 0/1 всегда 0 (резерв конфигураций). */
#define GFX_BANK_NORMAL 0x50 /* запись в видео-ОЗУ + ОЗУ-копию */
#define GFX_BANK_NOSHADOW 0x54 /* только видео-ОЗУ (врем. вывод) */
#define GFX_BANK_TRANSPARENT 0x58 /* байт 0xFF не записывается */
#define GFX_BANK_SPRITE 0x5C /* NOSHADOW + TRANSPARENT */
/* Прозрачный цвет аппаратной прозрачности (256-режим: пиксель;
* 16-режим: ПАРА пикселей цвета 15). Цвет 255 нельзя нарисовать
* через банк с битом 3 — он и есть прозрачный. */
#define GFX_TRANSPARENT 0xFF
inline void gfx_set_bank(uint8_t bank) { _gfx_bank = bank; } /* GFX_BANK_* */
inline uint8_t gfx_get_bank(void) { return _gfx_bank; }
/* Клип спрайтов по экрану (putsprite/movesprite/спрайтовый движок).
* on=1 (дефолт) — клипить (спрайты могут уходить за края); on=0 —
* ОТКЛЮЧИТЬ (приложение ГАРАНТИРУЕТ, что спрайты целиком на экране —
* иначе запись за край портит соседнюю память, см. tests/spriteclip).
* Выигрыш ~+19% на анимации: движок зовёт noclip-ядра без клип-кода
* (не просто скип проверки — вся клип-математика физически отсутствует,
* SDCC тесней раскладывает регистры). Переключение — перенаправлением
* внутренних указателей на ядра (один раз здесь, не ветка в горячем
* цикле движка). НЕ влияет на putimage/gfx_blit (у них клип всегда). */
extern uint8_t _gfx_sprite_clip;
void gfx_sprite_clip(uint8_t on);
inline uint8_t gfx_get_sprite_clip(void) { return _gfx_sprite_clip; }
/* ---- Блиттинг (ядро спрайтов; Фаза B sprite-api-design) ----------- *
* Формат картинки — getimage: uint16 w, uint16 h, пиксели построчно
* (1 байт/пиксель в 256-режиме). Буфер обязан лежать ВНЕ W3
* (< 0xC000) — на время операции W3 замаплен на видеобанк.
* Блит через accel block-copy (до 256 байт/burst), клиппинг по экрану
* есть всегда; рисует ТЕКУЩИМ банком (gfx_set_bank). */
/* Блит целой картинки левым-верхним углом в (x,y). */
void gfx_blit(int x, int y, const void *img);
/* Быстрый блит БЕЗ клипа в ТЕКУЩЕМ банке: картинка обязана быть целиком на
* экране, w/h ≤ 255. Идёт линейным спрайтовым ядром — ~2.9× быстрее
* gfx_blit (замер PoP roomtest 2026-07-27: 4617 против 13288 тактов на
* спрайт 32×3; общее ядро тратит время на клип/16-бит/split, а не на
* пиксели). Для тайловых движков; где нужен клип — gfx_blit/gfx_blit_part. */
void gfx_blit_noclip(int x, int y, const void *img);
/* Быстрый heal БЕЗ клипа: прямоугольник обязан быть целиком на экране,
* w,h ≤ 255. Пара к gfx_blit_noclip (общее ядро тратит время на клип и
* 16-бит, а не на пиксели: замер — 11 658 тактов на heal 22×22). */
void gfx_heal_noclip(int x, int y, uint8_t w, uint8_t h);
/* ЗЕРКАЛО ПО ВЕРТИКАЛИ (row-major картинки): последняя строка источника
* ложится в верхнюю строку прямоугольника. (x,y) — по-прежнему левый
* ВЕРХНИЙ угол результата, поэтому вызывающему не надо менять арифметику
* позиции — только выбрать другую функцию. Стоят ровно столько же, сколько
* не-зеркальные двойники: у row-major вертикальное зеркало это порядок
* строк, а он задаётся знаком src-страйда (accel копирует строку вперёд).
* Для column-major (gfx_blit_cols*) такого варианта НЕТ и быть не может —
* там пришлось бы реверсить внутри колонки; бесплатное зеркало у них
* горизонтальное (флаг flip). */
void gfx_blit_noclip_vflip(int x, int y, const void *img);
void gfx_blit_part_noclip_vflip(int x, int y, const void *img,
uint8_t sx, uint8_t sy, uint8_t w, uint8_t h);
void gfx_blit_part_vflip(int x, int y, const void *img,
int sx, int sy, int w, int h);
/* Блит под-прямоугольника (sx,sy,w,h) картинки (атлас кадров:
* кадр N ленты — sx = N*FRAME_W). Под-прямоугольник обязан лежать
* внутри img; по экрану — клиппится. */
void gfx_blit_part(int x, int y, const void *img,
int sx, int sy, int w, int h);
/* Тот же под-прямоугольник БЕЗ клипа по экрану — линейным спрайтовым
* ядром (пара к gfx_blit_noclip). Вызывающий гарантирует: под-
* прямоугольник внутри img, назначение целиком на экране, w,h,y <= 255.
* Для тайловых движков, кладущих поверх спрайта только перекрывающую его
* часть тайла (PoP: fore-слой). */
void gfx_blit_part_noclip(int x, int y, const void *img,
uint8_t sx, uint8_t sy, uint8_t w, uint8_t h);
/* Блит спрайта column-major (пиксели по колонкам) — вертикальным accel-
* проходом. flip!=0 = горизонтальное зеркало (направление персонажа) без
* CPU-реверса/второй копии; зеркало ВНУТРИ футпринта [x,x+w) (для «hot-
* point» подавай x=anchor-w при флипе). Клип по экрану есть.
* Для персонажей (флип); фон — обычный gfx_blit (row-major). */
void gfx_blit_cols(int x, int y, const void *img, uint8_t flip);
/* То же с ОБРЕЗКОЙ СВЕРХУ: рисуются только строки [skip, skip+rows) кадра,
* (x,y) — экранная позиция ПЕРВОЙ нарисованной строки (как gfx_blit_part).
* rows <= 0 = «до низа кадра». Для персонажа, ушедшего головой под пол
* (порт clip_char/obj_clip_top в PoP). Обрезка сверху бесплатна: колонка —
* непрерывный кусок ОЗУ, сдвигается только старт. */
void gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip,
int skip, int rows);
/* То же + ОГРАНИЧЕНИЕ ШИРИНЫ: рисуются только первые maxw колонок футпринта,
* считая от x (maxw = 0 — вся ширина кадра). То есть обрезка СПРАВА по
* произвольной линии, а не только по краю экрана.
*
* Для column-major это столь же дёшево, как обрезка сверху: колонок просто
* меньше. Работает и при flip — зеркало происходит внутри футпринта, а
* первые maxw нарисованных колонок всегда ложатся в его ЛЕВУЮ часть.
*
* Мотивация — clip_char в PoP (obj_clip_right): персонаж, поднимающийся по
* лестнице за дверью уровня, обязан уходить за правый косяк проёма, а сама
* дверь рисуется ПОД ним и перекрыть его не может. */
void gfx_blit_cols_part_w(int x, int y, const void *img, uint8_t flip,
int skip, int rows, uint8_t maxw);
/* То же + ОБРЕЗКА СЛЕВА: рисуются только колонки [skipw, skipw+maxw)
* футпринта, считая от x (skipw = 0 — от левого края, maxw = 0 — до правого
* края кадра). Для column-major левая обрезка стоит столько же, сколько
* правая: колонка — непрерывный кусок ОЗУ, меняется стартовая колонка
* источника и экранная X. Работает и при flip: НАРИСОВАННЫЕ колонки всегда
* идут слева направо по футпринту, независимо от направления чтения.
*
* Мотивация — зеркало в PoP (obj_clip_left, seg003:0798 / seg008:1699):
* отражение Кида видно только внутри проёма, тень уровня 4 — только справа
* от зеркала. Здесь живёт ПОЛНОЕ тело блита колонками, остальные три
* варианта — обёртки. */
void gfx_blit_cols_part_wx(int x, int y, const void *img, uint8_t flip,
int skip, int rows, uint8_t skipw, uint8_t maxw);
/* ---- Блит колонками с ЛОГИЧЕСКОЙ ОПЕРАЦИЕЙ ------------------------ *
* Акселератор умеет не только копировать блок, но и совмещать его с тем,
* что уже лежит в приёмнике: приёмник = приёмник <op> источник. Значения
* GFX_OP_* — это ОПКОДЫ Z80 `and/or/xor (hl)`: именно ими переключается
* автомат акселератора, и именно они патчатся SMC в ядре (одна функция на
* все три операции, ветвления в цикле нет). Иных значений не бывает.
*
* ПРОЗРАЧНОСТЬ. Аппаратная прозрачность (банк с битом 3 — не писать #FF)
* смотрит на РЕЗУЛЬТАТ, а не на источник:
* GFX_OP_AND — #FF нейтрален сам по себе, обычный атлас годится;
* GFX_OP_OR — #FF | bg = #FF, подавляется банком 0x58/0x5C — обычный
* атлас годится;
* GFX_OP_XOR — с прозрачным #FF даёт инверсию фона по всему футпринту;
* источник ОБЯЗАН хранить прозрачный пиксель как 0x00.
* Операция читает приёмник из ОЗУ-копии экрана: при банках 0x54/0x5C это
* ЧИСТЫЙ ФОН (нарисованного поверх не видно), при 0x50 — реальная картинка.
* Подробности — шапка common/gfx_blit_cols_part_wx_op.c. */
#define GFX_OP_AND 0xA6 /* опкод `and (hl)` */
#define GFX_OP_XOR 0xAE /* опкод `xor (hl)` */
#define GFX_OP_OR 0xB6 /* опкод `or (hl)` */
/* NOT — приёмник = ~источник (сам приёмник в операции НЕ участвует).
* Тоже через акселератор, но другой цепочкой: у него нет режима
* «инвертировать буфер» (`CPL` автомат не распознаёт — инвертируется
* регистр CPU, а не буфер), зато ~src = src XOR #FF, поэтому вторым
* burst'ом идёт XOR по константному блоку единиц (common/_bgi_ones256.c).
* Значение выбрано вне диапазона опкодов операций — это НЕ опкод. */
#define GFX_OP_NOT 0x01
/* --- строками (row-major, как gfx_blit/gfx_blit_part) --- */
void gfx_blit_op(int x, int y, const void *img, uint8_t op);
void gfx_blit_part_op(int x, int y, const void *img,
int sx, int sy, int w, int h, uint8_t op);
/* --- колонками (column-major, как gfx_blit_cols; flip бесплатный) --- */
void gfx_blit_cols_op(int x, int y, const void *img, uint8_t flip, uint8_t op);
/* Полный вариант: те же клип-параметры, что у gfx_blit_cols_part_wx
* (skip/rows — обрезка сверху, skipw/maxw — окно по колонкам). */
void gfx_blit_cols_part_wx_op(int x, int y, const void *img, uint8_t flip,
int skip, int rows, uint8_t skipw, uint8_t maxw,
uint8_t op);
/* То же БЕЗ клипа по экрану — пара к gfx_blit_cols_part так же, как
* gfx_blit_part_noclip к gfx_blit_part. Вызывающий гарантирует: спрайт
* целиком на экране, ширина/высота кадра и y <= 255, skip < высоты.
* rows = 0 — «до низа кадра». Экономит ~5.6 К тактов подготовки на вызов
* (замер PoP roomtest: подготовка 5 622 против 13 596 на сам accel-проход),
* что для персонажа со спрайтом-компаньоном (клинок) даёт заметную долю
* кадра. */
void gfx_blit_cols_part_noclip(int x, int y, const void *img, uint8_t flip,
uint8_t skip, uint8_t rows);
/* Восстановить прямоугольник экрана из ОЗУ-копии (стирание спрайта/
* оверлея, нарисованного банком с битом 2: 0x54/0x5C). Всегда
* работает банком GFX_BANK_NORMAL независимо от gfx_set_bank;
* клиппится по экрану. Буфер сохранения не нужен: ОЗУ-копия и есть
* бэкап фона (чтение из #50..#5F всегда возвращает её). */
void gfx_heal(int x, int y, int w, int h);
/* Block until the next frame (50 Hz). Типичный double-buffer:
* gfx_set_draw_page(hidden); draw_frame(); gfx_wait_vsync();
* gfx_set_visible_page(hidden); // tear-free flip */
void gfx_wait_vsync(void);
/* Что вызывать, ПОКА gfx_wait_vsync ждёт луч. Ожидание кадра — самый
* длинный простой в кадре (при пейсинге «3 растровых кадра на логический
* тик» это ~42 мс из 60), и хук занимает его, не отнимая тактов у
* полезной работы. Повод — вычерпывание FIFO клавиатуры по опросу
* (`kbd_raw_poll`, см. <kbd_raw.h>): трёхбайтовый приёмник переполняется
* на пачках «fake shift», а плотность нужна ~раз в 0.5 мс, которую иначе
* взять негде. Хук зовётся в тесном цикле: он обязан быть дешёвым, НЕ
* рисовать (окно W3 сейчас не за графикой) и не ждать сам. fn = 0 —
* снять. На путь FPS-делителя (gfx_set_fps_div n>=2) не влияет: там
* ожидание идёт через HALT, и процессор не крутит цикл. */
void gfx_set_idle_hook(void (*fn)(void));
/* FPS-делитель (frame pacing): логический кадр = РОВНО n кадровых
* интервалов, независимо от плавания длительности рендера. n: 1 = 50 fps
* (дефолт), 2 = 25, 3 = ~16.7, 4 = 12.5, ...; 0 трактуется как 1.
* Меняет ТОЛЬКО поведение gfx_wait_vsync() (call-sites не трогаются):
* при n>=2 vsync ждёт n фронтов по фоновому счётчику кадров (ISR),
* выравниваясь на ближайший фронт при переполнении слота (без дрейфа);
* при n<=1 — обычный лучевой поллинг.
* Возврат 0 / -1+errno: EINVAL (неподходящий memory mode — данные не в
* W2), ENOMEM (заняты все слоты цепочки кадровых обработчиков). */
int gfx_set_fps_div(uint8_t n);
/* ---- Bitmap-font text -------------------------------------------- *
* Шрифт 256 глифов × 8 рядов × 1 байт (ZX-Spectrum формат), 2 КБ.
* Грузится лениво при первом использовании BGI-текста; gfx_set_font
* подменяет указатель (storage должен жить, пока шрифт активен). */
void gfx_load_default_font(void);
void gfx_set_font(const uint8_t *font);
/* ---- Palette ----------------------------------------------------- *
* Каждая графическая страница имеет свою палитру (page 0 → palette 0,
* page 1 → palette 1). Для seamless double-buffering грузить одну
* палитру в обе. */
void gfx_pal_load(uint8_t pal_num, uint8_t start, uint8_t count,
const uint8_t *data);
void gfx_pal_set (uint8_t pal_num, uint8_t idx,
uint8_t r, uint8_t g, uint8_t b);
void gfx_pal_get (uint8_t pal_num, uint8_t start, uint8_t count,
uint8_t *data);
void gfx_pal_get_color(uint8_t pal_num, uint8_t idx,
uint8_t *r, uint8_t *g, uint8_t *b);
void gfx_pal_reset(void);
/* Палитра страницы 1 := палитре страницы 0 (все 256 записей).
* ОБЯЗАТЕЛЬНЫЙ шаг дабл-буфера: без него флип на страницу 1 показывает
* сцену чёрной (у каждой страницы своя палитра). Звать после initgraph
* и после правок палитры 0. */
void gfx_pal_sync(void);
/* Палитра ↔ файл. Формат: записи по 4 байта (B, G, R, 0 — родной
* BIOS $A4), полная палитра = 1024 байта. fload принимает и усечённый
* файл (напр. 64 Б = 16 цветов) — остальные слоты не трогает; возврат:
* число загруженных записей или -1. fsave пишет все 256; 0 / -1. */
int gfx_pal_fsave(uint8_t pal_num, const char *path);
int gfx_pal_fload(uint8_t pal_num, const char *path);
#endif