P15: метка «фон трогали» стала точной — минус 167 880 тактов на кадре

Две правки, обе про ложные срабатывания пропуска отрисовки персонажа.

1. МЕТКА: вместо «маска колонок по 32 px на ТРИ ряда по 63 px» теперь на
   каждую колонку хранится диапазон затронутых y (ymin/ymax, 40 байт на обе
   страницы).  Прежняя гранулярность склеивала касания внутри ряда: пламя
   факела занимает y 33..50, клинок стоящего стража — y 59..65, между ними
   девять пикселей зазора, а метка считала слот задетым.

2. ПРОВЕРКА: cd_quiet сверяет с меткой спрайт и накладной (клинок, брызги)
   ДВУМЯ ОТДЕЛЬНЫМИ прямоугольниками, а не объединённым bbox.  Объединение
   включает пустой угол между ними, и он ловил касания, которых нет: спрайт
   стража лежит в колонке 8, клинок уходит в колонку 7 на y 59..65, пламя
   метит колонку 7 на y 33..50 — прямоугольник «спрайт + клинок»
   (x 241..284, y 46..84) цеплял метку углом.

Без второй правки первая почти ничего не дала (632 676 против 628 542 до
неё): объединённый bbox продолжал ловить ложное пересечение.

Замер 11/15:

  фаза      до P15    после
  синяя    259 050   223 902   (heal тоже перестал платить)
  зелёная  181 494   181 494
  циан     194 262    59 406
  работа   632 676   464 796

Проверено в MAME: в статике картинка чистая, в динамике (пробежка, бой,
переход в соседнюю комнату) хвостов и просвечивания нет.  Хост-тесты
зелёные.

Заодно найден и исправлен собственный баг первой редакции: обе страницы
обновлялись по условию, проверяющему только страницу 0, и после
pop_cd_clear(0) метка страницы 1 переставала расти.  Плюс pop_cd_init:
пустая колонка обозначается ymin = 255, а нули от crt0 читались бы как
«затронута строка 0».

У ОРИГИНАЛА такой метки нет вовсе: и Apple II (FRAMEADV.S RedBlockFast,
шесть буферов по блокам), и SDLPoP (set_redraw_fore) метят целыми тайлами,
но им это не мешает — персонаж у них рисуется каждый кадр безусловно.
Пропуск неизменившегося персонажа — наша добавка, поэтому и точность метки
нужна выше оригинальной.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 16:04:46 +03:00
parent 17c41b32de
commit 767d6f78a4
4 changed files with 105 additions and 67 deletions
+76 -61
View File
@@ -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));
}