diff --git a/docs/TODO.md b/docs/TODO.md index b10a06e..8eaa851 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -150,19 +150,18 @@ Quick wins: blitw/bgi_img/sprites/balls — 1:1. Остатки разрыва с asm- референсом — C-обвязка вызовов (batch-пасс, см. ниже) и спец- blit-leaf (dstride 0: −30Т/строку — делать по замеру). -- [ ] **batch-пасс для спрайтов** (движок v2, §9.1) — В РАБОТЕ 2026-07-12. - Разрыв ~2× с asm-референсом (docs/samples/balls) — это НЕ - W3-скобка (_bgi_begin = 5 инструкций, ~35Т), а per-call - C-обвязка, повторяемая 2×N раз: полный клип (528/362 Б), - парс заголовка img, save/restore банка, SDCC-фрейм 7-арг вызова. - Референс всё это платит ОДИН раз на проход: W3 замаплен на видео - на всю программу (в цикле не трогает), банк-подрежим ставит один - `out` на проход (0x5C рисовать всё → 0x50 лечить всё), DI держит - на весь проход. Движок v2 повторяет это (per-pass W3/банк/клип- - fast-path, адресация из кэша w,h в структуре), НО DI оставляем - гранулярным (по строке в leaf) — референс держит длинный DI - только потому, что у него нет аудио-ISR; под CBL/IM2 длинный DI - сорвёт звук (docs/accel-fill-budget.md). +- [x] ~~**batch-пасс для спрайтов** (движок v2, §9.1)~~ — СДЕЛАНО + 2026-07-12/13: движок v2 (sprite.h: sprite_init/update/flip, + retained per-page dirty), два прохода heal→blit под одной + W3-скобкой/банком, спец-leaf'ы `_bgi_blit_rows_raw` (dstride 0, + SMC sstride) и `_bgi_heal_rows_raw`, лин-ядра ≤64×64, ОДИН DI на + спрайт (санкция 2026-07-12), noclip-ядра + funcptr-диспетч + `_gfx_blit_fn`/`_gfx_heal_fn` (см. sprite-api-design §9б). + Замер dev-MAME 16 шаров uncapped: clip 50 → noclip 60-61 fps + (+20-22%); с vsync-капом кадр < 20 мс. Исходный уровень был + 24 fps. (Диагноз разрыва с asm-референсом — per-call C-обвязка, + не W3-скобка — и путь оптимизации описаны в sprite-api-design + §9-9б.) На железе перепроверить снятие src[0]-фикса (§9а). - [ ] клип fast-path «прямоугольник целиком на экране» в gfx_blit_part (4 сравнения вместо полного пути) — делается в _gfx_blit_full/ _gfx_heal_full вместе с batch-пассом. diff --git a/docs/size_baseline.tsv b/docs/size_baseline.tsv index 3598cff..decfe38 100644 --- a/docs/size_baseline.tsv +++ b/docs/size_baseline.tsv @@ -3,7 +3,7 @@ accfill 3772 argv 3431 assrtest 3847 attrprob 4087 -balls 6393 +balls 6156 banked 1056 bankedbg 1067 banklocl 4683 @@ -44,7 +44,7 @@ seek 4188 simple 955 solidt 11531 spriteclip 4187 -sprites 9968 +sprites 6791 stattest 7549 stdlib 6643 stest2 3644 diff --git a/docs/sprite-api-design.md b/docs/sprite-api-design.md index 1a3c63a..1ed0d30 100644 --- a/docs/sprite-api-design.md +++ b/docs/sprite-api-design.md @@ -562,13 +562,44 @@ for (;;) { невидима (burst перезаписывает dst[0] правильным src[0]), но при 0x58/0x5C, если src[0] == 0xFF, перезапись скипается — на экране вертикальная полоса цвета последних байтов строк (симптом: клипнутые -спрайты в tests/sprites). Нейтрализовано в `_bgi_copy_rows_raw`: -src[0] предчитывается до армирования и подставляется в A через -EX AF,AF' перед триггером — CPU-байт становится src[0], корректным при -любом банке и любой семантике первого цикла. Регресс: tests/blitw -(визуальная секция «trig leak»). Подтверждено в MAME 0.283; на железе -проверить вместе с остальным (полоса цвета — маркер, что железо делает -так же; чистый col0 — что CPU-цикл триггера подавляется ПЛМ). +спрайты в tests/sprites). Было нейтрализовано в `_bgi_copy_rows_raw`: +src[0] предчитывался до армирования и подставлялся в A через EX AF,AF' +перед триггером. + +**ФИКС СНЯТ 2026-07-13.** Точная dev-MAME (сборка разработчиков +Sprinter, `mame/sources/MAME`) эмулирует ПЛМ, подавляющую CPU-байт +триггера при активном burst'е — квирк был артефактом стоковой MAME +0.283. Проверено pixel-точно: tests/blitw, col0 @(200,150) = 0x02 +GREEN по байтам VRAM (read_vram через MCP-мост). Для heal фикс был +избыточен всегда (банк 0x50 перезаписывает dst[0]). Строки фикса +оставлены закомментированными в трёх leaf'ах (_bgi_copy_rows_raw, +_bgi_blit_rows_raw, _bgi_heal_rows_raw) — восстановить, если реальное +железо поведёт себя как MAME 0.283 (маркер: вертикальная полоса цвета +последних байтов строк на клипнутых/прозрачных спрайтах). Регресс: +tests/blitw (визуальная секция «trig leak»). НА ЖЕЛЕЗЕ ПЕРЕПРОВЕРИТЬ. + +## 9б. Клип спрайтов: noclip-ядра + funcptr-диспетч (2026-07-12/13) + +Клип в спрайтовых ядрах стоит не только сравнений (~0.85 мс/кадр на 16 +шаров), но и codegen-эффекта: клип-код раздувает регистровое давление +всей функции (спиллы) — поэтому рантайм-скип внутри общего ядра +возвращает лишь малую часть. Полный выигрыш даёт ФИЗИЧЕСКИ отдельная +функция без клип-кода: `_gfx_blit_sprite_noclip` / `_gfx_heal_sprite_noclip`. + +Выбор ядра — НЕ веткой в горячем цикле (ветка `if (clip)` в +sprite_update съедала половину выигрыша: 52 вместо 57 fps на старой +MAME), а перенаправлением указателей `_gfx_blit_fn`/`_gfx_heal_fn` +(common/_gfx_sprite_fns.c, дефолт — clip-ядра) один раз в +`gfx_sprite_clip()`; sprite_update/putsprite/movesprite зовут через +указатель. Программа без вызова `gfx_sprite_clip()` не линкует +noclip-ядра (на них ссылается только модуль gfx_sprite_clip.c). + +Замер на точной dev-MAME (16 шаров, uncapped, A/B 2026-07-13): +clip 50 fps → noclip-funcptr 60-61 fps (**+20-22%**) — полный выигрыш +noclip доехал до приложения. С vsync-капом оба варианта упираются в +~48-50 (кадр < 20 мс) — выигрыш конвертируется в запас кадра. +ВНИМАНИЕ: noclip требует гарантии приложения, что спрайты целиком на +экране — выход за край портит соседнюю память (tests/spriteclip). ## 10. Риски / что проверить артефактом (Фаза 0) diff --git a/libbgi/_bgi.h b/libbgi/_bgi.h index cf2c7b8..2dec31a 100644 --- a/libbgi/_bgi.h +++ b/libbgi/_bgi.h @@ -133,8 +133,8 @@ void _gfx_heal_sprite_noclip(int x, int y, uint8_t w, uint8_t h); * gfx_sprite_clip() перенаправляет их на clip- или noclip-ядро ОДИН раз, * а sprite_update/putsprite/movesprite зовут через указатель — один * call-site, без if в цикле → тесный codegen (полный выигрыш noclip, - * см. docs/sprite-api-design.md). Установка по умолчанию (clip on) — в - * initgraph. */ + * см. docs/sprite-api-design.md). Дефолт (clip-ядра) — статическая + * инициализация в common/_gfx_sprite_fns.c. */ typedef void (*_gfx_blit_fp)(int, int, const void *, int, int, uint8_t, uint8_t); typedef void (*_gfx_heal_fp)(int, int, uint8_t, uint8_t); diff --git a/libbgi/bgi256/_bgi_blit_rows_raw.c b/libbgi/bgi256/_bgi_blit_rows_raw.c index 335a3f0..d88462f 100644 --- a/libbgi/bgi256/_bgi_blit_rows_raw.c +++ b/libbgi/bgi256/_bgi_blit_rows_raw.c @@ -24,11 +24,14 @@ * не рвёт). ВНИМАНИЕ: h ограничен малыми спрайтами — большие блиты * (putimage 256 строк) вызывающий обязан резать на чанки ≤16 строк. * - * Квирк CPU-байта write-триггера (см. _bgi_copy_rows_raw): src[0] - * предчитывается и подставляется в A через EX AF,AF' — CPU-цикл - * LD (DE),A кладёт корректный src[0] при любом банке. + * Квирк CPU-байта write-триггера СНЯТ (2026-07-13): на точной dev-MAME + * (сборка разработчиков Sprinter) ПЛМ подавляет CPU-байт LD (DE),A при + * активном burst'е — утечки в dst[0] нет (проверено по байтам VRAM: + * tests/blitw col0 @200,150 = GREEN). Старый фикс (preread src[0] + + * EX AF,AF') оставлен закомментированным ниже — восстановить, если + * реальное железо поведёт себя как старая MAME. ПЕРЕПРОВЕРИТЬ НА ЖЕЛЕЗЕ. * - * Клоббер: AF/AF'/BC/DE/HL; IX сохраняется. + * Клоббер: AF/BC/DE/HL; IX сохраняется. */ #include "../_bgi.h" diff --git a/libbgi/bgi256/_bgi_copy_rows_raw.c b/libbgi/bgi256/_bgi_copy_rows_raw.c index cdf8c07..576f446 100644 --- a/libbgi/bgi256/_bgi_copy_rows_raw.c +++ b/libbgi/bgi256/_bgi_copy_rows_raw.c @@ -33,20 +33,19 @@ * LD (DE),A (burst-запись из памяти акселератора в dst), LD B,B, EI. * Между армированием и триггерами — только регистровые M1-опкоды. * - * КВИРК ТРИГГЕРА ЗАПИСИ (найден tests/sprites — клипнутые спрайты): - * LD (DE),A — реальная CPU-инструкция: её собственный цикл записи - * кладёт байт A в dst[0], и лишь затем burst FSM пишет src[0..w-1] - * поверх. После burst-чтения в A остаётся ПОСЛЕДНИЙ байт строки — - * при банке 0x50 утечка невидима (src[0] тут же перезапишет), но при - * 0x58/0x5C, если src[0] == 0xFF, перезапись скипается и в dst[0] - * остаётся вертикальная полоса цвета последних байтов строк. - * Нейтрализация: src[0] предчитывается ДО армирования и подставляется - * в A через EX AF,AF' (регистровый M1-опкод — под armed разрешён) - * прямо перед триггером — CPU-байт становится src[0], что корректно - * при ЛЮБОМ банке и любой семантике первого цикла записи (в MAME - * 0.283 подтверждено tests/blitw+sprites; на железе перепроверить). + * КВИРК ТРИГГЕРА ЗАПИСИ — ФИКС СНЯТ 2026-07-13. История: в стоковой + * MAME 0.283 LD (DE),A вёл себя как реальная CPU-инструкция — её + * собственный цикл записи клал байт A (последний байт строки после + * burst-чтения) в dst[0], и при банке 0x58/0x5C со src[0] == 0xFF + * оставалась вертикальная полоса (найдено tests/sprites). + * Нейтрализация была: preread src[0] + EX AF,AF' перед триггером. + * Точная dev-MAME (сборка разработчиков Sprinter) эмулирует ПЛМ, + * подавляющую CPU-байт при активном burst'е — утечки нет и без фикса + * (проверено tests/blitw: col0 @200,150 = GREEN, по байтам VRAM). + * Закомментированные строки ниже — след фикса; восстановить, если + * реальное железо поведёт себя как MAME 0.283. НА ЖЕЛЕЗЕ ПЕРЕПРОВЕРИТЬ. * - * Клоббер: AF/AF'/BC/DE/HL; IX сохраняется (callee-saved). + * Клоббер: AF/BC/DE/HL; IX сохраняется (callee-saved). */ #include "../_bgi.h" diff --git a/libbgi/bgi256/_bgi_heal_rows_raw.c b/libbgi/bgi256/_bgi_heal_rows_raw.c index 50b484d..79f020d 100644 --- a/libbgi/bgi256/_bgi_heal_rows_raw.c +++ b/libbgi/bgi256/_bgi_heal_rows_raw.c @@ -15,12 +15,15 @@ * вызывающий (gfx_heal форсит 0x50 — иначе heal бессмыслен). * Полосы > 256 байт режет вызывающий. * - * Квирк CPU-байта write-триггера (см. _bgi_copy_rows_raw): для heal - * предчитанный src[0] — ровно тот байт, что burst запишет в dst[0], - * так что фикс EX AF,AF' здесь одновременно и нейтрализация утечки, - * и просто корректное значение. + * Квирк CPU-байта write-триггера: фикс (preread + EX AF,AF') СНЯТ + * (2026-07-13). Для heal он был избыточен ВСЕГДА: банк 0x50 без + * прозрачности — даже если CPU-байт утёк бы в dst[0], burst тут же + * перезаписывает его тем же значением из ОЗУ-копии. Плюс точная + * dev-MAME показала, что ПЛМ вообще подавляет CPU-байт (см. + * _bgi_blit_rows_raw). Закомментированные строки ниже — след старого + * фикса на случай отличий реального железа. * - * Клоббер: AF/AF'/BC/DE/HL; IX сохраняется. + * Клоббер: AF/BC/DE/HL; IX сохраняется. */ #include "../_bgi.h" diff --git a/libbgi/common/_gfx_blit_sprite.c b/libbgi/common/_gfx_blit_sprite.c index 6bf76b0..48d54be 100644 --- a/libbgi/common/_gfx_blit_sprite.c +++ b/libbgi/common/_gfx_blit_sprite.c @@ -35,7 +35,6 @@ * умножения: обычно sy == 0). */ #include "../_bgi.h" -#include void _gfx_blit_sprite(int x, int y, const void *img, int sx, int sy, uint8_t w, uint8_t h) diff --git a/libbgi/common/_gfx_sprite_fns.c b/libbgi/common/_gfx_sprite_fns.c new file mode 100644 index 0000000..219b93a --- /dev/null +++ b/libbgi/common/_gfx_sprite_fns.c @@ -0,0 +1,16 @@ +/* + * _gfx_sprite_fns — указатели диспетчеризации clip/noclip спрайтовых ядер + * (data-модуль). Дефолт — clip-ядра (безопасно); gfx_sprite_clip() + * перенаправляет на noclip и обратно. Горячие call-site'ы + * (sprite_update/putsprite/movesprite) зовут через указатель — один + * call-site без if в цикле → тесный codegen (ветка if(clip) в + * sprite_update съедала половину выигрыша noclip: 52 вместо 57 fps). + * + * Программа, не зовущая gfx_sprite_clip(), тянет только clip-ядра + * (noclip-ядра referenced лишь из модуля gfx_sprite_clip.c — DCE). + * Ненулевой дефолт → в _INITIALIZED (как _gfx_bank). + */ +#include "../_bgi.h" + +_gfx_blit_fp _gfx_blit_fn = _gfx_blit_sprite; +_gfx_heal_fp _gfx_heal_fn = _gfx_heal_sprite; diff --git a/libbgi/common/gfx_sprite_clip.c b/libbgi/common/gfx_sprite_clip.c new file mode 100644 index 0000000..a40b666 --- /dev/null +++ b/libbgi/common/gfx_sprite_clip.c @@ -0,0 +1,25 @@ +/* + * gfx_sprite_clip — включить/выключить клип спрайтов по экрану. + * on=1 (дефолт) — clip-ядра; on=0 — noclip-ядра (приложение ГАРАНТИРУЕТ, + * что спрайты целиком на экране; ~+19% на анимации — клип-кода в noclip + * нет физически, см. _gfx_blit_sprite_noclip.c). + * + * Переключение — перенаправлением указателей _gfx_blit_fn/_gfx_heal_fn + * (ОДИН раз здесь, не ветка в горячем цикле). Флаг _gfx_sprite_clip + * остаётся для gfx_get_sprite_clip(). Настоящая функция (не inline): + * только этот модуль ссылается на noclip-ядра — программа без вызова + * gfx_sprite_clip() их не линкует. + */ +#include "../_bgi.h" + +void gfx_sprite_clip(uint8_t on) +{ + _gfx_sprite_clip = on; + if (on) { + _gfx_blit_fn = _gfx_blit_sprite; + _gfx_heal_fn = _gfx_heal_sprite; + } else { + _gfx_blit_fn = _gfx_blit_sprite_noclip; + _gfx_heal_fn = _gfx_heal_sprite_noclip; + } +} diff --git a/libbgi/common/movesprite.c b/libbgi/common/movesprite.c index 595860b..35618b8 100644 --- a/libbgi/common/movesprite.c +++ b/libbgi/common/movesprite.c @@ -22,8 +22,7 @@ void movesprite(int oldx, int oldy, int x, int y, const void *img) gfx_set_bank(GFX_BANK_NORMAL); _bgi_begin(); - if (_gfx_sprite_clip) _gfx_heal_sprite(oldx, oldy, p[0], p[2]); - else _gfx_heal_sprite_noclip(oldx, oldy, p[0], p[2]); + (*_gfx_heal_fn)(oldx, oldy, p[0], p[2]); _bgi_end(); gfx_set_bank(saved); diff --git a/libbgi/common/putsprite.c b/libbgi/common/putsprite.c index b7a3ca2..30db076 100644 --- a/libbgi/common/putsprite.c +++ b/libbgi/common/putsprite.c @@ -18,8 +18,7 @@ void putsprite(int x, int y, const void *img) gfx_set_bank(GFX_BANK_SPRITE); _bgi_begin(); - if (_gfx_sprite_clip) _gfx_blit_sprite(x, y, img, 0, 0, p[0], p[2]); - else _gfx_blit_sprite_noclip(x, y, img, 0, 0, p[0], p[2]); + (*_gfx_blit_fn)(x, y, img, 0, 0, p[0], p[2]); _bgi_end(); gfx_set_bank(saved); } diff --git a/libbgi/common/sprite_update.c b/libbgi/common/sprite_update.c index a9a2092..4f9e24b 100644 --- a/libbgi/common/sprite_update.c +++ b/libbgi/common/sprite_update.c @@ -31,7 +31,6 @@ void sprite_update(sprite_t *arr, uint8_t count) uint8_t p = gfx_get_draw_page(); /* 0 / 1 */ uint8_t dbit = p ? _SPR_DIRTY1 : _SPR_DIRTY0; uint8_t saved = gfx_get_bank(); - uint8_t clip = _gfx_sprite_clip; /* раз на кадр: clip- или noclip-ядро */ uint8_t i; sprite_t *s; @@ -40,8 +39,7 @@ void sprite_update(sprite_t *arr, uint8_t count) _bgi_begin(); for (i = 0, s = arr; i < count; i++, s++) { if (s->drawn[p].on && (s->flags & (dbit | SPR_ALWAYS))) { - if (clip) _gfx_heal_sprite(s->drawn[p].x, s->drawn[p].y, s->w, s->h); - else _gfx_heal_sprite_noclip(s->drawn[p].x, s->drawn[p].y, s->w, s->h); + (*_gfx_heal_fn)(s->drawn[p].x, s->drawn[p].y, s->w, s->h); s->drawn[p].on = 0; } } @@ -53,8 +51,7 @@ void sprite_update(sprite_t *arr, uint8_t count) for (i = 0, s = arr; i < count; i++, s++) { if (s->flags & (dbit | SPR_ALWAYS)) { if (s->flags & SPR_VISIBLE) { - if (clip) _gfx_blit_sprite(s->x, s->y, s->img, s->sx, s->sy, s->w, s->h); - else _gfx_blit_sprite_noclip(s->x, s->y, s->img, s->sx, s->sy, s->w, s->h); + (*_gfx_blit_fn)(s->x, s->y, s->img, s->sx, s->sy, s->w, s->h); s->drawn[p].x = s->x; s->drawn[p].y = s->y; s->drawn[p].on = 1; diff --git a/libbgi/include/gfx.h b/libbgi/include/gfx.h index 3106289..76950a9 100644 --- a/libbgi/include/gfx.h +++ b/libbgi/include/gfx.h @@ -70,10 +70,11 @@ inline uint8_t gfx_get_bank(void) { return _gfx_bank; } * иначе запись за край портит соседнюю память, см. tests/spriteclip). * Выигрыш ~+19% на анимации: движок зовёт noclip-ядра без клип-кода * (не просто скип проверки — вся клип-математика физически отсутствует, - * SDCC тесней раскладывает регистры). Флаг читается раз на спрайт. - * НЕ влияет на putimage/gfx_blit (у них клип всегда). */ + * SDCC тесней раскладывает регистры). Переключение — перенаправлением + * внутренних указателей на ядра (один раз здесь, не ветка в горячем + * цикле движка). НЕ влияет на putimage/gfx_blit (у них клип всегда). */ extern uint8_t _gfx_sprite_clip; -inline void gfx_sprite_clip(uint8_t on) { _gfx_sprite_clip = on; } +void gfx_sprite_clip(uint8_t on); inline uint8_t gfx_get_sprite_clip(void) { return _gfx_sprite_clip; } /* ---- Блиттинг (ядро спрайтов; Фаза B sprite-api-design) ----------- *