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

222 lines
9.0 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.
/*
* _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;
}