Files
Sprinter-SDCC/applications/SprPoP/src/pop_pace.h
T
snark13 63bfd997a9 SprPoP: звук мигания, равномерные часы, полоса HP после рестарта, надпись ждёт мелодию
Четыре правки прогона 2026-08-31.  Две ПРОВЕРЕНЫ пользователем в MAME
(звук мигания, порядок «мелодия -> надпись»), две ждут проверки — образ
собран.

ЗВУК МИГАНИЯ (проверено).  Упаковщик научился синтезировать ноты PC-
спикера в обычный сэмпл: заголовок с темпом, тройки «частота + длина»,
меандр на нашей частоте вывода.  Берём из нот ТОЛЬКО номера, которых нет
ни в оцифровке, ни среди мелодий, — иначе синтез перекрыл бы музыку,
которую мы играем из MUS/.  На поставке SDLPoP это ровно один номер: 38,
сигнал под мигание «Press Button»; 31, 34 и 42 там пустые заглушки.
Громкость по слуховой проверке снижена вдвое (44 -> 22): на полном
размахе сигнал перекрикивал игру.  EXE не меняется — раскладка читается с
диска, набор занял те же 9 страниц.

НАДПИСЬ ЖДЁТ МЕЛОДИЮ (проверено).  Порядок оригинала: ветка мёртвого
(seg006:1351) на седьмом шаге выходит, пока звук играет, и «Press Button»
появляется только после музыки смерти.  Чтобы ожидание не было
принудительным, три быстрых пути (Ctrl+A, обе быстрые загрузки, пункты
меню) музыку глушат — оригинал при Ctrl+A делает то же (seg000:0617).
Обычная кнопка во время мелодии не действует: она ответ НА надпись.

ЧАСЫ (ждёт проверки).  Тик стоит столько кадров ЛУЧА, сколько их в кадре
режима NORMAL, поэтому FAST/FASTEST больше не ускоряют время.  Считаем
ФАКТИЧЕСКИ прошедшие кадры луча, а не ожидаемый делитель: логический кадр
не всегда укладывается в бюджет, и часы «по делителю» шли рывками (первый
прогон это показал — «несколько секунд быстро, потом притормаживание»).
Вклад одного вызова ограничен, иначе пауза и меню прыгнули бы вперёд.
Четыре новых теста: NORMAL не сдвинулся ни на тик (и вне боя, и в бою),
FAST и FASTEST держат реальное время.

ПОЛОСА HP ПОСЛЕ Ctrl+A (ждёт проверки).  Корень: счётчик считает
СТРАНИЦЫ, а тратился по КАДРАМ — между двумя вызовами переворота может не
быть, и оба прохода уходили в одну страницу, вторая оставалась с
делениями прошлого боя.  Теперь проход тратится только при смене
gfx_get_draw_page().  Плюс полная чистка всей ширины при инвалидации:
старая полоса могла заходить под статус-текст, где щадящая чистка её не
трогала; текст сразу перезапрашивается.

Все 16 наборов host-тестов зелёные, check_bank_calls чист.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
2026-08-31 23:10:35 +03:00

109 lines
8.6 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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);
/* ЭТАЛОН ХОДА ЧАСОВ — делитель РЕЖИМА NORMAL для того же признака боя.
* Игровое время меряется им, а не фактическим темпом: иначе FAST/FASTEST
* ускоряли бы и часы (минута проходила за две трети минуты). При NORMAL
* фактический делитель равен эталону, поэтому его ход не меняется вовсе —
* включая замедление в бою, которое есть и в оригинале. */
#define POP_PACE_BASE(fight) ((uint8_t)((fight) ? 5 : 4))
/* Счётчик кадров. 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