From c127a4b0d2f37098c82369a33308338ca32c24fd Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Mon, 27 Jul 2026 20:53:04 +0300 Subject: [PATCH] =?UTF-8?q?PoP:=20=D1=84=D0=B0=D0=BA=D0=B5=D0=BB=D1=8B=20?= =?UTF-8?q?=D0=B8=20=D0=B7=D0=B5=D0=BB=D1=8C=D1=8F=20(chtab=5F1)=20+=20?= =?UTF-8?q?=D0=B1=D1=8B=D1=81=D1=82=D1=80=D1=8B=D0=B9=20=D0=B1=D0=BB=D0=B8?= =?UTF-8?q?=D1=82=20=D1=84=D0=BE=D0=BD=D0=B0=20gfx=5Fblit=5Fnoclip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Графика chtab_1 (flame/sword/potion) — новый атлас pop_pot.atl: - склянка зелья берётся из chtab_1 (seg008 draw_tile_fore: id 12 малая / 13 большая при типах 2..4), а НЕ chtab_6 id 12 — res212 в VDUNGEON нет, каскад падал на VPALACE и рисовал кусок дворцовой декорации («мусорный объект» в комнате 5); - пакуем chtab_1 целиком (id 1..23): пламя, склянки, кадры пузырька; - MONO-блит (method_3_blit_mono, seg009:3040) красит спрайт цветом из ОБЩЕЙ 16-цветной палитры экрана — она добавлена в 0x30..0x3F, палитра chtab_1 в 0x40..0x4F; пузырёк пакуется силуэтом цвета 12 (красный), его маска — цвета 0; - в env-атлас добавлено основание факела (env 146, seg008:489 — рисует правый сосед); в статический render_room оно не попадало. Анимация (seg007 animate_torch/animate_potion + seg000:0B12 anim_tile_modif): при входе в комнату факелам и зельям задаётся случайная стартовая фаза и заводится trob; факел — get_torch_frame, пламя в ячейке ПРАВОГО соседа (xh = draw_xh+1, y = draw_main_y−40); зелье — bubble_next_frame по младшим 3 битам модификатора (старшие 5 = тип, при загрузке уровня modif <<= 3, seg009). Пламя и пузырёк не запекаются в фон: heal своей области + кадр поверх. Скорость пламени /2 — наш логический кадр короче игрового тика оригинала. Скорость отрисовки (профиль в MAME, кадр = 430 080 тактов): - замер показал, что цена блита почти НЕ зависит от размера — 13 288 тактов на спрайт 32×3 против 4 617 у линейного спрайтового ядра без клипа; платим за проход gfx_blit → gfx_blit_part → _gfx_blit_full; - в libbgi добавлен gfx_blit_noclip() — блит без клипа в ТЕКУЩЕМ банке (putsprite не годится: навязывает GFX_BANK_SPRITE, фону нужен TRANSPARENT ради ОЗУ-копии); pop_bg.blit_b уходит на него, когда спрайт целиком на экране и не нужен g_clip_top → ~2.9× на блит; - W3-скобку ставит САМА libbgi: из модуля с --w3 её вызывать нельзя — после _bgi_begin окно W3 занято видеобанком и код вызывающего исчезает (проявлялось белым экраном); - редрой шва больше не перерисовывает полосу потолка (bar начинается с POP_YOFF+3) — это удваивало стоимость блока при анимации решётки; - docs/size_optimization_plan.md §7–§8: замеры, сделанное и запас (батчинг W3-скобки, решётка одним спрайтом, лишние блиты). Co-Authored-By: Claude Opus 5 --- .../PoP/docs/size_optimization_plan.md | 44 +++++++ applications/PoP/poc/res/bg/pop_bg.pal | Bin 1024 -> 1024 bytes applications/PoP/poc/res/bg/pop_bg_atlas.h | 2 + applications/PoP/poc/res/bg/pop_env4.atl | Bin 8498 -> 8982 bytes applications/PoP/poc/res/bg/pop_pot.atl | Bin 0 -> 4504 bytes applications/PoP/poc/res/kid/kid.pal | Bin 1024 -> 1024 bytes applications/PoP/roomtest/Makefile | 5 +- applications/PoP/roomtest/pop_bg.c | 114 ++++++++++++++++-- applications/PoP/roomtest/pop_bg.h | 6 + applications/PoP/roomtest/pop_trob.c | 88 ++++++++++++++ applications/PoP/roomtest/pop_trob.h | 4 + applications/PoP/roomtest/roomtest.c | 5 +- applications/PoP/toolchain/pop_pack_bg.py | 74 +++++++++++- applications/PoP/toolchain/pop_pack_kid.py | 9 +- libbgi/common/gfx_blit_noclip.c | 35 ++++++ libbgi/include/gfx.h | 7 ++ 16 files changed, 377 insertions(+), 16 deletions(-) create mode 100644 applications/PoP/poc/res/bg/pop_pot.atl create mode 100644 libbgi/common/gfx_blit_noclip.c diff --git a/applications/PoP/docs/size_optimization_plan.md b/applications/PoP/docs/size_optimization_plan.md index 0c4ed98..90d3fa0 100644 --- a/applications/PoP/docs/size_optimization_plan.md +++ b/applications/PoP/docs/size_optimization_plan.md @@ -289,3 +289,47 @@ Kid, обычно 2–4 за кадр) рисовал ещё и `bottom_id` — `redraw_frames_above`; сузить до колонок, реально задетых спрайтом. 6. `wall_pattern` внутри fore/overlay — тяжёлая (PRNG + до 4 блитов); проверить, не зовём ли её там, где оригинал ограничивается `wall_fram_main`. + +--- + +## 8. Скорость отрисовки: замеры и запас (2026-07-27) + +Профилирование в MAME (маркеры в порт 0xFE + `wpiset … totalcycles`, приём из +memory `mame_mcp_bridge`). Кадр Sprinter = **430 080 тактов**. + +**Стоимость блита почти НЕ зависит от размера** — платим за проход по цепочке +`gfx_blit → gfx_blit_part → _gfx_blit_full` (16-битная арифметика, клип, +пересчёт src, нарезка полос >256), а не за пиксели: + +| путь (спрайт 32×3) | тактов | +|---|---| +| `gfx_blit` (общее ядро, с клипом) | 13 288 | +| линейное спрайтовое ядро без клипа (`putsprite` при `gfx_sprite_clip(0)`) | 4 617 | + +Отсюда `draw_tile(0,0)` тайла шва (9 блитов) стоил **183 690 тактов = 43 % +кадра**; сам `bar` — только 13 308. + +**СДЕЛАНО (шаг 1):** в libbgi добавлен `gfx_blit_noclip()` +(`common/gfx_blit_noclip.c`, прототип в `include/gfx.h`) — блит без клипа в +ТЕКУЩЕМ банке через линейное ядро; `pop_bg.blit_b` уходит на него, когда +спрайт целиком на экране и не нужен `g_clip_top`. Выигрыш ~2.9× на каждом +фоновом блите (подтверждено в MAME). +**ВАЖНО:** W3-скобку (`_bgi_begin/_bgi_end`) ставит САМА libbgi — вызывать её +из модуля, собранного с `--w3`, нельзя: после `_bgi_begin` окно W3 занято +видеобанком и код вызывающего исчезает из адресного пространства (проверено: +белый экран). + +**ЗАПАС (шаг 2), когда перестанет хватать бюджета кадра:** + +1. **Батчинг W3-скобки** — одна `_bgi_begin/_bgi_end` на весь `draw_tile` + вместо скобки на блит; нужен публичный batch-API в libbgi (как у + спрайтового движка). Осторожно: между begin/end стоит `DI` — длинная + серия задержит кадровое прерывание. +2. **Решётка ворот одним спрайтом** — `draw_gate_back` рисует бары по одному + (`env 52`, до 7 блитов). Сгенерировать в атласе «столб решётки» (повтор + бара на высоту тайла) и выводить одним `gfx_blit_part` с обрезкой по фазе + `gate_bot_y & 7`: 7 блитов → 1. +3. **Не перерисовывать статичные части шва** — грань ворот (env 47, 26×62), + пол (41) и кромка (43) при анимации решётки не меняются; если стирать + только полосу баров, уйдут ещё 3 блита из 9. +4. См. также §7 (лишние блиты, которых нет в оригинале). diff --git a/applications/PoP/poc/res/bg/pop_bg.pal b/applications/PoP/poc/res/bg/pop_bg.pal index ed5e3286ff9e5d32548e1cf38b88a78269a8891b..fc1752b665f57976c757d4aba4beae535a890f95 100644 GIT binary patch delta 139 zcmZqRXyBM|q<$3x5CDl)P{t|{Clp8m1wunZ8U6!t=zky$#G#@8f&BkKK9B}tFaXi5 z{}~K?YQbWgUj1j7dF?+#=B{517L}VAd< zvop45Hvu7#7<6Yp+t2pAn?$_6SwDSL{rz)wQdKAP^Y~x?1$T8Kb?yGx)A&Q zj@Z+?Vz17`4z<{K=a)75e7Ps~$Nl;o`0ayw346K}{BbFGwOX7ZzK-^Nw4bB>7VYAp z)IE##O|(#GRb5np&$XEJ``QMs3!Ey&p>oSG7%9~O-RLI~;Nzs80BeQ0T4M>MdUU6g zv4fO0?SR+0n!I_OH8KQqA=H717WY%wy)OhJrI48mKhy z>+o82M}AhbIgM;TzM@(H3fT$zdSjt0@f4fJB}XDQ=92@{STV{1(#Yh!W(A&mS4tz* z5W8V$-my?+B5erBAL4tJT|J<_1gBL)!D>Ga9K(7cwD(x?)w@y}=>n33pzcAwhEh;c zbi~$|qH7H>%sl?dJF7cQ<@IoUcs(`&^CzH6`mDM9_^y;jrosw@7C34s<07C6z|hcR z*Ut~|{rOp`hMZUZJ@(_KG*fhFq`Ffke?KXW41<9s(NtSPYvlmB7IQ%5saTi0J25nQ zd=zp(@*d~*G~C{C3THYIC*F~2_sbd?&S9YM!<7iAJ3>I5+~o*9l4eZ~w>$oQqqbp9 zJdVr$@;l-y=O%K6c`TY_MHoA%CG-6XJ_3sGUM*$NK^Hj8pH-Fj@9e;5?Xs%l{~;Cs ze{rKGFK5mx$VBJG+=-b#z3#u%upO68&KQWc<%IlpDS<7YlqxkuJ}FviDEuU;*YL>v zw03f2le#*`JM7xcHm;VO(2b$wbyU~)RM^B5x~8RnU9)XEUa;G2`|JfGw4G1_Qbet* zrA$oiCnk37mgfkcc-*twnC^KFFVQxerVs4G;nlDhU$5Bk5$&vP|yPDYS zq8y=*F6}6*Vcnjl$++wV+(oY1Q*ZJX)mi?2dM>FDN{2HD8v>$Z?P$3&hberP02=)RzyJUM diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index ebab738..7d3d6df 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -6,7 +6,7 @@ EXAMPLE := roomtest # small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе # CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge). MEMORY ?= small -EXTRA_FLAGS ?= --gfx 256 --w3 pop_trob.c --w3 pop_map.c pop_bg.c +EXTRA_FLAGS ?= --gfx 256 --w3 pop_trob.c pop_map.c --w3 pop_bg.c EXTRA_SRCS := pop_kid.c pop_ctrl.c pop_level.c BG_DIR := $(CURDIR)/../poc/res/bg @@ -15,7 +15,8 @@ LVL_DIR := $(CURDIR)/../SDLPoP/data/LEVELS LVL_DATA := $(LVL_DIR)/res2001.bin BG_DATA := $(BG_DIR)/pop_env0.atl $(BG_DIR)/pop_env1.atl $(BG_DIR)/pop_env2.atl \ $(BG_DIR)/pop_env3.atl $(BG_DIR)/pop_env4.atl \ - $(BG_DIR)/pop_wall.atl $(BG_DIR)/pop_fore.atl + $(BG_DIR)/pop_wall.atl $(BG_DIR)/pop_fore.atl \ + $(BG_DIR)/pop_pot.atl # ВСЕ 28 страниц Kid — иначе кадры торможения/разворота/приседа (image>23) # не рисуются (kid_draw пропускает незагруженную страницу). Палитра — # ЕДИНАЯ игровая (env+wall+kid). diff --git a/applications/PoP/roomtest/pop_bg.c b/applications/PoP/roomtest/pop_bg.c index e0fef16..85a0ec4 100644 --- a/applications/PoP/roomtest/pop_bg.c +++ b/applications/PoP/roomtest/pop_bg.c @@ -21,6 +21,7 @@ static atlas_t env[5]; static atlas_t wall_at; static atlas_t fore_at; +static atlas_t pot_at; /* chtab_1: зелья (склянка + пузырёк) */ static const char *const ENV_ATL[5] = { "pop_env0.atl", "pop_env1.atl", "pop_env2.atl", @@ -34,6 +35,7 @@ int pop_bg_load(void) if (atlas_load(&env[i], ENV_ATL[i]) != 0) return -1; if (atlas_load(&wall_at, "pop_wall.atl") != 0) return -1; if (atlas_load(&fore_at, "pop_fore.atl") != 0) return -1; + if (atlas_load(&pot_at, "pop_pot.atl") != 0) return -1; return 0; } @@ -44,6 +46,7 @@ void pop_bg_free(void) atlas_free(&env[i]); atlas_free(&wall_at); atlas_free(&fore_at); + atlas_free(&pot_at); } /* Верхний клип блитов фона (ЭКРАННАЯ y): !=0 — строки кадра с screen_y < @@ -72,8 +75,16 @@ static void blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) int sy = g_clip_top - top; /* пропустить верхние строки */ if (sy < (int)h) gfx_blit_part(x, g_clip_top, img, 0, sy, (int)w, (int)h - sy); + } else if (x >= 0 && top >= 0 && w < 256 && h < 256 && + x + (int)w <= 320 && top + (int)h <= 256) { + /* Спрайт целиком на экране — линейное ядро без клипа: ~2.9× + * быстрее (замер: 4617 против 13288 тактов на спрайт 32×3; + * общее ядро gfx_blit платит за клип/16-бит/split, а не за + * пиксели). W3-скобку ставит сама libbgi: из кода, собранного + * с --w3, её вызывать нельзя (окно W3 уходит под видеобанк). */ + gfx_blit_noclip(x, top, img); } else { - gfx_blit(x, top, img); + gfx_blit(x, top, img); /* край экрана — с клипом */ } } gfx_w0_unmap(); @@ -115,6 +126,9 @@ void pop_fore_heal(void) static void env_b(uint8_t id, int x, int yb) { blit_b(&env[id >> 5], (uint8_t)(id & 31), x, yb); } static void wall_b(uint8_t id, int x, int yb) { blit_b(&wall_at, id, x, yb); } static void fore_b(uint8_t id, int x, int yb) { blit_b(&fore_at, id, x, yb); } +static void pot_b(uint8_t id, int x, int yb) { blit_b(&pot_at, id, x, yb); } + + /* ---- Таблица тайлов (piece, seg008.c:27) --------------------------- */ typedef struct { @@ -180,6 +194,40 @@ static const uint8_t RPOS[4] = {52, 42, 31, 21}; #define RES_WALL_MARK_TR 16 #define RES_WALL_MARK_BR 17 +/* ---- Зелье (tiles_10_potion, seg008 draw_tile_anim/draw_tile_fore) ---- + * Тип = (modif & 0xF8) >> 3: 0 = пусто (ничего не рисуем), 2..4 — «большая» + * склянка (id 13) и pot_size=1, остальные — маленькая (id 12). Спрайты — из + * chtab_1 (атлас pop_pot), поэтому НЕ из env: раньше мы рисовали chtab_6 id 12 + * (res212), которого в VDUNGEON нет — оттуда и «мусорный объект». + * Позиции оригинала: склянка xh = fore_x + draw_xh, xl = 6, ybottom = fore_y + + * draw_main_y; пузырёк/маска xh = draw_xh + 3, xl = 1, + * ybottom = draw_main_y − (pot_size << 2) − 14. */ +#define POT_MASK_ID 23 +static const uint8_t POTION_FRAM_BUBB[8] = {0, 16, 17, 18, 19, 20, 21, 22}; + +static uint8_t potion_type(uint8_t modif) { return (uint8_t)((modif & 0xF8) >> 3); } + +static void potion_flask(int row, int col, uint8_t modif) /* draw_tile_fore */ +{ + uint8_t type = potion_type(modif); + uint8_t id = 12; + if (type == 0) return; + if (type >= 2 && type < 5) id = 13; /* большая склянка */ + pot_b(id, (tile_table[10].fore_x + COL_XH[col]) * 8 + 6, + tile_table[10].fore_y + 63 * row + 62); +} + +static void potion_bubble(int row, int col, uint8_t modif, uint8_t fore) +{ + uint8_t type = potion_type(modif); + int pot_size = (type >= 2 && type < 5) ? 1 : 0; + int x = (COL_XH[col] + 3) * 8 + 1; + int yb = 63 * row + 62 - (pot_size << 2) - 14; + if (type == 0) return; + if (fore) pot_b(POTION_FRAM_BUBB[modif & 7], x, yb); /* кадр пузырька */ + else pot_b(POT_MASK_ID, x, yb); /* маска (под ним) */ +} + /* ---- Текущая комната (globals для tile_code/mod) ------------------- */ static const uint8_t *g_fg, *g_bg, *g_lfg, *g_lbg; static const uint8_t *g_below; /* row0-коды комнаты СНИЗУ (для topright под нижним рядом) */ @@ -435,6 +483,9 @@ static void draw_tile(int row, int col) } else { if (lt->right_id) env_b(lt->right_id, x, lt->right_y + dmy); + /* Основание факела (seg008:489): рисует ПРАВЫЙ сосед. */ + if (lcode == 19 || lcode == 30) + env_b(146, x, dby - 28); /* stripe — только палас, dungeon пропускает */ } } @@ -452,6 +503,9 @@ static void draw_tile(int row, int col) if (sf < 10 && SPIKES_FRAM_RIGHT[sf]) env_b(SPIKES_FRAM_RIGHT[sf], x, dmy - 7); } + /* Пламя факела (seg008:560, chtab_1 id modifier+1) в фон НЕ запекаем: + * оно анимируется каждый кадр, а heal вернул бы запечённый кадр из-под + * нового. Рисует pop_torch_draw (динамика, pop_process_trobs). */ } /* --- draw_tile_bottom (seg008.c:570) --- */ @@ -497,8 +551,12 @@ static void draw_tile(int row, int col) wall_b(WALL_FRAM_MAIN[wmod], x, dmy); /* основная грань */ wall_pattern(row, col, 1); } - if (t->fore_id) + if (code == 10) { /* зелье: chtab_1, не env fore_id. + * Пузырёк — динамика (pop_potion_draw). */ + potion_flask(row, col, tile_mod(row, col)); + } else if (t->fore_id) { fore_b(t->fore_id, t->fore_x * 8 + x, t->fore_y + dmy); + } } /* ---- Loose floors — динамическая перерисовка (seg008 draw_loose) ---- * @@ -565,15 +623,13 @@ void pop_room_redraw_seam_left(void) { gfx_set_bank(GFX_BANK_NORMAL); setfillstyle(SOLID_FILL, BLACK); - bar(0, POP_YOFF, 31, POP_YOFF + 64); /* col0, ряд 0 (бары ворот) */ + /* Полосу кладки У ПОТОЛКА (ряд −1) НЕ трогаем: её низ на room-space y=2, + * а бары ворот начинаются с y=3 (gate_top_y = dby−62). Раньше bar шёл от + * POP_YOFF и стирал потолок, из-за чего приходилось перерисовывать ещё и + * draw_tile(-1,0) — вдвое дороже на КАЖДОМ кадре анимации решётки. */ + bar(0, POP_YOFF + 3, 31, POP_YOFF + 64); /* col0, ряд 0 (бары ворот) */ gfx_set_bank(GFX_BANK_TRANSPARENT); draw_tile(0, 0); /* фон col0 + бары ворот (живой modif) */ - /* Bar стёр и полосу кладки У ПОТОЛКА (ряд −1 = ряд 2 комнаты сверху, - * draw_tile_aboveroom): без её перерисовки потолок над решёткой пропадал - * после первой же анимации ворот. Клип — как в pop_room_draw. */ - g_clip_top = POP_YOFF; - draw_tile(-1, 0); - g_clip_top = 0; gfx_set_bank(GFX_BANK_NORMAL); } @@ -611,6 +667,41 @@ void pop_ceil_bake_empty(int col) gfx_set_bank(GFX_BANK_NORMAL); } +/* ---- Анимация факела и зелья (динамика, каждый кадр) ---------------- * + * Оба кадра рисуются ПОВЕРХ статического фона и стираются heal'ом своей + * области (фон запечён БЕЗ них). Факел: пламя chtab_1 id modifier+1 в + * ячейке ПРАВОГО соседа (seg008:560: xh = draw_xh + 1, ybottom = + * draw_main_y − 40). Зелье: маска (23, «прорезает» фон) + кадр пузырька + * potion_fram_bubb[modif & 7] на xh+3/xl=1, ybottom = draw_main_y − + * (pot_size<<2) − 14 (seg008:668). */ +void pop_torch_draw(int row, int col, uint8_t modif) +{ + /* Пламя рисует ПРАВЫЙ сосед со своим draw_xh: xh = COL_XH[col+1] + 1 + * (seg008:560), т.е. x = (col+1)*32 + 8 — как и основание факела (env + * 146), которое тоже лежит в ячейке соседа. */ + int x = (col + 1 < 10 ? COL_XH[col + 1] : COL_XH[col] + 4) * 8 + 8; + int yb = 63 * row + 62 - 40; + if (modif >= 9) return; + heal_off(x - 2, yb - 19, 22, 22); + gfx_set_bank(GFX_BANK_SPRITE); + pot_b((uint8_t)(modif + 1), x, yb); + gfx_set_bank(GFX_BANK_NORMAL); +} + +void pop_potion_draw(int row, int col, uint8_t modif) +{ + uint8_t type = potion_type(modif); + int pot_size = (type >= 2 && type < 5) ? 1 : 0; + int x = (COL_XH[col] + 3) * 8 + 1; + int yb = 63 * row + 62 - (pot_size << 2) - 14; + if (type == 0) return; + heal_off(x - 1, yb - 10, 8, 12); + gfx_set_bank(GFX_BANK_SPRITE); + pot_b(POT_MASK_ID, x, yb); /* маска (под пузырьком) */ + pot_b(POTION_FRAM_BUBB[modif & 7], x, yb); /* кадр пузырька */ + gfx_set_bank(GFX_BANK_NORMAL); +} + /* Перерисовать тайл КНОПКИ (row,col) на back-странице: нажатая/отжатая * (draw_tile подставляет пол/stuck по таймеру связи). Bar-в-чёрный + * redraw — иначе «шишка» отжатой кнопки (base 147) осталась бы при переходе @@ -917,8 +1008,11 @@ static void fore_only_tile(int row, int col) if (sf > 0 && sf < 10 && SPIKES_FRAM_FORE[sf]) env_b(SPIKES_FRAM_FORE[sf], x, dmy - 2); } - if (t->fore_id) + if (code == 10) { /* зелье: склянка (пузырёк — динамика) */ + potion_flask(row, col, tile_mod(row, col)); + } else if (t->fore_id) { fore_b(t->fore_id, t->fore_x * 8 + x, t->fore_y + dmy); + } } /* Полоса кладки У ПОТОЛКА поверх Kid — порт redraw_needed_above (seg008:0221). diff --git a/applications/PoP/roomtest/pop_bg.h b/applications/PoP/roomtest/pop_bg.h index f19b1e5..38a117b 100644 --- a/applications/PoP/roomtest/pop_bg.h +++ b/applications/PoP/roomtest/pop_bg.h @@ -85,6 +85,12 @@ extern uint8_t pop_loose_landed; * modif (pop_trob). Звать из pop_process_trobs. См. pop_bg.c. */ void pop_spike_redraw(int row, int col); +/* Анимация факела/зелья (динамика каждый кадр; фон запечён без них): + * modif — живой room_modif тайла. Для факела col — колонка САМОГО факела + * (пламя рисуется в ячейке правого соседа). */ +void pop_torch_draw(int row, int col, uint8_t modif); +void pop_potion_draw(int row, int col, uint8_t modif); + /* Ворота на ЛЕВОМ шве (col0) — перерисовать по живому modif соседа слева. * g_lbg (lcol_bg) должен быть обновлён вызывающим. Звать из основного цикла * на back-странице, когда сосед слева анимируется. См. pop_bg.c. */ diff --git a/applications/PoP/roomtest/pop_trob.c b/applications/PoP/roomtest/pop_trob.c index 9bd98a2..720526c 100644 --- a/applications/PoP/roomtest/pop_trob.c +++ b/applications/PoP/roomtest/pop_trob.c @@ -13,6 +13,9 @@ #define TILE_CLOSER 0x06 #define TILE_DEBRIS 0x0E /* button_type для «насовсем открыть» (died_on_button) */ #define TILE_OPENER 0x0F +#define TILE_POTION 0x0A +#define TILE_TORCH 0x13 +#define TILE_TORCH_DEBRIS 0x1E /* ---- Персистентное per-room modifier-состояние -------------------- */ static uint8_t room_modif[POP_NROOMS][POP_ROOMTILES]; @@ -38,6 +41,17 @@ static uint8_t button_rest[POP_ROOMTILES]; * расхождение было именно в безусловном редрое). */ static uint8_t trob_drawn[POP_ROOMTILES]; +/* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и + * выбора следующего кадра пламени. */ +static unsigned long trob_seed; + +static uint16_t trob_prandom(uint16_t maxv) +{ + trob_seed = trob_seed * 214013UL + 2531011UL; + return (uint16_t)((uint16_t)(trob_seed >> 16) % (uint16_t)(maxv + 1)); +} + + void pop_trob_reset(void) { uint8_t i; @@ -50,7 +64,15 @@ uint8_t *pop_trob_modif(uint8_t room) { if (room < 1 || room > POP_NROOMS) return 0; if (!room_seen[room - 1]) { + uint8_t i; pop_level_room_bg(room, room_modif[room - 1]); + /* seg009 load_level: у ЗЕЛЬЯ модификатор сдвигается влево на 3 — + * старшие биты = тип зелья, младшие 3 = фаза пузырька. */ + pop_level_access_begin(); + for (i = 0; i < POP_ROOMTILES; i++) + if ((pop_level_tile_raw(room, i) & 0x1F) == TILE_POTION) + room_modif[room - 1][i] = (uint8_t)(room_modif[room - 1][i] << 3); + pop_level_access_end(); room_seen[room - 1] = 1; } return room_modif[room - 1]; @@ -81,11 +103,51 @@ void pop_trob_room_changed(void) for (i = 0; i < POP_ROOMTILES; i++) { spike_rest[i] = 0; button_rest[i] = 0; trob_drawn[i] = 0; } } +/* anim_tile_modif (seg000:0B12) — при входе в комнату завести анимации её + * тайлов: факелам — случайная стартовая фаза пламени (start_anim_torch, + * seg007:0847: modif = prandom(8)), зельям — случайная фаза пузырька + * (start_anim_potion: младшие 3 бита = prandom(6)+1, старшие = тип). */ +void pop_trob_anim_room(uint8_t room) +{ + uint8_t *mod = pop_trob_modif(room); + uint8_t i, code; + if (!mod) return; + pop_level_access_begin(); + for (i = 0; i < POP_ROOMTILES; i++) { + code = (uint8_t)(pop_level_tile_raw(room, i) & 0x1F); + if (code == TILE_TORCH || code == TILE_TORCH_DEBRIS) { + mod[i] = (uint8_t)trob_prandom(8); + pop_add_trob(room, i, 1); + } else if (code == TILE_POTION) { + mod[i] = (uint8_t)((mod[i] & 0xF8) | (trob_prandom(6) + 1)); + pop_add_trob(room, i, 1); + } + } + pop_level_access_end(); +} + /* animate_spike (seg007:04D3): автомат по modifier. *m — modifier тайла, * *type — тип trob (>=0 активен, <0 = удалить). Выдвиг: ++modif (1..4), * на 5 → 0x8F (полностью вышли, безвредны), на 9 → 0 + конец. Убирание * (флаг 0x80): --modif; когда 0x7F-часть обнулилась → modif=6 (начать * визуальный уход 6..8). 0xFF = disabled. */ +/* get_torch_frame (seg007:06CD): случайный кадр 0..8, иначе следующий. */ +static uint8_t get_torch_frame(uint8_t curr) +{ + uint8_t next = (uint8_t)trob_prandom(255); + if (next != curr && next < 9) return next; + next = (uint8_t)(curr + 1); + if (next >= 9) next = 0; + return next; +} + +/* bubble_next_frame (seg007:0759): фазы пузырька 1..7 по кругу. */ +static uint8_t bubble_next_frame(uint8_t curr) +{ + uint8_t next = (uint8_t)(curr + 1); + return (next >= 8) ? 1 : next; +} + static void animate_spike(uint8_t *m, int8_t *type) { uint8_t mod = *m; @@ -230,9 +292,12 @@ uint8_t pop_trob_room_active(uint8_t room) return 0; } +static uint8_t torch_tick; /* делитель скорости анимации пламени */ + void pop_process_trobs(uint8_t cur_room) { uint8_t i, w = 0; + torch_tick++; /* Перерисовка «покоя» законченных пик/кнопок (обе страницы). * row/col ведём бегущими счётчиками, а НЕ через i/10, i%10: иначе SDCC @@ -286,12 +351,35 @@ void pop_process_trobs(uint8_t cur_room) case TILE_GATE: animate_door(&mod[tp], &type); break; + case TILE_TORCH: + case TILE_TORCH_DEBRIS: + /* animate_torch (seg007:03C1): кадр пламени; анимация вечная — + * type остаётся 1, пока комната отрисована. Смена кадра — через + * ЛОГИЧЕСКИЙ кадр (torch_tick): у нас кадр короче игрового тика + * оригинала, и пламя «кипело» вдвое быстрее нужного. */ + if (room == cur_room && (torch_tick & 1) == 0) + mod[tp] = get_torch_frame(mod[tp]); + break; + case TILE_POTION: + /* animate_potion (seg007:03E9): крутим младшие 3 бита (фаза + * пузырька), старшие 5 — тип зелья. */ + if (room == cur_room) + mod[tp] = (uint8_t)((mod[tp] & 0xF8) | bubble_next_frame(mod[tp] & 7)); + break; default: type = -1; break; } trobs[i].type = type; + if (room == cur_room && + (code == TILE_TORCH || code == TILE_TORCH_DEBRIS || code == TILE_POTION)) { + /* Пламя/пузырёк анимируются КАЖДЫЙ кадр и в фон не запечены — + * рисуем поверх (сами делают heal своей области). */ + uint8_t trow = tp / 10, tcol = tp % 10; + if (code == TILE_POTION) pop_potion_draw(trow, tcol, mod[tp]); + else pop_torch_draw(trow, tcol, mod[tp]); + } if (room == cur_room && (code == TILE_SPIKE || code == TILE_OPENER || code == TILE_CLOSER)) { /* Редрой ТОЛЬКО при изменении видимого состояния (redraw_11h из diff --git a/applications/PoP/roomtest/pop_trob.h b/applications/PoP/roomtest/pop_trob.h index a0223c6..5d08f11 100644 --- a/applications/PoP/roomtest/pop_trob.h +++ b/applications/PoP/roomtest/pop_trob.h @@ -58,4 +58,8 @@ uint8_t pop_trob_room_active(uint8_t room); * enter_room. */ void pop_trob_room_changed(void); +/* anim_tile_modif (seg000:0B12): завести анимации тайлов комнаты (факелы, + * зелья) со случайной стартовой фазой. Звать в enter_room. */ +void pop_trob_anim_room(uint8_t room); + #endif diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 3cdada6..1691c2c 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -134,6 +134,7 @@ static void enter_room(uint8_t room) pop_map_set_room(room); pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */ pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */ + pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */ /* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ * per-room (room_modif живёт между входами); рисуем и коллидируем по ним, * а не по свежему backtable. Фаза P0. */ @@ -280,7 +281,7 @@ int main(void) io_border = 6; /* голубой */ pop_process_trobs(cur_room); /* пики/ворота: продвинуть + перерисовать (тоже */ /* слой фона — ДО Kid) */ - io_border = 2; /* синий */ + io_border = 4; /* синий */ { /* ворота соседа слева (room8) видны в col0 — */ uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */ if (lft) { @@ -297,7 +298,7 @@ int main(void) } } } - io_border = 4; /* зеленый */ + io_border = 2; /* зеленый */ if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */ dead_frames++; if (dead_frames > RESPAWN_SETTLE && diff --git a/applications/PoP/toolchain/pop_pack_bg.py b/applications/PoP/toolchain/pop_pack_bg.py index a8160a6..287c033 100644 --- a/applications/PoP/toolchain/pop_pack_bg.py +++ b/applications/PoP/toolchain/pop_pack_bg.py @@ -71,6 +71,32 @@ SPIKE_ANIM_ENV_IDS = {128, 129, 130, 131, 132, # left # открытых состояний (54-59, 67) нужны анимации открытия — добавляем явно. GATE_ANIM_ENV_IDS = {50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 67} +# ---- chtab_1 (flame/sword/potion), база ресурса 150, папка PRINCE ---- +# Зелье (seg008 draw_tile_fore/draw_tile_anim): склянка id 12 (малая) / 13 +# (большая, типы 2..4), кадры пузырька 16..22 (potion_fram_bubb) и его маска +# 23. Пузырёк и маска рисуются MONO-блитом (одним цветом), поэтому пакуем их +# силуэтами уже в нужном цвете: маска — чёрный env[0], пузырёк — env[12] +# (красный, тип «лечение»). Цвета других типов (9 синий / 10 зелёный) — +# TODO, когда появятся такие зелья. +POT_DIR_NAME = "PRINCE" +POT_FLAME_IDS = tuple(range(1, 10)) # пламя факела: modifier+1 (seg008:560) +POT_FLASK_IDS = (12, 13) +POT_BUBBLE_IDS = (16, 17, 18, 19, 20, 21, 22) +POT_MASK_ID = 23 +POT_PAL_BASE = 0x40 # 16 цветов chtab_1 -> слоты 0x40..0x4F +# MONO-блит (method_3_blit_mono, seg009:3040) красит спрайт цветом из ОБЩЕЙ +# 16-цветной палитры экрана, а не из палитры chtab. Кладём её в 0x30..0x3F. +VGA16_PAL_BASE = 0x30 +VGA16 = [(0,0,0),(0,0,42),(0,42,0),(0,42,42),(42,0,0),(42,0,42),(42,21,0), + (42,42,42),(21,21,21),(21,21,63),(21,63,21),(21,63,63),(63,21,21), + (63,21,63),(63,63,21),(63,63,63)] # 6-битные каналы, как в .pal +POT_MASK_COLOR = VGA16_PAL_BASE + 0 # чёрный (blitters_40h_mono без цвета) +POT_BUBBLE_COLOR = VGA16_PAL_BASE + 12 # ярко-красный (color 12: «лечение») + +# Основание факела (seg008:489 draw_tile_right: при левом соседе-факеле +# рисуется chtab_6 id 146). В статический render_room не попадает. +TORCH_ENV_IDS = {146} + ENV_SHIFT = 5 # env фон: страница = id>>5, idx = id&31 ENV_MASK = (1 << ENV_SHIFT) - 1 @@ -139,6 +165,25 @@ def load_indexed(chtab, id): return im.width, im.height, out +def load_potion(id, mono=None): + """chtab_1: res(150+id).png из data/PRINCE. mono=None — обычный спрайт + (палитра chtab_1 -> слоты POT_PAL_BASE+i); mono=цвет — силуэт этим цветом + (порт blitters_40h_mono).""" + path = os.path.join(os.path.dirname(R.VDUNGEON_DIR), POT_DIR_NAME, + f"res{150 + id}.png") + if not os.path.exists(path): + die(f"нет файла {path}") + im = Image.open(path) + if im.mode != "P": + die(f"res{150+id}: mode {im.mode}, ожидался P") + src = im.tobytes() + if mono is None: + out = bytes((0xFF if v == 0 else POT_PAL_BASE + v) for v in src) + else: + out = bytes((0xFF if v == 0 else mono) for v in src) + return im.width, im.height, out + + def blob(w, h, pix): """getimage-блоб: u16 w, u16 h (LE) + пиксели.""" return w.to_bytes(2, "little") + h.to_bytes(2, "little") + pix @@ -206,6 +251,8 @@ def build_palette(out_path): env = read_pal("res200.pal", R.VDUNGEON_DIR) wall = read_pal("res360.pal", R.VDUNGEON_DIR) + pot = read_pal("res150.pal", os.path.join(os.path.dirname(R.VDUNGEON_DIR), + POT_DIR_NAME)) pal = bytearray(256 * 4) for i, (r, g, b) in enumerate(env): o = (0x50 + i) * 4 @@ -213,6 +260,12 @@ def build_palette(out_path): for i, (r, g, b) in enumerate(wall): o = (0x60 + i) * 4 pal[o:o + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0)) + for i, (r, g, b) in enumerate(pot): + o = (POT_PAL_BASE + i) * 4 + pal[o:o + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0)) + for i, (r, g, b) in enumerate(VGA16): + o = (VGA16_PAL_BASE + i) * 4 + pal[o:o + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0)) with open(out_path, "wb") as f: f.write(pal) @@ -249,6 +302,8 @@ def write_header(out_path, env_pages, wall_name, fore_name): "};", '#define POP_WALL_ATL "%s"' % wall_name, '#define POP_FORE_ATL "%s"' % fore_name, + '#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья */', + "#define POP_PAL_POT 0x%02X" % POT_PAL_BASE, '#define POP_BG_PAL "pop_bg.pal"', "", "#endif", @@ -265,7 +320,8 @@ def main(): | CLIMB_OVERLAY_ENV_IDS # + пол-оверлей подъёма | LOOSE_ANIM_ENV_IDS # + дрожащие кадры loose | SPIKE_ANIM_ENV_IDS # + кадры выдвижения пик - | GATE_ANIM_ENV_IDS) # + кадры открытия ворот + | GATE_ANIM_ENV_IDS # + кадры открытия ворот + | TORCH_ENV_IDS) # + основание факела wall_ids = sorted(id for ch, id in used if ch == R.CHTAB_WALL) env_bg = [i for i in env_ids if i not in FORE_ENV_IDS] @@ -302,6 +358,22 @@ def main(): total_bytes += sz print(f" pop_fore.atl: {len(env_fore)} спрайтов, каталог {cnt}, {sz} Б") + # POTION (chtab_1) -> idx=id: склянки + кадры пузырька + маска + # chtab_1 целиком (res151..res173 = id 1..23): пламя факелов, меч-предмет, + # склянки зелий. Пузырьки и их маска рисуются MONO-блитом — пакуем + # силуэтами в нужном цвете (см. POT_*_COLOR). + pot_entries = {} + for id in range(1, 24): + if id in POT_BUBBLE_IDS: + pot_entries[id] = load_potion(id, POT_BUBBLE_COLOR) + elif id == POT_MASK_ID: + pot_entries[id] = load_potion(id, POT_MASK_COLOR) + else: + pot_entries[id] = load_potion(id) + cnt, sz = pack_atlas(os.path.join(OUT_DIR, "pop_pot.atl"), pot_entries) + total_bytes += sz + print(f" pop_pot.atl (chtab_1: факелы+зелья): {len(pot_entries)} спрайтов, каталог {cnt}, {sz} Б") + build_palette(os.path.join(OUT_DIR, "pop_bg.pal")) write_header(os.path.join(OUT_DIR, "pop_bg_atlas.h"), env_page_names, "pop_wall.atl", "pop_fore.atl") diff --git a/applications/PoP/toolchain/pop_pack_kid.py b/applications/PoP/toolchain/pop_pack_kid.py index 4baf7ef..3f49e51 100644 --- a/applications/PoP/toolchain/pop_pack_kid.py +++ b/applications/PoP/toolchain/pop_pack_kid.py @@ -69,14 +69,21 @@ def _read16(path): def build_palette(out_path): """ЕДИНАЯ игровая палитра (gfx_pal_fload пишет с индекса 0, перезатирая - всё — bg и Kid обязаны жить в одном файле): env@0x50, wall@0x60, kid@0x70. + всё — bg и Kid обязаны жить в одном файле): chtab_1@0x40 (пламя/зелья), + env@0x50, wall@0x60, kid@0x70. """ pal = bytearray(256 * 4) groups = [ (0x50, os.path.join(B.R.VDUNGEON_DIR, "res200.pal")), # env (0x60, os.path.join(B.R.VDUNGEON_DIR, "res360.pal")), # wall (PAL_BASE, os.path.join(KID_DIR, "res400.pal")), # kid 0x70 + (B.POT_PAL_BASE, os.path.join(os.path.dirname(B.R.VDUNGEON_DIR), + B.POT_DIR_NAME, "res150.pal")), # chtab_1 0x40 ] + # 16 базовых цветов экрана (mono-блит красит ими: пламя, пузырьки зелья) + for i, (r, g, bl) in enumerate(B.VGA16): + o = (B.VGA16_PAL_BASE + i) * 4 + pal[o:o + 4] = bytes((B.scale6to8(bl), B.scale6to8(g), B.scale6to8(r), 0)) for base, path in groups: for i, (r, g, bl) in enumerate(_read16(path)): o = (base + i) * 4 diff --git a/libbgi/common/gfx_blit_noclip.c b/libbgi/common/gfx_blit_noclip.c new file mode 100644 index 0000000..412c9f1 --- /dev/null +++ b/libbgi/common/gfx_blit_noclip.c @@ -0,0 +1,35 @@ +/* + * gfx_blit_noclip — блит ЦЕЛОЙ картинки getimage-формата БЕЗ клипа, в + * ТЕКУЩЕМ банке (gfx_set_bank). Быстрый вариант gfx_blit для тайловых + * движков: вызывающий гарантирует, что картинка целиком на экране. + * + * Зачем отдельная функция: gfx_blit идёт через общее ядро _gfx_blit_full + * (16-битная арифметика, клип, пересчёт src, нарезка полос >256) — замер + * в MAME (PoP roomtest, 2026-07-27): 13 288 тактов на блит спрайта 32×3, + * т.е. цена почти не зависит от размера, платим за проход по цепочке. + * Линейное спрайтовое ядро на том же спрайте — 4 617 тактов (в 2.9 раза + * быстрее): 8-битные w/h, без split, без клип-кода. + * + * От putsprite отличается тем, что НЕ навязывает GFX_BANK_SPRITE: фон + * рисуется в GFX_BANK_TRANSPARENT (0xFF не пишется, но ОЗУ-копия + * обновляется — иначе gfx_heal сотрёт нарисованное). + * + * W3-скобка ставится ЗДЕСЬ (код libbgi живёт в W1): вызывать её из + * приложения, собранного с --w3, нельзя — после _bgi_begin окно W3 + * занято видеобанком и код вызывающего исчезает из адресного + * пространства (проверено: белый экран). + * + * Ограничения: w,h ≤ 255; картинка целиком в пределах экрана. Не + * подходит — зовите gfx_blit/gfx_blit_part. + */ +#include "../_bgi.h" + +void gfx_blit_noclip(int x, int y, const void *img) +{ + const uint8_t *p = (const uint8_t *)img; + + _bgi_begin(); + _gfx_blit_sprite_noclip(x, y, p + 4, (uint16_t)(p[0] | (p[1] << 8)), + p[0], p[2]); + _bgi_end(); +} diff --git a/libbgi/include/gfx.h b/libbgi/include/gfx.h index a3b2445..f5af11e 100644 --- a/libbgi/include/gfx.h +++ b/libbgi/include/gfx.h @@ -87,6 +87,13 @@ inline uint8_t gfx_get_sprite_clip(void) { return _gfx_sprite_clip; } /* Блит целой картинки левым-верхним углом в (x,y). */ void gfx_blit(int x, int y, const void *img); +/* Быстрый блит БЕЗ клипа в ТЕКУЩЕМ банке: картинка обязана быть целиком на + * экране, w/h ≤ 255. Идёт линейным спрайтовым ядром — ~2.9× быстрее + * gfx_blit (замер PoP roomtest 2026-07-27: 4617 против 13288 тактов на + * спрайт 32×3; общее ядро тратит время на клип/16-бит/split, а не на + * пиксели). Для тайловых движков; где нужен клип — gfx_blit/gfx_blit_part. */ +void gfx_blit_noclip(int x, int y, const void *img); + /* Блит под-прямоугольника (sx,sy,w,h) картинки (атлас кадров: * кадр N ленты — sx = N*FRAME_W). Под-прямоугольник обязан лежать * внутри img; по экрану — клиппится. */