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
+119 -50
View File
@@ -1,65 +1,140 @@
#!/usr/bin/env python3
"""
Parse an SDCC .map file produced by sdldz80 and verify that every named
bank fits inside its 16 KB window.
Отчёт по раскладке памяти образа + проверка лимитов, по .map-файлу
sdldz80. Печатается для ЛЮБОЙ модели памяти (не только при --bank):
The Sprinter toolchain expects each `_BANKn` area (with n >= 1) to occupy
at most 16384 bytes — that is the size of CPU window 3 where banked code
runs at execution time. The SDCC linker itself does not enforce this
limit, so we catch it post-link.
- _CODE — основной код (W1 0x4100 или W2 0x8100), размер, конец;
- данные — _DATA/_BSS/_INITIALIZED/… цепочкой за кодом, окно, конец;
- статика/куча/стек — где кончается статика и сколько свободно под кучу
(до ___sdcc_heap_end) и стек;
- _W3CODE — резидентный код окна W3 (--w3), если есть;
- _BANKn — банки (big/huge), каждый ≤ 16 КБ (размер окна W3/W1).
We also surface the HOME budget: anything left between the end of _CODE
and the start of window 2 (0x8000) is leftover space for adding code/data
without banking.
Раскладка режимов — docs/memory-management.md §4. Линкер сам лимиты не
проверяет — ловим здесь пост-линк.
Usage: check_banks.py <path-to.map>
Usage: check_banks.py <path.map> [--mode MODE] [--heap-top 0xNNNN]
[--stack 0xNNNN]
Exits non-zero with a clear message if any bank is over its limit.
Выходит с ненулевым кодом, если банк не влез в окно или статика заехала
за потолок кучи/окна.
"""
import argparse
import re
import sys
BANK_LIMIT = 16 * 1024
# HOME upper bound depends on layout:
# HUGE: CODE at 0x4100 (W1), HOME may spill into W2 → ceiling 0xC000
# BIG: CODE at 0x8100 (W2), HOME stays in W2 → ceiling 0xC000
# tiny/small: same ceiling — anything in W3 is bank territory.
# We pick the ceiling per-image based on where _CODE lives, so we don't
# falsely flag W2-resident code as "spilled into stack/heap".
HOME_CEILING = 0xC000
BANK_LIMIT = 16 * 1024 # окно W3/W1 под банк
W_CEILING = 0xC000 # верх W2 (дальше — только W3/банки)
# Области .map, составляющие HOME (код + данные), в порядке цепочки линкера.
# Всё, что не банк и не резидент W3 и лежит < 0xC000.
_BANK_RE = re.compile(r"_BANK\d+$")
def win_label(addr):
"""Окно по адресу (0-based физического образа игнорируем — берём Z80-view)."""
a = addr & 0xFFFF
if 0x4000 <= a < 0x8000:
return "W1"
if 0x8000 <= a < 0xC000:
return "W2"
if 0xC000 <= a <= 0xFFFF:
return "W3"
return "??"
def parse_map(path):
"""
Returns a dict {area_name: (addr, size)} for area lines like:
_CODE 00004100 00000313 = ...
"""
"""{area_name: (addr, size)} для строк вида `_CODE 00004100 00000313 = ...`."""
line_re = re.compile(r"^\s*(_\w+)\s+([0-9A-Fa-f]{8})\s+([0-9A-Fa-f]{8})\s*=")
areas = {}
with open(path) as f:
for ln in f:
m = line_re.match(ln)
if not m:
continue
name = m.group(1)
addr = int(m.group(2), 16)
size = int(m.group(3), 16)
areas[name] = (addr, size)
if m:
areas[m.group(1)] = (int(m.group(2), 16), int(m.group(3), 16))
return areas
def main():
if len(sys.argv) != 2:
sys.exit("usage: check_banks.py <path-to.map>")
MODE_DESC = {
"tiny": "CODE+DATA в W2 (0x8100)",
"small": "CODE в W1 (0x4100), DATA→W2",
"big": "CODE+DATA в W2 (0x8100), банки в W1",
"huge": "CODE в W1 (0x4100), DATA→W2, банки в W3",
"manual": "ручная раскладка",
}
areas = parse_map(sys.argv[1])
def main():
ap = argparse.ArgumentParser()
ap.add_argument("mapfile")
ap.add_argument("--mode", default="")
ap.add_argument("--heap-top", default="0xBB00")
ap.add_argument("--stack", default="0xBFFE")
args = ap.parse_args()
areas = parse_map(args.mapfile)
heap_top = int(args.heap_top, 0)
stack_top = int(args.stack, 0)
fails = []
bank_names = sorted(
n for n in areas if re.fullmatch(r"_BANK\d+", n)
)
desc = MODE_DESC.get(args.mode, "")
hdr = f"memory: {args.mode}" if args.mode else "memory:"
if desc:
hdr += f"{desc}"
print(hdr)
# --- HOME: код + данные (всё < 0xC000, кроме банков) --------------
home = {n: v for n, v in areas.items()
if not _BANK_RE.match(n) and n != "_W3CODE" and (v[0] & 0xFFFF) < W_CEILING}
if "_CODE" in areas:
caddr, csize = areas["_CODE"]
cend = caddr + csize
print(f" _CODE @ 0x{caddr & 0xFFFF:04X} size {csize:>6} "
f"({win_label(caddr)}) → 0x{cend & 0xFFFF:04X}")
# данные = все HOME-области, кроме _CODE (цепляются за кодом)
data = {n: v for n, v in home.items() if n != "_CODE"}
if data:
dstart = min(v[0] for v in data.values())
dend = max(v[0] + v[1] for v in data.values())
print(f" данные @ 0x{dstart & 0xFFFF:04X} size {dend - dstart:>6} "
f"({win_label(dstart)}) → 0x{dend & 0xFFFF:04X}"
f" [_DATA/_BSS/_INITIALIZED/…]")
else:
dend = cend
# статика/куча/стек
static_end = max(v[0] + v[1] for v in home.values())
heap_free = heap_top - static_end
stack_room = stack_top + 1 - heap_top
print(f" статика до 0x{static_end & 0xFFFF:04X} — куча "
f"0x{static_end & 0xFFFF:04X}..0x{heap_top & 0xFFFF:04X} ({heap_free} Б), "
f"стек 0x{heap_top & 0xFFFF:04X}..0x{stack_top & 0xFFFF:04X} ({stack_room} Б)")
if static_end > heap_top:
print(f" ОШИБКА: статика заехала за heap_top 0x{heap_top & 0xFFFF:04X} "
f"(нет места под кучу/стек)")
fails.append(("_HOME", static_end))
elif static_end > W_CEILING:
print(f" ОШИБКА: статика вышла за 0x{W_CEILING:04X}")
fails.append(("_HOME", static_end))
# --- Резидентный код окна W3 (--w3) -------------------------------
if "_W3CODE" in areas:
waddr, wsize = areas["_W3CODE"]
wend = waddr + wsize
wfree = 0x10000 - wend
marker = "OK"
if wend > 0x10000:
marker = "OVERFLOW"
fails.append(("_W3CODE", wsize))
print(f" _W3CODE @ 0x{waddr & 0xFFFF:04X} size {wsize:>6} "
f"(резидент W3) → 0x{wend & 0xFFFF:04X}, {wfree} Б свободно до 0x10000 {marker}")
# --- Банки (big/huge) ---------------------------------------------
bank_names = sorted(n for n in areas if _BANK_RE.match(n))
for name in bank_names:
addr, size = areas[name]
pct = size * 100.0 / BANK_LIMIT
@@ -67,24 +142,18 @@ def main():
if size > BANK_LIMIT:
marker = "OVERFLOW"
fails.append((name, size))
print(f" {name:<12} @ 0x{addr:08X} size {size:>5} / {BANK_LIMIT} ({pct:5.1f}%) {marker}")
# HOME budget — _CODE can land in W1 (huge/small) or W2 (big/tiny).
if "_CODE" in areas:
addr, size = areas["_CODE"]
end = addr + size
budget_remaining = HOME_CEILING - end
which_window = "W2 (0x8000-0xBFFF)" if addr >= 0x8000 else "HOME (0x4100-0xBFFF)"
print(f" _CODE @ 0x{addr:08X} size {size:>5} → ends at 0x{end:04X}, "
f"{which_window} has {budget_remaining} bytes free before 0x{HOME_CEILING:04X}")
if budget_remaining < 0:
print(f" ERROR: _CODE extends past 0x{HOME_CEILING:04X} (stack/heap territory)")
fails.append(("_CODE", size))
# виртуальный адрес: старший байт = номер банка, младшие 16 — окно
free = BANK_LIMIT - size
print(f" {name:<12} @ 0x{addr:08X} size {size:>6} / {BANK_LIMIT} "
f"({pct:4.1f}%) → {free} Б свободно {marker}")
if fails:
print()
for name, size in fails:
print(f" {name} too big: {size} bytes (limit {BANK_LIMIT})")
if name in ("_HOME", "_W3CODE"):
print(f" {name}: раскладка не влезла (конец 0x{size & 0xFFFF:04X})")
else:
print(f" {name} слишком большой: {size} Б (лимит {BANK_LIMIT})")
sys.exit(1)
+13 -5
View File
@@ -277,11 +277,14 @@ int main(int argc, char **argv) {
uint32_t pad_byte = 0xFF;
uint32_t bank_base = DEFAULT_BANK_BASE;
int verbose = 0;
int allow_w3_home = 0; /* -W: HOME может тянуться в W3 при W3-банках
(резидентный код --w3 в HUGE делит окно) */
for (int i = 1; i < argc; i++) {
const char *a = argv[i];
if (!strcmp(a, "-h") || !strcmp(a, "--help")) { usage(stdout); return 0; }
if (!strcmp(a, "-v")) { verbose = 1; continue; }
if (!strcmp(a, "-W")) { allow_w3_home = 1; continue; }
if (!strcmp(a, "-o") && i + 1 < argc) { out_path = argv[++i]; continue; }
if (!strcmp(a, "-L") && i + 1 < argc) {
if (parse_addr(argv[++i], &load_addr) < 0) { fprintf(stderr, "mkexe: bad -L\n"); return 1; }
@@ -386,12 +389,17 @@ int main(int argc, char **argv) {
}
}
/* HOME must not extend past 0xBFFF (W3 = bank territory in HUGE,
free in BIG but still off-limits to HOME). */
if (home->hi >= 0xC000u) {
/* HOME может тянуться в W3 (0xC000..0xFFFF) — это резидентный код
окна W3 (--w3, подход A): DSS грузит непрерывный образ и маппит
W1/W2/W3. Запрещаем ТОЛЬКО когда W3 реально занят трамплин-банками
(HUGE: bank_base=0xC000, max_bank>0) — тогда HOME и банки
столкнулись бы в одном окне. */
if (home->hi >= 0xC000u && max_bank > 0 && bank_base == DEFAULT_BANK_BASE
&& !allow_w3_home) {
fprintf(stderr,
"mkexe: HOME image extends to 0x%04X, past window 2 end (0xBFFF).\n"
" Code grew too big for HOME — move some .c files into a bank.\n",
"mkexe: HOME image extends to 0x%04X, into W3 (0xC000+) which\n"
" holds trampoline banks in HUGE mode — collision.\n"
" Move some .c files into a bank, or pass -W for --w3 residents.\n",
home->hi);
return 2;
}