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
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := sprites
EXTRA_FLAGS ?= --gfx 256 --memory small
include $(PROJ_ROOT)/app.mk
+189
View File
@@ -0,0 +1,189 @@
/*
* sprites — тест Фазы B спрайтового API (docs/sprite-api-design.md §3):
* putsprite / movesprite / gfx_heal / gfx_blit_part поверх accel-ядра.
*
* Сцена: полосатый фон (вертикальные полосы 8px RED/LIGHTGRAY, банк
* 0x50 → фон и в VRAM, и в ОЗУ-копии). Спрайт — шар 24×24 (YELLOW с
* RED-ободом), углы = 0xFF (GFX_TRANSPARENT).
*
* Стадии (паузы по RTC, скриншот после каждой):
* S1 фон + разметка.
* S2 putsprite:
* - в центре зоны (C) — прозрачные углы: полосы сквозь них;
* - 4 клип-позиции: слева x=-12, справа x=308, сверху y=-10,
* снизу y=244 — рисуется только видимая часть, соседняя
* память не портится;
* - heal-проба: putsprite в H=(60,130) и тут же gfx_heal того же
* rect — шара БЫТЬ НЕ ДОЛЖНО (первая проверка heal src==dst
* через акселератор — риск §10.4 дизайна);
* - атлас: gfx_blit_part кадра 1 (MAGENTA-шар) из ленты 2×24×24
* в (270,130) банком GFX_BANK_SPRITE.
* Проверка A (по ОЗУ, getpixel = ОЗУ-копия): полосы под C целы.
* S3 movesprite: шар из (16,60) 12 шагов по (+16,+8) до (208,156) —
* след обязан быть чистым (heal старой позиции на каждом шаге).
* Проверка B: ОЗУ вдоль пути — полосы.
*
* VRAM программно не читается (write-only) — корректность экрана
* подтверждают скриншоты; программные проверки ловят порчу ОЗУ-копии.
*
* Запуск:
* python3 toolchain/mame_interactive.py tests/sprites/sprites.exe \
* --snap 13,17,21 --timeout 23
*/
#include <graphics.h>
#include <gfx.h>
#include <time.h>
#define BW 24 /* сторона спрайта */
#define BG_Y0 40
#define BG_Y1 199
#define CX 150 /* центральный putsprite */
#define CY 100
static uint8_t ball[4 + BW * BW]; /* YELLOW шар */
static uint8_t strip[4 + 2 * BW * BW]; /* лента: кадр0 YELLOW, кадр1 MAGENTA */
static uint8_t stripe_color(int x)
{
return ((x >> 3) & 1) ? LIGHTGRAY : RED;
}
/* Шар: r<=9 — fill, r 9..11 — ring, дальше прозрачно (0xFF). */
static uint8_t ball_pixel(int x, int y, uint8_t fill, uint8_t ring)
{
int dx = x - BW / 2, dy = y - BW / 2;
int d2 = dx * dx + dy * dy;
if (((x == 0 || x == BW - 1) && ((y >=0 && y <= 3) || (y >=BW - 4 && y <= BW - 1))) ||
((y == 0 || y == BW - 1) && ((x >=0 && x <= 3) || (x >=BW - 4 && x <= BW - 1)))) return GREEN;
if (x==0) return GFX_TRANSPARENT;
if (d2 <= 81) return fill;
if (d2 <= 121) return ring;
return GFX_TRANSPARENT;
}
static void build_images(void)
{
uint8_t *p;
int x, y, f;
p = ball;
*p++ = BW; *p++ = 0; *p++ = BW; *p++ = 0;
for (y = 0; y < BW; y++)
for (x = 0; x < BW; x++)
*p++ = ball_pixel(x, y, YELLOW, RED);
/* Лента 48×24: кадры лежат рядом по x (кадр N: sx = N*BW). */
p = strip;
*p++ = 2 * BW; *p++ = 0; *p++ = BW; *p++ = 0;
for (y = 0; y < BW; y++)
for (f = 0; f < 2; f++)
for (x = 0; x < BW; x++)
*p++ = ball_pixel(x, y, f ? LIGHTMAGENTA : YELLOW,
f ? BLUE : RED);
}
/* ОЗУ-копия (getpixel) в трёх точках зоны 24×24 — нетронутые полосы? */
static uint8_t ram_is_stripes(int zx, int zy)
{
static const uint8_t off[3][2] = { {12, 12}, {8, 16}, {16, 6} };
uint8_t i;
for (i = 0; i < 3; i++) {
int x = zx + off[i][0];
if (getpixel(x, zy + off[i][1]) != stripe_color(x))
return 0;
}
return 1;
}
static void pause_sec(uint8_t n)
{
datetime_t dt;
uint8_t last;
getdatetime(&dt);
last = dt.second;
while (n) {
getdatetime(&dt);
if (dt.second != last) {
last = dt.second;
n--;
}
}
}
static void say(int y, const char *s)
{
outtextxy(4, y, s);
}
int main(void)
{
uint8_t chkA, chkB;
int x, i;
build_images();
initgraph();
cleardevice();
gfx_sprite_clip(1);
setcolor(WHITE);
/* ---- S1: фон -------------------------------------------------- */
say(4, "SPRITES: putsprite/movesprite/heal");
for (x = 0; x < 320; x += 8) {
setfillstyle(SOLID_FILL, stripe_color(x));
bar(x, BG_Y0, x + 7, BG_Y1);
}
say(208, "S1 bg");
(void)getchar();
/* ---- S2: putsprite + клипы + heal-проба + атлас ---------------- */
putsprite(CX, CY, ball); /* центр: прозрачные углы */
(void)getchar();
putsprite(-12, 52, ball); /* клип слева */
(void)getchar();
putsprite(308, 82, ball); /* клип справа */
(void)getchar();
putsprite(297, 162, ball); /* клип справа */
(void)getchar();
putsprite(60, -10, ball); /* клип сверху */
(void)getchar();
putsprite(290, 244, ball); /* клип снизу */
(void)getchar();
putsprite(60, 130, ball); /* heal-проба: нарисовать... */
(void)getchar();
gfx_heal(60, 130, BW, BW); /* ...и стереть (шара НЕТ) */
(void)getchar();
gfx_set_bank(GFX_BANK_SPRITE); /* атлас: кадр 1 (MAGENTA) */
(void)getchar();
gfx_blit_part(270, 130, strip, BW, 0, BW, BW);
(void)getchar();
gfx_set_bank(GFX_BANK_NORMAL);
(void)getchar();
chkA = ram_is_stripes(CX, CY);
say(218, "S2 putsprite+clip, heal@60,130");
say(228, chkA ? "A RAM under sprite: PASS"
: "A RAM under sprite: FAIL");
pause_sec(4);
/* ---- S3: movesprite — след обязан быть чистым ------------------ */
x = 16;
putsprite(x, 60, ball);
(void)getchar();
for (i = 0; i < 12; i++) {
movesprite(x, 60 + (x - 16) / 2, x + 16, 60 + (x - 16 + 16) / 2,
ball);
x += 16;
(void)getchar();
}
chkB = ram_is_stripes(16, 60) && ram_is_stripes(112, 108);
say(238, chkB ? "B RAM along path: PASS"
: "B RAM along path: FAIL");
say(248, "S3 moved 16,60 -> 208,156 done");
(void)getchar();
}