Files
Sprinter-SDCC/examples/scroll/scroll-impl-guide.md
T
Александр Петров 774b1cc7c4 docs(PoP): документация к актуальному статусу + план следующих уровней
Документы отстали от кода: PORT_PLAN писал «PoC не начат», хотя играется
весь уровень 1, а четыре плана были исполнены целиком.

- PORT_PLAN: таблица статусов по разделам; фазы 0-3 сделаны, 4-6 нет;
  риски §8 п.1/п.3 закрыты, п.2 переформулирован под реальный движок
  (спрайтовый движок для персонажей не используется, лимит «21 спрайт»
  неприменим), п.4 — найдено расхождение таймингов: оригинал считает
  логический кадр за 5 тиков при BASE_FPS=60 (83.3 мс, в бою 100 мс), а мы
  ждём три vsync (60 мс) — игра идёт примерно на 39 % быстрее эталона.
- levels_plan.md — новый: машинерия перехода между уровнями, второй
  тайлсет (palace), потабличные различия и читы SDLPoP, которые окупаются
  сразу.  Инвентарь тайлов снят прямо с res200N.bin: уровень 2 не требует
  ни одного нового ассета и ни одной новой механики.
- roomtest/TASKS.md — новый: доска текущих задач с критериями готовности.
- Удалены как исполненные и перекрытые кодом: clip_char_plan,
  double_buffer_plan, loose_floors_plan, size_optimization_plan.  Его §8
  (замеры скорости отрисовки) не был перекрыт — перенесён в
  layout_plan_v2 §9, чтобы не потерять цифры.
- KID_PLAN / gates_spikes_plan — шапки «реализовано, оставлено
  справочником»; room_model_plan — «S1 сделан, остальное не срочно».
- docs/README.md стал индексом с отметками актуальности.
- ideas_backlog: зелье переворота экрана — оригинал переворачивает готовый
  буфер построчно, спрайты не трогает; по данным уровней тип 4 встречается
  только на уровне 9, до него механика не нужна.
- examples/scroll: ссылка на удалённый план вела к неверному факту
  «теневая копия одна — общая»; заменено на подтверждённое «у каждой
  страницы своя».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:32:48 +03:00

227 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Руководство: скроллинг-примитивы для 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 ≈ **1415 мс** (<кадр); скролл
копирует `(320dx)` колонок → чуть меньше. Итого копия ~кадр + докод полосы +
спрайты; целевой бюджет ~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`.