libbgi: скролл-примитивы + --w3 + отчёт раскладки памяти
Скролл региона 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>
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
# Руководство: скроллинг-примитивы для 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`.
|
||||
Reference in New Issue
Block a user