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:
@@ -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
|
||||
блита) и модель зелёной фазы этой сцены.
|
||||
|
||||
@@ -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 | замер | низкий |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(); /* продвинуть+нарисовать падающий кусок (после тайлов) */
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 переносит его в комнату снизу; у нас симуляция там не
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user