diff --git a/applications/PoP/docs/perf_backlog.md b/applications/PoP/docs/perf_backlog.md index 727e9f2..53e9ff7 100644 --- a/applications/PoP/docs/perf_backlog.md +++ b/applications/PoP/docs/perf_backlog.md @@ -97,11 +97,14 @@ MAME (`z80_profiling_method`) на роомтесте, уровень 1 комн --- -## 0. Дворцовая кладка в fore-проходе — 27 % кадра НА НОЛЬ ПИКСЕЛЕЙ +## 0. Дворцовая кладка в fore-проходе — **СДЕЛАНО 2026-08-10** -**Цена: 139 863 такта за кадр (32 % растрового кадра), полезных пикселей — -ровно ноль.** Самая дорогая известная позиция; снятие её одной уводит -работу с 517 609 до ~378 000, то есть период с **4 растровых кадров на 3**. +Все три шага выполнены; замер после — в конце пункта. Ниже сохранён исходный +разбор: он объясняет, почему предфильтра `fore_tile` мало и откуда взялись +габариты кусков. + +**Было: 139 863 такта за кадр (32 % растрового кадра), полезных пикселей — +ровно ноль.** Замер (уровень 4, Кид на (1,7)). Окно fore-клипа в этот момент — `x 229..241, y 106..147` (прочитано из `pop_t_fclip_*` брейкпоинтом). @@ -154,6 +157,38 @@ fclip_y0`) намеренно грубый — габарит 40×78 на тай составом, а тем, что оригинал складывает всё в `foretable` и рисует одним `draw_table()`, у которого «посетить тайл» стоит копейки (см. п. 6). +### Что сделано и сколько дало + +1. **Ранний выход** из `wall_pattern_palace` по окну fore-клипа (узор целиком + в `x [xh*8, xh*8+32)`, `y [dmy−59, dby]`). +2. **Отсев каждого декаля** (`wp_blit`) по реальному габариту вместо + заведомо большего 64×64 в `pop_blit_b`. Размеры сняты из каталогов + атласов: `pal_wall.atl` — группы 3..5 = 8×7, 6..8 и 9..11 = 32×12, + 12..14 = 30×5, 15..17 = 32×3. +3. **То же для ПОДЗЕМЕЛЬЯ**: ранний выход `wall_pattern` (габарит там выше — + левая марка уходит на `dby+POP_YOFF−67`) плюс `wp_blit` на RNDBLOCK + (32×21), обоих разделителях (9×21) и обеих марках (`pop_wall.atl`: + 16/17 = 7×10, 14/15 = 14×5). + +Замер: уровень 4, комната 18, Кид сдвигается читом `]` по пикселю (skip +выключен, идёт полный путь), в fore-окне ШЕСТЬ тайлов, ТРИ из них — +дворцовая стена. + +| участок | тактов | +|---|---:| +| пролог `pop_char_fore` до первого `fore_tile` | 17 208 | +| обычный тайл | 4 000 – 4 600 | +| **тайл стены (было 60 000 – 74 000)** | **~12 700** | +| хвост + чистка бортов | 6 055 | +| **fore-проход целиком (было 171 693)** | **70 681** | + +Кадр целиком в этой сцене: работа **419 839**, период **3** растровых кадра +(было 517 609 и 4). + +Остаток в проходе — пролог 17 208, это уже пункт 1 ниже (футпринт из физики). +Отдельная находка: когда Кид стоит НА ШВЕ (окно `x −8..5`), пролог разбухает +до **134 730** — 86 % прохода; причина не разобрана, см. пункт 1. + --- ## 1. Футпринт персонажа — брать из физики, а не считать заново diff --git a/applications/PoP/roomtest/bug_closed.md b/applications/PoP/roomtest/bug_closed.md index 4aa5305..8fe213d 100644 --- a/applications/PoP/roomtest/bug_closed.md +++ b/applications/PoP/roomtest/bug_closed.md @@ -13,6 +13,51 @@ --- + +## BUG-CHEAT-FIGHT-1. Чит `+`/`−` в бою: Кид остаётся в режиме боя и теряет управление + +**Наблюдение (пользователь, 2026-08-07).** Если нажать `+`/`−` (ROOMNAV, +переход по комнатам) в момент, когда Кид вытащил меч для битвы, — в новой +комнате Кид не управляется: нажатия стрелок игнорируются. + +**Корень (прочитан по коду, сверен с seg005).** Чит ROOMNAV +(`roomtest.c:576`) телепортирует и сбрасывает позу и HP — +`enter_room()`, `kid_init(SEQ_STAND, …)`, `pop_kid_hp_reset()`, — но **не +трогает состояние боя**: `Kid.sword` остаётся `SWORD_2_DRAWN`. Диспетчер +`pop_control` (`pop_ctrl.c:578`) при вынутом мече уходит в +`control_with_sword`, а там единственный выход из режима боя — + +```c +if (Char.frame == FRAME_171_STAND_WITH_SWORD) { /* seg005:987 */ + Char.sword = SWORD_0_SHEATHED; + seqtbl_offset_char(SEQ_92_PUT_SWORD_AWAY); +} +``` + +Стража в новой комнате нет (`can_guard_see_kid` = 0), кадр после +`kid_init(SEQ_STAND)` — обычная стойка, а не 171, поэтому не срабатывает ни +`swordfight()`, ни ветка «убрать меч»: `control_with_sword` каждый кадр +не делает НИЧЕГО, и ввод не доходит до движения. Заклинивание вечное. + +Это баг **нашего чита**, а не порта: в оригинале телепорта между комнатами +нет, и в режим боя без соперника попасть нечем. + +**Как чинить.** В ветке ROOMNAV телепорт трактовать как выход из боя (то же +самое, что делает `pop_start_level`): `Kid.sword = SWORD_0_SHEATHED`, +`holding_sword = 0`, сбросить `offguard`/`guard_refrac` и состояние стража +(`pop_guard_reset()` вызывается по входу в комнату — проверить, что он +обнуляет `Opp`). Меч в инвентаре (`pop_have_sword`) при этом НЕ терять. +Второй кандидат на ту же болезнь — чит `K` (убить стража) в момент, когда +Кид в стойке с мечом, но не в кадре 171: там оригинал сам доводит до 171 +через `swordfight`, так что проверить сценарием, а не менять вслепую. + +**ЗАКРЫТ 2026-08-10.** Сделано ровно по плану выше: ветка ROOMNAV после +`kid_init`/`pop_kid_hp_reset` гасит состояние схватки — +`Kid.sword = SWORD_0_SHEATHED`, `holding_sword = 0`, `offguard = 0`, +`guard_refrac = 0`. Меч в инвентаре (`pop_have_sword`) не теряется, только +режим боя. + + ## BUG-LOOSE-BUTTON-1. Упавшая плита не нажимает кнопку — **ЗАКРЫТ 2026-08-10** diff --git a/applications/PoP/roomtest/bug_list.md b/applications/PoP/roomtest/bug_list.md index 6b552b8..e1e80b4 100644 --- a/applications/PoP/roomtest/bug_list.md +++ b/applications/PoP/roomtest/bug_list.md @@ -17,7 +17,7 @@ | ID | что | тип | статус | |----|-----|-----|--------| -| [BUG-CHEAT-FIGHT-1](#bug-cheat-fight-1) | `+`/`−` в бою с вынутым мечом → Кид теряет управление | Major (чит) | открыт | +| ~~BUG-CHEAT-FIGHT-1~~ | `+`/`−` в бою с вынутым мечом → Кид теряет управление | Major (чит) | **ЗАКРЫТ 2026-08-10** — [bug_closed.md](bug_closed.md#bug-cheat-fight-1) | | [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводимый: ни сценарием, ни попиксельной подгонкой X не поднимается | | [BUG-CHOMP-JUMP-1](#bug-chomp-jump-1) | прыжок с места вплотную к чомперу: кадр с отступом назад | **низкий** | маловоспроизводимый: на повторе не поднялся; на пререлиз | | [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт | @@ -52,44 +52,6 @@ Ниже — то, что осталось открытым после прогона 2026-08-07. - -## BUG-CHEAT-FIGHT-1. Чит `+`/`−` в бою: Кид остаётся в режиме боя и теряет управление - -**Наблюдение (пользователь, 2026-08-07).** Если нажать `+`/`−` (ROOMNAV, -переход по комнатам) в момент, когда Кид вытащил меч для битвы, — в новой -комнате Кид не управляется: нажатия стрелок игнорируются. - -**Корень (прочитан по коду, сверен с seg005).** Чит ROOMNAV -(`roomtest.c:576`) телепортирует и сбрасывает позу и HP — -`enter_room()`, `kid_init(SEQ_STAND, …)`, `pop_kid_hp_reset()`, — но **не -трогает состояние боя**: `Kid.sword` остаётся `SWORD_2_DRAWN`. Диспетчер -`pop_control` (`pop_ctrl.c:578`) при вынутом мече уходит в -`control_with_sword`, а там единственный выход из режима боя — - -```c -if (Char.frame == FRAME_171_STAND_WITH_SWORD) { /* seg005:987 */ - Char.sword = SWORD_0_SHEATHED; - seqtbl_offset_char(SEQ_92_PUT_SWORD_AWAY); -} -``` - -Стража в новой комнате нет (`can_guard_see_kid` = 0), кадр после -`kid_init(SEQ_STAND)` — обычная стойка, а не 171, поэтому не срабатывает ни -`swordfight()`, ни ветка «убрать меч»: `control_with_sword` каждый кадр -не делает НИЧЕГО, и ввод не доходит до движения. Заклинивание вечное. - -Это баг **нашего чита**, а не порта: в оригинале телепорта между комнатами -нет, и в режим боя без соперника попасть нечем. - -**Как чинить.** В ветке ROOMNAV телепорт трактовать как выход из боя (то же -самое, что делает `pop_start_level`): `Kid.sword = SWORD_0_SHEATHED`, -`holding_sword = 0`, сбросить `offguard`/`guard_refrac` и состояние стража -(`pop_guard_reset()` вызывается по входу в комнату — проверить, что он -обнуляет `Opp`). Меч в инвентаре (`pop_have_sword`) при этом НЕ терять. -Второй кандидат на ту же болезнь — чит `K` (убить стража) в момент, когда -Кид в стойке с мечом, но не в кадре 171: там оригинал сам доводит до 171 -через `swordfight`, так что проверить сценарием, а не менять вслепую. - ## BUG-SPIKE-1. Пики залипают выдвинутыми — НИЗКИЙ ПРИОРИТЕТ, МАЛОВОСПРОИЗВОДИМ @@ -310,7 +272,7 @@ Smoke-прогон уровня 3 (пользователь, 2026-08-05) дал - **ROOMNAV (`=`/`-`) — тоже наш чит**, которого в оригинале не было, как и `S`. Все они со временем съедутся в общий блок читов, разрешаемый в настройках; пока просто включены (`pop_cheats = 1` в `roomtest.c`). - Известный баг этого чита — [BUG-CHEAT-FIGHT-1](#bug-cheat-fight-1). + Известный баг этого чита закрыт — [BUG-CHEAT-FIGHT-1](bug_closed.md#bug-cheat-fight-1). - Комнаты **13, 18, 24 уровня 1 недостижимы** в обычной игре — это свойство данных уровня (разбор — «НЕ БАГИ» в [`bug_closed.md`](bug_closed.md)); приоритет багов в них низкий. Аналогично 23/24 на уровне 3. diff --git a/applications/PoP/roomtest/pop_bg.c b/applications/PoP/roomtest/pop_bg.c index 7e8f162..8b69cd9 100644 --- a/applications/PoP/roomtest/pop_bg.c +++ b/applications/PoP/roomtest/pop_bg.c @@ -78,6 +78,10 @@ static const uint8_t RPOS[4] = {52, 42, 31, 21}; * читает кэш раскладки кладки. */ #define prandom(maxv) pop_prandom(&pop_t_seed, (maxv)) +/* Отсев кусков кладки по окну fore-клипа — тела ниже, у wpp_fill. */ +static uint8_t wp_vis(int x, int w, int ytop, int ybot); +static void wp_blit(uint8_t id, int x, int ybot, uint8_t w, uint8_t h); + /* ---- Декали-марки стен (seg008.c:2041/2056) ------------------------ */ static void draw_left_mark(uint16_t dv, int arg2, int arg1, int row, int col) { @@ -87,7 +91,8 @@ static void draw_left_mark(uint16_t dv, int arg2, int arg1, int row, int col) int dby = 63 * row + 65; if (dv > 3) lv2 = arg1 + 6; else if (dv > 1) lv2 = arg2 + 6; - pop_wall_b(image_id, (xh + ((dv == 2 || dv == 3) ? 1 : 0)) * 8 + lv2, dby - LPOS[dv]); + wp_blit(image_id, (xh + ((dv == 2 || dv == 3) ? 1 : 0)) * 8 + lv2, + dby - LPOS[dv], 7, 10); /* pop_wall.atl: 16/17 = 7x10 */ } static void draw_right_mark(uint16_t dv, int arg1, int row, int col) @@ -96,7 +101,8 @@ static void draw_right_mark(uint16_t dv, int arg1, int row, int col) int a = (dv < 2) ? 24 : arg1 - 3; int xh = POP_COL_XH[col]; int dby = 63 * row + 65; - pop_wall_b(image_id, (xh + ((dv > 1) ? 1 : 0)) * 8 + a, dby - RPOS[dv]); + wp_blit(image_id, (xh + ((dv > 1) ? 1 : 0)) * 8 + a, + dby - RPOS[dv], 14, 5); /* pop_wall.atl: 14/15 = 14x5 */ } /* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- */ @@ -290,6 +296,32 @@ static void wall_rnd_palace(int row, int col) } } +/* Прямоугольник [x, x+w) x [ytop, ybot] (ЭКРАННЫЕ) задевает окно fore-клипа? + * Клипа нет — задевает всегда. */ +static uint8_t wp_vis(int x, int w, int ytop, int ybot) +{ + if (!pop_t_fclip_on) return 1; + return (uint8_t)(x < pop_t_fclip_x1 && x + w > pop_t_fclip_x0 && + ytop < pop_t_fclip_y1 && ybot >= pop_t_fclip_y0); +} + +/* Декаль кладки: ybot — БЕЗ POP_YOFF (как ждёт pop_wall_b), w/h — реальный + * габарит куска. Размеры взяты из каталогов атласов (pal_wall.atl / + * pop_wall.atl) и заданы верхней границей по группе, поэтому промахнуться в + * меньшую сторону нельзя: хуже отсева не будет, лишнего не сотрём. + * + * Зачем: pop_blit_b тоже отсеивает до маппинга страницы, но по ЗАВЕДОМО + * большему габариту 64x64 — куски кладки высотой 3..12 px он пропускает и + * платит полный atlas_image + gfx_w0_map + чтение w/h (~9 760 тактов на + * блит), чтобы там обнаружить, что рисовать нечего. Пять таких блитов на + * тайл — 48 500 тактов в никуда (docs/perf_backlog.md, пункт 0 шаг 2). */ +static void wp_blit(uint8_t id, int x, int ybot, uint8_t w, uint8_t h) +{ + int yb = ybot + POP_YOFF; + if (!wp_vis(x, (int)w, yb - (int)h + 1, yb)) return; + pop_wall_b(id, x, ybot); +} + /* Заливка куска стены. x/ytop — уже ЭКРАННЫЕ (с POP_YOFF). В fore-проходе * режем по окну клипа: без этого заливка легла бы поверх областей, которые * в этом кадре никто не восстанавливает. */ @@ -319,6 +351,23 @@ static void wall_pattern_palace(int row, int col, int which_part) int dbys = 63 * row + 65; /* база БЕЗ POP_YOFF — для блитов */ int dmys = dbys - 3; int dby = dbys + POP_YOFF, dmy = dmys + POP_YOFF; /* экранные — для заливок */ + /* РАННИЙ ВЫХОД по окну fore-клипа. Весь узор лежит в прямоугольнике + * x [xh*8, xh*8+32), y [dmy-59, dby]: сверху — первая заливка, снизу — + * нижний декаль, чей низ ровно на dby. + * + * Без этой проверки тайл стены под ногами Кида стоил ~60 000 тактов НА + * НОЛЬ ПИКСЕЛЕЙ: предфильтр fore_tile грубый (габарит 40x78), wpp_fill + * честно режет по окну, но платит ~3 100 тактов за арифметику клипа, а + * pop_wall_b об окне не знает вовсе и идёт в atlas_image + gfx_w0_map, + * чтобы там обнаружить, что рисовать нечего. Два таких тайла (ряд 2 под + * Кидом попадает в fore-окно всегда) = 139 863 такта за кадр, 32 % + * растрового кадра. См. docs/perf_backlog.md, пункт 0. */ + if (pop_t_fclip_on) { + int x0 = xh * 8; + if (x0 >= pop_t_fclip_x1 || x0 + 32 <= pop_t_fclip_x0 || + dmy - 59 >= pop_t_fclip_y1 || dby + 1 <= pop_t_fclip_y0) + return; + } /* Цвета лежат по рядам 0..2; полоса у потолка (row −1) — это ряд 2 * комнаты сверху, оригинал и сид для неё берёт по ряду 2. */ const uint8_t *cl = pop_pal_wall_col + 44 * (row < 0 ? 2 : row) + col; @@ -329,13 +378,16 @@ static void wall_pattern_palace(int row, int col, int which_part) wpp_fill((xh + 2) * 8, dmy - 39, 16, 21, cl[12]); wpp_fill(xh * 8, dmy - 18, 8, 19, cl[22]); wpp_fill((xh + 1) * 8, dmy - 18, 24, 19, cl[23]); - pop_wall_b((uint8_t)(wpp_d[0] + 3), (xh + 3) * 8, dmys - 53); - pop_wall_b((uint8_t)(wpp_d[1] + 6), xh * 8, dmys - 34); - pop_wall_b((uint8_t)(wpp_d[2] + 9), xh * 8, dmys - 13); - pop_wall_b((uint8_t)(wpp_d[3] + 12), xh * 8, dmys); + /* Габариты групп из pal_wall.atl: 3..5 = 8x7, 6..8 = 32x12, + * 9..11 = 32x12 (11-й 28x11), 12..14 = 30x5 (14-й 30x4), + * 15..17 = 32x3. Берём максимум по группе. */ + wp_blit((uint8_t)(wpp_d[0] + 3), (xh + 3) * 8, dmys - 53, 8, 7); + wp_blit((uint8_t)(wpp_d[1] + 6), xh * 8, dmys - 34, 32, 12); + wp_blit((uint8_t)(wpp_d[2] + 9), xh * 8, dmys - 13, 32, 12); + wp_blit((uint8_t)(wpp_d[3] + 12), xh * 8, dmys, 30, 5); } wpp_fill(xh * 8, dby - 2, 32, 3, cl[33]); - pop_wall_b((uint8_t)((which_part ? wpp_d[4] : wpp_d[0]) + 15), xh * 8, dbys); + wp_blit((uint8_t)((which_part ? wpp_d[4] : wpp_d[0]) + 15), xh * 8, dbys, 32, 3); } static void wall_pattern(int row, int col, int which_part) @@ -344,18 +396,29 @@ static void wall_pattern(int row, int col, int which_part) int dby = 63 * row + 65; uint8_t parts; if (pop_palace) { wall_pattern_palace(row, col, which_part); return; } + /* Ранний выход по окну fore-клипа — то же, что у паласной ветки, но свой + * габарит. Самый верхний кусок здесь — ЛЕВАЯ марка: LPOS[4] = 58 плюс её + * высота 10, то есть dby+POP_YOFF−67; самый нижний — нижний разделитель, + * низ ровно на dby+POP_YOFF. По x правая марка уходит до xh*8+46 + * (a = 24 при dv<2, ширина 14), берём 48. */ + { + int x0 = xh * 8, dbs = dby + POP_YOFF; + if (!wp_vis(x0, 48, dbs - 67, dbs)) return; + } parts = WP_PARTS[pop_wall_modifier(row, col)]; wall_rnd(row, col); if (which_part) { + /* Габариты из pop_wall.atl: 13 = 32x21, 11/12 = 9x21. */ if ((parts & WP_RND) && wr_rndblk) - pop_wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42); + wp_blit(RES_WALL_RNDBLOCK, xh * 8, dby - 42, 32, 21); if (parts & WP_MID) - pop_wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), - (xh + 1) * 8 + wr_mid_off, dby - 21); + wp_blit((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), + (xh + 1) * 8 + wr_mid_off, dby - 21, 9, 21); } if (parts & WP_BOT) - pop_wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_bot_div), xh * 8 + wr_bot_off, dby); + wp_blit((uint8_t)(RES_WALL_DIVIDER1 + wr_bot_div), + xh * 8 + wr_bot_off, dby, 9, 21); if (which_part) { if ((parts & WP_RIGHT) && wr_right) draw_right_mark(wr_rightv, wr_mid_off, row, col); diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index a407b9c..d7afba0 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -663,6 +663,21 @@ int main(void) kid_init(SEQ_STAND, (int8_t)(pos % 10), (int8_t)(pos / 10), 0); pop_kid_hp_reset(); dead_frames = 0; + /* ВЫХОД ИЗ БОЯ (BUG-CHEAT-FIGHT-1). Телепорт с вынутым мечом + * заклинивал управление намертво: pop_control при + * Kid.sword == SWORD_2_DRAWN уходит в control_with_sword, а + * там единственный выход — ветка «убрать меч» на кадре + * FRAME_171_STAND_WITH_SWORD. После kid_init(SEQ_STAND) кадр + * обычный, стража в новой комнате нет, swordfight не + * запускается — и диспетчер каждый кадр не делает НИЧЕГО. + * Это болезнь ЧИТА: в оригинале телепорта между комнатами + * нет, попасть в режим боя без соперника нечем. Меч в + * инвентаре (pop_have_sword) НЕ теряем — только состояние + * схватки. */ + Kid.sword = SWORD_0_SHEATHED; + holding_sword = 0; + offguard = 0; + guard_refrac = 0; } nav_prev = nav; }