P5: гейты холостого хода в pop_loose_tick — минус 33 840 тактов на кадре

Замер 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 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 10:49:19 +03:00
parent fd570c7eb8
commit 6d7c1c8b6b
9 changed files with 328 additions and 99 deletions
+13
View File
@@ -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
блита) и модель зелёной фазы этой сцены.
+89 -7
View File
@@ -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 <out> <сек> 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 | замер | низкий |
+11 -1
View File
@@ -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
+128 -89
View File
@@ -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(); /* продвинуть+нарисовать падающий кусок (после тайлов) */
+11 -2
View File
@@ -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)
+1
View File
@@ -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 переносит его в комнату снизу; у нас симуляция там не
+10
View File
@@ -9,6 +9,16 @@
#include <stdint.h>
/* Есть ли хоть один ЗАНЯТЫЙ слот падающего куска (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). */
+32
View File
@@ -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, ряд пересчитать)
@@ -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);
}