Files
Sprinter-SDCC/toolchain/mkatlas.py
T
snark13 0e935343d9 libbgi: W0-атласы спрайтов — загрузчик, движок, упаковщик (все проверки PASS)
Реализация §3.1/§9в: атлас = один .atl-файл = одна EMM-страница,
подключаемая в W0 на время блита.

- atlas_load: read() файла целиком в страницу через W3 + патч ISR-стаба
  (0x38: JP _gfx_w0_isr; 0x66: RETN); atlas_free/atlas_image/
  atlas_sprite_init; gfx_w0_map/unmap для ручных вызовов.
- _gfx_w0_isr (стаб из tests/w0page): свап на ядро DSS → честный 0x38 →
  restore спрайт-страницы; покрывает IM1 и IM2-чейн.
- sprite_t.page (0 = обычная память); sprite_update в блит-проходе
  мапит страницу по смене (один OUT на атлас), эпилог возвращает DSS
  (+52 Б на движок — цена фичи).
- toolchain/mkatlas.py: PNG (indexed) / raw → .atl; заголовок 0x100,
  каталог 0x68 (19 лент), файл-офсет == офсет страницы == W0-адрес.
- Сплит _gfx_sprite_fns → _gfx_blit_fn.c + _gfx_heal_fn.c (1 указатель
  = 1 модуль): putsprite-only программа не тянет heal-ядро (spriteclip
  ловил +292 Б; теперь −68 Б от эталона).

tests/atlas (MAME dev, PASS): загрузка (count/страница), каталог и
данные по W0-адресам (заголовок ленты через gfx_w0_map), 3 спрайта из
двух лент через движок — пиксели проверены read_vram побайтно
(0x10/0x12/0x21, прозрачный угол = фон).

docs: §9г — предложение авто-анимации (кадровая ±step без умножений,
tween-Брезенхэм порциями, тикер через funcptr) — ОБСУЖДАЕТСЯ.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 15:52:00 +03:00

116 lines
4.2 KiB
Python
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.
#!/usr/bin/env python3
"""
mkatlas.py — упаковщик атласов спрайтов Sprinter (.atl, вариант II —
docs/sprite-api-design.md §3.1).
Формат файла (== образ EMM-страницы, файл-офсет == W0-адрес):
0x000 'S','P','A','1', count u8, резерв ×3
0x008 резерв (0x38-0x3A и 0x66-0x67 — под патч ISR-стаба загрузчиком)
0x068 каталог: count × 8 Б {offset u16 LE, fw u8, fh u8, nx u8, ny u8,
резерв u16} — offset указывает на getimage-ЗАГОЛОВОК ленты
0x100 ленты подряд: u16 w=fw*nx, u16 h=fh*ny (LE) + пиксели построчно
Вход — список лент:
img.png:FWxFH индексированный PNG (mode P/L; байты как есть,
прозрачность = индекс 0xFF); сетка nx,ny из
размеров картинки
img.raw:FWxFH:NXxNY сырые байты (fw*nx)×(fh*ny), построчно
Пример:
mkatlas.py sprites.atl hero.png:16x16 coin.png:16x16 boss.raw:24x24:2x2
Ограничения: count ≤ 19, файл ≤ 16384 Б, кадр ≤ 64×64.
"""
import os
import sys
HDR_SIZE = 0x100
DIR_OFF = 0x68
MAX_ENTRY = (HDR_SIZE - DIR_OFF) // 8 # 19
PAGE_SIZE = 16384
def die(msg):
sys.exit(f"mkatlas: {msg}")
def load_png(path):
from PIL import Image
im = Image.open(path)
if im.mode not in ("P", "L"):
die(f"{path}: нужен индексированный PNG (mode P/L), не {im.mode}")
return im.width, im.height, bytes(im.tobytes())
def parse_spec(spec):
parts = spec.split(":")
if len(parts) < 2:
die(f"'{spec}': нужен вид file:FWxFH[:NXxNY]")
path = parts[0]
fw, fh = (int(v) for v in parts[1].lower().split("x"))
if not (1 <= fw <= 64 and 1 <= fh <= 64):
die(f"{path}: кадр {fw}x{fh} вне 1..64 (контракт спрайтов)")
if path.lower().endswith(".png"):
if len(parts) != 2:
die(f"{path}: для PNG сетка берётся из размеров картинки")
w, h, pix = load_png(path)
else:
if len(parts) != 3:
die(f"{path}: для raw обязательна сетка :NXxNY")
nx, ny = (int(v) for v in parts[2].lower().split("x"))
w, h = fw * nx, fh * ny
pix = open(path, "rb").read()
if len(pix) != w * h:
die(f"{path}: {len(pix)} байт, ожидалось {w}x{h}={w * h}")
if w % fw or h % fh:
die(f"{path}: {w}x{h} не делится на кадр {fw}x{fh}")
nx, ny = w // fw, h // fh
if not (1 <= nx <= 255 and 1 <= ny <= 255):
die(f"{path}: сетка {nx}x{ny} вне 1..255")
return dict(path=path, fw=fw, fh=fh, nx=nx, ny=ny, w=w, h=h, pix=pix)
def main():
if len(sys.argv) < 3:
sys.exit(__doc__.strip())
out, specs = sys.argv[1], sys.argv[2:]
if len(specs) > MAX_ENTRY:
die(f"лент {len(specs)} > {MAX_ENTRY} (каталог 0x68-0xFF)")
entries = [parse_spec(s) for s in specs]
blob = bytearray(HDR_SIZE)
blob[0:4] = b"SPA1"
blob[4] = len(entries)
off = HDR_SIZE
for i, e in enumerate(entries):
d = DIR_OFF + i * 8
blob[d:d + 2] = off.to_bytes(2, "little")
blob[d + 2] = e["fw"]
blob[d + 3] = e["fh"]
blob[d + 4] = e["nx"]
blob[d + 5] = e["ny"]
blob += e["w"].to_bytes(2, "little") # getimage-заголовок ленты
blob += e["h"].to_bytes(2, "little")
blob += e["pix"]
off = len(blob)
if len(blob) > PAGE_SIZE:
die(f"итог {len(blob)} Б > {PAGE_SIZE} (одна EMM-страница); "
f"разбейте на несколько .atl")
with open(out, "wb") as f:
f.write(blob)
print(f"mkatlas: {out}: лент {len(entries)}, "
f"{len(blob)} Б (свободно {PAGE_SIZE - len(blob)})")
for i, e in enumerate(entries):
print(f" [{i}] {os.path.basename(e['path'])}: кадр "
f"{e['fw']}x{e['fh']}, сетка {e['nx']}x{e['ny']}")
if __name__ == "__main__":
main()