libbgi: спрайтовый движок v2 + accel-блит/heal leaf'ы + noclip-путь

Спрайтовая графика поверх accel block-copy (docs/sprite-api-design.md):

- Ядро блиттинга: leaf'ы _bgi_blit_rows_raw (dst фикс, только src-страйд) /
  _bgi_copy_rows_raw (getimage) / _bgi_heal_rows_raw (src==dst). DI один на
  спрайт (санкция: малый спрайт под одним DI аудио не рвёт); src[0]-фикс
  снят (точная MAME подавляет CPU-байт триггера записи — на железе
  перепроверить; для heal был избыточен и снят безусловно).
- Общие bracket-free ядра _gfx_blit_full/_gfx_heal_full (полная ширина:
  клип по экрану + split >256 для putimage) + лин _gfx_blit_sprite/
  _gfx_heal_sprite (кадр ≤64, без split, 8-бит w/h) + noclip-варианты
  (клип-кода нет → полный codegen-win).  Имя *_full (не *_clip) — «clip»
  двусмысленно (sprite-ядра тоже клипуют; различитель — ширина/split).
- Движок retained-модели <sprite.h>: sprite_init/update/flip + inline
  move/frame/show/hide/touch; drawn[2] per-page внутри структуры; кадр —
  двухпроходно heal ВСЕ -> блит ВСЕ под одной W3-скобкой/банком на проход.
- Флаг gfx_sprite_clip(on/off): приложение, само следящее за границами,
  отключает клип (~+19% на анимации; диспетч пока через if — funcptr далее).
- putsprite/movesprite/gfx_blit/putimage(COPY)/getimage переведены на ядро.

Тесты: examples/balls (движок, дабл-буфер, boundary-тест клипа),
tests/sprites (RAM PASS, клип 4 края, атлас), tests/blitw (trig-leak),
tests/spriteclip (hardware-probe: железо НЕ режет за краем -> клип нужен),
tests/blitperf, tests/gfxbanks. size-baseline обновлён (53 программы).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 21:47:37 +03:00
parent 78161561e7
commit 72ce66275e
42 changed files with 2835 additions and 32 deletions
+107
View File
@@ -0,0 +1,107 @@
/*
* 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)
typedef struct {
const void *img; /* getimage-формат / атлас-лента (вне W3) */
int x, y; /* ЛОГИЧЕСКАЯ позиция левого-верхнего угла кадра */
int sx, sy; /* смещение кадра внутри img (атлас; 0 = целая) */
uint8_t w, h; /* размер кадра (кэш заголовка), 1..255 */
uint8_t flags; /* SPR_* | внутренние dirty */
/* --- внутреннее (владеет движок) --- */
struct {
int x, y; /* где нарисован на странице p */
uint8_t on; /* нарисован ли на странице p */
} drawn[2];
} sprite_t;
/* Инициализация: w,h из заголовка img, невидим, ничего не нарисовано.
* Для АТЛАСА (кадр — под-прямоугольник) после init выставить s->w/s->h
* в размер кадра и s->sx/s->sy — на нужный кадр (или sprite_frame). */
void sprite_init(sprite_t *s, const void *img);
/* Кадр всей сцены: на ТЕКУЩЕЙ draw-странице (gfx_get_draw_page)
* heal ВСЕХ старых позиций → блит ВСЕХ видимых (двухпроходно, одна
* скобка/банк на проход). Спрайты без изменений с прошлого визита этой
* страницы (и без SPR_ALWAYS) не трогаются. Для дабл-буфера звать после
* gfx_set_draw_page(hidden); для одиночной страницы/курсора — прямо на
* видимой. */
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);
/* --- 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 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; }
}
/* Показать (если был скрыт — грязнит обе страницы). */
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; }
#endif