Оптимизация каскада плит: -27% работы за кадр (пять правок с замерами)

Сцена — ур.13 комната 23, шесть плит с потолка (docs/perf_l13_room23.md).
Все замеры сняты одним и тем же прогоном «ESC -> зонды -> roomtest».

                       было       стало
  работа за кадр    1 437 150   1 051 332   -27%
  зелёная (фон)       805 000     575 730   -28%
  циан (спрайты)      631 800     438 546   -31%
  синяя                142 830     142 830   в бюджете

Цена одной перерисовки:
  RDA_CEIL_GONE (запечь колодец)  251 335 -> 137 975   -45%
  RD_FLOOR (щебень на посадке)    198 805 -> 182 124    -8%
  RDA_CEIL (дрожащая плита)        48 785 ->  43 543   -11%

C1. Пометки «фон трогали» в mob_render ПОДАВЛЕНЫ (pop_cd_mute): коридор
куска уже помечен одним вызовом в pop_loose_mob_tick, а три блита метили
подмножества того же прямоугольника по 4 502 такта.  Плюс пометка в
mob_spawn_copy — кусок, рождённый внутри тика, свою мог не получить
(слот выдаётся с начала таблицы, то есть уже пройденный).  -55 тыс.

G2. Контекст тайла — file-scope, а не локали draw_tile (порт
load_curr_and_left_tile, seg008:0339).  В функции 57 вызовов, и каждое
живое через вызов значение спиливалось: 26 байт кадра и 513 обращений
`-N(ix)`.  Стало 33 обращения, кадра нет, банк 7 -703 Б.  -55 тыс.

G1. Окно клипа для точечной перерисовки (pop_t_win_set/clear).  В
pop_ceil_bake_empty куски ряда 0 высотой 63 px рисовались целиком, хотя
восстановить надо девять строк: блит стоил 21 447, стал 7 619.  -90 тыс.
В pop_floor_bake окно попробовано и ОТКАЧЕНО (там нет клипа, блиты шли
быстрым путём, а окно уводило их в blit_b_clip и не отсеивало ничего:
187 266 -> 198 279) — вернуться после G3, запись в коде.

G3. blit_b_clip: байтовый габарит + file-scope вместо локалей.  Одного
байтового габарита НЕ ХВАТИЛО (кадр остался, 211 -> 173 обращений) —
значений, живых через шесть вызовов ядер libbgi, больше, чем регистров.
С file-scope: 51 обращение, кадр 22 -> 12 Б.  Клипованный блит подешевел.

C4. Падающий кусок КЛИПУЕТСЯ сам, вместо чистки бортов после.  Раньше он
рисовался в борт целиком и взводил border_dirty, а pop_room_clip_borders
стирал две полосы 320x28 — 150 978 тактов в каждом кадре, пока хоть один
кусок торчит выше поля (гряда 13-го рождается ровно у потолка, y=2), то
есть почти весь каскад.  Стало 1 722.  -138 тыс.

Заодно blit_b_oversize больше не ходит через blit_b_clip (у того габарит
теперь байтовый) — рисует напрямую gfx_blit_part.  Это путь под
полноэкранные подложки интро/финала (320x200, docs/perf_l13_room23.md §5).

Проверено: 8 наборов tests-host зелёные; в MAME каскад рисуется корректно
(кадр с летящими плитами, чистые борта, итоговая картинка как до правок).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 13:39:57 +03:00
parent babd40bc84
commit a3c473d910
4 changed files with 376 additions and 171 deletions
+133 -32
View File
@@ -246,11 +246,20 @@ void pop_cd_batch_end(void)
pop_cd_touch(cdb_x0, cdb_y0, cdb_x1 - cdb_x0 + 1, cdb_y1 - cdb_y0 + 1);
}
/* Подавление на серию блитов — см. шапку в pop_cdraw.h. Отдельным значением
* того же флага, а не вторым флагом: у pop_cd_touch тогда остаётся ОДНА
* проверка на общем пути. */
#define CD_BATCH_MUTE 2
void pop_cd_mute(void) { pop_cd_batch = CD_BATCH_MUTE; }
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;
if (x < cdb_x0) cdb_x0 = x;
if (y < cdb_y0) cdb_y0 = y;
@@ -310,12 +319,65 @@ void pop_cd_clear(uint8_t p)
* перерисовка тайлов — идёт с pop_t_fclip_on == 0. */
#define FLIP_TOP(top, h) POP_FLIP_TOP(top, h) /* формула — в pop_bg.h */
static void blit_b_clip(const uint8_t *img, int x, int top, int w, int h)
/* Окно под точечную перерисовку/запечку — контракт в шапке pop_tile.h.
* Логическая пара нужна для отсева тайлов (он считается из ряда комнаты),
* экранная — для клипа самого блита; при перевороте они разные. */
void pop_t_win_set(int x, int ytop, int w, int h)
{
int dx = x, dy = top, dw = w, dh = h, sx = 0, sy = 0;
pop_t_fclip_x0 = x;
pop_t_fclip_x1 = x + w;
pop_t_fclip_ly0 = ytop;
pop_t_fclip_ly1 = ytop + h;
if (pop_upside) {
int t = FLIP_TOP(ytop, h);
pop_t_fclip_y0 = t;
pop_t_fclip_y1 = t + h;
} else {
pop_t_fclip_y0 = ytop;
pop_t_fclip_y1 = ytop + h;
}
pop_t_fclip_on = 1;
}
void pop_t_win_clear(void) { pop_t_fclip_on = 0; }
/* Клипованный путь блита. Габарит и смещения внутри куска — БАЙТЫ, а вся
* рабочая переменная — file-scope, НЕ локали.
*
* Корректность байтового габарита по построению: кадры наших атласов не
* крупнее 56x63 (просканированы все 109 .atl, docs/perf_l13_room23.md §5), а
* кадр больше 255 в pop_blit_b уходит в blit_b_oversize и сюда не попадает.
*
* Зачем file-scope: с локалями SDCC заводил 22-байтовый стековый кадр и 211
* обращений `-N(ix)` — при ~46 замеренных тактах на обращение это ~9 700
* такта из 14 088 цены клипованного блита (замер 2026-08-17,
* docs/perf_green_phase.md §1.6). Одного байтового габарита НЕ ХВАТИЛО:
* кадр остался, обращений стало 173 — значений, живых через шесть вызовов
* ядер libbgi, всё равно больше, чем регистров у Z80. С file-scope
* спиливать нечего, а адресация абсолютная (13 тактов против 19).
* Тот же приём и по той же причине применён к draw_tile (pop_room.c).
*
* Реентерабельности не было и не требуется: единственный вызывающий —
* pop_blit_b, и он себя не зовёт.
*
* Арифметика клипа считается в int16 (bc_d), в байт уходит только результат —
* иначе вычитание ушло бы в перенос по модулю 256. */
static const uint8_t *bc_img;
static int bc_dx, bc_dy;
static int16_t bc_d;
static uint8_t bc_w, bc_h, bc_dw, bc_dh, bc_sx, bc_sy;
static void blit_b_clip(const uint8_t *img, int x, int top, uint8_t w, uint8_t h)
{
bc_img = img; bc_w = w; bc_h = h;
bc_dx = x; bc_dy = top;
bc_dw = w; bc_dh = h; bc_sx = 0; bc_sy = 0;
if (!pop_upside) {
if (pop_t_clip_top && dy < pop_t_clip_top) { /* полоса у потолка */
sy = pop_t_clip_top - dy; dy = pop_t_clip_top; dh -= sy;
if (pop_t_clip_top && bc_dy < pop_t_clip_top) { /* полоса у потолка */
bc_d = (int16_t)(pop_t_clip_top - bc_dy);
if (bc_d >= (int16_t)bc_dh) return;
bc_sy = (uint8_t)bc_d; bc_dy = pop_t_clip_top;
bc_dh = (uint8_t)(bc_dh - bc_sy);
}
} else {
/* ПЕРЕВОРОТ: то, что в обычном виде торчало ВЫШЕ поля (полоса кладки
@@ -323,56 +385,95 @@ static void blit_b_clip(const uint8_t *img, int x, int top, int w, int h)
* где живёт полоса HP (баг «мусор ниже 192», найден пользователем).
* Режем по границам поля ВСЕГДА, а не по pop_t_clip_top: зеркальный
* спрайт может вылезти и там, где в обычном виде клип не ставили. */
if (dy < POP_YOFF) { sy = POP_YOFF - dy; dy = POP_YOFF; dh -= sy; }
if (dy + dh > POP_CLIP_BOTTOM) dh = POP_CLIP_BOTTOM - dy;
if (bc_dy < POP_YOFF) {
bc_d = (int16_t)(POP_YOFF - bc_dy);
if (bc_d >= (int16_t)bc_dh) return;
bc_sy = (uint8_t)bc_d; bc_dy = POP_YOFF;
bc_dh = (uint8_t)(bc_dh - bc_sy);
}
if (bc_dy + bc_dh > POP_CLIP_BOTTOM) {
bc_d = (int16_t)(POP_CLIP_BOTTOM - bc_dy);
if (bc_d <= 0) return;
bc_dh = (uint8_t)bc_d;
}
}
if (pop_t_fclip_on) { /* окно fore-слоя */
if (dx < pop_t_fclip_x0) { int d = pop_t_fclip_x0 - dx; sx += d; dx += d; dw -= d; }
if (dy < pop_t_fclip_y0) { int d = pop_t_fclip_y0 - dy; sy += d; dy += d; dh -= d; }
if (dx + dw > pop_t_fclip_x1) dw = pop_t_fclip_x1 - dx;
if (dy + dh > pop_t_fclip_y1) dh = pop_t_fclip_y1 - dy;
if (pop_t_fclip_on) { /* окно fore-слоя / перерисовки */
if (bc_dx < pop_t_fclip_x0) {
bc_d = (int16_t)(pop_t_fclip_x0 - bc_dx);
if (bc_d >= (int16_t)bc_dw) return;
bc_sx = (uint8_t)(bc_sx + (uint8_t)bc_d);
bc_dx += bc_d; bc_dw = (uint8_t)(bc_dw - (uint8_t)bc_d);
}
if (bc_dy < pop_t_fclip_y0) {
bc_d = (int16_t)(pop_t_fclip_y0 - bc_dy);
if (bc_d >= (int16_t)bc_dh) return;
bc_sy = (uint8_t)(bc_sy + (uint8_t)bc_d);
bc_dy += bc_d; bc_dh = (uint8_t)(bc_dh - (uint8_t)bc_d);
}
if (bc_dx + bc_dw > pop_t_fclip_x1) {
bc_d = (int16_t)(pop_t_fclip_x1 - bc_dx);
if (bc_d <= 0) return;
bc_dw = (uint8_t)bc_d;
}
if (bc_dy + bc_dh > pop_t_fclip_y1) {
bc_d = (int16_t)(pop_t_fclip_y1 - bc_dy);
if (bc_d <= 0) return;
bc_dh = (uint8_t)bc_d;
}
}
if (dw <= 0 || dh <= 0) return;
if (!bc_dw || !bc_dh) return;
/* ПЕРЕВОРОТ: экранная строка (dy+i) берёт источник (h-1-i), поэтому
* обрезка СВЕРХУ экрана съедает НИЖНИЕ строки источника. В терминах
* куска источника это sy' = h - sy - dh (при обрезке только сверху даёт
* 0, при обрезке только снизу — верхний край). */
/* Проверки `dw < 256 && dh < 256 && w < 256` убраны как тождественные:
* это уже байты. */
if (pop_upside) {
int fsy = h - sy - dh;
if (fsy < 0) fsy = 0;
if (dx >= 0 && dy >= 0 && dw < 256 && dh < 256 &&
dx + dw <= 320 && dy + dh <= 256 && w < 256)
gfx_blit_part_noclip_vflip(dx, dy, img, (uint8_t)sx, (uint8_t)fsy,
(uint8_t)dw, (uint8_t)dh);
bc_d = (int16_t)bc_h - (int16_t)bc_sy - (int16_t)bc_dh;
if (bc_d < 0) bc_d = 0;
if (bc_dx >= 0 && bc_dy >= 0 &&
bc_dx + bc_dw <= 320 && bc_dy + bc_dh <= 256)
gfx_blit_part_noclip_vflip(bc_dx, bc_dy, bc_img, bc_sx,
(uint8_t)bc_d, bc_dw, bc_dh);
else
gfx_blit_part_vflip(dx, dy, img, sx, fsy, dw, dh);
gfx_blit_part_vflip(bc_dx, bc_dy, bc_img, bc_sx,
(uint8_t)bc_d, bc_dw, bc_dh);
} else
if (dx >= 0 && dy >= 0 && dw < 256 && dh < 256 &&
dx + dw <= 320 && dy + dh <= 256 && w < 256) {
if (sx == 0 && sy == 0 && dw == w && dh == h)
gfx_blit_noclip(dx, dy, img);
if (bc_dx >= 0 && bc_dy >= 0 &&
bc_dx + bc_dw <= 320 && bc_dy + bc_dh <= 256) {
if (bc_sx == 0 && bc_sy == 0 && bc_dw == bc_w && bc_dh == bc_h)
gfx_blit_noclip(bc_dx, bc_dy, bc_img);
else
gfx_blit_part_noclip(dx, dy, img, (uint8_t)sx, (uint8_t)sy,
(uint8_t)dw, (uint8_t)dh);
gfx_blit_part_noclip(bc_dx, bc_dy, bc_img, bc_sx, bc_sy,
bc_dw, bc_dh);
} else {
gfx_blit_part(dx, dy, img, sx, sy, dw, dh); /* край экрана */
gfx_blit_part(bc_dx, bc_dy, bc_img, bc_sx, bc_sy, bc_dw, bc_dh);
}
/* Кусок ФОНА лёг туда, где может лежать уже нарисованный персонаж —
* пометить область (DRAW-COST, pop_cdraw.h). Признак «это не fore-проход
* поверх персонажа» — окно клипа: его ставит только он. */
if (!pop_t_fclip_on) pop_cd_touch(dx, dy, dw, dh);
if (!pop_t_fclip_on) pop_cd_touch(bc_dx, bc_dy, bc_dw, bc_dh);
}
/* Кадр шире или выше 255 — общий (медленный) путь. Вынесен отдельно, чтобы
* 16-битная арифметика габарита не жила в горячем pop_blit_b: у нас таких
* кадров нет вовсе (максимум 32x63), но контракт формата .atl их допускает. */
* 16-битная арифметика габарита не жила ни в горячем pop_blit_b, ни в
* blit_b_clip (у того габарит теперь БАЙТОВЫЙ, см. его шапку). У нас таких
* кадров в игровом кадре нет вовсе (максимум 56x63), но контракт формата
* .atl их допускает, а интро/финал такие подложки принесут (320x200) —
* см. docs/perf_l13_room23.md §5.
*
* Клипа полосы у потолка и окна перерисовки здесь нет намеренно: такой кадр
* — полноэкранная подложка, ей ни то, ни другое не адресовано; за клип по
* экрану отвечает само ядро gfx_blit_part. */
static void blit_b_oversize(const uint8_t *img, int x, int ybottom)
{
uint16_t w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
uint16_t h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
int top = ybottom - (int)h + 1 + POP_YOFF;
if (pop_upside) top = FLIP_TOP(top, h);
if (w && h) blit_b_clip(img, x, top, (int)w, (int)h);
if (!w || !h) return;
gfx_blit_part(x, top, img, 0, 0, (int)w, (int)h);
if (!pop_t_fclip_on) pop_cd_touch(x, top, (int)w, (int)h);
}
void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
@@ -429,7 +530,7 @@ void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
/* В перевёрнутом виде быстрый путь годится, только если спрайт
* целиком внутри поля: иначе он залезет в борт (см. blit_b_clip). */
if (pop_upside && (top < POP_YOFF || top + (int)h > POP_CLIP_BOTTOM)) {
blit_b_clip(img, x, top, (int)w, (int)h);
blit_b_clip(img, x, top, w, h);
gfx_w0_unmap();
return;
}
@@ -447,7 +548,7 @@ void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
pop_cd_touch(x, top, (int)w, (int)h);
pop_dbg_b4(); /* ВРЕМЕННО */
} else {
blit_b_clip(img, x, top, (int)w, (int)h);
blit_b_clip(img, x, top, w, h);
pop_dbg_b3(); pop_dbg_b4(); /* ВРЕМЕННО: медленный путь целиком в b3 */
}
}