/* * 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 /* Режимы скорости. Растровый кадр 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