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>
This commit is contained in:
2026-08-26 10:14:55 +03:00
parent 0db4f94707
commit 5ede8b9045
5 changed files with 119 additions and 50 deletions
+17 -2
View File
@@ -899,10 +899,25 @@ load_intro у оригинала и так начинается с ожидан
тика, 128 тиков = 2,13 с; сцена перед уровнем 2 занимает с ними около семи тика, 128 тиков = 2,13 с; сцена перед уровнем 2 занимает с ними около семи
секунд. Наши четыре ступени укладывались в восемь сотых секунды, и сцена секунд. Наши четыре ступени укладывались в восемь сотых секунды, и сцена
выходила втрое короче. Теперь `INTRO_FADE = POP_T60(128)`, а ступеней в выходила втрое короче. Теперь `INTRO_FADE = POP_T60(128)`, а ступеней в
`pop_ui_palette_dim` восемь вместо четырёх (7/8, 3/4, 5/8, 1/2, 3/8, 1/4, `pop_ui_palette_dim` шестнадцать вместо четырёх: на четырёх растянутых
1/8, 0 — каждая парой сдвигов, без умножения): на четырёх растянутых
ступенях затемнение выглядело бы скачками. ступенях затемнение выглядело бы скачками.
**Цена ступени** (замеры в MAME 2026-08-26, такты 21 МГц; кадр 430 000):
| версия | такты | что изменилось |
|--------|-------|----------------|
| исходная | 2 440 000 | снимок копировался побайтовым циклом на C |
| + таблица яркости на стеке | 1 250 000 | 768 умножений uint16 заменены 256 сложениями |
| + memcpy для снимка | 487 000 | LDIR вместо цикла — главный выигрыш |
Из оставшихся 487 тысяч 136 тысяч — заливка палитры через BIOS (8 вызовов
`gfx_pal_load` по 17 000). Дальше можно было бы хранить готовые таблицы
яркости файлом, но при 1,2 мс на построение это уже незаметно.
ВАЖНО: ступень пересчитывается только когда она СМЕНИЛАСЬ. Наивный цикл
«ступень на каждый кадр» звал пересчёт сто раз и растягивал fade до
десяти с лишним секунд.
**Чего пока нет.** Финальный `won` (56): 115 с, 1,2 МБ, 78 страниц EMM — **Чего пока нет.** Финальный `won` (56): 115 с, 1,2 МБ, 78 страниц EMM —
в `POP_MUS_PAGES` (20) он не помещается даже теоретически. Ему нужен в `POP_MUS_PAGES` (20) он не помещается даже теоретически. Ему нужен
КОЛЬЦЕВОЙ стриминг: держать в памяти несколько страниц и дочитывать в те, КОЛЬЦЕВОЙ стриминг: держать в памяти несколько страниц и дочитывать в те,
+3 -3
View File
@@ -31,9 +31,9 @@
* 128 тиков, то есть 2,13 с (fade_in_1/fade_out_1 -> fade_*_frame -> * 128 тиков, то есть 2,13 с (fade_in_1/fade_out_1 -> fade_*_frame ->
* do_simple_wait(1) при wait_time = 2). Наши четыре кадра укладывались в * do_simple_wait(1) при wait_time = 2). Наши четыре кадра укладывались в
* восемь сотых секунды, и вся сцена выходила втрое короче оригинальной. * восемь сотых секунды, и вся сцена выходила втрое короче оригинальной.
* Число здесь — кадры луча, ступени яркости внутри распределяет * Точное число кадров считает POP_FADE_FRAMES (pop_ui.h): из
* pop_ui_fade_*. */ * заказанных 2,13 с вычитается то, что съедает сам пересчёт палитры. */
#define INTRO_FADE POP_T60(128) #define INTRO_FADE POP_FADE_FRAMES
/* Оцифрованные эффекты pv_scene() (SDLPoP seg001:422): створка ворот /* Оцифрованные эффекты pv_scene() (SDLPoP seg001:422): створка ворот
* закрывается, затем открывается дверь покоев, из которой входит Джафар. */ * закрывается, затем открывается дверь покоев, из которой входит Джафар. */
+1 -1
View File
@@ -26,7 +26,7 @@
#define TITLE_H 200 #define TITLE_H 200
#define TITLE_Y POP_YOFF #define TITLE_Y POP_YOFF
#define TITLE_PARTS 5 #define TITLE_PARTS 5
#define TITLE_FADE POP_T60(128) /* 64 шага по 2 тика, как fade_*_1 */ #define TITLE_FADE POP_FADE_FRAMES /* 2,13 с, как fade_*_1 оригинала */
enum { enum {
TITLE_MAIN = 0, TITLE_MAIN = 0,
+82 -40
View File
@@ -6,6 +6,7 @@
* выполняется строго после unmap. Это общий контракт для menu/title/PV. * выполняется строго после unmap. Это общий контракт для menu/title/PV.
*/ */
#include <stdint.h> #include <stdint.h>
#include <string.h>
#include <graphics.h> #include <graphics.h>
#include <gfx.h> #include <gfx.h>
#include <sprite.h> #include <sprite.h>
@@ -120,17 +121,18 @@ void pop_text_right_mapped(pop_text_font_t font, int right, int baseline,
/* W0 свободно: snapshot кладём в зарезервированный хвост страницы шрифта. /* W0 свободно: snapshot кладём в зарезервированный хвост страницы шрифта.
* 4 × 256 Б = ровно 1024 Б, [0x3C00,0x4000), и не пересекаются с атласом. */ * 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) static void snapshot_chunk(uint8_t chunk, uint8_t *buf, uint8_t store)
{ {
uint16_t i;
uint8_t *slot; uint8_t *slot;
gfx_w0_map(font_at.page); gfx_w0_map(font_at.page);
slot = (uint8_t *)(POP_FONT_SNAPSHOT_OFF + (uint16_t)chunk * 256u); slot = (uint8_t *)(POP_FONT_SNAPSHOT_OFF + (uint16_t)chunk * 256u);
for (i = 0; i < 256; i++) { if (store) memcpy(slot, buf, 256);
if (store) slot[i] = buf[i]; else memcpy(buf, slot, 256);
else buf[i] = slot[i];
}
gfx_w0_unmap(); gfx_w0_unmap();
} }
@@ -155,43 +157,62 @@ void pop_ui_palette_snapshot(void) __banked
/* ВОСЕМЬ СТУПЕНЕЙ, а не четыре. Fade у оригинала идёт 64 шагами по два /* ВОСЕМЬ СТУПЕНЕЙ, а не четыре. Fade у оригинала идёт 64 шагами по два
* тика — 2,13 с плавного затухания; наши четыре ступени укладывались в * тика — 2,13 с плавного затухания; наши четыре ступени укладывались в
* доли секунды и выглядели как моргание, а растянуть их по времени значило * доли секунды и выглядели как моргание, а растянуть их по времени значило
* получить четыре заметных скачка яркости. Восемь ступеней собираются * получить четыре заметных скачка яркости.
* пáрой сдвигов каждая (умножения на Z80 не нужно), а по времени их *
* распределяет вызывающий (pop_ui_fade_*). * Яркость ступени — простое умножение: (v * (8 - step)) >> 3. Через
* switch с парами сдвигов выходило дороже: 768 каналов на вызов, и цепочка
* сравнений SDCC съедала больше, чем стоит само умножение.
* *
* dim_step: 0 — исходная, 8 — чёрная. */ * dim_step: 0 — исходная, 8 — чёрная. */
static uint8_t dim_value(uint8_t v, uint8_t step)
{
switch (step) {
case 1: return (uint8_t)(v - (v >> 3)); /* 7/8 */
case 2: return (uint8_t)((v >> 1) + (v >> 2)); /* 3/4 */
case 3: return (uint8_t)((v >> 1) + (v >> 3)); /* 5/8 */
case 4: return (uint8_t)(v >> 1); /* 1/2 */
case 5: return (uint8_t)((v >> 2) + (v >> 3)); /* 3/8 */
case 6: return (uint8_t)(v >> 2); /* 1/4 */
case 7: return (uint8_t)(v >> 3); /* 1/8 */
default: return 0; /* 8 и выше */
}
}
void pop_ui_palette_dim(uint8_t dim_step, uint8_t keep_ui) __banked 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 chunk, i, start;
uint8_t *buf = pop_pal_buf; uint8_t *buf = pop_pal_buf;
if (!font_ready) return; if (!font_ready) return;
if (dim_step > POP_PAL_DIM_BLACK) dim_step = POP_PAL_DIM_BLACK; 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++) { for (chunk = 0; chunk < 4; chunk++) {
start = (uint8_t)(chunk << 6); start = (uint8_t)(chunk << 6);
snapshot_chunk(chunk, buf, 0); snapshot_chunk(chunk, buf, 0);
if (dim_step) { if (dim_step) {
for (i = 0; i < 64; i++) { uint8_t *p = buf;
for (i = 0; i < 64; i++, p += 4) {
uint8_t idx = (uint8_t)(start + i); uint8_t idx = (uint8_t)(start + i);
uint16_t p = (uint16_t)i * 4u;
if (keep_ui && idx >= 0x37 && idx <= 0x3F) continue; if (keep_ui && idx >= 0x37 && idx <= 0x3F) continue;
buf[p] = dim_value(buf[p], dim_step); p[0] = lut[p[0]];
buf[p + 1] = dim_value(buf[p + 1], dim_step); p[1] = lut[p[1]];
buf[p + 2] = dim_value(buf[p + 2], dim_step); p[2] = lut[p[2]];
} }
} }
gfx_pal_load(0, start, 64, buf); gfx_pal_load(0, start, 64, buf);
@@ -220,27 +241,48 @@ void pop_ui_palette_black(void) __banked
} }
} }
void pop_ui_fade_out(uint8_t steps) __banked /* ДЛИТЕЛЬНОСТЬ ЗАДАЁТСЯ КАДРАМИ, А ПАЛИТРА ТРОГАЕТСЯ ВОСЕМЬ РАЗ.
*
* 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; uint8_t i, shown = 0xFF;
if (!steps) return;
pop_ui_palette_snapshot(); if (!frames) {
for (i = 1; i <= steps; i++) { pop_ui_palette_dim(up ? 0 : POP_PAL_DIM_BLACK, 0);
uint8_t step = (uint8_t)((uint16_t)i * POP_PAL_DIM_BLACK / steps); return;
pop_ui_palette_dim(step, 0); }
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(); 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 void pop_ui_fade_in(uint8_t steps) __banked
{ {
uint8_t i;
if (!steps) { pop_ui_palette_restore(); return; } if (!steps) { pop_ui_palette_restore(); return; }
for (i = steps; i > 0; i--) { fade_run(steps, 1);
uint8_t step = (uint8_t)((uint16_t)(i - 1) * POP_PAL_DIM_BLACK / steps);
pop_ui_palette_dim(step, 0);
gfx_wait_vsync();
}
} }
uint8_t pop_screen_begin(void) __banked uint8_t pop_screen_begin(void) __banked
+16 -4
View File
@@ -35,12 +35,24 @@ void pop_text_right_mapped(pop_text_font_t font, int right, int baseline,
/* Снимок обеих (идентичных по контракту) палитр хранится в свободном хвосте /* Снимок обеих (идентичных по контракту) палитр хранится в свободном хвосте
* FONT.ATL. dim_step: 0 — исходная, POP_PAL_DIM_BLACK — чёрная; между ними * FONT.ATL. dim_step: 0 — исходная, POP_PAL_DIM_BLACK — чёрная; между ними
* восемь равных ступеней (7/8, 3/4, 5/8, 1/2, 3/8, 1/4, 1/8). Восемь, а не * равные ступени яркости (step/16). Шестнадцать, а не четыре, потому что
* четыре, потому что fade оригинала длится 2,13 с: на четырёх ступенях это * fade оригинала длится 2,13 с: на четырёх ступенях это четыре заметных
* четыре заметных скачка яркости. * скачка, а не затухание.
* keep_ui сохраняет диапазон 0x37..0x3F ярким для текста/рамки меню. */ * keep_ui сохраняет диапазон 0x37..0x3F ярким для текста/рамки меню. */
void pop_ui_palette_snapshot(void) __banked; void pop_ui_palette_snapshot(void) __banked;
#define POP_PAL_DIM_BLACK 8 #define POP_PAL_DIM_BLACK 16
/* СКОЛЬКО КАДРОВ ЖДАТЬ, чтобы fade длился столько же, сколько у оригинала
* (fade_in_1/fade_out_1 — 64 шага по 2 тика = 128 тиков = 2,13 с, то есть
* 106 кадров луча).
*
* Ждать все 106 нельзя: пересчёт палитры не бесплатен — 487 тысяч тактов
* на ступень, то есть чуть больше кадра (замер в MAME 2026-08-26; из них
* 136 тысяч уходит на заливку через BIOS). Семнадцать ступеней съедают
* около двадцати кадров, и без вычитания fade растянулся бы длиннее
* заказанного. */
#define POP_FADE_STEP_FRAMES 1 /* цена одного пересчёта, кадров луча */
#define POP_FADE_FRAMES (106 - (POP_PAL_DIM_BLACK + 1) * POP_FADE_STEP_FRAMES)
void pop_ui_palette_dim(uint8_t dim_step, uint8_t keep_ui) __banked; void pop_ui_palette_dim(uint8_t dim_step, uint8_t keep_ui) __banked;
void pop_ui_palette_restore(void) __banked; void pop_ui_palette_restore(void) __banked;