Files
Sprinter-SDCC/libbgi/include/sprite.h
T
snark13 95c22be9bd libbgi: скролл-примитивы + --w3 + отчёт раскладки памяти
Скролл региона video->video (неактивная страница -> активная, банк 0x50:
копия = скролл + heal цели):
- gfx_scroll_h / _bgi_scroll_rows_raw — горизонтальный, построчно без
  страйдов (~54Т/строку), DI/EI бандами по 16 строк, h=0=>256;
- gfx_scroll_v / _bgi_scroll_cols_raw — верт. И/ИЛИ гориз. за один проход
  без буфера (колонка = accel-burst LD A,A, STOP между read/write делает
  промежуточный OUT Port_Y безопасным), банды по 16 колонок;
- _gfx_addr_shadow_base (адрес неактивной страницы) + gfx_rect_t.
Пример examples/scroll.

check_banks.py + sprinter-cc: отчёт раскладки памяти для ЛЮБОЙ модели
(W1/W2 код/данные, остаток кучи/стека, W3 при --w3, банки при --bank),
не только при --bank.  Док docs/memory-management.md §10.

--w3 (резидентный код окна W3) + сопутствующее: crt0_banked W3_RESIDENT,
mkexe -W, tests/w3probe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:38:00 +03:00

324 lines
24 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.
/*
* sprite.h — managed-движок спрайтов (retained-модель) поверх ядра
* gfx_blit/gfx_heal. Дизайн: docs/sprite-api-design.md §9.1.
*
* Мотивация — profile examples/balls vs asm-референса docs/samples/balls
* (2026-07-12): ~2× разрыв не в W3-скобке, а в per-call C-обвязке
* (клип/парс заголовка/save-restore банка/SDCC-фрейм), повторяемой 2×N
* раз за кадр. Движок амортизирует её: sprite_update держит ОДНУ
* W3-скобку и ОДИН банк на весь ПРОХОД, вызывая bracket-free ядра
* (_gfx_heal_full/_gfx_blit_full) в цикле по спрайтам. DI остаётся
* гранулярным (по строке в leaf) — под CBL/IM2-аудио длинный DI
* недопустим.
*
* Retained-модель: sprite_move/frame/show/hide/touch меняют ТОЛЬКО
* состояние (O(1), сколько угодно раз за кадр); всё рисование —
* один раз в sprite_update, двухпроходно «heal ВСЕ → блит ВСЕ» (слияние
* per-sprite выкусывает соседей на перекрытии — проверено). Учёт
* «где спрайт реально нарисован на КАЖДОЙ из двух страниц» (drawn[2])
* ведёт движок, не приложение.
*
* Формат картинки — getimage (uint16 w, uint16 h, пиксели построчно,
* 1 байт/пиксель в 256-режиме); 0xFF = прозрачно (банк 0x5C). Буфер
* обязан лежать ВНЕ W3 (< 0xC000). Размер КАДРА спрайта ≤ 64×64
* (требование движка): кэш w,h — uint8_t, и блит идёт лин-ядром без
* split'а на полосы >256 (спрайт заведомо ≤256 в строке). Для больших
* картинок — gfx_blit/putimage напрямую (там общий путь со split'ом).
* ЛЕНТА-АТЛАС может быть шире 64 (img_w — шаг), но КАДР ≤ 64.
*
* z-order = индекс в массиве (последний рисуется сверху). Массив
* владеет приложением: без malloc, без лимитов; пересортировка z =
* перестановка элементов (drawn-состояние едет со структурой).
*/
#ifndef SPRITE_H
#define SPRITE_H
#include <stdint.h>
/* Публичные флаги sprite_t.flags. */
#define SPR_VISIBLE 0x01u /* спрайт рисуется в sprite_update */
#define SPR_ALWAYS 0x02u /* перерисовывать КАЖДЫЙ кадр (статик, над */
/* которым постоянно ходят активные спрайты) */
/* Внутренние dirty-биты (владеет движок): «страница p требует обновления
* этого спрайта». Приложению не трогать — только через sprite_*. */
#define _SPR_DIRTY0 0x04u
#define _SPR_DIRTY1 0x08u
#define _SPR_DIRTY (_SPR_DIRTY0 | _SPR_DIRTY1)
/* Статус-биты sprite_t.an_flags (чтение — sprite_anim_status()):
* *_ON — анимация активна (тикается в sprite_update); *_DONE — дошла
* до конца сама (one-shot до last / перемещение до цели) или была
* добита sprite_move_stop(at_target=1). DONE сбрасывается новым
* запуском соответствующей анимации. */
#define SPR_ANIM_ON 0x01u /* кадровая анимация идёт */
#define SPR_ANIM_DONE 0x02u /* one-shot дошёл до last */
#define SPR_MOVE_ON 0x04u /* tween-перемещение идёт */
#define SPR_MOVE_DONE 0x08u /* перемещение достигло цели */
/* Внутренние биты an_flags (владеет движок) — три РАЗНЫЕ категории:
* режим анимации (зеркало параметра mode), свойство ЛЕНТЫ (ориентация
* данных в атласе) и runtime-состояние (меняется само в процессе). */
#define _SPR_PP_BACK 0x10u /* runtime: маятник сейчас идёт назад */
#define _SPR_STRIP_HORZ 0x20u /* лента: кадры вдоль X (двигается sx) */
#define _SPR_ANIM_PP 0x40u /* режим: маятник */
#define _SPR_ANIM_ONCE 0x80u /* режим: one-shot */
/* Режимы sprite_anim(). */
#define ANIM_LOOP 0u /* 0,1,..,last,first,.. (цикл) */
#define ANIM_PINGPONG 1u /* 0,1,..,last,last-1,..,first,1,.. */
#define ANIM_ONCE 2u /* first..last, замереть на last + DONE */
#define ANIM_HORIZ 0x80u /* ИЛИ-флаг: кадры вдоль X (сетка nx>1) */
/* ВНИМАНИЕ: порядок полей ЗАШИТ в asm-ядро тикера (офсеты полей в
* common/_sprite_tick.c, tick_move) — при изменении порядка/типов
* пересчитать офсеты там (offsetof-пробником, рецепт в его шапке). */
typedef struct {
const void *img; /* getimage-формат / атлас-лента (вне W3) */
/* --- кэш адреса кадра (владеет движок; менять ТОЛЬКО через
* sprite_frame) ---
* src — ГОТОВЫЙ адрес первого пикселя ТЕКУЩЕГО кадра (img + 4 +
* sx + sy*stride). Блит берёт его как есть: ни чтения заголовка,
* ни умножения в кадровом цикле. Тикер анимации двигает src
* инкрементально (±an_step байт); sx/sy при этом НЕ обновляются —
* они «последняя позиция, установленная sprite_frame», и снова
* становятся истиной при следующем sprite_frame/sprite_anim
* (те ставят ось абсолютно, а не относительно). */
const uint8_t *src; /* адрес пикселей текущего кадра */
uint16_t stride; /* полная ширина img в байтах (шаг строки) */
int x, y; /* ЛОГИЧЕСКАЯ позиция левого-верхнего угла кадра */
int sx, sy; /* смещение кадра, заданное sprite_frame (см. src)*/
uint8_t w, h; /* размер кадра (кэш заголовка), 1..255 */
uint8_t flags; /* SPR_* | внутренние dirty */
uint8_t page; /* 0 = img в обычной памяти; иначе физ. страница *
* W0-атласа (img — адрес 0x0100-0x3FFF, движок *
* сам мапит страницу в W0 на время блита) */
/* --- авто-анимация (владеет тикер; настраивать ТОЛЬКО через
* sprite_anim../sprite_moveto.., читать — inline-аксессорами) --- */
uint8_t an_flags; /* статус: SPR_ANIM_.. | SPR_MOVE_.. */
uint8_t an_frame; /* ТЕКУЩИЙ индекс кадра (0-based по ленте) */
uint8_t an_first, an_last; /* границы анимации (индексы) */
uint8_t an_speed, an_timer; /* кадров между сменами / счётчик */
uint16_t an_step; /* шаг кадра в БАЙТАХ src: fw (гориз. лента) *
* или fh*stride (верт.) — считает sprite_anim */
const uint8_t *an_src0; /* src кадра first (для wrap цикла) */
uint8_t mv_speed, mv_timer; /* кадров между шагами / счётчик */
uint8_t mv_step; /* макс. пикселей вдоль большей оси за шаг */
int mv_tx, mv_ty; /* цель (для sprite_move_stop) */
/* DDA по главной оси (беззнаковый — на Z80 16-бит беззнаковые
* сравнения в разы дешевле знаковых Брезенхэма, 2026-07-14):
* главная ось шагает каждую итерацию, минорная подтягивается
* аккумулятором; «приехали» = mv_rem == 0 (сравнений координат нет). */
uint16_t mv_rem; /* осталось шагов главной оси до цели */
uint16_t mv_acc; /* аккумулятор минорной оси (старт dmaj/2) */
uint16_t mv_dmaj, mv_dmin; /* |дельты| главной / минорной осей */
uint8_t mv_ymaj; /* 1 = главная ось Y (иначе X) */
int8_t mv_sgnmaj, mv_sgnmin;/* знаки шага главной/минорной осей */
/* --- внутреннее (владеет движок) --- */
struct {
int x, y; /* где нарисован на странице p */
uint8_t on; /* нарисован ли на странице p */
} drawn[2];
/* Слой для Y-сортировки (см. gfx_sprite_ysort): при включённой
* сортировке спрайты упорядочиваются по {layer, y} — слой старше:
* слой 1 ВСЕГДА поверх слоя 0 (ходячие/летающие/курсор...), внутри
* слоя — painter по y. 0 после sprite_init; без сортировки не
* используется (порядок = индекс массива). Поле В КОНЦЕ структуры:
* офсеты полей выше зашиты в asm тикера/сортировщика. */
uint8_t layer;
} sprite_t;
/* Инициализация: w,h из заголовка img, кадр (0,0), невидим, ничего не
* нарисовано. Для АТЛАСА (кадр — под-прямоугольник) после init
* выставить s->w/s->h в размер кадра и перейти на нужный кадр ТОЛЬКО
* через sprite_frame (он ведёт кэш src; голые s->sx/s->sy кэш не
* обновят). */
void sprite_init(sprite_t *s, const void *img);
/* Кадр всей сцены: на ТЕКУЩЕЙ draw-странице (gfx_get_draw_page)
* heal ВСЕХ старых позиций → блит ВСЕХ видимых (двухпроходно, одна
* скобка/банк на проход). Спрайты без изменений с прошлого визита этой
* страницы (и без SPR_ALWAYS) не трогаются. Для дабл-буфера звать после
* gfx_set_draw_page(hidden); для одиночной страницы/курсора — прямо на
* видимой.
*
* ВСЯ СЦЕНА СТРАНИЦЫ — ОДИН ВЫЗОВ! Два sprite_update на одну страницу
* (например «сначала ходячие, потом летающие») ЛОМАЮТ перекрытия МЕЖДУ
* группами: heal второй группы берёт из ОЗУ-копии чистый ФОН (спрайты
* в копию не пишутся — банк 0x5C) и стирает нарисованных первой
* группой; двухпроходная дисциплина «heal все → блит все» покрывает
* перекрытия только ВНУТРИ одного вызова. Логические группы держать
* диапазонами индексов одного массива (z-порядок групп = порядок
* диапазонов; слоение при Y-сортировке — полем layer). */
void sprite_update(sprite_t *arr, uint8_t count);
/* Сахар канонического дабл-буфер-кадра: draw-страница = скрытая →
* sprite_update → gfx_wait_vsync → сделать скрытую видимой (tear-free).
* Динамический фон/HUD рисовать банком 0x50 на скрытой странице ДО
* sprite_flip — тогда heal учтёт его через ОЗУ-копию. */
void sprite_flip(sprite_t *arr, uint8_t count);
/* Режимы gfx_sprite_ysort (битовая маска компонентов ключа). */
#define YSORT_OFF 0u /* сортировки нет: z-order = индекс массива */
#define YSORT_Y 0x01u /* painter по y (нижние поверх верхних) */
#define YSORT_LAYER 0x02u /* слои: layer 1 поверх layer 0 */
/* Y-сортировка блит-прохода (painter's algorithm): mode — какие
* компоненты ключа участвуют в порядке отрисовки:
* YSORT_Y | YSORT_LAYER — полный порядок {layer, y}: сначала слои
* (спрайт слоя 1 ВСЕГДА поверх слоя 0 — ходячие/летающие/
* курсор...), внутри слоя — по y (нижние по экрану поверх
* верхних, «ближе к зрителю»);
* YSORT_Y — только painter по y, слои игнорируются;
* YSORT_LAYER — только слои; внутри слоя порядок стабилен
* (наследуется от прошлого кадра / порядка массива);
* YSORT_OFF (0, дефолт) — z-order = индекс массива.
* Выключенный компонент маскируется в ключе (2 AND на спрайт, ~1К на
* 15 спрайтов) — сортировщик не замедляется ветвлениями.
*
* ПЕРСИСТЕНТНОСТЬ: таблица порядка живёт МЕЖДУ кадрами — каждый кадр
* ключи обновляются и порядок пересортировывается вставками; спрайты
* двигаются на пиксели за кадр, порядок почти актуален → почти
* линейно. Цена (замер rpgwalk-15, 2026-07-14): ~28К тактов на 15
* спрайтов (~2К/спрайт, ~6% бюджета кадра; построение с нуля каждый
* кадр стоило 44К). Массив приложения при этом НЕ трогается (sp[2]
* всегда персонаж 2) — сортируются внутренние записи-указатели. При
* смене arr/count (другая сцена) таблица перестраивается сама; первый
* кадр после включения/смены — полная сортировка (разово дороже).
*
* Сортировка СТАБИЛЬНА относительно ПРОШЛОГО КАДРА: спрайты с равным
* ключом сохраняют взаимный порядок прошлого кадра (не мерцают), а не
* порядок массива. Лимит: до 32 спрайтов на вызов sprite_update;
* больше — кадр рисуется без сортировки (безопасный фолбэк).
* Выключено = 0 тактов И 0 байт: код сортировщика и таблица не
* линкуются (funcptr-DCE). heal-проход порядка не требует и остаётся
* линейным.
*
* Замечания: (1) перекрывающиеся спрайты обязаны быть dirty ОБА, чтобы
* смена их взаимного порядка отрисовалась — движущиеся грязнятся сами,
* статики помечать sprite_touch (правило то же, что и без сортировки);
* (2) сортировка перемешивает порядок W0-атласов — смен страницы W0
* может стать больше (один дешёвый OUT на смену). */
void gfx_sprite_ysort(uint8_t on);
/* --- O(1) изменения состояния (inline: без call-оверхеда в физике) --- */
/* Переместить: меняет только логическую позицию + грязнит обе страницы. */
inline void sprite_move(sprite_t *s, int x, int y) {
if (s->x != x || s->y != y) { s->x = x; s->y = y; s->flags |= _SPR_DIRTY; }
}
/* Сменить кадр атласа (под-прямоугольник img). Не inline: пересчитывает
* кэш src (одно 16-бит умножение sy*stride) — при АКТИВНОЙ кадровой
* анимации не звать (тикер ведёт src сам). */
void sprite_frame(sprite_t *s, int sx, int sy);
/* Показать (если был скрыт — грязнит обе страницы). */
inline void sprite_show(sprite_t *s) {
if (!(s->flags & SPR_VISIBLE)) s->flags |= (SPR_VISIBLE | _SPR_DIRTY);
}
/* Скрыть (если был виден — грязнит: sprite_update сотрёт heal'ом). */
inline void sprite_hide(sprite_t *s) {
if (s->flags & SPR_VISIBLE) { s->flags &= ~SPR_VISIBLE; s->flags |= _SPR_DIRTY; }
}
/* Принудительная перерисовка на ОБЕИХ страницах (напр. после того как
* активный спрайт прошёл над статиком — приложение помечает статик). */
inline void sprite_touch(sprite_t *s) { s->flags |= _SPR_DIRTY; }
/* ==== Авто-анимация (§9г дизайна) ====================================
* Тикается САМА в начале каждого sprite_update (раз на кадр), код
* тикера подключается через funcptr — программа без анимаций его не
* линкует. Кадровая анимация и tween-перемещение НЕЗАВИСИМЫ и могут
* идти одновременно. Пока кадровая активна — не трогать sprite_frame
* вручную (тикер ведёт ось сам); границы кадров должны лежать вдоль
* ОДНОЙ оси ленты (вертикальная по умолчанию, ANIM_HORIZ — вдоль X). */
/* Запустить кадровую анимацию: кадры first..last (индексы вдоль оси
* ленты), смена раз в speed кадров (1 = каждый кадр), mode = ANIM_LOOP/
* ANIM_PINGPONG/ANIM_ONCE (| ANIM_HORIZ для горизонтальной ленты).
* Старт с first; сбрасывает SPR_ANIM_DONE. */
void sprite_anim(sprite_t *s, uint8_t first, uint8_t last,
uint8_t speed, uint8_t mode);
/* Остановить кадровую анимацию: frame = индекс кадра, который останется
* на экране, или -1 — замереть на текущем. */
void sprite_anim_stop(sprite_t *s, int8_t frame);
/* Запустить перемещение к (tx,ty): раз в interval кадров — шаг до
* max_step пикселей вдоль БОЛЬШЕЙ оси (меньшая тянется Брезенхэмом,
* без деления). Сбрасывает SPR_MOVE_DONE. Пример: (0,0)→(100,50),
* max_step 5, interval 4 → 20 шагов × 4 кадра = 80 кадров (1.6 с). */
void sprite_moveto(sprite_t *s, int tx, int ty,
uint8_t max_step, uint8_t interval);
/* Остановить перемещение: at_target = 0 — замереть где есть;
* 1 — прыгнуть в цель (и взвести SPR_MOVE_DONE). */
void sprite_move_stop(sprite_t *s, uint8_t at_target);
/* Статус анимаций: битовое поле SPR_ANIM_ON/DONE | SPR_MOVE_ON/DONE. */
inline uint8_t sprite_anim_status(const sprite_t *s) { return s->an_flags; }
/* Текущий индекс кадра ленты (ведёт тикер; валиден и после stop). */
inline uint8_t sprite_anim_frame(const sprite_t *s) { return s->an_frame; }
/* Перемещение ещё идёт? */
inline uint8_t sprite_moving(const sprite_t *s) {
return s->an_flags & SPR_MOVE_ON;
}
/* ==== Атласы в EMM-страницах (W0) ====================================
* Файл .atl (docs/sprite-api-design.md §3.1, вариант II): 0x100-байтовый
* заголовок (магия 'SPA1', count; каталог с 0x68) + getimage-ленты;
* файл-офсет == офсет в странице == адрес в W0. Загрузчик читает файл
* ЦЕЛИКОМ в EMM-страницу через W3 и патчит ISR-стаб (0x38/0x66) —
* страница безопасна в W0 при включённых прерываниях (§9в).
*
* ВАЖНО: указатели лент — адреса 0x0100-0x3FFF, разыменовывать их можно
* ТОЛЬКО при подключенной странице (sprite_update мапит сам; для ручного
* putsprite/gfx_blit_part — обернуть в gfx_w0_map/gfx_w0_unmap).
* Пока страница в W0 — НИКАКИХ ESTEX/BIOS-вызовов. */
typedef struct {
uint8_t page; /* физ. страница (маппить в W0/W3) */
uint8_t blk; /* EMM-блок — для atlas_free */
uint8_t count; /* число лент в атласе */
} atlas_t;
/* Загрузить .atl в свежую EMM-страницу (read в W3 + патч стаба).
* 0 — OK; -1 + errno (файл/память/формат). */
int atlas_load(atlas_t *a, const char *path);
/* Освободить EMM-страницу атласа (все sprite_t с его лентами должны
* быть скрыты/переинициализированы ДО вызова). */
void atlas_free(atlas_t *a);
/* Указатель-лента idx (W0-адрес, см. ВАЖНО выше) — для gfx_blit_part
* и ручных вызовов; кадр (i,j) ленты: sx = i*fw, sy = j*fh. */
const void *atlas_image(const atlas_t *a, uint8_t idx);
/* Инициализировать спрайт лентой idx атласа: img/w/h (размер КАДРА из
* каталога) /page; невидим, кадр (0,0). Аналог sprite_init для W0. */
void atlas_sprite_init(sprite_t *s, const atlas_t *a, uint8_t idx);
/* Ручное подключение страницы атласа в W0 (для putsprite/gfx_blit_part
* вне движка). map ставит и _gfx_w0_cur (ISR-стаб восстановит страницу
* после прерывания), unmap возвращает страницу ядра DSS. */
void gfx_w0_map(uint8_t page);
void gfx_w0_unmap(void);
typedef struct {
int16_t x, y;
uint16_t w, h;
} gfx_rect_t;
/* Скролл региона area из НЕактивной страницы в активную со сдвигом на
* dx/dy (0 = чистая копия/heal, >0 = в сторону увеличения координат,
* <0 = уменьшения). Открывшуюся полосу |d| НЕ заполняют — возвращают
* в *dirty (может быть NULL). Банк 0x50: копия = скролл + heal цели. */
void gfx_scroll_h(const gfx_rect_t *area, int16_t dx, gfx_rect_t *dirty);
void gfx_scroll_v(const gfx_rect_t *area, int16_t dy, gfx_rect_t *dirty);
#endif