diff --git a/applications/PoP/roomtest/BUGS_CLOSED.md b/applications/PoP/roomtest/BUGS_CLOSED.md
index 3953d48..37b3deb 100644
--- a/applications/PoP/roomtest/BUGS_CLOSED.md
+++ b/applications/PoP/roomtest/BUGS_CLOSED.md
@@ -2909,3 +2909,81 @@ case tiles_20_wall:
после: изменения ровно в боксе `(344,199)-(358,206)`, 38 пикселей — сам
артефакт и ничего кроме. На подземных уровнях изменений нет по построению:
полоска целиком под `if (PALACE)`.
+
+---
+
+
+## SPIKE-BAKED. Выдвинутые пики консервировались в фон и оставались навсегда
+
+**Наблюдение (пользователь, 2026-08-18).** Уровень 7, комната 19. Пики стоят
+в `(0,6)` и из-за изометрии частью выходят в ячейку `(0,7)`. Выдвигаются
+корректно все; **прячутся корректно только те, что в `(0,7)`**, а в `(0,6)`
+остаются два вида мусора: куски остриёв на фоне дальней стены и белые точки в
+дырах пола. Пользователь сразу указал и на класс («первоначальная отрисовка
+фона, фикс не должен повлиять на скорость»), и на причину («в `(0,5)` не пол,
+а кнопка») — оба попадания верные.
+
+**Артефакты, снятые в MAME на живом кадре с мусором.**
+
+| что | значение |
+|-----|----------|
+| `pop_t_bg[6]` (живой modif пики) | `0x00` — пики УБРАНЫ |
+| `pop_t_fg[0..9]` | `03 04 13 0F 01 06 02 04 01 03` → `(0,5)` = `0x06` closer-кнопка, `(0,6)` = `0x02` spike |
+| мусор на экране | x 198..215, y 63..88 |
+
+Модификатор нулевой, а пиксели пик есть — значит рисует их не живое
+состояние. И `pop_spike_redraw` каждый кадр делает `pop_heal_off(192, 62,
+64, 40)` — ровно поверх этого прямоугольника. Мусор переживает СВОЙ ЖЕ heal,
+а heal восстанавливает ОЗУ-копию фона: пики в ней и лежат.
+
+**Корень.** Кнопка при смене состояния метится `POP_RD_FLOOR`, а это
+`pop_floor_bake(0,5)` — ЗАПЕЧКА, то есть рисование в банке, который пишет и в
+ОЗУ-копию. Запечка перерисовывает свой тайл **и правого соседа**:
+
+```c
+pop_t_bake_rest = 1;
+draw_tile(row, col);
+if (col + 1 < 10) draw_tile(row, col + 1); /* <- это пики (0,6) */
+```
+
+`pop_t_bake_rest` для того и заведён — «в фон кладём состояние ПОКОЯ», — но
+читал его ТОЛЬКО `pop_loose_frame`. Кадр пик считался прямо по месту,
+`(m & 0x80) ? 5 : m`, в ЧЕТЫРЁХ слоях, и ни один флага не спрашивал. Так
+выдвинутая пика попадала в ОЗУ-копию, и дальше heal возвращал её вечно.
+
+Всё сходится численно. Окно клипа запечки — `x 160..219` (ширина 60 от
+кнопки), спрайт пики — `x 192..222`, `y 62..88`. Пересечение `x 192..219` —
+это и есть наблюдаемые 198..215; а `(0,7)` начинается с 224, за окном
+запечки, — потому там и чисто. Правая часть пики живёт только в видео-ОЗУ и
+честно стирается.
+
+**Фикс (`pop_tile.c`).** Порт `get_spike_frame` (SDLPoP seg008:08A0), которого
+у нас не было, — плюс наша платформенная оговорка про запечку:
+
+```c
+uint8_t pop_spike_frame(uint8_t m)
+{
+ if (pop_t_bake_rest) return 0;
+ return (uint8_t)((m & 0x80) ? 5 : m);
+}
+```
+
+Все четыре слоя (`draw_tile_anim`, `draw_tile_anim_right` соседа, fore-проход,
+mid-оверлей) переведены на него. Заодно `pop_chomp_pose` тоже стал отдавать
+позу покоя в запечке: у чомпера ровно та же схема, а рядом с кнопкой он
+встретится так же легко.
+
+Одна точка вместо четырёх лечит ВСЕ пять путей запечки сразу, включая
+`pop_room_draw` — вход в комнату с выдвинутыми пиками тоже консервировал бы их.
+
+**Цена.** Резидент +28 Б, банки без изменений; в кадре — один `call` на слой,
+и только на тайлах-пиках. Скорость не затронута, как и предполагал
+пользователь.
+
+**Проверка.** Пользователь прошёл сценарий вручную на пересобранном билде —
+мусора нет.
+
+**Урок (записан в `pop_tile.h` над `pop_t_bake_rest`).** Флаг «сейчас
+запечка» обязана спрашивать САМА функция «кадр по модификатору», а не
+вызывающий слой: слоёв отрисовки у тайла четыре, и забыть один слишком легко.
+Для новой анимированной ловушки это теперь обязательный пункт.
diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md
index 763f1e9..fe38f0f 100644
--- a/applications/PoP/roomtest/TASKS_OPEN.md
+++ b/applications/PoP/roomtest/TASKS_OPEN.md
@@ -873,6 +873,46 @@ tp/10 у факелов таблицей, пустой слот соперник
## P1 — берётся в любой момент
+### T-RENDER. Тесты слоя отрисовки: «в запечку не попадает транзиент»
+
+**Откуда взялось** (пользователь, 2026-08-18, по итогам
+[SPIKE-BAKED](BUGS_CLOSED.md#spike-baked)): «такой сценарий (и другие с
+пиками) можно внести в наши тесткейсы?»
+
+**Что есть сейчас.** `tests-host/` — это ЛОГИКА: геометрия, физика Кида,
+`Char`, спецсобытия. Отрисовки там нет вовсе (`stubs.c` глушит все блиты
+пустышками), `pop_tile.c`/`pop_room.c`/`pop_bg.c` не линкуются — таблицы
+деления на ширину тайла в стабах даже продублированы копией. **Ловушки не
+покрыты ничем**: ни пик, ни чомперов, ни `animate_*`. То есть SPIKE-BAKED
+тестами поймать было нельзя в принципе.
+
+**Что покрывать.** Не «картинку» (её проверяет глаз в MAME), а инвариант,
+на котором держится вся наша запечка:
+
+> при `pop_t_bake_rest = 1` ни один слой отрисовки тайла не выдаёт
+> ТРАНЗИЕНТНЫЙ спрайт — ни своей ячейки, ни соседней.
+
+Он проверяем механически и стоит ровно того: именно его нарушение дало
+SPIKE-BAKED, а до того — мерцание плиты, ради которого флаг и заводился.
+
+**Как.** Новый набор `t_render`: линкуются НАСТОЯЩИЕ `pop_tile.c`,
+`pop_room.c`, `pop_bg.c`, а `pop_env_b`/`pop_wall_b`/`pop_fore_b` подменяются
+**записывающим** стабом — списком `(id, x, yb)` за вызов. Тогда проверка
+пишется в одну строку: «`draw_tile` при `bake_rest=1` и modif пик 4 не выдал
+ни одного id из `spikes_fram_*`». Тот же стаб сразу закрывает и чомпера, и
+плиту, и будущие ловушки — по одному тесту на каждую.
+
+Отдельно и дёшево — характеризация автоматов `animate_spike`/`animate_chomper`
+(сверка полного цикла модификатора с SDLPoP seg007), но она НЕ ловит этот
+класс: автомат был верен, врал слой отрисовки.
+
+**Чего это стоит.** Не одна строка: набор тянет за собой атласы и
+`pop_cd_*`, а `stubs.c` придётся разделить (сейчас он подменяет то, что
+здесь нужно настоящим). Прикидка — отдельная сессия. Место в `DATA_LOC`
+проверить заранее: набор линкует ещё три крупных модуля (см. грабли
+раскладки в `tests-host/README.md`).
+
+
### QSAVE. QuickSave / QuickLoad
> **План целиком — [`../docs/quicksave_plan.md`](../docs/quicksave_plan.md)**
diff --git a/applications/PoP/roomtest/pop_bg.c b/applications/PoP/roomtest/pop_bg.c
index 9b404f3..b6e5ef0 100644
--- a/applications/PoP/roomtest/pop_bg.c
+++ b/applications/PoP/roomtest/pop_bg.c
@@ -593,8 +593,7 @@ static void fore_only_tile(int row, int col)
return;
}
if (code == 2) { /* пики: fore-кадр */
- uint8_t sm = pop_tile_mod(row, col);
- uint8_t sf = (sm & 0x80) ? 5 : sm;
+ uint8_t sf = pop_spike_frame(pop_tile_mod(row, col));
if (sf > 0 && sf < 10 && SPIKES_FRAM_FORE[sf])
pop_env_b(SPIKES_FRAM_FORE[sf], x, dmy - 2);
}
@@ -770,8 +769,7 @@ static void overlay_mid_tile(int row, int col)
pop_env_b(base_id, x, t->base_y + dmy);
}
if (code == 2) { /* draw_tile_anim: свои пики */
- uint8_t sm = pop_tile_mod(row, col);
- uint8_t sf = (sm & 0x80) ? 5 : sm;
+ uint8_t sf = pop_spike_frame(pop_tile_mod(row, col));
if (sf > 0 && sf < 10 && POP_SPIKES_FRAM_LEFT[sf])
pop_env_b(POP_SPIKES_FRAM_LEFT[sf], x, dmy - 2);
}
diff --git a/applications/PoP/roomtest/pop_room.c b/applications/PoP/roomtest/pop_room.c
index ab5d5e8..0dfbc3c 100644
--- a/applications/PoP/roomtest/pop_room.c
+++ b/applications/PoP/roomtest/pop_room.c
@@ -492,7 +492,7 @@ static void draw_tile(int row, int col)
} else if (dt_lcode == 11) { /* loose: правая грань (анимир. по modif соседа-loose) */
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 (dt_lcode == 2) { /* spike: статич. кадр */
- uint8_t sf = (dt_lmod & 0x80) ? 5 : dt_lmod;
+ uint8_t sf = pop_spike_frame(dt_lmod);
if (sf < 10 && SPIKES_FRAM_RIGHT[sf])
pop_env_b(SPIKES_FRAM_RIGHT[sf], dt_x, dt_dmy - 7);
} else if (dt_lcode == 16) { /* дверь уровня */
@@ -551,8 +551,7 @@ static void draw_tile(int row, int col)
* рисуется только правым соседом со сдвигом → шахта над самой пикой
* пустая. */
if (dt_row >= 0 && dt_code == 2) {
- uint8_t sm = pop_tile_mod(dt_row, dt_col);
- uint8_t sf = (sm & 0x80) ? 5 : sm;
+ uint8_t sf = pop_spike_frame(pop_tile_mod(dt_row, dt_col));
if (sf > 0 && sf < 10 && POP_SPIKES_FRAM_LEFT[sf])
pop_env_b(POP_SPIKES_FRAM_LEFT[sf], dt_x, dt_dmy - 2);
}
diff --git a/applications/PoP/roomtest/pop_tile.c b/applications/PoP/roomtest/pop_tile.c
index 110606c..5472195 100644
--- a/applications/PoP/roomtest/pop_tile.c
+++ b/applications/PoP/roomtest/pop_tile.c
@@ -153,7 +153,11 @@ const uint8_t POP_CHOMP_FRAM_FOR[5] = {106, 107, 108, 109, 110};
uint8_t pop_chomp_pose(uint8_t modif)
{
uint8_t m = (uint8_t)(modif & 0x7F);
- if (m > 6) m = 6;
+ /* В ЗАПЕЧКЕ — поза ПОКОЯ, по той же причине, что у плиты и пик: фаза
+ * челюстей транзиентна. Покой и есть фаза 0 (она же 6, на которой
+ * animate_chomper снимает trob): CHOMP_FRAM1[0] == CHOMP_FRAM1[6]. */
+ if (pop_t_bake_rest) m = 0;
+ else if (m > 6) m = 6;
return CHOMP_FRAM1[m];
}
@@ -167,6 +171,20 @@ uint8_t pop_loose_frame(uint8_t m)
return m;
}
+/* Кадр пик по их модификатору: 1..4 — выдвижение, 0x80|n — «вышли целиком»
+ * (один кадр 5), 6..8 — уход. Как и у плиты, в режиме ЗАПЕЧКИ отдаёт кадр
+ * ПОКОЯ (0 = пик не видно): выдвинутая пика — транзиент, её место в ОЗУ-копии
+ * фона делает heal вечным (BUG-SPIKE-BAKED, см. BUGS_CLOSED.md).
+ *
+ * Отдельная функция, а не `(m & 0x80) ? 5 : m` по месту: считают кадр ЧЕТЫРЕ
+ * слоя (draw_tile_anim, draw_tile_anim_right соседа, fore-проход и
+ * mid-оверлей), и одного забытого хватило, чтобы пики законсервировались. */
+uint8_t pop_spike_frame(uint8_t m)
+{
+ if (pop_t_bake_rest) return 0;
+ return (uint8_t)((m & 0x80) ? 5 : m);
+}
+
/* ---- «Фон трогали ВОТ ЗДЕСЬ» (DRAW-COST; контракт — pop_cdraw.h) ----- *
*
* Метка — БИТОВАЯ МАСКА ТАЙЛОВ, по слову на ряд и по набору на страницу.
diff --git a/applications/PoP/roomtest/pop_tile.h b/applications/PoP/roomtest/pop_tile.h
index aede14a..5bee865 100644
--- a/applications/PoP/roomtest/pop_tile.h
+++ b/applications/PoP/roomtest/pop_tile.h
@@ -121,16 +121,27 @@ extern const uint8_t POP_LOOSE_FRAM_BOTTOM[12]; /* низ дрожащей пл
extern const uint8_t POP_CHOMP_FRAM_FOR[5]; /* чомпер: перед (pop_fore!) */
/* Режим ЗАПЕЧКИ статического фона (запись идёт и в ОЗУ-копию акселератора,
- * из которой потом восстанавливает heal). В этом режиме дрожащие кадры
- * ЧУЖИХ плит рисовать НЕЛЬЗЯ: транзиентный кадр законсервируется в копии, а
- * так как запечка идёт двумя кадрами (по одному на страницу дабл-буфера),
- * на страницах застынут РАЗНЫЕ кадры — вечное мерцание. Фон обязан быть
- * состоянием ПОКОЯ; анимация каждый кадр кладётся поверх (банк SPRITE). */
+ * из которой потом восстанавливает heal). В этом режиме ТРАНЗИЕНТНЫЕ кадры
+ * рисовать НЕЛЬЗЯ — ни свои, ни соседские: кадр законсервируется в копии, и
+ * heal будет возвращать его вечно, даже когда ловушка давно вернулась в
+ * покой (пики: BUG-SPIKE-BAKED). А так как запечка идёт двумя кадрами (по
+ * одному на страницу дабл-буфера), на страницах могут застыть ещё и РАЗНЫЕ
+ * кадры — вечное мерцание. Фон обязан быть состоянием ПОКОЯ; анимация
+ * каждый кадр кладётся поверх (банк SPRITE).
+ *
+ * Флаг читают ВСЕ функции «кадр по модификатору»: pop_loose_frame,
+ * pop_spike_frame, pop_chomp_pose. Появится новая анимированная ловушка —
+ * её кадр обязан спрашивать флаг ТАМ ЖЕ, а не в вызывающем слое: слоёв
+ * отрисовки у тайла четыре, и забыть один слишком легко. */
extern uint8_t pop_t_bake_rest;
/* Кадр дрожащей/падающей плиты по её room_modif (seg007 loose_fram_*). */
uint8_t pop_loose_frame(uint8_t modif);
+/* Кадр пик по их modif (индекс в spikes_fram_*): 0 = не видно, 5 = вышли
+ * целиком. В режиме запечки — всегда 0 (см. pop_t_bake_rest). */
+uint8_t pop_spike_frame(uint8_t modif);
+
/* Номер позы чомпера по его модификатору (seg008 chomper_fram1): 0..4 —
* индекс во ВСЕ три набора кусков (низ/верх/перед) и в кровь (+114/+119). */
uint8_t pop_chomp_pose(uint8_t modif);