f2093e0d89
Жалоба: стойка неподвижных Кида и стража занимала больше полутора
кадровых периодов. Гипотеза «виноват __banked» ЗАМЕРОМ НЕ
ПОДТВЕРДИЛАСЬ: весь банковый вызов (трамплин + смена страницы + тело
pop_guard_tick + возврат) стоит 654 такта при бюджете кадра 430 000.
Как мерил (выборка PC бесполезна — мост MAME отвечает из фреймового
колбэка, все сэмплы падают в обработчик прерывания): breakpoint'ы MAME с
действием {printf totalcycles; g} на входах фаз главного цикла, разности
соседних меток = стоимость фазы. Плюс профилирование полосами бордюра
(make PROF=1, макрос PROF() в roomtest.c) для быстрого взгляда.
Замер комнаты 3 (Kid + страж), такты, кадр = 430 000:
fore поверх стража 431 964
fore поверх Kid 402 816 -> 78 % всей работы кадра
остальное 241 956
ИТОГО 1 076 736 = 2.5 кадра
Две причины, обе устранены:
1. Fore-слой рисовал ЦЕЛЫЕ тайлы, хотя существует ровно для того, чтобы
вернуть куски поверх спрайта — за его прямоугольником в видеопамяти и
так правильный фон. Введено ОКНО клипа (pop_fore_set_clip): спрайт
сообщает свой итоговый габарит (у Kid — с клинком, брызгами и
обрезкой clip_char), fore-проход режет по нему. Отсев трёхступенчатый:
тайл целиком (tile_in_fclip, до обращения к атласу), кусок по грубому
габариту (до gfx_w0_map — w/h лежат в EMM-странице), и точный клип в
blit_b. Для последнего добавлен libbgi-примитив
gfx_blit_part_noclip — пара к gfx_blit_noclip, но под-прямоугольник.
2. Оставшиеся 341 К после клипа оказались НЕ пикселями: 18 вызовов
pop_prandom за проход, ~10 700 тактов каждый (32-битный LCG:
__mullong ~8 000 + __moduint). Раскладка кладки тайла — чистая
функция (комната, ряд, колонка), то есть константа комнаты, а
wall_pattern пересчитывал её каждый кадр. Теперь кэшируются готовые
РЕШЕНИЯ (что рисовать и с каким смещением), 3 байта на тайл, сброс в
pop_room_draw. Порядок вызовов prandom воспроизведён один в один,
включая то, что значение метки берётся только при сработавшем условии.
Итог того же замера: fore поверх стража 37 464, поверх Kid 46 464,
кадр целиком 334 716 = 0.78 периода (было 2.5). Ускорение 3.2x, сами
fore-проходы — 10x.
Проверка отсутствия регрессии: попиксельная разность скриншотов комнат
1/2/3 до и после — отличаются ТОЛЬКО языки пламени факелов (анимация),
кладка и метки совпадают байт в байт.
Побочно: sprinter-cc научился пробрасывать -DNAME в sdcc.
Память: куча W2 1245 -> 996 Б (кэш кладки 120 Б), резидент W3 12 819 ->
14 512 (свободно 1872 Б — становится тесно), банк 1 236/16384.
ВНИМАНИЕ: make size-check показывает рост 7 программ, но эталон
docs/size_baseline.tsv отстал (последний раз принят в 484b18d, libbgi
менялась в 95c22be/c127a4b/64ce633) — к этой правке рост отношения не
имеет: новый модуль библиотеки в чужие программы не линкуется.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
189 lines
13 KiB
C
189 lines
13 KiB
C
/*
|
||
* 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);
|
||
|
||
/* Блит под-прямоугольника (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);
|
||
|
||
/* Восстановить прямоугольник экрана из ОЗУ-копии (стирание спрайта/
|
||
* оверлея, нарисованного банком с битом 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);
|
||
|
||
/* 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
|