Композит падающего куска: циан в бюджете (632 000 -> 273 078)

Три части падающей плиты (env 70/74/72) складываются в ОДИН getimage-блоб
при загрузке тайлсета, и кусок рисуется одним блитом вместо трёх.  Блоб
лежит в обычной памяти (W2), поэтому вызов идёт без atlas_image и без
gfx_w0_map/unmap — ещё ~1 350 такта.  Прозрачность соблюдена: части
ПЕРЕКРЫВАЮТСЯ (74 и 70 обе от mob_x), поэтому композит собирается
попиксельно с пропуском 0xFF, то есть точно как три прозрачных блита.

Мотив: у блита ~8 800 такта постоянных накладных против ~5 000 на пиксели.
Шесть кусков в воздухе = 18 вызовов = 258 708 такта, больше половины
цианового блока.  Резервный путь на три блита оставлен (mob_spr_ok).

  циан пик   435 180 -> 273 078   (цель 400 000 — ВЫПОЛНЕНА)
  работа     1 037 250 -> 931 782
  зелёная      558 498 -> 557 706 (не затронута, ею занимаемся дальше)

Заодно НАЙДЕН И ПОФИКШЕН БАГ КОРИДОРА heal.  Снятые из каталогов реальные
габариты частей оказались другими, чем в комментарии, И РАЗНЫМИ у тайлсетов:

    подземелье  70 = 32x16   74 = 26x15   72 = 26x16
    дворец      70 = 32x13   74 = 25x15   72 = 31x13

То есть во ДВОРЦЕ (уровни 4-6, 10, 11, 13, 14) кусок достаёт до mob_x+62, а
коридор heal был mob_x-4 .. mob_x+59 — правые три пикселя не стирались
никогда.  Четыре пикселя слева при этом чистились впустую: левее mob_x
кусок не рисует ничего.  Коридор стал mob_x .. mob_x+63, площадь та же.
Высоту (24 строки при следе 19) НЕ сужаем: 24 — это след подземелья плюс
две строки поля с каждой стороны, «сужение по палаццовому следу» сломало бы
подземелье.

Ещё три правки того же захода:

- pop_blit_b: аргументы в file-scope.  Третий и дальше SDCC передаёт стеком,
  и каждое чтение шло через `-N(ix)` — 76 обращений.  Стало 11.
- pop_loose_mob_tick: пометки всех кусков одним пакетом (было по 4 502 такта
  на кусок).  mobTk 176 772 -> 168 600.
- pop_floor_bake: окно клипа проверено ВТОРОЙ раз (после того как
  blit_b_clip подешевел) и снова хуже — 182 124 -> 188 460.  Причина
  записана в коде: у этого тайла клипа нет, блиты идут быстрым путём, а окно
  их уводит в клипованный и при этом не отсеивает ни одного куска и не
  режет ни одной строки.  Не пробовать в третий раз.

Новый лист pop_mem_b (резидент): блит блоба из обычной памяти с теми же
клипом полосы у потолка и окном перерисовки, что у pop_blit_b.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 14:01:48 +03:00
parent a3c473d910
commit b2da0b85b5
3 changed files with 236 additions and 70 deletions
+162 -39
View File
@@ -84,6 +84,100 @@ static int bg_load_tile_pal(const char *name)
return 0;
}
/* ---- Композитный спрайт ПАДАЮЩЕГО КУСКА ------------------------------ *
* Три части (env 70 / 74 / 72, см. блок MOB_X0 ниже) складываются в ОДИН
* getimage-блоб в обычной памяти, и кусок рисуется одним блитом вместо трёх.
*
* Зачем: у блита ~8 800 такта постоянных накладных против ~5 000 на пиксели
* (memory blit_cost_model). Шесть кусков в воздухе давали 18 вызовов и
* 258 708 такта — БОЛЬШЕ ПОЛОВИНЫ цианового блока (замер 2026-08-17,
* docs/perf_cyan_phase.md). Плюс блоб в обычной памяти не требует
* atlas_image и gfx_w0_map/unmap — ещё ~1 350 такта на вызов.
*
* Прозрачность соблюдена: части ПЕРЕКРЫВАЮТСЯ (74 и 70 обе идут от mob_x),
* поэтому копируются попиксельно с пропуском 0xFF — ровно как три блита с
* прозрачностью друг поверх друга.
*
* Размер буфера — по худшему из тайлсетов (дворец даёт 63 в ширину,
* подземелье 19 в высоту), 1 201 Б статики W2. Куча roomtest не
* используется (malloc'ов в приложении нет), так что это её и не задевает.
*
* Собирается на КАЖДУЮ смену тайлсета: размеры частей у наборов разные. */
#define MOBS_MAXW 63
#define MOBS_MAXH 19
static uint8_t mob_spr[4 + MOBS_MAXW * MOBS_MAXH];
static uint8_t mob_spr_ok; /* 0 — не собрался, рисуем тремя блитами */
/* Одна часть в композит. xo — смещение по x, ybo — смещение НИЗА части
* относительно низа композита (0 = вплотную к низу, отрицательное = выше).
* top_off — смещение верхней строки композита относительно mob_y. */
static uint8_t mob_spr_put(uint8_t id, uint8_t xo, int8_t ybo, int8_t top_off)
{
atlas_t *a = &pop_env[id >> POP_ENV_SHIFT];
uint8_t idx = (uint8_t)(id & POP_ENV_MASK);
const uint8_t *src;
uint8_t w, h, r, c, v;
int row0;
if (idx >= a->count) return 0;
src = (const uint8_t *)atlas_image(a, idx);
gfx_w0_map(a->page);
w = src[0]; h = src[2];
if (src[1] || src[3] || !w || !h ||
(uint16_t)xo + w > MOBS_MAXW) { gfx_w0_unmap(); return 0; }
row0 = (int)ybo - (int)h + 1 - (int)top_off; /* верхняя строка части */
if (row0 < 0 || row0 + h > MOBS_MAXH) { gfx_w0_unmap(); return 0; }
src += 4;
for (r = 0; r < h; r++) {
const uint8_t *sp = src + (uint16_t)r * w;
uint8_t *dp = mob_spr + 4 + (uint16_t)(row0 + r) * MOBS_MAXW + xo;
for (c = 0; c < w; c++) { v = sp[c]; if (v != 0xFF) dp[c] = v; }
}
gfx_w0_unmap();
return 1;
}
/* Высота композита: самый верхний пиксель даёт часть 70 (ybo −3), у неё же
* самая большая высота. Считаем по фактическим габаритам, а не по константам
* — они у тайлсетов разные. */
static int8_t mob_spr_top_off(void)
{
static const uint8_t ids[3] = {74, 70, 72};
static const int8_t ybo[3] = {0, -3, -1};
int8_t best = 0;
uint8_t k;
for (k = 0; k < 3; k++) {
atlas_t *a = &pop_env[ids[k] >> POP_ENV_SHIFT];
uint8_t idx = (uint8_t)(ids[k] & POP_ENV_MASK);
const uint8_t *src;
int t;
if (idx >= a->count) continue;
src = (const uint8_t *)atlas_image(a, idx);
gfx_w0_map(a->page);
t = (int)ybo[k] - (int)src[2] + 1;
gfx_w0_unmap();
if (t < best) best = (int8_t)t;
}
return best;
}
static void mob_spr_build(void)
{
uint16_t i;
int8_t top_off;
mob_spr_ok = 0;
top_off = mob_spr_top_off();
if (top_off <= -MOBS_MAXH) return; /* не влезло — общий путь */
mob_spr[0] = MOBS_MAXW; mob_spr[1] = 0;
mob_spr[2] = (uint8_t)(1 - top_off); /* низ композита = mob_y */
mob_spr[3] = 0;
for (i = 0; i < (uint16_t)MOBS_MAXW * MOBS_MAXH; i++) mob_spr[4 + i] = 0xFF;
/* Порядок как в mob_render: низ, левая часть, правая часть. */
if (!mob_spr_put(74, 0, 0, top_off)) return;
if (!mob_spr_put(70, 0, -3, top_off)) return;
if (!mob_spr_put(72, 32, -1, top_off)) return;
mob_spr_ok = 1;
}
/* Загрузить набор set (0 подземелье, 1 дворец). Повторный вызов с тем же
* набором ничего не делает; со сменой — сначала выгружает старый, иначе
* EMM-страницы утекут (их по 12 на набор). */
@@ -105,6 +199,7 @@ int pop_bg_load(uint8_t set) __banked
atlas_load(&pop_pot_at, bg_path(POP_POT_ATL)) != 0) return -1;
bg_set = set;
pop_palace = (uint8_t)(set == 1);
mob_spr_build(); /* композит падающего куска — свой на набор */
/* Палитра — ПОСЛЕ атласов: если атлас не загрузился, цвета остаются от
* прежнего набора и картинка хотя бы не разъезжается. */
return bg_load_tile_pal(pop_tile_pal[set]);
@@ -773,13 +868,13 @@ void pop_floor_bake(int row, int col) __banked
gfx_set_bank(GFX_BANK_NORMAL);
pop_bar_black(x, yb + 26, 60, 39);
gfx_set_bank(GFX_BANK_TRANSPARENT);
/* ОКНА КЛИПА ЗДЕСЬ НЕТ ОСОЗНАННО (замер 2026-08-17). Оно попробовано и
* откачено: у этого тайла клипа нет вовсе, то есть блиты идут БЫСТРЫМ
* путём (gfx_blit_noclip, 10 422), а окно уводит их в blit_b_clip
* (14 088) — и не отсеивает ни одного куска (7,6 блита до и после).
* Итог был 187 266 -> 198 279, то есть хуже. Вернуться к этой идее
* после того, как blit_b_clip перестанет платить за 22-байтовый
* стековый кадр (позиция G3 в docs/perf_green_phase.md). */
/* ОКНА КЛИПА ЗДЕСЬ НЕТ — проверено ДВАЖДЫ, оба раза хуже (2026-08-17):
* до G3 187 266 -> 198 279
* после G3 182 124 -> 188 460 (блит 14 530 -> 15 593)
* Причина: у этого тайла клипа нет, блиты идут БЫСТРЫМ путём
* (gfx_blit_noclip), окно уводит их в blit_b_clip и при этом НЕ отсеивает
* ни одного куска и НЕ режет ни одной строки — все куски щебня и так
* лежат внутри бара yb+26..yb+64. Не пробовать в третий раз. */
pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */
draw_tile(row, col);
if (col + 1 < 10) draw_tile(row, col + 1);
@@ -924,28 +1019,36 @@ static const int16_t MOB_Y_LAND[5] = {2, 65, 128, 191, 254};
static const int16_t MOB_Y_BOUND[5] = {-1, 62, 125, 188, 25};
#define MOB_Y_NONE 0x7FFF
/* Габарит куска по X: left/bottom (41/43, w=32) с mob_x; right (42, w=26)
* с mob_x+32 → ПРАВЫЙ пиксель = mob_x+57 (замерено в pop_env1.atl, НЕ +46:
* ширина 42 = 26, не ~14). Коридор heal и клип ОБЯЗАНЫ покрыть ВСЮ ширину
* [mob_x-4 .. mob_x+57], иначе правая грань 42 остаётся хвостом на одной
* странице → тёмная тень на полу 2,7 + двоебуфер-мерцание. */
#define MOB_X0(mx) ((mx) - 4)
/* Коридор heal — по РЕАЛЬНОМУ следу спрайта, а не «с запасом».
* След: по x mob_x .. mob_x+58, по y mob_y-16 .. mob_y — это объединение
* трёх частей (41 = 32x13 с y-3, 42 = 26x15 с x+32,y-1, 43 = 32x3 с y).
/* Габарит падающего куска. Части и их места (порт draw_mob, seg007:13E5;
* индексы loose_fram_*[10], seg008:518/596/608):
*
* Ширина оставлена прежней (64 = mob_x-4 .. mob_x+60): нужных 62 плюс поля.
* А вот высота была 32 при нужных 16 — вдвое лишнего на самой дорогой
* операции кадра, помноженной на число падающих плит; сужена до 24.
* env 70 левая часть при mob_y-3
* env 74 низ при mob_y
* env 72 правая часть при mob_x+32, mob_y-1
*
* ПРО ЗАПАС ПО ШИРИНЕ: обоснования «выравнивание блока акселератора» я не
* подтвердил — ни pop_heal_fast, ни gfx_heal координаты не округляют, а в
* memory про акселератор таких требований нет. Единственный источник —
* наблюдение в pop_clip_sprite про 1-2 px слайверы правой грани. Сузить
* ширину можно, но ТОЛЬКО подтвердив прогоном, что слайверы не появятся. */
#define MOB_W 64 /* mob_x-4 .. mob_x+60 */
* РАЗМЕРЫ У ТАЙЛСЕТОВ РАЗНЫЕ (снято из каталогов атласов 2026-08-17; прежний
* комментарий «41=32x13, 42=26x15, 43=32x3» был неверен вдвойне — и id, и
* размеры):
*
* подземелье 70 = 32x16 74 = 26x15 72 = 26x16
* дворец 70 = 32x13 74 = 25x15 72 = 31x13
*
* Отсюда фактический след: по x mob_x .. mob_x+62 (дворец: 32+31),
* по y mob_y-18 .. mob_y (подземелье: 3+16-1).
*
* КОРИДОР ИСПРАВЛЕН 2026-08-17: было MOB_X0 = mob_x-4 при MOB_W = 64, то есть
* mob_x-4 .. mob_x+59 — на ДВОРЦОВЫХ уровнях (10, 11, 13, 14) правые три
* пикселя куска 72 не попадали в heal и оставались висеть на странице.
* Четыре пикселя слева при этом чистились впустую: левее mob_x кусок не
* рисует ничего. */
#define MOB_X0(mx) (mx)
#define MOB_W 64 /* mob_x .. mob_x+63 */
/* Коридор heal по высоте: mob_y-20 .. mob_y+3 при следе mob_y-18 .. mob_y.
* Уже НЕ сужать: 24 строки — это след подземелья (19) плюс две строки поля с
* каждой стороны. Сужение до 18 «по палаццовому следу» сломало бы
* подземелье. */
#define MOB_HEAL_UP 20 /* верх коридора = mob_y − 20 */
#define MOB_HEAL_H 24 /* .. mob_y + 4 */
#define MOB_HEAL_H 24 /* .. mob_y + 3 */
/* Свободный слот = не летит И дочистка завершена (иначе затрём хвост,
* который ещё не стёрт со второй страницы). Общий для обычного отрыва и
@@ -1304,6 +1407,16 @@ void pop_loose_mob_tick(void) __banked
* (замер 2026-08-13). Гард в самом mob_tick_one оставлен: функция
* зовётся и из других мест. */
mob_t *m = mobs;
/* Пометки всех кусков — ОДНИМ пакетом: настоящий pop_cd_touch стоит 4 502
* такта (замер 2026-08-17), а кусков в кадре каскада шесть. В пакете они
* копят общий прямоугольник четырьмя сравнениями, и настоящая пометка одна.
*
* Загрубление осознанное: объединение шести коридоров накрывает и ряды
* между ними. Безопасно в одну сторону — лишняя пометка стоит лишней
* перерисовки персонажа, пропущенная стоит ИСЧЕЗНУВШЕГО персонажа.
* Практически гряда идёт по колонкам 2..7, а Кид от неё убегает — на
* замерах разницы в числе перерисовок нет. */
pop_cd_batch_begin();
for (i = 0; i < MOB_MAX; i++, m++) {
/* Летящий кусок и его дочистка ходят по чужим пикселям СВОИМ heal'ом,
* мимо pop_blit_b — пока он в кадре, персонажей пропускать нельзя
@@ -1323,6 +1436,7 @@ void pop_loose_mob_tick(void) __banked
mob_tick_one(m, pg);
if (m->active) live++;
}
pop_cd_batch_end();
mobs_live = live;
}
@@ -1346,19 +1460,24 @@ static void mob_render(mob_t *m, uint8_t pg)
/* Спрайты ПАДАЮЩЕГО куска, а не лежащей плиты. У объекта в objtable
* obj_id = 10 (add_mob_to_objtable, seg007:1170), и три части берутся из
* таблиц по этому индексу (seg008:518/596/608):
* loose_fram_left[10] = 70 (32x13, y-3)
* loose_fram_bottom[10] = 74 (32x3, y)
* loose_fram_right[10] = 72 (26x16, x+32, y-1)
* loose_fram_left[10] = 70 при (x, y-3)
* loose_fram_bottom[10] = 74 при (x, y)
* loose_fram_right[10] = 72 при (x+32, y-1)
* Здесь стояли 41/43/42 — это индекс 0, то есть плита В ПОКОЕ. Отсюда и
* «цельная ровная плита» вместо двух частей со сдвигом правой половины
* на пиксель (замечено пользователем 2026-08-13 сравнением с оригиналом). */
/* Пометки «фон трогали» на время трёх блитов ПОДАВЛЕНЫ: коридор куска
* на пиксель (замечено пользователем 2026-08-13 сравнением с оригиналом).
* Габариты частей — в блоке MOB_X0 выше (у тайлсетов они РАЗНЫЕ).
*
* Рисуем ОДНИМ блитом по композиту (mob_spr_build): три вызова блита
* стоили 258 708 такта в кадре при шести кусках — больше половины
* цианового блока. Три отдельных блита остались резервным путём на
* случай, если композит не собрался. */
/* Пометки «фон трогали» на время блитов ПОДАВЛЕНЫ: коридор куска
* (MOB_X0 .. +MOB_W, y27 .. +37) уже помечен ОДНИМ вызовом — из
* pop_loose_mob_tick для летящих кусков и из mob_spawn_copy для куска,
* рождённого внутри тика. Спрайт целиком внутри коридора: он занимает
* x m->x .. m->x+57 (правая часть 72 идёт с x+32, ширина 26) при
* коридоре m->x4 .. m->x+59, и y m->y15 .. m->y при коридоре
* m->y27 .. m->y+37 с запасом на ускорение (speed ≤ 31 за кадр).
* рождённого внутри тика. Спрайт целиком внутри коридора: по x он
* занимает m->x .. m->x+62, по y m->y18 .. m->y, коридор — m->x ..
* m->x+63 и m->y27 .. m->y+37 (запас на ускорение, speed ≤ 31 за кадр).
* Замер 2026-08-17: три настоящих pop_cd_touch стоили 13 506 тактов на
* кусок, то есть 81 000 в кадре при шести кусках. */
pop_cd_mute();
@@ -1373,12 +1492,16 @@ static void mob_render(mob_t *m, uint8_t pg)
* Окно ставим только когда кусок реально задевает борт: внутри поля блиты
* идут быстрым путём (10 422 против 14 088). */
{
int ytop = m->y - 16 + POP_YOFF, ybot = m->y + POP_YOFF;
int ytop = m->y - 18 + POP_YOFF, ybot = m->y + POP_YOFF;
uint8_t clipped = (uint8_t)(ytop < POP_YOFF || ybot >= POP_CLIP_BOTTOM);
if (clipped) pop_t_win_set(0, POP_YOFF, 320, POP_PLAYFIELD_H);
pop_env_b(74, m->x, m->y);
pop_env_b(70, m->x, m->y - 3);
pop_env_b(72, m->x + 32, m->y - 1);
if (mob_spr_ok) {
pop_mem_b(mob_spr, m->x, m->y);
} else {
pop_env_b(74, m->x, m->y);
pop_env_b(70, m->x, m->y - 3);
pop_env_b(72, m->x + 32, m->y - 1);
}
if (clipped) pop_t_win_clear();
}
pop_cd_unmute();