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:
2026-07-12 21:47:37 +03:00
parent 78161561e7
commit 72ce66275e
42 changed files with 2835 additions and 32 deletions
+104 -13
View File
@@ -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
View File
@@ -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.
+7 -1
View File
@@ -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
1 # Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
3 argv
4 assrtest
5 attrprob
6 balls
7 banked
8 bankedbg
9 banklocl
10 banktest
11 bgi_img
12 bgitest
13 bios_text
14 blitperf
15 blitw
16 cat
17 cblstream
18 cbltest
27 gets
28 gfx_dbuf
29 gfx_demo
30 gfxbanks
31 hello
32 hello2
33 irqtest
43 seek
44 simple
45 solidt
46 spriteclip
47 sprites
48 stattest
49 stdlib
50 stest2
+594
View File
@@ -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 (~5080Т); ядро платит на
СТРОКУ ~4060Т 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: пп. 13 закрыты для 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.
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := balls
EXTRA_FLAGS ?= --gfx 256
include $(PROJ_ROOT)/app.mk
+185
View File
@@ -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;
}
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
make clean
make run
+66
View File
@@ -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:
+90
View File
@@ -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;
}
+130
View File
@@ -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;
}
+72
View File
@@ -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;
}
+57
View File
@@ -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);
}
+69
View File
@@ -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);
}
+32
View File
@@ -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);
}
+36
View File
@@ -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);
}
+25
View File
@@ -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);
}
+13
View File
@@ -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);
}
+7
View File
@@ -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;
+29 -5
View File
@@ -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();
} }
+14
View File
@@ -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));
}
+33
View File
@@ -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();
}
+29
View File
@@ -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);
}
+31
View File
@@ -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);
}
+22 -6
View File
@@ -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);
} }
+25
View File
@@ -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);
}
+22
View File
@@ -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);
}
+27
View File
@@ -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;
}
+68
View File
@@ -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
View File
@@ -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 */
+22 -1
View File
@@ -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 (системный). Рисуется текущим цветом на фоновом. */
+107
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := blitperf
EXTRA_FLAGS ?= --gfx 256 --memory small
include $(PROJ_ROOT)/app.mk
+131
View File
@@ -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 (;;) { }
}
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := blitw
EXTRA_FLAGS ?= --gfx 256 --memory small
include $(PROJ_ROOT)/app.mk
+136
View File
@@ -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 (;;) { }
}
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := gfxbanks
EXTRA_FLAGS ?= --gfx 256 --memory small
include $(PROJ_ROOT)/app.mk
+259
View File
@@ -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 (;;) { }
}
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := spriteclip
EXTRA_FLAGS ?= --gfx 256 --memory small
include $(PROJ_ROOT)/app.mk
+97
View File
@@ -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;
}
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := sprites
EXTRA_FLAGS ?= --gfx 256 --memory small
include $(PROJ_ROOT)/app.mk
+189
View File
@@ -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();
}
+6 -1
View File
@@ -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)