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

Замеры в 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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+17 -2
View File
@@ -42,6 +42,11 @@ EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) --ban
# MAME = время фазы в растровых строках.
PROF ?= 1
PROF_FLAGS := -DPROF_BORDER=$(PROF)
# Стартовый уровень (отладка): make LEVEL=9 — начать сразу с девятого.
# Дефолт держим на отлаживаемом сейчас уровне; для «настоящей» игры — LEVEL=1.
# Константа объявлена в pop_tune.h (её видят обе половины главного цикла).
LEVEL ?= 9
PROF_FLAGS += -DFIRST_LEVEL=$(LEVEL)
EXTRA_SRCS := pop_vflip.c pop_state.c pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c
BG_DIR := $(CURDIR)/../poc/res/bg
@@ -67,16 +72,26 @@ BG_DATA := $(foreach s,$(BG_SETS),\
# не рисуются (отрисовка пропускает незагруженную страницу). Палитра —
# ЕДИНАЯ игровая (env+wall+kid).
KID_PAGES := $(foreach n,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27,$(KID_DIR)/kid$(n).atl)
KID_DATA := $(KID_PAGES) $(KID_DIR)/kid.pal $(KID_DIR)/sword.atl
# Зеркальный ПО ВЕРТИКАЛИ набор (зелье инверсии, уровень 9): те же спрайты с
# развёрнутыми колонками. Готовит упаковщик, а не рантайм — разворот
# страницы стоит 0.59 с против 0.045 с на её чтение с диска (замер MAME
# 2026-08-12, протокол в pop_vflip.c).
KID_VPAGES := $(foreach n,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27,$(KID_DIR)/kid$(n)_v.atl)
KID_DATA := $(KID_PAGES) $(KID_VPAGES) $(KID_DIR)/kid.pal \
$(KID_DIR)/sword.atl $(KID_DIR)/sword_v.atl
# Таблицы анимации (кадры+seqtbl) — отдельный бинарь в EMM-странице, его
# делает pop_extract_kid_data.py (не упаковщик спрайтов) — своё правило ниже.
KID_BIN := $(KID_DIR)/kid_data.bin
GUARD_DIR := $(CURDIR)/../poc/res/guard
GUARD_DATA := $(GUARD_DIR)/g0.atl $(GUARD_DIR)/g1.atl $(GUARD_DIR)/g2.atl \
$(GUARD_DIR)/g3.atl $(GUARD_DIR)/g4.atl
$(GUARD_DIR)/g3.atl $(GUARD_DIR)/g4.atl \
$(GUARD_DIR)/g0_v.atl $(GUARD_DIR)/g1_v.atl $(GUARD_DIR)/g2_v.atl \
$(GUARD_DIR)/g3_v.atl $(GUARD_DIR)/g4_v.atl
# Скелет — ОТДЕЛЬНЫЙ набор спрайтов (SKEL.DAT в оригинале): его грузит
# уровень 3 (tbl_guard_type == 2), кадров меньше (28 против 34).
SKEL_DIR := $(CURDIR)/../poc/res/skel
# Зеркального набора у скелета НЕТ: зелье инверсии — уровень 9, там обычный
# страж (tbl_guard_type[9] = 0). См. pop_pack_guard.py.
SKEL_DATA := $(SKEL_DIR)/g0.atl $(SKEL_DIR)/g1.atl $(SKEL_DIR)/g2.atl \
$(SKEL_DIR)/g3.atl
+25
View File
@@ -105,6 +105,31 @@ int pop_guard_load(void) __banked
return 0;
}
/* Зеркальный набор соперника — пара к pop_guard_load: те же страницы, но
* файлы *_v.atl (см. pop_vflip.h). Здесь, а не в загрузчике набора,
* потому что gp[] и выбор GUARD/SKEL живут именно тут. */
int pop_guard_vflip_load(void) __banked
{
static const char *const guard_v[GUARD_PAGES] = {
"GUARD\\g0_v.atl", "GUARD\\g1_v.atl", "GUARD\\g2_v.atl",
"GUARD\\g3_v.atl", "GUARD\\g4_v.atl"
};
static const char *const skel_v[GUARD_PAGES] = {
"SKEL\\g0_v.atl", "SKEL\\g1_v.atl", "SKEL\\g2_v.atl",
"SKEL\\g3_v.atl", 0
};
const char *const *names;
int8_t type = pop_level_guard_type(pop_current_level);
uint8_t i;
if (type < 0 || !g_ok) return 0; /* соперников на уровне нет */
names = (type == 2) ? skel_v : guard_v;
for (i = 0; i < GUARD_PAGES; i++) {
if (names[i] == 0) break;
if (pop_vflip_add(&gp[i], names[i]) != 0) return -1;
}
return 0;
}
void pop_guard_free(void) __banked
{
uint8_t i;
+5
View File
@@ -236,6 +236,11 @@ uint8_t pop_guard_present(void);
/* Атласы стража (GUARD\\g0..g4.atl) — грузить ДО initgraph, как Kid.
* 0 — OK, -1 — ошибка (тогда страж просто не рисуется). */
int pop_guard_load(void) __banked;
/* Зеркальный ПО ВЕРТИКАЛИ набор соперника (файлы *_v.atl) — для зелья
* инверсии; зовётся из pop_vflip_load_all (pop_vflip.h). Живёт рядом с
* pop_guard_load: там же gp[] и выбор GUARD/SKEL. */
int pop_guard_vflip_load(void) __banked;
void pop_guard_free(void) __banked;
/* Вход в комнату: поднять стража из данных уровня (порт enter_guard,
+28
View File
@@ -14,6 +14,8 @@
#include "pop_kid.h"
#include "_pop_kid.h"
#include "kid_data.h"
#include "pop_vflip.h"
#include "pop_guard.h" /* pop_guard_vflip_load — зеркала соперника */
/* ISR-стаб W0-страницы (тот же приём, что atlas_load): при прерывании с
* замапленной в W0 страницей CPU прыгает на 0x0038 → там обязан быть JP на
@@ -56,3 +58,29 @@ void pop_kid_data_free(void) __banked
{
if (kdat_ok) { mem_free_block(kdat_blk); kdat_ok = 0; }
}
/* ---- Зеркальный набор (зелье инверсии, уровень 9) -------------------- *
* Грузится ОДНИМ вызовом и откуда угодно: со старта игры, из показа интро
* или при входе на уровень с зельем — см. pop_vflip.h. Внутри обычный
* atlas_load (он резидентный и сам мапит W3, банку это разрешено делать
* только через него). Идемпотентна. */
int pop_vflip_load_all(void) __banked
{
static const char *const kidv[KID_MAXPAGES] = {
"KID\\kid0_v.atl","KID\\kid1_v.atl","KID\\kid2_v.atl","KID\\kid3_v.atl",
"KID\\kid4_v.atl","KID\\kid5_v.atl","KID\\kid6_v.atl","KID\\kid7_v.atl",
"KID\\kid8_v.atl","KID\\kid9_v.atl","KID\\kid10_v.atl","KID\\kid11_v.atl",
"KID\\kid12_v.atl","KID\\kid13_v.atl","KID\\kid14_v.atl","KID\\kid15_v.atl",
"KID\\kid16_v.atl","KID\\kid17_v.atl","KID\\kid18_v.atl","KID\\kid19_v.atl",
"KID\\kid20_v.atl","KID\\kid21_v.atl","KID\\kid22_v.atl","KID\\kid23_v.atl",
"KID\\kid24_v.atl","KID\\kid25_v.atl","KID\\kid26_v.atl","KID\\kid27_v.atl"
};
uint8_t i;
int res = 0;
if (pop_vflip_ready()) return 0;
for (i = 0; i < kid_npages; i++)
if (pop_vflip_add(&kidp[i], kidv[i]) != 0) res = -1;
if (pop_sword_vflip_load() != 0) res = -1;
if (pop_guard_vflip_load() != 0) res = -1;
return res;
}
+7
View File
@@ -43,6 +43,13 @@ int pop_sword_load(void) __banked
return 0;
}
/* Зеркальная страница меча (зелье инверсии) — пара к pop_sword_load. */
int pop_sword_vflip_load(void) __banked
{
if (!sword_ok) return 0;
return pop_vflip_add(&swordp, "KID\\sword_v.atl");
}
/* add_sword_to_objtable (seg006:1798). Клинок рисуется, если персонаж в
* кадрах подъёма/ножен, ИЛИ держит меч, ИЛИ это ЖИВОЙ страж (у стража меч
* в руке всегда, даже пока sword ещё sheathed). Страницу W0 оставляем
+2
View File
@@ -48,6 +48,8 @@ void pop_kid_data_free(void) __banked;
* Грузить ДО initgraph, как и страницы Kid. 0 — OK, -1 — ошибка (тогда
* кадры рисуются без клинка). */
int pop_sword_load(void) __banked;
/* Зеркальная (вертикально) страница меча — для зелья инверсии. */
int pop_sword_vflip_load(void) __banked;
void pop_kid_free(void) __banked;
/* Сама отрисовка клинка (pop_sword_draw) и блит спрайта chtab_2 по id
+5
View File
@@ -121,4 +121,9 @@
#define FIRST_LEVEL 1
#endif
/* Уровень с зельем инверсии (переворот экрана). По нему делается
* ПРЕДзагрузка зеркального набора спрайтов персонажей — чтобы пауза на
* чтение 34 страниц пришлась на загрузку уровня, а не на глоток зелья. */
#define POP_UPSIDE_LEVEL 9
#endif /* POP_TUNE_H */
+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;
}
+30 -19
View File
@@ -1,37 +1,48 @@
/*
* pop_vflip.h — зеркальные по вертикали копии страниц атласов персонажей
* (зелье инверсии, уровень 9). Реализация и «почему постранично» —
* в pop_vflip.c.
* pop_vflip.h — ВЕРТИКАЛЬНО ЗЕРКАЛЬНЫЕ наборы спрайтов персонажей
* (зелье инверсии, уровень 9). Реализация и замеры — в pop_vflip.c.
*
* Кому нужно. Фон и все row-major спрайты переворачиваются даром (порядок
* строк = знак страйда, gfx_blit_*_vflip), а персонажи хранятся колонками
* им нужен заранее развёрнутый набор пикселей. Отсюда правило вызывающего:
* строк = знак страйда, gfx_blit_*_vflip), а персонажи лежат КОЛОНКАМИ
* у них даром получается горизонтальное зеркало, а вертикальное требует
* развернуть байты внутри колонки. Такой набор готовит упаковщик
* (kid0_v.atl, sword_v.atl, g0_v.atl…), а здесь он только грузится и
* связывается с оригиналом. Правило вызывающего:
*
* страница = pop_upside ? pop_vflip_page(a->page) : a->page;
* if (!страница) страница = a->page; // набор не загружен
*
* и больше НИЧЕГО не меняется: смещения лент, размеры кадров, флип по
* горизонтали (знак страйда) остаются как были.
* Ни смещения лент, ни размеры кадров, ни горизонтальный флип при этом не
* меняются: зеркальный файл имеет ТУ ЖЕ раскладку, отличаются только пиксели.
*
* Почему кэш не сбрасывается на каждом перевороте: зелье инверсии в
* оригинале переключает состояние туда-обратно (второе зелье, смерть Кида —
* seg000:1224), и держать 34 страницы EMM дешевле, чем перезеркаливать по
* 16 КБ при каждом переключении. Бюджет EMM это позволяет: свободно ~215
* страниц из 256 (memory sprinter_emm_budget). Освобождаем на смене
* уровня — вместе с самими атласами.
* КОГДА ГРУЗИТЬ — на усмотрение приложения: pop_vflip_load_all можно звать
* где угодно (старт игры, показ интро, вход на уровень с зельем). До
* загрузки движок просто рисует неперевёрнутыми кадрами — это заметно, но
* не ломает ни кадр, ни физику.
*/
#ifndef POP_VFLIP_H
#define POP_VFLIP_H
#include <stdint.h>
#include <sprite.h>
/* Номер страницы с ВЕРТИКАЛЬНО ЗЕРКАЛЬНОЙ копией атласа src. Первый вызов
* для страницы делает копию (~16 КБ побайтно — ощутимо, но однократно),
* дальше отдаёт из кэша. 0 — не хватило EMM-страниц (вызывающий должен
* нарисовать обычным атласом: лучше неперевёрнутый кадр, чем пустота). */
/* Загрузить зеркальный файл path и связать его с атласом orig.
* 0 — OK, -1 — файла нет / не хватило EMM / таблица полна. */
int pop_vflip_add(const atlas_t *orig, const char *path);
/* Страница-зеркало для страницы src; 0 — пары нет (набор не загружен). */
uint8_t pop_vflip_page(uint8_t src);
/* Освободить все копии (смена уровня). Атласы к этому моменту тоже
* перезагружаются, и старые копии стали бы мусором. */
/* Загружен ли хоть один зеркальный атлас (чтобы не грузить дважды). */
uint8_t pop_vflip_ready(void);
/* Освободить весь набор (смена набора спрайтов соперника / уровня). */
void pop_vflip_reset(void);
/* Загрузить ВЕСЬ зеркальный набор: страницы Кида, меч и текущий набор
* соперника. Идемпотентна — повторный вызов ничего не делает. 0 — OK,
* -1 — что-то не загрузилось (движок останется на обычных кадрах).
* Живёт в банке 8 (pop_kboot.c): зовётся раз за игру/уровень. */
int pop_vflip_load_all(void) __banked;
#endif /* POP_VFLIP_H */
+6
View File
@@ -22,6 +22,7 @@
#include "pop_cdraw.h" /* общая отрисовка персонажей (слоты Кид/соперник) */
#include "pop_cheat.h"
#include "pop_tune.h"
#include "pop_vflip.h" /* зеркальный набор персонажей (зелье инверсии) */
#include "roomtest_cold.h" /* холодная половина цикла — в банке */
__sfr __at (0xFE) io_border;
@@ -340,6 +341,11 @@ int main(void)
* рисуют зеркально сами. */
if (pop_upside_dirty) {
pop_upside_dirty = 0;
/* Страховка для ЧИТА U на «неподготовленном» уровне и для
* случая, когда зелье выпито раньше предзагрузки: набор
* поднимается здесь же. На уровне 9 он уже загружен, и вызов
* не стоит ничего (pop_vflip_ready). */
if (pop_upside && !pop_vflip_ready()) pop_vflip_load_all();
pop_flip_screen();
back = dbuf ? (uint8_t)(gfx_get_visible_page() ^ 1) : 0;
gfx_set_draw_page(back);
+11
View File
@@ -591,6 +591,12 @@ int pop_boot(void) __banked
* pop_start_level. Управление: ←/→ бег, ↓ присед, ↑+→ прыжок с места,
* Shift шаг, ESC — выход. Карта коллизии задана внутри enter_room. */
pop_start_level();
/* Зеркальный набор персонажей — ТУТ ЖЕ, если стартуем сразу на уровне с
* зельем инверсии (make LEVEL=9). Иначе первый переворот утащил бы
* чтение 34 файлов (~1.5 с) прямо в кадр: пользователь видел «затык» в
* синей фазе профиля. Тот же вызов есть в смене уровня; функция
* идемпотентна и звать её можно откуда угодно (pop_vflip.h). */
if (pop_current_level == POP_UPSIDE_LEVEL) pop_vflip_load_all();
gfx_set_draw_page(0);
pop_char_draw(POP_CH_KID);
pop_char_fore(POP_CH_KID);
@@ -640,6 +646,11 @@ int pop_level_switch(void) __banked
* перечитаются из НОВОЙ страницы), тайлы/стражи приезжают
* из неё же, loose чистит enter_room. */
pop_start_level();
/* Уровень с зельем инверсии — заранее поднять зеркальный
* набор персонажей (pop_vflip.h): 34 страницы читаются ~1.5 с,
* и делать это в момент глотка нельзя. Звать можно откуда
* угодно — функция идемпотентна. */
if (pop_current_level == POP_UPSIDE_LEVEL) pop_vflip_load_all();
}
return 0;
}
@@ -344,3 +344,15 @@ uint8_t pop_palace;
* pop_map зовёт pop_mirror_draw из pop_check_mirror, поэтому символ нужен. */
void pop_mirror_draw(int clip_top) __banked { (void)clip_top; }
void pop_mirror_heal(void) __banked { }
/* ---- Зеркальный набор персонажей (зелье инверсии) -------------------- *
* В host-тестах графики нет: pop_kboot линкуется ради pop_kid_data_load, а
* его хвост (загрузка *_v.atl) заглушен — проверять там нечего, атласов в
* тесте не существует вовсе. */
uint8_t pop_vflip_ready(void) { return 1; } /* «уже загружено» — грузить не станет */
int pop_vflip_add(const void *orig, const char *path)
{ (void)orig; (void)path; return 0; }
uint8_t pop_vflip_page(uint8_t src) { return src; }
void pop_vflip_reset(void) { }
int pop_sword_vflip_load(void) __banked { return 0; }
int pop_guard_vflip_load(void) __banked { return 0; }
+11 -1
View File
@@ -19,7 +19,7 @@ from PIL import Image
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import pop_pack_bg as B
from pop_pack_kid import transpose_cols
from pop_pack_kid import transpose_cols, vflip_page
# Набор спрайтов соперника выбирается ТИПОМ УРОВНЯ (load_lev_spr, seg000:1092):
# tbl_guard_type = {0 guard, 1 fat, 2 skeleton, 3 vizier, 4 shadow, 1 нет}.
@@ -185,6 +185,16 @@ def main():
nm = f"g{pg}.atl"
cnt, sz = B.pack_atlas(os.path.join(OUT_DIR, nm), pages.get(pg, {}))
total += sz
# Зеркальный по вертикали набор — пара к kid*_v.atl (см. vflip_cols).
# ТОЛЬКО для обычного стража: зелье инверсии есть на уровне 9, а там
# tbl_guard_type = 0, то есть страж; скелет (уровень 3), визирь и
# жирный с переворотом не встречаются. Если чит U всё-таки включат
# на таком уровне, соперник нарисуется неперевёрнутым кадром —
# pop_vflip_page вернёт 0 (pop_vflip.h).
if SET_NAME == "GUARD":
_, szv = B.pack_atlas(os.path.join(OUT_DIR, f"g{pg}_v.atl"),
vflip_page(pages.get(pg, {})))
total += szv
print(f" {nm}: {len(pages.get(pg, {}))} спрайтов, каталог {cnt}, {sz} Б")
write_pal_header(os.path.join(HERE, "..", "roomtest", "pop_guard_pal.h"))
print(f"ИТОГО: {n} спрайтов стража, {max(pages)+1} EMM-страниц, {total} Б -> {OUT_DIR}")
@@ -47,6 +47,31 @@ def transpose_cols(w, h, rowmajor):
return bytes(out)
def vflip_cols(w, h, colmajor):
"""COLUMN-MAJOR (out[col*h + row]) -> тот же формат, но кадр ЗЕРКАЛЕН ПО
ВЕРТИКАЛИ: внутри каждой колонки байты идут снизу вверх.
Зачем отдельный набор. Спрайты персонажей лежат колонками ради
БЕСПЛАТНОГО горизонтального флипа (accel копирует колонки, направление
задаётся знаком страйда). Вертикальное зеркало у такой раскладки — это
разворот байтов ВНУТРИ колонки, а accel копирует блок только вперёд:
в рантайме это стоит ~0.6 с на страницу (замер MAME 2026-08-12), тогда
как ГОТОВАЯ страница читается с диска за 0.045 с. Поэтому зелье
инверсии (уровень 9) обслуживается вторым набором файлов *_v.atl.
Раскладка каталога у него та же — движку достаточно подменить номер
EMM-страницы (см. roomtest/pop_vflip.h)."""
out = bytearray(len(colmajor))
for col in range(w):
b = col * h
out[b:b + h] = colmajor[b:b + h][::-1]
return bytes(out)
def vflip_page(page):
"""{idx: (w,h,pix)} -> то же с зеркальными по вертикали пикселями."""
return {i: (w, h, vflip_cols(w, h, pix)) for i, (w, h, pix) in page.items()}
def load_kid(aidx):
"""Атлас-индекс aidx (== frame.image) -> res(401+aidx).png -> (w, h, 8bpp
COLUMN-MAJOR). 0->0xFF, i->0x70+i. Пиксели транспонированы в колонки.
@@ -149,6 +174,10 @@ def main():
total += sz
names.append(nm)
print(f" {nm}: {len(pages[pg])} спрайтов, каталог {cnt}, {sz} Б")
# Зеркальный по вертикали набор (зелье инверсии, уровень 9).
nmv = f"kid{pg}_v.atl"
_, szv = B.pack_atlas(os.path.join(OUT_DIR, nmv), vflip_page(pages[pg]))
total += szv
else:
names.append(None)
print(f" (kid стр {pg}: пусто)")
@@ -160,6 +189,8 @@ def main():
sw[i] = (w, h, pix)
cnt, sz = B.pack_atlas(os.path.join(OUT_DIR, "sword.atl"), sw)
total += sz
_, szv = B.pack_atlas(os.path.join(OUT_DIR, "sword_v.atl"), vflip_page(sw))
total += szv
print(f" sword.atl (chtab_0, id {SWORD_IDS[0]}..{SWORD_IDS[-1]}): "
f"{len(sw)} спрайтов, каталог {cnt}, {sz} Б")