Ускорение холостого хода: указательный обход вместо arr[i] в горячих циклах
Замер зелёного блока (комната 23 уровня 13, MAME, такты эмулятора) показал, что 86% его стоимости в ПОКОЕ — это pop_loose_tick, который не делает ничего. Причина — кодоген SDCC, подтверждена чтением .asm. С `int pos` и записью pop_loose_modif[pos] компилятор держал счётчик в IX-фрейме, каждую итерацию заново складывал 16-битный адрес элемента, клал его в локал и тут же вычитывал обратно парами `pop bc / pop hl / push hl / push bc`. 40 холостых итераций (30 тайлов + 10 потолков) стоили 27 936 тактов. То же в pop_loose_mob_tick: запись mobs[i] заставляла умножать i на sizeof(mob_t)=15 заново под КАЖДОЕ поле (.active/.clean/.x/.y), 14 пустых слотов — 35 790. Правка — обход указателем, счётчик uint8_t, пустые слоты отсеиваются в вызывающем цикле (а не гардом внутри mob_tick_one, до которого надо ещё дойти). Холостая итерация стала `ld a,(de) / or a,a / jp Z` — три инструкции вместо дюжины с обращениями к памяти. Результат (такты MAME, холостой кадр): циклы по тайлам 27 936 -> 9 852 (2,8x) pop_loose_mob_tick 35 790 -> 7 416 (4,8x) pop_loose_tick 70 866 -> 24 384 (2,9x) ЗЕЛЁНЫЙ БЛОК 82 242 -> 35 760 (2,3x) Банки ужались: BANK3 -90 Б, BANK7 -74 Б. Все 8 наборов tests-host проходят; комната 23 в MAME рисуется корректно. Плюс ВРЕМЕННАЯ оснастка замера (маркеры m9..m16, pop_dbg_kind) — она же показала, что пик зелёного блока сидит НЕ в тряске плит, а в запекании тайлов; разбор продолжается, оснастку снять перед закрытием темы. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -570,16 +570,31 @@ void pop_room_redraw_seam_left(uint8_t rows) __banked
|
||||
* shake — на текущей (back) странице поверх фона; bake — «запечь» пустоту
|
||||
* после провала (NORMAL, в ОЗУ-копию, чтобы heal её сохранял). */
|
||||
|
||||
/* Полоса кладки у потолка: дрожащая плита ряда −1 — порт redraw_needed_above
|
||||
* (seg008:0221).
|
||||
*
|
||||
* Оригинал рисует ТОЛЬКО СВОЮ колонку: кусок, свисающий вправо, кладёт её же
|
||||
* draw_tile_right. Здесь дополнительно звался `draw_tile(-1, col + 1)` —
|
||||
* целый соседний тайл, — и это удваивало работу. Замер 2026-08-13: пик
|
||||
* зелёного блока 887 000 тактов при кадровом интервале 430 000 состоял ровно
|
||||
* из шести вызовов этой функции, то есть двенадцати отрисовок тайла.
|
||||
*
|
||||
* Батчинг смежных колонок (один heal на диапазон) пробовался и убран: плиты
|
||||
* стартуют со случайными задержками (check_fall_flo), в кадре дрожат
|
||||
* разрозненные колонки, пробег почти всегда длиной в одну — механика не
|
||||
* окупалась, но крутилась каждый кадр. */
|
||||
void pop_ceil_shake_draw(int col) __banked
|
||||
{
|
||||
int x = POP_COL_XH[col] * 8;
|
||||
pop_heal_off(x, 0, 64, CEIL_BAND_H); /* фон полосы обоих тайлов */
|
||||
pop_dbg_m13(); /* ЗАМЕР: вход pop_ceil_shake_draw */
|
||||
pop_heal_off(x, 0, 64, CEIL_BAND_H);
|
||||
pop_dbg_m14(); /* ЗАМЕР: heal сделан */
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
pop_t_clip_top = POP_YOFF;
|
||||
draw_tile(-1, col);
|
||||
if (col + 1 < 10) draw_tile(-1, col + 1);
|
||||
pop_t_clip_top = 0;
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
pop_dbg_m15(); /* ЗАМЕР: draw_tile сделан */
|
||||
}
|
||||
|
||||
void pop_ceil_bake_empty(int col) __banked
|
||||
@@ -850,6 +865,8 @@ typedef struct {
|
||||
* разбора артефактов множественного падения; наружу таблица не объявлена и
|
||||
* никем не читается. */
|
||||
mob_t mobs[MOB_MAX];
|
||||
/* Сколько кусков сейчас летит (active). Считает pop_loose_mob_tick. */
|
||||
static uint8_t mobs_live;
|
||||
|
||||
/* Кусок «приземлился» (порт loose_land): 0 = нет, иначе tilepos+1 тайла, на
|
||||
* который он лёг → приложение/pop_map ставит debris + do_knock. */
|
||||
@@ -863,14 +880,20 @@ 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
|
||||
|
||||
static void mob_spawn(uint8_t room, int row, int col)
|
||||
/* Свободный слот = не летит И дочистка завершена (иначе затрём хвост,
|
||||
* который ещё не стёрт со второй страницы). Общий для обычного отрыва и
|
||||
* для копии из loose_fall — раньше цикл был написан дважды. */
|
||||
static mob_t *mob_alloc(void)
|
||||
{
|
||||
uint8_t i;
|
||||
mob_t *m = 0;
|
||||
/* свободный слот = не летит И дочистка завершена (иначе затрём хвост,
|
||||
* который ещё не стёрт со второй страницы) */
|
||||
for (i = 0; i < MOB_MAX; i++)
|
||||
if (!mobs[i].active && !mobs[i].clean) { m = &mobs[i]; break; }
|
||||
if (!mobs[i].active && !mobs[i].clean) return &mobs[i];
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void mob_spawn(uint8_t room, int row, int col)
|
||||
{
|
||||
mob_t *m = mob_alloc();
|
||||
if (!m) return; /* таблица полна — кусок без спрайта (тайл всё равно пуст) */
|
||||
m->x = POP_COL_XH[col] * 8;
|
||||
m->y = MOB_Y_LAND[row + 1]; /* y_loose_land: низ куска в момент отрыва */
|
||||
@@ -897,8 +920,7 @@ void pop_loose_mob_spawn(int row, int col) __banked
|
||||
* Через границу комнаты — как mob_down_a_row: y −= 192, ряд 0, комната снизу. */
|
||||
static void mob_spawn_copy(const mob_t *src)
|
||||
{
|
||||
uint8_t i;
|
||||
mob_t *m = 0;
|
||||
mob_t *m;
|
||||
int16_t y = (int16_t)(src->y + 6);
|
||||
int8_t row = (int8_t)(src->row + 1);
|
||||
uint8_t room = src->room;
|
||||
@@ -909,8 +931,7 @@ static void mob_spawn_copy(const mob_t *src)
|
||||
room = pop_room_link(room, 3 /* вниз */);
|
||||
if (!room) return; /* ушло из уровня — куску негде лететь */
|
||||
}
|
||||
for (i = 0; i < MOB_MAX; i++)
|
||||
if (!mobs[i].active && !mobs[i].clean) { m = &mobs[i]; break; }
|
||||
m = mob_alloc();
|
||||
if (!m) return;
|
||||
m->x = src->x;
|
||||
m->y = y;
|
||||
@@ -1193,6 +1214,11 @@ static void mob_mark_neighbour(const mob_t *m)
|
||||
static void mob_draw_pass(uint8_t pg, uint8_t want_defer)
|
||||
{
|
||||
uint8_t order[MOB_MAX], n = 0, i, j;
|
||||
/* Ранний выход: проход зовётся ДВАЖДЫ за кадр (под Kid и поверх него), и
|
||||
* без него мы перебирали бы все MOB_MAX слотов 28 раз в кадре даже
|
||||
* когда в воздухе пусто. Счётчик ведёт тик, которому проход по слотам
|
||||
* нужен и так. */
|
||||
if (!mobs_live) return;
|
||||
for (i = 0; i < MOB_MAX; i++) {
|
||||
mob_t *m = &mobs[i];
|
||||
if (!m->active || m->room != pop_t_room) continue;
|
||||
@@ -1218,25 +1244,34 @@ static void mob_draw_pass(uint8_t pg, uint8_t want_defer)
|
||||
|
||||
void pop_loose_mob_tick(void) __banked
|
||||
{
|
||||
uint8_t pg = gfx_get_draw_page() & 1, i;
|
||||
for (i = 0; i < MOB_MAX; i++) {
|
||||
uint8_t pg = gfx_get_draw_page() & 1, i, live = 0;
|
||||
/* Слоты обходим УКАЗАТЕЛЕМ и пустые отсеиваем ЗДЕСЬ, а не внутри
|
||||
* mob_tick_one. Индексная запись mobs[i] заставляла SDCC умножать i на
|
||||
* sizeof(mob_t)=15 заново под каждое поле (.active/.clean/.x/.y), и 14
|
||||
* пустых слотов стоили 35 790 тактов в кадре, где не падает НИЧЕГО
|
||||
* (замер 2026-08-13). Гард в самом mob_tick_one оставлен: функция
|
||||
* зовётся и из других мест. */
|
||||
mob_t *m = mobs;
|
||||
for (i = 0; i < MOB_MAX; i++, m++) {
|
||||
/* Летящий кусок и его дочистка ходят по чужим пикселям СВОИМ heal'ом,
|
||||
* мимо pop_blit_b — пока он в кадре, персонажей пропускать нельзя
|
||||
* (DRAW-COST). Взводим ДО тика: кусок может исчезнуть прямо в нём, а
|
||||
* стереть себя ему всё равно надо будет. */
|
||||
if (mobs[i].active || mobs[i].clean)
|
||||
/* Метка «фон трогали вот здесь» обязана покрывать ВЕСЬ коридор
|
||||
* heal'а, а не габарит спрайта. Иначе персонаж, попавший в
|
||||
* расширенную часть коридора, стирается heal'ом, но перерисовать
|
||||
* себя не просит (pop_char_skip_mask его пропускает) — и просто
|
||||
* исчезает с экрана. Ровно это и случилось, когда коридор
|
||||
* расширили под транзитную перерисовку соседа (прогон
|
||||
* 2026-08-13: в комнате 23 после падения гряды пропадал Кид).
|
||||
* Запас сверху — на путь, пройденный за кадр (speed до 29): heal
|
||||
* стирает ПРОШЛУЮ позицию, она выше текущей. */
|
||||
pop_cd_touch(MOB_X0(mobs[i].x), mobs[i].y - 27 + POP_YOFF, MOB_W, 64);
|
||||
mob_tick_one(&mobs[i], pg);
|
||||
if (!m->active && !m->clean) continue; /* пустой слот — ни адреса, ни вызова */
|
||||
/* Метка «фон трогали вот здесь» обязана покрывать ВЕСЬ коридор
|
||||
* heal'а, а не габарит спрайта. Иначе персонаж, попавший в
|
||||
* расширенную часть коридора, стирается heal'ом, но перерисовать
|
||||
* себя не просит (pop_char_skip_mask его пропускает) — и просто
|
||||
* исчезает с экрана. Ровно это и случилось, когда коридор
|
||||
* расширили под транзитную перерисовку соседа (прогон
|
||||
* 2026-08-13: в комнате 23 после падения гряды пропадал Кид).
|
||||
* Запас сверху — на путь, пройденный за кадр (speed до 29): heal
|
||||
* стирает ПРОШЛУЮ позицию, она выше текущей. */
|
||||
pop_cd_touch(MOB_X0(m->x), m->y - 27 + POP_YOFF, MOB_W, 64);
|
||||
mob_tick_one(m, pg);
|
||||
if (m->active) live++;
|
||||
}
|
||||
mobs_live = live;
|
||||
}
|
||||
|
||||
static void mob_render(mob_t *m, uint8_t pg)
|
||||
|
||||
Reference in New Issue
Block a user