Ресурсы — в архивы 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:
2026-08-25 21:29:14 +03:00
parent d86adc54c4
commit 65797af44c
40 changed files with 997 additions and 244 deletions
+131 -75
View File
@@ -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;
}