Оптимизация каскада плит: -27% работы за кадр (пять правок с замерами)

Сцена — ур.13 комната 23, шесть плит с потолка (docs/perf_l13_room23.md).
Все замеры сняты одним и тем же прогоном «ESC -> зонды -> roomtest».

                       было       стало
  работа за кадр    1 437 150   1 051 332   -27%
  зелёная (фон)       805 000     575 730   -28%
  циан (спрайты)      631 800     438 546   -31%
  синяя                142 830     142 830   в бюджете

Цена одной перерисовки:
  RDA_CEIL_GONE (запечь колодец)  251 335 -> 137 975   -45%
  RD_FLOOR (щебень на посадке)    198 805 -> 182 124    -8%
  RDA_CEIL (дрожащая плита)        48 785 ->  43 543   -11%

C1. Пометки «фон трогали» в mob_render ПОДАВЛЕНЫ (pop_cd_mute): коридор
куска уже помечен одним вызовом в pop_loose_mob_tick, а три блита метили
подмножества того же прямоугольника по 4 502 такта.  Плюс пометка в
mob_spawn_copy — кусок, рождённый внутри тика, свою мог не получить
(слот выдаётся с начала таблицы, то есть уже пройденный).  -55 тыс.

G2. Контекст тайла — file-scope, а не локали draw_tile (порт
load_curr_and_left_tile, seg008:0339).  В функции 57 вызовов, и каждое
живое через вызов значение спиливалось: 26 байт кадра и 513 обращений
`-N(ix)`.  Стало 33 обращения, кадра нет, банк 7 -703 Б.  -55 тыс.

G1. Окно клипа для точечной перерисовки (pop_t_win_set/clear).  В
pop_ceil_bake_empty куски ряда 0 высотой 63 px рисовались целиком, хотя
восстановить надо девять строк: блит стоил 21 447, стал 7 619.  -90 тыс.
В pop_floor_bake окно попробовано и ОТКАЧЕНО (там нет клипа, блиты шли
быстрым путём, а окно уводило их в blit_b_clip и не отсеивало ничего:
187 266 -> 198 279) — вернуться после G3, запись в коде.

G3. blit_b_clip: байтовый габарит + file-scope вместо локалей.  Одного
байтового габарита НЕ ХВАТИЛО (кадр остался, 211 -> 173 обращений) —
значений, живых через шесть вызовов ядер libbgi, больше, чем регистров.
С file-scope: 51 обращение, кадр 22 -> 12 Б.  Клипованный блит подешевел.

C4. Падающий кусок КЛИПУЕТСЯ сам, вместо чистки бортов после.  Раньше он
рисовался в борт целиком и взводил border_dirty, а pop_room_clip_borders
стирал две полосы 320x28 — 150 978 тактов в каждом кадре, пока хоть один
кусок торчит выше поля (гряда 13-го рождается ровно у потолка, y=2), то
есть почти весь каскад.  Стало 1 722.  -138 тыс.

Заодно blit_b_oversize больше не ходит через blit_b_clip (у того габарит
теперь байтовый) — рисует напрямую gfx_blit_part.  Это путь под
полноэкранные подложки интро/финала (320x200, docs/perf_l13_room23.md §5).

Проверено: 8 наборов tests-host зелёные; в MAME каскад рисуется корректно
(кадр с летящими плитами, чистые борта, итоговая картинка как до правок).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 13:39:57 +03:00
parent babd40bc84
commit a3c473d910
4 changed files with 376 additions and 171 deletions
+14
View File
@@ -137,6 +137,20 @@ extern uint8_t pop_cd_batch;
void pop_cd_batch_begin(void); void pop_cd_batch_begin(void);
void pop_cd_batch_end(void); void pop_cd_batch_end(void);
/* ПОДАВИТЬ пометки на время серии блитов — для того, кто пометил область САМ
* и заведомо шире, чем нарисует (падающий кусок: pop_loose_mob_tick помечает
* весь коридор heal'а одним вызовом, а спрайт куска — три блита внутри него).
* Дешевле пакета: pop_cd_touch выходит второй проверкой, не считая ни колонок,
* ни рядов. Замер 2026-08-17: настоящий вызов pop_cd_touch = 4 502 такта,
* 28% всей цены блита.
*
* ОПАСНО применять без разбора: пропущенная пометка = персонаж, которого
* стёрли, но не попросили перерисоваться, то есть исчезнувший со экрана
* персонаж (ровно этот баг ловили 2026-08-13 в комнате 23). Ставить только
* там, где покрытие доказано. */
void pop_cd_mute(void);
void pop_cd_unmute(void);
/* Кого в этом кадре можно НЕ перерисовывать: бит 0 — Кид, бит 1 — соперник. /* Кого в этом кадре можно НЕ перерисовывать: бит 0 — Кид, бит 1 — соперник.
* Звать ДВАЖДЫ: перед heal (стирать нечего) и перед отрисовкой (тик мог * Звать ДВАЖДЫ: перед heal (стирать нечего) и перед отрисовкой (тик мог
* персонажа всё-таки сдвинуть — тогда бит снимется сам, а pop_char_draw * персонажа всё-таки сдвинуть — тогда бит снимется сам, а pop_char_draw
+210 -139
View File
@@ -255,47 +255,64 @@ static void draw_leveldoor(uint8_t modif, int xh, int dmy)
} }
/* ---- draw_tile (seg008.c:155) — одна клетка ------------------------ */ /* ---- draw_tile (seg008.c:155) — одна клетка ------------------------ */
/* ---- КОНТЕКСТ ТАЙЛА (порт load_curr_and_left_tile, seg008:0339) -------
* Не локали draw_tile, а file-scope переменные — ровно как curr_tile /
* curr_modifier / draw_xh / draw_main_y / draw_bottom_y у оригинала.
*
* Зачем: в draw_tile 57 вызовов, и КАЖДОЕ значение, живое через вызов, SDCC
* спиливал в стековый кадр — получались 26 байт кадра и 513 обращений
* `-N(ix)` (замер 2026-08-17: 23 700 тактов накладных на вызов при одном
* блите на тайл, docs/perf_green_phase.md §1.6). У file-scope переменных
* абсолютная адресация (`ld a,(nn)` — 13 тактов против 19) и спиливать
* нечего. Функция от этого не становится реентерабельной, но и не была:
* draw_tile ниоткуда себя не зовёт. */
static int dt_row, dt_col;
static uint8_t dt_code, dt_lcode, dt_lmod;
static int dt_xh, dt_x, dt_dby, dt_dmy;
static const piece *dt_t, *dt_lt;
static uint8_t dt_rbl, dt_rblm, dt_wmod, dt_base_id;
static int dt_base_yb;
static void draw_tile(int row, int col) static void draw_tile(int row, int col)
{ {
uint8_t code = pop_tile_code(row, col); dt_row = row; dt_col = col;
uint8_t lcode = pop_tile_code(row, col - 1); dt_code = pop_tile_code(dt_row, dt_col);
uint8_t lmod = pop_tile_mod(row, col - 1); dt_lcode = pop_tile_code(dt_row, dt_col - 1);
dt_lmod = pop_tile_mod(dt_row, dt_col - 1);
/* Нажатая кнопка (seg008 get_tile_to_draw): при таймере связи >1 opener /* Нажатая кнопка (seg008 get_tile_to_draw): при таймере связи >1 opener
* (0x0F) рисуется как ПОЛ (modif 0), closer (0x06) — как stuck (0x05). * (0x0F) рисуется как ПОЛ (modif 0), closer (0x06) — как stuck (0x05).
* Модификатор кнопки = индекс LINKLOC. И для самого тайла, и для левого * Модификатор кнопки = индекс LINKLOC. И для самого тайла, и для левого
* соседа (его правая грань). */ * соседа (его правая грань). */
if (code == 0x0F) { if ((pop_doorlink2(pop_tile_mod(row, col)) & 0x1F) > 1) code = 1; } if (dt_code == 0x0F) { if ((pop_doorlink2(pop_tile_mod(dt_row, dt_col)) & 0x1F) > 1) dt_code = 1; }
else if (code == 0x06) { if ((pop_doorlink2(pop_tile_mod(row, col)) & 0x1F) > 1) code = 5; } else if (dt_code == 0x06) { if ((pop_doorlink2(pop_tile_mod(dt_row, dt_col)) & 0x1F) > 1) dt_code = 5; }
if (lcode == 0x0F) { if ((pop_doorlink2(lmod) & 0x1F) > 1) { lcode = 1; lmod = 0; } } if (dt_lcode == 0x0F) { if ((pop_doorlink2(dt_lmod) & 0x1F) > 1) { dt_lcode = 1; dt_lmod = 0; } }
else if (lcode == 0x06) { if ((pop_doorlink2(lmod) & 0x1F) > 1) lcode = 5; } else if (dt_lcode == 0x06) { if ((pop_doorlink2(dt_lmod) & 0x1F) > 1) dt_lcode = 5; }
int xh = POP_COL_XH[col]; dt_xh = POP_COL_XH[dt_col];
int x = xh * 8; dt_x = dt_xh * 8;
int dby = 63 * row + 65; dt_dby = 63 * dt_row + 65;
int dmy = dby - 3; dt_dmy = dt_dby - 3;
const piece *t = &pop_tile_table[code]; dt_t = &pop_tile_table[dt_code];
const piece *lt = &pop_tile_table[lcode]; dt_lt = &pop_tile_table[dt_lcode];
uint8_t rbl_code, rbl_mod;
uint8_t wmod;
/* тайл снизу-слева (для topright). Ниже нижнего ряда — берём ряд 0 /* тайл снизу-слева (для topright). Ниже нижнего ряда — берём ряд 0
* комнаты снизу (load_rowbelow, seg008:368): row_below_left_[col] = * комнаты снизу (load_rowbelow, seg008:368): row_below_left_[dt_col] =
* room_B(0, col-1); col-1<0 → левая комната снизу = стена уровня. */ * room_B(0, dt_col-1); dt_col-1<0 → левая комната снизу = стена уровня. */
if (row + 1 > 2) { if (dt_row + 1 > 2) {
int lc = col - 1; int lc = dt_col - 1;
/* lc < 0 — угол (2,0): сосед снизу-слева лежит в ЧУЖОЙ комнате /* lc < 0 — угол (2,0): сосед снизу-слева лежит в ЧУЖОЙ комнате
* (room_BL). Её тайл (0,9) приходит одиннадцатым байтом below-ряда; * (room_BL). Её тайл (0,9) приходит одиннадцатым байтом below-ряда;
* «стена» там стоит, только если такой комнаты нет — как в * «стена» там стоит, только если такой комнаты нет — как в
* load_rowbelow (seg008:368). См. BUG-SEAM-WEDGE-1. */ * load_rowbelow (seg008:368). См. BUG-SEAM-WEDGE-1. */
if (lc < 0) rbl_code = pop_t_below ? (uint8_t)(pop_t_below[10] & 0x1F) : 20; if (lc < 0) dt_rbl = pop_t_below ? (uint8_t)(pop_t_below[10] & 0x1F) : 20;
else rbl_code = pop_t_below ? (uint8_t)(pop_t_below[lc] & 0x1F) : 0; else dt_rbl = pop_t_below ? (uint8_t)(pop_t_below[lc] & 0x1F) : 0;
/* Модификатора ряда снизу у нас нет (pop_t_below — только fg), а он /* Модификатора ряда снизу у нас нет (pop_t_below — только fg), а он
* нужен паласной панели над воротами. Ставим 0 = «панели нет»: * нужен паласной панели над воротами. Ставим 0 = «панели нет»:
* случай узкий (ворота в комнате СНИЗУ, по диагонали от пустого * случай узкий (ворота в комнате СНИЗУ, по диагонали от пустого
* тайла нижнего ряда), но расхождение с оригиналом честное. */ * тайла нижнего ряда), но расхождение с оригиналом честное. */
rbl_mod = 0; dt_rblm = 0;
} else { } else {
rbl_code = pop_tile_code(row + 1, col - 1); dt_rbl = pop_tile_code(dt_row + 1, dt_col - 1);
rbl_mod = pop_tile_mod(row + 1, col - 1); dt_rblm = pop_tile_mod(dt_row + 1, dt_col - 1);
} }
/* Пометка «фон трогали» — ОДНА на весь тайл, а не на каждый его кусок /* Пометка «фон трогали» — ОДНА на весь тайл, а не на каждый его кусок
@@ -306,72 +323,72 @@ static void draw_tile(int row, int col)
/* --- draw_tile_anim_topright (seg008:0568): верх ВОРОТ снизу-слева. /* --- draw_tile_anim_topright (seg008:0568): верх ВОРОТ снизу-слева.
* Условие дословно: свой тайл пуст / верх большой колонны / верх двери, * Условие дословно: свой тайл пуст / верх большой колонны / верх двери,
* а снизу-слева — ворота. --- */ * а снизу-слева — ворота. --- */
if ((code == 0 || code == 9 || code == 12) && rbl_code == 4) { if ((dt_code == 0 || dt_code == 9 || dt_code == 12) && dt_rbl == 4) {
uint8_t om = (rbl_mod > 188) ? 188 : rbl_mod; uint8_t om = (dt_rblm > 188) ? 188 : dt_rblm;
pop_env_b(GATE_TOP_MASK, x, dby); /* маска (mono, чёрным) */ pop_env_b(GATE_TOP_MASK, dt_x, dt_dby); /* маска (mono, чёрным) */
pop_env_b(DOOR_FRAM_TOP[(om >> 2) & 7], x, dby); pop_env_b(DOOR_FRAM_TOP[(om >> 2) & 7], dt_x, dt_dby);
} }
/* --- floorright (seg008.c:392) — только can_see_bottomleft --- */ /* --- floorright (seg008.c:392) — только can_see_bottomleft --- */
if (code == 0 || code == 9 || code == 12 || code == 26) { if (dt_code == 0 || dt_code == 9 || dt_code == 12 || dt_code == 26) {
draw_topright(rbl_code, rbl_mod, x, dby); draw_topright(dt_rbl, dt_rblm, dt_x, dt_dby);
/* floor-right тень (blitters_9_black) на ЧЁРНОМ фоне = no-op /* floor-right тень (blitters_9_black) на ЧЁРНОМ фоне = no-op
* (чёрный силуэт по чёрному) — не рисуем. Если tile_left имеет * (чёрный силуэт по чёрному) — не рисуем. Если tile_left имеет
* floor_right: lt->floor_right. */ * floor_right: dt_lt->floor_right. */
} }
/* --- draw_tile_right (seg008.c:456) --- */ /* --- draw_tile_right (seg008.c:456) --- */
if (code != 20) { if (dt_code != 20) {
if (lcode == 20) { if (dt_lcode == 20) {
/* Полоска на стене (seg008:510) — только палас и только если у /* Полоска на стене (seg008:510) — только палас и только если у
* стены не взведён старший бит модификатора. */ * стены не взведён старший бит модификатора. */
if (PALACE && (lmod & 0x80) == 0) if (PALACE && (dt_lmod & 0x80) == 0)
pop_env_b(84, x + 3 * 8, dmy - 27); pop_env_b(84, dt_x + 3 * 8, dt_dmy - 27);
pop_wall_b(1, x, pop_tile_table[20].right_y + dmy); /* грань стены */ pop_wall_b(1, dt_x, pop_tile_table[20].right_y + dt_dmy); /* грань стены */
} else if (lcode == 0) { } else if (dt_lcode == 0) {
if (lmod <= 3 && BLUELINE_FRAM1[lmod]) if (dt_lmod <= 3 && BLUELINE_FRAM1[dt_lmod])
pop_env_b(BLUELINE_FRAM1[lmod], x, BLUELINE_FRAM_Y[lmod] + dmy); pop_env_b(BLUELINE_FRAM1[dt_lmod], dt_x, BLUELINE_FRAM_Y[dt_lmod] + dt_dmy);
} else if (lcode == 1) { } else if (dt_lcode == 1) {
uint8_t num; uint8_t num;
pop_env_b(42, x, pop_tile_table[1].right_y + dmy); /* правый треуг. пола */ pop_env_b(42, dt_x, pop_tile_table[1].right_y + dt_dmy); /* правый треуг. пола */
num = (lmod <= 3) ? lmod : 0; num = (dt_lmod <= 3) ? dt_lmod : 0;
/* seg008:501 — `if (num == !!level_type) return;`: в подземелье /* seg008:501 — `if (num == !!level_type) return;`: в подземелье
* пропускается num==0, в паласе num==1. */ * пропускается num==0, в паласе num==1. */
if (num != (PALACE ? 1 : 0)) if (num != (PALACE ? 1 : 0))
pop_env_b(BLUELINE_FRAM3[num], x, dmy - 20); /* силуэт кладки */ pop_env_b(BLUELINE_FRAM3[num], dt_x, dt_dmy - 20); /* силуэт кладки */
} else if (lcode == 7 || lcode == 12) { } else if (dt_lcode == 7 || dt_lcode == 12) {
/* Низ декоративной панели над воротами (seg008:506) — только /* Низ декоративной панели над воротами (seg008:506) — только
* палас; в подземелье у doortop right_id нулевой и ветка пустая. */ * палас; в подземелье у doortop right_id нулевой и ветка пустая. */
if (PALACE && DOORTOP_FRAM_BOT[lmod & 3]) if (PALACE && DOORTOP_FRAM_BOT[dt_lmod & 3])
pop_env_b(DOORTOP_FRAM_BOT[lmod & 3], x, lt->right_y + dmy); pop_env_b(DOORTOP_FRAM_BOT[dt_lmod & 3], dt_x, dt_lt->right_y + dt_dmy);
} else { } else {
if (lt->right_id) if (dt_lt->right_id)
pop_env_b(lt->right_id, x, lt->right_y + dmy); pop_env_b(dt_lt->right_id, dt_x, dt_lt->right_y + dt_dmy);
/* Орнаментная лента соседа (seg008:486) — только палас. Она и /* Орнаментная лента соседа (seg008:486) — только палас. Она и
* даёт непрерывную полосу под окнами: stripe_id = 145 у пола, * даёт непрерывную полосу под окнами: stripe_id = 145 у пола,
* кнопок, зелья, loose, чомпера и меча. */ * кнопок, зелья, loose, чомпера и меча. */
if (PALACE && lt->stripe_id) if (PALACE && dt_lt->stripe_id)
pop_env_b(lt->stripe_id, x, dmy - 27); pop_env_b(dt_lt->stripe_id, dt_x, dt_dmy - 27);
/* Основание факела (seg008:489): рисует ПРАВЫЙ сосед. */ /* Основание факела (seg008:489): рисует ПРАВЫЙ сосед. */
if (lcode == 19 || lcode == 30) if (dt_lcode == 19 || dt_lcode == 30)
pop_env_b(146, x, dby - 28); pop_env_b(146, dt_x, dt_dby - 28);
} }
} }
/* --- draw_tile_anim_right (seg008.c:530) — статичные части. Для /* --- draw_tile_anim_right (seg008.c:530) — статичные части. Для
* верхней полосы (row<0, draw_tile_aboveroom) НЕ рисуется + защита от * верхней полосы (dt_row<0, draw_tile_aboveroom) НЕ рисуется + защита от
* отрицательного индекса в pop_loose_modif. --- */ * отрицательного индекса в pop_loose_modif. --- */
if (row >= 0) { if (dt_row >= 0) {
if (lcode == 4) { /* gate: решётка */ if (dt_lcode == 4) { /* gate: решётка */
pop_gate_back_b(lmod, xh, dby, dmy); pop_gate_back_b(dt_lmod, dt_xh, dt_dby, dt_dmy);
} else if (lcode == 11) { /* loose: правая грань (анимир. по modif соседа-loose) */ } else if (dt_lcode == 11) { /* loose: правая грань (анимир. по modif соседа-loose) */
pop_env_b(LOOSE_FRAM_RIGHT[pop_loose_frame(pop_loose_modif[row * 10 + (col - 1)])], x, dby - 1); pop_env_b(LOOSE_FRAM_RIGHT[pop_loose_frame(pop_loose_modif[dt_row * 10 + (dt_col - 1)])], dt_x, dt_dby - 1);
} else if (lcode == 2) { /* spike: статич. кадр */ } else if (dt_lcode == 2) { /* spike: статич. кадр */
uint8_t sf = (lmod & 0x80) ? 5 : lmod; uint8_t sf = (dt_lmod & 0x80) ? 5 : dt_lmod;
if (sf < 10 && SPIKES_FRAM_RIGHT[sf]) if (sf < 10 && SPIKES_FRAM_RIGHT[sf])
pop_env_b(SPIKES_FRAM_RIGHT[sf], x, dmy - 7); pop_env_b(SPIKES_FRAM_RIGHT[sf], dt_x, dt_dmy - 7);
} else if (lcode == 16) { /* дверь уровня */ } else if (dt_lcode == 16) { /* дверь уровня */
draw_leveldoor(lmod, xh, dmy); draw_leveldoor(dt_lmod, dt_xh, dt_dmy);
} }
/* Пламя факела (seg008:560, chtab_1 id modifier+1) здесь, в статичной /* Пламя факела (seg008:560, chtab_1 id modifier+1) здесь, в статичной
* отрисовке тайла, не рисуем — его ведёт pop_torch_draw из * отрисовке тайла, не рисуем — его ведёт pop_torch_draw из
@@ -381,101 +398,101 @@ static void draw_tile(int row, int col)
} }
/* --- draw_tile_bottom (seg008.c:570) --- */ /* --- draw_tile_bottom (seg008.c:570) --- */
if (code == 20) { if (dt_code == 20) {
wmod = pop_wall_modifier(row, col); dt_wmod = pop_wall_modifier(dt_row, dt_col);
if (!PALACE) /* во дворце тело стены даёт */ if (!PALACE) /* во дворце тело стены даёт */
pop_wall_b(POP_WALL_FRAM_BOTTOM[wmod], x, dby); /* wall_pattern */ pop_wall_b(POP_WALL_FRAM_BOTTOM[dt_wmod], dt_x, dt_dby); /* wall_pattern */
/* заливками (seg008:576) */ /* заливками (seg008:576) */
pop_wall_pattern_b(row, col, 0); pop_wall_pattern_b(dt_row, dt_col, 0);
} else if (t->bottom_id) { } else if (dt_t->bottom_id) {
pop_env_b(t->bottom_id, x, dby); pop_env_b(dt_t->bottom_id, dt_x, dt_dby);
} }
/* --- draw_loose (seg008.c:600) — нижняя грань (анимир. по modif). В /* --- draw_loose (seg008.c:600) — нижняя грань (анимир. по modif). В
* верхней полосе (row<0) это ПЛИТА-ПОТОЛОК (loose ряда 2 комнаты сверху): * верхней полосе (dt_row<0) это ПЛИТА-ПОТОЛОК (loose ряда 2 комнаты сверху):
* её состояние живёт отдельно, в pop_ceil_modif[col] (pop_map). --- */ * её состояние живёт отдельно, в pop_ceil_modif[dt_col] (pop_map). --- */
if (code == 11) if (dt_code == 11)
pop_env_b(POP_LOOSE_FRAM_BOTTOM[pop_loose_frame(row >= 0 ? pop_loose_modif[row * 10 + col] pop_env_b(POP_LOOSE_FRAM_BOTTOM[pop_loose_frame(dt_row >= 0 ? pop_loose_modif[dt_row * 10 + dt_col]
: pop_ceil_modif[col])], x, dby); : pop_ceil_modif[dt_col])], dt_x, dt_dby);
/* --- draw_tile_base (seg008.c:611). Верхняя полоса (row<0) базу НЕ /* --- draw_tile_base (seg008.c:611). Верхняя полоса (dt_row<0) базу НЕ
* рисует (draw_tile_aboveroom) — только нижние/правые грани. --- */ * рисует (draw_tile_aboveroom) — только нижние/правые грани. --- */
if (row >= 0) { if (dt_row >= 0) {
uint8_t base_id = t->base_id; dt_base_id = dt_t->base_id;
int base_yb = dmy; dt_base_yb = dt_dmy;
if (lcode == 26 && code == 12) { if (dt_lcode == 26 && dt_code == 12) {
/* «Lattice + door A» (seg008:622): у doortop своей базы нет /* «Lattice + door A» (seg008:622): у doortop своей базы нет
* (base_id 0), и когда слева от него стоит арка (lattice_down), * (dt_base_id 0), и когда слева от него стоит арка (lattice_down),
* пара рисуется ОДНИМ спрайтом 6, опущенным на 3 px. Без этой * пара рисуется ОДНИМ спрайтом 6, опущенным на 3 px. Без этой
* ветки на месте тайла оставалась чёрная дыра — уровень 5, * ветки на месте тайла оставалась чёрная дыра — уровень 5,
* комната 12, тайл (0,9) (BUG-LATTICE-DOORTOP). */ * комната 12, тайл (0,9) (BUG-LATTICE-DOORTOP). */
base_id = 6; dt_base_id = 6;
base_yb += 3; dt_base_yb += 3;
} }
else if (code == 11) /* loose: левая грань (анимир.) */ else if (dt_code == 11) /* loose: левая грань (анимир.) */
base_id = POP_LOOSE_FRAM_LEFT[pop_loose_frame(pop_loose_modif[row * 10 + col])]; dt_base_id = POP_LOOSE_FRAM_LEFT[pop_loose_frame(pop_loose_modif[dt_row * 10 + dt_col])];
else if (code == 0x0F && lcode == 0 && !PALACE) /* opener, слева пусто */ else if (dt_code == 0x0F && dt_lcode == 0 && !PALACE) /* opener, слева пусто */
base_id = 148; /* левая половина без пола — только */ dt_base_id = 148; /* левая половина без пола — только */
/* подземелье (seg008:628) */ /* подземелье (seg008:628) */
if (base_id) if (dt_base_id)
pop_env_b(base_id, x, t->base_y + base_yb); pop_env_b(dt_base_id, dt_x, dt_t->base_y + dt_base_yb);
} }
/* --- draw_tile_anim (seg008.c:0B2B) — левый кадр выдвинутых пик в /* --- draw_tile_anim (seg008.c:0B2B) — левый кадр выдвинутых пик в
* СВОЕЙ ячейке (анимир. по своему modif). Без него остриё пики * СВОЕЙ ячейке (анимир. по своему modif). Без него остриё пики
* рисуется только правым соседом со сдвигом → шахта над самой пикой * рисуется только правым соседом со сдвигом → шахта над самой пикой
* пустая. */ * пустая. */
if (row >= 0 && code == 2) { if (dt_row >= 0 && dt_code == 2) {
uint8_t sm = pop_tile_mod(row, col); uint8_t sm = pop_tile_mod(dt_row, dt_col);
uint8_t sf = (sm & 0x80) ? 5 : sm; uint8_t sf = (sm & 0x80) ? 5 : sm;
if (sf > 0 && sf < 10 && POP_SPIKES_FRAM_LEFT[sf]) if (sf > 0 && sf < 10 && POP_SPIKES_FRAM_LEFT[sf])
pop_env_b(POP_SPIKES_FRAM_LEFT[sf], x, dmy - 2); pop_env_b(POP_SPIKES_FRAM_LEFT[sf], dt_x, dt_dmy - 2);
} }
/* Чомпер (тот же draw_tile_anim, case tiles_18_chomper): нижняя челюсть /* Чомпер (тот же draw_tile_anim, case tiles_18_chomper): нижняя челюсть
* на draw_main_y, поверх неё кровь (если кого-то перемололо), затем * на draw_main_y, поверх неё кровь (если кого-то перемололо), затем
* верхняя челюсть со своим подъёмом. Всё это BACKtable — то есть ПОД * верхняя челюсть со своим подъёмом. Всё это BACKtable — то есть ПОД
* персонажем; передний слой рисует fore-проход. */ * персонажем; передний слой рисует fore-проход. */
if (row >= 0 && code == 0x12) { if (dt_row >= 0 && dt_code == 0x12) {
uint8_t cm = pop_tile_mod(row, col); uint8_t cm = pop_tile_mod(dt_row, dt_col);
uint8_t pose = pop_chomp_pose(cm); uint8_t pose = pop_chomp_pose(cm);
pop_env_b(CHOMP_FRAM_BOT[pose], x, dmy); pop_env_b(CHOMP_FRAM_BOT[pose], dt_x, dt_dmy);
if (cm & 0x80) if (cm & 0x80)
pop_env_b((uint8_t)(pose + 114), x + 8, dmy - 6); pop_env_b((uint8_t)(pose + 114), dt_x + 8, dt_dmy - 6);
if (CHOMP_FRAM_TOP[pose]) if (CHOMP_FRAM_TOP[pose])
pop_env_b(CHOMP_FRAM_TOP[pose], x, dmy - CHOMP_FRAM_Y[pose]); pop_env_b(CHOMP_FRAM_TOP[pose], dt_x, dt_dmy - CHOMP_FRAM_Y[pose]);
} }
/* Меч-предмет на полу (тот же draw_tile_anim, case tiles_22_sword): /* Меч-предмет на полу (тот же draw_tile_anim, case tiles_22_sword):
* chtab_1 (наш pot-атлас), id = (modif==1)+10, низ на draw_main_y-3. * chtab_1 (наш pot-атлас), id = (modif==1)+10, низ на draw_main_y-3.
* В оригинале это midtable (рисуется вместе с объектами) — у нас статикой * В оригинале это midtable (рисуется вместе с объектами) — у нас статикой
* в фоне: пока меч лежит, он не анимируется, а Kid и так поверх фона. */ * в фоне: пока меч лежит, он не анимируется, а Kid и так поверх фона. */
if (row >= 0 && code == 22) if (dt_row >= 0 && dt_code == 22)
pop_pot_b((uint8_t)((pop_tile_mod(row, col) == 1) + 10), x, dmy - 3); pop_pot_b((uint8_t)((pop_tile_mod(dt_row, dt_col) == 1) + 10), dt_x, dt_dmy - 3);
/* --- draw_tile_fore (seg008.c:690) --- */ /* --- draw_tile_fore (seg008.c:690) --- */
if (code == 20) { if (dt_code == 20) {
wmod = pop_wall_modifier(row, col); dt_wmod = pop_wall_modifier(dt_row, dt_col);
if (!PALACE) /* seg008:711 */ if (!PALACE) /* seg008:711 */
pop_wall_b(POP_WALL_FRAM_MAIN[wmod], x, dmy); /* основная грань */ pop_wall_b(POP_WALL_FRAM_MAIN[dt_wmod], dt_x, dt_dmy); /* основная грань */
pop_wall_pattern_b(row, col, 1); pop_wall_pattern_b(dt_row, dt_col, 1);
} }
if (code == 0x12) { /* чомпер: передние зубья + кровь. if (dt_code == 0x12) { /* чомпер: передние зубья + кровь.
* Отдельной веткой, как в оригинале: * Отдельной веткой, как в оригинале:
* у записи 0x12 fore_id == 0, и через * у записи 0x12 fore_id == 0, и через
* таблицу этот слой не идёт. */ * таблицу этот слой не идёт. */
uint8_t cm = pop_tile_mod(row, col); uint8_t cm = pop_tile_mod(dt_row, dt_col);
uint8_t pose = pop_chomp_pose(cm); uint8_t pose = pop_chomp_pose(cm);
/* Передние куски чомпера и его кровь лежат в pop_fore.atl /* Передние куски чомпера и его кровь лежат в pop_fore.atl
* (FORE_ENV_IDS в pop_pack_bg.py), а НЕ в env-страницах: блит * (FORE_ENV_IDS в pop_pack_bg.py), а НЕ в env-страницах: блит
* обязан быть pop_fore_b, иначе id уходит в чужую страницу. */ * обязан быть pop_fore_b, иначе id уходит в чужую страницу. */
pop_fore_b(POP_CHOMP_FRAM_FOR[pose], x, dmy); pop_fore_b(POP_CHOMP_FRAM_FOR[pose], dt_x, dt_dmy);
if (cm & 0x80) if (cm & 0x80)
pop_fore_b((uint8_t)(pose + 119), x + 8, dmy - 6); pop_fore_b((uint8_t)(pose + 119), dt_x + 8, dt_dmy - 6);
} }
if (code == 10) { /* зелье: chtab_1, не env fore_id. if (dt_code == 10) { /* зелье: chtab_1, не env fore_id.
* Пузырёк — динамика (pop_potion_draw). */ * Пузырёк — динамика (pop_potion_draw). */
pop_potion_flask(row, col, pop_tile_mod(row, col)); pop_potion_flask(dt_row, dt_col, pop_tile_mod(dt_row, dt_col));
} else if (t->fore_id) { } else if (dt_t->fore_id) {
pop_fore_b(t->fore_id, t->fore_x * 8 + x, t->fore_y + dmy); pop_fore_b(dt_t->fore_id, dt_t->fore_x * 8 + dt_x, dt_t->fore_y + dt_dmy);
} }
pop_cd_batch_end(); /* одна пометка на весь тайл */ pop_cd_batch_end(); /* одна пометка на весь тайл */
} }
@@ -595,9 +612,15 @@ void pop_ceil_shake_draw(int col) __banked
pop_dbg_m13(); /* ЗАМЕР: вход pop_ceil_shake_draw */ pop_dbg_m13(); /* ЗАМЕР: вход pop_ceil_shake_draw */
pop_heal_off(x, 0, 64, CEIL_BAND_H); pop_heal_off(x, 0, 64, CEIL_BAND_H);
pop_dbg_m14(); /* ЗАМЕР: heal сделан */ pop_dbg_m14(); /* ЗАМЕР: heal сделан */
/* Пометка «фон трогали» — ОДНА на восстановленный прямоугольник: пока
* стоит окно (ниже), pop_blit_b пометок не ставит. Область та же, что у
* heal'а, — он её и стирает. */
pop_cd_touch(x, POP_YOFF, 64, CEIL_BAND_H);
gfx_set_bank(GFX_BANK_SPRITE); gfx_set_bank(GFX_BANK_SPRITE);
pop_t_clip_top = POP_YOFF; pop_t_clip_top = POP_YOFF;
pop_t_win_set(x, POP_YOFF, 64, CEIL_BAND_H); /* восстанавливаем ровно полосу */
draw_tile(-1, col); draw_tile(-1, col);
pop_t_win_clear();
pop_t_clip_top = 0; pop_t_clip_top = 0;
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
pop_dbg_m15(); /* ЗАМЕР: draw_tile сделан */ pop_dbg_m15(); /* ЗАМЕР: draw_tile сделан */
@@ -606,11 +629,17 @@ void pop_ceil_shake_draw(int col) __banked
void pop_ceil_bake_empty(int col) __banked void pop_ceil_bake_empty(int col) __banked
{ {
int x = POP_COL_XH[col] * 8; int x = POP_COL_XH[col] * 8;
pop_cd_touch(x, POP_YOFF, 64, CEIL_BAND_H + 1); /* bar мимо pop_blit_b */
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
/* Отдельного pop_cd_touch тут не надо: pop_bar_black метит сам, и ровно
* этот же прямоугольник (было два вызова на одну область). */
pop_bar_black(x, POP_YOFF, 64, CEIL_BAND_H + 1); /* стереть плиту в ОЗУ-копии */ pop_bar_black(x, POP_YOFF, 64, CEIL_BAND_H + 1); /* стереть плиту в ОЗУ-копии */
gfx_set_bank(GFX_BANK_TRANSPARENT); gfx_set_bank(GFX_BANK_TRANSPARENT);
pop_t_clip_top = POP_YOFF; pop_t_clip_top = POP_YOFF;
/* Окно = ровно стёртая полоса. Главный выигрыш здесь не в отсеве, а в
* КЛИПЕ: куски ряда 0 высотой 63 px рисовались целиком (клип был только
* сверху, по pop_t_clip_top), хотя восстановить надо девять строк —
* отсюда 21 447 такта на блит против 14 384 в pop_floor_bake. */
pop_t_win_set(x, POP_YOFF, 64, CEIL_BAND_H + 1);
pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */ pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */
/* Восстанавливаем ВСЁ, чья графика попадает в стёртую полосу, а не только /* Восстанавливаем ВСЁ, чья графика попадает в стёртую полосу, а не только
* два тайла ряда 1 (BUG-LOOSE-3). Куски тайла рисуются ВВЕРХ от своей * два тайла ряда 1 (BUG-LOOSE-3). Куски тайла рисуются ВВЕРХ от своей
@@ -629,6 +658,7 @@ void pop_ceil_bake_empty(int col) __banked
for (c = c0; c <= c1; c++) draw_tile(0, c); for (c = c0; c <= c1; c++) draw_tile(0, c);
} }
pop_t_bake_rest = 0; pop_t_bake_rest = 0;
pop_t_win_clear();
pop_t_clip_top = 0; pop_t_clip_top = 0;
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
} }
@@ -734,14 +764,22 @@ void pop_torch_wipe(int row, int col) __banked
void pop_floor_bake(int row, int col) __banked void pop_floor_bake(int row, int col) __banked
{ {
int x = POP_COL_XH[col] * 8; int x = POP_COL_XH[col] * 8;
pop_cd_touch(x, POP_YOFF + 63 * row + 26, 60, 39); /* bar мимо pop_blit_b */
int yb = POP_YOFF + 63 * row; int yb = POP_YOFF + 63 * row;
/* Пометку ставит сам pop_bar_black — по тому же прямоугольнику; отдельный
* pop_cd_touch тут был вторым вызовом на одну область (4 502 такта). */
/* Wipe СВОЕЙ ячейки + правой грани (заходит в соседа) + перерисовка пола. /* Wipe СВОЕЙ ячейки + правой грани (заходит в соседа) + перерисовка пола.
* НИЗ строго yb+64 (≈ низ передней грани пола): ниже — СТЕНА ряда ниже * НИЗ строго yb+64 (≈ низ передней грани пола): ниже — СТЕНА ряда ниже
* (высокий спрайт), которую тут не перерисовываем → иначе чёрный бар. */ * (высокий спрайт), которую тут не перерисовываем → иначе чёрный бар. */
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
pop_bar_black(x, yb + 26, 60, 39); pop_bar_black(x, yb + 26, 60, 39);
gfx_set_bank(GFX_BANK_TRANSPARENT); gfx_set_bank(GFX_BANK_TRANSPARENT);
/* ОКНА КЛИПА ЗДЕСЬ НЕТ ОСОЗНАННО (замер 2026-08-17). Оно попробовано и
* откачено: у этого тайла клипа нет вовсе, то есть блиты идут БЫСТРЫМ
* путём (gfx_blit_noclip, 10 422), а окно уводит их в blit_b_clip
* (14 088) — и не отсеивает ни одного куска (7,6 блита до и после).
* Итог был 187 266 -> 198 279, то есть хуже. Вернуться к этой идее
* после того, как blit_b_clip перестанет платить за 22-байтовый
* стековый кадр (позиция G3 в docs/perf_green_phase.md). */
pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */ pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */
draw_tile(row, col); draw_tile(row, col);
if (col + 1 < 10) draw_tile(row, col + 1); if (col + 1 < 10) draw_tile(row, col + 1);
@@ -886,6 +924,29 @@ static const int16_t MOB_Y_LAND[5] = {2, 65, 128, 191, 254};
static const int16_t MOB_Y_BOUND[5] = {-1, 62, 125, 188, 25}; static const int16_t MOB_Y_BOUND[5] = {-1, 62, 125, 188, 25};
#define MOB_Y_NONE 0x7FFF #define MOB_Y_NONE 0x7FFF
/* Габарит куска по X: left/bottom (41/43, w=32) с mob_x; right (42, w=26)
* с mob_x+32 → ПРАВЫЙ пиксель = mob_x+57 (замерено в pop_env1.atl, НЕ +46:
* ширина 42 = 26, не ~14). Коридор heal и клип ОБЯЗАНЫ покрыть ВСЮ ширину
* [mob_x-4 .. mob_x+57], иначе правая грань 42 остаётся хвостом на одной
* странице → тёмная тень на полу 2,7 + двоебуфер-мерцание. */
#define MOB_X0(mx) ((mx) - 4)
/* Коридор heal — по РЕАЛЬНОМУ следу спрайта, а не «с запасом».
* След: по x mob_x .. mob_x+58, по y mob_y-16 .. mob_y — это объединение
* трёх частей (41 = 32x13 с y-3, 42 = 26x15 с x+32,y-1, 43 = 32x3 с y).
*
* Ширина оставлена прежней (64 = mob_x-4 .. mob_x+60): нужных 62 плюс поля.
* А вот высота была 32 при нужных 16 — вдвое лишнего на самой дорогой
* операции кадра, помноженной на число падающих плит; сужена до 24.
*
* ПРО ЗАПАС ПО ШИРИНЕ: обоснования «выравнивание блока акселератора» я не
* подтвердил — ни pop_heal_fast, ни gfx_heal координаты не округляют, а в
* memory про акселератор таких требований нет. Единственный источник —
* наблюдение в pop_clip_sprite про 1-2 px слайверы правой грани. Сузить
* ширину можно, но ТОЛЬКО подтвердив прогоном, что слайверы не появятся. */
#define MOB_W 64 /* mob_x-4 .. mob_x+60 */
#define MOB_HEAL_UP 20 /* верх коридора = mob_y − 20 */
#define MOB_HEAL_H 24 /* .. mob_y + 4 */
/* Свободный слот = не летит И дочистка завершена (иначе затрём хвост, /* Свободный слот = не летит И дочистка завершена (иначе затрём хвост,
* который ещё не стёрт со второй страницы). Общий для обычного отрыва и * который ещё не стёрт со второй страницы). Общий для обычного отрыва и
* для копии из loose_fall — раньше цикл был написан дважды. */ * для копии из loose_fall — раньше цикл был написан дважды. */
@@ -949,6 +1010,13 @@ static void mob_spawn_copy(const mob_t *src)
m->clean = 0; m->clean = 0;
m->defer = 0; m->defer = 0;
m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE; m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE;
/* Кусок рождается ВНУТРИ mob_tick_one (loose_fall сбил плиту), и слот ему
* mob_alloc выдаёт с начала таблицы — то есть, скорее всего, УЖЕ
* пройденный циклом pop_loose_mob_tick. Своей пометки «фон трогали» этот
* кусок в текущем кадре не получит, а нарисован будет; помечаем коридор
* здесь — иначе персонаж под ним стирается и не просит перерисовки
* (см. pop_cd_mute в mob_render). */
pop_cd_touch(MOB_X0(m->x), m->y - 27 + POP_YOFF, MOB_W, 64);
} }
/* Отрыв куска в ЧУЖОЙ комнате: плита, которую Кид расшатал и убежал, всё /* Отрыв куска в ЧУЖОЙ комнате: плита, которую Кид расшатал и убежал, всё
@@ -960,28 +1028,6 @@ void pop_loose_mob_spawn_at(uint8_t room, int row, int col) __banked
mob_spawn(room, row, col); mob_spawn(room, row, col);
} }
/* Габарит куска по X: left/bottom (41/43, w=32) с mob_x; right (42, w=26)
* с mob_x+32 → ПРАВЫЙ пиксель = mob_x+57 (замерено в pop_env1.atl, НЕ +46:
* ширина 42 = 26, не ~14). Коридор heal и клип ОБЯЗАНЫ покрыть ВСЮ ширину
* [mob_x-4 .. mob_x+57], иначе правая грань 42 остаётся хвостом на одной
* странице → тёмная тень на полу 2,7 + двоебуфер-мерцание. */
#define MOB_X0(mx) ((mx) - 4)
/* Коридор heal — по РЕАЛЬНОМУ следу спрайта, а не «с запасом».
* След: по x mob_x .. mob_x+58, по y mob_y-16 .. mob_y — это объединение
* трёх частей (41 = 32x13 с y-3, 42 = 26x15 с x+32,y-1, 43 = 32x3 с y).
*
* Ширина оставлена прежней (64 = mob_x-4 .. mob_x+60): нужных 62 плюс поля.
* А вот высота была 32 при нужных 16 — вдвое лишнего на самой дорогой
* операции кадра, помноженной на число падающих плит; сужена до 24.
*
* ПРО ЗАПАС ПО ШИРИНЕ: обоснования «выравнивание блока акселератора» я не
* подтвердил — ни pop_heal_fast, ни gfx_heal координаты не округляют, а в
* memory про акселератор таких требований нет. Единственный источник —
* наблюдение в pop_clip_sprite про 1-2 px слайверы правой грани. Сузить
* ширину можно, но ТОЛЬКО подтвердив прогоном, что слайверы не появятся. */
#define MOB_W 64 /* mob_x-4 .. mob_x+60 */
#define MOB_HEAL_UP 20 /* верх коридора = mob_y − 20 */
#define MOB_HEAL_H 24 /* .. mob_y + 4 */
/* Остановить падающий кусок (при смене комнаты — состояние per-tile не /* Остановить падающий кусок (при смене комнаты — состояние per-tile не
* должно утечь в новую комнату). */ * должно утечь в новую комнату). */
@@ -1306,13 +1352,38 @@ static void mob_render(mob_t *m, uint8_t pg)
* Здесь стояли 41/43/42 — это индекс 0, то есть плита В ПОКОЕ. Отсюда и * Здесь стояли 41/43/42 — это индекс 0, то есть плита В ПОКОЕ. Отсюда и
* «цельная ровная плита» вместо двух частей со сдвигом правой половины * «цельная ровная плита» вместо двух частей со сдвигом правой половины
* на пиксель (замечено пользователем 2026-08-13 сравнением с оригиналом). */ * на пиксель (замечено пользователем 2026-08-13 сравнением с оригиналом). */
pop_env_b(74, m->x, m->y); /* Пометки «фон трогали» на время трёх блитов ПОДАВЛЕНЫ: коридор куска
pop_env_b(70, m->x, m->y - 3); * (MOB_X0 .. +MOB_W, y27 .. +37) уже помечен ОДНИМ вызовом — из
pop_env_b(72, m->x + 32, m->y - 1); * pop_loose_mob_tick для летящих кусков и из mob_spawn_copy для куска,
* рождённого внутри тика. Спрайт целиком внутри коридора: он занимает
* x m->x .. m->x+57 (правая часть 72 идёт с x+32, ширина 26) при
* коридоре m->x4 .. m->x+59, и y m->y15 .. m->y при коридоре
* m->y27 .. m->y+37 с запасом на ускорение (speed ≤ 31 за кадр).
* Замер 2026-08-17: три настоящих pop_cd_touch стоили 13 506 тактов на
* кусок, то есть 81 000 в кадре при шести кусках. */
pop_cd_mute();
/* ВЫХОД ЗА ПОЛЕ РЕЖЕМ САМИ, а не чистим борта после. Раньше здесь стоял
* pop_clip_sprite, то есть кусок рисовался в борт целиком, взводил
* border_dirty, и pop_room_clip_borders потом стирал ДВЕ полосы во всю
* ширину экрана — 320x28 и 320x28. Замер 2026-08-17: 150 978 тактов в
* КАЖДОМ кадре, пока хоть один кусок торчит выше поля, то есть почти весь
* каскад (гряда на 13-м рождается ровно у потолка, y = 2). Это была
* четверть цианового блока.
*
* Окно ставим только когда кусок реально задевает борт: внутри поля блиты
* идут быстрым путём (10 422 против 14 088). */
{
int ytop = m->y - 16 + POP_YOFF, ybot = m->y + POP_YOFF;
uint8_t clipped = (uint8_t)(ytop < POP_YOFF || ybot >= POP_CLIP_BOTTOM);
if (clipped) pop_t_win_set(0, POP_YOFF, 320, POP_PLAYFIELD_H);
pop_env_b(74, m->x, m->y);
pop_env_b(70, m->x, m->y - 3);
pop_env_b(72, m->x + 32, m->y - 1);
if (clipped) pop_t_win_clear();
}
pop_cd_unmute();
m->prev_y[pg] = m->y; m->prev_y[pg] = m->y;
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
/* клип СВОЕГО выхода за поле (верхний борт / статус-полоса) — по ВСЕЙ ширине */
pop_clip_sprite(MOB_X0(m->x), MOB_W, m->y - 26 + POP_YOFF, 30);
} }
/* Отрисовка кусков, идущих ПОД Kid. Звать ПОСЛЕ pop_redraw_needed(): фон /* Отрисовка кусков, идущих ПОД Kid. Звать ПОСЛЕ pop_redraw_needed(): фон
+133 -32
View File
@@ -246,11 +246,20 @@ void pop_cd_batch_end(void)
pop_cd_touch(cdb_x0, cdb_y0, cdb_x1 - cdb_x0 + 1, cdb_y1 - cdb_y0 + 1); pop_cd_touch(cdb_x0, cdb_y0, cdb_x1 - cdb_x0 + 1, cdb_y1 - cdb_y0 + 1);
} }
/* Подавление на серию блитов — см. шапку в pop_cdraw.h. Отдельным значением
* того же флага, а не вторым флагом: у pop_cd_touch тогда остаётся ОДНА
* проверка на общем пути. */
#define CD_BATCH_MUTE 2
void pop_cd_mute(void) { pop_cd_batch = CD_BATCH_MUTE; }
void pop_cd_unmute(void) { pop_cd_batch = 0; }
void pop_cd_touch(int x, int y, int w, int h) void pop_cd_touch(int x, int y, int w, int h)
{ {
uint16_t cols; uint16_t cols;
uint8_t r0, r1; uint8_t r0, r1;
if (pop_cd_batch) { /* копим, не разбирая на колонки/ряды */ if (pop_cd_batch) { /* копим, не разбирая на колонки/ряды */
if (pop_cd_batch == CD_BATCH_MUTE) return; /* область помечена вызывающим */
int x1 = x + w - 1, y1 = y + h - 1; int x1 = x + w - 1, y1 = y + h - 1;
if (x < cdb_x0) cdb_x0 = x; if (x < cdb_x0) cdb_x0 = x;
if (y < cdb_y0) cdb_y0 = y; if (y < cdb_y0) cdb_y0 = y;
@@ -310,12 +319,65 @@ void pop_cd_clear(uint8_t p)
* перерисовка тайлов — идёт с pop_t_fclip_on == 0. */ * перерисовка тайлов — идёт с pop_t_fclip_on == 0. */
#define FLIP_TOP(top, h) POP_FLIP_TOP(top, h) /* формула — в pop_bg.h */ #define FLIP_TOP(top, h) POP_FLIP_TOP(top, h) /* формула — в pop_bg.h */
static void blit_b_clip(const uint8_t *img, int x, int top, int w, int h) /* Окно под точечную перерисовку/запечку — контракт в шапке pop_tile.h.
* Логическая пара нужна для отсева тайлов (он считается из ряда комнаты),
* экранная — для клипа самого блита; при перевороте они разные. */
void pop_t_win_set(int x, int ytop, int w, int h)
{ {
int dx = x, dy = top, dw = w, dh = h, sx = 0, sy = 0; pop_t_fclip_x0 = x;
pop_t_fclip_x1 = x + w;
pop_t_fclip_ly0 = ytop;
pop_t_fclip_ly1 = ytop + h;
if (pop_upside) {
int t = FLIP_TOP(ytop, h);
pop_t_fclip_y0 = t;
pop_t_fclip_y1 = t + h;
} else {
pop_t_fclip_y0 = ytop;
pop_t_fclip_y1 = ytop + h;
}
pop_t_fclip_on = 1;
}
void pop_t_win_clear(void) { pop_t_fclip_on = 0; }
/* Клипованный путь блита. Габарит и смещения внутри куска — БАЙТЫ, а вся
* рабочая переменная — file-scope, НЕ локали.
*
* Корректность байтового габарита по построению: кадры наших атласов не
* крупнее 56x63 (просканированы все 109 .atl, docs/perf_l13_room23.md §5), а
* кадр больше 255 в pop_blit_b уходит в blit_b_oversize и сюда не попадает.
*
* Зачем file-scope: с локалями SDCC заводил 22-байтовый стековый кадр и 211
* обращений `-N(ix)` — при ~46 замеренных тактах на обращение это ~9 700
* такта из 14 088 цены клипованного блита (замер 2026-08-17,
* docs/perf_green_phase.md §1.6). Одного байтового габарита НЕ ХВАТИЛО:
* кадр остался, обращений стало 173 — значений, живых через шесть вызовов
* ядер libbgi, всё равно больше, чем регистров у Z80. С file-scope
* спиливать нечего, а адресация абсолютная (13 тактов против 19).
* Тот же приём и по той же причине применён к draw_tile (pop_room.c).
*
* Реентерабельности не было и не требуется: единственный вызывающий —
* pop_blit_b, и он себя не зовёт.
*
* Арифметика клипа считается в int16 (bc_d), в байт уходит только результат —
* иначе вычитание ушло бы в перенос по модулю 256. */
static const uint8_t *bc_img;
static int bc_dx, bc_dy;
static int16_t bc_d;
static uint8_t bc_w, bc_h, bc_dw, bc_dh, bc_sx, bc_sy;
static void blit_b_clip(const uint8_t *img, int x, int top, uint8_t w, uint8_t h)
{
bc_img = img; bc_w = w; bc_h = h;
bc_dx = x; bc_dy = top;
bc_dw = w; bc_dh = h; bc_sx = 0; bc_sy = 0;
if (!pop_upside) { if (!pop_upside) {
if (pop_t_clip_top && dy < pop_t_clip_top) { /* полоса у потолка */ if (pop_t_clip_top && bc_dy < pop_t_clip_top) { /* полоса у потолка */
sy = pop_t_clip_top - dy; dy = pop_t_clip_top; dh -= sy; bc_d = (int16_t)(pop_t_clip_top - bc_dy);
if (bc_d >= (int16_t)bc_dh) return;
bc_sy = (uint8_t)bc_d; bc_dy = pop_t_clip_top;
bc_dh = (uint8_t)(bc_dh - bc_sy);
} }
} else { } else {
/* ПЕРЕВОРОТ: то, что в обычном виде торчало ВЫШЕ поля (полоса кладки /* ПЕРЕВОРОТ: то, что в обычном виде торчало ВЫШЕ поля (полоса кладки
@@ -323,56 +385,95 @@ static void blit_b_clip(const uint8_t *img, int x, int top, int w, int h)
* где живёт полоса HP (баг «мусор ниже 192», найден пользователем). * где живёт полоса HP (баг «мусор ниже 192», найден пользователем).
* Режем по границам поля ВСЕГДА, а не по pop_t_clip_top: зеркальный * Режем по границам поля ВСЕГДА, а не по pop_t_clip_top: зеркальный
* спрайт может вылезти и там, где в обычном виде клип не ставили. */ * спрайт может вылезти и там, где в обычном виде клип не ставили. */
if (dy < POP_YOFF) { sy = POP_YOFF - dy; dy = POP_YOFF; dh -= sy; } if (bc_dy < POP_YOFF) {
if (dy + dh > POP_CLIP_BOTTOM) dh = POP_CLIP_BOTTOM - dy; bc_d = (int16_t)(POP_YOFF - bc_dy);
if (bc_d >= (int16_t)bc_dh) return;
bc_sy = (uint8_t)bc_d; bc_dy = POP_YOFF;
bc_dh = (uint8_t)(bc_dh - bc_sy);
}
if (bc_dy + bc_dh > POP_CLIP_BOTTOM) {
bc_d = (int16_t)(POP_CLIP_BOTTOM - bc_dy);
if (bc_d <= 0) return;
bc_dh = (uint8_t)bc_d;
}
} }
if (pop_t_fclip_on) { /* окно fore-слоя */ if (pop_t_fclip_on) { /* окно fore-слоя / перерисовки */
if (dx < pop_t_fclip_x0) { int d = pop_t_fclip_x0 - dx; sx += d; dx += d; dw -= d; } if (bc_dx < pop_t_fclip_x0) {
if (dy < pop_t_fclip_y0) { int d = pop_t_fclip_y0 - dy; sy += d; dy += d; dh -= d; } bc_d = (int16_t)(pop_t_fclip_x0 - bc_dx);
if (dx + dw > pop_t_fclip_x1) dw = pop_t_fclip_x1 - dx; if (bc_d >= (int16_t)bc_dw) return;
if (dy + dh > pop_t_fclip_y1) dh = pop_t_fclip_y1 - dy; bc_sx = (uint8_t)(bc_sx + (uint8_t)bc_d);
bc_dx += bc_d; bc_dw = (uint8_t)(bc_dw - (uint8_t)bc_d);
}
if (bc_dy < pop_t_fclip_y0) {
bc_d = (int16_t)(pop_t_fclip_y0 - bc_dy);
if (bc_d >= (int16_t)bc_dh) return;
bc_sy = (uint8_t)(bc_sy + (uint8_t)bc_d);
bc_dy += bc_d; bc_dh = (uint8_t)(bc_dh - (uint8_t)bc_d);
}
if (bc_dx + bc_dw > pop_t_fclip_x1) {
bc_d = (int16_t)(pop_t_fclip_x1 - bc_dx);
if (bc_d <= 0) return;
bc_dw = (uint8_t)bc_d;
}
if (bc_dy + bc_dh > pop_t_fclip_y1) {
bc_d = (int16_t)(pop_t_fclip_y1 - bc_dy);
if (bc_d <= 0) return;
bc_dh = (uint8_t)bc_d;
}
} }
if (dw <= 0 || dh <= 0) return; if (!bc_dw || !bc_dh) return;
/* ПЕРЕВОРОТ: экранная строка (dy+i) берёт источник (h-1-i), поэтому /* ПЕРЕВОРОТ: экранная строка (dy+i) берёт источник (h-1-i), поэтому
* обрезка СВЕРХУ экрана съедает НИЖНИЕ строки источника. В терминах * обрезка СВЕРХУ экрана съедает НИЖНИЕ строки источника. В терминах
* куска источника это sy' = h - sy - dh (при обрезке только сверху даёт * куска источника это sy' = h - sy - dh (при обрезке только сверху даёт
* 0, при обрезке только снизу — верхний край). */ * 0, при обрезке только снизу — верхний край). */
/* Проверки `dw < 256 && dh < 256 && w < 256` убраны как тождественные:
* это уже байты. */
if (pop_upside) { if (pop_upside) {
int fsy = h - sy - dh; bc_d = (int16_t)bc_h - (int16_t)bc_sy - (int16_t)bc_dh;
if (fsy < 0) fsy = 0; if (bc_d < 0) bc_d = 0;
if (dx >= 0 && dy >= 0 && dw < 256 && dh < 256 && if (bc_dx >= 0 && bc_dy >= 0 &&
dx + dw <= 320 && dy + dh <= 256 && w < 256) bc_dx + bc_dw <= 320 && bc_dy + bc_dh <= 256)
gfx_blit_part_noclip_vflip(dx, dy, img, (uint8_t)sx, (uint8_t)fsy, gfx_blit_part_noclip_vflip(bc_dx, bc_dy, bc_img, bc_sx,
(uint8_t)dw, (uint8_t)dh); (uint8_t)bc_d, bc_dw, bc_dh);
else else
gfx_blit_part_vflip(dx, dy, img, sx, fsy, dw, dh); gfx_blit_part_vflip(bc_dx, bc_dy, bc_img, bc_sx,
(uint8_t)bc_d, bc_dw, bc_dh);
} else } else
if (dx >= 0 && dy >= 0 && dw < 256 && dh < 256 && if (bc_dx >= 0 && bc_dy >= 0 &&
dx + dw <= 320 && dy + dh <= 256 && w < 256) { bc_dx + bc_dw <= 320 && bc_dy + bc_dh <= 256) {
if (sx == 0 && sy == 0 && dw == w && dh == h) if (bc_sx == 0 && bc_sy == 0 && bc_dw == bc_w && bc_dh == bc_h)
gfx_blit_noclip(dx, dy, img); gfx_blit_noclip(bc_dx, bc_dy, bc_img);
else else
gfx_blit_part_noclip(dx, dy, img, (uint8_t)sx, (uint8_t)sy, gfx_blit_part_noclip(bc_dx, bc_dy, bc_img, bc_sx, bc_sy,
(uint8_t)dw, (uint8_t)dh); bc_dw, bc_dh);
} else { } else {
gfx_blit_part(dx, dy, img, sx, sy, dw, dh); /* край экрана */ gfx_blit_part(bc_dx, bc_dy, bc_img, bc_sx, bc_sy, bc_dw, bc_dh);
} }
/* Кусок ФОНА лёг туда, где может лежать уже нарисованный персонаж — /* Кусок ФОНА лёг туда, где может лежать уже нарисованный персонаж —
* пометить область (DRAW-COST, pop_cdraw.h). Признак «это не fore-проход * пометить область (DRAW-COST, pop_cdraw.h). Признак «это не fore-проход
* поверх персонажа» — окно клипа: его ставит только он. */ * поверх персонажа» — окно клипа: его ставит только он. */
if (!pop_t_fclip_on) pop_cd_touch(dx, dy, dw, dh); if (!pop_t_fclip_on) pop_cd_touch(bc_dx, bc_dy, bc_dw, bc_dh);
} }
/* Кадр шире или выше 255 — общий (медленный) путь. Вынесен отдельно, чтобы /* Кадр шире или выше 255 — общий (медленный) путь. Вынесен отдельно, чтобы
* 16-битная арифметика габарита не жила в горячем pop_blit_b: у нас таких * 16-битная арифметика габарита не жила ни в горячем pop_blit_b, ни в
* кадров нет вовсе (максимум 32x63), но контракт формата .atl их допускает. */ * blit_b_clip (у того габарит теперь БАЙТОВЫЙ, см. его шапку). У нас таких
* кадров в игровом кадре нет вовсе (максимум 56x63), но контракт формата
* .atl их допускает, а интро/финал такие подложки принесут (320x200) —
* см. docs/perf_l13_room23.md §5.
*
* Клипа полосы у потолка и окна перерисовки здесь нет намеренно: такой кадр
* — полноэкранная подложка, ей ни то, ни другое не адресовано; за клип по
* экрану отвечает само ядро gfx_blit_part. */
static void blit_b_oversize(const uint8_t *img, int x, int ybottom) static void blit_b_oversize(const uint8_t *img, int x, int ybottom)
{ {
uint16_t w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); uint16_t w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
uint16_t h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); uint16_t h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
int top = ybottom - (int)h + 1 + POP_YOFF; int top = ybottom - (int)h + 1 + POP_YOFF;
if (pop_upside) top = FLIP_TOP(top, h); if (pop_upside) top = FLIP_TOP(top, h);
if (w && h) blit_b_clip(img, x, top, (int)w, (int)h); if (!w || !h) return;
gfx_blit_part(x, top, img, 0, 0, (int)w, (int)h);
if (!pop_t_fclip_on) pop_cd_touch(x, top, (int)w, (int)h);
} }
void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
@@ -429,7 +530,7 @@ void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
/* В перевёрнутом виде быстрый путь годится, только если спрайт /* В перевёрнутом виде быстрый путь годится, только если спрайт
* целиком внутри поля: иначе он залезет в борт (см. blit_b_clip). */ * целиком внутри поля: иначе он залезет в борт (см. blit_b_clip). */
if (pop_upside && (top < POP_YOFF || top + (int)h > POP_CLIP_BOTTOM)) { if (pop_upside && (top < POP_YOFF || top + (int)h > POP_CLIP_BOTTOM)) {
blit_b_clip(img, x, top, (int)w, (int)h); blit_b_clip(img, x, top, w, h);
gfx_w0_unmap(); gfx_w0_unmap();
return; return;
} }
@@ -447,7 +548,7 @@ void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
pop_cd_touch(x, top, (int)w, (int)h); pop_cd_touch(x, top, (int)w, (int)h);
pop_dbg_b4(); /* ВРЕМЕННО */ pop_dbg_b4(); /* ВРЕМЕННО */
} else { } else {
blit_b_clip(img, x, top, (int)w, (int)h); blit_b_clip(img, x, top, w, h);
pop_dbg_b3(); pop_dbg_b4(); /* ВРЕМЕННО: медленный путь целиком в b3 */ pop_dbg_b3(); pop_dbg_b4(); /* ВРЕМЕННО: медленный путь целиком в b3 */
} }
} }
+19
View File
@@ -77,6 +77,25 @@ extern int pop_t_fclip_x0, pop_t_fclip_y0, pop_t_fclip_x1, pop_t_fclip_y1;
extern int pop_t_fclip_ly0, pop_t_fclip_ly1; extern int pop_t_fclip_ly0, pop_t_fclip_ly1;
extern uint8_t pop_t_fclip_on; extern uint8_t pop_t_fclip_on;
/* Поставить то же окно под ТОЧЕЧНУЮ ПЕРЕРИСОВКУ / ЗАПЕЧКУ. Прямоугольник —
* тот, который восстанавливаем (heal или бар-в-чёрный), в ЛОГИЧЕСКИХ
* экранных координатах (с POP_YOFF, но до переворота); экранную пару
* функция считает сама.
*
* Зачем: точечная перерисовка зовёт draw_tile, а тот честно рисует ВЕСЬ тайл.
* У полосы кладки у потолка это особенно дорого: куски ряда 0 высотой 63 px
* рисуются целиком, хотя восстановить надо девять верхних строк (замер
* 2026-08-17: блит в pop_ceil_bake_empty стоит 21 447 против 14 384 в
* pop_floor_bake — ровно разница в высоте). Окно и отсеивает куски, вовсе не
* попавшие в прямоугольник (предфильтр pop_blit_b — ДО atlas_image и
* gfx_w0_map), и режет остальные по нему.
*
* ВАЖНО: пока окно стоит, pop_blit_b НЕ ставит пометку «фон трогали» (признак
* fore-прохода) — вызывающий обязан пометить прямоугольник САМ (pop_cd_touch
* или pop_bar_black, который метит сам). */
void pop_t_win_set(int x, int ytop, int w, int h);
void pop_t_win_clear(void);
/* ---- Таблица тайлов (piece, seg008.c:27) ---------------------------- */ /* ---- Таблица тайлов (piece, seg008.c:27) ---------------------------- */
typedef struct { typedef struct {
uint8_t base_id, floor_left; int8_t base_y; uint8_t base_id, floor_left; int8_t base_y;