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>
This commit is contained in:
2026-07-14 17:19:24 +03:00
parent 0eec977630
commit b9ddce8d34
24 changed files with 1339 additions and 304 deletions
+221
View File
@@ -0,0 +1,221 @@
/*
* _spr_ysort — ПЕРСИСТЕНТНАЯ таблица порядка отрисовки спрайтов для
* блит-прохода sprite_update (gfx_sprite_ysort): 4-байтные записи
* {key16, ptr16}, отсортированные вставками по возрастанию ключа.
* Возвращает адрес таблицы.
*
* КЛЮЧ = {layer, y}: старший байт — слой спрайта (слой 1 всегда поверх
* слоя 0 — ходячие/летающие/курсор), младший — clamp(y, 0, 255)
* (идея пользователя: экран 256 строк — вся видимая зона сортируется
* точно, за краями порядок неважен, перекрытие невидимо). Компоненты
* отключаемы независимо (YSORT_Y/YSORT_LAYER): маски _spr_ysort_kmask
* затирают выключенный байт ключа — без ветвлений в горячем цикле.
*
* ПЕРСИСТЕНТНОСТЬ (идея пользователя): таблица живёт МЕЖДУ кадрами.
* Каждый вызов — «resort»: для каждой записи ключ пересчитывается по
* её спрайту и запись вставляется в отсортированный префикс. Спрайты
* двигаются на пиксели за кадр → порядок прошлого кадра почти актуален
* → вставки вырождаются в одно сравнение с соседом (почти линейно).
* Смена сцены (arr/count != прошлых, см. _spr_ysort_last/_lastn) —
* rebuild: заполнить ptr-поля из массива и прогнать тот же resort
* (первый кадр — полная сортировка, разово дороже). МАССИВ ПРИЛОЖЕНИЯ
* НЕ ТРОГАЕТСЯ — сортируются только внутренние записи-указатели.
*
* Стабильность: равные ключи сохраняют порядок ПРОШЛОГО КАДРА (спрайты
* на одной высоте не мерцают взаимным порядком, пока реально не
* разойдутся).
*
* C-эквивалент:
* if (arr != last || count != lastn) { // новая сцена
* for (i = 0; i < count; i++) tab[i].ptr = &arr[i];
* last = arr; lastn = count;
* }
* for (i = 0; i < count; i++) { // resort
* p = tab[i].ptr;
* key = (p->layer << 8) | clamp8(p->y);
* for (pos = i; pos && tab[pos-1].key > key; pos--)
* tab[pos] = tab[pos-1];
* tab[pos] = {key, p};
* }
* return tab;
*
* Офсеты: y = +8/9, layer = +56 в sprite_t, sizeof(sprite_t) = 57 —
* сверять пробником при правке структуры (рецепт в шапке tick_move).
* Вход __sdcccall(1): arr → HL, count → DE (используется E; count
* ограничен _GFX_YSORT_MAX вызывающим); ВОЗВРАТ — адрес таблицы (DE).
* IX callee-saved.
*/
#include "../_bgi.h"
/* Данные модуля (без вызова gfx_sprite_ysort не линкуются вместе с
* кодом). _spr_ysort_last обнуляет сеттер при включении — форсирует
* rebuild. НЕ инициализировать (crt0 зануляет). */
uint8_t _gfx_ysort_tab[4u * _GFX_YSORT_MAX];
void *_spr_ysort_last; /* arr прошлого кадра */
uint8_t _spr_ysort_lastn; /* count прошлого кадра */
uint8_t _spr_ysort_kmask[2]; /* маски ключа (сеттер): *
* [0] & y, [1] & layer */
uint8_t *_spr_ysort(void *arr, uint16_t count) __naked
{
(void)arr; (void)count;
__asm
push ix
ld a, e
or a, a
jr NZ, ys_have_sprites
;; count == 0: вернуть таблицу как есть (сцену не запоминаем)
ld de, #__gfx_ysort_tab
pop ix
ret
ys_have_sprites:
;; ---- та же сцена, что кадр назад? (arr и count совпадают) ----
ld a, (__spr_ysort_lastn)
cp a, e
jr NZ, ys_rebuild
ld bc, (__spr_ysort_last)
ld a, c
cp a, l
jr NZ, ys_rebuild
ld a, b
cp a, h
jr Z, ys_resort_entry ; сцена та же только resort
ys_rebuild:
;; ---- новая сцена: запомнить и заполнить ptr-поля записей ----
ld (__spr_ysort_last), hl
ld a, e
ld (__spr_ysort_lastn), a
ld b, a ; B = count
ld de, #__gfx_ysort_tab + 2 ; DE = &tab[0].ptr
ys_fill:
ld a, l
ld (de), a
inc de
ld a, h
ld (de), a
inc de
inc de
inc de ; DE = &tab[i+1].ptr
ld a, l ; cur += sizeof(sprite_t) = 57
add a, #57
ld l, a
jr NC, ys_fill_nc
inc h
ys_fill_nc:
djnz ys_fill
ys_resort_entry:
;; ---- resort: обновить ключи и пересортировать вставками ----
ld a, (__spr_ysort_lastn)
ld b, a ; B = счётчик записей
ld hl, #__gfx_ysort_tab ; HL = R (текущая запись)
ys_next_rec:
push bc ; [счётчик]
push hl ; [R]
;; BC = P (спрайт записи)
inc hl
inc hl
ld c, (hl)
inc hl
ld b, (hl)
;; ---- ключ: E = clamp8(P->y), D = P->layer ----
ld hl, #8
add hl, bc ; HL = &P->y
ld e, (hl)
inc hl
ld a, (hl) ; y hi
or a, a
jr Z, ys_key_y_done ; y в 0..255 точный ключ
ld e, #0
bit 7, a
jr NZ, ys_key_y_done ; y < 0 0
ld e, #255 ; y > 255 255
ys_key_y_done:
ld hl, #56
add hl, bc ; HL = &P->layer
ld d, (hl) ; key = layer:8 | y:8 (DE)
;; маски компонентов (YSORT_Y/YSORT_LAYER): выключенный в 0
ld a, (__spr_ysort_kmask + 0)
and a, e
ld e, a
ld a, (__spr_ysort_kmask + 1)
and a, d
ld d, a
;; стек к скану: [счётчик][R][P], HL = pos = R
pop hl ; R
push hl
push bc ; P
ys_scan:
;; pos == tab? место в голове
ld a, l
cp a, #<(__gfx_ysort_tab)
jr NZ, ys_cmp_prev
ld a, h
cp a, #>(__gfx_ysort_tab)
jr Z, ys_place
ys_cmp_prev:
;; key < prev.key (prev = pos-4)? сдвиг prev вправо и шаг
;; влево; key >= prev.key (включая равные стабильность)
;; место найдено.
push hl
ld a, l
sub a, #4
ld l, a
jr NC, ys_prev_nc
dec h
ys_prev_nc: ; HL = prev
ld a, e
sub a, (hl) ; key.lo - prev.lo
inc hl
ld a, d
sbc a, (hl) ; CF = key < prev.key (16-бит)
pop hl ; HL = pos
jr NC, ys_place
;; сдвиг: копировать prev pos (4 байта), pos = prev
push de ; key
ld d, h
ld e, l ; DE = pos (dst)
ld a, l
sub a, #4
ld l, a
jr NC, ys_shift_nc
dec h
ys_shift_nc: ; HL = prev (src)
push hl ; prev станет новым pos
ld bc, #4
ldir ; prev pos, 4 байта
pop hl ; pos = prev
pop de ; key
jr ys_scan
ys_place:
;; записать {key, P} в pos (HL)
ld (hl), e
inc hl
ld (hl), d
inc hl
pop bc ; P
ld (hl), c
inc hl
ld (hl), b
;; продвинуть R и счётчик
pop hl ; R
ld a, l
add a, #4
ld l, a
jr NC, ys_adv_nc
inc h
ys_adv_nc:
pop bc ; счётчик
dec b
jp NZ, ys_next_rec
ld de, #__gfx_ysort_tab ; вернуть адрес таблицы (ptr в DE)
pop ix
ret
__endasm;
}