PoP roomtest: fore-слой — окно клипа, кэш кладки; кадр 2.5 -> 0.78 периода

Жалоба: стойка неподвижных Кида и стража занимала больше полутора
кадровых периодов.  Гипотеза «виноват __banked» ЗАМЕРОМ НЕ
ПОДТВЕРДИЛАСЬ: весь банковый вызов (трамплин + смена страницы + тело
pop_guard_tick + возврат) стоит 654 такта при бюджете кадра 430 000.

Как мерил (выборка PC бесполезна — мост MAME отвечает из фреймового
колбэка, все сэмплы падают в обработчик прерывания): breakpoint'ы MAME с
действием {printf totalcycles; g} на входах фаз главного цикла, разности
соседних меток = стоимость фазы.  Плюс профилирование полосами бордюра
(make PROF=1, макрос PROF() в roomtest.c) для быстрого взгляда.

Замер комнаты 3 (Kid + страж), такты, кадр = 430 000:
  fore поверх стража  431 964
  fore поверх Kid     402 816   -> 78 % всей работы кадра
  остальное           241 956
  ИТОГО             1 076 736   = 2.5 кадра

Две причины, обе устранены:

1. Fore-слой рисовал ЦЕЛЫЕ тайлы, хотя существует ровно для того, чтобы
   вернуть куски поверх спрайта — за его прямоугольником в видеопамяти и
   так правильный фон.  Введено ОКНО клипа (pop_fore_set_clip): спрайт
   сообщает свой итоговый габарит (у Kid — с клинком, брызгами и
   обрезкой clip_char), fore-проход режет по нему.  Отсев трёхступенчатый:
   тайл целиком (tile_in_fclip, до обращения к атласу), кусок по грубому
   габариту (до gfx_w0_map — w/h лежат в EMM-странице), и точный клип в
   blit_b.  Для последнего добавлен libbgi-примитив
   gfx_blit_part_noclip — пара к gfx_blit_noclip, но под-прямоугольник.

2. Оставшиеся 341 К после клипа оказались НЕ пикселями: 18 вызовов
   pop_prandom за проход, ~10 700 тактов каждый (32-битный LCG:
   __mullong ~8 000 + __moduint).  Раскладка кладки тайла — чистая
   функция (комната, ряд, колонка), то есть константа комнаты, а
   wall_pattern пересчитывал её каждый кадр.  Теперь кэшируются готовые
   РЕШЕНИЯ (что рисовать и с каким смещением), 3 байта на тайл, сброс в
   pop_room_draw.  Порядок вызовов prandom воспроизведён один в один,
   включая то, что значение метки берётся только при сработавшем условии.

Итог того же замера: fore поверх стража 37 464, поверх Kid 46 464,
кадр целиком 334 716 = 0.78 периода (было 2.5).  Ускорение 3.2x, сами
fore-проходы — 10x.

Проверка отсутствия регрессии: попиксельная разность скриншотов комнат
1/2/3 до и после — отличаются ТОЛЬКО языки пламени факелов (анимация),
кладка и метки совпадают байт в байт.

Побочно: sprinter-cc научился пробрасывать -DNAME в sdcc.

Память: куча W2 1245 -> 996 Б (кэш кладки 120 Б), резидент W3 12 819 ->
14 512 (свободно 1872 Б — становится тесно), банк 1 236/16384.

ВНИМАНИЕ: make size-check показывает рост 7 программ, но эталон
docs/size_baseline.tsv отстал (последний раз принят в 484b18d, libbgi
менялась в 95c22be/c127a4b/64ce633) — к этой правке рост отношения не
имеет: новый модуль библиотеки в чужие программы не линкуется.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 00:19:32 +03:00
parent af5f0a4638
commit f2093e0d89
9 changed files with 304 additions and 67 deletions
+28 -11
View File
@@ -21,6 +21,26 @@
__sfr __at (0xFE) io_border;
/* ПРОФИЛИРОВАНИЕ ПОЛОСАМИ БОРДЮРА (make PROF=1). Цвет бордюра держится,
* пока фаза считает, поэтому высота полосы на скриншоте = время фазы в
* растровых строках (1 строка ≈ 224 такта Z80 при 3.5 МГц). Это
* единственный способ мерить здесь: мост MAME отвечает из фреймового
* колбэка, поэтому выборка PC вырождается (100 % сэмплов в обработчике
* прерывания). Фазы:
* 0 чёрный — ожидание vsync (запас кадра)
* 1 синий — ввод + heal (стереть прошлые спрайты)
* 2 красный — логика (kid_tick/физика/страж в банке)
* 3 лиловый — слой фона (loose/trobs/redraw_needed/шов)
* 4 зелёный — kid_draw
* 5 циан — guard_draw + fore поверх стража
* 6 жёлтый — fore поверх Kid + чистка бортов
* 7 белый — флип */
#ifdef PROF_BORDER
#define PROF(n) io_border = (n)
#else
#define PROF(n) ((void)0)
#endif
/* Сколько банков грузит crt0_banked (читает его стартовый код). */
const uint8_t n_banks = 1;
@@ -267,7 +287,7 @@ int main(void)
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
* переключ). Выключение возвращает показ на страницу 0. */
io_border = 2; /* синий - начало */
PROF(1); /* ввод + heal */
uint8_t sp = kbd_raw_down(KBD_SPACE);
if (sp && !sp_prev) {
@@ -291,7 +311,7 @@ int main(void)
cheat_prev = k;
}
if (frozen) {
io_border = 0; /* черный: ждём vsync */
PROF(0); /* чёрный: ждём vsync */
gfx_wait_vsync();
continue;
}
@@ -335,6 +355,7 @@ int main(void)
kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */
pop_guard_heal(); /* и прошлый кадр стража */
pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */
PROF(2); /* логика */
kid_tick(); /* play_seq: следующий кадр */
pop_phys_tick(); /* K3: падение/приземление/стена */
pop_guard_tick(); /* логика стража — В БАНКЕ (guards.c) */
@@ -360,16 +381,14 @@ int main(void)
}
}
io_border = 4; /* зеленый */
PROF(3); /* слой фона */
pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */
/* — ДО Kid, чтобы Kid всегда был поверх плиты) */
io_border = 6; /* голубой */
pop_process_trobs(cur_room); /* пики/ворота/дверь: продвинуть СОСТОЯНИЕ (порт */
/* process_trobs — рисования внутри нет) */
pop_redraw_needed(); /* и разобрать пометки перерисовки тайлов (порт */
/* redraw_needed, seg008:0178) — СЛОЙ ФОНА, до Kid */
io_border = 4; /* синий */
{ /* ворота соседа слева (room8) видны в col0 — */
uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */
if (lft) {
@@ -386,7 +405,6 @@ int main(void)
}
}
}
io_border = 2; /* зеленый */
if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */
dead_frames++;
if (dead_frames > RESPAWN_SETTLE &&
@@ -399,13 +417,11 @@ int main(void)
pop_kid_hp_reset();
}
}
io_border = 6; /* голубой */
if (pop_loose_fell) { /* упавший кусок → debris в комнате снизу */
uint8_t fpos = (uint8_t)(pop_loose_fell - 1);
uint8_t fcol = (uint8_t)(fpos % 10);
uint8_t down = pop_room_link(cur_room, LINK_DOWN);
pop_loose_fell = 0;
io_border = 2; /* синий */
add_tile_override(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */
if (down) {
int8_t lrow = pop_room_col_landing(down, fcol);
@@ -449,16 +465,17 @@ int main(void)
flash_bg(255, 85, 85); /* color_12_brightred */
flash_on = 1;
}
io_border = 4; /* зеленый */
update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */
PROF(5); /* страж: спрайт + fore поверх него */
pop_guard_draw(); /* страж — ДО Kid (порядок как в оригинале) */
PROF(4); /* kid_draw */
kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */
pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */
if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */
kid_draw_splash();
pop_kid_hurt = 0;
}
io_border = 0; /* черный */
PROF(6); /* fore поверх Kid + борта */
pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
kid_fp_width(), kid_fp_height(),
Kid.direction, Kid.frame, Kid.action);
@@ -468,7 +485,7 @@ int main(void)
* сделан (скрытая страница), wait_vsync ловит начало кадра,
* set_visible_page переключает во время бланка (gfx.h паттерн). */
io_border = 0; /* черный: ждём vsync */
PROF(0); /* чёрный: ждём vsync */
/* Отладка: номер отрисованной комнаты в верхнем борте — рисуем ТОЛЬКО
* при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера),