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 := blitperf
EXTRA_FLAGS ?= --gfx 256 --memory small
include $(PROJ_ROOT)/app.mk
+131
View File
@@ -0,0 +1,131 @@
/*
* blitperf — микробенчмарк спрайтового пути: операций в секунду (RTC)
* для каждого слоя цепочки putsprite/gfx_heal. Диагностика разрыва
* с asm-референсом (examples/balls ~1.5 мс/шар при теории ~0.3 мс):
* отделяет стоимость leaf'ов от C-обвязки (клип/банк/заголовок) и
* даёт опорные точки (bar — давно оптимизированный accel-путь).
*
* Методика: измеряется число вызовов за одну RTC-секунду (старт по
* смене секунды; время проверяется раз в 8 операций). Строка «empty»
* — цена самого цикла (вызов пустышки + getdatetime/8) — вычитать из
* остальных: t_op = 1/n_op - 1/n_empty.
*
* ВНИМАНИЕ: использует ВНУТРЕННИЕ символы libbgi (прототипы
* продублированы ниже — сверять с libbgi/_bgi.h при изменениях ABI).
*
* Запуск:
* python3 toolchain/mame_interactive.py tests/blitperf/blitperf.exe \
* --snap 22 --timeout 24
*/
#include <graphics.h>
#include <gfx.h>
#include <time.h>
/* --- внутренности libbgi (копия из _bgi.h, НЕ публичный API) ------- */
extern uint16_t _gfx_addr_base;
void _bgi_begin(void);
void _bgi_end(void);
void _bgi_heal_rows_raw(uint8_t *scr, int y0, uint8_t w, uint8_t h);
void _bgi_copy_rows_raw(const uint8_t *src, uint8_t *dst,
uint8_t w, uint8_t h,
int dstride, int sstride, uint8_t y0);
/* ------------------------------------------------------------------- */
#define BW 16
static uint8_t spr[4 + BW * BW];
static void build_sprite(void)
{
uint8_t *p = spr;
int i;
*p++ = BW; *p++ = 0; *p++ = BW; *p++ = 0;
for (i = 0; i < BW * BW; i++)
*p++ = (uint8_t)(LIGHTRED + ((i & 7) == 0)); /* без 0xFF */
}
/* --- операции ------------------------------------------------------ */
static void op_empty(void) { }
static void op_leaf_heal(void) /* leaf + W3-скобка, без C-обвязки */
{
_bgi_begin();
_bgi_heal_rows_raw((uint8_t *)(_gfx_addr_base + 100), 100, BW, BW);
_bgi_end();
}
static void op_leaf_blit(void) /* copy-leaf + скобка, без обвязки */
{
_bgi_begin();
_bgi_copy_rows_raw(spr + 4, (uint8_t *)(_gfx_addr_base + 100),
BW, BW, 0, BW, 100);
_bgi_end();
}
static void op_gfx_heal(void) { gfx_heal(100, 100, BW, BW); }
static void op_blit_part(void) { gfx_blit_part(100, 100, spr, 0, 0, BW, BW); }
static void op_putsprite(void) { putsprite(100, 100, spr); }
static void op_bar(void) /* опорная точка: accel-fill 16×16 */
{
bar(100, 100, 100 + BW - 1, 100 + BW - 1);
}
/* --- измеритель ---------------------------------------------------- */
static uint16_t bench(void (*op)(void))
{
datetime_t dt;
uint8_t s0, k;
uint16_t n = 0;
getdatetime(&dt);
s0 = dt.second;
do { getdatetime(&dt); } while (dt.second == s0); /* синхр. к границе */
s0 = dt.second;
for (;;) {
for (k = 0; k < 8; k++)
op();
n += 8;
getdatetime(&dt);
if (dt.second != s0)
return n;
}
}
static void say_num(int y, const char *tag, uint16_t v)
{
char buf[16];
uint8_t i;
uint16_t d = 10000;
for (i = 0; tag[i] && i < 9; i++)
buf[i] = tag[i];
while (i < 10) buf[i++] = ' ';
for (; d; d /= 10)
buf[i++] = (char)('0' + (v / d) % 10);
buf[i] = 0;
setcolor(WHITE);
outtextxy(4, y, buf);
}
int main(void)
{
build_sprite();
initgraph();
cleardevice();
setcolor(WHITE);
outtextxy(4, 4, "BLITPERF ops/sec (16x16)");
say_num(20, "empty", bench(op_empty));
say_num(30, "leafheal", bench(op_leaf_heal));
say_num(40, "leafblit", bench(op_leaf_blit));
say_num(50, "gfx_heal", bench(op_gfx_heal));
say_num(60, "blitpart", bench(op_blit_part));
say_num(70, "putsprite", bench(op_putsprite));
say_num(80, "bar", bench(op_bar));
outtextxy(4, 96, "done");
for (;;) { }
}
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := blitw
EXTRA_FLAGS ?= --gfx 256 --memory small
include $(PROJ_ROOT)/app.mk
+136
View File
@@ -0,0 +1,136 @@
/*
* blitw — программный регресс ядра блиттинга gfx_blit_part по ширинам.
*
* Репро артефакта tests/sprites (клипнутые шары w=12 рисуются со
* сдвигом): блитим паттерн-образ 24×24 разными ширинами банком 0x50
* (запись идёт и в ОЗУ-копию → getpixel читает ровно то, что записано)
* и побайтно сверяем прямоугольник + рамку 1px вокруг (должна остаться
* фоном). Вердикт по каждой ширине печатается на экране:
* w=NN PASS | w=NN FAIL @x,y eXX gYY (exp/got первого расхождения)
*
* Запуск:
* python3 toolchain/mame_interactive.py tests/blitw/blitw.exe \
* --snap 14 --timeout 16
*/
#include <graphics.h>
#include <gfx.h>
#define IW 24
#define IH 24
#define TX 200 /* позиция тестового прямоугольника (правее текста) */
#define TY 30
static uint8_t img[4 + IW * IH];
static char msg[40];
static uint8_t pat(int x, int y)
{
return (uint8_t)(((x + 1) * (y + 3)) & 0x7F); /* != 0xFF, != 0 кроме редких */
}
static void build_img(void)
{
uint8_t *p = img;
int x, y;
*p++ = IW; *p++ = 0; *p++ = IH; *p++ = 0;
for (y = 0; y < IH; y++)
for (x = 0; x < IW; x++)
*p++ = pat(x, y);
}
static char hexd(uint8_t v)
{
v &= 15;
return v < 10 ? '0' + v : 'A' + v - 10;
}
/* Проверить блит шириной w в (tx,TY): весь rect = pat, рамка = 0. */
static void check_w(int w, int tx, int ty, int row)
{
int x, y;
uint8_t exp, got;
uint8_t fail = 0;
/* фон под областью + рамкой */
setfillstyle(SOLID_FILL, 0);
bar(tx - 2, ty - 2, tx + w + 1, ty + IH + 1);
gfx_blit_part(tx, ty, img, 0, 0, w, IH);
for (y = -1; y <= IH && !fail; y++) {
for (x = -1; x <= w && !fail; x++) {
exp = (y >= 0 && y < IH && x >= 0 && x < w) ? pat(x, y) : 0;
got = (uint8_t)getpixel(tx + x, ty + y);
if (got != exp) {
fail = 1;
msg[0] = 'w'; msg[1] = '0' + w / 10; msg[2] = '0' + w % 10;
msg[3] = ' '; msg[4] = 'F'; msg[5] = 'A'; msg[6] = 'I';
msg[7] = 'L'; msg[8] = ' ';
msg[9] = 'x'; msg[10] = (char)('0' + (x + 1) / 10);
msg[11] = (char)('0' + (x + 1) % 10);
msg[12] = 'y'; msg[13] = (char)('0' + (y + 1) / 10);
msg[14] = (char)('0' + (y + 1) % 10);
msg[15] = ' '; msg[16] = 'e'; msg[17] = hexd(exp >> 4);
msg[18] = hexd(exp);
msg[19] = ' '; msg[20] = 'g'; msg[21] = hexd(got >> 4);
msg[22] = hexd(got);
msg[23] = 0;
}
}
}
if (!fail) {
msg[0] = 'w'; msg[1] = '0' + w / 10; msg[2] = '0' + w % 10;
msg[3] = ' '; msg[4] = 'P'; msg[5] = 'A'; msg[6] = 'S';
msg[7] = 'S'; msg[8] = 0;
}
outtextxy(4, 40 + row * 10, msg);
}
int main(void)
{
static const uint8_t widths[] = { 24, 16, 12, 8, 4, 2, 1 };
uint8_t i;
build_img();
initgraph();
cleardevice();
setcolor(WHITE);
outtextxy(4, 4, "BLITW: gfx_blit_part width sweep");
for (i = 0; i < sizeof(widths); i++)
check_w(widths[i], TX, TY, i);
/* и правый край, как в sprites: dst у конца строки page 0 */
check_w(12, 308 - 12, TY, 7); /* dst близко к краю, но без клипа */
/* Регресс утечки CPU-байта write-триггера (см. шапку
* _bgi_copy_rows_raw). Программно (getpixel = ОЗУ) утечка на MAME
* 0.283 НЕ ловится: ОЗУ-ветка 0x58 пишет все байты, и burst
* перезаписывает утечку в ОЗУ; жертвой остаётся только VRAM
* (FF-скип). Поэтому проверка ВИЗУАЛЬНАЯ: блит образа с col0=0xFF
* и последним столбцом YELLOW через 0x5C поверх ЗЕЛЁНОЙ плашки.
* col0 (x=TX..) обязан остаться ЗЕЛЁНЫМ; до фикса он был YELLOW
* (CPU-байт = последний байт burst-чтения). */
{
static uint8_t timg[4 + 16 * 8];
uint8_t *p = timg;
int x, y;
*p++ = 16; *p++ = 0; *p++ = 8; *p++ = 0;
for (y = 0; y < 8; y++)
for (x = 0; x < 16; x++)
*p++ = (x == 0) ? 0xFF : (x == 15 ? YELLOW : LIGHTBLUE);
setfillstyle(SOLID_FILL, GREEN);
bar(TX - 4, 150 - 4, TX + 19, 150 + 11);
gfx_set_bank(GFX_BANK_SPRITE);
gfx_blit(TX, 150, timg);
gfx_set_bank(GFX_BANK_NORMAL);
outtextxy(4, 40 + 8 * 10 + 4, "trig leak: col0 @200,150 green?");
}
outtextxy(4, 40 + 9 * 10 + 6, "done");
for (;;) { }
}
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := gfxbanks
EXTRA_FLAGS ?= --gfx 256 --memory small
include $(PROJ_ROOT)/app.mk
+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 (;;) { }
}
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := spriteclip
EXTRA_FLAGS ?= --gfx 256 --memory small
include $(PROJ_ROOT)/app.mk
+97
View File
@@ -0,0 +1,97 @@
/*
* spriteclip — ЭКСПЕРИМЕНТ (гипотеза пользователя 2026-07-12): режет ли
* ЖЕЛЕЗО спрайт, выведенный за ПРАВЫЙ/НИЖНИЙ край экрана, БЕЗ
* программного клипа? Или запись за x>=320 / y>=256 портит соседнюю
* память (следующую строку / страницу 1 / верх экрана)?
*
* Метод: блит 16×16 НЕПРОЗРАЧНОГО белого спрайта мимо клипа — прямым
* вызовом leaf'а _bgi_copy_rows_raw (в обход _gfx_blit_clip). Фон —
* синий; зелёные линии-маркеры на x=319 и y=255 (граница экрана);
* левый край (0..7) и верх (0..7) залиты красным-маркером, чтобы
* поймать «заворот» переполнения именно туда.
*
* Читать скриншот:
* - ПРАВЫЙ (спрайт в x=312): если видно только 8 левых колонок белого
* (x=312..319), а правых 8 нет И красная левая полоса цела →
* железо режет по правому краю. Если правые 8 колонок «завернулись»
* белым в x=0..7 (поверх красного) или появился мусор — НЕ режет,
* клип обязателен.
* - НИЖНИЙ (спрайт в y=250): если видно 6 строк белого (y=250..255),
* а строк за 256 нет И красная верхняя полоса цела → железо режет по
* нижнему. Если белое всплыло сверху (y=0..) — Port_Y завернулся.
* - УГОЛ (x=312,y=250): комбинация.
*
* НЕ для CI-регресса — разовый hardware-probe (и в MAME, и на железе).
*/
#include <graphics.h>
#include <gfx.h>
#include <conio.h>
#include <stdint.h>
/* Внутренние символы libbgi (глобальны в bgi256.lib) — тянем напрямую
* ради no-clip блита. Обычному коду так делать НЕ надо (есть gfx_blit). */
extern void _bgi_begin(void);
extern void _bgi_end(void);
extern void _bgi_copy_rows_raw(const uint8_t *src, uint8_t *dst,
uint8_t w, uint8_t h,
int dstride, int sstride, uint8_t y0);
extern uint16_t _gfx_addr_base;
#define BW 16
static uint8_t sprite[4 + BW * BW];
static void build_sprite(void)
{
uint8_t *p = sprite;
int i;
*p++ = BW; *p++ = 0; *p++ = BW; *p++ = 0;
for (i = 0; i < BW * BW; i++)
*p++ = WHITE; /* непрозрачный: каждый байт пишется */
}
/* Блит БЕЗ клипа: адрес = base + x, Port_Y стартует с y, строки шагают
* Port_Y'ем (dstride 0). x/y могут выходить за экран — в этом и суть. */
static void blit_noclip(int x, int y)
{
uint8_t *dst = (uint8_t *)(_gfx_addr_base + x);
gfx_set_bank(GFX_BANK_NORMAL);
_bgi_begin();
_bgi_copy_rows_raw(sprite + 4, dst, BW, BW, 0, BW, (uint8_t)y);
_bgi_end();
}
int main(void)
{
build_sprite();
initgraph();
/* Фон синий. */
setfillstyle(SOLID_FILL, BLUE);
bar(0, 0, 319, 255);
/* Красные маркер-полосы: левые 8 колонок и верхние 8 строк —
* сюда «завернётся» переполнение вправо/вниз, если железо не режет. */
setfillstyle(SOLID_FILL, RED);
bar(0, 0, 7, 255);
bar(0, 0, 319, 7);
/* Зелёные маркеры границы экрана. */
setcolor(GREEN);
line(319, 0, 319, 255);
line(0, 255, 319, 255);
/* Sanity: клипнутый putsprite тем же спрайтом в безопасном месте. */
putsprite(140, 120, sprite);
/* Эксперимент — три no-clip блита за края: */
blit_noclip(312, 100); /* правый: x+16=328, 8 колонок за 320 */
blit_noclip(100, 250); /* нижний: y+16=266, 10 строк за 256 */
blit_noclip(312, 250); /* угол: и вправо, и вниз */
getch();
closegraph();
return 0;
}
+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();
}