# Руководство: скроллинг-примитивы для 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`.