mob_tick_one в file-scope + снятие временной оснастки замеров

зелёная пик  423 558 -> 414 456
  работа пик   793 266 -> 783 798
  ПЕРИОД логического кадра в каскаде: было 5-6 растровых, стало РОВНО 4

mob_tick_one: рабочие переменные в file-scope (было 16 байт кадра и 99
обращений `-N(ix)`, стало 17) — то же лечение, что у draw_tile и blit_b_clip.

Снята временная оснастка из ГОРЯЧИХ путей: шесть вызовов pop_dbg_b1..b6 в
pop_blit_b (по ~65 такта каждый на КАЖДЫЙ блит), pop_dbg_kind/m16 и
подсчёт состава кадра в pop_redraw_needed, pop_dbg_m13..m15 в
pop_ceil_shake_draw.  Сами пустышки в pop_state.c оставлены — вставить их
обратно на один замер дешевле, чем заводить заново; как это делается,
записано в docs/perf_l13_room23.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 16:47:20 +03:00
parent 5ef721e340
commit 18ee60eb69
3 changed files with 47 additions and 61 deletions
-12
View File
@@ -80,13 +80,9 @@ void pop_set_redraw_above(uint8_t col, uint8_t kind, uint8_t pages) __banked
void pop_redraw_needed(void) __banked
{
uint8_t i, row = 0, col = 0, left = 0;
uint8_t cnt[10], tot = 0; /* ВРЕМЕННО: состав кадра */
if (!rd_pending) return;
for (i = 0; i < 10; i++) cnt[i] = 0;
for (i = 0; i < NTILES; i++) {
if (rd_cnt[i]) {
if (rd_kind[i] < 8) { cnt[rd_kind[i]]++; tot++; } /* ВРЕМЕННО */
pop_dbg_kind(rd_kind[i]); /* ВРЕМЕННО */
switch (rd_kind[i]) {
case POP_RD_SPIKE: pop_spike_redraw(row, col); break;
case POP_RD_LOOSE: pop_loose_shake_draw(row, col); break;
@@ -97,7 +93,6 @@ void pop_redraw_needed(void) __banked
case POP_RD_CHOMP: pop_chomp_redraw(row, col); break;
default: break;
}
pop_dbg_m16(); /* ВРЕМЕННО */
if (--rd_cnt[i]) left++;
else rd_kind[i] = 0;
}
@@ -105,18 +100,11 @@ void pop_redraw_needed(void) __banked
}
for (i = 0; i < NCOLS; i++) {
if (rda_cnt[i]) {
cnt[8 + (rda_kind[i] == POP_RDA_CEIL ? 0 : 1)]++; tot++; /* ВРЕМЕННО */
pop_dbg_kind((uint8_t)(8 + rda_kind[i])); /* ВРЕМЕННО */
if (rda_kind[i] == POP_RDA_CEIL) pop_ceil_shake_draw(i);
else pop_ceil_bake_empty(i);
pop_dbg_m16(); /* ВРЕМЕННО */
if (--rda_cnt[i]) left++;
else rda_kind[i] = 0;
}
}
if (tot > pop_dbg_rdmax_tot) { /* ВРЕМЕННО: расклад пика */
pop_dbg_rdmax_tot = tot;
for (i = 0; i < 10; i++) pop_dbg_rdmax[i] = cnt[i];
}
rd_pending = left;
}
+47 -42
View File
@@ -757,9 +757,7 @@ void pop_bake_forget(void) __banked
void pop_ceil_shake_draw(int col) __banked
{
int x = POP_COL_XH[col] * 8;
pop_dbg_m13(); /* ЗАМЕР: вход pop_ceil_shake_draw */
pop_heal_off(x, 0, 64, CEIL_BAND_H);
pop_dbg_m14(); /* ЗАМЕР: heal сделан */
/* Пометка «фон трогали» — ОДНА на восстановленный прямоугольник: пока
* стоит окно (ниже), pop_blit_b пометок не ставит. Область та же, что у
* heal'а, — он её и стирает. */
@@ -771,7 +769,6 @@ void pop_ceil_shake_draw(int col) __banked
pop_t_win_clear();
pop_t_clip_top = 0;
gfx_set_bank(GFX_BANK_NORMAL);
pop_dbg_m15(); /* ЗАМЕР: draw_tile сделан */
}
void pop_ceil_bake_empty(int col) __banked
@@ -1257,22 +1254,30 @@ static void mob_break_loose(mob_t *m)
mob_spawn_copy(m);
}
static mob_t *mt_m;
static uint8_t mt_pg, mt_here;
static void mob_tick_one(mob_t *m, uint8_t pg)
{
uint8_t here;
if (!m->active && !m->clean) return;
here = (uint8_t)(m->room == pop_t_room);
/* Рабочие переменные — file-scope, не локали: у функции пять вызовов, и
* живой через них указатель на слот SDCC спиливал в кадр (16 байт, 99
* обращений `-N(ix)`). То же лечение, что у draw_tile и blit_b_clip.
* Реентерабельности нет и не требуется: единственный вызывающий —
* pop_loose_mob_tick, и слоты он обходит последовательно. */
mt_m = m; mt_pg = pg;
if (!mt_m->active && !mt_m->clean) return;
mt_here = (uint8_t)(mt_m->room == pop_t_room);
/* Стереть кусок, нарисованный на ЭТОЙ странице в прошлый раз (у каждой
* страницы своя копия фона). Раньше коридор был фиксирован под ряд 2;
* плита-потолок падает через весь ряд 0, поэтому чистим по фактической
* прошлой позиции. */
/* Гейта по `here` здесь БЫТЬ НЕ ДОЛЖНО: prev_y[pg] — это след, который
/* Гейта по `mt_here` здесь БЫТЬ НЕ ДОЛЖНО: prev_y[mt_pg] — это след, который
* кусок оставил ИМЕННО НА ЭТОЙ странице, и стереть его надо независимо
* от того, где кусок сейчас. Пока чистка была под `here`, кусок,
* от того, где кусок сейчас. Пока чистка была под `mt_here`, кусок,
* улетевший в комнату снизу (mob_down_a_row), навсегда оставлял себя
* на одной из двух страниц дабл-буфера — «застрявшая плита», видимая
* через кадр (BUG-MOB-STALE-PAGE). */
if (m->prev_y[pg] != MOB_Y_NONE) {
if (mt_m->prev_y[mt_pg] != MOB_Y_NONE) {
/* Ширина/высота — по СЛЕДУ, а не по спрайту. Спрайт куска целиком
* влезает в 64x32 (41: 32x13 с y-3, 42: 26x15 с x+32,y-1, 43: 32x3),
* но mob_render рисует ещё и ЦЕЛЫЙ соседний тайл поверх правой части
@@ -1286,13 +1291,13 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
* ЗАДЕЛ: правильнее не перерисовывать соседа вовсе, а КЛИПОВАТЬ
* правую часть куска — оригинал так и делает (add_mob_to_objtable,
* seg007:1161: clip.right = 40). Тогда коридор вернётся к 64x32. */
pop_heal_off(MOB_X0(m->x), m->prev_y[pg] - mob_heal_up, MOB_W, mob_heal_h);
m->prev_y[pg] = MOB_Y_NONE;
pop_heal_off(MOB_X0(mt_m->x), mt_m->prev_y[mt_pg] - mob_heal_up, MOB_W, mob_heal_h);
mt_m->prev_y[mt_pg] = MOB_Y_NONE;
}
if (!m->active) { m->clean--; return; }
if (!mt_m->active) { mt_m->clean--; return; }
/* гравитация (move_loose): ускорение до 29 */
if (m->speed < 29) m->speed += 3;
m->y += m->speed;
if (mt_m->speed < 29) mt_m->speed += 3;
mt_m->y += mt_m->speed;
/* move_loose (seg007:1126): низ куска пересёк границу своего ряда —
* смотрим тайл этого ряда. Пусто/loose — валится дальше, пол —
* приземление (loose_land: debris + do_knock). Через LOOSE кусок именно
@@ -1304,9 +1309,9 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
* плиты (1,6) и (1,7), на которые сверху сыплется гряда из комнаты 1.
* Куски пролетали сквозь них правильно, а сами плиты оставались целыми и
* нарисованными (найдено прогоном 2026-08-13). */
if (m->row < 3 && m->y < 226 && MOB_Y_BOUND[m->row + 1] <= m->y) {
uint8_t t = mob_tile_at(m);
if (t == 11) mob_break_loose(m);
if (mt_m->row < 3 && mt_m->y < 226 && MOB_Y_BOUND[mt_m->row + 1] <= mt_m->y) {
uint8_t t = mob_tile_at(mt_m);
if (t == 11) mob_break_loose(mt_m);
if (t == 0 || t == 11) {
/* mob_down_a_row (seg007:1387): ряд ниже, а из НИЖНЕГО ряда —
* в комнату СНИЗУ (y -= 192, ряд 0). Именно так кусок пролетает
@@ -1315,55 +1320,55 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
* кнопку (2,7) комнаты 11. Раньше мы кусок на границе комнаты
* ГАСИЛИ и подменяли одним «переходом вниз» из главного цикла —
* дальше первой комнаты он не летел (BUG-MOB-MULTIROOM). */
m->row++;
if (m->row >= 3) {
m->y = (int16_t)(m->y - 192);
m->row = 0;
m->room = pop_room_link(m->room, 3 /* вниз */);
if (!m->room) { /* улетел из уровня — просто гаснет */
m->active = 0; m->clean = 2; /* дочистить ОБЕ страницы */
mt_m->row++;
if (mt_m->row >= 3) {
mt_m->y = (int16_t)(mt_m->y - 192);
mt_m->row = 0;
mt_m->room = pop_room_link(mt_m->room, 3 /* вниз */);
if (!mt_m->room) { /* улетел из уровня — просто гаснет */
mt_m->active = 0; mt_m->clean = 2; /* дочистить ОБЕ страницы */
return;
}
here = (uint8_t)(m->room == pop_t_room);
mt_here = (uint8_t)(mt_m->room == pop_t_room);
}
} else if (!here && pop_loose_exit) {
} else if (!mt_here && pop_loose_exit) {
/* Сигнал прошлой посадки в чужой комнате ещё не разобран (он
* ОДИН на кадр) — держим кусок на границе ряда и садим следующим
* кадром, как это уже сделано для pop_loose_landed ниже. Без
* этого второй кусок затирал первый, и из нескольких плит,
* улетевших в одну дыру, щебень оставляла только одна (найдено
* прогоном 2026-08-13, уровень 13). */
m->y = MOB_Y_BOUND[m->row + 1];
} else if (!here) {
mt_m->y = MOB_Y_BOUND[mt_m->row + 1];
} else if (!mt_here) {
/* Приземление в ПОКИНУТОЙ комнате: рисовать нечего, но щебень и
* КНОПКА под куском обязаны сработать — сигналим главному циклу
* комнатой и тайлом (loose_land, seg007:11E8, работает с
* curmob.room, а не с отрисованной). */
pop_loose_exit_room = m->room;
pop_loose_exit = (uint8_t)(m->row * 10 + m->col + 1);
pop_loose_exit_room = mt_m->room;
pop_loose_exit = (uint8_t)(mt_m->row * 10 + mt_m->col + 1);
/* clean = 2 даже для чужой комнаты: пока кусок летел, он мог
* рисоваться в ЭТОЙ, и след остался на обеих страницах. */
m->active = 0; m->clean = 2;
mt_m->active = 0; mt_m->clean = 2;
return;
} else if (pop_loose_landed) {
/* сигнал прошлого приземления ещё не разобран (один за кадр) —
* подержим кусок на границе и приземлим следующим кадром */
m->y = MOB_Y_BOUND[m->row + 1];
mt_m->y = MOB_Y_BOUND[mt_m->row + 1];
} else {
m->y = MOB_Y_BOUND[m->row + 1];
pop_loose_landed = (uint8_t)(m->row * 10 + m->col + 1);
m->active = 0; m->clean = 2; /* кусок исчез — на его месте debris */
mt_m->y = MOB_Y_BOUND[mt_m->row + 1];
pop_loose_landed = (uint8_t)(mt_m->row * 10 + mt_m->col + 1);
mt_m->active = 0; mt_m->clean = 2; /* кусок исчез — на его месте debris */
return;
}
}
/* Страховка: кусок ушёл ниже поля, не поймав границу ряда (в норме этого
* не бывает — mob_down_a_row уводит его в комнату снизу). Просто гаснет,
* как у оригинала кусок в комнате 0 (move_loose: `speed = -2`). */
if (m->y - 27 >= POP_PLAYFIELD_H) {
m->active = 0; m->clean = 2;
if (mt_m->y - 27 >= POP_PLAYFIELD_H) {
mt_m->active = 0; mt_m->clean = 2;
return;
}
if (!here) return; /* чужая комната — считаем, но не рисуем */
if (!mt_here) return; /* чужая комната — считаем, но не рисуем */
/* Порядок «плита vs Kid». В оригинале объекты рисуются при обходе СВОИХ
* тайлов (redraw_needed_tiles: ряды 2,1,0; колонки 0..9), и только внутри
* ОДНОГО тайла сортируются по obj_y (sort_curr_objs, seg008:1561: кто
@@ -1372,11 +1377,11 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
* поверх Kid, если его тайл идёт в обходе ПОЗЖЕ тайла Kid, а при
* совпадении тайлов — если он ниже по y. */
{
int8_t mrow = pop_y_to_row((int16_t)m->y); /* y_to_row_mod4 */
int8_t mrow = pop_y_to_row((int16_t)mt_m->y); /* y_to_row_mod4 */
uint8_t over;
if (mrow != pop_bg_obj_row) over = (mrow < pop_bg_obj_row); /* ряды идут 2,1,0 */
else if (m->col != pop_bg_obj_col) over = (m->col > pop_bg_obj_col);
else over = (m->y > pop_cd[POP_CH_KID].fpy);
else if (mt_m->col != pop_bg_obj_col) over = (mt_m->col > pop_bg_obj_col);
else over = (mt_m->y > pop_cd[POP_CH_KID].fpy);
/* РИСОВАНИЯ ЗДЕСЬ БОЛЬШЕ НЕТ — только решение, в какой проход кусок
* пойдёт. Пока heal и вывод шли в одной итерации, heal СЛЕДУЮЩЕГО
* куска срезал верхнюю кромку уже нарисованного предыдущего: на 13-м
@@ -1384,7 +1389,7 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
* по y, и перекрытие гарантировано (артефакт «чёрный бар», найден
* прогоном 2026-08-13, комната 23). Теперь сперва тикают ВСЕ куски
* (то есть heal'ятся все), и только потом рисуются. */
m->defer = over;
mt_m->defer = over;
}
}
-7
View File
@@ -503,10 +503,8 @@ void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
pb_yb + POP_YOFF + 1 <= pop_t_fclip_ly0 ||
pb_yb + POP_YOFF - 64 >= pop_t_fclip_ly1))
return;
pop_dbg_b1(); /* ВРЕМЕННО */
pb_img = (const uint8_t *)atlas_image(pb_a, pb_idx);
gfx_w0_map(pb_a->page);
pop_dbg_b2(); /* ВРЕМЕННО */
/* Габарит читаем БАЙТАМИ. С `uint16_t pb_w = pb_img[0] | (pb_img[1] << 8)` SDCC
* разворачивал каждую такую строку в два десятка IX-относительных
* пересылок: 16-битные pb_w/pb_h не влезали в регистры, и функция целиком
@@ -547,23 +545,18 @@ void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
/* pb_w<256 && pb_h<256 больше не проверяем — гарантировано типом. */
if (pb_x >= 0 && pb_top >= 0 &&
pb_x + (int)pb_w <= 320 && pb_top + (int)pb_h <= 256) {
pop_dbg_b6(); /* ВРЕМЕННО: пометка «шли в noclip» */
if (pop_upside) gfx_blit_noclip_vflip(pb_x, pb_top, pb_img);
else gfx_blit_noclip(pb_x, pb_top, pb_img);
} else {
if (pop_upside) gfx_blit_part_vflip(pb_x, pb_top, pb_img, 0, 0, (int)pb_w, (int)pb_h);
else gfx_blit_part(pb_x, pb_top, pb_img, 0, 0, (int)pb_w, (int)pb_h);
}
pop_dbg_b3(); /* ВРЕМЕННО */
pop_cd_touch(pb_x, pb_top, (int)pb_w, (int)pb_h);
pop_dbg_b4(); /* ВРЕМЕННО */
} else {
blit_b_clip(pb_img, pb_x, pb_top, pb_w, pb_h);
pop_dbg_b3(); pop_dbg_b4(); /* ВРЕМЕННО: медленный путь целиком в b3 */
}
}
gfx_w0_unmap();
pop_dbg_b5(); /* ВРЕМЕННО */
}
/* Блит getimage-блоба из ОБЫЧНОЙ памяти (W2), а не из EMM-страницы атласа: