diff --git a/applications/PoP/roomtest/pop_cdraw.c b/applications/PoP/roomtest/pop_cdraw.c index 252513b..8db6b05 100644 --- a/applications/PoP/roomtest/pop_cdraw.c +++ b/applications/PoP/roomtest/pop_cdraw.c @@ -378,11 +378,28 @@ static uint8_t cd_quiet(uint8_t who, uint8_t p) return 1; if (!pop_cd[who].valid[p]) return 0; if (pop_cd_dirty & (1 << p)) { /* фон правили — но задели ли нас? */ - int x0, y0, x1, y1; - cd_bbox(who, p, &x0, &y0, &x1, &y1); - y0 += POP_YOFF; y1 += POP_YOFF; /* метка фона — в ЭКРАННЫХ */ - /* cd_bbox отдаёт x1/y1 ИСКЛЮЧИТЕЛЬНО, pop_cd_hit ждёт включительно. */ - if (pop_cd_hit(p, x0, y0, x1 - 1, y1 - 1)) return 0; + /* СПРАЙТ И НАКЛАДНОЙ (клинок, брызги) — ДВУМЯ ОТДЕЛЬНЫМИ проверками, + * а не объединённым bbox. Объединение включает пустой угол между + * ними, и он ловит касания, которых на самом деле нет: у стоящего + * стража спрайт лежит в колонке 8, клинок уходит влево в колонку 7 на + * y 59..65, а пламя факела метит колонку 7 на y 33..50. Прямоугольник + * «спрайт + клинок» (x 241..284, y 46..84) пересекается с этой меткой + * углом — и персонаж перерисовывался целиком за 148 302 такта + * (замер 2026-08-19, сцена 11/15). + * + * Для skip_mask объединение по-прежнему годится: там вопрос «рядом ли + * два слота», и загрубление в бОльшую сторону безопасно. */ + const pop_cdraw_t *q = &pop_cd[who]; + int y0 = q->y[p] + POP_YOFF; + if (pop_cd_hit(p, q->x[p], y0, + q->x[p] + (int)q->w[p] - 1, y0 + (int)q->h[p] - 1)) + return 0; + if (q->ovalid[p]) { + int oy = q->oy[p] + POP_YOFF; + if (pop_cd_hit(p, q->ox[p], oy, + q->ox[p] + (int)q->ow[p] - 1, oy + (int)q->oh[p] - 1)) + return 0; + } } cd_sig_make(who, &g); a = (const uint8_t *)&g; b = (const uint8_t *)&cd_sig[who][p]; diff --git a/applications/PoP/roomtest/pop_cdraw.h b/applications/PoP/roomtest/pop_cdraw.h index 7da2c7d..3545047 100644 --- a/applications/PoP/roomtest/pop_cdraw.h +++ b/applications/PoP/roomtest/pop_cdraw.h @@ -121,11 +121,16 @@ void pop_char_set_render_dx(uint8_t who, int dx) __banked; * Резидент (pop_tile.c): зовёт и банковый слой фона, и резидентные листья, * а трамплин на КАЖДЫЙ кусок фона стоил бы дороже самой метки. */ extern uint8_t pop_cd_dirty; /* биты страниц: метка взведена */ -extern uint16_t pop_cd_dmask[2][3]; /* [страница][ряд] = биты колонок 0..9 */ +/* [страница][колонка] = диапазон затронутых ЭКРАННЫХ y; пусто = ymin 255, + * ymax 0. Раньше тут была маска колонок на три ряда по 63 px — она + * склеивала касания внутри ряда и заставляла перерисовывать нетронутого + * персонажа (разбор в шапке pop_cd_touch, pop_tile.c). */ +extern uint8_t pop_cd_ymin[2][10], pop_cd_ymax[2][10]; /* Задели ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 включительно) то, * что трогали на странице p. Резидент (pop_tile.c). */ uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1); void pop_cd_clear(uint8_t p); /* снять метку страницы целиком */ +void pop_cd_init(void); /* пустые диапазоны на обеих страницах */ void pop_cd_touch(int x, int y, int w, int h); #define POP_CD_TOUCH_ALL() pop_cd_touch(0, 0, 320, 256) diff --git a/applications/PoP/roomtest/pop_tile.c b/applications/PoP/roomtest/pop_tile.c index 5472195..2d6b0b3 100644 --- a/applications/PoP/roomtest/pop_tile.c +++ b/applications/PoP/roomtest/pop_tile.c @@ -195,43 +195,34 @@ uint8_t pop_spike_frame(uint8_t m) * и перерисовывался каждый кадр со всем fore-проходом (замер: циан 231 829 * тактов против 20 071, период 4 растровых кадра против 3). * - * Гранулярность тайла (32 x 63) вместо точного прямоугольника — осознанное - * огрубление: факел помечает всю свою колонку по высоте ряда. Персонаж и - * так занимает бОльшую часть высоты ряда, зато касания перестают - * склеиваться. См. docs/impl_diff.md. */ + * ГРАНУЛЯРНОСТЬ: колонка 32 px по горизонтали, ТОЧНЫЙ диапазон y по + * вертикали. Раньше по вертикали стоял номер ряда (три полосы по 63 px), и + * это склеивало касания, разнесённые внутри ряда. Найдено пользователем на + * сцене 11/15 (2026-08-19): пламя факела занимает y 5..22, клинок стоящего + * стража — y 31..37, между ними девять пикселей чистого зазора, а метка + * считала слот задетым, потому что оба попадают в ряд 0 и колонку 7. + * Стоило это 148 302 такта в кадр — 23 % работы — на перерисовку персонажа, + * которого никто не трогал. + * + * Почему диапазон, а не более мелкие полосы: полосы по 16 px эту пару всё + * равно склеивают (пламя кончается в полосе 1, клинок в ней же начинается), + * а 8-пиксельные потребовали бы 24 маски на страницу. Пара ymin/ymax на + * колонку — 40 байт на обе страницы, точнее любых полос и без битовой возни. + * + * У ОРИГИНАЛА такой метки нет вовсе: и Apple II (FRAMEADV.S RedBlockFast с + * шестью буферами wipebuf/redbuf/movebuf/floorbuf/halfbuf/objbuf), и SDLPoP + * (set_redraw_fore в redraw_at_char) метят ЦЕЛЫМИ тайлами, но им это не + * мешает — персонаж у них рисуется каждый кадр безусловно, а пометки нужны + * только фону. Пропуск неизменившегося персонажа — наша добавка, поэтому и + * точность метки нужна выше оригинальной. См. docs/impl_diff.md. */ uint8_t pop_cd_dirty; -uint16_t pop_cd_dmask[2][3]; +/* [страница][колонка] — диапазон затронутых экранных y. Пусто = ymin > ymax + * (заполняется как ymin = 255, ymax = 0). */ +uint8_t pop_cd_ymin[2][10], pop_cd_ymax[2][10]; -/* 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]); -} +/* CD_LOW, cd_row_of и cd_cols_of СНЯТЫ вместе с переходом на диапазон y: + * колонка теперь считается прямым сдвигом (x >> 5), а вертикаль сравнением + * отрезков — битовые маски больше не нужны. */ /* Обе страницы помечаются сразу (персонаж чинится на каждой в свой кадр), * поэтому цикл развёрнут: индекс-переменная заставляла SDCC считать адрес @@ -274,8 +265,6 @@ void pop_cd_unmute(void) { pop_cd_batch = 0; } void pop_cd_touch(int x, int y, int w, int h) { - uint16_t cols; - uint8_t r0, r1; if (pop_cd_batch) { /* копим, не разбирая на колонки/ряды */ if (pop_cd_batch == CD_BATCH_MUTE) return; /* область помечена вызывающим */ int x1 = x + w - 1, y1 = y + h - 1; @@ -285,18 +274,30 @@ void pop_cd_touch(int x, int y, int w, int h) if (y1 > cdb_y1) cdb_y1 = y1; return; } - cols = cd_cols_of(x, x + w - 1); - 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; + { + int8_t c0, c1, c; + uint8_t y0, y1; + int xr = x + w - 1; + if (xr < 0 || x > 319) return; /* весь прямоугольник вне поля */ + if (x < 0) x = 0; + if (xr > 319) xr = 319; + c0 = (int8_t)(x >> 5); + c1 = (int8_t)(xr >> 5); + /* Клип по вертикали: экранные y не выходят за байт, а всё, что выше + * поля или ниже его, персонажам всё равно не принадлежит. */ + if (y < 0) y = 0; + if (y > 255) return; + y0 = (uint8_t)y; + y1 = (y + (int)h - 1 > 255) ? 255 : (uint8_t)(y + h - 1); + /* Страницы обновляются НЕЗАВИСИМО. Общее условие по странице 0 + * («если ей стало теснее — записать в обе») ломается сразу после + * pop_cd_clear(0): страница 1 хранит свои старые границы, условие по + * нулевой уже не выполняется, и её метка перестаёт расти. */ + for (c = c0; c <= c1; c++) { + if (y0 < pop_cd_ymin[0][c]) pop_cd_ymin[0][c] = y0; + if (y1 > pop_cd_ymax[0][c]) pop_cd_ymax[0][c] = y1; + if (y0 < pop_cd_ymin[1][c]) pop_cd_ymin[1][c] = y0; + if (y1 > pop_cd_ymax[1][c]) pop_cd_ymax[1][c] = y1; } } pop_cd_dirty = 3; @@ -307,25 +308,39 @@ void pop_cd_touch(int x, int y, int w, int h) * вызовом, без трамплина. */ uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1) { - uint16_t cols; - uint8_t r0, r1; + int8_t c0, c1, c; + uint8_t ya, yb; 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; - } + if (x1 < 0 || x0 > 319) return 0; + if (x0 < 0) x0 = 0; + if (x1 > 319) x1 = 319; + if (y1 < 0 || y0 > 255) return 0; + ya = (uint8_t)(y0 < 0 ? 0 : y0); + yb = (uint8_t)(y1 > 255 ? 255 : y1); + c0 = (int8_t)(x0 >> 5); + c1 = (int8_t)(x1 >> 5); + /* Пересечение отрезков [ya,yb] и [ymin,ymax] хотя бы в одной колонке. + * Пустая колонка держит ymin = 255, ymax = 0 — условие ниже её отсеет + * само, отдельной проверки «пусто» не нужно. */ + for (c = c0; c <= c1; c++) + if (ya <= pop_cd_ymax[p][c] && yb >= pop_cd_ymin[p][c]) return 1; return 0; } /* Страница приведена в порядок — снять с неё метку целиком. */ +/* Начальное состояние обеих страниц — «ничего не трогали». ЯВНО, а не + * расчётом на обнуление _DATA: пустая колонка обозначается ymin = 255, + * ymax = 0, а нули от crt0 читались бы как «затронута строка 0». */ +void pop_cd_init(void) +{ + pop_cd_clear(0); + pop_cd_clear(1); +} + void pop_cd_clear(uint8_t p) { - pop_cd_dmask[p][0] = pop_cd_dmask[p][1] = pop_cd_dmask[p][2] = 0; + uint8_t c; + for (c = 0; c < 10; c++) { pop_cd_ymin[p][c] = 255; pop_cd_ymax[p][c] = 0; } pop_cd_dirty = (uint8_t)(pop_cd_dirty & ~(1 << p)); } diff --git a/applications/PoP/roomtest/roomtest_cold.c b/applications/PoP/roomtest/roomtest_cold.c index d5c7a1f..057140b 100644 --- a/applications/PoP/roomtest/roomtest_cold.c +++ b/applications/PoP/roomtest/roomtest_cold.c @@ -680,6 +680,7 @@ int pop_boot(void) __banked puts("pop_bg_load failed"); return -1; } + pop_cd_init(); /* метка «фон трогали» — пустые диапазоны */ pop_cheats = 1; /* режим разработки: читы включены */ pop_guard_reset(); if (pop_kid_data_load("KID\\kid_data.bin") != 0) { /* кадры+seqtbl в EMM-странице */