SprPoP: глубокое ревью находок А/Б — исправимость и цена по скорости

КОД НЕ МЕНЯЛСЯ.  Разбор одиннадцати находок рангов А и Б: что менять, во
что это обойдётся по скорости и памяти, каков риск.

СНЯТО ГЛАВНОЕ ПРЕПЯТСТВИЕ.  Обоснование двух упрощений (guards.c:1011 —
«нужны тайловые запросы от Char, а pop_map умеет только от Kid»)
УСТАРЕЛО: get_tile_at_char, get_tile_infrontof_char, get_tile_behind_char
и distance_to_edge_weight в pop_map.c уже работают от Char, они лишь не
выведены в заголовок.  Данные тоже на месте — pop_char_set_seq ставит
любую из 115 последовательностей, то есть seq_81 и seq_64 доступны без
единого нового байта.  Три находки упираются не в архитектуру, а в четыре
строки объявлений.

СКОРОСТЬ.  Места классифицированы по частоте вызова: play_seq и ИИ стража
— горячие, land/in_wall/bumped/hurt_by_sword — событийные.  Из
одиннадцати правок две УСКОРЯЮТ код (уходит условие из горячего цикла;
звук перестаёт играть в двух случаях из трёх), большинство бесплатны
(перестановка строк), и ни одна не требует переделки архитектуры.

Единственный конфликт со скоростью — отложенная побудка чомперов:
play_seq маппит страницу байткода в W0 один раз перед циклом, и звать
start_chompers внутри цикла значило бы снимать и возвращать окно на
каждый переход ряда.  Дешёвая замена: копить не один флаг, а битовую
маску рядов и разбудить их после цикла — теряться ряды перестанут, цена
в цикле нулевая.  Для стражей аналогично: не межбанковый вызов wall_type,
а копия таблицы в 32 байта в своём банке.

Порядок работ — от «одна-две строки, низкий риск» (13, 24) к тем, где
правка может компенсировать наши отличия в другом месте (1, 7).

Политика: для критичных фиксов скорость не вето — такие выносятся в
отдельный разбор с поиском дешёвого способа.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
This commit is contained in:
2026-08-31 18:31:34 +03:00
parent 3a0e847353
commit a5252b1c60
+162
View File
@@ -547,3 +547,165 @@
слоёв, `clip_char`.
4. **`do_fall`/`check_bumped`/`check_grab`** — остаток физики.
5. **ИИ стражей** и особенности скелета/Тени/Джафара.
---
# Глубокое ревью находок А и Б: можно ли починить и чем платим
> 2026-08-31. КОД ПО-ПРЕЖНЕМУ НЕ МЕНЯЛСЯ. Здесь только оценка.
>
> **Главное ограничение (требование пользователя): фикс не должен заметно
> замедлять игру.** Поэтому у каждой находки первым делом указана ЧАСТОТА
> вызова места, а уже потом сама правка.
>
> **Оговорка к ограничению:** для КРИТИЧНЫХ фиксов скорость — не вето.
> Если такой фикс всерьёз бьёт по производительности, он выносится в
> отдельный разбор, где ищется способ получить правильное поведение
> дёшево (иной момент вызова, кэш, предвычисление, перенос в холодный
> путь). То есть порядок такой: сначала решаем, критично ли поведение, и
> только потом — какой ценой его добиться.
## Снятое препятствие
Обоснование сразу двух упрощений (`guards.c:1011` — «нужны тайловые
запросы ОТ Char, а pop_map умеет только от Kid») **устарело**. Проверено:
`get_tile_at_char`, `get_tile_infrontof_char`, `get_tile_behind_char` и
`distance_to_edge_weight` в `pop_map.c` УЖЕ работают от `Char`
(строки 460, 465, 477, 561). Они лишь не выведены в `pop_map.h`.
Так же обстоит с данными: `pop_char_set_seq()` ставит любую из 115
последовательностей по индексу, то есть `seq_81` и `seq_64` доступны без
единого нового байта данных — таблица генерируется из оригинала целиком.
То есть три находки (12, 14, 20) упираются не в архитектуру, а в четыре
строки объявлений.
## Классификация мест по частоте вызова
| место | частота | вывод |
|---|---|---|
| `play_seq` (находки 7, 8) | КАЖДЫЙ кадр каждого персонажа | правка обязана быть бесплатной |
| `check_hurting` (18) | каждый кадр боя, дважды | почти горячий |
| ИИ стража (20) | каждый кадр, пока страж активен | почти горячий |
| `land`, `in_wall`, `bumped` (1, 2, 3, 14, 24) | событие раз в несколько секунд | холодный, цена не важна |
| `hurt_by_sword` (12, 13) | момент попадания | холодный |
## Разбор по находкам
### 12 + 13 (ранг А) — смерть на краю. Чинится, цена нулевая
*Место:* `guards.c`, `hurt_by_sword` — холодный путь.
*Правка:* (а) перенести две строки прижатия к полу внутрь ветки
выжившего удара — это чистое перемещение, минус ноль байт; (б) добавить
ветку выбора `seq_81` по тайлу позади и расстоянию до кромки.
*Что нужно:* экспорт `get_tile_behind_char()` и `distance_to_edge_weight()`
из `pop_map.c` в `pop_map.h` как `__banked`.
*Цена скорости:* два межбанковых вызова (`guards.c` — банк 1, `pop_map.c`
— банк 3) в момент попадания мечом, то есть несколько раз за бой.
Незаметно.
*Цена памяти:* банк 1 занят на 19,8 % (13 142 Б свободно) — места вдоволь;
банк 3 занят на 81,1 % (3 100 Б), но там прибавятся только две обёртки.
*Риск:* низкий. Ветка симметрична существующей, данные есть.
### 1 (ранг А) — лишний `determine_col()` в `land`
*Место:* холодный путь. *Правка:* убрать вызов и проверить, не
понадобился ли он нам вместо оригинального `load_fram_det_col`, который
оригинал делает в другом месте цепочки. *Цена:* отрицательная (кода
меньше). *Риск:* СРЕДНИЙ — вызов мог компенсировать наш иной порядок
загрузки кадра; убирать только с прогоном сцен падения и приземления.
### 2, 3 (ранг Б) — `land`: `fall_x` и порядок проверки пик
*Место:* холодный. *Правка 2:* сбрасывать только `fall_y` и после
`play_seq`, как оригинал. *Правка 3:* перенести проверку пик после
коррекции X. *Цена:* нулевая, это перестановка строк. *Риск:* низкий,
но обе меняют поведение на краю тайла — нужны прогоны с пиками.
### 24 (ранг Б) — `in_wall` не перезагружает кадр. Одна строка
*Место:* холодный. *Правка:* заменить `determine_col()` на
`pop_load_fram_det_col()` — он УЖЕ экспортирован (`pop_kid.h:109`) и, что
важно, НЕ банковый, то есть вызов прямой. *Цена скорости:* одна
перезагрузка кадра при выталкивании из стены — доли процента кадра.
*Риск:* низкий; это возврат к оригиналу.
### 14 (ранг Б) — нет `seq_64`
*Место:* `bumped`, холодный. *Правка:* добавить выбор между 64 и 65 по
направлению толчка. *Цена:* нулевая. *Риск:* низкий.
### 18 (ранг Б) — звук удара
*Место:* `check_hurting` — дважды за кадр боя. *Правка:* перенести звук
в конец функции и обвесить тремя условиями оригинала. *Цена:*
ОТРИЦАТЕЛЬНАЯ — звук перестанет играть в двух случаях из трёх, то есть
уменьшится и число обращений к звуковой очереди. *Риск:* низкий.
### 20 (ранг Б) — «стена» у стражей. Требует осторожности со скоростью
*Место:* ИИ стража — вызывается каждый кадр, пока страж активен.
*Плохой вариант:* экспортировать `wall_type` из `pop_map.c` и звать из
`guards.c`. Это МЕЖБАНКОВЫЙ вызов (банк 1 → банк 3) в почти горячем
пути — трамплин с переключением W3 на каждый шаг ИИ. Против требования
по скорости.
*Хороший вариант:* завести копию таблицы `wall_type_tbl` (32 байта) в
rodata банка 1 и обращаться к ней напрямую — стоимость чтения байта,
ноль переключений банка. Дублирование данных здесь оправдано: таблица
константная и вшита в формат уровней.
*Риск:* низкий, но нужно следить, чтобы копия не разошлась с оригиналом —
лучше генерировать обе из одного места или снабдить перекрёстным
комментарием.
### 7 (ранг Б) — перо только для Кида. Правка ускоряет
*Место:* `play_seq`, самый горячий путь. *Правка:* убрать лишнее условие
по `charid`. *Цена:* ОТРИЦАТЕЛЬНАЯ — из горячего цикла уходит сравнение.
*Риск:* средний: надо убедиться, что физика пера у нас применяется к
любому персонажу так же, как в оригинале, иначе анимация разойдётся с
физикой.
### 8 (ранг Б) — отложенные чомперы. Чинить ДЕШЁВЫМ способом
*Место:* `play_seq`, горячий путь.
*Почему отложено:* `play_seq` маппит страницу байткода в окно W0 ОДИН раз
перед циклом (`pop_kid.c:284`) и снимает после (`:389`). Вызвать
`start_chompers` внутри цикла — значит снять окно, позвать, вернуть окно,
и так на каждый переход ряда. Это прямая деградация горячего пути и
против требования по скорости.
*Дешёвая замена:* сейчас копится ОДИН флаг, из-за чего теряются
промежуточные ряды. Достаточно копить не флаг, а НОМЕРА рядов — один
байт-битовую маску (рядов всего 0..3) плюс запомненную колонку. После
цикла пройти по взведённым битам и разбудить чомперов в каждом. Цена в
цикле: одна операция «выставить бит» вместо присваивания флага, то есть
ноль. Разница с оригиналом останется только в МОМЕНТЕ побудки (после
цикла, а не внутри), но ряды перестанут теряться.
*Риск:* низкий. Полное совпадение с оригиналом здесь недостижимо без
потери скорости — это осознанный компромисс, который надо записать в
`docs/impl_diff.md`.
## Сводка: что делать в каком порядке
| приоритет | находки | почему |
|---|---|---|
| 1 | 13, 24 | по одной-две строки, риск низкий, оба в узле «смерть/стена» |
| 2 | 12, 14 | нужен экспорт двух функций; закрывают наблюдения пользователя |
| 3 | 18, 8 | обе УСКОРЯЮТ или бесплатны; 8 — по дешёвому варианту |
| 4 | 2, 3, 20 | перестановки и копия таблицы; нужны прогоны |
| 5 | 1, 7 | риск средний: обе могут компенсировать наши отличия в другом месте |
**Ни один фикс не требует переделки архитектуры и ни один не ложится на
горячий путь с накладными расходами** — при условии, что находка 20
делается копией таблицы, а находка 8 — битовой маской рядов.