Зеркальные кадры персонажей — готовыми файлами, а не разворотом в рантайме

Замеры в MAME (такты Z80, кадр = 430 000) решили вопрос однозначно:
  загрузка 28 атласов Кида с HDD  — 27 160 092 такта (1.26 с), 970 003 на страницу;
  разворот ОДНОЙ страницы на месте — 12 645 582 такта (0.59 с), т.е. 771 такт/байт.
Даже идеальный вариант (копия accel'ом ~0.25 кадра + asm-реверс ~2.2 кадра)
дал бы ~1.05 млн на страницу — то есть в лучшем случае сравнялся бы с чтением
готового файла.  На 34 страницы: 1.5 с загрузки против 20 с разворота.

* Упаковщики пишут второй набор: vflip_cols/vflip_page в pop_pack_kid.py ->
  kid0_v.atl…kid27_v.atl, sword_v.atl; pop_pack_guard.py -> g0_v.atl…g4_v.atl
  ТОЛЬКО для обычного стража (на уровне 9 tbl_guard_type = 0; тень рисуется
  атласами Кида, скелет/визирь с переворотом не встречаются).
* pop_vflip.c схлопнулся до таблицы «страница -> зеркальная пара»; весь
  побайтовый разворот и аллокация EMM удалены.
* pop_vflip_load_all (банк 8) грузит набор ОДНИМ вызовом и откуда угодно —
  задел под загрузку ресурсов во время интро.  Зовётся при старте и при
  смене уровня, если это POP_UPSIDE_LEVEL; чит U на других уровнях поднимает
  набор лениво (страховка в главном цикле).
* Стартовый уровень стал параметром сборки: make LEVEL=N (-DFIRST_LEVEL).

Проверено в MAME на уровне 9: переворот мгновенный, pop_flip_screen = 897 263
такта (2.1 кадра) — это сама копия экрана, загрузки в кадре больше нет.
tests-host 6/6.
This commit is contained in:
2026-08-12 23:17:22 +03:00
parent c4dd2c7a87
commit 5b6664ffce
48 changed files with 219 additions and 107 deletions
+29 -85
View File
@@ -1,111 +1,55 @@
/*
* pop_vflip.c — ЗЕРКАЛЬНЫЕ ПО ВЕРТИКАЛИ копии страниц атласов персонажей
* pop_vflip.c — таблица «страница атласа -> её ВЕРТИКАЛЬНО ЗЕРКАЛЬНАЯ пара»
* (зелье инверсии, уровень 9). См. pop_vflip.h.
*
* Зачем отдельный набор страниц. Спрайты персонажей хранятся COLUMN-MAJOR
* (out[col*h + row], transpose_cols в pop_pack_kid.py) — ради БЕСПЛАТНОГО
* горизонтального флипа: accel копирует колонки, и порядок колонок задаётся
* знаком страйда. А вертикальное зеркало у такой раскладки — это разворот
* байтов ВНУТРИ колонки, чего accel не умеет: блок он копирует только
* вперёд. Значит перевёрнутые кадры надо готовить заранее.
* Сами зеркальные ленты делает упаковщик (`vflip_cols` в pop_pack_kid.py) и
* кладёт рядом с обычными: kid0_v.atl, sword_v.atl, g0_v.atl… Здесь остаётся
* только связать пары: зеркальный файл грузится обычным atlas_load, а его
* раскладка байт-в-байт совпадает с оригиналом — значит движку достаточно
* подменить НОМЕР СТРАНИЦЫ, которую он мапит в W0.
*
* Устройство. Копия — ПОСТРАНИЧНАЯ и побайтово совпадает с оригиналом по
* раскладке: тот же заголовок .atl, тот же каталог, ленты на тех же
* смещениях; развёрнуты только сами пиксели. Поэтому вызывающему не нужно
* менять НИЧЕГО, кроме номера страницы, которую он мапит в W0 — все
* atlas_image-смещения и размеры кадров остаются прежними.
* Почему не считаем в рантайме (замеры MAME 2026-08-12, такты Z80):
* загрузка одной 16-КБ страницы с HDD — 970 003 такта (0.045 с);
* разворот той же страницы на месте — 12 645 582 такта (0.59 с).
* Даже идеальная конвертация (accel-копия + asm-реверс) даёт ~1.05 млн —
* то есть в лучшем случае сравнивается с чтением готового файла, а стоит
* asm-модуля. На все 34 страницы: 1.5 с загрузки против 20 с разворота.
*
* Кэш ЛЕНИВЫЙ: страница зеркалится при первом обращении к ней в
* перевёрнутом виде (~16 КБ копирования = заметно, но однократно на кадр
* анимации), дальше отдаётся из таблицы. Держится до конца уровня;
* pop_vflip_reset освобождает всё (смена уровня / зелье выпито обратно —
* см. pop_vflip.h про то, почему НЕ освобождаем при каждом перевороте).
*
* РЕЗИДЕНТ, а не банк: функция маппит окно W3 (приёмник копии), а банковый
* код в нём же и исполняется — правило из _pop_level.h.
* РЕЗИДЕНТ: pop_vflip_page зовёт отрисовка персонажей (банк 4) на каждый
* спрайт — трамплин тут ни к чему, а кода всего ничего.
*/
#include <stdint.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#include <sprite.h>
#include "pop_vflip.h"
/* Раскладка .atl (sprite.h §Атласы): 0x100-байтовый заголовок, каталог с
* 0x68 — по 8 байт на ленту: [0..1] смещение ленты, дальше размеры. */
#define ATL_DIR_OFF 0x68u
#define ATL_COUNT_OFF 0x04u /* число лент в заголовке */
/* Таблица кэша: страница-источник -> страница-копия. Персонажных атласов
* у нас 28 (Кид) + 5 (страж) + 1 (меч) = 34; берём с запасом. */
/* Персонажных страниц: 28 (Кид) + 5 (страж) + 1 (меч) = 34; с запасом. */
#define VF_MAX 40
static uint8_t vf_src[VF_MAX], vf_dst[VF_MAX], vf_blk[VF_MAX], vf_n;
static uint8_t vf_src[VF_MAX]; /* страница оригинала */
static atlas_t vf_atl[VF_MAX]; /* зеркальный атлас (его page) */
static uint8_t vf_n;
/* Развернуть ленту на месте В СТРАНИЦЕ-ПРИЁМНИКЕ (она замаплена в W3).
* off — смещение ленты от начала страницы; формат ленты: w, _, h, _, пиксели
* колонками по h байт. */
static void flip_strip(uint16_t off)
int pop_vflip_add(const atlas_t *orig, const char *path)
{
uint8_t *p = (uint8_t *)(0xC000u + off);
uint8_t w = p[0], h = p[2];
uint8_t *col = p + 4;
uint8_t c;
if (!w || !h) return;
for (c = 0; c < w; c++) {
uint8_t *a = col, *b = col + h - 1;
while (a < b) { uint8_t t = *a; *a++ = *b; *b-- = t; }
col += h;
}
}
/* Сделать зеркальную копию страницы src. Возврат номера копии или 0. */
static uint8_t make_flipped(uint8_t src)
{
uint8_t blk, dst, saved, count, i;
uint16_t k;
const uint8_t *s;
uint8_t *d;
blk = mem_alloc_pages(1);
if (!blk) return 0;
dst = mem_get_page(blk, 0);
/* Побайтовая копия страницы: источник в W0 (0x0000-0x3FFF), приёмник в
* W3 (0xC000). Оба окна свободны для резидентного кода W1/W2. */
saved = _io_page_w3;
gfx_w0_map(src);
sprinter_page_w3(dst);
s = (const uint8_t *)0x0000;
d = (uint8_t *)0xC000;
for (k = 0; k < 16384u; k++) d[k] = s[k];
gfx_w0_unmap();
/* Развернуть пиксели каждой ленты — уже в приёмнике (он в W3). */
count = ((const uint8_t *)0xC000)[ATL_COUNT_OFF];
for (i = 0; i < count; i++) {
const uint8_t *e = (const uint8_t *)(0xC000u + ATL_DIR_OFF) + (uint16_t)i * 8;
uint16_t off = (uint16_t)(e[0] | ((uint16_t)e[1] << 8));
if (off >= 0x100u && off < 0x4000u) flip_strip(off);
}
sprinter_page_w3(saved);
if (vf_n < VF_MAX) {
vf_src[vf_n] = src; vf_dst[vf_n] = dst; vf_blk[vf_n] = blk; vf_n++;
}
return dst;
if (!orig || vf_n >= VF_MAX) return -1;
if (atlas_load(&vf_atl[vf_n], path) != 0) return -1;
vf_src[vf_n] = orig->page;
vf_n++;
return 0;
}
uint8_t pop_vflip_page(uint8_t src)
{
uint8_t i;
for (i = 0; i < vf_n; i++)
if (vf_src[i] == src) return vf_dst[i];
return make_flipped(src);
if (vf_src[i] == src) return vf_atl[i].page;
return 0; /* пары нет — рисуем обычным атласом */
}
uint8_t pop_vflip_ready(void) { return vf_n != 0; }
void pop_vflip_reset(void)
{
uint8_t i;
for (i = 0; i < vf_n; i++) mem_free_block(vf_blk[i]);
for (i = 0; i < vf_n; i++) atlas_free(&vf_atl[i]);
vf_n = 0;
}