Скролл региона 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>
15 KiB
Руководство: скроллинг-примитивы для 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: page00xC000, page10xC140;- размер блока = высота 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-страйпов).
/* Горизонтальный скролл 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):
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)
- Верт. режим (
LD A,A) в банке0x50читает ОЗУ-копию (чистый фон) и пишет VRAM+тень. Гориз. heal это доказывает построчно; для верт. прохода — прогон: нарисовать фон, поверх спрайт банком0x5C, сделатьscroll_hна dx>0, убедиться что спрайт не размазался (копировался фон, не VRAM со спрайтом). Заодно выяснить, нужен ли сброс Port_Y на колонку (§4). - ОЗУ-копия per-page или общая.
C-Compiler/applications/PoP/docs/double_buffer_plan.mdпишет «теневая копия одна — общая». Если тень физически одна (а не адресуется по базе0xC000/0xC140как VRAM), page0 и page1 не удержат фон разных положений камеры во время скролла → пинг-понг (§8) сломается. Проверить: записать разный фон в page0 и page1 банком0x50, сверить чтение обеих. - (только для
scroll_v, путь A) accel-буфер переживаетOUT Port_Yмежду fill и flush одного burst'а. Доковый straight-copy этого не проверяет.
8. Порядок кадра в Loom (контекст использования scroll_h)
Пинг-понг двух страниц; спрайты в VRAM-only банк, поэтому тень каждой страницы остаётся чистым фоном:
gfx_set_draw_page(B)— B = скрытая;gfx_scroll_h(A, B, area, dx, &dirty)— B получает сдвинутый чистый фон в VRAM+тень; старые спрайты B затёрты (copy = heal);- декодировать EGA-страйпы фона в
dirty, банк0x50(иначе тень B останется с дырой — следующий скролл из B прочитает мусор); - вывести актора/спрайты в B, банк
0x5C; 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.