Files
Sprinter-SDCC/tests/spranim/spranim.c
T
snark13 b9ddce8d34 libbgi: спрайты — кэш адреса кадра, DDA+asm тик, Y-сортировка со слоями
Оптимизации A+B (профиль rpgwalk-15: активная часть кадра 410К → 307К
тактов из 430080; лимит спрайтов 16×16 на стабильные 48.8 fps: 14 → ~21):

- (B) sprite_t.src/stride — готовый адрес кадра: считают только
  sprite_frame (теперь функция, одно умножение на СМЕНУ кадра) и тикер
  (±an_step БАЙТ инкрементально); блит-ядра принимают src+stride,
  img/sx/sy из сигнатуры ушли.  Блит 177К → 146К на кадр.
- (A) тик 100К → 37.7К: tween переформулирован Брезенхэм → беззнаковый
  DDA (mv_rem/mv_acc, «приехали» = rem==0 — без знаковых 16-бит
  сравнений), tick_move и tick_anim — ручной asm (SDCC спиллит такие
  функции в IX-фрейм ~100 обращений; C-реструктуризации не помогали —
  проверено кодогеном).  Биты an_flags переименованы по категориям
  (_SPR_STRIP_HORZ, _SPR_PP_BACK).

Y-сортировка (gfx_sprite_ysort, идеи пользователя — 8-бит ключ,
персистентность):

- painter's algorithm по ключу {layer:8, clamp_y:8}; поле
  sprite_t.layer (в КОНЦЕ структуры — asm-офсеты не сдвигает): слои
  сцены в одном массиве/одном sprite_update;
- ПЕРСИСТЕНТНАЯ asm-таблица {key16, ptr16}: resort порядка прошлого
  кадра (почти линейно), rebuild при смене arr/count; массив
  приложения не трогается; ~28К/15 спрайтов (с нуля было 44К);
- компоненты YSORT_Y/YSORT_LAYER отключаемы независимо масками ключа
  (без ветвлений в сортировщике); ВНИМАНИЕ: mode=1 значит Y-only,
  полный порядок = YSORT_Y|YSORT_LAYER;
- funcptr-DCE: выключено = код и таблица не линкуются (rpgwalk −190 Б);
- ПРАВИЛО в sprite.h: два sprite_update на страницу запрещены (heal
  второй группы стирает спрайты первой — ОЗУ-копия чистая).

Попутные фиксы:

- libbgi/Makefile: .rel зависят от заголовков (HDRS) — stale .rel со
  старой раскладкой sprite_t молча ломал рантайм;
- rpgprof: --memory small (перерос tiny: BSS вылезал за W2 → мгновенный
  «Unexpected application termination»; mkexe это пока не ловит);
- tests/spranim: проверки переведены на кэш src, добавлены T6 (reframe
  после тикера) и T7 (Y-сортировка: порядок, слои, персистентный
  resort, LAYER-only) — 7/7 PASS в MAME.

Доки: §9д — новый бюджет (19.5К/спрайт), §9е — ПЛАН FPS-делителя
(frame pacing, gfx_set_fps_div); TODO — дизайн цепочки irq-обработчиков.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 17:19:24 +03:00

209 lines
10 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* spranim — тест авто-анимаций спрайтового движка (§9г):
* sprite_anim (LOOP/PINGPONG/ONCE) + sprite_moveto + статусы + стопы.
*
* Логика проверяется ПРОГРАММНО по полям sprite_t: спрайты невидимы,
* тикер работает независимо от видимости, блитов нет — sprite_update
* гоняется в текстовом режиме (скобка W3 безвредна). Ожидания:
*
* T1 LOOP 0..3 speed=2: смена раз в 2 тика: после 8 тиков frame
* снова 0 (0→1→2→3→0), src вернулся на кадр 0.
* T2 PINGPONG 0..3 speed=1: 1,2,3,2,1,0,1 — на тике 7 frame==1,
* разворот на границе без удвоения краёв.
* T3 ONCE 0..2 speed=1: после 2 тиков frame==2 и ANIM_ON ещё стоит,
* после 3-го — ON снят, DONE взведён, frame==2 (замер на last).
* T4 MOVETO (0,0)→(100,50) step=5 interval=4 (пример из §9г):
* тик 4: (5,3); тик 8: (10,5); тик 79: НЕ DONE; тик 80: (100,50),
* MOVE_ON снят, MOVE_DONE взведён. Параллельно у спрайта идёт
* LOOP-анимация (одновременность) — frame успел смениться.
* T5 sprite_anim_stop(1): ON снят, frame==1, ось = 16.
* sprite_move_stop(at_target=1) на полпути: прыжок в цель + DONE.
* T7 Y-СОРТИРОВКА: gfx_sprite_ysort(1) + прямой вызов сортировщика:
* порядок по {layer, y}; персистентный resort после смены y;
* слой поверх y (layer=1 в хвосте при минимальном y).
*
* Кадры: RAM-лента 16×16×4 (getimage), кадр k залит k — содержимое
* не важно (блитов нет), важен размер h=16 → an_step.
*/
#include <stdio.h>
#include <string.h>
#include <sprite.h>
#include "../../libbgi/_bgi.h" /* T7: прямой доступ к сорт-таблице */
static uint8_t strip[4 + 16 * 16 * 4];
static sprite_t sp[4];
/* Адрес пикселей кадра k вертикальной ленты (stride 16, кадр 16 строк):
* тикер двигает КЭШ src (sy не трогает — см. sprite.h), поэтому
* положение кадра проверяем по нему. */
#define FRAME_SRC(k) ((const uint8_t *)strip + 4 + (uint16_t)(k) * 256)
static void build_strip(void)
{
memset(strip, 0, sizeof strip);
strip[0] = 16; strip[2] = 64; /* w=16, h=64 (4 кадра) */
}
static void tick(sprite_t *arr, uint8_t n, uint16_t times)
{
while (times--)
sprite_update(arr, n);
}
int main(void)
{
uint8_t ok;
puts("SPRANIM: frame/tween auto-animation");
build_strip();
/* ---- T1: LOOP ---------------------------------------------------- */
sprite_init(&sp[0], strip);
sp[0].w = 16; sp[0].h = 16;
sprite_anim(&sp[0], 0, 3, 2, ANIM_LOOP);
tick(sp, 1, 2);
ok = (sprite_anim_frame(&sp[0]) == 1 && sp[0].src == FRAME_SRC(1));
tick(sp, 1, 4); /* всего 6: 0→1→2→3 */
ok = ok && (sprite_anim_frame(&sp[0]) == 3 && sp[0].src == FRAME_SRC(3));
tick(sp, 1, 2); /* всего 8: wrap → 0 */
ok = ok && (sprite_anim_frame(&sp[0]) == 0 && sp[0].src == FRAME_SRC(0));
printf("T1 LOOP: %s\n", ok ? "PASS" : "FAIL");
/* ---- T2: PINGPONG ------------------------------------------------ */
sprite_init(&sp[0], strip);
sp[0].w = 16; sp[0].h = 16;
sprite_anim(&sp[0], 0, 3, 1, ANIM_PINGPONG);
tick(sp, 1, 3); /* 1,2,3 */
ok = (sprite_anim_frame(&sp[0]) == 3);
tick(sp, 1, 1); /* разворот: 2 */
ok = ok && (sprite_anim_frame(&sp[0]) == 2 && sp[0].src == FRAME_SRC(2));
tick(sp, 1, 2); /* 1,0 */
ok = ok && (sprite_anim_frame(&sp[0]) == 0);
tick(sp, 1, 1); /* разворот: 1 */
ok = ok && (sprite_anim_frame(&sp[0]) == 1 && sp[0].src == FRAME_SRC(1));
printf("T2 PINGPONG: %s\n", ok ? "PASS" : "FAIL");
/* ---- T3: ONCE ---------------------------------------------------- */
sprite_init(&sp[0], strip);
sp[0].w = 16; sp[0].h = 16;
sprite_anim(&sp[0], 0, 2, 1, ANIM_ONCE);
tick(sp, 1, 2); /* 1,2 — дошли до last */
ok = (sprite_anim_frame(&sp[0]) == 2 &&
(sprite_anim_status(&sp[0]) & SPR_ANIM_ON) &&
!(sprite_anim_status(&sp[0]) & SPR_ANIM_DONE));
tick(sp, 1, 1); /* фиксация: ON→off,DONE */
ok = ok && (sprite_anim_frame(&sp[0]) == 2 && sp[0].src == FRAME_SRC(2) &&
!(sprite_anim_status(&sp[0]) & SPR_ANIM_ON) &&
(sprite_anim_status(&sp[0]) & SPR_ANIM_DONE));
tick(sp, 1, 5); /* стоит на месте */
ok = ok && (sprite_anim_frame(&sp[0]) == 2);
printf("T3 ONCE: %s\n", ok ? "PASS" : "FAIL");
/* ---- T4: MOVETO + одновременная кадровая ------------------------- */
sprite_init(&sp[0], strip);
sp[0].w = 16; sp[0].h = 16; /* x=y=0 после init */
sprite_anim(&sp[0], 0, 3, 3, ANIM_LOOP);
sprite_moveto(&sp[0], 100, 50, 5, 4);
ok = (sprite_moving(&sp[0]) != 0);
tick(sp, 1, 4);
ok = ok && (sp[0].x == 5 && sp[0].y == 3); /* Y: 3/2/3/2 */
tick(sp, 1, 4);
ok = ok && (sp[0].x == 10 && sp[0].y == 5);
tick(sp, 1, 71); /* всего 79 — ещё едет */
ok = ok && sprite_moving(&sp[0]) &&
!(sprite_anim_status(&sp[0]) & SPR_MOVE_DONE);
tick(sp, 1, 1); /* 80: приехали */
ok = ok && (sp[0].x == 100 && sp[0].y == 50 &&
!sprite_moving(&sp[0]) &&
(sprite_anim_status(&sp[0]) & SPR_MOVE_DONE));
ok = ok && (sprite_anim_frame(&sp[0]) != 0 ||
sp[0].an_timer != 3); /* кадровая тоже тикала */
printf("T4 MOVETO: %s (x=%d y=%d)\n",
ok ? "PASS" : "FAIL", sp[0].x, sp[0].y);
/* ---- T5: стопы ---------------------------------------------------- */
sprite_init(&sp[0], strip);
sp[0].w = 16; sp[0].h = 16;
sprite_anim(&sp[0], 0, 3, 1, ANIM_LOOP);
tick(sp, 1, 2); /* frame 2 */
sprite_anim_stop(&sp[0], 1); /* остановить на кадре 1 */
ok = (!(sprite_anim_status(&sp[0]) & SPR_ANIM_ON) &&
sprite_anim_frame(&sp[0]) == 1 && sp[0].sy == 16 &&
sp[0].src == FRAME_SRC(1));
tick(sp, 1, 4); /* не двигается */
ok = ok && (sprite_anim_frame(&sp[0]) == 1);
sprite_moveto(&sp[0], 60, 60, 3, 1);
tick(sp, 1, 5); /* на полпути */
sprite_move_stop(&sp[0], 1); /* добить в цель */
ok = ok && (sp[0].x == 60 && sp[0].y == 60 &&
!sprite_moving(&sp[0]) &&
(sprite_anim_status(&sp[0]) & SPR_MOVE_DONE));
sprite_moveto(&sp[0], 0, 0, 3, 1);
tick(sp, 1, 5);
sprite_move_stop(&sp[0], 0); /* замереть где есть */
ok = ok && (sp[0].x != 0 && !sprite_moving(&sp[0]) &&
!(sprite_anim_status(&sp[0]) & SPR_MOVE_DONE));
printf("T5 STOPS: %s\n", ok ? "PASS" : "FAIL");
/* ---- T6: sprite_frame ПОСЛЕ тикера ------------------------------- *
* Тикер двигает src, оставляя sx/sy как есть — sprite_frame обязан
* сверять КАДР по src, а не по (sx,sy): просьба вернуть кадр (0,0)
* при формально не изменившихся sx/sy должна вернуть src. */
sprite_init(&sp[0], strip);
sp[0].w = 16; sp[0].h = 16;
sprite_anim(&sp[0], 0, 3, 1, ANIM_LOOP);
tick(sp, 1, 2); /* src уехал на кадр 2 */
sprite_anim_stop(&sp[0], -1); /* замереть на текущем */
ok = (sp[0].src == FRAME_SRC(2) && sp[0].sy == 0);
sprite_frame(&sp[0], 0, 0); /* sx/sy те же, src ушёл */
ok = ok && (sp[0].src == FRAME_SRC(0));
printf("T6 REFRAME: %s\n", ok ? "PASS" : "FAIL");
/* ---- T7: Y-сортировка (слои + персистентность) -------------------- *
* Проверяем через возвращаемую таблицу (записи {key16, ptr16} по 4
* байта, ptr по +2): порядок указателей = порядок отрисовки. */
{
uint8_t *tab;
uint8_t i;
sprite_t *o0, *o1, *o2, *o3;
#define YS_PTR(t, i) (*(sprite_t **)((t) + (i) * 4 + 2))
for (i = 0; i < 4; i++) {
sprite_init(&sp[i], strip);
sp[i].w = 16; sp[i].h = 16;
}
sp[0].y = 50; sp[1].y = 10; sp[2].y = 30;
sp[3].y = 0; sp[3].layer = 1; /* слой поверх всех */
gfx_sprite_ysort(YSORT_Y | YSORT_LAYER); /* полный порядок */
tab = (*_spr_ysort_fn)(sp, 4);
o0 = YS_PTR(tab, 0); o1 = YS_PTR(tab, 1);
o2 = YS_PTR(tab, 2); o3 = YS_PTR(tab, 3);
/* ожидание: y10, y30, y50, затем layer1 (несмотря на y=0) */
ok = (o0 == &sp[1] && o1 == &sp[2] && o2 == &sp[0] && o3 == &sp[3]);
sp[1].y = 60; /* обгон: y10 → y60 */
tab = (*_spr_ysort_fn)(sp, 4); /* персистентный resort */
o0 = YS_PTR(tab, 0); o1 = YS_PTR(tab, 1);
o2 = YS_PTR(tab, 2); o3 = YS_PTR(tab, 3);
ok = ok && (o0 == &sp[2] && o1 == &sp[0] && o2 == &sp[1] &&
o3 == &sp[3]);
/* LAYER-only: y замаскирован — слой 0 в порядке МАССИВА
* (сеттер форсит rebuild), sp[3] (layer 1) в хвосте. */
gfx_sprite_ysort(YSORT_LAYER);
tab = (*_spr_ysort_fn)(sp, 4);
o0 = YS_PTR(tab, 0); o1 = YS_PTR(tab, 1);
o2 = YS_PTR(tab, 2); o3 = YS_PTR(tab, 3);
ok = ok && (o0 == &sp[0] && o1 == &sp[1] && o2 == &sp[2] &&
o3 == &sp[3]);
gfx_sprite_ysort(0);
printf("T7 YSORT: %s\n", ok ? "PASS" : "FAIL");
}
puts("done");
return 0;
}