Оптимизация логики, шаг 2: пробеги вместо ветвления на колонку
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 % скорости — граница проходит рядом.
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
* следующим шагом.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <string.h> /* 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
|
||||
|
||||
Reference in New Issue
Block a user