From d0030922ffffafa53908fec421f0b97fc9be9e8e Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Sun, 9 Aug 2026 15:09:51 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B8?= =?UTF-8?q?,=20=D1=88=D0=B0=D0=B3=202:=20=D0=BF=D1=80=D0=BE=D0=B1=D0=B5?= =?UTF-8?q?=D0=B3=D0=B8=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20=D0=B2?= =?UTF-8?q?=D0=B5=D1=82=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BB=D0=BE=D0=BD=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - move_coll_to_prev -> memcpy (LDIR): цикл на C пересчитывал адрес назначения через слот кадра IX и обходился в 5 514 тактов на 14 байт. - Окно перебора режется на НЕПРЕРЫВНЫЕ пробеги (комната слева / своя / справа), по каждому идёт coll_scan с шагающим указателем. Прежний «быстрый путь для окна внутри комнаты» не срабатывал почти никогда: Кид в колонке 0 даёт окно с −1, и всегда шёл медленный сбор во временный буфер с тернарником на колонку (1 340 тактов на колонку). - Пролог ряда (координата грани, длина окна, смещение слота) вынесен из тела ряда на кадр; базы соседних рядов — те же ±10 без пересчёта. check_collisions 44 022 -> 38 334, физика Кида 67 518 -> 63 102, работа за логический кадр 470 964 -> 466 560 (бюджет растрового кадра 430 000). Замер итерации: пустая колонка 750 тактов, колонка-стена ~1 700. tests-host: все 5 наборов прошли. Co-Authored-By: Claude Opus 5 --- applications/PoP/roomtest/TASKS_OPEN.md | 24 ++- applications/PoP/roomtest/pop_map.c | 235 +++++++++++++++++------- 2 files changed, 188 insertions(+), 71 deletions(-) diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md index 3249120..c7ba4ba 100644 --- a/applications/PoP/roomtest/TASKS_OPEN.md +++ b/applications/PoP/roomtest/TASKS_OPEN.md @@ -379,20 +379,30 @@ wait-state'ы, и замеренная стоимость выходит **≈ 2 |---|---|---| | окно перебора коллизии как в оригинале (было: все 14 колонок каждый кадр) | `check_collisions` 60 888 | 50 940 | | `get_tile_div_mod` — таблицей (`tile_div_tbl`/`tile_mod_tbl`), было `/14` и `%14` | 5 400 тактов на вызов, 13 вызовов за кадр ≈ 70 000 = **16 % кадра** | ~250 на вызов | -| разрешение ряда вынесено из цикла колонок + грань шагом 14 + `wall_type` таблицей | `check_collisions` 58 026 | **42 750** | +| разрешение ряда вынесено из цикла колонок + грань шагом 14 + `wall_type` таблицей | `check_collisions` 58 026 | 42 750 | +| `move_coll_to_prev` — `memcpy` (LDIR) вместо цикла на C | 14 байт за 5 514 тактов (390 на байт!) | ~1 200 | +| окно режется на непрерывные пробеги (левый сосед / своя / правый), пролог ряда вынесен на кадр | `check_collisions` 44 022 | **38 334** | + +Замер одной итерации перебора: **пустая колонка 750 тактов, колонка-стена +~1 700** (две 16-битные знаковые сверки граней — SDCC пишет их через +`jp PO / xor 0x80 / jp P`). Ловушка, на которую наступили: «быстрый путь для +окна внутри комнаты» не срабатывал ПОЧТИ НИКОГДА — Кид, стоящий в колонке 0, +даёт окно с −1, и шёл медленный сбор во временный буфер с тернарником на +колонку (1 340 тактов на колонку). Отсюда разбиение на пробеги: вопрос «чья +это колонка» решается раз на пробег, а `coll_scan` сам двигает `scan_left`. Самое дорогое было НЕ там, где ожидалось: `/14` и `%14` SDCC разворачивает в `__divsint` + `__modsint`, а `__modsint` внутри зовёт `__divsint` ещё раз — два полноценных 16-битных деления на каждый вопрос «в какой колонке точка». Оригинал делит таблицей (seg006:702) — мы просто не портировали это место. -**Профиль работы за один логический кадр ПОСЛЕ правок (470 964 такта):** +**Профиль работы за один логический кадр ПОСЛЕ правок (466 560 тактов):** | блок | тактов | % растрового кадра | |---|---|---| | see_kid + ctrl_tick + heal | 46 920 | 11 | | kid_tick (play_seq) | 6 468 | 1,5 | -| физика Кида (`pop_phys_tick`) | 67 518 | 16 | +| физика Кида (`pop_phys_tick`) | 63 102 | 15 | | страж + боёвка | 10 350 | 2,4 | | `pop_loose_tick` | 27 438 | 6,4 | | **`pop_process_trobs`** | **92 346** | **21,5** | @@ -402,8 +412,10 @@ wait-state'ы, и замеренная стоимость выходит **≈ 2 | борта | 732 | 0,2 | Синяя полоса (ввод+heal+логика) была ~60 % → стала ~30 %. Внутри физики -`check_collisions` — по-прежнему больше половины (42 750 из 67 518), и в ней -15 разрешений тайла на кадр: три ряда × окно 4–5 колонок, как в оригинале. +`check_collisions` — по-прежнему больше половины (38 334 из 63 102): три ряда +× окно 4–5 колонок, как в оригинале. Дальше там режется плохо — осталась +арифметика граней, а её удешевление требует перевода сверок в 8 бит и +доказательства, что `coll_xl`/`coll_xr` не выходят из 0..255. **Что осталось — по убыванию (это и есть остаток шага 2):** @@ -415,7 +427,7 @@ wait-state'ы, и замеренная стоимость выходит **≈ 2 сидят внутри process_trobs. Разобрать, где именно. 3. **`pop_loose_tick` 27 438** при полном отсутствии падающих плит в комнате — похоже, безусловный проход по всем 30 тайлам. -4. Работа за цикл 470 964 против 430 000 бюджета: **не хватает ~41 000**, +4. Работа за цикл 466 560 против 430 000 бюджета: **не хватает ~37 000**, чтобы уложиться в три растровых кадра вместо четырёх. Любая из правок выше даёт игре сразу +25 % скорости — граница проходит рядом. diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index 04b9f3e..97b7e49 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -16,6 +16,7 @@ * следующим шагом. */ #include +#include /* memcpy = LDIR: цикл на C тут стоил ~390 тактов на байт */ #include "pop_kid.h" #include "pop_map.h" #include "pop_ctrl.h" /* pop_ctrl_shift_held() — для check_grab */ @@ -1527,6 +1528,38 @@ static void set_char_collision(void) if (kid_cur_flags() & FRAME_THIN) { coll_xl += 4; coll_xr -= 4; } } +/* move_coll_to_prev (seg004:00DF): в prev кладём флаги ряда, в котором + * персонаж был НА ПРОШЛОМ кадре. Массивы curr/above/below в этот момент + * ещё хранят прошлый кадр, поэтому нужный ряд просто выбирается из них по + * разнице рядов. Условия — дословно оригинальные, включая ±3: ряд может + * «завернуться» при смене комнаты (2 -> 0 вниз, 0 -> 2 вверх), и тогда + * геометрия колонок та же, что у текущего ряда. + * + * Первый кадр (и кадр после смены комнаты, pop_coll_invalidate) прошлого + * ряда не имеет: prev = «уже перекрывал всё», бампа на нём нет. */ +static void move_coll_to_prev(int8_t row) +{ + const uint8_t *src; + /* Окно prev — то, по которому считались curr/above/below (у всех трёх оно + * одно). Забираем ДО того, как check_collisions пересчитает своё. */ + coll_prev_lo = coll_lo; coll_prev_hi = coll_hi; + if (coll_prev_row == COLL_ROW_NONE) { + memset(coll_prev, 3, COLL_N); + coll_prev_lo = COLL_C0; coll_prev_hi = COLL_C0 + COLL_N - 1; + return; + } + if (row == coll_prev_row || row + 3 == coll_prev_row || row - 3 == coll_prev_row) + src = coll_curr; /* ряд не менялся */ + else if (row + 1 == coll_prev_row || row - 2 == coll_prev_row) + src = coll_above; /* поднялся на ряд */ + else + src = coll_below; /* опустился на ряд */ + /* memcpy, а не цикл: SDCC пересчитывал адрес назначения на КАЖДОЙ + * итерации через слот кадра IX — 14 байт обходились в 5 514 тактов. */ + memcpy(coll_prev, src, COLL_N); +} + + /* Окно перебора (seg004:0047). Считать ПОСЛЕ set_char_collision — берётся из * coll_xl/coll_xr. Клампы: справа как в оригинале (не дальше последней * колонки), слева — по нашему COLL_C0 (оригиналу клампить нечего, у него @@ -1541,77 +1574,120 @@ static void calc_coll_window(void) win_lo = lo; win_hi = hi; } +/* Тело перебора: src — тайлы ряда ПОДРЯД, dst — куда класть флаги. Всё + * остальное вынесено в статики намеренно: у SDCC z80 каждый лишний + * параметр/локал уезжает в кадр IX, а `ld -n(ix)` стоит 19 тактов номинала + * (у нас с wait-state'ами ~46). + * + * Замер одной итерации в MAME: пустая колонка 750 тактов, колонка-стена + * ~1 700 (там две 16-битные знаковые сверки граней). */ +static uint8_t scan_n; /* сколько колонок осталось */ +static int scan_left; /* левая грань текущей колонки */ + +static void coll_scan(const uint8_t *src, uint8_t *dst) +{ + do { + uint8_t wt = wall_type_tbl[*src++ & 0x1F]; + uint8_t f = 0; + if (wt) { + if (wall_dl[wt] + scan_left < coll_xr) f = 1; + if (scan_left - wall_dr[wt] + TILE_RIGHTX > coll_xl) f |= 2; + } + *dst++ = f; + scan_left += TILE_SIZEX; + } while (--scan_n); +} + +/* Всё, что НЕ зависит от ряда, считается один раз на кадр. Раньше это жило + * в теле ряда, и пролог одного ряда стоил тысячи тактов при том, что сам + * цикл по четырём-пяти колонкам — около трёх. */ +static uint8_t scan_n0; +static int scan_left0; +static uint8_t scan_off; /* индекс первого слота в flags[] */ + +static void coll_scan_prepare(void) +{ + scan_left0 = pop_x_bump[win_lo + FIRST_ONSCREEN_COLUMN] + TILE_MIDX; + scan_n0 = (uint8_t)(win_hi - win_lo + 1); + scan_off = (uint8_t)COLL_IDX(win_lo); +} + +/* Базы ряда: указатели, которые индексируются НОМЕРОМ КОЛОНКИ (в том числе + * отрицательным). Ноль = «этой стороны нет» → стена. Ряд разрешается один + * раз на ряд, а не в каждом get_tile: тот на каждый вызов заново решал «ряд + * −1? за комнатой? слева/справа?» и считал row*10+col сдвигами (1 422 такта + * на вызов при 12-15 вызовах за кадр). */ +static const uint8_t *rb_own, *rb_lft, *rb_rgt; + +/* Ряд, целиком состоящий из стены — для стороны, которой нет (край уровня, + * ряд вне комнаты). Дешевле, чем ветка «а тут стена» внутри цикла. */ +static const uint8_t wall_row[COLL_N] = { + TILE_WALL,TILE_WALL,TILE_WALL,TILE_WALL,TILE_WALL,TILE_WALL,TILE_WALL, + TILE_WALL,TILE_WALL,TILE_WALL,TILE_WALL,TILE_WALL,TILE_WALL,TILE_WALL +}; + /* get_row_collision_data (seg004:0185) для одного ряда: бит0 = «персонаж * достал левую грань стены колонки», бит1 = «правую». Считаем ОКНО * win_lo..win_hi (см. calc_coll_window), слоты вне окна не трогаем — их - * годность определяют границы окна, а не содержимое. */ -/* move_coll_to_prev (seg004:00DF): в prev кладём флаги ряда, в котором - * персонаж был НА ПРОШЛОМ кадре. Массивы curr/above/below в этот момент - * ещё хранят прошлый кадр, поэтому нужный ряд просто выбирается из них по - * разнице рядов. Условия — дословно оригинальные, включая ±3: ряд может - * «завернуться» при смене комнаты (2 -> 0 вниз, 0 -> 2 вверх), и тогда - * геометрия колонок та же, что у текущего ряда. + * годность определяют границы окна, а не содержимое. * - * Первый кадр (и кадр после смены комнаты, pop_coll_invalidate) прошлого - * ряда не имеет: prev = «уже перекрывал всё», бампа на нём нет. */ -static void move_coll_to_prev(int8_t row) + * Окно режется на НЕПРЕРЫВНЫЕ пробеги: комната слева (колонки < 0), своя + * (0..9), справа (> 9). По каждому идёт тот же coll_scan с шагающим + * указателем, поэтому вопрос «а эта колонка чья?» решается по одному разу + * на пробег, а не на колонку. Первая версия собирала тайлы окна во + * временный буфер тернарником на колонку — 1 340 тактов на колонку, вдвое + * дороже самого перебора; у Кида, стоящего в колонке 0, окно начинается с + * −1, так что это был не редкий, а ОБЫЧНЫЙ путь. + * + * scan_left между пробегами не сбрасывается: coll_scan сам двигает его на + * тайл за колонку — ровно как оригинал (coll_tile_left_xpos += TILE_SIZEX). */ +static void coll_row(uint8_t *flags) { - const uint8_t *src; - int8_t i; - /* Окно prev — то, по которому считались curr/above/below (у всех трёх оно - * одно). Забираем ДО того, как check_collisions пересчитает своё. */ - coll_prev_lo = coll_lo; coll_prev_hi = coll_hi; - if (coll_prev_row == COLL_ROW_NONE) { - for (i = 0; i < COLL_N; i++) coll_prev[i] = 3; - coll_prev_lo = COLL_C0; coll_prev_hi = COLL_C0 + COLL_N - 1; - return; + int8_t lo = win_lo, last; + uint8_t n; + uint8_t *dst = flags + scan_off; + scan_left = scan_left0; /* coll_scan сам двигает его дальше */ + if (lo < 0) { /* комната СЛЕВА */ + last = win_hi < -1 ? win_hi : -1; + n = (uint8_t)(last - lo + 1); + scan_n = n; /* coll_scan обнулит */ + coll_scan(rb_lft ? rb_lft + lo : wall_row, dst); + dst += n; + lo = (int8_t)(last + 1); + } + if (lo <= win_hi && lo <= 9) { /* СВОЯ комната */ + last = win_hi < 9 ? win_hi : 9; + n = (uint8_t)(last - lo + 1); + scan_n = n; + coll_scan(rb_own ? rb_own + lo : wall_row, dst); + dst += n; + lo = (int8_t)(last + 1); + } + if (lo <= win_hi) { /* комната СПРАВА */ + scan_n = (uint8_t)(win_hi - lo + 1); + coll_scan(rb_rgt ? rb_rgt + lo : wall_row, dst); + } +} + +/* Разрешить ряд «с нуля» — для одиночного вызова (страж, check_chomped_guard). + * В check_collisions три ряда получаются сдвигом баз на ±10 без пересчёта. */ +static void coll_row_bases(int8_t row) +{ + rb_own = rb_lft = rb_rgt = 0; + if (row >= 0 && row <= 2) { + int ro = (int)row * 10; + rb_own = g_fg + ro; + if (g_link_l) rb_lft = g_lcol + ro + 10; + if (g_link_r) rb_rgt = g_rcol + ro - 10; + } else if (row == -1 && g_above && g_link_u) { + rb_own = g_above; /* ряд 2 комнаты сверху */ } - if (row == coll_prev_row || row + 3 == coll_prev_row || row - 3 == coll_prev_row) - src = coll_curr; /* ряд не менялся */ - else if (row + 1 == coll_prev_row || row - 2 == coll_prev_row) - src = coll_above; /* поднялся на ряд */ - else - src = coll_below; /* опустился на ряд */ - for (i = 0; i < COLL_N; i++) coll_prev[i] = src[i]; } static void get_row_collision_data(int8_t row, uint8_t *flags) { - int8_t col; - /* Ряд разрешается ОДИН раз на весь перебор, а не в каждом get_tile. - * get_tile — общий вход: он каждый вызов заново решает «ряд −1? за - * комнатой? слева/справа?» и считает row*10+col сдвигами; замер в MAME — - * 1 422 такта на вызов, то есть больше половины стоимости колонки. - * Здесь ряд фиксирован, значит хватает трёх баз. Побочки g_curr_tile - * (curr_tile2) тут никто не читает — она нужна только тем, кто зовёт - * get_tile ради одного тайла. */ - const uint8_t *own = 0, *lft = 0, *rgt = 0; - int coll_left; - if (row >= 0 && row <= 2) { - own = g_fg + row * 10; - if (g_link_l) lft = g_lcol + row * 10 + 10; /* индексируется col < 0 */ - if (g_link_r) rgt = g_rcol + row * 10 - 10; /* индексируется col > 9 */ - } else if (row == -1 && g_above && g_link_u) { - own = g_above; /* ряд 2 комнаты сверху; за её края — стена */ - } - /* Грань колонки идёт шагом ровно в тайл (pop_x_bump линейна, шаг 14) — - * оригинал так и делает: coll_tile_left_xpos += TILE_SIZEX (seg004:0185). - * Окно всегда внутри −2..11, поэтому индекс таблицы законный, а own/lft/ - * rgt не выходят за свои 30-байтовые раскладки. */ - coll_left = pop_x_bump[win_lo + FIRST_ONSCREEN_COLUMN] + TILE_MIDX; - for (col = win_lo; col <= win_hi; col++) { - uint8_t f = 0, t, wt; - if (col < 0) t = lft ? (uint8_t)(lft[col] & 0x1F) : TILE_WALL; - else if (col > 9) t = rgt ? (uint8_t)(rgt[col] & 0x1F) : TILE_WALL; - else t = own ? (uint8_t)(own[col] & 0x1F) : TILE_WALL; - wt = wall_type(t); - if (wt) { - if (wall_dl[wt] + coll_left < coll_xr) f |= 1; - if (coll_left - wall_dr[wt] + TILE_RIGHTX > coll_xl) f |= 2; - } - flags[COLL_IDX(col)] = f; - coll_left += TILE_SIZEX; - } + coll_row_bases(row); + coll_row(flags); } static void check_collisions(void) @@ -1635,11 +1711,40 @@ static void check_collisions(void) coll_prev_row = Char.curr_row; calc_coll_window(); coll_lo = win_lo; coll_hi = win_hi; + coll_scan_prepare(); /* Порядок важен: prev уже забран, теперь три ряда пересчитываются - * НА ЭТОТ кадр (в оригинале ровно так же, seg004:0004). */ - get_row_collision_data(Char.curr_row, coll_curr); - get_row_collision_data((int8_t)(Char.curr_row + 1), coll_below); - get_row_collision_data((int8_t)(Char.curr_row - 1), coll_above); + * НА ЭТОТ кадр (в оригинале ровно так же, seg004:0004). Ряд здесь + * гарантированно 0..2, поэтому соседние ряды — это те же базы ±10, без + * повторного разрешения. */ + { + int ro = (int)Char.curr_row * 10; + const uint8_t *own = g_fg + ro; + const uint8_t *lft = g_link_l ? g_lcol + ro + 10 : 0; + const uint8_t *rgt = g_link_r ? g_rcol + ro - 10 : 0; + + rb_own = own; rb_lft = lft; rb_rgt = rgt; + coll_row(coll_curr); + + if (Char.curr_row < 2) { /* ряд ниже */ + rb_own = own + 10; + rb_lft = lft ? lft + 10 : 0; + rb_rgt = rgt ? rgt + 10 : 0; + } else { + rb_own = rb_lft = rb_rgt = 0; /* ряд 3 — стена везде */ + } + coll_row(coll_below); + + if (Char.curr_row > 0) { /* ряд выше */ + rb_own = own - 10; + rb_lft = lft ? lft - 10 : 0; + rb_rgt = rgt ? rgt - 10 : 0; + } else if (g_above && g_link_u) { /* ряд −1 = ряд 2 сверху */ + rb_own = g_above; rb_lft = rb_rgt = 0; + } else { + rb_own = rb_lft = rb_rgt = 0; + } + coll_row(coll_above); + } /* Обход СВЕРХУ ВНИЗ, как в оригинале (9→0): побеждает МЛАДШАЯ колонка, * в которой флаг перешёл 0→1. Только по ПЕРЕСЕЧЕНИЮ окон: вне его * сравнивать нечего (у оригинала там prev_coll_room != curr_row_coll_room