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>
This commit is contained in:
2026-07-23 19:38:00 +03:00
parent d1bc97c589
commit 95c22be9bd
46 changed files with 4125 additions and 133 deletions
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := scroll
EXTRA_FLAGS ?= --gfx 256
include $(PROJ_ROOT)/app.mk
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
make clean
make run
+223
View File
@@ -0,0 +1,223 @@
# Руководство: скроллинг-примитивы для 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 ≈ **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`.
+87
View File
@@ -0,0 +1,87 @@
/*
* scroll — визуальная проверка скролл-примитивов libbgi (gfx_scroll_h /
* gfx_scroll_v). Копирует регион из НЕактивной страницы в активную со
* сдвигом и переключает страницы.
*
* ДИАГНОСТИКА. Страница 0 — диагональные полосы color=((x+y)>>3)&15
* (любой сдвиг ломает непрерывность диагоналей; фаза по Y ловит
* вертикальный «съезд» Port_Y-квирка). Страница 1 — сплошной серый
* фон-маркер: после скролла на нём проступают ровно скопированные
* прямоугольники, по их границам видно корректность (позицию/высоту).
*
* Два региона в одном кадре: слева ГОРИЗОНТАЛЬНЫЙ скролл (dx>0 — картинка
* вправо, открывается серая полоса слева), справа ВЕРТИКАЛЬНЫЙ (dy>0 —
* вниз, открывается серая полоса сверху). Если верт. Port_Y-квирк не
* вылечен — правый прямоугольник съедет вниз/опустеет сверху. Esc —
* выход.
*/
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include <conio.h>
static uint8_t egapal[16 * 4];
__sfr __at (0xFE) io_border;
/* Диагональные полосы 16-цветной палитры (8-пиксельные ступени). */
static void draw_diagonals(void)
{
int bx, by;
for (by = 0; by < 256; by += 8)
for (bx = 0; bx < 320; bx += 8) {
setfillstyle(SOLID_FILL, (((bx + by) >> 3) & 15));
bar(bx, by, bx + 7, by + 7);
}
}
/* Регион горизонтального скролла (слева) и вертикального (справа). */
static gfx_rect_t area_h = { 0, 56, 320, 144 };
// static gfx_rect_t area_h = { 16, 48, 80, 80 };
// static gfx_rect_t area_v = { 176, 48, 80, 80 };
int main(void)
{
uint8_t hidden;
initgraph();
/* Палитра страницы 1 = палитре страницы 0 (initgraph грузит EGA
* только в палитру 0). */
gfx_pal_get(0, 0, 16, egapal);
gfx_pal_load(1, 0, 16, egapal);
/* Страница 0 — диагонали (источник фона). */
gfx_set_draw_page(0);
draw_diagonals();
gfx_set_draw_page(1);
draw_diagonals();
gfx_set_visible_page(0);
/* Рисуем на скрытой странице (1), потом флип. */
hidden = gfx_get_visible_page() ^ 1; /* = 1 */
gfx_set_draw_page(hidden);
for (;;) {
if (kbhit() && getch() == 27)
break;
io_border = 2;
gfx_scroll_v(&area_h, 2, 0); /* вправо на 48 px */
// gfx_scroll_v(&area_v, 2, 0); /* вниз на 48 px */
io_border = 0;
gfx_wait_vsync();
gfx_wait_vsync();
hidden = gfx_get_visible_page() ^ 1;
gfx_set_draw_page(hidden ^ 1);
gfx_set_visible_page(hidden);
}
closegraph();
return 0;
}