Files
Sprinter-SDCC/libbgi/common/_sprite_tick.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

386 lines
16 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.
/*
* _sprite_tick — тикер авто-анимаций (§9г): вызывается из sprite_update
* (раз на КАДР, до heal/блит-проходов — dirty должен взводиться раньше).
* Кадровая анимация и tween-перемещение НЕЗАВИСИМЫ (идут одновременно);
* тикаются и у скрытых спрайтов («время идёт»).
*
* ГОРЯЧИЙ ПУТЬ (профиль rpgwalk 2026-07-13: тик ел 7.3К тактов/спрайт —
* №2 после блита; переписан 2026-07-14):
* - один ранний выход на спрайт без активных анимаций;
* - декремент таймеров — в цикле, до call'а: недозревший таймер не
* платит за вызов хелпера;
* - сама смена кадра/порция перемещения — в static-хелперах
* tick_anim/tick_move: читаемость + SDCC работает с ЛОКАЛЬНЫМИ
* копиями полей в регистрах вместо IX-обращений (~19Т каждое)
* на каждый чих;
* - кадровая анимация двигает КЭШ АДРЕСА кадра (s->src ± an_step
* байт; wrap — прыжок на готовый an_src0) — ни выбора оси, ни
* пиксельных смещений: всё посчитано sprite_anim'ом один раз.
*
* Кадровая: маятник разворачивается НА границе (…2,3,2,1,0,1… — без
* удвоения крайних); ANIM_ONCE замирает на last: SPR_ANIM_ON снимается,
* SPR_ANIM_DONE взводится. sx/sy НЕ обновляются (см. sprite.h: их
* абсолютно переустановит следующий sprite_frame/sprite_anim).
*
* Перемещение: раз в mv_speed кадров — порция ≤mv_step шагов главной
* оси беззнакового DDA (минорная подтягивается аккумулятором —
* нелинейные 2/3/2/3 сами собой, деления нет; знаковых 16-бит
* сравнений Брезенхэма нет — на Z80 они были главной статьёй расхода).
* Цель достигнута (mv_rem == 0) — SPR_MOVE_ON→off, SPR_MOVE_DONE.
*/
#include "../_bgi.h"
#include <sprite.h>
/* Одна смена кадра кадровой анимации (таймер уже дозрел).
*
* РУЧНОЙ ASM (2026-07-14): та же болезнь SDCC, что у tick_move —
* C-версия спиллилась в IX-фрейм (~120 обращений, ~4-5К тактов на
* вызов); asm ~1К. При медленных анимациях (speed ≥ 5) вклад мал,
* но при speed 1-2 tick_anim зовётся на каждый кадр для каждого
* спрайта и становился главной статьёй тика.
*
* C-эквивалент (семантика 1:1, тест tests/spranim T1/T2/T3/T6):
*
* s->an_timer = s->an_speed;
* if (!(fl & PP_BACK)) { // вперёд
* if (fr < last) { fr++; src += step; }
* else if (fl & ONCE) { ON→off, DONE→on; return; } // не грязнить
* else if (fl & PP) { BACK→on; // разворот на границе:
* if (fr > first) { fr--; src -= step; } else нет-шага; }
* else ..LOOP.. { fr = first; src = an_src0; }
* } else { // маятник, обратно
* if (fr > first) { fr--; src -= step; }
* else { BACK→off; // разворот на first
* if (fr < last) { fr++; src += step; } else нет-шага; }
* }
* если шаг был: an_frame = fr; s->src = src; flags |= DIRTY;
*
* Офсеты полей sprite_t (см. пробник в шапке tick_move):
* src=2 flags=16 an_flags=18 an_frame=19 an_first=20 an_last=21
* an_speed=22 an_timer=23 an_step=24 an_src0=26
* Биты an_flags: ON=0x01 DONE=0x02 PP_BACK=0x10 PP=0x40 ONCE=0x80.
* Вход __sdcccall(1): s → HL. IX callee-saved.
*/
static void tick_anim(sprite_t *s) __naked
{
(void)s;
__asm
push ix
push hl
pop ix ; IX = s
;; an_timer = an_speed перезарядить темп
ld a, 22 (ix)
ld 23 (ix), a
ld b, 19 (ix) ; B = fr (текущий кадр)
ld c, 18 (ix) ; C = an_flags
bit 4, c ; _SPR_PP_BACK?
jr NZ, ta_backward
;; ---- ход вперёд (LOOP/ONCE всегда; маятник фаза «туда») ----
ld a, b
cp a, 21 (ix) ; CF = (fr < an_last)
jr NC, ta_at_last
inc b ; шаг вперёд: fr++
call ta_src_fwd ; src += an_step
jr ta_commit ; commit
ta_at_last:
;; fr == last: конец отрезка по режиму
bit 7, c ; _SPR_ANIM_ONCE?
jr Z, ta_not_once
;; ONCE: замереть на last, отчитаться, НЕ грязнить
ld a, c
and a, #0xFE ; ~SPR_ANIM_ON
or a, #0x02 ; SPR_ANIM_DONE
ld 18 (ix), a
jr ta_done ; выход без dirty
ta_not_once:
bit 6, c ; _SPR_ANIM_PP?
jr Z, ta_loop_wrap
;; маятник: разворот на границе сразу шаг назад
;; (2,3,2, крайний кадр не удваивается)
set 4, c ; _SPR_PP_BACK
ld 18 (ix), c
ld a, 20 (ix) ; an_first
cp a, b ; CF = (first < fr)
jr NC, ta_done ; лента из 1 кадра шага нет
dec b
call ta_src_back ; src -= an_step
jr ta_commit
ta_loop_wrap:
;; LOOP: wrap прыжок на заранее посчитанный first
ld b, 20 (ix) ; fr = an_first
ld l, 26 (ix)
ld h, 27 (ix) ; HL = an_src0
ld 2 (ix), l
ld 3 (ix), h ; src = an_src0
jr ta_commit_fr ; commit (src уже записан)
;; ---- маятник, фаза «обратно» ----
ta_backward:
ld a, 20 (ix) ; an_first
cp a, b ; CF = (first < fr)
jr NC, ta_turn_fwd
dec b ; шаг назад
call ta_src_back ; src -= an_step
jr ta_commit
ta_turn_fwd:
;; на first: разворот сразу шаг вперёд
res 4, c ; снять _SPR_PP_BACK
ld 18 (ix), c
ld a, b
cp a, 21 (ix) ; CF = (fr < an_last)
jr NC, ta_done ; лента из 1 кадра шага нет
inc b
call ta_src_fwd ; src += an_step
;; ---- commit: кадр сменился ----
ta_commit: ; (src записан хелпером)
ta_commit_fr:
ld 19 (ix), b ; an_frame = fr
ld a, 16 (ix)
or a, #0x0C ; flags |= _SPR_DIRTY
ld 16 (ix), a
ta_done:
pop ix
ret
;; ---- хелперы: s->src ± an_step; клоббер A,DE,HL ----
ta_src_fwd: ; src += an_step
ld l, 2 (ix)
ld h, 3 (ix)
ld e, 24 (ix)
ld d, 25 (ix)
add hl, de
ld 2 (ix), l
ld 3 (ix), h
ret
ta_src_back: ; src -= an_step
ld l, 2 (ix)
ld h, 3 (ix)
ld e, 24 (ix)
ld d, 25 (ix)
or a, a ; CF = 0
sbc hl, de
ld 2 (ix), l
ld 3 (ix), h
ret
__endasm;
}
/* Одна порция tween-перемещения (таймер уже дозрел): до mv_step шагов
* главной оси DDA (см. sprite_moveto: вся арифметика беззнаковая,
* «приехали» = mv_rem == 0 — сравнений координат нет вообще).
*
* РУЧНОЙ ASM (2026-07-14): SDCC на этой функции спиллил ВСЁ в IX-фрейм
* (~100 IX-обращений по ~19Т, ~6.5К тактов на вызов при порции в один
* шаг) — и никакие реструктуризации C этого не лечили. asm держит
* горячее в регистрах: ~2К тактов на вызов.
*
* C-эквивалент (семантика 1:1, тест tests/spranim):
*
* uint8_t n = s->mv_step; // порция...
* if (n > s->mv_rem) n = s->mv_rem; // ...не длиннее остатка
* s->mv_rem -= n;
* s->mv_timer = s->mv_speed;
* mins = 0; // шагов минорной оси
* for (k = n; k--; ) { // DDA-аккумулятор
* acc += dmin;
* if (acc >= dmaj) { acc -= dmaj; mins++; }
* }
* major_coord += sgnmaj*n; minor_coord += sgnmin*mins;
* s->flags |= _SPR_DIRTY;
* if (!s->mv_rem) { MOVE_ON→off; MOVE_DONE→on; }
*
* Офсеты полей sprite_t — сверены offsetof-пробником по фактическому
* кодогену SDCC 4.5 (поля пакуются подряд, выравнивания нет):
* x=6 y=8 flags=16 an_flags=18 mv_speed=28 mv_timer=29 mv_step=30
* mv_rem=35 mv_acc=37 mv_dmaj=39 mv_dmin=41 mv_ymaj=43
* mv_sgnmaj=44 mv_sgnmin=45
* ПРИ ИЗМЕНЕНИИ ПОРЯДКА ПОЛЕЙ В sprite.h — ПЕРЕСЧИТАТЬ (см. заметку
* у sprite_t). Вход __sdcccall(1): s → HL. IX callee-saved.
*/
static void tick_move(sprite_t *s) __naked
{
(void)s;
__asm
push ix
push hl
pop ix ; IX = s
;; mv_timer = mv_speed перезарядить темп
ld a, 28 (ix)
ld 29 (ix), a
;; B = n = min(mv_step, mv_rem); HL = rem
ld b, 30 (ix)
ld l, 35 (ix)
ld h, 36 (ix)
ld a, h
or a, a
jr NZ, tm_n_ok ; rem >= 256 заведомо >= n
ld a, l
cp a, b
jr NC, tm_n_ok ; rem >= n порция целиком
ld b, l ; хвост маршрута короче: n = rem
tm_n_ok:
;; rem -= n (заём в старший байт вручную)
ld a, l
sub a, b
ld l, a
jr NC, tm_rem_stored
dec h
tm_rem_stored:
ld 35 (ix), l
ld 36 (ix), h
push bc ; сохранить n (в B) на время цикла
;; ---- DDA минорной оси: n прогонов аккумулятора ----
;; HL = acc, DE = dmin, C = mins (счётчик шагов минорной), B = k
ld l, 37 (ix)
ld h, 38 (ix)
ld e, 41 (ix)
ld d, 42 (ix)
ld c, #0
tm_dda_loop:
add hl, de ; acc += dmin
;; беззнаковое acc >= dmaj?
ld a, l
sub a, 39 (ix)
ld a, h
sbc a, 40 (ix)
jr C, tm_dda_next ; acc < dmaj минорная стоит
;; acc -= dmaj; mins++
ld a, l
sub a, 39 (ix)
ld l, a
ld a, h
sbc a, 40 (ix)
ld h, a
inc c
tm_dda_next:
djnz tm_dda_loop
ld 37 (ix), l
ld 38 (ix), h
ld e, c ; E = mins
pop bc ; B = n
;; ---- применить порцию к координатам ----
;; sgn* = ±1: rlca выносит знак в CF (0xFF CF=1)
ld a, 43 (ix) ; ymaj?
or a, a
jr NZ, tm_ymaj
;; --- главная ось X (поле +6), минорная Y (+8) ---
ld a, 44 (ix) ; sgnmaj
rlca
ld a, b ; A = n
jr NC, tm_xmaj_n_set
neg ; A = -n
tm_xmaj_n_set:
push ix
pop hl
ld bc, #6
add hl, bc ; HL = &x
call tm_add16 ; x += ±n
ld a, e
or a, a
jp Z, tm_mark_dirty ; минорная не шагала (далеко: jp)
ld a, 45 (ix) ; sgnmin
rlca
ld a, e ; A = mins
jr NC, tm_xmaj_min_set
neg
tm_xmaj_min_set:
push ix
pop hl
ld bc, #8
add hl, bc ; HL = &y
call tm_add16 ; y += ±mins
jp tm_mark_dirty
;; --- главная ось Y (+8), минорная X (+6) ---
tm_ymaj:
ld a, 44 (ix) ; sgnmaj
rlca
ld a, b
jr NC, tm_ymaj_n_set
neg
tm_ymaj_n_set:
push ix
pop hl
ld bc, #8
add hl, bc ; HL = &y
call tm_add16 ; y += ±n
ld a, e
or a, a
jr Z, tm_mark_dirty
ld a, 45 (ix) ; sgnmin
rlca
ld a, e
jr NC, tm_ymaj_min_set
neg
tm_ymaj_min_set:
push ix
pop hl
ld bc, #6
add hl, bc ; HL = &x
call tm_add16 ; x += ±mins
tm_mark_dirty:
;; flags |= _SPR_DIRTY (0x0C) порция всегда двигает major
ld a, 16 (ix)
or a, #0x0C
ld 16 (ix), a
;; rem == 0 приехали: MOVE_ON снять, MOVE_DONE взвести
ld a, 35 (ix)
or a, 36 (ix)
jr NZ, tm_done
ld a, 18 (ix)
and a, #0xFB ; ~SPR_MOVE_ON
or a, #0x08 ; SPR_MOVE_DONE
ld 18 (ix), a
tm_done:
pop ix
ret
;; ---- хелпер: *(int16 @HL) += sign_extend(A); клоббер A,BC ----
tm_add16:
ld c, a
add a, a ; CF = знак A
sbc a, a ; A = 0x00 / 0xFF
ld b, a ; BC = знак-расширенная дельта
ld a, (hl)
add a, c
ld (hl), a
inc hl
ld a, (hl)
adc a, b
ld (hl), a
ret
__endasm;
}
void _sprite_tick(void *arr, uint8_t count)
{
sprite_t *s = (sprite_t *)arr;
uint8_t i;
for (i = 0; i < count; i++, s++) {
uint8_t fl = s->an_flags;
/* Типичный спрайт в кадре НЕ дозрел ни до смены кадра, ни до
* шага — держим этот путь коротким: одна проверка флагов +
* декремент(ы) таймера. */
if (!(fl & (SPR_ANIM_ON | SPR_MOVE_ON)))
continue;
if ((fl & SPR_ANIM_ON) && --s->an_timer == 0)
tick_anim(s);
if ((fl & SPR_MOVE_ON) && --s->mv_timer == 0)
tick_move(s);
}
}