Files
Sprinter-SDCC/docs/sprite-api-design.md
T
snark13 72ce66275e 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>
2026-07-12 21:51:52 +03:00

595 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Спрайтовое расширение 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.