16d3262340
СИМПТОМ: затемнение (fade) хрипело — одинаково с играющей музыкой и в тишине. Ключевое наблюдение пользователя: повтор тишины обязан звучать тишиной, значит дело не в недоливе буфера. ПОИСК: отладочные клавиши, каждая делала ровно один кусок fade. Ожидание кадров — чисто; чтение палитры, запись палитры и 512 вызовов bios_get_place() (видео вообще не трогает) — скрежет во всех трёх. Последнее и решило: виновата не палитра, а ЛЮБОЙ вызов BIOS. ПРИЧИНА: `rst 8` раскрывается в `out ($7C),a`, который включает системное ПЗУ и перестраивает окно 0 (MAME sprinter.cpp, update_memory: m_pages[0] + m_bank_view0.select). ПЗУ ложится ПОВЕРХ страничного регистра, поэтому запись в порт 0x82 из прерывания бесполезна — OTIR вычитывает ПЗУ и отдаёт его в звук. Отсюда же старое правило «глушить CBL на время загрузки файлов»: причина была не в том, что ESTEX долго занимает CPU. РЕШЕНИЕ (идея пользователя): качать через W3. Он управляется только портом 0xE2, подмену из прерывания никто не перекрывает, а BIOS во время нашего ISR не исполняется — окно возвращается до выхода, и для него подмена невидима. После этого BIOS безопасен везде. * pop_sfx.c — насос берёт взаймы W3 вместо W0, чтение по 0xC000 + смещение. * pop_ui.c — буфер палитры по фиксированному 0x4000 (эти 256 байт DSS занимает только при загрузке программы): 256 байт со стека долой. * libbgi/common/gfx_pal_write.c — запись палитры прямо в видеопамять, минуя BIOS. Писалась как обход скрежета, после переноса насоса не нужна; оставлена как более быстрый примитив (2,5 тыс. тактов на 64 цвета против 10,8 тыс. у BIOS) с честной шапкой. Адресация разобрана по исходникам BIOS (FUNC_SCREEN.ASM): Port_Y = индекс цвета, адрес 0xC3E0 + pal*4, порядок R/G/B/Y. * libc/video/pal_get.c, pal_load.c — в шапках зафиксировано, что BIOS выбирает окно ПО АДРЕСУ БУФЕРА (`BIT 7,H`). * pop_intro.c — при пропуске интро клавишей не глушился CBL, и следующая загрузка уровня шла с открытым буфером; добавлен pop_sfx_pause. * pop_ctrl.c — убраны отладочные «осторожные шаги» на J/L (эмуляция Shift+стрелка для MAME), у них и стоял TODO. Разбор целиком — docs/sound_plan.md §5. Бюджет: _CODE 23966, куча 245 Б. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
311 lines
12 KiB
C
311 lines
12 KiB
C
/*
|
||
* 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_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);
|
||
}
|
||
|
||
/* Полноэкранные изображения не помещаются одним 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;
|
||
const uint8_t *img;
|
||
|
||
if (atlas_load(&at, parts[part]) != 0) return -1;
|
||
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;
|
||
}
|