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
+259
View File
@@ -0,0 +1,259 @@
/*
* gfxbanks — Фаза 0 спрайтового дизайна: верификация подрежимов
* видеостраниц #50..#5F (docs/sprite-api-design.md, «Фаза 0»).
*
* Семантика по доку (Иван Мак §4.3):
* bit3 (0x58/0x5C) — запись байта 0xFF НЕ производится (прозрачность);
* bit2 (0x54/0x5C) — запись только в видео-ОЗУ, ОЗУ-копия не трогается;
* чтение из #50..#5F всегда возвращает ОЗУ-копию (getpixel = ОЗУ).
*
* ВНИМАНИЕ: вердикт MAME НЕ финален — тест прогоняется и в MAME, и на
* железе. Известное расхождение: в MAME 0.283 (наш v306) FF-скип bit3
* гасит ТОЛЬКО запись в VRAM, а теневое ОЗУ при bit2=0 всё равно
* получает FF (ram_w: `if (BIT(~page,2)) ram[...]=data;
* if (!(BIT(page,3) && data==0xff)) vram_w(...)`); в master-драйвере
* уже полное подавление (`return`) — по доку. Через 0x5C расхождение
* не проявляется (bit2 гасит ОЗУ-ветку) — поэтому putsprite планово
* работает через 0x5C. На банке 0x58 зона SKIP это расхождение
* ловит: chkC = FAIL на 0.283, PASS по доку/master.
*
* Сценарий (4 стадии, скриншот после каждой; палитра[255] = синий;
* паузы — по RTC getdatetime, т.к. delayms в --memory small неработосп.
* (irq_install → EINVAL), а скорость CPU в паузах не участвует):
* S1 фон: вертикальные полосы RED/LIGHTGRAY через 0x50 (VRAM+ОЗУ).
* S2 «спрайты» (после Фазы A putimage(COPY) идёт через акселератор,
* поэтому CPU-путь в ZL — честный цикл putpixel):
* ZL (CPU-путь): банк 0x5C, putpixel-ями круг GREEN с полями
* 0xFF — ожидание: круг поверх полос, поля прозрачны;
* ZR (accel-путь): банк 0x5C, bar 0xFF 64×64 (accel-fill:
* ожидание no-op) + putimage того же круга (accel-COPY:
* прозрачность полей на burst-пути);
* ZB (bit3-skip): банк 0x58, putimage 0xFF (accel-copy, верх) +
* bar 0xFF (accel-fill, низ) — ожидание по доку: no-op
* экрана и ОЗУ.
* Проверки по ОЗУ (getpixel): A = ОЗУ под ZL/ZR не тронуто (bit2);
* C = ОЗУ под ZB не тронуто (полное подавление FF-записи).
* Hex-пробы ОЗУ печатаются рядом (ожидание — цвета полос 04/07).
* S3 КЛЮЧЕВОЙ ШАГ — 0xFF реально в VRAM: банк 0x54 (скипа нет),
* квадрат 0xFF 32×32 поверх круга (CPU putpixel-ями в ZL,
* accel-copy putimage'ем в ZR).
* Экран: СИНИЙ квадрат → FF отображается как цвет 255
* (семантика дока полная, стирание = heal);
* ПОЛОСЫ → видеокарта подставляет ОЗУ-копию
* (дешёвое стирание FF-заливкой работает);
* круг/зелёный → 0x54 не эмулируется.
* Проверка B = ОЗУ всё ещё полосы (bit2 у 0x54).
* S4 heal: чтение+запись через 0x50 ЛЕВЫХ половин всех зон —
* левые половины возвращаются к фону ИЗ ОЗУ-КОПИИ, правые
* сохраняют артефакты S2/S3. NB: если ОЗУ под зоной испорчено
* (chkC FAIL на 0.283), heal честно перенесёт порчу в VRAM —
* зона SKIP посинеет на этой стадии.
*
* Запуск:
* python3 toolchain/mame_interactive.py tests/gfxbanks/gfxbanks.exe \
* --snap 13,17,21,25 --timeout 27
*/
#include <graphics.h>
#include <gfx.h>
#include <time.h>
#define ZY 56 /* верх зон */
#define ZH 64 /* зоны 64×64 */
#define ZLX 16 /* 0x5C, CPU-путь */
#define ZRX 120 /* 0x5C, accel-путь */
#define ZBX 224 /* 0x58, bit3-skip */
#define STRIPE_Y0 48
#define STRIPE_Y1 135
/* Точки-пробы внутри области, куда S2 писал НЕ-FF байты (круг r=28 и
* квадрат +16..+47 содержат все три), — общие для проверок и hex-дампа. */
static const uint8_t probe[3][2] = { {32, 32}, {20, 28}, {40, 36} };
/* Полосы шириной 8: цвет по абсолютному x */
static uint8_t stripe_color(int x)
{
return ((x >> 3) & 1) ? LIGHTGRAY : RED;
}
/* Спрайт 64×64: круг GREEN r=28, вокруг — прозрачный 0xFF */
static uint8_t spr[4 + 64 * 64];
/* Квадрат 32×32 сплошного 0xFF */
static uint8_t ffsq[4 + 32 * 32];
static void build_images(void)
{
uint8_t *p = spr;
int x, y;
*p++ = 64; *p++ = 0; *p++ = 64; *p++ = 0;
for (y = 0; y < 64; y++)
for (x = 0; x < 64; x++) {
int dx = x - 32, dy = y - 32;
*p++ = (dx * dx + dy * dy <= 28 * 28) ? GREEN : 0xFF;
}
p = ffsq;
*p++ = 32; *p++ = 0; *p++ = 32; *p++ = 0;
for (x = 0; x < 32 * 32; x++)
*p++ = 0xFF;
}
/* ОЗУ-копия зоны — нетронутые полосы? */
static uint8_t ram_is_stripes(int zx)
{
uint8_t i;
for (i = 0; i < 3; i++) {
int x = zx + probe[i][0];
if (getpixel(x, ZY + probe[i][1]) != stripe_color(x))
return 0;
}
return 1;
}
static char hexd(uint8_t v)
{
v &= 15;
return v < 10 ? '0' + v : 'A' + v - 10;
}
/* Hex-дамп трёх ОЗУ-проб зоны: "ZX 04 04 07" в колонке x=216 */
static void show_probes(const char *tag, int zx, int ty)
{
char buf[12];
uint8_t i, v;
buf[0] = tag[0];
buf[1] = tag[1];
for (i = 0; i < 3; i++) {
v = (uint8_t)getpixel(zx + probe[i][0], ZY + probe[i][1]);
buf[2 + i * 3] = ' ';
buf[3 + i * 3] = hexd(v >> 4);
buf[4 + i * 3] = hexd(v);
}
buf[11] = 0;
outtextxy(216, ty, buf);
}
/* Пауза n секунд по RTC (гранулярность 1 с, не зависит от CPU/режима) */
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--;
}
}
}
/* CPU-путь: вывести rect w×h из img (getimage-формат) в (x0,y0)
* по-пиксельно через putpixel (LD (HL),A — НЕ акселератор). */
static void blit_cpu(int x0, int y0, const void *img, int w, int h)
{
const uint8_t *p = (const uint8_t *)img + 4;
int x, y;
for (y = 0; y < h; y++)
for (x = 0; x < w; x++)
putpixel(x0 + x, y0 + y, *p++);
}
/* CPU-путь: залить rect цветом по-пиксельно. */
static void fill_cpu(int x0, int y0, int w, int h, uint8_t color)
{
int x, y;
for (y = 0; y < h; y++)
for (x = 0; x < w; x++)
putpixel(x0 + x, y0 + y, color);
}
/* heal: прочитать (вернёт ОЗУ-копию) и записать (уйдёт в VRAM+ОЗУ)
* каждый пиксель прямоугольника банком 0x50. Прообраз gfx_heal. */
static void heal(int x0, int y0, int w, int h)
{
int x, y;
for (y = y0; y < y0 + h; y++)
for (x = x0; x < x0 + w; x++)
putpixel(x, y, getpixel(x, y));
}
static void say(int y, const char *s)
{
outtextxy(4, y, s);
}
int main(void)
{
uint8_t chkA, chkB, chkC;
int x;
build_images();
initgraph();
gfx_pal_set(0, 255, 0, 0, 255); /* цвет 255 = ярко-синий */
cleardevice();
setcolor(WHITE);
/* ---- S1: фон ------------------------------------------------ */
say(4, "GFXBANKS: page submodes 50/54/58/5C");
for (x = 0; x < 320; x += 8) {
setfillstyle(SOLID_FILL, stripe_color(x));
bar(x, STRIPE_Y0, x + 7, STRIPE_Y1);
}
outtextxy(ZLX, 44, "5C CPU");
outtextxy(ZRX, 44, "5C ACC");
outtextxy(ZBX, 44, "58 SKIP");
say(148, "S1 stripes via 50");
pause_sec(4);
/* ---- S2: спрайты через 0x5C / no-op через 0x58 --------------- */
gfx_set_bank(0x5C);
blit_cpu(ZLX, ZY, spr, 64, 64); /* CPU: круг + FF-поля */
setfillstyle(SOLID_FILL, 255);
bar(ZRX, ZY, ZRX + 63, ZY + 63); /* accel-fill FF — ждём no-op */
putimage(ZRX, ZY, spr, COPY_PUT); /* accel-COPY: круг + FF-поля */
gfx_set_bank(0x58);
putimage(ZBX, ZY, ffsq, COPY_PUT); /* accel-copy FF, верх зоны */
setfillstyle(SOLID_FILL, 255);
bar(ZBX, ZY + 32, ZBX + 31, ZY + 63); /* accel-fill FF, низ зоны */
gfx_set_bank(0x50);
chkA = ram_is_stripes(ZLX) && ram_is_stripes(ZRX);
chkC = ram_is_stripes(ZBX);
say(158, "S2 sprites 5C, ff-skip 58");
say(168, chkA ? "A bit2 RAM intact: PASS" : "A bit2 RAM intact: FAIL");
show_probes("ZL", ZLX, 168);
say(178, chkC ? "C bit3 skip (RAM): PASS" : "C bit3 skip (RAM): FAIL");
show_probes("ZB", ZBX, 178);
pause_sec(4);
/* ---- S3: 0xFF реально в VRAM (банк 0x54, скипа нет) ---------- */
gfx_set_bank(0x54);
fill_cpu(ZLX + 16, ZY + 16, 32, 32, 0xFF); /* CPU-путь */
putimage(ZRX + 16, ZY + 16, ffsq, COPY_PUT); /* accel-copy путь */
gfx_set_bank(0x50);
chkB = ram_is_stripes(ZLX) && ram_is_stripes(ZRX);
say(188, "S3 KEY: ff into vram via 54");
say(198, chkB ? "B 54 RAM intact: PASS" : "B 54 RAM intact: FAIL");
show_probes("ZR", ZRX, 198);
say(208, "S3 look: BLUE=doc STRIPES=subst");
pause_sec(4);
/* ---- S4: heal левых половин зон ------------------------------ */
heal(ZLX, ZY, 32, ZH);
heal(ZRX, ZY, 32, ZH);
heal(ZBX, ZY, 32, ZH);
say(218, "S4 healed left halves, done");
for (;;) { }
}