Плиты нижнего ряда пропадали без кадров падения: draw_mob рисует и соседей

Уровень 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 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 18:55:39 +03:00
parent 0832445614
commit ed5615a95a
2 changed files with 146 additions and 20 deletions
+66
View File
@@ -3238,3 +3238,69 @@ MAME на резидентном `pop_dbg_trap`; сборщик — `collect_kid
`pop_cd[].fpw`. Подозрение на паддинг кадров в атласе
(`png_strip_padding_tradeoff`) либо на мой пересчёт переднего края мимо
боевого `char_x_forward_edge`. Проверять отдельно.
---
<a id="mob-neighbour-room"></a>
## 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, отделившаяся плита видна
три кадра).
+80 -20
View File
@@ -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);
}