Фиксированный логический кадр по лучу + режимы FASTEST/FAST/NORMAL

Период стал max(n, ceil(W)) вместо ceil(W)+2: три gfx_wait_vsync после
работы отсчитывались от её КОНЦА, поэтому бюджет кадра был один растр.
Теперь ждём от якоря начала кадра, и при n=3 бюджет 1 290 240 тактов.

Счёт кадров — программный, по биту 5 порта 0xFE (положение луча), а не по
кадровым прерываниям: те теряются в DI-окнах акселератора фазозависимо
(замер: 0..2,8 %, на полной перерисовке три подряд).  Условие точности
одно — зазор между выборками меньше 64 512 тактов; точки выборки
расставлены по замеру, а не на глаз.

Режимы (pop_pace.h), клавиша P по кругу, дефолт FASTEST.  Условие боя
взято у оригинала буквально (SDLPoP seg003.c:363): Kid.sword ==
SWORD_2_DRAWN, а не «идёт бой».

Проверено в MAME на 11/15: счётчик без недосчёта на 270 кадров, период
ровно 3 растра на 302 логических кадрах (ни длиннее 3,1, ни короче 2,9),
NORMAL даёт ровно 4, с вынутым мечом — ровно 5.
This commit is contained in:
2026-08-19 23:06:27 +03:00
parent 61b8d80275
commit a6e39070af
11 changed files with 310 additions and 9 deletions
+57 -7
View File
@@ -23,6 +23,7 @@
#include "pop_cheat.h"
#include "pop_tune.h"
#include "pop_vflip.h" /* зеркальный набор персонажей (зелье инверсии) */
#include "pop_pace.h" /* фиксированный логический кадр по лучу */
#include "roomtest_cold.h" /* холодная половина цикла — в банке */
__sfr __at (0xFE) io_border;
@@ -225,6 +226,7 @@ int main(void)
* по умолчанию (без мерцания); SPACE переключает в однобуфер для
* отладки багов рисования (edge по sp_prev, чтоб не дёргать). */
uint8_t front = 0, back, dbuf = 1, sp_prev = 0, frozen = 0;
uint8_t pace_prev = 0; /* edge клавиши смены режима скорости */
uint8_t skip = 0; /* маска «слот перерисовывать не надо» */
uint16_t dead_frames = 0;
#ifdef ROOMNAV
@@ -243,6 +245,9 @@ int main(void)
return 1;
}
gfx_set_idle_hook(kbd_idle); /* вычерпывать FIFO, пока ждём кадр */
/* Темп кадра по лучу. Если бит 5 порта 0xFE не шевелится (иное
* железо), pop_pace_* молча откатываются на прежние gfx_wait_vsync. */
pop_pace_arm();
while (!kbd_raw_down(KBD_ESC)) {
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
@@ -260,6 +265,18 @@ int main(void)
}
sp_prev = sp;
/* P — переключить режим скорости по кругу (FASTEST -> FAST ->
* NORMAL). Edge, чтоб одно нажатие = один шаг. Дефолт FASTEST
* (crt0 зануляет _DATA). Что означают режимы — pop_pace.h. */
{
uint8_t pk = kbd_raw_down(KBD_SPEED_MODE);
if (pk && !pace_prev) {
uint8_t m = (uint8_t)(pop_speed_mode + 1);
pop_speed_mode = (uint8_t)(m >= POP_SPEED_MODES ? 0 : m);
}
pace_prev = pk;
}
/* Отладочный стоп-кадр: '1' (PS/2 0x16) — заморозить, '2' (0x1E) —
* продолжить. В заморозке пропускаем весь tick/draw/flip: картинка
* стоит, можно спокойно разбирать позу Kid и окклюзию. */
@@ -317,7 +334,10 @@ int main(void)
}
if (frozen) {
PROF(0); /* чёрный: ждём vsync */
gfx_wait_vsync();
/* Заморозка — тоже полноценный логический кадр: иначе якорь
* пейсинга уедет и первый кадр после разморозки будет рваным. */
pop_wait_edge();
pop_pace_end(pop_pace_n((uint8_t)(Kid.sword == SWORD_2_DRAWN)));
continue;
}
@@ -362,14 +382,18 @@ int main(void)
gfx_set_draw_page(back);
}
pop_dbg_m13(); /* ЗАМЕР: ввод и читы разобраны */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */
pop_check_mouse(); /* спецсобытие ур.8: мышь жмёт кнопку */
pop_check_killed_shadow(); /* спецсобытие ур.12: смерть тени = смерть Кида */
pop_frame_timers(); /* timers (seg003:0735): вспышка слияния */
pop_dbg_m9(); /* ЗАМЕР: pop_frame_timers сделан */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
pop_dbg_m14(); /* ЗАМЕР: спецсобытия + луч видимости */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_ctrl_tick(); /* ввод -> control(): смена seq */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
/* Кто в этом кадре не изменился с прошлой отрисовки ЭТОЙ страницы —
* тот на ней уже нарисован правильно: ни стирать, ни рисовать заново
* не надо (DRAW-COST, контракт в pop_cdraw.h). Решение
@@ -381,18 +405,25 @@ int main(void)
* skip-маске он не участвует (рисуется/стирается безусловно, живёт
* только пока Кид стоит на тайле зеркала). */
pop_mirror_heal();
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
PROF(2); /* логика (тот же синий) */
pop_dbg_m1(); /* ЗАМЕР: начало логики */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
kid_tick(); /* play_seq: следующий кадр */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_phys_tick(); /* K3: падение/приземление/стена */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_guard_tick(); /* логика стража — В БАНКЕ (guards.c) */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_guard_phys_tick(); /* и его физика — та же, что у Кида (play_guard_frame) */
/* Боёвка, порядок как в play_frame (seg000:872): сначала пометить
* попадания с обеих сторон, потом применить, потом свести HP. */
pop_check_sword_hurting();
pop_check_sword_hurt();
pop_do_delta_hp();
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_guard_fallout(); /* страж провалился ниже комнаты — убрать (seg002:241) */
if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */
uint8_t down = pop_room_link(cur_room, LINK_DOWN);
@@ -460,10 +491,12 @@ int main(void)
PROF(4); /* слой фона */
pop_dbg_m2(); /* ЗАМЕР: конец логики / начало фона */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */
/* — ДО Kid, чтобы Kid всегда был поверх плиты) */
pop_dbg_m5(); /* ЗАМЕР: pop_loose_tick сделан */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_process_trobs(cur_room); /* пики/ворота/дверь: продвинуть СОСТОЯНИЕ (порт */
/* process_trobs — рисования внутри нет) */
if (pop_neigh_dirty) {
@@ -491,8 +524,10 @@ int main(void)
}
}
pop_dbg_m6(); /* ЗАМЕР: pop_process_trobs сделан */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_redraw_needed();
pop_dbg_m7(); /* ЗАМЕР: pop_redraw_needed сделан */ /* и разобрать пометки перерисовки тайлов (порт */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
/* redraw_needed, seg008:0178) — СЛОЙ ФОНА, до Kid */
{ /* ворота соседа слева (room8) видны в col0 — */
uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */
@@ -630,6 +665,7 @@ int main(void)
update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */
PROF(6); /* спрайты: страж, Kid, fore поверх */
pop_dbg_m3(); /* ЗАМЕР: конец фона / начало спрайтов */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
/* check_mirror (seg003:0798) — ПЕРВАЯ строка draw_people
* (seg008:228A), до отрисовки кого бы то ни было: если Кид только что
* пролетел сквозь зеркало, тень должна родиться ДО того, как слот
@@ -640,8 +676,10 @@ int main(void)
* спрайта, иначе перепечатка ложится поверх уже нарисованного куска —
* так окно задней стены и передняя грань пола оказывались на плите. */
pop_dbg_m10(); /* ЗАМЕР: check_mirror сделан */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_loose_mob_draw();
pop_dbg_m11(); /* ЗАМЕР: pop_loose_mob_draw сделан */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
{ /* Кто позже — тот поверх; порядок задаёт обход тайлов, а не роль
* персонажа (см. guard_over_kid). Отрисовка одна на всех Char
* (pop_cdraw.c), различает их только слот. */
@@ -651,6 +689,7 @@ int main(void)
uint8_t g_after;
skip = pop_char_skip_mask();
pop_dbg_m12(); /* ЗАМЕР: skip_mask сделан */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
/* Порядок «кто поверх кого» нужен, только если кого-то РИСУЕМ.
* При skip == 3 оба слота тихие, рисовать некого — а вызов стоит
* трамплина в банк 8 и двух objtile_at_char, 14 424 такта
@@ -682,7 +721,9 @@ int main(void)
* оригинала (draw_tables, seg008:1373). Пометки ставят сами
* объекты вокруг себя (pop_redraw.h). */
pop_fore_needed();
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_hp_draw(); /* полоса HP — только при изменении */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
PROF(6); /* fore поверх Kid + борта */
/* Fore-слой Кида — В КОНЦЕ, после плиты и полосы HP: окно fore-клипа
* одно на всех, и его ставит каждый, кто рисовал персонажа, поэтому
@@ -695,11 +736,18 @@ int main(void)
* взводит бит и обязан быть учтён. Всё, что рисуется ниже
* (mob поверх Кида, борта), взводит его заново само. */
pop_cd_clear(back);
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_room_clip_borders(); /* почистить борта — только в кадрах падения (гейт) */
/* Пейсинг 3 кадра, затем tear-free флип на vblank: draw уже
* сделан (скрытая страница), wait_vsync ловит начало кадра,
* set_visible_page переключает во время бланка (gfx.h паттерн). */
/* ФИКСИРОВАННЫЙ ЛОГИЧЕСКИЙ КАДР (pop_pace.h): период = max(n,
* ceil(W)) вместо прежнего ceil(W) + 2. Раньше здесь стояли три
* gfx_wait_vsync ПОДРЯД — они отсчитывались от КОНЦА работы, из-за
* чего бюджет кадра был один растр, а превышение его на такт стоило
* целого лишнего. Теперь ждём от ЯКОРЯ начала кадра.
*
* Первый фронт — всегда: на нём и происходит tear-free своп (до
* начала картинки остаётся верхний бордер, 16 строк). Строб вспышки
* снимается ровно между первым фронтом и остальными, как и было. */
PROF(0); /* чёрный: ждём vsync */
@@ -711,13 +759,15 @@ int main(void)
#endif
pop_dbg_m4(); /* ЗАМЕР: конец спрайтов */
gfx_wait_vsync();
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_wait_edge();
if (flash_on) { /* строб: цвет держался один кадр растра */
flash_bg(0, 0, 0);
flash_on = 0;
}
gfx_wait_vsync();
gfx_wait_vsync();
/* Условие боя — буквально как у оригинала (SDLPoP seg003.c:363):
* не «идёт бой» и не «рядом страж», а «у Кида ВЫНУТ МЕЧ». */
pop_pace_end(pop_pace_n((uint8_t)(Kid.sword == SWORD_2_DRAWN)));
if (dbuf) {
gfx_set_visible_page(back);
front = back;