PoP: факелы и зелья (chtab_1) + быстрый блит фона gfx_blit_noclip

Графика 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 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 20:53:04 +03:00
parent 63a3b60524
commit c127a4b0d2
16 changed files with 377 additions and 16 deletions
@@ -289,3 +289,47 @@ Kid, обычно 2–4 за кадр) рисовал ещё и `bottom_id` —
`redraw_frames_above`; сузить до колонок, реально задетых спрайтом. `redraw_frames_above`; сузить до колонок, реально задетых спрайтом.
6. `wall_pattern` внутри fore/overlay — тяжёлая (PRNG + до 4 блитов); проверить, 6. `wall_pattern` внутри fore/overlay — тяжёлая (PRNG + до 4 блитов); проверить,
не зовём ли её там, где оригинал ограничивается `wall_fram_main`. не зовём ли её там, где оригинал ограничивается `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 (лишние блиты, которых нет в оригинале).
Binary file not shown.
@@ -27,6 +27,8 @@ static const char *const pop_env_atl[POP_ENV_PAGES] = {
}; };
#define POP_WALL_ATL "pop_wall.atl" #define POP_WALL_ATL "pop_wall.atl"
#define POP_FORE_ATL "pop_fore.atl" #define POP_FORE_ATL "pop_fore.atl"
#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья */
#define POP_PAL_POT 0x40
#define POP_BG_PAL "pop_bg.pal" #define POP_BG_PAL "pop_bg.pal"
#endif #endif
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -2
View File
@@ -6,7 +6,7 @@ EXAMPLE := roomtest
# small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе # small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе
# CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge). # CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge).
MEMORY ?= small 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 EXTRA_SRCS := pop_kid.c pop_ctrl.c pop_level.c
BG_DIR := $(CURDIR)/../poc/res/bg BG_DIR := $(CURDIR)/../poc/res/bg
@@ -15,7 +15,8 @@ LVL_DIR := $(CURDIR)/../SDLPoP/data/LEVELS
LVL_DATA := $(LVL_DIR)/res2001.bin LVL_DATA := $(LVL_DIR)/res2001.bin
BG_DATA := $(BG_DIR)/pop_env0.atl $(BG_DIR)/pop_env1.atl $(BG_DIR)/pop_env2.atl \ 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_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) # ВСЕ 28 страниц Kid — иначе кадры торможения/разворота/приседа (image>23)
# не рисуются (kid_draw пропускает незагруженную страницу). Палитра — # не рисуются (kid_draw пропускает незагруженную страницу). Палитра —
# ЕДИНАЯ игровая (env+wall+kid). # ЕДИНАЯ игровая (env+wall+kid).
+104 -10
View File
@@ -21,6 +21,7 @@
static atlas_t env[5]; static atlas_t env[5];
static atlas_t wall_at; static atlas_t wall_at;
static atlas_t fore_at; static atlas_t fore_at;
static atlas_t pot_at; /* chtab_1: зелья (склянка + пузырёк) */
static const char *const ENV_ATL[5] = { static const char *const ENV_ATL[5] = {
"pop_env0.atl", "pop_env1.atl", "pop_env2.atl", "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(&env[i], ENV_ATL[i]) != 0) return -1;
if (atlas_load(&wall_at, "pop_wall.atl") != 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(&fore_at, "pop_fore.atl") != 0) return -1;
if (atlas_load(&pot_at, "pop_pot.atl") != 0) return -1;
return 0; return 0;
} }
@@ -44,6 +46,7 @@ void pop_bg_free(void)
atlas_free(&env[i]); atlas_free(&env[i]);
atlas_free(&wall_at); atlas_free(&wall_at);
atlas_free(&fore_at); atlas_free(&fore_at);
atlas_free(&pot_at);
} }
/* Верхний клип блитов фона (ЭКРАННАЯ y): !=0 — строки кадра с screen_y < /* Верхний клип блитов фона (ЭКРАННАЯ 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; /* пропустить верхние строки */ int sy = g_clip_top - top; /* пропустить верхние строки */
if (sy < (int)h) if (sy < (int)h)
gfx_blit_part(x, g_clip_top, img, 0, sy, (int)w, (int)h - sy); 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 { } else {
gfx_blit(x, top, img); gfx_blit(x, top, img); /* край экрана — с клипом */
} }
} }
gfx_w0_unmap(); 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 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 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 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) --------------------------- */ /* ---- Таблица тайлов (piece, seg008.c:27) --------------------------- */
typedef struct { 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_TR 16
#define RES_WALL_MARK_BR 17 #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) ------------------- */ /* ---- Текущая комната (globals для tile_code/mod) ------------------- */
static const uint8_t *g_fg, *g_bg, *g_lfg, *g_lbg; static const uint8_t *g_fg, *g_bg, *g_lfg, *g_lbg;
static const uint8_t *g_below; /* row0-коды комнаты СНИЗУ (для topright под нижним рядом) */ static const uint8_t *g_below; /* row0-коды комнаты СНИЗУ (для topright под нижним рядом) */
@@ -435,6 +483,9 @@ static void draw_tile(int row, int col)
} else { } else {
if (lt->right_id) if (lt->right_id)
env_b(lt->right_id, x, lt->right_y + dmy); 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 пропускает */ /* stripe — только палас, dungeon пропускает */
} }
} }
@@ -452,6 +503,9 @@ static void draw_tile(int row, int col)
if (sf < 10 && SPIKES_FRAM_RIGHT[sf]) if (sf < 10 && SPIKES_FRAM_RIGHT[sf])
env_b(SPIKES_FRAM_RIGHT[sf], x, dmy - 7); 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) --- */ /* --- draw_tile_bottom (seg008.c:570) --- */
@@ -497,9 +551,13 @@ static void draw_tile(int row, int col)
wall_b(WALL_FRAM_MAIN[wmod], x, dmy); /* основная грань */ wall_b(WALL_FRAM_MAIN[wmod], x, dmy); /* основная грань */
wall_pattern(row, col, 1); 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); fore_b(t->fore_id, t->fore_x * 8 + x, t->fore_y + dmy);
} }
}
/* ---- Loose floors — динамическая перерисовка (seg008 draw_loose) ---- * /* ---- Loose floors — динамическая перерисовка (seg008 draw_loose) ---- *
* Порт SDLPoP redraw_tile_height: каждый кадр анимации перерисовать * Порт SDLPoP redraw_tile_height: каждый кадр анимации перерисовать
@@ -565,15 +623,13 @@ void pop_room_redraw_seam_left(void)
{ {
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK); setfillstyle(SOLID_FILL, BLACK);
bar(0, POP_YOFF, 31, POP_YOFF + 64); /* col0, ряд 0 (бары ворот) */ /* Полосу кладки У ПОТОЛКА (ряд −1) НЕ трогаем: её низ на room-space y=2,
* а бары ворот начинаются с y=3 (gate_top_y = dby62). Раньше bar шёл от
* POP_YOFF и стирал потолок, из-за чего приходилось перерисовывать ещё и
* draw_tile(-1,0) — вдвое дороже на КАЖДОМ кадре анимации решётки. */
bar(0, POP_YOFF + 3, 31, POP_YOFF + 64); /* col0, ряд 0 (бары ворот) */
gfx_set_bank(GFX_BANK_TRANSPARENT); gfx_set_bank(GFX_BANK_TRANSPARENT);
draw_tile(0, 0); /* фон col0 + бары ворот (живой modif) */ 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); gfx_set_bank(GFX_BANK_NORMAL);
} }
@@ -611,6 +667,41 @@ void pop_ceil_bake_empty(int col)
gfx_set_bank(GFX_BANK_NORMAL); 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-странице: нажатая/отжатая /* Перерисовать тайл КНОПКИ (row,col) на back-странице: нажатая/отжатая
* (draw_tile подставляет пол/stuck по таймеру связи). Bar-в-чёрный + * (draw_tile подставляет пол/stuck по таймеру связи). Bar-в-чёрный +
* redraw — иначе «шишка» отжатой кнопки (base 147) осталась бы при переходе * redraw — иначе «шишка» отжатой кнопки (base 147) осталась бы при переходе
@@ -917,9 +1008,12 @@ static void fore_only_tile(int row, int col)
if (sf > 0 && sf < 10 && SPIKES_FRAM_FORE[sf]) if (sf > 0 && sf < 10 && SPIKES_FRAM_FORE[sf])
env_b(SPIKES_FRAM_FORE[sf], x, dmy - 2); 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); fore_b(t->fore_id, t->fore_x * 8 + x, t->fore_y + dmy);
} }
}
/* Полоса кладки У ПОТОЛКА поверх Kid — порт redraw_needed_above (seg008:0221). /* Полоса кладки У ПОТОЛКА поверх Kid — порт redraw_needed_above (seg008:0221).
* Там draw_tile_bottom(1) и draw_loose(1): аргумент 1 = «добавить ещё и в * Там draw_tile_bottom(1) и draw_loose(1): аргумент 1 = «добавить ещё и в
+6
View File
@@ -85,6 +85,12 @@ extern uint8_t pop_loose_landed;
* modif (pop_trob). Звать из pop_process_trobs. См. pop_bg.c. */ * modif (pop_trob). Звать из pop_process_trobs. См. pop_bg.c. */
void pop_spike_redraw(int row, int col); 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 соседа слева. /* Ворота на ЛЕВОМ шве (col0) — перерисовать по живому modif соседа слева.
* g_lbg (lcol_bg) должен быть обновлён вызывающим. Звать из основного цикла * g_lbg (lcol_bg) должен быть обновлён вызывающим. Звать из основного цикла
* на back-странице, когда сосед слева анимируется. См. pop_bg.c. */ * на back-странице, когда сосед слева анимируется. См. pop_bg.c. */
+88
View File
@@ -13,6 +13,9 @@
#define TILE_CLOSER 0x06 #define TILE_CLOSER 0x06
#define TILE_DEBRIS 0x0E /* button_type для «насовсем открыть» (died_on_button) */ #define TILE_DEBRIS 0x0E /* button_type для «насовсем открыть» (died_on_button) */
#define TILE_OPENER 0x0F #define TILE_OPENER 0x0F
#define TILE_POTION 0x0A
#define TILE_TORCH 0x13
#define TILE_TORCH_DEBRIS 0x1E
/* ---- Персистентное per-room modifier-состояние -------------------- */ /* ---- Персистентное per-room modifier-состояние -------------------- */
static uint8_t room_modif[POP_NROOMS][POP_ROOMTILES]; 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]; 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) void pop_trob_reset(void)
{ {
uint8_t i; 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 < 1 || room > POP_NROOMS) return 0;
if (!room_seen[room - 1]) { if (!room_seen[room - 1]) {
uint8_t i;
pop_level_room_bg(room, room_modif[room - 1]); 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; room_seen[room - 1] = 1;
} }
return room_modif[room - 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; } 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 тайла, /* animate_spike (seg007:04D3): автомат по modifier. *m — modifier тайла,
* *type — тип trob (>=0 активен, <0 = удалить). Выдвиг: ++modif (1..4), * *type — тип trob (>=0 активен, <0 = удалить). Выдвиг: ++modif (1..4),
* на 5 → 0x8F (полностью вышли, безвредны), на 9 → 0 + конец. Убирание * на 5 → 0x8F (полностью вышли, безвредны), на 9 → 0 + конец. Убирание
* (флаг 0x80): --modif; когда 0x7F-часть обнулилась → modif=6 (начать * (флаг 0x80): --modif; когда 0x7F-часть обнулилась → modif=6 (начать
* визуальный уход 6..8). 0xFF = disabled. */ * визуальный уход 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) static void animate_spike(uint8_t *m, int8_t *type)
{ {
uint8_t mod = *m; uint8_t mod = *m;
@@ -230,9 +292,12 @@ uint8_t pop_trob_room_active(uint8_t room)
return 0; return 0;
} }
static uint8_t torch_tick; /* делитель скорости анимации пламени */
void pop_process_trobs(uint8_t cur_room) void pop_process_trobs(uint8_t cur_room)
{ {
uint8_t i, w = 0; uint8_t i, w = 0;
torch_tick++;
/* Перерисовка «покоя» законченных пик/кнопок (обе страницы). /* Перерисовка «покоя» законченных пик/кнопок (обе страницы).
* row/col ведём бегущими счётчиками, а НЕ через i/10, i%10: иначе SDCC * row/col ведём бегущими счётчиками, а НЕ через i/10, i%10: иначе SDCC
@@ -286,12 +351,35 @@ void pop_process_trobs(uint8_t cur_room)
case TILE_GATE: case TILE_GATE:
animate_door(&mod[tp], &type); animate_door(&mod[tp], &type);
break; 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: default:
type = -1; type = -1;
break; break;
} }
trobs[i].type = type; 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 && if (room == cur_room &&
(code == TILE_SPIKE || code == TILE_OPENER || code == TILE_CLOSER)) { (code == TILE_SPIKE || code == TILE_OPENER || code == TILE_CLOSER)) {
/* Редрой ТОЛЬКО при изменении видимого состояния (redraw_11h из /* Редрой ТОЛЬКО при изменении видимого состояния (redraw_11h из
+4
View File
@@ -58,4 +58,8 @@ uint8_t pop_trob_room_active(uint8_t room);
* enter_room. */ * enter_room. */
void pop_trob_room_changed(void); void pop_trob_room_changed(void);
/* anim_tile_modif (seg000:0B12): завести анимации тайлов комнаты (факелы,
* зелья) со случайной стартовой фазой. Звать в enter_room. */
void pop_trob_anim_room(uint8_t room);
#endif #endif
+3 -2
View File
@@ -134,6 +134,7 @@ static void enter_room(uint8_t room)
pop_map_set_room(room); pop_map_set_room(room);
pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */ pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */
pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */ pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */
pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */
/* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ /* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ
* per-room (room_modif живёт между входами); рисуем и коллидируем по ним, * per-room (room_modif живёт между входами); рисуем и коллидируем по ним,
* а не по свежему backtable. Фаза P0. */ * а не по свежему backtable. Фаза P0. */
@@ -280,7 +281,7 @@ int main(void)
io_border = 6; /* голубой */ io_border = 6; /* голубой */
pop_process_trobs(cur_room); /* пики/ворота: продвинуть + перерисовать (тоже */ pop_process_trobs(cur_room); /* пики/ворота: продвинуть + перерисовать (тоже */
/* слой фона — ДО Kid) */ /* слой фона — ДО Kid) */
io_border = 2; /* синий */ io_border = 4; /* синий */
{ /* ворота соседа слева (room8) видны в col0 — */ { /* ворота соседа слева (room8) видны в col0 — */
uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */ uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */
if (lft) { if (lft) {
@@ -297,7 +298,7 @@ int main(void)
} }
} }
} }
io_border = 4; /* зеленый */ io_border = 2; /* зеленый */
if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */ if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */
dead_frames++; dead_frames++;
if (dead_frames > RESPAWN_SETTLE && if (dead_frames > RESPAWN_SETTLE &&
+73 -1
View File
@@ -71,6 +71,32 @@ SPIKE_ANIM_ENV_IDS = {128, 129, 130, 131, 132, # left
# открытых состояний (54-59, 67) нужны анимации открытия — добавляем явно. # открытых состояний (54-59, 67) нужны анимации открытия — добавляем явно.
GATE_ANIM_ENV_IDS = {50, 51, 52, 53, 54, 55, 56, 57, 58, 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_SHIFT = 5 # env фон: страница = id>>5, idx = id&31
ENV_MASK = (1 << ENV_SHIFT) - 1 ENV_MASK = (1 << ENV_SHIFT) - 1
@@ -139,6 +165,25 @@ def load_indexed(chtab, id):
return im.width, im.height, out 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): def blob(w, h, pix):
"""getimage-блоб: u16 w, u16 h (LE) + пиксели.""" """getimage-блоб: u16 w, u16 h (LE) + пиксели."""
return w.to_bytes(2, "little") + h.to_bytes(2, "little") + pix 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) env = read_pal("res200.pal", R.VDUNGEON_DIR)
wall = read_pal("res360.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) pal = bytearray(256 * 4)
for i, (r, g, b) in enumerate(env): for i, (r, g, b) in enumerate(env):
o = (0x50 + i) * 4 o = (0x50 + i) * 4
@@ -213,6 +260,12 @@ def build_palette(out_path):
for i, (r, g, b) in enumerate(wall): for i, (r, g, b) in enumerate(wall):
o = (0x60 + i) * 4 o = (0x60 + i) * 4
pal[o:o + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0)) 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: with open(out_path, "wb") as f:
f.write(pal) 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_WALL_ATL "%s"' % wall_name,
'#define POP_FORE_ATL "%s"' % fore_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"', '#define POP_BG_PAL "pop_bg.pal"',
"", "",
"#endif", "#endif",
@@ -265,7 +320,8 @@ def main():
| CLIMB_OVERLAY_ENV_IDS # + пол-оверлей подъёма | CLIMB_OVERLAY_ENV_IDS # + пол-оверлей подъёма
| LOOSE_ANIM_ENV_IDS # + дрожащие кадры loose | LOOSE_ANIM_ENV_IDS # + дрожащие кадры loose
| SPIKE_ANIM_ENV_IDS # + кадры выдвижения пик | 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) 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] env_bg = [i for i in env_ids if i not in FORE_ENV_IDS]
@@ -302,6 +358,22 @@ def main():
total_bytes += sz total_bytes += sz
print(f" pop_fore.atl: {len(env_fore)} спрайтов, каталог {cnt}, {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")) build_palette(os.path.join(OUT_DIR, "pop_bg.pal"))
write_header(os.path.join(OUT_DIR, "pop_bg_atlas.h"), write_header(os.path.join(OUT_DIR, "pop_bg_atlas.h"),
env_page_names, "pop_wall.atl", "pop_fore.atl") env_page_names, "pop_wall.atl", "pop_fore.atl")
+8 -1
View File
@@ -69,14 +69,21 @@ def _read16(path):
def build_palette(out_path): def build_palette(out_path):
"""ЕДИНАЯ игровая палитра (gfx_pal_fload пишет с индекса 0, перезатирая """ЕДИНАЯ игровая палитра (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) pal = bytearray(256 * 4)
groups = [ groups = [
(0x50, os.path.join(B.R.VDUNGEON_DIR, "res200.pal")), # env (0x50, os.path.join(B.R.VDUNGEON_DIR, "res200.pal")), # env
(0x60, os.path.join(B.R.VDUNGEON_DIR, "res360.pal")), # wall (0x60, os.path.join(B.R.VDUNGEON_DIR, "res360.pal")), # wall
(PAL_BASE, os.path.join(KID_DIR, "res400.pal")), # kid 0x70 (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 base, path in groups:
for i, (r, g, bl) in enumerate(_read16(path)): for i, (r, g, bl) in enumerate(_read16(path)):
o = (base + i) * 4 o = (base + i) * 4
+35
View File
@@ -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();
}
+7
View File
@@ -87,6 +87,13 @@ inline uint8_t gfx_get_sprite_clip(void) { return _gfx_sprite_clip; }
/* Блит целой картинки левым-верхним углом в (x,y). */ /* Блит целой картинки левым-верхним углом в (x,y). */
void gfx_blit(int x, int y, const void *img); 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) картинки (атлас кадров: /* Блит под-прямоугольника (sx,sy,w,h) картинки (атлас кадров:
* кадр N ленты — sx = N*FRAME_W). Под-прямоугольник обязан лежать * кадр N ленты — sx = N*FRAME_W). Под-прямоугольник обязан лежать
* внутри img; по экрану — клиппится. */ * внутри img; по экрану — клиппится. */