/* * _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 /* Одна смена кадра кадровой анимации (таймер уже дозрел). * * РУЧНОЙ 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); } }