Ресурсы — в архивы PBA1; тайминги сцены PV — по часам насоса CBL
ТАЙМИНГИ. Пользователь заметил, что заклинание Джафара не совпадает с музыкой. Замер в MAME (breakpoint на вспышке + курсор трека): реплика m53 успевала ДОИГРАТЬ до молнии, хотя по шкале оригинала между входом Джафара и заклинанием ровно 822 тика = 13,7 с. Причина: пейсинг считал НАШИ ожидания vsync, а не прошедшее время. Полноэкранная копия страницы стоит больше кадра луча, и разница копилась. Кадровые прерывания для счёта не годятся (теряются в di-окнах акселератора), поэтому часами стал насос CBL: он идёт от расхода буфера железом, 85,4 Гц, и ему безразлично, чем занят главный цикл. Один тик оригинала = 57/40 тика насоса (0,07 % ошибки, без деления в кадре). Когда звук выключен, работает прежний путь по кадрам луча. После фикса замер даёт 229 блоков остатка m53 против расчётных 233 — расхождение 47 мс. АРХИВЫ. Группы ресурсов сложены в PBA1: kid (58 атласов), pv (78), shadow (32), bg (25), title (20), guard (10), vizier (5), skel (4). Было 232 файла на образе, стало 8 архивов плюс шесть палитр и таблицы уровней. При цене `open` 51,4 мс против 32,6 мс за чтение 16 КБ это возвращает секунды на каждой загрузке. - pop_pack_arc.py печатает индексы элементов константами (ARC_<GRP>_<FILE>), порядок задаёт Makefile, а серии код проверяет статически; - atlas_load разделён: atlas_attach принимает уже прочитанную страницу, поэтому чтение из архива не дублирует проверку магии и подготовку W0; - имена архивов живут в pop_arc.c и адресуются номером. Строковый литерал лежит в rodata своего банка, и указатель на него из другого банка после переключения W3 показывает на чужие данные — ровно так падала загрузка фона (поймано брейкпоинтом на puts). Проверено в MAME: заставка, интро и уровень 1 собираются из архивов, Тень грузит все 32 страницы. Host-тесты: 15 наборов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,8 @@
|
||||
#include "pop_shadow.h"
|
||||
#include "pop_sfx.h"
|
||||
#include "pop_music.h"
|
||||
#include "pop_arc.h"
|
||||
#include "pv_arc.h"
|
||||
#include "pop_cutscene.h"
|
||||
#include "pop_intro.h"
|
||||
#include "pop_pace.h" /* pop_wait_edge */
|
||||
@@ -85,31 +87,15 @@ enum {
|
||||
INTRO_PAL_PV = 1
|
||||
};
|
||||
|
||||
/* 0..4 — TITLE/res41 + тексты 42/43/45/44 и чистая рамка; 5..12 —
|
||||
* PV/res951 + персонажи.
|
||||
* 8.3-имена намеренно короткие: DSS не получает длинные path-компоненты. */
|
||||
static const char * const intro_part[13][INTRO_PARTS] = {
|
||||
{ "PV\\s0_0.atl", "PV\\s0_1.atl", "PV\\s0_2.atl", "PV\\s0_3.atl", "PV\\s0_4.atl" },
|
||||
{ "PV\\s1_0.atl", "PV\\s1_1.atl", "PV\\s1_2.atl", "PV\\s1_3.atl", "PV\\s1_4.atl" },
|
||||
{ "PV\\s2_0.atl", "PV\\s2_1.atl", "PV\\s2_2.atl", "PV\\s2_3.atl", "PV\\s2_4.atl" },
|
||||
{ "PV\\s3_0.atl", "PV\\s3_1.atl", "PV\\s3_2.atl", "PV\\s3_3.atl", "PV\\s3_4.atl" },
|
||||
{ "PV\\s4_0.atl", "PV\\s4_1.atl", "PV\\s4_2.atl", "PV\\s4_3.atl", "PV\\s4_4.atl" },
|
||||
{ "PV\\p0_0.atl", "PV\\p0_1.atl", "PV\\p0_2.atl", "PV\\p0_3.atl", "PV\\p0_4.atl" },
|
||||
{ "PV\\p1_0.atl", "PV\\p1_1.atl", "PV\\p1_2.atl", "PV\\p1_3.atl", "PV\\p1_4.atl" },
|
||||
{ "PV\\p2_0.atl", "PV\\p2_1.atl", "PV\\p2_2.atl", "PV\\p2_3.atl", "PV\\p2_4.atl" },
|
||||
{ "PV\\p3_0.atl", "PV\\p3_1.atl", "PV\\p3_2.atl", "PV\\p3_3.atl", "PV\\p3_4.atl" },
|
||||
{ "PV\\p4_0.atl", "PV\\p4_1.atl", "PV\\p4_2.atl", "PV\\p4_3.atl", "PV\\p4_4.atl" },
|
||||
{ "PV\\p5_0.atl", "PV\\p5_1.atl", "PV\\p5_2.atl", "PV\\p5_3.atl", "PV\\p5_4.atl" },
|
||||
{ "PV\\p6_0.atl", "PV\\p6_1.atl", "PV\\p6_2.atl", "PV\\p6_3.atl", "PV\\p6_4.atl" },
|
||||
{ "PV\\p7_0.atl", "PV\\p7_1.atl", "PV\\p7_2.atl", "PV\\p7_3.atl", "PV\\p7_4.atl" }
|
||||
};
|
||||
|
||||
/* Чистая Princess-room без персонажей. Shadow-копии обеих страниц содержат
|
||||
* именно этот фон: каждый анимационный кадр получает его accelerator-копией,
|
||||
* а полупрозрачные actor-спрайты не оставляют следа. */
|
||||
static const char * const pv_base_part[INTRO_PARTS] = {
|
||||
"PV\\b0_0.atl", "PV\\b0_1.atl", "PV\\b0_2.atl", "PV\\b0_3.atl", "PV\\b0_4.atl"
|
||||
};
|
||||
/* Экраны PV лежат в одном архиве по пять полос подряд, ровно в том же
|
||||
* порядке, в каком идут номера сцен: сначала пять текстовых (story
|
||||
* res41 + тексты 42/43/45/44 и чистая рамка), затем восемь стадий комнаты
|
||||
* принцессы (res951 + персонажи) и чистый фон b0. Поэтому имён здесь нет
|
||||
* — индекс первой полосы экрана считается умножением. */
|
||||
#if (ARC_PV_P0_0 - ARC_PV_S0_0) != 5 * INTRO_PARTS
|
||||
#error "экраны PV в архиве лежат не по пять полос подряд"
|
||||
#endif
|
||||
#define INTRO_SCENES 13
|
||||
|
||||
/* Две текстовые половины show_title(). Между ними PV теперь идёт отдельным
|
||||
* покадровым renderer-ом, а не четырьмя полными статичными композициями. */
|
||||
@@ -225,13 +211,12 @@ static uint8_t intro_scene;
|
||||
static uint8_t intro_pending_pal;
|
||||
static uint8_t intro_pal_pending;
|
||||
|
||||
/* Таблицы intro_part/pv_base_part расположены в этом же банке 11. Общий
|
||||
* pop_screen_blit_atlas_strips находится в банке 9, и передавать ему
|
||||
* указатель на W3-данные нельзя: после bcall тот же адрес уже видит другой
|
||||
* физический банк. Держим короткий полосовой loader рядом с его таблицами.
|
||||
* atlas_load сам временно занимает W3 только ПОСЛЕ open(path). */
|
||||
static int intro_blit_atlas_strips(const char * const *parts,
|
||||
uint8_t count, int x, int y)
|
||||
/* Полосовой загрузчик рядом со своими вызовами: общий из банка 9 звать
|
||||
* можно (индексы и таблица — числа на стеке W2, а не указатели на W3), но
|
||||
* лишний банк→банк на каждую полосу тут ни к чему.
|
||||
* pop_arc_atlas сам временно занимает W3 только на время чтения. */
|
||||
static int intro_blit_arc(pop_arc_t *arc, const uint8_t *tbl,
|
||||
uint8_t base, uint8_t count, int x, int y)
|
||||
{
|
||||
uint8_t part;
|
||||
|
||||
@@ -240,7 +225,8 @@ static int intro_blit_atlas_strips(const char * const *parts,
|
||||
atlas_t at;
|
||||
const uint8_t *img;
|
||||
|
||||
if (atlas_load(&at, parts[part]) != 0) return -1;
|
||||
if (pop_arc_atlas(arc, tbl, (uint8_t)(base + part), &at) != 0)
|
||||
return -1;
|
||||
gfx_w0_map(at.page);
|
||||
img = (const uint8_t *)atlas_image(&at, 0);
|
||||
gfx_blit(x, y, img);
|
||||
@@ -251,6 +237,30 @@ static int intro_blit_atlas_strips(const char * const *parts,
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Экран целиком: открыть архив, вылить пять полос, закрыть. Один `open`
|
||||
* на экран вместо пяти — при 51,4 мс за открытие это больше половины
|
||||
* времени показа. */
|
||||
static int intro_blit_screen(uint8_t base, int y)
|
||||
{
|
||||
uint8_t tbl[ARC_PV_COUNT * POP_ARC_REC];
|
||||
pop_arc_t arc;
|
||||
int res;
|
||||
|
||||
if (pop_arc_open_id(&arc, POP_ARC_PV, tbl, ARC_PV_COUNT) < 0) return -1;
|
||||
res = intro_blit_arc(&arc, tbl, base, INTRO_PARTS, 0, y);
|
||||
pop_arc_close(&arc);
|
||||
return res;
|
||||
}
|
||||
|
||||
/* Один актёрский атлас PV из архива. Открытие на каждый — всё равно
|
||||
* дешевле, чем прежние отдельные файлы (тот же `open`, но без поиска имени
|
||||
* в каталоге); там, где их несколько подряд, архив открывается один раз. */
|
||||
static int pv_atlas(pop_arc_t *arc, const uint8_t *tbl, uint8_t idx,
|
||||
atlas_t *at)
|
||||
{
|
||||
return pop_arc_atlas(arc, tbl, idx, at) != 0 ? -1 : 0;
|
||||
}
|
||||
|
||||
/* ---- «Пропустить сцену» — по НОВОМУ нажатию, а не по удержанию -------- *
|
||||
*
|
||||
* Клавиша, которой закончили уровень (движение Кида в дверь, чит Shift+L,
|
||||
@@ -280,14 +290,12 @@ static uint8_t intro_skip_requested(intro_skip_t *sk)
|
||||
|
||||
static int intro_draw_page(uint8_t scene, uint8_t page)
|
||||
{
|
||||
if (scene >= sizeof(intro_part) / sizeof(intro_part[0])) return -1;
|
||||
if (scene >= INTRO_SCENES) return -1;
|
||||
gfx_set_draw_page(page);
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
pop_screen_fill(BLACK);
|
||||
if (intro_blit_atlas_strips(intro_part[scene], INTRO_PARTS,
|
||||
0, POP_YOFF) != 0)
|
||||
return -1;
|
||||
return 0;
|
||||
return intro_blit_screen((uint8_t)(ARC_PV_S0_0 + scene * INTRO_PARTS),
|
||||
POP_YOFF);
|
||||
}
|
||||
|
||||
static int intro_draw(uint8_t scene)
|
||||
@@ -356,9 +364,7 @@ static int intro_pv_draw_base(void)
|
||||
gfx_set_draw_page(0);
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
pop_screen_fill(BLACK);
|
||||
if (intro_blit_atlas_strips(pv_base_part, INTRO_PARTS,
|
||||
0, POP_YOFF) != 0)
|
||||
return -1;
|
||||
if (intro_blit_screen(ARC_PV_B0_0, POP_YOFF) != 0) return -1;
|
||||
gfx_set_draw_page(1);
|
||||
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
|
||||
gfx_copy_page(&full, GFX_COPY_DIRECT);
|
||||
@@ -695,7 +701,13 @@ static int intro_pv_animated(void)
|
||||
uint16_t tick;
|
||||
uint16_t frame_end;
|
||||
uint16_t render_tick;
|
||||
uint16_t beam = 0; /* потрачено кадров луча с начала сцены */
|
||||
uint16_t beam = 0; /* запасной путь: кадров луча ждали */
|
||||
uint16_t snd_done = 0; /* тиков насоса прошло с начала сцены */
|
||||
uint16_t snd_want = 0; /* сколько их должно было пройти */
|
||||
uint16_t snd_acc = 0; /* дробный остаток пересчёта 57/40 */
|
||||
uint8_t snd_prev = 0;
|
||||
uint8_t snd_pace = 0;
|
||||
uint8_t period;
|
||||
uint8_t anim_step = 0;
|
||||
uint8_t torch_left = 1;
|
||||
uint8_t torch_right = 6;
|
||||
@@ -717,21 +729,21 @@ static int intro_pv_animated(void)
|
||||
* временная EMM-страница. Поэтому фон готовится до постоянных
|
||||
* actor-атласов: порядок сохраняет независимость загрузчика полос от
|
||||
* числа одновременно живущих анимационных ресурсов. */
|
||||
if (atlas_load(&princess, "PV\\a0.atl") != 0) return -1;
|
||||
if (atlas_load(&jaffar0, "PV\\j0.atl") != 0) {
|
||||
goto free_princess;
|
||||
}
|
||||
if (atlas_load(&jaffar1, "PV\\j1.atl") != 0) {
|
||||
goto free_jaffar0;
|
||||
}
|
||||
if (atlas_load(&jaffar2, "PV\\j2.atl") != 0) {
|
||||
goto free_jaffar1;
|
||||
}
|
||||
if (atlas_load(&hourglass, "PV\\h0.atl") != 0) {
|
||||
goto free_jaffar2;
|
||||
}
|
||||
if (atlas_load(&torch, "PV\\t0.atl") != 0) {
|
||||
goto free_hourglass;
|
||||
/* Шесть атласов сцены — из ОДНОГО открытого архива. */
|
||||
{
|
||||
uint8_t tbl[ARC_PV_COUNT * POP_ARC_REC];
|
||||
pop_arc_t arc;
|
||||
int8_t bad;
|
||||
|
||||
if (pop_arc_open_id(&arc, POP_ARC_PV, tbl, ARC_PV_COUNT) < 0) return -1;
|
||||
bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_A0, &princess) != 0);
|
||||
if (!bad) bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_J0, &jaffar0) != 0);
|
||||
if (!bad) bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_J1, &jaffar1) != 0);
|
||||
if (!bad) bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_J2, &jaffar2) != 0);
|
||||
if (!bad) bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_H0, &hourglass) != 0);
|
||||
if (!bad) bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_T0, &torch) != 0);
|
||||
pop_arc_close(&arc);
|
||||
if (bad) return -1;
|
||||
}
|
||||
|
||||
pop_pal_fade_in(INTRO_FADE);
|
||||
@@ -740,6 +752,18 @@ static int intro_pv_animated(void)
|
||||
* Полноэкранная accel-копия кадра насосу не мешает: leaf режет DI
|
||||
* бандами по 16 строк. */
|
||||
(void)pop_sfx_start();
|
||||
/* ЧЕМ МЕРИТЬ ВРЕМЯ. Пока сцена была немой, кадры просто считали
|
||||
* ожидания vsync — и этого хватало. С музыкой выяснилось, что так
|
||||
* считаются НАШИ ожидания, а не прошедшее время: полноэкранная копия
|
||||
* страницы (~0,55 млн тактов) съедает больше кадра луча, и сцена
|
||||
* незаметно отстаёт — в MAME молния била уже ПОСЛЕ конца реплики.
|
||||
* Часы берём у насоса CBL: он идёт от расхода буфера железом, 85,4 Гц,
|
||||
* и ему безразлично, чем занят главный цикл. Если звук выключен,
|
||||
* часы стоят — тогда работает прежний путь по кадрам луча. */
|
||||
snd_prev = pop_snd_tick;
|
||||
gfx_wait_vsync();
|
||||
snd_pace = (uint8_t)(pop_snd_tick != snd_prev); /* часы реально идут? */
|
||||
snd_prev = pop_snd_tick;
|
||||
result = 0;
|
||||
princess_actor.x = 120;
|
||||
princess_actor.frame = 11; /* Pstand, facing left */
|
||||
@@ -763,7 +787,8 @@ static int intro_pv_animated(void)
|
||||
* его исходные 6/8/7 тиков, снаружи draw_frame держит результат
|
||||
* четыре физических кадра — единые номинальные 12,5 FPS. */
|
||||
flash = 0;
|
||||
frame_end = (uint16_t)(tick + pv_seq_period(tick));
|
||||
period = pv_seq_period(tick);
|
||||
frame_end = (uint16_t)(tick + period);
|
||||
if (frame_end > PV_ANIM_TICKS) frame_end = PV_ANIM_TICKS;
|
||||
do {
|
||||
pv_princess_step(&princess_actor, tick);
|
||||
@@ -811,9 +836,23 @@ static int intro_pv_animated(void)
|
||||
* анимации не стоят. */
|
||||
if (pop_music_loading()) (void)pop_music_load_step();
|
||||
/* ПЕЙСИНГ ПО ОРИГИНАЛЬНОЙ ШКАЛЕ. Кадр cutscene — 6/8/7 тиков по
|
||||
* 1/60 с, наш экран идёт на 1/50: без пересчёта сцена растянулась
|
||||
* бы на пятую часть, и реплики разъехались бы с картинкой. */
|
||||
{
|
||||
* 1/60 с; один тик = 85,4/60 = 1,424 тика насоса, и 57/40 = 1,425
|
||||
* повторяет это с точностью 0,07 % (30 мс на всю сцену) без
|
||||
* деления в кадре и без 32-битной арифметики. */
|
||||
if (snd_pace) {
|
||||
snd_acc += (uint16_t)period * 57u;
|
||||
while (snd_acc >= 40u) { snd_acc -= 40u; snd_want++; }
|
||||
for (;;) {
|
||||
uint8_t now = pop_snd_tick;
|
||||
|
||||
/* Разность в uint8_t: счётчик переполняется каждые 3 с, а
|
||||
* заглядываем мы в него не реже раза в кадр. */
|
||||
snd_done += (uint8_t)(now - snd_prev);
|
||||
snd_prev = now;
|
||||
if (snd_done >= snd_want) break;
|
||||
gfx_wait_vsync();
|
||||
}
|
||||
} else {
|
||||
uint16_t target = POP_T60(tick);
|
||||
|
||||
while (beam < target) { gfx_wait_vsync(); beam++; }
|
||||
@@ -826,17 +865,11 @@ static int intro_pv_animated(void)
|
||||
if (result) { pop_music_free(); pop_sfx_pause(); }
|
||||
if (!result) pop_pal_fade_out(INTRO_FADE);
|
||||
|
||||
free_torch:
|
||||
atlas_free(&torch);
|
||||
free_hourglass:
|
||||
atlas_free(&hourglass);
|
||||
free_jaffar2:
|
||||
atlas_free(&jaffar2);
|
||||
free_jaffar1:
|
||||
atlas_free(&jaffar1);
|
||||
free_jaffar0:
|
||||
atlas_free(&jaffar0);
|
||||
free_princess:
|
||||
atlas_free(&princess);
|
||||
return result;
|
||||
}
|
||||
@@ -876,8 +909,17 @@ static int pre_princess_animated(pop_pre_cutscene_t scene)
|
||||
* рисуется GFX_BANK_SPRITE и потому не оседает в этом источнике. */
|
||||
if (intro_draw_page(picture, 0) != 0 ||
|
||||
intro_draw_page(picture, 1) != 0) return -1;
|
||||
if (atlas_load(&hourglass, "PV\\h0.atl") != 0) return -1;
|
||||
if (atlas_load(&torch, "PV\\t0.atl") != 0) goto free_hourglass_pre;
|
||||
{
|
||||
uint8_t tbl[ARC_PV_COUNT * POP_ARC_REC];
|
||||
pop_arc_t arc;
|
||||
int8_t bad;
|
||||
|
||||
if (pop_arc_open_id(&arc, POP_ARC_PV, tbl, ARC_PV_COUNT) < 0) return -1;
|
||||
bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_H0, &hourglass) != 0);
|
||||
if (!bad) bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_T0, &torch) != 0);
|
||||
pop_arc_close(&arc);
|
||||
if (bad) return -1;
|
||||
}
|
||||
|
||||
pop_screen_present(1);
|
||||
pop_pal_fade_in(INTRO_FADE);
|
||||
@@ -912,7 +954,6 @@ static int pre_princess_animated(pop_pre_cutscene_t scene)
|
||||
if (!result) pop_pal_fade_out(INTRO_FADE);
|
||||
|
||||
atlas_free(&torch);
|
||||
free_hourglass_pre:
|
||||
atlas_free(&hourglass);
|
||||
return result;
|
||||
}
|
||||
@@ -1026,20 +1067,35 @@ static void cut_free(void)
|
||||
|
||||
static int cut_load(void)
|
||||
{
|
||||
uint8_t tbl[ARC_PV_COUNT * POP_ARC_REC];
|
||||
pop_arc_t arc;
|
||||
|
||||
cut_atlases = 0;
|
||||
if (atlas_load(&cut_pv1, "PV\\a0.atl") != 0) return -1;
|
||||
if (pop_arc_open_id(&arc, POP_ARC_PV, tbl, ARC_PV_COUNT) < 0) return -1;
|
||||
if (pv_atlas(&arc, tbl, ARC_PV_A0, &cut_pv1) != 0) {
|
||||
pop_arc_close(&arc); return -1;
|
||||
}
|
||||
cut_atlases = 1;
|
||||
if (atlas_load(&cut_pv2a, "PV\\a1.atl") != 0) { cut_free(); return -1; }
|
||||
if (pv_atlas(&arc, tbl, ARC_PV_A1, &cut_pv2a) != 0) {
|
||||
pop_arc_close(&arc); cut_free(); return -1;
|
||||
}
|
||||
cut_atlases = 2;
|
||||
if (atlas_load(&cut_pv2b, "PV\\a2.atl") != 0) { cut_free(); return -1; }
|
||||
if (pv_atlas(&arc, tbl, ARC_PV_A2, &cut_pv2b) != 0) {
|
||||
pop_arc_close(&arc); cut_free(); return -1;
|
||||
}
|
||||
cut_atlases = 3;
|
||||
/* Комната принцессы живёт и в катсценах: пламя факелов, звёзды в окне,
|
||||
* песок в часах — и ПЕРЕДНЯЯ КОЛОННА, которая по оригиналу закрывает
|
||||
* персонажей (draw_princess_room_bg + foretable). */
|
||||
if (atlas_load(&cut_hg, "PV\\h0.atl") != 0) { cut_free(); return -1; }
|
||||
if (pv_atlas(&arc, tbl, ARC_PV_H0, &cut_hg) != 0) {
|
||||
pop_arc_close(&arc); cut_free(); return -1;
|
||||
}
|
||||
cut_atlases = 4;
|
||||
if (atlas_load(&cut_torch, "PV\\t0.atl") != 0) { cut_free(); return -1; }
|
||||
if (pv_atlas(&arc, tbl, ARC_PV_T0, &cut_torch) != 0) {
|
||||
pop_arc_close(&arc); cut_free(); return -1;
|
||||
}
|
||||
cut_atlases = 5;
|
||||
pop_arc_close(&arc);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user