Files
Sprinter-SDCC/applications/PoP/roomtest/bug_closed.md
T
Александр Петров 1146c57544 CLIP-1: heal Кида и стража — линейным ядром, когда клип не нужен
kid_heal/pop_guard_heal звали gfx_heal всегда, хотя рисуют ровно тот
прямоугольник, который блит в большинстве кадров кладёт noclip-ядром.
Все три места heal (+ heal_off фона) сведены к общему pop_heal_fast.

Замер в MAME, счётчики totalcycles на входах kid_heal и kid_tick (вся
группа heal за кадр), комната 1, Кид стоит:
  клипающее ядро  26 200 тактов/кадр (149 кадров)
  noclip          15 848 тактов/кадр (239 кадров)
−10 352 такта, −39.5 %.  A/B в одном прогоне: вторая половина снята с
пропатченным в памяти условием (jr nz → jr), то есть на той же геометрии.

Размер СУММАРНО −362 Б: _CODE +17, BANK2 −116 (свободно 2708 — это тесный
банк из рисков levels_plan §5), BANK3 −263, BANK4 без изменений.

Грабли по дороге: первым заходом хелпер был static inline в pop_bg.h —
SDCC 4.5 И встраивает тело (181 Б) в каждый вызов, И оставляет копию в
каждом TU, который видит заголовок.  pop_guard_heal раздулся с ~60 до
663 Б, итого +1091 Б в _CODE и +636 Б в банке стража.  Отсюда pop_draw.c:
обычная функция в резиденте W1, из банков это прямой call без трамплина.

pop_room_clip_borders оставлен клипающим осознанно (320 не лезет в 8 бит,
гейт border_dirty редкий) — причина записана в коде.

Проверено визуально: ходьба, прыжок, спуск, позиция за решёткой шва
(straddle — там работает клипающий фолбэк) — артефактов нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:53:30 +03:00

354 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# roomtest — архив закрытых багов
Сюда переезжает всё, что **закрыто**: подтверждённые фиксы, снятые
диагнозы, осознанные решения «не делать». Открытые баги — в
[`bug_list.md`](bug_list.md), текущие задачи — в [`TASKS.md`](TASKS.md).
Файл существует не ради истории как таковой: половина записей ниже — это
разбор КОРНЯ (odd-pixel арифметика `char_x`, подстановка тайла нажатой
кнопки, баг кодогенератора SDCC), и он экономит часы, когда похожий симптом
всплывёт снова. Прежде чем заводить новый баг — грепни здесь по симптому.
---
## Проверено в MAME 2026-08-01 (ревизия L1-TRIAGE)
Три бага стояли как **Critical** с 2026-07-21 и по исходникам выглядели
закрытыми, но переподтверждены не были. Прогон в MAME (roomtest, `ROOMNAV`,
чтение `_Kid` через мост) закрыл все три.
### BUG-1. Боковой переход через ворота: Kid проваливается на row 1 — **ЗАКРЫТ (не воспроизводится)**
**Был симптом:** при проходе через ОТКРЫТЫЕ ворота в соседнюю комнату
(через шов) Kid оказывался на ряду row 1 вместо row 0.
**Проверка 2026-08-01, точный сценарий бага.** Комната 6, Kid на ряду 0;
осторожный шаг на кнопку (0,2) — решётка room8 (0,9) поднимается; удержание
← через открытый шов:
| момент | `room` | `x` | `y` | `curr_col` | `curr_row` |
|--------|--------|-----|-----|-----------|-----------|
| на кнопке в room6 | 6 | 97 | 55 | 2 | 0 |
| после перехода | **8** | 181 | **55** | 8 | **0** |
Ряд и Y сохранены, Kid стоит на полу ряда 0 (скриншот `triage_06.png`).
Провала нет.
**Заодно снят и сам диагноз записи** — он был неверен. В записи стояло:
«Y/`curr_row` при боковом переходе НЕ репроецируются, в отличие от
`check_leave_below`». Это не дефект, а **точное поведение оригинала**:
`goto_other_room` (`SDLPoP/src/seg002.c:390`) для направлений left/right
меняет ТОЛЬКО `Char.x` (±140), а `Char.y`/`curr_row` трогает исключительно
для up/down. Наш `check_leave` (`pop_map.c:1402`) делает ровно то же.
Реальной причиной симптома была, судя по всему, кромочная коллизия — её
закрыл `char_x_forward_edge` (см. BUG-SEAM-PINGPONG ниже).
### BUG-2. Возврат из комнаты назад: Kid отбрасывается обратно (ping-pong) — **ЗАКРЫТ (не воспроизводится)**
**Был симптом:** после перехода в соседнюю комнату попытка сразу вернуться
приводила к тому, что Kid снова закидывался в ту же комнату — выйти нельзя.
**Проверка 2026-08-01.** Шов room2↔room3 (ряд 1, без ворот — чистый
горизонтальный переход), с намеренным разворотом СРАЗУ после пересечения:
| действие | `room` | `x` | `curr_col` |
|----------|--------|-----|-----------|
| старт в room2 | 2 | 86 | 1 |
| держим → | **3** | 122 | 3 |
| сразу держим ← | **2** | 195 | 9 |
| сразу держим → | **3** | 85 | 1 |
| сразу держим ← | **2** | 183 | 8 |
Комната меняется РОВНО один раз на пересечение, туда и обратно, без
осцилляции. Механизм на месте: `pop_leave_timer` (порт `exit_room_timer`,
`pop_map.c:112,1553`) + `char_x_forward_edge` (`pop_map.c:365`).
### BUG-3. Climb-up на тайл-кнопку: неправильная окклюзия — **ЗАКРЫТ фиксом от 2026-07-28**
**Был симптом:** при подтягивании на тайл, верх которого — кнопка
(opener/closer), Kid рисовался ПОВЕРХ кнопки вместо того, чтобы быть
перекрытым её передней гранью.
**Почему закрыт.** Запись требовала: «трактовать нажатую кнопку как
floor-тайл в climb-overlay, учесть подстановку из `get_tile_to_draw`».
Ровно это и сделано `tile_code_drawn()``climb_overlay_tile`
(`pop_bg.c:1346`) берёт ПОДСТАВЛЕННЫЙ код тайла, а не сырой. То есть
BUG-3 — дубль пункта «спуск с кнопки (room8, кромка (0,6))» из раздела
«Исправлено», заведённый до фикса.
**Оговорка, чтобы не выдавать желаемое:** покадрово в MAME снимался СПУСК
(кадры 148..138). Подъём идёт через ту же ветку и ту же таблицу
`FLOOR_LEFT_OVERLAY[fidx]`, поэтому отдельного дефекта тут быть не может,
но визуально направление «вверх» не переснималось. Если при сквозном
прохождении (L1-PASS) увидишь Kid поверх кнопки на подъёме — заводи заново.
---
## Косметика окклюзии — закрыта кодом, список отставал (сверено 2026-08-01)
Четыре записи от 2026-07-22 висели как открытые Medium. Каждая описывала
недостающий кусок порта; каждый из них с тех пор написан, но записи никто не
снял. Ниже — что именно закрывает каждую.
### BUG-CEIL-1. Прыжок вверх: руки Kid рисуются ПОВЕРХ потолка — **ЗАКРЫТ**
**Был симптом:** при прыжке вверх (SEQ up, кадры 67..79) руки/голова Kid
заходили в полосу кладки у потолка (row −1) и рисовались ПОВЕРХ неё.
**Чем закрыт:** `ceil_over_kid_tile()` (`pop_bg.c:1275`) — порт «нижняя грань
потолка и кадр плиты-потолка идут в FOREtable», т.е. поверх персонажа.
Зовётся из `pop_fore_over_kid` (`pop_bg.c:1575`) и из fore-прохода стража
(`:1607`). Комментарий на месте прямо называет причину: «иначе руки
прыгающего Kid лезут на кромку потолка».
### BUG-CEIL-2. Тряска/разбитие loose-плиты в потолке (row −1) — **ЗАКРЫТ**
**Был симптом:** loose-плита в ряду 2 верхнего соседа (room5 (2,5) → потолок
room6 над (0,5)) при прыжках Kid на (0,5) не тряслась и не разбивалась —
loose-состояние соседней комнаты не тянулось.
**Чем закрыт:** отдельное состояние плиты-потолка `pop_ceil_modif[10]`
(`pop_bg.c:547`, ведёт `pop_map`) + пара `pop_ceil_shake_draw()` /
`pop_ceil_bake_empty()` (`pop_bg.c:815,827`): дрожание рисуется на текущей
странице поверх фона, а провал «запекается» в ОЗУ-копию, чтобы heal его
сохранял. В полосе у потолка виден только НИЗ плиты (`draw_tile_aboveroom`),
всё выше режется клипом `POP_YOFF` — как в оригинале.
Из этого следует, что и запись «требует персистентного per-room modifier
соседей, это Фаза P0 gates_spikes_plan» больше не верна: понадобился не общий
механизм, а один массив на 10 байт под конкретный случай.
### BUG-CEIL-3. Анимация ворот стирает потолок над ними — **ЗАКРЫТ**
**Был симптом:** при анимации решётки шва полоса кладки у потолка НАД
воротами пропадала — чёрный `bar` по col0 стирал ряд −1, а redraw его не
восстанавливал.
**Чем закрыт:** `pop_room_redraw_seam_left()` (`pop_bg.c:793`) больше не
трогает полосу потолка — `bar` идёт с `POP_YOFF + 3`, а не с `POP_YOFF`:
низ полосы кладки на room-space y=2, бары ворот начинаются с y=3
(`gate_top_y = dby 62`). Приятный побочный эффект: отпала необходимость
перерисовывать `draw_tile(-1,0)` на КАЖДОМ кадре анимации решётки — то есть
фикс не только косметический, но и вдвое дешевле прежнего.
### BUG-OCCL-1. Тень дальней колонны перекрывает Kid — **ЗАКРЫТ**
**Был симптом:** Kid у (0,4)-(0,5) частично перекрыт тёмной штриховкой —
боковой гранью ДАЛЬНЕЙ колонны, которая окклюдить персонажа не должна
(over-occlusion fore-слоя, рисовавшего fore футпринт-тайлов без учёта
глубины).
**Чем закрыт:** разделение слоёв по признаку из оригинала, а не по нашим
соображениям о глубине (`overlay_mid_tile`, `pop_bg.c:1367`). Ключ,
подтверждённый трассой оригинала: `draw_tile_right`, `draw_tile_anim_right` и
`draw_loose` кладут спрайты через `add_backtable` НАПРЯМУЮ, минуя
`ptr_add_table` — значит правая грань левого соседа («шахматка» столба 93,
blueline, грани пик/loose соседа, кадр loose) **всегда** рисуется ПОД
персонажем. Через `ptr_add_table` (→ midtable при `draw_other_overlay`) идут
только «floor B» 42 при левом соседе-поле, `draw_tile_base`,
`draw_tile_anim` (свои пики) и `draw_tile_bottom`.
---
## BUG-SEAM-PINGPONG (#4). Пинг-понг drawn_room у шва с закрытыми воротами — **РЕШЁН 2026-07-22**
Настоящий корень найден потиковой трассой ЖИВОГО SDLPoP 1.23
(lldb-брейкпоинты на leave_room/bumped/safe_step с логом Char +
char_x_left/right; fixes выключены = vanilla). Прежние гипотезы оставлены
ниже для истории — они НЕ были причиной.
### КОРЕНЬ (подтверждён трассой + исходником)
`set_char_collision` (seg006:0723): `char_x_right = obj_x/2 + 58`, где
`load_frame_to_obj` (seg008:1728) считает `obj_x = 2*char_dx_forward(dx) - 116`
и **добавляет +1** для кадров «чётного пикселя»:
`if ((sbyte)(cur_frame.flags ^ obj_direction) >= 0) ++obj_x;`
(бит 0x80 флагов кадра XOR направление; вправо: +1 если бит НЕ стоит).
Деление `obj_x/2` — C-усечение К НУЛЮ, поэтому при `e = x+dx <= 57`
(obj_x < 0, зона левого шва) поправка +1 даёт `char_x_right = e+1`, а при
e >= 58 формула сокращается к чистому `e`.
Итог: Kid, осевший после отскока от ворот шва на x=57 (frame15, флаги 0x43 —
бит 0x80 не стоит), имеет **char_x_right = 58** и порога leave-left (<=57)
НЕ достигает. Наш движок считал передний край как `Kid.x + dx` без поправки
→ 57 → ложный leave → пинг-понг.
Эталонный цикл SDLPoP (нормализовано по трассе): стойка x=61 → тап вправо →
safe_step(d=0) → step, на первом dx(1) x=62 → bump (edge-триггер) → align 61
→ seq47 dx(-4) → **x=57** (скрыт за кромкой) → кадры 50/51/52 (cxr 61/60/58,
у всех бит 0x80 снят, e>57 — без сдвига) → стойка cxr=58 → leave НЕ
срабатывает; тап → safe_step d=3 → x=60 (1/3 видно); тап → step1 → x=61
(2/3 видно); тап → bump → 57 … по кругу. Char.room и drawn_room НЕ меняются.
### Фикс (pop_map.c)
`char_x_forward_edge()`: `e = char_dx_forward(dx); if (((flags ^ (dir<0 ?
0x80 : 0)) & 0x80) == 0 && e <= 57) e++;` — используется в `char_front_coll`
(коллизия/bump/edge_distance) и в `check_leave` (порог ухода). Плюс порт
doortop-гарда leave-right из leave_room (тайл (9,row) = doortop → правого
выхода нет). `pop_leave_timer` (exit_room_timer) оставлен — он реален в
seg002/seg003.
### Симптом (как выглядел)
Kid стоит за решёткой закрытых ворот шва (левый сосед room8 виден в кромке
room6). При удержании/нажатии ВПРАВО экран пинг-понгует между двумя
состояниями:
- **A**: показывается room6, Kid у левой кромки за решёткой (спрайт на 2/3);
- **B**: показывается room8, Kid у его правой кромки.
Эталон SDLPoP: drawn_room **всегда остаётся room6**, Kid осциллирует у
кромки (1/3→2/3→отступил→по кругу), в room8 экран НЕ переключается.
### Инструментальный диагноз (watchpoint на pop_leave_dir)
В момент лишнего свитча A→B: `pop_leave_dir=1 (LEFT)`, Kid **frame=15 (СТОЯ,
не transient!), Kid.x=57** (до репроекции +140). То есть:
- `char_x_right = char_dx_forward(kid_cur_dx()) = Kid.x + frame15.dx = 57+0 = 57`.
- Порог leave-left (взгляд вправо): `char_x_right <= 57` → срабатывает РОВНО на 57.
- Грань ворот (где их держит коллизия) = **61** (`wall_dist_from_left[1]=10 +
coll_tile_left_xpos=51`). Между 57 и 61 — **зазор 4px**: Kid НЕ удержан
воротами (d=6157=4≥0 → check_bumped не бампит), но уже на пороге ухода.
- Kid оседает на 57 из-за recoil отскока: seq_47 = `act(bumped), dx(-4),
frame_50, 51, 52`; SEQ_DX(4) двигает Char.x на −4 суммарно; frame_50.dx=4
компенсирует ТОЛЬКО точку коллизии НА кадре 50, но при возврате в стойку
(frame15, dx=0) `char_x_right = Char.x = aligned4 = 57`.
### Что было ИСКЛЮЧЕНО (сверено с исходниками SDLPoP, НЕ причина)
- Формула char_x: `char_x_right = obj_x/2+58 = Char.x+frame.dx` (seg006
set_char_collision) — совпадает с нашим char_dx_forward.
- Позиция грани ворот: `get_left_wall_xpos = wall_dist_from_left[1](10) +
xpos_in_drawn_room(x_bump[9+5])+7 = 10+(184140)+7 = 61` — совпадает с нашим
(`x_bump[1+5]=44`, +7, +10 = 61).
- Порог leave: SDLPoP leave_room looking-right `char_x_right<=57` — совпадает.
- Данные кадров: frame_50 (image=49,dx=4,flags=0x67) и frame_15
(image=14,dx=0,flags=0x43,sword=9) — БАЙТ-В-БАЙТ как в SDLPoP frame_table_kid.
- seq_47 (act bumped, dx(-4), frame 50/51/52) — совпадает.
### Почему точечные фиксы НЕ работали
- Гард в check_leave (подавить leave на закрытых воротах) — это ОТСЕБЯТИНА,
не SDLPoP (в leave_room такого нет); откачено.
- `exit_room_timer=2` (порт seg002 exit_room — РЕАЛЬНЫЙ механизм, оставлен как
pop_leave_timer): блокирует leave 2 кадра после входа в комнату. НЕ спасал:
положение Char.x=57 **устойчивое** (Kid стоит), а не transient.
### Задел, который НЕ понадобился: порт coll_room + отложенный drawn_room
Трасса показала, что в эталоне `Char.room`/`drawn_room` вообще не меняются,
поэтому план S2/S3 для этого бага не потребовался. Остаётся заготовкой под
стражей/двух персонажей в кадре:
1. **Раздельные комнаты.** `Char.room` ≠ `drawn_room`. Уже есть `kid_room`
(S1) + рендер-смещение `pop_kid_set_render_dx(∓140)`.
2. **Коллизия по Char.room через coll_room (seg004, S2).** Портировать
`check_collisions` → `get_row_collision_data` → `get_left_wall_xpos`/
`get_right_wall_xpos` → `curr_row_coll_room[]`/`_flags[]`,
`bump_col_left_of_wall`/`bump_col_right_of_wall` → `check_bumped_look_*` →
`bumped()`. Тайлы — из РЕАЛЬНОЙ комнаты колонки, НЕ из снапшота.
3. **Отрисовка по drawn_room**, персонаж со сдвигом ±140.
4. **Отложенная смена drawn_room (seg002/seg000, S3):** `leave_room` →
`goto_other_room` → `exit_room` (`next_room`) → `check_the_end`.
Реализовано на 2026-07-22: S1 + `pop_leave_timer`.
Память: [[pop_seam_room_model]], [[sdlpop_odd_pixel_char_x]].
---
## Решено НЕ делать
### OPT-1. Хирургический редрой левого шва (ворота соседа) — 2026-07-22
**Возможность:** `pop_room_redraw_seam_left()` (pop_bg.c) на каждое изменение
openness рисует `bar(BLACK)` по всему col0 + **полный `draw_tile(0,0)`**
(стены, `topright`, `wall_pattern` с `prandom()` — десятки блитов). Реально
анимируются только бары решётки — `draw_gate_back` (~9 `env_b`).
**Стоимость:** seam-блок (синий io_border) занимает ~30-50% кадрового периода,
**но только пока openness меняется** — во время открытия и медленного
авто-закрытия (~5 сек после схода с кнопки). В покое — 0%. Замерено в MAME:
брейк на `_pop_room_redraw_seam_left` (0x5A54) срабатывает ⟺ сегмент дорогой.
**Приём** (heal НЕ годится: печёные бары устаревшие, поэтому и стоит
`bar(BLACK)`+redraw): `bar(BLACK)` только по полосе баров (x=0,
gate_top..gate_bot) + `draw_gate_back(lmod,...)` + дорисовать статику тайла
(0,0), задетую полосой. **Пиксель-чувствительно** — обязательна выверка в
MAME по кромкам.
**Решение:** оставляем как есть — стоимость транзиентная, в бюджет
помещаемся. Делать, только если упрёмся в кадровый бюджет на сценах с
воротами.
---
## Исправлено
- **Спуск с кнопки (room8, кромка (0,6)): Кид просвечивал в щель, ближняя рука
срезана до одного пикселя** — ИСПРАВЛЕНО 2026-07-28. Две причины:
(а) не был портирован `clip_char()` (seg006:1749) — верхняя обрезка спрайта по
`y_clip[curr_row+1]`, когда тайл над головой стена/пол; сделано
(`pop_clip_char_top` в pop_map.c + `gfx_blit_cols_part` в libbgi, heal чистит
уже обрезанный прямоугольник);
(б) `climb_overlay_tile` выбирал ветку `draw_floor_overlay` (seg008:1E3A) по
СЫРОМУ коду тайла — а нажатая кнопка в `get_tile_to_draw` (seg008:240)
подменяется на floor/stuck. Тайл-кнопка не проходил тест floor, уходил в
`draw_other_overlay` и закрашивал Kid ЦЕЛЫМ тайлом вместо узкой кромки
`floor_left_overlay[frame-137]`. Фикс — `tile_code_drawn()` (одна подстановка
на все слои). Проверено покадрово в MAME (кадры 148..138). **Этим же
фиксом закрыт BUG-3** (см. выше).
- **Шов ворот жёг 50% кадра в покое (закрытая решётка)** — ИСПРАВЛЕНО
2026-07-22. Причина — **баг кодогенератора SDCC z80** (memory
`sdcc_z80_cmp_store_a_bug`): `if (m[9] != seam_sig) seam_sig = m[9];`
компилировался в `sub (seam_sig)` (A ← разность) + `ld (seam_sig),a` —
сохранял РАЗНОСТЬ `m[9]-seam_sig`, не `m[9]`. `seam_sig` осциллировала
(напр. 25↔231), `m[9]!=seam_sig` истинно каждый кадр → `draw_tile(0,0)`
каждый кадр даже у неподвижной решётки. Фикс: store-до-сравнения
(`seam_sig = g;` из чистого `g` ДО `sub`), подтверждён в .asm. Прочёс всех
модулей PoP: других случайных compare-then-store нет.
- **Пики: 2×полный `draw_tile` на кадр анимации** → хирургический редрой,
2026-07-22. `pop_spike_redraw`: `heal_off` уже возвращает всю печёную
статику (база 127, пол, грани); поверх анимируются только два острия
(`SPIKES_FRAM_LEFT` в своей ячейке + `SPIKES_FRAM_RIGHT` в соседней).
Заменили 2×`draw_tile` на 2×`env_b` — пиксель-в-пиксель тот же результат,
в разы дешевле (шахта из нескольких пик больше не съедает полный кадр).
- **Отрисовка нажатой кнопки (0,2)/(0,3)** — ИСПРАВЛЕНО. Причина: `fore_tile`
(pop_bg.c, fore-слой поверх Kid) рисовал переднюю грань КНОПКИ (bottom_id
149) поверх уже нарисованной грани пола (43) — «остаток нажатой кнопки».
Фикс: `fore_tile` применяет ту же подстановку нажатой кнопки, что и
`draw_tile` (opener→floor / closer→stuck при таймере связи >1). Плюс
`pop_button_redraw` — wipe своей ячейки + правой грани (дальний угол в
0,3), низ строго yb+64 (не залезать в стену ряда 1).
---
## НЕ БАГИ (кривая картинка, но совпадает с оригиналом — НЕ чинить)
- **Голова стоящего Кида поверх падающей на него loose-плиты.** Комната 12:
зацеп не удался, Кид остался стоять, сбитая плита падает прямо на него —
голова рисуется ПОВЕРХ плиты. Сверено покадрово с SDLPoP v1.24 (2026-07-29):
там ровно то же самое. Артефакт оригинального движка (порядок midtable), а
не наш баг; «починка» увела бы от эталона. Отличать от соседних случаев,
которые БЫЛИ нашими багами и исправлены: вис/подтягивание на кромке плиты и
падение вместе с плитой — там плита обязана быть поверх Кида.
- **Комнаты 13, 18, 24 недостижимы в обычной игре** — свойство ДАННЫХ уровня 1,
не наш баг. Обход графа от стартовой комнаты (по `res2001.bin`, links @1952)
показывает: у всех трёх ссылки наружу есть, а на них не ссылается никто
(24: `L→9`, но у 9 `R=0`; 13 и 18 связаны только друг с другом). Признак
«комнату выкинули из компоновки, связи не почистили» — несимметричные ссылки
ровно у этих трёх, у остальных 21 симметрия полная:
```
13 L→22, у 22 R=16 | 18 L→15, у 15 R=12 | 24 L→9, у 9 R=0
13 R→16, у 16 L=22 | 18 R→12, у 12 L=15
| 18 D→19, у 19 U=12
```
Следствие: в 13/18/24 возможен «мусор в шве» — наш рендер кромки читает
крайнюю колонку соседа ПО ССЫЛКЕ, а сосед этих комнат соседом себя не
считает. Приоритет багов в этих трёх комнатах — низкий (в игре не видно).