# Руководство: скроллинг-примитивы для 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 или общая.** Ранний план дабл-буфера PoP исходил из
   «теневая копия одна — общая»; **практикой это опровергнуто**: у каждой
   страницы СВОЯ теневая ОЗУ-копия, heal берёт фон из копии той страницы, в
   которую рисуем (`applications/PoP/roomtest/CLAUDE.md`, раздел
   «Дабл-буфер»; `roomtest.c enter_room` рисует фон в обе страницы по
   отдельности именно поэтому).  Значит пинг-понг (§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`.
