Files
Sprinter-SDCC/examples/scroll/scroll-impl-guide.md
T
snark13 95c22be9bd 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>
2026-07-23 19:38:00 +03:00

15 KiB
Raw Blame History

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

/* Горизонтальный скролл 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)

  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 ≈ 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.