Чёрные бары поверх стража: заливка запечки уезжала за правый край экрана

Найдено пользователем (2026-08-17, уровень 2 комната 4): после падения плит
(1,7) и (1,8) поверх стража в (1,0) появляются два чёрных бара, большой и
маленький, и МЕРЦАЮТ — то есть живут только на одной странице дабл-буфера.
Пока плиты не упали, баров нет.

Диагноз пользователя оказался верным дважды — и в причине, и в механизме.

ПРИЧИНА.  Падение плиты (1,8) помечает соседа (1,9), и pop_floor_bake заливает
там прямоугольник ШИРИНОЙ 60 от x = 288 — то есть 348, на 28 пикселей за
экран.  Ширина «свой тайл + свес соседа» (40/60/64 при шаге тайла 32) верна
для любой колонки, кроме последней.

А заливка по контракту НЕ КЛИПУЕТ, и это правильно: проверка координат стоила
бы дороже самой заливки (шапка _gfx_recthfill256: «Клиппинга НЕТ, координаты
обязаны быть валидны»).  Значит обрезать обязан ВЫЗЫВАЮЩИЙ — bar не трогаем.

МЕХАНИЗМ МЕРЦАНИЯ (объяснение пользователя).  Страница 1 начинается на 320
байт дальше страницы 0, поэтому запись в страницу 0 с x > 320 попадает в
страницу 1 по x−320.  Отсюда и «бар только на одной странице».  Хуже того,
такая же запись НА странице 1 уходит уже за пределы обеих страниц — в то, что
лежит дальше (палитры и прочее), так что баг не только косметический.

ПОДТВЕРЖДЕНИЕ АРТЕФАКТОМ.  Трасса всех вызовов pop_bar_black на прогоне:

    BAR x=288 y=117 w=60 h=39 ret=D754   <- pop_floor_bake, 288+60 = 348

и независимо снятое расхождение страниц: РОВНО x 0..27 (348−320 = 28 px) при
y 117..155 — то есть в точности y этого бара (yb+26 = 117, высота 39).

ФИКС.  Хелпер bake_w(x, w) обрезает ширину по правому краю (и отдаёт 0, если
прямоугольник целиком за экраном); применён во всех точечных запечках —
pop_floor_bake, pop_ceil_bake_empty, pop_loose_bake_empty.  Обрезка ничего не
теряет: правее 320 восстанавливать нечего.  Копия второй страницы (bake_copy)
берёт ту же обрезанную ширину, иначе запечка и копия разъехались бы.

Заодно, того же рода:
- pop_torch_wipe у факела в колонке 9 заливал x = 328, то есть ЦЕЛИКОМ за
  экраном — теперь пропускается;
- pop_loose_bake_empty читал POP_COL_XH[col + 1] БЕЗУСЛОВНО, то есть у
  последней колонки за концом массива (10 элементов); чтение убрано под
  проверку.

Отсечено по дороге (проверками, не рассуждением): копия запечки не виновата
(сборка с выключенной копией — бары остались, проверил пользователь).

8 наборов tests-host зелёные.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 18:46:09 +03:00
parent 40f0d46542
commit dd40c24e7a
+45 -10
View File
@@ -686,6 +686,29 @@ void pop_room_redraw_seam_left(uint8_t rows) __banked
}
}
/* Ширина прямоугольника запечки, ОБРЕЗАННАЯ по правому краю экрана.
*
* Точечные запечки берут ширину «свой тайл + свес соседа» (40, 60 или 64 при
* шаге тайла 32) — для последней колонки это уезжает за 320. А заливка и
* копия по контракту НЕ КЛИПУЮТ (см. шапку _gfx_recthfill256: «Клиппинга НЕТ,
* координаты обязаны быть валидны») — и правильно, что не клипуют: проверка
* координат стоила бы дороже самой заливки. Значит обрезать обязан
* вызывающий.
*
* Чем это было: падение плиты (1,8) помечает соседа (1,9), pop_floor_bake
* заливал x=288 шириной 60, то есть 28 пикселей за экраном, и эти 28 всплывали
* ЧЁРНОЙ ПОЛОСОЙ у левого края — поверх стража в (1,0), причём только на одной
* странице дабл-буфера, то есть с мерцанием (найдено пользователем
* 2026-08-17, уровень 2 комната 4; трасса заливок:
* BAR x=288 y=117 w=60 h=39, а расхождение страниц — ровно x 0..27, y 117..155).
*
* Обрезка ничего не теряет: правее 320 восстанавливать нечего. */
static uint8_t bake_w(int x, uint8_t w)
{
if (x >= 320) return 0; /* весь прямоугольник за экраном */
return (x + (int)w > 320) ? (uint8_t)(320 - x) : w;
}
/* ---- ЗАПЕЧКА НА ВТОРУЮ СТРАНИЦУ — копией, а не пересчётом ----------- *
* Точечные запечки ставятся с pages = 2, то есть срабатывают ДВА кадра подряд
* — по разу на каждую страницу дабл-буфера, — и оба раза считают одно и то же.
@@ -709,6 +732,11 @@ static uint8_t bake_copy(uint8_t *slot, int x, int ytop, int w, int h)
{
uint8_t pg = (uint8_t)(gfx_get_draw_page() & 1);
gfx_rect_t r;
#ifdef POP_DBG_NO_BAKE_COPY
(void)x; (void)ytop; (void)w; (void)h; /* A/B: копия выключена целиком */
*slot = (uint8_t)(pg + 1);
return 0;
#endif
if (!*slot || (uint8_t)(*slot - 1) == pg ||
gfx_get_draw_page() == gfx_get_visible_page()) {
*slot = (uint8_t)(pg + 1); /* это ПЕРВАЯ запечка */
@@ -788,19 +816,20 @@ void pop_ceil_shake_draw(int col) __banked
void pop_ceil_bake_empty(int col) __banked
{
int x = POP_COL_XH[col] * 8;
uint8_t bw = bake_w(x, 64); /* обрезка по краю экрана — см. bake_w */
/* Вторая страница — копией с первой (см. bake_copy). */
if (bake_copy(&bake_pg_above[col], x, POP_YOFF, 64, CEIL_BAND_H + 1)) return;
if (bake_copy(&bake_pg_above[col], x, POP_YOFF, bw, CEIL_BAND_H + 1)) return;
gfx_set_bank(GFX_BANK_NORMAL);
/* Отдельного pop_cd_touch тут не надо: pop_bar_black метит сам, и ровно
* этот же прямоугольник (было два вызова на одну область). */
pop_bar_black(x, POP_YOFF, 64, CEIL_BAND_H + 1); /* стереть плиту в ОЗУ-копии */
if (bw) pop_bar_black(x, POP_YOFF, bw, CEIL_BAND_H + 1); /* стереть плиту в ОЗУ-копии */
gfx_set_bank(GFX_BANK_TRANSPARENT);
pop_t_clip_top = POP_YOFF;
/* Окно = ровно стёртая полоса. Главный выигрыш здесь не в отсеве, а в
* КЛИПЕ: куски ряда 0 высотой 63 px рисовались целиком (клип был только
* сверху, по pop_t_clip_top), хотя восстановить надо девять строк —
* отсюда 21 447 такта на блит против 14 384 в pop_floor_bake. */
pop_t_win_set(x, POP_YOFF, 64, CEIL_BAND_H + 1);
pop_t_win_set(x, POP_YOFF, bw, CEIL_BAND_H + 1);
pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */
/* Восстанавливаем ВСЁ, чья графика попадает в стёртую полосу, а не только
* два тайла ряда 1 (BUG-LOOSE-3). Куски тайла рисуются ВВЕРХ от своей
@@ -907,9 +936,12 @@ void pop_torch_wipe(int row, int col) __banked
{
int x = (col + 1 < 10 ? POP_COL_XH[col + 1] : POP_COL_XH[col] + 4) * 8 + 8;
int yb = POP_YOFF + 63 * row + 62 - 40;
/* У факела в колонке 9 канвас пламени лежит на x = 328, то есть ЦЕЛИКОМ за
* правым краем — заливать нечего, а bar не клипует (см. bake_w). */
uint8_t tw = bake_w(x, 16);
pop_cd_touch(x, yb - 17, 16, 18); /* bar мимо pop_blit_b */
gfx_set_bank(GFX_BANK_NORMAL);
pop_bar_black(x, yb - 17, 16, 18);
if (tw) pop_bar_black(x, yb - 17, tw, 18);
gfx_set_bank(GFX_BANK_TRANSPARENT);
pop_t_bake_rest = 1; /* фон = покой */
draw_tile(row, col);
@@ -926,15 +958,16 @@ void pop_floor_bake(int row, int col) __banked
{
int x = POP_COL_XH[col] * 8;
int yb = POP_YOFF + 63 * row;
uint8_t bw = bake_w(x, 60); /* обрезка по краю экрана — см. bake_w */
/* Вторая страница — копией с первой (см. bake_copy). */
if (bake_copy(&bake_pg[row * 10 + col], x, yb + 26, 60, 39)) return;
if (bake_copy(&bake_pg[row * 10 + col], x, yb + 26, bw, 39)) return;
/* Пометку ставит сам pop_bar_black — по тому же прямоугольнику; отдельный
* pop_cd_touch тут был вторым вызовом на одну область (4 502 такта). */
/* Wipe СВОЕЙ ячейки + правой грани (заходит в соседа) + перерисовка пола.
* НИЗ строго yb+64 (≈ низ передней грани пола): ниже — СТЕНА ряда ниже
* (высокий спрайт), которую тут не перерисовываем → иначе чёрный бар. */
gfx_set_bank(GFX_BANK_NORMAL);
pop_bar_black(x, yb + 26, 60, 39);
if (bw) pop_bar_black(x, yb + 26, bw, 39);
gfx_set_bank(GFX_BANK_TRANSPARENT);
/* ОКНО КЛИПА ЗДЕСЬ ОБЯЗАТЕЛЬНО — и не ради скорости.
*
@@ -954,7 +987,7 @@ void pop_floor_bake(int row, int col) __banked
*
* Что бар покрывает ВСЁ изменившееся — это инвариант самого бара (иначе
* запечка не работала бы и без копии). */
pop_t_win_set(x, yb + 26, 60, 39);
pop_t_win_set(x, yb + 26, bw, 39);
pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */
draw_tile(row, col);
if (col + 1 < 10) draw_tile(row, col + 1);
@@ -1012,7 +1045,9 @@ void pop_gate_redraw(int row, int col) __banked
void pop_loose_bake_empty(int row, int col) __banked
{
int x = POP_COL_XH[col] * 8;
int xr = POP_COL_XH[col + 1] * 8;
/* col+1 читаем ТОЛЬКО в границах массива: у последней колонки соседа нет,
* а POP_COL_XH[10] — чтение за концом (10 элементов). */
int xr = (col + 1 < 10) ? POP_COL_XH[col + 1] * 8 : 0;
int yb = 63 * row + 46;
/* Чёрная плита (opaque NORMAL — пишет в VRAM И ОЗУ-копию акселератора):
* стирает ЗАПЕЧЁННЫЙ пиксель-в-пиксель loose-пол с обоих тайлов, иначе
@@ -1030,9 +1065,9 @@ void pop_loose_bake_empty(int row, int col) __banked
pop_cd_touch(x, yb, 72, POP_YOFF + 63 * row + 66 - yb);
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK);
pop_bar_black(x, yb, 40, POP_YOFF + 63 * row + 65 - yb + 1); /* сам тайл: +40 (кусок пола заходит вправо; сосед перерисуется) */
if (bake_w(x, 40)) pop_bar_black(x, yb, bake_w(x, 40), POP_YOFF + 63 * row + 65 - yb + 1); /* сам тайл: +40 (кусок пола заходит вправо; сосед перерисуется) */
if (col + 1 < 10)
pop_bar_black(xr, yb, 32, POP_YOFF + 63 * row + 65 - yb + 1); /* сосед: РОВНО 32 — не залезть в col+2 */
if (bake_w(xr, 32)) pop_bar_black(xr, yb, bake_w(xr, 32), POP_YOFF + 63 * row + 65 - yb + 1); /* сосед: РОВНО 32 — не залезть в col+2 */
/* Новый статич. фон: сам тайл (empty → «стена вниз» topright комнаты
* снизу) + правый сосед (его левая грань: был loose-край pop_env_b(42),
* теперь пусто). TRANSPARENT — 0xFF не забивает чёрный, пишет и в