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
+185
View File
@@ -0,0 +1,185 @@
/*
* balls — демо спрайтового движка v2 (<sprite.h>): N шаров 16×16 летают
* над чёрно-белой шахматкой, отражаясь от краёв. Esc — выход.
*
* РАНЬШЕ (до движка) приложение вело drawn[2][N] координаты вручную и
* само соблюдало дисциплину «heal ВСЕ → блит ВСЕ» на скрытой странице.
* Теперь это работа движка: sprite_move меняет только состояние,
* sprite_flip(sp, N) за кадр делает draw-page=скрытая → heal все старые →
* блит все видимые (одна W3-скобка + один банк на проход) → vsync → флип.
* Учёт drawn[2] спрятан в sprite_t. Профиль/дизайн:
* docs/sprite-api-design.md §9.1.
*
* BOUNDARY-ТЕСТ клипа: диапазон движения выходит ЗА края экрана на
* половину спрайта — шары штатно наполовину уходят за все 4 края,
* включая ОТРИЦАТЕЛЬНЫЕ x/y слева/сверху. Клип _gfx_blit_clip (общий
* для putsprite и движка) обязан авто-обрезать: правый/нижний — урезать
* w/h, левый/верхний — сдвинуть начало источника (sx/sy) и урезать.
*
* Двойная буферизация: рисуем на скрытой странице, флип по vsync (иначе
* heal+блит на видимой ловятся лучом — мерцание). Палитру страницы 1
* зеркалим из 0 (initgraph грузит EGA только в палитру 0).
*
* Скорость привязана к КАДРАМ (50 Гц): один проход цикла = один шаг.
*/
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include <conio.h>
#include <time.h>
#define FPS_METER 1 /* 1 = счётчик кадров/с слева-сверху (диагностика) */
#define NBALLS 16 /* количество шаров */
#define BW 16 /* сторона спрайта */
#define CELL 16 /* клетка шахматки */
/* Диапазон отражения — на пол-спрайта ЗА каждым краем (boundary-тест):
* x ∈ [-8 .. 312], y ∈ [-8 .. 248]. */
#define MINY (0) /* -8: половина за левым/верхним краем */
#define MINX (0) /* -8: половина за левым/верхним краем */
#define MAXX (320 - BW) /* 312: половина за правым краем */
#define MAXY (256 - BW) /* 248: половина за нижним краем */
/* Цвета шаров (не совпадают с фоном BLACK/WHITE) */
static const uint8_t ball_colors[] = {
YELLOW, LIGHTRED, LIGHTGREEN, LIGHTCYAN,
LIGHTMAGENTA, LIGHTBLUE, RED, GREEN
};
#define NCOLORS ((uint8_t)sizeof(ball_colors))
static sprite_t sp[NBALLS]; /* спрайты — движок ведёт drawn[2] */
static int8_t vx[NBALLS], vy[NBALLS]; /* направление ±1 (физика — наша) */
static uint8_t spr[NCOLORS][4 + BW * BW]; /* по спрайту на цвет */
static uint8_t egapal[16 * 4];
static void build_sprites(void)
{
uint8_t c;
int x, y;
for (c = 0; c < NCOLORS; c++) {
uint8_t *p = spr[c];
*p++ = BW; *p++ = 0; *p++ = BW; *p++ = 0;
for (y = 0; y < BW; y++)
for (x = 0; x < BW; x++) {
int dx = 2 * x - (BW - 1), dy = 2 * y - (BW - 1); /* ×2: центр между пикселями */
*p++ = (dx * dx + dy * dy <= 15 * 15)
? ball_colors[c] : GFX_TRANSPARENT;
}
}
}
static void draw_board(void)
{
int cx, cy;
for (cy = 0; cy < 256; cy += CELL)
for (cx = 0; cx < 320; cx += CELL) {
setfillstyle(SOLID_FILL,
(((cx ^ cy) >> 4) & 1) ? WHITE : BLACK);
bar(cx, cy, cx + CELL - 1, cy + CELL - 1);
}
}
static void init_balls(void)
{
uint8_t i;
for (i = 0; i < NBALLS; i++) {
sprite_init(&sp[i], spr[i % NCOLORS]);
sp[i].x = (20 + i * 37) % (MAXX - 1) + 1;
sp[i].y = (20 + i * 53) % (MAXY - 1) + 1;
sprite_show(&sp[i]);
vx[i] = (i & 1) ? 1 : -1;
vy[i] = (i & 2) ? 1 : -1;
}
}
#if FPS_METER
/* Кадров за последнюю RTC-секунду — белым в чёрной плашке слева-сверху
* на ТЕКУЩЕЙ draw-странице (рисуем ДО sprite_update, чтобы шары легли
* поверх и heal учитывал плашку через ОЗУ-копию). */
static void show_fps(uint8_t v)
{
char buf[4];
buf[0] = (char)('0' + (v / 100) % 10);
buf[1] = (char)('0' + (v / 10) % 10);
buf[2] = (char)('0' + v % 10);
buf[3] = 0;
setcolor(WHITE);
setfillstyle(SOLID_FILL, BLACK);
bar(0, 0, 27, 9);
outtextxy(2, 1, buf);
}
#endif
int main(void)
{
uint8_t i, page, hidden;
#if FPS_METER
datetime_t dt;
uint8_t frames = 0, last_sec;
#endif
build_sprites();
initgraph();
gfx_sprite_clip(0);
/* Палитра страницы 1 = палитре страницы 0. */
gfx_pal_get(0, 0, 16, egapal);
gfx_pal_load(1, 0, 16, egapal);
init_balls();
/* Обе страницы: шахматка (в ОЗУ-копию — бэкап для heal) + шары в
* стартовых позициях (sprite_update рисует на текущей draw-странице
* и снимает её dirty-бит). */
for (page = 0; page < 2; page++) {
gfx_set_draw_page(page);
draw_board();
sprite_update(sp, NBALLS);
}
gfx_set_visible_page(0);
#if FPS_METER
getdatetime(&dt);
last_sec = dt.second;
#endif
for (;;) {
if (kbhit() && getch() == 27)
break;
/* Физика: обновить позиции (только состояние спрайтов). */
for (i = 0; i < NBALLS; i++) {
int nx = sp[i].x + vx[i];
int ny = sp[i].y + vy[i];
if (nx <= MINX || nx >= MAXX) vx[i] = -vx[i];
if (ny <= MINY || ny >= MAXY) vy[i] = -vy[i];
sprite_move(&sp[i], nx, ny);
}
/* Кадр: рисуем на скрытой странице. sprite_flip развёрнут, чтобы
* плашку FPS нарисовать на скрытой странице ДО sprite_update. */
hidden = gfx_get_visible_page() ^ 1;
gfx_set_draw_page(hidden);
#if FPS_METER
frames++;
getdatetime(&dt);
if (dt.second != last_sec) {
last_sec = dt.second;
show_fps(frames);
frames = 0;
}
#endif
sprite_update(sp, NBALLS);
gfx_wait_vsync();
gfx_set_visible_page(hidden); /* флип в vblank — без мерцания */
}
closegraph();
return 0;
}