Замер P2 и P6: главные статьи — физика двух Char и луч видимости

Синяя фаза (286 518) разложена зондами на 12 участков, process_trobs
(89 784) — на три.

Гипотеза, с которой я входил в замер, ОТВЕРГНУТА.  Я ждал, что дорого
обходятся банковые трамплины на спецсобытиях уровней — по аналогии с
pop_clip_char_top, где трамплин ради одной проверки стоит 8 892.  На деле
три спецсобытия (skel, mouse, killed_shadow) вместе стоят 1 650: они
гейтятся внутри и на уровне 11 выходят сразу.

Настоящие статьи синей:

  физика двух Char        105 246  (61 266 Кид + 43 980 страж)
  heal двух Char           67 734
  луч видимости стража  до 37 032  (вместе с frame_timers)
  pop_ctrl_tick            18 648
  логика стража            19 932

Физика съедает 13,7 % работы кадра при том, что ОБА персонажа стоят и кадр
позы не меняется.  Цена измерена, причина нет — это отдельная позиция P2a.
Луч видимости считается каждый кадр, хотя никто не двигался: гейт по смене
позиции/комнаты — позиция P2b, ждём −30 000.  heal отдельной правки не
требует, он уйдёт вместе с P1/P3.

process_trobs: префетч кодов тайлов с маппингом окна 0 — 11 058, обход
самих trob'ов ~43 000 (pop_trob_modif зовётся банковым вызовом на КАЖДЫЙ
trob, хотя комната одна), два факела ~36 000.  Цена одного pop_pot_b
измерена отдельно: 17 346, и это единственная группа в распределении —
значит в кадре его зовут только факелы.  Пиксели пламени 16x18 — 1 716,
то есть 10 % цены.

Заодно посчитано, достижим ли период 3 растра: снять надо 338 000, а сумма
ВСЕХ известных позиций даёт 357 000, из которых 160 000 держатся на одной
(P1).  Цель достижима, но без запаса.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 11:05:58 +03:00
parent 6d7c1c8b6b
commit f81b30eb68
4 changed files with 101 additions and 14 deletions
+3
View File
@@ -110,6 +110,9 @@ heal 32×64 ≈ 33 700, значит на один `draw_tile` уходит ≈
| 2026-08-19 | базовый замер (`09f32ce`) | 801 768 | 293 238 | 320 916 | 187 758 | | 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 | | 2026-08-19 | **P5**: гейты холостого хода в `pop_loose_tick` | **767 928** | 285 864 | 294 384 | 187 764 |
| | | 33 840 | 7 374 | 26 532 | +6 | | | | 33 840 | 7 374 | 26 532 | +6 |
| 2026-08-19 | зонды для замера P2/P6 (временные) | 768 684 | 286 518 | 294 510 | 187 761 |
Оснастка P2/P6 стоит 756 тактов на кадр — замеры до и после сопоставимы.
Циан не изменился (+6 тактов — шум), и это ожидаемо: `loose_tick` целиком Циан не изменился (+6 тактов — шум), и это ожидаемо: `loose_tick` целиком
лежит в зелёной. Синяя просела на 7 374 без прямой причины в правке — лежит в зелёной. Синяя просела на 7 374 без прямой причины в правке —
+86 -14
View File
@@ -85,15 +85,45 @@
Новое, найдено 2026-08-19. Новое, найдено 2026-08-19.
### P2. Логика двух Char — 152 190 тактов БЕЗ единого блита [гипотеза: −40 000 … 70 000] ### P2. Синяя фаза разложена [ЗАМЕР 2026-08-19] — гипотеза не подтвердилась
`m1→C`: `kid_tick`, `pop_phys_tick`, `pop_guard_tick`, `pop_guard_phys_tick`, Замер зондами внутрь обеих половин синей (286 518 тактов):
боёвка, `pop_check_can_guard_see_kid`. Графики там нет вообще, а стоит это
19 % работы кадра — столько же, сколько вся отрисовка обоих персонажей.
**Сначала замер** (зонды внутрь синей фазы, нужна пересборка), потом решение. | участок | такты | доля работы |
Кандидаты по прошлым находкам: `sdcc_z80_stack_locals_hot_loop` (локали в |---|---:|---:|
IX-фрейме), деления в луче видимости, банковые трамплины на каждый шаг. | ввод + читы | 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. Новое, найдено 2026-08-19.
@@ -156,11 +186,28 @@ heal 141 048 + отрисовка 148 566 = 290 000 тактов (36 % рабо
(фаза восстановлена входом в комнату), которое не покрывал никто. (фаза восстановлена входом в комнату), которое не покрывал никто.
Мутационная проверка: со снятым взводом тест падает. Мутационная проверка: со снятым взводом тест падает.
### P6. Цикл `pop_process_trobs` — 59 100 без блитов пламени [гипотеза: 20 000 … 40 000] ### P6. `pop_process_trobs` разложен [ЗАМЕР 2026-08-19] — 89 784
Префетч кодов тайлов делает `pop_level_access_begin/end` (маппинг окна 0) | участок | такты |
каждый кадр; дальше switch по всем trob'ам комнаты. Надо померить, сколько |---|---:|
там trob'ов и куда уходит время. | вход + префетч кодов тайлов (маппинг окна 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. Новое, найдено 2026-08-19.
@@ -230,16 +277,41 @@ BUG-LATTICE-DOORTOP, BUG-SEAM-WEDGE-1) — только отдельным за
| # | что | статус | факт | | # | что | статус | факт |
|---|---|---|---| |---|---|---|---|
| P5 | `loose_tick`: гейты холостого хода | ✅ **сделано** 2026-08-19 | 33 840 на кадре (ждали −28 000) | | P5 | `loose_tick`: гейты холостого хода | ✅ **сделано** 2026-08-19 | 33 840 на кадре (ждали −28 000) |
| P2 | замер логики двух Char | ⬜ следующее | нужна пересборка с зондами | | P2 | замер синей фазы | ✅ замерено 2026-08-19 | гипотеза «трамплины» отвергнута |
| P6 | замер цикла `process_trobs` | ⬜ вместе с P2 | та же пересборка | | P6 | замер `process_trobs` | ✅ замерено 2026-08-19 | 11 058 префетч + ~43 000 цикл |
| P1 | чомпер под факелом: только слой anim | ⬜ | ждём 160 000 | | P1 | чомпер под факелом: только слой anim | ⬜ **следующее** | ждём 160 000 |
| P2b | гейт луча видимости стража | ⬜ | ждём −30 000, риск низкий |
| P4 | накладные блита (4 правки) | ⬜ | ждём −28 000 | | P4 | накладные блита (4 правки) | ⬜ | ждём −28 000 |
| P11 | мелочи с известной ценой | ⬜ | ждём −26 000 | | P11 | мелочи с известной ценой | ⬜ | ждём −26 000 |
| P10 | футпринт персонажа из физики | ⬜ | ждём −23 000 | | P10 | футпринт персонажа из физики | ⬜ | ждём −23 000 |
| P6a/P6b | `trob_modif` из цикла, кэш префетча | ⬜ | ждём −20 000 |
| P2a | разбор `pop_phys_tick` (105 246 на двоих) | ⬜ | цена известна, причина нет |
| P7 | раскол `draw_tile` (G5) | ⬜ | для 13/23, не для 11/15 | | P7 | раскол `draw_tile` (G5) | ⬜ | для 13/23, не для 11/15 |
| P8/P9 | HEAL-WIDTH + G8 | ⬜ обязательная | для сцен с плитами | | P8/P9 | HEAL-WIDTH + G8 | ⬜ обязательная | для сцен с плитами |
| P12 | снять оснастку | ⬜ ПОСЛЕДНЕЙ | без неё не мерить | | P12 | снять оснастку | ⬜ ПОСЛЕДНЕЙ | без неё не мерить |
### Достижим ли период 3 растра — арифметика после замеров
Работа сейчас **767 928**, цель **430 000**, снять надо **338 000**. Что
известно и посчитано:
| источник | ожидание |
|---|---:|
| P1 (чомпер: только слой anim) | 160 000 |
| P3 (Кид перестанет будиться, следствие P1) | −70 000 |
| P2b (гейт луча видимости) | −30 000 |
| P4 (накладные блита) | −28 000 |
| P11 (мелочи) | 26 000 |
| P10 (футпринт из физики) | −23 000 |
| P6a+P6b | 20 000 |
| **сумма** | **357 000** |
То есть цель достижима, но **впритык и только если сделать ВСЁ перечисленное**,
причём половина суммы держится на одной позиции (P1). Запаса нет, и любая
позиция, давшая меньше ожидаемого, возвращает нас к разбору `pop_phys_tick`
(P2a) — 105 246 тактов на физику двух НЕПОДВИЖНЫХ персонажей, где цена
измерена, а причина ещё не найдена.
**Текущее состояние бюджета 11/15:** **Текущее состояние бюджета 11/15:**
| | работа | синяя | зелёная | циан | период | | | работа | синяя | зелёная | циан | период |
+2
View File
@@ -633,6 +633,8 @@ void pop_process_trobs(uint8_t cur_room) __banked
pop_level_access_end(); pop_level_access_end();
} }
pop_dbg_m8(); /* ЗАМЕР: префетч кодов тайлов сделан */
for (i = 0; i < trobs_count; i++) { for (i = 0; i < trobs_count; i++) {
uint8_t room = trobs[i].room; uint8_t room = trobs[i].room;
uint8_t tp = trobs[i].tilepos; uint8_t tp = trobs[i].tilepos;
+10
View File
@@ -361,35 +361,45 @@ int main(void)
back = dbuf ? (uint8_t)(gfx_get_visible_page() ^ 1) : 0; back = dbuf ? (uint8_t)(gfx_get_visible_page() ^ 1) : 0;
gfx_set_draw_page(back); gfx_set_draw_page(back);
} }
pop_dbg_m13(); /* ЗАМЕР: ввод и читы разобраны */
pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */ pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */
pop_check_mouse(); /* спецсобытие ур.8: мышь жмёт кнопку */ pop_check_mouse(); /* спецсобытие ур.8: мышь жмёт кнопку */
pop_check_killed_shadow(); /* спецсобытие ур.12: смерть тени = смерть Кида */ pop_check_killed_shadow(); /* спецсобытие ур.12: смерть тени = смерть Кида */
pop_dbg_b6(); /* ЗАМЕР: три спецсобытия уровней сделаны */
pop_frame_timers(); /* timers (seg003:0735): вспышка слияния */ pop_frame_timers(); /* timers (seg003:0735): вспышка слияния */
pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */ pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
pop_dbg_m14(); /* ЗАМЕР: спецсобытия + луч видимости */
pop_ctrl_tick(); /* ввод -> control(): смена seq */ pop_ctrl_tick(); /* ввод -> control(): смена seq */
/* Кто в этом кадре не изменился с прошлой отрисовки ЭТОЙ страницы — /* Кто в этом кадре не изменился с прошлой отрисовки ЭТОЙ страницы —
* тот на ней уже нарисован правильно: ни стирать, ни рисовать заново * тот на ней уже нарисован правильно: ни стирать, ни рисовать заново
* не надо (DRAW-COST, контракт в pop_cdraw.h). Решение * не надо (DRAW-COST, контракт в pop_cdraw.h). Решение
* перепроверяется перед самой отрисовкой — тик ещё впереди. */ * перепроверяется перед самой отрисовкой — тик ещё впереди. */
pop_dbg_m15(); /* ЗАМЕР: pop_ctrl_tick сделан */
skip = pop_char_skip_mask(); skip = pop_char_skip_mask();
if (!(skip & 1)) pop_char_heal(POP_CH_KID); /* стереть прошлый кадр ЭТОЙ страницы */ if (!(skip & 1)) pop_char_heal(POP_CH_KID); /* стереть прошлый кадр ЭТОЙ страницы */
if (!(skip & 2)) pop_char_heal(POP_CH_OPP); /* и прошлый кадр соперника */ if (!(skip & 2)) pop_char_heal(POP_CH_OPP); /* и прошлый кадр соперника */
/* Отражение в зеркале — свой heal: у него нет слота pop_cd и в /* Отражение в зеркале — свой heal: у него нет слота pop_cd и в
* skip-маске он не участвует (рисуется/стирается безусловно, живёт * skip-маске он не участвует (рисуется/стирается безусловно, живёт
* только пока Кид стоит на тайле зеркала). */ * только пока Кид стоит на тайле зеркала). */
pop_dbg_m16(); /* ЗАМЕР: heal обоих персонажей сделан */
pop_mirror_heal(); pop_mirror_heal();
pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */ pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */
PROF(2); /* логика (тот же синий) */ PROF(2); /* логика (тот же синий) */
pop_dbg_m1(); /* ЗАМЕР: начало логики */ pop_dbg_m1(); /* ЗАМЕР: начало логики */
kid_tick(); /* play_seq: следующий кадр */ kid_tick(); /* play_seq: следующий кадр */
pop_dbg_b1(); /* ЗАМЕР: kid_tick сделан */
pop_phys_tick(); /* K3: падение/приземление/стена */ pop_phys_tick(); /* K3: падение/приземление/стена */
pop_dbg_b2(); /* ЗАМЕР: pop_phys_tick сделан */
pop_guard_tick(); /* логика стража — В БАНКЕ (guards.c) */ pop_guard_tick(); /* логика стража — В БАНКЕ (guards.c) */
pop_dbg_b3(); /* ЗАМЕР: pop_guard_tick сделан */
pop_guard_phys_tick(); /* и его физика — та же, что у Кида (play_guard_frame) */ pop_guard_phys_tick(); /* и его физика — та же, что у Кида (play_guard_frame) */
/* Боёвка, порядок как в play_frame (seg000:872): сначала пометить /* Боёвка, порядок как в play_frame (seg000:872): сначала пометить
* попадания с обеих сторон, потом применить, потом свести HP. */ * попадания с обеих сторон, потом применить, потом свести HP. */
pop_dbg_b4(); /* ЗАМЕР: pop_guard_phys_tick сделан */
pop_check_sword_hurting(); pop_check_sword_hurting();
pop_check_sword_hurt(); pop_check_sword_hurt();
pop_do_delta_hp(); pop_do_delta_hp();
pop_dbg_b5(); /* ЗАМЕР: боёвка сделана */
pop_guard_fallout(); /* страж провалился ниже комнаты — убрать (seg002:241) */ pop_guard_fallout(); /* страж провалился ниже комнаты — убрать (seg002:241) */
if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */ if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */
uint8_t down = pop_room_link(cur_room, LINK_DOWN); uint8_t down = pop_room_link(cur_room, LINK_DOWN);