SprPoP: единые часы в сцене с Джафаром + конец уровня ждёт свою мелодию
1. СЦЕНА С ДЖАФАРОМ — ОДНИ ЧАСЫ, КАДРЫ ЛУЧА. Было две шкалы, и выбор между ними делался гонкой на старте сцены: одна выборка pop_snd_tick через кадр, «успел ли диск раскрутить звук». От прогона к прогону сцена шла то по тикам насоса CBL, то по кадрам луча, и кода реплики приходилась каждый раз на другое место картинки (наблюдение пользователя). Насос был нужен потому, что кадр рисовался дольше своего интервала; теперь отрисовка разложена по интервалам (pv_restore_bg), и счёт кадров честен — ветка насоса убрана целиком. 2. ПОДКАЧКА ТРЕКА — ПОЛСТРАНИЦЫ ЗА ШАГ (pop_music_load_step). 8 КБ ≈ 16 мс влезают в кадровый интервал, целая страница (33 мс) не влезала и растягивала кадр сцены. В сцене шаг остаётся безусловным (иначе реплики не успевали грузиться, memory pv_music_stall_regression) и оплачивается ровно одним интервалом. 3. КОНЕЦ УРОВНЯ ЖДЁТ МЕЛОДИЮ. Оригинал (seg003:387, play_level_2) не сменяет уровень, пока `check_sound_playing()`: экран пройденного уровня живёт с анимацией факелов, пока звучит трек. Мы уходили на смену сразу и обрывали мелодию на первых нотах. Теперь ждём большего из двух: pop_music_busy() и счётчика pop_endmus_left по длине записи (gen/pop_music_ticks.h) — второе нужно потому, что при ВЫКЛЮЧЕННОЙ музыке busy ложен, а оригинал выдерживает паузу и молча. Стартовый уровень возвращён на 1 (отладочный LEVEL=14 был только для замера). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -795,12 +795,7 @@ static int intro_pv_animated(void)
|
||||
uint16_t tick;
|
||||
uint16_t frame_end;
|
||||
uint16_t render_tick;
|
||||
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;
|
||||
uint16_t beam = 0; /* кадров луча потрачено с начала сцены */
|
||||
uint8_t period;
|
||||
uint8_t anim_step = 0;
|
||||
uint8_t torch_left = 1;
|
||||
@@ -849,18 +844,22 @@ 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;
|
||||
/* ЧЕМ МЕРИТЬ ВРЕМЯ — ЕДИНЫЕ ЧАСЫ: КАДРЫ ЛУЧА.
|
||||
*
|
||||
* Была вторая шкала — тики насоса CBL, и выбор между ними делался
|
||||
* гонкой на старте сцены (одна выборка `pop_snd_tick` через кадр:
|
||||
* успел ли диск раскрутить звук). Значит от прогона к прогону сцена
|
||||
* шла то по одним часам, то по другим, и кода реплики приходилась
|
||||
* каждый раз на другое место картинки (наблюдение пользователя
|
||||
* 2026-08-28; баг SND-PACE-DEAD). Насос как часы был нужен потому,
|
||||
* что кадр сцены рисовался ДОЛЬШЕ своего интервала и счёт наших
|
||||
* ожиданий врал; теперь отрисовка разложена на блоки по кадровому
|
||||
* интервалу (см. pv_restore_bg), и счёт кадров луча честен.
|
||||
*
|
||||
* Осталось учесть единственную работу, которая в интервал не влезала
|
||||
* сама, — подкачку страницы трека: шаг читает ПОЛСТРАНИЦЫ (~16 мс,
|
||||
* pop_music_load_step) и оплачивается ровно одним интервалом, как
|
||||
* любой другой блок кадра. */
|
||||
result = 0;
|
||||
princess_actor.x = 120;
|
||||
princess_actor.frame = 11; /* Pstand, facing left */
|
||||
@@ -928,59 +927,42 @@ static int intro_pv_animated(void)
|
||||
torch_right, sand_frame, flash,
|
||||
render_tick);
|
||||
pop_sfx_tick(); /* как в оригинале — в конце отрисовки кадра */
|
||||
|
||||
/* ГАРАНТИРОВАННЫЙ ШАГ ПОДКАЧКИ, а не «если останется время».
|
||||
*
|
||||
* Ниже страница следующей реплики читается в ПАУЗЕ кадра, и это
|
||||
* Раньше страница следующей реплики читалась в ПАУЗЕ кадра, и это
|
||||
* верно ровно до тех пор, пока пауза есть. В этой сцене её почти
|
||||
* нет: кадр дорогой, шкала работает по принципу «долг не догоняем»
|
||||
* (snd_want подтягивается к snd_done), и цикл ожидания выходит
|
||||
* сразу. Замер в MAME 2026-08-26: ОДНА страница за семь секунд,
|
||||
* то есть одиннадцать страниц m53 не успевали за всю сцену —
|
||||
* pop_music_play() на своём тике не находил готового трека, и
|
||||
* реплики Джафара пропадали совсем.
|
||||
* не было: кадр дорогой, шкала работает по принципу «долг не
|
||||
* догоняем», и цикл ожидания выходил сразу. Замер в MAME
|
||||
* 2026-08-26: ОДНА страница за семь секунд, то есть одиннадцать
|
||||
* страниц m53 не успевали за всю сцену — pop_music_play() на своём
|
||||
* тике не находил готового трека, и реплики Джафара пропадали
|
||||
* совсем (memory pv_music_stall_regression).
|
||||
*
|
||||
* Страница стоит 33 мс против кадра сцены в 100-133 мс, поэтому
|
||||
* один безусловный шаг в кадр сцену не ломает: он лишь тратит
|
||||
* часть той же паузы, а если паузы не было — добавляет к кадру
|
||||
* четверть его длины, и шкала это учтёт сама. */
|
||||
if (pop_music_loading()) (void)pop_music_load_step();
|
||||
* Поэтому шаг безусловный — и оплачен как обычный блок кадра: он
|
||||
* читает полстраницы (~16 мс), а следующий gfx_wait_vsync ставит
|
||||
* нас на край интервала, который мы этим и заняли. */
|
||||
if (pop_music_loading()) {
|
||||
(void)pop_music_load_step();
|
||||
gfx_wait_vsync();
|
||||
beam++;
|
||||
}
|
||||
/* Первый кадр собран под чёрной палитрой — теперь проявляем его
|
||||
* целиком, вместе с принцессой и факелами. */
|
||||
if (!faded_in) {
|
||||
faded_in = 1;
|
||||
if (pop_pal_fade_in_skip(INTRO_FADE)) { result = 1; break; }
|
||||
}
|
||||
/* ПЕЙСИНГ ПО ОРИГИНАЛЬНОЙ ШКАЛЕ. Кадр cutscene — 6/8/7 тиков по
|
||||
* 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;
|
||||
/* Страницу следующей реплики читаем ЗДЕСЬ, в паузе кадра:
|
||||
* 33 мс диска укладываются в ожидание (кадр сцены — 133 мс),
|
||||
* и подкачка не добавляется к длительности кадра, а
|
||||
* растворяется в ней. */
|
||||
if (pop_music_loading()) (void)pop_music_load_step();
|
||||
else gfx_wait_vsync();
|
||||
}
|
||||
/* ДОЛГ НЕ ДОГОНЯЕМ. Подкачка следующей реплики (страница — 33 мс)
|
||||
* и любой тяжёлый кадр оставляют snd_done впереди snd_want, и
|
||||
* дальше цикл гнал кадры без единого ожидания: пламя факелов
|
||||
* мелькало, а события соседних тиков (створка ворот и дверь
|
||||
* покоев) слипались в один кадр — заявка перезаписывала заявку, и
|
||||
* один из звуков пропадал. Тот же принцип, что у pop_pace_end:
|
||||
* якорь ставится ПО ФАКТУ, пропущенное время не наверстывается. */
|
||||
if (snd_done > snd_want) snd_want = snd_done;
|
||||
} else {
|
||||
/* ПЕЙСИНГ ПО ОРИГИНАЛЬНОЙ ШКАЛЕ. Кадр сцены — 6/8/7 тиков по
|
||||
* 1/60 с (pv_seq_period), в кадры луча переводит POP_T60, и цель
|
||||
* КУМУЛЯТИВНА: нецелые 6,67 и 5,83 не копят ошибку.
|
||||
*
|
||||
* ДОЛГ НЕ ДОГОНЯЕМ: если кадр вышел дороже своего бюджета, цикл
|
||||
* ожидания просто не сработает, но beam уже учёл потраченное — и
|
||||
* следующая цель считается от той же общей шкалы. Наверстывать
|
||||
* пропущенное нельзя: события соседних тиков (створка ворот, дверь
|
||||
* покоев) слиплись бы в один кадр, и один из звуков пропал бы. */
|
||||
{
|
||||
uint16_t target = POP_T60(tick);
|
||||
|
||||
while (beam < target) { gfx_wait_vsync(); beam++; }
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#include "pop_map.h" /* pop_clip_char_top (clip_char) */
|
||||
#include "pop_geom.h"
|
||||
#include "pop_level.h" /* POP_LEVEL_LAST — level 15 исключён */
|
||||
#include "pop_cutscene.h" /* POP_T60 — тики оригинала в кадры луча */
|
||||
#include "pop_music_ticks.h" /* длина мелодии конца уровня */
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sprinter_mem.h>
|
||||
@@ -36,6 +38,14 @@ int8_t knock;
|
||||
* оригинале, чтобы читы «следующий/предыдущий уровень» (levels_plan.md §4)
|
||||
* легли сюда же без переделки. */
|
||||
uint8_t pop_next_level;
|
||||
/* СКОЛЬКО ЛОГИЧЕСКИХ КАДРОВ ЕЩЁ ДЕРЖАТЬ УРОВЕНЬ, пока играет мелодия конца.
|
||||
* У оригинала (seg003:387, play_level_2) уровень не сменяется, пока
|
||||
* `check_sound_playing()`: экран пройденного уровня живёт с анимацией
|
||||
* факелов ровно столько, сколько звучит трек. У нас признак «звучит» —
|
||||
* pop_music_busy(), но он ложен при ВЫКЛЮЧЕННОЙ музыке, а оригинал
|
||||
* выдерживает паузу и молча. Поэтому здесь взводится счётчик по длине
|
||||
* записи (gen/pop_music_ticks.h), а главный цикл ждёт большего из двух. */
|
||||
uint8_t pop_endmus_left;
|
||||
|
||||
|
||||
/* ---- Атласы Kid (прямая адресация image>>3) ------------------------ *
|
||||
@@ -358,9 +368,17 @@ void play_seq(void)
|
||||
else if (ev == 4) {
|
||||
/* seg006:646. На четвёртом уровне уход за дверь — это
|
||||
* встреча с тенью, и трек там свой; на 13-м (Джафар) и
|
||||
* 15-м оригинал молчит. */
|
||||
if (pop_current_level == 4) pop_sfx_play(32);
|
||||
else if (pop_current_level != 13) pop_sfx_play(41);
|
||||
* 15-м оригинал молчит. Вместе с треком взводим
|
||||
* задержку смены уровня (см. pop_endmus_left): кадров
|
||||
* = длина записи в тиках -> кадры луча -> делённое на
|
||||
* длину логического кадра (4 кадра луча вне боя). */
|
||||
if (pop_current_level == 4) {
|
||||
pop_sfx_play(32);
|
||||
pop_endmus_left = (uint8_t)(POP_T60(POP_MUS_TICKS_32) / 4);
|
||||
} else if (pop_current_level != 13) {
|
||||
pop_sfx_play(41);
|
||||
pop_endmus_left = (uint8_t)(POP_T60(POP_MUS_TICKS_41) / 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -26,6 +26,8 @@ extern int8_t knock;
|
||||
* пройден». Читает и обнуляет главный цикл. Пока уровень один, поэтому
|
||||
* там рестарт того же уровня; загрузка следующего — levels_plan.md. */
|
||||
extern uint8_t pop_next_level;
|
||||
/* Задержка смены уровня на время мелодии конца (см. pop_kid.c). */
|
||||
extern uint8_t pop_endmus_left;
|
||||
|
||||
/* Загрузить npages атласов Kid (kid0..kid{npages-1}.atl). ДО initgraph.
|
||||
* 0 — OK, -1 — ошибка. */
|
||||
|
||||
@@ -49,6 +49,7 @@ static uint8_t ld_busy;
|
||||
static int ld_fd;
|
||||
static uint8_t ld_slot;
|
||||
static uint8_t ld_next; /* следующая страница к чтению */
|
||||
static uint8_t ld_half; /* 0 = впереди первая половина страницы */
|
||||
static uint8_t ld_pages;
|
||||
static uint8_t ld_id;
|
||||
|
||||
@@ -110,13 +111,14 @@ void pop_music_free(void) __banked
|
||||
* ПОСТРАНИЧНО, А НЕ ЦЕЛИКОМ. Реплики сцены с принцессой идут встык, и
|
||||
* следующую надо читать, пока звучит текущая. Трек — до 320 КБ, целиком
|
||||
* это 0,7 с диска: на 7,5 кадрах в секунду сцена встала бы на пять кадров.
|
||||
* Одна страница стоит 33 мс — меньше четверти кадра, и подкачка растворя-
|
||||
* ется в анимации. Кто может позволить себе паузу (титры, чёрный экран
|
||||
* Шаг стоит 16 мс (полстраницы, см. pop_music_load_step) — меньше одного
|
||||
* кадрового интервала, и подкачка растворяется в анимации. Кто может позволить себе паузу (титры, чёрный экран
|
||||
* между сценами), зовёт pop_music_load — тот же цикл, но целиком. */
|
||||
static void load_abort(void)
|
||||
{
|
||||
if (ld_busy) close(ld_fd);
|
||||
ld_busy = 0;
|
||||
ld_half = 0;
|
||||
slot_release(ld_slot);
|
||||
}
|
||||
|
||||
@@ -146,23 +148,35 @@ int8_t pop_music_load_begin(uint8_t id) __banked
|
||||
ld_busy = 1;
|
||||
ld_slot = n;
|
||||
ld_next = 0;
|
||||
ld_half = 0;
|
||||
ld_pages = pages;
|
||||
ld_id = id;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ШАГ = ПОЛСТРАНИЦЫ (8 КБ, ~16 мс), а не страница (~33 мс).
|
||||
*
|
||||
* Шаг вызывают раз в кадр из циклов, где кадр разложен на блоки по одному
|
||||
* кадровому интервалу (20 мс): катсцены после перехода на блочную
|
||||
* отрисовку, см. pop_intro.c/pv_restore_bg. Целая страница в интервал не
|
||||
* влезала и незаметно растягивала кадр сцены; половина влезает, а цена —
|
||||
* вдвое больше вызовов (11 страниц реплики = 22 кадра вместо 11, то есть
|
||||
* меньше полусекунды). Игровому циклу это безразлично: там логический
|
||||
* кадр 80-100 мс. */
|
||||
int8_t pop_music_load_step(void) __banked
|
||||
{
|
||||
uint8_t p;
|
||||
|
||||
if (!ld_busy) return 1; /* нечего грузить */
|
||||
p = mem_get_page(slot_blk[ld_slot], ld_next);
|
||||
/* Последняя страница короче — читаем сколько есть; ошибку ловим по
|
||||
* отрицательному коду, а не по «не 16384». */
|
||||
if (!p || bank_read_page(ld_fd, p, 0, 16384) < 0) {
|
||||
/* Последняя страница короче — читаем сколько есть (вторая половина
|
||||
* может дать и ноль байт); ошибку ловим по отрицательному коду. */
|
||||
if (!p || bank_read_page(ld_fd, p, ld_half ? 8192 : 0, 8192) < 0) {
|
||||
load_abort();
|
||||
return -1;
|
||||
}
|
||||
if (!ld_half) { ld_half = 1; return 0; } /* дочитаем в следующий раз */
|
||||
ld_half = 0;
|
||||
if (++ld_next < ld_pages) return 0;
|
||||
|
||||
close(ld_fd);
|
||||
|
||||
@@ -357,6 +357,7 @@ int main(void)
|
||||
* девелоперская сборка может до title держать дворец LEVEL=11. */
|
||||
if (pop_app_state == POP_APP_DEMO) {
|
||||
pop_next_level = 0;
|
||||
pop_endmus_left = 0;
|
||||
pop_seamless = 0;
|
||||
pop_checkpoint = 0;
|
||||
if (pop_level_switch() != 0) {
|
||||
@@ -765,9 +766,28 @@ int main(void)
|
||||
/* Смена уровня целиком уехала в банк 8 (pop_level_change): это
|
||||
* событие раз в несколько минут, а кода на нём было под сотню строк
|
||||
* в самом горячем месте программы — резидент W1 у нас в дефиците. */
|
||||
if (!is_demo && pop_next_level != pop_current_level &&
|
||||
pop_level_change(back) != 0) {
|
||||
failed = 1; quit = 1; break;
|
||||
if (!is_demo && pop_next_level != pop_current_level) {
|
||||
/* КОНЕЦ УРОВНЯ ЖДЁТ МЕЛОДИЮ (порт seg003:387, play_level_2):
|
||||
*
|
||||
* if (next_level == current_level || check_sound_playing())
|
||||
* draw_game_frame(); ... // экран уровня живёт
|
||||
* else { stop_sounds(); return next_level; }
|
||||
*
|
||||
* То есть пройденный уровень остаётся на экране — с анимацией
|
||||
* факелов, идущим таймером и всем прочим — ровно столько,
|
||||
* сколько звучит трек конца уровня; мы же уходили на смену
|
||||
* уровня сразу и обрывали мелодию на первых нотах.
|
||||
*
|
||||
* «Звучит» проверяем двумя способами: pop_music_busy() пока
|
||||
* трек играет и счётчик pop_endmus_left по длине записи (его
|
||||
* взводит play_seq вместе с самим треком) — иначе с
|
||||
* ВЫКЛЮЧЕННОЙ музыкой паузы не было бы вовсе, а оригинал
|
||||
* выдерживает её и молча. */
|
||||
if (pop_endmus_left) pop_endmus_left--;
|
||||
if (!pop_endmus_left && !pop_music_busy() &&
|
||||
pop_level_change(back) != 0) {
|
||||
failed = 1; quit = 1; break;
|
||||
}
|
||||
}
|
||||
if (!is_demo && pop_kid_dead) { /* demo после смерти завершается, без respawn */
|
||||
dead_frames++;
|
||||
|
||||
Reference in New Issue
Block a user