Уровень 4: skip по маске тайлов + порт loose_land, ворота 0xFF, пламя в фон

ОПТИМИЗАЦИЯ.  Метка «фон трогали» была одним union-прямоугольником на
страницу, и три факела комнаты склеивались в полосу x 40..280 на всю
комнату — Кид, стоящий между крайними факелами, терял пропуск перерисовки
и каждый кадр платил полным fore-проходом.  Теперь это маска тайлов
(uint16_t pop_cd_dmask[2][3]: бит = колонка, слово = ряд, набор = страница),
проверка — три AND через резидентный pop_cd_hit.  Гранулярность тайла — это
гранулярность оригинала (redraw_frames_anim[tilepos], set_wipe;
подтайловое уточнение там только по высоте, wipe_heights).  Замер на (1,7):
циан 233 515 -> 24 781, работа за кадр 517 609 -> 306 553, период 4 -> 3
растровых кадра.

BUG-LOOSE-BUTTON-1.  Упавшая плита не нажимала кнопку.  Три слоя: порт
loose_land не звал trigger_button вовсе; pop_room_col_landing считала
площадкой только чистый пол, а у оригинала их семь (пол, пика, обе кнопки,
зелье, оба факела); сигнал приходил в момент ОТРЫВА плиты, из-за чего
ворота начинали открываться, пока она ещё в воздухе.  Нажатие в оригинале
ОДНО, но с button_type = tiles_14_debris — это «открыть НАСОВСЕМ»
(modifier 0xFF), и кнопка съедается.  Посадка в комнате снизу переехала на
новый сигнал pop_loose_exit (взводит mob_tick_one, когда кусок ушёл ниже
поля).

BUG-GATE-FF-1.  0xFF был перегружен: сторожевое «тайла нет» в gate_modif и
живое «открыто навсегда» из trigger_gate.  gate_passable заворачивал Кида в
воротах, нарисованных открытыми.  Мёртвая ветка убрана.

BUG-TORCH-CHOMP-1.  Чомпер (0,7) комнаты 23 healит x 224..255 / y 30..93 и
стирал пламя факела (0,6), которое рисуется в ячейке правого соседа.  Фикс —
запекать пламя (GFX_BANK_NORMAL): у факела все девять кадров на общем
канвасе 16x18 и непрозрачны, протухнуть в ОЗУ-копии нечему, а heal чомпера
сам возвращает огонь и кладёт челюсти поверх — z-порядок как в оригинале.
Пузырёк зелья так нельзя (ползёт вверх, нужен heal) — остаётся в SPRITE.

Заведено открытым: died_on_button (seg007:776) не портирован — нужен тайл
tiles_5_stuck в атласе.

Проверено пользователем в MAME (уровень 4: плита 16(1,1) -> кнопка 17(0,1)
-> ворота 23(0,9); комната 23 с чомпером и факелом); make -C tests-host —
все 5 наборов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 17:52:04 +03:00
parent 6a824dd7ab
commit 4718bff767
13 changed files with 364 additions and 47 deletions
+91 -27
View File
@@ -158,40 +158,104 @@ uint8_t pop_loose_frame(uint8_t m)
return m;
}
/* ---- «Фон трогали ВОТ ЗДЕСЬ» (DRAW-COST; контракт — pop_cdraw.h) ----- */
uint8_t pop_cd_dirty;
int pop_cd_dx0[2], pop_cd_dy0[2], pop_cd_dx1[2], pop_cd_dy1[2];
/* ---- «Фон трогали ВОТ ЗДЕСЬ» (DRAW-COST; контракт — pop_cdraw.h) ----- *
*
* Метка — БИТОВАЯ МАСКА ТАЙЛОВ, по слову на ряд и по набору на страницу.
* Раньше здесь лежал ОДИН union-прямоугольник на страницу, и это убивало
* пропуск перерисовки персонажа: три факела комнаты трогают по пятнышку
* 16x18 в колонках 1, 6 и 8, а их объединение — полоса x 40..280 на всю
* комнату. Кид, стоящий где угодно между крайними факелами, попадал в неё
* и перерисовывался каждый кадр со всем fore-проходом (замер: циан 231 829
* тактов против 20 071, период 4 растровых кадра против 3).
*
* Гранулярность тайла (32 x 63) вместо точного прямоугольника — осознанное
* огрубление: факел помечает всю свою колонку по высоте ряда. Персонаж и
* так занимает бОльшую часть высоты ряда, зато касания перестают
* склеиваться. См. docs/impl_diff.md. */
uint8_t pop_cd_dirty;
uint16_t pop_cd_dmask[2][3];
/* Цикл по страницам развёрнут, а x+w / y+h посчитаны один раз. Не
* вкусовщина: индекс-переменная заставляла SDCC считать адрес каждой из
* восьми ячеек сложением, а `x + w` в условии и в присваивании считались
* дважды — замер в MAME дал 6 846 тактов на вызов, а зовётся он с КАЖДОГО
* блита фона (два факела в комнате — уже 13 700 за кадр). */
/* CD_LOW[n] = n младших единиц: маска пробега колонок c0..c1 считается как
* CD_LOW[c1+1] & ~CD_LOW[c0] — два чтения таблицы вместо цикла сдвигов. */
static const uint16_t CD_LOW[11] = {
0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F,
0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF
};
/* Экранный y -> ряд комнаты 0..2 с клампом (полоса кладки у потолка и низ
* стены ложатся на крайние ряды). Цепочкой сравнений, а не делением на 63:
* у SDCC z80 одно деление ~5 400 тактов (memory sdcc_z80_division_hoisting). */
static uint8_t cd_row_of(int y)
{
y -= POP_YOFF;
if (y < 63) return 0;
if (y < 126) return 1;
return 2;
}
/* Колонки прямоугольника [x..x1] (включительно) -> битовая маска. Тайл ровно
* 32 px и начинается с x=0, поэтому колонка — просто сдвиг. */
static uint16_t cd_cols_of(int x, int x1)
{
uint8_t c0, c1;
if (x < 0) x = 0;
if (x1 > 319) x1 = 319;
if (x1 < x) return 0; /* весь прямоугольник вне экрана */
c0 = (uint8_t)(x >> 5);
c1 = (uint8_t)(x1 >> 5);
return (uint16_t)(CD_LOW[c1 + 1] & ~CD_LOW[c0]);
}
/* Обе страницы помечаются сразу (персонаж чинится на каждой в свой кадр),
* поэтому цикл развёрнут: индекс-переменная заставляла SDCC считать адрес
* каждой ячейки сложением. Зовётся с КАЖДОГО блита фона. */
void pop_cd_touch(int x, int y, int w, int h)
{
int x1 = x + w, y1 = y + h;
uint8_t d = pop_cd_dirty;
if (!(d & 1)) {
pop_cd_dx0[0] = x; pop_cd_dy0[0] = y;
pop_cd_dx1[0] = x1; pop_cd_dy1[0] = y1;
} else {
if (x < pop_cd_dx0[0]) pop_cd_dx0[0] = x;
if (y < pop_cd_dy0[0]) pop_cd_dy0[0] = y;
if (x1 > pop_cd_dx1[0]) pop_cd_dx1[0] = x1;
if (y1 > pop_cd_dy1[0]) pop_cd_dy1[0] = y1;
}
if (!(d & 2)) {
pop_cd_dx0[1] = x; pop_cd_dy0[1] = y;
pop_cd_dx1[1] = x1; pop_cd_dy1[1] = y1;
} else {
if (x < pop_cd_dx0[1]) pop_cd_dx0[1] = x;
if (y < pop_cd_dy0[1]) pop_cd_dy0[1] = y;
if (x1 > pop_cd_dx1[1]) pop_cd_dx1[1] = x1;
if (y1 > pop_cd_dy1[1]) pop_cd_dy1[1] = y1;
uint16_t cols = cd_cols_of(x, x + w - 1);
uint8_t r0, r1;
if (!cols) return;
r0 = cd_row_of(y);
r1 = cd_row_of(y + h - 1);
pop_cd_dmask[0][r0] |= cols;
pop_cd_dmask[1][r0] |= cols;
if (r1 != r0) {
pop_cd_dmask[0][r1] |= cols;
pop_cd_dmask[1][r1] |= cols;
if (r1 - r0 > 1) { /* прямоугольник накрыл все три ряда */
pop_cd_dmask[0][1] |= cols;
pop_cd_dmask[1][1] |= cols;
}
}
pop_cd_dirty = 3;
}
/* Задел ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 ВКЛЮЧИТЕЛЬНО) то, что
* трогали на странице p. Резидент: зовёт pop_cdraw.c из банка 4 прямым
* вызовом, без трамплина. */
uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1)
{
uint16_t cols;
uint8_t r0, r1;
if (!(pop_cd_dirty & (1 << p))) return 0;
cols = cd_cols_of(x0, x1);
if (!cols) return 0;
r0 = cd_row_of(y0);
r1 = cd_row_of(y1);
if (pop_cd_dmask[p][r0] & cols) return 1;
if (r1 != r0) {
if (pop_cd_dmask[p][r1] & cols) return 1;
if (r1 - r0 > 1 && (pop_cd_dmask[p][1] & cols)) return 1;
}
return 0;
}
/* Страница приведена в порядок — снять с неё метку целиком. */
void pop_cd_clear(uint8_t p)
{
pop_cd_dmask[p][0] = pop_cd_dmask[p][1] = pop_cd_dmask[p][2] = 0;
pop_cd_dirty = (uint8_t)(pop_cd_dirty & ~(1 << p));
}
/* ---- Блит куска атласа ---------------------------------------------- */
/* Клипованный путь — ОТДЕЛЬНОЙ функцией. У него девять 16-битных локалей,
* и SDCC заводит под них кадр IX; пока он жил в общем теле, за этот кадр