diff --git a/applications/PoP/roomtest/bug_closed.md b/applications/PoP/roomtest/bug_closed.md index d131b35..cd44d6e 100644 --- a/applications/PoP/roomtest/bug_closed.md +++ b/applications/PoP/roomtest/bug_closed.md @@ -13,6 +13,106 @@ --- + +## BUG-GUARD-IX-1. «Зависание» при бое со стражем — затёртый IX главного цикла — **ЗАКРЫТ 2026-08-10** + +**Симптом (пользователь, уровень 4).** Дважды подряд при ручном входе в +комнату 18 со стражем игра «повисала»: картинка стоит, управление не +отвечает. Нажатие «2» ненадолго возвращало движение, потом всё повторялось. + +**Диагноз — НЕ зависание.** Главный цикл всё это время крутился (маркер +верха цикла тикал раз в кадр). Стоял отладочный стоп-кадр: локаль `frozen` +в `main` сама собой становилась ненулевой. + +**Корень — однобайтовый выход за границу локального массива.** У `main` все +локали лежат по `IX-1..IX-19`, и IX затирался: вместо `0xBFFA` в нём +оказывался `0xBF00`. Признак железный — в теле цикла обязано выполняться +`SP == IX-19`, а по факту было `SP=0xBFE7, IX=0xBF00`. Дальше `main` читал +`frozen`, `dbuf`, `front/back`, `dead_frames` и edge-флаги читов из живого +стекового мусора. + +Виновник — `check_chomped_guard` (pop_map.c): + +```c +uint8_t flags[COLL_N]; /* 14 байт в кадре функции */ +calc_coll_window(); /* окно СТРАЖА -> win_lo/win_hi */ +get_row_collision_data(Char.curr_row, flags); +``` + +`coll_row()` пишет по `flags + scan_off`, а число байт берёт из +`win_lo/win_hi`. `scan_off`/`scan_left0` выставляет `coll_scan_prepare()`, +которую звал ТОЛЬКО `check_collisions` — путь Кида. То есть ряд стража +писался **по смещению Кида, длиной стража**. Диапазон записи +`[kid_win_lo+2 … kid_win_lo+2 + ширина_окна_стража − 1]`; при обычной ширине +4 он уезжает за `flags[13]`, когда `kid_win_lo > 8` — то есть **когда Кид +стоит у правого края комнаты** (взаимное расположение Кида и стража ни при +чём, стража даёт только длину). Сразу за массивом лежит сохранённый IX; +затёртый младший байт и превращал `0xBFFA` в `0xBF00`. + +Перелёт в отрицательные индексы был невозможен: `scan_off = kid_win_lo + 2`, +а `kid_win_lo >= COLL_C0 = −2` из-за клампа в `calc_coll_window`. + +**Почему не ловилось раньше.** На уровне 1 (комната 3) бой идёт левее +середины — запись оставалась внутри массива. Ничего не портилось, но флаги +стража всё равно читались по чужим смещениям и с иксами Кида: тихо неверные +данные, без последствий (чомперов там нет). + +**Фикс.** `coll_scan_prepare()` в начале `get_row_collision_data()` — теперь +запись всегда ложится в `[win_lo+2 … win_hi+2]`, а `calc_coll_window` клампит +окно в `[−2 … 11]`, то есть индексы гарантированно `0…13`. Заодно чинится +сам расчёт: `scan_left0` задаёт x колонок, и чомпер-коллизия стража считалась +по координатам Кида. + +**Как ловилось (приём на будущее).** Детекторы `ix != 0xBFFA` на границах +фаз `PROF()` с починкой IX в верху цикла: игра остаётся живой, а первый +сработавший детектор называет фазу. Дальше — инструкционная трасса MAME +(`trace file,0`), включаемая на подозрительном вызове и выключаемая сразу +после, со стопом только на плохом кадре: в файле остаётся ровно тот вызов, +где IX испортился. В хвосте трассы видно `ld sp,ix / pop ix`, достающий +`BF00` вместо `BFFA`. См. memory `z80_profiling_method`. + +**Проверка.** Бой со стражем в комнате 18 уровня 4 (пользователь) — +детекторы IX висели без починки и не сработали ни разу; `make -C tests-host` +— все 5 наборов. + +--- + + +## BUG-GATE-SEAM-ROW1. Решётка в шве не анимируется, если ворота НЕ в ряду 0 — **ЗАКРЫТ 2026-08-10** + +**Симптом (пользователь, уровень 4, стартовая комната).** Плита нажимается, +ворота физически открываются (Кид проходит), но визуально с решёткой ничего +не происходит. Отдельно: чёрный треугольник над воротами (верх решётки под +ковром) статичен, тогда как в оригинале он ездит вместе с барами. + +**Корень.** Зонд на `pop_add_trob` показал `room=8 tp=19 type=1`: плита +комнаты 1 открывает ворота **комнаты 8 в (1,9)** — соседней комнаты, видимые +через ЛЕВЫЙ шов (бары ворот в col9 рисует col0 соседа справа). Наш +change-driven редрой шва смотрел только один байт `m[9]` (ряд 0), а +`pop_room_redraw_seam_left()` перерисовывал жёстко `draw_tile(0, 0)`. +Изменение приходило в `m[19]` — сигнатура его не видела. + +На уровне 1 та же связка комнат 6/8 работала только потому, что решётка +соседа стояла в **(0,9)**. Тайлсет ни при чём: в подземелье просто не +попадалось ворот в ряду 1 у шва (и треугольника не было — над решётками +всегда потолок). + +**Фикс.** +1. `seam_sig` — массив на три ряда, сравниваются все три; накопленная маска + изменившихся рядов живёт в `seam_rows` и переживает оба кадра + дабл-буфера. +2. `pop_room_redraw_seam_left(uint8_t rows)` берёт маску и перерисовывает + только помеченные ряды — не все три, чтобы не платить каждый кадр + анимации. +3. В маску добавляется ряд ВЫШЕ (`changed | (changed >> 1)`): верх решётки + (`draw_tile_anim_topright`, seg008:0568 — маска 68 + кадр + `DOOR_FRAM_TOP`) рисует не сам тайл ворот, а тайл над-справа от него, у + нас `(r−1, 0)`. Без этого бары ездили, а треугольник над ними стоял. + +**Проверка.** Уровень 4, стартовая комната: нажатие плиты — решётка шва +поднимается/опускается вместе с верхом (пользователь). + + ## BUG-GUARD-COLOR-1. Страж и его полоса HP — всегда одного цвета — **ЗАКРЫТ 2026-08-07** diff --git a/applications/PoP/roomtest/bug_list.md b/applications/PoP/roomtest/bug_list.md index bd81356..2478a1e 100644 --- a/applications/PoP/roomtest/bug_list.md +++ b/applications/PoP/roomtest/bug_list.md @@ -23,6 +23,8 @@ | [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт | | ~~T-2~~ | Кид перерисовывается в покое | оптимизация | **ЗАКРЫТ 2026-08-08** — [DRAW-COST шаг 1](TASKS_OPEN.md#draw-cost) | | ~~BUG-GATE-PASS-1~~ | проход сквозь закрытую решётку шва | Major | **ЗАКРЫТ 2026-08-09** — [bug_closed.md](bug_closed.md#bug-gate-pass-1), смоук уровня 1 пройден | +| ~~BUG-GUARD-IX-1~~ | «зависание» в бою со стражем: затёрт IX главного цикла | Blocker | **ЗАКРЫТ 2026-08-10** — [bug_closed.md](bug_closed.md#bug-guard-ix-1) | +| ~~BUG-GATE-SEAM-ROW1~~ | решётка в шве не анимируется, если ворота не в ряду 0 | Major | **ЗАКРЫТ 2026-08-10** — [bug_closed.md](bug_closed.md#bug-gate-seam-row1) | --- diff --git a/applications/PoP/roomtest/pop_bg.h b/applications/PoP/roomtest/pop_bg.h index 8d60ca7..c928a63 100644 --- a/applications/PoP/roomtest/pop_bg.h +++ b/applications/PoP/roomtest/pop_bg.h @@ -125,7 +125,7 @@ void pop_potion_draw(int row, int col, uint8_t modif) __banked; /* Ворота на ЛЕВОМ шве (col0) — перерисовать по живому modif соседа слева. * g_lbg (lcol_bg) должен быть обновлён вызывающим. Звать из основного цикла * на back-странице, когда сосед слева анимируется. См. pop_bg.c. */ -void pop_room_redraw_seam_left(void) __banked; +void pop_room_redraw_seam_left(uint8_t rows) __banked; /* rows = маска рядов 0..2 */ /* Перерисовать ПОЛ тайла (row,col) + правую грань в соседе на back-странице * (wipe по площади пола, БЕЗ стены ряда ниже). Для смены тайла на месте: diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index 5d59b98..ea81a85 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -1636,8 +1636,23 @@ static void coll_row_bases(int8_t row) } } +/* Одиночный ряд во ВНЕШНИЙ массив (единственный вызывающий — + * check_chomped_guard). + * + * coll_scan_prepare() тут ОБЯЗАТЕЛЕН, и это не перестраховка: coll_row + * пишет по `flags + scan_off`, а число байт берёт из win_lo/win_hi. Без + * prepare scan_off/scan_left0 остаются от ПРОШЛОГО окна (его считает + * check_collisions для Кида), а окно тут уже своё, стражево. Когда + * стражево окно шире или правее кидова, запись уезжает за границу + * flags[COLL_N] — а сразу за ней в кадре check_chomped_guard лежит + * СОХРАНЁННЫЙ IX. Затёртый младший байт превращал кадровый указатель + * main (0xBFFA) в 0xBF00, и дальше main читал свои локали из чужого + * стекового мусора: игра «зависала», потому что отладочный `frozen` + * становился ненулевым (BUG-GUARD-IX-1). Заодно чинится и сам расчёт: + * scan_left0 задаёт x колонок, и страж считался по координатам Кида. */ static void get_row_collision_data(int8_t row, uint8_t *flags) { + coll_scan_prepare(); coll_row_bases(row); coll_row(flags); } diff --git a/applications/PoP/roomtest/pop_room.c b/applications/PoP/roomtest/pop_room.c index 29b4006..e57421c 100644 --- a/applications/PoP/roomtest/pop_room.c +++ b/applications/PoP/roomtest/pop_room.c @@ -516,19 +516,28 @@ void pop_chomp_redraw(int row, int col) __banked * room_modif соседа перед вызовом). Bar-в-чёрный + redraw (как loose bake): * heal вернул бы ЗАПЕЧЁННОЕ состояние ворот → при открытии нижние бары * оставались бы; чёрная плита стирает старые бары в VRAM+ОЗУ-копию. */ -void pop_room_redraw_seam_left(void) __banked +void pop_room_redraw_seam_left(uint8_t rows) __banked { - pop_cd_touch(0, POP_YOFF + 3, 32, 62); /* bar мимо pop_blit_b (DRAW-COST) */ - gfx_set_bank(GFX_BANK_NORMAL); - setfillstyle(SOLID_FILL, BLACK); - /* Полосу кладки У ПОТОЛКА (ряд −1) НЕ трогаем: её низ на room-space y=2, - * а бары ворот начинаются с y=3 (gate_top_y = dby−62). Раньше bar шёл от - * POP_YOFF и стирал потолок, из-за чего приходилось перерисовывать ещё и - * draw_tile(-1,0) — вдвое дороже на КАЖДОМ кадре анимации решётки. */ - bar(0, POP_YOFF + 3, 31, POP_YOFF + 64); /* col0, ряд 0 (бары ворот) */ - gfx_set_bank(GFX_BANK_TRANSPARENT); - draw_tile(0, 0); /* фон col0 + бары ворот (живой modif) */ - gfx_set_bank(GFX_BANK_NORMAL); + int8_t r; + /* rows — битовая маска рядов 0..2: перерисовываем ТОЛЬКО те, где openness + * реально изменился. Раньше ряд был жёстко нулевым, потому что на уровне 1 + * решётка соседа стояла в (0,9); на уровне 4 она в (1,9), и открытые ворота + * оставались нарисованными закрытыми (BUG-GATE-SEAM-ROW1). */ + for (r = 0; r < 3; r++) { + int yt = POP_YOFF + 63 * r; + if (!(rows & (1 << r))) continue; + pop_cd_touch(0, yt + 3, 32, 62); /* bar мимо pop_blit_b (DRAW-COST) */ + gfx_set_bank(GFX_BANK_NORMAL); + setfillstyle(SOLID_FILL, BLACK); + /* Полосу кладки НАД рядом НЕ трогаем: её низ на room-space 63*r+2, а + * бары ворот начинаются с 63*r+3 (gate_top_y = dby−62). Раньше bar шёл + * от POP_YOFF и стирал потолок, из-за чего приходилось перерисовывать + * ещё и draw_tile(-1,0) — вдвое дороже на КАЖДОМ кадре анимации. */ + bar(0, yt + 3, 31, yt + 64); /* col0 этого ряда (бары ворот) */ + gfx_set_bank(GFX_BANK_TRANSPARENT); + draw_tile(r, 0); /* фон col0 + бары ворот (живой modif) */ + gfx_set_bank(GFX_BANK_NORMAL); + } } /* ---- Плита-потолок (loose ряда 2 комнаты СВЕРХУ) ------------------- * diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 9fe8172..591a67b 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -228,8 +228,16 @@ static uint8_t guard_over_kid(void) * draw_tile(0,0) в pop_room_redraw_seam_left зовём ТОЛЬКО когда openness гейта * реально изменился. Пока Кид держит кнопку и гейт открыт — modifier стабилен * → 0 редроёв (раньше шов рисовался безусловно КАЖДЫЙ кадр, пока сосед активен - * — тот же класс, что был у кнопки). seam_redraw=2 = обе страницы дабл-буфера. */ -static uint8_t seam_sig; /* последняя нарисованная openness гейта в шве */ + * — тот же класс, что был у кнопки). seam_redraw=2 = обе страницы дабл-буфера. + * + * Сигнатура — ВСЕ ТРИ ряда колонки 9 соседа, а не только ряд 0: ворота в шве + * бывают в любом ряду. На уровне 1 решётка соседа стояла в (0,9), и одного + * байта хватало; на уровне 4 плита комнаты 1 открывает ворота (1,9) комнаты 8 + * — изменение приходило в m[19], сигнатура его не видела, редрой не + * заказывался, и открытые ворота оставались нарисованными закрытыми + * (BUG-GATE-SEAM-ROW1). */ +static uint8_t seam_sig[3]; /* последняя нарисованная openness гейта в шве */ +static uint8_t seam_rows; /* маска рядов, ждущих редроя (переживает 2 кадра) */ static uint8_t seam_redraw; /* оставшиеся кадры редроя (обе страницы) */ /* Отладочный лейбл номера комнаты — рисуем только при смене (outtextxy тяжёлая): @@ -331,11 +339,12 @@ static void enter_room_side(uint8_t room, uint8_t side) uint8_t lft = pop_room_link(room, 0), rgt = pop_room_link(room, 1); if (lft) { uint8_t *m = pop_trob_modif(lft); lcol_bg[0] = m[9]; lcol_bg[1] = m[19]; lcol_bg[2] = m[29]; - seam_sig = m[9]; } /* шов рисуется в обе страницы ниже — синхр. сигнатуру */ + /* шов рисуется в обе страницы ниже — синхр. сигнатуру */ + seam_sig[0] = m[9]; seam_sig[1] = m[19]; seam_sig[2] = m[29]; } if (rgt) { uint8_t *m = pop_trob_modif(rgt); rcol_bg[0] = m[0]; rcol_bg[1] = m[10]; rcol_bg[2] = m[20]; } } - seam_redraw = 0; /* шов свежий на обеих страницах — редрой не нужен */ + seam_redraw = 0; seam_rows = 0; /* шов свежий на обеих страницах — редрой не нужен */ for (pg = 0; pg < 2; pg++) { gfx_set_draw_page(pg); setfillstyle(SOLID_FILL, BLACK); @@ -733,15 +742,30 @@ int main(void) uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */ if (lft) { uint8_t *m = pop_trob_modif(lft); - uint8_t g = m[9], prev = seam_sig; - seam_sig = g; /* СНАЧАЛА сохранить (обход SDCC z80 sub-store */ + uint8_t i, changed = 0; + /* Все ТРИ ряда колонки 9: ворота в шве бывают в любом из них + * (BUG-GATE-SEAM-ROW1, см. объявление seam_sig). */ + for (i = 0; i < 3; i++) { + uint8_t g = m[9 + i * 10], prev = seam_sig[i]; + seam_sig[i] = g; /* СНАЧАЛА сохранить (обход SDCC z80 sub-store */ /* бага: `if(g!=s)s=g` эмитит store РАЗНОСТИ g-s, */ /* не g — sdcc_z80_cmp_store_a_bug) */ - if (g != prev) seam_redraw = 2; /* openness изменился → обе страницы дабл-буфера */ + if (g != prev) changed |= (uint8_t)(1 << i); + } + if (changed) { /* openness изменился → обе страницы дабл-буфера */ + /* Плюс ряд ВЫШЕ: верх решётки (draw_tile_anim_topright, + * seg008:0568 — маска 68 + кадр DOOR_FRAM_TOP) рисует не + * сам тайл ворот, а тайл над-справа от него, то есть у + * нас (r−1, 0). Он тоже анимируется вместе с openness; + * без этого чёрный треугольник над воротами оставался + * статичным, пока бары ездили (BUG-GATE-SEAM-ROW1). */ + seam_rows |= (uint8_t)(changed | (changed >> 1)); + seam_redraw = 2; + } if (seam_redraw) { lcol_bg[0] = m[9]; lcol_bg[1] = m[19]; lcol_bg[2] = m[29]; - pop_room_redraw_seam_left(); - seam_redraw--; + pop_room_redraw_seam_left(seam_rows); + if (--seam_redraw == 0) seam_rows = 0; } } }