63bfd997a9
Четыре правки прогона 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
109 lines
8.6 KiB
C
109 lines
8.6 KiB
C
/*
|
||
* 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
|