SprPoP: автономное приложение, выделенное из roomtest
Порт PoP переехал в applications/SprPoP — приложение, которое собирается само: код, оригинальные данные, конверторы ресурсов и сборка внутри одной папки. Наружу знает единственный путь — корень тулчейна (SPRINTER_ROOT, по умолчанию ../..). applications/PoP/roomtest ЗАМОРОЖЕНА и остаётся архивом закрытых задач, багов и исполненных планов. Скопировано из applications/PoP/roomtest@4b74478. Перенос проверен побайтово: собранный sprpop.exe совпал с roomtest.exe того же коммита, все 39 дисковых ресурсов и все 16 генерируемых заголовков — тоже, host- тесты зелёные (15/15). Раскладка: src/ рукописный C (roomtest.c -> sprpop.c) gen/ генерируемые заголовки, в репозитории assets/orig/ оригинальные данные игры, вне репозитория (копирайт) assets/packed/ то, что ложится на диск, в раскладке диска tools/ конверторы; все пути — в одном tools/paths.py build/ выход: exe, каталоги ресурсов, hdd/, промежуточные atl/ Сборка ресурсов: assets/packed и gen — версионируемые ВХОДЫ, а не то, что пересчитывается каждым make. Автоматика построена на ОТСУТСТВИИ файла, а не на таймстемпах: git не хранит времена, и в свежем клоне сравнение по времени превращалось бы в лотерею. Недостающий ресурс или заголовок чинится сам, рекурсивным вызовом в ветку генерации. Музыка собирается из любого из четырёх наборов записей (make music-mp3, music-mt32, ...); набор входит в имя stamp'а, поэтому смена набора сама делает музыку устаревшей. Длины реплик больше не захардкожены: упаковщик печатает их в gen/pop_music_ticks.h, и шкала сцены выражена через них — иначе mt32 (реплики на 6% длиннее) молча ломал катсцену. Тулчейн: в app.mk два обратносовместимых крючка (SRC_DIR/BUILD_DIR), HDD_IMG стал ?=; команда сборки roomtest не изменилась. Корневой make host-tests переключён на SprPoP. Подгонка тайминга катсцены с принцессой (PV_MAGIC_LEAD): сцена render-bound и идёт ~49 тиков/с вместо 60, из-за чего кода реплики приходила раньше молнии. Это обход, а не лечение; разбор с замерами — docs/BUGS_OPEN.md, записи SND-PACE-DEAD, PV-RENDER-BOUND, MUS-LEFT-TEAR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* pop_pace.h — фиксированный логический кадр (frame pacing) по ЛУЧУ.
|
||||
*
|
||||
* Задача: период логического кадра = max(n, ceil(W)) вместо нынешнего
|
||||
* ceil(W) + 2, где W — работа в растровых кадрах. Сегодня главный цикл
|
||||
* ждёт три gfx_wait_vsync ПОСЛЕ отрисовки, поэтому реальный бюджет кадра
|
||||
* — один растр (430 080 тактов), и превышение его на такт стоит целого
|
||||
* лишнего растра. При n=3 бюджет становится 1 290 240.
|
||||
*
|
||||
* ПОЧЕМУ НЕ СЧЁТЧИК КАДРОВЫХ ПРЕРЫВАНИЙ (замеры 2026-08-19, MAME, полный
|
||||
* разбор в docs/frame_pacing_plan.md). Импульс запроса кадрового
|
||||
* прерывания живёт 32 такта неразогнанного клока = 9,14 мкс, а ядра
|
||||
* акселератора держат di на весь чанк блита (~0,29 мс). Импульс,
|
||||
* попавший в такое окно, теряется насовсем — ядро z80 уровневое, защёлки
|
||||
* нет (то же и на настоящем Spectrum). Замерено: в сцене 11/15 теряется
|
||||
* от 0 до 2,8 % прерываний в зависимости от ФАЗЫ рендера относительно
|
||||
* луча, а на полной перерисовке комнаты — три подряд. Сейчас фаза
|
||||
* плавает (период то 3 растра, то 4) и потери размазаны; при жёстком
|
||||
* пейсинге фаза застынет, и сцена может залипнуть в плохой — это ровные
|
||||
* −25 % скорости, невидимые в профиле тактов.
|
||||
*
|
||||
* ЧТО ВМЕСТО. Бит 5 порта 0xFE — это ЧТЕНИЕ ПОЛОЖЕНИЯ ЛУЧА (1, когда
|
||||
* vpos >= 272; MAME sprinter.cpp kbd_fe_r), а не событие: ни импульса,
|
||||
* ни очереди, ни защёлки. Такой сигнал невозможно потерять — можно
|
||||
* только не посмотреть. Считаем фронты 1→0 (это vpos = 0, ровно тот же
|
||||
* момент, которого ждёт gfx_wait_vsync) программно, выборкой из точек,
|
||||
* которые код и так проходит.
|
||||
*
|
||||
* УСЛОВИЕ ТОЧНОСТИ — ОДНО: между соседними выборками должно проходить
|
||||
* меньше 64 512 тактов (длина окна «бит 5 = 1»: 48 строк из 320). Тогда
|
||||
* в каждый бланк попадает хотя бы одна выборка, а в промежуток между
|
||||
* бланками (365 568 тактов) — тем более; значит каждый фронт засчитан
|
||||
* ровно один раз. Двойной счёт невозможен: инкремент только на переходе,
|
||||
* и beam_prev тут же обновляется. Условие не про нагрузку, не про di,
|
||||
* не про прерывания и не про звук — только про зазор.
|
||||
*
|
||||
* Запас: самый длинный неделимый кусок без выборки — одно di-окно, то
|
||||
* есть один вызов _bgi_blit_rows_raw, а он по контракту режется на чанки
|
||||
* ≤16 строк. Даже если считать грубо — одна выборка на целый блит, а
|
||||
* самый дорогой замеренный блит 32 073 такта — зазор вдвое меньше окна.
|
||||
*
|
||||
* ВАЖНО: бит 5 читается осмысленно ТОЛЬКО при включённом cbl_mode (в
|
||||
* MAME `data &= ~0xa0` стоит внутри `if (cbl_mode())`, иначе бит выставлен
|
||||
* в 1 намертво). Его лениво взводит сам gfx_wait_vsync через
|
||||
* _cbl_port_ref, поэтому pop_pace_arm обязан один раз позвать
|
||||
* gfx_wait_vsync и ПРОВЕРИТЬ, что фронты реально идут.
|
||||
*/
|
||||
#ifndef POP_PACE_H
|
||||
#define POP_PACE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Режимы скорости. Растровый кадр Sprinter — 20,48 мс (48,83 Гц).
|
||||
* Оригинал (SDLPoP/src/seg003.c:363): base_speed 5 тиков при 60 Гц =
|
||||
* 83,3 мс, fight_speed 6 = 100 мс. Условие боя у оригинала одно и
|
||||
* буквальное — «у Кида ВЫНУТ МЕЧ» (Kid.sword == sword_2_drawn), не
|
||||
* «идёт бой» и не «рядом страж»; проверяется в верху главного цикла. */
|
||||
/* Нумерация НЕ произвольная: 0 обязан быть дефолтом (crt0 зануляет _DATA),
|
||||
* а обход по кругу инкрементом даёт ровно NORMAL -> FAST -> FASTEST.
|
||||
* Заодно номер режима = число палочек в отладочной метке минус один. */
|
||||
#define POP_SPEED_NORMAL 0 /* 4/5 — 81,9 / 102,4 мс; повтор оригинала */
|
||||
#define POP_SPEED_FAST 1 /* 3/4 — 61,4 / 81,9 мс; бой как в оригинале */
|
||||
#define POP_SPEED_FASTEST 2 /* 3/3 — 61,4 мс; не успели — подтормаживаем */
|
||||
#define POP_SPEED_MODES 3
|
||||
|
||||
extern uint8_t pop_speed_mode; /* POP_SPEED_*; дефолт — NORMAL */
|
||||
|
||||
/* Клавиша смены режима по кругу (PS/2 set 2). P — свободна: заняты
|
||||
* K/I/S/L/U/[/] (читы, pop_cheat.h) и +/- (обход комнат). */
|
||||
#define KBD_SPEED_MODE 0x4D /* P */
|
||||
|
||||
/* Делитель для текущего режима. fight = «у Кида вынут меч». */
|
||||
uint8_t pop_pace_n(uint8_t fight);
|
||||
|
||||
/* Счётчик кадров. volatile: его правит pop_beam_sample, а читают циклы
|
||||
* ожидания — перечитывать обязаны каждый оборот. */
|
||||
extern volatile uint8_t pop_frame_tick;
|
||||
|
||||
/* ВЫБОРКА ЛУЧА. Дёшево (~57 T с вызовом), не трогает bc/de/ix.
|
||||
* Звать отовсюду, где иначе получился бы зазор длиннее 64 512 тактов:
|
||||
* из обёрток рисования и с границ фаз. Модуль НЕ банковый, поэтому из
|
||||
* банков зовётся прямым call, без трамплина. */
|
||||
void pop_beam_sample(void) __preserves_regs(b, c, d, e);
|
||||
|
||||
/* Однократная подготовка: взвести cbl_mode, снять первое состояние бита
|
||||
* и убедиться, что фронты идут. 0 — луч не работает (тогда пейсинг
|
||||
* молча откатывается на прежнее поведение: n ожиданий gfx_wait_vsync). */
|
||||
uint8_t pop_pace_arm(void);
|
||||
|
||||
/* Ждать ОДИН фронт луча. Внутри — тот же плотный опрос клавиатуры, что
|
||||
* сейчас висит idle-хуком на gfx_wait_vsync (KBD-1: без опроса раз в
|
||||
* ~0,5 мс теряются нажатия и залипают клавиши). */
|
||||
void pop_wait_edge(void);
|
||||
|
||||
/* Добрать до n фронтов ОТ ЯКОРЯ и переставить якорь ПО ФАКТУ.
|
||||
* Вызывающий обязан перед этим сделать хотя бы один pop_wait_edge() —
|
||||
* на нём же и происходит своп страниц (tear-free). Якорь ставится
|
||||
* по факту, а не anchor += n: догонять пропущенное время нельзя, иначе
|
||||
* после тяжёлого кадра игра рванёт вперёд. */
|
||||
void pop_pace_end(uint8_t n);
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user