Архивы ресурсов 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:
2026-08-25 17:13:25 +03:00
parent 34ba5f71d5
commit ba3aca07bf
25 changed files with 357 additions and 67 deletions
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.
+14 -6
View File
@@ -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)
+76
View File
@@ -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;
}
+57
View File
@@ -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 */
+24 -13
View File
@@ -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;
+4 -1
View File
@@ -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 в это время обязан быть закрыт, иначе насос не успевает долить
+15 -3
View File
@@ -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;
+24 -8
View File
@@ -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()
+10 -8
View File
@@ -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
+6
View File
@@ -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
View File
@@ -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;
}
+35
View File
@@ -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;
}