0e935343d9
Реализация §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>
116 lines
4.2 KiB
Python
116 lines
4.2 KiB
Python
#!/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()
|