95c22be9bd
Скролл региона video->video (неактивная страница -> активная, банк 0x50: копия = скролл + heal цели): - gfx_scroll_h / _bgi_scroll_rows_raw — горизонтальный, построчно без страйдов (~54Т/строку), DI/EI бандами по 16 строк, h=0=>256; - gfx_scroll_v / _bgi_scroll_cols_raw — верт. И/ИЛИ гориз. за один проход без буфера (колонка = accel-burst LD A,A, STOP между read/write делает промежуточный OUT Port_Y безопасным), банды по 16 колонок; - _gfx_addr_shadow_base (адрес неактивной страницы) + gfx_rect_t. Пример examples/scroll. check_banks.py + sprinter-cc: отчёт раскладки памяти для ЛЮБОЙ модели (W1/W2 код/данные, остаток кучи/стека, W3 при --w3, банки при --bank), не только при --bank. Док docs/memory-management.md §10. --w3 (резидентный код окна W3) + сопутствующее: crt0_banked W3_RESIDENT, mkexe -W, tests/w3probe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
224 lines
15 KiB
Markdown
224 lines
15 KiB
Markdown
# Руководство: скроллинг-примитивы для libbgi
|
||
|
||
Самодостаточное ТЗ на реализацию двух функций скролла в libbgi. Написано так,
|
||
чтобы реализовать с нуля без остального контекста. Итог изысканий по железу
|
||
Sprinter (см. §1) и разбора существующего кода libbgi (см. §3).
|
||
|
||
**Что делаем:** `gfx_scroll_h` (горизонтальный — нужен Loom для комнат шире
|
||
320 px) и `gfx_scroll_v` (вертикальный — для симметрии API; Loom пока не
|
||
требует, высота комнат 144 < 256).
|
||
|
||
**Где живёт:** расширение `C-Compiler/libbgi` — accel-leaf в `bgi256/`,
|
||
обёртка в `common/`, объявление в `include/gfx.h`.
|
||
|
||
**СНАЧАЛА прочти §7 (блокеры).** Два пункта надо проверить в dev-MAME до
|
||
написания рабочего кода — если они не пройдут, схема «чистого фона» едет.
|
||
|
||
---
|
||
|
||
## 1. Аппаратная модель (всё, что нужно знать)
|
||
|
||
**Режим `0x81`: 320×256×256, chunky — 1 байт/пиксель.** Пиксель (x,y): CPU-адрес
|
||
`0xC000 + x`, строку выбирает **Port_Y** (порт `0x89`) `= y`. Горизонталь — в
|
||
адресе, вертикаль — в Port_Y. (Ссылка: `libbgi/include/gfx.h` шапка.)
|
||
|
||
**Две видеостраницы различаются ТОЛЬКО базой строки:** page0 → `0xC000`,
|
||
page1 → `0xC140` (шаг `0x140` = 320). Обе адресуемы в W3 **одновременно**;
|
||
Port_Y и номера банков — общие для обеих. Значит:
|
||
> `dst_addr = src_addr + 0x140` копирует page0→page1 без ремаппинга W3.
|
||
> Со сдвигом: `dst = src ± 0x140 + dx`.
|
||
|
||
**Банки (значение W3-страницы, порт `0xE2` = `0x50..0x5F`)** — режим ЗАПИСИ,
|
||
действует на все примитивы до смены (`libbgi/include/gfx.h`):
|
||
| Банк | Имя | Запись | Чтение |
|
||
|---|---|---|---|
|
||
| `0x50` | `GFX_BANK_NORMAL` | VRAM **+ ОЗУ-копия** | ОЗУ-копия |
|
||
| `0x54` | `GFX_BANK_NOSHADOW` | только VRAM | ОЗУ-копия |
|
||
| `0x58` | `GFX_BANK_TRANSPARENT` | VRAM, байт `0xFF` не пишется | ОЗУ-копия |
|
||
| `0x5C` | `GFX_BANK_SPRITE` | VRAM, `0xFF` не пишется | ОЗУ-копия |
|
||
|
||
Чтение `#50..#5F` **всегда** отдаёт ОЗУ-копию (видео-ОЗУ write-only). `0xFF` —
|
||
аппаратно-прозрачный цвет (`GFX_TRANSPARENT`).
|
||
|
||
**Акселератор** (`C-Compiler/docs/converted/accel_r.txt`): внутр. ОЗУ-буфер
|
||
1..256 байт. Управление опкодами-«NOP»:
|
||
- `LD B,B` — стоп; `LD D,D` затем `LD A,n` — размер блока (`n=0` → 256);
|
||
- `LD L,L` — **горизонтальная** копия (буфер ← `LD A,(HL)`; буфер → `LD (DE),A`);
|
||
- `LD A,A` (`0x7F`) — **вертикальная** копия (линии экрана, Port_Y шагает внутри
|
||
burst'а);
|
||
- `LD C,C` / `LD E,E` — fill (гориз./верт.).
|
||
|
||
Скорость ≈ **байт / 7 мкс**. Полный экран 320×256 ≈ **26 мс** (это **>1 кадра**
|
||
@ 50 Гц — держать в уме, §9). На время работы акселератора — **DI** (меняется
|
||
система команд, ISR сломается).
|
||
|
||
---
|
||
|
||
## 2. Ключевой инсайт: чистый фон достаётся бесплатно
|
||
|
||
- Спрайты рисуются банком с битом 2 (`0x54`/`0x5C`) → **только VRAM**,
|
||
ОЗУ-копию не трогают.
|
||
- ⇒ ОЗУ-копия (`0x50`) любой страницы — это фон **без спрайтов**, всегда.
|
||
- ⇒ скролл-копия из `0x50` в `0x50` читает чистый фон и пишет VRAM+тень — это
|
||
**одновременно скролл И heal** целевой страницы (её старые спрайты затираются
|
||
сдвинутым фоном).
|
||
|
||
Ровно так работает `gfx_heal` (частный случай `src==dst`, §3). Скролл — тот же
|
||
механизм, но `src != dst`.
|
||
|
||
---
|
||
|
||
## 3. Что уже есть в libbgi — образцы для копирования
|
||
|
||
| Файл | Что | Зачем образец |
|
||
|---|---|---|
|
||
| `bgi256/_bgi_copy_rows_raw.c` | гориз. leaf (`LD L,L`), src/dst + страйды, Port_Y на строку | ядро всех блитов; страйды, SMC, DI |
|
||
| `bgi256/_bgi_heal_rows_raw.c` | `src==dst==экран`, страйды 0 | спец-случай heal |
|
||
| `bgi256/_bgi_blit_cols_raw.c` | **верт. leaf** (`LD A,A`), колонка=burst, Port_Y сброс на колонку, `dst+=1` | **прямой образец для `scroll_h`** |
|
||
| `common/_gfx_heal_full.c` | клип по экрану + нарезка полос >256 + вызов leaf | образец обёртки-ядра |
|
||
| `common/gfx_heal.c` | save-банк → `0x50` → `_bgi_begin` → ядро → `_bgi_end` → restore | образец верхней обёртки |
|
||
| `docs/converted/accel_r.txt` | screen→screen верт. копия LDIR-идиомом, `dst=src+0x140` | канонический скролл-паттерн |
|
||
|
||
Правило из `_bgi_copy_rows_raw`: **одна сторона копии — видео (в W3, строку даёт
|
||
Port_Y), другая может быть линейным буфером ВНЕ W3** (`< 0xC000`). Для скролла
|
||
обе стороны — видео (см. §4).
|
||
|
||
---
|
||
|
||
## 4. `gfx_scroll_h` — горизонтальный (наш, приоритет)
|
||
|
||
**Идиом: вертикальный режим `LD A,A`, проход по колонкам.** Высота playfield
|
||
(144) ≤ 256 → одна колонка = один burst, **резать 320-строку на 256+64 НЕ надо**
|
||
(главная причина брать верт. режим). Это доковый screen→screen паттерн
|
||
(`accel_r.txt`) + `_bgi_blit_cols_raw`.
|
||
|
||
Параметры на колонку:
|
||
- `src = base(src_page) + x` — банк `0x50`, чтение = ОЗУ-копия = чистый фон;
|
||
- `dst = base(dst_page) + x + dx` — банк `0x50`, запись = VRAM + тень dst;
|
||
- `base`: page0 `0xC000`, page1 `0xC140`;
|
||
- размер блока = высота playfield (`h`, для Loom 144);
|
||
- Port_Y start = верх playfield (`y0`, для Loom 0);
|
||
- копируем `(w − |dx|)` колонок; вакантные `|dx|` колонок **не трогаем**.
|
||
|
||
**Знак:** зафиксировать `dx > 0` = сдвиг вправо (открывается слева), `dx < 0` =
|
||
влево; вернуть открывшийся край в `*dirty`.
|
||
|
||
**Порядок (по образцу докового LDIR-примера + `_bgi_blit_cols_raw`):** банда
|
||
колонок под DI → `LD D,D`/`LD A,h` (block) → `LD A,A` (верт. режим) → на колонку
|
||
`{ read col в accel; write col из accel; src++; dst++ }` → `EI`. При straight-copy
|
||
Port_Y авто-шагает (доковый LDIR его не трогает) — но `_bgi_blit_cols_raw`
|
||
сбрасывает Port_Y=`y0` на каждую колонку; **проверить, нужен ли сброс** (§7.1
|
||
покажет).
|
||
|
||
**Возврат:** `*dirty` = прямоугольник `|dx|` колонок с открывшегося края;
|
||
полосу не чистим — её дозаполняет вызывающий (в Loom — декодер EGA-страйпов).
|
||
|
||
```c
|
||
/* Горизонтальный скролл playfield. Копирует чистый фон (ОЗУ-копия src_page)
|
||
со сдвигом dx в dst_page (обе стороны банк 0x50: запись бьёт VRAM+тень).
|
||
dx>0 — вправо. Вакантную полосу НЕ заполняет — возвращает в *dirty. */
|
||
void gfx_scroll_h(uint8_t src_page, uint8_t dst_page,
|
||
const gfx_rect_t *area, int dx, gfx_rect_t *dirty);
|
||
```
|
||
|
||
---
|
||
|
||
## 5. `gfx_scroll_v` — вертикальный (симметрия; Loom не нужен)
|
||
|
||
**Асимметрия железа:** горизонтальный сдвиг — это смещение CPU-адреса (src и dst
|
||
независимы) → легко. **Вертикальный сдвиг — это смещение Port_Y, а Port_Y один
|
||
на burst** → в простом LDIR-идиоме read и write идут с одного Port_Y, сдвига по
|
||
Y нет. Поэтому `scroll_v` сложнее `scroll_h`. Два пути:
|
||
|
||
- **(A) менять Port_Y между fill и flush одного burst'а**: `OUT Port_Y=y_src`;
|
||
read col в accel; `OUT Port_Y=y_src+dy`; write col из accel. Требует, чтобы
|
||
accel-буфер пережил `OUT` между чтением и записью — **не доказано** доком/
|
||
тестами (§7.3). Если пройдёт — симметрично `scroll_h`, верт. режим.
|
||
- **(B) безопасный fallback**: grab (video→RAM-буфер вне W3, гориз. режим),
|
||
затем blit (буфер→video со сдвигом Y — буферная сторона CPU-адресуема, сдвиг
|
||
любой). Два прохода + буфер, но заведомо работает.
|
||
|
||
**Рекомендация:** реализовать (A), если §7.3 пройдёт; иначе (B). Делать
|
||
**последним** — для Loom не критично. API симметричный (`dy` вместо `dx`):
|
||
|
||
```c
|
||
void gfx_scroll_v(uint8_t src_page, uint8_t dst_page,
|
||
const gfx_rect_t *area, int dy, gfx_rect_t *dirty);
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Общие требования реализации
|
||
|
||
- **DI по банде, НЕ один DI на весь проход.** Полный playfield ~14 мс под DI
|
||
порвёт будущий CBL-звук. Одна колонка (144 Б ≈ 20 мкс) — безопасное DI-окно;
|
||
резать проход на банды колонок/строк с `EI` между (как `_bgi_copy_rows_raw`
|
||
режет ≤16 строк). Пока звука нет — не критично, но заложить сразу.
|
||
- **Клип по `area`** (образец — `_gfx_heal_full`).
|
||
- **Буфер (если путь (B))** — вне W3 (`< 0xC000`).
|
||
- **Верхняя обёртка**: save банк → `gfx_set_bank(GFX_BANK_NORMAL)` →
|
||
`_bgi_begin()` → ядро → `_bgi_end()` → restore (образец — `gfx_heal.c`).
|
||
- **Размещение**: leaf `bgi256/_bgi_scroll_cols_raw.c` (+ `_rows_raw` для (A)),
|
||
обёртки `common/gfx_scroll_h.c` / `gfx_scroll_v.c`, объявления в
|
||
`include/gfx.h`. `gfx_rect_t` — если ещё нет, определить там же
|
||
(`int x,y,w,h`).
|
||
- **Для Loom (не требование к примитиву, но контекст)**: вызывающий держит
|
||
`dx` кратным 8 (ширина EGA-страйпа) → `dirty` = целые страйпы. Обёртка этому
|
||
не мешает, но и не навязывает.
|
||
|
||
---
|
||
|
||
## 7. ПРОВЕРИТЬ ПЕРВЫМ — блокеры (dev-MAME)
|
||
|
||
1. **Верт. режим (`LD A,A`) в банке `0x50` читает ОЗУ-копию (чистый фон) и
|
||
пишет VRAM+тень.** Гориз. heal это доказывает построчно; для верт. прохода —
|
||
прогон: нарисовать фон, поверх спрайт банком `0x5C`, сделать `scroll_h` на
|
||
dx>0, убедиться что спрайт **не размазался** (копировался фон, не VRAM со
|
||
спрайтом). Заодно выяснить, нужен ли сброс Port_Y на колонку (§4).
|
||
2. **ОЗУ-копия per-page или общая.** `C-Compiler/applications/PoP/docs/double_buffer_plan.md`
|
||
пишет «теневая копия одна — общая». Если тень физически одна (а не адресуется
|
||
по базе `0xC000/0xC140` как VRAM), page0 и page1 не удержат фон **разных**
|
||
положений камеры во время скролла → пинг-понг (§8) сломается. Проверить:
|
||
записать разный фон в page0 и page1 банком `0x50`, сверить чтение обеих.
|
||
3. **(только для `scroll_v`, путь A)** accel-буфер переживает `OUT Port_Y` между
|
||
fill и flush одного burst'а. Доковый straight-copy этого не проверяет.
|
||
|
||
---
|
||
|
||
## 8. Порядок кадра в Loom (контекст использования `scroll_h`)
|
||
|
||
Пинг-понг двух страниц; спрайты в VRAM-only банк, поэтому тень каждой страницы
|
||
остаётся чистым фоном:
|
||
|
||
1. `gfx_set_draw_page(B)` — B = скрытая;
|
||
2. `gfx_scroll_h(A, B, area, dx, &dirty)` — B получает сдвинутый чистый фон в
|
||
VRAM+тень; старые спрайты B затёрты (copy = heal);
|
||
3. декодировать EGA-страйпы фона в `dirty`, **банк `0x50`** (иначе тень B
|
||
останется с дырой — следующий скролл из B прочитает мусор);
|
||
4. вывести актора/спрайты в B, банк `0x5C`;
|
||
5. `gfx_wait_vsync()` → `gfx_set_visible_page(B)`.
|
||
|
||
Следующий кадр: роли A/B меняются, читаем уже чистую тень B. Смаза нет by design
|
||
(§2). При `dx==0` (камера стоит) — обычный per-sprite `gfx_heal`, а не полная
|
||
копия.
|
||
|
||
---
|
||
|
||
## 9. Бюджет и проверка корректности
|
||
|
||
- 320×256 ≈ 26 мс (**>кадр**); playfield 320×144 ≈ **14–15 мс** (<кадр); скролл
|
||
копирует `(320−dx)` колонок → чуть меньше. Итого копия ~кадр + докод полосы +
|
||
спрайты; целевой бюджет ~2 кадра/шаг (plan.md §6). **Замерить на PoC-1**
|
||
маркерами `OUT (0xFE)` (до появления звука порт свободен).
|
||
- Корректность — визуально/дифф-тестом в dev-MAME. Фон-декодер уже проверен
|
||
диффом против ScummVM; скролл проверять на реальной комнате шире 320.
|
||
|
||
---
|
||
|
||
## 10. Связанные документы
|
||
|
||
`plan.md §6` (графика, скроллинг), `toolchain-requirements.md §1` (этот
|
||
примитив), `decisions.md`. Железо: `C-Compiler/docs/converted/accel_r.txt`,
|
||
`libbgi/include/gfx.h`, `C-Compiler/docs/sprite-api-design.md`,
|
||
`C-Compiler/docs/memory-management.md`.
|