Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39c3247532 | |||
| d0ac4b1c2a | |||
| d0de6dedf4 | |||
| b6b214e225 | |||
| f62989e358 | |||
| a29fb8da34 | |||
| 5a42b2d245 | |||
| 52a36caa75 | |||
| 9e03739bb0 | |||
| 79bcabde94 | |||
| 892f005ca5 | |||
| b3e754a66b | |||
| a993cb3b62 | |||
| 06fb4235f0 | |||
| 6f077b0d6d | |||
| 952879e7fa | |||
| 68e7d17b14 | |||
| e501982457 | |||
| f47d79ded8 | |||
| 379c513087 | |||
| 52bcc65a62 | |||
| 41deb69013 | |||
| 767d6f78a4 | |||
| 17c41b32de | |||
| a65da96960 | |||
| d3049693b8 | |||
| 6faf81016a | |||
| 068e21b56a | |||
| f8493a4c04 | |||
| da17a48576 | |||
| 2bdaf0f4cd | |||
| de68eb5cec | |||
| f81b30eb68 | |||
| 6d7c1c8b6b | |||
| fd570c7eb8 | |||
| 09f32ce834 | |||
| 4db60c750f |
@@ -439,3 +439,44 @@ BUG-CHEAT-FIGHT-1 (выход из боя), и лечится там же.
|
||||
|
||||
**Что проверять при регрессе.** Уровень 13: `K` на Джафаре → белая вспышка,
|
||||
уход ВЛЕВО открывает дверь уровня. Честная победа в бою даёт то же самое.
|
||||
|
||||
---
|
||||
|
||||
## Страж, вытесненный за правый край комнаты и там убитый, не виден нигде
|
||||
|
||||
**Не расхождение, а особенность оригинала.** Записано, чтобы вопрос не
|
||||
возникал повторно (спросил пользователь 2026-08-19: бой шёл в комнате 15,
|
||||
Кид вытеснил стража вправо — из-за края торчал только меч, — убил его, и
|
||||
труп не появился ни в комнате 15, ни в соседней справа).
|
||||
|
||||
**Почему так.** Три механизма складываются:
|
||||
|
||||
1. **комнату страж не менял.** Его физика работает только в полосе
|
||||
`x ∈ [44, 211)` (`seg000:1254`, у нас то же условие в
|
||||
`pop_guard_phys_tick`), поэтому своим ходом за край он не уходит —
|
||||
Кид вытолкнул его туда толчком, а `Guard.room` остался прежним;
|
||||
2. **мёртвый за Кидом не идёт.** Единственный способ сменить комнату —
|
||||
`follow_guard` при переходе Кида, и первое же условие там
|
||||
(`seg002:0346`) — `Guard.alive < 0 && Guard.sword == sword_2_drawn`,
|
||||
то есть ЖИВОЙ и с вынутым мечом. Мёртвый уходит веткой `leave_guard`,
|
||||
которая сохраняет его в **`Guard.room`** — в старую комнату. У нас
|
||||
ровно это же условие, `pop_guard_cold.c` (`pop_guard_follow`);
|
||||
3. **из соседней комнаты страж не рисуется.** Оригинал при
|
||||
`Guard.room != drawn_room` просто ГАСИТ слот (`seg000:422`:
|
||||
`Guard.direction = dir_56_none`). Механизм «видно из-за шва»
|
||||
(`xpos_in_drawn_room`) работает для коллизий и для Кида, но стража из
|
||||
чужой комнаты на экран не выводит.
|
||||
|
||||
Итог: труп приписан комнате, где страж стоял, а его `guards_x` — за
|
||||
правым краем. При возврате в ту комнату он честно восстанавливается там
|
||||
же, то есть за пределами видимого поля; в соседней комнате его нет,
|
||||
потому что в её данных стража и не было.
|
||||
|
||||
**Живой страж в этой ситуации ведёт себя иначе** — при уходе Кида вправо
|
||||
он идёт следом, если стоит достаточно близко к краю (`Guard.x >= 165`).
|
||||
Это портировано и работает.
|
||||
|
||||
**Чего я НЕ проверял:** живьём в SDLPoP этот сценарий не воспроизводил —
|
||||
вывод сделан чтением трёх мест кода. Если понадобится подтверждение,
|
||||
сценарий короткий: любой бой у правого края комнаты, вытеснить стража за
|
||||
край и добить.
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
# Сцена и замер: факел + чомпер + страж, уровень 11 комната 15
|
||||
|
||||
Вторая целевая сцена для оптимизации (первая — [`perf_l13_room23.md`](perf_l13_room23.md),
|
||||
каскад плит). Здесь узкое место другое: не разовый пик на каскаде, а
|
||||
**постоянная** цена статичной комнаты, в которой одновременно живут два
|
||||
факела, чомпер и страж.
|
||||
|
||||
Такты — `totalcycles` MAME (не такты Z80, ≈2,4× номинала, memory
|
||||
`sprinter_wait_states_2x`). **Растровый кадр = 430 000.** Хвост кадра — три
|
||||
`gfx_wait_vsync`, поэтому логический кадр занимает 3 растра, пока работа
|
||||
укладывается в один; при работе 1..2 растра период становится 4.
|
||||
|
||||
## 1. Сцена
|
||||
|
||||
Уровень 11, комната 15. Проверено чтением состояния машины:
|
||||
`pop_current_level` = 0x0B, `cur_room` = 15.
|
||||
|
||||
| кто | где | кадр |
|
||||
|---|---|---|
|
||||
| Кид | (0,2), x=98 | 15 (стойка с мечом) |
|
||||
| чомпер | (0,3) | застывший (trob мёртв) |
|
||||
| факел | (0,2) — пламя рисуется в ячейке (0,3) | анимируется каждый кадр |
|
||||
| факел | (0,7) — пламя в ячейке (0,8) | анимируется каждый кадр |
|
||||
| страж | (0,8), x=170 | 171 (боевая стойка) |
|
||||
|
||||
Ни Кид, ни страж не двигаются: сцена статична, разброс замера — сотни тактов
|
||||
на 800 000.
|
||||
|
||||
## 2. Замер (`09f32ce`, база модуля `roomtest.c` = 0x42AD)
|
||||
|
||||
867 кадров, зонды A/C/D/E; детализация — тремя отдельными прогонами
|
||||
(m1, m5..m7, M/F). **Период кадра: 4 растра во всех 866 интервалах.**
|
||||
|
||||
| участок | зонды | медиана | доля работы |
|
||||
|---|---|---:|---:|
|
||||
| **синяя: ввод + heal** | A→m1 | 141 048 | 17,6 % |
|
||||
| **синяя: логика** | m1→C | 152 190 | 19,0 % |
|
||||
| синяя, всего | A→C | **293 238** | 36,6 % |
|
||||
| зелёная: `pop_loose_tick` | C→m5 | 28 872 | 3,6 % |
|
||||
| **зелёная: `pop_process_trobs`** | m5→m6 | 92 448 | 11,5 % |
|
||||
| **зелёная: `pop_redraw_needed`** | m6→m7 | 190 260 | 23,7 % |
|
||||
| зелёная: шов/ворота соседа | m7→D | 9 336 | 1,2 % |
|
||||
| зелёная, всего | C→D | **320 916** | 40,0 % |
|
||||
| циан: `check_mirror` + `loose_mob_draw` | D→M | 37 458 | 4,7 % |
|
||||
| **циан: Кид + страж + fore + HP** | M→F | 148 566 | 18,5 % |
|
||||
| циан: `char_fore(KID)` + борта | F→E | 1 740 | 0,2 % |
|
||||
| циан, всего | D→E | **187 758** | 23,4 % |
|
||||
| **работа** | A→E | **801 768** | 1,86 растра |
|
||||
|
||||
Циан здесь **не** выделяется: 187 758 — это 0,44 растрового кадра, а разброс
|
||||
за 867 кадров всего 174 такта (187 674..187 848). Впечатление «циан ~150 %
|
||||
кадра» на глаз не подтвердилось — при периоде 4 растра полосы бордюра
|
||||
размазаны по кадрам и на глаз не читаются.
|
||||
|
||||
## 3. Главная находка: чомпер справа от факела — 190 260 тактов/кадр
|
||||
|
||||
`pop_dbg_rdmax_tot` = **1**: за кадр перерисовывается РОВНО ОДИН тайл, и
|
||||
стоит он все 190 260 тактов зелёной фазы.
|
||||
|
||||
> **ПОПРАВКА 2026-08-19 (после реализации P1).** Механизм ниже описан
|
||||
> верно, но ГЛАВНЫМ источником 190 260 тактов он НЕ был. Зонд
|
||||
> `pop_dbg_kind` показал, что все 312 перерисовок в прогоне — вид
|
||||
> `POP_RD_CHOMP` (полная), и ни одной от факела: собственная пометка
|
||||
> чомпера просто перебивала пометку соседа. Настоящая причина — в §6.
|
||||
> Урок ровно тот, что уже записан в `defer_unexplained_quirks`: механизм,
|
||||
> который правдоподобно объясняет цифру, ещё не доказан цифрой.
|
||||
|
||||
Цепочка:
|
||||
|
||||
1. `TORCH_ANIM_DIV = 1` — факел меняет кадр пламени КАЖДЫЙ логический кадр;
|
||||
2. пламя запекается в фон (`pop_torch_draw`, `GFX_BANK_NORMAL`), а канвас
|
||||
пламени 16×18 лежит **в ячейке правого соседа** (seg008:560) — то есть
|
||||
поверх чомпера;
|
||||
3. поэтому `pop_process_trobs` метит соседа: `if (trob_rcode[i] == TILE_CHOMP)
|
||||
pop_set_redraw(tp + 1, POP_RD_CHOMP, 1)` (порт `set_redraw_anim_right`);
|
||||
4. `pop_chomp_redraw` отвечает на пометку **heal 32×64 + полный `draw_tile`**.
|
||||
|
||||
Расхождение с оригиналом именно в шаге 4. `set_redraw_anim_right` метит
|
||||
слой **anim**, и оригинал возвращает только его (`draw_tile_anim_topright` →
|
||||
`draw_tile_anim_right` → `draw_tile_anim`) — одну графику чомпера поверх
|
||||
огня. Мы вместо этого стираем и пересобираем тайл целиком, со всеми слоями
|
||||
(`draw_tile_right`, `base`, `bottom`, `loose`), которые пламя вообще не
|
||||
трогало.
|
||||
|
||||
Цена по модели блита (`blit_cost_model`, 8791 + 198·h + 5,96·w·h):
|
||||
heal 32×64 ≈ 33 700, значит на один `draw_tile` уходит ≈ 156 000 — сходится
|
||||
с известным замером «полная запечка щебня 179 914».
|
||||
|
||||
Чомпер при этом **застывший**: своей анимации у него нет, поза не меняется,
|
||||
возвращать нужно ровно ту же графику поверх свежего пламени.
|
||||
|
||||
## 4. Что это даёт и куда смотреть дальше
|
||||
|
||||
Ранжирование по цене (доля от 801 768):
|
||||
|
||||
| # | участок | такты | что делать |
|
||||
|---|---|---:|---|
|
||||
| 1 | `redraw_needed`: чомпер под факелом | 190 260 | вернуть только слой anim, как в оригинале — без heal и без остальных слоёв |
|
||||
| 2 | синяя: логика двух Char | 152 190 | графики нет вообще; разобрать `pop_check_can_guard_see_kid` и два `play_seq` |
|
||||
| 3 | циан: Кид + страж | 148 566 | оба будятся каждый кадр — метки фона от чомпера/факела накрывают обоих |
|
||||
| 4 | синяя: heal двух Char | 141 048 | следствие того же: skip не срабатывает ни разу |
|
||||
| 5 | `process_trobs`: два факела | 92 448 | ≈46 000 на факел при блите пламени 16×18 ≈ 14 000 — разобрать накладные |
|
||||
| 6 | `loose_tick` | 28 872 | в комнате нет ни одной loose-плиты |
|
||||
|
||||
Пункты 3 и 4 — одна тема: пока фон трогают каждый кадр, `pop_char_skip_mask`
|
||||
не может пропустить ни Кида, ни стража. Пламя метит узко (16×18), а вот
|
||||
`pop_chomp_redraw` метит весь тайл со свесом — то есть пункт 1 чинит и часть
|
||||
пунктов 3/4.
|
||||
|
||||
Связанные задачи: `HEAL-WIDTH` и G8 в [`perf_green_phase.md`](perf_green_phase.md)
|
||||
— та же болезнь (полный тайл там, где хватает полосы), но на другом
|
||||
материале.
|
||||
|
||||
## 6. Настоящая причина 190 260 тактов: перерисовка неизменной позы
|
||||
|
||||
Найдено при реализации P1, сверкой с `animate_chomper` (seg007:0448).
|
||||
Функция оригинала заканчивается так:
|
||||
|
||||
```c
|
||||
if ((curr_modifier & 0x7F) < 6) {
|
||||
redraw_at_trob();
|
||||
}
|
||||
```
|
||||
|
||||
То есть чомпер перерисовывается **только пока фаза меньше 6** — пять кадров
|
||||
из пятнадцати (`POP_CHOMPER_SPEED = 15`). Это не оптимизация оригинала, а
|
||||
следствие таблицы поз: `chomper_fram1 = {3,2,0,1,4,3,3}`, и начиная с фазы 5
|
||||
и до конца круга поза одна и та же — 3. Рисовать её десять кадров подряд
|
||||
значит рисовать ровно ту же картинку.
|
||||
|
||||
Мы же метили тайл БЕЗУСЛОВНО, каждый кадр, пока trob жив — то есть платили
|
||||
полный `draw_tile` плюс heal 32×64 за неизменную картинку в двух третях
|
||||
кадров. А trob у чомпера живёт, пока Кид в том же РЯДУ (`animate_chomper`
|
||||
снимает его только при фазе ≥ 6 и ушедшем Киде) — в 11/15 Кид стоит в (0,2),
|
||||
чомпер в (0,3), ряд один.
|
||||
|
||||
**Что сделано:**
|
||||
|
||||
1. пометка только при фазе < 6, и на фазе 5 — на ОБЕ страницы дабл-буфера
|
||||
(она последняя рисуемая, её поза обязана лечь на обе; вторую страницу
|
||||
пометка догоняет в кадре фазы 6, где поза та же самая);
|
||||
2. пометка от факела (`set_redraw_anim_right`) переведена на новый вид
|
||||
`POP_RD_CHOMP_ANIM` → `pop_chomp_anim_draw`: три блита графики чомпера
|
||||
поверх свежего пламени, без heal и без остальных слоёв — порт ветки
|
||||
`redraw_frames_anim` (seg008:0211);
|
||||
3. приоритет полной перерисовки над anim в `pop_set_redraw` — у оригинала
|
||||
это два независимых счётчика, и `full` побеждает.
|
||||
|
||||
**Результат** (замер, 552 кадра): полная перерисовка теперь в **40 %**
|
||||
кадров, лёгкий возврат челюстей — в 60 %. Зелёная фаза: 291 888 в дорогом
|
||||
кадре против 183 414 в дешёвом.
|
||||
|
||||
| | работа | зелёная |
|
||||
|---|---:|---:|
|
||||
| до P1 | 768 684 | 294 510 |
|
||||
| после P1, медиана | **657 882** | **183 420** |
|
||||
| после P1, дорогой кадр (40 %) | 765 936 | 291 888 |
|
||||
|
||||
**−110 802 на медиане** при ожидании −160 000. Разница в том, что 40 %
|
||||
кадров по-прежнему платят полную цену: там поза реально меняется, и это уже
|
||||
не лишняя работа, а честная. Дальше её можно резать только раскладом
|
||||
`draw_tile` на части (P7) или сужением heal (P8).
|
||||
|
||||
## 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 |
|
||||
| 2026-08-19 | зонды для замера P2/P6 (временные) | 768 684 | 286 518 | 294 510 | 187 761 |
|
||||
| 2026-08-19 | **P1**: чомпер — перерисовка только при фазе < 6 (медиана) | **657 882** | 286 503 | 183 420 | 187 761 |
|
||||
| | | −110 802 | −15 | −111 090 | 0 |
|
||||
|
||||
Оснастка P2/P6 стоит 756 тактов на кадр — замеры до и после сопоставимы.
|
||||
|
||||
Циан не изменился (+6 тактов — шум), и это ожидаемо: `loose_tick` целиком
|
||||
лежит в зелёной. Синяя просела на 7 374 без прямой причины в правке —
|
||||
скорее всего перераскладка кода банка 3 компилятором; проверять отдельно
|
||||
не стали, знак верный.
|
||||
|
||||
## 7. ТЯЖЁЛАЯ позиция: Кид на шаг правее [замер 2026-08-19]
|
||||
|
||||
Поставлена пользователем: один осторожный шаг вправо (x = 106 вместо 99,
|
||||
колонка та же). Спрайт Кида начинает пересекаться с тайлом (0,3), где
|
||||
одновременно чомпер и пламя факела — и `skip_mask` перестаёт его
|
||||
пропускать.
|
||||
|
||||
| фаза | лёгкая | **тяжёлая** | разница |
|
||||
|---|---:|---:|---:|
|
||||
| синяя | 259 500 | 257 520 | −1 980 |
|
||||
| зелёная (медиана) | 181 068 | 180 870 | −198 |
|
||||
| **циан** | 187 761 | **319 842** | **+132 081** |
|
||||
| **работа (медиана)** | 628 542 | **758 358** | **+129 816** |
|
||||
| работа (максимум) | 744 384 | **876 612** | |
|
||||
|
||||
**Период кадра: 4 растра в 335 кадрах, 5 растров в 123 (27 %).** Это уже
|
||||
не «стабильно медленно», а рывки: каждый четвёртый кадр длиннее соседних.
|
||||
|
||||
Разбор циана показывает, куда ушли 132 тысячи:
|
||||
|
||||
| участок | лёгкая | тяжёлая |
|
||||
|---|---:|---:|
|
||||
| `check_mirror` | 3 198 | 3 198 |
|
||||
| `mob_draw` + `guard_over_kid` + `skip_mask` | 34 374 | 18 672 |
|
||||
| **`pop_char_draw(KID)`** | **204** | **54 738** |
|
||||
| соперник: `char_draw` + `char_fore` | 145 896 | 149 748 |
|
||||
| **`fore_needed` + `char_fore(KID)` + борта** | **4 356** | **93 486** |
|
||||
|
||||
То есть Кид из «пропущен за 204 такта» превращается в полноценного
|
||||
персонажа за ~144 000 — ровно столько же, сколько стоит страж.
|
||||
|
||||
**Главный вывод замера: самая дорогая единичная статья кадра — это
|
||||
fore-проход персонажа.** 62 778 у стража и ~89 000 у Кида, вместе около
|
||||
**152 000, то есть 20 % работы кадра**. У Кида он дороже потому, что в его
|
||||
футпринте лежит чомпер, а у чомпера есть собственный передний слой
|
||||
(`POP_CHOMP_FRAM_FOR`), который перерисовывается поверх персонажа каждый
|
||||
кадр.
|
||||
|
||||
## 8. После P15 (точная метка «фон трогали»)
|
||||
|
||||
| фаза | лёгкая до | лёгкая после | тяжёлая до | тяжёлая после |
|
||||
|---|---:|---:|---:|---:|
|
||||
| синяя | 259 050 | **223 902** | 257 520 | **245 808** |
|
||||
| зелёная | 181 494 | 181 494 | 180 870 | 181 761 |
|
||||
| циан | 194 262 | **59 406** | 319 842 | **192 090** |
|
||||
| **работа** | 628 542 | **464 796** | 758 358 | **617 487** |
|
||||
|
||||
В лёгкой позиции не рисуется НИ ОДИН персонаж (циан 59 406 — это уже только
|
||||
`check_mirror`, проверки и передний слой по пометкам). В тяжёлой рисуется
|
||||
один Кид: он действительно стоит под пламенем, а страж — нет.
|
||||
|
||||
**Пятирастровые кадры в тяжёлой позиции исчезли** (было 27 %), период стал
|
||||
ровно 4.
|
||||
|
||||
**Полная очередь оптимизаций с оценками — [`perf_registry.md`](perf_registry.md).**
|
||||
Там же разложена цена одного блита фона по этапам (замер 2026-08-19, 1603
|
||||
блита) и модель зелёной фазы этой сцены.
|
||||
@@ -237,3 +237,35 @@ memory `blit_cost_model`), а 16-битная арифметика в стеко
|
||||
|
||||
Существующая страховка уже есть и остаётся: `pop_blit_b` уводит кадр с
|
||||
`img[1] | img[3] != 0` на общий путь `blit_b_oversize`.
|
||||
|
||||
**Регресс после дня оптимизации 11/15 (`d0ac4b1`, 2026-08-19), 2367 кадров:**
|
||||
|
||||
| максимум по секции | эталон `mob-order-B-done` | сейчас | разница |
|
||||
|---|---:|---:|---:|
|
||||
| работа | 913 848 | **911 862** | −1 986 |
|
||||
| синяя | 159 810 | **149 106** | −10 704 |
|
||||
| зелёная | 440 418 | **436 494** | −3 924 |
|
||||
| циан | 393 000 | **382 770** | −10 230 |
|
||||
|
||||
Период: **3 растра в 2341 кадре, 4 в 23, 5 в 2** — как в эталоне.
|
||||
|
||||
Почему сумма минусов по фазам не равна минусу по работе: максимумы разных
|
||||
фаз достигаются В РАЗНЫХ КАДРАХ (пик синей — не тот кадр, где пик зелёной),
|
||||
а «работа» здесь — максимум СУММЫ, а не сумма максимумов.
|
||||
|
||||
Что из правок 11/15 сюда дошло: P16 и P2b дали синюю и циан (они про
|
||||
проверки и луч видимости, а те работают в любой сцене), HEAL-WIDTH дал
|
||||
зелёную (плита 64 → 58 на шести heal'ах кадра).
|
||||
|
||||
**Зелёная по-прежнему выше растрового кадра** (436 494 против 430 000).
|
||||
Главный оставшийся кандидат именно для этой сцены — **P9 (G8)**: при
|
||||
падении плиты помечаются ДВА тайла, и соседний перезапекается целиком и
|
||||
повторно (`draw_tile` соседа дважды на одну пометку), хотя потревожены у
|
||||
него только левые 28 пикселей. При шести падающих плитах это умножается
|
||||
на шесть.
|
||||
|
||||
**ВАЖНО ДЛЯ ПРОЦЕССА.** Этот прогон вскрыл регрессию, которую не поймали
|
||||
ни хост-тесты, ни сцена 11/15: гейт `loose_any` (позиция P5) не взводился
|
||||
в `check_fall_flo`, и плиты уровня 13 дрожали, не падая. Сцену 13/23 надо
|
||||
прогонять после КАЖДОЙ правки loose-механики, а не только когда меняешь её
|
||||
сознательно.
|
||||
|
||||
@@ -0,0 +1,853 @@
|
||||
# Реестр оптимизаций: всё отложенное, в одном списке
|
||||
|
||||
Собрано 2026-08-19 из [`perf_green_phase.md`](perf_green_phase.md) (G1-G9),
|
||||
[`perf_cyan_phase.md`](perf_cyan_phase.md) (C1-C7),
|
||||
[`perf_backlog.md`](perf_backlog.md) (позиции 1-7),
|
||||
[`../roomtest/TASKS_OPEN.md`](../roomtest/TASKS_OPEN.md) (HEAL-WIDTH) и из
|
||||
свежего разбора сцены [`perf_l11_room15.md`](perf_l11_room15.md).
|
||||
|
||||
**База для процентов — работа кадра в 11/15: 801 768 тактов** (замер
|
||||
`09f32ce`). Где эффект относится к другой сцене, это сказано явно.
|
||||
|
||||
Оценки помечены: **[замер]** — измерено; **[модель]** — посчитано по
|
||||
измеренным составляющим; **[гипотеза]** — не мерено, нужен прогон.
|
||||
|
||||
---
|
||||
|
||||
## 1. Цена одного блита фона — разложена [замер 2026-08-19]
|
||||
|
||||
Метод: брейкпоинты на резидентных адресах внутри `pop_blit_b` (0x5C50),
|
||||
`temp0` на входе, разница `totalcycles` на каждом вызове. 1603 блита.
|
||||
|
||||
| этап | такты | постоянство |
|
||||
|---|---:|---|
|
||||
| пролог + аргументы + грубый отсев | **810** | ровно, всегда |
|
||||
| `atlas_image` | **672** | ровно, всегда |
|
||||
| `gfx_w0_map` + чтение шапки ленты + арифметика клипа | **2 400** | ровно, всегда |
|
||||
| ядро блита (пиксели) | 4 380 … 26 592 | по размеру кадра |
|
||||
| `pop_cd_touch` | **2 069** (пакетный путь) / 4 115 (настоящая пометка) | почти ровно |
|
||||
| `gfx_w0_unmap` + эпилог | **175** | ровно, всегда |
|
||||
| **весь блит** | 10 458 … 32 718, медиана **16 674** | |
|
||||
|
||||
**Фиксированная накладная = 6 126 тактов на любой блит, хоть 8×8.**
|
||||
У самого дешёвого блита (10 458) это **59 % цены**; у пламени факела 16×18
|
||||
пиксели тянут ~1 700 из ~14 000, то есть **12 %**.
|
||||
|
||||
Это и есть ответ на вопрос «почему маленький блит стоит 14 000». Причины
|
||||
ровно те, о которых спрашивал пользователь:
|
||||
|
||||
- **810 на пролог** — `call ___sdcc_enter_ix`, IX-фрейм и шесть чтений
|
||||
`N(ix)`: третий и четвёртый аргументы (`int x`, `int ybottom`) идут
|
||||
СТЕКОМ, каждое обращение 19 тактов Z80;
|
||||
- **2 069 на `pop_cd_touch`** даже по пакетному пути, где вся работа — четыре
|
||||
сравнения. Сигнатура `(int x, int y, int w, int h)` = 8 байт аргументов,
|
||||
два из них через стек; `w`/`h` никогда не больше 64, `y` не больше 255,
|
||||
то есть три из четырёх могли быть `uint8_t`;
|
||||
- **2 400 на map + шапку** — `gfx_w0_map` (1 086) + `unmap` (264, платится в
|
||||
конце) + ~1 000 на чтение четырёх байт заголовка и арифметику;
|
||||
- **672 на `atlas_image`** — маппинг W3, чтение записи каталога, возврат W3;
|
||||
размеры при этом читаются и выбрасываются (см. §3, C6).
|
||||
|
||||
**Блитов за кадр в 11/15: 8** [замер] — все в зелёной фазе (6 на `draw_tile`
|
||||
чомпера, 2 на пламя факелов). Синяя и циан через `pop_blit_b` не ходят
|
||||
вовсе: heal и персонажи идут своими путями. Значит фиксированные накладные
|
||||
блита стоят сцене **8 × 6 126 = 49 000 тактов/кадр (6,1 % работы)**.
|
||||
|
||||
---
|
||||
|
||||
## 2. Модель зелёной фазы 11/15 [модель, сходится с 320 916 замера]
|
||||
|
||||
| статья | такты | доля фазы |
|
||||
|---|---:|---:|
|
||||
| 8 блитов фона (из них 6 126×8 = 49 000 накладных) | 133 400 | 41 % |
|
||||
| диспетчер `draw_tile` (один тайл чомпера) | 56 200 | 18 % |
|
||||
| цикл `pop_process_trobs` без блитов пламени | 59 100 | 18 % |
|
||||
| `pop_loose_tick` (плит в комнате НЕТ) | 28 872 | 9 % |
|
||||
| heal 32×64 в `pop_chomp_redraw` | 34 000 | 11 % |
|
||||
| шов / ворота соседа | 9 336 | 3 % |
|
||||
|
||||
Больше половины фазы — не пиксели, а обвязка вокруг них.
|
||||
|
||||
---
|
||||
|
||||
## 3. Список приёмов, отсортированный по эффекту
|
||||
|
||||
### P1. Чомпер: перерисовка неизменной позы ✅ СДЕЛАНО 2026-08-19 — −110 802
|
||||
|
||||
**Диагноз, с которым позиция заводилась, оказался неполным.** Я приписал
|
||||
190 260 тактов пометке от факела; зонд `pop_dbg_kind` показал, что все 312
|
||||
перерисовок прогона — вид `POP_RD_CHOMP` (полная), а пометку соседа она
|
||||
просто перебивала. Настоящая причина нашлась сверкой с `animate_chomper`
|
||||
(seg007:0448): оригинал перерисовывает чомпер **только при фазе < 6**, пять
|
||||
кадров из пятнадцати, потому что с фазы 5 поза не меняется
|
||||
(`chomper_fram1 = {3,2,0,1,4,3,3}`). Мы метили тайл каждый кадр.
|
||||
|
||||
Сделано три вещи: условие фазы (с пометкой обеих страниц на фазе 5), новый
|
||||
вид `POP_RD_CHOMP_ANIM` → `pop_chomp_anim_draw` (три блита поверх огня, порт
|
||||
ветки `redraw_frames_anim`) и приоритет полной перерисовки над anim в
|
||||
`pop_set_redraw`. Обе половины работают: замер даёт 40 % полных
|
||||
перерисовок и 60 % лёгких.
|
||||
|
||||
Работа 768 684 → **657 882** (медиана), зелёная 294 510 → **183 420**.
|
||||
В 40 % кадров цена осталась прежней — там поза действительно меняется, и это
|
||||
уже честная работа; резать её дальше только через P7 (раскол `draw_tile`)
|
||||
или P8 (ширина heal).
|
||||
|
||||
Полный разбор — [`perf_l11_room15.md`](perf_l11_room15.md) §6.
|
||||
|
||||
<details><summary>Исходная (неполная) постановка</summary>
|
||||
|
||||
Разбор в [`perf_l11_room15.md`](perf_l11_room15.md) §3. Сейчас пометка от
|
||||
факела обрабатывается как `heal 32×64 + полный draw_tile` (190 260 тактов на
|
||||
единственный перерисованный тайл кадра); оригинал в этом случае рисует
|
||||
ТОЛЬКО `draw_tile_anim` — графику чомпера поверх свежего пламени.
|
||||
|
||||
Останется 1-2 блита челюстей ≈ 20 000-33 000. **Риск низкий**: это
|
||||
сближение с оригиналом, а не отход от него. Побочно снимает широкую пометку
|
||||
«фон трогали» вокруг тайла чомпера — см. P3.
|
||||
|
||||
</details>
|
||||
|
||||
### P2. Синяя фаза разложена [ЗАМЕР 2026-08-19] — гипотеза не подтвердилась
|
||||
|
||||
Замер зондами внутрь обеих половин синей (286 518 тактов):
|
||||
|
||||
| участок | такты | доля работы |
|
||||
|---|---:|---:|
|
||||
| ввод + читы | 14 154 | 1,8 % |
|
||||
| три спецсобытия уровней (skel / mouse / killed_shadow) | **1 650** | 0,2 % |
|
||||
| `pop_frame_timers` + **луч видимости стража** | **37 032** | 4,8 % |
|
||||
| `pop_ctrl_tick` | 18 648 | 2,4 % |
|
||||
| `skip_mask` + **heal двух Char** | **67 734** | 8,8 % |
|
||||
| `mirror_heal` + `fore_heal` | 2 040 | 0,3 % |
|
||||
| `kid_tick` (play_seq) | 10 098 | 1,3 % |
|
||||
| **`pop_phys_tick`** (физика Кида) | **61 266** | 8,0 % |
|
||||
| `pop_guard_tick` (логика стража) | 19 932 | 2,6 % |
|
||||
| **`pop_guard_phys_tick`** (физика стража) | **43 980** | 5,7 % |
|
||||
| боёвка (`sword_hurting` / `sword_hurt` / `delta_hp`) | 9 558 | 1,2 % |
|
||||
| `guard_fallout` + уход из комнаты | 366 | — |
|
||||
|
||||
**Что оказалось не так, как ждали.** Я предполагал, что дорогие тут
|
||||
банковые трамплины на спецсобытиях (по аналогии с `pop_clip_char_top`,
|
||||
8 892 такта за трамплин ради одной проверки). Замер это отверг: три
|
||||
спецсобытия уровней вместе стоят **1 650** — они гейтятся внутри и на
|
||||
уровне 11 честно выходят сразу.
|
||||
|
||||
**Настоящие статьи — три:**
|
||||
|
||||
1. **Физика двух Char — 105 246** (61 266 + 43 980), при том что оба
|
||||
персонажа СТОЯТ и кадр позы не меняется. Это 13,7 % работы кадра и
|
||||
самая крупная статья синей. Нужен ещё один уровень разбора — внутрь
|
||||
`pop_phys_tick` (позиция **P2a**, отдельным заходом).
|
||||
2. **Луч видимости стража — до 37 032** (вместе с `pop_frame_timers`, но тот
|
||||
заведомо копеечный: три счётчика). Считается КАЖДЫЙ кадр, хотя ни Кид,
|
||||
ни страж не сдвинулись. Кандидат на гейт «пересчитывать только при
|
||||
смене позиции или комнаты любого из двоих» — позиция **P2b**,
|
||||
ожидание −30 000, риск низкий.
|
||||
3. **heal двух Char — 67 734.** Отдельной правки не требует: он платится
|
||||
ровно потому, что `skip_mask` никого не пропускает, и уйдёт вместе с
|
||||
P1/P3.
|
||||
|
||||
Новое, найдено 2026-08-19.
|
||||
|
||||
### P15. Точность метки «фон трогали» ✅ СДЕЛАНО 2026-08-19 — −163 746 / −140 871
|
||||
|
||||
Постановка пользователя: не перерисовывать стража, пока он не двигается.
|
||||
|
||||
**Две правки, и вторая оказалась решающей:**
|
||||
|
||||
1. **метка**: вместо «маска колонок × три ряда по 63 px» — диапазон y на
|
||||
каждую колонку (`ymin`/`ymax`, 40 байт на обе страницы). Прежняя
|
||||
гранулярность склеивала пламя факела (y 33..50) с клинком стоящего
|
||||
стража (y 59..65), между которыми девять пикселей зазора;
|
||||
2. **проверка**: `cd_quiet` сверяет спрайт и накладной (клинок, брызги)
|
||||
ДВУМЯ отдельными прямоугольниками вместо объединённого bbox.
|
||||
Объединение включает пустой угол: спрайт стража в колонке 8, клинок
|
||||
уходит в колонку 7 на y 59..65, пламя метит колонку 7 на y 33..50 —
|
||||
и прямоугольник «спрайт + клинок» цеплял метку углом.
|
||||
|
||||
**Без второй правки первая дала почти ноль** (632 676 против 628 542 до
|
||||
неё) — это стоит помнить: точность структуры бесполезна, пока запрос к ней
|
||||
остаётся грубым.
|
||||
|
||||
| | лёгкая позиция | тяжёлая позиция |
|
||||
|---|---:|---:|
|
||||
| синяя | 259 050 → **223 902** | 257 520 → **245 808** |
|
||||
| зелёная | 181 494 → 181 494 | 180 870 → 181 761 |
|
||||
| циан | 194 262 → **59 406** | 319 842 → **192 090** |
|
||||
| **работа** | 628 542 → **464 796** | 758 358 → **617 487** |
|
||||
|
||||
В тяжёлой позиции вдобавок исчезли пятирастровые кадры (было 27 %).
|
||||
|
||||
Проверено в MAME: статика чистая, динамика (пробежка, бой, переход в
|
||||
соседнюю комнату) без хвостов и просвечивания; хост-тесты зелёные.
|
||||
|
||||
Побочно исправлены два собственных дефекта первой редакции: обе страницы
|
||||
обновлялись по условию, проверяющему только страницу 0 (после
|
||||
`pop_cd_clear(0)` метка второй переставала расти), и отсутствовала явная
|
||||
инициализация — пустая колонка обозначается `ymin = 255`, а нули от crt0
|
||||
читались бы как «затронута строка 0».
|
||||
|
||||
### P16. Цианные проверки ✅ СДЕЛАНО 2026-08-19 — −25 818
|
||||
|
||||
Раскладка остатка цианной фазы (59 406) показала, что 47 883 из них — три
|
||||
вызова, а не отрисовка:
|
||||
|
||||
| вызов | было | стало |
|
||||
|---|---:|---:|
|
||||
| `pop_loose_mob_draw` | 978 | 978 (гейт `mobs_live` работает) |
|
||||
| `guard_over_kid` | 14 424 | **0** |
|
||||
| `pop_char_skip_mask` | 28 605 | ~21 000 |
|
||||
|
||||
Три правки:
|
||||
|
||||
1. **`cd_sig_same`** — сравнение снимка БЕЗ построения структуры.
|
||||
`cd_sig_make` записывал тринадцать полей в стековый кадр (через
|
||||
`-n(ix)`), и лишь потом шёл побайтовый цикл; теперь сравнение идёт прямо
|
||||
с источником и выходит на первом расхождении. **−8 016**;
|
||||
2. **`guard_over_kid` по условию** — вопрос «кто поверх кого» не имеет
|
||||
смысла, когда не рисуется никто. Вызов перенесён ПОСЛЕ `skip_mask` и
|
||||
идёт только при `skip != 3`. **−14 118**;
|
||||
3. **`pop_cd_hit_slot`** — проверка «задет ли слот» брала пять аргументов,
|
||||
три из них стеком (45 % тактов на IX). Теперь координаты берутся из
|
||||
`pop_cd`, а сравнение вынесено в `hit_rect` с file-scope аргументами.
|
||||
**−3 684**.
|
||||
|
||||
**Отрицательный результат внутри третьей правки** (не повторять): первая
|
||||
версия была обёрткой, которая внутри всё равно звала `pop_cd_hit` с пятью
|
||||
аргументами — стало ХУЖЕ (1799 тактов Z80 вместо 1318). Снимать аргументы
|
||||
со стека надо у того, кто их читает, а не этажом выше.
|
||||
|
||||
### Отрицательные результаты 2026-08-19 — НЕ ПОВТОРЯТЬ
|
||||
|
||||
Три попытки подряд сделали ХУЖЕ. Общая ошибка в двух из них — я оценивал
|
||||
правку по СУММЕ ТАКТОВ ИНСТРУКЦИЙ в листинге, а не по реально исполняемому
|
||||
пути.
|
||||
|
||||
**1. `cd_sig_same` блоком вместо тринадцати сравнений.** Снимок был
|
||||
переложен так, чтобы сравнивать непрерывные 10 байт начала `pop_char_t`
|
||||
циклом `do { if (*a++ != *b++) return 0; } while (--i)`. По листингу
|
||||
функция стала короче (1939 → 1290 тактов), а на машине **стало хуже:
|
||||
438 978 → 450 426 (+11 448)**.
|
||||
|
||||
Причина: сумма по листингу считает каждую инструкцию ОДИН раз, а тело
|
||||
цикла исполняется ДЕСЯТЬ раз. Тринадцать линейных сравнений выполняются по
|
||||
разу каждое и выходят раньше на первом же расхождении. **Урок: короткий
|
||||
листинг ≠ быстрый код; цикл надо разворачивать в уме.**
|
||||
|
||||
**2. `cd_touch_pb` — пометка «для блита» из file-scope.** `pop_cd_touch`
|
||||
зовётся из `pop_blit_b` с четырьмя аргументами, хотя тот держит те же
|
||||
значения в `pb_x`/`pb_top`/`pb_w`/`pb_h`. Специализированный вход без
|
||||
аргументов дал **438 978 → 442 242 (+3 264)**.
|
||||
|
||||
Причина: в зелёной фазе блиты идут ПАКЕТНЫМ путём (`draw_tile` открывает
|
||||
`pop_cd_batch`), а там нужны все четыре значения сразу — и в регистрах
|
||||
(`x`, `y` приходят в HL/DE) они дешевле, чем чтение из статиков.
|
||||
**Снятие аргументов со стека помогает не всегда: если значение и так живёт
|
||||
в регистре, статик его туда ещё и загружать заставит.**
|
||||
|
||||
**3. Обёртка `pop_cd_hit_slot` поверх `pop_cd_hit`** (описана в P16):
|
||||
внутри всё равно звала функцию с пятью аргументами и добавила свои — стало
|
||||
1799 тактов вместо 1318. Помогло только когда сравнение переехало внутрь.
|
||||
|
||||
### P14. Fore-проход персонажа — от 4 122 до 117 570 [замеры 2026-08-19]
|
||||
|
||||
**Самая НЕСТАБИЛЬНАЯ статья кадра.** Замеры на одной и той же сцене:
|
||||
|
||||
| ситуация | fore-проход |
|
||||
|---|---:|
|
||||
| персонаж пропущен (`skip`) | 4 122 |
|
||||
| стоящий страж | 62 778 |
|
||||
| живой Кид у чомпера | ~89 000 |
|
||||
| **труп Кида в челюстях** | **117 570** |
|
||||
|
||||
Растёт от двух вещей: ширины футпринта (широкий кадр смерти, вынутый меч
|
||||
добавляет колонку) и числа тайлов с передним слоем внутри футпринта (здесь
|
||||
чомпер со своими зубьями). Отсюда практический вывод: **в бою проход будет
|
||||
ближе к сотне тысяч, чем к шестидесяти** — кадры выпадов и ударов широкие.
|
||||
|
||||
Замер трупа сделан по просьбе пользователя. Сама по себе эта ситуация не
|
||||
игровая («когда Кид — труп, игры нет»), но именно она показала верхнюю
|
||||
границу цены.
|
||||
|
||||
**РАЗБОР 2026-08-19: P14 сводится к P4.** Fore-проход Кида в тяжёлой
|
||||
позиции (89 268) разложен зондами:
|
||||
|
||||
| участок | такты |
|
||||
|---|---:|
|
||||
| вход + `pop_fore_set_clip` + `char_footprint` | 10 872 |
|
||||
| арифметика границ окна | 3 786 |
|
||||
| шов ворот + overlay-цикл | 3 294 |
|
||||
| **цикл `fore_tile` по тайлам** | **67 854** (76 %) |
|
||||
| `pop_gate_over_char` + хвост | 3 462 |
|
||||
|
||||
А счётчик показал, что цикл обходит **всего 4 тайла**, и 3 из них реально
|
||||
рисуют (`FORE_ANY != 0`). То есть 67 854 — это НЕ перебор лишних тайлов
|
||||
(их четыре) и не проверки, а **цена самих блитов переднего слоя**: около
|
||||
четырёх блитов по ~16 000, из которых 6 765 на каждом — фиксированная
|
||||
накладная (см. §1).
|
||||
|
||||
**Отсюда вывод для плана:** отдельной «оптимизации fore-прохода» почти нет.
|
||||
Срезать там можно ровно три вещи, и только первая крупная:
|
||||
|
||||
1. **цену блита (P4)** — 4 блита × 6 765 накладных = ~27 000 из 67 854;
|
||||
2. `char_footprint` из физики (**P10**) — часть от 10 872;
|
||||
3. слияние двух трамплинов в банк 2 — ~4 000.
|
||||
|
||||
Иначе говоря, **P4 ускоряет и зелёную фазу (8 блитов), и fore-проход
|
||||
(4 блита), то есть работает и в статике, и в динамике** — в отличие от
|
||||
P14, который я считал самостоятельной позицией.
|
||||
|
||||
`pop_char_fore` = два трамплина в банк 2 (`pop_fore_set_clip` +
|
||||
`pop_fore_over_char`) плюс обход тайлов футпринта, в каждом `fore_tile`.
|
||||
У Кида дороже, чем у стража, потому что в его футпринте лежит чомпер, а у
|
||||
чомпера есть собственный передний слой (`POP_CHOMP_FRAM_FOR`), который
|
||||
перерисовывается поверх персонажа каждый кадр.
|
||||
|
||||
Что можно пробовать, по возрастанию радикальности:
|
||||
|
||||
1. слить два трамплина в один вызов (мелочь, ~4 000);
|
||||
2. **P10** — брать футпринт из физики, а не считать заново (−11 574 на
|
||||
проход, то есть до −23 000 на двоих);
|
||||
3. гейт по сигнатуре: пропускать проход, если не изменились ни кадр
|
||||
персонажа, ни тайлы его футпринта. **Это расхождение с оригиналом** —
|
||||
он рисует foretable безусловно;
|
||||
4. **P13** — objtable и отложенные таблицы: у оригинала «посетить тайл»
|
||||
стоит копейки именно потому, что таблицы только копят записи.
|
||||
|
||||
### P3. Персонажи будятся каждый кадр ✅ ЧАСТИЧНО СБЫЛОСЬ
|
||||
|
||||
**В лёгкой позиции — да:** после P1 `pop_char_draw(KID)` стоит 204 такта,
|
||||
метка от чомпера до Кида больше не дотягивается.
|
||||
|
||||
**В тяжёлой позиции — нет:** стоит Киду шагнуть на 7 пикселей вправо, и его
|
||||
спрайт пересекается с тайлом чомпера и пламени, `skip_mask` перестаёт
|
||||
пропускать, и он снова стоит ~144 000 (54 738 draw + ~89 000 fore). То
|
||||
есть выигрыш P3 держится только пока персонаж не подошёл к анимированному
|
||||
тайлу — а в игре он к нему подходит постоянно.
|
||||
|
||||
Исходная оценка (−70 000) была:
|
||||
|
||||
heal 141 048 + отрисовка 148 566 = 290 000 тактов (36 % работы) уходят на то,
|
||||
что `pop_char_skip_mask` не может пропустить ни Кида, ни стража: фон трогают
|
||||
каждый кадр.
|
||||
|
||||
- **Кида** спасает P1: широкая пометка вокруг чомпера исчезнет;
|
||||
- **стража спасти нельзя** — пламя правого факела (0,7) рисуется в ячейке
|
||||
(0,8), где он и стоит. Там фон честно меняется, и оригинал персонажа тоже
|
||||
перерисовывает.
|
||||
|
||||
### P17. 16 бит там, где хватает 8 ✅ 2026-08-19 (замечание пользователя)
|
||||
|
||||
**1. Границы экрана — беззнаковыми сравнениями.** Проверка «спрайт целиком
|
||||
на экране» стояла как четыре ЗНАКОВЫХ 16-битных сравнения, а знаковое у
|
||||
SDCC z80 разворачивается в `sbc` плюс `jp PO / xor 0x80 / jp P`.
|
||||
Беззнаковая форма делает то же двумя: отрицательная координата становится
|
||||
очень большой и проваливает условие так же, как `>= 0`. **−378.**
|
||||
|
||||
**2. Габариты спрайтов в байтах.** `w`/`h`, `ow`/`oh`, `fpw`/`fph`,
|
||||
`cw`/`ch` в `pop_cdraw_t`, параметры `cd_overlay_add`/`cd_clip_add`, локали
|
||||
в `pop_char_draw`/`cd_splash` и чтение габарита из шапки ленты были
|
||||
`uint16_t`, хотя спрайты атласов не крупнее 64×64 (memory
|
||||
`pop_sprite_size_limits`). **−276 в статике, −1 134 в циане динамики**,
|
||||
плюс 24 байта `_DATA`.
|
||||
|
||||
### P6a. Кэш указателя модификаторов ✅ 2026-08-19 — −840 (ждали −20 000)
|
||||
|
||||
`pop_trob_modif` объявлен `__banked`, а звался на КАЖДЫЙ trob внутри цикла
|
||||
`pop_process_trobs`, хотя комната у них в подавляющем большинстве кадров
|
||||
одна. Указатель теперь кэшируется между итерациями.
|
||||
|
||||
Цикл trobs 78 726 → **74 964**, работа кадра 438 324 → **437 484**.
|
||||
|
||||
**Оценка в реестре была завышена в двадцать раз**, и стоит понять почему:
|
||||
я перенёс её по аналогии с лучом видимости (P2b), где трамплин звался
|
||||
ДЕВЯТЬ раз за кадр. Здесь trob'ов в комнате всего несколько, и кэш
|
||||
экономит два-три вызова. **Урок: «тот же паттерн» не означает «тот же
|
||||
порядок величины» — считать надо число вызовов, а не узнавать шаблон.**
|
||||
|
||||
### P6b. Кэш префетча кодов тайлов — НЕ ДЕЛАЛОСЬ
|
||||
|
||||
Префетч (`pop_level_access_begin/end` плюс чтение кода на каждый trob)
|
||||
стоит **11 058** за кадр. Кэшировать мешает инвалидация: код тайла меняет
|
||||
`pop_level_set_tile` (кнопка → пол, loose → empty), вход в комнату и
|
||||
добавление trob'а — пропустить хоть один источник значит получить
|
||||
застывшую анимацию. С учётом того, что P6a дал 840 вместо 20 000,
|
||||
ожидаемый выигрыш тут тоже стоит считать скромным, а риск он несёт
|
||||
несоразмерный.
|
||||
|
||||
### P10. Футпринт из физики — РАЗБОР 2026-08-19 (без реализации)
|
||||
|
||||
Идея из `perf_backlog.md` §1: `redraw_at_char` (seg003:0430) берёт ГОТОВЫЕ
|
||||
`char_col_left/right`, `char_top_row`, `char_bottom_row`, посчитанные в том
|
||||
же кадре физикой (`set_char_collision`, seg006:0723), а наш
|
||||
`char_footprint` (pop_bg.c) считает их заново внутри fore-прохода.
|
||||
|
||||
**Разбор показал, что «просто передать» не получится: величины разные.**
|
||||
|
||||
| | `char_footprint` (банк 2, fore) | `set_char_collision` (банк 3, физика) |
|
||||
|---|---|---|
|
||||
| ширина | габарит КАДРА `w` из атласа, `wh = (w+1)/2` | то же `fpw`, но затем **`FRAME_THIN` сдвигает края на ±4** |
|
||||
| меч | расширяет диапазон на колонку (`sword >= DRAWN`) | не расширяет |
|
||||
| колонки | `cLraw` до клампа (нужен для шва), затем кламп 0..9 | `coll_xl`/`coll_xr` в пикселях, колонки считает уже `calc_coll_window` |
|
||||
| ряды | `rT`/`rB` от ВЕРХА и НИЗА спрайта, с форсом `rT = rB-1` | `Char.curr_row` — опорный ряд, это другое |
|
||||
|
||||
То есть у оригинала обе задачи пользуются ОДНИМИ величинами, потому что он
|
||||
считает их один раз в `set_char_collision`. У нас они исторически
|
||||
разошлись: коллизии считают своё окно (с поправкой `FRAME_THIN`), fore —
|
||||
своё (габарит кадра плюс колонка под меч).
|
||||
|
||||
**Значит P10 — это не «передать готовое», а сперва СВЕСТИ обе величины к
|
||||
одной, как в оригинале.** Работа не механическая: `FRAME_THIN` влияет на
|
||||
коллизии осознанно (узкие кадры не должны цеплять стену), а fore-проходу
|
||||
нужен полный габарит, иначе передние грани в крайней колонке не
|
||||
перерисуются.
|
||||
|
||||
**Чего не хватает для решения:** отдельного замера самого
|
||||
`char_footprint`. Сейчас известно только «вход + `pop_fore_set_clip` +
|
||||
`char_footprint` = 10 872», а оценка −11 574 в backlog взята из старого
|
||||
замера другой сборки. Первым шагом нужен зонд между `set_clip` и
|
||||
`char_footprint`.
|
||||
|
||||
**Оценка приоритета:** низкая. Даже если `char_footprint` окажется всеми
|
||||
10 872, он платится только когда персонаж рисуется (в статике fore-прохода
|
||||
нет), а сведение двух геометрий к одной — это риск для коллизий, то есть
|
||||
для физики, которая сейчас работает правильно.
|
||||
|
||||
### P18. Метка «фон трогали» огрублена по X — ОТЛОЖЕНО (решение пользователя)
|
||||
|
||||
**Найдено 2026-08-19 пользователем:** Кид перерисовывается, хотя с пламенем
|
||||
не пересекается; на пиксель левее — перестаёт.
|
||||
|
||||
Разбор по памяти машины. Кид `x = 156`, спрайт занимает **x 213..224**,
|
||||
экранные y 43..83. Метка колонки 7 — y 33..50 (пламя правого факела).
|
||||
Колонка считается как `x >> 5`, то есть по 32 пикселя, и спрайт достаёт до
|
||||
224 — ровно первый пиксель колонки 7. По вертикали пересечение с меткой
|
||||
настоящее (43..50), поэтому слот считается задетым.
|
||||
|
||||
А по горизонтали пересечения НЕТ: пламя лежит в колонке 7 на x 232..247,
|
||||
между ним и Кидом восемь пикселей зазора. На пиксель левее спрайт
|
||||
кончается на 223, `223 >> 5 = 6`, колонка 7 не задета — и перерисовка
|
||||
пропадает.
|
||||
|
||||
То есть P15 исправил огрубление по Y и оставил его по X.
|
||||
|
||||
**Почему отложено (аргументы пользователя):**
|
||||
|
||||
- x лежит в 0..319 и в байт не влезает — нужен `uint16_t` на границу, то
|
||||
есть 4 байта на колонку (80 байт на две страницы), и **16-битные
|
||||
сравнения в горячем пути**. А они у SDCC z80 дороги ровно настолько,
|
||||
что могут съесть весь выигрыш (см. отрицательные результаты выше);
|
||||
- огрубить x вдвое (`x >> 1`, диапазон 0..159 влезает в байт) — это лишний
|
||||
сдвиг и при записи, и при проверке, плюс точность падает до 2 пикселей.
|
||||
|
||||
**Непроверенная идея на будущее:** хранить границы НЕ в экранных x, а как
|
||||
смещение ВНУТРИ колонки (0..31, пять бит). Тогда байта хватает и сравнение
|
||||
8-битное, но запись усложняется: прямоугольник, пересекающий несколько
|
||||
колонок, даёт частичные диапазоны у крайних и полные у средних.
|
||||
|
||||
**Когда браться:** если после других позиций бюджет всё ещё не сойдётся.
|
||||
Выигрыш будет именно в пограничных положениях, а их в игре много —
|
||||
персонаж почти всегда стоит рядом с чем-то анимированным.
|
||||
|
||||
### P4. Накладные блита — ОТКАЧЕНО
|
||||
|
||||
**Правка сделана и отменена по решению пользователя.** Критерий: если
|
||||
выигрыш получен ценой сильно усложнённого кода — откатывать.
|
||||
|
||||
Что было: `atlas_image_w0` в libbgi читал каталог из уже подключённой в W0
|
||||
страницы. **−408 на кадре** при ожидании −5 400.
|
||||
|
||||
Почему откачено: цена — вторая публичная функция в API libbgi с НЕЯВНЫМ
|
||||
контрактом («страница обязана быть подключена до вызова»), которую легко
|
||||
вызвать неправильно и молча получить мусор, плюс дублирование чтения
|
||||
каталога. 408 тактов — 0,09 % кадра, меньше разброса между прогонами.
|
||||
|
||||
**Что осталось знанием:** сам `gfx_w0_map` стоит всего **324** такта, а 672
|
||||
у `atlas_image` — это почти целиком вызов функции и арифметика `idx * 8`.
|
||||
Значит непробованная часть P4 («один map на группу блитов») имеет потолок
|
||||
~2 600 за кадр, а не 10 000, как считалось.
|
||||
|
||||
Ожидание было −5 400 (672 такта × 8 блитов зелёной фазы), и оно НЕ
|
||||
оправдалось: цена блита 16 107 → 16 005, то есть −102. Причина в том, что
|
||||
эти 672 — почти целиком вызов функции и арифметика `idx * 8`, а не само
|
||||
переключение окна. Замер после правки показывает, что работа просто
|
||||
переехала между статьями:
|
||||
|
||||
| этап | до | после |
|
||||
|---|---:|---:|
|
||||
| пролог + отсев | 810 | 762 |
|
||||
| `gfx_w0_map` | (в составе 2 400) | **324** |
|
||||
| каталог + шапка ленты + клип | | **2 694** |
|
||||
| ядро | 8 508 | 8 508 |
|
||||
| `cd_touch` + `unmap` + эпилог | 2 883 | 2 883 |
|
||||
| **фиксированная накладная** | **6 765** | **6 663** |
|
||||
|
||||
Правка оставлена: не вредит, убирает лишнее переключение W3 и делает
|
||||
контракт честнее (страница мапится один раз). Но как способ снять
|
||||
накладные она не работает.
|
||||
|
||||
**Что осталось непробованным** (и во что я теперь верю меньше): один
|
||||
`gfx_w0_map` на ГРУППУ блитов — судя по замеру, сам map стоит 324, так что
|
||||
потолок этой правки ~2 600 за кадр, а не 10 000, как считалось.
|
||||
|
||||
<details><summary>Исходная постановка (модель −28 000)</summary>
|
||||
|
||||
| правка | на блит | источник |
|
||||
|---|---:|---|
|
||||
| `pop_cd_touch`: `uint8_t` вместо `int` для `y`/`w`/`h`, ранний выход пакетного пути | ~−1 300 | новое |
|
||||
| один `gfx_w0_map`/`unmap` на ГРУППУ блитов | ~−1 350 | C5 / backlog §3 |
|
||||
| размеры ленты из каталога, без `atlas_image` и чтения шапки | ~−670 | C6 / backlog §2 |
|
||||
| `pop_blit_b`: аргументы в 8 бит, где хватает | ~−400 | новое |
|
||||
|
||||
Все четыре — низкий риск, механическая работа. Вместе снимают ~3 700 из
|
||||
6 126 фиксированных.
|
||||
|
||||
</details>
|
||||
|
||||
### P5. `pop_loose_tick` при пустой комнате — 28 872 → 2 760 ✅ СДЕЛАНО 2026-08-19
|
||||
|
||||
**Получено −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` разложен [ЗАМЕР 2026-08-19] — 89 784
|
||||
|
||||
| участок | такты |
|
||||
|---|---:|
|
||||
| вход + префетч кодов тайлов (маппинг окна 0) | **11 058** |
|
||||
| цикл: два `pop_torch_draw` | ~36 000 |
|
||||
| цикл: обход самих trob'ов | ~43 000 |
|
||||
|
||||
Цена одного `pop_pot_b` (пламя факела) измерена отдельно, брейкпоинтами на
|
||||
резидентных адресах: **17 346 тактов**, и это ЕДИНСТВЕННАЯ группа в
|
||||
распределении — то есть `pop_pot_b` в кадре зовут только два факела. При
|
||||
канвасе пламени 16×18 сами пиксели там 1 716, то есть **10 % цены**; всё
|
||||
остальное — накладные (см. §1) плюс ~6 900 сверх `pop_blit_b` на самом
|
||||
`pop_pot_b`.
|
||||
|
||||
Направления:
|
||||
|
||||
- **P6a**: `pop_trob_modif(room)` зовётся банковым вызовом на КАЖДЫЙ trob
|
||||
внутри цикла, хотя комната у них одна и та же — вынести наружу;
|
||||
- **P6b**: префетч 11 058 маппит окно 0 каждый кадр, а коды тайлов trob'ов
|
||||
меняются редко — кэшировать с инвалидацией по смене тайла/комнаты;
|
||||
- **P6c**: цена факела — это цена блита, то есть позиция P4.
|
||||
|
||||
Новое, найдено 2026-08-19.
|
||||
|
||||
### P7. G5. Раскол `draw_tile` на узкие части [оценка дока: −50 000 … −60 000]
|
||||
|
||||
Диспетчер + контекст оплачиваются целиком всегда; у оригинала это девять
|
||||
независимых функций. В 11/15 это те самые 56 200 на один тайл — но если
|
||||
сделан P1, `draw_tile` чомпера вообще не вызывается, и здесь эффект пропадёт.
|
||||
Ценность приёма — в ДРУГИХ сценах (13/23, любая комната с плитами).
|
||||
|
||||
**Риск средний**: в `draw_tile` собрано много инвариантов (BUG-LOOSE-3,
|
||||
BUG-LATTICE-DOORTOP, BUG-SEAM-WEDGE-1) — только отдельным заходом с прогоном
|
||||
всех уровней.
|
||||
|
||||
### P8. HEAL-WIDTH — ширина heal'ов по фактическому следу [оценка: 5-6 % цены heal'ов]
|
||||
|
||||
ОБЯЗАТЕЛЬНАЯ по решению пользователя (2026-08-18). След плиты 58 px в
|
||||
подземелье / 57 во дворце против используемых 60 и 64.
|
||||
Постановка — `TASKS_OPEN.md`, якорь `heal-width`.
|
||||
|
||||
### P9. G8 — пометку СОСЕДА ставить узкой полосой (28 px), а не тайлом [гипотеза]
|
||||
|
||||
Парная к HEAL-WIDTH. Относится к сценам с падающими плитами (13/23), в 11/15
|
||||
не играет. Разбор — `perf_green_phase.md` §G8, там же три условия, из-за
|
||||
которых это не «просто уменьшить число».
|
||||
|
||||
### P10. Футпринт персонажа — из физики, а не считать заново [замер: −11 574 на fore-проход]
|
||||
|
||||
`backlog` §1. С двумя персонажами — ~23 000 за кадр. Мешает то, что физика
|
||||
(банк 3) держит `char_col_left/right` в статиках, а слой фона — банк 2.
|
||||
**Риск средний**: окно fore-клипа заводилось под клинок и брызги.
|
||||
|
||||
### P11. Мелочи с известной ценой [замер, `backlog` §7]
|
||||
|
||||
| что | цена | где |
|
||||
|---|---:|---|
|
||||
| `pop_clip_char_top` — трамплин банк 4 → банк 3 ради одной проверки | 8 892 | `pop_cdraw.c` |
|
||||
| `cd_sig_make` + возврат из `pop_char_draw` | 7 944 | `pop_cdraw.c` |
|
||||
| `pop_loadkid` + расчёт координат кадра | 7 410 | `pop_cdraw.c` |
|
||||
| `obj_x * 8 / 7` — последнее `__divsint` в горячем пути | ~2 400 | `pop_char_draw` |
|
||||
|
||||
### P12. G9 — снять временную оснастку [замер: −6 000]
|
||||
|
||||
`pop_dbg_b1..b6` в `pop_blit_b` (~400 на блит), `pop_dbg_kind`/`m16`,
|
||||
`pop_dbg_m5..m15`, счётчик `rd_cnt` в `pop_redraw_needed`.
|
||||
**Только ПОСЛЕ окончания оптимизации** — без них не мерить.
|
||||
|
||||
### P13. Крупные рефакторинги — брать, только если понадобится ещё запас
|
||||
|
||||
**Чем P13 НЕ является (вопрос пользователя 2026-08-19).** Это не «рисовать
|
||||
комнату заново каждый кадр в скрытый буфер». Такой вариант исключён
|
||||
арифметикой: 30 тайлов по 5-6 спрайтов при цене блита 16 674 (и 179 914 за
|
||||
полную запечку одного тайла) дают порядка **3 000 000 тактов — семь
|
||||
растровых кадров**. Оригинал так тоже не делает: у него та же
|
||||
инкрементальная схема с пометками (`redraw_frames_full` / `_anim` /
|
||||
`_fore`), перерисовываются только помеченные тайлы.
|
||||
|
||||
Разница не в объёме отрисовки, а в цене ПОСЕЩЕНИЯ тайла: у нас
|
||||
`fore_tile(r, c)` сразу блитит (со всеми 6 126 фиксированных накладных), а
|
||||
у оригинала `add_backtable`/`add_midtable`/`add_foretable` только кладут
|
||||
запись в массив, и рисует один `draw_table()` в конце. Плюс у него ОДИН
|
||||
обход тайлов за кадр против наших трёх.
|
||||
|
||||
- **C7 / backlog §5-6: objtable + отложенные таблицы back/mid/fore.** У
|
||||
оригинала «посетить тайл» стоит копейки, потому что таблицы только копят
|
||||
записи, а рисует один `draw_table()` в конце. У нас блит идёт сразу из
|
||||
обхода, и fore-проход отдельный НА КАЖДОГО персонажа.
|
||||
- **backlog §4: единый проход по тайлам вместо трёх** (`pop_redraw_needed`,
|
||||
`pop_process_trobs`, `pop_fore_over_char`) и семь счётчиков причин
|
||||
перерисовки вместо одного `kind`.
|
||||
- **G6: меньше блитов в `RD_FLOOR`**, **G7: `mob_tick_one` в file-scope**.
|
||||
|
||||
---
|
||||
|
||||
## 4. ПЛАН РАБОТ — состояние между сессиями
|
||||
|
||||
Рабочий чеклист. Правило: одна позиция = один заход = один коммит с замером
|
||||
до/после на сцене 11/15. Замер обязателен даже когда «очевидно» — из семи
|
||||
закрытых позиций ТРИ дали не то, что ожидалось (P1 — вдвое меньше, P2a —
|
||||
почти ничего, таблицы порогов — регресс).
|
||||
|
||||
### Закрыто
|
||||
|
||||
| # | что | факт |
|
||||
|---|---|---|
|
||||
| P15 | точность метки «фон трогали» + раздельная проверка клинка | **−163 746** лёгкая / **−140 871** тяжёлая |
|
||||
| P16 | цианные проверки: снимок без структуры, `guard_over_kid` по условию, `hit_slot` без пяти аргументов | **−25 818** |
|
||||
| P5 | `loose_tick`: гейты холостого хода | **−33 840** (ждали −28 000) |
|
||||
| P1 | чомпер: перерисовка только при фазе < 6 | **−110 802** (ждали −160 000) |
|
||||
| P2b | луч видимости: колонки + один банковый вызов | **−26 448** (ждали −30 000) |
|
||||
| P2a | `coll_scan` в 8 бит + снят с IX | **−2 892** (крупной статьи в физике нет) |
|
||||
| P3 | Кид перестал будиться каждый кадр | сбылось само после P1 — но только в ЛЁГКОЙ позиции |
|
||||
| P2/P6 | замеры синей фазы и `process_trobs` | гипотеза «трамплины на спецсобытиях» отвергнута |
|
||||
| — | замер цианной фазы | крупного лишнего в отрисовке персонажа нет |
|
||||
|
||||
### Осталось, по убыванию ожидаемого эффекта
|
||||
|
||||
| # | что | ожидание | риск | комментарий |
|
||||
|---|---|---:|---|---|
|
||||
|
||||
| P4 | накладные блита — 6 663 на КАЖДЫЙ блит | частично сделано: **−408** | низкий | из четырёх правок сработала слабо; разбор ниже |
|
||||
| P14 | fore-проход персонажа | сводится к P4 + P10 | — | разбор ниже: цикл обходит всего 4 тайла |
|
||||
| P11 | мелочи с известной ценой | −26 000 | низкий | `clip_char_top` 8 658 подтверждён замером |
|
||||
| P10 | футпринт персонажа из физики | ? (нужен замер) | **высокий** | разбор ниже: величины физики и fore РАЗНЫЕ |
|
||||
| P6a/P6b | `trob_modif` из цикла, кэш префетча | −20 000 | низкий | тот же паттерн трамплина в цикле |
|
||||
| P7 | раскол `draw_tile` (G5) | −50 000 в 13/23 | средний | в 11/15 не играет |
|
||||
| ~~P8~~ | HEAL-WIDTH | ✅ сделано: плита 64→58, чомпер 64→61 | — | эффект ждёт прогона 13/23 |
|
||||
| P9 | G8 — пометка соседа полосой | не оценено | средний | для сцен с плитами |
|
||||
| P13 | objtable + отложенные таблицы, единый проход по тайлам | не оценено | очень высокий | большой рефакторинг слоя фона |
|
||||
| P12 | снять оснастку | −6 000 | нулевой | **последней**: без неё не мерить |
|
||||
|
||||
### Текущее состояние бюджета
|
||||
|
||||
| | работа | синяя | зелёная | циан | период |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| до оптимизации | 801 768 | 293 238 | 320 916 | 187 758 | 4 растра |
|
||||
| после P5 | 767 928 | 285 864 | 294 384 | 187 764 | 4 |
|
||||
| после P1 (медиана) | 657 882 | 286 503 | 183 420 | 187 761 | 4 |
|
||||
| после P2a | 654 990 | 283 215 | 183 798 | 187 812 | 4 |
|
||||
| **после P2b (лёгкая позиция)** | **628 542** | 259 500 | 181 068 | 187 761 | 4 |
|
||||
| ТЯЖЁЛАЯ позиция (Кид на шаг правее) | 758 358 | 257 520 | 180 870 | 319 842 | **4 и 5** |
|
||||
| **после P15, лёгкая** | **464 796** | 223 902 | 181 494 | **59 406** | 4 |
|
||||
| после P15, тяжёлая | 617 487 | 245 808 | 181 761 | 192 090 | **4 везде** |
|
||||
| после P16, лёгкая | 438 978 | 218 052 | 181 494 | 39 438 | 4 |
|
||||
| ~~после P4~~ | ~~438 570~~ | | | | правка **ОТКАЧЕНА** |
|
||||
| после P17, лёгкая | 438 324 | 217 590 | 181 098 | 39 192 | 4 |
|
||||
| **после P6a, лёгкая** | **437 484** | 217 704 | 180 252 | 39 306 | 4 |
|
||||
| **после P17, тяжёлая** | **603 684** | 241 956 | 181 464 | 180 270 | 4 |
|
||||
|
||||
Итог восьми позиций: **801 768 → 464 796 в лёгкой позиции (−42 %)** и
|
||||
**758 358 → 617 487 в тяжёлой (−19 %)**. Отдельно важно: в тяжёлой позиции
|
||||
исчезли пятирастровые кадры (было 27 %), период стал ровно 4 — рывки ушли.
|
||||
|
||||
### Достижима ли цель — арифметика на 2026-08-19
|
||||
|
||||
Цель: работа ≤ 430 000, тогда период станет 3 растра (хвост кадра — три
|
||||
`gfx_wait_vsync`).
|
||||
|
||||
- в ЛЁГКОЙ позиции снять надо **7 484**;
|
||||
- в ТЯЖЁЛОЙ — **173 684**.
|
||||
|
||||
**Лёгких путей больше не осталось.** За 2026-08-19 отвергнуто ЧЕТЫРЕ
|
||||
правки подряд (три с регрессом, одна почти без эффекта), и все они целили
|
||||
в накладные проверок и блита. Фиксированная часть блита 6 663 держится
|
||||
ядром `gfx_w0_map`/`cd_touch`/чтения шапки, а не «лишними» вызовами.
|
||||
|
||||
Всё оставшееся в списке, кроме P13, даёт по оценкам **порядка 100 000** — и
|
||||
это оптимистично. **Арифметика не сходится:** сцена с двумя персонажами,
|
||||
чомпером и двумя факелами в три растра не укладывается без одного из трёх
|
||||
решений:
|
||||
|
||||
1. **P13** — переход на objtable и отложенные таблицы, как в оригинале
|
||||
(единственный резерв нужного размера, но это переписывание слоя фона);
|
||||
2. **осознанное расхождение с оригиналом** — например, не перерисовывать
|
||||
передний слой персонажа, пока не изменились ни персонаж, ни тайлы под
|
||||
ним (гейт по сигнатуре футпринта);
|
||||
3. **принять 4 растра** как рабочий режим для сцен такой плотности и
|
||||
выравнивать период, чтобы не было рывков 4/5.
|
||||
|
||||
Решение за пользователем — это выбор между точностью порта и скоростью.
|
||||
|
||||
### Как воспроизвести сцену (важно для следующей сессии)
|
||||
|
||||
Сборка стартует прямо в ней: `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
|
||||
|
||||
| # | приём | эффект | тип оценки | риск |
|
||||
|---|---|---:|---|---|
|
||||
| P1 | чомпер: только anim-слой | −160 000 | модель | низкий |
|
||||
| P3 | Кид перестанет будиться | −70 000 | модель | следствие P1 |
|
||||
| P2 | логика двух Char | −40 000 … −70 000 | гипотеза | ? |
|
||||
| P4 | накладные блита (4 правки) | −28 000 | модель | низкий |
|
||||
| P5 | `loose_tick` без плит | ✅ −33 840 | ФАКТ | сделано |
|
||||
| P6 | цикл `process_trobs` | −20 000 … −40 000 | гипотеза | ? |
|
||||
| P10 | футпринт из физики | −23 000 | замер | средний |
|
||||
| P11 | мелочи (4 штуки) | −26 000 | замер | низкий |
|
||||
| P12 | снять оснастку | −6 000 | замер | нулевой |
|
||||
| P7 | раскол `draw_tile` | 0 здесь (−50 000 в 13/23) | оценка | средний |
|
||||
| P8/P9 | HEAL-WIDTH / G8 | 0 здесь (сцены с плитами) | оценка | низкий/средний |
|
||||
|
||||
Верхняя часть списка (P1 + P3 + P4 + P5) — **около −286 000 из 801 768, то
|
||||
есть 36 % работы кадра**, и вся она низкого риска. Этого хватит, чтобы
|
||||
сцена ушла с 1,86 растрового кадра до ~1,2 — но НЕ хватит, чтобы период
|
||||
кадра упал с 4 растров до 3: для этого работа должна уложиться в 430 000,
|
||||
то есть нужны ещё ~90 000 сверху (P2 или P6).
|
||||
|
||||
---
|
||||
|
||||
## 5б. Цианная фаза разложена [ЗАМЕР 2026-08-19]
|
||||
|
||||
Фаза 188 004 тактов, и она НЕ менялась ни от P1, ни от P5, ни от P2b.
|
||||
|
||||
| участок | такты |
|
||||
|---|---:|
|
||||
| `check_mirror` | 3 198 |
|
||||
| `loose_mob_draw` + `guard_over_kid` + `skip_mask` | **34 374** |
|
||||
| **`pop_char_draw(KID)`** | **204** |
|
||||
| **соперник: `char_draw` + `char_fore`** | **145 896** |
|
||||
| `fore_needed` + `hp_draw` | 2 598 |
|
||||
| `char_fore(KID)` + борта | 1 758 |
|
||||
|
||||
**Кид уже пропускается** — 204 такта, то есть надежда P3 всё-таки сбылась
|
||||
после P1: метка от чомпера до него больше не дотягивается. А страж
|
||||
перерисовывается каждый кадр, и это ЧЕСТНО: пламя правого факела (0,7)
|
||||
рисуется в ячейке (0,8), где он стоит, и реально накрывает ему голову
|
||||
(пламя занимает y 5..22, страж 12..62).
|
||||
|
||||
Отрисовка стража (148 302) по частям:
|
||||
|
||||
| участок | такты | доля |
|
||||
|---|---:|---:|
|
||||
| **`pop_char_fore`** (два трамплина в банк 2 + обход тайлов) | **62 778** | 42 % |
|
||||
| клинок: `pop_sword_draw` + `cd_overlay_add` + `cd_clip_add` | 27 522 | 19 % |
|
||||
| блит спрайта + `clip_char_right` | 20 982 | 14 % |
|
||||
| загрузка кадра и геометрия | 12 696 | 9 % |
|
||||
| **`pop_clip_char_top`** (трамплин банк 4 → банк 3) | 8 658 | 6 % |
|
||||
| снимок прямоугольника + `cd_clip_add` | 7 890 | 5 % |
|
||||
| `gfx_w0_unmap` + `cd_sig_make` | 4 968 | 3 % |
|
||||
| вход + `cd_heal` | 2 946 | 2 % |
|
||||
|
||||
**Вывод: крупного лишнего здесь нет.** Единственная явно лишняя статья —
|
||||
трамплин `clip_char_top` (8 658), и убрать его непросто: функции нужны
|
||||
`get_tile` и таблицы деления из банка 3, а перенос в резидент вернёт тот же
|
||||
трамплин внутрь. Всё остальное — работа, которую персонаж действительно
|
||||
делает: рисует себя, клинок и передний слой поверх себя.
|
||||
|
||||
## 6. Иерархия референсов (уточнена 2026-08-19)
|
||||
|
||||
Сравнение трёх реализаций луча видимости показало, что источники не
|
||||
равноценны, и это важно для ЛЮБОЙ будущей оптимизации:
|
||||
|
||||
| источник | что берём | чего НЕ берём |
|
||||
|---|---|---|
|
||||
| **Apple II** (`Prince-of-Persia-Apple-II`) | как это делается на 8 битах: таблицы вместо делений, борьба за такты | ничего — но код на 6502, читать сложнее |
|
||||
| **SDLPoP** | эталон ПОВЕДЕНИЯ (декомпиляция DOS-версии) | реализацию: она нарочно «расслаблена» под 32 бита |
|
||||
| **mininim** | разбор краевых случаев, второе мнение о замысле | алгоритмы — переписан с нуля, механика местами своя |
|
||||
|
||||
Доказательство на конкретном месте: `get_tile_div_mod` в SDLPoP содержит
|
||||
комментарий
|
||||
|
||||
```c
|
||||
// DOS PoP does this:
|
||||
// obj_xl = tile_mod_tbl[xpos];
|
||||
// return tile_div_tbl[xpos];
|
||||
```
|
||||
|
||||
а вместо этого делает `x % TILE_SIZEX` и `x / TILE_SIZEX`. Таблицы в файле
|
||||
лежат, но нужны только для эмуляции чтения DOS-версии ЗА ГРАНИЦЕЙ массива.
|
||||
Apple II (`CTRLSUBS.S`, `GETBLOCKX`) читает ровно `BlockTable[x]`.
|
||||
|
||||
**Правило:** сверять поведение по SDLPoP, а реализацию под 8 бит — по
|
||||
Apple II и по комментариям вида «DOS PoP does this» в самом SDLPoP.
|
||||
|
||||
## 7. Повторяющийся источник цены: банковый трамплин в цикле
|
||||
|
||||
Уже трижды крупнейшей статьёй оказывался не алгоритм, а вызов `__banked`-
|
||||
функции ИЗ ЦИКЛА, идущего в другом банке:
|
||||
|
||||
| место | цена | лечение |
|
||||
|---|---:|---|
|
||||
| луч видимости: `pop_tile_at` по колонке (P2b) | 36 786 → 13 002 | один вызов на весь отрезок |
|
||||
| `pop_clip_char_top` — банк 4 → банк 3 ради одной проверки | 8 892 | не сделано (P11) |
|
||||
| `pop_trob_modif(room)` на каждый trob в цикле | не мерено | не сделано (P6a) |
|
||||
|
||||
**Что проверять в первую очередь при новом «дорогом» месте:** не сколько
|
||||
там арифметики, а сколько раз за кадр пересекается граница банка.
|
||||
@@ -3337,3 +3337,46 @@ draw_tile_anim(); draw_tile_bottom(0); draw_loose(0);
|
||||
`draw_tile_anim_right` — то есть анимированной правой грани СОСЕДА (пики,
|
||||
ворота, loose). Симптома на это пока не видели; если всплывёт «правая грань
|
||||
соседа под персонажем/куском» — смотреть сюда.
|
||||
|
||||
### Вариант B: порт корзины 30 (2026-08-18)
|
||||
|
||||
Реализовано то, что разобрано в коммите `272cf8f`. Кусок, у которого ряд
|
||||
объекта вне 0..2 (`y_to_row_mod4` даёт −1 и «выше потолка», и «ниже пола»),
|
||||
считается объектом ТАЙЛА 30 — а такие оригинал рисует ПЕРВЫМИ, до всего
|
||||
обхода тайлов. Два следствия:
|
||||
|
||||
- `defer = 0` — кусок идёт под всем, включая Кида (раньше сравнение рядов
|
||||
давало обратное: «−1 обходится последним» читалось как «поверх»);
|
||||
- оверлею отдаётся ориентир **3** («раньше любого ряда обхода 2,1,0»)
|
||||
вместо сырого −1, и в набор перекрываемых тайлов добавляется **своя
|
||||
клетка** — у куска в обычном ряду её перерисовывать нельзя, там объект
|
||||
вливается в midtable уже после частей своего тайла.
|
||||
|
||||
**Замер 13/23 (3032 кадра, зонды фаз + детализация зелёной):**
|
||||
|
||||
| секция | тег `mob-order-B-start` | после B | дельта |
|
||||
|---|---:|---:|---:|
|
||||
| работа | 888 984 | **913 848** | +24 864 |
|
||||
| синяя | 159 804 | 159 810 | +6 |
|
||||
| зелёная | 427 242 | **440 418** | +13 176 |
|
||||
| циан | 378 864 | 393 000 | +14 136 |
|
||||
|
||||
Период кадра не изменился: 4 растра в 23 кадрах, 5 в двух — как и до правки.
|
||||
|
||||
**Зелёная вышла за растровый кадр** (440 418 против 430 000) при цели
|
||||
400 000. Детализация показывает, где именно:
|
||||
|
||||
| участок зелёной | максимум |
|
||||
|---|---:|
|
||||
| `pop_loose_tick` целиком | 185 826 |
|
||||
| — из них `pop_loose_mob_tick` | **168 180** |
|
||||
| — циклы по тайлам | 23 736 |
|
||||
| — `check_loose_fall_on_kid` | 5 868 |
|
||||
| тробы + `pop_redraw_needed` | 337 800 |
|
||||
| шов и прочее | 11 964 |
|
||||
|
||||
(максимумы взяты по разным кадрам, поэтому не складываются в общий).
|
||||
|
||||
Основной кандидат на возврат этих тактов — задача
|
||||
[HEAL-WIDTH](TASKS_OPEN.md#heal-width): heal'ы кусков и плит берут ширину по
|
||||
клеткам, а фактический след — 58/57.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -55,7 +55,7 @@ ESC выход.
|
||||
|
||||
## Статус (2026-08-18)
|
||||
|
||||
**Уровни 1-3 приняты, 4-9 прошли предварительный тест.** Работает: комнаты и
|
||||
**Уровни 1-3 приняты, 4-12 прошли предварительный тест.** Работает: комнаты и
|
||||
переходы, Kid (анимация, управление, коллизия/падение, зацеп/подтягивание/
|
||||
спуск, окклюзия), кнопки и ворота, пики, чомперы, проваливающиеся полы, дверь
|
||||
уровня и переход на следующий, меч и бой, стражи с ИИ (включая скелета и
|
||||
|
||||
@@ -44,7 +44,10 @@
|
||||
| 7 | **предварительно пройден** (2026-08-18) | [SPIKE-BAKED](BUGS_CLOSED.md#spike-baked) — выдвинутые пики консервировались в фон запечкой соседней кнопки и оставались навсегда (`980d48c`); [DIED-ON-BUTTON](BUGS_CLOSED.md#died-on-button) — смерть на кнопке не ломала её насовсем: до `check_press` труп не доходил, самой `died_on_button` не было, а таймер связи переживал респавн (`6feab5d`) |
|
||||
| 8 | **предварительно пройден** (2026-08-18) | [GUARD-RESPAWN-COL0](BUGS_CLOSED.md#guard-respawn-col0) — при возврате в комнату страж телепортировался в колонку 0 и падал насмерть: колонку несёт `guards_x`, а не тайл (`c40ae3f`); [SEAM-FIGHT-FLICKER](BUGS_CLOSED.md#seam-fight-flicker) — бой у шва перерисовывал комнату туда-обратно: `leave_room` запрещает уход ещё и на кадрах меча (`a498255`, сверено с SDLPoP покадрово) |
|
||||
| 9 | **предварительно пройден** (2026-08-18) | багов не найдено |
|
||||
| 10+ | не начат | |
|
||||
| 10 | **предварительно пройден** (2026-08-18) | багов не найдено |
|
||||
| 11 | **предварительно пройден** (2026-08-18) | [MOB-NEIGHBOUR-ROOM](BUGS_CLOSED.md#mob-neighbour-room) — плиты нижнего ряда пропадали без кадров падения (кусок в соседней комнате не рисовался, `ed5615a`); порядок куска относительно соседней плиты и Кида — порт корзины 30 (`4db60c7`) |
|
||||
| 12 | **предварительно пройден** (2026-08-18) | багов не найдено |
|
||||
| 13+ | не начат | |
|
||||
|
||||
«Предварительно пройден» ≠ «принят»: уровни 4-6 пройдены прогоном по
|
||||
сценарию, а не обходом ВСЕХ комнат — полные обходы делаются по готовности
|
||||
@@ -258,6 +261,64 @@ MEM-BANK2, MEM-BANK5, CLIP-1, KBD-1, DBG-CHEATS.
|
||||
|
||||
## P0 — делаем сейчас
|
||||
|
||||
### <a id="char-partial-redraw"></a>CHAR-PARTIAL-REDRAW. Неподвижный персонаж перерисовывается зря: метка «фон трогали» слишком груба — ОБЯЗАТЕЛЬНО
|
||||
|
||||
**Постановка (пользователь, 2026-08-19).** Проверять, нужна ли отрисовка
|
||||
стража, когда он НЕ ДВИГАЕТСЯ. Если движется — лишние ~150 000 тактов
|
||||
приемлемы: в оригинале во время боя число физических кадров на логический
|
||||
тоже растёт на единицу.
|
||||
|
||||
**Измерено** (сцена 11/15, чтение `pop_cd` из памяти машины):
|
||||
|
||||
| объект | x | y |
|
||||
|---|---|---|
|
||||
| страж, спрайт | 257..284 | 18..56 |
|
||||
| страж, клинок (накладной) | 241..261 | 31..37 |
|
||||
| пламя факела (колонка 6 → рисуется в ячейке 7) | 232..247 | 5..22 |
|
||||
|
||||
**Физического перекрытия НЕТ.** По x клинок и пламя пересекаются
|
||||
(241..247), но по y расходятся: пламя кончается на 22, клинок начинается с
|
||||
31 — девять пикселей чистого зазора. Пользователь увидел это на экране
|
||||
раньше, чем я в числах: «страж полностью правее пламени, только меч может
|
||||
пересекаться».
|
||||
|
||||
**Почему тогда он перерисовывается.** Метка «фон трогали» (`pop_cd_touch`
|
||||
в `pop_tile.c`) хранится как битовая маска КОЛОНОК по 32 px, отдельно на
|
||||
каждый из ТРЁХ рядов по 63 px (`cd_row_of`). Пламя и клинок попадают в
|
||||
один ряд 0 и в одну колонку 7 — и `cd_quiet` считает слот задетым.
|
||||
Расплата: **148 302 такта, 23 % работы кадра** (85 524 спрайт с клинком и
|
||||
снимком + 62 778 fore-проход) за перекрытие, которого нет.
|
||||
|
||||
**Решение — поднять вертикальную точность метки.** Вместо «маска колонок ×
|
||||
3 ряда» хранить на каждую колонку ДИАПАЗОН y (`ymin`/`ymax`): 10 колонок ×
|
||||
2 байта × 2 страницы = 40 байт. `pop_cd_touch` расширяет диапазон
|
||||
затронутых колонок, `pop_cd_hit` проверяет пересечение диапазонов.
|
||||
|
||||
Проверка решения на обоих случаях сцены:
|
||||
|
||||
- **страж:** клинок в колонках 7-8. В колонке 7 у метки лежит y 5..22
|
||||
(пламя), у клинка 31..37 — не пересекаются, колонку 8 пламя не трогало.
|
||||
Слот остаётся «тихим» → экономия 148 302;
|
||||
- **Кид в тяжёлой позиции:** пламя левого факела y 5..22, Кид y 15..55 —
|
||||
пересечение НАСТОЯЩЕЕ, и он честно перерисовывается. Так и должно быть.
|
||||
|
||||
Вариант с 8-пиксельными полосами вместо диапазонов тоже работает
|
||||
(24 полосы), но 16-пиксельные УЖЕ НЕТ: пламя и клинок снова слипаются в
|
||||
одной полосе. Диапазон на колонку точнее и не требует битовой возни.
|
||||
|
||||
**Чего делать НЕ надо** (проверено расчётом, не повторять): частичную
|
||||
перерисовку персонажа по пересечению и обрезку фона под ним. Обе правки
|
||||
решают задачу, которой нет — перекрытия не существует.
|
||||
|
||||
**Проверка:** замер 11/15 в обеих позициях Кида (лёгкая x = 99, тяжёлая
|
||||
x = 106; разбор — [`../docs/perf_l11_room15.md`](../docs/perf_l11_room15.md)
|
||||
§7), плюс визуальный прогон боя и прохода Кида под факелами: персонаж не
|
||||
должен оставлять хвостов и не должен просвечивать сквозь пламя.
|
||||
|
||||
**Место в очереди — [`../docs/perf_registry.md`](../docs/perf_registry.md),
|
||||
позиция P15.** Смежное: P14 (fore-проход — вторая половина той же цены).
|
||||
|
||||
|
||||
### <a id="heal-width"></a>HEAL-WIDTH. Ширина точечных heal'ов не совпадает со следом перерисовки — ОБЯЗАТЕЛЬНО
|
||||
|
||||
**Постановка (пользователь, 2026-08-18).** Ширины heal'ов взяты «на глаз по
|
||||
@@ -303,6 +364,18 @@ MEM-BANK2, MEM-BANK5, CLIP-1, KBD-1, DBG-CHEATS.
|
||||
прогон комнат с плитами, пиками и чомперами: сужение heal'а — самый прямой
|
||||
способ получить «недочищенный хвост».
|
||||
|
||||
**Родня — [G8](../docs/perf_green_phase.md) (пометка соседа узкой полосой).**
|
||||
Это две половины одной темы, и брать их логично вместе: G8 про ширину
|
||||
ЗАПЕЧКИ соседнего тайла (60 вместо 28 нужных, да ещё `draw_tile` соседа
|
||||
дважды на пометку), HEAL-WIDTH — про ширину HEAL'ов (64 вместо 58/57).
|
||||
Числа там уже разобраны: 60 = 32 свой тайл + 28 собственный свес, и для
|
||||
запечки САМОГО тайла 60 минимальны — сужать можно только пометку СОСЕДА.
|
||||
|
||||
**Место в общей очереди — [`../docs/perf_registry.md`](../docs/perf_registry.md)
|
||||
(позиция P8).** Там же собраны ВСЕ отложенные оптимизации из трёх фазовых
|
||||
доков, отсортированные по измеренному эффекту, и разложена цена одного блита
|
||||
(6 126 тактов фиксированной накладной на любой блит, независимо от размера).
|
||||
|
||||
|
||||
### <a id="l12-shadow"></a>L12-SHADOW. Уровень 12: встреча с тенью, бой, слияние
|
||||
|
||||
|
||||
@@ -56,39 +56,16 @@ static uint8_t tile_is_floor(uint8_t t)
|
||||
}
|
||||
}
|
||||
|
||||
/* Тайл в ряду Кида по X-координате персонажа (порт get_tile_at_kid,
|
||||
* seg003:761): xpos − 7 делится на ширину тайла с округлением вниз. */
|
||||
static int8_t tile_col; /* колонка последнего tile_at_kid */
|
||||
/* get_tile_at_kid (seg003:761) СНЯТ вместе с переходом луча на колонки:
|
||||
* единственным его вызывающим был этот луч, а он больше не работает с
|
||||
* X-координатами (разбор — в теле pop_check_can_guard_see_kid ниже).
|
||||
* Таблица POP_TILE_DIV осталась в резиденте, ею пользуется физика. */
|
||||
|
||||
static uint8_t tile_at_kid(int16_t xpos)
|
||||
{
|
||||
/* get_tile_div_mod_m7 (seg006:697): колонка = floor((xpos − 7 − 58)/14),
|
||||
* СРАЗУ в координатах комнаты (58 = x_bump[FIRST_ONSCREEN_COLUMN]).
|
||||
* Оригинал берёт её из tile_div_tbl — и мы теперь тоже: резидентная
|
||||
* POP_TILE_DIV[] это ровно она (индекс = xpos, значение = floor((xpos−58)/14)),
|
||||
* поэтому здесь индексируем её на xpos−7.
|
||||
*
|
||||
* Было честное `/` и `%`: у SDCC z80 это __divsint плюс __modsint, а тот
|
||||
* внутри снова зовёт __divsint — около 5 400 тактов на вызов. А зовут
|
||||
* нас В ЦИКЛЕ по колонкам между стражем и Кидом
|
||||
* (check_can_guard_see_kid, seg003:761). Когда Кид у шва, его
|
||||
* curr_col = −1, луч тянется через всю комнату, и за кадр набегало ВОСЕМЬ
|
||||
* пар делений — ~43 000 тактов, 10 % растрового кадра, в фазе логики
|
||||
* (замер MAME 2026-08-10, уровень 1 комната 2).
|
||||
*
|
||||
* Медленный хвост остаётся для xpos за пределами таблицы (страж и Кид в
|
||||
* разных комнатах: логическая X уезжает на ±140). */
|
||||
int16_t x = xpos - 7;
|
||||
if ((uint16_t)x < 256u) {
|
||||
tile_col = POP_TILE_DIV[x];
|
||||
} else {
|
||||
int16_t v = x - SCREENSPACE_X;
|
||||
int16_t col = v / TILE_SIZEX;
|
||||
if (v % TILE_SIZEX < 0) --col;
|
||||
tile_col = (int8_t)col;
|
||||
}
|
||||
return pop_tile_at(tile_col, Kid.curr_row);
|
||||
}
|
||||
/* Буфер тайлов луча — file-scope, а не локальный массив: локальный уезжает
|
||||
* в стековый кадр, и каждое чтение из него стоит `-n(ix)` (19 тактов
|
||||
* номинала, ~46 с wait-state'ами). Тот же приём, что у draw_tile и
|
||||
* pop_blit_b. Размер — колонки −1..10 плюс запас. */
|
||||
static uint8_t row_t[12];
|
||||
|
||||
/* check_can_guard_see_kid (seg003:688) — луч видимости по ряду.
|
||||
* Результат в can_guard_see_kid: 0 не видит / 1 видит, но не пойдёт /
|
||||
@@ -96,7 +73,6 @@ static uint8_t tile_at_kid(int16_t xpos)
|
||||
void pop_check_can_guard_see_kid(void) __banked
|
||||
{
|
||||
uint8_t kid_frame = Kid.frame;
|
||||
int16_t left_pos, right_pos;
|
||||
uint8_t tile;
|
||||
|
||||
/* МЫШЬ Кида «не видит» (seg003:0965): иначе он выхватит меч на зверька,
|
||||
@@ -122,20 +98,39 @@ void pop_check_can_guard_see_kid(void) __banked
|
||||
}
|
||||
|
||||
can_guard_see_kid = 2;
|
||||
left_pos = pop_x_bump[Kid.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
|
||||
right_pos = pop_x_bump[Guard.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
|
||||
if (left_pos > right_pos) {
|
||||
int16_t t = left_pos; left_pos = right_pos; right_pos = t;
|
||||
}
|
||||
/* Чомпер стоит у ЛЕВОГО края тайла — свой тайл он не перекрывает. */
|
||||
if (tile_at_kid(left_pos) == TILE_CHOMPER)
|
||||
left_pos += TILE_SIZEX;
|
||||
/* Ворота — у ПРАВОГО края, поэтому крайний правый тайл не считается. */
|
||||
if (tile_at_kid(right_pos) == TILE_GATE)
|
||||
right_pos -= TILE_SIZEX;
|
||||
/* ПО КОЛОНКАМ, а не по X-координатам.
|
||||
*
|
||||
* Оригинал (и SDLPoP, seg003:688) идёт по x с шагом TILE_SIZEX и на
|
||||
* каждом шаге переводит x в колонку — потому что x у него уже под рукой.
|
||||
* Но начальные x — это РОВНО центры тайлов персонажей
|
||||
* (x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX), а перевод обратно
|
||||
* даёт ту же колонку: floor((58 + col*14 - 58) / 14) == col. Значит
|
||||
* весь проход эквивалентен обходу колонок, и ни одна x-координата тут не
|
||||
* нужна: уходят 16-битный шаг, 16-битное сравнение и индексация таблицы
|
||||
* POP_TILE_DIV на каждой итерации.
|
||||
*
|
||||
* Тайлы отрезка забираем ОДНИМ банковым вызовом. Раньше на каждую
|
||||
* колонку шёл свой pop_tile_at (__banked, банк 1 -> банк 3): на сцене
|
||||
* 11/15 это девять трамплинов и 36 786 тактов на весь луч (замер
|
||||
* 2026-08-19). */
|
||||
{
|
||||
int8_t lcol = Kid.curr_col, rcol = Guard.curr_col, col, lcol0;
|
||||
|
||||
while (left_pos <= right_pos) {
|
||||
tile = tile_at_kid(left_pos);
|
||||
if (lcol > rcol) { int8_t t = lcol; lcol = rcol; rcol = t; }
|
||||
lcol0 = lcol; /* БАЗА индексации буфера — фиксируем
|
||||
* ДО сдвигов ниже: row_t[0] это
|
||||
* тайл lcol0, и сдвиг lcol/rcol на
|
||||
* чомпере и воротах её менять не
|
||||
* должен. */
|
||||
pop_row_tiles(Kid.curr_row, lcol, rcol, row_t);
|
||||
|
||||
/* Чомпер стоит у ЛЕВОГО края тайла — свой тайл он не перекрывает. */
|
||||
if (row_t[0] == TILE_CHOMPER) lcol++;
|
||||
/* Ворота — у ПРАВОГО края, поэтому крайний правый тайл не считается. */
|
||||
if (row_t[rcol - lcol0] == TILE_GATE) rcol--;
|
||||
|
||||
for (col = lcol; col <= rcol; col++) {
|
||||
tile = row_t[col - lcol0];
|
||||
/* Сквозь это не видно вовсе. */
|
||||
if (tile == TILE_WALL || tile == TILE_DOORTOP_FLOOR || tile == TILE_DOORTOP) {
|
||||
can_guard_see_kid = 0;
|
||||
@@ -146,10 +141,10 @@ void pop_check_can_guard_see_kid(void) __banked
|
||||
if (tile == TILE_LOOSE || tile == TILE_CHOMPER || !tile_is_floor(tile)) {
|
||||
can_guard_see_kid = 1;
|
||||
} else if (tile == TILE_GATE &&
|
||||
pop_gate_modif(tile_col, Kid.curr_row) < 112) {
|
||||
pop_gate_modif(col, Kid.curr_row) < 112) {
|
||||
can_guard_see_kid = 1; /* ворота подняты не до конца */
|
||||
}
|
||||
left_pos += TILE_SIZEX;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -670,7 +670,9 @@ static uint8_t tile_in_fclip(int row, int col)
|
||||
|
||||
static void fore_tile(int row, int col) /* передний слой одного тайла */
|
||||
{
|
||||
pop_dbg_rdmax[1]++; /* ВРЕМЕННО: сколько тайлов обошли */
|
||||
if (!tile_in_fclip(row, col)) return;
|
||||
pop_dbg_rdmax[2]++; /* ВРЕМЕННО: прошли окно клипа */
|
||||
/* draw_loose (seg008:0A38) — единственный кусок ТАЙЛА, который оригинал
|
||||
* кладёт В ОБЕ таблицы БЕЗУСЛОВНО: нижняя грань loose-плиты идёт и в
|
||||
* backtable, и в foretable. Значит передняя грань плиты рисуется ПОВЕРХ
|
||||
@@ -681,6 +683,7 @@ static void fore_tile(int row, int col) /* передний слой одно
|
||||
* кромке loose над дырой от соседней упавшей плиты). */
|
||||
ft_code = pop_tile_code_drawn(row, col); /* код тайла — ОДИН раз на тайл */
|
||||
if (!FORE_ANY[ft_code]) return; /* нечего класть в передний слой */
|
||||
pop_dbg_rdmax[3]++; /* ВРЕМЕННО: реально рисуем */
|
||||
ft_x = POP_COL_XH[col] * 8;
|
||||
ft_dmy = 63 * row + 62;
|
||||
if (row >= 0 && ft_code == 11)
|
||||
@@ -1000,6 +1003,8 @@ void pop_fore_over_char(const pop_char_t *ch, int obj_x, int obj_y,
|
||||
uint8_t frame = ch->frame, action = ch->action;
|
||||
pop_t_fclip_on = 1; /* окно = прямоугольник спрайта */
|
||||
char_footprint(obj_x, obj_y, w, h, ch->direction, ch->sword);
|
||||
pop_dbg_b1(); /* ЗАМЕР: char_footprint */
|
||||
pop_dbg_rdmax[1] = pop_dbg_rdmax[2] = pop_dbg_rdmax[3] = 0; /* ВРЕМЕННО */
|
||||
/* Футпринт считается по КАДРУ персонажа (char_x_left/right, seg006:1021)
|
||||
* и потому УЖЕ спрайта, а рядом с ним рисуются ещё клинок и «брызги»
|
||||
* удара — они уходят ВПЕРЁД-ВВЕРХ и запросто попадают в соседнюю
|
||||
@@ -1043,6 +1048,7 @@ void pop_fore_over_char(const pop_char_t *ch, int obj_x, int obj_y,
|
||||
}
|
||||
}
|
||||
}
|
||||
pop_dbg_b2(); /* ЗАМЕР: границы окна */
|
||||
cL = fp_cL; cR = fp_cR; cLraw = fp_cLraw;
|
||||
rT = fp_rT; rB = fp_rB; rTraw = fp_rTraw;
|
||||
/* set_objtile_at_char (seg006:1833): тайл, при обработке которого спрайт
|
||||
@@ -1109,6 +1115,7 @@ void pop_fore_over_char(const pop_char_t *ch, int obj_x, int obj_y,
|
||||
}
|
||||
}
|
||||
}
|
||||
pop_dbg_b3(); /* ЗАМЕР: оверлеи сделаны */
|
||||
/* draw_tile_fore (seg008:690) — foretable, рисуется ПОСЛЕ midtable, т.е.
|
||||
* ПОСЛЕ оверлеев выше: передняя грань колонны перекрывает скос пола
|
||||
* (floor_left_overlay), а не наоборот. Раньше fore шёл первым, и тёмный
|
||||
@@ -1125,6 +1132,7 @@ void pop_fore_over_char(const pop_char_t *ch, int obj_x, int obj_y,
|
||||
* Один вызов через трамплин на персонажа за кадр — не в тайловом цикле:
|
||||
* тайл-кандидат всегда ровно один, а fore-проход и так самый горячий
|
||||
* кусок кадра (memory pop_fore_layer_cost). */
|
||||
pop_dbg_b4(); /* ЗАМЕР: цикл fore_tile */
|
||||
if (pop_gate_over_char(ch)) {
|
||||
int8_t gr = ch->curr_row, gc = (int8_t)(ch->curr_col + 1);
|
||||
if (tile_in_fclip(gr, gc)) {
|
||||
|
||||
@@ -124,6 +124,17 @@ void pop_spike_redraw(int row, int col) __banked;
|
||||
* поэтому соседа перерисовывать не надо). */
|
||||
void pop_chomp_redraw(int row, int col) __banked;
|
||||
|
||||
/* Вернуть ТОЛЬКО слой anim чомпера — его собственную графику поверх свежего
|
||||
* пламени соседнего факела. Порт ветки redraw_frames_anim в redraw_needed
|
||||
* (seg008:0211): оригинал на такую пометку делает ровно три вызова —
|
||||
* draw_tile_anim_topright / draw_tile_anim_right / draw_tile_anim — и НЕ
|
||||
* делает ни wipe, ни полного draw_tile (wipe у него отдельный счётчик).
|
||||
*
|
||||
* Из этих трёх нам нужен один: topright — про верх ВОРОТ под пустой
|
||||
* клеткой, к чомперу не относится, а anim_right (пламя левого соседа) у нас
|
||||
* уже нарисован — pop_torch_draw запекает его в фон ДО redraw_needed. */
|
||||
void pop_chomp_anim_draw(int row, int col) __banked;
|
||||
|
||||
/* Анимация факела/зелья (динамика каждый кадр; фон запечён без них):
|
||||
* modif — живой room_modif тайла. Для факела col — колонка САМОГО факела
|
||||
* (пламя рисуется в ячейке правого соседа). */
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "_pop_kdraw.h"
|
||||
#include "pop_vflip.h" /* зеркальные страницы атласов (зелье инверсии) */ /* меч + блит по id — сосед по банку 4 */ /* атласы/кадр Кида, pop_sword_draw, окна Char */
|
||||
#include "pop_cdraw.h"
|
||||
#include "pop_state.h" /* ВРЕМЕННО: зонды pop_dbg_b* */
|
||||
#include "pop_bg.h" /* POP_YOFF, pop_fore_over_char, pop_fore_set_clip */
|
||||
#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */
|
||||
#include "pop_map.h" /* hitp_curr/hitp_max — HP Кида; clip_char */
|
||||
@@ -257,7 +258,7 @@ static int scr_x(int x)
|
||||
* страницы. Оба мелкие и в одном кадре встречаются редко — их объединение
|
||||
* дешевле третьего heal-слота. */
|
||||
static void cd_overlay_add(pop_cdraw_t *s, uint8_t dp, int x, int y,
|
||||
uint16_t w, uint16_t h)
|
||||
uint8_t w, uint8_t h)
|
||||
{
|
||||
if (!s->ovalid[dp]) {
|
||||
s->ox[dp] = x; s->oy[dp] = y; s->ow[dp] = w; s->oh[dp] = h;
|
||||
@@ -272,14 +273,14 @@ static void cd_overlay_add(pop_cdraw_t *s, uint8_t dp, int x, int y,
|
||||
if (x + (int)w > x1) x1 = x + (int)w;
|
||||
if (y + (int)h > y1) y1 = y + (int)h;
|
||||
s->ox[dp] = x0; s->oy[dp] = y0;
|
||||
s->ow[dp] = (uint16_t)(x1 - x0); s->oh[dp] = (uint16_t)(y1 - y0);
|
||||
s->ow[dp] = (uint8_t)(x1 - x0); s->oh[dp] = (uint8_t)(y1 - y0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Добавить прямоугольник в окно fore-клипа слота (объединение «спрайт +
|
||||
* накладные»): по нему fore-проход возвращает куски тайлов и по нему же
|
||||
* расширяет перебор колонок/рядов. */
|
||||
static void cd_clip_add(pop_cdraw_t *s, int x, int y, uint16_t w, uint16_t h)
|
||||
static void cd_clip_add(pop_cdraw_t *s, int x, int y, uint8_t w, uint8_t h)
|
||||
{
|
||||
if (!s->cw) {
|
||||
s->cx = x; s->cy = y; s->cw = w; s->ch = h;
|
||||
@@ -291,8 +292,8 @@ static void cd_clip_add(pop_cdraw_t *s, int x, int y, uint16_t w, uint16_t h)
|
||||
if (y < s->cy) s->cy = y;
|
||||
if (x + (int)w > x1) x1 = x + (int)w;
|
||||
if (y + (int)h > y1) y1 = y + (int)h;
|
||||
s->cw = (uint16_t)(x1 - s->cx);
|
||||
s->ch = (uint16_t)(y1 - s->cy);
|
||||
s->cw = (uint8_t)(x1 - s->cx);
|
||||
s->ch = (uint8_t)(y1 - s->cy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,6 +344,34 @@ static void cd_sig_make(uint8_t who, cd_sig_t *g)
|
||||
g->dx = pop_cd[who].render_dx;
|
||||
}
|
||||
|
||||
/* Совпадает ли снимок страницы с ТЕКУЩИМ состоянием персонажа.
|
||||
*
|
||||
* Отдельно от cd_sig_make намеренно: тот СТРОИТ структуру, и для проверки
|
||||
* это лишняя работа — тринадцать записей в стековый кадр (а значит через
|
||||
* `-n(ix)`), после которых идёт побайтовый цикл сравнения. А зовут проверку
|
||||
* четыре раза за кадр: pop_char_skip_mask дважды, и в ней по два слота.
|
||||
* Здесь сравниваем поля прямо с источником, и первое же расхождение
|
||||
* заканчивает работу — у ДВИГАЮЩЕГОСЯ персонажа это обычно первое поле. */
|
||||
static uint8_t cd_sig_same(uint8_t who, uint8_t p)
|
||||
{
|
||||
const pop_char_t *ch = (who == POP_CH_KID) ? &Kid : &Guard;
|
||||
const cd_sig_t *g = &cd_sig[who][p];
|
||||
if (g->frame != ch->frame) return 0;
|
||||
if (g->x != ch->x) return 0;
|
||||
if (g->y != ch->y) return 0;
|
||||
if (g->dir != ch->direction) return 0;
|
||||
if (g->action != ch->action) return 0;
|
||||
if (g->ccol != ch->curr_col) return 0;
|
||||
if (g->crow != ch->curr_row) return 0;
|
||||
if (g->sword != ch->sword) return 0;
|
||||
if (g->charid != ch->charid) return 0;
|
||||
if (g->room != ch->room) return 0;
|
||||
if (g->hurt != (uint8_t)((who == POP_CH_KID) ? pop_kid_hurt : pop_guard_hurt))
|
||||
return 0;
|
||||
if (g->dx != pop_cd[who].render_dx) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Габарит слота на странице: спрайт + накладные (клинок/брызги). y —
|
||||
* КОМНАТНЫЙ (как в pop_cd), перевод в экранный делает вызывающий. */
|
||||
static void cd_bbox(uint8_t who, uint8_t p, int *x0, int *y0, int *x1, int *y1)
|
||||
@@ -365,9 +394,6 @@ static void cd_bbox(uint8_t who, uint8_t p, int *x0, int *y0, int *x1, int *y1)
|
||||
* ни перепроверяй за кадр. */
|
||||
static uint8_t cd_quiet(uint8_t who, uint8_t p)
|
||||
{
|
||||
cd_sig_t g;
|
||||
const uint8_t *a, *b;
|
||||
uint8_t i;
|
||||
/* Соперника на сцене нет и на ЭТОЙ странице от него ничего не осталось —
|
||||
* стирать и рисовать нечего, слот «тихий». Без этого пустой слот каждый
|
||||
* кадр честно проходил heal + вход в pop_char_draw + fore-проход и стоил
|
||||
@@ -377,17 +403,20 @@ static uint8_t cd_quiet(uint8_t who, uint8_t p)
|
||||
return 1;
|
||||
if (!pop_cd[who].valid[p]) return 0;
|
||||
if (pop_cd_dirty & (1 << p)) { /* фон правили — но задели ли нас? */
|
||||
int x0, y0, x1, y1;
|
||||
cd_bbox(who, p, &x0, &y0, &x1, &y1);
|
||||
y0 += POP_YOFF; y1 += POP_YOFF; /* метка фона — в ЭКРАННЫХ */
|
||||
/* cd_bbox отдаёт x1/y1 ИСКЛЮЧИТЕЛЬНО, pop_cd_hit ждёт включительно. */
|
||||
if (pop_cd_hit(p, x0, y0, x1 - 1, y1 - 1)) return 0;
|
||||
/* СПРАЙТ И НАКЛАДНОЙ (клинок, брызги) — ДВУМЯ ОТДЕЛЬНЫМИ проверками,
|
||||
* а не объединённым bbox. Объединение включает пустой угол между
|
||||
* ними, и он ловит касания, которых на самом деле нет: у стоящего
|
||||
* стража спрайт лежит в колонке 8, клинок уходит влево в колонку 7 на
|
||||
* y 59..65, а пламя факела метит колонку 7 на y 33..50. Прямоугольник
|
||||
* «спрайт + клинок» (x 241..284, y 46..84) пересекается с этой меткой
|
||||
* углом — и персонаж перерисовывался целиком за 148 302 такта
|
||||
* (замер 2026-08-19, сцена 11/15).
|
||||
*
|
||||
* Для skip_mask объединение по-прежнему годится: там вопрос «рядом ли
|
||||
* два слота», и загрубление в бОльшую сторону безопасно. */
|
||||
if (pop_cd_hit_slot(who, p)) return 0;
|
||||
}
|
||||
cd_sig_make(who, &g);
|
||||
a = (const uint8_t *)&g; b = (const uint8_t *)&cd_sig[who][p];
|
||||
for (i = 0; i < sizeof(cd_sig_t); i++)
|
||||
if (*a++ != *b++) return 0;
|
||||
return 1;
|
||||
return cd_sig_same(who, p);
|
||||
}
|
||||
|
||||
/* Запас вокруг габарита соседа: за кадр персонаж проходит заметно меньше
|
||||
@@ -440,7 +469,7 @@ static void cd_splash(pop_cdraw_t *s, uint8_t who, const atlas_t *pages,
|
||||
const atlas_t *ap;
|
||||
uint8_t aidx;
|
||||
const uint8_t *img;
|
||||
uint16_t w, h;
|
||||
uint8_t w, h;
|
||||
int qx = fp_x, qy = obj_y;
|
||||
int fw = (Char.direction < 0) ? -5 : 5; /* obj_dx_forward(5) */
|
||||
|
||||
@@ -462,8 +491,12 @@ static void cd_splash(pop_cdraw_t *s, uint8_t who, const atlas_t *pages,
|
||||
qx = scr_x(qx + fw); /* calc_screen_x_coord */
|
||||
img = (const uint8_t *)atlas_image(ap, aidx);
|
||||
gfx_w0_map(ap->page);
|
||||
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
|
||||
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
|
||||
/* Габарит БАЙТАМИ: спрайты персонажей и накладных не крупнее 64x64
|
||||
* (memory pop_sprite_size_limits — весь игровой кадр PoP <= 56x63),
|
||||
* а 16-битные w/h заставляли SDCC держать их в стековом кадре и
|
||||
* считать каждое сравнение парой загрузок. */
|
||||
w = img[0];
|
||||
h = img[2];
|
||||
if (w && h) {
|
||||
int top = qy - (int)h + 1;
|
||||
uint8_t flip = (uint8_t)(Char.direction >= 0);
|
||||
@@ -474,15 +507,15 @@ static void cd_splash(pop_cdraw_t *s, uint8_t who, const atlas_t *pages,
|
||||
if (top + rows > 192) rows = 192 - top;
|
||||
if (rows <= 0) return;
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
if (pop_onscreen_cols(qx, top + POP_YOFF, w, (uint16_t)rows))
|
||||
if (pop_onscreen_cols(qx, top + POP_YOFF, w, (uint8_t)rows))
|
||||
gfx_blit_cols_part_noclip(qx, top + POP_YOFF, img, flip,
|
||||
(uint8_t)skip, (uint8_t)rows);
|
||||
else
|
||||
gfx_blit_cols_part(qx, top + POP_YOFF, img, flip, skip, rows);
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
dp = gfx_get_draw_page() & 1;
|
||||
cd_overlay_add(s, dp, qx, top, w, (uint16_t)rows);
|
||||
cd_clip_add(s, qx, top, w, (uint16_t)rows);
|
||||
cd_overlay_add(s, dp, qx, top, w, (uint8_t)rows);
|
||||
cd_clip_add(s, qx, top, w, (uint8_t)rows);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,7 +527,7 @@ void pop_char_draw(uint8_t who) __banked
|
||||
const atlas_t *pages;
|
||||
const uint8_t *img;
|
||||
uint8_t npages, page, idx, dp, flip, hurt, lskip;
|
||||
uint16_t w, h, vis_w;
|
||||
uint8_t w, h, vis_w;
|
||||
int obj_x, obj_y, fp_x, fwd, top, bx, skip, rows, ct, cr;
|
||||
int bcut; /* срез СНИЗУ (clip_char в перевёрнутом виде) */
|
||||
|
||||
@@ -596,8 +629,12 @@ void pop_char_draw(uint8_t who) __banked
|
||||
}
|
||||
gfx_w0_map(vpg);
|
||||
}
|
||||
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
|
||||
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
|
||||
/* Габарит БАЙТАМИ: спрайты персонажей и накладных не крупнее 64x64
|
||||
* (memory pop_sprite_size_limits — весь игровой кадр PoP <= 56x63),
|
||||
* а 16-битные w/h заставляли SDCC держать их в стековом кадре и
|
||||
* считать каждое сравнение парой загрузок. */
|
||||
w = img[0];
|
||||
h = img[2];
|
||||
dp = gfx_get_draw_page() & 1;
|
||||
if (w && h) {
|
||||
/* Спрайты нарисованы ЛИЦОМ ВЛЕВО (как в оригинале); seg008:864 —
|
||||
@@ -654,7 +691,7 @@ void pop_char_draw(uint8_t who) __banked
|
||||
if (cr) {
|
||||
int avail = cr - bx;
|
||||
if (avail <= 0) vis_w = 0; /* весь спрайт за косяком */
|
||||
else if (avail < (int)w) vis_w = (uint16_t)avail;
|
||||
else if (avail < (int)w) vis_w = (uint8_t)avail;
|
||||
}
|
||||
/* КЛИП ТЕНИ СЛЕВА (seg008:1699): на уровне зеркала она может
|
||||
* показываться ТОЛЬКО СПРАВА от него —
|
||||
@@ -683,7 +720,7 @@ void pop_char_draw(uint8_t who) __banked
|
||||
if (lskip) /* тень у зеркала: срез слева */
|
||||
gfx_blit_cols_part_wx(bx, top + POP_YOFF, img, flip, skip, rows,
|
||||
lskip, (uint8_t)(vis_w - lskip));
|
||||
else if (vis_w == w && pop_onscreen_cols(bx, top + POP_YOFF, w, (uint16_t)rows))
|
||||
else if (vis_w == w && pop_onscreen_cols(bx, top + POP_YOFF, w, (uint8_t)rows))
|
||||
gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip,
|
||||
(uint8_t)skip, (uint8_t)rows);
|
||||
else
|
||||
@@ -694,12 +731,13 @@ void pop_char_draw(uint8_t who) __banked
|
||||
/* heal чистит ТОЛЬКО нарисованное: иначе стирается кромка пола над
|
||||
* срезом (мусор/дыра в кладке на второй странице дабл-буфера). */
|
||||
s->x[dp] = bx + lskip; s->y[dp] = top;
|
||||
s->w[dp] = (uint16_t)(vis_w - lskip); s->h[dp] = (uint16_t)rows;
|
||||
s->w[dp] = (uint8_t)(vis_w - lskip); s->h[dp] = (uint8_t)rows;
|
||||
s->valid[dp] = (uint8_t)(rows != 0);
|
||||
if (rows) cd_clip_add(s, bx, top, vis_w, (uint16_t)rows);
|
||||
if (rows) cd_clip_add(s, bx, top, vis_w, (uint8_t)rows);
|
||||
s->fpw = w; s->fph = h; /* габарит КАДРА (не обрезанный): */
|
||||
/* по нему считается футпринт */
|
||||
}
|
||||
pop_dbg_m15(); /* ЗАМЕР: снимок прямоугольника сделан */
|
||||
if (hurt) cd_splash(s, who, pages, fp_x, obj_y);
|
||||
/* add_sword_to_objtable (seg006:1798): клинок отдельным спрайтом поверх
|
||||
* персонажа — со своим прямоугольником heal. */
|
||||
@@ -710,6 +748,7 @@ void pop_char_draw(uint8_t who) __banked
|
||||
cd_clip_add(s, sx, sy, sw, sh);
|
||||
}
|
||||
}
|
||||
pop_dbg_m16(); /* ЗАМЕР: splash + клинок сделаны */
|
||||
gfx_w0_unmap();
|
||||
/* Что именно нарисовано на ЭТОЙ странице — снимок для пропуска
|
||||
* следующих кадров (DRAW-COST). Только если кадр РЕАЛЬНО рисовали:
|
||||
@@ -738,6 +777,7 @@ void pop_char_fore(uint8_t who) __banked
|
||||
if (s->fpw)
|
||||
pop_fore_over_char(who == POP_CH_KID ? &Kid : &Guard,
|
||||
s->fpx, s->fpy, s->fpw, s->fph);
|
||||
pop_dbg_b6(); /* ЗАМЕР: pop_char_fore целиком */
|
||||
}
|
||||
|
||||
/* ---- ОТРАЖЕНИЕ В ЗЕРКАЛЕ (check_mirror, seg003:0798) ---------------- *
|
||||
@@ -774,7 +814,7 @@ void pop_mirror_draw(int clip_top) __banked
|
||||
const kframe *fr = &kid_frame;
|
||||
const uint8_t *img;
|
||||
uint8_t page, idx, flip;
|
||||
uint16_t w, h, vis_w;
|
||||
uint8_t w, h, vis_w;
|
||||
int fwd, obj_x, obj_y, fp_x, bx, top, skip = 0, rows, cl;
|
||||
uint8_t lskip = 0;
|
||||
|
||||
@@ -792,8 +832,12 @@ void pop_mirror_draw(int clip_top) __banked
|
||||
|
||||
img = (const uint8_t *)atlas_image(&kidp[page], idx);
|
||||
gfx_w0_map(kidp[page].page);
|
||||
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
|
||||
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
|
||||
/* Габарит БАЙТАМИ: спрайты персонажей и накладных не крупнее 64x64
|
||||
* (memory pop_sprite_size_limits — весь игровой кадр PoP <= 56x63),
|
||||
* а 16-битные w/h заставляли SDCC держать их в стековом кадре и
|
||||
* считать каждое сравнение парой загрузок. */
|
||||
w = img[0];
|
||||
h = img[2];
|
||||
if (!w || !h) { gfx_w0_unmap(); return; }
|
||||
flip = (uint8_t)(Char.direction >= 0);
|
||||
bx = flip ? obj_x - (int)w : obj_x;
|
||||
@@ -914,7 +958,7 @@ void pop_hp_draw(void) __banked
|
||||
if (g_ok && Guard.charid != 0 && Guard.charid != CHARID_4_SKELETON &&
|
||||
Guard.charid != CHARID_24_MOUSE && gd) {
|
||||
const uint8_t *img = (const uint8_t *)atlas_image(&gp[0], 0);
|
||||
uint16_t w, h;
|
||||
uint8_t w, h;
|
||||
gfx_w0_map(gp[0].page);
|
||||
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
|
||||
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
|
||||
|
||||
@@ -51,21 +51,21 @@ typedef struct {
|
||||
* видео-ОЗУ и своя ОЗУ-копия, поэтому heal обязан стирать спрайт именно
|
||||
* той страницы, в которую сейчас рисуем */
|
||||
int x[2], y[2];
|
||||
uint16_t w[2], h[2];
|
||||
uint8_t w[2], h[2]; /* габарит: спрайты игры не крупнее 64x64 */
|
||||
uint8_t valid[2];
|
||||
/* НАКЛАДНЫЕ спрайты (клинок + брызги урона) — свой прямоугольник, не
|
||||
* объединение с персонажем: объединение сильно больше суммы двух
|
||||
* (клинок уходит вперёд-вверх), а heal стоит ровно по площади */
|
||||
int ox[2], oy[2];
|
||||
uint16_t ow[2], oh[2];
|
||||
uint8_t ow[2], oh[2];
|
||||
uint8_t ovalid[2];
|
||||
/* габарит кадра для fore-прохода: fpx — ЛОГИЧЕСКАЯ X (до ×8/7),
|
||||
* fpy — низ спрайта; fpw == 0 — в этом кадре рисовать было нечего */
|
||||
int fpx, fpy;
|
||||
uint16_t fpw, fph;
|
||||
uint8_t fpw, fph;
|
||||
/* окно fore-клипа слота (объединение «спрайт + накладные»), КОМНАТНЫЙ y */
|
||||
int cx, cy;
|
||||
uint16_t cw, ch;
|
||||
uint8_t cw, ch;
|
||||
/* straddle: рендерное смещение по ЛОГИЧЕСКОЙ X, когда комната персонажа
|
||||
* не совпадает с отрисованной (порт xpos_in_drawn_room) */
|
||||
int render_dx;
|
||||
@@ -121,11 +121,19 @@ void pop_char_set_render_dx(uint8_t who, int dx) __banked;
|
||||
* Резидент (pop_tile.c): зовёт и банковый слой фона, и резидентные листья,
|
||||
* а трамплин на КАЖДЫЙ кусок фона стоил бы дороже самой метки. */
|
||||
extern uint8_t pop_cd_dirty; /* биты страниц: метка взведена */
|
||||
extern uint16_t pop_cd_dmask[2][3]; /* [страница][ряд] = биты колонок 0..9 */
|
||||
/* [страница][колонка] = диапазон затронутых ЭКРАННЫХ y; пусто = ymin 255,
|
||||
* ymax 0. Раньше тут была маска колонок на три ряда по 63 px — она
|
||||
* склеивала касания внутри ряда и заставляла перерисовывать нетронутого
|
||||
* персонажа (разбор в шапке pop_cd_touch, pop_tile.c). */
|
||||
extern uint8_t pop_cd_ymin[2][10], pop_cd_ymax[2][10];
|
||||
/* Задели ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 включительно) то,
|
||||
* что трогали на странице p. Резидент (pop_tile.c). */
|
||||
uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1);
|
||||
/* То же для СЛОТА персонажа: спрайт и накладной, координаты изнутри pop_cd
|
||||
* (см. тело — там про цену пяти аргументов). */
|
||||
uint8_t pop_cd_hit_slot(uint8_t who, uint8_t p);
|
||||
void pop_cd_clear(uint8_t p); /* снять метку страницы целиком */
|
||||
void pop_cd_init(void); /* пустые диапазоны на обеих страницах */
|
||||
void pop_cd_touch(int x, int y, int w, int h);
|
||||
#define POP_CD_TOUCH_ALL() pop_cd_touch(0, 0, 320, 256)
|
||||
|
||||
|
||||
@@ -330,6 +330,38 @@ uint8_t pop_tile_at(int8_t col, int8_t row) __banked
|
||||
return get_tile(col, row);
|
||||
}
|
||||
|
||||
/* Тайлы ОТРЕЗКА ряда одним вызовом — для луча видимости стража.
|
||||
*
|
||||
* Зачем: pop_tile_at объявлен __banked, а луч живёт в guards.c (банк 1) и
|
||||
* звал его ПО ОДНОЙ КОЛОНКЕ. На сцене 11/15 (Кид в колонке 2, страж в 8)
|
||||
* это девять трамплинов банк 1 -> банк 3 за кадр, и весь луч стоил 36 786
|
||||
* тактов — 5,6 % работы кадра при том, что делает он девять чтений байта
|
||||
* (замер 2026-08-19). Цена трамплина здесь та же, что уже измерена у
|
||||
* pop_clip_char_top (8 892 такта ради одной проверки тайла).
|
||||
*
|
||||
* Колонки за пределами 0..9 разрешает сам get_tile (шов с соседней
|
||||
* комнатой), поэтому диапазон отдаём как есть, без клипа.
|
||||
*
|
||||
* out обязан вмещать c1 - c0 + 1 байт; вызывающий даёт буфер на 12
|
||||
* (колонки −1..10 плюс запас). */
|
||||
void pop_row_tiles(int8_t row, int8_t c0, int8_t c1, uint8_t *out) __banked
|
||||
{
|
||||
int8_t col;
|
||||
/* БЫСТРЫЙ ПУТЬ — отрезок целиком внутри комнаты. get_tile для ряда
|
||||
* 0..2 и колонки 0..9 сводится ровно к `g_fg[row*10+col] & 0x1F`
|
||||
* (см. его тело выше), а всё остальное там — разбор швов и краёв
|
||||
* уровня. Идём указателем: иначе на каждую колонку заново считается
|
||||
* row*10 + col. */
|
||||
if (row >= 0 && row <= 2 && c0 >= 0 && c1 <= 9 && g_fg) {
|
||||
const uint8_t *p = g_fg + (int)row * 10 + c0;
|
||||
for (col = c0; col <= c1; col++)
|
||||
*out++ = (uint8_t)(*p++ & 0x1F);
|
||||
return;
|
||||
}
|
||||
for (col = c0; col <= c1; col++)
|
||||
*out++ = get_tile(col, row);
|
||||
}
|
||||
|
||||
/* can_bump_into_gate (seg004:373): ОТКРЫТЫЕ ворота проходимы — Kid не бампит и
|
||||
* идёт сквозь них (на шве — уходит в соседнюю комнату; внутри комнаты — просто
|
||||
* проходит по полу под поднятыми барами). ВАЖНО: ворота остаются тайлом 4 (в
|
||||
@@ -1769,18 +1801,67 @@ static void calc_coll_window(void)
|
||||
* Замер одной итерации в MAME: пустая колонка 750 тактов, колонка-стена
|
||||
* ~1 700 (там две 16-битные знаковые сверки граней). */
|
||||
static uint8_t scan_n; /* сколько колонок осталось */
|
||||
static int scan_left; /* левая грань текущей колонки */
|
||||
static uint8_t scan_left; /* левая грань текущей колонки */
|
||||
|
||||
static void coll_scan(const uint8_t *src, uint8_t *dst)
|
||||
/* ПОРОГИ СРАВНЕНИЯ, предпосчитанные на кадр (по типу стены 1..5).
|
||||
*
|
||||
* В теле цикла стояло `wall_dl[wt] + scan_left < coll_xr` и
|
||||
* `scan_left - wall_dr[wt] + TILE_RIGHTX > coll_xl` — четыре 16-битных
|
||||
* операции на КАЖДУЮ колонку, при том что от колонки зависит ровно один
|
||||
* операнд (scan_left). Переносим всё остальное в порог:
|
||||
*
|
||||
* scan_left < coll_xr - wall_dl[wt] = thr_l[wt]
|
||||
* scan_left > coll_xl + wall_dr[wt] - TILE_RIGHTX = thr_r[wt]
|
||||
*
|
||||
* Оригинал считает это в лоб (get_left_wall_xpos / get_right_wall_xpos,
|
||||
* seg004:0226) — он писался под 386, где 16-битная арифметика бесплатна.
|
||||
*
|
||||
* ПОЧЕМУ 8 БИТ КОРРЕКТНЫ (доказательство относится и к границам, и к
|
||||
* слагаемым в самом цикле). scan_left = x_bump[col + 5] + TILE_MIDX, а
|
||||
* колонка окна лежит в COLL_C0 .. COLL_C0+COLL_N-1, то есть −2..11:
|
||||
* значит scan_left ∈ [x_bump[3]+7, x_bump[16]+7] = [37, 219], и после
|
||||
* последней колонки максимум 233. Ни одного выхода за uint8_t.
|
||||
* Сами пороги считаются в int и КЛИПУЮТСЯ к 0..255 — это не приближение,
|
||||
* а точное сохранение результата: при пороге ниже 37 условие «меньше» не
|
||||
* выполнится никогда (клип к 0 даёт то же), при пороге выше 219 оно
|
||||
* выполнится всегда (клип к 255 даёт то же), и симметрично для «больше». */
|
||||
static uint8_t coll_xr8, coll_xl8;
|
||||
|
||||
static uint8_t clip_thr(int v)
|
||||
{
|
||||
if (v < 0) return 0;
|
||||
if (v > 255) return 255;
|
||||
return (uint8_t)v;
|
||||
}
|
||||
|
||||
/* Две границы персонажа, приведённые к 8 битам. ТАБЛИЦЫ ПОРОГОВ ПО ТИПУ
|
||||
* СТЕНЫ ЗДЕСЬ БЫЛА И ОКАЗАЛАСЬ ХУЖЕ: предпосчёт пяти пар порогов стоил
|
||||
* дороже, чем экономил, потому что колонок в окне всего четыре-пять, а
|
||||
* типов стен пять (замер 2026-08-19: check_collisions 33 846 -> 36 570,
|
||||
* синяя фаза +10 269). Классическая ошибка — кэшировать больше, чем
|
||||
* потребляешь. */
|
||||
static void coll_thresholds(void)
|
||||
{
|
||||
coll_xr8 = clip_thr(coll_xr);
|
||||
coll_xl8 = clip_thr(coll_xl);
|
||||
}
|
||||
|
||||
static uint8_t *scan_dst;
|
||||
|
||||
static void coll_scan(const uint8_t *src)
|
||||
{
|
||||
do {
|
||||
uint8_t wt = wall_type_tbl[*src++ & 0x1F];
|
||||
uint8_t f = 0;
|
||||
if (wt) {
|
||||
if (wall_dl[wt] + scan_left < coll_xr) f = 1;
|
||||
if (scan_left - wall_dr[wt] + TILE_RIGHTX > coll_xl) f |= 2;
|
||||
/* ВСЯ арифметика в 8 битах — см. доказательство диапазонов
|
||||
* над coll_thresholds: scan_left ∈ [37, 233], wall_dl ∈ [−1, 10],
|
||||
* wall_dr ∈ [0, 13], значит обе суммы лежат в [36, 246] и
|
||||
* переполниться не могут. */
|
||||
if ((uint8_t)(scan_left + wall_dl[wt]) < coll_xr8) f = 1;
|
||||
if ((uint8_t)(scan_left - wall_dr[wt] + TILE_RIGHTX) > coll_xl8) f |= 2;
|
||||
}
|
||||
*dst++ = f;
|
||||
*scan_dst++ = f;
|
||||
scan_left += TILE_SIZEX;
|
||||
} while (--scan_n);
|
||||
}
|
||||
@@ -1789,14 +1870,18 @@ static void coll_scan(const uint8_t *src, uint8_t *dst)
|
||||
* в теле ряда, и пролог одного ряда стоил тысячи тактов при том, что сам
|
||||
* цикл по четырём-пяти колонкам — около трёх. */
|
||||
static uint8_t scan_n0;
|
||||
static int scan_left0;
|
||||
static uint8_t scan_left0;
|
||||
static uint8_t scan_off; /* индекс первого слота в flags[] */
|
||||
|
||||
static void coll_scan_prepare(void)
|
||||
{
|
||||
scan_left0 = pop_x_bump[win_lo + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
|
||||
scan_left0 = (uint8_t)(pop_x_bump[win_lo + FIRST_ONSCREEN_COLUMN] + TILE_MIDX);
|
||||
scan_n0 = (uint8_t)(win_hi - win_lo + 1);
|
||||
scan_off = (uint8_t)COLL_IDX(win_lo);
|
||||
coll_thresholds(); /* здесь же, чтобы порог и окно всегда были от
|
||||
* ОДНОГО персонажа — та же причина, по которой
|
||||
* тут считается scan_left0 (см. BUG-GUARD-IX-1
|
||||
* в шапке get_row_collision_data). */
|
||||
}
|
||||
|
||||
/* Базы ряда: указатели, которые индексируются НОМЕРОМ КОЛОНКИ (в том числе
|
||||
@@ -1838,7 +1923,7 @@ static void coll_row(uint8_t *flags)
|
||||
last = win_hi < -1 ? win_hi : -1;
|
||||
n = (uint8_t)(last - lo + 1);
|
||||
scan_n = n; /* coll_scan обнулит */
|
||||
coll_scan(rb_lft ? rb_lft + lo : wall_row, dst);
|
||||
scan_dst = dst; coll_scan(rb_lft ? rb_lft + lo : wall_row);
|
||||
dst += n;
|
||||
lo = (int8_t)(last + 1);
|
||||
}
|
||||
@@ -1846,13 +1931,13 @@ static void coll_row(uint8_t *flags)
|
||||
last = win_hi < 9 ? win_hi : 9;
|
||||
n = (uint8_t)(last - lo + 1);
|
||||
scan_n = n;
|
||||
coll_scan(rb_own ? rb_own + lo : wall_row, dst);
|
||||
scan_dst = dst; coll_scan(rb_own ? rb_own + lo : wall_row);
|
||||
dst += n;
|
||||
lo = (int8_t)(last + 1);
|
||||
}
|
||||
if (lo <= win_hi) { /* комната СПРАВА */
|
||||
scan_n = (uint8_t)(win_hi - lo + 1);
|
||||
coll_scan(rb_rgt ? rb_rgt + lo : wall_row, dst);
|
||||
scan_dst = dst; coll_scan(rb_rgt ? rb_rgt + lo : wall_row);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1908,12 +1993,15 @@ static void check_collisions(void)
|
||||
return;
|
||||
}
|
||||
|
||||
pop_dbg_p2(); /* ЗАМЕР: вход в тело */
|
||||
set_char_collision();
|
||||
pop_dbg_p3(); /* ЗАМЕР: set_char_collision */
|
||||
move_coll_to_prev(Char.curr_row); /* заодно снимет окно prev */
|
||||
coll_prev_row = Char.curr_row;
|
||||
calc_coll_window();
|
||||
coll_lo = win_lo; coll_hi = win_hi;
|
||||
coll_scan_prepare();
|
||||
pop_dbg_p4(); /* ЗАМЕР: окно посчитано */
|
||||
/* Порядок важен: prev уже забран, теперь три ряда пересчитываются
|
||||
* НА ЭТОТ кадр (в оригинале ровно так же, seg004:0004). Ряд здесь
|
||||
* гарантированно 0..2, поэтому соседние ряды — это те же базы ±10, без
|
||||
@@ -1947,6 +2035,7 @@ static void check_collisions(void)
|
||||
}
|
||||
coll_row(coll_above);
|
||||
}
|
||||
pop_dbg_p5(); /* ЗАМЕР: три ряда просканированы */
|
||||
/* Обход СВЕРХУ ВНИЗ, как в оригинале (9→0): побеждает МЛАДШАЯ колонка,
|
||||
* в которой флаг перешёл 0→1. Только по ПЕРЕСЕЧЕНИЮ окон: вне его
|
||||
* сравнивать нечего (у оригинала там prev_coll_room != curr_row_coll_room
|
||||
@@ -2133,6 +2222,26 @@ 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 и раздача
|
||||
* отложенного старта в check_fall_flo — уровень 13).
|
||||
*
|
||||
* ИМЕННО ЗДЕСЬ УЖЕ ОШИБЛИСЬ ОДИН РАЗ: check_fall_flo забыли, и каскад
|
||||
* уровня 13 перестал падать — плиты дрожали и застывали. Добавляя новое
|
||||
* место записи фазы, добавляй и взвод. Снимает его
|
||||
* САМ цикл, когда прошёл оба массива и не встретил ни одной ненулевой фазы.
|
||||
*
|
||||
* Асимметрия намеренная: ложная единица стоит одного холостого прохода,
|
||||
* ложный ноль — застывшей навсегда плиты. Поэтому взвод обязан стоять
|
||||
* рядом с КАЖДОЙ записью, а снятие — только по факту пустого прохода. */
|
||||
static uint8_t loose_any;
|
||||
|
||||
/* Плита-ПОТОЛОК: состояние/сигналы — см. объявления перед get_tile. */
|
||||
|
||||
/* make_loose_fall (seg007:0EF6): взвести отсчёт падения, если ещё не идёт. */
|
||||
@@ -2141,8 +2250,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 +2324,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 +2378,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 +2509,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, — и она проваливалась бы
|
||||
@@ -2483,6 +2601,12 @@ void pop_check_fall_flo(void) __banked
|
||||
if ((int8_t)pop_ceil_modif[col] > 0) continue;
|
||||
pop_ceil_modif[col] =
|
||||
(uint8_t)(-(int8_t)(pop_prandom(&loose_seed, 255) & 0x0F) - 1);
|
||||
loose_any = 1; /* ШЕСТОЕ место взвода гейта — см. loose_any.
|
||||
* Пропуск именно здесь стоил каскада на
|
||||
* уровне 13: плиты получали отложенный
|
||||
* старт, но цикл их не досчитывал, и они
|
||||
* дрожали, не падая (нашёл пользователь
|
||||
* 2026-08-19). */
|
||||
pop_set_redraw_above(col, POP_RDA_CEIL, 1);
|
||||
}
|
||||
}
|
||||
@@ -2525,6 +2649,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,6 +2685,13 @@ void pop_loose_tick(void) __banked
|
||||
* Инкремент+сравнение считают row/col без деления. */
|
||||
uint8_t row = 0, col = 0;
|
||||
pop_dbg_m9(); /* ЗАМЕР: вход pop_loose_tick */
|
||||
/* Гейт холостого хода: если ни одна фаза не взведена, оба цикла (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;
|
||||
@@ -2614,6 +2749,7 @@ void pop_loose_tick(void) __banked
|
||||
} else { /* отсчёт 1..10 — дрожащий кадр */
|
||||
pop_set_redraw(pos, POP_RD_LOOSE, 1);
|
||||
}
|
||||
if (*lm) found = 1; /* фаза ещё жива — гейт держим */
|
||||
}
|
||||
if (++col == 10) { col = 0; row++; } /* row/col без деления */
|
||||
}
|
||||
@@ -2646,13 +2782,13 @@ void pop_loose_tick(void) __banked
|
||||
} else {
|
||||
pop_set_redraw_above(col, POP_RDA_CEIL, 1); /* отсчёт 1..10 */
|
||||
}
|
||||
if (*lm) found = 1; /* фаза ещё жива — гейт держим */
|
||||
}
|
||||
}
|
||||
pop_dbg_m10(); /* ЗАМЕР: оба цикла по тайлам пройдены */
|
||||
loose_any = found;
|
||||
}
|
||||
pop_loose_mob_tick(); /* продвинуть+нарисовать падающий кусок (после тайлов) */
|
||||
pop_dbg_m11(); /* ЗАМЕР: pop_loose_mob_tick сделан */
|
||||
check_loose_fall_on_kid(); /* seg007:1192 — плита падает Киду на голову */
|
||||
pop_dbg_m12(); /* ЗАМЕР: check_loose_fall_on_kid сделан */
|
||||
/* Отложенная допечка ПРОШЛОГО приземления на вторую страницу — ПЕРЕД
|
||||
* разбором нового: иначе при двух плитах, севших подряд, land_bake
|
||||
* затирался новым и на одной странице оставался старый пол. */
|
||||
@@ -3027,9 +3163,12 @@ static void kid_phys(void)
|
||||
determine_col();
|
||||
bump_into_opponent(); /* seg003: безоружный Кид отскакивает от стража */
|
||||
check_collisions(); /* seg004: флаги перекрытия по колонкам ряда */
|
||||
pop_dbg_p6(); /* ЗАМЕР: check_collisions целиком */
|
||||
check_bumped(); /* удержать у стены до check_action (порядок PoP) */
|
||||
check_action();
|
||||
pop_dbg_p7(); /* ЗАМЕР */
|
||||
check_press(); /* seg006: стойка/пробой loose → make_loose_fall */
|
||||
pop_dbg_p8(); /* ЗАМЕР */
|
||||
check_spike_below(); /* seg006: над колонкой с пиками → выдвинуть пики */
|
||||
check_spiked(); /* seg006: напоролся на вредные пики → смерть */
|
||||
check_chomped_kid(); /* seg004: перемололо в сомкнутых челюстях → смерть */
|
||||
@@ -3089,6 +3228,7 @@ void pop_phys_tick(void) __banked
|
||||
* оригинале: кадр смерти не двигается сам (dx/dy нулевые, sequence
|
||||
* кончился), а уход из комнаты и сотрясение отсечены внутри kid_phys. */
|
||||
pop_loadkid_and_opp();
|
||||
pop_dbg_p1(); /* ЗАМЕР: окно Char загружено */
|
||||
kid_phys();
|
||||
pop_savekid_and_opp();
|
||||
}
|
||||
|
||||
@@ -58,6 +58,10 @@ void pop_map_set_room(uint8_t room) __banked;
|
||||
* (guards.c): он идёт по ряду между Кидом и стражем. */
|
||||
uint8_t pop_tile_at(int8_t col, int8_t row) __banked;
|
||||
|
||||
/* Тайлы отрезка ряда c0..c1 одним банковым вызовом (см. тело в pop_map.c):
|
||||
* луч видимости стража иначе платит по трамплину за КАЖДУЮ колонку. */
|
||||
void pop_row_tiles(int8_t row, int8_t c0, int8_t c1, uint8_t *out) __banked;
|
||||
|
||||
/* determine_col (seg006:014D): Kid.curr_col = m7(dx_weight()). Наружу — для
|
||||
* pop_load_fram_det_col (pop_kid.c), порт load_fram_det_col. */
|
||||
void pop_determine_col(void) __banked;
|
||||
|
||||
@@ -88,6 +88,17 @@ void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages) __banked
|
||||
if (tilepos >= NTILES) return;
|
||||
if (!rd_cnt[tilepos]) rd_pending++;
|
||||
else pop_bake_slot_reset(tilepos); /* перепометка — копия запечки не годится */
|
||||
/* ПРИОРИТЕТ ПОЛНОЙ ПЕРЕРИСОВКИ НАД слоем anim. У оригинала это не
|
||||
* конфликт видов, а два независимых счётчика, и полная побеждает
|
||||
* (redraw_needed, seg008:0211: `if (redraw_frames_full) draw_tile();
|
||||
* else if (redraw_frames_anim) {...}`). У нас вид ОДИН на тайл, и без
|
||||
* этой проверки исход решал бы порядок trob'ов в списке: чомпер, который
|
||||
* СЕЙЧАС смыкает челюсти, ставит POP_RD_CHOMP (ему нужен heal — поза
|
||||
* меняется), а факел слева тут же метил бы тот же тайл как
|
||||
* POP_RD_CHOMP_ANIM, и heal пропал бы — новая поза легла бы поверх
|
||||
* старой. */
|
||||
if (!(kind == POP_RD_CHOMP_ANIM && rd_kind[tilepos] == POP_RD_CHOMP &&
|
||||
rd_cnt[tilepos]))
|
||||
rd_kind[tilepos] = kind;
|
||||
if (pages > rd_cnt[tilepos]) rd_cnt[tilepos] = pages;
|
||||
}
|
||||
@@ -112,6 +123,7 @@ void pop_redraw_needed(void) __banked
|
||||
if (!rd_pending) return;
|
||||
for (i = 0; i < NTILES; i++) {
|
||||
if (rd_cnt[i]) {
|
||||
pop_dbg_kind(rd_kind[i]); /* ВРЕМЕННО: какой вид перерисовки */
|
||||
switch (rd_kind[i]) {
|
||||
case POP_RD_SPIKE: pop_spike_redraw(row, col); break;
|
||||
case POP_RD_LOOSE: pop_loose_shake_draw(row, col); break;
|
||||
@@ -120,6 +132,7 @@ void pop_redraw_needed(void) __banked
|
||||
case POP_RD_LEVELDOOR: pop_leveldoor_redraw(row, col); break;
|
||||
case POP_RD_GATE: pop_gate_redraw(row, col); break;
|
||||
case POP_RD_CHOMP: pop_chomp_redraw(row, col); break;
|
||||
case POP_RD_CHOMP_ANIM: pop_chomp_anim_draw(row, col); break;
|
||||
default: break;
|
||||
}
|
||||
cnt++; /* ВРЕМЕННО: замер */
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#define POP_RD_LEVELDOOR 5 /* створка двери уровня (ПРАВАЯ половина) */
|
||||
#define POP_RD_GATE 6 /* решётка ворот (tilepos = САМИ ворота) */
|
||||
#define POP_RD_CHOMP 7 /* чомпер: кадр смыкания челюстей */
|
||||
#define POP_RD_CHOMP_ANIM 8 /* чомпер: вернуть ТОЛЬКО слой anim поверх огня */
|
||||
|
||||
/* Виды перерисовки полосы у потолка (по колонке). */
|
||||
#define POP_RDA_CEIL 1 /* плита-потолок: дрожащий кадр / покой */
|
||||
|
||||
@@ -618,7 +618,12 @@ void pop_loose_shake_draw(int row, int col) __banked
|
||||
* NB: печёный кадр-0 может слегка просвечивать по кромкам — чистое
|
||||
* решение (bake тайла как empty + рисовать loose всегда динамически)
|
||||
* отложено; wipe-в-чёрный делал плиту невидимой — не годится. */
|
||||
pop_heal_off(x, 63 * row + 46, 64, 24);
|
||||
/* Ширина 58, а не 64 = «две клетки»: реальный след дрожащей плиты —
|
||||
* свой тайл (верх/левая грань 32 px) плюс правая грань, которая уходит
|
||||
* в соседнюю клетку на 26 px (25 во дворце). Габариты сняты из
|
||||
* каталогов атласов: 41/69/70 = 32x13-14, 43/73/74 = 32x3,
|
||||
* 42/71/72 = 26x15-16. Задача HEAL-WIDTH. */
|
||||
pop_heal_off(x, 63 * row + 46, 58, 24);
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
draw_tile(row, col); /* loose: лево+низ (анимир.) */
|
||||
if (col + 1 < 10)
|
||||
@@ -626,6 +631,47 @@ void pop_loose_shake_draw(int row, int col) __banked
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
}
|
||||
|
||||
/* Вернуть ТОЛЬКО слой anim чомпера — порт ветки redraw_frames_anim в
|
||||
* redraw_needed (seg008:0211). Контракт и разбор — в шапке объявления
|
||||
* (pop_bg.h).
|
||||
*
|
||||
* Чем это отличается от pop_chomp_redraw и почему нужна отдельная функция.
|
||||
* Пометку ставит АНИМАЦИЯ ФАКЕЛА: пламя лежит в ячейке ПРАВОГО соседа
|
||||
* (seg008:560), то есть поверх чомпера, и запекается в фон каждый кадр.
|
||||
* Возвращать после него надо ровно графику чомпера — а pop_chomp_redraw
|
||||
* делал heal 32x64 плюс ПОЛНЫЙ draw_tile, то есть пересобирал тайл со всеми
|
||||
* слоями (правая грань, база, низ, loose), которых пламя не касалось.
|
||||
* Замер 11/15 (2026-08-19): 190 260 тактов на этот единственный тайл, 24 %
|
||||
* работы кадра.
|
||||
*
|
||||
* heal не нужен: поза чомпера здесь НЕ меняется (свою анимацию ведёт
|
||||
* pop_chomp_redraw по своей пометке), стирать нечего — рисуем ту же
|
||||
* графику поверх свежего пламени. Оригинал на пометку anim wipe тоже не
|
||||
* делает: у него это отдельный счётчик wipe_frames.
|
||||
*
|
||||
* Передний слой (зубья, POP_CHOMP_FRAM_FOR) сюда НЕ входит — как и в
|
||||
* оригинале, где fore идёт по своему счётчику redraw_frames_fore. Геометрия
|
||||
* это подтверждает: пламя занимает 18 строк с низом на 63*row+22, передние
|
||||
* зубья — на dmy = 63*row+62, то есть на 40 строк ниже; пересекается с огнём
|
||||
* только ВЕРХНЯЯ челюсть (подъём до 0x32 = 50 px), а она в back-слое. */
|
||||
void pop_chomp_anim_draw(int row, int col) __banked
|
||||
{
|
||||
int x = POP_COL_XH[col] * 8;
|
||||
int dmy = 63 * row + 62; /* dby - 3, как в draw_tile */
|
||||
uint8_t cm = pop_tile_mod(row, col);
|
||||
uint8_t pose = pop_chomp_pose(cm);
|
||||
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
pop_cd_batch_begin(); /* одна пометка на все куски */
|
||||
pop_env_b(CHOMP_FRAM_BOT[pose], x, dmy);
|
||||
if (cm & 0x80) /* кого-то перемололо */
|
||||
pop_env_b((uint8_t)(pose + 114), x + 8, dmy - 6);
|
||||
if (CHOMP_FRAM_TOP[pose])
|
||||
pop_env_b(CHOMP_FRAM_TOP[pose], x, dmy - CHOMP_FRAM_Y[pose]);
|
||||
pop_cd_batch_end();
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
}
|
||||
|
||||
/* Перерисовать пики тайла (row,col) на back-странице по ЖИВОМУ modif
|
||||
* (pop_trob). Кадр выдвижения рисует ПРАВЫЙ сосед (draw_tile ветка lcode==2
|
||||
* читает lmod = pop_t_bg[row*10+col] = живой modif пики). pop_t_bg должен указывать
|
||||
@@ -658,7 +704,15 @@ void pop_spike_redraw(int row, int col) __banked
|
||||
void pop_chomp_redraw(int row, int col) __banked
|
||||
{
|
||||
int x = POP_COL_XH[col] * 8;
|
||||
pop_heal_off(x, 63 * row + 2, 32, 64);
|
||||
/* РОВНО 32x60 — весь чомпер помещается в свой тайл, и это его точный
|
||||
* след (замечание пользователя, проверено по каталогу атласа):
|
||||
* нижняя челюсть 101/102 = 32x60 низом на dmy = 63*row+62, то есть
|
||||
* занимает 63*row+3 .. +62;
|
||||
* верхние челюсти дают ТОТ ЖЕ верх — подъём 0x25 при высоте 23,
|
||||
* 0x2F при 13 и 0x32 при 10 все три дают 63*row+3;
|
||||
* кровь 114..118 шириной 6 рисуется на x+8, то есть внутри 32.
|
||||
* Было 64 «на всю высоту тайла» — четыре лишние строки. */
|
||||
pop_heal_off(x, 63 * row + 3, 32, 60);
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
draw_tile(row, col);
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
@@ -1184,7 +1238,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;
|
||||
}
|
||||
|
||||
@@ -1203,13 +1260,6 @@ static void mob_spawn(uint8_t room, int row, int col)
|
||||
m->defer = 0;
|
||||
m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE;
|
||||
m->draw_y = MOB_Y_NONE;
|
||||
{ /* ВРЕМЕННО: журнал решения оверлея — копим за весь полёт */
|
||||
uint8_t k = (uint8_t)(m - mobs);
|
||||
pop_dbg_pass[k] = 0;
|
||||
pop_dbg_ovl[k * 4 + 2] = 0;
|
||||
pop_dbg_ovl[k * 4 + 3] = 0;
|
||||
pop_dbg_tiles[k] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Отрыв куска в ТЕКУЩЕЙ комнате (обычный случай — pop_loose_tick). */
|
||||
@@ -1497,7 +1547,18 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
|
||||
{
|
||||
int8_t mrow = pop_y_to_row((int16_t)mt_m->draw_y); /* y_to_row_mod4 */
|
||||
uint8_t over;
|
||||
if (mrow != pop_bg_obj_row) over = (mrow < pop_bg_obj_row); /* ряды идут 2,1,0 */
|
||||
if (mrow < 0 || mrow > 2) {
|
||||
/* КОРЗИНА 30 (get_tilepos_nominus, seg006:110). Ряд объекта вне
|
||||
* комнаты: y_to_row_mod4 берёт остаток по 4, поэтому «выше
|
||||
* потолка» и «ниже пола» дают одно и то же −1, а оригинал сводит
|
||||
* оба в тайл 30. Объекты этого тайла рисуются ПЕРВЫМИ —
|
||||
* draw_objtable_items_at_tile(30) стоит в redraw_needed_tiles до
|
||||
* всего обхода тайлов. Значит кусок под ВСЕМ, включая Кида; до
|
||||
* сих пор сравнение рядов давало обратное («−1 обходится
|
||||
* последним» = поверх). */
|
||||
over = 0;
|
||||
}
|
||||
else if (mrow != pop_bg_obj_row) over = (mrow < pop_bg_obj_row); /* ряды идут 2,1,0 */
|
||||
else if (mt_m->col != pop_bg_obj_col) over = (mt_m->col > pop_bg_obj_col);
|
||||
else over = (mt_m->draw_y > pop_cd[POP_CH_KID].fpy);
|
||||
/* РИСОВАНИЯ ЗДЕСЬ БОЛЬШЕ НЕТ — только решение, в какой проход кусок
|
||||
@@ -1586,30 +1647,30 @@ static void mob_overlay_neighbour(const mob_t *m)
|
||||
{
|
||||
int8_t r = pop_y_to_row((int16_t)m->draw_y);
|
||||
int8_t rt = pop_y_to_row((int16_t)(m->draw_y - 18));
|
||||
int8_t c = (int8_t)(m->col + 1);
|
||||
/* КОРЗИНА 30 оригинала (get_tilepos_nominus, seg006:110): ряд объекта вне
|
||||
* комнаты. y_to_row_mod4 берёт остаток по 4, поэтому «выше потолка» и
|
||||
* «ниже пола» дают одно и то же −1 — оригинал сводит оба случая в тайл 30
|
||||
* и рисует такие объекты ПЕРВЫМИ, до всего обхода тайлов
|
||||
* (draw_objtable_items_at_tile(30) в redraw_needed_tiles).
|
||||
*
|
||||
* Отсюда два отличия от обычного куска:
|
||||
* - ориентир «тайл объекта» = 3, то есть РАНЬШЕ любого ряда обхода
|
||||
* (2,1,0). Сюда уходил сырой −1, и гейт other_overlay_tile
|
||||
* `row > pop_bg_obj_row` читал его наоборот — «объект поверх всего» —
|
||||
* и соседа не возвращал вовсе (тело плиты пропадало, оставался
|
||||
* только её торец из переднего слоя);
|
||||
* - перекрыть кусок должна и СВОЯ клетка, а не только сосед справа:
|
||||
* после объекта у оригинала рисуются ВСЕ тайлы. У куска в обычном
|
||||
* ряду своя клетка не перерисовывается — там объект вливается в
|
||||
* midtable уже ПОСЛЕ частей своего тайла. */
|
||||
uint8_t b30 = (uint8_t)(r < 0 || r > 2);
|
||||
int8_t oref = b30 ? (int8_t)3 : r;
|
||||
int8_t c, cend = (int8_t)(m->col + 1);
|
||||
int ytop, ybot;
|
||||
uint8_t *dbg = &pop_dbg_ovl[(m - mobs) * 4]; /* ВРЕМЕННО: журнал решения */
|
||||
{ /* ВРЕМЕННО: перебор ВСЕХ тайлов, чьи габариты пересекаются со спрайтом
|
||||
* куска — чтобы понять, скольким тайлам реально нужно лечь поверх. */
|
||||
int8_t rr, kk;
|
||||
int t0 = m->draw_y - (int)mob_spr[2] + 1, t1 = m->draw_y;
|
||||
for (rr = -1; rr <= 2; rr++)
|
||||
for (kk = 0; kk <= 1; kk++) {
|
||||
int8_t cc = (int8_t)(m->col + kk);
|
||||
if (cc > 9) continue;
|
||||
if (pop_tile_code(rr, cc) &&
|
||||
t1 >= 63 * rr + 2 && t0 <= 63 * rr + 66)
|
||||
pop_dbg_tiles[m - mobs] |= (uint8_t)(1 << ((rr + 1) * 2 + kk));
|
||||
}
|
||||
}
|
||||
dbg[0] = (uint8_t)(r + 128); dbg[1] = (uint8_t)(rt + 128);
|
||||
dbg[2] |= 1;
|
||||
if (c > 9) return;
|
||||
dbg[2] |= 2;
|
||||
/* Габарит куска — по ЭКРАННОЙ координате, как и ряд выше: у куска,
|
||||
* видимого из соседней комнаты, draw_y отличается от y на 192, и
|
||||
* сравнение с габаритом тайла по y давало заведомо ложный ответ. */
|
||||
ytop = m->draw_y - (int)mob_spr[2] + 1; /* верх спрайта куска */
|
||||
if (cend > 9) cend = 9;
|
||||
/* Габарит куска — по ЭКРАННОЙ координате: у куска, видимого из соседней
|
||||
* комнаты, draw_y отличается от y на 192. */
|
||||
ytop = m->draw_y - (int)mob_spr[2] + 1;
|
||||
ybot = m->draw_y;
|
||||
/* ПРЕДУСЛОВИЯ ПРОВЕРЯЕМ ЗДЕСЬ, до вызова в банк 2. pop_mob_overlay_tile
|
||||
* живёт в банке 2, то есть каждый вызов из банка 7 платит межбанковый
|
||||
@@ -1620,21 +1681,15 @@ static void mob_overlay_neighbour(const mob_t *m)
|
||||
* ничего не рисовало.
|
||||
*
|
||||
* Габарит тайла берём ПОЛНЫМ (63*row+2 .. +66) — какая именно часть
|
||||
* соседа окажется поверх куска, решит уже окно клипа внутри. Сужать по
|
||||
* реальной высоте кусков нельзя: у колонн и зеркала база высокая. */
|
||||
if (r >= 0 && r <= 2 && pop_tile_code(r, c)) dbg[2] |= 4;
|
||||
if (r >= 0 && r <= 2 && ybot >= 63 * r + 2 && ytop <= 63 * r + 66) dbg[2] |= 8;
|
||||
* соседа окажется поверх куска, решит уже окно клипа внутри. */
|
||||
for (c = b30 ? (int8_t)m->col : cend; c <= cend; c++) {
|
||||
if (c < 0) continue;
|
||||
if (r >= 0 && r <= 2 && pop_tile_code(r, c) &&
|
||||
ybot >= 63 * r + 2 && ytop <= 63 * r + 66) {
|
||||
dbg[2] |= 0x10; dbg[3]++;
|
||||
pop_mob_overlay_tile(r, c, r, (int8_t)m->col, m->x, m->draw_y, mob_spr[2]);
|
||||
}
|
||||
if (rt != r && rt >= 0 && rt <= 2 && pop_tile_code(rt, c)) dbg[2] |= 0x20;
|
||||
if (rt != r && rt >= 0 && rt <= 2 && ybot >= 63 * rt + 2 && ytop <= 63 * rt + 66) dbg[2] |= 0x40;
|
||||
ybot >= 63 * r + 2 && ytop <= 63 * r + 66)
|
||||
pop_mob_overlay_tile(r, c, oref, (int8_t)m->col, m->x, m->draw_y, mob_spr[2]);
|
||||
if (rt != r && rt >= 0 && rt <= 2 && pop_tile_code(rt, c) &&
|
||||
ybot >= 63 * rt + 2 && ytop <= 63 * rt + 66) {
|
||||
dbg[2] |= 0x80; dbg[3]++;
|
||||
pop_mob_overlay_tile(rt, c, r, (int8_t)m->col, m->x, m->draw_y, mob_spr[2]);
|
||||
ybot >= 63 * rt + 2 && ytop <= 63 * rt + 66)
|
||||
pop_mob_overlay_tile(rt, c, oref, (int8_t)m->col, m->x, m->draw_y, mob_spr[2]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1650,10 +1705,6 @@ static void mob_draw_pass(uint8_t pg, uint8_t want_defer)
|
||||
mob_t *m = &mobs[i];
|
||||
/* Отбор по draw_y, а НЕ по комнате: кусок, только что провалившийся
|
||||
* в комнату снизу, ещё виден из нашей (см. mob_tick_one). */
|
||||
pop_dbg_pass[i] |= 1;
|
||||
if (m->active) pop_dbg_pass[i] |= 2;
|
||||
if (m->draw_y != MOB_Y_NONE) pop_dbg_pass[i] |= 4;
|
||||
if ((uint8_t)(m->defer != 0) == want_defer) pop_dbg_pass[i] |= 8;
|
||||
if (!m->active || m->draw_y == MOB_Y_NONE) continue;
|
||||
if ((uint8_t)(m->defer != 0) != want_defer) continue;
|
||||
for (j = n; j > 0 && mobs[order[j - 1]].draw_y < m->draw_y; j--)
|
||||
@@ -1670,17 +1721,15 @@ static void mob_draw_pass(uint8_t pg, uint8_t want_defer)
|
||||
* в кадр пересечения границы пометка указывала на прежний тайл, и
|
||||
* передняя часть колонны кусок не перекрывала — ровно ОДИН кадр,
|
||||
* как и наблюдалось (прогон 2026-08-13, комната 23, колонка 8). */
|
||||
pop_dbg_pass[order[i]] |= 0x10;
|
||||
mob_mark_neighbour(&mobs[order[i]]);
|
||||
mob_render(&mobs[order[i]], pg);
|
||||
pop_dbg_pass[order[i]] |= 0x20;
|
||||
mob_overlay_neighbour(&mobs[order[i]]);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1688,6 +1737,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), а кусков в кадре каскада шесть. В пакете они
|
||||
* копят общий прямоугольник четырьмя сравнениями, и настоящая пометка одна.
|
||||
@@ -1722,9 +1775,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 переносит его в комнату снизу; у нас симуляция там не
|
||||
@@ -179,6 +180,16 @@ void pop_dbg_b4(void) { } /* pop_cd_touch */
|
||||
void pop_dbg_b5(void) { } /* gfx_w0_unmap (конец) */
|
||||
void pop_dbg_b6(void) { } /* взведён = пошли в gfx_blit_noclip, а не в _part */
|
||||
|
||||
/* ВРЕМЕННО (разбор pop_phys_tick 2026-08-19): звенья цепочки kid_phys. */
|
||||
void pop_dbg_p1(void) { } /* loadkid_and_opp сделан */
|
||||
void pop_dbg_p2(void) { } /* fall_accel + fall_speed */
|
||||
void pop_dbg_p3(void) { } /* determine_col */
|
||||
void pop_dbg_p4(void) { } /* bump_into_opponent */
|
||||
void pop_dbg_p5(void) { } /* check_collisions */
|
||||
void pop_dbg_p6(void) { } /* check_bumped */
|
||||
void pop_dbg_p7(void) { } /* check_action */
|
||||
void pop_dbg_p8(void) { } /* check_press */
|
||||
|
||||
/* ВРЕМЕННО (регрессия цены блита 2026-08-13): w и h упакованы в один
|
||||
* 16-битный аргумент (arg1 -> HL при __sdcccall(1)), брейкпоинт логирует
|
||||
* HL — дальше цена раскладывается как a + b*h + c*w*h, где b и есть
|
||||
|
||||
@@ -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). */
|
||||
@@ -95,6 +105,12 @@ void pop_dbg_m15(void);
|
||||
void pop_dbg_kind(uint8_t k); void pop_dbg_m16(void);
|
||||
void pop_dbg_b1(void); void pop_dbg_b2(void); void pop_dbg_b3(void);
|
||||
void pop_dbg_b4(void); void pop_dbg_b5(void); void pop_dbg_b6(void);
|
||||
/* ВРЕМЕННО (разбор pop_phys_tick 2026-08-19, позиция P2a): физика Кида —
|
||||
* 61 266 тактов на НЕПОДВИЖНОМ персонаже. По одному зонду на звено
|
||||
* цепочки kid_phys. */
|
||||
void pop_dbg_p1(void); void pop_dbg_p2(void); void pop_dbg_p3(void);
|
||||
void pop_dbg_p4(void); void pop_dbg_p5(void); void pop_dbg_p6(void);
|
||||
void pop_dbg_p7(void); void pop_dbg_p8(void);
|
||||
void pop_dbg_wh(uint16_t wh);
|
||||
|
||||
/* ВРЕМЕННО: трасса kidobj (см. pop_state.c). Порядок байт:
|
||||
|
||||
@@ -195,43 +195,34 @@ uint8_t pop_spike_frame(uint8_t m)
|
||||
* и перерисовывался каждый кадр со всем fore-проходом (замер: циан 231 829
|
||||
* тактов против 20 071, период 4 растровых кадра против 3).
|
||||
*
|
||||
* Гранулярность тайла (32 x 63) вместо точного прямоугольника — осознанное
|
||||
* огрубление: факел помечает всю свою колонку по высоте ряда. Персонаж и
|
||||
* так занимает бОльшую часть высоты ряда, зато касания перестают
|
||||
* склеиваться. См. docs/impl_diff.md. */
|
||||
* ГРАНУЛЯРНОСТЬ: колонка 32 px по горизонтали, ТОЧНЫЙ диапазон y по
|
||||
* вертикали. Раньше по вертикали стоял номер ряда (три полосы по 63 px), и
|
||||
* это склеивало касания, разнесённые внутри ряда. Найдено пользователем на
|
||||
* сцене 11/15 (2026-08-19): пламя факела занимает y 5..22, клинок стоящего
|
||||
* стража — y 31..37, между ними девять пикселей чистого зазора, а метка
|
||||
* считала слот задетым, потому что оба попадают в ряд 0 и колонку 7.
|
||||
* Стоило это 148 302 такта в кадр — 23 % работы — на перерисовку персонажа,
|
||||
* которого никто не трогал.
|
||||
*
|
||||
* Почему диапазон, а не более мелкие полосы: полосы по 16 px эту пару всё
|
||||
* равно склеивают (пламя кончается в полосе 1, клинок в ней же начинается),
|
||||
* а 8-пиксельные потребовали бы 24 маски на страницу. Пара ymin/ymax на
|
||||
* колонку — 40 байт на обе страницы, точнее любых полос и без битовой возни.
|
||||
*
|
||||
* У ОРИГИНАЛА такой метки нет вовсе: и Apple II (FRAMEADV.S RedBlockFast с
|
||||
* шестью буферами wipebuf/redbuf/movebuf/floorbuf/halfbuf/objbuf), и SDLPoP
|
||||
* (set_redraw_fore в redraw_at_char) метят ЦЕЛЫМИ тайлами, но им это не
|
||||
* мешает — персонаж у них рисуется каждый кадр безусловно, а пометки нужны
|
||||
* только фону. Пропуск неизменившегося персонажа — наша добавка, поэтому и
|
||||
* точность метки нужна выше оригинальной. См. docs/impl_diff.md. */
|
||||
uint8_t pop_cd_dirty;
|
||||
uint16_t pop_cd_dmask[2][3];
|
||||
/* [страница][колонка] — диапазон затронутых экранных y. Пусто = ymin > ymax
|
||||
* (заполняется как ymin = 255, ymax = 0). */
|
||||
uint8_t pop_cd_ymin[2][10], pop_cd_ymax[2][10];
|
||||
|
||||
/* CD_LOW[n] = n младших единиц: маска пробега колонок c0..c1 считается как
|
||||
* CD_LOW[c1+1] & ~CD_LOW[c0] — два чтения таблицы вместо цикла сдвигов. */
|
||||
static const uint16_t CD_LOW[11] = {
|
||||
0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F,
|
||||
0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF
|
||||
};
|
||||
|
||||
/* Экранный y -> ряд комнаты 0..2 с клампом (полоса кладки у потолка и низ
|
||||
* стены ложатся на крайние ряды). Цепочкой сравнений, а не делением на 63:
|
||||
* у SDCC z80 одно деление ~5 400 тактов (memory sdcc_z80_division_hoisting). */
|
||||
static uint8_t cd_row_of(int y)
|
||||
{
|
||||
y -= POP_YOFF;
|
||||
if (y < 63) return 0;
|
||||
if (y < 126) return 1;
|
||||
return 2;
|
||||
}
|
||||
|
||||
/* Колонки прямоугольника [x..x1] (включительно) -> битовая маска. Тайл ровно
|
||||
* 32 px и начинается с x=0, поэтому колонка — просто сдвиг. */
|
||||
static uint16_t cd_cols_of(int x, int x1)
|
||||
{
|
||||
uint8_t c0, c1;
|
||||
if (x < 0) x = 0;
|
||||
if (x1 > 319) x1 = 319;
|
||||
if (x1 < x) return 0; /* весь прямоугольник вне экрана */
|
||||
c0 = (uint8_t)(x >> 5);
|
||||
c1 = (uint8_t)(x1 >> 5);
|
||||
return (uint16_t)(CD_LOW[c1 + 1] & ~CD_LOW[c0]);
|
||||
}
|
||||
/* CD_LOW, cd_row_of и cd_cols_of СНЯТЫ вместе с переходом на диапазон y:
|
||||
* колонка теперь считается прямым сдвигом (x >> 5), а вертикаль сравнением
|
||||
* отрезков — битовые маски больше не нужны. */
|
||||
|
||||
/* Обе страницы помечаются сразу (персонаж чинится на каждой в свой кадр),
|
||||
* поэтому цикл развёрнут: индекс-переменная заставляла SDCC считать адрес
|
||||
@@ -274,8 +265,6 @@ void pop_cd_unmute(void) { pop_cd_batch = 0; }
|
||||
|
||||
void pop_cd_touch(int x, int y, int w, int h)
|
||||
{
|
||||
uint16_t cols;
|
||||
uint8_t r0, r1;
|
||||
if (pop_cd_batch) { /* копим, не разбирая на колонки/ряды */
|
||||
if (pop_cd_batch == CD_BATCH_MUTE) return; /* область помечена вызывающим */
|
||||
int x1 = x + w - 1, y1 = y + h - 1;
|
||||
@@ -285,18 +274,30 @@ void pop_cd_touch(int x, int y, int w, int h)
|
||||
if (y1 > cdb_y1) cdb_y1 = y1;
|
||||
return;
|
||||
}
|
||||
cols = cd_cols_of(x, x + w - 1);
|
||||
if (!cols) return;
|
||||
r0 = cd_row_of(y);
|
||||
r1 = cd_row_of(y + h - 1);
|
||||
pop_cd_dmask[0][r0] |= cols;
|
||||
pop_cd_dmask[1][r0] |= cols;
|
||||
if (r1 != r0) {
|
||||
pop_cd_dmask[0][r1] |= cols;
|
||||
pop_cd_dmask[1][r1] |= cols;
|
||||
if (r1 - r0 > 1) { /* прямоугольник накрыл все три ряда */
|
||||
pop_cd_dmask[0][1] |= cols;
|
||||
pop_cd_dmask[1][1] |= cols;
|
||||
{
|
||||
int8_t c0, c1, c;
|
||||
uint8_t y0, y1;
|
||||
int xr = x + w - 1;
|
||||
if (xr < 0 || x > 319) return; /* весь прямоугольник вне поля */
|
||||
if (x < 0) x = 0;
|
||||
if (xr > 319) xr = 319;
|
||||
c0 = (int8_t)(x >> 5);
|
||||
c1 = (int8_t)(xr >> 5);
|
||||
/* Клип по вертикали: экранные y не выходят за байт, а всё, что выше
|
||||
* поля или ниже его, персонажам всё равно не принадлежит. */
|
||||
if (y < 0) y = 0;
|
||||
if (y > 255) return;
|
||||
y0 = (uint8_t)y;
|
||||
y1 = (y + (int)h - 1 > 255) ? 255 : (uint8_t)(y + h - 1);
|
||||
/* Страницы обновляются НЕЗАВИСИМО. Общее условие по странице 0
|
||||
* («если ей стало теснее — записать в обе») ломается сразу после
|
||||
* pop_cd_clear(0): страница 1 хранит свои старые границы, условие по
|
||||
* нулевой уже не выполняется, и её метка перестаёт расти. */
|
||||
for (c = c0; c <= c1; c++) {
|
||||
if (y0 < pop_cd_ymin[0][c]) pop_cd_ymin[0][c] = y0;
|
||||
if (y1 > pop_cd_ymax[0][c]) pop_cd_ymax[0][c] = y1;
|
||||
if (y0 < pop_cd_ymin[1][c]) pop_cd_ymin[1][c] = y0;
|
||||
if (y1 > pop_cd_ymax[1][c]) pop_cd_ymax[1][c] = y1;
|
||||
}
|
||||
}
|
||||
pop_cd_dirty = 3;
|
||||
@@ -305,27 +306,87 @@ void pop_cd_touch(int x, int y, int w, int h)
|
||||
/* Задел ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 ВКЛЮЧИТЕЛЬНО) то, что
|
||||
* трогали на странице p. Резидент: зовёт pop_cdraw.c из банка 4 прямым
|
||||
* вызовом, без трамплина. */
|
||||
/* Прямоугольник слота против метки — БЕЗ передачи пяти аргументов.
|
||||
*
|
||||
* pop_cd_hit принимает (p, x0, y0, x1, y1): три последних идут стеком, и
|
||||
* функция целиком уезжает в IX-фрейм — 45 % её тактов уходит на `-n(ix)`
|
||||
* (замер asm 2026-08-19). А зовут её из cd_quiet дважды на слот, то есть
|
||||
* до восьми раз за кадр. Здесь координаты берутся прямо из pop_cd, и
|
||||
* аргументов остаётся два.
|
||||
*
|
||||
* Спрайт и накладной проверяются РАЗДЕЛЬНО — объединённый bbox ловит
|
||||
* касания углом, которых нет (разбор в cd_quiet, pop_cdraw.c). */
|
||||
static int hit_x, hit_y; /* аргументы hit_rect — file-scope, не стек */
|
||||
static uint16_t hit_w, hit_h;
|
||||
static uint8_t hit_p;
|
||||
|
||||
static uint8_t hit_rect(void)
|
||||
{
|
||||
int8_t c0, c1, c;
|
||||
uint8_t ya, yb;
|
||||
int x1 = hit_x + (int)hit_w - 1;
|
||||
int y1 = hit_y + (int)hit_h - 1;
|
||||
if (!hit_w || !hit_h) return 0;
|
||||
if (x1 < 0 || hit_x > 319) return 0;
|
||||
if (y1 < 0 || hit_y > 255) return 0;
|
||||
ya = (uint8_t)(hit_y < 0 ? 0 : hit_y);
|
||||
yb = (uint8_t)(y1 > 255 ? 255 : y1);
|
||||
c0 = (int8_t)((hit_x < 0 ? 0 : hit_x) >> 5);
|
||||
c1 = (int8_t)((x1 > 319 ? 319 : x1) >> 5);
|
||||
for (c = c0; c <= c1; c++)
|
||||
if (ya <= pop_cd_ymax[hit_p][c] && yb >= pop_cd_ymin[hit_p][c]) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t pop_cd_hit_slot(uint8_t who, uint8_t p)
|
||||
{
|
||||
const pop_cdraw_t *s = &pop_cd[who];
|
||||
if (!(pop_cd_dirty & (1 << p))) return 0;
|
||||
hit_p = p;
|
||||
hit_x = s->x[p]; hit_y = s->y[p] + POP_YOFF;
|
||||
hit_w = s->w[p]; hit_h = s->h[p];
|
||||
if (hit_rect()) return 1;
|
||||
if (!s->ovalid[p]) return 0;
|
||||
hit_x = s->ox[p]; hit_y = s->oy[p] + POP_YOFF;
|
||||
hit_w = s->ow[p]; hit_h = s->oh[p];
|
||||
return hit_rect();
|
||||
}
|
||||
|
||||
uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1)
|
||||
{
|
||||
uint16_t cols;
|
||||
uint8_t r0, r1;
|
||||
int8_t c0, c1, c;
|
||||
uint8_t ya, yb;
|
||||
if (!(pop_cd_dirty & (1 << p))) return 0;
|
||||
cols = cd_cols_of(x0, x1);
|
||||
if (!cols) return 0;
|
||||
r0 = cd_row_of(y0);
|
||||
r1 = cd_row_of(y1);
|
||||
if (pop_cd_dmask[p][r0] & cols) return 1;
|
||||
if (r1 != r0) {
|
||||
if (pop_cd_dmask[p][r1] & cols) return 1;
|
||||
if (r1 - r0 > 1 && (pop_cd_dmask[p][1] & cols)) return 1;
|
||||
}
|
||||
if (x1 < 0 || x0 > 319) return 0;
|
||||
if (x0 < 0) x0 = 0;
|
||||
if (x1 > 319) x1 = 319;
|
||||
if (y1 < 0 || y0 > 255) return 0;
|
||||
ya = (uint8_t)(y0 < 0 ? 0 : y0);
|
||||
yb = (uint8_t)(y1 > 255 ? 255 : y1);
|
||||
c0 = (int8_t)(x0 >> 5);
|
||||
c1 = (int8_t)(x1 >> 5);
|
||||
/* Пересечение отрезков [ya,yb] и [ymin,ymax] хотя бы в одной колонке.
|
||||
* Пустая колонка держит ymin = 255, ymax = 0 — условие ниже её отсеет
|
||||
* само, отдельной проверки «пусто» не нужно. */
|
||||
for (c = c0; c <= c1; c++)
|
||||
if (ya <= pop_cd_ymax[p][c] && yb >= pop_cd_ymin[p][c]) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Страница приведена в порядок — снять с неё метку целиком. */
|
||||
/* Начальное состояние обеих страниц — «ничего не трогали». ЯВНО, а не
|
||||
* расчётом на обнуление _DATA: пустая колонка обозначается ymin = 255,
|
||||
* ymax = 0, а нули от crt0 читались бы как «затронута строка 0». */
|
||||
void pop_cd_init(void)
|
||||
{
|
||||
pop_cd_clear(0);
|
||||
pop_cd_clear(1);
|
||||
}
|
||||
|
||||
void pop_cd_clear(uint8_t p)
|
||||
{
|
||||
pop_cd_dmask[p][0] = pop_cd_dmask[p][1] = pop_cd_dmask[p][2] = 0;
|
||||
uint8_t c;
|
||||
for (c = 0; c < 10; c++) { pop_cd_ymin[p][c] = 255; pop_cd_ymax[p][c] = 0; }
|
||||
pop_cd_dirty = (uint8_t)(pop_cd_dirty & ~(1 << p));
|
||||
}
|
||||
|
||||
@@ -561,8 +622,18 @@ void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
|
||||
return;
|
||||
}
|
||||
/* pb_w<256 && pb_h<256 больше не проверяем — гарантировано типом. */
|
||||
if (pb_x >= 0 && pb_top >= 0 &&
|
||||
pb_x + (int)pb_w <= 320 && pb_top + (int)pb_h <= 256) {
|
||||
/* «Целиком на экране?» — ДВУМЯ беззнаковыми сравнениями вместо
|
||||
* четырёх знаковых. Отрицательная координата в беззнаковом виде
|
||||
* становится очень большой и проваливает то же условие, что
|
||||
* проверка `>= 0`, а верхняя граница переносится в правую часть,
|
||||
* так что сложение уходит вместе с ней. Знаковое сравнение у
|
||||
* SDCC z80 стоит дорого: пара `sbc` плюс `jp PO / xor 0x80 / jp P`
|
||||
* на каждое (видно в листинге).
|
||||
*
|
||||
* Границы правой части неотрицательны по построению: pb_w и pb_h
|
||||
* не больше 255, значит 320-pb_w >= 65 и 256-pb_h >= 1. */
|
||||
if ((unsigned int)pb_x <= (unsigned int)(320 - (int)pb_w) &&
|
||||
(unsigned int)pb_top <= (unsigned int)(256 - (int)pb_h)) {
|
||||
if (pop_upside) gfx_blit_noclip_vflip(pb_x, pb_top, pb_img);
|
||||
else gfx_blit_noclip(pb_x, pb_top, pb_img);
|
||||
} else {
|
||||
|
||||
@@ -633,13 +633,33 @@ void pop_process_trobs(uint8_t cur_room) __banked
|
||||
pop_level_access_end();
|
||||
}
|
||||
|
||||
pop_dbg_m8(); /* ЗАМЕР: префетч кодов тайлов сделан */
|
||||
|
||||
/* Указатель на модификаторы КОМНАТЫ кэшируется между итерациями.
|
||||
*
|
||||
* pop_trob_modif объявлен __banked, и вызов его на КАЖДЫЙ trob — это
|
||||
* трамплин из банка 6 в банк 6 же... нет: из горячего цикла в банковую
|
||||
* функцию, то есть полный переход через резидент. А комната у trob'ов
|
||||
* в подавляющем большинстве кадров ОДНА (все они из cur_room, чужие
|
||||
* появляются лишь у брошенных плит соседней комнаты). Тот же паттерн
|
||||
* «трамплин в цикле», что дал −23 784 на луче видимости стража
|
||||
* (P2b) и −14 118 на guard_over_kid (P16).
|
||||
*
|
||||
* Сбрасывается на каждом кадре: room_seen внутри pop_trob_modif живёт
|
||||
* дольше, но указатель на строку room_modif может смениться при
|
||||
* перезагрузке комнаты, поэтому кэш локальный для одного прохода. */
|
||||
{
|
||||
uint8_t mod_room = 0; /* 0 = «ещё не брали» (комнаты 1..24) */
|
||||
uint8_t *mod = 0;
|
||||
|
||||
for (i = 0; i < trobs_count; i++) {
|
||||
uint8_t room = trobs[i].room;
|
||||
uint8_t tp = trobs[i].tilepos;
|
||||
uint8_t code = trob_code[i];
|
||||
uint8_t *mod = pop_trob_modif(room);
|
||||
int8_t type = trobs[i].type;
|
||||
|
||||
if (room != mod_room) { mod = pop_trob_modif(room); mod_room = room; }
|
||||
|
||||
switch (code) {
|
||||
case TILE_SPIKE:
|
||||
animate_spike(&mod[tp], &type);
|
||||
@@ -728,7 +748,7 @@ void pop_process_trobs(uint8_t cur_room) __banked
|
||||
* случай «застыл справа от факела» в BUGS_OPEN.md как открытый:
|
||||
* пики и меч рядом с факелом на уровнях 1-4 не встретились. */
|
||||
if (trob_rcode[i] == TILE_CHOMP)
|
||||
pop_set_redraw((uint8_t)(tp + 1), POP_RD_CHOMP, 1);
|
||||
pop_set_redraw((uint8_t)(tp + 1), POP_RD_CHOMP_ANIM, 1);
|
||||
}
|
||||
}
|
||||
if (room == cur_room && code == TILE_SPIKE) {
|
||||
@@ -747,11 +767,31 @@ void pop_process_trobs(uint8_t cur_room) __banked
|
||||
pop_set_redraw(tp, POP_RD_SPIKE, (uint8_t)(type < 0 ? 2 : 1));
|
||||
}
|
||||
if (room == cur_room && code == TILE_CHOMP) {
|
||||
/* Кадр меняется каждый тик, пока trob жив — как у пик, метим
|
||||
* текущую страницу; последний кадр (челюсти встали) — обе,
|
||||
* иначе на второй странице дабл-буфера застынет предыдущая поза
|
||||
* и чомпер дрожит через кадр. */
|
||||
pop_set_redraw(tp, POP_RD_CHOMP, (uint8_t)(type < 0 ? 2 : 1));
|
||||
/* ТОЛЬКО ПОКА ФАЗА < 6 — как в оригинале (animate_chomper,
|
||||
* seg007:0448 заканчивается `if ((curr_modifier & 0x7F) < 6)
|
||||
* redraw_at_trob();`).
|
||||
*
|
||||
* Это не оптимизация оригинала, а точное следствие таблицы поз:
|
||||
* CHOMP_FRAM1 = {3,2,0,1,4,3,3}, то есть с фазы 5 и до конца
|
||||
* круга (POP_CHOMPER_SPEED = 15) поза одна и та же — 3.
|
||||
* Перерисовывать её десять кадров подряд значит рисовать ровно
|
||||
* ту же картинку, а стоит это 190 260 тактов НА КАДР: полный
|
||||
* draw_tile плюс heal 32x64, 24 % работы кадра в 11/15
|
||||
* (замер 2026-08-19).
|
||||
*
|
||||
* На фазе 5 метим ОБЕ страницы дабл-буфера: она последняя
|
||||
* рисуемая, и её поза обязана лечь на обе, иначе на второй
|
||||
* останется поза фазы 4 и чомпер будет дрожать через кадр.
|
||||
* Сходится и по кадрам: вторую страницу эта пометка догоняет в
|
||||
* кадре фазы 6, где поза та же самая (CHOMP_FRAM1[6] == 3).
|
||||
*
|
||||
* Про type < 0 отдельной ветки больше нет: trob снимается сам
|
||||
* только при frame >= 6 (см. animate_chomper выше), то есть
|
||||
* когда перерисовки уже не идут, а на экране с фазы 5 лежит
|
||||
* финальная поза на обеих страницах. */
|
||||
uint8_t ph = (uint8_t)(mod[tp] & 0x7F);
|
||||
if (ph < 6)
|
||||
pop_set_redraw(tp, POP_RD_CHOMP, (uint8_t)(ph == 5 ? 2 : 1));
|
||||
}
|
||||
if (room == cur_room && code == TILE_GATE) {
|
||||
/* draw_trob (seg007:01E6), которым заканчивается animate_door:
|
||||
@@ -797,6 +837,7 @@ void pop_process_trobs(uint8_t cur_room) __banked
|
||||
}
|
||||
}
|
||||
}
|
||||
} /* конец блока с кэшем mod/mod_room */
|
||||
|
||||
/* compact: удалить завершённые (type < 0). */
|
||||
for (i = 0; i < trobs_count; i++)
|
||||
|
||||
@@ -361,11 +361,14 @@ int main(void)
|
||||
back = dbuf ? (uint8_t)(gfx_get_visible_page() ^ 1) : 0;
|
||||
gfx_set_draw_page(back);
|
||||
}
|
||||
pop_dbg_m13(); /* ЗАМЕР: ввод и читы разобраны */
|
||||
pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */
|
||||
pop_check_mouse(); /* спецсобытие ур.8: мышь жмёт кнопку */
|
||||
pop_check_killed_shadow(); /* спецсобытие ур.12: смерть тени = смерть Кида */
|
||||
pop_frame_timers(); /* timers (seg003:0735): вспышка слияния */
|
||||
pop_dbg_m9(); /* ЗАМЕР: pop_frame_timers сделан */
|
||||
pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
|
||||
pop_dbg_m14(); /* ЗАМЕР: спецсобытия + луч видимости */
|
||||
pop_ctrl_tick(); /* ввод -> control(): смена seq */
|
||||
/* Кто в этом кадре не изменился с прошлой отрисовки ЭТОЙ страницы —
|
||||
* тот на ней уже нарисован правильно: ни стирать, ни рисовать заново
|
||||
@@ -636,15 +639,25 @@ int main(void)
|
||||
* фон кадра (перепечатки тайлов, шов) обязан быть готов ДО любого
|
||||
* спрайта, иначе перепечатка ложится поверх уже нарисованного куска —
|
||||
* так окно задней стены и передняя грань пола оказывались на плите. */
|
||||
pop_dbg_m10(); /* ЗАМЕР: check_mirror сделан */
|
||||
pop_loose_mob_draw();
|
||||
pop_dbg_m11(); /* ЗАМЕР: pop_loose_mob_draw сделан */
|
||||
{ /* Кто позже — тот поверх; порядок задаёт обход тайлов, а не роль
|
||||
* персонажа (см. guard_over_kid). Отрисовка одна на всех Char
|
||||
* (pop_cdraw.c), различает их только слот. */
|
||||
uint8_t g_after = guard_over_kid();
|
||||
/* Пересчёт после тика: слот, за который heal не платили, мог
|
||||
* всё-таки сдвинуться — тогда бит снимется, а прошлый кадр
|
||||
* сотрёт сам pop_char_draw (страховка cd_heal). */
|
||||
uint8_t g_after;
|
||||
skip = pop_char_skip_mask();
|
||||
pop_dbg_m12(); /* ЗАМЕР: skip_mask сделан */
|
||||
/* Порядок «кто поверх кого» нужен, только если кого-то РИСУЕМ.
|
||||
* При skip == 3 оба слота тихие, рисовать некого — а вызов стоит
|
||||
* трамплина в банк 8 и двух objtile_at_char, 14 424 такта
|
||||
* (замер 11/15, 2026-08-19). Перестановка безопасна: обе
|
||||
* функции только читают, и читают разное — skip_mask снимок
|
||||
* cd_sig, guard_over_kid габариты pop_cd прошлого кадра. */
|
||||
g_after = (skip == 3) ? 0 : guard_over_kid();
|
||||
if (!g_after && !(skip & 2)) { pop_char_draw(POP_CH_OPP); pop_char_fore(POP_CH_OPP); }
|
||||
PROF(6); /* кадр Кида (тот же циан) */
|
||||
if (!(skip & 1)) pop_char_draw(POP_CH_KID); /* спрайт + брызги + клинок */
|
||||
|
||||
@@ -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, ряд пересчитать) —
|
||||
@@ -648,6 +680,7 @@ int pop_boot(void) __banked
|
||||
puts("pop_bg_load failed");
|
||||
return -1;
|
||||
}
|
||||
pop_cd_init(); /* метка «фон трогали» — пустые диапазоны */
|
||||
pop_cheats = 1; /* режим разработки: читы включены */
|
||||
pop_guard_reset();
|
||||
if (pop_kid_data_load("KID\\kid_data.bin") != 0) { /* кадры+seqtbl в EMM-странице */
|
||||
|
||||
@@ -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