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, - [ ] **П6/железо**: MAME-смоук всех тестов после libc-сплита (conio,
ptime, stattest, mouse, gfx_demo/gfx_d16/gfx_text/gfx_mous — ptime, stattest, mouse, gfx_demo/gfx_d16/gfx_text/gfx_mous —
трогался asm акселератора); затем прогон на реальном Sprinter трогался asm акселератора); затем прогон на реальном Sprinter
@@ -125,15 +145,14 @@ Quick wins:
putsprite/movesprite в graphics.h; проверено tests/sprites putsprite/movesprite в graphics.h; проверено tests/sprites
(прозрачность, клиппинг 4 краёв, heal src==dst, чистый след (прозрачность, клиппинг 4 краёв, heal src==dst, чистый след
movesprite, атлас). Осталась Фаза C — пример-курсор. movesprite, атлас). Осталась Фаза C — пример-курсор.
- [ ] **managed-движок спрайтов v2** — sprite_t (drawn-координаты - [x] ~~**managed-движок спрайтов v2**~~ — СДЕЛАН 2026-07-12/13
per-page ВНУТРИ структуры) + retained-модель: (sprite.h: retained-модель + batch-пасс, см. пункт batch-пасса
sprite_move/frame/show/hide меняют только state, ниже) и расширен: W0-атласы (.atl, atlas_load/atlas_sprite_init,
sprite_update(arr,n) делает «heal все → блит все» на текущей ISR-стаб — §9в), авто-анимация кадровая + tween (sprite_anim/
draw-странице, sprite_flip — сахар с vsync+флипом; z-order = sprite_moveto/sprite_update тикер — §9г), gfx_pal_sync/fload/
индекс массива. Интегрирует спрайты со сменой страниц — fsave. Замерен бюджет: ~26К тактов на спрайт 16×16, лимит 14
снимает с приложения учёт drawn[2][N] (источник багов, спрайтов на стабильные 48 fps (§9д); демо examples/space,
examples/balls 2026-07-11). Полный эскиз: examples/rpgwalk (+rpgprof — профайлер кадра).
docs/sprite-api-design.md §9.1.
- [ ] `gfx_xor_rect` / `gfx_or_rect` / `gfx_and_rect` / `gfx_invert_rect` - [ ] `gfx_xor_rect` / `gfx_or_rect` / `gfx_and_rect` / `gfx_invert_rect`
- [ ] шрифты ≠ 8×8: gfx_set_font_data(ptr,w,h,advance), proportional, - [ ] шрифты ≠ 8×8: gfx_set_font_data(ptr,w,h,advance), proportional,
8×16/16×16, отдельный font_id API; font-quad для 640×256 8×16/16×16, отдельный font_id API; font-quad для 640×256
+5 -4
View File
@@ -4,7 +4,7 @@ argv 3431
assrtest 3847 assrtest 3847
atlas 9193 atlas 9193
attrprob 4087 attrprob 4087
balls 6249 balls 6031
banked 1056 banked 1056
bankedbg 1067 bankedbg 1067
banklocl 4683 banklocl 4683
@@ -41,13 +41,14 @@ mouse 4382
openenv 6124 openenv 6124
palfile 5406 palfile 5406
ptime 5744 ptime 5744
rpgwalk 9683 rpgprof 11852
rpgwalk 9501
rt_test 4892 rt_test 4892
seek 4188 seek 4188
simple 955 simple 955
solidt 11531 solidt 11531
space 10976 space 11272
spranim 8752 spranim 9431
spriteclip 4119 spriteclip 4119
sprites 6791 sprites 6791
stattest 7549 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). watchpoint на OUT-маркеры + printf/g + clog (memory/mame_mcp_bridge).
Цена анимированного спрайта 16×16 в sprite_update (heal+blit на Цена анимированного спрайта 16×16 в sprite_update (heal+blit на
кадр, W0-атлас, noclip): кадр, W0-атлас, noclip) — два среза: до и после оптимизаций A+B
2026-07-14 (кэш src/stride в sprite_t — blit не считает адрес кадра;
беззнаковый DDA вместо Брезенхэма + asm-ядро tick_move):
| фаза | тактов/спрайт | | фаза | до A+B | после A+B |
|--------------------|---------------| |--------------------|---------------|---------------|
| тик (anim+tween) | ~7.3К (4.3-9.2К) | | тик (anim+tween) | ~7.3К | **~3.0К** |
| heal (банк 0x50) | ~6.4К | | heal (банк 0x50) | ~6.4К | ~6.4К |
| blit (банк 0x5C) | ~11.8К | | blit (банк 0x5C) | ~11.8К | **~9.7К** |
| **итого** | **~26К** | | **итого** | **~26К** | **~19.5К** |
Фикс-часть цикла демо (kbhit+getdatetime+свопы страниц) ~14-19К. Фикс-часть цикла демо (kbhit+getdatetime+свопы страниц) ~15-20К.
Отсюда лимит на стабильные 48 fps (КАЖДЫЙ кадр в один интервал): Лимит на стабильные 48 fps (КАЖДЫЙ кадр в один интервал): было
**14 спрайтов** (замерено: все обычные кадры ≤406К, 10/232 двойных — **14 спрайтов**, после A+B — расчётно **~21** (15 замерено: активная
только кадры с FPS-плашкой); 15 — 94% кадров, 16 — на грани (50%). часть 307К из 430К, все периоды 1.0). Дорогой HUD ломает бюджет:
bar()+outtextxy() FPS-плашки стоили ~210К (полкадра!) — рисовать
putimage-заготовкой/маленьким bar.
Уроки профиля: Уроки профиля:
- O(sy)-цикл `src += img_w` в blit-обёртках стоил до 64К тактов на - 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К/спрайт, __mulint 2026-07-13 (O(1) ~300Т): blit упал 45К → 11.8К/спрайт,
rpgwalk 24 fps → стабильные 48. rpgwalk 24 fps → стабильные 48.
- bar()+outtextxy() FPS-плашки = ~210К тактов (~10 мс, полкадра!) — - bar()+outtextxy() FPS-плашки = ~210К тактов (~10 мс, полкадра!) —
текст BGI дорогой; на 14 спрайтах кадры с плашкой уходят в два текст BGI дорогой; для HUD в бюджете рисовать заготовленным
интервала. Для HUD в бюджете рисовать заготовленным putimage. putimage/узким bar.
- Резерв: тик в C ~7.3К/спрайт — asm/упрощение подняло бы лимит к - Оптимизации A+B (2026-07-14): (A) тик — SDCC спиллит функции с
~17-18 спрайтам. «многими» 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) ## 10. Риски / что проверить артефактом (Фаза 0)
+19 -1
View File
@@ -1,9 +1,27 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..) PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := rpgwalk EXAMPLE := rpgwalk
EXTRA_FLAGS ?= --gfx 256 --memory tiny 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 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 КБ пикселей — НЕ лезут в одну # 8 персонажей по 12 кадров = 24 КБ пикселей — НЕ лезут в одну
# EMM-страницу (лимит 16К-0x100): два атласа по 4 персонажа. # EMM-страницу (лимит 16К-0x100): два атласа по 4 персонажа.
sprites.stamp: conv_sprites.py $(PROJ_ROOT)/toolchain/mkatlas.py sprites.stamp: conv_sprites.py $(PROJ_ROOT)/toolchain/mkatlas.py
+81 -36
View File
@@ -2,25 +2,27 @@
* rpgprof — профилировочная копия rpgwalk (НЕ собирается по умолчанию). * rpgprof — профилировочная копия rpgwalk (НЕ собирается по умолчанию).
* Два инструмента в одном: * Два инструмента в одном:
* *
* 1. ВИЗУАЛЬНЫЙ профайлер — цвет ГРАНИЦЫ экрана (порт #FE, биты 0-2) * 1. ВИЗУАЛЬНЫЙ профайлер — цвет ГРАНИЦЫ экрана меняется на входе в
* меняется на входе в каждую секцию кадра; луч бежит сверху вниз * каждую секцию кадра; луч бежит сверху вниз (~320 строк растра за
* (~320 строк растра за кадр 48.83 Гц, 1344 такта на строку), так * кадр 48.83 Гц, 1344 такта на строку), так что толщина цветной
* что толщина цветной полосы бордера ∝ времени секции: * полосы бордера ∝ времени секции.
* красный (2) — логика: kbhit, машина маршрутов, getdatetime/FPS * ВНИМАНИЕ: бордер принимает только биты 1-2 порта #FE (бит 0 =
* жёлтый (6) — тик авто-анимаций (_spr_tick_fn) * красный ИГНОРИРУЕТСЯ — проверено 2026-07-14) → доступны РОВНО
* magenta (3) — heal-проход (банк 0x50) * четыре цвета: 0 чёрный, 2 синий, 4 зелёный, 6 голубой:
* белый (7) — blit-проход (банк 0x5C) * синий (2) — логика (короткая) и blit-проход (банк 0x5C)
* синий (1) gfx_wait_vsync (простой до конца кадра) * зелёный (4) — тик авто-анимаций (_spr_tick_fn)
* Много синего = запас; белый/жёлтый через весь экран = не влезаем. * голубой (6) — heal-проход (банк 0x50)
* ВНИМАНИЕ: dev-MAME перерисовывает бордер крупными кусками (не * чёрный (0) — gfx_wait_vsync (простой до конца кадра)
* per-scanline), полосы приблизительные и «пляшут» — для точных * Много чёрного = запас; синий+зелёный через весь бордер = не
* цифр см. п.2. * влезаем. dev-MAME перерисовывает бордер крупными кусками (не
* per-scanline) — полосы приблизительные, для точных цифр см. п.2.
* *
* 2. ТАКТОВЫЙ профайлер — те же OUT'ы ловятся watchpoint'ом дебаггера * 2. ТАКТОВЫЙ профайлер — те же OUT'ы ловятся watchpoint'ом дебаггера
* dev-MAME (рецепт: memory/mame_mcp_bridge и docs/sprite-api-design.md * dev-MAME (рецепт: memory/mame_mcp_bridge и docs/sprite-api-design.md
* §9д): wpiset по маске порта + printf totalcycles/wpdata + авто-g, * §9д): wpiset по маске порта + printf totalcycles/wpdata + авто-g,
* затем оп clog моста. Значения маркеров (2/6/3/7/1) уникальны — * затем оп clog моста. Для уникальности wpdata при одинаковом цвете
* по wpdata видно, какая секция началась. * можно кодировать секцию в битах 3+ (бордер их не видит):
* напр. логика 0x12, blit 0x02 — оба синие, но различимы в логе.
* *
* Сборка и запуск (из этого каталога; атласы уже собраны make'ом): * Сборка и запуск (из этого каталога; атласы уже собраны make'ом):
* ../../bin/sprinter-cc --memory tiny --gfx 256 -o rpgprof.exe rpgprof.c * ../../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 dbit = p ? _SPR_DIRTY1 : _SPR_DIRTY0;
uint8_t saved = gfx_get_bank(); uint8_t saved = gfx_get_bank();
uint8_t i; uint8_t i;
const uint8_t *rec;
sprite_t *s; sprite_t *s;
io_border = 6; /* жёлтый: тик анимаций */ io_border = 4; /* зеленый: тик анимаций */
if (_spr_tick_fn) if (_spr_tick_fn)
(*_spr_tick_fn)(arr, count); (*_spr_tick_fn)(arr, count);
io_border = 3; /* magenta: heal */ io_border = 6; /* голубой: heal */
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
_bgi_begin(); _bgi_begin();
for (i = 0, s = arr; i < count; i++, s++) { 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(); _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); gfx_set_bank(GFX_BANK_SPRITE);
_bgi_begin(); _bgi_begin();
{ {
uint8_t mapped = 0; 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 & (dbit | SPR_ALWAYS)) {
if (s->flags & SPR_VISIBLE) { if (s->flags & SPR_VISIBLE) {
if (s->page != mapped && s->page) { 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; _io_page_w0 = s->page;
mapped = 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].x = s->x;
s->drawn[p].y = s->y; s->drawn[p].y = s->y;
s->drawn[p].on = 1; s->drawn[p].on = 1;
@@ -100,7 +113,7 @@ static void my_update(sprite_t *arr, uint8_t count)
gfx_set_bank(saved); gfx_set_bank(saved);
} }
#define NCHR 8 #define NCHR 15
#define MINX 0 #define MINX 0
#define MAXX (320 - 16) #define MAXX (320 - 16)
#define MINY 14 #define MINY 14
@@ -116,8 +129,10 @@ static uint8_t phase[NCHR];
static void set_dir(uint8_t i, uint8_t d) static void set_dir(uint8_t i, uint8_t d)
{ {
dir[i] = 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), 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) 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; int tx = s->x, ty = s->y, avail, dist;
uint8_t d = dir[i]; 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); set_dir(i, d);
switch (d) { switch (d) {
case 0: avail = MAXY - s->y; dist = QY; break; case 0: avail = MAXY - s->y; dist = QY; break;
@@ -173,18 +188,38 @@ static void turn_90(uint8_t i)
go_edge(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) static void show_fps(uint8_t v)
{ {
char buf[4]; uint8_t color = BLACK;
setfillstyle(SOLID_FILL, color);
buf[0] = (char)('0' + (v / 100) % 10); bar(0, 2, 60, 5);
buf[1] = (char)('0' + (v / 10) % 10); if(v < 30) {
buf[2] = (char)('0' + v % 10); color = RED;
buf[3] = 0; } else if(v < 40) {
setcolor(WHITE); color = BLUE;
setfillstyle(SOLID_FILL, BLACK); } else if(v < 47) {
bar(0, 0, 27, 9); color = MAGENTA;
outtextxy(2, 1, buf); } else if(v < 52) {
color = CYAN;
} else {
color = YELLOW;
}
setfillstyle(SOLID_FILL, color);
bar(0, 2, v, 5);
} }
static void draw_field(void) static void draw_field(void)
@@ -203,6 +238,16 @@ static void draw_field(void)
putpixel(x - 1, y, LIGHTGREEN); putpixel(x - 1, y, LIGHTGREEN);
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); setcolor(WHITE);
outtextxy(52, 3, "RPGPROF: border = frame profile, ESC"); outtextxy(52, 3, "RPGPROF: border = frame profile, ESC");
} }
@@ -251,7 +296,7 @@ int main(void)
last_sec = dt.second; last_sec = dt.second;
for (;;) { for (;;) {
io_border = 2; /* красный: логика */ io_border = 2; /* синий: логика */
if (kbhit() && getch() == 27) if (kbhit() && getch() == 27)
break; break;
@@ -277,8 +322,8 @@ int main(void)
fps_draw--; fps_draw--;
show_fps(fps_val); show_fps(fps_val);
} }
my_update(sp, NCHR); /* маркеры 6/3/7 внутри */ my_update(sp, NCHR); /* маркеры 2/4/6 внутри */
io_border = 1; /* синий: ждём vsync */ io_border = 0; /* черный: ждём vsync */
gfx_wait_vsync(); gfx_wait_vsync();
gfx_set_visible_page(hidden); gfx_set_visible_page(hidden);
} }
+6 -3
View File
@@ -24,7 +24,7 @@
#include <time.h> #include <time.h>
#include <stdlib.h> #include <stdlib.h>
#define NCHR 8 #define NCHR 15
#define MINX 0 #define MINX 0
#define MAXX (320 - 16) #define MAXX (320 - 16)
#define MINY 14 /* под заголовком */ #define MINY 14 /* под заголовком */
@@ -41,8 +41,10 @@ static uint8_t phase[NCHR]; /* 0 = до края, 1 = до случайно
static void set_dir(uint8_t i, uint8_t d) static void set_dir(uint8_t i, uint8_t d)
{ {
dir[i] = 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), 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). */ /* Идти до края экрана вдоль dir[i] (скорость: 1px, интервал 1-2). */
@@ -68,7 +70,8 @@ static void go_back_random(uint8_t i)
int tx = s->x, ty = s->y, avail, dist; int tx = s->x, ty = s->y, avail, dist;
uint8_t d = dir[i]; 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); set_dir(i, d);
switch (d) { switch (d) {
case 0: avail = MAXY - s->y; dist = QY; break; case 0: avail = MAXY - s->y; dist = QY; break;
+10 -4
View File
@@ -28,6 +28,12 @@ BUILD := $(CURDIR)/build
COMMON_C := $(wildcard $(CURDIR)/common/*.c) COMMON_C := $(wildcard $(CURDIR)/common/*.c)
BGI256_C := $(wildcard $(CURDIR)/bgi256/*.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)) 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)) 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)) 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'и выключены) --- # --- Fast version: -DGFX_NOCHECK (guard'и выключены) ---
$(BUILD)/fast/common/%.rel: $(CURDIR)/common/%.c $(BUILD)/fast/common/%.rel: $(CURDIR)/common/%.c $(HDRS)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(SDCC) -mz80 --no-std-crt0 --std-c99 --opt-code-size -DGFX_NOCHECK --max-allocs-per-node 100000 $(INC) -c -o $@ $< $(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 $@) @mkdir -p $(dir $@)
$(SDCC) -mz80 --no-std-crt0 --std-c99 --opt-code-size -DGFX_NOCHECK --max-allocs-per-node 100000 $(INC) -c -o $@ $< $(SDCC) -mz80 --no-std-crt0 --std-c99 --opt-code-size -DGFX_NOCHECK --max-allocs-per-node 100000 $(INC) -c -o $@ $<
# --- Safe version: guard'и включены (по умолчанию) --- # --- Safe version: guard'и включены (по умолчанию) ---
$(BUILD)/safe/common/%.rel: $(CURDIR)/common/%.c $(BUILD)/safe/common/%.rel: $(CURDIR)/common/%.c $(HDRS)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(SDCC) -mz80 --no-std-crt0 --std-c99 --opt-code-size $(INC) -c -o $@ $< $(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 $@) @mkdir -p $(dir $@)
$(SDCC) -mz80 --no-std-crt0 --std-c99 --opt-code-size $(INC) -c -o $@ $< $(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, но без /* Лин-ядра для СПРАЙТОВ (кадр ≤ 64×64, sprite.h): как *_full, но без
* split'а >256 и с 8-битными w/h (движок/putsprite — горячий путь; * split'а >256 и с 8-битными w/h (движок/putsprite — горячий путь;
* программа только со спрайтами не тянет общие *_full со split'ом). * программа только со спрайтами не тянет общие *_full со split'ом).
* common/_gfx_blit_sprite.c, _gfx_heal_sprite.c. */ * Сигнатура БЕЗ img/sx/sy (2026-07-14): src — ГОТОВЫЙ адрес пикселей
void _gfx_blit_sprite(int x, int y, const void *img, * кадра, stride — шаг строки источника; считает их вызывающий один раз
int sx, int sy, uint8_t w, uint8_t h); * (кэш в 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); void _gfx_heal_sprite(int x, int y, uint8_t w, uint8_t h);
/* NOCLIP-ядра: клип-кода нет вообще (полный выигрыш от codegen, не только /* NOCLIP-ядра: клип-кода нет вообще (полный выигрыш от codegen, не только
* скип проверок). Зовутся при gfx_sprite_clip(0) — приложение * скип проверок). Зовутся при gfx_sprite_clip(0) — приложение
* гарантирует пределы. common/_gfx_blit_sprite_noclip.c, _gfx_heal_… */ * гарантирует пределы. common/_gfx_blit_sprite_noclip.c, _gfx_heal_… */
void _gfx_blit_sprite_noclip(int x, int y, const void *img, void _gfx_blit_sprite_noclip(int x, int y, const uint8_t *src,
int sx, int sy, uint8_t w, uint8_t h); uint16_t stride, uint8_t w, uint8_t h);
void _gfx_heal_sprite_noclip(int x, int y, uint8_t w, uint8_t h); void _gfx_heal_sprite_noclip(int x, int y, uint8_t w, uint8_t h);
/* Диспетчеризация clip/noclip через УКАЗАТЕЛИ (не ветка в горячем цикле): /* Диспетчеризация 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-ядра) — статическая * см. docs/sprite-api-design.md). Дефолт (clip-ядра) — статическая
* инициализация; указатели в РАЗДЕЛЬНЫХ data-модулях (_gfx_blit_fn.c, * инициализация; указатели в РАЗДЕЛЬНЫХ data-модулях (_gfx_blit_fn.c,
* _gfx_heal_fn.c): putsprite-only программа не тянет heal-ядро. */ * _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); uint8_t, uint8_t);
typedef void (*_gfx_heal_fp)(int, int, uint8_t, uint8_t); typedef void (*_gfx_heal_fp)(int, int, uint8_t, uint8_t);
extern _gfx_blit_fp _gfx_blit_fn; extern _gfx_blit_fp _gfx_blit_fn;
extern _gfx_heal_fp _gfx_heal_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в дизайна) ---------------- * /* ---- Атласы в страницах W0 (sprite.h, §9в дизайна) ---------------- *
* _gfx_w0_dss — страница ядра DSS (снимается при первом map/load); * _gfx_w0_dss — страница ядра DSS (снимается при первом map/load);
* _gfx_w0_cur — какая спрайт-страница должна стоять в W0 (читает * _gfx_w0_cur — какая спрайт-страница должна стоять в W0 (читает
+17 -49
View File
@@ -6,66 +6,34 @@
* и смены банка — вызывающий (sprite_update/putsprite) держит скобку и * и смены банка — вызывающий (sprite_update/putsprite) держит скобку и
* банк 0x5C. Проверок размера НЕТ (контракт ≤64 — в доке, не в коде). * банк 0x5C. Проверок размера НЕТ (контракт ≤64 — в доке, не в коде).
* *
* src — ГОТОВЫЙ адрес первого пикселя кадра, stride — шаг строки
* источника (полная ширина img/ленты); считает вызывающий один раз
* (кэш sprite_t.src/stride; putsprite — из заголовка). Заголовок
* getimage и (sx,sy) ядру больше не передаются (2026-07-14) — весь
* пересчёт адреса кадра ушёл в редкие точки его смены (sprite_frame,
* тикер анимации инкрементально).
*
* Клип по ЭКРАНУ остаётся (это функциональность — спрайты штатно уходят * Клип по ЭКРАНУ остаётся (это функциональность — спрайты штатно уходят
* за края): fast-path «целиком на экране» = 4 сравнения. * за края): fast-path «целиком на экране» = 4 сравнения; при выходе
* * корректируются края И src (левый — src += -x; верхний — src +=
* ФОРМАТ getimage и параметры атласа * (-y)*stride, одно 16-бит умножение ТОЛЬКО на реально клипнутом
* ---------------------------------- * кадре — редкость рядом с кадровым циклом).
* 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).
*/ */
#include "../_bgi.h" #include "../_bgi.h"
void _gfx_blit_sprite(int x, int y, const void *img, void _gfx_blit_sprite(int x, int y, const uint8_t *src,
int sx, int sy, uint8_t w, uint8_t h) 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 кадра (для клипа) */ 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 || y < 0 || x + cw > _bgi_maxx + 1 || y + ch > _bgi_maxy + 1) {
if (x < 0) { sx -= x; cw += x; x = 0; } if (x < 0) { src += -x; cw += x; x = 0; }
if (y < 0) { sy -= y; ch += y; y = 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 (x + cw > _bgi_maxx + 1) cw = _bgi_maxx + 1 - x;
if (y + ch > _bgi_maxy + 1) ch = _bgi_maxy + 1 - y; if (y + ch > _bgi_maxy + 1) ch = _bgi_maxy + 1 - y;
if (cw <= 0 || ch <= 0) return; if (cw <= 0 || ch <= 0) return;
} }
_bgi_blit_rows_raw(src, (uint8_t *)(_gfx_addr_base + x),
/* Адрес первого пикселя кадра: заголовок(4) + sx вправо + sy строк (uint8_t)cw, (uint8_t)ch, (int)stride, (uint8_t)y);
* вниз. 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);
} }
+11 -18
View File
@@ -11,27 +11,20 @@
* заворачивается наверх, x за 320 — в соседнюю строку/страницу; см. * заворачивается наверх, x за 320 — в соседнюю строку/страницу; см.
* tests/spriteclip). Вызывать только когда спрайт заведомо в пределах. * tests/spriteclip). Вызывать только когда спрайт заведомо в пределах.
* *
* Формат img/img_w/sx/sy — как в _gfx_blit_sprite (getimage-заголовок + * src — ГОТОВЫЙ адрес первого пикселя кадра, stride — шаг строки
* лента-атлас). БЕЗ _bgi_begin/_bgi_end и смены банка (держит вызывающий). * источника (полная ширина 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" #include "../_bgi.h"
void _gfx_blit_sprite_noclip(int x, int y, const void *img, void _gfx_blit_sprite_noclip(int x, int y, const uint8_t *src,
int sx, int sy, uint8_t w, uint8_t h) 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), _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 * _sprite_tick — тикер авто-анимаций (§9г): вызывается из sprite_update
* (раз на КАДР, до heal/блит-проходов — dirty должен взводиться раньше). * (раз на КАДР, до heal/блит-проходов — dirty должен взводиться раньше).
* Кадровая анимация и tween-перемещение НЕЗАВИСИМЫ (идут одновременно); * Кадровая анимация и tween-перемещение НЕЗАВИСИМЫ (идут одновременно);
* тикаются и у скрытых спрайтов («время идёт», ветка дороже). * тикаются и у скрытых спрайтов («время идёт»).
* *
* Кадровая: смена кадра = ±an_step к оси ленты (sy; при _SPR_ANIM_HORZ * ГОРЯЧИЙ ПУТЬ (профиль rpgwalk 2026-07-13: тик ел 7.3К тактов/спрайт —
* — sx) — БЕЗ умножения; wrap цикла — прыжок оси в an_lo (посчитан * №2 после блита; переписан 2026-07-14):
* sprite_anim'ом один раз). Маятник разворачивается НА границе (…2,3, * - один ранний выход на спрайт без активных анимаций;
* 2,1,0,1…, без удвоения крайних). ANIM_ONCE замирает на last: * - декремент таймеров — в цикле, до call'а: недозревший таймер не
* SPR_ANIM_ON снимается, SPR_ANIM_DONE взводится. * платит за вызов хелпера;
* - сама смена кадра/порция перемещения — в static-хелперах
* tick_anim/tick_move: читаемость + SDCC работает с ЛОКАЛЬНЫМИ
* копиями полей в регистрах вместо IX-обращений (~19Т каждое)
* на каждый чих;
* - кадровая анимация двигает КЭШ АДРЕСА кадра (s->src ± an_step
* байт; wrap — прыжок на готовый an_src0) — ни выбора оси, ни
* пиксельных смещений: всё посчитано sprite_anim'ом один раз.
* *
* Перемещение: раз в mv_speed кадров — порция ≤mv_step итераций * Кадровая: маятник разворачивается НА границе (…2,3,2,1,0,1… — без
* классического Брезенхэма (err-аккумулятор: большая ось шагает каждую * удвоения крайних); ANIM_ONCE замирает на last: SPR_ANIM_ON снимается,
* итерацию, меньшая подтягивается — нелинейные 2/3/2/3 сами собой, * SPR_ANIM_DONE взводится. sx/sy НЕ обновляются (см. sprite.h: их
* деления нет). Цель достигнута — SPR_MOVE_ON→off, SPR_MOVE_DONE. * абсолютно переустановит следующий 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 "../_bgi.h"
#include <sprite.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) void _sprite_tick(void *arr, uint8_t count)
{ {
sprite_t *s = (sprite_t *)arr; sprite_t *s = (sprite_t *)arr;
uint8_t i; uint8_t i;
for (i = 0; i < count; i++, s++) { for (i = 0; i < count; i++, s++) {
/* ---- кадровая анимация ---- */ uint8_t fl = s->an_flags;
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;
s->an_timer = s->an_speed; /* Типичный спрайт в кадре НЕ дозрел ни до смены кадра, ни до
if (!(s->an_flags & _SPR_ANIM_BACK)) { * шага — держим этот путь коротким: одна проверка флагов +
if (s->an_frame < s->an_last) { * декремент(ы) таймера. */
s->an_frame++; if (!(fl & (SPR_ANIM_ON | SPR_MOVE_ON)))
*axis += s->an_step; continue;
} else if (s->an_flags & _SPR_ANIM_ONCE) { if ((fl & SPR_ANIM_ON) && --s->an_timer == 0)
s->an_flags &= (uint8_t)~SPR_ANIM_ON; tick_anim(s);
s->an_flags |= SPR_ANIM_DONE; if ((fl & SPR_MOVE_ON) && --s->mv_timer == 0)
moved = 0; tick_move(s);
} 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;
}
}
} }
} }
+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; uint8_t saved = _io_page_w3;
const uint8_t *e; const uint8_t *e;
uint16_t img; /* W0-адрес шапки ленты */
sprinter_page_w3(a->page); sprinter_page_w3(a->page);
e = (const uint8_t *)(0xC000 + _ATL_DIR_OFF) + (uint16_t)idx * 8; 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->w = e[2]; /* fw — размер КАДРА */
s->h = e[3]; /* fh */ 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); sprinter_page_w3(saved);
/* src — W0-адрес пикселей кадра (0,0); разыменовывается блитом
* только при подключенной странице (sprite_update мапит сам). */
s->src = (const uint8_t *)img + 4;
s->x = 0; s->y = 0; s->x = 0; s->y = 0;
s->sx = 0; s->sy = 0; s->sx = 0; s->sy = 0;
s->flags = 0; s->flags = 0;
s->page = a->page; s->page = a->page;
s->an_flags = 0; /* авто-анимации выключены */ s->an_flags = 0; /* авто-анимации выключены */
s->an_frame = 0; s->an_frame = 0;
s->layer = 0; /* нижний слой Y-сортировки */
s->drawn[0].on = 0; s->drawn[0].on = 0;
s->drawn[1].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); gfx_set_bank(GFX_BANK_SPRITE);
_bgi_begin(); _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(); _bgi_end();
gfx_set_bank(saved); gfx_set_bank(saved);
} }
+26 -14
View File
@@ -3,9 +3,18 @@
* вдоль оси ленты (вертикальная; ANIM_HORIZ — вдоль X), смена раз в * вдоль оси ленты (вертикальная; ANIM_HORIZ — вдоль X), смена раз в
* speed кадров, mode = ANIM_LOOP / ANIM_PINGPONG / ANIM_ONCE. * speed кадров, mode = ANIM_LOOP / ANIM_PINGPONG / ANIM_ONCE.
* *
* Всё дорогое считается ЗДЕСЬ один раз (не в тике): шаг оси = размер * Всё дорогое считается ЗДЕСЬ один раз, чтобы тикеру осталась только
* кадра, осевое смещение first — циклом сложений (без __mulint). * 16-бит арифметика указателя:
* Ставит текущий кадр на first, сбрасывает SPR_ANIM_DONE, подключает * 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 * тикер (_spr_tick_fn) — до первого вызова sprite_anim/sprite_moveto
* код тикера в программу не линкуется. Требование: индексы ≤ 127, * код тикера в программу не линкуется. Требование: индексы ≤ 127,
* first ≤ last. Move-статусы (SPR_MOVE_*) не трогаются. * 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 speed, uint8_t mode)
{ {
uint8_t f = s->an_flags & (SPR_MOVE_ON | SPR_MOVE_DONE); 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; int lo = 0;
uint8_t i; uint8_t i;
for (i = 0; i < first; i++) for (i = 0; i < first; i++)
lo += step; /* first*step без умножения */ lo += step_px; /* first*step без умножения */
if (!speed) if (!speed)
speed = 1; speed = 1;
s->an_step = step; /* Ось — на кадр first (sprite_frame пересчитает кэш src + dirty). */
s->an_lo = lo; 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_first = first;
s->an_last = last; s->an_last = last;
s->an_frame = first; s->an_frame = first;
@@ -37,18 +55,12 @@ void sprite_anim(sprite_t *s, uint8_t first, uint8_t last,
f |= SPR_ANIM_ON; f |= SPR_ANIM_ON;
if (mode & ANIM_HORIZ) if (mode & ANIM_HORIZ)
f |= _SPR_ANIM_HORZ; f |= _SPR_STRIP_HORZ;
if ((mode & 0x03u) == ANIM_PINGPONG) if ((mode & 0x03u) == ANIM_PINGPONG)
f |= _SPR_ANIM_PP; f |= _SPR_ANIM_PP;
if ((mode & 0x03u) == ANIM_ONCE) if ((mode & 0x03u) == ANIM_ONCE)
f |= _SPR_ANIM_ONCE; f |= _SPR_ANIM_ONCE;
s->an_flags = f; 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; _spr_tick_fn = _sprite_tick;
} }
+14 -7
View File
@@ -1,25 +1,32 @@
/* /*
* sprite_anim_stop — остановить кадровую анимацию. frame ≥ 0 — какой * sprite_anim_stop — остановить кадровую анимацию. frame ≥ 0 — какой
* кадр останется на экране (индекс вдоль оси ленты, ≤127); -1 — * кадр останется на экране (индекс вдоль оси ленты, ≤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 "../_bgi.h"
#include <sprite.h> #include <sprite.h>
void sprite_anim_stop(sprite_t *s, int8_t frame) 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; int off = 0;
uint8_t i; uint8_t i;
for (i = 0; i < (uint8_t)frame; i++) for (i = 0; i < (uint8_t)frame; i++)
off += s->an_step; off += step_px;
s->an_frame = (uint8_t)frame; 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); sprite_frame(s, off, s->sy);
else else
sprite_frame(s, s->sx, off); 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) и нигде не * getimage (uint16 w, uint16 h). Спрайт невидим (flags = 0) и нигде не
* нарисован (drawn[*].on = 0) — первый sprite_show + sprite_update его * нарисован (drawn[*].on = 0) — первый sprite_show + sprite_update его
* выведет. Для атласа после init перезадать s->w/s->h (размер кадра) и * выведет. Для атласа после 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 "../_bgi.h"
#include <sprite.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; const uint8_t *p = (const uint8_t *)img;
s->img = img; s->img = img;
s->stride = (uint16_t)(p[0] | (p[1] << 8)); /* ширина img/ленты */
s->src = p + 4; /* кадр (0,0) */
s->x = 0; s->x = 0;
s->y = 0; s->y = 0;
s->sx = 0; s->sx = 0;
@@ -25,6 +31,7 @@ void sprite_init(sprite_t *s, const void *img)
s->page = 0; /* img в обычной памяти (не W0-атлас) */ s->page = 0; /* img в обычной памяти (не W0-атлас) */
s->an_flags = 0; /* авто-анимации выключены */ s->an_flags = 0; /* авто-анимации выключены */
s->an_frame = 0; s->an_frame = 0;
s->layer = 0; /* нижний слой Y-сортировки */
s->drawn[0].on = 0; s->drawn[0].on = 0;
s->drawn[1].on = 0; s->drawn[1].on = 0;
} }
+35 -14
View File
@@ -1,13 +1,20 @@
/* /*
* sprite_moveto — запустить tween-перемещение к (tx,ty): раз в interval * sprite_moveto — запустить tween-перемещение к (tx,ty): раз в interval
* кадров тикер делает до max_step итераций Брезенхэма (большая ось — * кадров тикер делает до max_step шагов вдоль ГЛАВНОЙ оси (большая
* итерация = пиксель; меньшая подтягивается err-аккумулятором). * |дельта|); минорная ось подтягивается DDA-аккумулятором — нелинейные
* Пример: (0,0)→(100,50), max_step 5, interval 4 → 20 шагов × 4 кадра * 2/3/2/3 сами собой, деления нет. Пример: (0,0)→(100,50), max_step 5,
* = 80 кадров (1.6 с), Y идёт 2/3/2/3. * interval 4 → 20 порций × 4 кадра = 80 кадров (1.6 с).
* *
* Дельты/знаки/аккумулятор считаются здесь один раз. Если цель == * Всё дорогое (выбор главной оси, |дельты|, знаки) считается здесь один
* текущая позиция — сразу SPR_MOVE_DONE (тикер не включается). * раз; тикеру остаётся БЕЗЗНАКОВАЯ арифметика (см. sprite.h: acc/rem —
* Кадровые статусы (SPR_ANIM_*) не трогаются. * на Z80 беззнаковое 16-бит сравнение в разы дешевле знаковых сравнений
* классического Брезенхэма, ради этого и переформулировано 2026-07-14).
* За mv_dmaj шагов главной оси минорная получает ровно mv_dmin шагов
* (аккумулятор со стартом dmaj/2 — симметричное округление), так что
* обе координаты приходят в цель одновременно; «приехали» = mv_rem == 0.
*
* Если цель == текущая позиция — сразу SPR_MOVE_DONE (тикер не
* включается). Кадровые статусы (SPR_ANIM_*) не трогаются.
*/ */
#include "../_bgi.h" #include "../_bgi.h"
#include <sprite.h> #include <sprite.h>
@@ -17,11 +24,10 @@ void sprite_moveto(sprite_t *s, int tx, int ty,
{ {
int dx = tx - s->x; int dx = tx - s->x;
int dy = ty - s->y; int dy = ty - s->y;
int8_t sgnx = 1, sgny = 1;
s->mv_sgnx = 1; if (dx < 0) { dx = -dx; sgnx = -1; }
if (dx < 0) { dx = -dx; s->mv_sgnx = -1; } if (dy < 0) { dy = -dy; sgny = -1; }
s->mv_sgny = 1;
if (dy < 0) { dy = -dy; s->mv_sgny = -1; }
s->an_flags &= (uint8_t)~SPR_MOVE_DONE; s->an_flags &= (uint8_t)~SPR_MOVE_DONE;
if (dx == 0 && dy == 0) { if (dx == 0 && dy == 0) {
@@ -31,9 +37,24 @@ void sprite_moveto(sprite_t *s, int tx, int ty,
s->mv_tx = tx; s->mv_tx = tx;
s->mv_ty = ty; s->mv_ty = ty;
s->mv_dx = dx;
s->mv_dy = -dy; /* Главная ось — с большей |дельтой| (при равенстве — X, диагональ
s->mv_err = dx - dy; * ровная в обе стороны). */
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 (!max_step) max_step = 1;
if (!interval) interval = 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 dbit = p ? _SPR_DIRTY1 : _SPR_DIRTY0;
uint8_t saved = gfx_get_bank(); uint8_t saved = gfx_get_bank();
uint8_t i; uint8_t i;
const uint8_t *rec; /* курсор сорт-таблицы (NULL = без) */
sprite_t *s; sprite_t *s;
/* Проход 0: тик авто-анимаций (§9г) — ДО heal/блита (двигает x/y и /* Проход 0: тик авто-анимаций (§9г) — ДО heal/блита (двигает x/y и
@@ -52,18 +53,33 @@ void sprite_update(sprite_t *arr, uint8_t count)
} }
_bgi_end(); _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: блит ВСЕХ видимых, требующих обновления. Спрайты из /* Проход 2: блит ВСЕХ видимых, требующих обновления. Спрайты из
* W0-атласов (s->page): подключить страницу перед блитом — один OUT * W0-атласов (s->page): подключить страницу перед блитом — один OUT
* на СМЕНУ страницы (спрайты одного атласа подряд = один OUT); * на СМЕНУ страницы (спрайты одного атласа подряд = один OUT; при
* порядок «_gfx_w0_cur, затем OUT» закрывает гонку с ISR-стабом * Y-сортировке смен может быть больше); порядок «_gfx_w0_cur,
* (он восстановит уже новую). heal-проходу страница не нужна * затем OUT» закрывает гонку с ISR-стабом (он восстановит уже
* (img не читается). */ * новую). heal-проходу страница и порядок не нужны. */
gfx_set_bank(GFX_BANK_SPRITE); gfx_set_bank(GFX_BANK_SPRITE);
_bgi_begin(); _bgi_begin();
{ {
uint8_t mapped = 0; /* 0 = W0 не тронута (там ядро DSS) */ 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 & (dbit | SPR_ALWAYS)) {
if (s->flags & SPR_VISIBLE) { if (s->flags & SPR_VISIBLE) {
if (s->page != mapped && s->page) { if (s->page != mapped && s->page) {
@@ -71,7 +87,9 @@ void sprite_update(sprite_t *arr, uint8_t count)
_io_page_w0 = s->page; _io_page_w0 = s->page;
mapped = 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].x = s->x;
s->drawn[p].y = s->y; s->drawn[p].y = s->y;
s->drawn[p].on = 1; s->drawn[p].on = 1;
+105 -18
View File
@@ -55,10 +55,14 @@
#define SPR_ANIM_DONE 0x02u /* one-shot дошёл до last */ #define SPR_ANIM_DONE 0x02u /* one-shot дошёл до last */
#define SPR_MOVE_ON 0x04u /* tween-перемещение идёт */ #define SPR_MOVE_ON 0x04u /* tween-перемещение идёт */
#define SPR_MOVE_DONE 0x08u /* перемещение достигло цели */ #define SPR_MOVE_DONE 0x08u /* перемещение достигло цели */
#define _SPR_ANIM_BACK 0x10u /* внутр.: маятник идёт назад */
#define _SPR_ANIM_HORZ 0x20u /* внутр.: лента горизонтальная (ось sx) */ /* Внутренние биты an_flags (владеет движок) — три РАЗНЫЕ категории:
#define _SPR_ANIM_PP 0x40u /* внутр.: режим маятника */ * режим анимации (зеркало параметра mode), свойство ЛЕНТЫ (ориентация
#define _SPR_ANIM_ONCE 0x80u /* внутр.: режим one-shot */ * данных в атласе) и 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(). */ /* Режимы sprite_anim(). */
#define ANIM_LOOP 0u /* 0,1,..,last,first,.. (цикл) */ #define ANIM_LOOP 0u /* 0,1,..,last,first,.. (цикл) */
@@ -66,10 +70,24 @@
#define ANIM_ONCE 2u /* first..last, замереть на last + DONE */ #define ANIM_ONCE 2u /* first..last, замереть на last + DONE */
#define ANIM_HORIZ 0x80u /* ИЛИ-флаг: кадры вдоль X (сетка nx>1) */ #define ANIM_HORIZ 0x80u /* ИЛИ-флаг: кадры вдоль X (сетка nx>1) */
/* ВНИМАНИЕ: порядок полей ЗАШИТ в asm-ядро тикера (офсеты полей в
* common/_sprite_tick.c, tick_move) — при изменении порядка/типов
* пересчитать офсеты там (offsetof-пробником, рецепт в его шапке). */
typedef struct { typedef struct {
const void *img; /* getimage-формат / атлас-лента (вне W3) */ 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 x, y; /* ЛОГИЧЕСКАЯ позиция левого-верхнего угла кадра */
int sx, sy; /* смещение кадра внутри img (атлас; 0 = целая) */ int sx, sy; /* смещение кадра, заданное sprite_frame (см. src)*/
uint8_t w, h; /* размер кадра (кэш заголовка), 1..255 */ uint8_t w, h; /* размер кадра (кэш заголовка), 1..255 */
uint8_t flags; /* SPR_* | внутренние dirty */ uint8_t flags; /* SPR_* | внутренние dirty */
uint8_t page; /* 0 = img в обычной памяти; иначе физ. страница * uint8_t page; /* 0 = img в обычной памяти; иначе физ. страница *
@@ -81,23 +99,40 @@ typedef struct {
uint8_t an_frame; /* ТЕКУЩИЙ индекс кадра (0-based по ленте) */ uint8_t an_frame; /* ТЕКУЩИЙ индекс кадра (0-based по ленте) */
uint8_t an_first, an_last; /* границы анимации (индексы) */ uint8_t an_first, an_last; /* границы анимации (индексы) */
uint8_t an_speed, an_timer; /* кадров между сменами / счётчик */ uint8_t an_speed, an_timer; /* кадров между сменами / счётчик */
uint8_t an_step; /* шаг оси = fh (верт. лента) или fw (гориз.) */ uint16_t an_step; /* шаг кадра в БАЙТАХ src: fw (гориз. лента) *
int an_lo; /* осевое смещение кадра first (для wrap) */ * или fh*stride (верт.) — считает sprite_anim */
const uint8_t *an_src0; /* src кадра first (для wrap цикла) */
uint8_t mv_speed, mv_timer; /* кадров между шагами / счётчик */ uint8_t mv_speed, mv_timer; /* кадров между шагами / счётчик */
uint8_t mv_step; /* макс. пикселей вдоль большей оси за шаг */ uint8_t mv_step; /* макс. пикселей вдоль большей оси за шаг */
int mv_tx, mv_ty; /* цель */ int mv_tx, mv_ty; /* цель (для sprite_move_stop) */
int mv_dx, mv_dy, mv_err;/* Брезенхэм: dx, -dy, аккумулятор */ /* DDA по главной оси (беззнаковый — на Z80 16-бит беззнаковые
int8_t mv_sgnx, mv_sgny; /* знаки шага осей */ * сравнения в разы дешевле знаковых Брезенхэма, 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 { struct {
int x, y; /* где нарисован на странице p */ int x, y; /* где нарисован на странице p */
uint8_t on; /* нарисован ли на странице p */ uint8_t on; /* нарисован ли на странице p */
} drawn[2]; } drawn[2];
/* Слой для Y-сортировки (см. gfx_sprite_ysort): при включённой
* сортировке спрайты упорядочиваются по {layer, y} — слой старше:
* слой 1 ВСЕГДА поверх слоя 0 (ходячие/летающие/курсор...), внутри
* слоя — painter по y. 0 после sprite_init; без сортировки не
* используется (порядок = индекс массива). Поле В КОНЦЕ структуры:
* офсеты полей выше зашиты в asm тикера/сортировщика. */
uint8_t layer;
} sprite_t; } sprite_t;
/* Инициализация: w,h из заголовка img, невидим, ничего не нарисовано. /* Инициализация: w,h из заголовка img, кадр (0,0), невидим, ничего не
* Для АТЛАСА (кадр — под-прямоугольник) после init выставить s->w/s->h * нарисовано. Для АТЛАСА (кадр — под-прямоугольник) после init
* в размер кадра и s->sx/s->sy — на нужный кадр (или sprite_frame). */ * выставить s->w/s->h в размер кадра и перейти на нужный кадр ТОЛЬКО
* через sprite_frame (он ведёт кэш src; голые s->sx/s->sy кэш не
* обновят). */
void sprite_init(sprite_t *s, const void *img); void sprite_init(sprite_t *s, const void *img);
/* Кадр всей сцены: на ТЕКУЩЕЙ draw-странице (gfx_get_draw_page) /* Кадр всей сцены: на ТЕКУЩЕЙ draw-странице (gfx_get_draw_page)
@@ -105,7 +140,16 @@ void sprite_init(sprite_t *s, const void *img);
* скобка/банк на проход). Спрайты без изменений с прошлого визита этой * скобка/банк на проход). Спрайты без изменений с прошлого визита этой
* страницы (и без SPR_ALWAYS) не трогаются. Для дабл-буфера звать после * страницы (и без SPR_ALWAYS) не трогаются. Для дабл-буфера звать после
* gfx_set_draw_page(hidden); для одиночной страницы/курсора — прямо на * gfx_set_draw_page(hidden); для одиночной страницы/курсора — прямо на
* видимой. */ * видимой.
*
* ВСЯ СЦЕНА СТРАНИЦЫ — ОДИН ВЫЗОВ! Два sprite_update на одну страницу
* (например «сначала ходячие, потом летающие») ЛОМАЮТ перекрытия МЕЖДУ
* группами: heal второй группы берёт из ОЗУ-копии чистый ФОН (спрайты
* в копию не пишутся — банк 0x5C) и стирает нарисованных первой
* группой; двухпроходная дисциплина «heal все → блит все» покрывает
* перекрытия только ВНУТРИ одного вызова. Логические группы держать
* диапазонами индексов одного массива (z-порядок групп = порядок
* диапазонов; слоение при Y-сортировке — полем layer). */
void sprite_update(sprite_t *arr, uint8_t count); void sprite_update(sprite_t *arr, uint8_t count);
/* Сахар канонического дабл-буфер-кадра: draw-страница = скрытая → /* Сахар канонического дабл-буфер-кадра: draw-страница = скрытая →
@@ -114,6 +158,49 @@ void sprite_update(sprite_t *arr, uint8_t count);
* sprite_flip — тогда heal учтёт его через ОЗУ-копию. */ * sprite_flip — тогда heal учтёт его через ОЗУ-копию. */
void sprite_flip(sprite_t *arr, uint8_t count); 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-оверхеда в физике) --- */ /* --- 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; } if (s->x != x || s->y != y) { s->x = x; s->y = y; s->flags |= _SPR_DIRTY; }
} }
/* Сменить кадр атласа (под-прямоугольник img). */ /* Сменить кадр атласа (под-прямоугольник img). Не inline: пересчитывает
inline void sprite_frame(sprite_t *s, int sx, int sy) { * кэш src (одно 16-бит умножение sy*stride) — при АКТИВНОЙ кадровой
if (s->sx != sx || s->sy != sy) { s->sx = sx; s->sy = sy; s->flags |= _SPR_DIRTY; } * анимации не звать (тикер ведёт src сам). */
} void sprite_frame(sprite_t *s, int sx, int sy);
/* Показать (если был скрыт — грязнит обе страницы). */ /* Показать (если был скрыт — грязнит обе страницы). */
inline void sprite_show(sprite_t *s) { inline void sprite_show(sprite_t *s) {
+74 -8
View File
@@ -7,7 +7,7 @@
* гоняется в текстовом режиме (скобка W3 безвредна). Ожидания: * гоняется в текстовом режиме (скобка W3 безвредна). Ожидания:
* *
* T1 LOOP 0..3 speed=2: смена раз в 2 тика: после 8 тиков frame * 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, * 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 ещё стоит, * T3 ONCE 0..2 speed=1: после 2 тиков frame==2 и ANIM_ON ещё стоит,
@@ -18,6 +18,9 @@
* LOOP-анимация (одновременность) — frame успел смениться. * LOOP-анимация (одновременность) — frame успел смениться.
* T5 sprite_anim_stop(1): ON снят, frame==1, ось = 16. * T5 sprite_anim_stop(1): ON снят, frame==1, ось = 16.
* sprite_move_stop(at_target=1) на полпути: прыжок в цель + DONE. * 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 — содержимое * Кадры: RAM-лента 16×16×4 (getimage), кадр k залит k — содержимое
* не важно (блитов нет), важен размер h=16 → an_step. * не важно (блитов нет), важен размер h=16 → an_step.
@@ -25,10 +28,16 @@
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <sprite.h> #include <sprite.h>
#include "../../libbgi/_bgi.h" /* T7: прямой доступ к сорт-таблице */
static uint8_t strip[4 + 16 * 16 * 4]; static uint8_t strip[4 + 16 * 16 * 4];
static sprite_t sp[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) static void build_strip(void)
{ {
memset(strip, 0, sizeof strip); memset(strip, 0, sizeof strip);
@@ -53,11 +62,11 @@ int main(void)
sp[0].w = 16; sp[0].h = 16; sp[0].w = 16; sp[0].h = 16;
sprite_anim(&sp[0], 0, 3, 2, ANIM_LOOP); sprite_anim(&sp[0], 0, 3, 2, ANIM_LOOP);
tick(sp, 1, 2); 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 */ 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 */ 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"); printf("T1 LOOP: %s\n", ok ? "PASS" : "FAIL");
/* ---- T2: PINGPONG ------------------------------------------------ */ /* ---- T2: PINGPONG ------------------------------------------------ */
@@ -67,11 +76,11 @@ int main(void)
tick(sp, 1, 3); /* 1,2,3 */ tick(sp, 1, 3); /* 1,2,3 */
ok = (sprite_anim_frame(&sp[0]) == 3); ok = (sprite_anim_frame(&sp[0]) == 3);
tick(sp, 1, 1); /* разворот: 2 */ 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 */ tick(sp, 1, 2); /* 1,0 */
ok = ok && (sprite_anim_frame(&sp[0]) == 0); ok = ok && (sprite_anim_frame(&sp[0]) == 0);
tick(sp, 1, 1); /* разворот: 1 */ 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"); printf("T2 PINGPONG: %s\n", ok ? "PASS" : "FAIL");
/* ---- T3: ONCE ---------------------------------------------------- */ /* ---- T3: ONCE ---------------------------------------------------- */
@@ -83,7 +92,7 @@ int main(void)
(sprite_anim_status(&sp[0]) & SPR_ANIM_ON) && (sprite_anim_status(&sp[0]) & SPR_ANIM_ON) &&
!(sprite_anim_status(&sp[0]) & SPR_ANIM_DONE)); !(sprite_anim_status(&sp[0]) & SPR_ANIM_DONE));
tick(sp, 1, 1); /* фиксация: ON→off,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_ON) &&
(sprite_anim_status(&sp[0]) & SPR_ANIM_DONE)); (sprite_anim_status(&sp[0]) & SPR_ANIM_DONE));
tick(sp, 1, 5); /* стоит на месте */ tick(sp, 1, 5); /* стоит на месте */
@@ -119,7 +128,8 @@ int main(void)
tick(sp, 1, 2); /* frame 2 */ tick(sp, 1, 2); /* frame 2 */
sprite_anim_stop(&sp[0], 1); /* остановить на кадре 1 */ sprite_anim_stop(&sp[0], 1); /* остановить на кадре 1 */
ok = (!(sprite_anim_status(&sp[0]) & SPR_ANIM_ON) && 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); /* не двигается */ tick(sp, 1, 4); /* не двигается */
ok = ok && (sprite_anim_frame(&sp[0]) == 1); ok = ok && (sprite_anim_frame(&sp[0]) == 1);
@@ -137,6 +147,62 @@ int main(void)
!(sprite_anim_status(&sp[0]) & SPR_MOVE_DONE)); !(sprite_anim_status(&sp[0]) & SPR_MOVE_DONE));
printf("T5 STOPS: %s\n", ok ? "PASS" : "FAIL"); 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"); puts("done");
return 0; return 0;
} }