diff --git a/applications/PoP/docs/l9_invert_plan.md b/applications/PoP/docs/l9_invert_plan.md
index d9d53fb..481fe1f 100644
--- a/applications/PoP/docs/l9_invert_plan.md
+++ b/applications/PoP/docs/l9_invert_plan.md
@@ -297,7 +297,20 @@ void gfx_copy_rect(uint8_t src_page, int sx, int sy,
---
-# Часть III — побочная задача ENTER-ROOM-FAST
+# Часть III — ENTER-ROOM-FAST — СДЕЛАНО 2026-08-12
+
+Комната теперь рисуется ОДИН раз — в скрытую страницу, — тут же
+показывается флипом, а вторая страница получает её `gfx_copy_page(DIRECT)`.
+Процесс рисования тайлов больше не виден: игрок получает готовый кадр.
+Проверено в MAME: старт уровня и чит-переход между комнатами, два
+последовательных снимка идентичны (обе страницы синхронны, мерцания нет).
+
+Побочно понадобилось: видимую страницу главный цикл больше не ведёт сам, а
+перечитывает у железа в начале кадра (`front = gfx_get_visible_page()`) —
+её меняют и вход в комнату, и переворот, причём оба из банка, где локальные
+переменные `main` недоступны.
+
+# Часть III (исходное описание)
Идея пользователя, самостоятельная ценность (сейчас при входе в комнату видно,
как рисуются тайлы): рисовать комнату ОДИН раз в скрытую страницу, показать её
diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md
index bc82caf..f7b53c7 100644
--- a/applications/PoP/roomtest/TASKS_OPEN.md
+++ b/applications/PoP/roomtest/TASKS_OPEN.md
@@ -158,6 +158,11 @@ fore-слоя, и клип (`clip_char`), и брызги; полоса HP и л
### MEM-COLD2. Второй шаг разгрузки резидента
+> **Пункт 2 сделан 2026-08-12**: `guard_over_kid` с хелперами
+> (`objtile_at_char` / `char_x_left_of` / `tile_div_mod`) уехал в банк 8 —
+> куча 1005 → **1574 Б**. Зовётся раз в кадр, то есть цена — один трамплин.
+> Остались пункты 1 (`enter_room_side`) и 3 (блок редроя шва).
+
После [MEM-COLD1](TASKS_CLOSED.md#mem-cold1) куча 1707 Б. Когда упрётся
снова, в банк 8 переезжают следующие кандидаты (в порядке выгоды):
diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c
index db7a225..d678ab1 100644
--- a/applications/PoP/roomtest/roomtest.c
+++ b/applications/PoP/roomtest/roomtest.c
@@ -9,6 +9,7 @@
#include
#include
#include
+#include /* gfx_copy_page — вход в комнату и переворот */
#include "pop_bg.h"
#include "pop_kid.h"
#include "pop_ctrl.h"
@@ -145,89 +146,6 @@ static void update_kid_render_dx(void)
pop_char_set_render_dx(POP_CH_KID, dx);
}
-/* ---- Порядок «Kid vs страж» (BUG-DRAWORDER-1) -----------------------
- *
- * В оригинале никто не «поверх» по определению: и Кид, и страж попадают в
- * midtable при обработке СВОЕГО тайла (set_objtile_at_char, seg006:13F3), а
- * тайлы обходятся в фиксированном порядке — redraw_needed_tiles (seg008:1B06)
- * идёт рядами 2,1,0 и внутри ряда колонками 0..9. Кто нарисован ПОЗЖЕ — тот
- * поверх. Отсюда «Кид проходит ЗА телом стража»: тело лежит правее и/или
- * выше, его тайл в обходе позже. Совпали тайлы — решает sort_curr_objs
- * (seg008:203C): у обычных объектов (не тень, не плита) ниже по obj_y = позже.
- *
- * Мы рисовали страж→Кид всегда, то есть Кид был поверх безусловно.
- *
- * Ключ обхода: чем больше, тем позже. Ряды идут 2,1,0 → (2 - row). */
-#define OBJ_ORDER_KEY(row, col) ((int)(2 - (row)) * 10 + (int)(col))
-
-/* get_tile_div_mod (seg006:750): колонка комнаты по X (БЕЗ поправки −7). */
-static int8_t tile_div_mod(int x)
-{
- int v = x - 58; /* pop_x_bump[FIRST_ONSCREEN_COLUMN] */
- int c = v / TILE_SIZEX;
- if (v % TILE_SIZEX < 0) --c;
- return (int8_t)c;
-}
-
-/* char_x_left из set_char_collision (seg006:0723) — левая граница габарита.
- * ВАЖНО: величина НЕ утоньшённая; поправку THIN оригинал применяет только к
- * *_coll, а колонки char_col_left/right считает по этой. */
-static int char_x_left_of(uint8_t x, int8_t dir, int8_t dx,
- uint8_t flags, uint16_t w)
-{
- int e = (int)x + ((dir < 0) ? -dx : dx); /* char_dx_forward */
- uint8_t fl = flags;
- if (dir < 0) fl ^= 0x80;
- /* odd-pixel + усечение деления к нулю — та же поправка, что в pop_map */
- if (!(fl & 0x80) && e <= 57) e++;
- if (dir >= 0) e -= (int)((w + 1) >> 1); /* char_width_half */
- return e;
-}
-
-/* set_objtile_at_char (seg006:13F3) — ТАЙЛ ОБЪЕКТА персонажа.
- *
- * Ветка actions_1_run_jump обязательна, а не «упрощаемая» (как я решил
- * сначала): в беге оригинал берёт НЕ curr_row/curr_col, а нижний ряд и ЛЕВУЮ
- * колонку ГАБАРИТА. При беге вправо левая колонка меньше curr_col, поэтому
- * бегущий Кид уходит ЗА объекты справа от себя. Без этой ветки часть кадров
- * бега рисовала Кида поверх лежащего стража. Считаем одинаково для обоих:
- * enter_guard (seg002) тоже ставит стражу action = 1, и односторонний учёт
- * дал бы перекос в другую сторону. */
-static void objtile_at_char(int8_t *row, int8_t *col,
- const pop_char_t *ch, int8_t dx, int8_t dy,
- uint8_t flags, uint16_t w)
-{
- int8_t r = ch->curr_row, c = ch->curr_col;
- if (ch->action == 1) { /* actions_1_run_jump */
- int8_t br = pop_y_to_row((int16_t)((int)ch->y + dy));
- if (br == -1) br = 3; /* char_bottom_row */
- r = br;
- c = tile_div_mod(char_x_left_of(ch->x, ch->direction, dx, flags, w));
- if (c < 0) c = 0; /* MAX(char_col_left, 0) */
- }
- if ((ch->frame >= 135 && ch->frame < 149) ||
- ch->action == 2 || ch->action == 3 ||
- ch->action == 4 || ch->action == 6) c--;
- *row = r; *col = c;
-}
-
-/* 1 = страж рисуется ПОСЛЕ Кида (поверх). */
-static uint8_t guard_over_kid(void)
-{
- int8_t kr, kc, gr, gc;
- int kk, gk;
- if (Guard.charid == 0 || Guard.room != cur_room) return 0;
- objtile_at_char(&kr, &kc, &Kid, kid_cur_dx(), kid_cur_dy(),
- kid_cur_flags(), pop_cd[POP_CH_KID].fpw);
- objtile_at_char(&gr, &gc, &Guard, pop_gframe.dx, pop_gframe.dy,
- pop_gframe.flags, pop_cd[POP_CH_OPP].fpw);
- kk = OBJ_ORDER_KEY(kr, kc);
- gk = OBJ_ORDER_KEY(gr, gc);
- if (gk != kk) return (uint8_t)(gk > kk);
- /* один тайл — ниже по obj_y рисуется позже (sort_curr_objs) */
- return (uint8_t)((int)Guard.y + pop_gframe.dy > pop_cd[POP_CH_KID].fpy);
-}
-
/* Change-driven редрой ЛЕВОГО шва (ворота соседа room8 видны в col0): тяжёлый
* draw_tile(0,0) в pop_room_redraw_seam_left зовём ТОЛЬКО когда openness гейта
* реально изменился. Пока Кид держит кнопку и гейт открыт — modifier стабилен
@@ -379,11 +297,31 @@ static void enter_room_side(uint8_t room, uint8_t side)
rcol_bg[0] = m[0]; rcol_bg[1] = m[10]; rcol_bg[2] = m[20]; }
}
seam_redraw = 0; seam_rows = 0; /* шов свежий на обеих страницах — редрой не нужен */
- for (pg = 0; pg < 2; pg++) {
+ /* ENTER-ROOM-FAST: комната рисуется ОДИН раз — в скрытую страницу, — и
+ * тут же показывается флипом; вторая страница дабл-буфера получает её
+ * КОПИЕЙ акселератора (gfx_copy_page, ~384 burst-строки против почти
+ * полумиллиона тактов на вторую отрисовку). Побочный эффект важнее
+ * тактов: игрок больше не видит, КАК рисуются тайлы — только готовый
+ * кадр.
+ *
+ * Копия идёт банком 0x50, то есть обновляет и VRAM, и ОЗУ-копию
+ * страницы-приёмника — иначе heal на ней восстанавливал бы фон прошлой
+ * комнаты (инвариант gfx_copy_page, регресс tests/pageflip).
+ *
+ * Видимая страница здесь МЕНЯЕТСЯ, поэтому главный цикл перечитывает её
+ * в начале каждого кадра (front = gfx_get_visible_page()), а не ведёт
+ * свой счёт. */
+ {
+ gfx_rect_t full;
+ pg = (uint8_t)(gfx_get_visible_page() ^ 1); /* скрытая */
gfx_set_draw_page(pg);
setfillstyle(SOLID_FILL, BLACK);
bar(0, 0, 319, 255);
pop_room_draw(room, room_fg, room_mod, lcol_fg, lcol_bg);
+ gfx_set_visible_page(pg); /* показать готовое */
+ gfx_set_draw_page((uint8_t)(pg ^ 1));
+ full.x = 0; full.y = 0; full.w = 320; full.h = 256;
+ gfx_copy_page(&full, GFX_COPY_DIRECT); /* вторую — копией */
}
cur_room = room;
kid_room = room; /* S1: комната Kid синхронна отрисованной (S3 разведёт) */
@@ -584,7 +522,11 @@ int main(void)
}
#endif
- /* Рисуем в СКРЫТУЮ страницу (при dbuf); в однобуфере — в 0. */
+ /* Рисуем в СКРЫТУЮ страницу (при dbuf); в однобуфере — в 0.
+ * Видимую перечитываем У ЖЕЛЕЗА, а не ведём свой счёт: её меняют и
+ * вход в комнату (ENTER-ROOM-FAST), и переворот экрана
+ * (pop_flip_screen), причём оба — из банка, где front недоступен. */
+ front = gfx_get_visible_page();
back = dbuf ? (uint8_t)(front ^ 1) : 0;
gfx_set_draw_page(back);
@@ -595,8 +537,7 @@ int main(void)
if (pop_upside_dirty) {
pop_upside_dirty = 0;
pop_flip_screen();
- front = gfx_get_visible_page();
- back = dbuf ? (uint8_t)(front ^ 1) : 0;
+ back = dbuf ? (uint8_t)(gfx_get_visible_page() ^ 1) : 0;
gfx_set_draw_page(back);
}
pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */
diff --git a/applications/PoP/roomtest/roomtest_cold.c b/applications/PoP/roomtest/roomtest_cold.c
index 315e33d..602c307 100644
--- a/applications/PoP/roomtest/roomtest_cold.c
+++ b/applications/PoP/roomtest/roomtest_cold.c
@@ -255,6 +255,91 @@ void pop_dbg_room_label(uint8_t room) __banked
room_label_redraw--;
}
+/* ---- Порядок «Kid vs страж» (BUG-DRAWORDER-1) -----------------------
+ *
+ * В оригинале никто не «поверх» по определению: и Кид, и страж попадают в
+ * midtable при обработке СВОЕГО тайла (set_objtile_at_char, seg006:13F3), а
+ * тайлы обходятся в фиксированном порядке — redraw_needed_tiles (seg008:1B06)
+ * идёт рядами 2,1,0 и внутри ряда колонками 0..9. Кто нарисован ПОЗЖЕ — тот
+ * поверх. Отсюда «Кид проходит ЗА телом стража»: тело лежит правее и/или
+ * выше, его тайл в обходе позже. Совпали тайлы — решает sort_curr_objs
+ * (seg008:203C): у обычных объектов (не тень, не плита) ниже по obj_y = позже.
+ *
+ * Мы рисовали страж→Кид всегда, то есть Кид был поверх безусловно.
+ *
+ * Ключ обхода: чем больше, тем позже. Ряды идут 2,1,0 → (2 - row). */
+#define OBJ_ORDER_KEY(row, col) ((int)(2 - (row)) * 10 + (int)(col))
+
+/* get_tile_div_mod (seg006:750): колонка комнаты по X (БЕЗ поправки −7). */
+static int8_t tile_div_mod(int x)
+{
+ int v = x - 58; /* pop_x_bump[FIRST_ONSCREEN_COLUMN] */
+ int c = v / TILE_SIZEX;
+ if (v % TILE_SIZEX < 0) --c;
+ return (int8_t)c;
+}
+
+/* char_x_left из set_char_collision (seg006:0723) — левая граница габарита.
+ * ВАЖНО: величина НЕ утоньшённая; поправку THIN оригинал применяет только к
+ * *_coll, а колонки char_col_left/right считает по этой. */
+static int char_x_left_of(uint8_t x, int8_t dir, int8_t dx,
+ uint8_t flags, uint16_t w)
+{
+ int e = (int)x + ((dir < 0) ? -dx : dx); /* char_dx_forward */
+ uint8_t fl = flags;
+ if (dir < 0) fl ^= 0x80;
+ /* odd-pixel + усечение деления к нулю — та же поправка, что в pop_map */
+ if (!(fl & 0x80) && e <= 57) e++;
+ if (dir >= 0) e -= (int)((w + 1) >> 1); /* char_width_half */
+ return e;
+}
+
+/* set_objtile_at_char (seg006:13F3) — ТАЙЛ ОБЪЕКТА персонажа.
+ *
+ * Ветка actions_1_run_jump обязательна, а не «упрощаемая» (как я решил
+ * сначала): в беге оригинал берёт НЕ curr_row/curr_col, а нижний ряд и ЛЕВУЮ
+ * колонку ГАБАРИТА. При беге вправо левая колонка меньше curr_col, поэтому
+ * бегущий Кид уходит ЗА объекты справа от себя. Без этой ветки часть кадров
+ * бега рисовала Кида поверх лежащего стража. Считаем одинаково для обоих:
+ * enter_guard (seg002) тоже ставит стражу action = 1, и односторонний учёт
+ * дал бы перекос в другую сторону. */
+static void objtile_at_char(int8_t *row, int8_t *col,
+ const pop_char_t *ch, int8_t dx, int8_t dy,
+ uint8_t flags, uint16_t w)
+{
+ int8_t r = ch->curr_row, c = ch->curr_col;
+ if (ch->action == 1) { /* actions_1_run_jump */
+ int8_t br = pop_y_to_row((int16_t)((int)ch->y + dy));
+ if (br == -1) br = 3; /* char_bottom_row */
+ r = br;
+ c = tile_div_mod(char_x_left_of(ch->x, ch->direction, dx, flags, w));
+ if (c < 0) c = 0; /* MAX(char_col_left, 0) */
+ }
+ if ((ch->frame >= 135 && ch->frame < 149) ||
+ ch->action == 2 || ch->action == 3 ||
+ ch->action == 4 || ch->action == 6) c--;
+ *row = r; *col = c;
+}
+
+/* 1 = страж рисуется ПОСЛЕ Кида (поверх). __banked: зовётся раз в кадр,
+ * то есть один трамплин — а 500 Б кода в резиденте дороже. */
+uint8_t guard_over_kid(void) __banked
+{
+ int8_t kr, kc, gr, gc;
+ int kk, gk;
+ if (Guard.charid == 0 || Guard.room != cur_room) return 0;
+ objtile_at_char(&kr, &kc, &Kid, kid_cur_dx(), kid_cur_dy(),
+ kid_cur_flags(), pop_cd[POP_CH_KID].fpw);
+ objtile_at_char(&gr, &gc, &Guard, pop_gframe.dx, pop_gframe.dy,
+ pop_gframe.flags, pop_cd[POP_CH_OPP].fpw);
+ kk = OBJ_ORDER_KEY(kr, kc);
+ gk = OBJ_ORDER_KEY(gr, gc);
+ if (gk != kk) return (uint8_t)(gk > kk);
+ /* один тайл — ниже по obj_y рисуется позже (sort_curr_objs) */
+ return (uint8_t)((int)Guard.y + pop_gframe.dy > pop_cd[POP_CH_KID].fpy);
+}
+
+
/* ---- Переворот уже нарисованного (зелье инверсии, уровень 9) --------- *
*
* Порт need_redraw_because_flipped (seg000:15E9 + 939/946). Оригинал
diff --git a/applications/PoP/roomtest/roomtest_cold.h b/applications/PoP/roomtest/roomtest_cold.h
index cadcb65..4fbefe8 100644
--- a/applications/PoP/roomtest/roomtest_cold.h
+++ b/applications/PoP/roomtest/roomtest_cold.h
@@ -50,6 +50,12 @@ uint8_t pop_dbg_roomnav(uint8_t dir) __banked;
* звать можно каждый кадр. */
void pop_dbg_room_label(uint8_t room) __banked;
+/* Кто рисуется ПОЗЖЕ — Кид или соперник (порядок обхода тайлов оригинала,
+ * set_objtile_at_char + sort_curr_objs). 1 = соперник поверх. Зовётся раз
+ * в кадр, поэтому живёт в банке: трамплин на кадр дешевле полукилобайта
+ * резидента. */
+uint8_t guard_over_kid(void) __banked;
+
/* Перевернуть уже нарисованное (зелье инверсии, уровень 9): две копии
* акселератором + инвалидация слотов персонажей и полосы HP. Звать РАЗ на
* переключение, по флагу pop_upside_dirty. */