diff --git a/docs/TODO.md b/docs/TODO.md index 8eaa851..8e483aa 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -5,6 +5,26 @@ ## Ближайшее +- [ ] **FPS-делитель (frame pacing)** — gfx_set_fps_div(n): логический + кадр = ровно n кадровых интервалов (50/25/16.7 fps), при + переполнении слота — выравнивание на ближайший фронт; счётчик + кадров через irq_install (только tiny/big в первой версии). + ПОЛНЫЙ ПЛАН: docs/sprite-api-design.md §9е (обсуждён 2026-07-14). +- [ ] **irq: цепочка обработчиков** (дизайн 2026-07-14, реализация по + потребности — понадобится, когда в программе сойдутся fps-делитель + + свой тик/звук). Вместо одного слота (EBUSY) — фиксированный + массив `isr_t _irq_chain[IRQ_CHAIN_MAX /* 4 */]` в W2-data: + - трамплин вместо SMC-вызова одного handler'а обходит слоты + (load/or a/skip-NULL/call — пустой слот ~40Т, копейки на кадре); + - API: `irq_chain_add(h)` (0 / -1+ENOMEM когда слоты кончились), + `irq_chain_remove(h)` (по указателю; слот → NULL); + - совместимость: `irq_install(h)` становится обёрткой chain_add + (EBUSY исчезает), `irq_remove()` БЕЗ аргумента снимает ВСЮ цепь + и возвращает IM 1 — семантика exit/atexit сохранена; + - порядок вызова = порядок слотов; взаимный порядок хендлеров НЕ + гарантируется после remove/add (первая дыра) — хендлеры обязаны + быть независимы; дисциплина ISR прежняя (irq.h); + - CTC-цепочка (irq_ctc_*) — тем же паттерном, если понадобится. - [ ] **П6/железо**: MAME-смоук всех тестов после libc-сплита (conio, ptime, stattest, mouse, gfx_demo/gfx_d16/gfx_text/gfx_mous — трогался asm акселератора); затем прогон на реальном Sprinter @@ -125,15 +145,14 @@ Quick wins: putsprite/movesprite в graphics.h; проверено tests/sprites (прозрачность, клиппинг 4 краёв, heal src==dst, чистый след movesprite, атлас). Осталась Фаза C — пример-курсор. -- [ ] **managed-движок спрайтов v2** — sprite_t (drawn-координаты - per-page ВНУТРИ структуры) + retained-модель: - sprite_move/frame/show/hide меняют только state, - sprite_update(arr,n) делает «heal все → блит все» на текущей - draw-странице, sprite_flip — сахар с vsync+флипом; z-order = - индекс массива. Интегрирует спрайты со сменой страниц — - снимает с приложения учёт drawn[2][N] (источник багов, - examples/balls 2026-07-11). Полный эскиз: - docs/sprite-api-design.md §9.1. +- [x] ~~**managed-движок спрайтов v2**~~ — СДЕЛАН 2026-07-12/13 + (sprite.h: retained-модель + batch-пасс, см. пункт batch-пасса + ниже) и расширен: W0-атласы (.atl, atlas_load/atlas_sprite_init, + ISR-стаб — §9в), авто-анимация кадровая + tween (sprite_anim/ + sprite_moveto/sprite_update тикер — §9г), gfx_pal_sync/fload/ + fsave. Замерен бюджет: ~26К тактов на спрайт 16×16, лимит 14 + спрайтов на стабильные 48 fps (§9д); демо examples/space, + examples/rpgwalk (+rpgprof — профайлер кадра). - [ ] `gfx_xor_rect` / `gfx_or_rect` / `gfx_and_rect` / `gfx_invert_rect` - [ ] шрифты ≠ 8×8: gfx_set_font_data(ptr,w,h,advance), proportional, 8×16/16×16, отдельный font_id API; font-quad для 640×256 diff --git a/docs/size_baseline.tsv b/docs/size_baseline.tsv index 1427363..7fc8065 100644 --- a/docs/size_baseline.tsv +++ b/docs/size_baseline.tsv @@ -4,7 +4,7 @@ argv 3431 assrtest 3847 atlas 9193 attrprob 4087 -balls 6249 +balls 6031 banked 1056 bankedbg 1067 banklocl 4683 @@ -41,13 +41,14 @@ mouse 4382 openenv 6124 palfile 5406 ptime 5744 -rpgwalk 9683 +rpgprof 11852 +rpgwalk 9501 rt_test 4892 seek 4188 simple 955 solidt 11531 -space 10976 -spranim 8752 +space 11272 +spranim 9431 spriteclip 4119 sprites 6791 stattest 7549 diff --git a/docs/sprite-api-design.md b/docs/sprite-api-design.md index 5907347..dd2cb67 100644 --- a/docs/sprite-api-design.md +++ b/docs/sprite-api-design.md @@ -782,19 +782,23 @@ callback НЕ делаем — опрос проще и дешевле). watchpoint на OUT-маркеры + printf/g + clog (memory/mame_mcp_bridge). Цена анимированного спрайта 16×16 в sprite_update (heal+blit на -кадр, W0-атлас, noclip): +кадр, W0-атлас, noclip) — два среза: до и после оптимизаций A+B +2026-07-14 (кэш src/stride в sprite_t — blit не считает адрес кадра; +беззнаковый DDA вместо Брезенхэма + asm-ядро tick_move): -| фаза | тактов/спрайт | -|--------------------|---------------| -| тик (anim+tween) | ~7.3К (4.3-9.2К) | -| heal (банк 0x50) | ~6.4К | -| blit (банк 0x5C) | ~11.8К | -| **итого** | **~26К** | +| фаза | до A+B | после A+B | +|--------------------|---------------|---------------| +| тик (anim+tween) | ~7.3К | **~3.0К** | +| heal (банк 0x50) | ~6.4К | ~6.4К | +| blit (банк 0x5C) | ~11.8К | **~9.7К** | +| **итого** | **~26К** | **~19.5К** | -Фикс-часть цикла демо (kbhit+getdatetime+свопы страниц) ~14-19К. -Отсюда лимит на стабильные 48 fps (КАЖДЫЙ кадр в один интервал): -**14 спрайтов** (замерено: все обычные кадры ≤406К, 10/232 двойных — -только кадры с FPS-плашкой); 15 — 94% кадров, 16 — на грани (50%). +Фикс-часть цикла демо (kbhit+getdatetime+свопы страниц) ~15-20К. +Лимит на стабильные 48 fps (КАЖДЫЙ кадр в один интервал): было +**14 спрайтов**, после A+B — расчётно **~21** (15 замерено: активная +часть 307К из 430К, все периоды 1.0). Дорогой HUD ломает бюджет: +bar()+outtextxy() FPS-плашки стоили ~210К (полкадра!) — рисовать +putimage-заготовкой/маленьким bar. Уроки профиля: - O(sy)-цикл `src += img_w` в blit-обёртках стоил до 64К тактов на @@ -802,10 +806,148 @@ watchpoint на OUT-маркеры + printf/g + clog (memory/mame_mcp_bridge). __mulint 2026-07-13 (O(1) ~300Т): blit упал 45К → 11.8К/спрайт, rpgwalk 24 fps → стабильные 48. - bar()+outtextxy() FPS-плашки = ~210К тактов (~10 мс, полкадра!) — - текст BGI дорогой; на 14 спрайтах кадры с плашкой уходят в два - интервала. Для HUD в бюджете рисовать заготовленным putimage. -- Резерв: тик в C ~7.3К/спрайт — asm/упрощение подняло бы лимит к - ~17-18 спрайтам. + текст BGI дорогой; для HUD в бюджете рисовать заготовленным + putimage/узким bar. +- Оптимизации A+B (2026-07-14): (A) тик — SDCC спиллит функции с + «многими» 16-бит локалами в IX-фрейм (~100 обращений по ~19Т), + реструктуризации C НЕ помогали → tick_move переписан на asm + (__naked, ~2К/вызов вместо ~6.5К) поверх переформулировки + Брезенхэм → беззнаковый DDA (mv_rem/mv_acc; знаковых 16-бит + сравнений нет, «приехали» = rem==0); (B) кэш src/stride в sprite_t — + адрес кадра считают только sprite_frame (одно умножение на СМЕНУ + кадра) и тикер (± an_step байт инкрементально), блит берёт готовый. + Итог: тик 100К → 44.6К на 15 спрайтах, блит 177К → 146К. +- ВНИМАНИЕ: правка sprite.h без пересборки libbgi молча ломала + рантайм (stale .rel со старой раскладкой sprite_t) — libbgi/Makefile + теперь зависит от заголовков (HDRS). Профилировочный rpgprof + перерос tiny (BSS за 0xC000, мгновенная смерть) — собирается + --memory small; mkexe переполнение tiny пока не ловит. +- Y-сортировка (gfx_sprite_ysort, 2026-07-14, все идеи пользователя): + ПЕРСИСТЕНТНАЯ asm-таблица 4-байтных записей {key16 = layer:8| + clamp_y:8, ptr16} — каждый кадр resort вставками порядка ПРОШЛОГО + кадра (обновив ключи), rebuild только при смене arr/count. + Ключ младшим байтом — clamp(y,0,255): экран 256 строк, видимая зона + точно, за краями порядок неважен; старшим — sprite_t.layer (поле В + КОНЦЕ структуры, офсеты asm не сдвигает): слои сцены — «ходячие + слоем 0, летающие слоем 1» В ОДНОМ массиве/одном sprite_update. + ВАЖНО: два sprite_update на страницу ЗАПРЕЩЕНЫ (heal второй группы + стирает первую — ОЗУ-копия чистая от спрайтов; задокументировано в + sprite.h). Подключение funcptr-DCE (выключено = не линкуется). + Цена: ~28К/15 спрайтов персистентно (~2К/спрайт; с нуля было 44К, + ожидание 12-15К не достигнуто — персонажи постоянно обгоняют друг + друга по y, вставки не нулевые). Компоненты порядка отключаемы + НЕЗАВИСИМО (2026-07-14): mode = YSORT_Y | YSORT_LAYER — выключенный + компонент затирается МАСКОЙ ключа (_spr_ysort_kmask, 2 AND на + спрайт ≈ 1К на 15) — в сортировщике нет ветвлений по режиму. + Тест: spranim T7 (порядок, слой поверх y, персистентный resort, + LAYER-only). + +## 9е. FPS-делитель — frame pacing (ПЛАН, обсуждён 2026-07-14, НЕ реализовано) + +Цель (постановка пользователя): управляемые режимы 50/25/~16.7 fps — +логический кадр занимает РОВНО n кадровых интервалов независимо от +того, плавает ли длительность отрисовки; при переполнении слота — +выравнивание на ближайший фронт (без накопления фазовой ошибки). +Пример (n=3): рендер <1 интервала → ждать 3 фронта; рендер пересёк +1 фронт → ждать ещё 2; пересёк 2+ → ждать ближайший 1. + +### Почему нельзя поллингом (текущий gfx_wait_vsync) + +Поллинг видит СОСТОЯНИЕ луча (бит 5 порта #FE), а не события: фронты, +прошедшие во время рендера, не наблюдаемы → «сколько интервалов занял +рендер» узнать нельзя. Наивное «подождать n фронтов подряд» ломается +на рендере в 1.5 интервала: съеденный фронт не засчитан, период +становится n+1 — то самое плавание скорости. + +### Механика: фоновый счётчик кадров (ISR) + +irq_install (libc/irq, механика уже обкатана калибровкой sleep) с +однострочным обработчиком `_gfx_frame_tick++` (volatile uint8; wrap +безопасен — вся арифметика разностная по модулю 256). + +Семантика ожидания при делителе n (last — тик прошлого выхода): + + elapsed = _gfx_frame_tick - last; + if (elapsed >= n) { /* слот истёк — опоздали */ + t = _gfx_frame_tick; + while (_gfx_frame_tick == t) HALT; /* ближайший фронт */ + } else { + while ((uint8_t)(_gfx_frame_tick - last) < n) HALT; + } + last = _gfx_frame_tick; + +HALT будится ЛЮБЫМ прерыванием (клавиатура/CBL) — цикл перепроверяет +счётчик; просыпание на кадровом фронте + хвост ISR-цепочки → своп +страницы попадает в верхний бордер (16 строк запаса), tear-free. +Прерывания к моменту вызова включены (_bgi_end делает EI); подстраховка +EI перед HALT обязательна (как в fallback текущего gfx_wait_vsync). + +### API + + int gfx_set_fps_div(uint8_t n); /* 1 = 50 fps (дефолт), 2 = 25, + 3 = ~16.7, 4 = 12.5, ... + 0 трактовать как 1 */ + +Call-sites НЕ меняются: gfx_wait_vsync() один; n<=1 — СТАРЫЙ луч- +поллинг (ISR не ставится, поведение бит-в-бит текущее), n>=2 — +счётчиковый путь. Отдельные gfx_wait_vsync25fps/17fps НЕ вводим — +сеттер в духе gfx_sprite_clip/ysort, переключение на лету. + +### Файлы (по канону 1 модуль = 1 сущность) + +- common/_gfx_fps_state.c — `uint8_t _gfx_fps_div;` + `volatile uint8_t _gfx_frame_tick;` `uint8_t _gfx_fps_last;` + (данные; div=0/1 → старый путь; НЕ инициализировать). +- common/_gfx_frame_isr.c — ISR: `_gfx_frame_tick++` (правила irq.h: + без ESTEX/gfx/банков — здесь чистый инкремент). +- common/gfx_set_fps_div.c — сеттер: n>=2 → irq_install(_gfx_frame_isr) + (однократно; повторные вызовы только меняют div), n<=1 → + irq_remove (если ставили) + div=1. Возврат 0 / -1+errno + (EINVAL/EBUSY от irq_install). ТОЛЬКО этот модуль ссылается на + irq-механику и ISR — DCE: программа без сеттера не тянет ничего + (wait ссылается лишь на data-модуль). +- gfx_wait_vsync.c — ветка `if (_gfx_fps_div >= 2)` перед текущим + поллингом. + +### Ограничения (задокументировать в gfx.h) + +1. irq_install работает ТОЛЬКО в tiny/big; в small/huge вернёт EINVAL + → gfx_set_fps_div(2+) там недоступен (rpgprof — small!). ПРИЧИНА + (см. docs/im2_isr_design.md): IM2-таблица/трамплин/handler обязаны + лежать по адресам, валидным в ЛЮБОЙ момент прихода прерывания — + стабильно только W2 (стек обязан быть там по требованию DSS, окно + не перемапляется); W1 «может swap'нуться» (банки кода big/huge; + системные вызовы посреди которых приходит прерывание). Это + ограничение РЕАЛИЗАЦИИ, не платформы: обходы — (а) area _TRAMP_W2 + ~0xBE00 (спроектирована в im2-доке), (б) для НАШЕГО микро-ISR + (инкремент байта) — эмит стаба прямо в W2-BSS-буфер, минуя + C-handler в W1; (в) CTC-канал (вектор 0x06, отдельный от + кадрового; пресет IRQ_CTC_VSYNC_DIV2/3 = 17920 тактов = точный + период кадра 896×320, дрейфа нет; фаза не привязана к лучу — разовая + синхронизация поллингом фронта при установке). Делать по + потребности. +2. Один слот user-ISR: приложение со СВОИМ irq_install получит EBUSY + от сеттера. Обход v1: приложение инкрементирует _gfx_frame_tick из + своего обработчика и выставляет _gfx_fps_div напрямую (внутренние + символы задокументировать); правильное решение — ЦЕПОЧКА + обработчиков в libc/irq (дизайн записан в docs/TODO.md 2026-07-14, + реализация по потребности). +3. W0-атласы + IM2: прерывание при замапленном атласе идёт через + IM2-таблицу приложения (W2) → чейн на 0x0038 = ISR-стаб страницы + атласа → DSS. Путь совместим по построению, но ПРОВЕРИТЬ в MAME + (пункт теста ниже). + +### Тест (tests/fpsdiv) + +Рендер-заглушки калиброванной длины busy-wait'ом (~0.5 / 1.5 / 2.5 +интервала; калибровка циклом с известными тактами, как в sleep): +- n=1: периоды по счётчику 1/2/3 (текущее поведение); +- n=2: 2/2/3; +- n=3: 3/3/3; +плюс возврат ошибки в small (собрать вариант) и живой прогон +rpgwalk с n=2 — FPS-бар стоит на 25 без плавания; визуально скорость +персонажей стабильна. Отдельно: rpgprof-сценарий с W0-атласами + +включённый делитель (проверка п.3 ограничений). ## 10. Риски / что проверить артефактом (Фаза 0) diff --git a/examples/rpgwalk/Makefile b/examples/rpgwalk/Makefile index 8ae0aac..0dccc2e 100644 --- a/examples/rpgwalk/Makefile +++ b/examples/rpgwalk/Makefile @@ -1,9 +1,27 @@ PROJ_ROOT := $(abspath $(CURDIR)/../..) EXAMPLE := rpgwalk EXTRA_FLAGS ?= --gfx 256 --memory tiny -EXTRA_DATA := bard1.atl bard2.atl bard.pal +# rpgprof.exe в EXTRA_DATA — `make floppy` кладёт на дискету ОБА exe. +EXTRA_DATA := bard1.atl bard2.atl bard.pal rpgprof.exe include $(PROJ_ROOT)/app.mk +# Профилировочная копия демо (бордер-полосы + маркеры для wpiset) — +# собирается вместе с rpgwalk; инструкция в шапке rpgprof.c. +# ВНИМАНИЕ: --memory small (не tiny!) — rpgprof толще rpgwalk +# (my_update + профиль-обвязка): в tiny его BSS вылезал за W2 +# (0xC05B > 0xBFFF) → мгновенный «Unexpected application termination» +# (кусало 2026-07-14; mkexe это переполнение пока НЕ ловит). +all: rpgprof.exe +floppy: rpgprof.exe + +rpgprof.exe: rpgprof.c $(MKEXE) $(LIB) $(RUNTIME_DEPS) $(PROJ_ROOT)/libbgi/_bgi.h + $(SPRINTER_CC) --gfx 256 --memory small -o $@ rpgprof.c + +clean: clean-prof +clean-prof: + rm -f rpgprof.exe +.PHONY: clean-prof + # 8 персонажей по 12 кадров = 24 КБ пикселей — НЕ лезут в одну # EMM-страницу (лимит 16К-0x100): два атласа по 4 персонажа. sprites.stamp: conv_sprites.py $(PROJ_ROOT)/toolchain/mkatlas.py diff --git a/examples/rpgwalk/rpgprof.c b/examples/rpgwalk/rpgprof.c index f99ecf1..83d2adb 100644 --- a/examples/rpgwalk/rpgprof.c +++ b/examples/rpgwalk/rpgprof.c @@ -2,25 +2,27 @@ * rpgprof — профилировочная копия rpgwalk (НЕ собирается по умолчанию). * Два инструмента в одном: * - * 1. ВИЗУАЛЬНЫЙ профайлер — цвет ГРАНИЦЫ экрана (порт #FE, биты 0-2) - * меняется на входе в каждую секцию кадра; луч бежит сверху вниз - * (~320 строк растра за кадр 48.83 Гц, 1344 такта на строку), так - * что толщина цветной полосы бордера ∝ времени секции: - * красный (2) — логика: kbhit, машина маршрутов, getdatetime/FPS - * жёлтый (6) — тик авто-анимаций (_spr_tick_fn) - * magenta (3) — heal-проход (банк 0x50) - * белый (7) — blit-проход (банк 0x5C) - * синий (1) — gfx_wait_vsync (простой до конца кадра) - * Много синего = запас; белый/жёлтый через весь экран = не влезаем. - * ВНИМАНИЕ: dev-MAME перерисовывает бордер крупными кусками (не - * per-scanline), полосы приблизительные и «пляшут» — для точных - * цифр см. п.2. + * 1. ВИЗУАЛЬНЫЙ профайлер — цвет ГРАНИЦЫ экрана меняется на входе в + * каждую секцию кадра; луч бежит сверху вниз (~320 строк растра за + * кадр 48.83 Гц, 1344 такта на строку), так что толщина цветной + * полосы бордера ∝ времени секции. + * ВНИМАНИЕ: бордер принимает только биты 1-2 порта #FE (бит 0 = + * красный ИГНОРИРУЕТСЯ — проверено 2026-07-14) → доступны РОВНО + * четыре цвета: 0 чёрный, 2 синий, 4 зелёный, 6 голубой: + * синий (2) — логика (короткая) и blit-проход (банк 0x5C) + * зелёный (4) — тик авто-анимаций (_spr_tick_fn) + * голубой (6) — heal-проход (банк 0x50) + * чёрный (0) — gfx_wait_vsync (простой до конца кадра) + * Много чёрного = запас; синий+зелёный через весь бордер = не + * влезаем. dev-MAME перерисовывает бордер крупными кусками (не + * per-scanline) — полосы приблизительные, для точных цифр см. п.2. * * 2. ТАКТОВЫЙ профайлер — те же OUT'ы ловятся watchpoint'ом дебаггера * dev-MAME (рецепт: memory/mame_mcp_bridge и docs/sprite-api-design.md * §9д): wpiset по маске порта + printf totalcycles/wpdata + авто-g, - * затем оп clog моста. Значения маркеров (2/6/3/7/1) уникальны — - * по wpdata видно, какая секция началась. + * затем оп clog моста. Для уникальности wpdata при одинаковом цвете + * можно кодировать секцию в битах 3+ (бордер их не видит): + * напр. логика 0x12, blit 0x02 — оба синие, но различимы в логе. * * Сборка и запуск (из этого каталога; атласы уже собраны make'ом): * ../../bin/sprinter-cc --memory tiny --gfx 256 -o rpgprof.exe rpgprof.c @@ -53,13 +55,14 @@ static void my_update(sprite_t *arr, uint8_t count) uint8_t dbit = p ? _SPR_DIRTY1 : _SPR_DIRTY0; uint8_t saved = gfx_get_bank(); uint8_t i; + const uint8_t *rec; sprite_t *s; - io_border = 6; /* жёлтый: тик анимаций */ + io_border = 4; /* зеленый: тик анимаций */ if (_spr_tick_fn) (*_spr_tick_fn)(arr, count); - io_border = 3; /* magenta: heal */ + io_border = 6; /* голубой: heal */ gfx_set_bank(GFX_BANK_NORMAL); _bgi_begin(); for (i = 0, s = arr; i < count; i++, s++) { @@ -70,13 +73,23 @@ static void my_update(sprite_t *arr, uint8_t count) } _bgi_end(); - io_border = 7; /* белый: blit */ + /* Y-сортировка (как в sprite_update; входит в «синюю» blit-секцию + * профиля — её цену видно по маркерам). */ + io_border = 2; /* синий: blit (+ сортировка) */ + rec = 0; + if (_spr_ysort_fn && count <= _GFX_YSORT_MAX) + rec = (*_spr_ysort_fn)(arr, count) + 2; gfx_set_bank(GFX_BANK_SPRITE); _bgi_begin(); { uint8_t mapped = 0; - for (i = 0, s = arr; i < count; i++, s++) { + for (i = 0; i < count; i++) { + if (rec) { + s = *(sprite_t * const *)rec; /* Y-порядок */ + rec += 4; + } else + s = arr + i; if (s->flags & (dbit | SPR_ALWAYS)) { if (s->flags & SPR_VISIBLE) { if (s->page != mapped && s->page) { @@ -84,7 +97,7 @@ static void my_update(sprite_t *arr, uint8_t count) _io_page_w0 = s->page; mapped = s->page; } - (*_gfx_blit_fn)(s->x, s->y, s->img, s->sx, s->sy, s->w, s->h); + (*_gfx_blit_fn)(s->x, s->y, s->src, s->stride, s->w, s->h); s->drawn[p].x = s->x; s->drawn[p].y = s->y; s->drawn[p].on = 1; @@ -100,7 +113,7 @@ static void my_update(sprite_t *arr, uint8_t count) gfx_set_bank(saved); } -#define NCHR 8 +#define NCHR 15 #define MINX 0 #define MAXX (320 - 16) #define MINY 14 @@ -116,8 +129,10 @@ static uint8_t phase[NCHR]; static void set_dir(uint8_t i, uint8_t d) { dir[i] = d; +// sprite_anim(&sp[i], (uint8_t)(d * 3), (uint8_t)(d * 3 + 2), +// (uint8_t)(5 + (i & 3)), ANIM_PINGPONG); sprite_anim(&sp[i], (uint8_t)(d * 3), (uint8_t)(d * 3 + 2), - (uint8_t)(5 + (i & 3)), ANIM_PINGPONG); + (uint8_t)5, ANIM_PINGPONG); } static void go_edge(uint8_t i) @@ -141,7 +156,7 @@ static void go_back_random(uint8_t i) int tx = s->x, ty = s->y, avail, dist; uint8_t d = dir[i]; - d = (uint8_t)(d ^ ((d < 2) ? 3 : 1)); + d = (uint8_t)(3 - d); /* 0↔3, 1↔2 — разворот */ set_dir(i, d); switch (d) { case 0: avail = MAXY - s->y; dist = QY; break; @@ -173,18 +188,38 @@ static void turn_90(uint8_t i) go_edge(i); } +// static void show_fps(uint8_t v) +// { +// char buf[4]; + +// buf[0] = (char)('0' + (v / 100) % 10); +// buf[1] = (char)('0' + (v / 10) % 10); +// buf[2] = (char)('0' + v % 10); +// buf[3] = 0; +// setcolor(WHITE); +// setfillstyle(SOLID_FILL, BLACK); +// bar(0, 0, 27, 9); +// outtextxy(2, 1, buf); +// } + static void show_fps(uint8_t v) { - char buf[4]; - - buf[0] = (char)('0' + (v / 100) % 10); - buf[1] = (char)('0' + (v / 10) % 10); - buf[2] = (char)('0' + v % 10); - buf[3] = 0; - setcolor(WHITE); - setfillstyle(SOLID_FILL, BLACK); - bar(0, 0, 27, 9); - outtextxy(2, 1, buf); + uint8_t color = BLACK; + setfillstyle(SOLID_FILL, color); + bar(0, 2, 60, 5); + if(v < 30) { + color = RED; + } else if(v < 40) { + color = BLUE; + } else if(v < 47) { + color = MAGENTA; + } else if(v < 52) { + color = CYAN; + } else { + color = YELLOW; + } + setfillstyle(SOLID_FILL, color); + bar(0, 2, v, 5); } static void draw_field(void) @@ -203,6 +238,16 @@ static void draw_field(void) putpixel(x - 1, y, LIGHTGREEN); putpixel(x + 1, y, LIGHTGREEN); } + setfillstyle(SOLID_FILL, BLACK); + setcolor(BLACK); + line(0, 0, 0, 7); + line(10, 0, 10, 7); + line(20, 0, 20, 7); + line(30, 0, 30, 7); + line(40, 0, 40, 7); + line(50, 0, 50, 7); + line(60, 0, 60, 7); + setcolor(WHITE); outtextxy(52, 3, "RPGPROF: border = frame profile, ESC"); } @@ -251,7 +296,7 @@ int main(void) last_sec = dt.second; for (;;) { - io_border = 2; /* красный: логика */ + io_border = 2; /* синий: логика */ if (kbhit() && getch() == 27) break; @@ -277,8 +322,8 @@ int main(void) fps_draw--; show_fps(fps_val); } - my_update(sp, NCHR); /* маркеры 6/3/7 внутри */ - io_border = 1; /* синий: ждём vsync */ + my_update(sp, NCHR); /* маркеры 2/4/6 внутри */ + io_border = 0; /* черный: ждём vsync */ gfx_wait_vsync(); gfx_set_visible_page(hidden); } diff --git a/examples/rpgwalk/rpgwalk.c b/examples/rpgwalk/rpgwalk.c index ea54bda..7e1d346 100644 --- a/examples/rpgwalk/rpgwalk.c +++ b/examples/rpgwalk/rpgwalk.c @@ -24,7 +24,7 @@ #include #include -#define NCHR 8 +#define NCHR 15 #define MINX 0 #define MAXX (320 - 16) #define MINY 14 /* под заголовком */ @@ -41,8 +41,10 @@ static uint8_t phase[NCHR]; /* 0 = до края, 1 = до случайно static void set_dir(uint8_t i, uint8_t d) { dir[i] = d; +// sprite_anim(&sp[i], (uint8_t)(d * 3), (uint8_t)(d * 3 + 2), +// (uint8_t)(5 + (i & 3)), ANIM_PINGPONG); sprite_anim(&sp[i], (uint8_t)(d * 3), (uint8_t)(d * 3 + 2), - (uint8_t)(5 + (i & 3)), ANIM_PINGPONG); + (uint8_t)20, ANIM_PINGPONG); } /* Идти до края экрана вдоль dir[i] (скорость: 1px, интервал 1-2). */ @@ -52,10 +54,10 @@ static void go_edge(uint8_t i) int tx = s->x, ty = s->y; switch (dir[i]) { - case 0: ty = MAXY; break; - case 1: tx = MINX; break; - case 2: tx = MAXX; break; - default: ty = MINY; break; + case 0: ty = MAXY; break; + case 1: tx = MINX; break; + case 2: tx = MAXX; break; + default: ty = MINY; break; } sprite_moveto(s, tx, ty, 1, (uint8_t)(1 + (i & 1))); phase[i] = 0; @@ -68,23 +70,24 @@ static void go_back_random(uint8_t i) int tx = s->x, ty = s->y, avail, dist; uint8_t d = dir[i]; - d = (uint8_t)(d ^ ((d < 2) ? 3 : 1)); /* 0↔3, 1↔2 — разворот */ + // d = (uint8_t)(d ^ ((d < 2) ? 3 : 1)); /* 0↔3, 1↔2 — разворот */ + d = 3 - d; /* 0↔3, 1↔2 — разворот */ set_dir(i, d); switch (d) { - case 0: avail = MAXY - s->y; dist = QY; break; - case 1: avail = s->x - MINX; dist = QX; break; - case 2: avail = MAXX - s->x; dist = QX; break; - default: avail = s->y - MINY; dist = QY; break; + case 0: avail = MAXY - s->y; dist = QY; break; + case 1: avail = s->x - MINX; dist = QX; break; + case 2: avail = MAXX - s->x; dist = QX; break; + default: avail = s->y - MINY; dist = QY; break; } if (avail > dist) dist += rand() % (avail - dist + 1); else dist = avail; /* некуда — до края */ switch (d) { - case 0: ty += dist; break; - case 1: tx -= dist; break; - case 2: tx += dist; break; - default: ty -= dist; break; + case 0: ty += dist; break; + case 1: tx -= dist; break; + case 2: tx += dist; break; + default: ty -= dist; break; } sprite_moveto(s, tx, ty, 1, (uint8_t)(1 + (i & 1))); phase[i] = 1; diff --git a/libbgi/Makefile b/libbgi/Makefile index 1fbf39b..793621c 100644 --- a/libbgi/Makefile +++ b/libbgi/Makefile @@ -28,6 +28,12 @@ BUILD := $(CURDIR)/build COMMON_C := $(wildcard $(CURDIR)/common/*.c) BGI256_C := $(wildcard $(CURDIR)/bgi256/*.c) +# Заголовки либы: любой .rel зависит от всех — правка _bgi.h/sprite.h/… +# пересобирает модули (иначе stale .rel со СТАРОЙ раскладкой sprite_t +# молча ломает рантайм — кусало 2026-07-14: sprite_moveto писал флаги +# по старому офсету, T4/T5 spranim FAIL). +HDRS := $(wildcard $(CURDIR)/*.h $(CURDIR)/include/*.h) + COMMON_FAST_RELS := $(patsubst $(CURDIR)/common/%.c,$(BUILD)/fast/common/%.rel,$(COMMON_C)) BGI256_FAST_RELS := $(patsubst $(CURDIR)/bgi256/%.c,$(BUILD)/fast/bgi256/%.rel,$(BGI256_C)) COMMON_SAFE_RELS := $(patsubst $(CURDIR)/common/%.c,$(BUILD)/safe/common/%.rel,$(COMMON_C)) @@ -45,21 +51,21 @@ safe: $(LIB_SAFE) # --- Fast version: -DGFX_NOCHECK (guard'и выключены) --- -$(BUILD)/fast/common/%.rel: $(CURDIR)/common/%.c +$(BUILD)/fast/common/%.rel: $(CURDIR)/common/%.c $(HDRS) @mkdir -p $(dir $@) $(SDCC) -mz80 --no-std-crt0 --std-c99 --opt-code-size -DGFX_NOCHECK --max-allocs-per-node 100000 $(INC) -c -o $@ $< -$(BUILD)/fast/bgi256/%.rel: $(CURDIR)/bgi256/%.c +$(BUILD)/fast/bgi256/%.rel: $(CURDIR)/bgi256/%.c $(HDRS) @mkdir -p $(dir $@) $(SDCC) -mz80 --no-std-crt0 --std-c99 --opt-code-size -DGFX_NOCHECK --max-allocs-per-node 100000 $(INC) -c -o $@ $< # --- Safe version: guard'и включены (по умолчанию) --- -$(BUILD)/safe/common/%.rel: $(CURDIR)/common/%.c +$(BUILD)/safe/common/%.rel: $(CURDIR)/common/%.c $(HDRS) @mkdir -p $(dir $@) $(SDCC) -mz80 --no-std-crt0 --std-c99 --opt-code-size $(INC) -c -o $@ $< -$(BUILD)/safe/bgi256/%.rel: $(CURDIR)/bgi256/%.c +$(BUILD)/safe/bgi256/%.rel: $(CURDIR)/bgi256/%.c $(HDRS) @mkdir -p $(dir $@) $(SDCC) -mz80 --no-std-crt0 --std-c99 --opt-code-size $(INC) -c -o $@ $< diff --git a/libbgi/_bgi.h b/libbgi/_bgi.h index 1416e2e..3359ec9 100644 --- a/libbgi/_bgi.h +++ b/libbgi/_bgi.h @@ -117,16 +117,19 @@ void _gfx_heal_full(int x, int y, int w, int h); /* Лин-ядра для СПРАЙТОВ (кадр ≤ 64×64, sprite.h): как *_full, но без * split'а >256 и с 8-битными w/h (движок/putsprite — горячий путь; * программа только со спрайтами не тянет общие *_full со split'ом). - * common/_gfx_blit_sprite.c, _gfx_heal_sprite.c. */ -void _gfx_blit_sprite(int x, int y, const void *img, - int sx, int sy, uint8_t w, uint8_t h); + * Сигнатура БЕЗ img/sx/sy (2026-07-14): src — ГОТОВЫЙ адрес пикселей + * кадра, stride — шаг строки источника; считает их вызывающий один раз + * (кэш в sprite_t / putsprite из заголовка) — ни чтения заголовка, ни + * умножения в ядре. common/_gfx_blit_sprite.c, _gfx_heal_sprite.c. */ +void _gfx_blit_sprite(int x, int y, const uint8_t *src, + uint16_t stride, uint8_t w, uint8_t h); void _gfx_heal_sprite(int x, int y, uint8_t w, uint8_t h); /* NOCLIP-ядра: клип-кода нет вообще (полный выигрыш от codegen, не только * скип проверок). Зовутся при gfx_sprite_clip(0) — приложение * гарантирует пределы. common/_gfx_blit_sprite_noclip.c, _gfx_heal_… */ -void _gfx_blit_sprite_noclip(int x, int y, const void *img, - int sx, int sy, uint8_t w, uint8_t h); +void _gfx_blit_sprite_noclip(int x, int y, const uint8_t *src, + uint16_t stride, uint8_t w, uint8_t h); void _gfx_heal_sprite_noclip(int x, int y, uint8_t w, uint8_t h); /* Диспетчеризация clip/noclip через УКАЗАТЕЛИ (не ветка в горячем цикле): @@ -136,12 +139,30 @@ void _gfx_heal_sprite_noclip(int x, int y, uint8_t w, uint8_t h); * см. docs/sprite-api-design.md). Дефолт (clip-ядра) — статическая * инициализация; указатели в РАЗДЕЛЬНЫХ data-модулях (_gfx_blit_fn.c, * _gfx_heal_fn.c): putsprite-only программа не тянет heal-ядро. */ -typedef void (*_gfx_blit_fp)(int, int, const void *, int, int, +typedef void (*_gfx_blit_fp)(int, int, const uint8_t *, uint16_t, uint8_t, uint8_t); typedef void (*_gfx_heal_fp)(int, int, uint8_t, uint8_t); extern _gfx_blit_fp _gfx_blit_fn; extern _gfx_heal_fp _gfx_heal_fn; +/* ---- Y-сортировка блит-прохода (sprite.h: gfx_sprite_ysort) ------- * + * _spr_ysort ведёт ПЕРСИСТЕНТНУЮ таблицу 4-байтных записей + * {key16 = layer:8|clamp_y:8, ptr16} (вставки, стабильно; таблица и + * состояние сцены — приватные данные его модуля) и ВОЗВРАЩАЕТ её + * адрес. Указатель спрайта — в записи по смещению +2. Лимит спрайтов + * на кадр — _GFX_YSORT_MAX; больше — sprite_update рисует БЕЗ + * сортировки (фолбэк). + * Подключение — указателем _spr_ysort_fn (NULL = выкл, дефолт; ставит + * gfx_sprite_ysort, обнуляя _spr_ysort_last — форс rebuild) — + * программа без вызова сеттера не линкует ни код сортировщика, ни + * таблицу (funcptr-DCE, как _spr_tick_fn). + * common/_spr_ysort.c, _spr_ysort_fn.c, gfx_sprite_ysort.c. */ +#define _GFX_YSORT_MAX 32u +uint8_t *_spr_ysort(void *arr, uint16_t count); +extern uint8_t *(*_spr_ysort_fn)(void *arr, uint16_t count); +extern void *_spr_ysort_last; /* arr прошлого кадра (сеттер сбрасывает) */ +extern uint8_t _spr_ysort_kmask[2]; /* маски ключа: [0] y, [1] layer (0/FF) */ + /* ---- Атласы в страницах W0 (sprite.h, §9в дизайна) ---------------- * * _gfx_w0_dss — страница ядра DSS (снимается при первом map/load); * _gfx_w0_cur — какая спрайт-страница должна стоять в W0 (читает diff --git a/libbgi/common/_gfx_blit_sprite.c b/libbgi/common/_gfx_blit_sprite.c index 298e1b5..953afb8 100644 --- a/libbgi/common/_gfx_blit_sprite.c +++ b/libbgi/common/_gfx_blit_sprite.c @@ -6,66 +6,34 @@ * и смены банка — вызывающий (sprite_update/putsprite) держит скобку и * банк 0x5C. Проверок размера НЕТ (контракт ≤64 — в доке, не в коде). * + * src — ГОТОВЫЙ адрес первого пикселя кадра, stride — шаг строки + * источника (полная ширина img/ленты); считает вызывающий один раз + * (кэш sprite_t.src/stride; putsprite — из заголовка). Заголовок + * getimage и (sx,sy) ядру больше не передаются (2026-07-14) — весь + * пересчёт адреса кадра ушёл в редкие точки его смены (sprite_frame, + * тикер анимации инкрементально). + * * Клип по ЭКРАНУ остаётся (это функциональность — спрайты штатно уходят - * за края): fast-path «целиком на экране» = 4 сравнения. - * - * ФОРМАТ getimage и параметры атласа - * ---------------------------------- - * img указывает на getimage-буфер: 4-байтовый заголовок + пиксели. - * Заголовок = uint16 width, uint16 height (little-endian): - * p[0]=width lo, p[1]=width hi, p[2]=height lo, p[3]=height hi. - * Дальше (p+4) — пиксели ПОСТРОЧНО, по width байт на строку. - * - * int img_w = p[0] | (p[1] << 8); - * — это ПОЛНАЯ ширина картинки/ленты-атласа (склейка двух байтов - * заголовка в 16-бит). Используется как ШАГ между строками источника - * (sstride): чтобы спуститься на строку ниже в буфере, надо прибавить к - * адресу целую ширину картинки. Для одиночного спрайта img_w == ширине - * кадра; для ЛЕНТЫ-АТЛАСА (много кадров в ряд) img_w — ширина всей ленты - * (> ширины кадра w), поэтому img_w остаётся int (лента может быть шире - * 64, хотя КАДР ≤ 64). - * - * (sx, sy) — смещение КАДРА (под-прямоугольника) ВНУТРИ картинки: - * левый-верхний угол вырезаемого кадра в пикселях. Для целого спрайта - * sx = sy = 0. Для кадра N горизонтальной ленты: sx = N*FRAME_W, sy = 0 - * (вертикальные атласы — sy). Отсюда адрес первого пикселя кадра: - * src = p + 4 (пропустить заголовок) - * + sx (сместиться на sx пикселей вправо в первой строке) - * + sy*img_w (спуститься на sy строк — цикл сложений ниже, без - * умножения: обычно sy == 0). + * за края): fast-path «целиком на экране» = 4 сравнения; при выходе + * корректируются края И src (левый — src += -x; верхний — src += + * (-y)*stride, одно 16-бит умножение ТОЛЬКО на реально клипнутом + * кадре — редкость рядом с кадровым циклом). */ #include "../_bgi.h" -void _gfx_blit_sprite(int x, int y, const void *img, - int sx, int sy, uint8_t w, uint8_t h) +void _gfx_blit_sprite(int x, int y, const uint8_t *src, + uint16_t stride, uint8_t w, uint8_t h) { - const uint8_t *p = (const uint8_t *)img; - const uint8_t *src; - uint16_t dst; - int img_w = p[0] | (p[1] << 8); /* ширина картинки/ленты = sstride */ int cw = w, ch = h; /* рабочие w,h кадра (для клипа) */ if (x < 0 || y < 0 || x + cw > _bgi_maxx + 1 || y + ch > _bgi_maxy + 1) { - if (x < 0) { sx -= x; cw += x; x = 0; } - if (y < 0) { sy -= y; ch += y; y = 0; } + if (x < 0) { src += -x; cw += x; x = 0; } + if (y < 0) { src += (uint16_t)(-y) * stride; ch += y; y = 0; } if (x + cw > _bgi_maxx + 1) cw = _bgi_maxx + 1 - x; if (y + ch > _bgi_maxy + 1) ch = _bgi_maxy + 1 - y; if (cw <= 0 || ch <= 0) return; } - - /* Адрес первого пикселя кадра: заголовок(4) + sx вправо + sy строк - * вниз. sy*img_w умножением (__mulint, O(1) ~300Т): вертикальные - * ленты атласов сделали sy != 0 нормой — цикл сложений был O(sy), - * до ~64К тактов на блит кадра 11 (профиль rpgwalk 2026-07-13). - * if (sy): горизонтальные ленты/одиночные спрайты не платят и за - * умножение. */ - src = p + 4 + sx; - if (sy) - src += (uint16_t)sy * (uint16_t)img_w; - - dst = _gfx_addr_base + x; - - _bgi_blit_rows_raw(src, (uint8_t *)dst, (uint8_t)cw, (uint8_t)ch, - img_w, (uint8_t)y); + _bgi_blit_rows_raw(src, (uint8_t *)(_gfx_addr_base + x), + (uint8_t)cw, (uint8_t)ch, (int)stride, (uint8_t)y); } diff --git a/libbgi/common/_gfx_blit_sprite_noclip.c b/libbgi/common/_gfx_blit_sprite_noclip.c index c87e56b..d3de5b5 100644 --- a/libbgi/common/_gfx_blit_sprite_noclip.c +++ b/libbgi/common/_gfx_blit_sprite_noclip.c @@ -11,27 +11,20 @@ * заворачивается наверх, x за 320 — в соседнюю строку/страницу; см. * tests/spriteclip). Вызывать только когда спрайт заведомо в пределах. * - * Формат img/img_w/sx/sy — как в _gfx_blit_sprite (getimage-заголовок + - * лента-атлас). БЕЗ _bgi_begin/_bgi_end и смены банка (держит вызывающий). + * src — ГОТОВЫЙ адрес первого пикселя кадра, stride — шаг строки + * источника (полная ширина img); считает вызывающий один раз (кэш + * sprite_t.src/stride ведут sprite_frame и тикер; putsprite — из + * заголовка). До 2026-07-14 здесь были img+sx/sy с пересчётом адреса + * на КАЖДЫЙ блит (цикл O(sy), потом __mulint) — профиль rpgwalk снёс + * весь пересчёт в редкие точки смены кадра. + * + * БЕЗ _bgi_begin/_bgi_end и смены банка (держит вызывающий). */ #include "../_bgi.h" -void _gfx_blit_sprite_noclip(int x, int y, const void *img, - int sx, int sy, uint8_t w, uint8_t h) +void _gfx_blit_sprite_noclip(int x, int y, const uint8_t *src, + uint16_t stride, uint8_t w, uint8_t h) { - const uint8_t *p = (const uint8_t *)img; - const uint8_t *src; - int img_w = p[0] | (p[1] << 8); /* ширина картинки/ленты = sstride */ - - /* sy*img_w умножением (__mulint, O(1) ~300Т): вертикальные ленты - * атласов сделали sy != 0 нормой — старый цикл сложений был O(sy), - * до ~64К тактов на блит кадра 11 (профиль rpgwalk 2026-07-13). - * if (sy): горизонтальные ленты/одиночные спрайты (sy == 0) не - * платят и за умножение. */ - src = p + 4 + sx; - if (sy) - src += (uint16_t)sy * (uint16_t)img_w; - _bgi_blit_rows_raw(src, (uint8_t *)(_gfx_addr_base + x), - w, h, img_w, (uint8_t)y); + w, h, (int)stride, (uint8_t)y); } diff --git a/libbgi/common/_spr_ysort.c b/libbgi/common/_spr_ysort.c new file mode 100644 index 0000000..c372d13 --- /dev/null +++ b/libbgi/common/_spr_ysort.c @@ -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; +} diff --git a/libbgi/common/_spr_ysort_fn.c b/libbgi/common/_spr_ysort_fn.c new file mode 100644 index 0000000..0691558 --- /dev/null +++ b/libbgi/common/_spr_ysort_fn.c @@ -0,0 +1,9 @@ +/* + * _spr_ysort_fn — указатель подключения Y-сортировки к sprite_update + * (DCE-паттерн как у _spr_tick_fn): NULL (дефолт, _BSS) — сортировки + * нет и её код НЕ линкуется; gfx_sprite_ysort(1) ставит _spr_ysort. + * Заодно указатель — это и флаг включения (отдельного нет). + */ +#include "../_bgi.h" + +uint8_t *(*_spr_ysort_fn)(void *arr, uint16_t count); diff --git a/libbgi/common/_sprite_tick.c b/libbgi/common/_sprite_tick.c index c1bae9f..2639bce 100644 --- a/libbgi/common/_sprite_tick.c +++ b/libbgi/common/_sprite_tick.c @@ -2,93 +2,384 @@ * _sprite_tick — тикер авто-анимаций (§9г): вызывается из sprite_update * (раз на КАДР, до heal/блит-проходов — dirty должен взводиться раньше). * Кадровая анимация и tween-перемещение НЕЗАВИСИМЫ (идут одновременно); - * тикаются и у скрытых спрайтов («время идёт», ветка дороже). + * тикаются и у скрытых спрайтов («время идёт»). * - * Кадровая: смена кадра = ±an_step к оси ленты (sy; при _SPR_ANIM_HORZ - * — sx) — БЕЗ умножения; wrap цикла — прыжок оси в an_lo (посчитан - * sprite_anim'ом один раз). Маятник разворачивается НА границе (…2,3, - * 2,1,0,1…, без удвоения крайних). ANIM_ONCE замирает на last: - * SPR_ANIM_ON снимается, SPR_ANIM_DONE взводится. + * ГОРЯЧИЙ ПУТЬ (профиль 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'ом один раз. * - * Перемещение: раз в mv_speed кадров — порция ≤mv_step итераций - * классического Брезенхэма (err-аккумулятор: большая ось шагает каждую - * итерацию, меньшая подтягивается — нелинейные 2/3/2/3 сами собой, - * деления нет). Цель достигнута — SPR_MOVE_ON→off, SPR_MOVE_DONE. + * Кадровая: маятник разворачивается НА границе (…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++) { - /* ---- кадровая анимация ---- */ - if ((s->an_flags & SPR_ANIM_ON) && --s->an_timer == 0) { - int *axis = (s->an_flags & _SPR_ANIM_HORZ) ? &s->sx : &s->sy; - uint8_t moved = 1; + uint8_t fl = s->an_flags; - s->an_timer = s->an_speed; - if (!(s->an_flags & _SPR_ANIM_BACK)) { - if (s->an_frame < s->an_last) { - s->an_frame++; - *axis += s->an_step; - } else if (s->an_flags & _SPR_ANIM_ONCE) { - s->an_flags &= (uint8_t)~SPR_ANIM_ON; - s->an_flags |= SPR_ANIM_DONE; - moved = 0; - } else if (s->an_flags & _SPR_ANIM_PP) { - s->an_flags |= _SPR_ANIM_BACK; /* разворот на границе */ - if (s->an_frame > s->an_first) { - s->an_frame--; - *axis -= s->an_step; - } else - moved = 0; /* лента из 1 кадра */ - } else { /* ANIM_LOOP: wrap */ - s->an_frame = s->an_first; - *axis = s->an_lo; - } - } else { /* маятник, назад */ - if (s->an_frame > s->an_first) { - s->an_frame--; - *axis -= s->an_step; - } else { - s->an_flags &= (uint8_t)~_SPR_ANIM_BACK; - if (s->an_frame < s->an_last) { - s->an_frame++; - *axis += s->an_step; - } else - moved = 0; - } - } - if (moved) - s->flags |= _SPR_DIRTY; - } - - /* ---- tween-перемещение ---- */ - if ((s->an_flags & SPR_MOVE_ON) && --s->mv_timer == 0) { - uint8_t n = s->mv_step; - int x = s->x, y = s->y; - - s->mv_timer = s->mv_speed; - while (n--) { - int e2; - if (x == s->mv_tx && y == s->mv_ty) - break; - e2 = s->mv_err * 2; - if (e2 >= s->mv_dy) { s->mv_err += s->mv_dy; x += s->mv_sgnx; } - if (e2 <= s->mv_dx) { s->mv_err += s->mv_dx; y += s->mv_sgny; } - } - if (x != s->x || y != s->y) { - s->x = x; - s->y = y; - s->flags |= _SPR_DIRTY; - } - if (x == s->mv_tx && y == s->mv_ty) { - s->an_flags &= (uint8_t)~SPR_MOVE_ON; - s->an_flags |= SPR_MOVE_DONE; - } - } + /* Типичный спрайт в кадре НЕ дозрел ни до смены кадра, ни до + * шага — держим этот путь коротким: одна проверка флагов + + * декремент(ы) таймера. */ + 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); } } diff --git a/libbgi/common/atlas_sprite_init.c b/libbgi/common/atlas_sprite_init.c index 4d5b470..7bf235a 100644 --- a/libbgi/common/atlas_sprite_init.c +++ b/libbgi/common/atlas_sprite_init.c @@ -13,20 +13,35 @@ void atlas_sprite_init(sprite_t *s, const atlas_t *a, uint8_t idx) { uint8_t saved = _io_page_w3; const uint8_t *e; + uint16_t img; /* W0-адрес шапки ленты */ sprinter_page_w3(a->page); e = (const uint8_t *)(0xC000 + _ATL_DIR_OFF) + (uint16_t)idx * 8; - s->img = (const void *)(e[0] | (uint16_t)(e[1] << 8)); + img = (uint16_t)(e[0] | (e[1] << 8)); + s->img = (const void *)img; s->w = e[2]; /* fw — размер КАДРА */ s->h = e[3]; /* fh */ + /* stride = полная ширина ЛЕНТЫ из её getimage-шапки (каталог хранит + * только размер кадра; лента nx>1 шире кадра). Шапка лежит в + * странице атласа — читаем через ТЕКУЩИЙ W3-маппинг: адрес в W3 = + * 0xC000 + W0-адрес. */ + { + const uint8_t *hdr = (const uint8_t *)(0xC000 + img); + s->stride = (uint16_t)(hdr[0] | (hdr[1] << 8)); + } sprinter_page_w3(saved); + /* src — W0-адрес пикселей кадра (0,0); разыменовывается блитом + * только при подключенной странице (sprite_update мапит сам). */ + s->src = (const uint8_t *)img + 4; + s->x = 0; s->y = 0; s->sx = 0; s->sy = 0; s->flags = 0; s->page = a->page; s->an_flags = 0; /* авто-анимации выключены */ s->an_frame = 0; + s->layer = 0; /* нижний слой Y-сортировки */ s->drawn[0].on = 0; s->drawn[1].on = 0; } diff --git a/libbgi/common/gfx_sprite_ysort.c b/libbgi/common/gfx_sprite_ysort.c new file mode 100644 index 0000000..af5db18 --- /dev/null +++ b/libbgi/common/gfx_sprite_ysort.c @@ -0,0 +1,28 @@ +/* + * gfx_sprite_ysort — включить/выключить сортировку блит-прохода + * sprite_update (painter's algorithm) и выбрать компоненты порядка: + * mode = YSORT_Y | YSORT_LAYER (см. sprite.h). Дефолт 0 (выкл): + * z-order = индекс массива. + * + * Компоненты управляются МАСКАМИ ключа (_spr_ysort_kmask): выключенный + * компонент затирается в 0 при построении ключа — в сортировщике нет + * ни одного ветвления по режиму. + * + * Включение — подключением указателя _spr_ysort_fn (паттерн funcptr-DCE + * как у тикера): только этот модуль ссылается на код сортировщика — + * программа без вызова gfx_sprite_ysort() не линкует ни его, ни таблицу. + */ +#include "../_bgi.h" +#include /* YSORT_* */ + +void gfx_sprite_ysort(uint8_t mode) +{ + if (mode) { + _spr_ysort_kmask[0] = (mode & YSORT_Y) ? 0xFF : 0x00; + _spr_ysort_kmask[1] = (mode & YSORT_LAYER) ? 0xFF : 0x00; + _spr_ysort_fn = _spr_ysort; + _spr_ysort_last = 0; /* форс rebuild: пока было выключено / + * при смене режима порядок мог уехать */ + } else + _spr_ysort_fn = 0; +} diff --git a/libbgi/common/putsprite.c b/libbgi/common/putsprite.c index 30db076..8fb7dab 100644 --- a/libbgi/common/putsprite.c +++ b/libbgi/common/putsprite.c @@ -18,7 +18,10 @@ void putsprite(int x, int y, const void *img) gfx_set_bank(GFX_BANK_SPRITE); _bgi_begin(); - (*_gfx_blit_fn)(x, y, img, 0, 0, p[0], p[2]); + /* Ядро принимает готовый src+stride (кэш-схема 2026-07-14): для + * целой картинки src = пиксели за заголовком, stride = её ширина. */ + (*_gfx_blit_fn)(x, y, p + 4, (uint16_t)(p[0] | (p[1] << 8)), + p[0], p[2]); _bgi_end(); gfx_set_bank(saved); } diff --git a/libbgi/common/sprite_anim.c b/libbgi/common/sprite_anim.c index 41fed06..66761aa 100644 --- a/libbgi/common/sprite_anim.c +++ b/libbgi/common/sprite_anim.c @@ -3,9 +3,18 @@ * вдоль оси ленты (вертикальная; ANIM_HORIZ — вдоль X), смена раз в * speed кадров, mode = ANIM_LOOP / ANIM_PINGPONG / ANIM_ONCE. * - * Всё дорогое считается ЗДЕСЬ один раз (не в тике): шаг оси = размер - * кадра, осевое смещение first — циклом сложений (без __mulint). - * Ставит текущий кадр на first, сбрасывает SPR_ANIM_DONE, подключает + * Всё дорогое считается ЗДЕСЬ один раз, чтобы тикеру осталась только + * 16-бит арифметика указателя: + * an_step — шаг кадра в БАЙТАХ src: fw (гориз. лента: следующий кадр + * правее на ширину кадра) или fh*stride (верт.: ниже на + * fh строк источника); + * an_src0 — готовый адрес кадра first (для wrap ANIM_LOOP: тикер + * прыгает указателем, ничего не пересчитывая). + * Смена кадра тикером = s->src ± an_step; sx/sy тикер НЕ трогает + * (см. sprite.h про кэш src). + * + * Ставит текущий кадр на first (через sprite_frame — единственную + * точку пересчёта (sx,sy)→src), сбрасывает SPR_ANIM_DONE, подключает * тикер (_spr_tick_fn) — до первого вызова sprite_anim/sprite_moveto * код тикера в программу не линкуется. Требование: индексы ≤ 127, * first ≤ last. Move-статусы (SPR_MOVE_*) не трогаются. @@ -17,18 +26,27 @@ void sprite_anim(sprite_t *s, uint8_t first, uint8_t last, uint8_t speed, uint8_t mode) { uint8_t f = s->an_flags & (SPR_MOVE_ON | SPR_MOVE_DONE); - uint8_t step = (mode & ANIM_HORIZ) ? s->w : s->h; + uint8_t step_px = (mode & ANIM_HORIZ) ? s->w : s->h; int lo = 0; uint8_t i; for (i = 0; i < first; i++) - lo += step; /* first*step без умножения */ + lo += step_px; /* first*step без умножения */ if (!speed) speed = 1; - s->an_step = step; - s->an_lo = lo; + /* Ось — на кадр first (sprite_frame пересчитает кэш src + dirty). */ + if (mode & ANIM_HORIZ) + sprite_frame(s, lo, s->sy); + else + sprite_frame(s, s->sx, lo); + + /* Байтовый шаг кадра и адрес first-кадра — всё, что нужно тикеру. */ + s->an_step = (mode & ANIM_HORIZ) + ? s->w + : (uint16_t)s->h * s->stride; + s->an_src0 = s->src; /* src уже стоит на first */ s->an_first = first; s->an_last = last; s->an_frame = first; @@ -37,18 +55,12 @@ void sprite_anim(sprite_t *s, uint8_t first, uint8_t last, f |= SPR_ANIM_ON; if (mode & ANIM_HORIZ) - f |= _SPR_ANIM_HORZ; + f |= _SPR_STRIP_HORZ; if ((mode & 0x03u) == ANIM_PINGPONG) f |= _SPR_ANIM_PP; if ((mode & 0x03u) == ANIM_ONCE) f |= _SPR_ANIM_ONCE; s->an_flags = f; - /* Ось — на кадр first (обычный путь sprite_frame: dirty при смене). */ - if (mode & ANIM_HORIZ) - sprite_frame(s, lo, s->sy); - else - sprite_frame(s, s->sx, lo); - _spr_tick_fn = _sprite_tick; } diff --git a/libbgi/common/sprite_anim_stop.c b/libbgi/common/sprite_anim_stop.c index 97ccb4c..1124ea3 100644 --- a/libbgi/common/sprite_anim_stop.c +++ b/libbgi/common/sprite_anim_stop.c @@ -1,25 +1,32 @@ /* * sprite_anim_stop — остановить кадровую анимацию. frame ≥ 0 — какой * кадр останется на экране (индекс вдоль оси ленты, ≤127); -1 — - * замереть на текущем. Ось кадра считается циклом сложений (стоп — - * редкий вызов, __mulint не тянем). Валидна после sprite_anim - * (an_step/ось настроены им); SPR_ANIM_DONE не трогает. + * замереть на текущем. + * + * Пиксельный шаг оси берётся из размера кадра (w — горизонтальная + * лента, h — вертикальная; an_step НЕ годится — он в БАЙТАХ src), + * смещение кадра — циклом сложений (стоп — редкий вызов). Установка + * кадра — через sprite_frame (единственная точка пересчёта кэша src; + * важно и при frame == an_frame: src мог уехать тикером, а sx/sy — + * нет, sprite_frame сверяет именно src). Валидна после sprite_anim; + * SPR_ANIM_DONE не трогает. */ #include "../_bgi.h" #include void sprite_anim_stop(sprite_t *s, int8_t frame) { - s->an_flags &= (uint8_t)~(SPR_ANIM_ON | _SPR_ANIM_BACK); + s->an_flags &= (uint8_t)~(SPR_ANIM_ON | _SPR_PP_BACK); - if (frame >= 0 && (uint8_t)frame != s->an_frame) { + if (frame >= 0) { + uint8_t step_px = (s->an_flags & _SPR_STRIP_HORZ) ? s->w : s->h; int off = 0; uint8_t i; for (i = 0; i < (uint8_t)frame; i++) - off += s->an_step; + off += step_px; s->an_frame = (uint8_t)frame; - if (s->an_flags & _SPR_ANIM_HORZ) + if (s->an_flags & _SPR_STRIP_HORZ) sprite_frame(s, off, s->sy); else sprite_frame(s, s->sx, off); diff --git a/libbgi/common/sprite_frame.c b/libbgi/common/sprite_frame.c new file mode 100644 index 0000000..947fa13 --- /dev/null +++ b/libbgi/common/sprite_frame.c @@ -0,0 +1,34 @@ +/* + * sprite_frame — сменить кадр атласа: запомнить (sx,sy) и пересчитать + * кэш src = img + 4 + sx + sy*stride (адрес первого пикселя кадра). + * + * Кэш src — то, что реально использует блит (без чтения заголовка и + * умножения на каждый кадр экрана); здесь единственное место, где + * (sx,sy) конвертируются в адрес «дорогим» путём — одно 16-бит + * умножение (__mulint; при sy < 256 он сам идёт коротким 8-итерационным + * путём). Смена кадра — редкая операция рядом с блитом. + * + * Указатель НЕ разыменовывается — для W0-атласов пересчёт безопасен + * без подключения страницы (вся арифметика на кэше stride из init). + * + * Была inline-функцией (только sx/sy+dirty) до появления кэша src + * (2026-07-14, оптимизация B: blit 11.8К → ~10К тактов/спрайт). + */ +#include "../_bgi.h" +#include + +void sprite_frame(sprite_t *s, int sx, int sy) +{ + /* «Кадр не сменился» определяет src, НЕ (sx,sy): тикер анимации + * двигает src, оставляя sx/sy нетронутыми, поэтому старые sx/sy + * могут совпасть с запрошенными при уехавшем src. */ + const uint8_t *src = (const uint8_t *)s->img + 4 + sx + + (uint16_t)sy * s->stride; + + s->sx = sx; + s->sy = sy; + if (s->src != src) { + s->src = src; + s->flags |= _SPR_DIRTY; + } +} diff --git a/libbgi/common/sprite_init.c b/libbgi/common/sprite_init.c index 5b1dc86..d446190 100644 --- a/libbgi/common/sprite_init.c +++ b/libbgi/common/sprite_init.c @@ -3,7 +3,11 @@ * getimage (uint16 w, uint16 h). Спрайт невидим (flags = 0) и нигде не * нарисован (drawn[*].on = 0) — первый sprite_show + sprite_update его * выведет. Для атласа после init перезадать s->w/s->h (размер кадра) и - * s->sx/s->sy. См. docs/sprite-api-design.md §9.1. + * перейти на кадр через sprite_frame. См. docs/sprite-api-design.md §9.1. + * + * Заполняет кэш блита: stride (полная ширина img = шаг строки источника) + * и src (адрес пикселей кадра (0,0) = img+4) — дальше их ведут + * sprite_frame и тикер анимации, блит берёт готовыми. */ #include "../_bgi.h" #include @@ -13,6 +17,8 @@ void sprite_init(sprite_t *s, const void *img) const uint8_t *p = (const uint8_t *)img; s->img = img; + s->stride = (uint16_t)(p[0] | (p[1] << 8)); /* ширина img/ленты */ + s->src = p + 4; /* кадр (0,0) */ s->x = 0; s->y = 0; s->sx = 0; @@ -25,6 +31,7 @@ void sprite_init(sprite_t *s, const void *img) s->page = 0; /* img в обычной памяти (не W0-атлас) */ s->an_flags = 0; /* авто-анимации выключены */ s->an_frame = 0; + s->layer = 0; /* нижний слой Y-сортировки */ s->drawn[0].on = 0; s->drawn[1].on = 0; } diff --git a/libbgi/common/sprite_moveto.c b/libbgi/common/sprite_moveto.c index 3ab2478..360a1f4 100644 --- a/libbgi/common/sprite_moveto.c +++ b/libbgi/common/sprite_moveto.c @@ -1,13 +1,20 @@ /* * sprite_moveto — запустить tween-перемещение к (tx,ty): раз в interval - * кадров тикер делает до max_step итераций Брезенхэма (большая ось — - * итерация = пиксель; меньшая подтягивается err-аккумулятором). - * Пример: (0,0)→(100,50), max_step 5, interval 4 → 20 шагов × 4 кадра - * = 80 кадров (1.6 с), Y идёт 2/3/2/3. + * кадров тикер делает до max_step шагов вдоль ГЛАВНОЙ оси (большая + * |дельта|); минорная ось подтягивается DDA-аккумулятором — нелинейные + * 2/3/2/3 сами собой, деления нет. Пример: (0,0)→(100,50), max_step 5, + * interval 4 → 20 порций × 4 кадра = 80 кадров (1.6 с). * - * Дельты/знаки/аккумулятор считаются здесь один раз. Если цель == - * текущая позиция — сразу SPR_MOVE_DONE (тикер не включается). - * Кадровые статусы (SPR_ANIM_*) не трогаются. + * Всё дорогое (выбор главной оси, |дельты|, знаки) считается здесь один + * раз; тикеру остаётся БЕЗЗНАКОВАЯ арифметика (см. sprite.h: acc/rem — + * на Z80 беззнаковое 16-бит сравнение в разы дешевле знаковых сравнений + * классического Брезенхэма, ради этого и переформулировано 2026-07-14). + * За mv_dmaj шагов главной оси минорная получает ровно mv_dmin шагов + * (аккумулятор со стартом dmaj/2 — симметричное округление), так что + * обе координаты приходят в цель одновременно; «приехали» = mv_rem == 0. + * + * Если цель == текущая позиция — сразу SPR_MOVE_DONE (тикер не + * включается). Кадровые статусы (SPR_ANIM_*) не трогаются. */ #include "../_bgi.h" #include @@ -17,11 +24,10 @@ void sprite_moveto(sprite_t *s, int tx, int ty, { int dx = tx - s->x; int dy = ty - s->y; + int8_t sgnx = 1, sgny = 1; - s->mv_sgnx = 1; - if (dx < 0) { dx = -dx; s->mv_sgnx = -1; } - s->mv_sgny = 1; - if (dy < 0) { dy = -dy; s->mv_sgny = -1; } + if (dx < 0) { dx = -dx; sgnx = -1; } + if (dy < 0) { dy = -dy; sgny = -1; } s->an_flags &= (uint8_t)~SPR_MOVE_DONE; if (dx == 0 && dy == 0) { @@ -29,11 +35,26 @@ void sprite_moveto(sprite_t *s, int tx, int ty, return; } - s->mv_tx = tx; - s->mv_ty = ty; - s->mv_dx = dx; - s->mv_dy = -dy; - s->mv_err = dx - dy; + s->mv_tx = tx; + s->mv_ty = ty; + + /* Главная ось — с большей |дельтой| (при равенстве — X, диагональ + * ровная в обе стороны). */ + if ((uint16_t)dy > (uint16_t)dx) { + s->mv_ymaj = 1; + s->mv_dmaj = (uint16_t)dy; + s->mv_dmin = (uint16_t)dx; + s->mv_sgnmaj = sgny; + s->mv_sgnmin = sgnx; + } else { + s->mv_ymaj = 0; + s->mv_dmaj = (uint16_t)dx; + s->mv_dmin = (uint16_t)dy; + s->mv_sgnmaj = sgnx; + s->mv_sgnmin = sgny; + } + s->mv_rem = s->mv_dmaj; /* шагов до цели */ + s->mv_acc = s->mv_dmaj >> 1; /* симметричное округление minor */ if (!max_step) max_step = 1; if (!interval) interval = 1; diff --git a/libbgi/common/sprite_update.c b/libbgi/common/sprite_update.c index 18073d8..3d6568d 100644 --- a/libbgi/common/sprite_update.c +++ b/libbgi/common/sprite_update.c @@ -33,6 +33,7 @@ void sprite_update(sprite_t *arr, uint8_t count) uint8_t dbit = p ? _SPR_DIRTY1 : _SPR_DIRTY0; uint8_t saved = gfx_get_bank(); uint8_t i; + const uint8_t *rec; /* курсор сорт-таблицы (NULL = без) */ sprite_t *s; /* Проход 0: тик авто-анимаций (§9г) — ДО heal/блита (двигает x/y и @@ -52,18 +53,33 @@ void sprite_update(sprite_t *arr, uint8_t count) } _bgi_end(); + /* Y-сортировка блит-прохода (gfx_sprite_ysort): asm-сортировка + * строит таблицу записей {key,ptr} по ВОЗРАСТАНИЮ y (нижние + * рисуются последними = поверх) и возвращает её адрес; rec — курсор + * по полю ptr записей (+1, шаг 3), NULL = сортировки нет. + * Подключена указателем (funcptr-DCE). Сортируем ПОСЛЕ тика (y уже + * сдвинуты). count больше таблицы — фолбэк на порядок массива. */ + rec = 0; + if (_spr_ysort_fn && count <= _GFX_YSORT_MAX) + rec = (*_spr_ysort_fn)(arr, count) + 2; /* поле ptr записи */ + /* Проход 2: блит ВСЕХ видимых, требующих обновления. Спрайты из * W0-атласов (s->page): подключить страницу перед блитом — один OUT - * на СМЕНУ страницы (спрайты одного атласа подряд = один OUT); - * порядок «_gfx_w0_cur, затем OUT» закрывает гонку с ISR-стабом - * (он восстановит уже новую). heal-проходу страница не нужна - * (img не читается). */ + * на СМЕНУ страницы (спрайты одного атласа подряд = один OUT; при + * Y-сортировке смен может быть больше); порядок «_gfx_w0_cur, + * затем OUT» закрывает гонку с ISR-стабом (он восстановит уже + * новую). heal-проходу страница и порядок не нужны. */ gfx_set_bank(GFX_BANK_SPRITE); _bgi_begin(); { uint8_t mapped = 0; /* 0 = W0 не тронута (там ядро DSS) */ - for (i = 0, s = arr; i < count; i++, s++) { + for (i = 0; i < count; i++) { + if (rec) { + s = *(sprite_t * const *)rec; /* Y-порядок */ + rec += 4; + } else + s = arr + i; /* z = индекс массива */ if (s->flags & (dbit | SPR_ALWAYS)) { if (s->flags & SPR_VISIBLE) { if (s->page != mapped && s->page) { @@ -71,7 +87,9 @@ void sprite_update(sprite_t *arr, uint8_t count) _io_page_w0 = s->page; mapped = s->page; } - (*_gfx_blit_fn)(s->x, s->y, s->img, s->sx, s->sy, s->w, s->h); + /* src/stride — готовый кэш (sprite_frame/тикер), + * ядро не считает адрес кадра вообще. */ + (*_gfx_blit_fn)(s->x, s->y, s->src, s->stride, s->w, s->h); s->drawn[p].x = s->x; s->drawn[p].y = s->y; s->drawn[p].on = 1; diff --git a/libbgi/include/sprite.h b/libbgi/include/sprite.h index a99318f..a3df7e1 100644 --- a/libbgi/include/sprite.h +++ b/libbgi/include/sprite.h @@ -55,10 +55,14 @@ #define SPR_ANIM_DONE 0x02u /* one-shot дошёл до last */ #define SPR_MOVE_ON 0x04u /* tween-перемещение идёт */ #define SPR_MOVE_DONE 0x08u /* перемещение достигло цели */ -#define _SPR_ANIM_BACK 0x10u /* внутр.: маятник идёт назад */ -#define _SPR_ANIM_HORZ 0x20u /* внутр.: лента горизонтальная (ось sx) */ -#define _SPR_ANIM_PP 0x40u /* внутр.: режим маятника */ -#define _SPR_ANIM_ONCE 0x80u /* внутр.: режим one-shot */ + +/* Внутренние биты an_flags (владеет движок) — три РАЗНЫЕ категории: + * режим анимации (зеркало параметра mode), свойство ЛЕНТЫ (ориентация + * данных в атласе) и runtime-состояние (меняется само в процессе). */ +#define _SPR_PP_BACK 0x10u /* runtime: маятник сейчас идёт назад */ +#define _SPR_STRIP_HORZ 0x20u /* лента: кадры вдоль X (двигается sx) */ +#define _SPR_ANIM_PP 0x40u /* режим: маятник */ +#define _SPR_ANIM_ONCE 0x80u /* режим: one-shot */ /* Режимы sprite_anim(). */ #define ANIM_LOOP 0u /* 0,1,..,last,first,.. (цикл) */ @@ -66,10 +70,24 @@ #define ANIM_ONCE 2u /* first..last, замереть на last + DONE */ #define ANIM_HORIZ 0x80u /* ИЛИ-флаг: кадры вдоль X (сетка nx>1) */ +/* ВНИМАНИЕ: порядок полей ЗАШИТ в asm-ядро тикера (офсеты полей в + * common/_sprite_tick.c, tick_move) — при изменении порядка/типов + * пересчитать офсеты там (offsetof-пробником, рецепт в его шапке). */ typedef struct { const void *img; /* getimage-формат / атлас-лента (вне W3) */ + /* --- кэш адреса кадра (владеет движок; менять ТОЛЬКО через + * sprite_frame) --- + * src — ГОТОВЫЙ адрес первого пикселя ТЕКУЩЕГО кадра (img + 4 + + * sx + sy*stride). Блит берёт его как есть: ни чтения заголовка, + * ни умножения в кадровом цикле. Тикер анимации двигает src + * инкрементально (±an_step байт); sx/sy при этом НЕ обновляются — + * они «последняя позиция, установленная sprite_frame», и снова + * становятся истиной при следующем sprite_frame/sprite_anim + * (те ставят ось абсолютно, а не относительно). */ + const uint8_t *src; /* адрес пикселей текущего кадра */ + uint16_t stride; /* полная ширина img в байтах (шаг строки) */ int x, y; /* ЛОГИЧЕСКАЯ позиция левого-верхнего угла кадра */ - int sx, sy; /* смещение кадра внутри img (атлас; 0 = целая) */ + int sx, sy; /* смещение кадра, заданное sprite_frame (см. src)*/ uint8_t w, h; /* размер кадра (кэш заголовка), 1..255 */ uint8_t flags; /* SPR_* | внутренние dirty */ uint8_t page; /* 0 = img в обычной памяти; иначе физ. страница * @@ -81,23 +99,40 @@ typedef struct { uint8_t an_frame; /* ТЕКУЩИЙ индекс кадра (0-based по ленте) */ uint8_t an_first, an_last; /* границы анимации (индексы) */ uint8_t an_speed, an_timer; /* кадров между сменами / счётчик */ - uint8_t an_step; /* шаг оси = fh (верт. лента) или fw (гориз.) */ - int an_lo; /* осевое смещение кадра first (для wrap) */ + uint16_t an_step; /* шаг кадра в БАЙТАХ src: fw (гориз. лента) * + * или fh*stride (верт.) — считает sprite_anim */ + const uint8_t *an_src0; /* src кадра first (для wrap цикла) */ uint8_t mv_speed, mv_timer; /* кадров между шагами / счётчик */ uint8_t mv_step; /* макс. пикселей вдоль большей оси за шаг */ - int mv_tx, mv_ty; /* цель */ - int mv_dx, mv_dy, mv_err;/* Брезенхэм: dx, -dy, аккумулятор */ - int8_t mv_sgnx, mv_sgny; /* знаки шага осей */ + int mv_tx, mv_ty; /* цель (для sprite_move_stop) */ + /* DDA по главной оси (беззнаковый — на Z80 16-бит беззнаковые + * сравнения в разы дешевле знаковых Брезенхэма, 2026-07-14): + * главная ось шагает каждую итерацию, минорная подтягивается + * аккумулятором; «приехали» = mv_rem == 0 (сравнений координат нет). */ + uint16_t mv_rem; /* осталось шагов главной оси до цели */ + uint16_t mv_acc; /* аккумулятор минорной оси (старт dmaj/2) */ + uint16_t mv_dmaj, mv_dmin; /* |дельты| главной / минорной осей */ + uint8_t mv_ymaj; /* 1 = главная ось Y (иначе X) */ + int8_t mv_sgnmaj, mv_sgnmin;/* знаки шага главной/минорной осей */ /* --- внутреннее (владеет движок) --- */ struct { int x, y; /* где нарисован на странице p */ uint8_t on; /* нарисован ли на странице p */ } drawn[2]; + /* Слой для Y-сортировки (см. gfx_sprite_ysort): при включённой + * сортировке спрайты упорядочиваются по {layer, y} — слой старше: + * слой 1 ВСЕГДА поверх слоя 0 (ходячие/летающие/курсор...), внутри + * слоя — painter по y. 0 после sprite_init; без сортировки не + * используется (порядок = индекс массива). Поле В КОНЦЕ структуры: + * офсеты полей выше зашиты в asm тикера/сортировщика. */ + uint8_t layer; } sprite_t; -/* Инициализация: w,h из заголовка img, невидим, ничего не нарисовано. - * Для АТЛАСА (кадр — под-прямоугольник) после init выставить s->w/s->h - * в размер кадра и s->sx/s->sy — на нужный кадр (или sprite_frame). */ +/* Инициализация: w,h из заголовка img, кадр (0,0), невидим, ничего не + * нарисовано. Для АТЛАСА (кадр — под-прямоугольник) после init + * выставить s->w/s->h в размер кадра и перейти на нужный кадр ТОЛЬКО + * через sprite_frame (он ведёт кэш src; голые s->sx/s->sy кэш не + * обновят). */ void sprite_init(sprite_t *s, const void *img); /* Кадр всей сцены: на ТЕКУЩЕЙ draw-странице (gfx_get_draw_page) @@ -105,7 +140,16 @@ void sprite_init(sprite_t *s, const void *img); * скобка/банк на проход). Спрайты без изменений с прошлого визита этой * страницы (и без SPR_ALWAYS) не трогаются. Для дабл-буфера звать после * gfx_set_draw_page(hidden); для одиночной страницы/курсора — прямо на - * видимой. */ + * видимой. + * + * ВСЯ СЦЕНА СТРАНИЦЫ — ОДИН ВЫЗОВ! Два sprite_update на одну страницу + * (например «сначала ходячие, потом летающие») ЛОМАЮТ перекрытия МЕЖДУ + * группами: heal второй группы берёт из ОЗУ-копии чистый ФОН (спрайты + * в копию не пишутся — банк 0x5C) и стирает нарисованных первой + * группой; двухпроходная дисциплина «heal все → блит все» покрывает + * перекрытия только ВНУТРИ одного вызова. Логические группы держать + * диапазонами индексов одного массива (z-порядок групп = порядок + * диапазонов; слоение при Y-сортировке — полем layer). */ void sprite_update(sprite_t *arr, uint8_t count); /* Сахар канонического дабл-буфер-кадра: draw-страница = скрытая → @@ -114,6 +158,49 @@ void sprite_update(sprite_t *arr, uint8_t count); * sprite_flip — тогда heal учтёт его через ОЗУ-копию. */ void sprite_flip(sprite_t *arr, uint8_t count); +/* Режимы gfx_sprite_ysort (битовая маска компонентов ключа). */ +#define YSORT_OFF 0u /* сортировки нет: z-order = индекс массива */ +#define YSORT_Y 0x01u /* painter по y (нижние поверх верхних) */ +#define YSORT_LAYER 0x02u /* слои: layer 1 поверх layer 0 */ + +/* Y-сортировка блит-прохода (painter's algorithm): mode — какие + * компоненты ключа участвуют в порядке отрисовки: + * YSORT_Y | YSORT_LAYER — полный порядок {layer, y}: сначала слои + * (спрайт слоя 1 ВСЕГДА поверх слоя 0 — ходячие/летающие/ + * курсор...), внутри слоя — по y (нижние по экрану поверх + * верхних, «ближе к зрителю»); + * YSORT_Y — только painter по y, слои игнорируются; + * YSORT_LAYER — только слои; внутри слоя порядок стабилен + * (наследуется от прошлого кадра / порядка массива); + * YSORT_OFF (0, дефолт) — z-order = индекс массива. + * Выключенный компонент маскируется в ключе (2 AND на спрайт, ~1К на + * 15 спрайтов) — сортировщик не замедляется ветвлениями. + * + * ПЕРСИСТЕНТНОСТЬ: таблица порядка живёт МЕЖДУ кадрами — каждый кадр + * ключи обновляются и порядок пересортировывается вставками; спрайты + * двигаются на пиксели за кадр, порядок почти актуален → почти + * линейно. Цена (замер rpgwalk-15, 2026-07-14): ~28К тактов на 15 + * спрайтов (~2К/спрайт, ~6% бюджета кадра; построение с нуля каждый + * кадр стоило 44К). Массив приложения при этом НЕ трогается (sp[2] + * всегда персонаж 2) — сортируются внутренние записи-указатели. При + * смене arr/count (другая сцена) таблица перестраивается сама; первый + * кадр после включения/смены — полная сортировка (разово дороже). + * + * Сортировка СТАБИЛЬНА относительно ПРОШЛОГО КАДРА: спрайты с равным + * ключом сохраняют взаимный порядок прошлого кадра (не мерцают), а не + * порядок массива. Лимит: до 32 спрайтов на вызов sprite_update; + * больше — кадр рисуется без сортировки (безопасный фолбэк). + * Выключено = 0 тактов И 0 байт: код сортировщика и таблица не + * линкуются (funcptr-DCE). heal-проход порядка не требует и остаётся + * линейным. + * + * Замечания: (1) перекрывающиеся спрайты обязаны быть dirty ОБА, чтобы + * смена их взаимного порядка отрисовалась — движущиеся грязнятся сами, + * статики помечать sprite_touch (правило то же, что и без сортировки); + * (2) сортировка перемешивает порядок W0-атласов — смен страницы W0 + * может стать больше (один дешёвый OUT на смену). */ +void gfx_sprite_ysort(uint8_t on); + /* --- O(1) изменения состояния (inline: без call-оверхеда в физике) --- */ /* Переместить: меняет только логическую позицию + грязнит обе страницы. */ @@ -121,10 +208,10 @@ inline void sprite_move(sprite_t *s, int x, int y) { if (s->x != x || s->y != y) { s->x = x; s->y = y; s->flags |= _SPR_DIRTY; } } -/* Сменить кадр атласа (под-прямоугольник img). */ -inline void sprite_frame(sprite_t *s, int sx, int sy) { - if (s->sx != sx || s->sy != sy) { s->sx = sx; s->sy = sy; s->flags |= _SPR_DIRTY; } -} +/* Сменить кадр атласа (под-прямоугольник img). Не inline: пересчитывает + * кэш src (одно 16-бит умножение sy*stride) — при АКТИВНОЙ кадровой + * анимации не звать (тикер ведёт src сам). */ +void sprite_frame(sprite_t *s, int sx, int sy); /* Показать (если был скрыт — грязнит обе страницы). */ inline void sprite_show(sprite_t *s) { diff --git a/tests/spranim/spranim.c b/tests/spranim/spranim.c index 9b2efbe..1f2b78e 100644 --- a/tests/spranim/spranim.c +++ b/tests/spranim/spranim.c @@ -7,7 +7,7 @@ * гоняется в текстовом режиме (скобка W3 безвредна). Ожидания: * * T1 LOOP 0..3 speed=2: смена раз в 2 тика: после 8 тиков frame - * снова 0 (0→1→2→3→0), sy вернулся в 0. + * снова 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 ещё стоит, @@ -18,6 +18,9 @@ * 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. @@ -25,10 +28,16 @@ #include #include #include +#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); @@ -53,11 +62,11 @@ int main(void) 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].sy == 16); + 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].sy == 48); + 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].sy == 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 ------------------------------------------------ */ @@ -67,11 +76,11 @@ int main(void) 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].sy == 32); + 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].sy == 16); + ok = ok && (sprite_anim_frame(&sp[0]) == 1 && sp[0].src == FRAME_SRC(1)); printf("T2 PINGPONG: %s\n", ok ? "PASS" : "FAIL"); /* ---- T3: ONCE ---------------------------------------------------- */ @@ -83,7 +92,7 @@ int main(void) (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].sy == 32 && + 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); /* стоит на месте */ @@ -119,7 +128,8 @@ int main(void) 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); + 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); @@ -137,6 +147,62 @@ int main(void) !(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; }