From 72ce66275ebcefd5b8ed8e7a8f1728e7b8a45d8e Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Sun, 12 Jul 2026 21:47:37 +0300 Subject: [PATCH] =?UTF-8?q?libbgi:=20=D1=81=D0=BF=D1=80=D0=B0=D0=B9=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=D1=8B=D0=B9=20=D0=B4=D0=B2=D0=B8=D0=B6=D0=BE=D0=BA?= =?UTF-8?q?=20v2=20+=20accel-=D0=B1=D0=BB=D0=B8=D1=82/heal=20leaf'=D1=8B?= =?UTF-8?q?=20+=20noclip-=D0=BF=D1=83=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Спрайтовая графика поверх accel block-copy (docs/sprite-api-design.md): - Ядро блиттинга: leaf'ы _bgi_blit_rows_raw (dst фикс, только src-страйд) / _bgi_copy_rows_raw (getimage) / _bgi_heal_rows_raw (src==dst). DI один на спрайт (санкция: малый спрайт под одним DI аудио не рвёт); src[0]-фикс снят (точная MAME подавляет CPU-байт триггера записи — на железе перепроверить; для heal был избыточен и снят безусловно). - Общие bracket-free ядра _gfx_blit_full/_gfx_heal_full (полная ширина: клип по экрану + split >256 для putimage) + лин _gfx_blit_sprite/ _gfx_heal_sprite (кадр ≤64, без split, 8-бит w/h) + noclip-варианты (клип-кода нет → полный codegen-win). Имя *_full (не *_clip) — «clip» двусмысленно (sprite-ядра тоже клипуют; различитель — ширина/split). - Движок retained-модели : sprite_init/update/flip + inline move/frame/show/hide/touch; drawn[2] per-page внутри структуры; кадр — двухпроходно heal ВСЕ -> блит ВСЕ под одной W3-скобкой/банком на проход. - Флаг gfx_sprite_clip(on/off): приложение, само следящее за границами, отключает клип (~+19% на анимации; диспетч пока через if — funcptr далее). - putsprite/movesprite/gfx_blit/putimage(COPY)/getimage переведены на ядро. Тесты: examples/balls (движок, дабл-буфер, boundary-тест клипа), tests/sprites (RAM PASS, клип 4 края, атлас), tests/blitw (trig-leak), tests/spriteclip (hardware-probe: железо НЕ режет за краем -> клип нужен), tests/blitperf, tests/gfxbanks. size-baseline обновлён (53 программы). Co-Authored-By: Claude Opus 4.8 --- docs/TODO.md | 117 ++++- docs/libc-reference.md | 26 +- docs/size_baseline.tsv | 8 +- docs/sprite-api-design.md | 594 ++++++++++++++++++++++++ examples/balls/Makefile | 4 + examples/balls/balls.c | 185 ++++++++ examples/balls/run.sh | 4 + libbgi/_bgi.h | 66 +++ libbgi/bgi256/_bgi_blit_rows_raw.c | 90 ++++ libbgi/bgi256/_bgi_copy_rows_raw.c | 130 ++++++ libbgi/bgi256/_bgi_heal_rows_raw.c | 72 +++ libbgi/common/_gfx_blit_full.c | 57 +++ libbgi/common/_gfx_blit_sprite.c | 69 +++ libbgi/common/_gfx_blit_sprite_noclip.c | 32 ++ libbgi/common/_gfx_heal_full.c | 36 ++ libbgi/common/_gfx_heal_sprite.c | 25 + libbgi/common/_gfx_heal_sprite_noclip.c | 13 + libbgi/common/_gfx_state.c | 7 + libbgi/common/getimage.c | 34 +- libbgi/common/gfx_blit.c | 14 + libbgi/common/gfx_blit_part.c | 33 ++ libbgi/common/gfx_heal.c | 29 ++ libbgi/common/movesprite.c | 31 ++ libbgi/common/putimage.c | 28 +- libbgi/common/putsprite.c | 25 + libbgi/common/sprite_flip.c | 22 + libbgi/common/sprite_init.c | 27 ++ libbgi/common/sprite_update.c | 68 +++ libbgi/include/gfx.h | 52 ++- libbgi/include/graphics.h | 23 +- libbgi/include/sprite.h | 107 +++++ tests/blitperf/Makefile | 4 + tests/blitperf/blitperf.c | 131 ++++++ tests/blitw/Makefile | 4 + tests/blitw/blitw.c | 136 ++++++ tests/gfxbanks/Makefile | 4 + tests/gfxbanks/gfxbanks.c | 259 +++++++++++ tests/spriteclip/Makefile | 4 + tests/spriteclip/spriteclip.c | 97 ++++ tests/sprites/Makefile | 4 + tests/sprites/sprites.c | 189 ++++++++ toolchain/mame_interactive.py | 7 +- 42 files changed, 2835 insertions(+), 32 deletions(-) create mode 100644 docs/sprite-api-design.md create mode 100644 examples/balls/Makefile create mode 100644 examples/balls/balls.c create mode 100755 examples/balls/run.sh create mode 100644 libbgi/bgi256/_bgi_blit_rows_raw.c create mode 100644 libbgi/bgi256/_bgi_copy_rows_raw.c create mode 100644 libbgi/bgi256/_bgi_heal_rows_raw.c create mode 100644 libbgi/common/_gfx_blit_full.c create mode 100644 libbgi/common/_gfx_blit_sprite.c create mode 100644 libbgi/common/_gfx_blit_sprite_noclip.c create mode 100644 libbgi/common/_gfx_heal_full.c create mode 100644 libbgi/common/_gfx_heal_sprite.c create mode 100644 libbgi/common/_gfx_heal_sprite_noclip.c create mode 100644 libbgi/common/gfx_blit.c create mode 100644 libbgi/common/gfx_blit_part.c create mode 100644 libbgi/common/gfx_heal.c create mode 100644 libbgi/common/movesprite.c create mode 100644 libbgi/common/putsprite.c create mode 100644 libbgi/common/sprite_flip.c create mode 100644 libbgi/common/sprite_init.c create mode 100644 libbgi/common/sprite_update.c create mode 100644 libbgi/include/sprite.h create mode 100644 tests/blitperf/Makefile create mode 100644 tests/blitperf/blitperf.c create mode 100644 tests/blitw/Makefile create mode 100644 tests/blitw/blitw.c create mode 100644 tests/gfxbanks/Makefile create mode 100644 tests/gfxbanks/gfxbanks.c create mode 100644 tests/spriteclip/Makefile create mode 100644 tests/spriteclip/spriteclip.c create mode 100644 tests/sprites/Makefile create mode 100644 tests/sprites/sprites.c diff --git a/docs/TODO.md b/docs/TODO.md index acc43cd..b10a06e 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -84,27 +84,110 @@ Quick wins: - [ ] кэширование block-size между burst'ами (accel помнит размер) Новые возможности: -- [ ] **пакетное чтение/запись массива байт через акселератор** — - отдельные ф-ии, которые в ОДНОМ вызове читают/пишут строку или - столбец пикселей (гориз./верт.), а не по пикселю через - _bgi_read_raw/_bgi_plot_raw. Аналогично тому, как сейчас рисуются - линии (_bgi_hspan_raw/_bgi_vspan_raw задают Port_Y и адрес один раз, - далее burst). Цель — ускорить блиттинг: getimage/putimage сейчас - зовут _bgi_read_raw/_gfx_getpixel256_raw по-пиксельно, из-за чего на - каждый пиксель повторяются out Port_Y + ld bc,(base) + bounds-check - (см. разбор _bgi_read_raw). Дать `_bgi_hread_raw`/`_bgi_hwrite_raw` - (+ vertical) с ABI как у fill-сегментов (HL=addr, B=len, E=y, буфер - в DE) и перевести getimage/putimage на них. -- [ ] `gfx_blit` / `gfx_blit_transparent` — block copy (LD L,L / LD A,A), - прозрачность через bank 0x58 («FF is transparent») +- [x] ~~пакетное чтение/запись массива байт через акселератор~~ — + сделано 2026-07-11 (Фаза A спрайтового дизайна, + docs/sprite-api-design.md): leaf `_bgi_copy_rows_raw` (accel + block-copy LD L,L, до 256 байт/burst, размер блока армируется + один раз) + ядро `gfx_blit_part` (клиппинг, полосы ≤256); + putimage(COPY_PUT) и getimage переведены (регресс tests/bgi_img + 1:1 с per-pixel эталоном, подрежимы банков на accel-пути — + tests/gfxbanks). +- [ ] **вертикальный copy-leaf `_bgi_copy_cols_raw`** (режим LD A,A — + вертикальная копия, Port_Y двигается сам как у vfill). Анализ + 2026-07-12: для СПРАЙТОВ требует column-major хранения (читать + линейный буфер вертикально нельзя — Port_Y не действует вне + видеоокна; референс docs/samples/balls пишет колонками и потому + выводит спрайт ТРАНСПОНИРОВАННЫМ — незаметно только на + симметричном шаре) → формат несовместим с getimage, не делать. + А вот **вертикальный HEAL формат-независим** (экран→экран) и + выгоден для узких высоких областей: L-полоска 2×16 при + горизонтальном движении спрайта = 2 burst'а вместо 16 (8× + меньше оверхеда); выбор ориентации по форме — как в + _gfx_rectfill256. Нюанс: между read и write колонки Port_Y + надо вернуть на y0 (стоп → OUT → ре-арм, ~20Т/колонку). + Делать вместе с L-strip оптимизацией heal. +- [ ] **паттерны через акселератор + FF-прозрачность** (идея 2026-07-11): + всё, что сейчас рисуется по-пиксельно из-за «дырок», можно гнать + burst'ами через банк 0x58 — дырки паттерна кодируются 0xFF и + отбрасываются железом на записи: + - стилизованные линии (_bgi_styled_line: DOTTED/DASHED/CENTER/ + USERBIT сейчас per-pixel): построить 16-байтовый шаблон строки + из 16-бит маски (бит=цвет, 0=0xFF) и повторять accel-copy; + - fill-паттерны (_bgi_fill_span, LTSLASH_FILL и пр.): 8-байтовые + строки-шаблоны 8×8 паттерна тем же способом; + - ВНИМАНИЕ: на MAME 0.283 запись FF через 0x58 портит теневое + ОЗУ (частичный скип, см. sprite-api-design «Результаты Фазы 0») + — включать после подтверждения полного подавления на железе, + либо через 0x5C + пере-heal. +- [x] ~~gfx_blit / putsprite / gfx_heal / movesprite~~ — Фаза B + сделана 2026-07-11 (docs/sprite-api-design.md §3): GFX_BANK_* + константы + gfx_blit/gfx_blit_part/gfx_heal в gfx.h, + putsprite/movesprite в graphics.h; проверено tests/sprites + (прозрачность, клиппинг 4 краёв, heal src==dst, чистый след + movesprite, атлас). Осталась Фаза C — пример-курсор. +- [ ] **managed-движок спрайтов v2** — sprite_t (drawn-координаты + per-page ВНУТРИ структуры) + retained-модель: + sprite_move/frame/show/hide меняют только state, + sprite_update(arr,n) делает «heal все → блит все» на текущей + draw-странице, sprite_flip — сахар с vsync+флипом; z-order = + индекс массива. Интегрирует спрайты со сменой страниц — + снимает с приложения учёт drawn[2][N] (источник багов, + examples/balls 2026-07-11). Полный эскиз: + docs/sprite-api-design.md §9.1. - [ ] `gfx_xor_rect` / `gfx_or_rect` / `gfx_and_rect` / `gfx_invert_rect` - [ ] шрифты ≠ 8×8: gfx_set_font_data(ptr,w,h,advance), proportional, 8×16/16×16, отдельный font_id API; font-quad для 640×256 (per-cell палитра через дескрипторы 0x0300..0x039F) Оптимизации (не сейчас): +- [x] ~~stride-арифметика в _bgi_copy_rows_raw — вон из горячего + цикла~~ — сделано 2026-07-12 (профиль examples/balls vs + docs/samples/balls): универсальный leaf — SMC-патч страйдов в + 8-битные add/adc-цепочки при входе (140Т → 73Т/строку, BC/push/ + pop/ex ушли); heal — отдельный `_bgi_heal_rows_raw` без адресной + арифметики вообще (77Т/строку против 204Т; референсный уровень). + Шар 16×16: ~9.6 → ~6.6 кТ (heal 16×173 + блит 16×236). Регресс: + 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). +- [ ] клип fast-path «прямоугольник целиком на экране» в gfx_blit_part + (4 сравнения вместо полного пути) — делается в _gfx_blit_full/ + _gfx_heal_full вместе с batch-пассом. +- [ ] размер: общий clip-хелпер для gfx_blit_part (528 Б) и gfx_heal + (362 Б) — клиппинг сейчас продублирован; кандидат −300..400 Б. - [ ] gfx_line через accel для пологих диагоналей (runs ≥ 4-5 px) - [ ] композитные примитивы с одним W3-swap на операцию +- [ ] ~~**спрайт-анимация: heal только открывшейся L-полоски**~~ — + ОТВЕРГНУТО 2026-07-12 для ПРОЗРАЧНЫХ спрайтов (наш случай). + L-полоска (bbox старой позиции минус новой) корректна ТОЛЬКО для + непрозрачного full-box спрайта: тогда зону перекрытия целиком + перекрашивает новый блит. У прозрачного спрайта (0xFF через + 0x5C) в перекрытии дырки нового кадра НЕ перекрывают старые + непрозрачные пиксели → на хвосте остаётся «полумесяц» старого + изображения ВНУТРИ bbox-перекрытия, куда L-полоска не достаёт. + Точный «новооткрытый» набор = old_opaque AND NOT new_opaque — + это heal-с-маской по форме, а не по bbox, на Z80 дороже самого + heal. Подтверждено чтением референса docs/samples/balls: + restore_bg лечит ПОЛНЫЙ 16×16 (ld b,16 + 16 байт/строку), + L-полоску не использует. Итог: heal остаётся full-box; экономия + только через batch-пасс (амортизация обвязки), не через L-полоску. + L-полоска годна лишь для непрозрачных тайлов фона — не спрайтов. + Дисциплина «heal ВСЕ → блит ВСЕ» по-прежнему обязательна (слияние + heal+блит per-sprite выкусывает соседа на перекрытии — проверено + examples/balls 2026-07-11). - [ ] **span-примитивы для узких прямоугольников**: при узкой стороне ≤ 8 линий chunked-rectfill проигрывает простому циклу _bgi_hspan_raw/_bgi_vspan_raw (подготовка+precompute ~340Т уходят @@ -134,6 +217,14 @@ Quick wins: Y..Y+15): в MAME 0.283 НЕ работает (2026-06-01). На железе: dual-write тест → если работает, кэшировать Port_Y в putpixel (~8× меньше OUT для Брезенхэма); если нет — вычистить из доков. +- [ ] **Подрежимы видеостраниц #50..#5F** (tests/gfxbanks + + tests/sprites, дизайн docs/sprite-api-design.md): перепрогнать + на железе и сверить со скриншотами MAME. Три открытых вопроса: (1) 0x58 — полное + подавление FF-записи (док/master-MAME) или FF протекает в + теневое ОЗУ (MAME 0.283)? (2) FF в VRAM — цвет 255 (MAME) или + display-подстановка фона из ОЗУ (тогда дешёвое стирание + FF-заливкой, tests/fferase)? (3) accel-путь = CPU-пути (в ПЛМ + пути физически разные). - [ ] fdmax: лимит манипуляторов и зависание 9-го OPEN — MAME vs железо. - [ ] CBL: щелчок перед первым проигрыванием звука за сессию (tests/ cbltest, tests/cblwav) — воспроизводится ТОЛЬКО на первом запуске diff --git a/docs/libc-reference.md b/docs/libc-reference.md index 6bf493f..4821d60 100644 --- a/docs/libc-reference.md +++ b/docs/libc-reference.md @@ -190,8 +190,13 @@ wrchar rdchar`; режимы `gettextmode/settextmode` (0x02=40×32, Setup: `gfx_init(mode,page)→prev`, `gfx_done(prev)`. Страницы/банк: `gfx_set/get_visible_page`, `gfx_set/get_draw_page` -(double buffering), `gfx_set/get_bank` (0x50..0x5F, 0x58 = FF- -прозрачность). +(double buffering), `gfx_set/get_bank` (GFX_BANK_NORMAL 0x50 / +NOSHADOW 0x54 / TRANSPARENT 0x58 / SPRITE 0x5C — аппаратные подрежимы +записи, действуют на ВСЕ примитивы; см. docs/sprite-api-design.md). +Блиттинг (Фаза B, 2026-07-11): `gfx_blit(x,y,img)`, +`gfx_blit_part(x,y,img,sx,sy,w,h)` (атлас), `gfx_heal(x,y,w,h)` +(восстановить фон из ОЗУ-копии; стирание спрайтов без save-буфера); +img — getimage-формат, буфер вне W3; клиппинг по экрану есть. `gfx_wait_vsync()` (2026-07-07, редизайн): ждёт переход бита 5 порта 0xFE из 1 в 0 — реальное аппаратное состояние луча (Y>256 → начало @@ -234,8 +239,21 @@ CP), `line(x1,y1,x2,y2)` (не двигает). `fillpoly(n,pts)`, `fillellipse(x,y,xr,yr)`. Заливка областей (Ф2c): `floodfill(x,y,border)` (медленно, но верно), `pieslice(x,y,st,end,r)`, `sector(x,y,st,end,xr,yr)`. -Спрайты (Ф2d): `imagesize/getimage/putimage` (COPY/XOR/OR/AND/NOT_PUT; -формат буфера: uint16 w,h + w*h байт). +Образы (Ф2d): `imagesize/getimage/putimage` (COPY/XOR/OR/AND/NOT_PUT; +формат буфера: uint16 w,h + w*h байт). COPY_PUT и getimage — через +accel block-copy (2026-07-11, Фаза A sprite-api-design), COPY с +клиппингом и текущим банком; XOR/OR/AND/NOT — per-pixel. +Спрайты (2026-07-11, Фаза B sprite-api-design): `putsprite(x,y,img)` — +блит getimage-буфера с аппаратной прозрачностью (0xFF = +GFX_TRANSPARENT не пишется) банком GFX_BANK_SPRITE (0x5C, фон в +ОЗУ-копии цел); `movesprite(ox,oy,x,y,img)` — heal старой позиции + +putsprite новой (save-буфер не нужен). Низкий уровень в : +`gfx_blit(x,y,img)`, `gfx_blit_part(x,y,img,sx,sy,w,h)` (атлас +кадров), `gfx_heal(x,y,w,h)` (восстановление фона из ОЗУ-копии), +константы GFX_BANK_NORMAL/NOSHADOW/TRANSPARENT/SPRITE. Правило: фон +рисовать банком 0x50, спрайты/оверлеи — putsprite/0x5C; буферы +образов — вне W3 (< 0xC000). Тесты: tests/sprites, tests/gfxbanks, +tests/bgi_img. Стиль линий (Ф2d): `setlinestyle(style,upattern,thick)`/`getlinesettings` (SOLID/DOTTED/CENTER/DASHED/USERBIT + NORM/THICK) — на line/rectangle/ drawpoly. diff --git a/docs/size_baseline.tsv b/docs/size_baseline.tsv index d913ca6..3598cff 100644 --- a/docs/size_baseline.tsv +++ b/docs/size_baseline.tsv @@ -3,13 +3,16 @@ accfill 3772 argv 3431 assrtest 3847 attrprob 4087 +balls 6393 banked 1056 bankedbg 1067 banklocl 4683 banktest 3767 -bgi_img 6222 +bgi_img 7060 bgitest 3737 bios_text 4461 +blitperf 5860 +blitw 4939 cat 913 cblstream 6217 cbltest 6366 @@ -24,6 +27,7 @@ filetest 10547 gets 509 gfx_dbuf 5032 gfx_demo 4103 +gfxbanks 6044 hello 4167 hello2 4244 irqtest 5850 @@ -39,6 +43,8 @@ rt_test 4892 seek 4188 simple 955 solidt 11531 +spriteclip 4187 +sprites 9968 stattest 7549 stdlib 6643 stest2 3644 diff --git a/docs/sprite-api-design.md b/docs/sprite-api-design.md new file mode 100644 index 0000000..1a3c63a --- /dev/null +++ b/docs/sprite-api-design.md @@ -0,0 +1,594 @@ +# Спрайтовое расширение BGI — дизайн + +Статус: дизайн на ревью (2026-07-11). Реализация не начата. + +Расширение libbgi для спрайтовой графики на аппаратных подрежимах +видеостраниц `#50..#5F` + переписывание putimage/getimage через +акселератор (одно ядро на всё). + +Зафиксированные решения (обсуждены 2026-07-11): +1. **Два слоя**: быстрое ядро в `` (`gfx_*`), тонкие BGI-обёртки + в `` (`putsprite`/`movesprite`, ускоренный `putimage`). +2. **Примитивы + move-хелпер**, без managed-движка (таблица спрайтов, + z-order — «следующая версия», см. §9). +3. **Формат данных — getimage** (uint16 w, uint16 h, пиксели построчно). + Единственный in-memory формат; атласы — через блит + под-прямоугольника, а не через новый формат. +4. **Курсор мыши** — пример + MAME-тест на новых примитивах, НЕ в + библиотеке. + +## 1. Аппаратная база + +### 1.1 Подрежимы вывода (Иван Мак §4.3, Architecture) + +Страницы `#50..#5F` — графическая видео-область. Биты 3..0 номера +страницы на АДРЕС не влияют (адрес задают PORT_Y `0x89` + 10 младших +бит CPU-адреса); биты 2 и 3 задают независимые подрежимы ЗАПИСИ. +Биты 0 и 1 обязаны быть 0 (зарезервированы под будущие конфигурации). + +| Банк | bit3 | bit2 | Семантика записи | +|-------|------|------|------------------------------------------------------| +| 0x50 | 0 | 0 | обычная: в видео-ОЗУ **и** в ОЗУ-копию | +| 0x58 | 1 | 0 | байт `0xFF` **не записывается вообще** (прозрачность)| +| 0x54 | 0 | 1 | **только** в видео-ОЗУ; ОЗУ-копия не трогается | +| 0x5C | 1 | 1 | оба эффекта — режим подвижного спрайта | + +Ключевые факты: + +- **Прозрачность (bit3) — на пути ЗАПИСИ.** «В процессе записи + проверяется, не равен ли записываемый байт значению #FF. Если + равен, то запись не производится» (официальный док). Проверку + делает ПЛМ — блиттеру не нужно смотреть ни одного байта. + ОТКРЫТЫЙ вопрос (док молчит): как видеокарта ОТОБРАЖАЕТ байт FF, + реально попавший в VRAM (записанный через банк без bit3) — как + обычный цвет 255 или подставляет фон из ОЗУ-копии? Проверяется + шагом 3 теста Фазы 0; от ответа зависит возможность дешёвого + стирания FF-заливкой (tests/fferase). +- **Чтение из `#50..#5F` всегда возвращает ОЗУ-копию** — видео-ОЗУ + write-only для CPU. Отсюда «heal»: в банке 0x50 прочитать + прямоугольник (придёт фон из ОЗУ) и записать обратно (уйдёт в + видео-ОЗУ) — фон под спрайтом восстановлен **без save-буфера**. +- Подрежимы комбинируются с акселератором (то же оборудование ПЛМ); + это их заявленное назначение — «ускорение работы со спрайтовой + графикой». Подтвердить в MAME — Фаза 0. + +### 1.2 Accel block-copy (accelerator_doc.txt) + +Режим `LD L,L` (горизонтальная копия): после задания размера блока +(`LD D,D` + immediate у `LD A,n` — SMC, см. memory/sprinter_accelerator) +триггер `LD A,(HL)` burst-читает блок в память акселератора, триггер +`LD (DE),A` burst-пишет его по DE. До 256 байт за выстрел, источник и +приёмник — любое ОЗУ `0x0000..0xBFFF` + видеоокно (не ПЗУ/FastRAM). +Размер блока переживает `LD B,B` (подтверждено tests/accfill) — задаём +один раз на блит, дальше по паре триггеров на строку. + +Канонический референс — `Draw_Restangle_Data` из accelerator_doc.txt: +источник (строки подряд, шаг = ширина), приёмник — один и тот же +CPU-адрес, строку выбирает PORT_Y (инкремент на строку). **Это ровно +layout формата getimage** — совпадение формата и железа буквальное. + +## 2. Одно ядро на четыре операции + +Вся разница между операциями — какой банк в W3 и куда смотрят src/dst: + +| Операция | src → dst | Банк W3 | +|-----------------------|-----------------------------|----------------| +| `putimage(COPY)` | буфер → экран | текущий (0x50) | +| `putsprite` | буфер → экран | 0x5C | +| `getimage` | экран (ОЗУ-копия) → буфер | любой | +| `gfx_heal` | экран → экран (src == dst) | 0x50 (форс) | + +Отдельного «спрайтового блиттера» нет: прозрачный блит — это обычный +блит с банком 0x58/0x5C. heal — это блит, у которого src-указатель +лежит в самом видеоокне (чтение вернёт ОЗУ-копию) и совпадает с dst; +шаг строк источника = 0, потому что строку и там и там выбирает один +PORT_Y. + +## 3. Публичный API + +### 3.1 `` — ядро + +```c +/* Семантические имена банков для gfx_set_bank (значения — номер + * страницы W3; биты 0,1 всегда 0 — резерв конфигураций): */ +#define GFX_BANK_NORMAL 0x50 /* запись в видео-ОЗУ + ОЗУ-копию */ +#define GFX_BANK_NOSHADOW 0x54 /* только видео-ОЗУ (врем. вывод) */ +#define GFX_BANK_TRANSPARENT 0x58 /* байт 0xFF не записывается */ +#define GFX_BANK_SPRITE 0x5C /* NOSHADOW + TRANSPARENT */ + +#define GFX_TRANSPARENT 0xFF /* прозрачный цвет (256-режим: пиксель; + 16-режим: ПАРА пикселей цвета 15) */ + +/* Блит картинки getimage-формата в (x,y) ТЕКУЩИМ банком (gfx_set_bank). + * Клиппинг по экрану есть (в отличие от putimage Turbo C). + * Требование: img вне W3 (адрес < 0xC000) — W3 занят видеобанком. */ +void gfx_blit(int x, int y, const void *img); + +/* Блит под-прямоугольника картинки (атлас кадров): прямоугольник + * (sx,sy,w,h) внутри img выводится в (x,y). Под-прямоугольник обязан + * лежать внутри img (по экрану — клиппится). */ +void gfx_blit_part(int x, int y, const void *img, + int sx, int sy, int w, int h); + +/* Восстановить прямоугольник экрана из ОЗУ-копии (стирание спрайта/ + * оверлея). Всегда работает банком 0x50 независимо от gfx_set_bank. + * Клиппится по экрану. */ +void gfx_heal(int x, int y, int w, int h); +``` + +Заметки по семантике: + +- `gfx_blit*` уважают текущий `gfx_set_bank` — так же, как уже сегодня + его уважают ВСЕ примитивы через `_bgi_begin` (т.е. `gfx_set_bank + (GFX_BANK_NOSHADOW); line(...)` — легальный способ рисовать временную + линию-перекрестье). Спрайтовые обёртки (§3.2) ставят банк сами на + время вызова и восстанавливают. +- Клиппинг в ядре всегда, в т.ч. в fast-версии: это не валидация + параметров, а функциональность (спрайты штатно уходят за края). + Цена — один расчёт на блит, не на пиксель. +- Цвет 255 через 0x58/0x5C нарисовать нельзя — он и есть прозрачный. + В initgraph палитра 0..15 занята EGA, 255 рекомендуется не занимать. + +### 3.2 `` — BGI-обёртки + +```c +/* Вывести спрайт (getimage-формат, 0xFF = прозрачно) в (x,y). + * Банк на время вызова — GFX_BANK_SPRITE (0x5C): прозрачные точки не + * пишутся, фон в ОЗУ-копии не портится → стирается gfx_heal'ом / + * movesprite'ом. Клиппится по экрану. */ +void putsprite(int x, int y, const void *img); + +/* Переместить спрайт: heal прямоугольника (oldx,oldy,w,h) по размерам + * img + putsprite в (x,y). Порядок heal→draw; перекрытие старой и + * новой позиций безопасно (heal берёт фон из ОЗУ-копии). */ +void movesprite(int oldx, int oldy, int x, int y, const void *img); +``` + +`putimage`/`getimage`/`imagesize` — сигнатуры и формат без изменений, +`putimage(COPY_PUT)` и `getimage` переезжают на accel-ядро (§5). + +Хотспот и кадры атласа НЕ кодируются в данных — это аргументы вызова: +`putsprite(x - HOT_X, y - HOT_Y, img)`; +кадр N — `gfx_blit_part(x, y, sheet, n*FRAME_W, 0, FRAME_W, FRAME_H)` +(банк выставить `GFX_BANK_SPRITE` вокруг — или см. §9 про +`putsprite_part`). + +## 4. Внутренности + +### 4.1 Leaf-примитив (per-driver, asm) + +Один новый leaf в `bgi256/` (и позже `bgi16/`), объявление в `_bgi.h`: + +```c +/* Копирование h строк по w пикселей через accel block-copy. + * dst/src — CPU-адреса ПЕРВОЙ строки; dstride/sstride — шаг адреса + * между строками (0 = адрес не двигается, строку выбирает PORT_Y — + * сторона, живущая в видеоокне); y0 — стартовый PORT_Y (инкремент + * на строку). Raw: без клиппинга, W3 уже замаплен (_bgi_begin), + * вызывающий гарантирует 1 <= w <= 256 (полоса). + * DI/EI — вокруг каждой пары триггеров (как в fill-сегментах). */ +void _bgi_copy_rows_raw(uint8_t *dst, const uint8_t *src, + uint8_t w /*0=256*/, uint8_t h /*0=256*/, + int dstride, int sstride, uint8_t y0); +``` + +Схема тела (референс — `Draw_Restangle_Data` + существующий SMC-паттерн +`_gfx_hfill256_segment`): + +``` + SMC: размер блока <- w (LD D,D ; LD A,#imm ; LD B,B — один раз) + loop h раз: + out (0x89), y ; y++ + di + LD L,L ; режим копии + LD A,(HL) ; burst-чтение src + LD (DE),A ; burst-запись dst + LD B,B ; стоп + ei + HL += sstride ; DE += dstride +``` + +Один leaf покрывает все четыре операции §2: + +| Вызов | dst, dstride | src, sstride | +|--------------|-------------------------|----------------------------| +| блит | экран (base+x), 0 | буфер (row0), img_w | +| getimage | буфер, w | экран (base+x), 0 | +| heal | экран (base+x), 0 | тот же адрес экрана, 0 | + +Регистровая раскладка — на этапе реализации; ориентир: HL=src, DE=dst +внутри цикла (триггеры именно такие), w через SMC, остальное со стека +в локальные регистры/IX (вызывается один-два раза на блит — не горячий +ABI, в отличие от fill-сегментов). + +### 4.2 Ядро (common/) + +`gfx_blit_part` — единственная «умная» функция (реальная логика): + +1. прочитать w,h из заголовка img (для gfx_blit: весь rect); +2. клиппинг: x<0 → сдвиг sx и сужение w; правый/нижний край → сужение; + пустой результат → выход; +3. `src0 = img + 4 + sy*img_w + sx`; +4. полосы: если w > 256 — разрезать на вертикальные полосы ≤ 256 байт + (при экране 320 полос максимум две; каждая полоса = один SMC размера + блока); +5. `_bgi_begin()` → `_bgi_copy_rows_raw(...)` на полосу → `_bgi_end()`. + +Остальное — тонкие модули (1 функция = 1 .rel): + +- `gfx_blit.c` — читает w,h, зовёт gfx_blit_part(x,y,img,0,0,w,h); +- `gfx_heal.c` — клип; банк: сохранить `_gfx_bank`, форс 0x50, + `_bgi_begin`; leaf с dst=src=base+x, strides 0; восстановить банк; +- `putsprite.c` — сохранить `_gfx_bank`, `|` → 0x5C, `gfx_blit`, + восстановить (именно `save/restore`, а не тупо 0x50 — уважаем + вложенность и пользовательский temp-режим); +- `movesprite.c` — `gfx_heal(oldx,oldy,w,h)` (w,h из заголовка img) + + `putsprite(x,y,img)`. + +### 4.3 Совместимость с существующим кодом + +- `_bgi_begin/_bgi_end` не меняются (банк уже берут из `_gfx_bank`). +- `gfx_set_bank/gfx_get_bank` не меняются; в `gfx.h` добавляются только + `GFX_BANK_*`/`GFX_TRANSPARENT` и три прототипа. +- Довесок к контракту (задокументировать у putimage/getimage тоже): + буферы картинок обязаны лежать вне W3 (`< 0xC000`) — на время + операции W3 замаплен на видеобанк. Это верно и сегодня (per-pixel + путь), просто не было записано. +- Второй draw-page: ядро использует `_gfx_addr_base` — двойная + буферизация работает автоматически. + +## 5. Переезд putimage/getimage на ядро + +- `putimage(COPY_PUT)` → `gfx_blit` (текущим банком — поведение + обратно-совместимо: дефолтный банк 0x50). +- `getimage` → `_bgi_copy_rows_raw` (dst=буфер). Клиппинга нет, как в + BGI (контракт: rect валиден); safe-версия сохраняет текущие проверки. +- `XOR/OR/AND/NOT_PUT` — остаются на per-pixel пути (v1). У + акселератора есть блочные AND/OR/XOR (пример «encode» в + accelerator_doc.txt) — ускорение этих op — «следующая версия» (§9). +- `imagesize` — без изменений (256-режим); для 16-режима станет + mode-specific (§8). + +Ожидание по скорости: per-pixel путь платит на КАЖДЫЙ пиксель +`out Port_Y` + пересчёт адреса + bounds-check (~50–80Т); ядро платит на +СТРОКУ ~40–60Т CPU + burst акселератора (~байт/7МГц). На спрайте 16×16 +это порядка 10–20× (замерить в Фазе A, добавить в size-baseline). + +## 6. Паттерны использования (войдут в libc-reference) + +Подвижный спрайт (канон): + +```c +initgraph(); +/* фон рисуем обычным банком — он попадает и в ОЗУ-копию (бэкап) */ +draw_background(); +putsprite(x, y, hero); /* 0x5C: фон в ОЗУ цел */ +while (game) { + int nx = x + dx, ny = y + dy; + gfx_wait_vsync(); + movesprite(x, y, nx, ny, hero); /* heal старого + блит нового */ + x = nx; y = ny; +} +``` + +Курсор мыши (пример examples/, не API): то же самое с +`GFX_BANK_SPRITE`; фон под курсором живёт в ОЗУ-копии, никакой +getimage/буфер не нужен. Перерисовка — из главного цикла по +`mouse_getxy()`. + +Впечатать спрайт в фон навсегда (декорация): +`gfx_set_bank(GFX_BANK_TRANSPARENT); gfx_blit(...); gfx_set_bank(GFX_BANK_NORMAL);` +— 0x58 без bit2: спрайт уходит и в ОЗУ-копию, heal его уже «не сотрёт». +ВНИМАНИЕ: на MAME 0.283 этот паттерн ломается — FF-байты спрайта +попадают в ОЗУ-копию (частичный скип, см. результаты Фазы 0); +до подтверждения на железе печатать декорации без FF в данных. + +ВАЖНО про cleardevice/bar поверх спрайтов: пока действует банк с bit2 +(0x54/0x5C), «фоновые» операции не обновляют ОЗУ-копию. Правило: +сцена/фон — только обычным банком (или 0x58), спрайты/оверлеи — 0x5C. + +## 7. План работ + +- **Фаза 0 — верификация подрежимов** (`tests/gfxbanks`). + ВНИМАНИЕ: поведение подрежимов в MAME может отличаться от реального + устройства (прецедент — Port_Y banking, memory/gfx_port_y_banking). + Результат в MAME НЕ финален: тест гоняется и в MAME, и на железе; + до прогона на железе вердикты считаются предварительными. + Последовательный сценарий на одном экране, скриншот после каждого + шага (палитра[255] = синий; фон — узнаваемый паттерн, не заливка): + 1. **Фон** через 0x50 (уходит и в VRAM, и в ОЗУ-копию). + 2. **Спрайт через 0x5C** (bit2+bit3): квадрат, внутри зелёный круг, + вокруг круга — байты 0xFF. Ожидание по доку: виден круг поверх + фона, FF-точки скипнуты (фон вокруг круга цел). Проверяет + bit3-скип на CPU-пути; если вместо фона вокруг круга синяя рамка — + bit3 не эмулируется/не работает. + 3. **Ключевой шаг — FF реально в VRAM**: через 0x54 (bit2, БЕЗ bit3 — + скипа нет, FF пишется) залить квадрат байтом 0xFF поверх круга. + Что отображается в квадрате: + - **синий** → FF в VRAM — обычный цвет 255, подстановки при + отображении нет (семантика дока полная) → стирание только heal; + - **фон** → видеокарта подставляет байт из ОЗУ-копии, когда в + VRAM лежит FF → «слой спрайтов» существует на уровне + отображения, дешёвое стирание FF-заливкой работает + (tests/fferase становится штатным путём); + - **круг остался** → запись через 0x54 не произошла — bit2/банк + не эмулируется. + 4. **Heal** через 0x50 (чтение+запись того же rect): фон должен + восстановиться полностью. Заодно различает bit2: если bit2 не + работал, шаги 2–3 испортили ОЗУ-копию и heal вернёт не фон. + 5. **Accel-путь**: повторить шаги 2–3 записью через акселератор + (copy/fill) — работает ли bit3-скип и поведение FF на burst-пути. + Если MAME не эмулирует биты 2/3 — API остаётся как есть (семантика + задана железом/доком), но функциональные тесты уезжают в раздел + «проверить на железе» TODO, а пример курсора делаем без bit2 + (fallback: save/restore через getimage). + + **РЕЗУЛЬТАТЫ (MAME 0.283 = сборка v306, 2026-07-11; железо — TODO):** + | Вопрос | Вердикт в MAME | + |---|---| + | bit2 (0x54/0x5C): ОЗУ-копия не трогается | РАБОТАЕТ (A: PASS, B: PASS) | + | bit3 через 0x5C: скип FF | РАБОТАЕТ, полный no-op (CPU и accel) | + | bit3 через 0x58: скип FF | **ЧАСТИЧНО**: VRAM скипается, но теневое ОЗУ ПОЛУЧАЕТ FF (C: FAIL) — код драйвера 0.283 гасит только vram_w; в master-драйвере исправлено на полное подавление (по доку) | + | Отображение FF в VRAM (шаг 3) | обычный **цвет 255** (синий), подстановки ОЗУ нет → стирание = heal; FF-заливка НЕ работает | + | heal (чтение ОЗУ + запись 0x50) | РАБОТАЕТ (полосы восстановлены; FF-порчу ОЗУ честно переносит в VRAM) | + | accel-путь vs CPU-путь | идентичны во всех подрежимах (в MAME оба через ram_w) | + + Следствия: (1) putsprite через 0x5C работает одинаково во всех + версиях — расхождение 0.283 не задевает; (2) паттерн «впечатать + декорацию через 0x58» на 0.283 портит ОЗУ-копию FF-байтами спрайта — + использовать только после проверки на железе; (3) tests/fferase в + MAME заведомо FAIL — остаётся для прогона на железе. + Квирк инфраструктуры: delayms/sleep в --memory small не работают + (irq_install → EINVAL, калибровка молча не происходит) — паузы в + тесте сделаны по RTC (getdatetime). +- **tests/fferase — альтернативное стирание FF-заливкой через 0x54** + (существует ДО подтверждения итога шага 3 и на MAME, И на железе; + после — либо удаляется, либо становится основой оптимизации). + Сценарий: фон → спрайт через 0x5C → залить прямоугольник спрайта + байтом 0xFF через 0x54 (bit3=0 — FF реально пишется в VRAM) → + скриншот: восстановился ли фон. Если да (на железе!) — стирание + вдвое дешевле heal (fill-burst без фазы чтения, готовый + _gfx_rectfill256), реализацию gfx_heal можно переключить, сигнатура + и movesprite не меняются. Основной механизм move в любом случае — + heal (чтение/запись через 0x50): он работает при ОБЕИХ семантиках. +- **Фаза A — ядро + переезд** (без нового публичного API, чистое + ускорение): `_bgi_copy_rows_raw` (bgi256), `gfx_blit_part` + полосы + + клиппинг, порт putimage(COPY)/getimage. Регресс — tests/bgitest + + `make size-check`. + + **ГОТОВО 2026-07-11**: leaf `bgi256/_bgi_copy_rows_raw.c` (регистры + BC = счётчик строк/y, stride-сложения через push bc; SMC размера + блока один раз на вызов), ядро `common/gfx_blit_part.c` (клиппинг + всегда, sy без __mulint — циклом сложений, полосы ≤256), putimage + (COPY_PUT → ядро; XOR/OR/AND/NOT — прежний per-pixel путь) и + getimage (grab-leaf, dstride = w) переведены. Прототип + gfx_blit_part пока в _bgi.h (Фаза B перенесёт в gfx.h как есть). + Проверено в MAME: tests/bgi_img финальный кадр 1:1 с per-pixel + эталоном (src == COPY, XOR×2 чист); tests/gfxbanks — прозрачность + FF и bit2 работают на accel-COPY пути идентично CPU-пути (круг с + прозрачными полями через putimage/0x5C, FF→VRAM через putimage/0x54). + Размер: bgi_img +592 Б (_CODE; leaf + клиппинг-ядро + ветка COPY), + baseline принят. Попутно: pgrep-фильтр в mame_interactive.py сужен + до эмулятора (ложно срабатывал на параллельную сборку MAME из + исходников). +- **Фаза B — спрайтовый API**: GFX_BANK_*, gfx_blit, gfx_heal, + putsprite, movesprite; тест tests/sprites (анимация по синусоиде + поверх пёстрого фона, скриншоты «фон не разрушен»). + + **ГОТОВО 2026-07-11**: константы GFX_BANK_*/GFX_TRANSPARENT и + gfx_blit/gfx_blit_part/gfx_heal — в gfx.h; putsprite/movesprite — в + graphics.h; модули common/gfx_blit.c, gfx_heal.c, putsprite.c, + movesprite.c (тонкие, поверх ядра Фазы A). putsprite/gfx_heal + сохраняют и восстанавливают пользовательский банк (heal — форс + 0x50, putsprite — форс 0x5C на время вызова). Проверено в MAME + (tests/sprites): прозрачные углы, клиппинг во все 4 края, + heal-стирание (accel src==dst — риск §10.4 закрыт), movesprite + 12 шагов с чистым следом, кадр атласа через gfx_blit_part; ОЗУ-копия + цела (проверки A/B PASS). size-check: роста существующих программ + нет (новые модули тянутся только пользователями API). + + Демо **examples/balls** (2026-07-11): 8 разноцветных шаров 16×16 + (по спрайту на цвет, прозрачные углы) над чёрно-белой шахматкой; + скорости 1..4 привязаны к кадрам (gfx_wait_vsync, speed = кадров на + шаг: 50/25/~17/12.5 px/с — подтверждено покадровыми скриншотами), + отражение от краёв, Esc — выход. **Двойная буферизация** по образцу + docs/samples/balls (реф. asm-демо Sprinter): рисование только на + скрытой странице (heal ВСЕХ старых позиций этой страницы → блит + ВСЕХ шаров), флип по vsync — одиночная страница мерцала (heal+блит + на видимой ловились лучом; пользователь заметил, реф не мерцает). + Требует зеркалирования палитры в палитру 1 (initgraph грузит EGA + только в 0) и координат drawn[2][N] per-page. Побочно ушли и + одно-кадровые артефакты перекрытий/снапшотов. +- **Фаза C — пример курсора** (examples/): мышь + putsprite/gfx_heal, + он же живой тест temp-режима. +- Документация: libc-reference (раздел «Спрайты»), обновить TODO. + +## 8. Режим 16 цветов (Фаза 2 libbgi — заметки на будущее) + +- Прозрачная единица — БАЙТ = пара пикселей цвета 15 (0xFF). Фигурные + края спрайта квантуются парами; прозрачного «одиночного» пикселя нет. +- Leaf `bgi16/_bgi_copy_rows_raw`: те же триггеры, адресация x/2; + клиппинг и координаты x — по чётным границам (нечётные края потребуют + RMW-кромок per-row, v1 16-режима: требовать чётные x/w). +- `imagesize`/формат: данные packed (2 пикселя/байт) — imagesize станет + mode-specific leaf'ом. + +## 9. Не в этой версии (кандидаты в следующую) + +- **Managed-движок**: см. эскиз §9.1 (sprite_t + retained-модель). +- **putsprite_part** (кадр атласа одним вызовом, без ручного банка) — + добавить, как только появится первый пользователь-игра. +- **.spr-ресурс**: файловый заголовок-обёртка (магия/версия/hotspot) + вокруг getimage-payload + загрузчик + PC-конвертер (PNG→spr, iconv- + стиль пайплайн). In-memory формат не меняется. +- **XOR/OR/AND_PUT через акселератор** (блочные режимы есть в железе). +- **RLE-сжатие** (распаковка при загрузке — фича загрузчика). +- **Вертикальный блит** (`LD A,A`-режим) — для повёрнутых спрайтов не + хватает и его; не тянем. +- **ISR-курсор** (перерисовка из IM2-тика) — после Audio/IM2 v2. + +## 9.1 Эскиз managed-движка: sprite_t + retained-модель (v2, НЕ реализовано) + +Мотивация — опыт examples/balls (2026-07-11): при двойной буферизации +приложение обязано помнить, где каждый спрайт РЕАЛЬНО нарисован на +КАЖДОЙ из двух страниц (drawn[2][N]), и соблюдать двухпроходную +дисциплину «heal все → блит все». Оба правила легко нарушить (heal по +позиции прошлого кадра вместо позапрошлого → фон зарастает кромками; +слияние heal+блит per-sprite → укусы на перекрытиях — проверено +экспериментально). Этот учёт — работа библиотеки, не приложения. + +Разбор asm-референса (docs/samples/balls, 2026-07-12) подтвердил +направление: наш ~2× разрыв — не W3-скобка (_bgi_begin = 5 инструкций), +а per-call C-обвязка (клип 528/362 Б, парс заголовка, save/restore +банка, SDCC-фрейм 7-арг), повторяемая 2×N раз. Референс платит это +ОДИН раз на проход: W3 замаплен на видео на всю программу, банк-подрежим +ставит один `out` на проход (0x5C рисовать всё → флип → 0x50 лечить +всё), клипа нет вообще (адрес спрайта = сдвиги), heal — полный 16×16 +(restore_bg). Движок v2 повторяет ту же per-pass структуру (одна +W3-скобка + один банк + клип-fast-path на проход; w,h из кэша структуры). +ЕДИНСТВЕННОЕ отличие, которое НЕ копируем: референс держит DI на весь +проход (32 шара) — у него нет аудио-ISR; у нас DI остаётся гранулярным +(по строке в leaf), иначе длинный DI сорвёт CBL/IM2-звук. + +### Структура (черновик) + +```c +typedef struct { + const void *img; /* getimage-формат / атлас-лента */ + int x, y; /* ЛОГИЧЕСКАЯ позиция (куда хочет) */ + int sx, sy; /* кадр атласа (под-прямоугольник img) */ + uint8_t w, h; /* размер кадра, 0 = 256 (кэш заголовка) */ + uint8_t flags; /* bit0 SPR_VISIBLE; bit1/2 dirty[page] */ + /* --- внутреннее (владеет движок) --- */ + struct { + int x, y; /* где нарисован на странице p */ + uint8_t on; /* нарисован ли вообще */ + } drawn[2]; +} sprite_t; /* ~24 байта на спрайт */ +``` + +Инварианты: `img` живёт, пока спрайт активен; размер кадра постоянен +(смена кадра = смена sx/sy в той же ленте); прозрачность — 0xFF в +данных, как везде. + +### API (черновик; префикс sprite_, заголовок в libbgi) + +```c +void sprite_init (sprite_t *s, const void *img); /* w,h из заголовка, + невидим, не рисует */ +void sprite_move (sprite_t *s, int x, int y); /* O(1): только state */ +void sprite_frame(sprite_t *s, int sx, int sy); /* кадр атласа, O(1) */ +void sprite_show (sprite_t *s); /* O(1) */ +void sprite_hide (sprite_t *s); /* O(1) */ +void sprite_touch(sprite_t *s); /* O(1): принудительная + перерисовка на обеих + страницах (dirty) */ +/* flags: SPR_ALWAYS — перерисовывать каждый кадр (эквивалент touch + * на каждом кадре; для статики, над которой постоянно ходят). */ + +/* Вся реальная работа — один вызов на кадр. Рисует на ТЕКУЩЕЙ + * draw-странице p (для дабл-буфера вызывать после + * gfx_set_draw_page(hidden); для одиночной страницы/курсора — прямо + * на видимой): + * проход 1: heal всех, у кого drawn[p].on и (скрыт ИЛИ сместился + * относительно drawn[p] ИЛИ dirty[p]); + * проход 2: блит всех видимых из них же (gfx_blit_part через 0x5C) + * в порядке массива (индекс = z-order, последний сверху); + * обновить drawn[p]/dirty[p]. + * Спрайты, не менявшиеся с прошлого визита этой страницы (и без + * touch/SPR_ALWAYS), не трогаются. */ +void sprite_update(sprite_t *arr, uint8_t count); + +/* Сахар поверх update для канонического дабл-буфер-кадра: + * set_draw_page(hidden) + sprite_update + gfx_wait_vsync + + * set_visible_page(hidden). Приложение, рисующее свой HUD/фон, + * зовёт составные части само (HUD банком 0x50 НА СКРЫТОЙ странице — + * тогда heal учитывает его автоматически через ОЗУ-копию). */ +void sprite_flip(sprite_t *arr, uint8_t count); +``` + +Пример (balls сжимается до): + +```c +sprite_t sp[N]; +... sprite_init/sprite_move/sprite_show ... +for (;;) { + for (i...) sprite_move(&sp[i], new_x, new_y); /* физика */ + sprite_flip(sp, N); /* весь кадр */ +} +``` + +### Решения и границы + +- **Retained-модель**: move/frame/show/hide меняют только state (O(1), + сколько угодно раз за кадр) — рисование один раз, в update. Это + железно закрепляет дисциплину двух проходов внутри библиотеки. +- **Пересечения со статикой — ответственность приложения** (touch / + SPR_ALWAYS), автодетекта НЕТ — осознанно: heal активного спрайта + выкусывает статичный под ним, но автоматическое обнаружение — это + не только O(N²) rect-проверок; для корректного z-порядка пришлось бы + перерисовывать и пересекающих НОВЫЕ позиции, а перерисовка статика + полным блитом затирает спрайты выше него уже ВНЕ грязной зоны → + транзитивный каскад либо клиппинг блита по произвольной области. + На Z80 это дороже самого рисования; приложение же знает сцену и + помечает дёшево (обсуждено с пользователем 2026-07-11: ручное + назначение «активных» выгоднее координатных проверок). +- **Массив, а не handle-таблица**: владеет приложение, без malloc и + лимитов; z-order = индекс. Пересортировка z = перестановка массива + (drawn-состояние едет вместе со структурой — безопасно). +- **Интеграция со страницами**: движок НЕ владеет флипом (sprite_flip + лишь сахар) — приложение может рисовать динамический фон/HUD на + скрытой странице до sprite_update. Правило прежнее: всё «фоновое» — + банком 0x50, спрайты — движком. +- **Оптимизация внутри update, невидимая снаружи**: объединение + heal-прямоугольников соседей и т.п. — меняется реализация update, + не API. ВНИМАНИЕ: «heal только открывшейся L-полоски» для + ПРОЗРАЧНЫХ спрайтов НЕ годится (отвергнуто 2026-07-12): в зоне + перекрытия старой и новой позиций дырки нового кадра не перекрывают + старые непрозрачные пиксели → трейлинг-«полумесяц» остаётся внутри + bbox-перекрытия, куда L-полоска не достаёт. Точный набор для heal = + old_opaque AND NOT new_opaque (маска по форме, дороже full-box heal). + Референс docs/samples/balls (restore_bg) лечит полный 16×16 — full-box + heal обязателен. Экономия — только амортизация обвязки (batch-пасс). +- Вне скоупа v2: скроллящийся фон (инвалидирует ОЗУ-копию целиком — + другой движок), ISR-перерисовка (курсор из IM2 — после Audio/IM2), + коллизии (у приложения есть x/y — пусть считает само). + +## 9а. Квирк CPU-байта write-триггера accel-копии (найден 2026-07-11) + +Триггер записи `LD (DE),A` — реальная CPU-инструкция: её собственный +цикл записи кладёт байт A в dst[0] ДО burst'а FSM. После burst-чтения +`LD A,(HL)` в A остаётся ПОСЛЕДНИЙ байт строки. При банке 0x50 утечка +невидима (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-цикл триггера подавляется ПЛМ). + +## 10. Риски / что проверить артефактом (Фаза 0) + +Статус 2026-07-11: пп. 1–3 закрыты для MAME 0.283 (tests/gfxbanks, +результаты в §7); на железе — всё ещё TODO. + +1. ~~Эмулирует ли MAME биты 2/3~~ — эмулирует (с квирком 0x58: FF + протекает в теневое ОЗУ; в master-драйвере исправлено). Железо — + проверить. +2. ~~Отображение байта FF в VRAM~~ — в MAME обычный цвет 255, + подстановки нет → стирание = heal. Железо может отличаться — + tests/fferase остаётся до прогона на реальном Sprinter. +3. ~~Подавление 0xFF на accel-пути~~ — в MAME accel-путь идентичен + CPU-пути (оба через ram_w). Железо — проверить (в ПЛМ пути + физически разные). +4. ~~Копия accel'ом при src в видеоокне и при src == dst~~ — оба + подтверждены: getimage через accel (образ 1:1, tests/bgi_img) и + gfx_heal (стирание чисто, tests/sprites S2/S3). Железо — как всё + остальное, перепроверить. +5. `LD A,(nn)`/`LD A,(HL)` как источник размера блока (quick-win из + TODO) — если работает, SMC в leaf'е не нужен. +6. Долгие burst'ы и IM2/CBL: пара триггеров на строку под DI — бюджет + как у fill (docs/accel-fill-budget.md), пересчитать для copy. diff --git a/examples/balls/Makefile b/examples/balls/Makefile new file mode 100644 index 0000000..09a01ea --- /dev/null +++ b/examples/balls/Makefile @@ -0,0 +1,4 @@ +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := balls +EXTRA_FLAGS ?= --gfx 256 +include $(PROJ_ROOT)/app.mk diff --git a/examples/balls/balls.c b/examples/balls/balls.c new file mode 100644 index 0000000..c3edad1 --- /dev/null +++ b/examples/balls/balls.c @@ -0,0 +1,185 @@ +/* + * balls — демо спрайтового движка v2 (): N шаров 16×16 летают + * над чёрно-белой шахматкой, отражаясь от краёв. Esc — выход. + * + * РАНЬШЕ (до движка) приложение вело drawn[2][N] координаты вручную и + * само соблюдало дисциплину «heal ВСЕ → блит ВСЕ» на скрытой странице. + * Теперь это работа движка: sprite_move меняет только состояние, + * sprite_flip(sp, N) за кадр делает draw-page=скрытая → heal все старые → + * блит все видимые (одна W3-скобка + один банк на проход) → vsync → флип. + * Учёт drawn[2] спрятан в sprite_t. Профиль/дизайн: + * docs/sprite-api-design.md §9.1. + * + * BOUNDARY-ТЕСТ клипа: диапазон движения выходит ЗА края экрана на + * половину спрайта — шары штатно наполовину уходят за все 4 края, + * включая ОТРИЦАТЕЛЬНЫЕ x/y слева/сверху. Клип _gfx_blit_clip (общий + * для putsprite и движка) обязан авто-обрезать: правый/нижний — урезать + * w/h, левый/верхний — сдвинуть начало источника (sx/sy) и урезать. + * + * Двойная буферизация: рисуем на скрытой странице, флип по vsync (иначе + * heal+блит на видимой ловятся лучом — мерцание). Палитру страницы 1 + * зеркалим из 0 (initgraph грузит EGA только в палитру 0). + * + * Скорость привязана к КАДРАМ (50 Гц): один проход цикла = один шаг. + */ + +#include +#include +#include +#include +#include + +#define FPS_METER 1 /* 1 = счётчик кадров/с слева-сверху (диагностика) */ + +#define NBALLS 16 /* количество шаров */ +#define BW 16 /* сторона спрайта */ +#define CELL 16 /* клетка шахматки */ + +/* Диапазон отражения — на пол-спрайта ЗА каждым краем (boundary-тест): + * x ∈ [-8 .. 312], y ∈ [-8 .. 248]. */ +#define MINY (0) /* -8: половина за левым/верхним краем */ +#define MINX (0) /* -8: половина за левым/верхним краем */ +#define MAXX (320 - BW) /* 312: половина за правым краем */ +#define MAXY (256 - BW) /* 248: половина за нижним краем */ + +/* Цвета шаров (не совпадают с фоном BLACK/WHITE) */ +static const uint8_t ball_colors[] = { + YELLOW, LIGHTRED, LIGHTGREEN, LIGHTCYAN, + LIGHTMAGENTA, LIGHTBLUE, RED, GREEN +}; +#define NCOLORS ((uint8_t)sizeof(ball_colors)) + +static sprite_t sp[NBALLS]; /* спрайты — движок ведёт drawn[2] */ +static int8_t vx[NBALLS], vy[NBALLS]; /* направление ±1 (физика — наша) */ +static uint8_t spr[NCOLORS][4 + BW * BW]; /* по спрайту на цвет */ +static uint8_t egapal[16 * 4]; + +static void build_sprites(void) +{ + uint8_t c; + int x, y; + + for (c = 0; c < NCOLORS; c++) { + uint8_t *p = spr[c]; + + *p++ = BW; *p++ = 0; *p++ = BW; *p++ = 0; + for (y = 0; y < BW; y++) + for (x = 0; x < BW; x++) { + int dx = 2 * x - (BW - 1), dy = 2 * y - (BW - 1); /* ×2: центр между пикселями */ + *p++ = (dx * dx + dy * dy <= 15 * 15) + ? ball_colors[c] : GFX_TRANSPARENT; + } + } +} + +static void draw_board(void) +{ + int cx, cy; + + for (cy = 0; cy < 256; cy += CELL) + for (cx = 0; cx < 320; cx += CELL) { + setfillstyle(SOLID_FILL, + (((cx ^ cy) >> 4) & 1) ? WHITE : BLACK); + bar(cx, cy, cx + CELL - 1, cy + CELL - 1); + } +} + +static void init_balls(void) +{ + uint8_t i; + + for (i = 0; i < NBALLS; i++) { + sprite_init(&sp[i], spr[i % NCOLORS]); + sp[i].x = (20 + i * 37) % (MAXX - 1) + 1; + sp[i].y = (20 + i * 53) % (MAXY - 1) + 1; + sprite_show(&sp[i]); + vx[i] = (i & 1) ? 1 : -1; + vy[i] = (i & 2) ? 1 : -1; + } +} + +#if FPS_METER +/* Кадров за последнюю RTC-секунду — белым в чёрной плашке слева-сверху + * на ТЕКУЩЕЙ draw-странице (рисуем ДО sprite_update, чтобы шары легли + * поверх и heal учитывал плашку через ОЗУ-копию). */ +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); +} +#endif + +int main(void) +{ + uint8_t i, page, hidden; +#if FPS_METER + datetime_t dt; + uint8_t frames = 0, last_sec; +#endif + + build_sprites(); + initgraph(); + gfx_sprite_clip(0); + + /* Палитра страницы 1 = палитре страницы 0. */ + gfx_pal_get(0, 0, 16, egapal); + gfx_pal_load(1, 0, 16, egapal); + + init_balls(); + + /* Обе страницы: шахматка (в ОЗУ-копию — бэкап для heal) + шары в + * стартовых позициях (sprite_update рисует на текущей draw-странице + * и снимает её dirty-бит). */ + for (page = 0; page < 2; page++) { + gfx_set_draw_page(page); + draw_board(); + sprite_update(sp, NBALLS); + } + gfx_set_visible_page(0); +#if FPS_METER + getdatetime(&dt); + last_sec = dt.second; +#endif + + for (;;) { + if (kbhit() && getch() == 27) + break; + + /* Физика: обновить позиции (только состояние спрайтов). */ + for (i = 0; i < NBALLS; i++) { + int nx = sp[i].x + vx[i]; + int ny = sp[i].y + vy[i]; + if (nx <= MINX || nx >= MAXX) vx[i] = -vx[i]; + if (ny <= MINY || ny >= MAXY) vy[i] = -vy[i]; + sprite_move(&sp[i], nx, ny); + } + + /* Кадр: рисуем на скрытой странице. sprite_flip развёрнут, чтобы + * плашку FPS нарисовать на скрытой странице ДО sprite_update. */ + hidden = gfx_get_visible_page() ^ 1; + gfx_set_draw_page(hidden); +#if FPS_METER + frames++; + getdatetime(&dt); + if (dt.second != last_sec) { + last_sec = dt.second; + show_fps(frames); + frames = 0; + } +#endif + sprite_update(sp, NBALLS); + gfx_wait_vsync(); + gfx_set_visible_page(hidden); /* флип в vblank — без мерцания */ + } + + closegraph(); + return 0; +} diff --git a/examples/balls/run.sh b/examples/balls/run.sh new file mode 100755 index 0000000..e9e8c78 --- /dev/null +++ b/examples/balls/run.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +make clean +make run diff --git a/libbgi/_bgi.h b/libbgi/_bgi.h index cfa0b58..cf2c7b8 100644 --- a/libbgi/_bgi.h +++ b/libbgi/_bgi.h @@ -68,6 +68,30 @@ void _bgi_hspan_raw(int x, int y, int len, uint8_t color) __preserves_regs(d void _bgi_vspan_raw(int x, int y, int len, uint8_t color) __preserves_regs(d,e); /* V-отрезок, без скобки */ void _bgi_clear_raw(uint8_t color); /* заливка экрана, без скобки (клоббер всех) */ +/* Копирование h строк по w байт через accel block-copy (LD L,L) — + * ядро блиттинга (putimage, getimage, gfx_blit, gfx_heal). Без скобки и + * клиппинга; w,h: 0 = 256; stride 0 = адрес на месте (сторона экрана, + * строку выбирает Port_Y); y0 — стартовый Port_Y. Направление задают + * указатели (буфер обязан быть вне W3). Контракт/схема — в + * bgi256/_bgi_copy_rows_raw.c. */ +void _bgi_copy_rows_raw(const uint8_t *src, uint8_t *dst, + uint8_t w, uint8_t h, + int dstride, int sstride, uint8_t y0); + +/* Специализация для БЛИТА: dst фиксирован (dstride 0), src двигается на + * sstride (SMC-константа); dst-цепочка выкинута (~24Т/строку). Ядро + * gfx_blit_part/putsprite/спрайтового движка. Без скобки/клиппинга; банк + * ставит вызывающий (0x5C прозрачный). bgi256/_bgi_blit_rows_raw.c. */ +void _bgi_blit_rows_raw(const uint8_t *src, uint8_t *dst, + uint8_t w, uint8_t h, + int sstride, uint8_t y0); + +/* Специализация для heal: src == dst == адрес экрана, оба страйда 0 + * (строку выбирает Port_Y) — адресной арифметики в цикле нет, строка + * 77Т + burst'ы. y0 — стартовый Port_Y; w,h: 0 = 256. Без скобки и + * клиппинга; банк ставит вызывающий (gfx_heal форсит 0x50). */ +void _bgi_heal_rows_raw(uint8_t *scr, int y0, uint8_t w, uint8_t h); + /* Self-bracket версии (begin + raw + end) — для разовых вызовов из * математики, которой не нужен пакетный режим (floodfill, cleardevice). * Разовое чтение пикселя — публичный getpixel (та же композиция; @@ -75,6 +99,48 @@ void _bgi_clear_raw(uint8_t color); /* заливка экр void _bgi_hspan (int x, int y, int len, uint8_t color); void _bgi_clearall(uint8_t color); +/* Ядро блиттинга gfx_blit_part/gfx_blit/gfx_heal (Фаза B) — публичные, + * прототипы в ; спрайтовые обёртки putsprite/movesprite — в + * . Реализации: common/gfx_blit_part.c и соседи. */ + +/* Bracket-free общие ядра (полная ширина) (common/_gfx_blit_full.c, _gfx_heal_full.c): + * клип по экрану + нарезка полос ≤256 + accel-leaf, БЕЗ _bgi_begin/ + * _bgi_end и БЕЗ смены банка. Вызывающий обязан замапить W3 (_bgi_begin) + * и выставить нужный банк ДО вызова. Общее ядро для разовых обёрток + * (gfx_blit_part/gfx_heal) и batch-пасса спрайтового движка v2, который + * держит одну скобку + один банк на весь кадр (docs/sprite-api-design.md + * §9.1). blit: банк 0x5C для прозрачности; heal: банк 0x50. */ +void _gfx_blit_full(int x, int y, const void *img, + int sx, int sy, int w, int h); +void _gfx_heal_full(int x, int y, int w, int h); + +/* Лин-ядра для СПРАЙТОВ (кадр ≤ 64×64, sprite.h): как *_full, но без + * split'а >256 и с 8-битными w/h (движок/putsprite — горячий путь; + * программа только со спрайтами не тянет общие *_full со split'ом). + * common/_gfx_blit_sprite.c, _gfx_heal_sprite.c. */ +void _gfx_blit_sprite(int x, int y, const void *img, + int sx, int sy, uint8_t w, uint8_t h); +void _gfx_heal_sprite(int x, int y, uint8_t w, uint8_t h); + +/* NOCLIP-ядра: клип-кода нет вообще (полный выигрыш от codegen, не только + * скип проверок). Зовутся при gfx_sprite_clip(0) — приложение + * гарантирует пределы. common/_gfx_blit_sprite_noclip.c, _gfx_heal_… */ +void _gfx_blit_sprite_noclip(int x, int y, const void *img, + int sx, int sy, uint8_t w, uint8_t h); +void _gfx_heal_sprite_noclip(int x, int y, uint8_t w, uint8_t h); + +/* Диспетчеризация clip/noclip через УКАЗАТЕЛИ (не ветка в горячем цикле): + * gfx_sprite_clip() перенаправляет их на clip- или noclip-ядро ОДИН раз, + * а sprite_update/putsprite/movesprite зовут через указатель — один + * call-site, без if в цикле → тесный codegen (полный выигрыш noclip, + * см. docs/sprite-api-design.md). Установка по умолчанию (clip on) — в + * initgraph. */ +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); +extern _gfx_blit_fp _gfx_blit_fn; +extern _gfx_heal_fp _gfx_heal_fn; + /* ---- Fill-burst'ы акселератора 256 (register-ABI, asm-only) ------ * * Вызываются ТОЛЬКО из asm raw-примитивов; аргументы в регистрах, без * глобального скретча в _DATA: diff --git a/libbgi/bgi256/_bgi_blit_rows_raw.c b/libbgi/bgi256/_bgi_blit_rows_raw.c new file mode 100644 index 0000000..335a3f0 --- /dev/null +++ b/libbgi/bgi256/_bgi_blit_rows_raw.c @@ -0,0 +1,90 @@ +/* + * _bgi_blit_rows_raw — специализация accel block-copy для БЛИТА + * (буфер → экран): dst ФИКСИРОВАН (адрес экрана base+x; строку выбирает + * Port_Y, dstride == 0 по определению), src двигается на sstride (ширину + * картинки) на строку. Выделен из _bgi_copy_rows_raw (2026-07-12, + * профиль examples/balls): у блита dst-цепочка (ld a,e; add; ld e,a; + * ld a,d; adc; ld d,a ≈ 24Т/строку) — мёртвый груз, т.к. dst не меняется; + * здесь её нет вообще. src-страйд SMC-патчится в 8-битную adc-цепочку + * (константа на вызов). Универсальный _bgi_copy_rows_raw остаётся для + * getimage (grab: dst двигается, src фикс — другая специализация). + * + * ВХОД (__sdcccall(1)): src→HL, dst→DE (фикс); стек: w 4(ix), h 5(ix), + * sstride 6/7(ix), y0 8(ix). w — байт/строку за burst (1..256, 0=256); + * h — строк (1..256, 0=256); sstride — шаг адреса src между строками; + * y0 — стартовый Port_Y. Callee-pop 5 байт. + * + * Raw: клиппинга НЕТ (вызывающий), W3 замаплен (_bgi_begin/_bgi_end + * снаружи), банк выставлен вызывающим (0x5C прозрачный / 0x50 обычный). + * Буфер src обязан быть ВНЕ W3 (< 0xC000). Полосы > 256 режет вызывающий. + * + * ОДИН DI на весь блит (arm + все строки): убирает per-row EI-окно, где + * CBL-ISR мог пере-армировать акселератор своим размером блока, и тесней + * loop (санкция пользователя 2026-07-12: малый спрайт под одним DI аудио + * не рвёт). ВНИМАНИЕ: h ограничен малыми спрайтами — большие блиты + * (putimage 256 строк) вызывающий обязан резать на чанки ≤16 строк. + * + * Квирк CPU-байта write-триггера (см. _bgi_copy_rows_raw): src[0] + * предчитывается и подставляется в A через EX AF,AF' — CPU-цикл + * LD (DE),A кладёт корректный src[0] при любом банке. + * + * Клоббер: AF/AF'/BC/DE/HL; IX сохраняется. + */ + +#include "../_bgi.h" + +void _bgi_blit_rows_raw(const uint8_t *src, uint8_t *dst, + uint8_t w, uint8_t h, + int sstride, uint8_t y0) __naked +{ + (void)src; (void)dst; (void)w; (void)h; (void)sstride; (void)y0; + __asm + push ix + ld ix, #0 + add ix, sp + ;; SMC: размер блока <- w (immediate у ld a,#n — требование FSM) + ld a, 4 (ix) + ld (br_len_imm), a + ;; SMC: src-страйд — константа на вызов (8-битная adc-цепочка) + ld a, 6 (ix) + ld (br_slo_imm), a + ld a, 7 (ix) + ld (br_shi_imm), a + ld b, 5 (ix) ; B = счётчик строк (0 => 256: djnz) + ld c, 8 (ix) ; C = y + di ; один DI на весь блит + ld d, d ; 0x52 — режим размера блока + ld a, #0 + br_len_imm = . - 1 + ld b, b ; 0x40 — стоп (B=счётчик не тронут) + br_row: + ld a, c + out (#0x89), a ; Port_Y = y + inc c + ;; ld a, (hl) ; A = src[0] (accel выключен) + ;; ex af, af' ; спрятать src[0] + ld l, l ; 0x6D — армировать копию строки + ld a, (hl) ; burst: src -> память акселератора + ;; ex af, af' ; A = src[0] — CPU-байт триггера записи + ld (de), a ; burst: память акселератора -> dst (фикс) + ld b, b ; 0x40 — стоп + ;; src += sstride (dst НЕ двигается — dstride 0) + ld a, l + add a, #0 + br_slo_imm = . - 1 + ld l, a + ld a, h + adc a, #0 + br_shi_imm = . - 1 + ld h, a + djnz br_row + ei ; закрыть DI-окно блита + pop ix + ;; callee-pop 5 байт (w,h,sstride:2,y0) + pop hl ; ret-адрес + pop af + pop af + inc sp + jp (hl) + __endasm; +} diff --git a/libbgi/bgi256/_bgi_copy_rows_raw.c b/libbgi/bgi256/_bgi_copy_rows_raw.c new file mode 100644 index 0000000..cdf8c07 --- /dev/null +++ b/libbgi/bgi256/_bgi_copy_rows_raw.c @@ -0,0 +1,130 @@ +/* + * _bgi_copy_rows_raw — копирование h строк по w байт через block-copy + * акселератора (LD L,L), mode 0x81. Ядро блиттинга: putimage(COPY), + * getimage, gfx_blit*, gfx_heal — все операции блиттинга это один этот + * leaf с разными src/dst (docs/sprite-api-design.md §2, §4.1): + * + * блит src=буфер, sstride=w картинки; dst=экран, dstride=0 + * grab src=экран, sstride=0; dst=буфер, dstride=w + * Для heal (src == dst == экран, оба страйда 0) — специализированный + * _bgi_heal_rows_raw (без адресной арифметики в цикле вообще). + * Страйды здесь — константы на весь вызов и патчатся SMC при входе + * (8-битные цепочки add/adc, 73Т/строку вместо 140Т у прежнего + * варианта с перечитыванием через IX; профиль examples/balls). + * + * ВХОД (__sdcccall(1)): src→HL, dst→DE; стек: w 4(ix), h 5(ix), + * dstride 6/7(ix), sstride 8/9(ix), y0 10(ix). + * w — байт в строке за burst, 1..256 (0 = 256 — конвенция акселератора); + * h — строк, 1..256 (0 = 256); + * dstride/sstride — шаг адреса между строками (0 = адрес на месте, + * строку выбирает Port_Y — сторона, живущая в видеоокне W3); + * y0 — стартовый Port_Y, инкремент на строку. + * + * Raw: клиппинга НЕТ (весь клип делает вызывающий), W3 уже замаплен + * (_bgi_begin/_bgi_end снаружи). Буферная сторона обязана быть ВНЕ W3 + * (< 0xC000) — на время операции W3 занят видеобанком. Полосы > 256 + * байт режет вызывающий (см. gfx_blit_part). + * + * Схема (референс Draw_Restangle_Data, accelerator_doc.txt): размер + * блока армируется ОДИН раз (SMC у LD A,#n — операнд ОБЯЗАН быть + * immediate, memory/sprinter_accelerator; размер переживает LD B,B — + * tests/accfill), далее на строку: OUT Port_Y, DI, LD L,L (армировать + * копию), LD A,(HL) (burst-чтение src в память акселератора), + * 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; на железе перепроверить). + * + * Клоббер: AF/AF'/BC/DE/HL; IX сохраняется (callee-saved). + */ + +#include "../_bgi.h" + +void _bgi_copy_rows_raw(const uint8_t *src, uint8_t *dst, + uint8_t w, uint8_t h, + int dstride, int sstride, uint8_t y0) __naked +{ + __asm + push ix + ld ix, #0 + add ix, sp + ;; SMC: размер блока <- w (immediate у ld a,#n — требование FSM) + ld a, 4 (ix) + ld (cr_len_imm), a + ;; SMC: страйды — константы на весь вызов; патчим immediate + ;; 8-битных цепочек сложения вместо перечитывания из стека + ;; через IX на каждой строке (профиль examples/balls + ;; 2026-07-12: было 140Т/строку, стало 73Т — и BC не нужен, + ;; push/pop и ex de,hl ушли). + ld a, 8 (ix) + ld (cr_slo_imm), a + ld a, 9 (ix) + ld (cr_shi_imm), a + ld a, 6 (ix) + ld (cr_dlo_imm), a + ld a, 7 (ix) + ld (cr_dhi_imm), a + ld b, 5 (ix) ; B = счётчик строк (0 => 256: djnz) + ld c, 10 (ix) ; C = y + ;; ОДИН DI на весь блит (arm + все строки): убирает per-row + ;; EI-окно (CBL-ISR не пере-армирует акселератор посреди спрайта) + ;; и тесней loop. Санкция пользователя 2026-07-12: малый спрайт + ;; под одним DI аудио не рвёт. ВНИМАНИЕ: для больших h (putimage + ;; 256 строк) вызывающий обязан резать на чанки ≤16 строк. + di + ld d, d ; 0x52 — режим размера блока + ld a, #0 + cr_len_imm = . - 1 + ld b, b ; 0x40 — стоп (B=счётчик не тронут) + cr_row: + ld a, c + out (#0x89), a ; Port_Y = y + inc c + ;; ld a, (hl) ; A = src[0] (accel выключен) + ;; ex af, af' ; спрятать src[0] в A' + ld l, l ; 0x6D — армировать копию строки + ld a, (hl) ; burst: src -> память акселератора + ;; ex af, af' ; A = src[0] — CPU-байт триггера записи + ld (de), a ; burst: память акселератора -> dst + ld b, b ; 0x40 — стоп (no-op по регистрам) + ;; src += sstride (SMC-immediates, 16-бит через adc) + ld a, l + add a, #0 + cr_slo_imm = . - 1 + ld l, a + ld a, h + adc a, #0 + cr_shi_imm = . - 1 + ld h, a + ;; dst += dstride + ld a, e + add a, #0 + cr_dlo_imm = . - 1 + ld e, a + ld a, d + adc a, #0 + cr_dhi_imm = . - 1 + ld d, a + djnz cr_row + ei ; закрыть DI-окно блита + pop ix + ;; callee-pop 7 байт стековых аргументов + pop hl ; ret-адрес + pop af + pop af + pop af + inc sp + jp (hl) + __endasm; +} diff --git a/libbgi/bgi256/_bgi_heal_rows_raw.c b/libbgi/bgi256/_bgi_heal_rows_raw.c new file mode 100644 index 0000000..50b484d --- /dev/null +++ b/libbgi/bgi256/_bgi_heal_rows_raw.c @@ -0,0 +1,72 @@ +/* + * _bgi_heal_rows_raw — специализация accel-копии для heal (src == dst == + * адрес экрана): h строк по w байт «прочитать ОЗУ-копию → записать в + * VRAM+ОЗУ» на одном месте. Выделен из _bgi_copy_rows_raw по профилю + * examples/balls (2026-07-12): у heal ОБА страйда нулевые по + * определению (строку выбирает Port_Y, CPU-адрес не меняется), поэтому + * адресной арифметики в цикле нет вообще — строка 77Т + burst'ы против + * 204Т у универсального leaf'а до оптимизации. + * + * ВХОД (__sdcccall(1)): scr→HL (base + x), y0→DE (E — стартовый + * Port_Y); стек: w 4(ix), h 5(ix) — 1..256, 0 = 256 (конвенция + * акселератора). Callee-pops 2 байта. + * + * Raw: клиппинга нет, W3 замаплен вызывающим; банк выбирает + * вызывающий (gfx_heal форсит 0x50 — иначе heal бессмыслен). + * Полосы > 256 байт режет вызывающий. + * + * Квирк CPU-байта write-триггера (см. _bgi_copy_rows_raw): для heal + * предчитанный src[0] — ровно тот байт, что burst запишет в dst[0], + * так что фикс EX AF,AF' здесь одновременно и нейтрализация утечки, + * и просто корректное значение. + * + * Клоббер: AF/AF'/BC/DE/HL; IX сохраняется. + */ + +#include "../_bgi.h" + +void _bgi_heal_rows_raw(uint8_t *scr, int y0, uint8_t w, uint8_t h) __naked +{ + __asm + push ix + ld ix, #0 + add ix, sp + ;; SMC: размер блока <- w (immediate — требование FSM) + ld a, 4 (ix) + ld (hr_len_imm), a + ld c, e ; C = y (взять ДО затирания DE) + ld b, 5 (ix) ; B = счётчик строк (0 => 256) + ld d, h + ld e, l ; DE = HL = scr (src == dst) + ;; ОДИН DI на весь спрайт (arm + все строки): убирает per-row + ;; EI-окно, где CBL-ISR мог пере-армировать акселератор своим + ;; размером блока (латентная реентерабельность), и тесней loop. + ;; Санкция пользователя 2026-07-12: 16×16 = 256 байт под одним DI + ;; (как 1×256 fill) на аудио не влияет. ВНИМАНИЕ: h ограничен + ;; малыми спрайтами — для больших блитов (putimage 256 строк) + ;; вызывающий обязан резать на чанки ≤16 строк (иначе долгий DI). + di + ld d, d ; 0x52 — режим размера блока + ld a, #0 + hr_len_imm = . - 1 + ld b, b ; 0x40 — стоп (B=счётчик не тронут) + hr_row: + ld a, c + out (#0x89), a ; Port_Y = y + inc c + ;; ld a, (hl) ; предчитать ОЗУ[0] (accel выключен) + ;; ex af, af' + ld l, l ; 0x6D — армировать копию строки + ld a, (hl) ; burst: ОЗУ-копия -> память акселератора + ;; ex af, af' ; A = ОЗУ[0] — CPU-байт триггера корректен + ld (de), a ; burst: память акселератора -> VRAM+ОЗУ + ld b, b ; 0x40 — стоп + djnz hr_row ; адреса не меняются — только Port_Y + ei ; закрыть DI-окно спрайта + pop ix + pop hl ; ret-адрес; callee-pop 2 байта (w,h) + inc sp + inc sp + jp (hl) + __endasm; +} diff --git a/libbgi/common/_gfx_blit_full.c b/libbgi/common/_gfx_blit_full.c new file mode 100644 index 0000000..29592b0 --- /dev/null +++ b/libbgi/common/_gfx_blit_full.c @@ -0,0 +1,57 @@ +/* + * _gfx_blit_full — bracket-free ядро блита под-прямоугольника картинки + * getimage-формата (см. gfx_blit_part). Клип по экрану + нарезка на + * полосы ≤256 + вызов accel-leaf'а _bgi_copy_rows_raw. БЕЗ _bgi_begin/ + * _bgi_end и БЕЗ смены банка — вызывающий обязан замапить W3 (_bgi_begin) + * и выставить нужный _gfx_bank ДО вызова. + * + * Вынесено из gfx_blit_part (2026-07-12) ради batch-пасса спрайтового + * движка v2: sprite_update держит одну W3-скобку и один банк на ВЕСЬ + * проход, вызывая это ядро в цикле по спрайтам (амортизация обвязки — + * см. docs/sprite-api-design.md §9.1). gfx_blit_part теперь = _bgi_begin + * + это ядро + _bgi_end. + * + * Clip fast-path: если прямоугольник целиком на экране — 4 сравнения + * вместо полного пути коррекции краёв (TODO «клип fast-path»). + */ +#include "../_bgi.h" + +void _gfx_blit_full(int x, int y, const void *img, + int sx, int sy, int w, int h) +{ + const uint8_t *p = (const uint8_t *)img; + const uint8_t *src; + uint16_t dst; + int img_w; + + /* img_w — ПОЛНАЯ ширина картинки/ленты из заголовка getimage + * (p[0]=lo, p[1]=hi); это шаг между строками источника (sstride). + * sx/sy — смещение кадра внутри картинки (атлас). Подробный разбор + * формата и sx/sy — в common/_gfx_blit_sprite.c. */ + img_w = p[0] | (p[1] << 8); + + /* Клип по экрану — только если хоть один край вылезает. */ + if (x < 0 || y < 0 || x + w > _bgi_maxx + 1 || y + h > _bgi_maxy + 1) { + if (x < 0) { sx -= x; w += x; x = 0; } + if (y < 0) { sy -= y; h += y; y = 0; } + if (x + w > _bgi_maxx + 1) w = _bgi_maxx + 1 - x; + if (y + h > _bgi_maxy + 1) h = _bgi_maxy + 1 - y; + if (w <= 0 || h <= 0) return; + } + + /* src = img + 4 + sy*img_w + sx — без __mulint (обычно sy == 0). */ + src = p + 4 + sx; + while (sy--) + src += img_w; + + dst = _gfx_addr_base + x; + while (w > 256) { + _bgi_blit_rows_raw(src, (uint8_t *)dst, 0 /* =256 */, (uint8_t)h, + img_w, (uint8_t)y); + src += 256; + dst += 256; + w -= 256; + } + _bgi_blit_rows_raw(src, (uint8_t *)dst, (uint8_t)w, (uint8_t)h, + img_w, (uint8_t)y); +} diff --git a/libbgi/common/_gfx_blit_sprite.c b/libbgi/common/_gfx_blit_sprite.c new file mode 100644 index 0000000..6bf76b0 --- /dev/null +++ b/libbgi/common/_gfx_blit_sprite.c @@ -0,0 +1,69 @@ +/* + * _gfx_blit_sprite — лин-ядро блита СПРАЙТА (кадр ≤ 64×64, см. sprite.h): + * то же, что _gfx_blit_full, но БЕЗ split'а на полосы >256 (спрайт + * заведомо ≤256 байт в строке) и с 8-битными w/h (дешевле передача + * аргументов). Один вызов _bgi_blit_rows_raw. БЕЗ _bgi_begin/_bgi_end + * и смены банка — вызывающий (sprite_update/putsprite) держит скобку и + * банк 0x5C. Проверок размера НЕТ (контракт ≤64 — в доке, не в коде). + * + * Клип по ЭКРАНУ остаётся (это функциональность — спрайты штатно уходят + * за края): fast-path «целиком на экране» = 4 сравнения. + * + * ФОРМАТ getimage и параметры атласа + * ---------------------------------- + * img указывает на getimage-буфер: 4-байтовый заголовок + пиксели. + * Заголовок = uint16 width, uint16 height (little-endian): + * p[0]=width lo, p[1]=width hi, p[2]=height lo, p[3]=height hi. + * Дальше (p+4) — пиксели ПОСТРОЧНО, по width байт на строку. + * + * int img_w = p[0] | (p[1] << 8); + * — это ПОЛНАЯ ширина картинки/ленты-атласа (склейка двух байтов + * заголовка в 16-бит). Используется как ШАГ между строками источника + * (sstride): чтобы спуститься на строку ниже в буфере, надо прибавить к + * адресу целую ширину картинки. Для одиночного спрайта img_w == ширине + * кадра; для ЛЕНТЫ-АТЛАСА (много кадров в ряд) img_w — ширина всей ленты + * (> ширины кадра w), поэтому img_w остаётся int (лента может быть шире + * 64, хотя КАДР ≤ 64). + * + * (sx, sy) — смещение КАДРА (под-прямоугольника) ВНУТРИ картинки: + * левый-верхний угол вырезаемого кадра в пикселях. Для целого спрайта + * sx = sy = 0. Для кадра N горизонтальной ленты: sx = N*FRAME_W, sy = 0 + * (вертикальные атласы — sy). Отсюда адрес первого пикселя кадра: + * src = p + 4 (пропустить заголовок) + * + sx (сместиться на sx пикселей вправо в первой строке) + * + sy*img_w (спуститься на sy строк — цикл сложений ниже, без + * умножения: обычно sy == 0). + */ +#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) +{ + const uint8_t *p = (const uint8_t *)img; + const uint8_t *src; + uint16_t dst; + int img_w = p[0] | (p[1] << 8); /* ширина картинки/ленты = sstride */ + int cw = w, ch = h; /* рабочие w,h кадра (для клипа) */ + + if (x < 0 || y < 0 || x + cw > _bgi_maxx + 1 || y + ch > _bgi_maxy + 1) { + if (x < 0) { sx -= x; cw += x; x = 0; } + if (y < 0) { sy -= y; ch += y; y = 0; } + if (x + cw > _bgi_maxx + 1) cw = _bgi_maxx + 1 - x; + if (y + ch > _bgi_maxy + 1) ch = _bgi_maxy + 1 - y; + if (cw <= 0 || ch <= 0) return; + } + + + /* Адрес первого пикселя кадра: заголовок(4) + sx вправо, затем sy + * строк вниз (по img_w на строку) — циклом сложений, без __mulint + * (обычно sy == 0: целый спрайт или горизонтальная лента). */ + src = p + 4 + sx; + while (sy--) + src += 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); +} diff --git a/libbgi/common/_gfx_blit_sprite_noclip.c b/libbgi/common/_gfx_blit_sprite_noclip.c new file mode 100644 index 0000000..25f0ef9 --- /dev/null +++ b/libbgi/common/_gfx_blit_sprite_noclip.c @@ -0,0 +1,32 @@ +/* + * _gfx_blit_sprite_noclip — блит спрайта БЕЗ клипа по экрану. Как + * _gfx_blit_sprite, но клип-математики нет ВООБЩЕ (не скип проверки — + * кода нет физически): приложение ГАРАНТИРУЕТ, что спрайт целиком на + * экране (флаг gfx_sprite_clip(0)). Отдельная функция, а не ветка в + * _gfx_blit_sprite: без клип-кода SDCC тесней раскладывает регистры + * (меньше спиллов) — отсюда полный выигрыш ~+19% на анимации (замер + * A/B на examples/balls 2026-07-12), а не только скип сравнений. + * + * ВНИМАНИЕ: запись за край экрана портит соседнюю память (Port_Y за 256 + * заворачивается наверх, x за 320 — в соседнюю строку/страницу; см. + * tests/spriteclip). Вызывать только когда спрайт заведомо в пределах. + * + * Формат img/img_w/sx/sy — как в _gfx_blit_sprite (getimage-заголовок + + * лента-атлас). БЕЗ _bgi_begin/_bgi_end и смены банка (держит вызывающий). + */ +#include "../_bgi.h" + +void _gfx_blit_sprite_noclip(int x, int y, const void *img, + int sx, int sy, uint8_t w, uint8_t h) +{ + const uint8_t *p = (const uint8_t *)img; + const uint8_t *src; + int img_w = p[0] | (p[1] << 8); /* ширина картинки/ленты = sstride */ + + src = p + 4 + sx; + while (sy--) + src += img_w; + + _bgi_blit_rows_raw(src, (uint8_t *)(_gfx_addr_base + x), + w, h, img_w, (uint8_t)y); +} diff --git a/libbgi/common/_gfx_heal_full.c b/libbgi/common/_gfx_heal_full.c new file mode 100644 index 0000000..fb7c552 --- /dev/null +++ b/libbgi/common/_gfx_heal_full.c @@ -0,0 +1,36 @@ +/* + * _gfx_heal_full — bracket-free ядро heal прямоугольника экрана из + * ОЗУ-копии (см. gfx_heal). Клип по экрану + нарезка на полосы ≤256 + + * вызов accel-leaf'а _bgi_heal_rows_raw (src == dst, strides 0). БЕЗ + * _bgi_begin/_bgi_end и БЕЗ смены банка — вызывающий обязан замапить W3 + * и выставить банк GFX_BANK_NORMAL (0x50) ДО вызова. + * + * Вынесено из gfx_heal (2026-07-12) ради batch-пасса движка v2: + * sprite_update лечит ВСЕ старые позиции под одной W3-скобкой и одним + * банком 0x50 (docs/sprite-api-design.md §9.1). gfx_heal теперь = + * save-банк + set 0x50 + _bgi_begin + это ядро + _bgi_end + restore. + * + * Clip fast-path: целиком на экране → 4 сравнения вместо полного пути. + */ +#include "../_bgi.h" + +void _gfx_heal_full(int x, int y, int w, int h) +{ + uint8_t *scr; + + if (x < 0 || y < 0 || x + w > _bgi_maxx + 1 || y + h > _bgi_maxy + 1) { + if (x < 0) { w += x; x = 0; } + if (y < 0) { h += y; y = 0; } + if (x + w > _bgi_maxx + 1) w = _bgi_maxx + 1 - x; + if (y + h > _bgi_maxy + 1) h = _bgi_maxy + 1 - y; + if (w <= 0 || h <= 0) return; + } + + scr = (uint8_t *)(_gfx_addr_base + x); + while (w > 256) { + _bgi_heal_rows_raw(scr, y, 0 /* =256 */, (uint8_t)h); + scr += 256; + w -= 256; + } + _bgi_heal_rows_raw(scr, y, (uint8_t)w, (uint8_t)h); +} diff --git a/libbgi/common/_gfx_heal_sprite.c b/libbgi/common/_gfx_heal_sprite.c new file mode 100644 index 0000000..af4aee1 --- /dev/null +++ b/libbgi/common/_gfx_heal_sprite.c @@ -0,0 +1,25 @@ +/* + * _gfx_heal_sprite — лин-ядро heal СПРАЙТА (≤ 64×64, см. sprite.h): как + * _gfx_heal_full, но БЕЗ split'а на полосы >256 и с 8-битными w/h. Один + * вызов _bgi_heal_rows_raw. БЕЗ _bgi_begin/_bgi_end и смены банка — + * вызывающий (sprite_update) держит скобку и банк 0x50. Проверок + * размера НЕТ (контракт ≤64 — в доке). Клип по экрану остаётся. + */ +#include "../_bgi.h" + +void _gfx_heal_sprite(int x, int y, uint8_t w, uint8_t h) +{ + uint8_t *scr; + int cw = w, ch = h; + + if (x < 0 || y < 0 || x + cw > _bgi_maxx + 1 || y + ch > _bgi_maxy + 1) { + if (x < 0) { cw += x; x = 0; } + if (y < 0) { ch += y; y = 0; } + if (x + cw > _bgi_maxx + 1) cw = _bgi_maxx + 1 - x; + if (y + ch > _bgi_maxy + 1) ch = _bgi_maxy + 1 - y; + if (cw <= 0 || ch <= 0) return; + } + + scr = (uint8_t *)(_gfx_addr_base + x); + _bgi_heal_rows_raw(scr, y, (uint8_t)cw, (uint8_t)ch); +} diff --git a/libbgi/common/_gfx_heal_sprite_noclip.c b/libbgi/common/_gfx_heal_sprite_noclip.c new file mode 100644 index 0000000..10b8642 --- /dev/null +++ b/libbgi/common/_gfx_heal_sprite_noclip.c @@ -0,0 +1,13 @@ +/* + * _gfx_heal_sprite_noclip — heal спрайта БЕЗ клипа по экрану. Как + * _gfx_heal_sprite, но клип-математики нет (приложение гарантирует + * пределы, флаг gfx_sprite_clip(0)). Отдельная функция ради полного + * выигрыша (тесней codegen без клип-кода). БЕЗ скобки/смены банка. + * ВНИМАНИЕ про запись за край — см. _gfx_blit_sprite_noclip. + */ +#include "../_bgi.h" + +void _gfx_heal_sprite_noclip(int x, int y, uint8_t w, uint8_t h) +{ + _bgi_heal_rows_raw((uint8_t *)(_gfx_addr_base + x), y, w, h); +} diff --git a/libbgi/common/_gfx_state.c b/libbgi/common/_gfx_state.c index 60ec462..b093212 100644 --- a/libbgi/common/_gfx_state.c +++ b/libbgi/common/_gfx_state.c @@ -18,3 +18,10 @@ uint8_t _gfx_bank = 0x50; * (CPU 0xC000+), page 1 — 320..639 (CPU 0xC140+), остаток — дескрипторы * режима/палитра, их не трогаем. */ uint16_t _gfx_addr_base = 0xC000; + +/* Клип спрайтов по экрану: 1 = включён (безопасно, дефолт), 0 = выключен + * (приложение само гарантирует, что спрайты не выходят за края — даёт + * ~+19% на анимации, см. docs/sprite-api-design.md). Читает движок/ + * putsprite/movesprite: при 1 зовут clip-ядра, при 0 — noclip-ядра. + * Ненулевой дефолт → в _INITIALIZED (как _gfx_bank). */ +uint8_t _gfx_sprite_clip = 1; diff --git a/libbgi/common/getimage.c b/libbgi/common/getimage.c index 176c71d..e7d284e 100644 --- a/libbgi/common/getimage.c +++ b/libbgi/common/getimage.c @@ -1,27 +1,51 @@ /* * getimage — сохранить прямоугольник экрана в буфер bitmap. * Формат: uint16 width, uint16 height, затем w*h байт (по строкам). - * Чтение raw в одной W3-скобке. + * + * Через accel-ядро: _bgi_copy_rows_raw строками экран→буфер (Фаза A + * спрайтового дизайна). Чтение экрана возвращает ОЗУ-копию (видео-ОЗУ + * write-only) — после рисования в temp-банке (0x54/0x5C) getimage + * вернёт ФОН, а не то, что на экране (это и есть механизм heal). + * + * Клиппинга нет (контракт BGI: rect валиден); safe-версия отсекает + * выход за экран целиком. Буфер bitmap обязан лежать вне W3 + * (< 0xC000) — на время операции W3 замаплен на видеобанк. */ #include "../_bgi.h" void getimage(int left, int top, int right, int bottom, void *bitmap) { uint8_t *p = (uint8_t *)bitmap; + const uint8_t *src; int w = right - left + 1; int h = bottom - top + 1; - int x, y; + int rem; if (w <= 0 || h <= 0) return; +#ifndef GFX_NOCHECK + if (left < 0 || top < 0 || right > _bgi_maxx || bottom > _bgi_maxy) + return; +#endif *p++ = (uint8_t)w; *p++ = (uint8_t)(w >> 8); *p++ = (uint8_t)h; *p++ = (uint8_t)(h >> 8); + /* Полосы <= 256 байт (лимит блока акселератора); шаг строки + * буфера — полная ширина w у ОБЕИХ полос (полосы одной строки + * лежат в буфере подряд). */ + src = (const uint8_t *)(_gfx_addr_base + left); + rem = w; _bgi_begin(); - for (y = 0; y < h; y++) - for (x = 0; x < w; x++) - *p++ = (uint8_t)_bgi_read_raw(left + x, top + y); + while (rem > 256) { + _bgi_copy_rows_raw(src, p, 0 /* =256 */, (uint8_t)h, + w, 0, (uint8_t)top); + src += 256; + p += 256; + rem -= 256; + } + _bgi_copy_rows_raw(src, p, (uint8_t)rem, (uint8_t)h, + w, 0, (uint8_t)top); _bgi_end(); } diff --git a/libbgi/common/gfx_blit.c b/libbgi/common/gfx_blit.c new file mode 100644 index 0000000..3ffdfd8 --- /dev/null +++ b/libbgi/common/gfx_blit.c @@ -0,0 +1,14 @@ +/* + * gfx_blit — блит ЦЕЛОЙ картинки getimage-формата в (x,y) текущим + * банком (gfx_set_bank). Читает w,h из заголовка и делегирует ядру + * gfx_blit_part (клиппинг/полосы/accel — там). + */ +#include "../_bgi.h" + +void gfx_blit(int x, int y, const void *img) +{ + const uint8_t *p = (const uint8_t *)img; + + gfx_blit_part(x, y, img, 0, 0, + p[0] | (p[1] << 8), p[2] | (p[3] << 8)); +} diff --git a/libbgi/common/gfx_blit_part.c b/libbgi/common/gfx_blit_part.c new file mode 100644 index 0000000..ac72669 --- /dev/null +++ b/libbgi/common/gfx_blit_part.c @@ -0,0 +1,33 @@ +/* + * gfx_blit_part — блит под-прямоугольника картинки getimage-формата + * (uint16 w, uint16 h, пиксели построчно) в (x,y) ТЕКУЩИМ банком + * (gfx_set_bank): 0x50 обычный, 0x58 прозрачный (0xFF не пишется), + * 0x54/0x5C временный (ОЗУ-копия не трогается). Ядро блиттинга поверх + * accel-leaf'а _bgi_copy_rows_raw (docs/sprite-api-design.md §4.2): + * putimage(COPY_PUT) — тонкая обёртка над этой функцией. + * + * Под-прямоугольник (sx,sy,w,h) ОБЯЗАН лежать внутри картинки (для + * атласов кадров); по экрану клиппится здесь — всегда, в т.ч. в fast + * (спрайты штатно уходят за края; цена — один расчёт на блит). + * + * Требование: img вне W3 (< 0xC000) — на время блита W3 замаплен на + * видеобанк. + * + * Фаза A: объявление в _bgi.h (internal); Фаза B переносит прототип в + * публичный gfx.h — символ уже итоговый. 256-режим: пиксель = байт; + * при добавлении 16-цветного режима (Фаза 2 libbgi) адресную + * арифметику по x/сдвигам придётся спустить в mode-specific leaf. + */ + +#include "../_bgi.h" + +void gfx_blit_part(int x, int y, const void *img, + int sx, int sy, int w, int h) +{ + /* Разовый вызов: своя W3-скобка вокруг bracket-free ядра. Клип, + * нарезку на полосы и accel-копию делает _gfx_blit_full; batch-путь + * спрайтового движка зовёт то же ядро под ОДНОЙ скобкой на кадр. */ + _bgi_begin(); + _gfx_blit_full(x, y, img, sx, sy, w, h); + _bgi_end(); +} diff --git a/libbgi/common/gfx_heal.c b/libbgi/common/gfx_heal.c new file mode 100644 index 0000000..3e3209d --- /dev/null +++ b/libbgi/common/gfx_heal.c @@ -0,0 +1,29 @@ +/* + * gfx_heal — восстановить прямоугольник экрана из ОЗУ-копии (стирание + * спрайта/оверлея, нарисованного банком с битом 2: 0x54/0x5C). + * + * Механизм (docs/sprite-api-design.md §2): чтение из #50..#5F всегда + * возвращает ОЗУ-копию (видео-ОЗУ write-only), поэтому accel-копия + * src == dst == адрес экрана в банке 0x50 читает ФОН из ОЗУ и пишет + * его в видео-ОЗУ (+ в ОЗУ то же самое — безвредно). Оба stride = 0: + * строку и чтения и записи выбирает один Port_Y. Save-буфер не нужен. + * + * Банк на время операции — ВСЕГДА GFX_BANK_NORMAL (независимо от + * gfx_set_bank: heal через 0x54/0x5C не имел бы смысла — запись не + * дошла бы до ОЗУ, а через 0x58 дырявила бы FF-байты фона); + * пользовательский банк сохраняется и восстанавливается. Клип, полосы + * и accel-heal — в bracket-free ядре _gfx_heal_full (общем с batch-путём + * спрайтового движка); здесь — только банк-дисциплина и W3-скобка. + */ +#include "../_bgi.h" + +void gfx_heal(int x, int y, int w, int h) +{ + uint8_t saved = gfx_get_bank(); + + gfx_set_bank(GFX_BANK_NORMAL); + _bgi_begin(); + _gfx_heal_full(x, y, w, h); + _bgi_end(); + gfx_set_bank(saved); +} diff --git a/libbgi/common/movesprite.c b/libbgi/common/movesprite.c new file mode 100644 index 0000000..595860b --- /dev/null +++ b/libbgi/common/movesprite.c @@ -0,0 +1,31 @@ +/* + * movesprite — переместить спрайт (кадр ≤ 64×64): восстановить фон под + * старой позицией (heal по размерам img из заголовка) и вывести спрайт в + * новой (putsprite). Порядок heal→draw делает перекрытие старой и новой + * позиций безопасным: heal читает фон из ОЗУ-копии, которую спрайт + * (банк 0x5C) никогда не трогал. + * + * Идёт через лин-ядро _gfx_heal_sprite (без split'а, 8-бит w/h) — банк + * на время heal форсится 0x50 (иначе heal бессмыслен), затем putsprite. + * Спрайтовая программа не тянет общие *_full. + * + * Канонический цикл анимации — docs/sprite-api-design.md §6: + * putsprite(x, y, hero); ... + * gfx_wait_vsync(); movesprite(x, y, nx, ny, hero); + */ +#include "../_bgi.h" + +void movesprite(int oldx, int oldy, int x, int y, const void *img) +{ + const uint8_t *p = (const uint8_t *)img; + uint8_t saved = gfx_get_bank(); + + 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]); + _bgi_end(); + gfx_set_bank(saved); + + putsprite(x, y, img); +} diff --git a/libbgi/common/putimage.c b/libbgi/common/putimage.c index 37d8a58..2f1bdb0 100644 --- a/libbgi/common/putimage.c +++ b/libbgi/common/putimage.c @@ -2,8 +2,20 @@ * putimage — вывести образ bitmap левым-верхним углом в (left,top) * операцией op. Заголовок буфера: uint16 width, uint16 height. * - * COPY — пишем src; XOR/OR/AND — комбинируем с экраном (нужно raw- - * чтение); NOT — пишем ~src. Всё в одной W3-скобке (raw-плот/чтение). + * COPY_PUT — через accel-ядро gfx_blit_part (block-copy строками, до + * 256 байт за burst; Фаза A спрайтового дизайна) — С КЛИППИНГОМ по + * экрану (superset поведения Turbo C, где выход за экран — UB) и + * с учётом текущего банка (gfx_set_bank: 0x58 даёт аппаратную + * прозрачность 0xFF, 0x54/0x5C — временный вывод). + * + * XOR/OR/AND — комбинируем с экраном по-пиксельно (raw-чтение + + * raw-плот в одной W3-скобке); NOT — пишем ~src. Ускорение этих op + * блочными режимами акселератора — «следующая версия» (см. + * docs/sprite-api-design.md §9). Клиппинга на этом пути НЕТ + * (per-pixel bounds-check остаётся только в safe-версии). + * + * Буфер bitmap обязан лежать вне W3 (< 0xC000) — на время операции W3 + * замаплен на видеобанк. */ #include "../_bgi.h" @@ -15,9 +27,14 @@ void putimage(int left, int top, const void *bitmap, int op) w = p[0] | (p[1] << 8); h = p[2] | (p[3] << 8); - p += 4; if (w <= 0 || h <= 0) return; + if (op == COPY_PUT) { + gfx_blit_part(left, top, bitmap, 0, 0, w, h); + return; + } + + p += 4; _bgi_begin(); for (y = 0; y < h; y++) { for (x = 0; x < w; x++) { @@ -26,9 +43,8 @@ void putimage(int left, int top, const void *bitmap, int op) case XOR_PUT: dst = (uint8_t)_bgi_read_raw(left + x, top + y) ^ src; break; case OR_PUT: dst = (uint8_t)_bgi_read_raw(left + x, top + y) | src; break; case AND_PUT: dst = (uint8_t)_bgi_read_raw(left + x, top + y) & src; break; - case NOT_PUT: dst = (uint8_t)~src; break; - case COPY_PUT: - default: dst = src; break; + case NOT_PUT: + default: dst = (uint8_t)~src; break; } _bgi_plot_raw(left + x, top + y, dst); } diff --git a/libbgi/common/putsprite.c b/libbgi/common/putsprite.c new file mode 100644 index 0000000..b7a3ca2 --- /dev/null +++ b/libbgi/common/putsprite.c @@ -0,0 +1,25 @@ +/* + * putsprite — вывести спрайт getimage-формата (0xFF = прозрачно, кадр + * ≤ 64×64) в (x,y). Банк на время вызова — GFX_BANK_SPRITE (0x5C): + * прозрачные байты (bit3) не пишутся, ОЗУ-копия фона (bit2) цела → + * спрайт стирается gfx_heal/movesprite без save-буфера. Пользовательский + * банк сохраняется/восстанавливается (уважаем вложенный temp-режим). + * + * Идёт через лин-ядро _gfx_blit_sprite (без split'а >256, 8-бит w/h) — + * спрайтовая программа не тянет общий _gfx_blit_full. Клип по экрану — + * в ядре. w,h — младшие байты заголовка (кадр ≤ 64). + */ +#include "../_bgi.h" + +void putsprite(int x, int y, const void *img) +{ + const uint8_t *p = (const uint8_t *)img; + uint8_t saved = gfx_get_bank(); + + 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]); + _bgi_end(); + gfx_set_bank(saved); +} diff --git a/libbgi/common/sprite_flip.c b/libbgi/common/sprite_flip.c new file mode 100644 index 0000000..4580d84 --- /dev/null +++ b/libbgi/common/sprite_flip.c @@ -0,0 +1,22 @@ +/* + * sprite_flip — сахар канонического дабл-буфер-кадра поверх + * sprite_update (docs/sprite-api-design.md §9.1): + * draw-страница = скрытая → sprite_update → ждать кадр → показать её. + * Флип по vsync делает переход tear-free. Приложение, рисующее свой + * динамический фон/HUD, делает это банком 0x50 на СКРЫТОЙ странице ДО + * sprite_flip (тогда heal учтёт его через ОЗУ-копию) — либо зовёт + * составные части (gfx_set_draw_page/sprite_update/gfx_wait_vsync/ + * gfx_set_visible_page) сам. + */ +#include "../_bgi.h" +#include + +void sprite_flip(sprite_t *arr, uint8_t count) +{ + uint8_t hidden = gfx_get_visible_page() ^ 1; + + gfx_set_draw_page(hidden); + sprite_update(arr, count); + gfx_wait_vsync(); + gfx_set_visible_page(hidden); +} diff --git a/libbgi/common/sprite_init.c b/libbgi/common/sprite_init.c new file mode 100644 index 0000000..f0e5c09 --- /dev/null +++ b/libbgi/common/sprite_init.c @@ -0,0 +1,27 @@ +/* + * sprite_init — инициализировать спрайт: img + размер кадра из заголовка + * getimage (uint16 w, uint16 h). Спрайт невидим (flags = 0) и нигде не + * нарисован (drawn[*].on = 0) — первый sprite_show + sprite_update его + * выведет. Для атласа после init перезадать s->w/s->h (размер кадра) и + * s->sx/s->sy. См. docs/sprite-api-design.md §9.1. + */ +#include "../_bgi.h" +#include + +void sprite_init(sprite_t *s, const void *img) +{ + const uint8_t *p = (const uint8_t *)img; + + s->img = img; + s->x = 0; + s->y = 0; + s->sx = 0; + s->sy = 0; + /* Размер кадра ≤ 255 (см. sprite.h) → младший байт uint16-заголовка; + * старший байт для спрайта-кадра всегда 0. */ + s->w = p[0]; + s->h = p[2]; + s->flags = 0; + s->drawn[0].on = 0; + s->drawn[1].on = 0; +} diff --git a/libbgi/common/sprite_update.c b/libbgi/common/sprite_update.c new file mode 100644 index 0000000..a9a2092 --- /dev/null +++ b/libbgi/common/sprite_update.c @@ -0,0 +1,68 @@ +/* + * sprite_update — отрисовать кадр сцены на ТЕКУЩЕЙ draw-странице p + * (gfx_get_draw_page). Ядро batch-пасса движка v2 (см. + * docs/sprite-api-design.md §9.1): вся работа кадра — здесь, двухпроходно + * «heal ВСЕ → блит ВСЕ», под ОДНОЙ W3-скобкой и ОДНИМ банком на проход. + * + * Дисциплина двух проходов ОБЯЗАТЕЛЬНА: слияние heal+блит per-sprite не + * работает на перекрытиях (heal соседа выкусывает только что + * нарисованный спрайт — проверено на examples/balls 2026-07-11). + * + * Что перерисовывается: спрайт, у которого страница p ГРЯЗНАЯ (dbit — + * взведён sprite_move/frame/show/hide/touch) ИЛИ флаг SPR_ALWAYS. + * Неизменившиеся с прошлого визита этой страницы (и без SPR_ALWAYS) не + * трогаются — суть retained-модели. + * + * Проход 1 (банк 0x50): heal старой позиции drawn[p] у тех, кто нарисован + * (drawn[p].on) и требует обновления → снять drawn[p].on. + * Проход 2 (банк 0x5C): блит видимых, требующих обновления, в (x,y) кадром + * (sx,sy) → записать drawn[p] = (x,y), on = 1; снять dbit (страница p + * актуальна для этого спрайта). z-order = порядок массива. + * + * DI живёт внутри leaf'ов (по строке) — скобка _bgi_begin/_bgi_end + * прерывания НЕ трогает (только мапит W3), поэтому длина прохода не + * влияет на аудио-ISR. w,h кадра ≤ 255 (см. sprite.h). + */ +#include "../_bgi.h" +#include + +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; + + /* Проход 1: heal ВСЕХ старых позиций, требующих обновления. */ + gfx_set_bank(GFX_BANK_NORMAL); + _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); + s->drawn[p].on = 0; + } + } + _bgi_end(); + + /* Проход 2: блит ВСЕХ видимых, требующих обновления. */ + gfx_set_bank(GFX_BANK_SPRITE); + _bgi_begin(); + 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); + s->drawn[p].x = s->x; + s->drawn[p].y = s->y; + s->drawn[p].on = 1; + } + s->flags &= ~dbit; /* страница p обновлена для спрайта */ + } + } + _bgi_end(); + + gfx_set_bank(saved); +} diff --git a/libbgi/include/gfx.h b/libbgi/include/gfx.h index adf2967..3106289 100644 --- a/libbgi/include/gfx.h +++ b/libbgi/include/gfx.h @@ -46,9 +46,59 @@ void gfx_set_visible_page(uint8_t page); /* 0 or 1 */ inline uint8_t gfx_get_visible_page(void) { return _gfx_visible_page; } void gfx_set_draw_page(uint8_t page); /* 0 or 1 */ inline uint8_t gfx_get_draw_page(void) { return _gfx_draw_page; } -inline void gfx_set_bank(uint8_t bank) { _gfx_bank = bank; } /* 0x50..0x5F */ + +/* Видеобанк W3 (0x50..0x5F): биты 2/3 номера страницы — аппаратные + * подрежимы ЗАПИСИ (Иван Мак §4.3; docs/sprite-api-design.md §1), + * действуют на ВСЕ примитивы рисования до следующего gfx_set_bank. + * Биты 0/1 всегда 0 (резерв конфигураций). */ +#define GFX_BANK_NORMAL 0x50 /* запись в видео-ОЗУ + ОЗУ-копию */ +#define GFX_BANK_NOSHADOW 0x54 /* только видео-ОЗУ (врем. вывод) */ +#define GFX_BANK_TRANSPARENT 0x58 /* байт 0xFF не записывается */ +#define GFX_BANK_SPRITE 0x5C /* NOSHADOW + TRANSPARENT */ + +/* Прозрачный цвет аппаратной прозрачности (256-режим: пиксель; + * 16-режим: ПАРА пикселей цвета 15). Цвет 255 нельзя нарисовать + * через банк с битом 3 — он и есть прозрачный. */ +#define GFX_TRANSPARENT 0xFF + +inline void gfx_set_bank(uint8_t bank) { _gfx_bank = bank; } /* GFX_BANK_* */ inline uint8_t gfx_get_bank(void) { return _gfx_bank; } +/* Клип спрайтов по экрану (putsprite/movesprite/спрайтовый движок). + * on=1 (дефолт) — клипить (спрайты могут уходить за края); on=0 — + * ОТКЛЮЧИТЬ (приложение ГАРАНТИРУЕТ, что спрайты целиком на экране — + * иначе запись за край портит соседнюю память, см. tests/spriteclip). + * Выигрыш ~+19% на анимации: движок зовёт noclip-ядра без клип-кода + * (не просто скип проверки — вся клип-математика физически отсутствует, + * SDCC тесней раскладывает регистры). Флаг читается раз на спрайт. + * НЕ влияет на putimage/gfx_blit (у них клип всегда). */ +extern uint8_t _gfx_sprite_clip; +inline void gfx_sprite_clip(uint8_t on) { _gfx_sprite_clip = on; } +inline uint8_t gfx_get_sprite_clip(void) { return _gfx_sprite_clip; } + +/* ---- Блиттинг (ядро спрайтов; Фаза B sprite-api-design) ----------- * + * Формат картинки — getimage: uint16 w, uint16 h, пиксели построчно + * (1 байт/пиксель в 256-режиме). Буфер обязан лежать ВНЕ W3 + * (< 0xC000) — на время операции W3 замаплен на видеобанк. + * Блит через accel block-copy (до 256 байт/burst), клиппинг по экрану + * есть всегда; рисует ТЕКУЩИМ банком (gfx_set_bank). */ + +/* Блит целой картинки левым-верхним углом в (x,y). */ +void gfx_blit(int x, int y, const void *img); + +/* Блит под-прямоугольника (sx,sy,w,h) картинки (атлас кадров: + * кадр N ленты — sx = N*FRAME_W). Под-прямоугольник обязан лежать + * внутри img; по экрану — клиппится. */ +void gfx_blit_part(int x, int y, const void *img, + int sx, int sy, int w, int h); + +/* Восстановить прямоугольник экрана из ОЗУ-копии (стирание спрайта/ + * оверлея, нарисованного банком с битом 2: 0x54/0x5C). Всегда + * работает банком GFX_BANK_NORMAL независимо от gfx_set_bank; + * клиппится по экрану. Буфер сохранения не нужен: ОЗУ-копия и есть + * бэкап фона (чтение из #50..#5F всегда возвращает её). */ +void gfx_heal(int x, int y, int w, int h); + /* Block until the next frame (50 Hz). Типичный double-buffer: * gfx_set_draw_page(hidden); draw_frame(); gfx_wait_vsync(); * gfx_set_visible_page(hidden); // tear-free flip */ diff --git a/libbgi/include/graphics.h b/libbgi/include/graphics.h index 57a12ab..b2661de 100644 --- a/libbgi/include/graphics.h +++ b/libbgi/include/graphics.h @@ -194,9 +194,30 @@ unsigned imagesize(int left, int top, int right, int bottom); void getimage(int left, int top, int right, int bottom, void *bitmap); /* Вывести образ левым-верхним углом в (left,top) операцией op - * (COPY/XOR/OR/AND/NOT_PUT). */ + * (COPY/XOR/OR/AND/NOT_PUT). COPY_PUT — через акселератор, с + * клиппингом по экрану и текущим банком gfx_set_bank (см. ); + * XOR/OR/AND/NOT — по-пиксельно, без клиппинга. */ void putimage(int left, int top, const void *bitmap, int op); +/* ---- Спрайты (аппаратная прозрачность; docs/sprite-api-design.md) - * + * Спрайт — тот же буфер getimage-формата; прозрачные точки в данных = + * байты 0xFF (GFX_TRANSPARENT). Прозрачность и сохранность фона + * обеспечивает железо (банк GFX_BANK_SPRITE = 0x5C): прозрачные байты + * не записываются, ОЗУ-копия фона не трогается — стирание без + * save-буфера через gfx_heal (см. ). + * ВАЖНО: фон/сцену рисовать ОБЫЧНЫМ банком (0x50) — то, что нарисовано + * при активном бите 2 (0x54/0x5C), в ОЗУ-копию не попадает и heal + * вернёт то, что лежало под ним. */ + +/* Вывести спрайт в (x,y): банк на время вызова — GFX_BANK_SPRITE, + * затем прежний. Клиппится по экрану. */ +void putsprite(int x, int y, const void *img); + +/* Переместить спрайт: восстановить фон под (oldx,oldy) (gfx_heal по + * размерам img) и вывести спрайт в (x,y). Перекрытие старой и новой + * позиций безопасно (heal читает фон из ОЗУ-копии). */ +void movesprite(int oldx, int oldy, int x, int y, const void *img); + /* ---- Текст ------------------------------------------------------- * * Шрифт 8×8 (системный). Рисуется текущим цветом на фоновом. */ diff --git a/libbgi/include/sprite.h b/libbgi/include/sprite.h new file mode 100644 index 0000000..38d3ac4 --- /dev/null +++ b/libbgi/include/sprite.h @@ -0,0 +1,107 @@ +/* + * sprite.h — managed-движок спрайтов (retained-модель) поверх ядра + * gfx_blit/gfx_heal. Дизайн: docs/sprite-api-design.md §9.1. + * + * Мотивация — profile examples/balls vs asm-референса docs/samples/balls + * (2026-07-12): ~2× разрыв не в W3-скобке, а в per-call C-обвязке + * (клип/парс заголовка/save-restore банка/SDCC-фрейм), повторяемой 2×N + * раз за кадр. Движок амортизирует её: sprite_update держит ОДНУ + * W3-скобку и ОДИН банк на весь ПРОХОД, вызывая bracket-free ядра + * (_gfx_heal_full/_gfx_blit_full) в цикле по спрайтам. DI остаётся + * гранулярным (по строке в leaf) — под CBL/IM2-аудио длинный DI + * недопустим. + * + * Retained-модель: sprite_move/frame/show/hide/touch меняют ТОЛЬКО + * состояние (O(1), сколько угодно раз за кадр); всё рисование — + * один раз в sprite_update, двухпроходно «heal ВСЕ → блит ВСЕ» (слияние + * per-sprite выкусывает соседей на перекрытии — проверено). Учёт + * «где спрайт реально нарисован на КАЖДОЙ из двух страниц» (drawn[2]) + * ведёт движок, не приложение. + * + * Формат картинки — getimage (uint16 w, uint16 h, пиксели построчно, + * 1 байт/пиксель в 256-режиме); 0xFF = прозрачно (банк 0x5C). Буфер + * обязан лежать ВНЕ W3 (< 0xC000). Размер КАДРА спрайта ≤ 64×64 + * (требование движка): кэш w,h — uint8_t, и блит идёт лин-ядром без + * split'а на полосы >256 (спрайт заведомо ≤256 в строке). Для больших + * картинок — gfx_blit/putimage напрямую (там общий путь со split'ом). + * ЛЕНТА-АТЛАС может быть шире 64 (img_w — шаг), но КАДР ≤ 64. + * + * z-order = индекс в массиве (последний рисуется сверху). Массив + * владеет приложением: без malloc, без лимитов; пересортировка z = + * перестановка элементов (drawn-состояние едет со структурой). + */ +#ifndef SPRITE_H +#define SPRITE_H + +#include + +/* Публичные флаги sprite_t.flags. */ +#define SPR_VISIBLE 0x01u /* спрайт рисуется в sprite_update */ +#define SPR_ALWAYS 0x02u /* перерисовывать КАЖДЫЙ кадр (статик, над */ + /* которым постоянно ходят активные спрайты) */ + +/* Внутренние dirty-биты (владеет движок): «страница p требует обновления + * этого спрайта». Приложению не трогать — только через sprite_*. */ +#define _SPR_DIRTY0 0x04u +#define _SPR_DIRTY1 0x08u +#define _SPR_DIRTY (_SPR_DIRTY0 | _SPR_DIRTY1) + +typedef struct { + const void *img; /* getimage-формат / атлас-лента (вне W3) */ + int x, y; /* ЛОГИЧЕСКАЯ позиция левого-верхнего угла кадра */ + int sx, sy; /* смещение кадра внутри img (атлас; 0 = целая) */ + uint8_t w, h; /* размер кадра (кэш заголовка), 1..255 */ + uint8_t flags; /* SPR_* | внутренние dirty */ + /* --- внутреннее (владеет движок) --- */ + struct { + int x, y; /* где нарисован на странице p */ + uint8_t on; /* нарисован ли на странице p */ + } drawn[2]; +} sprite_t; + +/* Инициализация: w,h из заголовка img, невидим, ничего не нарисовано. + * Для АТЛАСА (кадр — под-прямоугольник) после init выставить s->w/s->h + * в размер кадра и s->sx/s->sy — на нужный кадр (или sprite_frame). */ +void sprite_init(sprite_t *s, const void *img); + +/* Кадр всей сцены: на ТЕКУЩЕЙ draw-странице (gfx_get_draw_page) + * heal ВСЕХ старых позиций → блит ВСЕХ видимых (двухпроходно, одна + * скобка/банк на проход). Спрайты без изменений с прошлого визита этой + * страницы (и без SPR_ALWAYS) не трогаются. Для дабл-буфера звать после + * gfx_set_draw_page(hidden); для одиночной страницы/курсора — прямо на + * видимой. */ +void sprite_update(sprite_t *arr, uint8_t count); + +/* Сахар канонического дабл-буфер-кадра: draw-страница = скрытая → + * sprite_update → gfx_wait_vsync → сделать скрытую видимой (tear-free). + * Динамический фон/HUD рисовать банком 0x50 на скрытой странице ДО + * sprite_flip — тогда heal учтёт его через ОЗУ-копию. */ +void sprite_flip(sprite_t *arr, uint8_t count); + +/* --- O(1) изменения состояния (inline: без call-оверхеда в физике) --- */ + +/* Переместить: меняет только логическую позицию + грязнит обе страницы. */ +inline void sprite_move(sprite_t *s, int x, int y) { + if (s->x != x || s->y != y) { s->x = x; s->y = y; s->flags |= _SPR_DIRTY; } +} + +/* Сменить кадр атласа (под-прямоугольник img). */ +inline void sprite_frame(sprite_t *s, int sx, int sy) { + if (s->sx != sx || s->sy != sy) { s->sx = sx; s->sy = sy; s->flags |= _SPR_DIRTY; } +} + +/* Показать (если был скрыт — грязнит обе страницы). */ +inline void sprite_show(sprite_t *s) { + if (!(s->flags & SPR_VISIBLE)) s->flags |= (SPR_VISIBLE | _SPR_DIRTY); +} + +/* Скрыть (если был виден — грязнит: sprite_update сотрёт heal'ом). */ +inline void sprite_hide(sprite_t *s) { + if (s->flags & SPR_VISIBLE) { s->flags &= ~SPR_VISIBLE; s->flags |= _SPR_DIRTY; } +} + +/* Принудительная перерисовка на ОБЕИХ страницах (напр. после того как + * активный спрайт прошёл над статиком — приложение помечает статик). */ +inline void sprite_touch(sprite_t *s) { s->flags |= _SPR_DIRTY; } + +#endif diff --git a/tests/blitperf/Makefile b/tests/blitperf/Makefile new file mode 100644 index 0000000..f9d8a81 --- /dev/null +++ b/tests/blitperf/Makefile @@ -0,0 +1,4 @@ +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := blitperf +EXTRA_FLAGS ?= --gfx 256 --memory small +include $(PROJ_ROOT)/app.mk diff --git a/tests/blitperf/blitperf.c b/tests/blitperf/blitperf.c new file mode 100644 index 0000000..960a1fa --- /dev/null +++ b/tests/blitperf/blitperf.c @@ -0,0 +1,131 @@ +/* + * blitperf — микробенчмарк спрайтового пути: операций в секунду (RTC) + * для каждого слоя цепочки putsprite/gfx_heal. Диагностика разрыва + * с asm-референсом (examples/balls ~1.5 мс/шар при теории ~0.3 мс): + * отделяет стоимость leaf'ов от C-обвязки (клип/банк/заголовок) и + * даёт опорные точки (bar — давно оптимизированный accel-путь). + * + * Методика: измеряется число вызовов за одну RTC-секунду (старт по + * смене секунды; время проверяется раз в 8 операций). Строка «empty» + * — цена самого цикла (вызов пустышки + getdatetime/8) — вычитать из + * остальных: t_op = 1/n_op - 1/n_empty. + * + * ВНИМАНИЕ: использует ВНУТРЕННИЕ символы libbgi (прототипы + * продублированы ниже — сверять с libbgi/_bgi.h при изменениях ABI). + * + * Запуск: + * python3 toolchain/mame_interactive.py tests/blitperf/blitperf.exe \ + * --snap 22 --timeout 24 + */ + +#include +#include +#include + +/* --- внутренности libbgi (копия из _bgi.h, НЕ публичный API) ------- */ +extern uint16_t _gfx_addr_base; +void _bgi_begin(void); +void _bgi_end(void); +void _bgi_heal_rows_raw(uint8_t *scr, int y0, uint8_t w, uint8_t h); +void _bgi_copy_rows_raw(const uint8_t *src, uint8_t *dst, + uint8_t w, uint8_t h, + int dstride, int sstride, uint8_t y0); +/* ------------------------------------------------------------------- */ + +#define BW 16 +static uint8_t spr[4 + BW * BW]; + +static void build_sprite(void) +{ + uint8_t *p = spr; + int i; + + *p++ = BW; *p++ = 0; *p++ = BW; *p++ = 0; + for (i = 0; i < BW * BW; i++) + *p++ = (uint8_t)(LIGHTRED + ((i & 7) == 0)); /* без 0xFF */ +} + +/* --- операции ------------------------------------------------------ */ + +static void op_empty(void) { } + +static void op_leaf_heal(void) /* leaf + W3-скобка, без C-обвязки */ +{ + _bgi_begin(); + _bgi_heal_rows_raw((uint8_t *)(_gfx_addr_base + 100), 100, BW, BW); + _bgi_end(); +} + +static void op_leaf_blit(void) /* copy-leaf + скобка, без обвязки */ +{ + _bgi_begin(); + _bgi_copy_rows_raw(spr + 4, (uint8_t *)(_gfx_addr_base + 100), + BW, BW, 0, BW, 100); + _bgi_end(); +} + +static void op_gfx_heal(void) { gfx_heal(100, 100, BW, BW); } +static void op_blit_part(void) { gfx_blit_part(100, 100, spr, 0, 0, BW, BW); } +static void op_putsprite(void) { putsprite(100, 100, spr); } +static void op_bar(void) /* опорная точка: accel-fill 16×16 */ +{ + bar(100, 100, 100 + BW - 1, 100 + BW - 1); +} + +/* --- измеритель ---------------------------------------------------- */ + +static uint16_t bench(void (*op)(void)) +{ + datetime_t dt; + uint8_t s0, k; + uint16_t n = 0; + + getdatetime(&dt); + s0 = dt.second; + do { getdatetime(&dt); } while (dt.second == s0); /* синхр. к границе */ + s0 = dt.second; + for (;;) { + for (k = 0; k < 8; k++) + op(); + n += 8; + getdatetime(&dt); + if (dt.second != s0) + return n; + } +} + +static void say_num(int y, const char *tag, uint16_t v) +{ + char buf[16]; + uint8_t i; + uint16_t d = 10000; + + for (i = 0; tag[i] && i < 9; i++) + buf[i] = tag[i]; + while (i < 10) buf[i++] = ' '; + for (; d; d /= 10) + buf[i++] = (char)('0' + (v / d) % 10); + buf[i] = 0; + setcolor(WHITE); + outtextxy(4, y, buf); +} + +int main(void) +{ + build_sprite(); + initgraph(); + cleardevice(); + setcolor(WHITE); + outtextxy(4, 4, "BLITPERF ops/sec (16x16)"); + + say_num(20, "empty", bench(op_empty)); + say_num(30, "leafheal", bench(op_leaf_heal)); + say_num(40, "leafblit", bench(op_leaf_blit)); + say_num(50, "gfx_heal", bench(op_gfx_heal)); + say_num(60, "blitpart", bench(op_blit_part)); + say_num(70, "putsprite", bench(op_putsprite)); + say_num(80, "bar", bench(op_bar)); + outtextxy(4, 96, "done"); + + for (;;) { } +} diff --git a/tests/blitw/Makefile b/tests/blitw/Makefile new file mode 100644 index 0000000..faccf65 --- /dev/null +++ b/tests/blitw/Makefile @@ -0,0 +1,4 @@ +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := blitw +EXTRA_FLAGS ?= --gfx 256 --memory small +include $(PROJ_ROOT)/app.mk diff --git a/tests/blitw/blitw.c b/tests/blitw/blitw.c new file mode 100644 index 0000000..a478eb4 --- /dev/null +++ b/tests/blitw/blitw.c @@ -0,0 +1,136 @@ +/* + * blitw — программный регресс ядра блиттинга gfx_blit_part по ширинам. + * + * Репро артефакта tests/sprites (клипнутые шары w=12 рисуются со + * сдвигом): блитим паттерн-образ 24×24 разными ширинами банком 0x50 + * (запись идёт и в ОЗУ-копию → getpixel читает ровно то, что записано) + * и побайтно сверяем прямоугольник + рамку 1px вокруг (должна остаться + * фоном). Вердикт по каждой ширине печатается на экране: + * w=NN PASS | w=NN FAIL @x,y eXX gYY (exp/got первого расхождения) + * + * Запуск: + * python3 toolchain/mame_interactive.py tests/blitw/blitw.exe \ + * --snap 14 --timeout 16 + */ + +#include +#include + +#define IW 24 +#define IH 24 +#define TX 200 /* позиция тестового прямоугольника (правее текста) */ +#define TY 30 + +static uint8_t img[4 + IW * IH]; +static char msg[40]; + +static uint8_t pat(int x, int y) +{ + return (uint8_t)(((x + 1) * (y + 3)) & 0x7F); /* != 0xFF, != 0 кроме редких */ +} + +static void build_img(void) +{ + uint8_t *p = img; + int x, y; + + *p++ = IW; *p++ = 0; *p++ = IH; *p++ = 0; + for (y = 0; y < IH; y++) + for (x = 0; x < IW; x++) + *p++ = pat(x, y); +} + +static char hexd(uint8_t v) +{ + v &= 15; + return v < 10 ? '0' + v : 'A' + v - 10; +} + +/* Проверить блит шириной w в (tx,TY): весь rect = pat, рамка = 0. */ +static void check_w(int w, int tx, int ty, int row) +{ + int x, y; + uint8_t exp, got; + uint8_t fail = 0; + + /* фон под областью + рамкой */ + setfillstyle(SOLID_FILL, 0); + bar(tx - 2, ty - 2, tx + w + 1, ty + IH + 1); + + gfx_blit_part(tx, ty, img, 0, 0, w, IH); + + for (y = -1; y <= IH && !fail; y++) { + for (x = -1; x <= w && !fail; x++) { + exp = (y >= 0 && y < IH && x >= 0 && x < w) ? pat(x, y) : 0; + got = (uint8_t)getpixel(tx + x, ty + y); + if (got != exp) { + fail = 1; + msg[0] = 'w'; msg[1] = '0' + w / 10; msg[2] = '0' + w % 10; + msg[3] = ' '; msg[4] = 'F'; msg[5] = 'A'; msg[6] = 'I'; + msg[7] = 'L'; msg[8] = ' '; + msg[9] = 'x'; msg[10] = (char)('0' + (x + 1) / 10); + msg[11] = (char)('0' + (x + 1) % 10); + msg[12] = 'y'; msg[13] = (char)('0' + (y + 1) / 10); + msg[14] = (char)('0' + (y + 1) % 10); + msg[15] = ' '; msg[16] = 'e'; msg[17] = hexd(exp >> 4); + msg[18] = hexd(exp); + msg[19] = ' '; msg[20] = 'g'; msg[21] = hexd(got >> 4); + msg[22] = hexd(got); + msg[23] = 0; + } + } + } + if (!fail) { + msg[0] = 'w'; msg[1] = '0' + w / 10; msg[2] = '0' + w % 10; + msg[3] = ' '; msg[4] = 'P'; msg[5] = 'A'; msg[6] = 'S'; + msg[7] = 'S'; msg[8] = 0; + } + outtextxy(4, 40 + row * 10, msg); +} + +int main(void) +{ + static const uint8_t widths[] = { 24, 16, 12, 8, 4, 2, 1 }; + uint8_t i; + + build_img(); + initgraph(); + cleardevice(); + setcolor(WHITE); + outtextxy(4, 4, "BLITW: gfx_blit_part width sweep"); + + for (i = 0; i < sizeof(widths); i++) + check_w(widths[i], TX, TY, i); + + /* и правый край, как в sprites: dst у конца строки page 0 */ + check_w(12, 308 - 12, TY, 7); /* dst близко к краю, но без клипа */ + + /* Регресс утечки CPU-байта write-триггера (см. шапку + * _bgi_copy_rows_raw). Программно (getpixel = ОЗУ) утечка на MAME + * 0.283 НЕ ловится: ОЗУ-ветка 0x58 пишет все байты, и burst + * перезаписывает утечку в ОЗУ; жертвой остаётся только VRAM + * (FF-скип). Поэтому проверка ВИЗУАЛЬНАЯ: блит образа с col0=0xFF + * и последним столбцом YELLOW через 0x5C поверх ЗЕЛЁНОЙ плашки. + * col0 (x=TX..) обязан остаться ЗЕЛЁНЫМ; до фикса он был YELLOW + * (CPU-байт = последний байт burst-чтения). */ + { + static uint8_t timg[4 + 16 * 8]; + uint8_t *p = timg; + int x, y; + + *p++ = 16; *p++ = 0; *p++ = 8; *p++ = 0; + for (y = 0; y < 8; y++) + for (x = 0; x < 16; x++) + *p++ = (x == 0) ? 0xFF : (x == 15 ? YELLOW : LIGHTBLUE); + + setfillstyle(SOLID_FILL, GREEN); + bar(TX - 4, 150 - 4, TX + 19, 150 + 11); + gfx_set_bank(GFX_BANK_SPRITE); + gfx_blit(TX, 150, timg); + gfx_set_bank(GFX_BANK_NORMAL); + outtextxy(4, 40 + 8 * 10 + 4, "trig leak: col0 @200,150 green?"); + } + + outtextxy(4, 40 + 9 * 10 + 6, "done"); + for (;;) { } +} diff --git a/tests/gfxbanks/Makefile b/tests/gfxbanks/Makefile new file mode 100644 index 0000000..1c105b3 --- /dev/null +++ b/tests/gfxbanks/Makefile @@ -0,0 +1,4 @@ +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := gfxbanks +EXTRA_FLAGS ?= --gfx 256 --memory small +include $(PROJ_ROOT)/app.mk diff --git a/tests/gfxbanks/gfxbanks.c b/tests/gfxbanks/gfxbanks.c new file mode 100644 index 0000000..bd1bba7 --- /dev/null +++ b/tests/gfxbanks/gfxbanks.c @@ -0,0 +1,259 @@ +/* + * gfxbanks — Фаза 0 спрайтового дизайна: верификация подрежимов + * видеостраниц #50..#5F (docs/sprite-api-design.md, «Фаза 0»). + * + * Семантика по доку (Иван Мак §4.3): + * bit3 (0x58/0x5C) — запись байта 0xFF НЕ производится (прозрачность); + * bit2 (0x54/0x5C) — запись только в видео-ОЗУ, ОЗУ-копия не трогается; + * чтение из #50..#5F всегда возвращает ОЗУ-копию (getpixel = ОЗУ). + * + * ВНИМАНИЕ: вердикт MAME НЕ финален — тест прогоняется и в MAME, и на + * железе. Известное расхождение: в MAME 0.283 (наш v306) FF-скип bit3 + * гасит ТОЛЬКО запись в VRAM, а теневое ОЗУ при bit2=0 всё равно + * получает FF (ram_w: `if (BIT(~page,2)) ram[...]=data; + * if (!(BIT(page,3) && data==0xff)) vram_w(...)`); в master-драйвере + * уже полное подавление (`return`) — по доку. Через 0x5C расхождение + * не проявляется (bit2 гасит ОЗУ-ветку) — поэтому putsprite планово + * работает через 0x5C. На банке 0x58 зона SKIP это расхождение + * ловит: chkC = FAIL на 0.283, PASS по доку/master. + * + * Сценарий (4 стадии, скриншот после каждой; палитра[255] = синий; + * паузы — по RTC getdatetime, т.к. delayms в --memory small неработосп. + * (irq_install → EINVAL), а скорость CPU в паузах не участвует): + * S1 фон: вертикальные полосы RED/LIGHTGRAY через 0x50 (VRAM+ОЗУ). + * S2 «спрайты» (после Фазы A putimage(COPY) идёт через акселератор, + * поэтому CPU-путь в ZL — честный цикл putpixel): + * ZL (CPU-путь): банк 0x5C, putpixel-ями круг GREEN с полями + * 0xFF — ожидание: круг поверх полос, поля прозрачны; + * ZR (accel-путь): банк 0x5C, bar 0xFF 64×64 (accel-fill: + * ожидание no-op) + putimage того же круга (accel-COPY: + * прозрачность полей на burst-пути); + * ZB (bit3-skip): банк 0x58, putimage 0xFF (accel-copy, верх) + + * bar 0xFF (accel-fill, низ) — ожидание по доку: no-op + * экрана и ОЗУ. + * Проверки по ОЗУ (getpixel): A = ОЗУ под ZL/ZR не тронуто (bit2); + * C = ОЗУ под ZB не тронуто (полное подавление FF-записи). + * Hex-пробы ОЗУ печатаются рядом (ожидание — цвета полос 04/07). + * S3 КЛЮЧЕВОЙ ШАГ — 0xFF реально в VRAM: банк 0x54 (скипа нет), + * квадрат 0xFF 32×32 поверх круга (CPU putpixel-ями в ZL, + * accel-copy putimage'ем в ZR). + * Экран: СИНИЙ квадрат → FF отображается как цвет 255 + * (семантика дока полная, стирание = heal); + * ПОЛОСЫ → видеокарта подставляет ОЗУ-копию + * (дешёвое стирание FF-заливкой работает); + * круг/зелёный → 0x54 не эмулируется. + * Проверка B = ОЗУ всё ещё полосы (bit2 у 0x54). + * S4 heal: чтение+запись через 0x50 ЛЕВЫХ половин всех зон — + * левые половины возвращаются к фону ИЗ ОЗУ-КОПИИ, правые + * сохраняют артефакты S2/S3. NB: если ОЗУ под зоной испорчено + * (chkC FAIL на 0.283), heal честно перенесёт порчу в VRAM — + * зона SKIP посинеет на этой стадии. + * + * Запуск: + * python3 toolchain/mame_interactive.py tests/gfxbanks/gfxbanks.exe \ + * --snap 13,17,21,25 --timeout 27 + */ + +#include +#include +#include + +#define ZY 56 /* верх зон */ +#define ZH 64 /* зоны 64×64 */ +#define ZLX 16 /* 0x5C, CPU-путь */ +#define ZRX 120 /* 0x5C, accel-путь */ +#define ZBX 224 /* 0x58, bit3-skip */ + +#define STRIPE_Y0 48 +#define STRIPE_Y1 135 + +/* Точки-пробы внутри области, куда S2 писал НЕ-FF байты (круг r=28 и + * квадрат +16..+47 содержат все три), — общие для проверок и hex-дампа. */ +static const uint8_t probe[3][2] = { {32, 32}, {20, 28}, {40, 36} }; + +/* Полосы шириной 8: цвет по абсолютному x */ +static uint8_t stripe_color(int x) +{ + return ((x >> 3) & 1) ? LIGHTGRAY : RED; +} + +/* Спрайт 64×64: круг GREEN r=28, вокруг — прозрачный 0xFF */ +static uint8_t spr[4 + 64 * 64]; +/* Квадрат 32×32 сплошного 0xFF */ +static uint8_t ffsq[4 + 32 * 32]; + +static void build_images(void) +{ + uint8_t *p = spr; + int x, y; + + *p++ = 64; *p++ = 0; *p++ = 64; *p++ = 0; + for (y = 0; y < 64; y++) + for (x = 0; x < 64; x++) { + int dx = x - 32, dy = y - 32; + *p++ = (dx * dx + dy * dy <= 28 * 28) ? GREEN : 0xFF; + } + + p = ffsq; + *p++ = 32; *p++ = 0; *p++ = 32; *p++ = 0; + for (x = 0; x < 32 * 32; x++) + *p++ = 0xFF; +} + +/* ОЗУ-копия зоны — нетронутые полосы? */ +static uint8_t ram_is_stripes(int zx) +{ + uint8_t i; + + for (i = 0; i < 3; i++) { + int x = zx + probe[i][0]; + if (getpixel(x, ZY + probe[i][1]) != stripe_color(x)) + return 0; + } + return 1; +} + +static char hexd(uint8_t v) +{ + v &= 15; + return v < 10 ? '0' + v : 'A' + v - 10; +} + +/* Hex-дамп трёх ОЗУ-проб зоны: "ZX 04 04 07" в колонке x=216 */ +static void show_probes(const char *tag, int zx, int ty) +{ + char buf[12]; + uint8_t i, v; + + buf[0] = tag[0]; + buf[1] = tag[1]; + for (i = 0; i < 3; i++) { + v = (uint8_t)getpixel(zx + probe[i][0], ZY + probe[i][1]); + buf[2 + i * 3] = ' '; + buf[3 + i * 3] = hexd(v >> 4); + buf[4 + i * 3] = hexd(v); + } + buf[11] = 0; + outtextxy(216, ty, buf); +} + +/* Пауза n секунд по RTC (гранулярность 1 с, не зависит от CPU/режима) */ +static void pause_sec(uint8_t n) +{ + datetime_t dt; + uint8_t last; + + getdatetime(&dt); + last = dt.second; + while (n) { + getdatetime(&dt); + if (dt.second != last) { + last = dt.second; + n--; + } + } +} + +/* CPU-путь: вывести rect w×h из img (getimage-формат) в (x0,y0) + * по-пиксельно через putpixel (LD (HL),A — НЕ акселератор). */ +static void blit_cpu(int x0, int y0, const void *img, int w, int h) +{ + const uint8_t *p = (const uint8_t *)img + 4; + int x, y; + + for (y = 0; y < h; y++) + for (x = 0; x < w; x++) + putpixel(x0 + x, y0 + y, *p++); +} + +/* CPU-путь: залить rect цветом по-пиксельно. */ +static void fill_cpu(int x0, int y0, int w, int h, uint8_t color) +{ + int x, y; + + for (y = 0; y < h; y++) + for (x = 0; x < w; x++) + putpixel(x0 + x, y0 + y, color); +} + +/* heal: прочитать (вернёт ОЗУ-копию) и записать (уйдёт в VRAM+ОЗУ) + * каждый пиксель прямоугольника банком 0x50. Прообраз gfx_heal. */ +static void heal(int x0, int y0, int w, int h) +{ + int x, y; + + for (y = y0; y < y0 + h; y++) + for (x = x0; x < x0 + w; x++) + putpixel(x, y, getpixel(x, y)); +} + +static void say(int y, const char *s) +{ + outtextxy(4, y, s); +} + +int main(void) +{ + uint8_t chkA, chkB, chkC; + int x; + + build_images(); + initgraph(); + gfx_pal_set(0, 255, 0, 0, 255); /* цвет 255 = ярко-синий */ + cleardevice(); + setcolor(WHITE); + + /* ---- S1: фон ------------------------------------------------ */ + say(4, "GFXBANKS: page submodes 50/54/58/5C"); + for (x = 0; x < 320; x += 8) { + setfillstyle(SOLID_FILL, stripe_color(x)); + bar(x, STRIPE_Y0, x + 7, STRIPE_Y1); + } + outtextxy(ZLX, 44, "5C CPU"); + outtextxy(ZRX, 44, "5C ACC"); + outtextxy(ZBX, 44, "58 SKIP"); + say(148, "S1 stripes via 50"); + pause_sec(4); + + /* ---- S2: спрайты через 0x5C / no-op через 0x58 --------------- */ + gfx_set_bank(0x5C); + blit_cpu(ZLX, ZY, spr, 64, 64); /* CPU: круг + FF-поля */ + setfillstyle(SOLID_FILL, 255); + bar(ZRX, ZY, ZRX + 63, ZY + 63); /* accel-fill FF — ждём no-op */ + putimage(ZRX, ZY, spr, COPY_PUT); /* accel-COPY: круг + FF-поля */ + gfx_set_bank(0x58); + putimage(ZBX, ZY, ffsq, COPY_PUT); /* accel-copy FF, верх зоны */ + setfillstyle(SOLID_FILL, 255); + bar(ZBX, ZY + 32, ZBX + 31, ZY + 63); /* accel-fill FF, низ зоны */ + gfx_set_bank(0x50); + + chkA = ram_is_stripes(ZLX) && ram_is_stripes(ZRX); + chkC = ram_is_stripes(ZBX); + say(158, "S2 sprites 5C, ff-skip 58"); + say(168, chkA ? "A bit2 RAM intact: PASS" : "A bit2 RAM intact: FAIL"); + show_probes("ZL", ZLX, 168); + say(178, chkC ? "C bit3 skip (RAM): PASS" : "C bit3 skip (RAM): FAIL"); + show_probes("ZB", ZBX, 178); + pause_sec(4); + + /* ---- S3: 0xFF реально в VRAM (банк 0x54, скипа нет) ---------- */ + gfx_set_bank(0x54); + fill_cpu(ZLX + 16, ZY + 16, 32, 32, 0xFF); /* CPU-путь */ + putimage(ZRX + 16, ZY + 16, ffsq, COPY_PUT); /* accel-copy путь */ + gfx_set_bank(0x50); + + chkB = ram_is_stripes(ZLX) && ram_is_stripes(ZRX); + say(188, "S3 KEY: ff into vram via 54"); + say(198, chkB ? "B 54 RAM intact: PASS" : "B 54 RAM intact: FAIL"); + show_probes("ZR", ZRX, 198); + say(208, "S3 look: BLUE=doc STRIPES=subst"); + pause_sec(4); + + /* ---- S4: heal левых половин зон ------------------------------ */ + heal(ZLX, ZY, 32, ZH); + heal(ZRX, ZY, 32, ZH); + heal(ZBX, ZY, 32, ZH); + say(218, "S4 healed left halves, done"); + + for (;;) { } +} diff --git a/tests/spriteclip/Makefile b/tests/spriteclip/Makefile new file mode 100644 index 0000000..602088e --- /dev/null +++ b/tests/spriteclip/Makefile @@ -0,0 +1,4 @@ +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := spriteclip +EXTRA_FLAGS ?= --gfx 256 --memory small +include $(PROJ_ROOT)/app.mk diff --git a/tests/spriteclip/spriteclip.c b/tests/spriteclip/spriteclip.c new file mode 100644 index 0000000..a0563f7 --- /dev/null +++ b/tests/spriteclip/spriteclip.c @@ -0,0 +1,97 @@ +/* + * spriteclip — ЭКСПЕРИМЕНТ (гипотеза пользователя 2026-07-12): режет ли + * ЖЕЛЕЗО спрайт, выведенный за ПРАВЫЙ/НИЖНИЙ край экрана, БЕЗ + * программного клипа? Или запись за x>=320 / y>=256 портит соседнюю + * память (следующую строку / страницу 1 / верх экрана)? + * + * Метод: блит 16×16 НЕПРОЗРАЧНОГО белого спрайта мимо клипа — прямым + * вызовом leaf'а _bgi_copy_rows_raw (в обход _gfx_blit_clip). Фон — + * синий; зелёные линии-маркеры на x=319 и y=255 (граница экрана); + * левый край (0..7) и верх (0..7) залиты красным-маркером, чтобы + * поймать «заворот» переполнения именно туда. + * + * Читать скриншот: + * - ПРАВЫЙ (спрайт в x=312): если видно только 8 левых колонок белого + * (x=312..319), а правых 8 нет И красная левая полоса цела → + * железо режет по правому краю. Если правые 8 колонок «завернулись» + * белым в x=0..7 (поверх красного) или появился мусор — НЕ режет, + * клип обязателен. + * - НИЖНИЙ (спрайт в y=250): если видно 6 строк белого (y=250..255), + * а строк за 256 нет И красная верхняя полоса цела → железо режет по + * нижнему. Если белое всплыло сверху (y=0..) — Port_Y завернулся. + * - УГОЛ (x=312,y=250): комбинация. + * + * НЕ для CI-регресса — разовый hardware-probe (и в MAME, и на железе). + */ + +#include +#include +#include +#include + +/* Внутренние символы libbgi (глобальны в bgi256.lib) — тянем напрямую + * ради no-clip блита. Обычному коду так делать НЕ надо (есть gfx_blit). */ +extern void _bgi_begin(void); +extern void _bgi_end(void); +extern void _bgi_copy_rows_raw(const uint8_t *src, uint8_t *dst, + uint8_t w, uint8_t h, + int dstride, int sstride, uint8_t y0); +extern uint16_t _gfx_addr_base; + +#define BW 16 +static uint8_t sprite[4 + BW * BW]; + +static void build_sprite(void) +{ + uint8_t *p = sprite; + int i; + + *p++ = BW; *p++ = 0; *p++ = BW; *p++ = 0; + for (i = 0; i < BW * BW; i++) + *p++ = WHITE; /* непрозрачный: каждый байт пишется */ +} + +/* Блит БЕЗ клипа: адрес = base + x, Port_Y стартует с y, строки шагают + * Port_Y'ем (dstride 0). x/y могут выходить за экран — в этом и суть. */ +static void blit_noclip(int x, int y) +{ + uint8_t *dst = (uint8_t *)(_gfx_addr_base + x); + + gfx_set_bank(GFX_BANK_NORMAL); + _bgi_begin(); + _bgi_copy_rows_raw(sprite + 4, dst, BW, BW, 0, BW, (uint8_t)y); + _bgi_end(); +} + +int main(void) +{ + build_sprite(); + initgraph(); + + /* Фон синий. */ + setfillstyle(SOLID_FILL, BLUE); + bar(0, 0, 319, 255); + + /* Красные маркер-полосы: левые 8 колонок и верхние 8 строк — + * сюда «завернётся» переполнение вправо/вниз, если железо не режет. */ + setfillstyle(SOLID_FILL, RED); + bar(0, 0, 7, 255); + bar(0, 0, 319, 7); + + /* Зелёные маркеры границы экрана. */ + setcolor(GREEN); + line(319, 0, 319, 255); + line(0, 255, 319, 255); + + /* Sanity: клипнутый putsprite тем же спрайтом в безопасном месте. */ + putsprite(140, 120, sprite); + + /* Эксперимент — три no-clip блита за края: */ + blit_noclip(312, 100); /* правый: x+16=328, 8 колонок за 320 */ + blit_noclip(100, 250); /* нижний: y+16=266, 10 строк за 256 */ + blit_noclip(312, 250); /* угол: и вправо, и вниз */ + + getch(); + closegraph(); + return 0; +} diff --git a/tests/sprites/Makefile b/tests/sprites/Makefile new file mode 100644 index 0000000..17b5a3c --- /dev/null +++ b/tests/sprites/Makefile @@ -0,0 +1,4 @@ +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := sprites +EXTRA_FLAGS ?= --gfx 256 --memory small +include $(PROJ_ROOT)/app.mk diff --git a/tests/sprites/sprites.c b/tests/sprites/sprites.c new file mode 100644 index 0000000..848dcfb --- /dev/null +++ b/tests/sprites/sprites.c @@ -0,0 +1,189 @@ +/* + * sprites — тест Фазы B спрайтового API (docs/sprite-api-design.md §3): + * putsprite / movesprite / gfx_heal / gfx_blit_part поверх accel-ядра. + * + * Сцена: полосатый фон (вертикальные полосы 8px RED/LIGHTGRAY, банк + * 0x50 → фон и в VRAM, и в ОЗУ-копии). Спрайт — шар 24×24 (YELLOW с + * RED-ободом), углы = 0xFF (GFX_TRANSPARENT). + * + * Стадии (паузы по RTC, скриншот после каждой): + * S1 фон + разметка. + * S2 putsprite: + * - в центре зоны (C) — прозрачные углы: полосы сквозь них; + * - 4 клип-позиции: слева x=-12, справа x=308, сверху y=-10, + * снизу y=244 — рисуется только видимая часть, соседняя + * память не портится; + * - heal-проба: putsprite в H=(60,130) и тут же gfx_heal того же + * rect — шара БЫТЬ НЕ ДОЛЖНО (первая проверка heal src==dst + * через акселератор — риск §10.4 дизайна); + * - атлас: gfx_blit_part кадра 1 (MAGENTA-шар) из ленты 2×24×24 + * в (270,130) банком GFX_BANK_SPRITE. + * Проверка A (по ОЗУ, getpixel = ОЗУ-копия): полосы под C целы. + * S3 movesprite: шар из (16,60) 12 шагов по (+16,+8) до (208,156) — + * след обязан быть чистым (heal старой позиции на каждом шаге). + * Проверка B: ОЗУ вдоль пути — полосы. + * + * VRAM программно не читается (write-only) — корректность экрана + * подтверждают скриншоты; программные проверки ловят порчу ОЗУ-копии. + * + * Запуск: + * python3 toolchain/mame_interactive.py tests/sprites/sprites.exe \ + * --snap 13,17,21 --timeout 23 + */ + +#include +#include +#include + +#define BW 24 /* сторона спрайта */ +#define BG_Y0 40 +#define BG_Y1 199 + +#define CX 150 /* центральный putsprite */ +#define CY 100 + +static uint8_t ball[4 + BW * BW]; /* YELLOW шар */ +static uint8_t strip[4 + 2 * BW * BW]; /* лента: кадр0 YELLOW, кадр1 MAGENTA */ + +static uint8_t stripe_color(int x) +{ + return ((x >> 3) & 1) ? LIGHTGRAY : RED; +} + +/* Шар: r<=9 — fill, r 9..11 — ring, дальше прозрачно (0xFF). */ +static uint8_t ball_pixel(int x, int y, uint8_t fill, uint8_t ring) +{ + int dx = x - BW / 2, dy = y - BW / 2; + int d2 = dx * dx + dy * dy; + + if (((x == 0 || x == BW - 1) && ((y >=0 && y <= 3) || (y >=BW - 4 && y <= BW - 1))) || + ((y == 0 || y == BW - 1) && ((x >=0 && x <= 3) || (x >=BW - 4 && x <= BW - 1)))) return GREEN; + if (x==0) return GFX_TRANSPARENT; + if (d2 <= 81) return fill; + if (d2 <= 121) return ring; + return GFX_TRANSPARENT; +} + +static void build_images(void) +{ + uint8_t *p; + int x, y, f; + + p = ball; + *p++ = BW; *p++ = 0; *p++ = BW; *p++ = 0; + for (y = 0; y < BW; y++) + for (x = 0; x < BW; x++) + *p++ = ball_pixel(x, y, YELLOW, RED); + + /* Лента 48×24: кадры лежат рядом по x (кадр N: sx = N*BW). */ + p = strip; + *p++ = 2 * BW; *p++ = 0; *p++ = BW; *p++ = 0; + for (y = 0; y < BW; y++) + for (f = 0; f < 2; f++) + for (x = 0; x < BW; x++) + *p++ = ball_pixel(x, y, f ? LIGHTMAGENTA : YELLOW, + f ? BLUE : RED); +} + +/* ОЗУ-копия (getpixel) в трёх точках зоны 24×24 — нетронутые полосы? */ +static uint8_t ram_is_stripes(int zx, int zy) +{ + static const uint8_t off[3][2] = { {12, 12}, {8, 16}, {16, 6} }; + uint8_t i; + + for (i = 0; i < 3; i++) { + int x = zx + off[i][0]; + if (getpixel(x, zy + off[i][1]) != stripe_color(x)) + return 0; + } + return 1; +} + +static void pause_sec(uint8_t n) +{ + datetime_t dt; + uint8_t last; + + getdatetime(&dt); + last = dt.second; + while (n) { + getdatetime(&dt); + if (dt.second != last) { + last = dt.second; + n--; + } + } +} + +static void say(int y, const char *s) +{ + outtextxy(4, y, s); +} + +int main(void) +{ + uint8_t chkA, chkB; + int x, i; + + build_images(); + initgraph(); + cleardevice(); + gfx_sprite_clip(1); + setcolor(WHITE); + + /* ---- S1: фон -------------------------------------------------- */ + say(4, "SPRITES: putsprite/movesprite/heal"); + for (x = 0; x < 320; x += 8) { + setfillstyle(SOLID_FILL, stripe_color(x)); + bar(x, BG_Y0, x + 7, BG_Y1); + } + say(208, "S1 bg"); + (void)getchar(); + + /* ---- S2: putsprite + клипы + heal-проба + атлас ---------------- */ + putsprite(CX, CY, ball); /* центр: прозрачные углы */ + (void)getchar(); + putsprite(-12, 52, ball); /* клип слева */ + (void)getchar(); + putsprite(308, 82, ball); /* клип справа */ + (void)getchar(); + putsprite(297, 162, ball); /* клип справа */ + (void)getchar(); + putsprite(60, -10, ball); /* клип сверху */ + (void)getchar(); + putsprite(290, 244, ball); /* клип снизу */ + (void)getchar(); + putsprite(60, 130, ball); /* heal-проба: нарисовать... */ + (void)getchar(); + gfx_heal(60, 130, BW, BW); /* ...и стереть (шара НЕТ) */ + (void)getchar(); + gfx_set_bank(GFX_BANK_SPRITE); /* атлас: кадр 1 (MAGENTA) */ + (void)getchar(); + gfx_blit_part(270, 130, strip, BW, 0, BW, BW); + (void)getchar(); + gfx_set_bank(GFX_BANK_NORMAL); + (void)getchar(); + + chkA = ram_is_stripes(CX, CY); + say(218, "S2 putsprite+clip, heal@60,130"); + say(228, chkA ? "A RAM under sprite: PASS" + : "A RAM under sprite: FAIL"); + pause_sec(4); + + /* ---- S3: movesprite — след обязан быть чистым ------------------ */ + x = 16; + putsprite(x, 60, ball); + (void)getchar(); + for (i = 0; i < 12; i++) { + movesprite(x, 60 + (x - 16) / 2, x + 16, 60 + (x - 16 + 16) / 2, + ball); + x += 16; + (void)getchar(); + } + chkB = ram_is_stripes(16, 60) && ram_is_stripes(112, 108); + say(238, chkB ? "B RAM along path: PASS" + : "B RAM along path: FAIL"); + say(248, "S3 moved 16,60 -> 208,156 done"); + + (void)getchar(); +} diff --git a/toolchain/mame_interactive.py b/toolchain/mame_interactive.py index a7446f4..bccd1cd 100644 --- a/toolchain/mame_interactive.py +++ b/toolchain/mame_interactive.py @@ -212,7 +212,12 @@ end) def check_no_stray_mame(): - out = subprocess.run(["pgrep", "-fl", "mame"], capture_output=True, text=True).stdout + # Ловим только запущенный ЭМУЛЯТОР (mame/mame.arm/mame.exe c машиной + # sprinter), а не всё, где в командной строке встречается «mame»: + # голый паттерн ложно срабатывал на параллельную сборку MAME из + # исходников (clang/make с путями .../src/mame/...). + out = subprocess.run(["pgrep", "-fl", r"mame[^ ]* sprinter( |$)"], + capture_output=True, text=True).stdout if out.strip(): print("ОШИБКА: есть запущенные копии MAME — закройте их:") print(out)