Files
Sprinter-SDCC/applications/PoP/toolchain/pop_pack_arc.py
T
snark13 ba3aca07bf Архивы ресурсов PBA1: звук одним файлом, музыка потоком; ALLOCS 6000
ЗАМЕР, из которого всё выросло (MAME, 2026-08-25): `open` стоит 51,4 мс,
а чтение 16 КБ из уже открытого файла — 32,6 мс.  То есть 61 % времени
загрузки уходит на ОТКРЫТИЕ, а не на данные, и чистая скорость чтения —
491 КБ/с, а не 260.  На диске игры 343 файла; одна только стартовая пачка
(kid+shadow+bg+sound+guard, ~142 файла) — это 7,3 секунды чистого open.

* toolchain/pop_pack_arc.py + pop_arc.c/h (банк 8) — формат PBA1:
  заголовок 512 Б, магия, count, записи по 4 байта (offset в СЕКТОРАХ —
  архив бывает больше 64 КБ; size в байтах — ресурс всегда <= 16 КБ,
  он обязан лезть в EMM-страницу).  Таблицу держит вызывающий на своём
  стеке: в W2 её класть нельзя, а стека в точке загрузки израсходовано
  75 байт из 1279 (замер там же).
* libc: bank_read_page — чтение из УЖЕ ОТКРЫТОГО файла в EMM-страницу.
  bank_load_file переписана через неё, поэтому дублирования работы с W3
  не осталось и резидент почти не вырос.
* Набор эффектов: 10 файлов -> SND\snd.arc, один open вместо десяти.
* Музыка: трек лежит ОДНИМ файлом и читается порциями по странице —
  нарезка на куски больше не нужна.  Этот же путь понадобится финалу
  `won` (1,2 МБ), который в память целиком не влезает.
* Менеджер звука приведён к оригиналу: играющая музыка участвует в
  таблицах приоритета наравне с эффектами (seg000:1672 + data.h:433).
  Почти вся музыка НЕперебиваема, поэтому поверх неё эффект не стартует —
  ровно как в SDLPoP; раньше эффект её перебивал.
* ALLOCS 3000 -> 6000 (решение пользователя): сборка +20 с, резидент
  -161 байт.  Куча 87 -> 248 Б.

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

79 lines
3.5 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 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 main() -> None:
if len(sys.argv) < 3:
raise SystemExit("usage: pop_pack_arc.py OUT.ARC file [file ...]")
out = Path(sys.argv[1])
files = [Path(a) for a in sys.argv[2:]]
for f in files:
if not f.exists():
raise SystemExit(f"нет файла: {f}")
build(out, files)
total = out.stat().st_size
print(f"{out.name}: {len(files)} записей, {total / 1024:.0f} КБ")
if __name__ == "__main__":
main()