SprPoP: кадр катсцены — блоками по кадровому интервалу, а не «отрисовка плюс пять»

Катсцены шли ~8,2 fps вместо десяти.  Делитель тут ни при чём: у оригинала
cutscene_frame_time = 6 тиков по 1/60 с (reset_cutscene, seg001:527; его
зовёт load_intro прямо перед сценой) = 100 мс, у нас 5 кадров луча по 50 Гц
= те же 100 мс.  Причина в том, ГДЕ отсчитывался интервал: сначала рисовали
кадр целиком, и только потом ждали vsync и ещё четыре — то есть отрисовка
ПРИБАВЛЯЛАСЬ к делителю.  Полноэкранная gfx_copy_page стоит ~547 000 тактов
= 1,27 кадра, отсюда 6+ кадров вместо 5 (замер PV-RENDER-BOUND: 49 тиков/с
вместо 60).

Теперь отрисовка разложена на блоки, каждый из которых заведомо влезает в
кадровый интервал, и после каждого честно ждём vsync:
  фон верхняя половина -> vsync | фон нижняя половина -> vsync |
  актёры и декорации -> vsync (+ флип) | служебный блок (звук, подкачка
  трека) и добор до CUT_FRAME_VSYNC.
Фон восстанавливаем только по картинке (200 строк с POP_YOFF), а не по всем
256: сверху и снизу чёрная рамка.  Общий хелпер pv_restore_bg на все три
цикла — cut_run (сцены 8/9/12 и финал), pre_room_animated (2_6/4/12 и
time_expired) и intro_pv_draw_frame (сцена с Джафаром); последний теперь
возвращает 3 кадра вместо 1 (13 вместо 11 со вспышкой), вызывающий их и так
учитывал.

Сверка делителей с SDLPoP: у всех сцен 6 тиков = 5 наших кадров; плавает
только pv_scene (6 -> 8 -> 7, seg001:434/455) — это уже сделано кумулятивно
через pv_seq_period + POP_T60, и минимальный бюджет (5 кадров) больше трёх
съедаемых блоками.

Замер в MAME пока НЕ сделан: до финальной сцены на отладочном старте
LEVEL=14 не добраться (решётка перед комнатой 5 закрывается по таймеру, а
Ctrl-комбинации через MCP-мост до игры не доходят).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 12:55:27 +03:00
parent 6dabe9b4b1
commit 3449f6f8c9
+65 -20
View File
@@ -430,6 +430,44 @@ static int intro_pv_draw_base(void)
return 0; return 0;
} }
/* ВОССТАНОВЛЕНИЕ ФОНА ЗАДНЕЙ СТРАНИЦЫ — ДВУМЯ БЛОКАМИ ПО КАДРОВОМУ ИНТЕРВАЛУ.
*
* Персонажи и живой фон рисуются банком спрайтов и в ОЗУ-копию не оседают,
* поэтому «стереть прошлый кадр» = скопировать чистый фон из теневой копии
* соседней страницы. Раньше это была ОДНА копия на весь экран, и она стоила
* ~547 000 тактов = 1,27 кадра луча (модель blit_cost_model). Хвост кадра
* сцены ждал vsync ПОСЛЕ отрисовки, так что эти 1,27 кадра ПРИБАВЛЯЛИСЬ к
* пяти интервалам делителя: сцена шла 6+ кадров вместо 5, то есть ~8,2 fps
* вместо десяти (замер 49/60 тиков — docs/BUGS_OPEN.md PV-RENDER-BOUND).
*
* Лечение — не «удешевить», а РАЗЛОЖИТЬ по интервалам: каждый блок заведомо
* влезает в кадр, и после него честно ждём vsync. Тогда отрисовка живёт
* ВНУТРИ делителя, а не сверх него.
* - копируем только картинку (200 строк с POP_YOFF), а не все 256:
* сверху и снизу чёрная рамка, туда никто не рисует;
* - двумя половинами по 100 строк: ~219 000 тактов каждая, чуть больше
* половины кадра — запас двукратный.
*
* ТРАТИТ РОВНО ДВА кадровых интервала (по gfx_wait_vsync на блок) — это
* учтено в раскладке кадра у всех трёх сцен (см. вызовы). */
#define PV_BG_Y0 POP_YOFF
#define PV_BG_H 200
static void pv_restore_bg(void)
{
gfx_rect_t r;
r.x = 0;
r.w = GFX_WIDTH;
r.h = PV_BG_H / 2;
r.y = PV_BG_Y0;
gfx_copy_page(&r, GFX_COPY_DIRECT);
gfx_wait_vsync();
r.y = PV_BG_Y0 + PV_BG_H / 2;
gfx_copy_page(&r, GFX_COPY_DIRECT);
gfx_wait_vsync();
}
static void pv_blit_floor(const atlas_t *at, uint8_t frame, int x, int floor) static void pv_blit_floor(const atlas_t *at, uint8_t frame, int x, int floor)
{ {
const uint8_t *img; const uint8_t *img;
@@ -701,12 +739,10 @@ static uint8_t intro_pv_draw_frame(const atlas_t *princess, const atlas_t *j0,
uint8_t sand_frame, uint8_t flash, uint8_t sand_frame, uint8_t flash,
uint16_t tick) uint16_t tick)
{ {
gfx_rect_t full;
uint8_t back = (uint8_t)(gfx_get_visible_page() ^ 1); uint8_t back = (uint8_t)(gfx_get_visible_page() ^ 1);
gfx_set_draw_page(back); gfx_set_draw_page(back);
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT; pv_restore_bg(); /* блоки 1-2: фон, два интервала */
gfx_copy_page(&full, GFX_COPY_DIRECT);
gfx_set_bank(GFX_BANK_SPRITE); gfx_set_bank(GFX_BANK_SPRITE);
/* y=116 в princess_room_torch — нижняя строка, не верх спрайта. */ /* y=116 в princess_room_torch — нижняя строка, не верх спрайта. */
pv_blit_floor(torch, torch_left, 93, 116); pv_blit_floor(torch, torch_left, 93, 116);
@@ -730,7 +766,7 @@ static uint8_t intro_pv_draw_frame(const atlas_t *princess, const atlas_t *j0,
* его надо повторить ПОСЛЕ актёров, иначе Jaffar проходит перед * его надо повторить ПОСЛЕ актёров, иначе Jaffar проходит перед
* правой передней колонной. */ * правой передней колонной. */
pv_blit_floor(hourglass, 0, 240, 167); pv_blit_floor(hourglass, 0, 240, 167);
gfx_wait_vsync(); gfx_wait_vsync(); /* блок 3: актёры и декорации */
gfx_set_visible_page(back); gfx_set_visible_page(back);
if (flash) { if (flash) {
uint8_t pulse; uint8_t pulse;
@@ -743,9 +779,9 @@ static uint8_t intro_pv_draw_frame(const atlas_t *princess, const atlas_t *j0,
pop_pal_flash_white(0); pop_pal_flash_white(0);
gfx_wait_vsync(); gfx_wait_vsync();
} }
return 11; return 13; /* три интервала кадра + десять на молнию */
} }
return 1; return 3; /* сколько кадровых интервалов съели */
} }
/* Сцена pv_scene() SDLPoP: ожидание Princess с факелами/звёздами, поворот, /* Сцена pv_scene() SDLPoP: ожидание Princess с факелами/звёздами, поворот,
@@ -995,7 +1031,6 @@ static uint8_t pre_hourglass_state(void)
static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames) static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames)
{ {
atlas_t hourglass, torch; atlas_t hourglass, torch;
gfx_rect_t full;
uint8_t torch_left = 1, torch_right = 6, sand = 0, frame; uint8_t torch_left = 1, torch_right = 6, sand = 0, frame;
uint8_t star_phase[6] = { 1, 1, 1, 1, 1, 1 }; uint8_t star_phase[6] = { 1, 1, 1, 1, 1, 1 };
intro_skip_t skip; intro_skip_t skip;
@@ -1025,7 +1060,6 @@ static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames)
pop_screen_present(1); pop_screen_present(1);
result = 0; result = 0;
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
intro_skip_begin(&skip); intro_skip_begin(&skip);
for (frame = 0; frame < frames; frame++) { for (frame = 0; frame < frames; frame++) {
uint8_t back, hold, star; uint8_t back, hold, star;
@@ -1033,7 +1067,7 @@ static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames)
if (intro_skip_requested(&skip)) { result = 1; break; } if (intro_skip_requested(&skip)) { result = 1; break; }
back = (uint8_t)(gfx_get_visible_page() ^ 1); back = (uint8_t)(gfx_get_visible_page() ^ 1);
gfx_set_draw_page(back); gfx_set_draw_page(back);
gfx_copy_page(&full, GFX_COPY_DIRECT); pv_restore_bg(); /* блоки 1-2: фон, два интервала */
gfx_set_bank(GFX_BANK_SPRITE); gfx_set_bank(GFX_BANK_SPRITE);
pv_blit_floor(&torch, torch_left, 93, 116); pv_blit_floor(&torch, torch_left, 93, 116);
pv_blit_floor(&torch, torch_right, 211, 116); pv_blit_floor(&torch, torch_right, 211, 116);
@@ -1046,10 +1080,11 @@ static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames)
pv_blit_floor(&hourglass, (uint8_t)(8 + sand), 160, 164); pv_blit_floor(&hourglass, (uint8_t)(8 + sand), 160, 164);
} }
pv_blit_floor(&hourglass, 0, 240, 167); /* передняя колонна */ pv_blit_floor(&hourglass, 0, 240, 167); /* передняя колонна */
gfx_wait_vsync(); gfx_wait_vsync(); /* блок 3: живой фон */
gfx_set_visible_page(back); gfx_set_visible_page(back);
/* Первый тик уже потрачен на commit: ещё четыре = 5/50 с. */ /* Три интервала уже потрачены на блоки отрисовки — добираем до
for (hold = 1; hold < 5; hold++) gfx_wait_vsync(); * пяти (5/50 с = кадр катсцены оригинала). */
for (hold = 3; hold < 5; hold++) gfx_wait_vsync();
/* Проявляем ПОСЛЕ первого кадра: иначе две секунды fade идут по /* Проявляем ПОСЛЕ первого кадра: иначе две секунды fade идут по
* пустой комнате, а часы и пламя возникают уже на свету. */ * пустой комнате, а часы и пламя возникают уже на свету. */
@@ -1128,8 +1163,14 @@ static void intro_restore_game_palette(void)
/* Набор PV2 (res901..930) не влезает в одну EMM: image 0..16 в a1, /* Набор PV2 (res901..930) не влезает в одну EMM: image 0..16 в a1,
* 17..29 в a2. Номер кадра при этом остаётся исходным image-id. */ * 17..29 в a2. Номер кадра при этом остаётся исходным image-id. */
#define CUT_PV2_SPLIT 17 #define CUT_PV2_SPLIT 17
/* Один логический кадр катсцены = 5 vsync, как у остальных PV-сцен /* Один логический кадр катсцены = 5 vsync, как у остальных PV-сцен: у
* (cutscene_frame_time SDLPoP; см. pre_room_animated). */ * оригинала `cutscene_frame_time = 6` тиков по 1/60 с (reset_cutscene,
* seg001:527, и его зовёт load_intro прямо перед сценой) = 100 мс, у нас
* 5 кадров луча по 50 Гц = те же 100 мс.
*
* РАСКЛАДКА ПО ИНТЕРВАЛАМ (иначе отрисовка прибавлялась к делителю, а не
* жила внутри него — см. pv_restore_bg): фон половинами 1-2, актёры и
* декорации 3, служебный блок (звук/подкачка музыки) 4-5. */
#define CUT_FRAME_VSYNC 5 #define CUT_FRAME_VSYNC 5
static atlas_t cut_pv1, cut_pv2a, cut_pv2b, cut_hg, cut_torch; static atlas_t cut_pv1, cut_pv2a, cut_pv2b, cut_hg, cut_torch;
@@ -1290,11 +1331,9 @@ static void cut_shad_init(uint8_t x, uint8_t y, uint8_t seq)
* прервал сцену (любое НОВОЕ нажатие). */ * прервал сцену (любое НОВОЕ нажатие). */
static uint8_t cut_run(uint16_t frames, uint8_t kid_visible) static uint8_t cut_run(uint16_t frames, uint8_t kid_visible)
{ {
gfx_rect_t full;
uint16_t i; uint16_t i;
uint8_t hold; uint8_t hold;
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
for (i = 0; i < frames; i++) { for (i = 0; i < frames; i++) {
uint8_t back; uint8_t back;
kframe kid_fr, pr_fr; kframe kid_fr, pr_fr;
@@ -1316,8 +1355,9 @@ static uint8_t cut_run(uint16_t frames, uint8_t kid_visible)
back = (uint8_t)(gfx_get_visible_page() ^ 1); back = (uint8_t)(gfx_get_visible_page() ^ 1);
gfx_set_draw_page(back); gfx_set_draw_page(back);
/* Чистый фон обеих страниц лежит в shadow-копии; персонажи рисуются /* Чистый фон обеих страниц лежит в shadow-копии; персонажи рисуются
* банком спрайтов и потому в неё не оседают. */ * банком спрайтов и потому в неё не оседают. Двумя блоками по
gfx_copy_page(&full, GFX_COPY_DIRECT); * кадровому интервалу — см. pv_restore_bg. */
pv_restore_bg(); /* блоки 1-2 */
gfx_set_bank(GFX_BANK_SPRITE); gfx_set_bank(GFX_BANK_SPRITE);
pv_blit_floor(&cut_torch, cut_torch_l, 93, 116); pv_blit_floor(&cut_torch, cut_torch_l, 93, 116);
pv_blit_floor(&cut_torch, cut_torch_r, 211, 116); pv_blit_floor(&cut_torch, cut_torch_r, 211, 116);
@@ -1339,12 +1379,17 @@ static uint8_t cut_run(uint16_t frames, uint8_t kid_visible)
* сквозь неё — поймано пользователем 2026-08-25). */ * сквозь неё — поймано пользователем 2026-08-25). */
pv_blit_floor(&cut_hg, 0, 240, 167); pv_blit_floor(&cut_hg, 0, 240, 167);
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
gfx_wait_vsync(); gfx_wait_vsync(); /* блок 3: актёры и декорации */
gfx_set_visible_page(back); gfx_set_visible_page(back);
for (hold = 1; hold < CUT_FRAME_VSYNC; hold++) gfx_wait_vsync();
/* Блок 4 — служебный, и он НЕ добавляется к кадру, а живёт внутри
* оставшихся интервалов: подкачка страницы трека стоит ~33 мс, то
* есть меньше двух интервалов, которые всё равно надо переждать.
* Порядок как в сцене с Джафаром: шаг подкачки — безусловный (см.
* memory pv_music_stall_regression). */
pop_sfx_tick(); pop_sfx_tick();
pop_music_service(); /* «встреча с принцессой» — трек, а не эффект */ pop_music_service(); /* «встреча с принцессой» — трек, а не эффект */
for (hold = 3; hold < CUT_FRAME_VSYNC; hold++) gfx_wait_vsync();
cut_torch_l = (uint8_t)((cut_torch_l + 1) % 9); cut_torch_l = (uint8_t)((cut_torch_l + 1) % 9);
cut_torch_r = (uint8_t)((cut_torch_r + 1) % 9); cut_torch_r = (uint8_t)((cut_torch_r + 1) % 9);
cut_sand = (uint8_t)((cut_sand + 1) % 3); cut_sand = (uint8_t)((cut_sand + 1) % 3);