Архивы ресурсов 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>
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -34,7 +34,10 @@ MEMORY ?= huge # small-раскладка + банки кода в W
|
||||
#
|
||||
# ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том
|
||||
# же ALLOCS — иначе сравниваются не правки, а уровни оптимизации.
|
||||
ALLOCS ?= 3000
|
||||
# 6000, а не 3000 (решение пользователя 2026-08-25): сборка дольше секунд на
|
||||
# двадцать, зато SDCC размещает регистры плотнее и резидент W1 худеет
|
||||
# примерно на 150 байт — а это ровно те байты, которых не хватает куче.
|
||||
ALLOCS ?= 6000
|
||||
# Build info не хранится вручную в C: ID и дата извлекаются Make из текущего
|
||||
# commit. Переопределения удобны для воспроизводимой release-сборки.
|
||||
BUILD_ID ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo dev)
|
||||
@@ -51,7 +54,7 @@ BANK4_SRCS := pop_cdraw.c pop_kdraw.c
|
||||
BANK5_SRCS := pop_ctrl.c pop_tile_cold.c
|
||||
BANK6_SRCS := pop_trob.c
|
||||
BANK7_SRCS := pop_room.c pop_redraw.c
|
||||
BANK8_SRCS := roomtest_cold.c pop_level_cold.c pop_kboot.c pop_guard_cold.c \
|
||||
BANK8_SRCS := roomtest_cold.c pop_arc.c pop_level_cold.c pop_kboot.c pop_guard_cold.c \
|
||||
pop_shadow.c pop_sfx_cold.c pop_qsave.c pop_qsave_coldio.c \
|
||||
pop_level_qsave.c
|
||||
# Банк 9 — оболочка: menu/settings/config/font, автомат, global timer,
|
||||
@@ -171,7 +174,11 @@ SND_DIR := $(CURDIR)/../poc/res/sound
|
||||
# Число файлов задаёт упаковщик (POP_SND_PAGES в pop_sound_tbl.h): с
|
||||
# 2026-08-25 звук не режется по границе страницы, поэтому страниц стало 10.
|
||||
# Список ОБЯЗАН совпадать: недостающий файл — это молча пропавшие эффекты.
|
||||
SND_DATA := $(foreach n,0 1 2 3 4 5 6 7 8 9,$(SND_DIR)/s$(n).bin)
|
||||
SND_PARTS := $(foreach n,0 1 2 3 4 5 6 7 8 9,$(SND_DIR)/s$(n).bin)
|
||||
# На диск уходит ОДИН архив вместо десяти файлов: `open` дороже чтения
|
||||
# (51,4 мс против 32,6 мс на 16 КБ, замер 2026-08-25), см. pop_arc.h.
|
||||
SND_ARC := $(SND_DIR)/snd.arc
|
||||
SND_DATA := $(SND_ARC)
|
||||
|
||||
# МУЗЫКА: записи DOS-версии, приведённые к формату CBL и порезанные на
|
||||
# страницы EMM (toolchain/pop_pack_music.py). Пока в игре звучит один
|
||||
@@ -179,8 +186,7 @@ SND_DATA := $(foreach n,0 1 2 3 4 5 6 7 8 9,$(SND_DIR)/s$(n).bin)
|
||||
# добавлением номеров в MUS_IDS, имена кусков задаёт упаковщик.
|
||||
MUS_DIR := $(CURDIR)/../poc/res/music
|
||||
MUS_IDS := 55
|
||||
MUS_CHUNKS := 00 01 02 03 04 05 06 07 08 09 10
|
||||
MUS_DATA := $(foreach i,$(MUS_IDS),$(foreach c,$(MUS_CHUNKS),$(MUS_DIR)/m$(i)_$(c).bin))
|
||||
MUS_DATA := $(foreach i,$(MUS_IDS),$(MUS_DIR)/m$(i).bin)
|
||||
|
||||
# UI: два исходных шрифта SDLPoP в одном EMM-атласе. Файл короче страницы:
|
||||
# хвост использует menu для временного snapshot палитры (без расхода W2).
|
||||
@@ -294,8 +300,10 @@ $(SHADOW_DATA) pop_shadow_atlas.h: $(SHADOW_STAMP)
|
||||
$(SOUND_STAMP): $(TC)/pop_pack_sound.py | $(RESOURCE_STAMP_DIR)
|
||||
cd $(TC) && python3 pop_pack_sound.py
|
||||
touch $@
|
||||
$(SND_DATA) pop_sound_tbl.h: $(SOUND_STAMP)
|
||||
$(SND_PARTS) pop_sound_tbl.h: $(SOUND_STAMP)
|
||||
@test -f $@ || { $(MAKE) -B $(SOUND_STAMP); test -f $@; }
|
||||
$(SND_ARC): $(SND_PARTS) $(TC)/pop_pack_arc.py
|
||||
python3 $(TC)/pop_pack_arc.py $@ $(SND_PARTS)
|
||||
|
||||
$(MUSIC_STAMP): $(TC)/pop_pack_music.py | $(RESOURCE_STAMP_DIR)
|
||||
cd $(TC) && python3 pop_pack_music.py $(MUS_IDS)
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* pop_arc.c — чтение архива ресурсов PBA1, БАНК 8.
|
||||
*
|
||||
* Контракт и мотивация — в pop_arc.h. Здесь всё сводится к трём вызовам
|
||||
* libc: open один раз на группу, lseek к элементу и bank_read_page прямо в
|
||||
* EMM-страницу (тот резидентен и потому безопасен из банка huge — он же
|
||||
* переключает W3, где живёт наш код).
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
#include <sprinter_mem.h>
|
||||
#include "pop_arc.h"
|
||||
|
||||
int8_t pop_arc_open(pop_arc_t *a, const char *path,
|
||||
uint8_t *tbl, uint8_t tbl_max) __banked
|
||||
{
|
||||
uint8_t head[8];
|
||||
int fd;
|
||||
|
||||
a->fd = -1;
|
||||
a->count = 0;
|
||||
fd = open(path, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
if (read(fd, head, sizeof(head)) != (int)sizeof(head) ||
|
||||
memcmp(head, "PBA1", 4) != 0 ||
|
||||
head[4] == 0 || head[4] > tbl_max || head[4] > POP_ARC_MAX) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
if (read(fd, tbl, (uint16_t)head[4] * POP_ARC_REC) !=
|
||||
(int)((uint16_t)head[4] * POP_ARC_REC)) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
a->fd = fd;
|
||||
a->count = head[4];
|
||||
return (int8_t)head[4];
|
||||
}
|
||||
|
||||
uint16_t pop_arc_size(const uint8_t *tbl, uint8_t idx)
|
||||
{
|
||||
const uint8_t *rec = tbl + (uint16_t)idx * POP_ARC_REC;
|
||||
return (uint16_t)(rec[2] | ((uint16_t)rec[3] << 8));
|
||||
}
|
||||
|
||||
int pop_arc_read(pop_arc_t *a, const uint8_t *tbl, uint8_t idx, uint16_t skip,
|
||||
uint8_t page, uint16_t off, uint16_t size) __banked
|
||||
{
|
||||
const uint8_t *rec;
|
||||
uint16_t sec, len;
|
||||
long pos;
|
||||
|
||||
if (a->fd < 0 || idx >= a->count) return -1;
|
||||
rec = tbl + (uint16_t)idx * POP_ARC_REC;
|
||||
sec = (uint16_t)(rec[0] | ((uint16_t)rec[1] << 8));
|
||||
len = (uint16_t)(rec[2] | ((uint16_t)rec[3] << 8));
|
||||
if (skip >= len) return 0;
|
||||
len = (uint16_t)(len - skip);
|
||||
if (size && size < len) len = size;
|
||||
|
||||
/* Сектора в байты — единственное место, где нужна 32-битная величина:
|
||||
* архив может быть больше 64 КБ, и lseek принимает long. Путь холодный
|
||||
* (загрузка ресурсов), в кадре этого нет. */
|
||||
pos = ((long)sec << 9) + (long)skip;
|
||||
if (lseek(a->fd, pos, SEEK_SET) < 0) return -1;
|
||||
return bank_read_page(a->fd, page, off, len);
|
||||
}
|
||||
|
||||
void pop_arc_close(pop_arc_t *a) __banked
|
||||
{
|
||||
if (a->fd >= 0) close(a->fd);
|
||||
a->fd = -1;
|
||||
a->count = 0;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* pop_arc.h — архив ресурсов PBA1: много файлов в одном, один open на всех.
|
||||
*
|
||||
* ЗАЧЕМ. Замер в MAME (2026-08-25): `open` стоит 51,4 мс, а чтение 16 КБ из
|
||||
* уже открытого файла — 32,6 мс. То есть 61 % времени загрузки уходит на
|
||||
* открытие, а не на данные, и одна только стартовая пачка ресурсов (~142
|
||||
* файла) — это 7,3 секунды чистого `open`. Архив открывается ОДИН раз на
|
||||
* группу, и эта цена платится один раз вместо сотни.
|
||||
*
|
||||
* ФОРМАТ (генерирует toolchain/pop_pack_arc.py):
|
||||
* 0..3 'PBA1' магия и версия
|
||||
* 4 count число записей, 1..126
|
||||
* 8.. записи по 4 байта: offset в СЕКТОРАХ по 512 (uint16),
|
||||
* size в байтах (uint16)
|
||||
* данные с 512, каждый элемент выровнен на 512.
|
||||
* Оффсет в секторах, потому что архив может быть больше 64 КБ (pv — мегабайт);
|
||||
* размер в байтах, потому что ни один ресурс не крупнее 16 КБ — он обязан
|
||||
* помещаться в EMM-страницу.
|
||||
*
|
||||
* ТАБЛИЦУ ДЕРЖИТ ВЫЗЫВАЮЩИЙ, на своём стеке: 4 байта на запись, у самой
|
||||
* большой группы 320 байт. В W2 её класть нельзя — там осталось около сотни
|
||||
* байт, — а стека в точке загрузки ресурсов израсходовано 75 байт из 1279
|
||||
* (замер там же). Так модуль не занимает постоянной памяти вовсе.
|
||||
*/
|
||||
#ifndef POP_ARC_H
|
||||
#define POP_ARC_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define POP_ARC_HEADER 512
|
||||
#define POP_ARC_SECTOR 512
|
||||
#define POP_ARC_MAX 126 /* (512-8)/4 */
|
||||
#define POP_ARC_REC 4 /* байт на запись таблицы */
|
||||
|
||||
typedef struct {
|
||||
int fd;
|
||||
uint8_t count;
|
||||
} pop_arc_t;
|
||||
|
||||
/* Открыть архив и вычитать таблицу в буфер вызывающего (count*4 байт).
|
||||
* tbl_max — сколько записей помещается в буфер.
|
||||
* Возврат: число записей; -1 — не открылся, не PBA1 либо таблица не влезла. */
|
||||
int8_t pop_arc_open(pop_arc_t *a, const char *path,
|
||||
uint8_t *tbl, uint8_t tbl_max) __banked;
|
||||
|
||||
/* Размер элемента idx по таблице (0, если индекс за пределами). */
|
||||
uint16_t pop_arc_size(const uint8_t *tbl, uint8_t idx);
|
||||
|
||||
/* Прочитать кусок элемента idx в EMM-страницу: skip байт от его начала,
|
||||
* size байт, в страницу page со смещением off. size == 0 — весь элемент.
|
||||
* Возврат: сколько прочитано, -1 — ошибка. */
|
||||
int pop_arc_read(pop_arc_t *a, const uint8_t *tbl, uint8_t idx, uint16_t skip,
|
||||
uint8_t page, uint16_t off, uint16_t size) __banked;
|
||||
|
||||
void pop_arc_close(pop_arc_t *a) __banked;
|
||||
|
||||
#endif /* POP_ARC_H */
|
||||
@@ -10,6 +10,8 @@
|
||||
* тянем, а тут и формат-то из двух чисел.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sprinter_mem.h>
|
||||
#include "pop_music.h"
|
||||
#include "pop_music_tbl.h"
|
||||
@@ -19,19 +21,17 @@ uint8_t pop_mus_page[POP_MUS_PAGES];
|
||||
static uint8_t mus_blk; /* блок EMM под трек */
|
||||
static uint8_t mus_pages; /* сколько страниц реально занято */
|
||||
static uint16_t mus_blocks; /* длина трека в порциях по 128 Б */
|
||||
static uint8_t mus_id; /* что загружено — для менеджера звука */
|
||||
|
||||
/* "MUS\mNN_KK.bin" — ровно 8.3, как требует DSS. */
|
||||
static void mus_name(char *buf, uint8_t id, uint8_t chunk)
|
||||
/* "MUS\mNN.bin" — ровно 8.3, как требует DSS. */
|
||||
static void mus_name(char *buf, uint8_t id)
|
||||
{
|
||||
buf[0] = 'M'; buf[1] = 'U'; buf[2] = 'S'; buf[3] = '\\';
|
||||
buf[4] = 'm';
|
||||
buf[5] = (char)('0' + id / 10);
|
||||
buf[6] = (char)('0' + id % 10);
|
||||
buf[7] = '_';
|
||||
buf[8] = (char)('0' + chunk / 10);
|
||||
buf[9] = (char)('0' + chunk % 10);
|
||||
buf[10] = '.'; buf[11] = 'b'; buf[12] = 'i'; buf[13] = 'n';
|
||||
buf[14] = 0;
|
||||
buf[7] = '.'; buf[8] = 'b'; buf[9] = 'i'; buf[10] = 'n';
|
||||
buf[11] = 0;
|
||||
}
|
||||
|
||||
void pop_music_stop(void) __banked
|
||||
@@ -52,29 +52,39 @@ void pop_music_free(void) __banked
|
||||
|
||||
int8_t pop_music_load(uint8_t id) __banked
|
||||
{
|
||||
char name[15];
|
||||
char name[12];
|
||||
uint8_t i, pages;
|
||||
int fd;
|
||||
|
||||
if (id >= POP_MUS_COUNT) return -1;
|
||||
pages = pop_mus_tbl[id].pages;
|
||||
if (!pages || pages > POP_MUS_PAGES) return -1;
|
||||
|
||||
pop_music_free();
|
||||
mus_name(name, id);
|
||||
/* Трек лежит ОДНИМ файлом и читается порциями по странице: `open` стоит
|
||||
* дороже самого чтения (51,4 мс против 32,6 мс на 16 КБ), и платить его
|
||||
* на каждую страницу — чистые потери. Тот же путь понадобится финалу
|
||||
* `won`, который в память целиком не влезает. */
|
||||
fd = open(name, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
mus_blk = mem_alloc_pages(pages);
|
||||
if (!mus_blk) return -1;
|
||||
if (!mus_blk) { close(fd); return -1; }
|
||||
for (i = 0; i < pages; i++) {
|
||||
pop_mus_page[i] = mem_get_page(mus_blk, i);
|
||||
mus_name(name, id, i);
|
||||
/* Последний кусок короче страницы — читаем сколько есть; ошибку
|
||||
* ловим по отрицательному коду, а не по «не 16384». */
|
||||
/* Последняя страница короче — читаем сколько есть; ошибку ловим по
|
||||
* отрицательному коду, а не по «не 16384». */
|
||||
if (!pop_mus_page[i] ||
|
||||
bank_load_file(pop_mus_page[i], 0, name, 16384) < 0) {
|
||||
bank_read_page(fd, pop_mus_page[i], 0, 16384) < 0) {
|
||||
close(fd);
|
||||
pop_music_free();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
mus_pages = pages;
|
||||
mus_blocks = pop_mus_tbl[id].blocks;
|
||||
mus_id = id;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -84,6 +94,7 @@ void pop_music_play(void) __banked
|
||||
/* Порядок важен: сначала позиция, потом длина. Насос стартует ровно
|
||||
* с той порции, где left стал ненулевым, и промежуточное состояние
|
||||
* «играем, но курсор от прошлого трека» не возникает. */
|
||||
pop_mus_id = mus_id; /* до left: менеджер спросит уже готовое */
|
||||
pop_mus_pg = 0;
|
||||
pop_mus_ptr = 0;
|
||||
pop_mus_left = mus_blocks;
|
||||
|
||||
@@ -37,7 +37,10 @@
|
||||
extern uint8_t pop_mus_page[POP_MUS_PAGES]; /* физические страницы EMM */
|
||||
extern volatile uint8_t pop_mus_pg; /* индекс текущей страницы */
|
||||
extern volatile uint16_t pop_mus_ptr; /* адрес в окне W0 */
|
||||
extern volatile uint16_t pop_mus_left; /* байт до конца трека, 0 — молчим */
|
||||
extern volatile uint16_t pop_mus_left; /* порций до конца, 0 — молчим */
|
||||
/* Номер играющего трека (нумерация оригинала). Нужен менеджеру звука:
|
||||
* музыка участвует в таблицах приоритета наравне с эффектами. */
|
||||
extern uint8_t pop_mus_id;
|
||||
|
||||
/* Загрузить трек в EMM (НЕ запускает). Звать там, где допустима пауза на
|
||||
* диск: CBL в это время обязан быть закрыт, иначе насос не успевает долить
|
||||
|
||||
@@ -48,6 +48,9 @@ uint8_t pop_snd_want;
|
||||
volatile uint8_t pop_mus_pg;
|
||||
volatile uint16_t pop_mus_ptr;
|
||||
volatile uint16_t pop_mus_left;
|
||||
/* Номер играющего трека в нумерации ОРИГИНАЛА: музыка участвует в тех же
|
||||
* таблицах приоритета и перебиваемости, что и эффекты (см. pop_sfx_tick). */
|
||||
uint8_t pop_mus_id;
|
||||
|
||||
static volatile uint8_t sfx_pg; /* индекс в pop_snd_page[] */
|
||||
static volatile uint16_t sfx_ptr; /* адрес в окне W0: 0x0000..0x3FFF */
|
||||
@@ -210,9 +213,18 @@ void pop_sfx_tick(void)
|
||||
if (!snd_next1) return;
|
||||
id = (uint8_t)(snd_next1 - 1);
|
||||
snd_next1 = 0; /* номинант живёт ровно кадр */
|
||||
if (sfx_left != 0 && /* что-то играет... */
|
||||
(!snd_interruptible(snd_curr) || snd_prio[id] > snd_prio[snd_curr]))
|
||||
return; /* ...и уступать не обязано */
|
||||
/* ЧТО СЕЙЧАС ЗВУЧИТ — эффект либо музыка. У оригинала это один и тот
|
||||
* же вопрос: check_sound_playing() отвечает «играет хоть что-то», без
|
||||
* различия типа, а музыкальные id лежат в тех же таблицах приоритета и
|
||||
* перебиваемости (seg000:1672 + data.h:433). Почти вся музыка помечена
|
||||
* НЕперебиваемой, поэтому пока она играет, эффект просто не стартует —
|
||||
* ровно это слышно в оригинале. Исключение одно: 29 meet_Jaffar. */
|
||||
{
|
||||
uint8_t cur = sfx_left ? snd_curr : pop_mus_id;
|
||||
if ((sfx_left != 0 || pop_mus_left != 0) &&
|
||||
(!snd_interruptible(cur) || snd_prio[id] > snd_prio[cur]))
|
||||
return; /* ...и уступать не обязано */
|
||||
}
|
||||
snd_curr = id;
|
||||
pg = pop_snd_tbl[id].page;
|
||||
off = pop_snd_tbl[id].off;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <sprinter_mem.h>
|
||||
#include <cbl.h>
|
||||
#include "pop_sfx.h"
|
||||
#include "pop_arc.h"
|
||||
#define POP_SND_FILES /* имена файлов нужны ТОЛЬКО здесь, см. pop_sound_tbl.h */
|
||||
#include "_pop_sfx.h"
|
||||
|
||||
@@ -20,16 +21,31 @@ int pop_sfx_init(void) __banked
|
||||
if (pop_snd_ok) return 0;
|
||||
|
||||
/* Одним блоком: страницы блока идут подряд по логическому индексу, и
|
||||
* таблица звуков адресует их именно так (page = смещение >> 14). */
|
||||
blk = mem_alloc_pages(POP_SND_PAGES);
|
||||
if (!blk) return -1;
|
||||
for (i = 0; i < POP_SND_PAGES; i++) {
|
||||
pop_snd_page[i] = mem_get_page(blk, i);
|
||||
if (!pop_snd_page[i] ||
|
||||
bank_load_file(pop_snd_page[i],0,pop_snd_files[i],16384) != 16384) {
|
||||
mem_free_block(blk);
|
||||
* таблица звуков адресует их именно так (page = смещение >> 14).
|
||||
*
|
||||
* Из АРХИВА, а не десятью файлами: `open` стоит 51,4 мс против 32,6 мс
|
||||
* на чтение 16 КБ (замер 2026-08-25), то есть девять лишних открытий —
|
||||
* это почти полсекунды на ровном месте. Таблица живёт на стеке, см.
|
||||
* pop_arc.h. */
|
||||
{
|
||||
pop_arc_t arc;
|
||||
uint8_t tbl[POP_SND_PAGES * POP_ARC_REC];
|
||||
|
||||
if (pop_arc_open(&arc, "SND\\snd.arc", tbl, POP_SND_PAGES) !=
|
||||
POP_SND_PAGES)
|
||||
return -1;
|
||||
blk = mem_alloc_pages(POP_SND_PAGES);
|
||||
if (!blk) { pop_arc_close(&arc); return -1; }
|
||||
for (i = 0; i < POP_SND_PAGES; i++) {
|
||||
pop_snd_page[i] = mem_get_page(blk, i);
|
||||
if (!pop_snd_page[i] ||
|
||||
pop_arc_read(&arc, tbl, i, 0, pop_snd_page[i], 0, 0) != 16384) {
|
||||
pop_arc_close(&arc);
|
||||
mem_free_block(blk);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
pop_arc_close(&arc);
|
||||
}
|
||||
snd_blk = blk;
|
||||
pop_snd_want = 1; /* набор есть — звук по умолчанию включён */
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/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()
|
||||
@@ -12,11 +12,15 @@
|
||||
берём не 11 000, а частоту железа: тогда CBL открывается ОДИН раз за
|
||||
запуск и не переключается ни между эффектами, ни при переходе на музыку.
|
||||
|
||||
РЕЖЕМ НА СТРАНИЦЫ EMM по 16 КБ. bank_load_file умеет читать файл только
|
||||
целиком и только в одну страницу, поэтому трек на диске лежит пачкой
|
||||
кусков M<id>_<nn>.BIN — ровно как набор эффектов лежит s0..s9.bin.
|
||||
Последний кусок дополняется тишиной (0x80) до кратности 128: насос отдаёт
|
||||
CBL блоками по 128 байт, и остаток «не полного блока» ему пришлось бы
|
||||
ОДИН ФАЙЛ НА ТРЕК. Резать его на куски не нужно: читаем порциями по
|
||||
16 КБ из ОДНОГО открытого файла (`bank_read_page` + `lseek`), и «дорогой»
|
||||
open платится раз на трек, а не на каждую страницу — замер 2026-08-25:
|
||||
open 51,4 мс против 32,6 мс на чтение 16 КБ. Этот же путь работает для
|
||||
финального `won` (1,2 МБ), который в память целиком не помещается и
|
||||
обязан подгружаться по ходу.
|
||||
|
||||
Хвост дополняется тишиной (0x80) до кратности 128: насос отдаёт CBL
|
||||
блоками по 128 байт, и остаток «не полного блока» ему пришлось бы
|
||||
досчитывать в прерывании.
|
||||
|
||||
Длина трека уходит в pop_music_tbl.h блоками по 128 байт, а не байтами:
|
||||
@@ -90,9 +94,7 @@ def pack(track_id: int, name: str) -> tuple[int, int]:
|
||||
|
||||
chunks = (len(data) + PAGE - 1) // PAGE
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
for i in range(chunks):
|
||||
part = data[i * PAGE:(i + 1) * PAGE]
|
||||
(OUT_DIR / f"m{track_id:02d}_{i:02d}.bin").write_bytes(part)
|
||||
(OUT_DIR / f"m{track_id:02d}.bin").write_bytes(bytes(data))
|
||||
return chunks, len(data) // BLOCK
|
||||
|
||||
|
||||
|
||||
@@ -116,6 +116,12 @@ void bank_write(uint8_t phys_page, uint16_t off, const void *src, uint16_t n)
|
||||
* Возвращает фактический размер либо -1 + errno. */
|
||||
int bank_load_file(uint8_t phys_page, uint16_t off, const char *path,
|
||||
uint16_t max_size);
|
||||
|
||||
/* То же, но из УЖЕ ОТКРЫТОГО файла и с текущей позиции (её ставит вызывающий
|
||||
* через lseek). Нужна тем, кто читает из одного файла много кусков: `open`
|
||||
* стоит дороже самого чтения (замер: 51,4 мс против 32,6 мс на 16 КБ), и
|
||||
* держать fd открытым — единственный способ эту цену не платить. */
|
||||
int bank_read_page(int fd, uint8_t phys_page, uint16_t off, uint16_t size);
|
||||
int bank_save_file(uint8_t phys_page, uint16_t off, const char *path,
|
||||
uint16_t size);
|
||||
|
||||
|
||||
+11
-15
@@ -1,33 +1,29 @@
|
||||
/*
|
||||
* bank_load_file — прочитать файл непосредственно в физическую EMM-страницу.
|
||||
* bank_load_file — прочитать файл целиком в физическую EMM-страницу.
|
||||
*
|
||||
* ESTEX принимает файловый буфер только в W1..W3; используем W3, сохраняя и
|
||||
* восстанавливая его раскладку. Функция резидентна, поэтому её безопасно
|
||||
* вызывать из банкового кода huge: после open() указатель path больше не
|
||||
* читается, пока W3 временно занят целевой страницей.
|
||||
* Тонкая обёртка над bank_read_page: открыть, перенести, закрыть. Вся
|
||||
* работа с W3 (ESTEX принимает файловый буфер только в W1..W3) живёт там,
|
||||
* в одном месте — дублировать её здесь незачем, а резидент W1 у
|
||||
* приложений тесный.
|
||||
*
|
||||
* Кто читает из одного файла НЕСКОЛЬКО кусков (архив ресурсов, поток
|
||||
* музыки), обязан звать bank_read_page напрямую, держа fd открытым:
|
||||
* `open` дороже самого чтения — 51,4 мс против 32,6 мс на 16 КБ (замер в
|
||||
* MAME, PoP, 2026-08-25).
|
||||
*
|
||||
* Возврат: фактически прочитанный размер либо -1 + errno.
|
||||
*/
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <sprinter.h>
|
||||
#include <sprinter_mem.h>
|
||||
|
||||
int bank_load_file(uint8_t phys_page, uint16_t off, const char *path,
|
||||
uint16_t max_size)
|
||||
{
|
||||
uint8_t saved;
|
||||
int fd, n;
|
||||
if (off > 0x3FFFu || max_size > (uint16_t)(0x4000u - off)) {
|
||||
errno=EINVAL; return -1;
|
||||
}
|
||||
fd = open(path, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
saved = _io_page_w3;
|
||||
sprinter_page_w3(phys_page);
|
||||
n = read(fd, (void *)(0xC000u + off), max_size);
|
||||
sprinter_page_w3(saved);
|
||||
n = bank_read_page(fd, phys_page, off, max_size);
|
||||
close(fd);
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* bank_read_page — прочитать из УЖЕ ОТКРЫТОГО файла прямо в EMM-страницу.
|
||||
*
|
||||
* Зачем отдельно от bank_load_file: та открывает и закрывает файл на каждый
|
||||
* вызов, а `open` на Sprinter стоит дорого — замер в MAME (PoP, 2026-08-25):
|
||||
* open 51,4 мс против 32,6 мс на чтение 16 КБ, то есть 61 % времени уходит
|
||||
* на открытие. Кто читает из одного файла несколько кусков (архив ресурсов,
|
||||
* поток музыки), обязан держать fd открытым — тогда эта цена платится раз.
|
||||
*
|
||||
* ESTEX принимает файловый буфер только в W1..W3; используем W3, сохраняя и
|
||||
* восстанавливая раскладку. Функция резидентна, поэтому её безопасно
|
||||
* вызывать из банкового кода huge: пока W3 занят целевой страницей, никакой
|
||||
* памяти вызывающего мы не читаем. Позицию в файле вызывающий ставит сам
|
||||
* (lseek) — здесь только перенос.
|
||||
*
|
||||
* Возврат: фактически прочитанный размер либо -1 + errno.
|
||||
*/
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <sprinter.h>
|
||||
#include <sprinter_mem.h>
|
||||
|
||||
int bank_read_page(int fd, uint8_t phys_page, uint16_t off, uint16_t size)
|
||||
{
|
||||
uint8_t saved;
|
||||
int n;
|
||||
if (off > 0x3FFFu || size > (uint16_t)(0x4000u - off)) {
|
||||
errno = EINVAL; return -1;
|
||||
}
|
||||
saved = _io_page_w3;
|
||||
sprinter_page_w3(phys_page);
|
||||
n = read(fd, (void *)(0xC000u + off), size);
|
||||
sprinter_page_w3(saved);
|
||||
return n;
|
||||
}
|
||||
Reference in New Issue
Block a user