Files
Sprinter-SDCC/applications/PoP/roomtest/pop_ui.c
T
snark13 a3c5c600da Таблица рекордов: оба показа оригинала, ввод имени, переходы полосами
pop_hof.c был написан, но никуда не вызывался: после финала автомат уходил
мимо него в title, а на титрах экран получался чёрным. Теперь это хвост
end_sequence и шаг show_title между credits и attract-demo, как в оригинале.

Два корня пустого экрана:

- story.pal — 256 записей, и запись 0x3F (цвет глифов шрифта) в ней чёрная:
  текст рисовался, но был не виден. Генератор кладёт туда золотой 0xB7;
- полосовой переход копирует страницу акселератором, а тот читает ОЗУ-копию,
  куда прозрачный блит текста не пишет. Строки рисуются после перехода,
  прямо в видимую страницу.

Попутно:

- s5 в архиве PV — фон таблицы (рамка story + логотип на y=24, HOF_POP);
- отдельный индекс палитры под фон текстовой рамки: финал красит его в
  #800000, титры оставляют #100060 (load_title_images(bgcolor)). Прежний
  ремап в индекс 9 подменить было нельзя — им нарисована сама титульная
  картинка;
- второй набор глифов в font.atl цветом 0x3E: у SDLPoP шрифт маска и
  show_hof_text рисует текст дважды разным цветом, у нас цвет запечён в
  пиксели. Каталог SPA1 держит счётчик в байте, поэтому набор обрезан по '_';
- общий pop_screen_present_ltr(): им теперь пользуются story-переход интро,
  титры, таблица и титульная картинка финала. Заодно вернулся пропущенный
  шаг show_title — экран с логотипом и Jordan Mechner между «свадьбой» и
  титрами (BUGS_CLOSED#cutscene-ltr).

Формат POP.HOF — 6 записей, имя до 15 символов, версия 2. Проверено в MAME
на сборке LEVEL=14: HAIL → титульная картинка → таблица с полосой ввода и
временем справа → ввод имени → титульная картинка; на следующем запуске
таблица показывается между credits и demo.

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

435 lines
19 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_ui.c — текст, палитровый fade и full-screen примитивы (FG5, банк 9).
*
* Шрифты извлечены из SDLPoP в FONT\FONT.ATL. Внутри map/unmap разрешены
* лишь обращения к EMM/W0: всё, что ждёт кадр или трогает BIOS-палитру,
* выполняется строго после unmap. Это общий контракт для menu/title/PV.
*/
#include <stdint.h>
#include <string.h>
#include <kbd_raw.h>
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_bg.h" /* POP_YOFF/POP_PLAYFIELD_H — граница игрового поля */
#include "pop_font.h"
#include "pop_arc.h"
#include "pop_ui.h"
#include "pop_status.h"
typedef struct {
uint8_t base;
uint8_t ascent;
uint8_t space;
uint8_t first; /* первый код, который есть в наборе */
uint8_t last;
} pop_font_desc_t;
/* Тёмный набор — тот же мелкий шрифт другим цветом; глифов в нём меньше
* (см. pop_font.h), поэтому диапазон кодов хранится у каждого набора. */
static const pop_font_desc_t font_desc[] = {
{ POP_FONT_SMALL_BASE, POP_FONT_SMALL_ASCENT, POP_FONT_SMALL_SPACE,
POP_FONT_FIRST, POP_FONT_LAST },
{ POP_FONT_BIG_BASE, POP_FONT_BIG_ASCENT, POP_FONT_BIG_SPACE,
POP_FONT_FIRST, POP_FONT_LAST },
{ POP_FONT_DARK_BASE, POP_FONT_BIG_ASCENT, POP_FONT_BIG_SPACE,
POP_FONT_DARK_FIRST, POP_FONT_DARK_LAST }
};
static atlas_t font_at;
static uint8_t font_ready;
/* БУФЕР ПАЛИТРЫ ПО ФИКСИРОВАННОМУ АДРЕСУ 0x4000. DSS занимает эти 256
* байт только при загрузке программы, дальше они наши (код начинается с
* 0x4100), и держать буфер здесь дешевле, чем класть 256 байт на стек в
* каждом вызове — идея пользователя, 2026-08-25. */
__at (0x4000) uint8_t pop_pal_buf[256];
static uint8_t text_index(const pop_font_desc_t *font, uint8_t ch)
{
if (ch < font->first || ch > font->last) ch = '?';
return (uint8_t)(font->base + ch - font->first);
}
int pop_ui_init(void) __banked
{
if (font_ready) return 0;
if (atlas_load(&font_at, "FONT\\font.atl") != 0) return -1;
font_ready = 1;
return 0;
}
void pop_ui_free(void) __banked
{
if (!font_ready) return;
atlas_free(&font_at);
font_ready = 0;
}
uint8_t pop_ui_ready(void)
{
return font_ready;
}
void pop_text_map(void)
{
gfx_w0_map(font_at.page);
}
void pop_text_unmap(void)
{
gfx_w0_unmap();
}
uint8_t pop_text_width_mapped(pop_text_font_t font, const char *s)
{
const pop_font_desc_t *desc = &font_desc[font];
uint8_t width = 0;
while (*s) {
const uint8_t *img = (const uint8_t *)atlas_image(&font_at,
text_index(desc, (uint8_t)*s++));
width = (uint8_t)(width + img[0] + desc->space);
}
return width ? (uint8_t)(width - desc->space) : 0;
}
void pop_text_draw_mapped(pop_text_font_t font, int x, int baseline,
const char *s)
{
const pop_font_desc_t *desc = &font_desc[font];
while (*s) {
const uint8_t *img = (const uint8_t *)atlas_image(&font_at,
text_index(desc, (uint8_t)*s++));
/* Пробел не рисует НИ ОДНОГО пикселя: его глиф целиком 0xFF, а
* текст всегда идёт прозрачным банком. Блит при этом стоит те же
* ~4,6 тыс. тактов, что и буква, поэтому в строке из полусотни
* символов пропуск пробелов — самая дешёвая экономия, какая тут
* есть (важно для статус-строк, они рисуются в игровом кадре).
* У крупного пробела вдобавок высота 0: для accel это 256 строк,
* и нулевой блит нельзя отдавать ядру даже noclip-вариантом. */
if (img[2] && s[-1] != ' ') gfx_blit_noclip(x, baseline - desc->ascent, img);
x += img[0] + desc->space;
}
}
void pop_text_center_mapped(pop_text_font_t font, int center, int baseline,
const char *s)
{
pop_text_draw_mapped(font, center - pop_text_width_mapped(font, s) / 2,
baseline, s);
}
void pop_text_right_mapped(pop_text_font_t font, int right, int baseline,
const char *s)
{
pop_text_draw_mapped(font, right - pop_text_width_mapped(font, s),
baseline, s);
}
/* W0 свободно: snapshot кладём в зарезервированный хвост страницы шрифта.
* 4 × 256 Б = ровно 1024 Б, [0x3C00,0x4000), и не пересекаются с атласом. */
/* Снимок палитры лежит в странице шрифта (в W2 килобайта нет). Копируем
* memcpy, а не циклом: у SDCC это LDIR — 256 байт за ~5 400 тактов против
* примерно ста тысяч у побайтового цикла на C. Именно этот цикл и делал
* пересчёт палитры шестикадровым (замер 2026-08-26). */
static void snapshot_chunk(uint8_t chunk, uint8_t *buf, uint8_t store)
{
uint8_t *slot;
gfx_w0_map(font_at.page);
slot = (uint8_t *)(POP_FONT_SNAPSHOT_OFF + (uint16_t)chunk * 256u);
if (store) memcpy(slot, buf, 256);
else memcpy(buf, slot, 256);
gfx_w0_unmap();
}
/* Снимок палитры целиком: дробить на мелкие порции больше не нужно —
* причина скрежета была не в длительности BIOS-вызова, а в том, какое окно
* он под себя забирал (см. pop_pal_buf выше). */
void pop_ui_palette_snapshot(void) __banked
{
uint8_t chunk;
if (!font_ready) return;
for (chunk = 0; chunk < 4; chunk++) {
gfx_pal_get(0, (uint8_t)(chunk << 6), 64, pop_pal_buf);
snapshot_chunk(chunk, pop_pal_buf, 1);
}
}
/* Из одного исходного снимка приготовить обе экранные палитры. Умножения
* нет: шкала 1/4, 1/2, 3/4 на 8-битном значении не должна тащить делитель
* в постоянно вызываемый (во время fade) путь. */
/* ВОСЕМЬ СТУПЕНЕЙ, а не четыре. Fade у оригинала идёт 64 шагами по два
* тика — 2,13 с плавного затухания; наши четыре ступени укладывались в
* доли секунды и выглядели как моргание, а растянуть их по времени значило
* получить четыре заметных скачка яркости.
*
* Яркость ступени — простое умножение: (v * (8 - step)) >> 3. Через
* switch с парами сдвигов выходило дороже: 768 каналов на вызов, и цепочка
* сравнений SDCC съедала больше, чем стоит само умножение.
*
* dim_step: 0 — исходная, 8 — чёрная. */
void pop_ui_palette_dim(uint8_t dim_step, uint8_t keep_ui) __banked
{
/* ТАБЛИЦА ЯРКОСТИ — НА СТЕКЕ, и это главное здесь по времени.
*
* Замер в MAME (2026-08-26): вызов стоил 2,44 млн тактов, то есть почти
* шесть кадров, из которых на BIOS-заливку палитры уходило всего 136
* тысяч (8 x 17 000). Остальное съедал пересчёт: 768 умножений
* uint16 в кадровом цикле — на Z80 это вызов библиотечной функции на
* каждый канал. Со ста вызовами подряд (fade кадр за кадром) набегало
* больше десяти секунд — ровно то «очень долгое затемнение», которое
* поймал пользователь.
*
* Теперь таблица строится один раз на ступень (256 сложений, без
* единого умножения), а каналы её просто читают. 256 байт стека —
* дешёвая плата: в этой точке израсходована едва сотня байт из 1279.
*
* Вместе с memcpy для снимка это дало 487 тысяч тактов на вызов против
* 2,44 млн (замер в MAME 2026-08-26). */
uint8_t lut[256];
uint8_t chunk, i, start;
uint8_t *buf = pop_pal_buf;
if (!font_ready) return;
if (dim_step > POP_PAL_DIM_BLACK) dim_step = POP_PAL_DIM_BLACK;
if (dim_step) {
uint8_t mul = (uint8_t)(POP_PAL_DIM_BLACK - dim_step);
uint16_t v = 0;
/* Без умножения вовсе: значение растёт на mul/32 с каждым шагом,
* дробная часть копится в младших пяти битах аккумулятора. */
for (i = 0; ; i++) {
lut[i] = (uint8_t)(v >> 5);
v += mul;
if (i == 255) break;
}
}
for (chunk = 0; chunk < 4; chunk++) {
start = (uint8_t)(chunk << 6);
snapshot_chunk(chunk, buf, 0);
if (dim_step) {
uint8_t *p = buf;
for (i = 0; i < 64; i++, p += 4) {
uint8_t idx = (uint8_t)(start + i);
if (keep_ui && idx >= 0x37 && idx <= 0x3F) continue;
p[0] = lut[p[0]];
p[1] = lut[p[1]];
p[2] = lut[p[2]];
}
}
gfx_pal_load(0, start, 64, buf);
gfx_pal_load(1, start, 64, buf);
}
}
void pop_ui_palette_restore(void) __banked
{
pop_ui_palette_dim(0, 0);
}
void pop_ui_palette_black(void) __banked
{
uint8_t chunk;
uint16_t i;
uint8_t *black = pop_pal_buf;
/* BIOS принимает максимум 64 записи за вызов; одним буфером покрываем
* все четыре четверти палитры, не занимая постоянную память. */
for (i = 0; i < sizeof(black); i++) black[i] = 0;
for (chunk = 0; chunk < 4; chunk++) {
uint8_t start = (uint8_t)(chunk << 6);
gfx_pal_load(0, start, 64, black);
gfx_pal_load(1, start, 64, black);
}
}
/* ДЛИТЕЛЬНОСТЬ ЗАДАЁТСЯ КАДРАМИ, А ПАЛИТРА ТРОГАЕТСЯ ВОСЕМЬ РАЗ.
*
* frames — сколько кадров луча длится затухание целиком (у оригинала это
* 2,13 с). Наивный цикл «ступень на каждый кадр» звал pop_ui_palette_dim
* сотню раз, а он не бесплатный: 4 чтения снимка по 256 байт, 768 каналов
* арифметики и 8 вызовов BIOS на палитру — около 9 мс, то есть половина
* кадра сверху на каждой итерации. Fade растягивался втрое против
* заказанного (поймано пользователем: «один fade in секунд восемь»).
* Теперь ступень пересчитывается только когда она СМЕНИЛАСЬ. */
static void fade_run(uint8_t frames, uint8_t up)
{
uint8_t i, shown = 0xFF;
if (!frames) {
pop_ui_palette_dim(up ? 0 : POP_PAL_DIM_BLACK, 0);
return;
}
for (i = 1; i <= frames; i++) {
uint8_t step = (uint8_t)((uint16_t)i * POP_PAL_DIM_BLACK / frames);
if (up) step = (uint8_t)(POP_PAL_DIM_BLACK - step);
if (step != shown) {
pop_ui_palette_dim(step, 0);
shown = step;
}
gfx_wait_vsync();
}
/* Довести до края: целочисленное деление могло не дойти до 8 (или 0). */
if (shown != (up ? 0 : POP_PAL_DIM_BLACK))
pop_ui_palette_dim(up ? 0 : POP_PAL_DIM_BLACK, 0);
}
void pop_ui_fade_out(uint8_t steps) __banked
{
pop_ui_palette_snapshot();
fade_run(steps, 0);
}
void pop_ui_fade_in(uint8_t steps) __banked
{
if (!steps) { pop_ui_palette_restore(); return; }
fade_run(steps, 1);
}
uint8_t pop_screen_begin(void) __banked
{
uint8_t page = (uint8_t)(gfx_get_visible_page() ^ 1);
gfx_set_draw_page(page);
gfx_set_bank(GFX_BANK_NORMAL);
return page;
}
void pop_screen_fill(uint8_t color) __banked
{
setfillstyle(SOLID_FILL, color);
bar(0, 0, GFX_WIDTH - 1, GFX_HEIGHT - 1);
}
/* Экран под ИГРУ: вне поля — цвет POP_COL_OUTSIDE, внутри — цвет 0.
*
* Зачем два разных чёрных. Вспышки (молния Джафара, урон, зелья) сделаны
* подменой ЦВЕТА 0 всей палитры — как в оригинале. Пока борта и полоса HP
* тоже были нулём, мигал весь экран целиком, вместе с рамкой и жизнями.
* Красим всё, что вне поля 320x192, отдельным индексом, а в палитре держим
* его чёрным (pop_pal_outside_apply): картинка та же, но вспышка достаёт
* только игровую область. Идея пользователя, 2026-08-25.
*
* Индексы 1..15 у нас свободны: атласы кладут либо 0xFF (прозрачность),
* либо PAL_BASE+v (0x40 и выше), шрифт — 0x3F. */
void pop_screen_fill_game(void) __banked
{
setfillstyle(SOLID_FILL, POP_COL_OUTSIDE);
bar(0, 0, GFX_WIDTH - 1, GFX_HEIGHT - 1);
setfillstyle(SOLID_FILL, BLACK);
bar(0, POP_YOFF, GFX_WIDTH - 1, POP_CLIP_BOTTOM - 1);
/* Заливка съела и обе служебные полосы — они ВНЕ игрового поля.
* Инвалидируем здесь, а не у вызывающих: точек заливки несколько
* (старт игры, смена уровня), и пропустить одну из них — ровно тот
* баг, из-за которого строка исчезала при переходе между комнатами. */
pop_status_invalidate();
}
/* Только ИГРОВОЕ ПОЛЕ, без бортов. Вход в комнату перерисовывает лишь
* его: борта от комнаты к комнате не меняются, а всё, что на них живёт —
* полоса HP, статус-строка, отладочная строка — обязано вход пережить.
* Заодно это на четверть меньше заливки (192 строки вместо 256). */
void pop_screen_fill_field(void) __banked
{
setfillstyle(SOLID_FILL, BLACK);
bar(0, POP_YOFF, GFX_WIDTH - 1, POP_CLIP_BOTTOM - 1);
}
void pop_screen_present(uint8_t page) __banked
{
gfx_wait_vsync();
gfx_set_visible_page(page);
}
/* transition_ltr() SDLPoP: подготовленная СКРЫТАЯ страница переползает на
* видимую узкими вертикальными полосами слева направо. У оригинала
* 2px/120 FPS; здесь 4px/60 FPS даёт ту же длительность (~1,3 с), не
* удваивая число полных копий 320x256.
*
* Страницы при этом НЕ меняются местами: копируем в видимую, как оригинал
* копирует offscreen в onscreen. Поэтому у вызывающего после перехода на
* ЭКРАНЕ и в скрытой странице лежит одно и то же. */
void pop_screen_present_ltr(void) __banked
{
gfx_rect_t stripe;
uint8_t visible = gfx_get_visible_page();
int x;
gfx_set_draw_page(visible); /* source gfx_copy_page = target */
stripe.y = 0;
stripe.h = GFX_HEIGHT;
for (x = 0; x < GFX_WIDTH; x += 4) {
if (kbd_raw_any_down()) {
/* Прервали — дорисовать остаток разом, а не бросить полукадр. */
stripe.x = 0; stripe.w = GFX_WIDTH;
gfx_copy_page(&stripe, GFX_COPY_DIRECT);
return;
}
stripe.x = x;
stripe.w = (uint16_t)((GFX_WIDTH - x < 4) ? GFX_WIDTH - x : 4);
gfx_copy_page(&stripe, GFX_COPY_DIRECT);
gfx_wait_vsync();
}
}
/* Нарисовать одну полосу и отпустить её страницу. Общая часть обоих
* загрузчиков ниже: они отличаются только тем, откуда взялся атлас. */
static int strip_blit(atlas_t *at, int x, int *y)
{
const uint8_t *img;
gfx_w0_map(at->page);
img = (const uint8_t *)atlas_image(at, 0);
/* gfx_blit_noclip ограничен шириной uint8 и обрезает 320px. */
gfx_blit(x, *y, img);
*y += img[2];
gfx_w0_unmap();
atlas_free(at);
return 0;
}
/* Полноэкранные изображения не помещаются одним getimage в EMM-страницу.
* Общий загрузчик полос держит лишь одну страницу и не трогает page flip:
* caller уже собрал скрытую страницу и сам решает, когда её показать. */
int pop_screen_blit_atlas_strips(const char * const *parts,
uint8_t count, int x, int y) __banked
{
uint8_t part;
gfx_set_bank(GFX_BANK_NORMAL);
for (part = 0; part < count; part++) {
atlas_t at;
if (atlas_load(&at, parts[part]) != 0) return -1;
strip_blit(&at, x, &y);
}
return 0;
}
/* То же самое, но полосы лежат подряд в АРХИВЕ (заставка, story, PV): один
* `open` на весь экран вместо пяти — при цене открытия 51,4 мс против
* 32,6 мс на чтение это больше половины времени показа. */
int pop_screen_blit_atlas_arc(pop_arc_t *arc, const uint8_t *tbl,
uint8_t base, uint8_t count,
int x, int y) __banked
{
uint8_t part;
gfx_set_bank(GFX_BANK_NORMAL);
for (part = 0; part < count; part++) {
atlas_t at;
if (pop_arc_atlas(arc, tbl, (uint8_t)(base + part), &at) != 0)
return -1;
strip_blit(&at, x, &y);
}
return 0;
}