b9ddce8d34
Оптимизации 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>
209 lines
10 KiB
C
209 lines
10 KiB
C
/*
|
||
* 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;
|
||
}
|