Files
Sprinter-SDCC/applications/PoP/roomtest/pop_ui.c
T
snark13 65797af44c Ресурсы — в архивы 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>
2026-08-25 21:29:14 +03:00

341 lines
13 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 <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), и не пересекаются с атласом. */
static void snapshot_chunk(uint8_t chunk, uint8_t *buf, uint8_t store)
{
uint16_t i;
uint8_t *slot;
gfx_w0_map(font_at.page);
slot = (uint8_t *)(POP_FONT_SNAPSHOT_OFF + (uint16_t)chunk * 256u);
for (i = 0; i < 256; i++) {
if (store) slot[i] = buf[i];
else buf[i] = slot[i];
}
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) путь. */
void pop_ui_palette_dim(uint8_t dim_step, uint8_t keep_ui) __banked
{
uint8_t chunk, i, start;
uint8_t *buf = pop_pal_buf;
if (!font_ready) return;
if (dim_step > 4) dim_step = 4;
for (chunk = 0; chunk < 4; chunk++) {
start = (uint8_t)(chunk << 6);
snapshot_chunk(chunk, buf, 0);
if (dim_step) {
for (i = 0; i < 64; i++) {
uint8_t idx = (uint8_t)(start + i);
uint16_t p = (uint16_t)i * 4u;
if (keep_ui && idx >= 0x37 && idx <= 0x3F) continue;
if (dim_step == 1) {
buf[p] = (uint8_t)((buf[p] >> 1) + (buf[p] >> 2));
buf[p + 1] = (uint8_t)((buf[p + 1] >> 1) +
(buf[p + 1] >> 2));
buf[p + 2] = (uint8_t)((buf[p + 2] >> 1) +
(buf[p + 2] >> 2));
} else if (dim_step == 2) {
buf[p] >>= 1; buf[p + 1] >>= 1; buf[p + 2] >>= 1;
} else if (dim_step == 3) {
buf[p] >>= 2; buf[p + 1] >>= 2; buf[p + 2] >>= 2;
} else {
buf[p] = buf[p + 1] = buf[p + 2] = 0;
}
}
}
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);
}
}
void pop_ui_fade_out(uint8_t steps) __banked
{
uint8_t i;
if (!steps) return;
pop_ui_palette_snapshot();
for (i = 1; i <= steps; i++) {
uint8_t step = (uint8_t)((uint16_t)i * 4u / steps);
pop_ui_palette_dim(step, 0);
gfx_wait_vsync();
}
}
void pop_ui_fade_in(uint8_t steps) __banked
{
uint8_t i;
if (!steps) { pop_ui_palette_restore(); return; }
for (i = steps; i > 0; i--) {
uint8_t step = (uint8_t)((uint16_t)(i - 1) * 4u / steps);
pop_ui_palette_dim(step, 0);
gfx_wait_vsync();
}
}
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;
}