Files
Sprinter-SDCC/applications/PoP/roomtest/pop_ui.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

395 lines
17 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 <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;
} pop_font_desc_t;
static const pop_font_desc_t font_desc[] = {
{ POP_FONT_SMALL_BASE, POP_FONT_SMALL_ASCENT, POP_FONT_SMALL_SPACE },
{ POP_FONT_BIG_BASE, POP_FONT_BIG_ASCENT, POP_FONT_BIG_SPACE }
};
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 < POP_FONT_FIRST || ch > POP_FONT_LAST) ch = '?';
return (uint8_t)(font->base + ch - POP_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/16 с каждым шагом,
* дробная часть копится в младших четырёх битах аккумулятора. */
for (i = 0; ; i++) {
lut[i] = (uint8_t)(v >> 4);
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);
}
/* Нарисовать одну полосу и отпустить её страницу. Общая часть обоих
* загрузчиков ниже: они отличаются только тем, откуда взялся атлас. */
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;
}