0eec977630
Профилирование rpgwalk в dev-MAME (watchpoint на OUT-маркеры + totalcycles) показало: blit 8 спрайтов ел 17 мс из 20.5 — 85% в цикле `while (sy--) src += img_w;` blit-обёрток (писался под «обычно sy==0», а вертикальные ленты атласов дают sy до 176 → до 64К тактов на кадр). Фикс: src += sy*img_w через __mulint (O(1); __mul16 адаптивен — при sy < 256 крутит 8 итераций, ~500Т) + if (sy): горизонтальные ленты и одиночные спрайты не платят и за умножение. Blit: 45К → 11.8К тактов/спрайт. Замерен бюджет кадра (docs/sprite-api-design.md §9д): кадр 48.83 Гц = 430080 тактов @21МГц; спрайт 16×16 ≈ 26К (тик 7.3К + heal 6.4К + blit 11.8К) → лимит стабильных 48 fps = 14 спрайтов (15 — 94% кадров, 16 — на грани). FPS-плашка bar+outtextxy стоит ~210К (полкадра!) — HUD рисовать putimage-заготовкой. examples/rpgwalk/rpgprof.c — профилировочная копия демо: визуальный профайлер (цвет бордера по секциям кадра) + маркеры для тактового профайла через wpiset дебаггера (рецепт в шапке и §9д). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
832 lines
62 KiB
Markdown
832 lines
62 KiB
Markdown
# Спрайтовое расширение 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`).
|
||
|
||
### 3.1 Атласы: рекомендация по компоновке и файл-формат (предложение)
|
||
|
||
Решение 2026-07-13: in-memory адресация кадра остаётся как есть —
|
||
атлас = обычная getimage-картинка, кадр выбирается sx/sy (единичные
|
||
изображения, ленты по горизонтали и по вертикали, сетки nx×ny).
|
||
|
||
В одном атласе могут храниться изображения разных размеров и разного
|
||
количества вариаций. РЕКОМЕНДАЦИЯ: для одного спрайта держать кадры
|
||
одинакового размера и полностью заполнять его сетку (nx*ny == число
|
||
вариаций) — иначе в прямоугольнике атласа возникают пустые сегменты,
|
||
теряющие место и на диске, и в памяти при загрузке. Смешивать
|
||
спрайты РАЗНЫХ размеров в одном прямоугольнике не надо вообще: для
|
||
этого предлагается файл-формат БЕЗ пустых мест — контейнер независимых
|
||
лент, где паддинг невозможен по построению.
|
||
|
||
Файл-атлас (.atl, предложение — НЕ реализовано):
|
||
|
||
```
|
||
Шапка (8 байт): 'S','P','A','1' | count u8 | резерв ×3
|
||
Каталог (count × 8 байт):
|
||
offset u16 смещение ленты от начала файла
|
||
fw, fh u8,u8 размер кадра
|
||
nx, ny u8,u8 сетка кадров (вариаций = nx*ny)
|
||
резерв u16 (id/флаги)
|
||
Данные: count лент подряд, каждая — обычный getimage-блоб
|
||
(u16 w = fw*nx, u16 h = fh*ny, пиксели построчно)
|
||
```
|
||
|
||
ВАЖНО: offset указывает на ПЕРВЫЙ БАЙТ getimage-шапки ленты (не на
|
||
пиксели) — 4 байта `u16 w, u16 h` хранятся В ФАЙЛЕ в начале каждого
|
||
блоба. Поэтому каждая лента — самостоятельная getimage-картинка со
|
||
своим stride: после загрузки файла целиком в память указатель
|
||
`base + dir[i].offset` напрямую годится в putsprite/sprite_init/
|
||
gfx_blit_part — рантайм не меняется вовсе, кадр (i,j) = sx=i*fw,
|
||
sy=j*fh. (w/h ленты при этом задублированы: выводимы из fw*nx/fh*ny
|
||
каталога И лежат в шапке блоба — осознанные 4 байта на спрайт за
|
||
прямую совместимость указателя. Каталог же несёт то, чего в шапке
|
||
нет: размер КАДРА и сетку — без них лента 128×16 неотличима от
|
||
одного кадра 128×16.) Пустых байтов нет по
|
||
построению: ленты разных размеров просто конкатенируются. Пример
|
||
(два спрайта 16×16 на 8 и 12 вариаций + один 24×24 на 4): шапка 8 +
|
||
каталог 24 + (4+2048) + (4+3072) + (4+2304) = 7468 байт, паддинг 0.
|
||
Привязка к W0-страницам (решение 2026-07-13, см. §9в): один атлас =
|
||
одна EMM-страница = один файл. Размер данных атласа ограничен
|
||
16К − 0x100 (страница минус резерв ISR-стаба); если данных больше —
|
||
следующий атлас в ОТДЕЛЬНОМ файле на своей странице (sprite_t.page их
|
||
различает). Два варианта офсетов в каталоге:
|
||
|
||
I. offset = адрес в W0 (0x0100-based): каталог сразу содержит
|
||
готовые указатели, данные в файле идут после каталога впритык.
|
||
II. файл несёт 0x100-байтовый заголовок, so that файл-офсет ==
|
||
офсет в странице == W0-адрес: загрузчик читает файл ЦЕЛИКОМ в
|
||
offset 0 страницы и лишь патчит стаб (3 байта JP в 0x38, RETN в
|
||
0x66). Раскладка заголовка: 0x00-0x07 магия+count,
|
||
0x08-0x37 резерв, 0x38-0x3A и 0x66-0x67 — место под патч стаба,
|
||
0x68-0xFF каталог (19 записей × 8 Б). Файл ≤ 16384 Б ровно.
|
||
|
||
Рекомендуется II: загрузка = один read() страницы, замапленной в W3
|
||
(приём проверен в mdview2) + патч 5 байт; офсеты совпадают в файле,
|
||
странице и W0 — нечего пересчитывать. Вариант I оставляет атласам
|
||
возможность жить в обычной памяти (malloc-буфер) без страницы.
|
||
Python-упаковщик (PNG → .atl) в toolchain и загрузчик — делать по
|
||
потребности первого реального приложения.
|
||
|
||
## 4. Внутренности
|
||
|
||
### 4.1 Leaf-примитив (per-driver, asm)
|
||
|
||
Один новый leaf в `bgi256/` (и позже `bgi16/`), объявление в `_bgi.h`:
|
||
|
||
```c
|
||
/* Копирование h строк по w пикселей через accel block-copy.
|
||
* dst/src — CPU-адреса ПЕРВОЙ строки; dstride/sstride — шаг адреса
|
||
* между строками (0 = адрес не двигается, строку выбирает PORT_Y —
|
||
* сторона, живущая в видеоокне); y0 — стартовый PORT_Y (инкремент
|
||
* на строку). Raw: без клиппинга, W3 уже замаплен (_bgi_begin),
|
||
* вызывающий гарантирует 1 <= w <= 256 (полоса).
|
||
* DI/EI — вокруг каждой пары триггеров (как в fill-сегментах). */
|
||
void _bgi_copy_rows_raw(uint8_t *dst, const uint8_t *src,
|
||
uint8_t w /*0=256*/, uint8_t h /*0=256*/,
|
||
int dstride, int sstride, uint8_t y0);
|
||
```
|
||
|
||
Схема тела (референс — `Draw_Restangle_Data` + существующий SMC-паттерн
|
||
`_gfx_hfill256_segment`):
|
||
|
||
```
|
||
SMC: размер блока <- w (LD D,D ; LD A,#imm ; LD B,B — один раз)
|
||
loop h раз:
|
||
out (0x89), y ; y++
|
||
di
|
||
LD L,L ; режим копии
|
||
LD A,(HL) ; burst-чтение src
|
||
LD (DE),A ; burst-запись dst
|
||
LD B,B ; стоп
|
||
ei
|
||
HL += sstride ; DE += dstride
|
||
```
|
||
|
||
Один leaf покрывает все четыре операции §2:
|
||
|
||
| Вызов | dst, dstride | src, sstride |
|
||
|--------------|-------------------------|----------------------------|
|
||
| блит | экран (base+x), 0 | буфер (row0), img_w |
|
||
| getimage | буфер, w | экран (base+x), 0 |
|
||
| heal | экран (base+x), 0 | тот же адрес экрана, 0 |
|
||
|
||
Регистровая раскладка — на этапе реализации; ориентир: HL=src, DE=dst
|
||
внутри цикла (триггеры именно такие), w через SMC, остальное со стека
|
||
в локальные регистры/IX (вызывается один-два раза на блит — не горячий
|
||
ABI, в отличие от fill-сегментов).
|
||
|
||
### 4.2 Ядро (common/)
|
||
|
||
`gfx_blit_part` — единственная «умная» функция (реальная логика):
|
||
|
||
1. прочитать w,h из заголовка img (для gfx_blit: весь rect);
|
||
2. клиппинг: x<0 → сдвиг sx и сужение w; правый/нижний край → сужение;
|
||
пустой результат → выход;
|
||
3. `src0 = img + 4 + sy*img_w + sx`;
|
||
4. полосы: если w > 256 — разрезать на вертикальные полосы ≤ 256 байт
|
||
(при экране 320 полос максимум две; каждая полоса = один SMC размера
|
||
блока);
|
||
5. `_bgi_begin()` → `_bgi_copy_rows_raw(...)` на полосу → `_bgi_end()`.
|
||
|
||
Остальное — тонкие модули (1 функция = 1 .rel):
|
||
|
||
- `gfx_blit.c` — читает w,h, зовёт gfx_blit_part(x,y,img,0,0,w,h);
|
||
- `gfx_heal.c` — клип; банк: сохранить `_gfx_bank`, форс 0x50,
|
||
`_bgi_begin`; leaf с dst=src=base+x, strides 0; восстановить банк;
|
||
- `putsprite.c` — сохранить `_gfx_bank`, `|` → 0x5C, `gfx_blit`,
|
||
восстановить (именно `save/restore`, а не тупо 0x50 — уважаем
|
||
вложенность и пользовательский temp-режим);
|
||
- `movesprite.c` — `gfx_heal(oldx,oldy,w,h)` (w,h из заголовка img) +
|
||
`putsprite(x,y,img)`.
|
||
|
||
### 4.3 Совместимость с существующим кодом
|
||
|
||
- `_bgi_begin/_bgi_end` не меняются (банк уже берут из `_gfx_bank`).
|
||
- `gfx_set_bank/gfx_get_bank` не меняются; в `gfx.h` добавляются только
|
||
`GFX_BANK_*`/`GFX_TRANSPARENT` и три прототипа.
|
||
- Довесок к контракту (задокументировать у putimage/getimage тоже):
|
||
буферы картинок обязаны лежать вне W3 (`< 0xC000`) — на время
|
||
операции W3 замаплен на видеобанк. Это верно и сегодня (per-pixel
|
||
путь), просто не было записано.
|
||
- Второй draw-page: ядро использует `_gfx_addr_base` — двойная
|
||
буферизация работает автоматически.
|
||
|
||
## 5. Переезд putimage/getimage на ядро
|
||
|
||
- `putimage(COPY_PUT)` → `gfx_blit` (текущим банком — поведение
|
||
обратно-совместимо: дефолтный банк 0x50).
|
||
- `getimage` → `_bgi_copy_rows_raw` (dst=буфер). Клиппинга нет, как в
|
||
BGI (контракт: rect валиден); safe-версия сохраняет текущие проверки.
|
||
- `XOR/OR/AND/NOT_PUT` — остаются на per-pixel пути (v1). У
|
||
акселератора есть блочные AND/OR/XOR (пример «encode» в
|
||
accelerator_doc.txt) — ускорение этих op — «следующая версия» (§9).
|
||
- `imagesize` — без изменений (256-режим); для 16-режима станет
|
||
mode-specific (§8).
|
||
|
||
Ожидание по скорости: per-pixel путь платит на КАЖДЫЙ пиксель
|
||
`out Port_Y` + пересчёт адреса + bounds-check (~50–80Т); ядро платит на
|
||
СТРОКУ ~40–60Т CPU + burst акселератора (~байт/7МГц). На спрайте 16×16
|
||
это порядка 10–20× (замерить в Фазе A, добавить в size-baseline).
|
||
|
||
## 6. Паттерны использования (войдут в libc-reference)
|
||
|
||
Подвижный спрайт (канон):
|
||
|
||
```c
|
||
initgraph();
|
||
/* фон рисуем обычным банком — он попадает и в ОЗУ-копию (бэкап) */
|
||
draw_background();
|
||
putsprite(x, y, hero); /* 0x5C: фон в ОЗУ цел */
|
||
while (game) {
|
||
int nx = x + dx, ny = y + dy;
|
||
gfx_wait_vsync();
|
||
movesprite(x, y, nx, ny, hero); /* heal старого + блит нового */
|
||
x = nx; y = ny;
|
||
}
|
||
```
|
||
|
||
Курсор мыши (пример examples/, не API): то же самое с
|
||
`GFX_BANK_SPRITE`; фон под курсором живёт в ОЗУ-копии, никакой
|
||
getimage/буфер не нужен. Перерисовка — из главного цикла по
|
||
`mouse_getxy()`.
|
||
|
||
Впечатать спрайт в фон навсегда (декорация):
|
||
`gfx_set_bank(GFX_BANK_TRANSPARENT); gfx_blit(...); gfx_set_bank(GFX_BANK_NORMAL);`
|
||
— 0x58 без bit2: спрайт уходит и в ОЗУ-копию, heal его уже «не сотрёт».
|
||
ВНИМАНИЕ: на MAME 0.283 этот паттерн ломается — FF-байты спрайта
|
||
попадают в ОЗУ-копию (частичный скип, см. результаты Фазы 0);
|
||
до подтверждения на железе печатать декорации без FF в данных.
|
||
|
||
ВАЖНО про cleardevice/bar поверх спрайтов: пока действует банк с bit2
|
||
(0x54/0x5C), «фоновые» операции не обновляют ОЗУ-копию. Правило:
|
||
сцена/фон — только обычным банком (или 0x58), спрайты/оверлеи — 0x5C.
|
||
|
||
## 7. План работ
|
||
|
||
- **Фаза 0 — верификация подрежимов** (`tests/gfxbanks`).
|
||
ВНИМАНИЕ: поведение подрежимов в MAME может отличаться от реального
|
||
устройства (прецедент — Port_Y banking, memory/gfx_port_y_banking).
|
||
Результат в MAME НЕ финален: тест гоняется и в MAME, и на железе;
|
||
до прогона на железе вердикты считаются предварительными.
|
||
Последовательный сценарий на одном экране, скриншот после каждого
|
||
шага (палитра[255] = синий; фон — узнаваемый паттерн, не заливка):
|
||
1. **Фон** через 0x50 (уходит и в VRAM, и в ОЗУ-копию).
|
||
2. **Спрайт через 0x5C** (bit2+bit3): квадрат, внутри зелёный круг,
|
||
вокруг круга — байты 0xFF. Ожидание по доку: виден круг поверх
|
||
фона, FF-точки скипнуты (фон вокруг круга цел). Проверяет
|
||
bit3-скип на CPU-пути; если вместо фона вокруг круга синяя рамка —
|
||
bit3 не эмулируется/не работает.
|
||
3. **Ключевой шаг — FF реально в VRAM**: через 0x54 (bit2, БЕЗ bit3 —
|
||
скипа нет, FF пишется) залить квадрат байтом 0xFF поверх круга.
|
||
Что отображается в квадрате:
|
||
- **синий** → FF в VRAM — обычный цвет 255, подстановки при
|
||
отображении нет (семантика дока полная) → стирание только heal;
|
||
- **фон** → видеокарта подставляет байт из ОЗУ-копии, когда в
|
||
VRAM лежит FF → «слой спрайтов» существует на уровне
|
||
отображения, дешёвое стирание FF-заливкой работает
|
||
(tests/fferase становится штатным путём);
|
||
- **круг остался** → запись через 0x54 не произошла — bit2/банк
|
||
не эмулируется.
|
||
4. **Heal** через 0x50 (чтение+запись того же rect): фон должен
|
||
восстановиться полностью. Заодно различает bit2: если bit2 не
|
||
работал, шаги 2–3 испортили ОЗУ-копию и heal вернёт не фон.
|
||
5. **Accel-путь**: повторить шаги 2–3 записью через акселератор
|
||
(copy/fill) — работает ли bit3-скип и поведение FF на burst-пути.
|
||
Если MAME не эмулирует биты 2/3 — API остаётся как есть (семантика
|
||
задана железом/доком), но функциональные тесты уезжают в раздел
|
||
«проверить на железе» TODO, а пример курсора делаем без bit2
|
||
(fallback: save/restore через getimage).
|
||
|
||
**РЕЗУЛЬТАТЫ (MAME 0.283 = сборка v306, 2026-07-11; железо — TODO):**
|
||
| Вопрос | Вердикт в MAME |
|
||
|---|---|
|
||
| bit2 (0x54/0x5C): ОЗУ-копия не трогается | РАБОТАЕТ (A: PASS, B: PASS) |
|
||
| bit3 через 0x5C: скип FF | РАБОТАЕТ, полный no-op (CPU и accel) |
|
||
| bit3 через 0x58: скип FF | **ЧАСТИЧНО**: VRAM скипается, но теневое ОЗУ ПОЛУЧАЕТ FF (C: FAIL) — код драйвера 0.283 гасит только vram_w; в master-драйвере исправлено на полное подавление (по доку) |
|
||
| Отображение FF в VRAM (шаг 3) | обычный **цвет 255** (синий), подстановки ОЗУ нет → стирание = heal; FF-заливка НЕ работает |
|
||
| heal (чтение ОЗУ + запись 0x50) | РАБОТАЕТ (полосы восстановлены; FF-порчу ОЗУ честно переносит в VRAM) |
|
||
| accel-путь vs CPU-путь | идентичны во всех подрежимах (в MAME оба через ram_w) |
|
||
|
||
Следствия: (1) putsprite через 0x5C работает одинаково во всех
|
||
версиях — расхождение 0.283 не задевает; (2) паттерн «впечатать
|
||
декорацию через 0x58» на 0.283 портит ОЗУ-копию FF-байтами спрайта —
|
||
использовать только после проверки на железе; (3) tests/fferase в
|
||
MAME заведомо FAIL — остаётся для прогона на железе.
|
||
Квирк инфраструктуры: delayms/sleep в --memory small не работают
|
||
(irq_install → EINVAL, калибровка молча не происходит) — паузы в
|
||
тесте сделаны по RTC (getdatetime).
|
||
- **tests/fferase — альтернативное стирание FF-заливкой через 0x54**
|
||
(существует ДО подтверждения итога шага 3 и на MAME, И на железе;
|
||
после — либо удаляется, либо становится основой оптимизации).
|
||
Сценарий: фон → спрайт через 0x5C → залить прямоугольник спрайта
|
||
байтом 0xFF через 0x54 (bit3=0 — FF реально пишется в VRAM) →
|
||
скриншот: восстановился ли фон. Если да (на железе!) — стирание
|
||
вдвое дешевле heal (fill-burst без фазы чтения, готовый
|
||
_gfx_rectfill256), реализацию gfx_heal можно переключить, сигнатура
|
||
и movesprite не меняются. Основной механизм move в любом случае —
|
||
heal (чтение/запись через 0x50): он работает при ОБЕИХ семантиках.
|
||
- **Фаза A — ядро + переезд** (без нового публичного API, чистое
|
||
ускорение): `_bgi_copy_rows_raw` (bgi256), `gfx_blit_part` + полосы +
|
||
клиппинг, порт putimage(COPY)/getimage. Регресс — tests/bgitest +
|
||
`make size-check`.
|
||
|
||
**ГОТОВО 2026-07-11**: leaf `bgi256/_bgi_copy_rows_raw.c` (регистры
|
||
BC = счётчик строк/y, stride-сложения через push bc; SMC размера
|
||
блока один раз на вызов), ядро `common/gfx_blit_part.c` (клиппинг
|
||
всегда, sy без __mulint — циклом сложений, полосы ≤256), putimage
|
||
(COPY_PUT → ядро; XOR/OR/AND/NOT — прежний per-pixel путь) и
|
||
getimage (grab-leaf, dstride = w) переведены. Прототип
|
||
gfx_blit_part пока в _bgi.h (Фаза B перенесёт в gfx.h как есть).
|
||
Проверено в MAME: tests/bgi_img финальный кадр 1:1 с per-pixel
|
||
эталоном (src == COPY, XOR×2 чист); tests/gfxbanks — прозрачность
|
||
FF и bit2 работают на accel-COPY пути идентично CPU-пути (круг с
|
||
прозрачными полями через putimage/0x5C, FF→VRAM через putimage/0x54).
|
||
Размер: bgi_img +592 Б (_CODE; leaf + клиппинг-ядро + ветка COPY),
|
||
baseline принят. Попутно: pgrep-фильтр в mame_interactive.py сужен
|
||
до эмулятора (ложно срабатывал на параллельную сборку MAME из
|
||
исходников).
|
||
- **Фаза B — спрайтовый API**: GFX_BANK_*, gfx_blit, gfx_heal,
|
||
putsprite, movesprite; тест tests/sprites (анимация по синусоиде
|
||
поверх пёстрого фона, скриншоты «фон не разрушен»).
|
||
|
||
**ГОТОВО 2026-07-11**: константы GFX_BANK_*/GFX_TRANSPARENT и
|
||
gfx_blit/gfx_blit_part/gfx_heal — в gfx.h; putsprite/movesprite — в
|
||
graphics.h; модули common/gfx_blit.c, gfx_heal.c, putsprite.c,
|
||
movesprite.c (тонкие, поверх ядра Фазы A). putsprite/gfx_heal
|
||
сохраняют и восстанавливают пользовательский банк (heal — форс
|
||
0x50, putsprite — форс 0x5C на время вызова). Проверено в MAME
|
||
(tests/sprites): прозрачные углы, клиппинг во все 4 края,
|
||
heal-стирание (accel src==dst — риск §10.4 закрыт), movesprite
|
||
12 шагов с чистым следом, кадр атласа через gfx_blit_part; ОЗУ-копия
|
||
цела (проверки A/B PASS). size-check: роста существующих программ
|
||
нет (новые модули тянутся только пользователями API).
|
||
|
||
Демо **examples/balls** (2026-07-11): 8 разноцветных шаров 16×16
|
||
(по спрайту на цвет, прозрачные углы) над чёрно-белой шахматкой;
|
||
скорости 1..4 привязаны к кадрам (gfx_wait_vsync, speed = кадров на
|
||
шаг: 50/25/~17/12.5 px/с — подтверждено покадровыми скриншотами),
|
||
отражение от краёв, Esc — выход. **Двойная буферизация** по образцу
|
||
docs/samples/balls (реф. asm-демо Sprinter): рисование только на
|
||
скрытой странице (heal ВСЕХ старых позиций этой страницы → блит
|
||
ВСЕХ шаров), флип по vsync — одиночная страница мерцала (heal+блит
|
||
на видимой ловились лучом; пользователь заметил, реф не мерцает).
|
||
Требует зеркалирования палитры в палитру 1 (initgraph грузит EGA
|
||
только в 0) и координат drawn[2][N] per-page. Побочно ушли и
|
||
одно-кадровые артефакты перекрытий/снапшотов.
|
||
- **Фаза C — пример курсора** (examples/): мышь + putsprite/gfx_heal,
|
||
он же живой тест temp-режима.
|
||
- Документация: libc-reference (раздел «Спрайты»), обновить TODO.
|
||
|
||
## 8. Режим 16 цветов (Фаза 2 libbgi — заметки на будущее)
|
||
|
||
- Прозрачная единица — БАЙТ = пара пикселей цвета 15 (0xFF). Фигурные
|
||
края спрайта квантуются парами; прозрачного «одиночного» пикселя нет.
|
||
- Leaf `bgi16/_bgi_copy_rows_raw`: те же триггеры, адресация x/2;
|
||
клиппинг и координаты x — по чётным границам (нечётные края потребуют
|
||
RMW-кромок per-row, v1 16-режима: требовать чётные x/w).
|
||
- `imagesize`/формат: данные packed (2 пикселя/байт) — imagesize станет
|
||
mode-specific leaf'ом.
|
||
|
||
## 9. Не в этой версии (кандидаты в следующую)
|
||
|
||
- **Managed-движок**: см. эскиз §9.1 (sprite_t + retained-модель).
|
||
- **putsprite_part** (кадр атласа одним вызовом, без ручного банка) —
|
||
добавить, как только появится первый пользователь-игра.
|
||
- **.spr-ресурс**: файловый заголовок-обёртка (магия/версия/hotspot)
|
||
вокруг getimage-payload + загрузчик + PC-конвертер (PNG→spr, iconv-
|
||
стиль пайплайн). In-memory формат не меняется.
|
||
- **XOR/OR/AND_PUT через акселератор** (блочные режимы есть в железе).
|
||
- **RLE-сжатие** (распаковка при загрузке — фича загрузчика).
|
||
- **Вертикальный блит** (`LD A,A`-режим) — для повёрнутых спрайтов не
|
||
хватает и его; не тянем.
|
||
- **ISR-курсор** (перерисовка из IM2-тика) — после Audio/IM2 v2.
|
||
|
||
## 9.1 Эскиз managed-движка: sprite_t + retained-модель (v2, НЕ реализовано)
|
||
|
||
Мотивация — опыт examples/balls (2026-07-11): при двойной буферизации
|
||
приложение обязано помнить, где каждый спрайт РЕАЛЬНО нарисован на
|
||
КАЖДОЙ из двух страниц (drawn[2][N]), и соблюдать двухпроходную
|
||
дисциплину «heal все → блит все». Оба правила легко нарушить (heal по
|
||
позиции прошлого кадра вместо позапрошлого → фон зарастает кромками;
|
||
слияние heal+блит per-sprite → укусы на перекрытиях — проверено
|
||
экспериментально). Этот учёт — работа библиотеки, не приложения.
|
||
|
||
Разбор asm-референса (docs/samples/balls, 2026-07-12) подтвердил
|
||
направление: наш ~2× разрыв — не W3-скобка (_bgi_begin = 5 инструкций),
|
||
а per-call C-обвязка (клип 528/362 Б, парс заголовка, save/restore
|
||
банка, SDCC-фрейм 7-арг), повторяемая 2×N раз. Референс платит это
|
||
ОДИН раз на проход: W3 замаплен на видео на всю программу, банк-подрежим
|
||
ставит один `out` на проход (0x5C рисовать всё → флип → 0x50 лечить
|
||
всё), клипа нет вообще (адрес спрайта = сдвиги), heal — полный 16×16
|
||
(restore_bg). Движок v2 повторяет ту же per-pass структуру (одна
|
||
W3-скобка + один банк + клип-fast-path на проход; w,h из кэша структуры).
|
||
ЕДИНСТВЕННОЕ отличие, которое НЕ копируем: референс держит DI на весь
|
||
проход (32 шара) — у него нет аудио-ISR; у нас DI остаётся гранулярным
|
||
(по строке в leaf), иначе длинный DI сорвёт CBL/IM2-звук.
|
||
|
||
### Структура (черновик)
|
||
|
||
```c
|
||
typedef struct {
|
||
const void *img; /* getimage-формат / атлас-лента */
|
||
int x, y; /* ЛОГИЧЕСКАЯ позиция (куда хочет) */
|
||
int sx, sy; /* кадр атласа (под-прямоугольник img) */
|
||
uint8_t w, h; /* размер кадра, 0 = 256 (кэш заголовка) */
|
||
uint8_t flags; /* bit0 SPR_VISIBLE; bit1/2 dirty[page] */
|
||
/* --- внутреннее (владеет движок) --- */
|
||
struct {
|
||
int x, y; /* где нарисован на странице p */
|
||
uint8_t on; /* нарисован ли вообще */
|
||
} drawn[2];
|
||
} sprite_t; /* ~24 байта на спрайт */
|
||
```
|
||
|
||
Инварианты: `img` живёт, пока спрайт активен; размер кадра постоянен
|
||
(смена кадра = смена sx/sy в той же ленте); прозрачность — 0xFF в
|
||
данных, как везде.
|
||
|
||
### API (черновик; префикс sprite_, заголовок <sprite.h> в libbgi)
|
||
|
||
```c
|
||
void sprite_init (sprite_t *s, const void *img); /* w,h из заголовка,
|
||
невидим, не рисует */
|
||
void sprite_move (sprite_t *s, int x, int y); /* O(1): только state */
|
||
void sprite_frame(sprite_t *s, int sx, int sy); /* кадр атласа, O(1) */
|
||
void sprite_show (sprite_t *s); /* O(1) */
|
||
void sprite_hide (sprite_t *s); /* O(1) */
|
||
void sprite_touch(sprite_t *s); /* O(1): принудительная
|
||
перерисовка на обеих
|
||
страницах (dirty) */
|
||
/* flags: SPR_ALWAYS — перерисовывать каждый кадр (эквивалент touch
|
||
* на каждом кадре; для статики, над которой постоянно ходят). */
|
||
|
||
/* Вся реальная работа — один вызов на кадр. Рисует на ТЕКУЩЕЙ
|
||
* draw-странице p (для дабл-буфера вызывать после
|
||
* gfx_set_draw_page(hidden); для одиночной страницы/курсора — прямо
|
||
* на видимой):
|
||
* проход 1: heal всех, у кого drawn[p].on и (скрыт ИЛИ сместился
|
||
* относительно drawn[p] ИЛИ dirty[p]);
|
||
* проход 2: блит всех видимых из них же (gfx_blit_part через 0x5C)
|
||
* в порядке массива (индекс = z-order, последний сверху);
|
||
* обновить drawn[p]/dirty[p].
|
||
* Спрайты, не менявшиеся с прошлого визита этой страницы (и без
|
||
* touch/SPR_ALWAYS), не трогаются. */
|
||
void sprite_update(sprite_t *arr, uint8_t count);
|
||
|
||
/* Сахар поверх update для канонического дабл-буфер-кадра:
|
||
* set_draw_page(hidden) + sprite_update + gfx_wait_vsync +
|
||
* set_visible_page(hidden). Приложение, рисующее свой HUD/фон,
|
||
* зовёт составные части само (HUD банком 0x50 НА СКРЫТОЙ странице —
|
||
* тогда heal учитывает его автоматически через ОЗУ-копию). */
|
||
void sprite_flip(sprite_t *arr, uint8_t count);
|
||
```
|
||
|
||
Пример (balls сжимается до):
|
||
|
||
```c
|
||
sprite_t sp[N];
|
||
... sprite_init/sprite_move/sprite_show ...
|
||
for (;;) {
|
||
for (i...) sprite_move(&sp[i], new_x, new_y); /* физика */
|
||
sprite_flip(sp, N); /* весь кадр */
|
||
}
|
||
```
|
||
|
||
### Решения и границы
|
||
|
||
- **Retained-модель**: move/frame/show/hide меняют только state (O(1),
|
||
сколько угодно раз за кадр) — рисование один раз, в update. Это
|
||
железно закрепляет дисциплину двух проходов внутри библиотеки.
|
||
- **Пересечения со статикой — ответственность приложения** (touch /
|
||
SPR_ALWAYS), автодетекта НЕТ — осознанно: heal активного спрайта
|
||
выкусывает статичный под ним, но автоматическое обнаружение — это
|
||
не только O(N²) rect-проверок; для корректного z-порядка пришлось бы
|
||
перерисовывать и пересекающих НОВЫЕ позиции, а перерисовка статика
|
||
полным блитом затирает спрайты выше него уже ВНЕ грязной зоны →
|
||
транзитивный каскад либо клиппинг блита по произвольной области.
|
||
На Z80 это дороже самого рисования; приложение же знает сцену и
|
||
помечает дёшево (обсуждено с пользователем 2026-07-11: ручное
|
||
назначение «активных» выгоднее координатных проверок).
|
||
- **Массив, а не handle-таблица**: владеет приложение, без malloc и
|
||
лимитов; z-order = индекс. Пересортировка z = перестановка массива
|
||
(drawn-состояние едет вместе со структурой — безопасно).
|
||
- **Интеграция со страницами**: движок НЕ владеет флипом (sprite_flip
|
||
лишь сахар) — приложение может рисовать динамический фон/HUD на
|
||
скрытой странице до sprite_update. Правило прежнее: всё «фоновое» —
|
||
банком 0x50, спрайты — движком.
|
||
- **Оптимизация внутри update, невидимая снаружи**: объединение
|
||
heal-прямоугольников соседей и т.п. — меняется реализация update,
|
||
не API. ВНИМАНИЕ: «heal только открывшейся L-полоски» для
|
||
ПРОЗРАЧНЫХ спрайтов НЕ годится (отвергнуто 2026-07-12): в зоне
|
||
перекрытия старой и новой позиций дырки нового кадра не перекрывают
|
||
старые непрозрачные пиксели → трейлинг-«полумесяц» остаётся внутри
|
||
bbox-перекрытия, куда L-полоска не достаёт. Точный набор для heal =
|
||
old_opaque AND NOT new_opaque (маска по форме, дороже full-box heal).
|
||
Референс docs/samples/balls (restore_bg) лечит полный 16×16 — full-box
|
||
heal обязателен. Экономия — только амортизация обвязки (batch-пасс).
|
||
- Вне скоупа v2: скроллящийся фон (инвалидирует ОЗУ-копию целиком —
|
||
другой движок), ISR-перерисовка (курсор из IM2 — после Audio/IM2),
|
||
коллизии (у приложения есть x/y — пусть считает само).
|
||
|
||
## 9а. Квирк CPU-байта write-триггера accel-копии (найден 2026-07-11)
|
||
|
||
Триггер записи `LD (DE),A` — реальная CPU-инструкция: её собственный
|
||
цикл записи кладёт байт A в dst[0] ДО burst'а FSM. После burst-чтения
|
||
`LD A,(HL)` в A остаётся ПОСЛЕДНИЙ байт строки. При банке 0x50 утечка
|
||
невидима (burst перезаписывает dst[0] правильным src[0]), но при
|
||
0x58/0x5C, если src[0] == 0xFF, перезапись скипается — на экране
|
||
вертикальная полоса цвета последних байтов строк (симптом: клипнутые
|
||
спрайты в tests/sprites). Было нейтрализовано в `_bgi_copy_rows_raw`:
|
||
src[0] предчитывался до армирования и подставлялся в A через EX AF,AF'
|
||
перед триггером.
|
||
|
||
**ФИКС СНЯТ 2026-07-13.** Точная dev-MAME (сборка разработчиков
|
||
Sprinter, `mame/sources/MAME`) эмулирует ПЛМ, подавляющую CPU-байт
|
||
триггера при активном burst'е — квирк был артефактом стоковой MAME
|
||
0.283. Проверено pixel-точно: tests/blitw, col0 @(200,150) = 0x02
|
||
GREEN по байтам VRAM (read_vram через MCP-мост). Для heal фикс был
|
||
избыточен всегда (банк 0x50 перезаписывает dst[0]). Строки фикса
|
||
оставлены закомментированными в трёх leaf'ах (_bgi_copy_rows_raw,
|
||
_bgi_blit_rows_raw, _bgi_heal_rows_raw) — восстановить, если реальное
|
||
железо поведёт себя как MAME 0.283 (маркер: вертикальная полоса цвета
|
||
последних байтов строк на клипнутых/прозрачных спрайтах). Регресс:
|
||
tests/blitw (визуальная секция «trig leak»). НА ЖЕЛЕЗЕ ПЕРЕПРОВЕРИТЬ.
|
||
|
||
## 9б. Клип спрайтов: noclip-ядра + funcptr-диспетч (2026-07-12/13)
|
||
|
||
Клип в спрайтовых ядрах стоит не только сравнений (~0.85 мс/кадр на 16
|
||
шаров), но и codegen-эффекта: клип-код раздувает регистровое давление
|
||
всей функции (спиллы) — поэтому рантайм-скип внутри общего ядра
|
||
возвращает лишь малую часть. Полный выигрыш даёт ФИЗИЧЕСКИ отдельная
|
||
функция без клип-кода: `_gfx_blit_sprite_noclip` / `_gfx_heal_sprite_noclip`.
|
||
|
||
Выбор ядра — НЕ веткой в горячем цикле (ветка `if (clip)` в
|
||
sprite_update съедала половину выигрыша: 52 вместо 57 fps на старой
|
||
MAME), а перенаправлением указателей `_gfx_blit_fn`/`_gfx_heal_fn`
|
||
(common/_gfx_sprite_fns.c, дефолт — clip-ядра) один раз в
|
||
`gfx_sprite_clip()`; sprite_update/putsprite/movesprite зовут через
|
||
указатель. Программа без вызова `gfx_sprite_clip()` не линкует
|
||
noclip-ядра (на них ссылается только модуль gfx_sprite_clip.c).
|
||
|
||
Замер на точной dev-MAME (16 шаров, uncapped, A/B 2026-07-13):
|
||
clip 50 fps → noclip-funcptr 60-61 fps (**+20-22%**) — полный выигрыш
|
||
noclip доехал до приложения. С vsync-капом оба варианта упираются в
|
||
~48-50 (кадр < 20 мс) — выигрыш конвертируется в запас кадра.
|
||
ВНИМАНИЕ: noclip требует гарантии приложения, что спрайты целиком на
|
||
экране — выход за край портит соседнюю память (tests/spriteclip).
|
||
|
||
## 9в. Атласы в страницах W0 (дизайн 2026-07-13; probe-тест PASS)
|
||
|
||
Атласы грузятся в EMM-страницы (mem_alloc_pages) и на время
|
||
sprite_update подключаются в W0 (OUT 0x82) — W2-heap свободен от
|
||
спрайтов, объём = EMM (~3.4 МБ). Загрузка — read() в страницу,
|
||
замапленную в W3 (приём mdview2; вне рендера W3 свободна).
|
||
|
||
ОПАСНОСТЬ: все прерывания исполняют 0x38 текущей страницы W0 — IM1
|
||
напрямую, наш IM2-трамплин чейнит jp 0x0038 (libc/irq/_irq_tramp.c);
|
||
не ходит в W0 только личный CBL-путь. ЗАЩИТА — стаб на каждой
|
||
странице (первые 0x100 байт зарезервированы):
|
||
0x38: JP <w0_isr_stub в W2>; 0x66: RETN (NMI).
|
||
Стаб: свап W0 на страницу ядра DSS (снята со старта IN A,(0x82)) →
|
||
подложить возврат на стек → jp 0x0038 (честный обработчик, EI/RETI) →
|
||
хвост: ВОССТАНОВИТЬ спрайт-страницу (_w0_cur) → ret. Restore
|
||
обязателен (закрывает EI-щель между out(0x82) движка и DI leaf'а);
|
||
один стаб покрывает IM1 и IM2-чейн; вложенное прерывание в хвосте
|
||
безопасно (W0 в этот момент = DSS, стаб не реентерится).
|
||
|
||
ПРАВИЛО: пока спрайт-страница в W0 — НИКАКИХ ESTEX/BIOS (RST-диспетчер
|
||
живёт на странице DSS). Движок: пролог/эпилог sprite_update
|
||
сохраняет/возвращает страницу DSS; sprite_t получит поле page.
|
||
|
||
Probe-тест tests/w0page (MAME dev, 2026-07-13, ВСЕ PASS):
|
||
P1 ESTEX READ в W3-замапленную страницу;
|
||
P3 IM1: busy-цикл ~5 c с EI при странице в W0 — жив, сентинел цел;
|
||
P4 IM2 (irq_install): 100 кадровых прерываний посчитаны при
|
||
подключенной странице (полный тракт трамплин→чейн→стаб→DSS→
|
||
restore), сентинел цел;
|
||
P5 accel-блит src = 0x0100 (W0) банком 0x50 — пиксели верны по
|
||
ОЗУ-копии (getpixel).
|
||
На железе перепроверить вместе с остальным. Альтернатива без стабов —
|
||
W1, но только tiny (в small/big/huge в W1 код) — отвергнута в пользу
|
||
универсального W0.
|
||
|
||
## 9г. Авто-анимация спрайтов (РЕАЛИЗОВАНО 2026-07-13; tests/spranim PASS)
|
||
|
||
Идея пользователя: sprite_update (вызывается раз на кадр) сам тикает
|
||
анимации — приложение только описывает их декларативно. Реализация:
|
||
sprite_anim(first,last,speed,mode: LOOP/PINGPONG/ONCE|HORIZ) /
|
||
sprite_anim_stop(frame|-1) / sprite_moveto(tx,ty,max_step,interval) /
|
||
sprite_move_stop(at_target) / статус sprite_anim_status() (биты
|
||
SPR_ANIM_ON/DONE, SPR_MOVE_ON/DONE) / sprite_anim_frame() /
|
||
sprite_moving(). Кадровая и tween независимы (одновременно — да);
|
||
тикер _sprite_tick подключается указателем _spr_tick_fn (DCE),
|
||
sprite_update зовёт его проходом 0. Кадровая — ±an_step к оси без
|
||
умножений; tween — Брезенхэм порциями ≤max_step по большей оси.
|
||
Проверено tests/spranim (T1 LOOP / T2 PINGPONG / T3 ONCE+DONE /
|
||
T4 пример (0,0)→(100,50)/5/4 = ровно 80 кадров, Y 3/2/3/2 /
|
||
T5 стопы) — все PASS. Цена: ~+40 Б программе с движком, sprite_t
|
||
+18 Б. Ниже — исходный дизайн-разбор.
|
||
|
||
### Кадровая анимация (смена изображений)
|
||
|
||
«Крути кадры от M до P с интервалом N кадров»; два режима: линейный
|
||
(0/1/2/3/0/1/2/3) и маятник (0/1/2/3/2/1/0/...).
|
||
|
||
Ключ к дешёвому тику на Z80: индекс кадра НЕ хранить — переход между
|
||
соседними кадрами ленты это ±step к sx или sy (step = fw или fh),
|
||
БЕЗ умножения/деления. Состояние: axis-смещение текущего кадра уже
|
||
живёт в s->sx/s->sy; добавляются границы и шаг:
|
||
|
||
uint8_t an_speed; /* кадров между сменами; 0 = анимации нет */
|
||
uint8_t an_timer; /* счётчик до следующей смены */
|
||
uint8_t an_step; /* fw или fh (ось ленты) */
|
||
int an_lo,an_hi; /* smin..smax по оси (M и P умножены заранее*
|
||
* в sprite_anim() — один раз, не в тике) */
|
||
флаги: ANIM_PINGPONG, ANIM_DIR (текущее направление), ANIM_ONESHOT?
|
||
|
||
API: sprite_anim(s, first, last, speed, mode) / sprite_anim_stop(s).
|
||
Тик (в sprite_update, до проходов): --an_timer; при 0: sy ± an_step,
|
||
на границе — wrap (линейный: sy=an_lo) или разворот (маятник:
|
||
инверсия DIR), s->flags |= _SPR_DIRTY. Стоимость: декремент + редкая
|
||
ветка — копейки.
|
||
|
||
### Анимированное перемещение (tween)
|
||
|
||
«Плыви к (tx,ty), максимум max_step пикселей за срабатывание, раз в
|
||
interval кадров». Пример: (0,0)→(100,50), шаг 5, интервал 4 → 20
|
||
срабатываний × 4 кадра = 80 кадров (1.6 с); Y идёт нелинейно 2/3/2/3.
|
||
|
||
Реализация — инкрементальный Брезенхэм (как _bgi_lineseg), но
|
||
«порциями» по ≤max_step вдоль БОЛЬШЕЙ оси за срабатывание: ошибка-
|
||
аккумулятор тянет меньшую ось, деления нет. Состояние ~10 байт:
|
||
tx,ty, err, adx,ady (абс. дельты), знаки, mv_speed/mv_timer.
|
||
Завершение: x==tx && y==ty → снять SPR_MOVING (опрос sprite_moving(s);
|
||
callback НЕ делаем — опрос проще и дешевле).
|
||
|
||
### Общее / критика / расширения
|
||
|
||
- Тик — отдельный проход в НАЧАЛЕ sprite_update (до heal: dirty должен
|
||
взводиться раньше проходов). ВАЖНО: тик должен идти раз на КАДР —
|
||
sprite_update и так зовётся на кадр (дабл-буфер: попеременно на
|
||
страницу); при просадке fps анимации замедляются вместе с кадром
|
||
(стандартное поведение, принимаем).
|
||
- DCE: тикер — в отдельном модуле, sprite_update зовёт его через
|
||
указатель _spr_tick_fn (дефолт NULL → один if на кадр); указатель
|
||
ставит первый вызов sprite_anim()/sprite_moveto(). Программа без
|
||
анимации не тянет код тикера (паттерн funcptr как у clip/noclip).
|
||
- Цена памяти: sprite_t вырастает на ~16-18 байт (кадровая ~10 +
|
||
tween ~10 с перекрытием полей?). Вариант «отдельная anim-структура
|
||
по указателю (NULL = нет)» экономит память статиков, но добавляет
|
||
indirection в тик и второй массив приложению. ПРЕДЛОЖЕНИЕ: поля в
|
||
sprite_t (спрайтов десятки — до 64×~34 Б ≈ 2 КБ, W2 переживёт),
|
||
но решение за пользователем.
|
||
- Расширения (потом): one-shot кадровая анимация (проиграть раз и
|
||
замереть/скрыться); дробная скорость 8.8 fixed-point (равномерное
|
||
медленное движение «1 пиксель в 1.5 кадра»); цепочки целей
|
||
(waypoints) — приложение ставит новую цель по sprite_moving()==0.
|
||
|
||
## 9д. Тактовый бюджет кадра (профиль rpgwalk в dev-MAME, 2026-07-13)
|
||
|
||
Кадр Sprinter = 48.83 Гц (растр 896×320 @14МГц) = **430080 тактов**
|
||
@21МГц (такты «эффективные», с wait-state'ами ОЗУ). Метод замера —
|
||
watchpoint на OUT-маркеры + printf/g + clog (memory/mame_mcp_bridge).
|
||
|
||
Цена анимированного спрайта 16×16 в sprite_update (heal+blit на
|
||
кадр, W0-атлас, noclip):
|
||
|
||
| фаза | тактов/спрайт |
|
||
|--------------------|---------------|
|
||
| тик (anim+tween) | ~7.3К (4.3-9.2К) |
|
||
| heal (банк 0x50) | ~6.4К |
|
||
| blit (банк 0x5C) | ~11.8К |
|
||
| **итого** | **~26К** |
|
||
|
||
Фикс-часть цикла демо (kbhit+getdatetime+свопы страниц) ~14-19К.
|
||
Отсюда лимит на стабильные 48 fps (КАЖДЫЙ кадр в один интервал):
|
||
**14 спрайтов** (замерено: все обычные кадры ≤406К, 10/232 двойных —
|
||
только кадры с FPS-плашкой); 15 — 94% кадров, 16 — на грани (50%).
|
||
|
||
Уроки профиля:
|
||
- O(sy)-цикл `src += img_w` в blit-обёртках стоил до 64К тактов на
|
||
кадр 11 вертикальной ленты (~85% времени блита!) — заменён на
|
||
__mulint 2026-07-13 (O(1) ~300Т): blit упал 45К → 11.8К/спрайт,
|
||
rpgwalk 24 fps → стабильные 48.
|
||
- bar()+outtextxy() FPS-плашки = ~210К тактов (~10 мс, полкадра!) —
|
||
текст BGI дорогой; на 14 спрайтах кадры с плашкой уходят в два
|
||
интервала. Для HUD в бюджете рисовать заготовленным putimage.
|
||
- Резерв: тик в C ~7.3К/спрайт — asm/упрощение подняло бы лимит к
|
||
~17-18 спрайтам.
|
||
|
||
## 10. Риски / что проверить артефактом (Фаза 0)
|
||
|
||
Статус 2026-07-11: пп. 1–3 закрыты для MAME 0.283 (tests/gfxbanks,
|
||
результаты в §7); на железе — всё ещё TODO.
|
||
|
||
1. ~~Эмулирует ли MAME биты 2/3~~ — эмулирует (с квирком 0x58: FF
|
||
протекает в теневое ОЗУ; в master-драйвере исправлено). Железо —
|
||
проверить.
|
||
2. ~~Отображение байта FF в VRAM~~ — в MAME обычный цвет 255,
|
||
подстановки нет → стирание = heal. Железо может отличаться —
|
||
tests/fferase остаётся до прогона на реальном Sprinter.
|
||
3. ~~Подавление 0xFF на accel-пути~~ — в MAME accel-путь идентичен
|
||
CPU-пути (оба через ram_w). Железо — проверить (в ПЛМ пути
|
||
физически разные).
|
||
4. ~~Копия accel'ом при src в видеоокне и при src == dst~~ — оба
|
||
подтверждены: getimage через accel (образ 1:1, tests/bgi_img) и
|
||
gfx_heal (стирание чисто, tests/sprites S2/S3). Железо — как всё
|
||
остальное, перепроверить.
|
||
5. `LD A,(nn)`/`LD A,(HL)` как источник размера блока (quick-win из
|
||
TODO) — если работает, SMC в leaf'е не нужен.
|
||
6. Долгие burst'ы и IM2/CBL: пара триггеров на строку под DI — бюджет
|
||
как у fill (docs/accel-fill-budget.md), пересчитать для copy.
|