Compare commits

...

4 Commits

Author SHA1 Message Date
snark13 623199337e SprPoP: HDD-раскладка и пути от каталога EXE 2026-08-30 11:21:11 +03:00
snark13 602c3a20fa SprPoP: сняты последние глушения насоса — палитра на смене уровня и загрузки треков
Замер тем же способом (брейк на pop_sfx_fill + печать totalcycles, три окна
по 800 вызовов = 9 секунд каждое, с уже снятыми глушениями):

  период насоса   245 760 тактов (медиана во всех окнах)
  максимум        245 832 / 270 096 / 311 346 (1,00 / 1,10 / 1,27 периода)
  пропущено порций 0

Порция считается пропущенной, когда зазор доходит до ДВУХ периодов: сама
порция отдаётся железу за период до того, как она понадобится, поэтому
опоздание обработчика на 1,1 мс — джиттер, а не потеря.  Запас
десятикратный.

Поэтому сняты и оставшиеся места:
* палитра через BIOS на переходе БЕЗ катсцены (sprpop_cold.c) — то самое,
  где ловили скрежет 2026-08-25; в комментарии помечено, что при возврате
  скрежета возвращать надо именно сюда;
* массовые чтения треков и ресурсов под чёрным экраном (pop_intro.c):
  первая реплика PV, трек заставки между уровнями, «время вышло», ресурсы
  финала.

Осталось только то, что глушит звук ПО СМЫСЛУ, а не ради защиты: выходы из
сцен (pop_music_free + pause), уход в титры и в игру, выключение по Ctrl+S.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 19:01:27 +03:00
snark13 4ac3584bf9 SprPoP: идея «готовить следующий уровень под мелодию» — в бэклог, на дальнюю версию
Записана с оговорками, найденными при сегодняшнем разборе: загрузку придётся
разрезать на дисковую и палитро-экранную половины, шаг подкачки держать
полустраничным, проверить EMM-бюджет на два уровня разом.  Половина идеи уже
работает — трек заставки играет поверх загрузки (порядок оригинала, замер
насоса приложен в записи).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 18:55:31 +03:00
snark13 3bf28da8ee SprPoP: трек заставки переживает загрузку уровня — порядок как в оригинале
ЗАМЕР (MAME, 2026-08-28).  Брейк на pop_sfx_fill с печатью totalcycles, 500
подряд вызовов насоса через всю загрузку уровня 1->2 под звучащий трек 27:
медиана интервала 245 760 тактов, максимум 245 832 при дедлайне 251 000
(11,7 мс) — НИ ОДНОЙ пропущенной порции.  Загрузка уровня насос не морит.

Поэтому снята двойная заплатка:
* pop_level_switch больше не глушит насос перед pop_level_load_num;
* pre_cut_finish больше не досиживает трек на чёрном экране (это делалось
  только чтобы глушение не обрубило его на полуслове; ценой были ~8 секунд
  пустого экрана — трек 27 длиннее сцены: 10,7 с против 2,6 с).

Взамен восстановлен порядок оригинала (seg003:68-108, play_level):
катсцена возвращает управление сразу -> уровень грузится ПОД музыку ->
ожидание конца трека на чёрном экране (порт `while (check_sound_playing())`
+ stop_sounds) -> показ уровня.  Общая чернота теперь max(трек, загрузка), а
не их сумма, и трек не обрывается.  Ожидание со страховкой на ~20 с, чтобы
потоковый трек не подвесил переход.

Пропуск сцены по-прежнему обрывает музыку — как и было.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 18:53:44 +03:00
22 changed files with 360 additions and 67 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img
# образ у себя и связывает его с MAME символьной ссылкой.
HDD_IMG ?= $(MAME_DIR)/IMG/test_hdd.chd
MAKE_DISK := $(MAME_DIR)/make_disk.py
MAKE_HDD := $(PROJ_ROOT)/toolchain/make_hdd.sh
MAKE_HDD ?= $(PROJ_ROOT)/toolchain/make_hdd.sh
RUN_MAME := $(MAME_DIR)/run_mame.sh
# Optional knobs — see top of file.
+3 -1
View File
@@ -34,6 +34,8 @@ EXAMPLE := sprpop
SRC_DIR := src
BUILD_DIR := build
MAKE_HDD := tools/make_hdd.sh
# huge: small-раскладка (CODE в W1, DATA в W2) + банки кода в W3.
MEMORY ?= huge
# --max-allocs-per-node: во сколько вариантов размещения регистров упирается
@@ -91,7 +93,7 @@ BANK9_SRCS := pop_menu.c pop_status.c pop_music.c pop_app.c pop_timer.c \
# стоит 51 мс, так что трамплин банк→банк на их фоне не существует.
# Отдельный банк для этого не нужен: в десятом лежал один pop_pal.c на
# 485 байт, то есть 3% страницы.
BANK10_SRCS := pop_pal.c pop_config.c pop_hof.c
BANK10_SRCS := pop_pal.c pop_config.c pop_hof.c pop_path_bank.c
BANK11_SRCS := pop_cutscene.c pop_intro.c
BANK_NUMS := 1 2 3 4 5 6 7 8 9 10 11
BANKED_SRCS := $(foreach n,$(BANK_NUMS),$(BANK$(n)_SRCS))
+44
View File
@@ -89,3 +89,47 @@ raw-клавиатура (`<kbd_raw.h>`), которую мы и так заби
после выхода — заметный. Делать после того, как закроем стражей и
займёмся бюджетом всерьёз (там же, где батчинг кроссбанковых вызовов и
возможный возврат `pop_bg` в резидент `--w3`).
## Готовить следующий уровень, пока играет мелодия конца текущего
**САМАЯ ДАЛЬНЯЯ ВЕРСИЯ.** Не полишинг и не порт: это улучшение ПРОТИВ
оригинала. Планируем, но не раньше, чем закроем уровни и полишинг.
**Идея (пользователь, 2026-08-28).** Пока звучит мелодия конца уровня (и
трек заставки между уровнями), экран не меняется — значит в это время можно
успеть прочитать с диска следующий уровень, чтобы после музыки он появлялся
сразу, а не через секунду-другую загрузки.
**Что УЖЕ сделано и мерено (2026-08-28).** Половина этого уже работает:
трек заставки играет ПОВЕРХ загрузки уровня — так же, как в оригинале
(`seg003:68-108`: `load_intro` возвращает управление, `load_level()` идёт под
музыку, и только потом `while (check_sound_playing())` на чёрном экране).
Насос это переживает: 500 подряд вызовов `pop_sfx_fill` через всю загрузку —
максимальный зазор 245 832 такта при дедлайне 251 000, ни одной пропущенной
порции. Осталась вторая половина: мелодия конца уровня досиживается на
ЖИВОМ экране пройденного уровня (как в оригинале, `play_level_2`), и только
после неё начинается загрузка.
**Оговорки, найденные при разборе.**
1. **Загрузку придётся разрезать надвое.** Сейчас `pop_level_switch` мешает
дисковую работу (файл уровня в EMM-страницу, атласы стража и тайлсета) с
экранно-палитровой (`pop_bg_load` переписывает записи палитры и атласы,
`pop_guard_load` физически применяет цветовые слоты, обе страницы
заливаются, `pop_pal_black`). Вторую половину НЕЛЬЗЯ выполнять, пока на
экране ещё живёт пройденный уровень — иначе палитра поедет прямо на
картинке. То есть в «музыкальное окно» можно вынести только диск, а всё,
что трогает палитру и VRAM, остаётся после.
2. **Бюджет кадра.** Ожидание мелодии идёт в игровом цикле (гейт
`pop_endmus_left` + `pop_music_busy()`), кадр там обычный, 80-100 мс.
Шаг подкачки должен быть такого же размера, как музыкальный — полстраницы
(~16 мс), а не страница целиком; иначе кадры на экране пройденного уровня
начнут дёргаться.
3. **Память.** Страницы следующего уровня придётся держать одновременно со
страницами текущего — проверить EMM-бюджет (memory `sprinter_emm_budget`),
тяжёлые тут не файл уровня, а атласы тайлсета и стража.
4. **Риск невелик, но он в самом хрупком месте.** Палитро-атласный обмен —
источник уже пойманных багов (`PAL-DUNGEON-STALE`, `PAL-L1-AFTER-INTRO`),
и трогать его ради экономии секунды стоит только на спокойную голову.
**Выигрыш.** Одна-две секунды один раз на уровень.
+33
View File
@@ -0,0 +1,33 @@
/*
* _pop_file.h — внутренний файловый слой SprPoP для старых версий DSS.
*
* DSS до исправления относительных путей оставляет рабочий каталог внутри
* последнего подкаталога. POP_PATH_CALL восстанавливает абсолютный каталог
* приложения до и после операции. Это макрос намеренно: код исполняется в
* банке вызывающего, где доступен переданный указатель, и не расходует W1.
*/
#ifndef _POP_FILE_H
#define _POP_FILE_H
#include <stdint.h>
#include <errno.h>
/* BANK10: получить каталог EXE через APPINFO B=1 и сделать его текущим. */
int8_t pop_path_init(void) __banked;
int8_t pop_chdir_home(void) __banked;
/* expression вычисляется только после успешного chdir. errno файловой
* операции сохраняется через завершающее восстановление каталога. */
#define POP_PATH_CALL(result, expression) do { \
if (pop_chdir_home() != 0) { \
(result) = -1; \
} else { \
int _pop_path_errno; \
(result) = (expression); \
_pop_path_errno = errno; \
(void)pop_chdir_home(); \
errno = _pop_path_errno; \
} \
} while (0)
#endif
+2 -1
View File
@@ -12,6 +12,7 @@
#include <string.h>
#include <sprinter_mem.h>
#include "pop_arc.h"
#include "_pop_file.h"
/* Пути 8.3, как требует DSS. Держим их ЗДЕСЬ: строка обязана лежать в том
* же банке, что и код, который её читает (см. pop_arc.h). */
@@ -42,7 +43,7 @@ int8_t pop_arc_open(pop_arc_t *a, const char *path,
a->fd = -1;
a->count = 0;
fd = open(path, O_RDONLY);
POP_PATH_CALL(fd, open(path, O_RDONLY));
if (fd < 0) return -1;
if (read(fd, head, sizeof(head)) != (int)sizeof(head) ||
memcmp(head, "PBA1", 4) != 0 ||
+4 -3
View File
@@ -29,6 +29,7 @@
#include "pop_cheat.h"
#include "pop_sfx.h"
#include "_pop_config.h"
#include "_pop_file.h"
#define CFG_BASE 0x100
@@ -151,7 +152,7 @@ static uint8_t cfg_read_file(uint8_t *buf, uint16_t *size)
int fd, got;
long n;
fd = open("POP.CFG", O_RDONLY);
POP_PATH_CALL(fd, open("POP.CFG", O_RDONLY));
if (fd < 0) return 0;
n = lseek(fd, 0, SEEK_END);
if (n < POP_CFG_MIN_SIZE || n > POP_CFG_MAX_SIZE ||
@@ -172,7 +173,7 @@ int8_t pop_cfg_load(void) __banked
if (!cfg_read_file(buf, &size) || !pop_cfg_decode(buf, size)) return 0;
if (buf[9] != POP_PROFILE_VANILLA)
printf("POP.CFG: ENHANCED profile -> VANILLA\n");
puts("POP.CFG: ENHANCED profile -> VANILLA\n");
return 1;
}
@@ -192,7 +193,7 @@ int8_t pop_cfg_save(void) __banked
gfx_w0_unmap();
/* Не unlink: bank_save_file сам делает O_TRUNC и не создаёт лишнего
* окна, когда POP.CFG вообще отсутствует после ошибки unlink. */
rc = bank_save_file(page, CFG_BASE, "POP.CFG", sizeof(buf));
POP_PATH_CALL(rc, bank_save_file(page, CFG_BASE, "POP.CFG", sizeof(buf)));
mem_free_block(blk);
return (rc == sizeof(buf)) ? 1 : 0;
}
+7 -2
View File
@@ -31,6 +31,7 @@
#include "pop_title.h" /* титульная картинка после ввода имени */
#include "pop_music.h" /* подкачка потоковой темы победы */
#include "pop_hof.h"
#include "_pop_file.h"
/* MAX_HOF_COUNT оригинала — шесть строк. */
#define HOF_COUNT 6
@@ -162,7 +163,7 @@ static void hof_load(void)
if (!blk) return;
page = mem_get_page(blk, 0);
gfx_w0_page_prepare(page);
got = bank_load_file(page, HOF_BASE, "POP.HOF", HOF_SIZE);
POP_PATH_CALL(got, bank_load_file(page, HOF_BASE, "POP.HOF", HOF_SIZE));
if (got == HOF_SIZE) {
gfx_w0_map(page);
for (i = 0; i < HOF_SIZE; i++)
@@ -187,7 +188,11 @@ static void hof_save(void)
gfx_w0_unmap();
/* Неудача записи не меняет только что показанную таблицу: следующее
* открытие разумно начнёт с прежнего файла или с пустого списка. */
bank_save_file(page, HOF_BASE, "POP.HOF", HOF_SIZE);
{
int saved;
POP_PATH_CALL(saved,
bank_save_file(page, HOF_BASE, "POP.HOF", HOF_SIZE));
}
mem_free_block(blk);
}
+20 -12
View File
@@ -1687,7 +1687,9 @@ uint8_t pop_intro_show(void) __banked
/* Первая реплика PV читается ЗДЕСЬ, под чёрным экраном и в тишине:
* дальше внутри сцены остаётся только пуск, а следующие две реплики
* подкачиваются по странице за кадр. */
pop_sfx_pause();
/* Глушение снято 2026-08-28: массовое чтение страниц насос не морит
* (замер на загрузке уровня — ни одной пропущенной порции), а трек
* титров может и должен доигрывать. */
(void)pop_music_load(POP_MUS_STORY_2);
pv_result = intro_pv_animated();
if (pv_result > 0) {
@@ -1765,15 +1767,21 @@ uint8_t pop_intro_show(void) __banked
* дальше, а не чтобы досидеть до конца трека. */
static void pre_cut_finish(uint8_t aborted)
{
if (!aborted) {
intro_skip_t tail;
intro_skip_begin(&tail);
while (pop_music_busy() && !intro_skip_requested(&tail))
pop_wait_edge();
/* ТРЕК ПЕРЕЖИВАЕТ СЦЕНУ — как в оригинале. load_intro (seg001:690)
* ничего не ждёт: гасит экран и возвращает управление, а звук играет
* дальше поверх загрузки уровня и первых секунд игры. Мы вместо этого
* ДОСИЖИВАЛИ трек на чёрном экране (27 — 10,7 с против 2,6 с картинки),
* то есть дарили игроку восемь секунд пустого экрана, и только потом
* глушили. ЗАМЕР 2026-08-28: снято вместе с глушением на загрузке
* уровня; если загрузка не укладывается в дедлайн насоса (251 000
* тактов), вернуть оба места разом.
*
* Пропуск сцены по-прежнему обрывает и музыку: игрок нажал клавишу,
* чтобы идти дальше, а не чтобы дослушать. */
if (aborted) {
pop_music_free();
pop_sfx_pause();
}
pop_music_free();
pop_sfx_pause();
intro_restore_game_palette();
}
@@ -1781,7 +1789,7 @@ void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked
{
const pop_cs_cmd_t *script = 0;
pop_sfx_pause();
/* Глушение перед чтением трека снято 2026-08-28 (см. замер насоса). */
/* МУЗЫКА ПЕРЕЖИВАЕТ СЦЕНУ. Треки заставок между уровнями длиннее самих
* сцен (27 — 10,7 с против 2,6 с картинки), и оригинал их не обрывает:
* load_intro просто гасит экран и возвращает управление, а звук
@@ -1849,7 +1857,7 @@ int pop_intro_hof_bg(uint8_t page) __banked
void pop_time_expired_show(void) __banked
{
pop_sfx_pause();
/* Глушение снято 2026-08-28 (см. замер насоса). */
/* time_expired (seg001:04D3) начинается с play_sound(sound_36). */
if (pop_music_load(36) == 0) {
(void)pop_sfx_start();
@@ -1878,7 +1886,7 @@ void pop_time_expired_show(void) __banked
void pop_ending_show(void) __banked
{
pop_sfx_pause();
/* Глушение снято 2026-08-28 (см. замер насоса). */
/* Экран гасим ПЕРВЫМ действием — как load_intro оригинала, который
* начинается с draw_rect(&screen_rect, color_0_black). Иначе загрузка
* ресурсов сцены идёт поверх ЖИВОЙ комнаты уровня 14, и она «мелькает»
+3 -1
View File
@@ -18,6 +18,7 @@
#include "pop_guard.h" /* pop_guard_vflip_load — зеркала соперника */
#include "pop_arc.h"
#include "kid_arc.h"
#include "_pop_file.h"
/* Серии в архиве обязаны лежать подряд — так их кладёт список в Makefile,
* и только поэтому индекс считается сложением. Если порядок собьётся,
@@ -75,7 +76,8 @@ int pop_kid_data_load(const char *path) __banked
blk=mem_alloc_pages(1);
if (!blk) return -1;
page=mem_get_page(blk,0);
n=bank_load_file(page,KD_DATA_OFF,path,16384-KD_DATA_OFF);
POP_PATH_CALL(n, bank_load_file(page,KD_DATA_OFF,path,
16384-KD_DATA_OFF));
if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
mem_free_block(blk); return -1;
}
+1 -1
View File
@@ -45,7 +45,7 @@ extern uint8_t pop_current_level;
void pop_level_qsave(pop_qs_io_t *io);
/* Загрузить уровень НОМЕРОМ: освободить страницу прошлого, взять
* `LEVELS\res20NN.bin` (fallback `a:\res20NN.bin`) и выставить
* `LEVELS\res20NN.bin` относительно каталога EXE и выставить
* pop_current_level. 0 — OK, -1 — файла нет / не влез (тогда
* pop_current_level и страница НЕ тронуты — играем дальше на старом).
* Порт связки load_lev_spr + load_level (seg000:1098/1169). */
+6 -5
View File
@@ -17,6 +17,7 @@
#include "pop_level.h"
#include "_pop_level.h"
#include "pop_geom.h" /* pop_x_bump / FIRST_ONSCREEN_COLUMN — pos_guards */
#include "_pop_file.h"
/* Низкоуровневое чтение через W3 выполняет резидентный bank_load_file();
* остальная одноразовая подготовка уровня безопасно остаётся в банке 8. */
@@ -58,7 +59,8 @@ static int level_read_file(const char *path)
blk=mem_alloc_pages(1);
if (!blk) return -1;
page=mem_get_page(blk,0);
n=bank_load_file(page,LVL_DATA_OFF,path,16384-LVL_DATA_OFF);
POP_PATH_CALL(n, bank_load_file(page,LVL_DATA_OFF,path,
16384-LVL_DATA_OFF));
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
gfx_w0_page_prepare(page);
gfx_w0_map(page);
@@ -80,18 +82,17 @@ int pop_level_load_num(uint8_t n) __banked
/* Имена патчим на месте, а не через sprintf: printf-семейство тянет в
* резидент W1 форматтер, которого здесь больше негде применить. */
static char pri[] = "LEVELS\\res2000.bin"; /* цифры — [12],[13] */
static char alt[] = "a:\\res2000.bin"; /* цифры — [8],[9] */
uint8_t saved_blk = pop_lvl_blk, saved_page = pop_lvl_page, saved_ok = pop_lvl_ok;
if (n > POP_LEVEL_LAST) return -1;
pri[12] = alt[8] = (char)('0' + n / 10);
pri[13] = alt[9] = (char)('0' + n % 10);
pri[12] = (char)('0' + n / 10);
pri[13] = (char)('0' + n % 10);
/* Старую страницу отпускаем ТОЛЬКО после успешной загрузки новой:
* level_read_file сам аллоцирует страницу, и если файла нет — остаёмся
* на текущем уровне вместо падения в пустой уровень. */
pop_lvl_ok = 0; /* чтобы level_load_path не мешался */
if (level_read_file(pri) != 0 && level_read_file(alt) != 0) {
if (level_read_file(pri) != 0) {
pop_lvl_blk = saved_blk; pop_lvl_page = saved_page; pop_lvl_ok = saved_ok;
return -1;
}
+3 -2
View File
@@ -17,6 +17,7 @@
#include "pop_music_tbl.h"
#include "pop_sfx.h"
#include "pop_app.h" /* POP_APP_DEMO — в демке музыки нет */
#include "_pop_file.h"
uint8_t pop_mus_page[POP_MUS_PAGES];
@@ -141,7 +142,7 @@ int8_t pop_music_load_begin(uint8_t id) __banked
/* Трек лежит ОДНИМ файлом, и fd остаётся открытым до последней
* страницы: `open` стоит дороже самого чтения (51,4 мс против 32,6 мс
* на 16 КБ), и платить его на каждую страницу — чистые потери. */
ld_fd = open(name, O_RDONLY);
POP_PATH_CALL(ld_fd, open(name, O_RDONLY));
if (ld_fd < 0) return -1;
slot_blk[n] = mem_alloc_pages(pages);
if (!slot_blk[n]) { close(ld_fd); return -1; }
@@ -288,7 +289,7 @@ int8_t pop_music_ring_start(uint8_t id) __banked
pop_music_free(); /* обычные слоты кольцу не нужны */
pop_music_ring_stop();
mus_name(name, id);
ring_fd = open(name, O_RDONLY);
POP_PATH_CALL(ring_fd, open(name, O_RDONLY));
if (ring_fd < 0) return -1;
ring_fd_open = 1;
ring_blk = mem_alloc_pages(RING_PAGES);
+8 -6
View File
@@ -14,10 +14,12 @@
#include "pop_guard.h"
#include "pop_pal.h"
#include "pop_ui.h" /* POP_COL_OUTSIDE */
#include "_pop_file.h"
static void pal_file_load(const char *path, const char *fallback)
static void pal_file_load(const char *path)
{
if (gfx_pal_fload(0, path) < 0) gfx_pal_fload(0, fallback);
int rc;
POP_PATH_CALL(rc, gfx_pal_fload(0, path));
gfx_pal_sync();
pop_ui_palette_snapshot();
}
@@ -84,7 +86,7 @@ void pop_pal_menu_dim(uint8_t fade) __banked
void pop_pal_game_load(void) __banked
{
pal_file_load("KID\\kid.pal", "a:\\kid.pal");
pal_file_load("KID\\kid.pal");
/* kid.pal содержит все 256 записей и намеренно затирает эти три
* динамических диапазона (тайлсет, Тень, набор соперника).
* Восстановить их надо ДО snapshot. */
@@ -110,7 +112,7 @@ void pop_pal_level_load(uint8_t full) __banked
void pop_pal_title_load(void) __banked
{
pal_file_load("TITLE\\title.pal", "a:\\title.pal");
pal_file_load("TITLE\\title.pal");
}
/* ЕДИНСТВЕННОЕ ОТЛИЧИЕ ФИНАЛА ОТ ИНТРО — цвет фона текстовой рамки.
@@ -120,7 +122,7 @@ void pop_pal_title_load(void) __banked
* файла и до нового снимка — как тайлсет/Тень в pop_pal_game_load. */
void pop_pal_story_load(uint8_t ending) __banked
{
pal_file_load("PV\\story.pal", "a:\\story.pal");
pal_file_load("PV\\story.pal");
if (!ending) return;
gfx_pal_set(0, POP_PAL_STORY_BG, 0x80, 0, 0);
gfx_pal_set(1, POP_PAL_STORY_BG, 0x80, 0, 0);
@@ -129,5 +131,5 @@ void pop_pal_story_load(uint8_t ending) __banked
void pop_pal_pv_load(void) __banked
{
pal_file_load("PV\\pv.pal", "a:\\pv.pal");
pal_file_load("PV\\pv.pal");
}
+52
View File
@@ -0,0 +1,52 @@
/*
* pop_path_bank.c — каталог приложения и восстановление cwd, BANK10.
*
* APPINFO B=1 возвращает абсолютный каталог запущенного EXE вместе с диском.
* Получаем его один раз при старте. Буфер объявлен const намеренно: у
* Sprinter банковые страницы — RAM, а --constseg кладёт объект в BANK10,
* не расходуя дефицитную резидентную _DATA в W2. Записывает его только
* ESTEX во время pop_path_init().
*
* ABI: публичные функции __banked; их вызывает POP_PATH_CALL из банка
* файлового клиента. После возврата трамплин восстанавливает прежнюю W3,
* поэтому переданный самой файловой операции указатель снова доступен.
*/
#include <stdint.h>
#include <unistd.h>
#include "_pop_file.h"
static const char pop_app_dir[256] = { 0 };
/* ESTEX APPINFO ($47), B=1: HL=буфер, CF/A=ошибка. */
static int appinfo_dir(const char *buf) __naked
{
(void)buf;
__asm
push ix
ld b, #1
ld c, #0x47
rst #0x10
pop ix
jr c, _paid_err
ld de, #0
ret
_paid_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
int8_t pop_path_init(void) __banked
{
const char *dir = pop_app_dir;
if (appinfo_dir(dir) != 0 || !dir[0]) return -1;
return (int8_t)chdir(dir);
}
int8_t pop_chdir_home(void) __banked
{
return (int8_t)chdir((const char *)pop_app_dir);
}
+17 -10
View File
@@ -22,6 +22,7 @@
#include "pop_pace.h"
#include "pop_timer.h"
#include "sprpop_cold.h"
#include "_pop_file.h"
#define QS_VERSION 4
#define QS_SAVE 1
@@ -154,6 +155,7 @@ static int8_t qs_save(uint8_t page)
uint8_t tail[4];
const uint8_t h[7]={'P','O','P','Q',QS_VERSION,pop_current_level,0};
uint16_t size;
int rc;
qs_init(&io,page,QS_LIMIT,0,0);
pop_qs_raw_put(&io,h,sizeof(h));
qs_payload(&io);
@@ -162,14 +164,18 @@ static int8_t qs_save(uint8_t page)
tail[2]=(uint8_t)io.checksum; tail[3]=(uint8_t)(io.checksum >> 8);
pop_qs_raw_put(&io,tail,4); size=(uint16_t)(io.pos-QS_BASE);
if (io.failed) return -1;
unlink("POP.NEW");
if (bank_save_file(page,QS_BASE,"POP.NEW",size) != size) {
unlink("POP.NEW"); return -1;
POP_PATH_CALL(rc, unlink("POP.NEW"));
POP_PATH_CALL(rc, bank_save_file(page,QS_BASE,"POP.NEW",size));
if (rc != size) {
POP_PATH_CALL(rc, unlink("POP.NEW"));
return -1;
}
unlink("POP.BAK");
(void)rename("POP.SAV", "POP.BAK");
if (rename("POP.NEW", "POP.SAV") != 0) {
(void)rename("POP.BAK", "POP.SAV"); return -1;
POP_PATH_CALL(rc, unlink("POP.BAK"));
POP_PATH_CALL(rc, rename("POP.SAV", "POP.BAK"));
POP_PATH_CALL(rc, rename("POP.NEW", "POP.SAV"));
if (rc != 0) {
POP_PATH_CALL(rc, rename("POP.BAK", "POP.SAV"));
return -1;
}
return 1;
}
@@ -178,7 +184,8 @@ static int8_t qs_load_one(const char *name, uint8_t page)
{
uint8_t level;
uint16_t payload_size;
int size=bank_load_file(page,QS_BASE,name,QS_LIMIT);
int size;
POP_PATH_CALL(size, bank_load_file(page,QS_BASE,name,QS_LIMIT));
if (size <= 0 || !qs_validate_page(page,(uint16_t)size,&level,
&payload_size)) return -1;
if (level != pop_current_level) {
@@ -205,11 +212,11 @@ uint8_t pop_qsave_probe(void) __banked
page = mem_get_page(blk, 0);
gfx_w0_page_prepare(page);
size = bank_load_file(page, QS_BASE, "POP.SAV", QS_LIMIT);
POP_PATH_CALL(size, bank_load_file(page, QS_BASE, "POP.SAV", QS_LIMIT));
if (size > 0 && qs_validate_page(page, (uint16_t)size, &level,
&payload_size))
slots |= POP_QS_SLOT_SAVE;
size = bank_load_file(page, QS_BASE, "POP.BAK", QS_LIMIT);
POP_PATH_CALL(size, bank_load_file(page, QS_BASE, "POP.BAK", QS_LIMIT));
if (size > 0 && qs_validate_page(page, (uint16_t)size, &level,
&payload_size))
slots |= POP_QS_SLOT_BACKUP;
+3 -1
View File
@@ -31,6 +31,7 @@
#include "pop_kid.h" /* Kid.curr_col/curr_row — порядок оверлеев */
#include "pop_cdraw.h" /* pop_cd — габарит кадра Кида (y-порядок плиты) */
#include "pop_map.h" /* pop_upside — переворот (зелье инверсии) */
#include "_pop_file.h"
#include <fcntl.h>
#include <unistd.h>
@@ -83,7 +84,8 @@ static const char *bg_path(const char *name)
static int bg_load_tile_pal(const char *name)
{
uint8_t buf[64];
int fd = open(bg_path(name), O_RDONLY);
int fd;
POP_PATH_CALL(fd, open(bg_path(name), O_RDONLY));
if (fd < 0) return -1;
if (read(fd, buf, 64) != 64) { close(fd); return -1; }
gfx_pal_load(0, POP_PAL_ENV, 16, buf);
+11 -4
View File
@@ -17,6 +17,7 @@
#include "pop_ui.h"
#include "pop_status.h"
#include "pop_music.h" /* pop_music_service — подкачка кольца в долгих фейдах */
#include "_pop_file.h"
typedef struct {
uint8_t base;
@@ -54,8 +55,10 @@ static uint8_t text_index(const pop_font_desc_t *font, uint8_t ch)
int pop_ui_init(void) __banked
{
int rc;
if (font_ready) return 0;
if (atlas_load(&font_at, "FONT\\font.atl") != 0) return -1;
POP_PATH_CALL(rc, atlas_load(&font_at, "FONT\\font.atl"));
if (rc != 0) return -1;
font_ready = 1;
return 0;
}
@@ -242,8 +245,10 @@ void pop_ui_palette_black(void) __banked
uint8_t *black = pop_pal_buf;
/* BIOS принимает максимум 64 записи за вызов; одним буфером покрываем
* все четыре четверти палитры, не занимая постоянную память. */
for (i = 0; i < sizeof(black); i++) black[i] = 0;
* все четыре четверти палитры, не занимая постоянную память.
* sizeof(black) здесь нельзя: black 16-битный указатель, и тогда
* обнулятся лишь первые два байта, а цвет заливки останется случайным. */
for (i = 0; i < sizeof(pop_pal_buf); i++) black[i] = 0;
for (chunk = 0; chunk < 4; chunk++) {
uint8_t start = (uint8_t)(chunk << 6);
gfx_pal_load(0, start, 64, black);
@@ -468,8 +473,10 @@ int pop_screen_blit_atlas_strips(const char * const *parts,
gfx_set_bank(GFX_BANK_NORMAL);
for (part = 0; part < count; part++) {
atlas_t at;
int rc;
if (atlas_load(&at, parts[part]) != 0) return -1;
POP_PATH_CALL(rc, atlas_load(&at, parts[part]));
if (rc != 0) return -1;
strip_blit(&at, x, &y);
}
return 0;
-9
View File
@@ -40,15 +40,6 @@ int pop_vflip_take(const atlas_t *orig, const atlas_t *mirror)
return 0;
}
int pop_vflip_add(const atlas_t *orig, const char *path)
{
atlas_t m;
if (!orig || vf_n >= VF_MAX) return -1;
if (atlas_load(&m, path) != 0) return -1;
return pop_vflip_take(orig, &m);
}
uint8_t pop_vflip_page(uint8_t src)
{
uint8_t i;
+1 -5
View File
@@ -26,11 +26,7 @@
#include <stdint.h>
#include <sprite.h>
/* Загрузить зеркальный файл path и связать его с атласом orig.
* 0 OK, -1 файла нет / не хватило EMM / таблица полна. */
int pop_vflip_add(const atlas_t *orig, const char *path);
/* То же, но зеркало УЖЕ загружено (например, прочитано из архива): таблица
/* Зеркало УЖЕ загружено из архива: таблица
* забирает его себе, и дальше он освобождается вместе с остальными. */
int pop_vflip_take(const atlas_t *orig, const atlas_t *mirror);
+9
View File
@@ -41,6 +41,10 @@
#include "pop_pal.h" /* lifecycle палитры title/intro/level */
#include "pop_hof.h" /* persistent Hall of Fame — bank 9 */
#include "pop_ui.h" /* чёрные обе страницы перед сменой палитры */
#include "_pop_file.h" /* cwd приложения для старых DSS */
#include <sprinter_exit.h>
#include <unistd.h>
__sfr __at (0xFE) io_border;
@@ -260,6 +264,11 @@ int main(void)
uint8_t xdec_prev = 0, xinc_prev = 0, upsd_prev = 0;
uint8_t failed = 0;
/* До первой файловой операции запоминаем абсолютный каталог EXE.
* Без этого старый DSS теряет последний компонент cwd после OPEN пути
* с подкаталогом и следующий файл ищет уровнем выше. */
if (pop_path_init() != 0) return 1;
/* Загрузка ресурсов, палитра, первый экран — В БАНКЕ (sprpop_cold.c):
* отрабатывает ровно один раз за игру, а кода там на полкилобайта. */
pop_app_reset();
+40 -3
View File
@@ -25,6 +25,7 @@
#include "pop_ctrl.h" /* pop_ctrl_init/close — ввод поднимает и закрывает boot */
#include "pop_shadow.h" /* запечённый набор Тени */
#include "pop_sfx.h" /* звуковые эффекты через CBL */
#include "pop_music.h" /* pop_music_busy/free — ждём трек заставки */
#include "pop_vflip.h" /* сброс зеркальных страниц на смене уровня */
#include "pop_cheat.h" /* pop_cheats — режим разработки включает pop_boot */
#include "pop_pace.h" /* pop_speed_mode — режим скорости в отладочной метке */
@@ -226,7 +227,12 @@ int8_t pop_level_change(uint8_t back) __banked
* почти всегда поймано пользователем 2026-08-25. Ветка с
* катсценой свободна от этого: pop_pre_cutscene_show глушит
* звук первым действием. */
pop_sfx_pause();
/* Глушение снято 2026-08-28 (замер: загрузка уровня насос не
* морит 500 вызовов подряд, максимум 245 832 такта при
* дедлайне 251 000). Прежнее обоснование скрежет на палитре
* через BIOS ($A4 с запрещёнными прерываниями), пойманный на
* переходе без катсцены 2026-08-25. Если он вернётся, это
* единственное место, куда его надо возвращать. */
/* Без fade: старый кадр гасим сразу и обязательно даём
* показать один чёрный растр ДО диска/перерисовки. */
pop_pal_apply(POP_PAL_DIM_BLACK);
@@ -1145,7 +1151,7 @@ int pop_boot(void) __banked
/* pop_cheats/speed применит pop_settings_apply после pop_sfx_init. */
pop_guard_reset();
if (pop_kid_data_load("KID\\kid.ani") != 0) { /* кадры+seqtbl в EMM-странице */
printf("kid.ani load failed\n");
puts("kid.ani load failed\n");
return -1;
}
pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */
@@ -1227,7 +1233,10 @@ int pop_level_switch(void) __banked
* дальше, а урон нет (do_startpos дольёт до потолка). */
hitp_beg_lev = hitp_max;
pop_checkpoint = 0; /* уровень пройден (seg003:413) */
pop_sfx_pause(); /* загрузка уровня — ESTEX надолго, звук глушим */
/* ЗАМЕР 2026-08-28: глушение снято ВРЕМЕННО, чтобы измерить
* зазоры насоса на загрузке уровня (дедлайн 251 000 тактов).
* Если зазоры укладываются снятие остаётся, и трек заставки
* доигрывает поверх загрузки, как в оригинале (seg001:690). */
if (pop_level_load_num(pop_next_level) != 0) {
pop_next_level = pop_current_level; /* файла нет — остаёмся */
} else {
@@ -1294,6 +1303,34 @@ int pop_level_switch(void) __banked
/* Атомарный commit на границе растра: впервые показать
* готовую комнату уже с окончательной палитрой уровня.
* Вторую страницу синхронизировать аппаратной копией. */
/* ЖДЁМ КОНЦА ТРЕКА ЗАСТАВКИ НА ЧЁРНОМ ЭКРАНЕ — порт
* seg003:107:
* while (check_sound_playing() && !do_paused()) idle();
* stop_sounds();
* draw_level_first();
* Порядок оригинала важен: загрузка уровня идёт ПОД
* звучащий трек, и только потом чернота до его конца. Тогда
* общее время черноты = max(трек, загрузка), а не их сумма,
* и трек не обрывается на полуслове. Раньше у нас было
* наоборот: pre_cut_finish досиживал трек, потом глушил
* насос и грузил уровень.
*
* ЗАМЕР 2026-08-28: 500 подряд вызовов насоса через всю
* загрузку максимальный зазор 245 832 такта при дедлайне
* 251 000, ни одной пропущенной порции. Поэтому глушение на
* загрузке снято и музыка её переживает.
*
* Счётчик страховка от вечного ожидания (потоковый трек,
* который никогда не кончится): ~20 с и идём дальше. */
{
uint16_t mus_guard = 1000;
while (pop_music_busy() && mus_guard) {
mus_guard--;
pop_wait_edge();
}
pop_music_free(); /* порт stop_sounds() */
}
pop_wait_edge();
gfx_set_visible_page(level_prepared_page);
gfx_set_draw_page(old_page);
+92
View File
@@ -0,0 +1,92 @@
#!/bin/bash
# make_hdd.sh OUT.chd FILE1 [FILE2 ...]
#
# Собирает 32 МБ RAW-образ жёсткого диска (MBR + один раздел FAT16) с
# указанными файлами в корне и конвертит его в CHD для MAME (монтируется
# как -hard2). Быстрая альтернатива floppy (mc.img): MAME эмулирует FDD
# с реальной черепашьей скоростью, HDD — в разы быстрее.
#
# Требует: mtools (mpartition/mformat/mcopy) + chdman (из MAME).
# Sprinter DSS штатно монтирует стандартный MBR+FAT16 (подтверждено).
#
# Использование:
# tools/make_hdd.sh mame/v306/IMG/test_hdd.chd prog.exe data1 data2 ...
# tools/make_hdd.sh out.chd prog.exe KID:kid0.atl GUARD:g0.atl ...
# — аргумент вида КАТАЛОГ:файл кладёт файл в подкаталог образа
# (каталог создаётся); без префикса файл идёт в корень.
set -e
if [ $# -lt 2 ]; then
echo "usage: $0 OUT.chd FILE1 [FILE2 ...]" >&2
exit 1
fi
OUT_CHD="$1"; shift
# Геометрия: 128*16*32*512 = 32 МБ. BPS=512.
CYLS=128; HEADS=16; SECS=32
SECTORS=$((CYLS * HEADS * SECS)) # 65536
# macOS mktemp требует X'ы в КОНЦЕ шаблона.
RAW=$(mktemp /tmp/sphdd.raw.XXXXXX)
MTC=$(mktemp /tmp/sphdd.mtc.XXXXXX)
trap 'rm -f "$RAW" "$MTC"' EXIT
dd if=/dev/zero of="$RAW" bs=512 count=$SECTORS status=none
# mtools-диск z: = раздел 1 нашего RAW-образа.
printf 'drive z: file="%s" partition=1\n' "$RAW" > "$MTC"
export MTOOLSRC="$MTC" MTOOLS_SKIP_CHECK=1
# mtools собран с enable-raw-term и, когда ему нужно что-то спросить,
# открывает УПРАВЛЯЮЩИЙ ТЕРМИНАЛ напрямую (/dev/tty) — в обход stdin.
# Поэтому ни `< /dev/null`, ни перенаправление stdio не спасают: из
# терминала сборка молча вставала навсегда на `mmd z:/BG` (подтверждено
# lsof: fd 0 = /dev/null, fd 4 = /dev/tty, процесс спит). Лечится только
# отсутствием управляющего терминала — запускаем mtools в новой сессии,
# тогда открыть /dev/tty нечего и mtools идёт неинтерактивным путём.
#
# setsid(1) в macOS нет, поэтому делаем то же самое питоном.
# Каналов, откуда mtools может ждать ответ, ДВА, и закрывать надо оба:
# /dev/tty — убирается новой сессией (os.setsid);
# stdin — убирается перенаправлением из /dev/null.
# Закрыть только один недостаточно: с одним лишь setsid mmd переключался на
# stdin и вис уже на нём (lsof: fd 0 = /dev/ttys002).
mt() {
python3 -c 'import os,sys; os.setsid(); os.execvp(sys.argv[1], sys.argv[1:])' \
"$@" < /dev/null
}
# MBR + раздел 1 на весь диск (тип FAT16), затем формат FAT16.
echo "--- разметка и формат образа ---"
mt mpartition -I -c -t $CYLS -h $HEADS -s $SECS z:
mt mformat z:
echo "--- копирование файлов ---"
# Аргумент может быть либо файлом (кладём в корень), либо парой
# КАТАЛОГ:файл — тогда каталог создаётся на образе и файл кладётся туда.
# Имена на Sprinter/DSS — 8.3, каталоги тоже (KID, GUARD, BG, LEVELS...).
mt mmd "z:/games" >/dev/null 2>&1 || true
mt mmd "z:/games/sprpop" >/dev/null 2>&1 || true
for f in "$@"; do
case "$f" in
*:*) dir="${f%%:*}"; src="${f#*:}"
# Каталог может уже быть — это не ошибка; stderr НЕ прячем, иначе
# непрошедший вопрос mtools снова окажется невидимым.
mt mmd "z:/games/sprpop/$dir" >/dev/null 2>&1 || true
echo "COPY $src to z:/games/sprpop/$dir/"
mt mcopy -o "$src" "z:/games/sprpop/$dir/" ;;
*) mt mcopy -o "$f" "z:/games/sprpop/" ;;
esac
done
echo "--- содержимое образа ---"
mt mdir -/ z:
# RAW -> CHD (без сжатия = быстро; та же геометрия).
echo "--- конвертация RAW -> CHD ---"
chdman createhd -i "$RAW" -o "$OUT_CHD" -c none \
-chs $CYLS,$HEADS,$SECS -ss 512 -f
echo "готово: $OUT_CHD"