diff --git a/applications/PoP/poc/res/bg/bg.arc b/applications/PoP/poc/res/bg/bg.arc new file mode 100644 index 0000000..cc36258 Binary files /dev/null and b/applications/PoP/poc/res/bg/bg.arc differ diff --git a/applications/PoP/poc/res/guard/guard.arc b/applications/PoP/poc/res/guard/guard.arc new file mode 100644 index 0000000..ad3aed9 Binary files /dev/null and b/applications/PoP/poc/res/guard/guard.arc differ diff --git a/applications/PoP/poc/res/kid/kid.arc b/applications/PoP/poc/res/kid/kid.arc new file mode 100644 index 0000000..112e465 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid.arc differ diff --git a/applications/PoP/poc/res/pv/pv.arc b/applications/PoP/poc/res/pv/pv.arc new file mode 100644 index 0000000..7d7cb2e Binary files /dev/null and b/applications/PoP/poc/res/pv/pv.arc differ diff --git a/applications/PoP/poc/res/shadow/shadow.arc b/applications/PoP/poc/res/shadow/shadow.arc new file mode 100644 index 0000000..1c0f9b1 Binary files /dev/null and b/applications/PoP/poc/res/shadow/shadow.arc differ diff --git a/applications/PoP/poc/res/skel/skel.arc b/applications/PoP/poc/res/skel/skel.arc new file mode 100644 index 0000000..be05df5 Binary files /dev/null and b/applications/PoP/poc/res/skel/skel.arc differ diff --git a/applications/PoP/poc/res/title/title.arc b/applications/PoP/poc/res/title/title.arc new file mode 100644 index 0000000..3cf9f33 Binary files /dev/null and b/applications/PoP/poc/res/title/title.arc differ diff --git a/applications/PoP/poc/res/vizier/vizier.arc b/applications/PoP/poc/res/vizier/vizier.arc new file mode 100644 index 0000000..ffb7818 Binary files /dev/null and b/applications/PoP/poc/res/vizier/vizier.arc differ diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index 6ba88d6..d0934b9 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -121,11 +121,14 @@ LVL_DATA := $(foreach n,$(LVL_NUMS),$(LVL_DIR)/res20$(n).bin) # пламя, меч-предмет, зелья) от набора не зависит — один на оба. BG_SETS := pop pal BG_PAGES := 0 1 2 3 4 5 6 7 8 9 -BG_DATA := $(foreach s,$(BG_SETS),\ +# Двенадцать атласов на тайлсет идут подряд — код считает индекс как +# set*12 + страница, а упаковщик печатает константы в bg_arc.h. +BG_ATL := $(foreach s,$(BG_SETS),\ $(foreach n,$(BG_PAGES),$(BG_DIR)/$(s)_env$(n).atl) \ - $(BG_DIR)/$(s)_wall.atl $(BG_DIR)/$(s)_fore.atl \ - $(BG_DIR)/$(s)_tile.pal) \ + $(BG_DIR)/$(s)_wall.atl $(BG_DIR)/$(s)_fore.atl) \ $(BG_DIR)/pop_pot.atl +BG_ARC := $(BG_DIR)/bg.arc +BG_DATA := $(BG_ARC) $(foreach s,$(BG_SETS),$(BG_DIR)/$(s)_tile.pal) # ВСЕ 28 страниц Kid — иначе кадры торможения/разворота/приседа (image>23) # не рисуются (отрисовка пропускает незагруженную страницу). Палитра — # ЕДИНАЯ игровая (env+wall+kid). @@ -140,8 +143,19 @@ KID_DATA := $(KID_PAGES) $(KID_VPAGES) $(KID_DIR)/kid.pal \ # Таблицы анимации (кадры+seqtbl) — отдельный бинарь в EMM-странице, его # делает pop_extract_kid_data.py (не упаковщик спрайтов) — своё правило ниже. KID_BIN := $(KID_DIR)/kid_data.bin + +# АРХИВ KID. Шестьдесят файлов в одном: замер 2026-08-25 — `open` 51,4 мс +# против 32,6 мс на чтение 16 КБ, то есть на одних только открытиях набор +# Кида терял три секунды. Порядок в списке = порядок индексов, а их +# упаковщик печатает в kid_arc.h (константы ARC_KID_*). +KID_ARC := $(KID_DIR)/kid.arc +# В архив идут только атласы. Палитру читает gfx_pal_fload прямо в +# видеопамять, а kid_data.bin — host-тесты своей файловой заглушкой; ради +# двух файлов заводить обходные пути незачем. +KID_ARC_SRC := $(KID_PAGES) $(KID_VPAGES) $(KID_DIR)/sword.atl \ + $(KID_DIR)/sword_v.atl GUARD_DIR := $(CURDIR)/../poc/res/guard -GUARD_DATA := $(GUARD_DIR)/g0.atl $(GUARD_DIR)/g1.atl $(GUARD_DIR)/g2.atl \ +GUARD_ATL := $(GUARD_DIR)/g0.atl $(GUARD_DIR)/g1.atl $(GUARD_DIR)/g2.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 @@ -150,14 +164,14 @@ GUARD_DATA := $(GUARD_DIR)/g0.atl $(GUARD_DIR)/g1.atl $(GUARD_DIR)/g2.atl \ 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_ATL := $(SKEL_DIR)/g0.atl $(SKEL_DIR)/g1.atl $(SKEL_DIR)/g2.atl \ $(SKEL_DIR)/g3.atl # Джафар — тоже отдельный набор (VIZIER.DAT): его грузит уровень 13 # (tbl_guard_type == 3). Кадров столько же, сколько у стража (34), но # палитра своя, из файла набора: curr_guard_color для не-стражей = 0. # Зеркального набора нет по той же причине, что у скелета. VIZIER_DIR := $(CURDIR)/../poc/res/vizier -VIZIER_DATA := $(VIZIER_DIR)/g0.atl $(VIZIER_DIR)/g1.atl $(VIZIER_DIR)/g2.atl \ +VIZIER_ATL := $(VIZIER_DIR)/g0.atl $(VIZIER_DIR)/g1.atl $(VIZIER_DIR)/g2.atl \ $(VIZIER_DIR)/g3.atl $(VIZIER_DIR)/g4.atl # ТЕНЬ — свой набор, запечённый упаковщиком (наложение спрайта на себя со @@ -165,9 +179,21 @@ VIZIER_DATA := $(VIZIER_DIR)/g0.atl $(VIZIER_DIR)/g1.atl $(VIZIER_DIR)/g2.atl \ # половины в одном наборе: sk* — кадры вне боя (спрайты Кида), sf* — кадры # 150..189 (SHADOW.DAT, это тоже графика Кида, а не стража). SHADOW_DIR := $(CURDIR)/../poc/res/shadow -SHADOW_DATA := $(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,$(SHADOW_DIR)/sk$(n).atl) \ +SHADOW_ATL := $(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,$(SHADOW_DIR)/sk$(n).atl) \ $(foreach n,0 1 2 3,$(SHADOW_DIR)/sf$(n).atl) +# Четыре архива, а не один: имена внутри наборов совпадают (g0.atl есть и у +# стража, и у скелета, и у Джафара), а константы индексов упаковщик делает +# из имени файла. Наборы всё равно грузятся поодиночке — по типу уровня. +GUARD_ARC := $(GUARD_DIR)/guard.arc +SKEL_ARC := $(SKEL_DIR)/skel.arc +VIZIER_ARC := $(VIZIER_DIR)/vizier.arc +SHADOW_ARC := $(SHADOW_DIR)/shadow.arc +GUARD_DATA := $(GUARD_ARC) +SKEL_DATA := $(SKEL_ARC) +VIZIER_DATA := $(VIZIER_ARC) +SHADOW_DATA := $(SHADOW_ARC) + # ЗВУКОВЫЕ ЭФФЕКТЫ — оригинальная оцифровка, приведённая к частоте CBL # (10 937,5 Гц, 8 бит моно). 8 страниц по 16 КБ; см. ../docs/sound_plan.md. SND_DIR := $(CURDIR)/../poc/res/sound @@ -200,8 +226,9 @@ FONT_DATA := $(FONT_ATL) TITLE_DIR := $(CURDIR)/../poc/res/title TITLE_SCENES := 0 1 2 3 TITLE_PARTS := 0 1 2 3 4 -TITLE_DATA := $(TITLE_DIR)/title.pal \ - $(foreach s,$(TITLE_SCENES),$(foreach p,$(TITLE_PARTS),$(TITLE_DIR)/t$(s)_$(p).atl)) +TITLE_ATL := $(foreach s,$(TITLE_SCENES),$(foreach p,$(TITLE_PARTS),$(TITLE_DIR)/t$(s)_$(p).atl)) +TITLE_ARC := $(TITLE_DIR)/title.arc +TITLE_DATA := $(TITLE_DIR)/title.pal $(TITLE_ARC) # PV/INTRO: story и fallback-стадии режутся на полосы. Для настоящего PV # intro есть ещё чистый фон `b0_*` и шесть <=16K actor-атласов: они остаются # в EMM на время сцены, а не перечитывают полный 320x200 кадр с HDD. @@ -213,12 +240,14 @@ PV_ANIM := $(foreach p,$(PV_PARTS),$(PV_DIR)/b0_$(p).atl) \ $(PV_DIR)/a0.atl $(PV_DIR)/a1.atl $(PV_DIR)/a2.atl \ $(PV_DIR)/j0.atl $(PV_DIR)/j1.atl \ $(PV_DIR)/j2.atl $(PV_DIR)/h0.atl $(PV_DIR)/t0.atl -PV_DATA := $(PV_DIR)/story.pal $(PV_DIR)/pv.pal \ +PV_ATL := \ $(foreach s,$(PV_STORY),$(foreach p,$(PV_PARTS),$(PV_DIR)/s$(s)_$(p).atl)) \ $(foreach s,$(PV_SCENES),$(foreach p,$(PV_PARTS),$(PV_DIR)/p$(s)_$(p).atl)) \ $(PV_ANIM) +PV_ARC := $(PV_DIR)/pv.arc +PV_DATA := $(PV_DIR)/story.pal $(PV_DIR)/pv.pal $(PV_ARC) -EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(SKEL_DATA) \ +EXTRA_DATA := $(BG_DATA) $(KID_ARC) $(KID_DIR)/kid.pal $(KID_BIN) $(GUARD_DATA) $(SKEL_DATA) \ $(VIZIER_DATA) $(SHADOW_DATA) $(SND_DATA) $(MUS_DATA) $(FONT_DATA) $(TITLE_DATA) $(PV_DATA) $(LVL_DATA) # Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS), # чтобы корень диска не зарастал десятками .atl. @@ -231,7 +260,7 @@ EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(SKEL_DATA) \ # LEVELS\ — уровни HDD_PACK_ARGS := $(EXAMPLE).exe \ $(foreach f,$(BG_DATA),BG:$(f)) \ - $(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \ + KID:$(KID_ARC) KID:$(KID_DIR)/kid.pal KID:$(KID_BIN) \ $(foreach f,$(GUARD_DATA),GUARD:$(f)) \ $(foreach f,$(SKEL_DATA),SKEL:$(f)) \ $(foreach f,$(VIZIER_DATA),VIZIER:$(f)) \ @@ -271,31 +300,31 @@ $(RESOURCE_STAMP_DIR): $(BG_STAMP): $(TC)/pop_pack_bg.py $(TC)/render_room.py | $(RESOURCE_STAMP_DIR) cd $(TC) && python3 pop_pack_bg.py dungeon && python3 pop_pack_bg.py palace touch $@ -$(BG_DATA): $(BG_STAMP) +$(BG_ATL) $(BG_DIR)/pop_tile.pal $(BG_DIR)/pal_tile.pal: $(BG_STAMP) @test -f $@ || { $(MAKE) -B $(BG_STAMP); test -f $@; } $(GUARD_STAMP): $(TC)/pop_pack_guard.py | $(RESOURCE_STAMP_DIR) cd $(TC) && python3 pop_pack_guard.py touch $@ -$(GUARD_DATA): $(GUARD_STAMP) +$(GUARD_ATL) $(GUARD_DIR)/guard.pal: $(GUARD_STAMP) @test -f $@ || { $(MAKE) -B $(GUARD_STAMP); test -f $@; } $(SKEL_STAMP): $(TC)/pop_pack_guard.py | $(RESOURCE_STAMP_DIR) cd $(TC) && python3 pop_pack_guard.py SKEL touch $@ -$(SKEL_DATA): $(SKEL_STAMP) +$(SKEL_ATL): $(SKEL_STAMP) @test -f $@ || { $(MAKE) -B $(SKEL_STAMP); test -f $@; } $(VIZIER_STAMP): $(TC)/pop_pack_guard.py | $(RESOURCE_STAMP_DIR) cd $(TC) && python3 pop_pack_guard.py VIZIER touch $@ -$(VIZIER_DATA): $(VIZIER_STAMP) +$(VIZIER_ATL): $(VIZIER_STAMP) @test -f $@ || { $(MAKE) -B $(VIZIER_STAMP); test -f $@; } $(SHADOW_STAMP): $(TC)/pop_pack_shadow.py $(TC)/pop_pack_bg.py $(TC)/pop_pack_kid.py | $(RESOURCE_STAMP_DIR) cd $(TC) && python3 pop_pack_shadow.py touch $@ -$(SHADOW_DATA) pop_shadow_atlas.h: $(SHADOW_STAMP) +$(SHADOW_ATL) pop_shadow_atlas.h: $(SHADOW_STAMP) @test -f $@ || { $(MAKE) -B $(SHADOW_STAMP); test -f $@; } $(SOUND_STAMP): $(TC)/pop_pack_sound.py | $(RESOURCE_STAMP_DIR) @@ -306,6 +335,38 @@ $(SND_PARTS) pop_sound_tbl.h: $(SOUND_STAMP) $(SND_ARC): $(SND_PARTS) $(TC)/pop_pack_arc.py python3 $(TC)/pop_pack_arc.py $@ $(SND_PARTS) +$(GUARD_ARC) guard_arc.h: $(GUARD_ATL) $(TC)/pop_pack_arc.py + python3 $(TC)/pop_pack_arc.py --header guard_arc.h --prefix GUARD \ + $(GUARD_ARC) $(GUARD_ATL) + +$(SKEL_ARC) skel_arc.h: $(SKEL_ATL) $(TC)/pop_pack_arc.py + python3 $(TC)/pop_pack_arc.py --header skel_arc.h --prefix SKEL \ + $(SKEL_ARC) $(SKEL_ATL) + +$(VIZIER_ARC) vizier_arc.h: $(VIZIER_ATL) $(TC)/pop_pack_arc.py + python3 $(TC)/pop_pack_arc.py --header vizier_arc.h --prefix VIZIER \ + $(VIZIER_ARC) $(VIZIER_ATL) + +$(SHADOW_ARC) shadow_arc.h: $(SHADOW_ATL) $(TC)/pop_pack_arc.py + python3 $(TC)/pop_pack_arc.py --header shadow_arc.h --prefix SHADOW \ + $(SHADOW_ARC) $(SHADOW_ATL) + +$(BG_ARC) bg_arc.h: $(BG_ATL) $(TC)/pop_pack_arc.py + python3 $(TC)/pop_pack_arc.py --header bg_arc.h --prefix BG \ + $(BG_ARC) $(BG_ATL) + +$(TITLE_ARC) title_arc.h: $(TITLE_ATL) $(TC)/pop_pack_arc.py + python3 $(TC)/pop_pack_arc.py --header title_arc.h --prefix TITLE \ + $(TITLE_ARC) $(TITLE_ATL) + +$(PV_ARC) pv_arc.h: $(PV_ATL) $(TC)/pop_pack_arc.py + python3 $(TC)/pop_pack_arc.py --header pv_arc.h --prefix PV \ + $(PV_ARC) $(PV_ATL) + +$(KID_ARC) kid_arc.h: $(KID_ARC_SRC) $(TC)/pop_pack_arc.py + python3 $(TC)/pop_pack_arc.py --header kid_arc.h --prefix KID \ + $(KID_ARC) $(KID_ARC_SRC) + $(MUSIC_STAMP): $(TC)/pop_pack_music.py | $(RESOURCE_STAMP_DIR) cd $(TC) && python3 pop_pack_music.py $(MUS_IDS) touch $@ @@ -339,7 +400,7 @@ $(TITLE_STAMP): $(PROJ_ROOT)/toolchain/pop_pack_title.py \ $(CURDIR)/../SDLPoP/data/TITLE/res55.png | $(RESOURCE_STAMP_DIR) cd $(PROJ_ROOT) && python3 toolchain/pop_pack_title.py touch $@ -$(TITLE_DATA): $(TITLE_STAMP) +$(TITLE_ATL) $(TITLE_DIR)/title.pal: $(TITLE_STAMP) @test -f $@ || { $(MAKE) -B $(TITLE_STAMP); test -f $@; } $(PV_STAMP): $(PROJ_ROOT)/toolchain/pop_pack_intro.py \ @@ -366,13 +427,13 @@ $(PV_STAMP): $(PROJ_ROOT)/toolchain/pop_pack_intro.py \ $(foreach n,151 152 153 154 155 156 157 158 159,$(CURDIR)/../SDLPoP/data/PRINCE/res$(n).png) | $(RESOURCE_STAMP_DIR) cd $(PROJ_ROOT) && python3 toolchain/pop_pack_intro.py touch $@ -$(PV_DATA): $(PV_STAMP) +$(PV_ATL) $(PV_DIR)/story.pal $(PV_DIR)/pv.pal: $(PV_STAMP) @test -f $@ || { $(MAKE) -B $(PV_STAMP); test -f $@; } # Явные цели позволяют собирать ресурсы без `make -B`, который по смыслу # принудительно пересобирает также libc, libbgi и само приложение. resources-bg: $(BG_DATA) -resources-kid: $(KID_DATA) $(KID_BIN) +resources-kid: $(KID_ARC) resources-actors: $(GUARD_DATA) $(SKEL_DATA) $(VIZIER_DATA) $(SHADOW_DATA) resources-sound: $(SND_DATA) resources-music: $(MUS_DATA) @@ -399,7 +460,7 @@ hdd: resources # широкий: цена лишней полной сборки ниже риска получить stale-бинарник. LOCAL_HEADERS := $(wildcard *.h) $(EXAMPLE).exe: Makefile $(PROJ_ROOT)/app.mk $(BANKED_SRCS) $(LOCAL_HEADERS) \ - $(BG_DATA) $(KID_DATA) $(KID_BIN) $(FONT_DATA) $(TITLE_DATA) $(PV_DATA) $(LVL_DATA) + $(BG_DATA) $(KID_ARC) $(FONT_DATA) $(TITLE_DATA) $(PV_DATA) $(LVL_DATA) clean-resources: rm -rf $(RESOURCE_STAMP_DIR) diff --git a/applications/PoP/roomtest/bg_arc.h b/applications/PoP/roomtest/bg_arc.h new file mode 100644 index 0000000..581908e --- /dev/null +++ b/applications/PoP/roomtest/bg_arc.h @@ -0,0 +1,34 @@ +/* bg_arc.h — индексы в архиве BG. СГЕНЕРИРОВАН + * (toolchain/pop_pack_arc.py), руками не править. */ +#ifndef BG_ARC_H +#define BG_ARC_H + +#define ARC_BG_COUNT 25 + +#define ARC_BG_POP_ENV0 0 +#define ARC_BG_POP_ENV1 1 +#define ARC_BG_POP_ENV2 2 +#define ARC_BG_POP_ENV3 3 +#define ARC_BG_POP_ENV4 4 +#define ARC_BG_POP_ENV5 5 +#define ARC_BG_POP_ENV6 6 +#define ARC_BG_POP_ENV7 7 +#define ARC_BG_POP_ENV8 8 +#define ARC_BG_POP_ENV9 9 +#define ARC_BG_POP_WALL 10 +#define ARC_BG_POP_FORE 11 +#define ARC_BG_PAL_ENV0 12 +#define ARC_BG_PAL_ENV1 13 +#define ARC_BG_PAL_ENV2 14 +#define ARC_BG_PAL_ENV3 15 +#define ARC_BG_PAL_ENV4 16 +#define ARC_BG_PAL_ENV5 17 +#define ARC_BG_PAL_ENV6 18 +#define ARC_BG_PAL_ENV7 19 +#define ARC_BG_PAL_ENV8 20 +#define ARC_BG_PAL_ENV9 21 +#define ARC_BG_PAL_WALL 22 +#define ARC_BG_PAL_FORE 23 +#define ARC_BG_POP_POT 24 + +#endif diff --git a/applications/PoP/roomtest/guard_arc.h b/applications/PoP/roomtest/guard_arc.h new file mode 100644 index 0000000..f930261 --- /dev/null +++ b/applications/PoP/roomtest/guard_arc.h @@ -0,0 +1,19 @@ +/* guard_arc.h — индексы в архиве GUARD. СГЕНЕРИРОВАН + * (toolchain/pop_pack_arc.py), руками не править. */ +#ifndef GUARD_ARC_H +#define GUARD_ARC_H + +#define ARC_GUARD_COUNT 10 + +#define ARC_GUARD_G0 0 +#define ARC_GUARD_G1 1 +#define ARC_GUARD_G2 2 +#define ARC_GUARD_G3 3 +#define ARC_GUARD_G4 4 +#define ARC_GUARD_G0_V 5 +#define ARC_GUARD_G1_V 6 +#define ARC_GUARD_G2_V 7 +#define ARC_GUARD_G3_V 8 +#define ARC_GUARD_G4_V 9 + +#endif diff --git a/applications/PoP/roomtest/kid_arc.h b/applications/PoP/roomtest/kid_arc.h new file mode 100644 index 0000000..db3d846 --- /dev/null +++ b/applications/PoP/roomtest/kid_arc.h @@ -0,0 +1,67 @@ +/* kid_arc.h — индексы в архиве KID. СГЕНЕРИРОВАН + * (toolchain/pop_pack_arc.py), руками не править. */ +#ifndef KID_ARC_H +#define KID_ARC_H + +#define ARC_KID_COUNT 58 + +#define ARC_KID_KID0 0 +#define ARC_KID_KID1 1 +#define ARC_KID_KID2 2 +#define ARC_KID_KID3 3 +#define ARC_KID_KID4 4 +#define ARC_KID_KID5 5 +#define ARC_KID_KID6 6 +#define ARC_KID_KID7 7 +#define ARC_KID_KID8 8 +#define ARC_KID_KID9 9 +#define ARC_KID_KID10 10 +#define ARC_KID_KID11 11 +#define ARC_KID_KID12 12 +#define ARC_KID_KID13 13 +#define ARC_KID_KID14 14 +#define ARC_KID_KID15 15 +#define ARC_KID_KID16 16 +#define ARC_KID_KID17 17 +#define ARC_KID_KID18 18 +#define ARC_KID_KID19 19 +#define ARC_KID_KID20 20 +#define ARC_KID_KID21 21 +#define ARC_KID_KID22 22 +#define ARC_KID_KID23 23 +#define ARC_KID_KID24 24 +#define ARC_KID_KID25 25 +#define ARC_KID_KID26 26 +#define ARC_KID_KID27 27 +#define ARC_KID_KID0_V 28 +#define ARC_KID_KID1_V 29 +#define ARC_KID_KID2_V 30 +#define ARC_KID_KID3_V 31 +#define ARC_KID_KID4_V 32 +#define ARC_KID_KID5_V 33 +#define ARC_KID_KID6_V 34 +#define ARC_KID_KID7_V 35 +#define ARC_KID_KID8_V 36 +#define ARC_KID_KID9_V 37 +#define ARC_KID_KID10_V 38 +#define ARC_KID_KID11_V 39 +#define ARC_KID_KID12_V 40 +#define ARC_KID_KID13_V 41 +#define ARC_KID_KID14_V 42 +#define ARC_KID_KID15_V 43 +#define ARC_KID_KID16_V 44 +#define ARC_KID_KID17_V 45 +#define ARC_KID_KID18_V 46 +#define ARC_KID_KID19_V 47 +#define ARC_KID_KID20_V 48 +#define ARC_KID_KID21_V 49 +#define ARC_KID_KID22_V 50 +#define ARC_KID_KID23_V 51 +#define ARC_KID_KID24_V 52 +#define ARC_KID_KID25_V 53 +#define ARC_KID_KID26_V 54 +#define ARC_KID_KID27_V 55 +#define ARC_KID_SWORD 56 +#define ARC_KID_SWORD_V 57 + +#endif diff --git a/applications/PoP/roomtest/pop_arc.c b/applications/PoP/roomtest/pop_arc.c index 871c9a5..12ad845 100644 --- a/applications/PoP/roomtest/pop_arc.c +++ b/applications/PoP/roomtest/pop_arc.c @@ -13,6 +13,27 @@ #include #include "pop_arc.h" +/* Пути 8.3, как требует DSS. Держим их ЗДЕСЬ: строка обязана лежать в том + * же банке, что и код, который её читает (см. pop_arc.h). */ +static const char *const arc_path[POP_ARC_IDS] = { + "BG\\bg.arc", + "KID\\kid.arc", + "GUARD\\guard.arc", + "SKEL\\skel.arc", + "VIZIER\\vizier.arc", + "SHADOW\\shadow.arc", + "TITLE\\title.arc", + "PV\\pv.arc", + "SND\\snd.arc" +}; + +int8_t pop_arc_open_id(pop_arc_t *a, uint8_t id, + uint8_t *tbl, uint8_t tbl_max) __banked +{ + if (id >= POP_ARC_IDS) return -1; + return pop_arc_open(a, arc_path[id], tbl, tbl_max); +} + int8_t pop_arc_open(pop_arc_t *a, const char *path, uint8_t *tbl, uint8_t tbl_max) __banked { @@ -68,6 +89,21 @@ int pop_arc_read(pop_arc_t *a, const uint8_t *tbl, uint8_t idx, uint16_t skip, return bank_read_page(a->fd, page, off, len); } +int8_t pop_arc_atlas(pop_arc_t *a, const uint8_t *tbl, uint8_t idx, + atlas_t *at) __banked +{ + uint8_t blk, page; + int n; + + blk = mem_alloc_pages(1); + if (!blk) return -1; + page = mem_get_page(blk, 0); + n = pop_arc_read(a, tbl, idx, 0, page, 0, 0); + /* Дальше всё общее с atlas_load: магия 'SPA1', разбор заголовка и + * gfx_w0_page_prepare. При ошибке atlas_attach сам отпускает блок. */ + return (int8_t)atlas_attach(at, blk, page, n); +} + void pop_arc_close(pop_arc_t *a) __banked { if (a->fd >= 0) close(a->fd); diff --git a/applications/PoP/roomtest/pop_arc.h b/applications/PoP/roomtest/pop_arc.h index 3e4d5e2..20cd249 100644 --- a/applications/PoP/roomtest/pop_arc.h +++ b/applications/PoP/roomtest/pop_arc.h @@ -26,17 +26,43 @@ #define POP_ARC_H #include +#include /* atlas_t */ #define POP_ARC_HEADER 512 #define POP_ARC_SECTOR 512 #define POP_ARC_MAX 126 /* (512-8)/4 */ #define POP_ARC_REC 4 /* байт на запись таблицы */ -typedef struct { +/* Имя у структуры есть намеренно: так на неё можно сослаться из заголовков, + * которым незачем тянуть весь pop_arc.h (pop_ui.h). */ +typedef struct pop_arc_s { int fd; uint8_t count; } pop_arc_t; +/* ИМЕНА АРХИВОВ ЗНАЕТ САМ МОДУЛЬ, и это не вкусовщина: строковый литерал + * лежит в rodata СВОЕГО банка, а pop_arc.c живёт в банке 8. Указатель на + * литерал из другого банка после переключения W3 показывает на чужие + * данные — ровно так «BG\bg.arc» превращался в мусор и загрузка фона + * падала (поймано 2026-08-26). Поэтому наружу отдаём НОМЕР архива. */ +typedef enum { + POP_ARC_BG = 0, + POP_ARC_KID, + POP_ARC_GUARD, + POP_ARC_SKEL, + POP_ARC_VIZIER, + POP_ARC_SHADOW, + POP_ARC_TITLE, + POP_ARC_PV, + POP_ARC_SND, + POP_ARC_IDS +} pop_arc_id_t; + +/* Открыть архив по номеру: то же, что pop_arc_open, но имя берётся из + * таблицы внутри модуля. */ +int8_t pop_arc_open_id(pop_arc_t *a, uint8_t id, + uint8_t *tbl, uint8_t tbl_max) __banked; + /* Открыть архив и вычитать таблицу в буфер вызывающего (count*4 байт). * tbl_max — сколько записей помещается в буфер. * Возврат: число записей; -1 — не открылся, не PBA1 либо таблица не влезла. */ @@ -54,4 +80,11 @@ int pop_arc_read(pop_arc_t *a, const uint8_t *tbl, uint8_t idx, uint16_t skip, void pop_arc_close(pop_arc_t *a) __banked; +/* Атлас ПРЯМО ИЗ АРХИВА: выделить страницу, вычитать в неё элемент idx и + * отдать libbgi (atlas_attach — проверка магии + подготовка страницы к + * маппингу в W0). Полный эквивалент atlas_load, только источник другой. + * 0 — OK, -1 — ошибка (страница освобождена). */ +int8_t pop_arc_atlas(pop_arc_t *a, const uint8_t *tbl, uint8_t idx, + atlas_t *at) __banked; + #endif /* POP_ARC_H */ diff --git a/applications/PoP/roomtest/pop_cdraw.c b/applications/PoP/roomtest/pop_cdraw.c index d4f3575..ec9d807 100644 --- a/applications/PoP/roomtest/pop_cdraw.c +++ b/applications/PoP/roomtest/pop_cdraw.c @@ -18,6 +18,10 @@ #include #include #include +#include "pop_arc.h" +#include "guard_arc.h" +#include "skel_arc.h" +#include "vizier_arc.h" #include "pop_guard.h" #include "pop_kid.h" #include "_pop_kdraw.h" @@ -107,31 +111,30 @@ pop_cdraw_t pop_cd[POP_CH_N]; * грузит. Уровень БЕЗ соперников (type < 0) не грузит ничего. */ int pop_guard_load(void) __banked { - static const char *const guard_names[GUARD_PAGES] = { - "GUARD\\g0.atl", "GUARD\\g1.atl", "GUARD\\g2.atl", - "GUARD\\g3.atl", "GUARD\\g4.atl" - }; - static const char *const skel_names[GUARD_PAGES] = { - "SKEL\\g0.atl", "SKEL\\g1.atl", "SKEL\\g2.atl", - "SKEL\\g3.atl", 0 - }; - /* Джафар (уровень 13, tbl_guard_type == 3) — VIZIER.DAT: кадров столько - * же, сколько у стража, страниц пять. */ - static const char *const vizier_names[GUARD_PAGES] = { - "VIZIER\\g0.atl", "VIZIER\\g1.atl", "VIZIER\\g2.atl", - "VIZIER\\g3.atl", "VIZIER\\g4.atl" - }; - const char *const *names; + uint8_t tbl[ARC_GUARD_COUNT * POP_ARC_REC]; /* самый крупный из трёх */ + pop_arc_t arc; int8_t type = pop_level_guard_type(pop_current_level); - uint8_t i; + uint8_t i, pages, count, id; + if (type < 0) return 0; /* соперников на уровне нет */ - names = (type == 2) ? skel_names : (type == 3) ? vizier_names : guard_names; - for (i = 0; i < GUARD_PAGES; i++) { - if (names[i] == 0) break; - if (atlas_load(&gp[i], names[i]) != 0) return -1; - } + /* Наборы разложены по своим архивам, а не в один: имена внутри у них + * совпадают (g0.atl у всех троих), да и грузится всегда ровно один. */ + if (type == 2) { id = POP_ARC_SKEL; count = ARC_SKEL_COUNT; + pages = ARC_SKEL_COUNT; } + else if (type == 3) { id = POP_ARC_VIZIER; count = ARC_VIZIER_COUNT; + pages = ARC_VIZIER_COUNT; } + else { id = POP_ARC_GUARD; count = ARC_GUARD_COUNT; + pages = GUARD_PAGES; } + if (pages > GUARD_PAGES) pages = GUARD_PAGES; + if (pop_arc_open_id(&arc, id, tbl, count) < 0) return -1; + for (i = 0; i < pages; i++) + if (pop_arc_atlas(&arc, tbl, i, &gp[i]) != 0) { + pop_arc_close(&arc); + return -1; + } + pop_arc_close(&arc); g_ok = 1; - /* Цвет: у скелета он СВОЙ (палитра из его набора), у стража подменяется +/* Цвет: у скелета он СВОЙ (палитра из его набора), у стража подменяется * из res10.bin по guards_color — это делает pop_guard_set_palette при * входе в комнату (seg002:186: у не-guard curr_guard_color = 0). */ pop_guard_pal_apply(); @@ -143,24 +146,29 @@ int pop_guard_load(void) __banked * потому что 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; + uint8_t tbl[ARC_GUARD_COUNT * POP_ARC_REC]; + pop_arc_t arc; int8_t type = pop_level_guard_type(pop_current_level); uint8_t i; + int res = 0; + if (type < 0 || !g_ok) return 0; /* соперников на уровне нет */ - names = (type == 2) ? skel_v : guard_v; + /* Зеркала есть ТОЛЬКО у обычного стража: зелье инверсии — уровень 9, а + * там tbl_guard_type = 0 (у скелета и Джафара своих *_v нет вовсе). */ + if (type != 0 && type != 1) return 0; + if (pop_arc_open_id(&arc, POP_ARC_GUARD, tbl, ARC_GUARD_COUNT) < 0) + return -1; for (i = 0; i < GUARD_PAGES; i++) { - if (names[i] == 0) break; - if (pop_vflip_add(&gp[i], names[i]) != 0) return -1; + atlas_t m; + + if (pop_arc_atlas(&arc, tbl, (uint8_t)(ARC_GUARD_G0_V + i), &m) != 0 || + pop_vflip_take(&gp[i], &m) != 0) { + res = -1; + break; + } } - return 0; + pop_arc_close(&arc); + return res; } void pop_guard_free(void) __banked diff --git a/applications/PoP/roomtest/pop_intro.c b/applications/PoP/roomtest/pop_intro.c index 34e5872..d477736 100644 --- a/applications/PoP/roomtest/pop_intro.c +++ b/applications/PoP/roomtest/pop_intro.c @@ -15,6 +15,8 @@ #include "pop_shadow.h" #include "pop_sfx.h" #include "pop_music.h" +#include "pop_arc.h" +#include "pv_arc.h" #include "pop_cutscene.h" #include "pop_intro.h" #include "pop_pace.h" /* pop_wait_edge */ @@ -85,31 +87,15 @@ enum { INTRO_PAL_PV = 1 }; -/* 0..4 — TITLE/res41 + тексты 42/43/45/44 и чистая рамка; 5..12 — - * PV/res951 + персонажи. - * 8.3-имена намеренно короткие: DSS не получает длинные path-компоненты. */ -static const char * const intro_part[13][INTRO_PARTS] = { - { "PV\\s0_0.atl", "PV\\s0_1.atl", "PV\\s0_2.atl", "PV\\s0_3.atl", "PV\\s0_4.atl" }, - { "PV\\s1_0.atl", "PV\\s1_1.atl", "PV\\s1_2.atl", "PV\\s1_3.atl", "PV\\s1_4.atl" }, - { "PV\\s2_0.atl", "PV\\s2_1.atl", "PV\\s2_2.atl", "PV\\s2_3.atl", "PV\\s2_4.atl" }, - { "PV\\s3_0.atl", "PV\\s3_1.atl", "PV\\s3_2.atl", "PV\\s3_3.atl", "PV\\s3_4.atl" }, - { "PV\\s4_0.atl", "PV\\s4_1.atl", "PV\\s4_2.atl", "PV\\s4_3.atl", "PV\\s4_4.atl" }, - { "PV\\p0_0.atl", "PV\\p0_1.atl", "PV\\p0_2.atl", "PV\\p0_3.atl", "PV\\p0_4.atl" }, - { "PV\\p1_0.atl", "PV\\p1_1.atl", "PV\\p1_2.atl", "PV\\p1_3.atl", "PV\\p1_4.atl" }, - { "PV\\p2_0.atl", "PV\\p2_1.atl", "PV\\p2_2.atl", "PV\\p2_3.atl", "PV\\p2_4.atl" }, - { "PV\\p3_0.atl", "PV\\p3_1.atl", "PV\\p3_2.atl", "PV\\p3_3.atl", "PV\\p3_4.atl" }, - { "PV\\p4_0.atl", "PV\\p4_1.atl", "PV\\p4_2.atl", "PV\\p4_3.atl", "PV\\p4_4.atl" }, - { "PV\\p5_0.atl", "PV\\p5_1.atl", "PV\\p5_2.atl", "PV\\p5_3.atl", "PV\\p5_4.atl" }, - { "PV\\p6_0.atl", "PV\\p6_1.atl", "PV\\p6_2.atl", "PV\\p6_3.atl", "PV\\p6_4.atl" }, - { "PV\\p7_0.atl", "PV\\p7_1.atl", "PV\\p7_2.atl", "PV\\p7_3.atl", "PV\\p7_4.atl" } -}; - -/* Чистая Princess-room без персонажей. Shadow-копии обеих страниц содержат - * именно этот фон: каждый анимационный кадр получает его accelerator-копией, - * а полупрозрачные actor-спрайты не оставляют следа. */ -static const char * const pv_base_part[INTRO_PARTS] = { - "PV\\b0_0.atl", "PV\\b0_1.atl", "PV\\b0_2.atl", "PV\\b0_3.atl", "PV\\b0_4.atl" -}; +/* Экраны PV лежат в одном архиве по пять полос подряд, ровно в том же + * порядке, в каком идут номера сцен: сначала пять текстовых (story + * res41 + тексты 42/43/45/44 и чистая рамка), затем восемь стадий комнаты + * принцессы (res951 + персонажи) и чистый фон b0. Поэтому имён здесь нет + * — индекс первой полосы экрана считается умножением. */ +#if (ARC_PV_P0_0 - ARC_PV_S0_0) != 5 * INTRO_PARTS +#error "экраны PV в архиве лежат не по пять полос подряд" +#endif +#define INTRO_SCENES 13 /* Две текстовые половины show_title(). Между ними PV теперь идёт отдельным * покадровым renderer-ом, а не четырьмя полными статичными композициями. */ @@ -225,13 +211,12 @@ static uint8_t intro_scene; static uint8_t intro_pending_pal; static uint8_t intro_pal_pending; -/* Таблицы intro_part/pv_base_part расположены в этом же банке 11. Общий - * pop_screen_blit_atlas_strips находится в банке 9, и передавать ему - * указатель на W3-данные нельзя: после bcall тот же адрес уже видит другой - * физический банк. Держим короткий полосовой loader рядом с его таблицами. - * atlas_load сам временно занимает W3 только ПОСЛЕ open(path). */ -static int intro_blit_atlas_strips(const char * const *parts, - uint8_t count, int x, int y) +/* Полосовой загрузчик рядом со своими вызовами: общий из банка 9 звать + * можно (индексы и таблица — числа на стеке W2, а не указатели на W3), но + * лишний банк→банк на каждую полосу тут ни к чему. + * pop_arc_atlas сам временно занимает W3 только на время чтения. */ +static int intro_blit_arc(pop_arc_t *arc, const uint8_t *tbl, + uint8_t base, uint8_t count, int x, int y) { uint8_t part; @@ -240,7 +225,8 @@ static int intro_blit_atlas_strips(const char * const *parts, atlas_t at; const uint8_t *img; - if (atlas_load(&at, parts[part]) != 0) return -1; + if (pop_arc_atlas(arc, tbl, (uint8_t)(base + part), &at) != 0) + return -1; gfx_w0_map(at.page); img = (const uint8_t *)atlas_image(&at, 0); gfx_blit(x, y, img); @@ -251,6 +237,30 @@ static int intro_blit_atlas_strips(const char * const *parts, return 0; } +/* Экран целиком: открыть архив, вылить пять полос, закрыть. Один `open` + * на экран вместо пяти — при 51,4 мс за открытие это больше половины + * времени показа. */ +static int intro_blit_screen(uint8_t base, int y) +{ + uint8_t tbl[ARC_PV_COUNT * POP_ARC_REC]; + pop_arc_t arc; + int res; + + if (pop_arc_open_id(&arc, POP_ARC_PV, tbl, ARC_PV_COUNT) < 0) return -1; + res = intro_blit_arc(&arc, tbl, base, INTRO_PARTS, 0, y); + pop_arc_close(&arc); + return res; +} + +/* Один актёрский атлас PV из архива. Открытие на каждый — всё равно + * дешевле, чем прежние отдельные файлы (тот же `open`, но без поиска имени + * в каталоге); там, где их несколько подряд, архив открывается один раз. */ +static int pv_atlas(pop_arc_t *arc, const uint8_t *tbl, uint8_t idx, + atlas_t *at) +{ + return pop_arc_atlas(arc, tbl, idx, at) != 0 ? -1 : 0; +} + /* ---- «Пропустить сцену» — по НОВОМУ нажатию, а не по удержанию -------- * * * Клавиша, которой закончили уровень (движение Кида в дверь, чит Shift+L, @@ -280,14 +290,12 @@ static uint8_t intro_skip_requested(intro_skip_t *sk) static int intro_draw_page(uint8_t scene, uint8_t page) { - if (scene >= sizeof(intro_part) / sizeof(intro_part[0])) return -1; + if (scene >= INTRO_SCENES) return -1; gfx_set_draw_page(page); gfx_set_bank(GFX_BANK_NORMAL); pop_screen_fill(BLACK); - if (intro_blit_atlas_strips(intro_part[scene], INTRO_PARTS, - 0, POP_YOFF) != 0) - return -1; - return 0; + return intro_blit_screen((uint8_t)(ARC_PV_S0_0 + scene * INTRO_PARTS), + POP_YOFF); } static int intro_draw(uint8_t scene) @@ -356,9 +364,7 @@ static int intro_pv_draw_base(void) gfx_set_draw_page(0); gfx_set_bank(GFX_BANK_NORMAL); pop_screen_fill(BLACK); - if (intro_blit_atlas_strips(pv_base_part, INTRO_PARTS, - 0, POP_YOFF) != 0) - return -1; + if (intro_blit_screen(ARC_PV_B0_0, POP_YOFF) != 0) return -1; gfx_set_draw_page(1); full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT; gfx_copy_page(&full, GFX_COPY_DIRECT); @@ -695,7 +701,13 @@ static int intro_pv_animated(void) uint16_t tick; uint16_t frame_end; uint16_t render_tick; - uint16_t beam = 0; /* потрачено кадров луча с начала сцены */ + uint16_t beam = 0; /* запасной путь: кадров луча ждали */ + uint16_t snd_done = 0; /* тиков насоса прошло с начала сцены */ + uint16_t snd_want = 0; /* сколько их должно было пройти */ + uint16_t snd_acc = 0; /* дробный остаток пересчёта 57/40 */ + uint8_t snd_prev = 0; + uint8_t snd_pace = 0; + uint8_t period; uint8_t anim_step = 0; uint8_t torch_left = 1; uint8_t torch_right = 6; @@ -717,21 +729,21 @@ static int intro_pv_animated(void) * временная EMM-страница. Поэтому фон готовится до постоянных * actor-атласов: порядок сохраняет независимость загрузчика полос от * числа одновременно живущих анимационных ресурсов. */ - if (atlas_load(&princess, "PV\\a0.atl") != 0) return -1; - if (atlas_load(&jaffar0, "PV\\j0.atl") != 0) { - goto free_princess; - } - if (atlas_load(&jaffar1, "PV\\j1.atl") != 0) { - goto free_jaffar0; - } - if (atlas_load(&jaffar2, "PV\\j2.atl") != 0) { - goto free_jaffar1; - } - if (atlas_load(&hourglass, "PV\\h0.atl") != 0) { - goto free_jaffar2; - } - if (atlas_load(&torch, "PV\\t0.atl") != 0) { - goto free_hourglass; + /* Шесть атласов сцены — из ОДНОГО открытого архива. */ + { + uint8_t tbl[ARC_PV_COUNT * POP_ARC_REC]; + pop_arc_t arc; + int8_t bad; + + if (pop_arc_open_id(&arc, POP_ARC_PV, tbl, ARC_PV_COUNT) < 0) return -1; + bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_A0, &princess) != 0); + if (!bad) bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_J0, &jaffar0) != 0); + if (!bad) bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_J1, &jaffar1) != 0); + if (!bad) bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_J2, &jaffar2) != 0); + if (!bad) bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_H0, &hourglass) != 0); + if (!bad) bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_T0, &torch) != 0); + pop_arc_close(&arc); + if (bad) return -1; } pop_pal_fade_in(INTRO_FADE); @@ -740,6 +752,18 @@ static int intro_pv_animated(void) * Полноэкранная accel-копия кадра насосу не мешает: leaf режет DI * бандами по 16 строк. */ (void)pop_sfx_start(); + /* ЧЕМ МЕРИТЬ ВРЕМЯ. Пока сцена была немой, кадры просто считали + * ожидания vsync — и этого хватало. С музыкой выяснилось, что так + * считаются НАШИ ожидания, а не прошедшее время: полноэкранная копия + * страницы (~0,55 млн тактов) съедает больше кадра луча, и сцена + * незаметно отстаёт — в MAME молния била уже ПОСЛЕ конца реплики. + * Часы берём у насоса CBL: он идёт от расхода буфера железом, 85,4 Гц, + * и ему безразлично, чем занят главный цикл. Если звук выключен, + * часы стоят — тогда работает прежний путь по кадрам луча. */ + snd_prev = pop_snd_tick; + gfx_wait_vsync(); + snd_pace = (uint8_t)(pop_snd_tick != snd_prev); /* часы реально идут? */ + snd_prev = pop_snd_tick; result = 0; princess_actor.x = 120; princess_actor.frame = 11; /* Pstand, facing left */ @@ -763,7 +787,8 @@ static int intro_pv_animated(void) * его исходные 6/8/7 тиков, снаружи draw_frame держит результат * четыре физических кадра — единые номинальные 12,5 FPS. */ flash = 0; - frame_end = (uint16_t)(tick + pv_seq_period(tick)); + period = pv_seq_period(tick); + frame_end = (uint16_t)(tick + period); if (frame_end > PV_ANIM_TICKS) frame_end = PV_ANIM_TICKS; do { pv_princess_step(&princess_actor, tick); @@ -811,9 +836,23 @@ static int intro_pv_animated(void) * анимации не стоят. */ if (pop_music_loading()) (void)pop_music_load_step(); /* ПЕЙСИНГ ПО ОРИГИНАЛЬНОЙ ШКАЛЕ. Кадр cutscene — 6/8/7 тиков по - * 1/60 с, наш экран идёт на 1/50: без пересчёта сцена растянулась - * бы на пятую часть, и реплики разъехались бы с картинкой. */ - { + * 1/60 с; один тик = 85,4/60 = 1,424 тика насоса, и 57/40 = 1,425 + * повторяет это с точностью 0,07 % (30 мс на всю сцену) без + * деления в кадре и без 32-битной арифметики. */ + if (snd_pace) { + snd_acc += (uint16_t)period * 57u; + while (snd_acc >= 40u) { snd_acc -= 40u; snd_want++; } + for (;;) { + uint8_t now = pop_snd_tick; + + /* Разность в uint8_t: счётчик переполняется каждые 3 с, а + * заглядываем мы в него не реже раза в кадр. */ + snd_done += (uint8_t)(now - snd_prev); + snd_prev = now; + if (snd_done >= snd_want) break; + gfx_wait_vsync(); + } + } else { uint16_t target = POP_T60(tick); while (beam < target) { gfx_wait_vsync(); beam++; } @@ -826,17 +865,11 @@ static int intro_pv_animated(void) if (result) { pop_music_free(); pop_sfx_pause(); } if (!result) pop_pal_fade_out(INTRO_FADE); -free_torch: atlas_free(&torch); -free_hourglass: atlas_free(&hourglass); -free_jaffar2: atlas_free(&jaffar2); -free_jaffar1: atlas_free(&jaffar1); -free_jaffar0: atlas_free(&jaffar0); -free_princess: atlas_free(&princess); return result; } @@ -876,8 +909,17 @@ static int pre_princess_animated(pop_pre_cutscene_t scene) * рисуется GFX_BANK_SPRITE и потому не оседает в этом источнике. */ if (intro_draw_page(picture, 0) != 0 || intro_draw_page(picture, 1) != 0) return -1; - if (atlas_load(&hourglass, "PV\\h0.atl") != 0) return -1; - if (atlas_load(&torch, "PV\\t0.atl") != 0) goto free_hourglass_pre; + { + uint8_t tbl[ARC_PV_COUNT * POP_ARC_REC]; + pop_arc_t arc; + int8_t bad; + + if (pop_arc_open_id(&arc, POP_ARC_PV, tbl, ARC_PV_COUNT) < 0) return -1; + bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_H0, &hourglass) != 0); + if (!bad) bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_T0, &torch) != 0); + pop_arc_close(&arc); + if (bad) return -1; + } pop_screen_present(1); pop_pal_fade_in(INTRO_FADE); @@ -912,7 +954,6 @@ static int pre_princess_animated(pop_pre_cutscene_t scene) if (!result) pop_pal_fade_out(INTRO_FADE); atlas_free(&torch); -free_hourglass_pre: atlas_free(&hourglass); return result; } @@ -1026,20 +1067,35 @@ static void cut_free(void) static int cut_load(void) { + uint8_t tbl[ARC_PV_COUNT * POP_ARC_REC]; + pop_arc_t arc; + cut_atlases = 0; - if (atlas_load(&cut_pv1, "PV\\a0.atl") != 0) return -1; + if (pop_arc_open_id(&arc, POP_ARC_PV, tbl, ARC_PV_COUNT) < 0) return -1; + if (pv_atlas(&arc, tbl, ARC_PV_A0, &cut_pv1) != 0) { + pop_arc_close(&arc); return -1; + } cut_atlases = 1; - if (atlas_load(&cut_pv2a, "PV\\a1.atl") != 0) { cut_free(); return -1; } + if (pv_atlas(&arc, tbl, ARC_PV_A1, &cut_pv2a) != 0) { + pop_arc_close(&arc); cut_free(); return -1; + } cut_atlases = 2; - if (atlas_load(&cut_pv2b, "PV\\a2.atl") != 0) { cut_free(); return -1; } + if (pv_atlas(&arc, tbl, ARC_PV_A2, &cut_pv2b) != 0) { + pop_arc_close(&arc); cut_free(); return -1; + } cut_atlases = 3; /* Комната принцессы живёт и в катсценах: пламя факелов, звёзды в окне, * песок в часах — и ПЕРЕДНЯЯ КОЛОННА, которая по оригиналу закрывает * персонажей (draw_princess_room_bg + foretable). */ - if (atlas_load(&cut_hg, "PV\\h0.atl") != 0) { cut_free(); return -1; } + if (pv_atlas(&arc, tbl, ARC_PV_H0, &cut_hg) != 0) { + pop_arc_close(&arc); cut_free(); return -1; + } cut_atlases = 4; - if (atlas_load(&cut_torch, "PV\\t0.atl") != 0) { cut_free(); return -1; } + if (pv_atlas(&arc, tbl, ARC_PV_T0, &cut_torch) != 0) { + pop_arc_close(&arc); cut_free(); return -1; + } cut_atlases = 5; + pop_arc_close(&arc); return 0; } diff --git a/applications/PoP/roomtest/pop_kboot.c b/applications/PoP/roomtest/pop_kboot.c index 4646145..612494a 100644 --- a/applications/PoP/roomtest/pop_kboot.c +++ b/applications/PoP/roomtest/pop_kboot.c @@ -16,23 +16,46 @@ #include "kid_data.h" #include "pop_vflip.h" #include "pop_guard.h" /* pop_guard_vflip_load — зеркала соперника */ +#include "pop_arc.h" +#include "kid_arc.h" + +/* Серии в архиве обязаны лежать подряд — так их кладёт список в Makefile, + * и только поэтому индекс считается сложением. Если порядок собьётся, + * сборка встанет здесь, а не рассыплется картинкой на экране. */ +#if (ARC_KID_KID27 - ARC_KID_KID0) != 27 +#error "kid0..kid27 в архиве не подряд" +#endif +#if (ARC_KID_KID27_V - ARC_KID_KID0_V) != 27 +#error "kid0_v..kid27_v в архиве не подряд" +#endif #define KID_MAXPAGES 28 +/* Таблица архива живёт на стеке вызывающего (4 байта на запись, здесь 232) + * — держать её постоянно незачем, а W2 у игры и без того тесный. */ int pop_kid_load(uint8_t npages) __banked { - static const char *const names[KID_MAXPAGES] = { - "KID\\kid0.atl","KID\\kid1.atl","KID\\kid2.atl","KID\\kid3.atl","KID\\kid4.atl","KID\\kid5.atl", - "KID\\kid6.atl","KID\\kid7.atl","KID\\kid8.atl","KID\\kid9.atl","KID\\kid10.atl","KID\\kid11.atl", - "KID\\kid12.atl","KID\\kid13.atl","KID\\kid14.atl","KID\\kid15.atl","KID\\kid16.atl","KID\\kid17.atl", - "KID\\kid18.atl","KID\\kid19.atl","KID\\kid20.atl","KID\\kid21.atl","KID\\kid22.atl","KID\\kid23.atl", - "KID\\kid24.atl","KID\\kid25.atl","KID\\kid26.atl","KID\\kid27.atl" - }; + uint8_t tbl[ARC_KID_COUNT * POP_ARC_REC]; + pop_arc_t arc; uint8_t i; + if (npages > KID_MAXPAGES) npages = KID_MAXPAGES; + if (pop_arc_open_id(&arc, POP_ARC_KID, tbl, ARC_KID_COUNT) < 0) return -1; for (i = 0; i < npages; i++) - if (atlas_load(&kidp[i], names[i]) != 0) + if (pop_arc_atlas(&arc, tbl, (uint8_t)(ARC_KID_KID0 + i), + &kidp[i]) != 0) { + pop_arc_close(&arc); return -1; + } + /* Меч — из того же открытого архива: без него кадры 229+ идут без + * клинка, а второй open стоил бы дороже самого чтения. */ + { + atlas_t sword; + + if (pop_arc_atlas(&arc, tbl, ARC_KID_SWORD, &sword) == 0) + (void)pop_sword_take(&sword); + } + pop_arc_close(&arc); kid_npages = npages; return 0; } @@ -73,21 +96,28 @@ void pop_kid_data_free(void) __banked * только через него). Идемпотентна. */ 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 tbl[ARC_KID_COUNT * POP_ARC_REC]; + pop_arc_t arc; 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_arc_open_id(&arc, POP_ARC_KID, tbl, ARC_KID_COUNT) < 0) return -1; + for (i = 0; i < kid_npages; i++) { + atlas_t m; + + if (pop_arc_atlas(&arc, tbl, (uint8_t)(ARC_KID_KID0_V + i), &m) != 0 || + pop_vflip_take(&kidp[i], &m) != 0) + res = -1; + } + { + atlas_t sword_v; + + if (pop_arc_atlas(&arc, tbl, ARC_KID_SWORD_V, &sword_v) != 0 || + pop_sword_vflip_take(&sword_v) != 0) + res = -1; + } + pop_arc_close(&arc); 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 ac19876..dcc6fdc 100644 --- a/applications/PoP/roomtest/pop_kdraw.c +++ b/applications/PoP/roomtest/pop_kdraw.c @@ -37,18 +37,22 @@ static const int8_t SWORD_DY[KID_NSWORD] = KID_SWORD_DY_INIT; static atlas_t swordp; static uint8_t sword_ok; -int pop_sword_load(void) __banked +/* Меч приходит УЖЕ ЗАГРУЖЕННЫМ: его читает pop_kid_load из того же архива, + * что и остальные страницы Кида, — открывать архив второй раз ради одного + * элемента незачем (`open` дороже самого чтения). */ +int pop_sword_take(const atlas_t *a) __banked { - if (atlas_load(&swordp, "KID\\sword.atl") != 0) return -1; + if (!a) return -1; + swordp = *a; sword_ok = 1; return 0; } -/* Зеркальная страница меча (зелье инверсии) — пара к pop_sword_load. */ -int pop_sword_vflip_load(void) __banked +/* Зеркальная страница меча (зелье инверсии) — пара к pop_sword_take. */ +int pop_sword_vflip_take(const atlas_t *m) __banked { if (!sword_ok) return 0; - return pop_vflip_add(&swordp, "KID\\sword_v.atl"); + return pop_vflip_take(&swordp, m); } /* add_sword_to_objtable (seg006:1798). Клинок рисуется, если персонаж в diff --git a/applications/PoP/roomtest/pop_kid.h b/applications/PoP/roomtest/pop_kid.h index 5980e37..8527a66 100644 --- a/applications/PoP/roomtest/pop_kid.h +++ b/applications/PoP/roomtest/pop_kid.h @@ -47,9 +47,9 @@ void pop_kid_data_free(void) __banked; * подъёма меча/убирания в ножны (add_sword_to_objtable, seg006:1798). * Грузить ДО initgraph, как и страницы Kid. 0 — OK, -1 — ошибка (тогда * кадры рисуются без клинка). */ -int pop_sword_load(void) __banked; /* Зеркальная (вертикально) страница меча — для зелья инверсии. */ -int pop_sword_vflip_load(void) __banked; +int pop_sword_take(const atlas_t *a) __banked; +int pop_sword_vflip_take(const atlas_t *m) __banked; void pop_kid_free(void) __banked; /* Сама отрисовка клинка (pop_sword_draw) и блит спрайта chtab_2 по id diff --git a/applications/PoP/roomtest/pop_room.c b/applications/PoP/roomtest/pop_room.c index eb29305..23e4f7a 100644 --- a/applications/PoP/roomtest/pop_room.c +++ b/applications/PoP/roomtest/pop_room.c @@ -22,6 +22,8 @@ #include "_pop_bg.h" /* контракт с горячей половиной (pop_bg.c, банк 2) */ #include "pop_tile.h" /* общие листья слоя фона (резидент): блит, тайлы, таблица */ #include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */ +#include "pop_arc.h" +#include "bg_arc.h" #include "pop_state.h" #include "pop_redraw.h" /* пометки тайлов: сосед падающего куска (draw_mob) */ #include "pop_geom.h" @@ -57,6 +59,12 @@ static uint8_t bg_set = 0xFF; /* какой набор сейча * персонаж, GUARD — стражи, LEVELS — уровни. Иначе корень диска зарастает * десятками .atl. Путь склеиваем на месте: держать по строке на каждый из * двадцати атласов — лишние полкилобайта в банке. */ +/* Шаг набора в архиве: десять env + wall + fore. */ +#define BG_SET_STRIDE (POP_ENV_PAGES + 2) +#if (ARC_BG_PAL_ENV0 - ARC_BG_POP_ENV0) != BG_SET_STRIDE +#error "тайлсеты в архиве фона лежат не по двенадцать атласов" +#endif + static char bg_path_buf[20]; static const char *bg_path(const char *name) @@ -198,12 +206,33 @@ int pop_bg_load(uint8_t set) __banked atlas_free(&pop_wall_at); atlas_free(&pop_fore_at); } - for (i = 0; i < POP_ENV_PAGES; i++) - if (atlas_load(&pop_env[i], bg_path(pop_env_atl[set][i])) != 0) return -1; - if (atlas_load(&pop_wall_at, bg_path(pop_wall_atl[set])) != 0) return -1; - if (atlas_load(&pop_fore_at, bg_path(pop_fore_atl[set])) != 0) return -1; - if (bg_set == 0xFF && /* chtab_1 от набора не зависит */ - atlas_load(&pop_pot_at, bg_path(POP_POT_ATL)) != 0) return -1; + /* Двадцать пять атласов фона лежат в одном архиве: двенадцать на + * тайлсет подряд плюс общий chtab_1. Открываем его ОДИН раз на смену + * набора — при цене `open` 51,4 мс это и есть основная экономия. */ + { + uint8_t tbl[ARC_BG_COUNT * POP_ARC_REC]; + pop_arc_t arc; + uint8_t base = (uint8_t)(set * BG_SET_STRIDE); + int8_t bad = 0; + + if (pop_arc_open_id(&arc, POP_ARC_BG, tbl, ARC_BG_COUNT) < 0) return -1; + for (i = 0; i < POP_ENV_PAGES && !bad; i++) + bad = (int8_t)(pop_arc_atlas(&arc, tbl, (uint8_t)(base + i), + &pop_env[i]) != 0); + if (!bad) + bad = (int8_t)(pop_arc_atlas(&arc, tbl, + (uint8_t)(base + POP_ENV_PAGES), + &pop_wall_at) != 0); + if (!bad) + bad = (int8_t)(pop_arc_atlas(&arc, tbl, + (uint8_t)(base + POP_ENV_PAGES + 1), + &pop_fore_at) != 0); + if (!bad && bg_set == 0xFF) /* chtab_1 от набора не зависит */ + bad = (int8_t)(pop_arc_atlas(&arc, tbl, ARC_BG_POP_POT, + &pop_pot_at) != 0); + pop_arc_close(&arc); + if (bad) return -1; + } bg_set = set; pop_palace = (uint8_t)(set == 1); mob_spr_build(); /* композит падающего куска — свой на набор */ diff --git a/applications/PoP/roomtest/pop_sfx.c b/applications/PoP/roomtest/pop_sfx.c index a0162af..bc4647e 100644 --- a/applications/PoP/roomtest/pop_sfx.c +++ b/applications/PoP/roomtest/pop_sfx.c @@ -52,6 +52,9 @@ volatile uint16_t pop_mus_left; * таблицах приоритета и перебиваемости, что и эффекты (см. pop_sfx_tick). */ uint8_t pop_mus_id; +/* Тик насоса: +1 на каждый отданный CBL блок, то есть 85,4 Гц. */ +volatile uint8_t pop_snd_tick; + static volatile uint8_t sfx_pg; /* индекс в pop_snd_page[] */ static volatile uint16_t sfx_ptr; /* адрес в окне W0: 0x0000..0x3FFF */ static volatile uint16_t sfx_left; /* сколько байт осталось (кратно 128) */ @@ -94,6 +97,14 @@ int pop_sfx_fill(uint16_t n) uint8_t saved, phys; uint16_t ptr; + /* ЧАСЫ СЦЕН. Насос зовётся строго по расходу буфера CBL — 85,4 раза в + * секунду, что бы ни делал главный цикл. Это единственный источник времени, + * которому не мешают ни долгие блиты, ни чтение с диска: кадровые + * прерывания теряются в di-окнах акселератора (pop_pace.h), а счёт + * ожиданий vsync считает НАШИ ожидания, а не прошедшее время. + * Катсцены пейсятся по нему — иначе картинка уезжает от музыки. */ + pop_snd_tick++; + /* Три источника по убыванию важности: эффект, музыка, тишина. Эффект * перебивает музыку — как у оригинала, где play_sound глушит всё, что * звучало; голос-то один. Музыка при этом НЕ теряет позицию: её diff --git a/applications/PoP/roomtest/pop_sfx.h b/applications/PoP/roomtest/pop_sfx.h index a6b521c..2ec19cf 100644 --- a/applications/PoP/roomtest/pop_sfx.h +++ b/applications/PoP/roomtest/pop_sfx.h @@ -49,6 +49,16 @@ void pop_sfx_toggle(void) __banked; * честно means «звука не будет». */ extern uint8_t pop_snd_want; +/* ЧАСЫ ДЛЯ СЦЕН: +1 на каждый блок, отданный CBL, то есть ровно 85,4 Гц + * (10 937,5 / 128), пока звук открыт. Идут от расхода буфера железом, а + * не от нашего кода, поэтому не врут ни на долгих блитах, ни на чтении с + * диска — в отличие от счёта ожиданий vsync (тот считает НАШИ ожидания) и + * от кадровых прерываний (те теряются в di-окнах акселератора, pop_pace.h). + * Переполняется каждые 3 с — разности брать в uint8_t и накапливать. + * Когда CBL закрыт (звук выключён), НЕ ИДУТ: у пейсинга обязан быть + * запасной путь по кадрам луча. */ +extern volatile uint8_t pop_snd_tick; + /* Заявить эффект с номером id (0..56 — нумерация оригинала, см. * sound_plan.md). НЕ запускает его немедленно и НЕ перебивает играющий: * это порт play_sound (seg000:12C5), который лишь НОМИНИРУЕТ кандидата на diff --git a/applications/PoP/roomtest/pop_sfx_cold.c b/applications/PoP/roomtest/pop_sfx_cold.c index 0211e31..425a8e4 100644 --- a/applications/PoP/roomtest/pop_sfx_cold.c +++ b/applications/PoP/roomtest/pop_sfx_cold.c @@ -31,7 +31,7 @@ int pop_sfx_init(void) __banked pop_arc_t arc; uint8_t tbl[POP_SND_PAGES * POP_ARC_REC]; - if (pop_arc_open(&arc, "SND\\snd.arc", tbl, POP_SND_PAGES) != + if (pop_arc_open_id(&arc, POP_ARC_SND, tbl, POP_SND_PAGES) != POP_SND_PAGES) return -1; blk = mem_alloc_pages(POP_SND_PAGES); diff --git a/applications/PoP/roomtest/pop_shadow.c b/applications/PoP/roomtest/pop_shadow.c index 34e3b3d..721dcb2 100644 --- a/applications/PoP/roomtest/pop_shadow.c +++ b/applications/PoP/roomtest/pop_shadow.c @@ -10,6 +10,8 @@ #include #include #include +#include "pop_arc.h" +#include "shadow_arc.h" #include "pop_shadow.h" #include "pop_shadow_atlas.h" @@ -37,19 +39,34 @@ int pop_shadow_load(void) __banked uint8_t i; if (pop_shadow_kid_n || pop_shadow_fgt_n) return 0; /* уже загружен */ - for (i = 0; i < POP_SHADOW_KID_PAGES; i++) { - if (atlas_load(&pop_shadow_kid[i], pop_shadow_kid_atl[i]) != 0) { - pop_shadow_free(); + { + uint8_t tbl[ARC_SHADOW_COUNT * POP_ARC_REC]; + pop_arc_t arc; + + /* Тридцать два атласа — из одного архива: раньше это были тридцать + * два `open` по 51,4 мс, то есть полторы секунды на ровном месте. */ + if (pop_arc_open_id(&arc, POP_ARC_SHADOW, tbl, + ARC_SHADOW_COUNT) < 0) return -1; + for (i = 0; i < POP_SHADOW_KID_PAGES; i++) { + if (pop_arc_atlas(&arc, tbl, (uint8_t)(ARC_SHADOW_SK0 + i), + &pop_shadow_kid[i]) != 0) { + pop_arc_close(&arc); + pop_shadow_free(); + return -1; + } + pop_shadow_kid_n = (uint8_t)(i + 1); } - pop_shadow_kid_n = (uint8_t)(i + 1); - } - for (i = 0; i < POP_SHADOW_FGT_PAGES; i++) { - if (atlas_load(&pop_shadow_fgt[i], pop_shadow_fgt_atl[i]) != 0) { - pop_shadow_free(); - return -1; + for (i = 0; i < POP_SHADOW_FGT_PAGES; i++) { + if (pop_arc_atlas(&arc, tbl, (uint8_t)(ARC_SHADOW_SF0 + i), + &pop_shadow_fgt[i]) != 0) { + pop_arc_close(&arc); + pop_shadow_free(); + return -1; + } + pop_shadow_fgt_n = (uint8_t)(i + 1); } - pop_shadow_fgt_n = (uint8_t)(i + 1); + pop_arc_close(&arc); } pop_shadow_pal_apply(); diff --git a/applications/PoP/roomtest/pop_title.c b/applications/PoP/roomtest/pop_title.c index ea6025f..d51a88f 100644 --- a/applications/PoP/roomtest/pop_title.c +++ b/applications/PoP/roomtest/pop_title.c @@ -14,6 +14,8 @@ #include "pop_bg.h" /* POP_YOFF: title занимает 200px по центру 256px */ #include "pop_sfx.h" #include "pop_music.h" +#include "pop_arc.h" +#include "title_arc.h" #include "pop_cutscene.h" /* POP_T60 */ #include "pop_settings.h" #include "pop_pal.h" @@ -33,13 +35,11 @@ enum { TITLE_LOGO }; -/* Имена короче 8.3; строка = композиция, столбец = полоса 49/49/49/49/4. */ -static const char * const title_part[4][TITLE_PARTS] = { - { "TITLE\\t0_0.atl", "TITLE\\t0_1.atl", "TITLE\\t0_2.atl", "TITLE\\t0_3.atl", "TITLE\\t0_4.atl" }, - { "TITLE\\t1_0.atl", "TITLE\\t1_1.atl", "TITLE\\t1_2.atl", "TITLE\\t1_3.atl", "TITLE\\t1_4.atl" }, - { "TITLE\\t2_0.atl", "TITLE\\t2_1.atl", "TITLE\\t2_2.atl", "TITLE\\t2_3.atl", "TITLE\\t2_4.atl" }, - { "TITLE\\t3_0.atl", "TITLE\\t3_1.atl", "TITLE\\t3_2.atl", "TITLE\\t3_3.atl", "TITLE\\t3_4.atl" } -}; +/* Композиции лежат в одном архиве подряд по пять полос 49/49/49/49/4, + * поэтому имён здесь больше нет: индекс = сцена * TITLE_PARTS. */ +#if (ARC_TITLE_T1_0 - ARC_TITLE_T0_0) != TITLE_PARTS +#error "полосы титров в архиве лежат не по пять подряд" +#endif /* Последовательность и интервалы буквально из show_title(): * main --0x82--> presents --0xCD--> main --0x41--> game @@ -74,12 +74,20 @@ static uint8_t title_skip_pressed(void) * подготовленной страницы; show=0 оставляет её скрытой для palette commit. */ static int title_draw(uint8_t scene, uint8_t show) { + uint8_t tbl[ARC_TITLE_COUNT * POP_ARC_REC]; + pop_arc_t arc; uint8_t page = pop_screen_begin(); + int res; pop_screen_fill(BLACK); - if (pop_screen_blit_atlas_strips(title_part[scene], TITLE_PARTS, - 0, TITLE_Y) != 0) + if (pop_arc_open_id(&arc, POP_ARC_TITLE, tbl, ARC_TITLE_COUNT) < 0) return -1; + res = pop_screen_blit_atlas_arc(&arc, tbl, + (uint8_t)(ARC_TITLE_T0_0 + + scene * TITLE_PARTS), + TITLE_PARTS, 0, TITLE_Y); + pop_arc_close(&arc); + if (res != 0) return -1; if (show) pop_screen_present(page); return page; } diff --git a/applications/PoP/roomtest/pop_ui.c b/applications/PoP/roomtest/pop_ui.c index 6b08bf8..8a3bbc8 100644 --- a/applications/PoP/roomtest/pop_ui.c +++ b/applications/PoP/roomtest/pop_ui.c @@ -11,6 +11,7 @@ #include #include "pop_bg.h" /* POP_YOFF/POP_PLAYFIELD_H — граница игрового поля */ #include "pop_font.h" +#include "pop_arc.h" #include "pop_ui.h" #include "pop_status.h" @@ -284,6 +285,22 @@ void pop_screen_present(uint8_t page) __banked gfx_set_visible_page(page); } +/* Нарисовать одну полосу и отпустить её страницу. Общая часть обоих + * загрузчиков ниже: они отличаются только тем, откуда взялся атлас. */ +static int strip_blit(atlas_t *at, int x, int *y) +{ + const uint8_t *img; + + 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; +} + /* Полноэкранные изображения не помещаются одним getimage в EMM-страницу. * Общий загрузчик полос держит лишь одну страницу и не трогает page flip: * caller уже собрал скрытую страницу и сам решает, когда её показать. */ @@ -295,16 +312,29 @@ int pop_screen_blit_atlas_strips(const char * const *parts, 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); + strip_blit(&at, x, &y); + } + return 0; +} + +/* То же самое, но полосы лежат подряд в АРХИВЕ (заставка, story, PV): один + * `open` на весь экран вместо пяти — при цене открытия 51,4 мс против + * 32,6 мс на чтение это больше половины времени показа. */ +int pop_screen_blit_atlas_arc(pop_arc_t *arc, const uint8_t *tbl, + uint8_t base, 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; + + if (pop_arc_atlas(arc, tbl, (uint8_t)(base + part), &at) != 0) + return -1; + strip_blit(&at, x, &y); } return 0; } diff --git a/applications/PoP/roomtest/pop_ui.h b/applications/PoP/roomtest/pop_ui.h index aeafa57..11cad31 100644 --- a/applications/PoP/roomtest/pop_ui.h +++ b/applications/PoP/roomtest/pop_ui.h @@ -75,4 +75,10 @@ void pop_screen_present(uint8_t page) __banked; int pop_screen_blit_atlas_strips(const char * const *parts, uint8_t count, int x, int y) __banked; +/* Те же полосы, но из архива ресурсов: элементы base..base+count-1. + * Один `open` на экран вместо одного на полосу. */ +int pop_screen_blit_atlas_arc(struct pop_arc_s *arc, const uint8_t *tbl, + uint8_t base, uint8_t count, + int x, int y) __banked; + #endif /* POP_UI_H */ diff --git a/applications/PoP/roomtest/pop_vflip.c b/applications/PoP/roomtest/pop_vflip.c index f4d0663..4b8e138 100644 --- a/applications/PoP/roomtest/pop_vflip.c +++ b/applications/PoP/roomtest/pop_vflip.c @@ -28,15 +28,27 @@ static uint8_t vf_src[VF_MAX]; /* страница оригинала static atlas_t vf_atl[VF_MAX]; /* зеркальный атлас (его page) */ static uint8_t vf_n; -int pop_vflip_add(const atlas_t *orig, const char *path) +/* Взять УЖЕ ЗАГРУЖЕННЫЙ зеркальный атлас под своё управление (дальше его + * освобождает pop_vflip_reset). Нужен тем, кто читает зеркала не файлом, а + * куском архива ресурсов: загрузку делает вызывающий, связывание — мы. */ +int pop_vflip_take(const atlas_t *orig, const atlas_t *mirror) { - if (!orig || vf_n >= VF_MAX) return -1; - if (atlas_load(&vf_atl[vf_n], path) != 0) return -1; + if (!orig || !mirror || vf_n >= VF_MAX) return -1; + vf_atl[vf_n] = *mirror; vf_src[vf_n] = orig->page; vf_n++; return 0; } +int pop_vflip_add(const atlas_t *orig, const char *path) +{ + atlas_t m; + + if (!orig || vf_n >= VF_MAX) return -1; + if (atlas_load(&m, path) != 0) return -1; + return pop_vflip_take(orig, &m); +} + uint8_t pop_vflip_page(uint8_t src) { uint8_t i; diff --git a/applications/PoP/roomtest/pop_vflip.h b/applications/PoP/roomtest/pop_vflip.h index 3f239ca..81fefec 100644 --- a/applications/PoP/roomtest/pop_vflip.h +++ b/applications/PoP/roomtest/pop_vflip.h @@ -30,6 +30,10 @@ * 0 — OK, -1 — файла нет / не хватило EMM / таблица полна. */ int pop_vflip_add(const atlas_t *orig, const char *path); +/* То же, но зеркало УЖЕ загружено (например, прочитано из архива): таблица + * забирает его себе, и дальше он освобождается вместе с остальными. */ +int pop_vflip_take(const atlas_t *orig, const atlas_t *mirror); + /* Страница-зеркало для страницы src; 0 — пары нет (набор не загружен). */ uint8_t pop_vflip_page(uint8_t src); diff --git a/applications/PoP/roomtest/pv_arc.h b/applications/PoP/roomtest/pv_arc.h new file mode 100644 index 0000000..a0f74ff --- /dev/null +++ b/applications/PoP/roomtest/pv_arc.h @@ -0,0 +1,87 @@ +/* pv_arc.h — индексы в архиве PV. СГЕНЕРИРОВАН + * (toolchain/pop_pack_arc.py), руками не править. */ +#ifndef PV_ARC_H +#define PV_ARC_H + +#define ARC_PV_COUNT 78 + +#define ARC_PV_S0_0 0 +#define ARC_PV_S0_1 1 +#define ARC_PV_S0_2 2 +#define ARC_PV_S0_3 3 +#define ARC_PV_S0_4 4 +#define ARC_PV_S1_0 5 +#define ARC_PV_S1_1 6 +#define ARC_PV_S1_2 7 +#define ARC_PV_S1_3 8 +#define ARC_PV_S1_4 9 +#define ARC_PV_S2_0 10 +#define ARC_PV_S2_1 11 +#define ARC_PV_S2_2 12 +#define ARC_PV_S2_3 13 +#define ARC_PV_S2_4 14 +#define ARC_PV_S3_0 15 +#define ARC_PV_S3_1 16 +#define ARC_PV_S3_2 17 +#define ARC_PV_S3_3 18 +#define ARC_PV_S3_4 19 +#define ARC_PV_S4_0 20 +#define ARC_PV_S4_1 21 +#define ARC_PV_S4_2 22 +#define ARC_PV_S4_3 23 +#define ARC_PV_S4_4 24 +#define ARC_PV_P0_0 25 +#define ARC_PV_P0_1 26 +#define ARC_PV_P0_2 27 +#define ARC_PV_P0_3 28 +#define ARC_PV_P0_4 29 +#define ARC_PV_P1_0 30 +#define ARC_PV_P1_1 31 +#define ARC_PV_P1_2 32 +#define ARC_PV_P1_3 33 +#define ARC_PV_P1_4 34 +#define ARC_PV_P2_0 35 +#define ARC_PV_P2_1 36 +#define ARC_PV_P2_2 37 +#define ARC_PV_P2_3 38 +#define ARC_PV_P2_4 39 +#define ARC_PV_P3_0 40 +#define ARC_PV_P3_1 41 +#define ARC_PV_P3_2 42 +#define ARC_PV_P3_3 43 +#define ARC_PV_P3_4 44 +#define ARC_PV_P4_0 45 +#define ARC_PV_P4_1 46 +#define ARC_PV_P4_2 47 +#define ARC_PV_P4_3 48 +#define ARC_PV_P4_4 49 +#define ARC_PV_P5_0 50 +#define ARC_PV_P5_1 51 +#define ARC_PV_P5_2 52 +#define ARC_PV_P5_3 53 +#define ARC_PV_P5_4 54 +#define ARC_PV_P6_0 55 +#define ARC_PV_P6_1 56 +#define ARC_PV_P6_2 57 +#define ARC_PV_P6_3 58 +#define ARC_PV_P6_4 59 +#define ARC_PV_P7_0 60 +#define ARC_PV_P7_1 61 +#define ARC_PV_P7_2 62 +#define ARC_PV_P7_3 63 +#define ARC_PV_P7_4 64 +#define ARC_PV_B0_0 65 +#define ARC_PV_B0_1 66 +#define ARC_PV_B0_2 67 +#define ARC_PV_B0_3 68 +#define ARC_PV_B0_4 69 +#define ARC_PV_A0 70 +#define ARC_PV_A1 71 +#define ARC_PV_A2 72 +#define ARC_PV_J0 73 +#define ARC_PV_J1 74 +#define ARC_PV_J2 75 +#define ARC_PV_H0 76 +#define ARC_PV_T0 77 + +#endif diff --git a/applications/PoP/roomtest/roomtest_cold.c b/applications/PoP/roomtest/roomtest_cold.c index 106cf75..01e443a 100644 --- a/applications/PoP/roomtest/roomtest_cold.c +++ b/applications/PoP/roomtest/roomtest_cold.c @@ -900,7 +900,6 @@ int pop_boot(void) __banked printf("kid_data.bin load failed\n"); return -1; } - pop_sword_load(); /* меч в руке (без него кадры 229+ без клинка) */ pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */ pop_shadow_load(); /* запечённый набор Тени — один раз на запуск */ pop_sfx_init(); /* звук: 8 страниц оцифровки (вывод пока молчит) */ diff --git a/applications/PoP/roomtest/shadow_arc.h b/applications/PoP/roomtest/shadow_arc.h new file mode 100644 index 0000000..09a719a --- /dev/null +++ b/applications/PoP/roomtest/shadow_arc.h @@ -0,0 +1,41 @@ +/* shadow_arc.h — индексы в архиве SHADOW. СГЕНЕРИРОВАН + * (toolchain/pop_pack_arc.py), руками не править. */ +#ifndef SHADOW_ARC_H +#define SHADOW_ARC_H + +#define ARC_SHADOW_COUNT 32 + +#define ARC_SHADOW_SK0 0 +#define ARC_SHADOW_SK1 1 +#define ARC_SHADOW_SK2 2 +#define ARC_SHADOW_SK3 3 +#define ARC_SHADOW_SK4 4 +#define ARC_SHADOW_SK5 5 +#define ARC_SHADOW_SK6 6 +#define ARC_SHADOW_SK7 7 +#define ARC_SHADOW_SK8 8 +#define ARC_SHADOW_SK9 9 +#define ARC_SHADOW_SK10 10 +#define ARC_SHADOW_SK11 11 +#define ARC_SHADOW_SK12 12 +#define ARC_SHADOW_SK13 13 +#define ARC_SHADOW_SK14 14 +#define ARC_SHADOW_SK15 15 +#define ARC_SHADOW_SK16 16 +#define ARC_SHADOW_SK17 17 +#define ARC_SHADOW_SK18 18 +#define ARC_SHADOW_SK19 19 +#define ARC_SHADOW_SK20 20 +#define ARC_SHADOW_SK21 21 +#define ARC_SHADOW_SK22 22 +#define ARC_SHADOW_SK23 23 +#define ARC_SHADOW_SK24 24 +#define ARC_SHADOW_SK25 25 +#define ARC_SHADOW_SK26 26 +#define ARC_SHADOW_SK27 27 +#define ARC_SHADOW_SF0 28 +#define ARC_SHADOW_SF1 29 +#define ARC_SHADOW_SF2 30 +#define ARC_SHADOW_SF3 31 + +#endif diff --git a/applications/PoP/roomtest/skel_arc.h b/applications/PoP/roomtest/skel_arc.h new file mode 100644 index 0000000..eca2b21 --- /dev/null +++ b/applications/PoP/roomtest/skel_arc.h @@ -0,0 +1,13 @@ +/* skel_arc.h — индексы в архиве SKEL. СГЕНЕРИРОВАН + * (toolchain/pop_pack_arc.py), руками не править. */ +#ifndef SKEL_ARC_H +#define SKEL_ARC_H + +#define ARC_SKEL_COUNT 4 + +#define ARC_SKEL_G0 0 +#define ARC_SKEL_G1 1 +#define ARC_SKEL_G2 2 +#define ARC_SKEL_G3 3 + +#endif diff --git a/applications/PoP/roomtest/tests-host/stubs.c b/applications/PoP/roomtest/tests-host/stubs.c index 0cced7d..65cc4db 100644 --- a/applications/PoP/roomtest/tests-host/stubs.c +++ b/applications/PoP/roomtest/tests-host/stubs.c @@ -387,7 +387,24 @@ void pop_mirror_heal(void) __banked { } uint8_t pop_vflip_ready(void) { return 1; } /* «уже загружено» — грузить не станет */ int pop_vflip_add(const void *orig, const char *path) { (void)orig; (void)path; return 0; } +int pop_vflip_take(const void *orig, const void *mirror) +{ (void)orig; (void)mirror; return 0; } + +/* Архив ресурсов: на устройстве из него читаются атласы Кида, в тестах + * графики нет вовсе — pop_kboot линкуется только ради pop_kid_data_load, + * а тот по-прежнему ходит в файл (заглушка open/read выше). */ +signed char pop_arc_open(void *a, const char *path, unsigned char *tbl, + unsigned char max) __banked +{ (void)a; (void)path; (void)tbl; (void)max; return -1; } +signed char pop_arc_open_id(void *a, unsigned char id, unsigned char *tbl, + unsigned char max) __banked +{ (void)a; (void)id; (void)tbl; (void)max; return -1; } +signed char pop_arc_atlas(void *a, const unsigned char *tbl, + unsigned char idx, void *at) __banked +{ (void)a; (void)tbl; (void)idx; (void)at; return -1; } +void pop_arc_close(void *a) __banked { (void)a; } 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_sword_take(const atlas_t *a) __banked { (void)a; return 0; } +int pop_sword_vflip_take(const atlas_t *m) __banked { (void)m; return 0; } int pop_guard_vflip_load(void) __banked { return 0; } diff --git a/applications/PoP/roomtest/title_arc.h b/applications/PoP/roomtest/title_arc.h new file mode 100644 index 0000000..e02e7f2 --- /dev/null +++ b/applications/PoP/roomtest/title_arc.h @@ -0,0 +1,29 @@ +/* title_arc.h — индексы в архиве TITLE. СГЕНЕРИРОВАН + * (toolchain/pop_pack_arc.py), руками не править. */ +#ifndef TITLE_ARC_H +#define TITLE_ARC_H + +#define ARC_TITLE_COUNT 20 + +#define ARC_TITLE_T0_0 0 +#define ARC_TITLE_T0_1 1 +#define ARC_TITLE_T0_2 2 +#define ARC_TITLE_T0_3 3 +#define ARC_TITLE_T0_4 4 +#define ARC_TITLE_T1_0 5 +#define ARC_TITLE_T1_1 6 +#define ARC_TITLE_T1_2 7 +#define ARC_TITLE_T1_3 8 +#define ARC_TITLE_T1_4 9 +#define ARC_TITLE_T2_0 10 +#define ARC_TITLE_T2_1 11 +#define ARC_TITLE_T2_2 12 +#define ARC_TITLE_T2_3 13 +#define ARC_TITLE_T2_4 14 +#define ARC_TITLE_T3_0 15 +#define ARC_TITLE_T3_1 16 +#define ARC_TITLE_T3_2 17 +#define ARC_TITLE_T3_3 18 +#define ARC_TITLE_T3_4 19 + +#endif diff --git a/applications/PoP/roomtest/vizier_arc.h b/applications/PoP/roomtest/vizier_arc.h new file mode 100644 index 0000000..8f30b67 --- /dev/null +++ b/applications/PoP/roomtest/vizier_arc.h @@ -0,0 +1,14 @@ +/* vizier_arc.h — индексы в архиве VIZIER. СГЕНЕРИРОВАН + * (toolchain/pop_pack_arc.py), руками не править. */ +#ifndef VIZIER_ARC_H +#define VIZIER_ARC_H + +#define ARC_VIZIER_COUNT 5 + +#define ARC_VIZIER_G0 0 +#define ARC_VIZIER_G1 1 +#define ARC_VIZIER_G2 2 +#define ARC_VIZIER_G3 3 +#define ARC_VIZIER_G4 4 + +#endif diff --git a/applications/PoP/toolchain/pop_pack_arc.py b/applications/PoP/toolchain/pop_pack_arc.py index 5cb780b..fd12db3 100644 --- a/applications/PoP/toolchain/pop_pack_arc.py +++ b/applications/PoP/toolchain/pop_pack_arc.py @@ -26,6 +26,7 @@ Данные каждого элемента выровнены на 512, поэтому его смещение всегда целое число секторов, а чтение попадает в границу сектора DSS. """ +import re import sys from pathlib import Path @@ -61,15 +62,59 @@ def build(out_path: Path, files: list[Path]) -> None: out_path.write_bytes(bytes(header) + bytes(blob)) +def const_name(prefix: str, path: Path) -> str: + """kid12.atl -> ARC_KID_KID12; kid.pal -> ARC_KID_KID_PAL.""" + stem = path.name + if stem.lower().endswith(".atl"): + stem = stem[:-4] + return f"ARC_{prefix}_" + re.sub(r"[^A-Za-z0-9]", "_", stem).upper() + + +def write_header(path: Path, prefix: str, files: list[Path]) -> None: + """Индексы элементов — константами, чтобы код не считал их на память. + + Порядок задаёт Makefile, и он же единственный источник истины: сдвинулся + список — сдвинулись константы, а серии вроде kid0..kid27 код проверяет + статически (`#if ARC_KID_KID27 - ARC_KID_KID0 != 27`). + """ + lines = [f"/* {path.name} — индексы в архиве {prefix}. СГЕНЕРИРОВАН", + " * (toolchain/pop_pack_arc.py), руками не править. */", + f"#ifndef {path.stem.upper()}_H", + f"#define {path.stem.upper()}_H", + "", + f"#define ARC_{prefix}_COUNT {len(files)}", + ""] + width = max(len(const_name(prefix, f)) for f in files) + for i, f in enumerate(files): + lines.append(f"#define {const_name(prefix, f):<{width}} {i}") + lines += ["", "#endif", ""] + path.write_text("\n".join(lines)) + + def main() -> None: - if len(sys.argv) < 3: - raise SystemExit("usage: pop_pack_arc.py OUT.ARC file [file ...]") - out = Path(sys.argv[1]) - files = [Path(a) for a in sys.argv[2:]] + args = sys.argv[1:] + header = prefix = None + while args and args[0].startswith("--"): + opt = args.pop(0) + if opt == "--header": + header = Path(args.pop(0)) + elif opt == "--prefix": + prefix = args.pop(0) + else: + raise SystemExit(f"неизвестный ключ {opt}") + if len(args) < 2: + raise SystemExit("usage: pop_pack_arc.py [--header H.h --prefix NAME] " + "OUT.ARC file [file ...]") + out = Path(args[0]) + files = [Path(a) for a in args[1:]] for f in files: if not f.exists(): raise SystemExit(f"нет файла: {f}") build(out, files) + if header: + if not prefix: + raise SystemExit("--header требует --prefix") + write_header(header, prefix, files) total = out.stat().st_size print(f"{out.name}: {len(files)} записей, {total / 1024:.0f} КБ") diff --git a/libbgi/common/atlas_attach.c b/libbgi/common/atlas_attach.c new file mode 100644 index 0000000..082a7aa --- /dev/null +++ b/libbgi/common/atlas_attach.c @@ -0,0 +1,57 @@ +/* + * atlas_attach — принять .atl, УЖЕ прочитанный в EMM-страницу. + * + * Вторая половина atlas_load, отделённая ради архивов ресурсов: кто читает + * атлас не из отдельного файла, а куском общего архива (PoP: pop_arc.c, + * формат PBA1), делает своё чтение сам, а проверку магии, gfx_w0_page_prepare + * и разбор заголовка отдаёт сюда — иначе эти двадцать строк пришлось бы + * повторять в каждом приложении. + * + * n — сколько байт реально прочитано (или -1, если чтение не удалось). + * При любой ошибке блок ОСВОБОЖДАЕТСЯ: вызывающему остаётся только + * вернуть -1, и «полузагруженных» атласов не бывает. + * + * Ошибки: EREAD — короткое чтение, EINVAL — не .atl. + */ +#include +#include +#include +#include "../_bgi.h" + +int atlas_attach(atlas_t *a, uint8_t blk, uint8_t page, int n) +{ + uint8_t ok, count, h[5]; + + a->blk = blk; + a->page = page; + if (n < (int)sizeof(h)) { + mem_free_block(blk); + a->page = 0; + if (n >= 0) errno = EREAD; + return -1; + } + bank_read(page, 0, h, sizeof(h)); + ok = (n >= (int)_ATL_DATA_OFF && + h[0] == 'S' && h[1] == 'P' && h[2] == 'A' && h[3] == '1'); + count = 0; + if (ok) { + count = h[4]; + gfx_w0_page_prepare(page); + } + + /* Восстановить W3 ДО записи в *a: если atlas_t лежит в банковской + * памяти (её адрес тоже резолвится через W3), запись при ещё + * замапленной странице атласа ушла бы не в тот физический банк + * (найдено 2026-07-15 — PoP room.c держит tile_atlas[] в --bank 1, + * a->count оставался мусором, room_draw читал пиксели не с того + * оффсета). a->blk/a->page выше пишутся ДО ремапа — их это не + * касалось, только a->count. */ + if (!ok) { + mem_free_block(blk); + a->page = 0; + errno = EINVAL; + return -1; + } + a->count = count; + return 0; +} diff --git a/libbgi/common/atlas_load.c b/libbgi/common/atlas_load.c index f88a7ab..6003ac2 100644 --- a/libbgi/common/atlas_load.c +++ b/libbgi/common/atlas_load.c @@ -2,62 +2,32 @@ * atlas_load — загрузить .atl-файл в свежую EMM-страницу (§3.1/§9в * дизайна, вариант II: файл-офсет == офсет страницы == W0-адрес). * - * Порядок: выделить страницу → bank_load_file() → проверить магию 'SPA1' - * → gfx_w0_page_prepare(). Никакого пересчёта офсетов: каталог и ленты - * уже лежат по своим W0-адресам. + * Порядок: выделить страницу → bank_load_file() → atlas_attach (магия + * 'SPA1' + gfx_w0_page_prepare). Никакого пересчёта офсетов: каталог и + * ленты уже лежат по своим W0-адресам. + * + * Кто держит атласы в АРХИВЕ, а не отдельными файлами, повторяет ту же + * пару вызовов со своим чтением: mem_alloc_pages + своё чтение в страницу + * + atlas_attach. * * Вызывать ВНЕ рендера (W3 на время загрузки занята страницей атласа). * Ошибки: -1 + errno (open/read/alloc; EINVAL — не .atl). */ -#include #include #include #include "../_bgi.h" int atlas_load(atlas_t *a, const char *path) { - uint8_t blk, ok, count, h[5]; + uint8_t blk, page; int n; blk = mem_alloc_pages(1); if (!blk) return -1; + page = mem_get_page(blk, 0); - a->blk = blk; - a->page = mem_get_page(blk, 0); - - n = bank_load_file(a->page, 0, path, 16384); /* Не затирать errno от open/read: вызывающему важно отличить отсутствие - * файла или короткое чтение от файла, который действительно не .atl. - * Раньше оба случая проваливались в EINVAL ниже, что маскировало - * фактическую причину сбоя загрузки экранных полос. */ - if (n < (int)sizeof(h)) { - mem_free_block(blk); - a->page = 0; - if (n >= 0) errno = EREAD; - return -1; - } - bank_read(a->page, 0, h, sizeof(h)); - ok = (n >= (int)_ATL_DATA_OFF && - h[0] == 'S' && h[1] == 'P' && h[2] == 'A' && h[3] == '1'); - count = 0; - if (ok) { - count = h[4]; - gfx_w0_page_prepare(a->page); - } - - /* Восстановить W3 ДО записи в *a: если atlas_t лежит в банковской - * памяти (её адрес тоже резолвится через W3), запись при ещё - * замапленной странице атласа ушла бы не в тот физический банк - * (найдено 2026-07-15 — PoP room.c держит tile_atlas[] в --bank 1, - * a->count оставался мусором, room_draw читал пиксели не с того - * оффсета). a->blk/a->page выше пишутся ДО ремапа — их это не - * касалось, только a->count. */ - if (!ok) { - mem_free_block(blk); - a->page = 0; - errno = EINVAL; - return -1; - } - a->count = count; - return 0; + * файла или короткое чтение от файла, который действительно не .atl. */ + n = bank_load_file(page, 0, path, 16384); + return atlas_attach(a, blk, page, n); } diff --git a/libbgi/include/sprite.h b/libbgi/include/sprite.h index 036707b..8579189 100644 --- a/libbgi/include/sprite.h +++ b/libbgi/include/sprite.h @@ -290,6 +290,12 @@ typedef struct { * 0 — OK; -1 + errno (файл/память/формат). */ int atlas_load(atlas_t *a, const char *path); +/* Принять .atl, уже прочитанный в страницу СВОИМИ силами — например, куском + * общего архива ресурсов. n — сколько байт прочитано (-1, если не вышло). + * Проверяет магию, готовит страницу к маппингу в W0 и заполняет *a; при + * любой ошибке освобождает блок сам. 0 — OK; -1 + errno. */ +int atlas_attach(atlas_t *a, uint8_t blk, uint8_t page, int n); + /* Подготовить уже выделенную EMM-страницу для безопасного маппинга в W0: * установить IRQ/NMI-стабы и запомнить штатную страницу DSS. */ void gfx_w0_page_prepare(uint8_t page);