libbgi: спрайтовый движок v2 + accel-блит/heal leaf'ы + noclip-путь
Спрайтовая графика поверх 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.h>: 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 <noreply@anthropic.com>
This commit is contained in:
+104
-13
@@ -84,27 +84,110 @@ Quick wins:
|
|||||||
- [ ] кэширование block-size между burst'ами (accel помнит размер)
|
- [ ] кэширование block-size между burst'ами (accel помнит размер)
|
||||||
|
|
||||||
Новые возможности:
|
Новые возможности:
|
||||||
- [ ] **пакетное чтение/запись массива байт через акселератор** —
|
- [x] ~~пакетное чтение/запись массива байт через акселератор~~ —
|
||||||
отдельные ф-ии, которые в ОДНОМ вызове читают/пишут строку или
|
сделано 2026-07-11 (Фаза A спрайтового дизайна,
|
||||||
столбец пикселей (гориз./верт.), а не по пикселю через
|
docs/sprite-api-design.md): leaf `_bgi_copy_rows_raw` (accel
|
||||||
_bgi_read_raw/_bgi_plot_raw. Аналогично тому, как сейчас рисуются
|
block-copy LD L,L, до 256 байт/burst, размер блока армируется
|
||||||
линии (_bgi_hspan_raw/_bgi_vspan_raw задают Port_Y и адрес один раз,
|
один раз) + ядро `gfx_blit_part` (клиппинг, полосы ≤256);
|
||||||
далее burst). Цель — ускорить блиттинг: getimage/putimage сейчас
|
putimage(COPY_PUT) и getimage переведены (регресс tests/bgi_img
|
||||||
зовут _bgi_read_raw/_gfx_getpixel256_raw по-пиксельно, из-за чего на
|
1:1 с per-pixel эталоном, подрежимы банков на accel-пути —
|
||||||
каждый пиксель повторяются out Port_Y + ld bc,(base) + bounds-check
|
tests/gfxbanks).
|
||||||
(см. разбор _bgi_read_raw). Дать `_bgi_hread_raw`/`_bgi_hwrite_raw`
|
- [ ] **вертикальный copy-leaf `_bgi_copy_cols_raw`** (режим LD A,A —
|
||||||
(+ vertical) с ABI как у fill-сегментов (HL=addr, B=len, E=y, буфер
|
вертикальная копия, Port_Y двигается сам как у vfill). Анализ
|
||||||
в DE) и перевести getimage/putimage на них.
|
2026-07-12: для СПРАЙТОВ требует column-major хранения (читать
|
||||||
- [ ] `gfx_blit` / `gfx_blit_transparent` — block copy (LD L,L / LD A,A),
|
линейный буфер вертикально нельзя — Port_Y не действует вне
|
||||||
прозрачность через bank 0x58 («FF is transparent»)
|
видеоокна; референс 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`
|
- [ ] `gfx_xor_rect` / `gfx_or_rect` / `gfx_and_rect` / `gfx_invert_rect`
|
||||||
- [ ] шрифты ≠ 8×8: gfx_set_font_data(ptr,w,h,advance), proportional,
|
- [ ] шрифты ≠ 8×8: gfx_set_font_data(ptr,w,h,advance), proportional,
|
||||||
8×16/16×16, отдельный font_id API; font-quad для 640×256
|
8×16/16×16, отдельный font_id API; font-quad для 640×256
|
||||||
(per-cell палитра через дескрипторы 0x0300..0x039F)
|
(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)
|
- [ ] gfx_line через accel для пологих диагоналей (runs ≥ 4-5 px)
|
||||||
- [ ] композитные примитивы с одним W3-swap на операцию
|
- [ ] композитные примитивы с одним 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-примитивы для узких прямоугольников**: при узкой стороне
|
- [ ] **span-примитивы для узких прямоугольников**: при узкой стороне
|
||||||
≤ 8 линий chunked-rectfill проигрывает простому циклу
|
≤ 8 линий chunked-rectfill проигрывает простому циклу
|
||||||
_bgi_hspan_raw/_bgi_vspan_raw (подготовка+precompute ~340Т уходят
|
_bgi_hspan_raw/_bgi_vspan_raw (подготовка+precompute ~340Т уходят
|
||||||
@@ -134,6 +217,14 @@ Quick wins:
|
|||||||
Y..Y+15): в MAME 0.283 НЕ работает (2026-06-01). На железе:
|
Y..Y+15): в MAME 0.283 НЕ работает (2026-06-01). На железе:
|
||||||
dual-write тест → если работает, кэшировать Port_Y в putpixel
|
dual-write тест → если работает, кэшировать Port_Y в putpixel
|
||||||
(~8× меньше OUT для Брезенхэма); если нет — вычистить из доков.
|
(~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 железо.
|
- [ ] fdmax: лимит манипуляторов и зависание 9-го OPEN — MAME vs железо.
|
||||||
- [ ] CBL: щелчок перед первым проигрыванием звука за сессию (tests/
|
- [ ] CBL: щелчок перед первым проигрыванием звука за сессию (tests/
|
||||||
cbltest, tests/cblwav) — воспроизводится ТОЛЬКО на первом запуске
|
cbltest, tests/cblwav) — воспроизводится ТОЛЬКО на первом запуске
|
||||||
|
|||||||
+22
-4
@@ -190,8 +190,13 @@ wrchar rdchar`; режимы `gettextmode/settextmode` (0x02=40×32,
|
|||||||
|
|
||||||
Setup: `gfx_init(mode,page)→prev`, `gfx_done(prev)`.
|
Setup: `gfx_init(mode,page)→prev`, `gfx_done(prev)`.
|
||||||
Страницы/банк: `gfx_set/get_visible_page`, `gfx_set/get_draw_page`
|
Страницы/банк: `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 порта
|
`gfx_wait_vsync()` (2026-07-07, редизайн): ждёт переход бита 5 порта
|
||||||
0xFE из 1 в 0 — реальное аппаратное состояние луча (Y>256 → начало
|
0xFE из 1 в 0 — реальное аппаратное состояние луча (Y>256 → начало
|
||||||
@@ -234,8 +239,21 @@ CP), `line(x1,y1,x2,y2)` (не двигает).
|
|||||||
`fillpoly(n,pts)`, `fillellipse(x,y,xr,yr)`.
|
`fillpoly(n,pts)`, `fillellipse(x,y,xr,yr)`.
|
||||||
Заливка областей (Ф2c): `floodfill(x,y,border)` (медленно, но верно),
|
Заливка областей (Ф2c): `floodfill(x,y,border)` (медленно, но верно),
|
||||||
`pieslice(x,y,st,end,r)`, `sector(x,y,st,end,xr,yr)`.
|
`pieslice(x,y,st,end,r)`, `sector(x,y,st,end,xr,yr)`.
|
||||||
Спрайты (Ф2d): `imagesize/getimage/putimage` (COPY/XOR/OR/AND/NOT_PUT;
|
Образы (Ф2d): `imagesize/getimage/putimage` (COPY/XOR/OR/AND/NOT_PUT;
|
||||||
формат буфера: uint16 w,h + w*h байт).
|
формат буфера: 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.h>:
|
||||||
|
`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`
|
Стиль линий (Ф2d): `setlinestyle(style,upattern,thick)`/`getlinesettings`
|
||||||
(SOLID/DOTTED/CENTER/DASHED/USERBIT + NORM/THICK) — на line/rectangle/
|
(SOLID/DOTTED/CENTER/DASHED/USERBIT + NORM/THICK) — на line/rectangle/
|
||||||
drawpoly.
|
drawpoly.
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ accfill 3772
|
|||||||
argv 3431
|
argv 3431
|
||||||
assrtest 3847
|
assrtest 3847
|
||||||
attrprob 4087
|
attrprob 4087
|
||||||
|
balls 6393
|
||||||
banked 1056
|
banked 1056
|
||||||
bankedbg 1067
|
bankedbg 1067
|
||||||
banklocl 4683
|
banklocl 4683
|
||||||
banktest 3767
|
banktest 3767
|
||||||
bgi_img 6222
|
bgi_img 7060
|
||||||
bgitest 3737
|
bgitest 3737
|
||||||
bios_text 4461
|
bios_text 4461
|
||||||
|
blitperf 5860
|
||||||
|
blitw 4939
|
||||||
cat 913
|
cat 913
|
||||||
cblstream 6217
|
cblstream 6217
|
||||||
cbltest 6366
|
cbltest 6366
|
||||||
@@ -24,6 +27,7 @@ filetest 10547
|
|||||||
gets 509
|
gets 509
|
||||||
gfx_dbuf 5032
|
gfx_dbuf 5032
|
||||||
gfx_demo 4103
|
gfx_demo 4103
|
||||||
|
gfxbanks 6044
|
||||||
hello 4167
|
hello 4167
|
||||||
hello2 4244
|
hello2 4244
|
||||||
irqtest 5850
|
irqtest 5850
|
||||||
@@ -39,6 +43,8 @@ rt_test 4892
|
|||||||
seek 4188
|
seek 4188
|
||||||
simple 955
|
simple 955
|
||||||
solidt 11531
|
solidt 11531
|
||||||
|
spriteclip 4187
|
||||||
|
sprites 9968
|
||||||
stattest 7549
|
stattest 7549
|
||||||
stdlib 6643
|
stdlib 6643
|
||||||
stest2 3644
|
stest2 3644
|
||||||
|
|||||||
|
@@ -0,0 +1,594 @@
|
|||||||
|
# Спрайтовое расширение BGI — дизайн
|
||||||
|
|
||||||
|
Статус: дизайн на ревью (2026-07-11). Реализация не начата.
|
||||||
|
|
||||||
|
Расширение libbgi для спрайтовой графики на аппаратных подрежимах
|
||||||
|
видеостраниц `#50..#5F` + переписывание putimage/getimage через
|
||||||
|
акселератор (одно ядро на всё).
|
||||||
|
|
||||||
|
Зафиксированные решения (обсуждены 2026-07-11):
|
||||||
|
1. **Два слоя**: быстрое ядро в `<gfx.h>` (`gfx_*`), тонкие BGI-обёртки
|
||||||
|
в `<graphics.h>` (`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 `<gfx.h>` — ядро
|
||||||
|
|
||||||
|
```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 `<graphics.h>` — 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_, заголовок <sprite.h> в 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.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
|
EXAMPLE := balls
|
||||||
|
EXTRA_FLAGS ?= --gfx 256
|
||||||
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
/*
|
||||||
|
* balls — демо спрайтового движка v2 (<sprite.h>): 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 <graphics.h>
|
||||||
|
#include <gfx.h>
|
||||||
|
#include <sprite.h>
|
||||||
|
#include <conio.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
Executable
+4
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
make clean
|
||||||
|
make run
|
||||||
@@ -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_vspan_raw(int x, int y, int len, uint8_t color) __preserves_regs(d,e); /* V-отрезок, без скобки */
|
||||||
void _bgi_clear_raw(uint8_t color); /* заливка экрана, без скобки (клоббер всех) */
|
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) — для разовых вызовов из
|
/* Self-bracket версии (begin + raw + end) — для разовых вызовов из
|
||||||
* математики, которой не нужен пакетный режим (floodfill, cleardevice).
|
* математики, которой не нужен пакетный режим (floodfill, cleardevice).
|
||||||
* Разовое чтение пикселя — публичный getpixel (та же композиция;
|
* Разовое чтение пикселя — публичный 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_hspan (int x, int y, int len, uint8_t color);
|
||||||
void _bgi_clearall(uint8_t color);
|
void _bgi_clearall(uint8_t color);
|
||||||
|
|
||||||
|
/* Ядро блиттинга gfx_blit_part/gfx_blit/gfx_heal (Фаза B) — публичные,
|
||||||
|
* прототипы в <gfx.h>; спрайтовые обёртки putsprite/movesprite — в
|
||||||
|
* <graphics.h>. Реализации: 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) ------ *
|
/* ---- Fill-burst'ы акселератора 256 (register-ABI, asm-only) ------ *
|
||||||
* Вызываются ТОЛЬКО из asm raw-примитивов; аргументы в регистрах, без
|
* Вызываются ТОЛЬКО из asm raw-примитивов; аргументы в регистрах, без
|
||||||
* глобального скретча в _DATA:
|
* глобального скретча в _DATA:
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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 <stdio.h>
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -18,3 +18,10 @@ uint8_t _gfx_bank = 0x50;
|
|||||||
* (CPU 0xC000+), page 1 — 320..639 (CPU 0xC140+), остаток — дескрипторы
|
* (CPU 0xC000+), page 1 — 320..639 (CPU 0xC140+), остаток — дескрипторы
|
||||||
* режима/палитра, их не трогаем. */
|
* режима/палитра, их не трогаем. */
|
||||||
uint16_t _gfx_addr_base = 0xC000;
|
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;
|
||||||
|
|||||||
@@ -1,27 +1,51 @@
|
|||||||
/*
|
/*
|
||||||
* getimage — сохранить прямоугольник экрана в буфер bitmap.
|
* getimage — сохранить прямоугольник экрана в буфер bitmap.
|
||||||
* Формат: uint16 width, uint16 height, затем w*h байт (по строкам).
|
* Формат: 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"
|
#include "../_bgi.h"
|
||||||
|
|
||||||
void getimage(int left, int top, int right, int bottom, void *bitmap)
|
void getimage(int left, int top, int right, int bottom, void *bitmap)
|
||||||
{
|
{
|
||||||
uint8_t *p = (uint8_t *)bitmap;
|
uint8_t *p = (uint8_t *)bitmap;
|
||||||
|
const uint8_t *src;
|
||||||
int w = right - left + 1;
|
int w = right - left + 1;
|
||||||
int h = bottom - top + 1;
|
int h = bottom - top + 1;
|
||||||
int x, y;
|
int rem;
|
||||||
|
|
||||||
if (w <= 0 || h <= 0) return;
|
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;
|
||||||
*p++ = (uint8_t)(w >> 8);
|
*p++ = (uint8_t)(w >> 8);
|
||||||
*p++ = (uint8_t)h;
|
*p++ = (uint8_t)h;
|
||||||
*p++ = (uint8_t)(h >> 8);
|
*p++ = (uint8_t)(h >> 8);
|
||||||
|
|
||||||
|
/* Полосы <= 256 байт (лимит блока акселератора); шаг строки
|
||||||
|
* буфера — полная ширина w у ОБЕИХ полос (полосы одной строки
|
||||||
|
* лежат в буфере подряд). */
|
||||||
|
src = (const uint8_t *)(_gfx_addr_base + left);
|
||||||
|
rem = w;
|
||||||
_bgi_begin();
|
_bgi_begin();
|
||||||
for (y = 0; y < h; y++)
|
while (rem > 256) {
|
||||||
for (x = 0; x < w; x++)
|
_bgi_copy_rows_raw(src, p, 0 /* =256 */, (uint8_t)h,
|
||||||
*p++ = (uint8_t)_bgi_read_raw(left + x, top + y);
|
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();
|
_bgi_end();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -2,8 +2,20 @@
|
|||||||
* putimage — вывести образ bitmap левым-верхним углом в (left,top)
|
* putimage — вывести образ bitmap левым-верхним углом в (left,top)
|
||||||
* операцией op. Заголовок буфера: uint16 width, uint16 height.
|
* операцией op. Заголовок буфера: uint16 width, uint16 height.
|
||||||
*
|
*
|
||||||
* COPY — пишем src; XOR/OR/AND — комбинируем с экраном (нужно raw-
|
* COPY_PUT — через accel-ядро gfx_blit_part (block-copy строками, до
|
||||||
* чтение); NOT — пишем ~src. Всё в одной W3-скобке (raw-плот/чтение).
|
* 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"
|
#include "../_bgi.h"
|
||||||
|
|
||||||
@@ -15,9 +27,14 @@ void putimage(int left, int top, const void *bitmap, int op)
|
|||||||
|
|
||||||
w = p[0] | (p[1] << 8);
|
w = p[0] | (p[1] << 8);
|
||||||
h = p[2] | (p[3] << 8);
|
h = p[2] | (p[3] << 8);
|
||||||
p += 4;
|
|
||||||
if (w <= 0 || h <= 0) return;
|
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();
|
_bgi_begin();
|
||||||
for (y = 0; y < h; y++) {
|
for (y = 0; y < h; y++) {
|
||||||
for (x = 0; x < w; x++) {
|
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 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 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 AND_PUT: dst = (uint8_t)_bgi_read_raw(left + x, top + y) & src; break;
|
||||||
case NOT_PUT: dst = (uint8_t)~src; break;
|
case NOT_PUT:
|
||||||
case COPY_PUT:
|
default: dst = (uint8_t)~src; break;
|
||||||
default: dst = src; break;
|
|
||||||
}
|
}
|
||||||
_bgi_plot_raw(left + x, top + y, dst);
|
_bgi_plot_raw(left + x, top + y, dst);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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 <sprite.h>
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -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 <sprite.h>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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 <sprite.h>
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
+51
-1
@@ -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; }
|
inline uint8_t gfx_get_visible_page(void) { return _gfx_visible_page; }
|
||||||
void gfx_set_draw_page(uint8_t page); /* 0 or 1 */
|
void gfx_set_draw_page(uint8_t page); /* 0 or 1 */
|
||||||
inline uint8_t gfx_get_draw_page(void) { return _gfx_draw_page; }
|
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; }
|
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:
|
/* Block until the next frame (50 Hz). Типичный double-buffer:
|
||||||
* gfx_set_draw_page(hidden); draw_frame(); gfx_wait_vsync();
|
* gfx_set_draw_page(hidden); draw_frame(); gfx_wait_vsync();
|
||||||
* gfx_set_visible_page(hidden); // tear-free flip */
|
* gfx_set_visible_page(hidden); // tear-free flip */
|
||||||
|
|||||||
@@ -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);
|
void getimage(int left, int top, int right, int bottom, void *bitmap);
|
||||||
|
|
||||||
/* Вывести образ левым-верхним углом в (left,top) операцией op
|
/* Вывести образ левым-верхним углом в (left,top) операцией op
|
||||||
* (COPY/XOR/OR/AND/NOT_PUT). */
|
* (COPY/XOR/OR/AND/NOT_PUT). COPY_PUT — через акселератор, с
|
||||||
|
* клиппингом по экрану и текущим банком gfx_set_bank (см. <gfx.h>);
|
||||||
|
* XOR/OR/AND/NOT — по-пиксельно, без клиппинга. */
|
||||||
void putimage(int left, int top, const void *bitmap, int op);
|
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 (см. <gfx.h>).
|
||||||
|
* ВАЖНО: фон/сцену рисовать ОБЫЧНЫМ банком (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 (системный). Рисуется текущим цветом на фоновом. */
|
* Шрифт 8×8 (системный). Рисуется текущим цветом на фоновом. */
|
||||||
|
|
||||||
|
|||||||
@@ -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 <stdint.h>
|
||||||
|
|
||||||
|
/* Публичные флаги 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
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
|
EXAMPLE := blitperf
|
||||||
|
EXTRA_FLAGS ?= --gfx 256 --memory small
|
||||||
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -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 <graphics.h>
|
||||||
|
#include <gfx.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
/* --- внутренности 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 (;;) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
|
EXAMPLE := blitw
|
||||||
|
EXTRA_FLAGS ?= --gfx 256 --memory small
|
||||||
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -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 <graphics.h>
|
||||||
|
#include <gfx.h>
|
||||||
|
|
||||||
|
#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 (;;) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
|
EXAMPLE := gfxbanks
|
||||||
|
EXTRA_FLAGS ?= --gfx 256 --memory small
|
||||||
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -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 <graphics.h>
|
||||||
|
#include <gfx.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#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 (;;) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
|
EXAMPLE := spriteclip
|
||||||
|
EXTRA_FLAGS ?= --gfx 256 --memory small
|
||||||
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -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 <graphics.h>
|
||||||
|
#include <gfx.h>
|
||||||
|
#include <conio.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/* Внутренние символы 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
|
EXAMPLE := sprites
|
||||||
|
EXTRA_FLAGS ?= --gfx 256 --memory small
|
||||||
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -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 <graphics.h>
|
||||||
|
#include <gfx.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#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();
|
||||||
|
}
|
||||||
@@ -212,7 +212,12 @@ end)
|
|||||||
|
|
||||||
|
|
||||||
def check_no_stray_mame():
|
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():
|
if out.strip():
|
||||||
print("ОШИБКА: есть запущенные копии MAME — закройте их:")
|
print("ОШИБКА: есть запущенные копии MAME — закройте их:")
|
||||||
print(out)
|
print(out)
|
||||||
|
|||||||
Reference in New Issue
Block a user