libbgi: спрайты — кэш адреса кадра, DDA+asm тик, Y-сортировка со слоями

Оптимизации A+B (профиль rpgwalk-15: активная часть кадра 410К → 307К
тактов из 430080; лимит спрайтов 16×16 на стабильные 48.8 fps: 14 → ~21):

- (B) sprite_t.src/stride — готовый адрес кадра: считают только
  sprite_frame (теперь функция, одно умножение на СМЕНУ кадра) и тикер
  (±an_step БАЙТ инкрементально); блит-ядра принимают src+stride,
  img/sx/sy из сигнатуры ушли.  Блит 177К → 146К на кадр.
- (A) тик 100К → 37.7К: tween переформулирован Брезенхэм → беззнаковый
  DDA (mv_rem/mv_acc, «приехали» = rem==0 — без знаковых 16-бит
  сравнений), tick_move и tick_anim — ручной asm (SDCC спиллит такие
  функции в IX-фрейм ~100 обращений; C-реструктуризации не помогали —
  проверено кодогеном).  Биты an_flags переименованы по категориям
  (_SPR_STRIP_HORZ, _SPR_PP_BACK).

Y-сортировка (gfx_sprite_ysort, идеи пользователя — 8-бит ключ,
персистентность):

- painter's algorithm по ключу {layer:8, clamp_y:8}; поле
  sprite_t.layer (в КОНЦЕ структуры — asm-офсеты не сдвигает): слои
  сцены в одном массиве/одном sprite_update;
- ПЕРСИСТЕНТНАЯ asm-таблица {key16, ptr16}: resort порядка прошлого
  кадра (почти линейно), rebuild при смене arr/count; массив
  приложения не трогается; ~28К/15 спрайтов (с нуля было 44К);
- компоненты YSORT_Y/YSORT_LAYER отключаемы независимо масками ключа
  (без ветвлений в сортировщике); ВНИМАНИЕ: mode=1 значит Y-only,
  полный порядок = YSORT_Y|YSORT_LAYER;
- funcptr-DCE: выключено = код и таблица не линкуются (rpgwalk −190 Б);
- ПРАВИЛО в sprite.h: два sprite_update на страницу запрещены (heal
  второй группы стирает спрайты первой — ОЗУ-копия чистая).

Попутные фиксы:

- libbgi/Makefile: .rel зависят от заголовков (HDRS) — stale .rel со
  старой раскладкой sprite_t молча ломал рантайм;
- rpgprof: --memory small (перерос tiny: BSS вылезал за W2 → мгновенный
  «Unexpected application termination»; mkexe это пока не ловит);
- tests/spranim: проверки переведены на кэш src, добавлены T6 (reframe
  после тикера) и T7 (Y-сортировка: порядок, слои, персистентный
  resort, LAYER-only) — 7/7 PASS в MAME.

Доки: §9д — новый бюджет (19.5К/спрайт), §9е — ПЛАН FPS-делителя
(frame pacing, gfx_set_fps_div); TODO — дизайн цепочки irq-обработчиков.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 17:19:24 +03:00
parent 0eec977630
commit b9ddce8d34
24 changed files with 1339 additions and 304 deletions
+28 -9
View File
@@ -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
+5 -4
View File
@@ -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
1 # Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
4 assrtest
5 atlas
6 attrprob
7 balls
8 banked
9 bankedbg
10 banklocl
41 openenv
42 palfile
43 ptime
44 rpgwalk rpgprof
45 rpgwalk
46 rt_test
47 seek
48 simple
49 solidt
50 space
51 spranim
52 spriteclip
53 sprites
54 stattest
+157 -15
View File
@@ -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)
+19 -1
View File
@@ -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
+81 -36
View File
@@ -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);
}
+18 -15
View File
@@ -24,7 +24,7 @@
#include <time.h>
#include <stdlib.h>
#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;
+10 -4
View File
@@ -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 $@ $<
+27 -6
View File
@@ -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 (читает
+17 -49
View File
@@ -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);
}
+11 -18
View File
@@ -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);
}
+221
View File
@@ -0,0 +1,221 @@
/*
* _spr_ysort — ПЕРСИСТЕНТНАЯ таблица порядка отрисовки спрайтов для
* блит-прохода sprite_update (gfx_sprite_ysort): 4-байтные записи
* {key16, ptr16}, отсортированные вставками по возрастанию ключа.
* Возвращает адрес таблицы.
*
* КЛЮЧ = {layer, y}: старший байт — слой спрайта (слой 1 всегда поверх
* слоя 0 — ходячие/летающие/курсор), младший — clamp(y, 0, 255)
* (идея пользователя: экран 256 строк — вся видимая зона сортируется
* точно, за краями порядок неважен, перекрытие невидимо). Компоненты
* отключаемы независимо (YSORT_Y/YSORT_LAYER): маски _spr_ysort_kmask
* затирают выключенный байт ключа — без ветвлений в горячем цикле.
*
* ПЕРСИСТЕНТНОСТЬ (идея пользователя): таблица живёт МЕЖДУ кадрами.
* Каждый вызов — «resort»: для каждой записи ключ пересчитывается по
* её спрайту и запись вставляется в отсортированный префикс. Спрайты
* двигаются на пиксели за кадр → порядок прошлого кадра почти актуален
* → вставки вырождаются в одно сравнение с соседом (почти линейно).
* Смена сцены (arr/count != прошлых, см. _spr_ysort_last/_lastn) —
* rebuild: заполнить ptr-поля из массива и прогнать тот же resort
* (первый кадр — полная сортировка, разово дороже). МАССИВ ПРИЛОЖЕНИЯ
* НЕ ТРОГАЕТСЯ — сортируются только внутренние записи-указатели.
*
* Стабильность: равные ключи сохраняют порядок ПРОШЛОГО КАДРА (спрайты
* на одной высоте не мерцают взаимным порядком, пока реально не
* разойдутся).
*
* C-эквивалент:
* if (arr != last || count != lastn) { // новая сцена
* for (i = 0; i < count; i++) tab[i].ptr = &arr[i];
* last = arr; lastn = count;
* }
* for (i = 0; i < count; i++) { // resort
* p = tab[i].ptr;
* key = (p->layer << 8) | clamp8(p->y);
* for (pos = i; pos && tab[pos-1].key > key; pos--)
* tab[pos] = tab[pos-1];
* tab[pos] = {key, p};
* }
* return tab;
*
* Офсеты: y = +8/9, layer = +56 в sprite_t, sizeof(sprite_t) = 57 —
* сверять пробником при правке структуры (рецепт в шапке tick_move).
* Вход __sdcccall(1): arr → HL, count → DE (используется E; count
* ограничен _GFX_YSORT_MAX вызывающим); ВОЗВРАТ — адрес таблицы (DE).
* IX callee-saved.
*/
#include "../_bgi.h"
/* Данные модуля (без вызова gfx_sprite_ysort не линкуются вместе с
* кодом). _spr_ysort_last обнуляет сеттер при включении — форсирует
* rebuild. НЕ инициализировать (crt0 зануляет). */
uint8_t _gfx_ysort_tab[4u * _GFX_YSORT_MAX];
void *_spr_ysort_last; /* arr прошлого кадра */
uint8_t _spr_ysort_lastn; /* count прошлого кадра */
uint8_t _spr_ysort_kmask[2]; /* маски ключа (сеттер): *
* [0] & y, [1] & layer */
uint8_t *_spr_ysort(void *arr, uint16_t count) __naked
{
(void)arr; (void)count;
__asm
push ix
ld a, e
or a, a
jr NZ, ys_have_sprites
;; count == 0: вернуть таблицу как есть (сцену не запоминаем)
ld de, #__gfx_ysort_tab
pop ix
ret
ys_have_sprites:
;; ---- та же сцена, что кадр назад? (arr и count совпадают) ----
ld a, (__spr_ysort_lastn)
cp a, e
jr NZ, ys_rebuild
ld bc, (__spr_ysort_last)
ld a, c
cp a, l
jr NZ, ys_rebuild
ld a, b
cp a, h
jr Z, ys_resort_entry ; сцена та же только resort
ys_rebuild:
;; ---- новая сцена: запомнить и заполнить ptr-поля записей ----
ld (__spr_ysort_last), hl
ld a, e
ld (__spr_ysort_lastn), a
ld b, a ; B = count
ld de, #__gfx_ysort_tab + 2 ; DE = &tab[0].ptr
ys_fill:
ld a, l
ld (de), a
inc de
ld a, h
ld (de), a
inc de
inc de
inc de ; DE = &tab[i+1].ptr
ld a, l ; cur += sizeof(sprite_t) = 57
add a, #57
ld l, a
jr NC, ys_fill_nc
inc h
ys_fill_nc:
djnz ys_fill
ys_resort_entry:
;; ---- resort: обновить ключи и пересортировать вставками ----
ld a, (__spr_ysort_lastn)
ld b, a ; B = счётчик записей
ld hl, #__gfx_ysort_tab ; HL = R (текущая запись)
ys_next_rec:
push bc ; [счётчик]
push hl ; [R]
;; BC = P (спрайт записи)
inc hl
inc hl
ld c, (hl)
inc hl
ld b, (hl)
;; ---- ключ: E = clamp8(P->y), D = P->layer ----
ld hl, #8
add hl, bc ; HL = &P->y
ld e, (hl)
inc hl
ld a, (hl) ; y hi
or a, a
jr Z, ys_key_y_done ; y в 0..255 точный ключ
ld e, #0
bit 7, a
jr NZ, ys_key_y_done ; y < 0 0
ld e, #255 ; y > 255 255
ys_key_y_done:
ld hl, #56
add hl, bc ; HL = &P->layer
ld d, (hl) ; key = layer:8 | y:8 (DE)
;; маски компонентов (YSORT_Y/YSORT_LAYER): выключенный в 0
ld a, (__spr_ysort_kmask + 0)
and a, e
ld e, a
ld a, (__spr_ysort_kmask + 1)
and a, d
ld d, a
;; стек к скану: [счётчик][R][P], HL = pos = R
pop hl ; R
push hl
push bc ; P
ys_scan:
;; pos == tab? место в голове
ld a, l
cp a, #<(__gfx_ysort_tab)
jr NZ, ys_cmp_prev
ld a, h
cp a, #>(__gfx_ysort_tab)
jr Z, ys_place
ys_cmp_prev:
;; key < prev.key (prev = pos-4)? сдвиг prev вправо и шаг
;; влево; key >= prev.key (включая равные стабильность)
;; место найдено.
push hl
ld a, l
sub a, #4
ld l, a
jr NC, ys_prev_nc
dec h
ys_prev_nc: ; HL = prev
ld a, e
sub a, (hl) ; key.lo - prev.lo
inc hl
ld a, d
sbc a, (hl) ; CF = key < prev.key (16-бит)
pop hl ; HL = pos
jr NC, ys_place
;; сдвиг: копировать prev pos (4 байта), pos = prev
push de ; key
ld d, h
ld e, l ; DE = pos (dst)
ld a, l
sub a, #4
ld l, a
jr NC, ys_shift_nc
dec h
ys_shift_nc: ; HL = prev (src)
push hl ; prev станет новым pos
ld bc, #4
ldir ; prev pos, 4 байта
pop hl ; pos = prev
pop de ; key
jr ys_scan
ys_place:
;; записать {key, P} в pos (HL)
ld (hl), e
inc hl
ld (hl), d
inc hl
pop bc ; P
ld (hl), c
inc hl
ld (hl), b
;; продвинуть R и счётчик
pop hl ; R
ld a, l
add a, #4
ld l, a
jr NC, ys_adv_nc
inc h
ys_adv_nc:
pop bc ; счётчик
dec b
jp NZ, ys_next_rec
ld de, #__gfx_ysort_tab ; вернуть адрес таблицы (ptr в DE)
pop ix
ret
__endasm;
}
+9
View File
@@ -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);
+366 -75
View File
@@ -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 <sprite.h>
/* Одна смена кадра кадровой анимации (таймер уже дозрел).
*
* РУЧНОЙ ASM (2026-07-14): та же болезнь SDCC, что у tick_move —
* C-версия спиллилась в IX-фрейм (~120 обращений, ~4-5К тактов на
* вызов); asm ~1К. При медленных анимациях (speed ≥ 5) вклад мал,
* но при speed 1-2 tick_anim зовётся на каждый кадр для каждого
* спрайта и становился главной статьёй тика.
*
* C-эквивалент (семантика 1:1, тест tests/spranim T1/T2/T3/T6):
*
* s->an_timer = s->an_speed;
* if (!(fl & PP_BACK)) { // вперёд
* if (fr < last) { fr++; src += step; }
* else if (fl & ONCE) { ON→off, DONE→on; return; } // не грязнить
* else if (fl & PP) { BACK→on; // разворот на границе:
* if (fr > first) { fr--; src -= step; } else нет-шага; }
* else ..LOOP.. { fr = first; src = an_src0; }
* } else { // маятник, обратно
* if (fr > first) { fr--; src -= step; }
* else { BACK→off; // разворот на first
* if (fr < last) { fr++; src += step; } else нет-шага; }
* }
* если шаг был: an_frame = fr; s->src = src; flags |= DIRTY;
*
* Офсеты полей sprite_t (см. пробник в шапке tick_move):
* src=2 flags=16 an_flags=18 an_frame=19 an_first=20 an_last=21
* an_speed=22 an_timer=23 an_step=24 an_src0=26
* Биты an_flags: ON=0x01 DONE=0x02 PP_BACK=0x10 PP=0x40 ONCE=0x80.
* Вход __sdcccall(1): s → HL. IX callee-saved.
*/
static void tick_anim(sprite_t *s) __naked
{
(void)s;
__asm
push ix
push hl
pop ix ; IX = s
;; an_timer = an_speed перезарядить темп
ld a, 22 (ix)
ld 23 (ix), a
ld b, 19 (ix) ; B = fr (текущий кадр)
ld c, 18 (ix) ; C = an_flags
bit 4, c ; _SPR_PP_BACK?
jr NZ, ta_backward
;; ---- ход вперёд (LOOP/ONCE всегда; маятник фаза «туда») ----
ld a, b
cp a, 21 (ix) ; CF = (fr < an_last)
jr NC, ta_at_last
inc b ; шаг вперёд: fr++
call ta_src_fwd ; src += an_step
jr ta_commit ; commit
ta_at_last:
;; fr == last: конец отрезка по режиму
bit 7, c ; _SPR_ANIM_ONCE?
jr Z, ta_not_once
;; ONCE: замереть на last, отчитаться, НЕ грязнить
ld a, c
and a, #0xFE ; ~SPR_ANIM_ON
or a, #0x02 ; SPR_ANIM_DONE
ld 18 (ix), a
jr ta_done ; выход без dirty
ta_not_once:
bit 6, c ; _SPR_ANIM_PP?
jr Z, ta_loop_wrap
;; маятник: разворот на границе сразу шаг назад
;; (2,3,2, крайний кадр не удваивается)
set 4, c ; _SPR_PP_BACK
ld 18 (ix), c
ld a, 20 (ix) ; an_first
cp a, b ; CF = (first < fr)
jr NC, ta_done ; лента из 1 кадра шага нет
dec b
call ta_src_back ; src -= an_step
jr ta_commit
ta_loop_wrap:
;; LOOP: wrap прыжок на заранее посчитанный first
ld b, 20 (ix) ; fr = an_first
ld l, 26 (ix)
ld h, 27 (ix) ; HL = an_src0
ld 2 (ix), l
ld 3 (ix), h ; src = an_src0
jr ta_commit_fr ; commit (src уже записан)
;; ---- маятник, фаза «обратно» ----
ta_backward:
ld a, 20 (ix) ; an_first
cp a, b ; CF = (first < fr)
jr NC, ta_turn_fwd
dec b ; шаг назад
call ta_src_back ; src -= an_step
jr ta_commit
ta_turn_fwd:
;; на first: разворот сразу шаг вперёд
res 4, c ; снять _SPR_PP_BACK
ld 18 (ix), c
ld a, b
cp a, 21 (ix) ; CF = (fr < an_last)
jr NC, ta_done ; лента из 1 кадра шага нет
inc b
call ta_src_fwd ; src += an_step
;; ---- commit: кадр сменился ----
ta_commit: ; (src записан хелпером)
ta_commit_fr:
ld 19 (ix), b ; an_frame = fr
ld a, 16 (ix)
or a, #0x0C ; flags |= _SPR_DIRTY
ld 16 (ix), a
ta_done:
pop ix
ret
;; ---- хелперы: s->src ± an_step; клоббер A,DE,HL ----
ta_src_fwd: ; src += an_step
ld l, 2 (ix)
ld h, 3 (ix)
ld e, 24 (ix)
ld d, 25 (ix)
add hl, de
ld 2 (ix), l
ld 3 (ix), h
ret
ta_src_back: ; src -= an_step
ld l, 2 (ix)
ld h, 3 (ix)
ld e, 24 (ix)
ld d, 25 (ix)
or a, a ; CF = 0
sbc hl, de
ld 2 (ix), l
ld 3 (ix), h
ret
__endasm;
}
/* Одна порция tween-перемещения (таймер уже дозрел): до mv_step шагов
* главной оси DDA (см. sprite_moveto: вся арифметика беззнаковая,
* «приехали» = mv_rem == 0 — сравнений координат нет вообще).
*
* РУЧНОЙ ASM (2026-07-14): SDCC на этой функции спиллил ВСЁ в IX-фрейм
* (~100 IX-обращений по ~19Т, ~6.5К тактов на вызов при порции в один
* шаг) — и никакие реструктуризации C этого не лечили. asm держит
* горячее в регистрах: ~2К тактов на вызов.
*
* C-эквивалент (семантика 1:1, тест tests/spranim):
*
* uint8_t n = s->mv_step; // порция...
* if (n > s->mv_rem) n = s->mv_rem; // ...не длиннее остатка
* s->mv_rem -= n;
* s->mv_timer = s->mv_speed;
* mins = 0; // шагов минорной оси
* for (k = n; k--; ) { // DDA-аккумулятор
* acc += dmin;
* if (acc >= dmaj) { acc -= dmaj; mins++; }
* }
* major_coord += sgnmaj*n; minor_coord += sgnmin*mins;
* s->flags |= _SPR_DIRTY;
* if (!s->mv_rem) { MOVE_ON→off; MOVE_DONE→on; }
*
* Офсеты полей sprite_t — сверены offsetof-пробником по фактическому
* кодогену SDCC 4.5 (поля пакуются подряд, выравнивания нет):
* x=6 y=8 flags=16 an_flags=18 mv_speed=28 mv_timer=29 mv_step=30
* mv_rem=35 mv_acc=37 mv_dmaj=39 mv_dmin=41 mv_ymaj=43
* mv_sgnmaj=44 mv_sgnmin=45
* ПРИ ИЗМЕНЕНИИ ПОРЯДКА ПОЛЕЙ В sprite.h — ПЕРЕСЧИТАТЬ (см. заметку
* у sprite_t). Вход __sdcccall(1): s → HL. IX callee-saved.
*/
static void tick_move(sprite_t *s) __naked
{
(void)s;
__asm
push ix
push hl
pop ix ; IX = s
;; mv_timer = mv_speed перезарядить темп
ld a, 28 (ix)
ld 29 (ix), a
;; B = n = min(mv_step, mv_rem); HL = rem
ld b, 30 (ix)
ld l, 35 (ix)
ld h, 36 (ix)
ld a, h
or a, a
jr NZ, tm_n_ok ; rem >= 256 заведомо >= n
ld a, l
cp a, b
jr NC, tm_n_ok ; rem >= n порция целиком
ld b, l ; хвост маршрута короче: n = rem
tm_n_ok:
;; rem -= n (заём в старший байт вручную)
ld a, l
sub a, b
ld l, a
jr NC, tm_rem_stored
dec h
tm_rem_stored:
ld 35 (ix), l
ld 36 (ix), h
push bc ; сохранить n (в B) на время цикла
;; ---- DDA минорной оси: n прогонов аккумулятора ----
;; HL = acc, DE = dmin, C = mins (счётчик шагов минорной), B = k
ld l, 37 (ix)
ld h, 38 (ix)
ld e, 41 (ix)
ld d, 42 (ix)
ld c, #0
tm_dda_loop:
add hl, de ; acc += dmin
;; беззнаковое acc >= dmaj?
ld a, l
sub a, 39 (ix)
ld a, h
sbc a, 40 (ix)
jr C, tm_dda_next ; acc < dmaj минорная стоит
;; acc -= dmaj; mins++
ld a, l
sub a, 39 (ix)
ld l, a
ld a, h
sbc a, 40 (ix)
ld h, a
inc c
tm_dda_next:
djnz tm_dda_loop
ld 37 (ix), l
ld 38 (ix), h
ld e, c ; E = mins
pop bc ; B = n
;; ---- применить порцию к координатам ----
;; sgn* = ±1: rlca выносит знак в CF (0xFF CF=1)
ld a, 43 (ix) ; ymaj?
or a, a
jr NZ, tm_ymaj
;; --- главная ось X (поле +6), минорная Y (+8) ---
ld a, 44 (ix) ; sgnmaj
rlca
ld a, b ; A = n
jr NC, tm_xmaj_n_set
neg ; A = -n
tm_xmaj_n_set:
push ix
pop hl
ld bc, #6
add hl, bc ; HL = &x
call tm_add16 ; x += ±n
ld a, e
or a, a
jp Z, tm_mark_dirty ; минорная не шагала (далеко: jp)
ld a, 45 (ix) ; sgnmin
rlca
ld a, e ; A = mins
jr NC, tm_xmaj_min_set
neg
tm_xmaj_min_set:
push ix
pop hl
ld bc, #8
add hl, bc ; HL = &y
call tm_add16 ; y += ±mins
jp tm_mark_dirty
;; --- главная ось Y (+8), минорная X (+6) ---
tm_ymaj:
ld a, 44 (ix) ; sgnmaj
rlca
ld a, b
jr NC, tm_ymaj_n_set
neg
tm_ymaj_n_set:
push ix
pop hl
ld bc, #8
add hl, bc ; HL = &y
call tm_add16 ; y += ±n
ld a, e
or a, a
jr Z, tm_mark_dirty
ld a, 45 (ix) ; sgnmin
rlca
ld a, e
jr NC, tm_ymaj_min_set
neg
tm_ymaj_min_set:
push ix
pop hl
ld bc, #6
add hl, bc ; HL = &x
call tm_add16 ; x += ±mins
tm_mark_dirty:
;; flags |= _SPR_DIRTY (0x0C) порция всегда двигает major
ld a, 16 (ix)
or a, #0x0C
ld 16 (ix), a
;; rem == 0 приехали: MOVE_ON снять, MOVE_DONE взвести
ld a, 35 (ix)
or a, 36 (ix)
jr NZ, tm_done
ld a, 18 (ix)
and a, #0xFB ; ~SPR_MOVE_ON
or a, #0x08 ; SPR_MOVE_DONE
ld 18 (ix), a
tm_done:
pop ix
ret
;; ---- хелпер: *(int16 @HL) += sign_extend(A); клоббер A,BC ----
tm_add16:
ld c, a
add a, a ; CF = знак A
sbc a, a ; A = 0x00 / 0xFF
ld b, a ; BC = знак-расширенная дельта
ld a, (hl)
add a, c
ld (hl), a
inc hl
ld a, (hl)
adc a, b
ld (hl), a
ret
__endasm;
}
void _sprite_tick(void *arr, uint8_t count)
{
sprite_t *s = (sprite_t *)arr;
uint8_t i;
for (i = 0; i < count; i++, s++) {
/* ---- кадровая анимация ---- */
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);
}
}
+16 -1
View File
@@ -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;
}
+28
View File
@@ -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 <sprite.h> /* 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;
}
+4 -1
View File
@@ -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);
}
+26 -14
View File
@@ -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;
}
+14 -7
View File
@@ -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 <sprite.h>
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);
+34
View File
@@ -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 <sprite.h>
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;
}
}
+8 -1
View File
@@ -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 <sprite.h>
@@ -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;
}
+37 -16
View File
@@ -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 <sprite.h>
@@ -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;
+24 -6
View File
@@ -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;
+105 -18
View File
@@ -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) {
+74 -8
View File
@@ -7,7 +7,7 @@
* гоняется в текстовом режиме (скобка W3 безвредна). Ожидания:
*
* T1 LOOP 0..3 speed=2: смена раз в 2 тика: после 8 тиков frame
* снова 0 (01230), sy вернулся в 0.
* снова 0 (01230), 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 <stdio.h>
#include <string.h>
#include <sprite.h>
#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;
}