libbgi: спрайты — кэш адреса кадра, DDA+asm тик, Y-сортировка со слоями

Оптимизации A+B (профиль rpgwalk-15: активная часть кадра 410К → 307К
тактов из 430080; лимит спрайтов 16×16 на стабильные 48.8 fps: 14 → ~21):

- (B) sprite_t.src/stride — готовый адрес кадра: считают только
  sprite_frame (теперь функция, одно умножение на СМЕНУ кадра) и тикер
  (±an_step БАЙТ инкрементально); блит-ядра принимают src+stride,
  img/sx/sy из сигнатуры ушли.  Блит 177К → 146К на кадр.
- (A) тик 100К → 37.7К: tween переформулирован Брезенхэм → беззнаковый
  DDA (mv_rem/mv_acc, «приехали» = rem==0 — без знаковых 16-бит
  сравнений), tick_move и tick_anim — ручной asm (SDCC спиллит такие
  функции в IX-фрейм ~100 обращений; C-реструктуризации не помогали —
  проверено кодогеном).  Биты an_flags переименованы по категориям
  (_SPR_STRIP_HORZ, _SPR_PP_BACK).

Y-сортировка (gfx_sprite_ysort, идеи пользователя — 8-бит ключ,
персистентность):

- painter's algorithm по ключу {layer:8, clamp_y:8}; поле
  sprite_t.layer (в КОНЦЕ структуры — asm-офсеты не сдвигает): слои
  сцены в одном массиве/одном sprite_update;
- ПЕРСИСТЕНТНАЯ asm-таблица {key16, ptr16}: resort порядка прошлого
  кадра (почти линейно), rebuild при смене arr/count; массив
  приложения не трогается; ~28К/15 спрайтов (с нуля было 44К);
- компоненты YSORT_Y/YSORT_LAYER отключаемы независимо масками ключа
  (без ветвлений в сортировщике); ВНИМАНИЕ: mode=1 значит Y-only,
  полный порядок = YSORT_Y|YSORT_LAYER;
- funcptr-DCE: выключено = код и таблица не линкуются (rpgwalk −190 Б);
- ПРАВИЛО в sprite.h: два sprite_update на страницу запрещены (heal
  второй группы стирает спрайты первой — ОЗУ-копия чистая).

Попутные фиксы:

- libbgi/Makefile: .rel зависят от заголовков (HDRS) — stale .rel со
  старой раскладкой sprite_t молча ломал рантайм;
- rpgprof: --memory small (перерос tiny: BSS вылезал за W2 → мгновенный
  «Unexpected application termination»; mkexe это пока не ловит);
- tests/spranim: проверки переведены на кэш src, добавлены T6 (reframe
  после тикера) и T7 (Y-сортировка: порядок, слои, персистентный
  resort, LAYER-only) — 7/7 PASS в MAME.

Доки: §9д — новый бюджет (19.5К/спрайт), §9е — ПЛАН FPS-делителя
(frame pacing, gfx_set_fps_div); TODO — дизайн цепочки irq-обработчиков.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 17:19:24 +03:00
parent 0eec977630
commit b9ddce8d34
24 changed files with 1339 additions and 304 deletions
+105 -18
View File
@@ -55,10 +55,14 @@
#define SPR_ANIM_DONE 0x02u /* one-shot дошёл до last */
#define SPR_MOVE_ON 0x04u /* tween-перемещение идёт */
#define SPR_MOVE_DONE 0x08u /* перемещение достигло цели */
#define _SPR_ANIM_BACK 0x10u /* внутр.: маятник идёт назад */
#define _SPR_ANIM_HORZ 0x20u /* внутр.: лента горизонтальная (ось sx) */
#define _SPR_ANIM_PP 0x40u /* внутр.: режим маятника */
#define _SPR_ANIM_ONCE 0x80u /* внутр.: режим one-shot */
/* Внутренние биты 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,.. (цикл) */
@@ -66,10 +70,24 @@
#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; /* смещение кадра внутри img (атлас; 0 = целая) */
int sx, sy; /* смещение кадра, заданное sprite_frame (см. src)*/
uint8_t w, h; /* размер кадра (кэш заголовка), 1..255 */
uint8_t flags; /* SPR_* | внутренние dirty */
uint8_t page; /* 0 = img в обычной памяти; иначе физ. страница *
@@ -81,23 +99,40 @@ typedef struct {
uint8_t an_frame; /* ТЕКУЩИЙ индекс кадра (0-based по ленте) */
uint8_t an_first, an_last; /* границы анимации (индексы) */
uint8_t an_speed, an_timer; /* кадров между сменами / счётчик */
uint8_t an_step; /* шаг оси = fh (верт. лента) или fw (гориз.) */
int an_lo; /* осевое смещение кадра first (для wrap) */
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; /* цель */
int mv_dx, mv_dy, mv_err;/* Брезенхэм: dx, -dy, аккумулятор */
int8_t mv_sgnx, mv_sgny; /* знаки шага осей */
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, невидим, ничего не нарисовано.
* Для АТЛАСА (кадр — под-прямоугольник) после init выставить s->w/s->h
* в размер кадра и s->sx/s->sy — на нужный кадр (или sprite_frame). */
/* Инициализация: 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)
@@ -105,7 +140,16 @@ void sprite_init(sprite_t *s, const void *img);
* скобка/банк на проход). Спрайты без изменений с прошлого визита этой
* страницы (и без 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-страница = скрытая →
@@ -114,6 +158,49 @@ void sprite_update(sprite_t *arr, uint8_t count);
* 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-оверхеда в физике) --- */
/* Переместить: меняет только логическую позицию + грязнит обе страницы. */
@@ -121,10 +208,10 @@ 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 void sprite_frame(sprite_t *s, int sx, int sy) {
if (s->sx != sx || s->sy != sy) { s->sx = sx; s->sy = sy; 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) {