Files
Sprinter-SDCC/applications/PoP/toolchain/pop_pack_arc.py
T
snark13 65797af44c Ресурсы — в архивы PBA1; тайминги сцены PV — по часам насоса CBL
ТАЙМИНГИ.  Пользователь заметил, что заклинание Джафара не совпадает с
музыкой. Замер в MAME (breakpoint на вспышке + курсор трека): реплика m53
успевала ДОИГРАТЬ до молнии, хотя по шкале оригинала между входом Джафара
и заклинанием ровно 822 тика = 13,7 с.

Причина: пейсинг считал НАШИ ожидания vsync, а не прошедшее время.
Полноэкранная копия страницы стоит больше кадра луча, и разница копилась.
Кадровые прерывания для счёта не годятся (теряются в di-окнах
акселератора), поэтому часами стал насос CBL: он идёт от расхода буфера
железом, 85,4 Гц, и ему безразлично, чем занят главный цикл. Один тик
оригинала = 57/40 тика насоса (0,07 % ошибки, без деления в кадре).
Когда звук выключен, работает прежний путь по кадрам луча.

После фикса замер даёт 229 блоков остатка m53 против расчётных 233 —
расхождение 47 мс.

АРХИВЫ.  Группы ресурсов сложены в PBA1: kid (58 атласов), pv (78),
shadow (32), bg (25), title (20), guard (10), vizier (5), skel (4). Было
232 файла на образе, стало 8 архивов плюс шесть палитр и таблицы уровней.
При цене `open` 51,4 мс против 32,6 мс за чтение 16 КБ это возвращает
секунды на каждой загрузке.

- pop_pack_arc.py печатает индексы элементов константами (ARC_<GRP>_<FILE>),
  порядок задаёт Makefile, а серии код проверяет статически;
- atlas_load разделён: atlas_attach принимает уже прочитанную страницу,
  поэтому чтение из архива не дублирует проверку магии и подготовку W0;
- имена архивов живут в pop_arc.c и адресуются номером. Строковый литерал
  лежит в rodata своего банка, и указатель на него из другого банка после
  переключения W3 показывает на чужие данные — ровно так падала загрузка
  фона (поймано брейкпоинтом на puts).

Проверено в MAME: заставка, интро и уровень 1 собираются из архивов,
Тень грузит все 32 страницы. Host-тесты: 15 наборов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 21:29:14 +03:00

124 lines
5.4 KiB
Python

#!/usr/bin/env python3
"""pop_pack_arc.py — сложить несколько ресурсов в один архив PBA1.
ЗАЧЕМ. На диске игры лежит больше трёхсот отдельных файлов, и дело не в
беспорядке: замер в MAME (2026-08-25) показал, что ОДИН `open` стоит
51,4 мс, а чтение 16 КБ из уже открытого файла — 32,6 мс. То есть 61 %
времени загрузки уходит на открытие, а не на данные. Одна только стартовая
пачка (kid + shadow + bg + sound + guard, ~142 файла) — это 7,3 секунды
чистого `open`. Сложив группу в архив и открыв его ОДИН раз, мы эти
секунды возвращаем.
ФОРМАТ (512-байтовый заголовок, дальше данные):
0..3 'PBA1' магия и версия — как SPA1 у атласов, PCFG, POPQ
4 count число записей, 1..126
5..7 резерв (нули)
8.. записи по 4 байта:
+0..1 offset в СЕКТОРАХ по 512 от начала файла (uint16 LE)
+2..3 size в байтах (uint16 LE)
Оффсет именно в секторах: архив `pv` — мегабайт, в байтах uint16 не хватило
бы. В секторах uint16 покрывает 32 МБ, то есть весь наш образ. Размер при
этом спокойно живёт в байтах — ни один ресурс не крупнее 16 КБ, потому что
каждый обязан помещаться в EMM-страницу.
Данные каждого элемента выровнены на 512, поэтому его смещение всегда
целое число секторов, а чтение попадает в границу сектора DSS.
"""
import re
import sys
from pathlib import Path
MAGIC = b"PBA1"
HEADER = 512
SECTOR = 512
MAX_ENTRIES = (HEADER - 8) // 4 # 126
def build(out_path: Path, files: list[Path]) -> None:
if len(files) > MAX_ENTRIES:
raise SystemExit(f"{out_path.name}: {len(files)} файлов, "
f"в заголовок влезает {MAX_ENTRIES}")
header = bytearray(HEADER)
header[0:4] = MAGIC
header[4] = len(files)
blob = bytearray()
for i, f in enumerate(files):
data = f.read_bytes()
if len(data) > 0xFFFF:
raise SystemExit(f"{f}: {len(data)} Б — размер не влезает в uint16")
offset = HEADER + len(blob)
assert offset % SECTOR == 0
rec = 8 + i * 4
header[rec:rec + 2] = (offset // SECTOR).to_bytes(2, "little")
header[rec + 2:rec + 4] = len(data).to_bytes(2, "little")
blob += data
pad = -len(blob) % SECTOR
blob += bytes(pad)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_bytes(bytes(header) + bytes(blob))
def const_name(prefix: str, path: Path) -> str:
"""kid12.atl -> ARC_KID_KID12; kid.pal -> ARC_KID_KID_PAL."""
stem = path.name
if stem.lower().endswith(".atl"):
stem = stem[:-4]
return f"ARC_{prefix}_" + re.sub(r"[^A-Za-z0-9]", "_", stem).upper()
def write_header(path: Path, prefix: str, files: list[Path]) -> None:
"""Индексы элементов — константами, чтобы код не считал их на память.
Порядок задаёт Makefile, и он же единственный источник истины: сдвинулся
список — сдвинулись константы, а серии вроде kid0..kid27 код проверяет
статически (`#if ARC_KID_KID27 - ARC_KID_KID0 != 27`).
"""
lines = [f"/* {path.name} — индексы в архиве {prefix}. СГЕНЕРИРОВАН",
" * (toolchain/pop_pack_arc.py), руками не править. */",
f"#ifndef {path.stem.upper()}_H",
f"#define {path.stem.upper()}_H",
"",
f"#define ARC_{prefix}_COUNT {len(files)}",
""]
width = max(len(const_name(prefix, f)) for f in files)
for i, f in enumerate(files):
lines.append(f"#define {const_name(prefix, f):<{width}} {i}")
lines += ["", "#endif", ""]
path.write_text("\n".join(lines))
def main() -> None:
args = sys.argv[1:]
header = prefix = None
while args and args[0].startswith("--"):
opt = args.pop(0)
if opt == "--header":
header = Path(args.pop(0))
elif opt == "--prefix":
prefix = args.pop(0)
else:
raise SystemExit(f"неизвестный ключ {opt}")
if len(args) < 2:
raise SystemExit("usage: pop_pack_arc.py [--header H.h --prefix NAME] "
"OUT.ARC file [file ...]")
out = Path(args[0])
files = [Path(a) for a in args[1:]]
for f in files:
if not f.exists():
raise SystemExit(f"нет файла: {f}")
build(out, files)
if header:
if not prefix:
raise SystemExit("--header требует --prefix")
write_header(header, prefix, files)
total = out.stat().st_size
print(f"{out.name}: {len(files)} записей, {total / 1024:.0f} КБ")
if __name__ == "__main__":
main()