diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md
index 1326f60..b194b82 100644
--- a/applications/PoP/roomtest/TASKS_OPEN.md
+++ b/applications/PoP/roomtest/TASKS_OPEN.md
@@ -258,6 +258,52 @@ MEM-BANK2, MEM-BANK5, CLIP-1, KBD-1, DBG-CHEATS.
## P0 — делаем сейчас
+### HEAL-WIDTH. Ширина точечных heal'ов не совпадает со следом перерисовки — ОБЯЗАТЕЛЬНО
+
+**Постановка (пользователь, 2026-08-18).** Ширины heal'ов взяты «на глаз по
+клеткам», а не по реальному следу. Сжатие до фактического следа даёт около
+**10 % площади** самой дорогой операции акселератора; по оценке пользователя
+5-6 % реального выигрыша. Задача обязательная.
+
+**Измерено** (каталоги атласов, спрайты плиты):
+
+| часть плиты | подземелье | дворец |
+|---|---|---|
+| верх (`41/69/70`) | 32 × 13-14 | 32 × 13-14 |
+| правая грань (`42/71/72`) | **26** × 15-16 | **25** × 15-16 |
+
+То есть анимированный след ОДНОЙ плиты — `32 + 26 = 58` пикселей в
+подземелье и `57` во дворце.
+
+**Что стоит сейчас.** В коде уживаются ДВЕ разные договорённости:
+
+- **60** = «свой тайл + свес соседа» — `pop_floor_bake` (там это записано
+ комментарием);
+- **64** = «две клетки» — `pop_loose_shake_draw`, `pop_spike_redraw`
+ («ширина 64 = обе ячейки»).
+
+Для трясущейся плиты 64 избыточны: хватает 58 (подземелье) / 57 (дворец),
+то есть **6 пикселей из 64 — впустую, 9,4 % площади heal'а**.
+
+**И обратная сторона, которую тоже надо разобрать.** Ни 60, ни 64 не
+покрывают того, что перерисовка РИСУЕТ: два полных `draw_tile` красят до
+**90** пикселей (32 свои + 32 соседа + 26 правой грани соседа, уходящей в
+третью клетку). Видимого артефакта отсюда пока нет — содержимое третьей
+клетки статично и перерисовывается одинаково, а трясущийся сосед накрывает
+эту область своим heal'ом, — но несоответствие «heal уже, чем след» надо
+либо закрыть, либо записать как осознанное.
+
+**Что сделать.** Свести ВСЕ точечные heal'ы (`pop_loose_shake_draw`,
+`pop_spike_redraw`, `pop_chomp_redraw`, `pop_floor_bake`, `pop_loose_bake_empty`,
+`pop_ceil_*`, коридор `mob_heal_*`) в таблицу «что рисует / что чистит» и
+привести ширину каждого к фактическому следу. Ширины брать ИЗ АТЛАСА, а не
+из номера клетки — они разные у двух тайлсетов (26 против 25).
+
+**Проверка:** замер 13/23 до/после (heal'ы — зелёная фаза) плюс визуальный
+прогон комнат с плитами, пиками и чомперами: сужение heal'а — самый прямой
+способ получить «недочищенный хвост».
+
+
### L12-SHADOW. Уровень 12: встреча с тенью, бой, слияние
> **Статус 2026-08-13: код написан, хост-тесты зелёные (45 проверок,
diff --git a/applications/PoP/roomtest/pop_bg.c b/applications/PoP/roomtest/pop_bg.c
index 3a5bb64..a474904 100644
--- a/applications/PoP/roomtest/pop_bg.c
+++ b/applications/PoP/roomtest/pop_bg.c
@@ -781,20 +781,10 @@ static void overlay_mid_tile(int row, int col)
} else if (t->bottom_id) {
pop_env_b(t->bottom_id, x, dby);
}
- /* draw_loose(0) — ПЕРЕДНИЙ ТОРЕЦ плиты, последний вызов draw_tile2
- * (seg008: draw_tile_right, draw_tile_anim_right, draw_tile_base,
- * draw_tile_anim, draw_tile_bottom, draw_loose). У loose в
- * pop_tile_table bottom_id = 0, поэтому ветка выше его НЕ рисует, и в
- * оверлее торца не было вовсе: падающий кусок оставался ПОВЕРХ торца
- * соседней плиты (найдено пользователем 2026-08-18, ур.11 к.14 —
- * трясущаяся плита (2,6) и кусок, отвалившийся от (2,5)).
- *
- * Кадр берётся по ЖИВОМУ modif, как и верх плиты выше: оверлей рисуется
- * поверх уже готовой картинки, и дрожащая плита обязана остаться
- * дрожащей. */
- if (code == 11 && row >= 0)
- pop_env_b(POP_LOOSE_FRAM_BOTTOM[pop_loose_frame(pop_loose_modif[row * 10 + col])],
- x, dby);
+ /* draw_loose тут НЕТ, и это не забывчивость: в оригинале (seg008
+ * draw_loose) нижняя грань плиты идёт в add_backtable + add_foretable,
+ * мимо ptr_add_table — то есть в midtable не попадает никогда. Перекрыть
+ * ею падающий кусок должен ПЕРЕДНИЙ слой (fore_only_tile), а не оверлей. */
}
/* draw_tile_fore (seg008:690) — ТОЛЬКО передний слой (add_foretable): он
diff --git a/applications/PoP/roomtest/pop_redraw.c b/applications/PoP/roomtest/pop_redraw.c
index 5b23a3b..a0eba7c 100644
--- a/applications/PoP/roomtest/pop_redraw.c
+++ b/applications/PoP/roomtest/pop_redraw.c
@@ -107,7 +107,8 @@ void pop_set_redraw_above(uint8_t col, uint8_t kind, uint8_t pages) __banked
* аргументов (30*2 делений за кадр даже когда рисовать нечего). */
void pop_redraw_needed(void) __banked
{
- uint8_t i, row = 0, col = 0, left = 0;
+ uint8_t i, row = 0, col = 0, left = 0, cnt = 0; /* cnt — ВРЕМЕННО, замер */
+ pop_dbg_rdmax_tot = 0;
if (!rd_pending) return;
for (i = 0; i < NTILES; i++) {
if (rd_cnt[i]) {
@@ -121,6 +122,7 @@ void pop_redraw_needed(void) __banked
case POP_RD_CHOMP: pop_chomp_redraw(row, col); break;
default: break;
}
+ cnt++; /* ВРЕМЕННО: замер */
if (--rd_cnt[i]) left++;
else rd_kind[i] = 0;
}
@@ -130,9 +132,12 @@ void pop_redraw_needed(void) __banked
if (rda_cnt[i]) {
if (rda_kind[i] == POP_RDA_CEIL) pop_ceil_shake_draw(i);
else pop_ceil_bake_empty(i);
+ cnt++; /* ВРЕМЕННО: замер */
if (--rda_cnt[i]) left++;
else rda_kind[i] = 0;
}
}
rd_pending = left;
+ pop_dbg_rdmax_tot = cnt; /* ВРЕМЕННО: замер */
+ if (cnt > pop_dbg_rdmax[0]) pop_dbg_rdmax[0] = cnt;
}
diff --git a/applications/PoP/roomtest/pop_room.c b/applications/PoP/roomtest/pop_room.c
index 02fbcf9..4ab7fe9 100644
--- a/applications/PoP/roomtest/pop_room.c
+++ b/applications/PoP/roomtest/pop_room.c
@@ -1203,6 +1203,13 @@ static void mob_spawn(uint8_t room, int row, int col)
m->defer = 0;
m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE;
m->draw_y = MOB_Y_NONE;
+ { /* ВРЕМЕННО: журнал решения оверлея — копим за весь полёт */
+ uint8_t k = (uint8_t)(m - mobs);
+ pop_dbg_pass[k] = 0;
+ pop_dbg_ovl[k * 4 + 2] = 0;
+ pop_dbg_ovl[k * 4 + 3] = 0;
+ pop_dbg_tiles[k] = 0;
+ }
}
/* Отрыв куска в ТЕКУЩЕЙ комнате (обычный случай — pop_loose_tick). */
@@ -1581,7 +1588,24 @@ static void mob_overlay_neighbour(const mob_t *m)
int8_t rt = pop_y_to_row((int16_t)(m->draw_y - 18));
int8_t c = (int8_t)(m->col + 1);
int ytop, ybot;
+ uint8_t *dbg = &pop_dbg_ovl[(m - mobs) * 4]; /* ВРЕМЕННО: журнал решения */
+ { /* ВРЕМЕННО: перебор ВСЕХ тайлов, чьи габариты пересекаются со спрайтом
+ * куска — чтобы понять, скольким тайлам реально нужно лечь поверх. */
+ int8_t rr, kk;
+ int t0 = m->draw_y - (int)mob_spr[2] + 1, t1 = m->draw_y;
+ for (rr = -1; rr <= 2; rr++)
+ for (kk = 0; kk <= 1; kk++) {
+ int8_t cc = (int8_t)(m->col + kk);
+ if (cc > 9) continue;
+ if (pop_tile_code(rr, cc) &&
+ t1 >= 63 * rr + 2 && t0 <= 63 * rr + 66)
+ pop_dbg_tiles[m - mobs] |= (uint8_t)(1 << ((rr + 1) * 2 + kk));
+ }
+ }
+ dbg[0] = (uint8_t)(r + 128); dbg[1] = (uint8_t)(rt + 128);
+ dbg[2] |= 1;
if (c > 9) return;
+ dbg[2] |= 2;
/* Габарит куска — по ЭКРАННОЙ координате, как и ряд выше: у куска,
* видимого из соседней комнаты, draw_y отличается от y на 192, и
* сравнение с габаритом тайла по y давало заведомо ложный ответ. */
@@ -1598,12 +1622,20 @@ static void mob_overlay_neighbour(const mob_t *m)
* Габарит тайла берём ПОЛНЫМ (63*row+2 .. +66) — какая именно часть
* соседа окажется поверх куска, решит уже окно клипа внутри. Сужать по
* реальной высоте кусков нельзя: у колонн и зеркала база высокая. */
+ if (r >= 0 && r <= 2 && pop_tile_code(r, c)) dbg[2] |= 4;
+ if (r >= 0 && r <= 2 && ybot >= 63 * r + 2 && ytop <= 63 * r + 66) dbg[2] |= 8;
if (r >= 0 && r <= 2 && pop_tile_code(r, c) &&
- ybot >= 63 * r + 2 && ytop <= 63 * r + 66)
+ ybot >= 63 * r + 2 && ytop <= 63 * r + 66) {
+ dbg[2] |= 0x10; dbg[3]++;
pop_mob_overlay_tile(r, c, r, (int8_t)m->col, m->x, m->draw_y, mob_spr[2]);
+ }
+ if (rt != r && rt >= 0 && rt <= 2 && pop_tile_code(rt, c)) dbg[2] |= 0x20;
+ if (rt != r && rt >= 0 && rt <= 2 && ybot >= 63 * rt + 2 && ytop <= 63 * rt + 66) dbg[2] |= 0x40;
if (rt != r && rt >= 0 && rt <= 2 && pop_tile_code(rt, c) &&
- ybot >= 63 * rt + 2 && ytop <= 63 * rt + 66)
+ ybot >= 63 * rt + 2 && ytop <= 63 * rt + 66) {
+ dbg[2] |= 0x80; dbg[3]++;
pop_mob_overlay_tile(rt, c, r, (int8_t)m->col, m->x, m->draw_y, mob_spr[2]);
+ }
}
static void mob_draw_pass(uint8_t pg, uint8_t want_defer)
@@ -1618,6 +1650,10 @@ static void mob_draw_pass(uint8_t pg, uint8_t want_defer)
mob_t *m = &mobs[i];
/* Отбор по draw_y, а НЕ по комнате: кусок, только что провалившийся
* в комнату снизу, ещё виден из нашей (см. mob_tick_one). */
+ pop_dbg_pass[i] |= 1;
+ if (m->active) pop_dbg_pass[i] |= 2;
+ if (m->draw_y != MOB_Y_NONE) pop_dbg_pass[i] |= 4;
+ if ((uint8_t)(m->defer != 0) == want_defer) pop_dbg_pass[i] |= 8;
if (!m->active || m->draw_y == MOB_Y_NONE) continue;
if ((uint8_t)(m->defer != 0) != want_defer) continue;
for (j = n; j > 0 && mobs[order[j - 1]].draw_y < m->draw_y; j--)
@@ -1634,8 +1670,10 @@ static void mob_draw_pass(uint8_t pg, uint8_t want_defer)
* в кадр пересечения границы пометка указывала на прежний тайл, и
* передняя часть колонны кусок не перекрывала — ровно ОДИН кадр,
* как и наблюдалось (прогон 2026-08-13, комната 23, колонка 8). */
+ pop_dbg_pass[order[i]] |= 0x10;
mob_mark_neighbour(&mobs[order[i]]);
mob_render(&mobs[order[i]], pg);
+ pop_dbg_pass[order[i]] |= 0x20;
mob_overlay_neighbour(&mobs[order[i]]);
}
}
diff --git a/applications/PoP/roomtest/pop_state.c b/applications/PoP/roomtest/pop_state.c
index 27f8bb1..e97c9cf 100644
--- a/applications/PoP/roomtest/pop_state.c
+++ b/applications/PoP/roomtest/pop_state.c
@@ -87,6 +87,20 @@ void pop_dbg_trap(void) { pop_dbg_hits++; }
* смена комнаты видна по скачку tilepos, а нам удобнее явно). */
int8_t pop_dbg_obj[9];
+/* ВРЕМЕННО (разбор оверлея падающего куска): по слоту mob.
+ * pop_dbg_pass[i] — что было в проходе отрисовки:
+ * bit0 рассмотрен, bit1 active, bit2 draw_y задан, bit3 defer совпал,
+ * bit4 нарисован, bit5 позван mob_overlay_neighbour.
+ * pop_dbg_ovl[i*4 + 0..3] — что решил mob_overlay_neighbour:
+ * [0] r+128, [1] rt+128, [2] флаги, [3] col.
+ * Флаги: bit0 вошли, bit1 c<=9, bit2 tile(r,c), bit3 габарит r,
+ * bit4 ВЫЗОВ по r, bit5 tile(rt,c), bit6 габарит rt, bit7 ВЫЗОВ по rt. */
+uint8_t pop_dbg_pass[14];
+uint8_t pop_dbg_ovl[14 * 4];
+/* ВРЕМЕННО: какие тайлы РЕАЛЬНО перекрывают спрайт куска. Бит
+ * (ряд+1)*2 + (0 своя колонка / 1 соседняя), ряды −1..2. */
+uint8_t pop_dbg_tiles[14];
+
/* Зеркало уровня 4 — см. pop_state.h. */
int8_t pop_jumped_mirror;
diff --git a/applications/PoP/roomtest/pop_state.h b/applications/PoP/roomtest/pop_state.h
index 2cd512e..d3a5f87 100644
--- a/applications/PoP/roomtest/pop_state.h
+++ b/applications/PoP/roomtest/pop_state.h
@@ -101,4 +101,8 @@ void pop_dbg_wh(uint16_t wh);
* 0 obj_tilepos, 1 frame, 2 action, 3 curr_col, 4 char_col_left,
* 5 char_col_right, 6 char_top_row, 7 char_bottom_row, 8 комната. */
extern int8_t pop_dbg_obj[9];
+/* ВРЕМЕННО: разбор оверлея падающего куска (см. pop_state.c). */
+extern uint8_t pop_dbg_pass[14];
+extern uint8_t pop_dbg_ovl[14 * 4];
+extern uint8_t pop_dbg_tiles[14];
void pop_dbg_kidobj(void) __banked;