From ed5615a95a7010c3e8af824663f2e457998deeb7 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Tue, 18 Aug 2026 18:55:39 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BB=D0=B8=D1=82=D1=8B=20=D0=BD=D0=B8?= =?UTF-8?q?=D0=B6=D0=BD=D0=B5=D0=B3=D0=BE=20=D1=80=D1=8F=D0=B4=D0=B0=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=BF=D0=B0=D0=B4=D0=B0=D0=BB=D0=B8=20=D0=B1?= =?UTF-8?q?=D0=B5=D0=B7=20=D0=BA=D0=B0=D0=B4=D1=80=D0=BE=D0=B2=20=D0=BF?= =?UTF-8?q?=D0=B0=D0=B4=D0=B5=D0=BD=D0=B8=D1=8F:=20draw=5Fmob=20=D1=80?= =?UTF-8?q?=D0=B8=D1=81=D1=83=D0=B5=D1=82=20=D0=B8=20=D1=81=D0=BE=D1=81?= =?UTF-8?q?=D0=B5=D0=B4=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Уровень 11 комната 14: плита ряда 2 уходит в комнату снизу на первом же move_loose (спавн y=191 при границе ряда 188), а у нас кусок в чужой комнате не рисовался вовсе — плита исчезала мгновенно. Оригинал (seg007:13E5) рисует его ещё три кадра, выглядывающим из нижней кромки (+192), и симметрично из комнаты сверху (−189). У куска появилась экранная координата draw_y; по ней идут отрисовка, heal, порядок относительно Кида, пометки соседа и оверлей, отбор в проходе — по ней же, а не по комнате. Ссылки вверх/вниз кэшируются. Цена (13/23): зелёная 419 562 -> 427 242, работа 880 272 -> 888 984. Первый вариант стоил втрое дороже (445 248) из-за безусловной пометки по прошлой нарисованной позиции — у куска из соседней комнаты она в 192 пикселях, и объединение растягивалось на весь экран; гейт вернул 18 000. Co-Authored-By: Claude Opus 5 --- applications/PoP/roomtest/BUGS_CLOSED.md | 66 +++++++++++++++ applications/PoP/roomtest/pop_room.c | 100 ++++++++++++++++++----- 2 files changed, 146 insertions(+), 20 deletions(-) diff --git a/applications/PoP/roomtest/BUGS_CLOSED.md b/applications/PoP/roomtest/BUGS_CLOSED.md index 7beec8f..889948e 100644 --- a/applications/PoP/roomtest/BUGS_CLOSED.md +++ b/applications/PoP/roomtest/BUGS_CLOSED.md @@ -3238,3 +3238,69 @@ MAME на резидентном `pop_dbg_trap`; сборщик — `collect_kid `pop_cd[].fpw`. Подозрение на паддинг кадров в атласе (`png_strip_padding_tradeoff`) либо на мой пересчёт переднего края мимо боевого `char_x_forward_edge`. Проверять отдельно. + +--- + + +## MOB-NEIGHBOUR-ROOM. Плиты нижнего ряда пропадали без кадров падения + +**Наблюдение (пользователь, 2026-08-18).** Уровень 11, комната 14: Кид бежит +по ряду проваливающихся плит, они трясутся и «падают сразу полностью +пропадая, а должен быть промежуточный кадр». Догадка пользователя — «для +плит на 2-ом ряду мы рисуем только тряску» — оказалась верной по симптому и +почти верной по причине. + +**Корень.** Порт `draw_mob` (seg007:13E5) был неполным. У оригинала кусок +рисуется НЕ ТОЛЬКО в своей комнате: + +```c +if (curmob.room == drawn_room) { if (curmob.y >= 210) return; } +else if (curmob.room == room_B) { if (ABS((sbyte)ypos) >= 18) return; curmob.y += 192; } +else if (curmob.room == room_A) { if (curmob.y < 174) return; ypos = curmob.y - 189; } +else return; +``` + +У нас на этом месте стояло `if (!mt_here) return;` — «чужая комната: считаем, +но не рисуем». + +Почему страдал ИМЕННО ряд 2: кусок спавнится на `y_loose_land[row+1]`, для +ряда 2 это **191**, а граница ряда `y_something[3]` — **188**. То есть на +ПЕРВОМ же `move_loose` он уходит в комнату снизу и у нас переставал +рисоваться мгновенно. Ряды 0 и 1 летят внутри комнаты, поэтому там всё было +видно. Промежуточных кадров ровно три — пока `|y| < 18`. + +**Фикс.** У куска появилась отдельная ЭКРАННАЯ координата `draw_y` (своя +комната / +192 из комнаты снизу / −189 из комнаты сверху, `MOB_Y_NONE` = +не рисуется). По ней идут отрисовка, `prev_y`/heal, порядок относительно +Кида, пометки соседа и оверлей; отбор в проходе отрисовки — по `draw_y`, а не +по комнате. Ссылки вверх/вниз кэшируются (иначе `pop_room_link` мапил бы W0 +каждый кадр). + +Вторым заходом пришлось поправить `mob_overlay_neighbour`: ряд там уже считался +по `draw_y`, а габарит спрайта и вызов `pop_mob_overlay_tile` остались по `y` — +для куска из соседней комнаты это расходится на 192, и пересечение с габаритом +тайла считалось заведомо неверно (порядок переднего слоя). + +**Цена — и урок про то, ЧТО именно дорого.** Замер 13/23: + +| секция | до | первый вариант | после гейта | +|---|---:|---:|---:| +| работа | 880 272 | 898 578 | **888 984** | +| зелёная | 419 562 | 445 248 | **427 242** | +| циан | 380 202 | 379 818 | 378 864 | + +Первый вариант метил «фон трогали» по ПРОШЛОЙ нарисованной позиции +безусловно. У куска, видимого из соседней комнаты, она отличается от `y` на +192 — объединение прямоугольников растягивалось почти на весь экран и тянуло +за собой лишние перерисовки. Гейт `m->room != pop_t_room` вернул 18 000 из +26 000. + +Я сперва списал рост на клипованный блит (кусок у кромки по определению +клипуется, а клипованный путь дороже: 14 088 против 10 422) — замер это +опроверг: дело было в пометке. Остаток +7 680 в зелёной (+1,8 %) — +собственно отрисовка трёх кадров на плиту; зелёная остаётся ниже растрового +кадра 430 000. + +Host-тесты 5106 проверок без расхождений (`pop_room.c` они не линкуют — +проверка визуальная, в MAME: уровень 11 комната 14, отделившаяся плита видна +три кадра). diff --git a/applications/PoP/roomtest/pop_room.c b/applications/PoP/roomtest/pop_room.c index 0dfbc3c..02fbcf9 100644 --- a/applications/PoP/roomtest/pop_room.c +++ b/applications/PoP/roomtest/pop_room.c @@ -1116,6 +1116,11 @@ typedef struct { uint8_t defer; /* кусок ниже Kid → рисуем ПОСЛЕ него (y-порядок) */ int x; /* левый край (POP_COL_XH*8) */ int y; /* = curmob.y оригинала (низ куска, dby), растёт вниз */ + /* Где кусок рисуется НА ЭКРАНЕ в этом кадре, MOB_Y_NONE = не рисуется. + * Отдельно от y, потому что кусок виден и из СОСЕДНЕЙ комнаты: порт + * draw_mob (seg007:13E5) сдвигает его на +192 (комната снизу) или на + * −189 (комната сверху). См. mob_tick_one. */ + int draw_y; int prev_y[2]; /* последняя нарисованная y на КАЖДОЙ странице (heal) */ int8_t speed; int8_t row, col; /* тайл-позиция куска (для окклюзии соседа) */ @@ -1197,6 +1202,7 @@ static void mob_spawn(uint8_t room, int row, int col) m->clean = 0; m->defer = 0; m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE; + m->draw_y = MOB_Y_NONE; } /* Отрыв куска в ТЕКУЩЕЙ комнате (обычный случай — pop_loose_tick). */ @@ -1235,6 +1241,7 @@ static void mob_spawn_copy(const mob_t *src) m->clean = 0; m->defer = 0; m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE; + m->draw_y = MOB_Y_NONE; /* Кусок рождается ВНУТРИ mob_tick_one (loose_fall сбил плиту), и слот ему * mob_alloc выдаёт с начала таблицы — то есть, скорее всего, УЖЕ * пройденный циклом pop_loose_mob_tick. Своей пометки «фон трогали» этот @@ -1273,15 +1280,30 @@ void pop_loose_mob_reset(void) __banked * не досмотрел, всё равно долетает и кладёт щебень (BUG-LOOSE-2: в комнате 12 * плита 0,1 не успевала упасть до ухода в комнату 15). Сбросить надо только * heal-историю: новая комната перерисована целиком, старых пикселей нет. */ +/* Соседи ОТРИСОВАННОЙ комнаты сверху/снизу — для draw_mob (см. mob_tick_one). + * Кэш, а не вызов каждый кадр: pop_room_link лезет в страницу уровня, то есть + * мапит и размапливает W0. Инвалидируется сменой комнаты. */ +static uint8_t mob_link_room, mob_link_up, mob_link_dn; + +static void mob_links(void) +{ + if (mob_link_room == pop_t_room) return; + mob_link_room = pop_t_room; + mob_link_up = pop_room_link(pop_t_room, 2); + mob_link_dn = pop_room_link(pop_t_room, 3); +} + void pop_loose_mob_room_changed(void) __banked { uint8_t i; for (i = 0; i < MOB_MAX; i++) { mobs[i].defer = 0; mobs[i].prev_y[0] = mobs[i].prev_y[1] = MOB_Y_NONE; + mobs[i].draw_y = MOB_Y_NONE; /* дочистка хвоста относилась к покинутой комнате — она уже не нужна */ if (!mobs[i].active) mobs[i].clean = 0; } + mob_link_room = 0; /* кэш соседей — под новую комнату */ } /* Тайл под куском. В своей комнате — обычный tile_code (он знает про кромки @@ -1328,6 +1350,7 @@ static void mob_tick_one(mob_t *m, uint8_t pg) * pop_loose_mob_tick, и слоты он обходит последовательно. */ mt_m = m; mt_pg = pg; if (!mt_m->active && !mt_m->clean) return; + mt_m->draw_y = MOB_Y_NONE; /* по умолчанию не рисуемся; решим в конце */ mt_here = (uint8_t)(mt_m->room == pop_t_room); /* Стереть кусок, нарисованный на ЭТОЙ странице в прошлый раз (у каждой * страницы своя копия фона). Раньше коридор был фиксирован под ряд 2; @@ -1430,7 +1453,33 @@ static void mob_tick_one(mob_t *m, uint8_t pg) mt_m->active = 0; mt_m->clean = 2; return; } - if (!mt_here) return; /* чужая комната — считаем, но не рисуем */ + /* Порт ГОЛОВЫ draw_mob (seg007:13E5). Кусок виден НЕ ТОЛЬКО в своей + * комнате: оригинал рисует его ещё и когда он только что провалился в + * комнату СНИЗУ (пока |y| < 18 — со сдвигом +192, то есть выглядывает из + * нижней кромки) или летит в комнате СВЕРХУ и уже подошёл к её низу + * (y >= 174 — со сдвигом −189, вываливается из верхней кромки). + * + * Без этого плита, падающая из НИЖНЕГО ряда, исчезала мгновенно: она + * уходит в комнату снизу на ПЕРВОМ же move_loose (спавн y = 191 при + * границе ряда 188), и у нас после этого не рисовалась вовсе. Ряды 0 и 1 + * летят внутри комнаты, поэтому баг был виден только на ряду 2 — ровно + * как заметил пользователь (уровень 11, комната 14). */ + if (mt_here) { + if (mt_m->y >= 210) return; /* ниже поля — оригинал не рисует */ + mt_m->draw_y = mt_m->y; + } else { + mob_links(); + if (mt_m->room == mob_link_dn) { + int8_t yb = (int8_t)mt_m->y; /* ABS((sbyte)ypos) < 18 */ + if (yb >= 18 || yb <= -18) return; + mt_m->draw_y = mt_m->y + 192; + } else if (mt_m->room == mob_link_up) { + if (mt_m->y < 174) return; + mt_m->draw_y = mt_m->y - 189; + } else { + return; /* дальше соседей не рисуем */ + } + } /* Порядок «плита vs Kid». В оригинале объекты рисуются при обходе СВОИХ * тайлов (redraw_needed_tiles: ряды 2,1,0; колонки 0..9), и только внутри * ОДНОГО тайла сортируются по obj_y (sort_curr_objs, seg008:1561: кто @@ -1439,11 +1488,11 @@ static void mob_tick_one(mob_t *m, uint8_t pg) * поверх Kid, если его тайл идёт в обходе ПОЗЖЕ тайла Kid, а при * совпадении тайлов — если он ниже по y. */ { - int8_t mrow = pop_y_to_row((int16_t)mt_m->y); /* y_to_row_mod4 */ + 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 */ else if (mt_m->col != pop_bg_obj_col) over = (mt_m->col > pop_bg_obj_col); - else over = (mt_m->y > pop_cd[POP_CH_KID].fpy); + else over = (mt_m->draw_y > pop_cd[POP_CH_KID].fpy); /* РИСОВАНИЯ ЗДЕСЬ БОЛЬШЕ НЕТ — только решение, в какой проход кусок * пойдёт. Пока heal и вывод шли в одной итерации, heal СЛЕДУЮЩЕГО * куска срезал верхнюю кромку уже нарисованного предыдущего: на 13-м @@ -1488,8 +1537,8 @@ static void mob_tick_one(mob_t *m, uint8_t pg) * отрисовке тайла на кусок за кадр (MOB-CLIP-RIGHT). */ static void mob_mark_neighbour(const mob_t *m) { - int8_t r = pop_y_to_row((int16_t)m->y); - int8_t rt = pop_y_to_row((int16_t)(m->y - 18)); + 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); /* ПОЛОСА У ПОТОЛКА (ряд −1). Её передние куски (нижняя кромка потолка и * кадр плиты-потолка) живут в трёх верхних строках поля — dby = 2 при @@ -1504,7 +1553,7 @@ static void mob_mark_neighbour(const mob_t *m) * плита (−1,5) перекрывала потолок (−1,6)). В оригинале порядок даёт * сама структура таблиц: кусок идёт в midtable (draw_mobs), а полоса * потолка — в foretable, и та рисуется позже (draw_tables, seg008:1373). */ - if (m->y <= 18) { + if (m->draw_y <= 18) { /* Пустой тайл полосы потолка не рисует В ПЕРЕДНЕМ СЛОЕ ничего * (bottom_id = 0, FORE_ANY = 0), а пометка стоит потом двух трамплинов * — в банк 7 за разбором пометок и в банк 2 за самой отрисовкой, @@ -1528,13 +1577,16 @@ static void mob_mark_neighbour(const mob_t *m) * последовательностью вызовов. */ static void mob_overlay_neighbour(const mob_t *m) { - int8_t r = pop_y_to_row((int16_t)m->y); - int8_t rt = pop_y_to_row((int16_t)(m->y - 18)); + 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); int ytop, ybot; if (c > 9) return; - ytop = m->y - (int)mob_spr[2] + 1; /* верх спрайта куска */ - ybot = m->y; + /* Габарит куска — по ЭКРАННОЙ координате, как и ряд выше: у куска, + * видимого из соседней комнаты, draw_y отличается от y на 192, и + * сравнение с габаритом тайла по y давало заведомо ложный ответ. */ + ytop = m->draw_y - (int)mob_spr[2] + 1; /* верх спрайта куска */ + ybot = m->draw_y; /* ПРЕДУСЛОВИЯ ПРОВЕРЯЕМ ЗДЕСЬ, до вызова в банк 2. pop_mob_overlay_tile * живёт в банке 2, то есть каждый вызов из банка 7 платит межбанковый * трамплин (~8 900 такта); а условий всего два — тайл не пуст и габарит @@ -1548,10 +1600,10 @@ static void mob_overlay_neighbour(const mob_t *m) * реальной высоте кусков нельзя: у колонн и зеркала база высокая. */ if (r >= 0 && r <= 2 && pop_tile_code(r, c) && ybot >= 63 * r + 2 && ytop <= 63 * r + 66) - pop_mob_overlay_tile(r, c, r, (int8_t)m->col, m->x, m->y, mob_spr[2]); + 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) && ybot >= 63 * rt + 2 && ytop <= 63 * rt + 66) - pop_mob_overlay_tile(rt, c, r, (int8_t)m->col, m->x, m->y, mob_spr[2]); + pop_mob_overlay_tile(rt, c, r, (int8_t)m->col, m->x, m->draw_y, mob_spr[2]); } static void mob_draw_pass(uint8_t pg, uint8_t want_defer) @@ -1564,9 +1616,11 @@ static void mob_draw_pass(uint8_t pg, uint8_t want_defer) if (!mobs_live) return; for (i = 0; i < MOB_MAX; i++) { mob_t *m = &mobs[i]; - if (!m->active || m->room != pop_t_room) continue; + /* Отбор по draw_y, а НЕ по комнате: кусок, только что провалившийся + * в комнату снизу, ещё виден из нашей (см. mob_tick_one). */ + 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]].y < m->y; j--) + for (j = n; j > 0 && mobs[order[j - 1]].draw_y < m->draw_y; j--) order[j] = order[j - 1]; order[j] = i; n++; @@ -1622,6 +1676,12 @@ void pop_loose_mob_tick(void) __banked * Запас сверху — на путь, пройденный за кадр (speed до 29): heal * стирает ПРОШЛУЮ позицию, она выше текущей. */ pop_cd_touch(MOB_X0(m->x), m->y - 27 + POP_YOFF, MOB_W, 64); + /* ...и по ПРОШЛОЙ нарисованной позиции: у куска, видимого из соседней + * комнаты, экранная координата отличается от y на 192 — коридор heal'а + * лежит совсем не там, где коридор по y. В пакете это четыре + * сравнения, настоящая пометка всё равно одна. */ + if (m->room != pop_t_room && m->prev_y[pg] != MOB_Y_NONE) + 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++; } @@ -1682,20 +1742,20 @@ static void mob_render(mob_t *m, uint8_t pg) * Окно ставим только когда кусок реально задевает борт: внутри поля блиты * идут быстрым путём (10 422 против 14 088). */ { - int ytop = m->y - 16 + POP_YOFF, ybot = m->y + POP_YOFF; + int ytop = m->draw_y - 16 + POP_YOFF, ybot = m->draw_y + POP_YOFF; uint8_t clipped = (uint8_t)(ytop < POP_YOFF || ybot >= POP_CLIP_BOTTOM); if (clipped) pop_t_win_set(0, POP_YOFF, 320, POP_PLAYFIELD_H); if (mob_spr_ok) { - pop_mem_b(mob_spr, m->x, m->y); + pop_mem_b(mob_spr, m->x, m->draw_y); } else { - pop_env_b(74, m->x, m->y); - pop_env_b(70, m->x, m->y - 3); - pop_env_b(72, m->x + 32, m->y - 1); + pop_env_b(74, m->x, m->draw_y); + pop_env_b(70, m->x, m->draw_y - 3); + pop_env_b(72, m->x + 32, m->draw_y - 1); } if (clipped) pop_t_win_clear(); } pop_cd_unmute(); - m->prev_y[pg] = m->y; + m->prev_y[pg] = m->draw_y; gfx_set_bank(GFX_BANK_NORMAL); }