Мигание тени: кадр приходил из кэша, а снимок писался по Guard.frame

Тень на уровне 6 стояла на двух страницах дабл-буфера в разных позах.
Отрисовка брала image из кэша kid_frame/pop_gframe, который наполняет тик,
а снимок пропуска кадра писала по Char.frame — расхождение застревало
навсегда, потому что снимок совпадал и страница больше не перерисовывалась.

Кадр теперь грузит сама отрисовка, как в оригинале (add_*_to_objtable →
load_fram_det_col, seg008:22F0/2324).  Разбор — BUGS_CLOSED.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 11:48:45 +03:00
parent 89b603ae04
commit 085a198c20
3 changed files with 131 additions and 11 deletions
+79
View File
@@ -2785,3 +2785,82 @@ if (custom->tbl_level_type[current_level]) leveldoor_right += 8;
функции оригинал в СТАРТОВОЙ комнате кладёт затирающий прямоугольник вместо
лестницы (и там своя дворцовая/подземная разница 48/39 и сдвиг 2 px), а мы
рисуем марш 144 безусловно. Заведено отдельным багом.
---
<a id="shadow-stale-frame"></a>
## SHADOW-STALE-FRAME. Тень мигала между двумя РАЗНЫМИ позами на страницах дабл-буфера — ЗАКРЫТ 2026-08-18
**Наблюдение (пользователь, 2026-08-18).** Уровень 6, комната 1 (Кид и через
большой провал — тень). «У Тени на двух экранах отрисованы разные позиции —
одна стоящая, одна бегущая; увидел сразу, как вошёл в комнату. Повторяется
нечасто, но очень неприятно». Гипотеза пользователя — «в один из экранов
попала случайная позиция Тени» — оказалась дословно верной.
**Улика (живое состояние, снято через мост MAME до перезапуска).** Слот
соперника `pop_cd[POP_CH_OPP]`:
```
стр.0: x=41 y=78 w=12 h=41 valid=1 <- кадр 15 «стойка»
стр.1: x=25 y=85 w=34 h=38 valid=1 <- ЧТО-ТО ДРУГОЕ
cd_sig[OPP][0] == cd_sig[OPP][1] == { frame=15, x=81, y=118, dir=0, charid=1 }
```
Обе страницы «тихие» (снимок совпал с живым `Guard`), поэтому ни одна больше
не перерисовывается — мигание навсегда. Прямоугольник страницы 1
восстанавливается однозначно: `bx = scr_x(obj_x) w` и `top = obj_y h + 1`
при `Guard.x = 81`, `Guard.y = 118`, `dir = 0` дают `dx = 3`, `dy = 4`,
`flags ≥ 0x80` и картинку 34×38. В таблице кадров ровно одна такая запись —
**`frame_tbl_guard[36]`, то есть кадр 185 «страж мёртв»** (`image = 33`,
`dx = 3`, `dy = 4`, `flags = 0xC9`; 34×38 — это размер `image 33` в атласе
КИДА, что сходится: набор спрайтов выбирается по `Guard.frame = 15`, а
`image` приходил из кэша).
**Корень.** Оригинал грузит кадр В САМОЙ ОТРИСОВКЕ: `add_kid_to_objtable` и
`add_guard_to_objtable` (seg008:22F0/2324) первым делом после
`loadkid`/`loadshad` зовут `load_fram_det_col()`. У нас `kid_frame` /
`pop_gframe` были КЭШЕМ, который наполняет тик (`play_seq` → `load_frame`), а
отрисовка брала готовое. Кэш отстаёт от `Char.frame` у всех, кто пишет кадр
мимо `play_seq` — например `do_init_shad` (`guards.c`) кладёт тени
`frame = 15` прямой записью. А `pop_gframe` в этот момент держал кадр 185
убитого стража с прошлого уровня: после смерти соперника `pop_guard_tick`
выходит по `charid == 0` РАНЬШЕ `pop_load_fram_det_col`, и кэш не трогается
вообще, сколько бы комнат Кид ни прошёл.
Само по себе это моргнуло бы один кадр. Смертельным его делает **пропуск
неизменившегося кадра** (DRAW-COST): снимок `cd_sig` пишется по
`Guard.frame`, то есть страница запоминает «нарисован кадр 15», хотя на ней
чужой спрайт. Тень дальше стоит, снимок совпадает — страница не
перерисовывается НИКОГДА.
**Фикс.** `pop_char_draw` зовёт `pop_load_frame()` сразу после
`pop_loadkid`/`pop_loadshad` — там же, где это делает оригинал. После этой
строки `image` однозначно определяется парой `(charid, frame)`, то есть
снимок `cd_sig` снова описывает ровно то, что нарисовано, и расхождение
«пиксели против снимка» становится невозможным независимо от того, кто и как
испортил кэш. Цена: +3 байта в банке 4, кадровый бюджет не трогает
(отрисовка и так пропускается у «тихого» слота).
Заодно приведена к оригиналу ветка «кадра нет в таблице»: `load_frame`
(`pop_kid.c`) ставила `cur_frame.image = 255` и выходила, НЕ обновив кэш
владельца, — то есть вместо «не рисовать» рисовалась прошлая картинка.
Оригинал кладёт туда `blank_frame {255,0,0,0,0}` (`get_frame_internal`,
seg006:507). Выход из функции теперь один на обе ветки: +46 байт резидента.
**Проверка (MAME, уровень 6 комната 1).** Брейкпоинт на трамплине
`___sdcc_bcall_ehl` с условием `hl == _pop_char_draw` и действием
«записать в `pop_gframe` кадр 185 и продолжить» — то есть кэш травится
ровно перед КАЖДОЙ отрисовкой персонажа. Слот при принудительной
перерисовке (`valid[0] = valid[1] = 0`) остаётся `12×41` на обеих
страницах, `pop_gframe` после кадра — снова кадр 15. До фикса та же
подмена дала бы `34×38 @ (25,85)` — ровно числа из улики.
**Что осталось невыясненным.** Не удалось воспроизвести САМ МОМЕНТ порчи:
вход в комнату с заведомо испорченным кэшем (подмена `pop_gframe` в соседней
комнате + возврат читом обхода) самолечится — метка «фон трогали» от
перерисовки комнаты стоит на обеих страницах, и слот честно перерисовывается
следующим кадром. Значит порча случилась в кадре, где фон НЕ трогали, и
конкретный путь остался неизвестен. На вывод это не влияет: фикс закрывает
класс целиком, а не найденный путь. Ловушка на будущее, если симптом
всплывёт снова: `wp <адрес pop_cd[OPP].w>,4,w` — сработает на первой же
записи «чужой» ширины.