/* * gfx.h — Sprinter graphics: mode-agnostic API (BGI_GFX). * * Рисование (putpixel/line/bar/circle/…) вынесено в BGI — см. * и 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 /* 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); /* ---- Блит колонками с ЛОГИЧЕСКОЙ ОПЕРАЦИЕЙ ------------------------ * * Акселератор умеет не только копировать блок, но и совмещать его с тем, * что уже лежит в приёмнике: приёмник = приёмник источник. Значения * 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`, см. ): трёхбайтовый приёмник переполняется * на пачках «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