From 3449f6f8c976b2746203652c32beb3f11b7882a6 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Fri, 28 Aug 2026 12:55:27 +0300 Subject: [PATCH] =?UTF-8?q?SprPoP:=20=D0=BA=D0=B0=D0=B4=D1=80=20=D0=BA?= =?UTF-8?q?=D0=B0=D1=82=D1=81=D1=86=D0=B5=D0=BD=D1=8B=20=E2=80=94=20=D0=B1?= =?UTF-8?q?=D0=BB=D0=BE=D0=BA=D0=B0=D0=BC=D0=B8=20=D0=BF=D0=BE=20=D0=BA?= =?UTF-8?q?=D0=B0=D0=B4=D1=80=D0=BE=D0=B2=D0=BE=D0=BC=D1=83=20=D0=B8=D0=BD?= =?UTF-8?q?=D1=82=D0=B5=D1=80=D0=B2=D0=B0=D0=BB=D1=83,=20=D0=B0=20=D0=BD?= =?UTF-8?q?=D0=B5=20=C2=AB=D0=BE=D1=82=D1=80=D0=B8=D1=81=D0=BE=D0=B2=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=BB=D1=8E=D1=81=20=D0=BF=D1=8F=D1=82=D1=8C?= =?UTF-8?q?=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Катсцены шли ~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 --- applications/SprPoP/src/pop_intro.c | 85 ++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 20 deletions(-) diff --git a/applications/SprPoP/src/pop_intro.c b/applications/SprPoP/src/pop_intro.c index 834fb35..f53822b 100644 --- a/applications/SprPoP/src/pop_intro.c +++ b/applications/SprPoP/src/pop_intro.c @@ -430,6 +430,44 @@ static int intro_pv_draw_base(void) 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) { 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, uint16_t tick) { - gfx_rect_t full; uint8_t back = (uint8_t)(gfx_get_visible_page() ^ 1); gfx_set_draw_page(back); - full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT; - gfx_copy_page(&full, GFX_COPY_DIRECT); + pv_restore_bg(); /* блоки 1-2: фон, два интервала */ gfx_set_bank(GFX_BANK_SPRITE); /* y=116 в princess_room_torch — нижняя строка, не верх спрайта. */ 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 проходит перед * правой передней колонной. */ pv_blit_floor(hourglass, 0, 240, 167); - gfx_wait_vsync(); + gfx_wait_vsync(); /* блок 3: актёры и декорации */ gfx_set_visible_page(back); if (flash) { 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); gfx_wait_vsync(); } - return 11; + return 13; /* три интервала кадра + десять на молнию */ } - return 1; + return 3; /* сколько кадровых интервалов съели */ } /* Сцена 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) { atlas_t hourglass, torch; - gfx_rect_t full; uint8_t torch_left = 1, torch_right = 6, sand = 0, frame; uint8_t star_phase[6] = { 1, 1, 1, 1, 1, 1 }; 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); result = 0; - full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT; intro_skip_begin(&skip); for (frame = 0; frame < frames; frame++) { 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; } back = (uint8_t)(gfx_get_visible_page() ^ 1); gfx_set_draw_page(back); - gfx_copy_page(&full, GFX_COPY_DIRECT); + pv_restore_bg(); /* блоки 1-2: фон, два интервала */ gfx_set_bank(GFX_BANK_SPRITE); pv_blit_floor(&torch, torch_left, 93, 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, 0, 240, 167); /* передняя колонна */ - gfx_wait_vsync(); + gfx_wait_vsync(); /* блок 3: живой фон */ 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 идут по * пустой комнате, а часы и пламя возникают уже на свету. */ @@ -1128,8 +1163,14 @@ static void intro_restore_game_palette(void) /* Набор PV2 (res901..930) не влезает в одну EMM: image 0..16 в a1, * 17..29 в a2. Номер кадра при этом остаётся исходным image-id. */ #define CUT_PV2_SPLIT 17 -/* Один логический кадр катсцены = 5 vsync, как у остальных PV-сцен - * (cutscene_frame_time SDLPoP; см. pre_room_animated). */ +/* Один логический кадр катсцены = 5 vsync, как у остальных PV-сцен: у + * оригинала `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 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) { - gfx_rect_t full; uint16_t i; uint8_t hold; - full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT; for (i = 0; i < frames; i++) { uint8_t back; 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); gfx_set_draw_page(back); /* Чистый фон обеих страниц лежит в shadow-копии; персонажи рисуются - * банком спрайтов и потому в неё не оседают. */ - gfx_copy_page(&full, GFX_COPY_DIRECT); + * банком спрайтов и потому в неё не оседают. Двумя блоками по + * кадровому интервалу — см. pv_restore_bg. */ + pv_restore_bg(); /* блоки 1-2 */ gfx_set_bank(GFX_BANK_SPRITE); pv_blit_floor(&cut_torch, cut_torch_l, 93, 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). */ pv_blit_floor(&cut_hg, 0, 240, 167); gfx_set_bank(GFX_BANK_NORMAL); - gfx_wait_vsync(); + gfx_wait_vsync(); /* блок 3: актёры и декорации */ 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_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_r = (uint8_t)((cut_torch_r + 1) % 9); cut_sand = (uint8_t)((cut_sand + 1) % 3);