diff --git a/applications/PoP/poc/res/guard/g0_v.atl b/applications/PoP/poc/res/guard/g0_v.atl new file mode 100644 index 0000000..f2e09a1 Binary files /dev/null and b/applications/PoP/poc/res/guard/g0_v.atl differ diff --git a/applications/PoP/poc/res/guard/g1_v.atl b/applications/PoP/poc/res/guard/g1_v.atl new file mode 100644 index 0000000..3968333 Binary files /dev/null and b/applications/PoP/poc/res/guard/g1_v.atl differ diff --git a/applications/PoP/poc/res/guard/g2_v.atl b/applications/PoP/poc/res/guard/g2_v.atl new file mode 100644 index 0000000..27c83b1 Binary files /dev/null and b/applications/PoP/poc/res/guard/g2_v.atl differ diff --git a/applications/PoP/poc/res/guard/g3_v.atl b/applications/PoP/poc/res/guard/g3_v.atl new file mode 100644 index 0000000..0cc147a Binary files /dev/null and b/applications/PoP/poc/res/guard/g3_v.atl differ diff --git a/applications/PoP/poc/res/guard/g4_v.atl b/applications/PoP/poc/res/guard/g4_v.atl new file mode 100644 index 0000000..a385014 Binary files /dev/null and b/applications/PoP/poc/res/guard/g4_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid0_v.atl b/applications/PoP/poc/res/kid/kid0_v.atl new file mode 100644 index 0000000..65fb32c Binary files /dev/null and b/applications/PoP/poc/res/kid/kid0_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid10_v.atl b/applications/PoP/poc/res/kid/kid10_v.atl new file mode 100644 index 0000000..3656692 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid10_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid11_v.atl b/applications/PoP/poc/res/kid/kid11_v.atl new file mode 100644 index 0000000..5622703 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid11_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid12_v.atl b/applications/PoP/poc/res/kid/kid12_v.atl new file mode 100644 index 0000000..d199db7 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid12_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid13_v.atl b/applications/PoP/poc/res/kid/kid13_v.atl new file mode 100644 index 0000000..1f9e0a7 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid13_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid14_v.atl b/applications/PoP/poc/res/kid/kid14_v.atl new file mode 100644 index 0000000..e5c7157 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid14_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid15_v.atl b/applications/PoP/poc/res/kid/kid15_v.atl new file mode 100644 index 0000000..fa2529b Binary files /dev/null and b/applications/PoP/poc/res/kid/kid15_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid16_v.atl b/applications/PoP/poc/res/kid/kid16_v.atl new file mode 100644 index 0000000..06cbd36 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid16_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid17_v.atl b/applications/PoP/poc/res/kid/kid17_v.atl new file mode 100644 index 0000000..6801a59 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid17_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid18_v.atl b/applications/PoP/poc/res/kid/kid18_v.atl new file mode 100644 index 0000000..b333607 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid18_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid19_v.atl b/applications/PoP/poc/res/kid/kid19_v.atl new file mode 100644 index 0000000..25a2f72 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid19_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid1_v.atl b/applications/PoP/poc/res/kid/kid1_v.atl new file mode 100644 index 0000000..1c3d8e0 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid1_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid20_v.atl b/applications/PoP/poc/res/kid/kid20_v.atl new file mode 100644 index 0000000..5f9c5f2 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid20_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid21_v.atl b/applications/PoP/poc/res/kid/kid21_v.atl new file mode 100644 index 0000000..deb1f23 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid21_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid22_v.atl b/applications/PoP/poc/res/kid/kid22_v.atl new file mode 100644 index 0000000..786b495 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid22_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid23_v.atl b/applications/PoP/poc/res/kid/kid23_v.atl new file mode 100644 index 0000000..f9326aa Binary files /dev/null and b/applications/PoP/poc/res/kid/kid23_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid24_v.atl b/applications/PoP/poc/res/kid/kid24_v.atl new file mode 100644 index 0000000..19c80a4 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid24_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid25_v.atl b/applications/PoP/poc/res/kid/kid25_v.atl new file mode 100644 index 0000000..caece9e Binary files /dev/null and b/applications/PoP/poc/res/kid/kid25_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid26_v.atl b/applications/PoP/poc/res/kid/kid26_v.atl new file mode 100644 index 0000000..82d69f1 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid26_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid27_v.atl b/applications/PoP/poc/res/kid/kid27_v.atl new file mode 100644 index 0000000..0788362 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid27_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid2_v.atl b/applications/PoP/poc/res/kid/kid2_v.atl new file mode 100644 index 0000000..0969b6b Binary files /dev/null and b/applications/PoP/poc/res/kid/kid2_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid3_v.atl b/applications/PoP/poc/res/kid/kid3_v.atl new file mode 100644 index 0000000..3a8199a Binary files /dev/null and b/applications/PoP/poc/res/kid/kid3_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid4_v.atl b/applications/PoP/poc/res/kid/kid4_v.atl new file mode 100644 index 0000000..28ddd41 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid4_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid5_v.atl b/applications/PoP/poc/res/kid/kid5_v.atl new file mode 100644 index 0000000..b15e6f2 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid5_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid6_v.atl b/applications/PoP/poc/res/kid/kid6_v.atl new file mode 100644 index 0000000..8c3f82f Binary files /dev/null and b/applications/PoP/poc/res/kid/kid6_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid7_v.atl b/applications/PoP/poc/res/kid/kid7_v.atl new file mode 100644 index 0000000..afe4669 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid7_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid8_v.atl b/applications/PoP/poc/res/kid/kid8_v.atl new file mode 100644 index 0000000..5813b5d Binary files /dev/null and b/applications/PoP/poc/res/kid/kid8_v.atl differ diff --git a/applications/PoP/poc/res/kid/kid9_v.atl b/applications/PoP/poc/res/kid/kid9_v.atl new file mode 100644 index 0000000..606cab4 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid9_v.atl differ diff --git a/applications/PoP/poc/res/kid/sword_v.atl b/applications/PoP/poc/res/kid/sword_v.atl new file mode 100644 index 0000000..f453547 Binary files /dev/null and b/applications/PoP/poc/res/kid/sword_v.atl differ diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index 6934e28..71a17c0 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -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 diff --git a/applications/PoP/roomtest/pop_cdraw.c b/applications/PoP/roomtest/pop_cdraw.c index 7945bdc..14ef0ff 100644 --- a/applications/PoP/roomtest/pop_cdraw.c +++ b/applications/PoP/roomtest/pop_cdraw.c @@ -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; diff --git a/applications/PoP/roomtest/pop_guard.h b/applications/PoP/roomtest/pop_guard.h index 0ac1d57..af9d118 100644 --- a/applications/PoP/roomtest/pop_guard.h +++ b/applications/PoP/roomtest/pop_guard.h @@ -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, diff --git a/applications/PoP/roomtest/pop_kboot.c b/applications/PoP/roomtest/pop_kboot.c index df9cde2..e878cd7 100644 --- a/applications/PoP/roomtest/pop_kboot.c +++ b/applications/PoP/roomtest/pop_kboot.c @@ -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; +} diff --git a/applications/PoP/roomtest/pop_kdraw.c b/applications/PoP/roomtest/pop_kdraw.c index f070810..d28a4ed 100644 --- a/applications/PoP/roomtest/pop_kdraw.c +++ b/applications/PoP/roomtest/pop_kdraw.c @@ -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 оставляем diff --git a/applications/PoP/roomtest/pop_kid.h b/applications/PoP/roomtest/pop_kid.h index bbe1ef5..9ba6fba 100644 --- a/applications/PoP/roomtest/pop_kid.h +++ b/applications/PoP/roomtest/pop_kid.h @@ -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 diff --git a/applications/PoP/roomtest/pop_tune.h b/applications/PoP/roomtest/pop_tune.h index 3c1181c..f36b2bd 100644 --- a/applications/PoP/roomtest/pop_tune.h +++ b/applications/PoP/roomtest/pop_tune.h @@ -121,4 +121,9 @@ #define FIRST_LEVEL 1 #endif +/* Уровень с зельем инверсии (переворот экрана). По нему делается + * ПРЕДзагрузка зеркального набора спрайтов персонажей — чтобы пауза на + * чтение 34 страниц пришлась на загрузку уровня, а не на глоток зелья. */ +#define POP_UPSIDE_LEVEL 9 + #endif /* POP_TUNE_H */ diff --git a/applications/PoP/roomtest/pop_vflip.c b/applications/PoP/roomtest/pop_vflip.c index 4f97bf4..f4d0663 100644 --- a/applications/PoP/roomtest/pop_vflip.c +++ b/applications/PoP/roomtest/pop_vflip.c @@ -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 -#include -#include #include #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; } diff --git a/applications/PoP/roomtest/pop_vflip.h b/applications/PoP/roomtest/pop_vflip.h index a0f6687..3f239ca 100644 --- a/applications/PoP/roomtest/pop_vflip.h +++ b/applications/PoP/roomtest/pop_vflip.h @@ -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 +#include -/* Номер страницы с ВЕРТИКАЛЬНО ЗЕРКАЛЬНОЙ копией атласа 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 */ diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 4866b26..4366600 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -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); diff --git a/applications/PoP/roomtest/roomtest_cold.c b/applications/PoP/roomtest/roomtest_cold.c index f534dcc..48544d5 100644 --- a/applications/PoP/roomtest/roomtest_cold.c +++ b/applications/PoP/roomtest/roomtest_cold.c @@ -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; } diff --git a/applications/PoP/roomtest/tests-host/stubs.c b/applications/PoP/roomtest/tests-host/stubs.c index 41438f2..2b5a47f 100644 --- a/applications/PoP/roomtest/tests-host/stubs.c +++ b/applications/PoP/roomtest/tests-host/stubs.c @@ -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; } diff --git a/applications/PoP/toolchain/pop_pack_guard.py b/applications/PoP/toolchain/pop_pack_guard.py index d95dce8..89215ff 100644 --- a/applications/PoP/toolchain/pop_pack_guard.py +++ b/applications/PoP/toolchain/pop_pack_guard.py @@ -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}") diff --git a/applications/PoP/toolchain/pop_pack_kid.py b/applications/PoP/toolchain/pop_pack_kid.py index 33ec649..60a9596 100644 --- a/applications/PoP/toolchain/pop_pack_kid.py +++ b/applications/PoP/toolchain/pop_pack_kid.py @@ -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} Б")