Files
Sprinter-SDCC/applications/PoP/roomtest/pop_title.c
T
snark13 5ede8b9045 Fade: пересчёт палитры в пять раз дешевле, шестнадцать ступеней
Затемнение длилось около восьми секунд вместо заказанных двух. Причин
две, и обе измерены в MAME.

Первая: пересчёт палитры звался на КАЖДОМ кадре — сто с лишним раз за
fade, хотя ступеней всего восемь. Теперь пересчёт идёт только при смене
ступени, остальное время цикл просто ждёт луча.

Вторая: сам пересчёт стоил 2,44 млн тактов — почти шесть кадров.
Разложение показало, что заливка палитры через BIOS тут ни при чём
(136 тысяч на восемь вызовов). Съедали два цикла на C: 768 умножений
uint16 на канал и побайтовое копирование снимка из страницы шрифта.
Умножения заменены таблицей яркости на стеке (256 сложений, без единого
умножения), копирование — memcpy, то есть LDIR. Итог: 487 тысяч тактов,
в пять раз меньше.

Освободившийся запас потрачен на плавность: ступеней теперь шестнадцать
вместо восьми. Длительность fade считает POP_FADE_FRAMES — из 106 кадров
(2,13 с оригинала) вычитается то, что съедает сам пересчёт.

Host-тесты: 15 наборов. Проверено в MAME: заставка проходит цепочку
кадров штатно.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 10:14:55 +03:00

174 lines
7.4 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* pop_title.c — оригинальная графическая часть show_title(), банк 9 (FG6).
*
* Готовые композиции создаёт toolchain/pop_pack_title.py непосредственно из
* TITLE/res51..55.png SDLPoP. Каждый 320x200 экран режется на пять лент:
* загружаем одну в EMM, блитим непрозрачно в normal bank, освобождаем. Это
* сознательно не держит title-ресурсы рядом с игровыми атласами.
*/
#include <stdint.h>
#include <kbd_raw.h>
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_bg.h" /* POP_YOFF: title занимает 200px по центру 256px */
#include "pop_sfx.h"
#include "pop_music.h"
#include "pop_arc.h"
#include "title_arc.h"
#include "pop_cutscene.h" /* POP_T60 */
#include "pop_settings.h"
#include "pop_pal.h"
#include "pop_title.h"
#include "pop_ui.h"
#define TITLE_W 320
#define TITLE_H 200
#define TITLE_Y POP_YOFF
#define TITLE_PARTS 5
#define TITLE_FADE POP_FADE_FRAMES /* 2,13 с, как fade_*_1 оригинала */
enum {
TITLE_MAIN = 0,
TITLE_PRESENTS,
TITLE_GAME,
TITLE_LOGO
};
/* Композиции лежат в одном архиве подряд по пять полос 49/49/49/49/4,
* поэтому имён здесь больше нет: индекс = сцена * TITLE_PARTS. */
#if (ARC_TITLE_T1_0 - ARC_TITLE_T0_0) != TITLE_PARTS
#error "полосы титров в архиве лежат не по пять подряд"
#endif
/* Последовательность и интервалы буквально из show_title():
* main --0x82--> presents --0xCD--> main --0x41--> game
* --0x10E--> main --0xEB--> logo+Mechner.
* Счёт — в тиках SDLPoP (60 Гц), в кадры луча переводит POP_T60: под
* заставку играет тема, и разбегание на 20 % было бы слышно.
*
* Последний интервал у оригинала не фиксирован: показав логотип, он ждёт
* ОКОНЧАНИЯ ТЕМЫ (`while (check_sound_playing())`, seg000:2027) и лишь
* потом уходит в историю. 0xEB здесь — нижняя граница на случай, когда
* музыки нет (нет файла, выключен звук). */
static const uint8_t title_scene[] = {
TITLE_MAIN, TITLE_PRESENTS, TITLE_MAIN, TITLE_GAME, TITLE_MAIN, TITLE_LOGO
};
static const uint16_t title_wait_ticks[] = {
POP_T60(0x82), POP_T60(0xCD), POP_T60(0x41),
POP_T60(0x10E), POP_T60(0xEB), POP_T60(0xEB)
};
static uint8_t title_shown;
static uint8_t title_skip_pressed(void)
{
/* Как и в attract-demo, любой удерживаемый raw-код означает «не ждать».
* Это заодно не дублирует список клавиш у title, intro и cutscene. */
return kbd_raw_any_down();
}
/* Нарисовать одну из готовых 320x200 композиций в скрытую страницу.
* Сопоставление W0 охватывает ТОЛЬКО atlas_image/blit: BIOS/vsync сюда
* не попадает, значит не конфликтует с EMM и CBL. Возвращает номер
* подготовленной страницы; show=0 оставляет её скрытой для palette commit. */
static int title_draw(uint8_t scene, uint8_t show)
{
uint8_t tbl[ARC_TITLE_COUNT * POP_ARC_REC];
pop_arc_t arc;
uint8_t page = pop_screen_begin();
int res;
pop_screen_fill(BLACK);
if (pop_arc_open_id(&arc, POP_ARC_TITLE, tbl, ARC_TITLE_COUNT) < 0)
return -1;
res = pop_screen_blit_atlas_arc(&arc, tbl,
(uint8_t)(ARC_TITLE_T0_0 +
scene * TITLE_PARTS),
TITLE_PARTS, 0, TITLE_Y);
pop_arc_close(&arc);
if (res != 0) return -1;
if (show) pop_screen_present(page);
return page;
}
static uint8_t title_wait(uint16_t ticks)
{
while (ticks--) {
if (title_skip_pressed()) return 1;
gfx_wait_vsync();
}
return 0;
}
/* Новый источник не становится видимым сам: cold boot уже чёрный, а
* последующий fade_in выбирает яркость только после сборки t0. */
static void title_load_palette_dimmed(void)
{
pop_pal_title_load();
pop_pal_apply(POP_PAL_DIM_BLACK);
}
uint8_t pop_title_show(void) __banked
{
uint8_t step;
uint8_t skipped = 0, music_ok = 0;
int prepared;
uint8_t cold_start = (uint8_t)(!title_shown &&
!pop_settings.show_build_info);
/* Игровые SFX не должны тянуться сюда из прерванной партии: заставка
* начинается с тишины, а через несколько строк её займёт тема титров. */
pop_sfx_pause();
if (!cold_start) pop_pal_fade_out(TITLE_FADE);
/* Видимая VRAM могла сохраниться от предыдущей программы. Обнулить её
* при чёрной палитре, затем собрать t0 СКРЫТО: загрузка title.pal тогда
* освещает только индекс 0, а не старый экран или недорисованный title. */
gfx_set_draw_page(gfx_get_visible_page());
gfx_set_bank(GFX_BANK_NORMAL);
pop_screen_fill(BLACK);
prepared = title_draw(TITLE_MAIN, 0);
if (prepared < 0) return 0;
title_load_palette_dimmed();
pop_screen_present((uint8_t)prepared);
title_shown = 1;
/* Тему читаем с диска ЗАРАНЕЕ (пока экран ещё тёмный), а пускаем ниже —
* после fade_in, ровно как show_title: fade_in_2, затем
* play_sound(sound_54). */
music_ok = (uint8_t)(pop_music_load(POP_MUS_INTRO) == 0);
for (step = 0; step < sizeof(title_scene); step++) {
if (step && title_draw(title_scene[step], 1) < 0)
break;
if (step == 0) {
pop_pal_fade_in(TITLE_FADE);
if (music_ok) {
(void)pop_sfx_start();
pop_music_play();
}
}
if (title_wait(title_wait_ticks[step])) {
skipped = 1;
break;
}
/* Логотип держим до конца темы — так оригинал стыкует заставку с
* историей: музыка не обрывается, а сменяется следующей репликой. */
if (step + 1 == sizeof(title_scene)) {
while (pop_music_busy()) {
if (title_skip_pressed()) { skipped = 1; break; }
gfx_wait_vsync();
}
}
}
/* Уходим В ИГРУ — заставочная музыка не должна тянуться в уровень.
* Дальше по сценарию (intro) трек, наоборот, ОСТАЁТСЯ звучать: там
* следующая реплика подгружается во второй слот, пока играет эта. */
if (skipped) {
pop_music_free();
pop_sfx_pause();
}
return skipped;
}