From 3f643532c93cc659acad1d7ab5a085b68681f57d Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Tue, 18 Aug 2026 17:07:01 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A2=D1=80=D0=B0=D1=81=D1=81=D0=B0=20kidobj?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20?= =?UTF-8?q?=D1=81=20SDLPoP=20+=20=D1=80=D0=B0=D0=B7=D0=B1=D0=BE=D1=80=20?= =?UTF-8?q?=D0=B1=D0=BE=D1=8F=20=D1=83=20=D1=88=D0=B2=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Инструмент: pop_dbg_kidobj печатает то же, что вывод, добавленный пользователем в SDLPoP add_kid_to_objtable — tilepos/frame/act/col/cols/rows (+ наша комната). Считает по set_char_collision и set_objtile_at_char. Выключен по умолчанию (DBG_KIDOBJ 0), включается одним define; вывод забирает брейкпоинт MAME на резидентном pop_dbg_trap. Сверка подтвердила фикс a498255: окно перехода совпало кадр в кадр, за бой на уровне 1 комната сменилась один раз, на уровне 8 у шва 24/18 — четыре раза на 387 кадров боя, и все четыре на РАЗРЕШЁННЫХ кадрах (170/164/170/165). Смен на запрещённых кадрах во всей трассе нет. Разбор целиком — BUGS_CLOSED.md#seam-fight-flicker, включая невыясненное расхождение поля cols. Co-Authored-By: Claude Opus 5 --- applications/PoP/roomtest/BUGS_CLOSED.md | 86 +++++++++++++++++++++++ applications/PoP/roomtest/pop_state.c | 8 +++ applications/PoP/roomtest/pop_state.h | 6 ++ applications/PoP/roomtest/roomtest.c | 14 ++++ applications/PoP/roomtest/roomtest_cold.c | 43 ++++++++++++ 5 files changed, 157 insertions(+) diff --git a/applications/PoP/roomtest/BUGS_CLOSED.md b/applications/PoP/roomtest/BUGS_CLOSED.md index e35a405..7beec8f 100644 --- a/applications/PoP/roomtest/BUGS_CLOSED.md +++ b/applications/PoP/roomtest/BUGS_CLOSED.md @@ -3152,3 +3152,89 @@ else died_on_button(...); /* мёртв — кнопка С Вероятнее всего это кадр уже НАЧАВШЕГОСЯ падения (при падении X сносит), но доказательства нет. Если `(0,-1)` появится на исправленном билде — это отдельный баг. + +--- + + +## SEAM-FIGHT-FLICKER. Бой у шва: комната перерисовывалась то одна, то другая + +**Наблюдение (пользователь, 2026-08-18).** Уровень 8, шов комнат 24/18: во +время схватки со стражем экран постоянно переключался между комнатами — +«драться очень неудобно». Отдельно пользователь заметил, что уходит Кид, +даже когда просто держит «вверх» (защита), и предположил верную причину: +стойка защиты не одна, Кид периодически проходит через обычную. + +**Корень.** `leave_room` (seg002:0490) запрещает смену комнаты не только на +развороте, подъёме-с-зацепа (135..149) и вставании из приседа (110..119), но +и на ВСЕЙ боевой анимации — кадры **150..162** и **166..168**. У нас были +только первые три условия. + +Разрешены при этом 163..165, 169 (`begin_block`) и 170/171 (`stand with +sword`) — то есть уйти можно, но только на «легальном» кадре. Цикл защиты +(seqtbl: `readyblock` = 169 -> `blocking` = 150, петля) как раз проходит +через 169, а между атаками Кид возвращается в 158/170 — отсюда и ощущение +«стоять в защите нельзя». + +Побочный эффект известен и в оригинале эксплуатируется — **Trick 35, +«retreat without leaving the room»**: определённым ритмом «назад» кадр 170 не +наступает никогда, и Кид пятится из комнаты, не переключая её. В SDLPoP это +чинит `FIX_RETREAT_WITHOUT_LEAVING_ROOM`, по умолчанию ВЫКЛЮЧЕННЫЙ, — мы +портируем оригинал. + +**Сторона стража проверена отдельно, расхождений нет.** `play_guard_frame` +(seg000:0F48) ухода из комнаты не содержит вовсе (ни `leave_room`, ни +`check_leave_below`), физика загейчена окном `Char.x ∈ [44, 211)`. Комнату +страж меняет только через `follow_guard` (у нас `pop_guard_follow`) и +`check_guard_fallout` (у нас `pop_guard_fallout`) — оба портированы. + +**Проверка: покадровая сверка с SDLPoP.** Пользователь добавил в SDLPoP +вывод в конце `add_kid_to_objtable` (seg008:1679); мы навесили такой же +(`pop_dbg_kidobj`, см. ниже). Окно перехода совпало КАДР В КАДР: + +| | SDLPoP | наша | +|---|---|---| +| | `tilepos=10 frame=161 col=-1` | `tilepos=10 frame=161 col=-1` | +| | `tilepos=10 frame=160 col=-1` | `tilepos=10 frame=160 col=-1` | +| | `tilepos=10 frame=157 col=-2` | `tilepos=10 frame=157 col=-2` | +| | `tilepos=10 frame=158 col=-2` | `tilepos=10 frame=158 col=-2` | +| **смена** | `tilepos=18 frame=169 col=9` | `tilepos=18 frame=169 col=9` | + +Прогон уровня 1 комнаты 3 (1779 кадров): за весь бой комната сменилась +ОДИН раз; 34 боевых кадра прошли за краем комнаты (`col<0`), самый длинный +непрерывный прогон — 17 кадров, и комната при этом не переключалась — ровно +как в трассе SDLPoP. + +Прогон уровня 8, шов 24/18 (1433 кадра, 360 кадров с мечом): внутри боя +**4 смены комнаты на 387 кадров** — то есть примерно раз в 6 секунд, и это +настоящее выдавливание, а не мерцание. Все четыре пришлись на РАЗРЕШЁННЫЕ +кадры (170, 164, 170, 165); ни одной смены на запрещённом кадре во всей +трассе нет. Кид подолгу держался на `col=-1` и `col=10`, комната не +переключалась. + +**Инструмент (оставлен в дереве, выключен).** `pop_dbg_kidobj` +(`roomtest_cold.c`) + резидентный буфер `pop_dbg_obj` (`pop_state.c`), +включается `#define DBG_KIDOBJ 1` в `roomtest.c`. Считает то же, что +`set_char_collision` + `set_objtile_at_char`. Вывод забирает брейкпоинт +MAME на резидентном `pop_dbg_trap`; сборщик — `collect_kidobj.py`. + +Грабли, стоившие времени: + +- `#define DBG_KIDOBJ` сперва положили внутрь `#ifndef PROF_BORDER`, а + `PROF_BORDER` задаётся ключом сборки — блок не выполнялся, трасса молча + не включалась. Нужен свой `#ifndef`. +- Знаковые значения через выражения отладчика MAME получить не вышло: `b@` + связывается слабее `+`, и `b@ADDR + 128` читает ДРУГОЙ адрес; скобки не + помогают. Зонд печатает сырые байты, перевод — в сборщике. +- Кольцо `clog` не потребляющее, одинаковые строки от повторов не отличить — + в строку добавлен `totalcycles`, по нему и склеиваются чанки. +- Замерено: один зонд с коротким `printf` — **98 % полной скорости** + эмуляции, играть можно (в отличие от четырёх зондов профилировщика). + +**Осталось невыясненным.** Поле `cols` (`char_col_left..char_col_right`) у +нас систематически на единицу меньше, чем в SDLPoP, и дважды дало `0..-1`, +чего в оригинале не бывает. Все поля, которые считает ДВИЖОК (`tilepos`, +`frame`, `act`, `col`, точка перехода), совпали полностью — расхождение +только в величине, которую выводит сам отладочный помощник из ширины кадра +`pop_cd[].fpw`. Подозрение на паддинг кадров в атласе +(`png_strip_padding_tradeoff`) либо на мой пересчёт переднего края мимо +боевого `char_x_forward_edge`. Проверять отдельно. diff --git a/applications/PoP/roomtest/pop_state.c b/applications/PoP/roomtest/pop_state.c index 7a445c3..27f8bb1 100644 --- a/applications/PoP/roomtest/pop_state.c +++ b/applications/PoP/roomtest/pop_state.c @@ -79,6 +79,14 @@ uint8_t pop_dbg_hits; void pop_dbg_trap(void) { pop_dbg_hits++; } +/* ВРЕМЕННО: трасса kidobj — порт вывода, который пользователь добавил в + * SDLPoP add_kid_to_objtable (seg008:1679), чтобы сверить поведение у шва + * кадр в кадр. Буфер РЕЗИДЕНТНЫЙ, потому что читает его брейкпоинт MAME: + * printf из действия точки берёт байты по фиксированному адресу. Поля — + * ровно те же и в том же порядке, плюс наша комната девятым (у оригинала + * смена комнаты видна по скачку tilepos, а нам удобнее явно). */ +int8_t pop_dbg_obj[9]; + /* Зеркало уровня 4 — см. pop_state.h. */ int8_t pop_jumped_mirror; diff --git a/applications/PoP/roomtest/pop_state.h b/applications/PoP/roomtest/pop_state.h index d10b78f..2cd512e 100644 --- a/applications/PoP/roomtest/pop_state.h +++ b/applications/PoP/roomtest/pop_state.h @@ -96,3 +96,9 @@ 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); void pop_dbg_wh(uint16_t wh); + +/* ВРЕМЕННО: трасса kidobj (см. pop_state.c). Порядок байт: + * 0 obj_tilepos, 1 frame, 2 action, 3 curr_col, 4 char_col_left, + * 5 char_col_right, 6 char_top_row, 7 char_bottom_row, 8 комната. */ +extern int8_t pop_dbg_obj[9]; +void pop_dbg_kidobj(void) __banked; diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 0d0b17f..721aa2a 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -46,6 +46,14 @@ __sfr __at (0xFE) io_border; #ifndef PROF_BORDER #define PROF_BORDER 1 #endif +/* ВРЕМЕННО: покадровая трасса kidobj для сверки с SDLPoP (см. + * pop_dbg_kidobj в roomtest_cold.c). 0 — выключить. + * СВОЙ #ifndef, а не общий с PROF_BORDER: тот задаётся ключом сборки + * (-DPROF_BORDER=1), и всё, что лежало внутри его блока, не определялось + * вовсе — трасса молча не включалась. */ +#ifndef DBG_KIDOBJ +#define DBG_KIDOBJ 0 +#endif #if PROF_BORDER #define PROF(n) io_border = (n) #else @@ -640,6 +648,12 @@ int main(void) 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); /* спрайт + брызги + клинок */ +#if DBG_KIDOBJ + /* ВРЕМЕННО: трасса для сверки с SDLPoP кадр в кадр. Место — то + * же, что у оригинала (конец add_kid_to_objtable), и ВНЕ skip: + * пропуск перерисовки не должен рвать трассу. */ + pop_dbg_kidobj(); +#endif if (g_after && !(skip & 2)) { pop_char_draw(POP_CH_OPP); pop_char_fore(POP_CH_OPP); } /* Брызги урона рисует сама отрисовка персонажа (порядок objtable: * сразу после него и до клинка), а флаги снимаем здесь: соперника diff --git a/applications/PoP/roomtest/roomtest_cold.c b/applications/PoP/roomtest/roomtest_cold.c index 6d93a02..ecac411 100644 --- a/applications/PoP/roomtest/roomtest_cold.c +++ b/applications/PoP/roomtest/roomtest_cold.c @@ -499,6 +499,49 @@ static void objtile_at_char(int8_t *row, int8_t *col, *row = r; *col = c; } +/* ВРЕМЕННО: трасса kidobj — порт отладочного вывода, добавленного + * пользователем в SDLPoP add_kid_to_objtable (seg008:1679): + * + * DBG kidobj tilepos=%d frame=%d act=%d col=%d cols=%d..%d rows=%d..%d + * + * Считаем ровно то же, что set_char_collision (seg006:0723) + + * set_objtile_at_char (seg006:13F3): габарит НЕутоньшённый (THIN правит + * только *_coll, а колонки берутся по «толстой» границе), колонки + * клампятся в 0..9, нижний ряд −1 читается как 3. Звать ПОСЛЕ отрисовки + * Кида: ширину/высоту кадра (fpw/fph) заполняет она. + * + * Вывод забирает брейкпоинт MAME на резидентном pop_dbg_trap — сама + * функция банковая, её адрес в W3 неоднозначен. */ +void pop_dbg_kidobj(void) __banked +{ + int8_t dx = kid_cur_dx(), dy = kid_cur_dy(); + uint8_t fl = kid_cur_flags(); + uint16_t w = pop_cd[POP_CH_KID].fpw; + uint16_t h = pop_cd[POP_CH_KID].fph; + int xl = char_x_left_of(Kid.x, Kid.direction, dx, fl, w); + int xr = xl + (int)((w + 1) >> 1); /* + char_width_half */ + int oy = (int)Kid.y + dy; + int ty = oy - (int)h + 1; + int8_t r, c, tr, br, cl, cr; + objtile_at_char(&r, &c, &Kid, dx, dy, fl, w); + br = pop_y_to_row((int16_t)oy); + if (br == -1) br = 3; + if (ty >= 192) ty = 0; + tr = pop_y_to_row((int16_t)ty); + cl = tile_div_mod(xl); if (cl < 0) cl = 0; + cr = tile_div_mod(xr); if (cr > 9) cr = 9; + pop_dbg_obj[0] = (int8_t)(r * 10 + c); + pop_dbg_obj[1] = (int8_t)Kid.frame; + pop_dbg_obj[2] = (int8_t)Kid.action; + pop_dbg_obj[3] = Kid.curr_col; + pop_dbg_obj[4] = cl; + pop_dbg_obj[5] = cr; + pop_dbg_obj[6] = tr; + pop_dbg_obj[7] = br; + pop_dbg_obj[8] = (int8_t)cur_room; + pop_dbg_trap(); +} + /* 1 = страж рисуется ПОСЛЕ Кида (поверх). __banked: зовётся раз в кадр, * то есть один трамплин — а 500 Б кода в резиденте дороже. */ uint8_t guard_over_kid(void) __banked