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 помнит размер)
Новые возможности:
- [ ] **пакетное чтение/запись массива байт через акселератор**
отдельные ф-ии, которые в ОДНОМ вызове читают/пишут строку или
столбец пикселей (гориз./верт.), а не по пикселю через
_bgi_read_raw/_bgi_plot_raw. Аналогично тому, как сейчас рисуются
линии (_bgi_hspan_raw/_bgi_vspan_raw задают Port_Y и адрес один раз,
далее burst). Цель — ускорить блиттинг: getimage/putimage сейчас
зовут _bgi_read_raw/_gfx_getpixel256_raw по-пиксельно, из-за чего на
каждый пиксель повторяются out Port_Y + ld bc,(base) + bounds-check
(см. разбор _bgi_read_raw). Дать `_bgi_hread_raw`/`_bgi_hwrite_raw`
(+ vertical) с ABI как у fill-сегментов (HL=addr, B=len, E=y, буфер
в DE) и перевести getimage/putimage на них.
- [ ] `gfx_blit` / `gfx_blit_transparent` — block copy (LD L,L / LD A,A),
прозрачность через bank 0x58 («FF is transparent»)
- [x] ~~пакетное чтение/запись массива байт через акселератор~~
сделано 2026-07-11 (Фаза A спрайтового дизайна,
docs/sprite-api-design.md): leaf `_bgi_copy_rows_raw` (accel
block-copy LD L,L, до 256 байт/burst, размер блока армируется
один раз) + ядро `gfx_blit_part` (клиппинг, полосы ≤256);
putimage(COPY_PUT) и getimage переведены (регресс tests/bgi_img
1:1 с per-pixel эталоном, подрежимы банков на accel-пути —
tests/gfxbanks).
- [ ] **вертикальный copy-leaf `_bgi_copy_cols_raw`** (режим LD A,A —
вертикальная копия, Port_Y двигается сам как у vfill). Анализ
2026-07-12: для СПРАЙТОВ требует column-major хранения (читать
линейный буфер вертикально нельзя — Port_Y не действует вне
видеоокна; референс docs/samples/balls пишет колонками и потому
выводит спрайт ТРАНСПОНИРОВАННЫМ — незаметно только на
симметричном шаре) → формат несовместим с getimage, не делать.
А вот **вертикальный HEAL формат-независим** (экран→экран) и
выгоден для узких высоких областей: L-полоска 2×16 при
горизонтальном движении спрайта = 2 burst'а вместо 16 (8×
меньше оверхеда); выбор ориентации по форме — как в
_gfx_rectfill256. Нюанс: между read и write колонки Port_Y
надо вернуть на y0 (стоп → OUT → ре-арм, ~20Т/колонку).
Делать вместе с L-strip оптимизацией heal.
- [ ] **паттерны через акселератор + FF-прозрачность** (идея 2026-07-11):
всё, что сейчас рисуется по-пиксельно из-за «дырок», можно гнать
burst'ами через банк 0x58 — дырки паттерна кодируются 0xFF и
отбрасываются железом на записи:
- стилизованные линии (_bgi_styled_line: DOTTED/DASHED/CENTER/
USERBIT сейчас per-pixel): построить 16-байтовый шаблон строки
из 16-бит маски (бит=цвет, 0=0xFF) и повторять accel-copy;
- fill-паттерны (_bgi_fill_span, LTSLASH_FILL и пр.): 8-байтовые
строки-шаблоны 8×8 паттерна тем же способом;
- ВНИМАНИЕ: на MAME 0.283 запись FF через 0x58 портит теневое
ОЗУ (частичный скип, см. sprite-api-design «Результаты Фазы 0»)
— включать после подтверждения полного подавления на железе,
либо через 0x5C + пере-heal.
- [x] ~~gfx_blit / putsprite / gfx_heal / movesprite~~ — Фаза B
сделана 2026-07-11 (docs/sprite-api-design.md §3): GFX_BANK_*
константы + gfx_blit/gfx_blit_part/gfx_heal в gfx.h,
putsprite/movesprite в graphics.h; проверено tests/sprites
(прозрачность, клиппинг 4 краёв, heal src==dst, чистый след
movesprite, атлас). Осталась Фаза C — пример-курсор.
- [ ] **managed-движок спрайтов v2** — sprite_t (drawn-координаты
per-page ВНУТРИ структуры) + retained-модель:
sprite_move/frame/show/hide меняют только state,
sprite_update(arr,n) делает «heal все → блит все» на текущей
draw-странице, sprite_flip — сахар с vsync+флипом; z-order =
индекс массива. Интегрирует спрайты со сменой страниц —
снимает с приложения учёт drawn[2][N] (источник багов,
examples/balls 2026-07-11). Полный эскиз:
docs/sprite-api-design.md §9.1.
- [ ] `gfx_xor_rect` / `gfx_or_rect` / `gfx_and_rect` / `gfx_invert_rect`
- [ ] шрифты ≠ 8×8: gfx_set_font_data(ptr,w,h,advance), proportional,
8×16/16×16, отдельный font_id API; font-quad для 640×256
(per-cell палитра через дескрипторы 0x0300..0x039F)
Оптимизации (не сейчас):
- [x] ~~stride-арифметика в _bgi_copy_rows_raw — вон из горячего
цикла~~ — сделано 2026-07-12 (профиль examples/balls vs
docs/samples/balls): универсальный leaf — SMC-патч страйдов в
8-битные add/adc-цепочки при входе (140Т → 73Т/строку, BC/push/
pop/ex ушли); heal — отдельный `_bgi_heal_rows_raw` без адресной
арифметики вообще (77Т/строку против 204Т; референсный уровень).
Шар 16×16: ~9.6 → ~6.6 кТ (heal 16×173 + блит 16×236). Регресс:
blitw/bgi_img/sprites/balls — 1:1. Остатки разрыва с asm-
референсом — C-обвязка вызовов (batch-пасс, см. ниже) и спец-
blit-leaf (dstride 0: 30Т/строку — делать по замеру).
- [ ] **batch-пасс для спрайтов** (движок v2, §9.1) — В РАБОТЕ 2026-07-12.
Разрыв ~2× с asm-референсом (docs/samples/balls) — это НЕ
W3-скобка (_bgi_begin = 5 инструкций, ~35Т), а per-call
C-обвязка, повторяемая 2×N раз: полный клип (528/362 Б),
парс заголовка img, save/restore банка, SDCC-фрейм 7-арг вызова.
Референс всё это платит ОДИН раз на проход: W3 замаплен на видео
на всю программу (в цикле не трогает), банк-подрежим ставит один
`out` на проход (0x5C рисовать всё → 0x50 лечить всё), DI держит
на весь проход. Движок v2 повторяет это (per-pass W3/банк/клип-
fast-path, адресация из кэша w,h в структуре), НО DI оставляем
гранулярным (по строке в leaf) — референс держит длинный DI
только потому, что у него нет аудио-ISR; под CBL/IM2 длинный DI
сорвёт звук (docs/accel-fill-budget.md).
- [ ] клип fast-path «прямоугольник целиком на экране» в gfx_blit_part
(4 сравнения вместо полного пути) — делается в _gfx_blit_full/
_gfx_heal_full вместе с batch-пассом.
- [ ] размер: общий clip-хелпер для gfx_blit_part (528 Б) и gfx_heal
(362 Б) — клиппинг сейчас продублирован; кандидат −300..400 Б.
- [ ] gfx_line через accel для пологих диагоналей (runs ≥ 4-5 px)
- [ ] композитные примитивы с одним W3-swap на операцию
- [ ] ~~**спрайт-анимация: heal только открывшейся L-полоски**~~
ОТВЕРГНУТО 2026-07-12 для ПРОЗРАЧНЫХ спрайтов (наш случай).
L-полоска (bbox старой позиции минус новой) корректна ТОЛЬКО для
непрозрачного full-box спрайта: тогда зону перекрытия целиком
перекрашивает новый блит. У прозрачного спрайта (0xFF через
0x5C) в перекрытии дырки нового кадра НЕ перекрывают старые
непрозрачные пиксели → на хвосте остаётся «полумесяц» старого
изображения ВНУТРИ bbox-перекрытия, куда L-полоска не достаёт.
Точный «новооткрытый» набор = old_opaque AND NOT new_opaque —
это heal-с-маской по форме, а не по bbox, на Z80 дороже самого
heal. Подтверждено чтением референса docs/samples/balls:
restore_bg лечит ПОЛНЫЙ 16×16 (ld b,16 + 16 байт/строку),
L-полоску не использует. Итог: heal остаётся full-box; экономия
только через batch-пасс (амортизация обвязки), не через L-полоску.
L-полоска годна лишь для непрозрачных тайлов фона — не спрайтов.
Дисциплина «heal ВСЕ → блит ВСЕ» по-прежнему обязательна (слияние
heal+блит per-sprite выкусывает соседа на перекрытии — проверено
examples/balls 2026-07-11).
- [ ] **span-примитивы для узких прямоугольников**: при узкой стороне
≤ 8 линий chunked-rectfill проигрывает простому циклу
_bgi_hspan_raw/_bgi_vspan_raw (подготовка+precompute ~340Т уходят
@@ -134,6 +217,14 @@ Quick wins:
Y..Y+15): в MAME 0.283 НЕ работает (2026-06-01). На железе:
dual-write тест → если работает, кэшировать Port_Y в putpixel
(~8× меньше OUT для Брезенхэма); если нет — вычистить из доков.
- [ ] **Подрежимы видеостраниц #50..#5F** (tests/gfxbanks +
tests/sprites, дизайн docs/sprite-api-design.md): перепрогнать
на железе и сверить со скриншотами MAME. Три открытых вопроса: (1) 0x58 — полное
подавление FF-записи (док/master-MAME) или FF протекает в
теневое ОЗУ (MAME 0.283)? (2) FF в VRAM — цвет 255 (MAME) или
display-подстановка фона из ОЗУ (тогда дешёвое стирание
FF-заливкой, tests/fferase)? (3) accel-путь = CPU-пути (в ПЛМ
пути физически разные).
- [ ] fdmax: лимит манипуляторов и зависание 9-го OPEN — MAME vs железо.
- [ ] CBL: щелчок перед первым проигрыванием звука за сессию (tests/
cbltest, tests/cblwav) — воспроизводится ТОЛЬКО на первом запуске
+22 -4
View File
@@ -190,8 +190,13 @@ wrchar rdchar`; режимы `gettextmode/settextmode` (0x02=40×32,
Setup: `gfx_init(mode,page)→prev`, `gfx_done(prev)`.
Страницы/банк: `gfx_set/get_visible_page`, `gfx_set/get_draw_page`
(double buffering), `gfx_set/get_bank` (0x50..0x5F, 0x58 = FF-
прозрачность).
(double buffering), `gfx_set/get_bank` (GFX_BANK_NORMAL 0x50 /
NOSHADOW 0x54 / TRANSPARENT 0x58 / SPRITE 0x5C — аппаратные подрежимы
записи, действуют на ВСЕ примитивы; см. docs/sprite-api-design.md).
Блиттинг (Фаза B, 2026-07-11): `gfx_blit(x,y,img)`,
`gfx_blit_part(x,y,img,sx,sy,w,h)` (атлас), `gfx_heal(x,y,w,h)`
(восстановить фон из ОЗУ-копии; стирание спрайтов без save-буфера);
img — getimage-формат, буфер вне W3; клиппинг по экрану есть.
`gfx_wait_vsync()` (2026-07-07, редизайн): ждёт переход бита 5 порта
0xFE из 1 в 0 — реальное аппаратное состояние луча (Y>256 → начало
@@ -234,8 +239,21 @@ CP), `line(x1,y1,x2,y2)` (не двигает).
`fillpoly(n,pts)`, `fillellipse(x,y,xr,yr)`.
Заливка областей (Ф2c): `floodfill(x,y,border)` (медленно, но верно),
`pieslice(x,y,st,end,r)`, `sector(x,y,st,end,xr,yr)`.
Спрайты (Ф2d): `imagesize/getimage/putimage` (COPY/XOR/OR/AND/NOT_PUT;
формат буфера: uint16 w,h + w*h байт).
Образы (Ф2d): `imagesize/getimage/putimage` (COPY/XOR/OR/AND/NOT_PUT;
формат буфера: uint16 w,h + w*h байт). COPY_PUT и getimage — через
accel block-copy (2026-07-11, Фаза A sprite-api-design), COPY с
клиппингом и текущим банком; XOR/OR/AND/NOT — per-pixel.
Спрайты (2026-07-11, Фаза B sprite-api-design): `putsprite(x,y,img)`
блит getimage-буфера с аппаратной прозрачностью (0xFF =
GFX_TRANSPARENT не пишется) банком GFX_BANK_SPRITE (0x5C, фон в
ОЗУ-копии цел); `movesprite(ox,oy,x,y,img)` — heal старой позиции +
putsprite новой (save-буфер не нужен). Низкий уровень в <gfx.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`
(SOLID/DOTTED/CENTER/DASHED/USERBIT + NORM/THICK) — на line/rectangle/
drawpoly.
+7 -1
View File
@@ -3,13 +3,16 @@ accfill 3772
argv 3431
assrtest 3847
attrprob 4087
balls 6393
banked 1056
bankedbg 1067
banklocl 4683
banktest 3767
bgi_img 6222
bgi_img 7060
bgitest 3737
bios_text 4461
blitperf 5860
blitw 4939
cat 913
cblstream 6217
cbltest 6366
@@ -24,6 +27,7 @@ filetest 10547
gets 509
gfx_dbuf 5032
gfx_demo 4103
gfxbanks 6044
hello 4167
hello2 4244
irqtest 5850
@@ -39,6 +43,8 @@ rt_test 4892
seek 4188
simple 955
solidt 11531
spriteclip 4187
sprites 9968
stattest 7549
stdlib 6643
stest2 3644
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.