DRAW-COST шаг 1: пропуск неизменившегося персонажа — 210% -> 116% кадра

Персонаж, у которого с прошлой отрисовки ЭТОЙ страницы дабл-буфера не
изменился ни один вход отрисовки, а фон в его прямоугольнике не трогали,
уже нарисован правильно: heal, блит и fore-проход пропускаются целиком.
Не спецкейс «мёртвый страж», а общее правило — покрывает и труп, и
стоящего Кида, и ждущего стража.

Механизм: снимок входов по страницам (pop_cdraw.c, cd_sig/cd_quiet) +
позиционная метка «фон трогали вот здесь» (pop_cd_touch в резидентном
pop_tile.c, зовёт сам pop_blit_b).  Решение перепроверяется перед
отрисовкой, а pop_char_draw страхуется собственным heal — если тик всё-таки
сдвинул персонажа, прошлый кадр стирается там.  Слоты рядом (32 px) —
перерисовываем оба, иначе heal соседа выест кусок из «тихого».

Метка обязана быть ПОЗИЦИОННОЙ: с флагом «фон трогали хоть где-то» выигрыш
был ровно нулевым — факелы анимируются каждый кадр и гасили пропуск для
всех сразу (597 684 такта, как без оптимизации).

Замеры (MAME, брейкпоинты по totalcycles, бюджет кадра 430 000):
  комн. 1.3, труп стража, Кид стоит: 210 % -> 116 % (500 772 такта),
  ноль вызовов pop_heal_fast за кадр, весь фон — 2 блита (44 136);
  комн. 1.1, Кид стоит вдали от факелов: 404 112 (94 %), цикл 4 -> 3 кадра.

Узкое место сместилось на ЛОГИКУ: 60 % кадра уходит на тик персонажей,
которые СТОЯТ, ещё 28 % — на loose_tick + process_trobs в комнате без
единой ловушки.  Разбивка и план — TASKS_OPEN.md#draw-cost.
This commit is contained in:
2026-08-08 18:39:00 +03:00
parent fd54bc78c0
commit a25ce58869
8 changed files with 283 additions and 60 deletions
+111 -2
View File
@@ -156,8 +156,12 @@ static void cd_clip_add(pop_cdraw_t *s, int x, int y, uint16_t w, uint16_t h)
/* Стирание прошлого кадра персонажа и его накладных. pop_heal_fast, а не
* голый gfx_heal: стираем ровно то, что рисовали, а рисовали в подавляющем
* большинстве кадров noclip-ядром; клипающий путь остаётся для straddle и
* выхода за поле. */
void pop_char_heal(uint8_t who) __banked
* выхода за поле.
*
* Тело — static: pop_char_draw зовёт его же «страховкой» (см. ниже), а
* трамплин выбирается ОБЪЯВЛЕНИЕМ, и вызов через `__banked`-имя стоил бы
* 654 такта внутри собственного банка (memory sdcc_banked_call_rules). */
static void cd_heal(uint8_t who)
{
pop_cdraw_t *s = &pop_cd[who];
uint8_t p = gfx_get_draw_page() & 1;
@@ -166,6 +170,101 @@ void pop_char_heal(uint8_t who) __banked
s->valid[p] = s->ovalid[p] = 0;
}
void pop_char_heal(uint8_t who) __banked { cd_heal(who); }
/* ---- Пропуск неизменившегося кадра (DRAW-COST; контракт — pop_cdraw.h) -- */
/* Снимок ВХОДОВ отрисовки слота. Кадр анимации (kid_frame/pop_gframe)
* однозначно определяется полем frame, поэтому его самого в снимке нет;
* тайлы вокруг персонажа (clip_char, fore-проход) в снимок тоже не входят —
* их изменение ловит pop_cd_dirty. */
typedef struct {
uint8_t frame, x, y, action, sword, charid, room, hurt;
int8_t dir, ccol, crow;
int dx; /* render_dx: straddle-смещение */
} cd_sig_t;
static cd_sig_t cd_sig[POP_CH_N][2];
static void cd_sig_make(uint8_t who, cd_sig_t *g)
{
const pop_char_t *ch = (who == POP_CH_KID) ? &Kid : &Guard;
g->frame = ch->frame; g->x = ch->x; g->y = ch->y;
g->action = ch->action; g->sword = ch->sword; g->charid = ch->charid;
g->room = ch->room;
g->hurt = (uint8_t)((who == POP_CH_KID) ? pop_kid_hurt : pop_guard_hurt);
g->dir = ch->direction;
g->ccol = ch->curr_col; g->crow = ch->curr_row;
g->dx = pop_cd[who].render_dx;
}
/* Габарит слота на странице: спрайт + накладные (клинок/брызги). y —
* КОМНАТНЫЙ (как в pop_cd), перевод в экранный делает вызывающий. */
static void cd_bbox(uint8_t who, uint8_t p, int *x0, int *y0, int *x1, int *y1)
{
const pop_cdraw_t *s = &pop_cd[who];
*x0 = s->x[p]; *y0 = s->y[p];
*x1 = *x0 + (int)s->w[p]; *y1 = *y0 + (int)s->h[p];
if (s->ovalid[p]) {
if (s->ox[p] < *x0) *x0 = s->ox[p];
if (s->oy[p] < *y0) *y0 = s->oy[p];
if (s->ox[p] + (int)s->ow[p] > *x1) *x1 = s->ox[p] + (int)s->ow[p];
if (s->oy[p] + (int)s->oh[p] > *y1) *y1 = s->oy[p] + (int)s->oh[p];
}
}
/* «Тихий» слот: спрайт этой страницы цел (heal его не трогал), фон в его
* прямоугольнике не трогали, и все входы отрисовки те же, что в момент,
* когда этот кадр на эту страницу и рисовали. valid[p] здесь ключевой: он
* снимается heal'ом, поэтому решение остаётся согласованным, сколько раз его
* ни перепроверяй за кадр. */
static uint8_t cd_quiet(uint8_t who, uint8_t p)
{
cd_sig_t g;
const uint8_t *a, *b;
uint8_t i;
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; /* метка фона — в ЭКРАННЫХ */
if (x0 < pop_cd_dx1[p] && pop_cd_dx0[p] < x1 &&
y0 < pop_cd_dy1[p] && pop_cd_dy0[p] < y1) return 0;
}
cd_sig_make(who, &g);
a = (const uint8_t *)&g; b = (const uint8_t *)&cd_sig[who][p];
for (i = 0; i < sizeof(cd_sig_t); i++)
if (*a++ != *b++) return 0;
return 1;
}
/* Запас вокруг габарита соседа: за кадр персонаж проходит заметно меньше
* (бег — ~11 экранных px), а прыжок с падением укладывается сюда же. */
#define CD_MARGIN 32
uint8_t pop_char_skip_mask(void) __banked
{
uint8_t p = gfx_get_draw_page() & 1;
uint8_t q0 = cd_quiet(POP_CH_KID, p);
uint8_t q1 = cd_quiet(POP_CH_OPP, p);
if (q0 && q1) return 3; /* оба стоят — пересечься нечему */
if (q0 || q1) {
/* Один перерисовывается: его heal (и fore-проход по его окну) может
* задеть «тихого» соседа и выесть из него кусок. Прямоугольники
* рядом — перерисовываем ОБОИХ. */
uint8_t st = (uint8_t)(q0 ? POP_CH_KID : POP_CH_OPP);
uint8_t mv = (uint8_t)(st ^ 1);
if (pop_cd[mv].valid[p]) {
int ax0, ay0, ax1, ay1, bx0, by0, bx1, by1;
cd_bbox(st, p, &ax0, &ay0, &ax1, &ay1);
cd_bbox(mv, p, &bx0, &by0, &bx1, &by1);
if (ax0 < bx1 + CD_MARGIN && bx0 < ax1 + CD_MARGIN &&
ay0 < by1 + CD_MARGIN && by0 < ay1 + CD_MARGIN) return 0;
}
}
return (uint8_t)((q0 ? 1 : 0) | (q1 ? 2 : 0));
}
void pop_char_set_render_dx(uint8_t who, int dx) __banked
{
pop_cd[who].render_dx = dx;
@@ -243,6 +342,12 @@ void pop_char_draw(uint8_t who) __banked
uint16_t w, h, vis_w;
int obj_x, obj_y, fp_x, fwd, top, bx, skip, rows, ct, cr;
/* Страховка пропуска (DRAW-COST): главный цикл мог решить, что слот
* «тихий», и heal не звать — а тик после этого персонажа всё-таки
* сдвинул. Здесь прошлый кадр уже точно надо стереть. Если heal в этом
* кадре был, вызов бесплатен: valid/ovalid уже сняты. */
cd_heal(who);
s->cw = 0; /* окно fore-клипа: пусто, пока не нарисовали */
s->fpw = 0; /* габарит кадра: нечего перекрывать */
@@ -360,6 +465,10 @@ void pop_char_draw(uint8_t who) __banked
}
}
gfx_w0_unmap();
/* Что именно нарисовано на ЭТОЙ странице — снимок для пропуска
* следующих кадров (DRAW-COST). Только на полном пути: ранние выходы
* оставляют valid[dp] снятым, и cd_quiet отсеет слот и без снимка. */
cd_sig_make(who, &cd_sig[who][dp]);
}
void pop_char_fore(uint8_t who) __banked