From 6d7c1c8b6bf85717e59c960d07612e3dcd158a55 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Wed, 19 Aug 2026 10:49:19 +0300 Subject: [PATCH] =?UTF-8?q?P5:=20=D0=B3=D0=B5=D0=B9=D1=82=D1=8B=20=D1=85?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D1=81=D1=82=D0=BE=D0=B3=D0=BE=20=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=D0=B0=20=D0=B2=20pop=5Floose=5Ftick=20=E2=80=94=20=D0=BC?= =?UTF-8?q?=D0=B8=D0=BD=D1=83=D1=81=2033=20840=20=D1=82=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=20=D0=BD=D0=B0=20=D0=BA=D0=B0=D0=B4=D1=80=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Замер 11/15 показал, что loose-механика берёт 28 872 такта в комнате, где не анимируется ни одна плита и не летит ни один кусок. Раскладка зондами m9..m12: два цикла по тайлам 9 852, обход 14 слотов mob 12 090, поиск куска над головой Кида 5 868 (там ещё и банковый трамплин). Два гейта: loose_any (статик pop_map.c) — «идёт ли анимация плит». Ставят пять мест записи ненулевой фазы: make_loose_fall, ветка потолка в check_press, do_knock для обоих рядов и восстановление фазы из room_modif при входе в комнату. Снимает его сам цикл, по факту прохода, в котором не осталось ни одной живой фазы. pop_mob_busy (резидент pop_state.c) — «занят ли слот падающего куска» (active или дочистка clean). Ставит mob_alloc, снимает обход по факту пустой таблицы. В резиденте, а не в pop_room.c, потому что читает его pop_map из банка 3, а писучие статики банкового модуля наружу не видны. Гейт отвечает не на «есть ли в комнате плиты», а на «идёт ли анимация»: у лежащей плиты-потолка фаза нулевая, и крутить нечего (вопрос пользователя). Асимметрия намеренная — ложная единица стоит одного холостого прохода, ложный ноль стоит застывшей навсегда плиты, поэтому взвод стоит рядом с КАЖДОЙ записью, а снятие только по факту пустого прохода. Стало: 132 / 996 / 546, вся функция 28 872 -> 2 760. На кадре работа 801 768 -> 767 928. Ожидание по реестру было -28 000. Покрытие: новый phys_loose_gate_survives_room_change на пятое место взвода (фаза восстановлена входом в комнату) — единственное, которое не прогонял ни один тест, и дающее самый тихий отказ. Мутационная проверка: со снятым взводом тест падает (фаза 3 вместо 4). Заодно отладочный старт сразу в целевую комнату: make ROOM=15 POS=2 (дефолт), roomtest стартует в 11/15 с Кидом в (0,2). kid_init ставит x = x_bump[col] + TILE_SIZEX, а это левая граница СЛЕДУЮЩЕЙ колонки — с неё физика относила Кида в тайл чомпера, и он погибал на старте (найдено пользователем). Сдвиг внутрь на 2: колонку определяет весовая точка кадра, поэтому число снято замером, а не выведено геометрией. План работ между сессиями — docs/perf_registry.md §4: очередь позиций со статусами, текущий бюджет сцены, рецепт её воспроизведения и метод замера. Co-Authored-By: Claude Opus 5 --- applications/PoP/docs/perf_l11_room15.md | 13 ++ applications/PoP/docs/perf_registry.md | 96 +++++++- applications/PoP/roomtest/Makefile | 12 +- applications/PoP/roomtest/pop_map.c | 217 +++++++++++------- applications/PoP/roomtest/pop_room.c | 13 +- applications/PoP/roomtest/pop_state.c | 1 + applications/PoP/roomtest/pop_state.h | 10 + applications/PoP/roomtest/roomtest_cold.c | 32 +++ applications/PoP/roomtest/tests-host/t_phys.c | 33 +++ 9 files changed, 328 insertions(+), 99 deletions(-) diff --git a/applications/PoP/docs/perf_l11_room15.md b/applications/PoP/docs/perf_l11_room15.md index 5b30360..b2bc1e7 100644 --- a/applications/PoP/docs/perf_l11_room15.md +++ b/applications/PoP/docs/perf_l11_room15.md @@ -103,6 +103,19 @@ heal 32×64 ≈ 33 700, значит на один `draw_tile` уходит ≈ — та же болезнь (полный тайл там, где хватает полосы), но на другом материале. +## 5. Журнал правок по этой сцене + +| дата | правка | работа | синяя | зелёная | циан | +|---|---|---:|---:|---:|---:| +| 2026-08-19 | базовый замер (`09f32ce`) | 801 768 | 293 238 | 320 916 | 187 758 | +| 2026-08-19 | **P5**: гейты холостого хода в `pop_loose_tick` | **767 928** | 285 864 | 294 384 | 187 764 | +| | | −33 840 | −7 374 | −26 532 | +6 | + +Циан не изменился (+6 тактов — шум), и это ожидаемо: `loose_tick` целиком +лежит в зелёной. Синяя просела на 7 374 без прямой причины в правке — +скорее всего перераскладка кода банка 3 компилятором; проверять отдельно +не стали, знак верный. + **Полная очередь оптимизаций с оценками — [`perf_registry.md`](perf_registry.md).** Там же разложена цена одного блита фона по этапам (замер 2026-08-19, 1603 блита) и модель зелёной фазы этой сцены. diff --git a/applications/PoP/docs/perf_registry.md b/applications/PoP/docs/perf_registry.md index 0463249..bf2e4b7 100644 --- a/applications/PoP/docs/perf_registry.md +++ b/applications/PoP/docs/perf_registry.md @@ -120,13 +120,41 @@ heal 141 048 + отрисовка 148 566 = 290 000 тактов (36 % рабо Все четыре — низкий риск, механическая работа. Вместе снимают ~3 700 из 6 126 фиксированных. -### P5. `pop_loose_tick` при пустой комнате — 28 872 [замер: −28 000] +### P5. `pop_loose_tick` при пустой комнате — 28 872 → 2 760 ✅ СДЕЛАНО 2026-08-19 -В комнате 15 нет ни одной loose-плиты, а фаза всё равно платит 9 % зелёной. -Записано в `perf_backlog.md` §7 как 27 438 — цифра подтвердилась в другой -сцене, значит это постоянная плата, а не особенность 13/23. +**Получено −26 112 внутри функции, −33 840 на кадре** (замер до/после в +11/15). Оценка была −28 000. -Нужен ранний выход по «в комнате нет плит и нет летящих кусков». +Раскладка холостого хода (замер зондами m9..m12) и что с ней стало: + +| участок | было | стало | +|---|---:|---:| +| два цикла по тайлам (30 + 10 позиций) | 9 852 | **132** | +| `pop_loose_mob_tick` (обход 14 слотов) | 12 090 | **996** | +| `check_loose_fall_on_kid` (трамплин + обход) | 5 868 | **546** | +| вход + хвост | 1 062 | 1 086 | +| **итого** | **28 872** | **2 760** | + +Сделано двумя гейтами: + +- `loose_any` (статик `pop_map.c`) — «идёт ли анимация плит». Ставится в + пяти местах записи ненулевой фазы, снимается САМИМ циклом по факту + прохода, где не осталось ни одной живой фазы; +- `pop_mob_busy` (резидент `pop_state.c`) — «занят ли хоть один слот + падающего куска» (`active` или дочистка `clean`). Ставит `mob_alloc`, + снимает обход по факту пустой таблицы. В резиденте, а не в `pop_room.c`, + потому что читает его `pop_map` из банка 3. + +**Важно про границу:** гейт отвечает не на «есть ли в комнате плиты», а на +«идёт ли анимация». У лежащей плиты-потолка фаза 0, и крутить нечего — +вопрос пользователя 2026-08-19. Асимметрия намеренная: ложная единица +стоит одного холостого прохода, ложный ноль — застывшей навсегда плиты, +поэтому взвод стоит рядом с КАЖДОЙ записью, а снятие только по факту. + +Покрытие: `phys_loose_floor_breaks` (взвод от шага и сотрясения) и новый +`phys_loose_gate_survives_room_change` — на пятое место взвода +(фаза восстановлена входом в комнату), которое не покрывал никто. +Мутационная проверка: со снятым взводом тест падает. ### P6. Цикл `pop_process_trobs` — 59 100 без блитов пламени [гипотеза: −20 000 … −40 000] @@ -193,7 +221,61 @@ BUG-LATTICE-DOORTOP, BUG-SEAM-WEDGE-1) — только отдельным за --- -## 4. Сводка: что сколько даёт в 11/15 +## 4. ПЛАН РАБОТ — состояние между сессиями + +Рабочий чеклист. Правило: одна позиция = один заход = один коммит с замером +до/после на сцене 11/15. Замер обязателен даже когда «очевидно» — из шести +сделанных позиций две дали не то, что ожидалось. + +| # | что | статус | факт | +|---|---|---|---| +| P5 | `loose_tick`: гейты холостого хода | ✅ **сделано** 2026-08-19 | −33 840 на кадре (ждали −28 000) | +| P2 | замер логики двух Char | ⬜ следующее | нужна пересборка с зондами | +| P6 | замер цикла `process_trobs` | ⬜ вместе с P2 | та же пересборка | +| P1 | чомпер под факелом: только слой anim | ⬜ | ждём −160 000 | +| P4 | накладные блита (4 правки) | ⬜ | ждём −28 000 | +| P11 | мелочи с известной ценой | ⬜ | ждём −26 000 | +| P10 | футпринт персонажа из физики | ⬜ | ждём −23 000 | +| P7 | раскол `draw_tile` (G5) | ⬜ | для 13/23, не для 11/15 | +| P8/P9 | HEAL-WIDTH + G8 | ⬜ обязательная | для сцен с плитами | +| P12 | снять оснастку | ⬜ ПОСЛЕДНЕЙ | без неё не мерить | + +**Текущее состояние бюджета 11/15:** + +| | работа | синяя | зелёная | циан | период | +|---|---:|---:|---:|---:|---:| +| до оптимизации | 801 768 | 293 238 | 320 916 | 187 758 | 4 растра | +| после P5 | **767 928** | 285 864 | 294 384 | 187 764 | 4 растра | + +Цель — уложить РАБОТУ в 430 000 (тогда период станет 3 растра вместо 4; +хвост кадра — три `gfx_wait_vsync`). Осталось снять **338 000**. + +### Как воспроизвести сцену (важно для следующей сессии) + +Сборка стартует прямо в ней: `make` (дефолты `LEVEL=11 ROOM=15 POS=2`) → +`make hdd` → **полный рестарт MAME** (`chdman -f` даёт новый inode, memory +`mame_hdd_rebuild_restart`) → в DSS набрать `d:` и `roomtest`. Кид встаёт в +(0,2) лицом к чомперу, справа факел и страж — та самая сцена замеров. +Штатный старт уровня возвращается через `make ROOM=`. + +Проверка, что программа ЖИВА, обязательна перед любым чтением памяти: +`cur_room` (0x97AA) должен лежать в 1..24 — на этом уже был сорван один +замер (прочитаны два случайных байта остановленной машины). + +### Метод замера + +Зонды — `out (_io_border)` в `roomtest.c` (база модуля 0x42AD) плюс +резидентные пустышки `pop_dbg_m*` из `pop_state.c`. Адреса брать ЗАНОВО из +`.sprinter-cc-roomtest/roomtest.map` после каждой пересборки. Скрипты +сессии: `perfrun.py <сек> tag=addr ...` и `parseseq.py <файл> ПОСЛЕД`. + +Цену отдельной РЕЗИДЕНТНОЙ функции можно снять вообще без пересборки: +`bpset <вход>,1,{temp0=totalcycles; g}` плюс `bpset <точка>,1,{printf "… +%d",totalcycles-temp0; g}`. Так разложен блит в §1. + +--- + +## 5. Сводка: что сколько даёт в 11/15 | # | приём | эффект | тип оценки | риск | |---|---|---:|---|---| @@ -201,7 +283,7 @@ BUG-LATTICE-DOORTOP, BUG-SEAM-WEDGE-1) — только отдельным за | P3 | Кид перестанет будиться | −70 000 | модель | следствие P1 | | P2 | логика двух Char | −40 000 … −70 000 | гипотеза | ? | | P4 | накладные блита (4 правки) | −28 000 | модель | низкий | -| P5 | `loose_tick` без плит | −28 000 | замер | низкий | +| P5 | `loose_tick` без плит | ✅ −33 840 | ФАКТ | сделано | | P6 | цикл `process_trobs` | −20 000 … −40 000 | гипотеза | ? | | P10 | футпринт из физики | −23 000 | замер | средний | | P11 | мелочи (4 штуки) | −26 000 | замер | низкий | diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index 30127e2..3722c56 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -45,8 +45,18 @@ PROF_FLAGS := -DPROF_BORDER=$(PROF) # Стартовый уровень (отладка): make LEVEL=9 — начать сразу с девятого. # Дефолт держим на отлаживаемом сейчас уровне; для «настоящей» игры — LEVEL=1. # Константа объявлена в pop_tune.h (её видят обе половины главного цикла). -LEVEL ?= 9 +LEVEL ?= 11 PROF_FLAGS += -DFIRST_LEVEL=$(LEVEL) +# Стартовая КОМНАТА и позиция Кида в ней (отладка): make ROOM=15 POS=2 — +# начать прямо в целевой комнате оптимизации, минуя проход уровня. POS — +# тайл 0..29, то есть row*10+col. Дефолт — сцена 11/15 (Кид в 0,2), по +# которой сняты замеры в docs/perf_l11_room15.md. make ROOM= (пусто) +# возвращает штатный старт из данных уровня. +ROOM ?= 15 +POS ?= 2 +ifneq ($(strip $(ROOM)),) +PROF_FLAGS += -DDBG_START_ROOM=$(ROOM) -DDBG_START_POS=$(POS) +endif EXTRA_SRCS := pop_vflip.c pop_state.c pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c BG_DIR := $(CURDIR)/../poc/res/bg diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index 48b05e9..7ab3454 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -2133,6 +2133,21 @@ static void check_bumped(void) * make_loose_fall/check_press. Перерисовка — в pop_bg (shake/bake). */ uint8_t pop_loose_modif[30]; /* публично: читает pop_bg при отрисовке */ +/* Гейт холостого хода pop_loose_tick. В подавляющем большинстве кадров + * (и в целых комнатах) НИ ОДНА плита не анимируется, а два цикла по 30 и 10 + * позициям всё равно отрабатывают: замер 11/15 — 9 852 такта на комнату, + * где loose-плит нет вовсе. + * + * Ставится ПРИ ВЗВОДЕ фазы — все пять мест записи ненулевого значения лежат + * в этом файле (make_loose_fall, ветка потолка в check_press, do_knock для + * обоих рядов, восстановление из room_modif в pop_loose_reset). Снимает его + * САМ цикл, когда прошёл оба массива и не встретил ни одной ненулевой фазы. + * + * Асимметрия намеренная: ложная единица стоит одного холостого прохода, + * ложный ноль — застывшей навсегда плиты. Поэтому взвод обязан стоять + * рядом с КАЖДОЙ записью, а снятие — только по факту пустого прохода. */ +static uint8_t loose_any; + /* Плита-ПОТОЛОК: состояние/сигналы — см. объявления перед get_tile. */ /* make_loose_fall (seg007:0EF6): взвести отсчёт падения, если ещё не идёт. */ @@ -2141,8 +2156,10 @@ static void make_loose_fall(int pos, uint8_t modifier) if (pos < 0 || pos >= 30) return; if ((g_fg[pos] & 0x1F) != TILE_LOOSE) return; if ((g_fg[pos] & 0x20)) return; /* «solid» loose — не от шага */ - if ((int8_t)pop_loose_modif[pos] <= 0) /* покой/тряска, не отсчёт */ + if ((int8_t)pop_loose_modif[pos] <= 0) { /* покой/тряска, не отсчёт */ pop_loose_modif[pos] = modifier; + loose_any = 1; + } } /* died_on_button (seg007:0776): на кнопке КТО-ТО УМЕР — эффект становится @@ -2213,6 +2230,7 @@ static void check_press(void) (g_above[c] & 0x1F) == TILE_LOOSE && !(g_above[c] & 0x20) && (int8_t)pop_ceil_modif[c] <= 0) { pop_ceil_modif[c] = 1; /* make_loose_fall(1) */ + loose_any = 1; is_guard_notice = 1; /* seg006:1734 */ } } else if (get_tile_above_char() == TILE_LOOSE) { @@ -2266,16 +2284,20 @@ static void do_knock(int tile_row) if (tile_row == -1) { /* ряд 2 комнаты сверху — плиты-потолки */ if (!g_above || !g_link_u) return; for (col = 0; col < 10; col++) - if ((g_above[col] & 0x1F) == TILE_LOOSE && pop_ceil_modif[col] == 0) + if ((g_above[col] & 0x1F) == TILE_LOOSE && pop_ceil_modif[col] == 0) { pop_ceil_modif[col] = 0x80; + loose_any = 1; + } return; } if (tile_row < 0 || tile_row > 2) return; for (col = 0; col < 10; col++) { pos = tile_row * 10 + col; if ((g_fg[pos] & 0x1F) != TILE_LOOSE) continue; - if (pop_loose_modif[pos] == 0) + if (pop_loose_modif[pos] == 0) { pop_loose_modif[pos] = 0x80; + loose_any = 1; + } } } @@ -2393,11 +2415,13 @@ void pop_loose_reset(void) __banked for (i = 0; i < 30; i++) { pop_loose_modif[i] = (mod && g_fg && (g_fg[i] & 0x1F) == TILE_LOOSE) ? mod[i] : 0; + if (pop_loose_modif[i]) loose_any = 1; /* взвод гейта — см. loose_any */ } mod = (g_above && g_link_u) ? pop_trob_modif(g_link_u) : 0; for (i = 0; i < 10; i++) { pop_ceil_modif[i] = (mod && (g_above[i] & 0x1F) == TILE_LOOSE) ? mod[20 + i] : 0; + if (pop_ceil_modif[i]) loose_any = 1; /* взвод гейта — см. loose_any */ /* ...и СНЯТЬ заочный trob: плиту-потолок отсюда снова ведёт * pop_loose_tick. Без этого её крутили бы ДВОЕ — trob по * room_modif и мы по pop_ceil_modif, — и она проваливалась бы @@ -2525,6 +2549,10 @@ static void check_loose_fall_on_kid(void) { int mcol, my; if (pop_kid_dead) return; + /* Ни одного куска в воздухе — не платим за банковый трамплин в + * pop_loose_mob_pos с обходом 14 слотов (замер 11/15: 5 868 тактов на + * комнату, где не падает ничего). */ + if (!pop_mob_busy) return; if (!pop_loose_mob_pos(&mcol, &my)) return; /* Своё окно Char, как в оригинале (seg007:1199 — loadkid/savekid внутри * самой функции): зовут нас из pop_loose_tick, а тот идёт в главном @@ -2557,96 +2585,107 @@ void pop_loose_tick(void) __banked * Инкремент+сравнение считают row/col без деления. */ uint8_t row = 0, col = 0; pop_dbg_m9(); /* ЗАМЕР: вход pop_loose_tick */ - for (pos = 0; pos < 30; pos++, lm++) { - if (*lm != 0) { - uint8_t m = ++*lm; - /* Отложенный старт уровня 13 досчитал до нуля. Ноль у нас - * означает «не анимируется», и плита застряла бы навсегда — - * перескакиваем на 1. Стартовое значение выбрано с учётом - * этого перескока (см. pop_check_fall_flo). */ - if (m == 0) m = *lm = 1; - if (m & 0x80) { /* тряска (do_knock) */ - /* СПЕЦКЕЙС УРОВНЯ 13 (seg007:823): фазу со старшим битом НЕ - * гасим и не трясём. На нём check_fall_flo раздаёт плитам - * ОТРИЦАТЕЛЬНЫЙ старт (0xF0..0xFF) как отложенный таймер, и - * обычная ветка «тряска кончилась на 0x84» убила бы его в - * первом же кадре — плита не упала бы никогда. Пропущенная - * инкрементом фаза сама дойдёт до 0x00, там старший бит - * снимется, и дальше пойдёт обычный отсчёт до провала. */ - if (pop_current_level == POP_LOOSE_TILES_LEVEL) { - /* ждём молча */ - } else if (m >= 0x84) { /* конец тряски: сброс + покой */ + /* Гейт холостого хода: если ни одна фаза не взведена, оба цикла (30 + 10 + * позиций) пропускаем целиком — замер 11/15 дал 9 852 такта на комнату, + * где loose-плит нет вовсе. Флаг ставят места записи фазы, снимаем его + * здесь и только по факту прохода, в котором не осталось ни одной живой + * фазы (см. объявление loose_any). */ + if (loose_any) { + uint8_t found = 0; + for (pos = 0; pos < 30; pos++, lm++) { + if (*lm != 0) { + uint8_t m = ++*lm; + /* Отложенный старт уровня 13 досчитал до нуля. Ноль у нас + * означает «не анимируется», и плита застряла бы навсегда — + * перескакиваем на 1. Стартовое значение выбрано с учётом + * этого перескока (см. pop_check_fall_flo). */ + if (m == 0) m = *lm = 1; + if (m & 0x80) { /* тряска (do_knock) */ + /* СПЕЦКЕЙС УРОВНЯ 13 (seg007:823): фазу со старшим битом НЕ + * гасим и не трясём. На нём check_fall_flo раздаёт плитам + * ОТРИЦАТЕЛЬНЫЙ старт (0xF0..0xFF) как отложенный таймер, и + * обычная ветка «тряска кончилась на 0x84» убила бы его в + * первом же кадре — плита не упала бы никогда. Пропущенная + * инкрементом фаза сама дойдёт до 0x00, там старший бит + * снимется, и дальше пойдёт обычный отсчёт до провала. */ + if (pop_current_level == POP_LOOSE_TILES_LEVEL) { + /* ждём молча */ + } else if (m >= 0x84) { /* конец тряски: сброс + покой */ + *lm = 0; + /* modif уже 0 → кадр покоя обоих тайлов (col и col+1: + * правая грань loose живёт в соседе). ОБЕ страницы — + * иначе на одной застывает дрожащий кадр → мерцание. */ + pop_set_redraw(pos, POP_RD_LOOSE, 2); + } else { + pop_set_redraw(pos, POP_RD_LOOSE, 1); /* дрожащий кадр */ + } + } else if (m >= 11 && pop_loose_fell) { + /* Сигнал прошлого провала ещё не разобран главным циклом (он + * ОДИН на кадр) — придержим плиту кадр, откатив фазу к 10. + * Иначе из двух плит, провалившихся в одном кадре, дыру + * запекала бы только вторая, а первая оставалась нарисованной + * целой (уровень 13: check_fall_flo роняет гряду разом). */ + *lm = 10; + } else if (m >= 11) { /* loose_floor_delay — падение */ + /* remove_loose (seg007:0EB8): тайл → EMPTY (не debris!). + * Debris появляется ТОЛЬКО там, где приземлился падающий + * кусок (mob), т.е. рядом ниже. Loose в нижнем ряду (2,6) + * — кусок улетает из комнаты вниз, на месте остаётся пусто + * (стена-вниз от тайла снизу + чёрный фон). */ + g_fg[pos] = TILE_EMPTY; + /* remove_loose возвращает ТИП УРОВНЯ, и вызывающий кладёт + * его модификатором пустой клетки (seg007:846/1083): по нему + * draw_tile_right выбирает blueline_fram1 у дыры. */ + pop_trob_modif(g_room)[pos] = pop_palace; *lm = 0; - /* modif уже 0 → кадр покоя обоих тайлов (col и col+1: - * правая грань loose живёт в соседе). ОБЕ страницы — - * иначе на одной застывает дрожащий кадр → мерцание. */ - pop_set_redraw(pos, POP_RD_LOOSE, 2); - } else { - pop_set_redraw(pos, POP_RD_LOOSE, 1); /* дрожащий кадр */ + pop_set_redraw(pos, POP_RD_LOOSE_GONE, 2); /* запечь пусто, обе стр. */ + /* ...и СОСЕДА СПРАВА: передний торец пола заезжает в его + * клетку, и без этой пометки он оставался висеть над пустым + * тайлом. То же, что делает pop_skel_wake_tile. */ + if ((pos % 10) < 9) + pop_set_redraw((uint8_t)(pos + 1), POP_RD_FLOOR, 2); + pop_loose_mob_spawn(row, col); /* отрыв падающего куска */ + pop_loose_fell = (uint8_t)(pos + 1); /* сигнал приложению */ + } else { /* отсчёт 1..10 — дрожащий кадр */ + pop_set_redraw(pos, POP_RD_LOOSE, 1); } - } else if (m >= 11 && pop_loose_fell) { - /* Сигнал прошлого провала ещё не разобран главным циклом (он - * ОДИН на кадр) — придержим плиту кадр, откатив фазу к 10. - * Иначе из двух плит, провалившихся в одном кадре, дыру - * запекала бы только вторая, а первая оставалась нарисованной - * целой (уровень 13: check_fall_flo роняет гряду разом). */ - *lm = 10; - } else if (m >= 11) { /* loose_floor_delay — падение */ - /* remove_loose (seg007:0EB8): тайл → EMPTY (не debris!). - * Debris появляется ТОЛЬКО там, где приземлился падающий - * кусок (mob), т.е. рядом ниже. Loose в нижнем ряду (2,6) - * — кусок улетает из комнаты вниз, на месте остаётся пусто - * (стена-вниз от тайла снизу + чёрный фон). */ - g_fg[pos] = TILE_EMPTY; - /* remove_loose возвращает ТИП УРОВНЯ, и вызывающий кладёт - * его модификатором пустой клетки (seg007:846/1083): по нему - * draw_tile_right выбирает blueline_fram1 у дыры. */ - pop_trob_modif(g_room)[pos] = pop_palace; - *lm = 0; - pop_set_redraw(pos, POP_RD_LOOSE_GONE, 2); /* запечь пусто, обе стр. */ - /* ...и СОСЕДА СПРАВА: передний торец пола заезжает в его - * клетку, и без этой пометки он оставался висеть над пустым - * тайлом. То же, что делает pop_skel_wake_tile. */ - if ((pos % 10) < 9) - pop_set_redraw((uint8_t)(pos + 1), POP_RD_FLOOR, 2); - pop_loose_mob_spawn(row, col); /* отрыв падающего куска */ - pop_loose_fell = (uint8_t)(pos + 1); /* сигнал приложению */ - } else { /* отсчёт 1..10 — дрожащий кадр */ - pop_set_redraw(pos, POP_RD_LOOSE, 1); - } - } - if (++col == 10) { col = 0; row++; } /* row/col без деления */ - } - /* Плиты-ПОТОЛКИ (ряд 2 комнаты сверху): тот же animate_loose, но кадры - * рисуются в полосе у потолка, а провалившаяся плита становится EMPTY в - * НАШЕЙ копии ряда (g_above) — сверху открывается колодец. */ - lm = pop_ceil_modif; /* тот же указательный обход */ - for (col = 0; col < 10; col++, lm++) { - if (*lm != 0) { - uint8_t m = ++*lm; - if (m == 0) m = *lm = 1; /* см. pop_loose_tick выше */ - if (m & 0x80) { /* тряска от сотрясения (do_knock) */ - /* Тот же спецкейс уровня 13, и здесь он ГЛАВНЫЙ: плиты, - * которым check_fall_flo раздал отложенный старт, лежат - * именно в ряду 2 комнаты СВЕРХУ, то есть ведёт их этот - * цикл, а не соседний. */ - if (pop_current_level == POP_LOOSE_TILES_LEVEL) { - /* ждём молча */ - } else if (m >= 0x84) { *lm = 0; - pop_set_redraw_above(col, POP_RDA_CEIL, 2); } - else pop_set_redraw_above(col, POP_RDA_CEIL, 1); - } else if (m >= 11 && pop_ceil_fell) { - *lm = 10; /* сигнал занят — кадр подождём */ - } else if (m >= 11) { /* loose_floor_delay — плита рушится */ - *lm = 0; - if (g_above) g_above[col] = TILE_EMPTY; /* remove_loose */ - pop_set_redraw_above(col, POP_RDA_CEIL_GONE, 2); /* колодец, обе стр. */ - pop_loose_mob_spawn(-1, col); /* кусок отрывается от потолка */ - pop_ceil_fell = (uint8_t)(col + 1); /* сигнал приложению */ - } else { - pop_set_redraw_above(col, POP_RDA_CEIL, 1); /* отсчёт 1..10 */ + if (*lm) found = 1; /* фаза ещё жива — гейт держим */ + } + if (++col == 10) { col = 0; row++; } /* row/col без деления */ + } + /* Плиты-ПОТОЛКИ (ряд 2 комнаты сверху): тот же animate_loose, но кадры + * рисуются в полосе у потолка, а провалившаяся плита становится EMPTY в + * НАШЕЙ копии ряда (g_above) — сверху открывается колодец. */ + lm = pop_ceil_modif; /* тот же указательный обход */ + for (col = 0; col < 10; col++, lm++) { + if (*lm != 0) { + uint8_t m = ++*lm; + if (m == 0) m = *lm = 1; /* см. pop_loose_tick выше */ + if (m & 0x80) { /* тряска от сотрясения (do_knock) */ + /* Тот же спецкейс уровня 13, и здесь он ГЛАВНЫЙ: плиты, + * которым check_fall_flo раздал отложенный старт, лежат + * именно в ряду 2 комнаты СВЕРХУ, то есть ведёт их этот + * цикл, а не соседний. */ + if (pop_current_level == POP_LOOSE_TILES_LEVEL) { + /* ждём молча */ + } else if (m >= 0x84) { *lm = 0; + pop_set_redraw_above(col, POP_RDA_CEIL, 2); } + else pop_set_redraw_above(col, POP_RDA_CEIL, 1); + } else if (m >= 11 && pop_ceil_fell) { + *lm = 10; /* сигнал занят — кадр подождём */ + } else if (m >= 11) { /* loose_floor_delay — плита рушится */ + *lm = 0; + if (g_above) g_above[col] = TILE_EMPTY; /* remove_loose */ + pop_set_redraw_above(col, POP_RDA_CEIL_GONE, 2); /* колодец, обе стр. */ + pop_loose_mob_spawn(-1, col); /* кусок отрывается от потолка */ + pop_ceil_fell = (uint8_t)(col + 1); /* сигнал приложению */ + } else { + pop_set_redraw_above(col, POP_RDA_CEIL, 1); /* отсчёт 1..10 */ + } + if (*lm) found = 1; /* фаза ещё жива — гейт держим */ } } + loose_any = found; } pop_dbg_m10(); /* ЗАМЕР: оба цикла по тайлам пройдены */ pop_loose_mob_tick(); /* продвинуть+нарисовать падающий кусок (после тайлов) */ diff --git a/applications/PoP/roomtest/pop_room.c b/applications/PoP/roomtest/pop_room.c index 2007c21..9b98842 100644 --- a/applications/PoP/roomtest/pop_room.c +++ b/applications/PoP/roomtest/pop_room.c @@ -1184,7 +1184,10 @@ static mob_t *mob_alloc(void) { uint8_t i; for (i = 0; i < MOB_MAX; i++) - if (!mobs[i].active && !mobs[i].clean) return &mobs[i]; + if (!mobs[i].active && !mobs[i].clean) { + pop_mob_busy = 1; /* гейт холостого хода — см. pop_state.h */ + return &mobs[i]; + } return 0; } @@ -1672,7 +1675,7 @@ static void mob_draw_pass(uint8_t pg, uint8_t want_defer) void pop_loose_mob_tick(void) __banked { - uint8_t pg = gfx_get_draw_page() & 1, i, live = 0; + uint8_t pg = gfx_get_draw_page() & 1, i, live = 0, busy = 0; /* Слоты обходим УКАЗАТЕЛЕМ и пустые отсеиваем ЗДЕСЬ, а не внутри * mob_tick_one. Индексная запись mobs[i] заставляла SDCC умножать i на * sizeof(mob_t)=15 заново под каждое поле (.active/.clean/.x/.y), и 14 @@ -1680,6 +1683,10 @@ void pop_loose_mob_tick(void) __banked * (замер 2026-08-13). Гард в самом mob_tick_one оставлен: функция * зовётся и из других мест. */ mob_t *m = mobs; + /* Ни одного занятого слота — обходить 14 штук незачем (замер 11/15: + * 12 090 тактов холостого хода). Гейт снимается ниже, по факту прохода, + * в котором не осталось ни active, ни дочистки. */ + if (!pop_mob_busy) return; /* Пометки всех кусков — ОДНИМ пакетом: настоящий pop_cd_touch стоит 4 502 * такта (замер 2026-08-17), а кусков в кадре каскада шесть. В пакете они * копят общий прямоугольник четырьмя сравнениями, и настоящая пометка одна. @@ -1714,9 +1721,11 @@ void pop_loose_mob_tick(void) __banked pop_cd_touch(MOB_X0(m->x), m->prev_y[pg] - 27 + POP_YOFF, MOB_W, 64); mob_tick_one(m, pg); if (m->active) live++; + if (m->active || m->clean) busy++; /* слот ещё занят — гейт держим */ } pop_cd_batch_end(); mobs_live = live; + pop_mob_busy = busy; } static void mob_render(mob_t *m, uint8_t pg) diff --git a/applications/PoP/roomtest/pop_state.c b/applications/PoP/roomtest/pop_state.c index e97c9cf..e108528 100644 --- a/applications/PoP/roomtest/pop_state.c +++ b/applications/PoP/roomtest/pop_state.c @@ -13,6 +13,7 @@ #include "pop_state.h" #include "pop_ctrl.h" /* объявления шины control_* (живёт здесь) */ +uint8_t pop_mob_busy; uint8_t pop_loose_landed; /* Кусок loose УШЁЛ ВНИЗ из комнаты (порт хвоста move_loose, seg007:1126: * mob_down_a_row переносит его в комнату снизу; у нас симуляция там не diff --git a/applications/PoP/roomtest/pop_state.h b/applications/PoP/roomtest/pop_state.h index d3a5f87..482b1ca 100644 --- a/applications/PoP/roomtest/pop_state.h +++ b/applications/PoP/roomtest/pop_state.h @@ -9,6 +9,16 @@ #include +/* Есть ли хоть один ЗАНЯТЫЙ слот падающего куска (active или дочистка + * clean). Гейт холостого хода: пока кусков нет, ни обход 14 слотов в + * pop_loose_mob_tick, ни поиск куска над головой Кида делать не нужно + * (замер 11/15: 12 090 + 5 868 тактов на комнату, где не падает ничего). + * + * Живёт в резиденте, а не в pop_room.c, потому что читает его pop_map + * (банк 3), а писучие статики банкового модуля наружу не видны. Ставит + * отрыв куска, снимает сам обход по факту пустой таблицы. */ +extern uint8_t pop_mob_busy; + /* Кусок loose приземлился (loose_land, seg007:11E8): 0 = нет, иначе * tilepos+1 тайла, на который он лёг. Ставит отрисовка mob (банк), * разбирает логика loose-полов (pop_map, W1/W2). */ diff --git a/applications/PoP/roomtest/roomtest_cold.c b/applications/PoP/roomtest/roomtest_cold.c index ecac411..d5c7a1f 100644 --- a/applications/PoP/roomtest/roomtest_cold.c +++ b/applications/PoP/roomtest/roomtest_cold.c @@ -101,6 +101,20 @@ void pop_start_level(void) __banked * и разворачивается seq_5 */ } +#ifdef DBG_START_ROOM + /* ОТЛАДОЧНЫЙ СТАРТ: сразу в целевую комнату оптимизации, минуя проход + * уровня. Задаётся сборкой (make ROOM=15 POS=2), по умолчанию — сцена + * 11/15 из docs/perf_l11_room15.md: Кид в (0,2), справа чомпер под + * факелом, дальше второй факел и страж. + * + * Ставится ПОСЛЕ чекпойнта намеренно: отладочная позиция должна + * перебивать и его тоже, иначе рестарт уровня уводил бы Кида из + * измеряемой комнаты. Направление и позу входа не трогаем — они из + * данных уровня, как у обычного старта. */ + start_room = DBG_START_ROOM; + pos = DBG_START_POS; +#endif + /* Рестарт уровня = load_level() заново (play_level, seg003:57): в * исходное возвращаются И модификаторы (пики/ворота), И сами тайлы — * разбитые плиты целы, выпитые зелья на месте (BUG-RESPAWN-1), и @@ -146,6 +160,24 @@ void pop_start_level(void) __banked enter_room(start_room); kid_init(seq, (int8_t)(pos % 10), (int8_t)(pos / 10), dir); +#ifdef DBG_START_ROOM + /* ...и поправить x: kid_init ставит `x_bump[col] + TILE_SIZEX`, а это + * ЛЕВАЯ ГРАНИЦА СЛЕДУЮЩЕЙ колонки (порт set_start_pos — в данных уровня + * стартовые позиции подобраны под такую договорённость). Отладочный + * старт называет тайл, В КОТОРОМ Кид должен оказаться, поэтому сдвигаем + * его внутрь названного: с границы физика относит Кида к колонке правее, + * и в 11/15 он стартовал прямо в челюстях (найдено пользователем + * 2026-08-19). + * + * Отступ ровно 2, а не «половина тайла»: колонку определяет не сам x, а + * ВЕСОВАЯ ТОЧКА кадра (determine_col -> dx_weight: dx позы минус + * weight-биты, с учётом направления), поэтому геометрически ровной + * середины тут нет. Число снято замером живой сцены: x = 98 при позе + * стойки даёт curr_col = 2, x = 94 — уже 1. */ + Kid.x = (uint8_t)(pop_x_bump[(pos % 10) + FIRST_ONSCREEN_COLUMN] + + TILE_SIZEX - 2); + Kid.curr_col = (int8_t)(pos % 10); +#endif /* Спецсобытие «вход падением» (set_start_pos, seg003:0196): на 7-м * уровне Кид ставится в комнату 17, а экран тут же переводится на * комнату ПОД ней (`goto_other_room(3)`: y −= 189, ряд пересчитать) — diff --git a/applications/PoP/roomtest/tests-host/t_phys.c b/applications/PoP/roomtest/tests-host/t_phys.c index bdb68cc..cdfe94f 100644 --- a/applications/PoP/roomtest/tests-host/t_phys.c +++ b/applications/PoP/roomtest/tests-host/t_phys.c @@ -249,6 +249,38 @@ TC_TEST(phys_loose_survives_room_change) TC_EQ(pop_loose_modif[pos], 0); } +TC_TEST(phys_loose_gate_survives_room_change) +{ + /* GUARD против гейта холостого хода (loose_any, pop_map.c). Гейт + * пропускает оба цикла pop_loose_tick, пока ни одна фаза не взведена, и + * взводится ПЯТЬЮ местами записи. Четыре из них — взвод дрожи от шага и + * сотрясения — уже прогоняет phys_loose_floor_breaks; пятое (фаза + * ВОССТАНОВЛЕНА входом в комнату) не покрывал никто, а именно оно даёт + * самый тихий из возможных отказов: плита, к которой Кид вернулся, + * застыла бы на полудроже навсегда. + * + * Поэтому проверяем не флаг (он статик модуля), а НАБЛЮДАЕМОЕ следствие: + * после возврата в комнату тик обязан ДВИГАТЬ фазу. */ + uint8_t pos = 1 * 10 + 4; /* loose-плита сцены room_loose */ + uint8_t phase = 3; /* середина отсчёта до провала (1..10) */ + + /* Начинаем со снятого гейта: старт уровня забывает все фазы, и ни одно + * место взвода после этого не срабатывает. */ + sc_room(room_loose, 1); + pop_loose_forget(); + pop_loose_tick(); /* холостой проход — гейт снят */ + + /* Плита осталась недодрожавшей в room_modif, Кид возвращается в комнату. */ + pop_loose_modif[pos] = phase; + pop_loose_leave_room(); + pop_loose_reset(); + TC_EQ(pop_loose_modif[pos], phase); + + /* И вот теперь тик обязан её ДВИНУТЬ, а не пропустить по снятому гейту. */ + pop_loose_tick(); + TC_EQ(pop_loose_modif[pos], (uint8_t)(phase + 1)); +} + TC_TEST(phys_running_jump_over_3tile_gap) { /* BUG-RJUMP-1. Разбег влево от колонки 8, дальше Up — разбег-прыжок. @@ -327,6 +359,7 @@ void main(void) TC_RUN(phys_run_off_ledge); TC_RUN(phys_loose_floor_breaks); TC_RUN(phys_loose_survives_room_change); + TC_RUN(phys_loose_gate_survives_room_change); TC_RUN(phys_running_jump_over_3tile_gap); TC_RUN(phys_feather_fall_is_slow_and_harmless); }