P2b: луч видимости стража 36 786 -> 13 002 (-65 %)

Замер отделил луч от pop_frame_timers: таймеры со всеми тремя
спецсобытиями уровней стоят 1 962, луч — 36 786, то есть 5,6 % работы
кадра на девять чтений байта.

Причина оказалась НЕ в алгоритме.  Сверка трёх референсов:

  SDLPoP (seg003:688) — идёт по x с шагом 14 и на каждом шаге переводит x
    в колонку делением.  Причём сам SDLPoP признаёт в комментарии, что
    «DOS PoP does this: tile_div_tbl[xpos]» — то есть оригинал брал
    таблицу, а порт заменил её на / и %, потому что на 32 битах так проще.
  Apple II (MISC.S CHECKALERT) — тот же алгоритм байт в байт, но перевод
    x -> блок через таблицу BlockTable[x].  Ровно то, что у нас уже было
    сделано (POP_TILE_DIV, 2026-08-10).
  mininim — другая архитектура (тайловые позиции, своя механика), для
    сравнения реализации не годится.

То есть алгоритмически мы уже были на уровне Apple II, а платили за
другое: pop_tile_at объявлен __banked, луч живёт в guards.c (банк 1), и
на КАЖДУЮ колонку шёл трамплин банк 1 -> банк 3.  На сцене 11/15 (Кид в
колонке 2, страж в 8) это девять трамплинов за кадр.

Сделано:

  1. луч переведён на КОЛОНКИ вместо x-координат.  Это эквивалентно:
     начальные x — ровно центры тайлов персонажей, а обратный перевод даёт
     ту же колонку (floor((58 + col*14 - 58)/14) == col).  Ушли 16-битный
     шаг, 16-битное сравнение и индексация таблицы на каждой итерации;
  2. тайлы отрезка забираются ОДНИМ банковым вызовом (pop_row_tiles)
     вместо девяти;
  3. внутри pop_row_tiles — быстрый путь для отрезка целиком внутри
     комнаты: get_tile при ряде 0..2 и колонке 0..9 сводится ровно к
     g_fg[row*10+col] & 0x1F, идём указателем;
  4. буфер тайлов — file-scope, а не локальный массив (иначе каждое
     чтение это -n(ix)).

Замер по шагам: 36 786 -> 24 048 (колонки + один вызов) -> 13 002
(быстрый путь + буфер).  Синяя фаза 283 215 -> 259 500, работа кадра
654 990 -> 628 542, то есть -26 448 при ожидании -30 000.

Кэш-гейт «пересчитывать только при смене позиции» НЕ понадобился:
расхождения с оригиналом нет, луч считается каждый кадр, как и должен.

Поведение проверено в MAME: страж в боевой стойке, но не идёт — между ним
и Кидом чомпер, то есть can_guard_see_kid = 1 («видит, но не пойдёт»).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 12:37:06 +03:00
parent da17a48576
commit f8493a4c04
4 changed files with 91 additions and 59 deletions
+54 -59
View File
@@ -56,39 +56,16 @@ static uint8_t tile_is_floor(uint8_t t)
} }
} }
/* Тайл в ряду Кида по X-координате персонажа (порт get_tile_at_kid, /* get_tile_at_kid (seg003:761) СНЯТ вместе с переходом луча на колонки:
* seg003:761): xpos 7 делится на ширину тайла с округлением вниз. */ * единственным его вызывающим был этот луч, а он больше не работает с
static int8_t tile_col; /* колонка последнего tile_at_kid */ * X-координатами (разбор — в теле pop_check_can_guard_see_kid ниже).
* Таблица POP_TILE_DIV осталась в резиденте, ею пользуется физика. */
static uint8_t tile_at_kid(int16_t xpos) /* Буфер тайлов луча — file-scope, а не локальный массив: локальный уезжает
{ * в стековый кадр, и каждое чтение из него стоит `-n(ix)` (19 тактов
/* get_tile_div_mod_m7 (seg006:697): колонка = floor((xpos 7 58)/14), * номинала, ~46 с wait-state'ами). Тот же приём, что у draw_tile и
* СРАЗУ в координатах комнаты (58 = x_bump[FIRST_ONSCREEN_COLUMN]). * pop_blit_b. Размер — колонки −1..10 плюс запас. */
* Оригинал берёт её из tile_div_tbl — и мы теперь тоже: резидентная static uint8_t row_t[12];
* POP_TILE_DIV[] это ровно она (индекс = xpos, значение = floor((xpos58)/14)),
* поэтому здесь индексируем её на xpos−7.
*
* Было честное `/` и `%`: у SDCC z80 это __divsint плюс __modsint, а тот
* внутри снова зовёт __divsint — около 5 400 тактов на вызов. А зовут
* нас В ЦИКЛЕ по колонкам между стражем и Кидом
* (check_can_guard_see_kid, seg003:761). Когда Кид у шва, его
* curr_col = −1, луч тянется через всю комнату, и за кадр набегало ВОСЕМЬ
* пар делений — ~43 000 тактов, 10 % растрового кадра, в фазе логики
* (замер MAME 2026-08-10, уровень 1 комната 2).
*
* Медленный хвост остаётся для xpos за пределами таблицы (страж и Кид в
* разных комнатах: логическая X уезжает на ±140). */
int16_t x = xpos - 7;
if ((uint16_t)x < 256u) {
tile_col = POP_TILE_DIV[x];
} else {
int16_t v = x - SCREENSPACE_X;
int16_t col = v / TILE_SIZEX;
if (v % TILE_SIZEX < 0) --col;
tile_col = (int8_t)col;
}
return pop_tile_at(tile_col, Kid.curr_row);
}
/* check_can_guard_see_kid (seg003:688) — луч видимости по ряду. /* check_can_guard_see_kid (seg003:688) — луч видимости по ряду.
* Результат в can_guard_see_kid: 0 не видит / 1 видит, но не пойдёт / * Результат в can_guard_see_kid: 0 не видит / 1 видит, но не пойдёт /
@@ -96,7 +73,6 @@ static uint8_t tile_at_kid(int16_t xpos)
void pop_check_can_guard_see_kid(void) __banked void pop_check_can_guard_see_kid(void) __banked
{ {
uint8_t kid_frame = Kid.frame; uint8_t kid_frame = Kid.frame;
int16_t left_pos, right_pos;
uint8_t tile; uint8_t tile;
/* МЫШЬ Кида «не видит» (seg003:0965): иначе он выхватит меч на зверька, /* МЫШЬ Кида «не видит» (seg003:0965): иначе он выхватит меч на зверька,
@@ -122,34 +98,53 @@ void pop_check_can_guard_see_kid(void) __banked
} }
can_guard_see_kid = 2; can_guard_see_kid = 2;
left_pos = pop_x_bump[Kid.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX; /* ПО КОЛОНКАМ, а не по X-координатам.
right_pos = pop_x_bump[Guard.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX; *
if (left_pos > right_pos) { * Оригинал (и SDLPoP, seg003:688) идёт по x с шагом TILE_SIZEX и на
int16_t t = left_pos; left_pos = right_pos; right_pos = t; * каждом шаге переводит x в колонку — потому что x у него уже под рукой.
} * Но начальные x — это РОВНО центры тайлов персонажей
/* Чомпер стоит у ЛЕВОГО края тайла — свой тайл он не перекрывает. */ * (x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX), а перевод обратно
if (tile_at_kid(left_pos) == TILE_CHOMPER) * даёт ту же колонку: floor((58 + col*14 - 58) / 14) == col. Значит
left_pos += TILE_SIZEX; * весь проход эквивалентен обходу колонок, и ни одна x-координата тут не
/* Ворота — у ПРАВОГО края, поэтому крайний правый тайл не считается. */ * нужна: уходят 16-битный шаг, 16-битное сравнение и индексация таблицы
if (tile_at_kid(right_pos) == TILE_GATE) * POP_TILE_DIV на каждой итерации.
right_pos -= TILE_SIZEX; *
* Тайлы отрезка забираем ОДНИМ банковым вызовом. Раньше на каждую
* колонку шёл свой pop_tile_at (__banked, банк 1 -> банк 3): на сцене
* 11/15 это девять трамплинов и 36 786 тактов на весь луч (замер
* 2026-08-19). */
{
int8_t lcol = Kid.curr_col, rcol = Guard.curr_col, col, lcol0;
while (left_pos <= right_pos) { if (lcol > rcol) { int8_t t = lcol; lcol = rcol; rcol = t; }
tile = tile_at_kid(left_pos); lcol0 = lcol; /* БАЗА индексации буфера — фиксируем
/* Сквозь это не видно вовсе. */ * ДО сдвигов ниже: row_t[0] это
if (tile == TILE_WALL || tile == TILE_DOORTOP_FLOOR || tile == TILE_DOORTOP) { * тайл lcol0, и сдвиг lcol/rcol на
can_guard_see_kid = 0; * чомпере и воротах её менять не
return; * должен. */
pop_row_tiles(Kid.curr_row, lcol, rcol, row_t);
/* Чомпер стоит у ЛЕВОГО края тайла — свой тайл он не перекрывает. */
if (row_t[0] == TILE_CHOMPER) lcol++;
/* Ворота — у ПРАВОГО края, поэтому крайний правый тайл не считается. */
if (row_t[rcol - lcol0] == TILE_GATE) rcol--;
for (col = lcol; col <= rcol; col++) {
tile = row_t[col - lcol0];
/* Сквозь это не видно вовсе. */
if (tile == TILE_WALL || tile == TILE_DOORTOP_FLOOR || tile == TILE_DOORTOP) {
can_guard_see_kid = 0;
return;
}
/* Видно, но страж не пойдёт: провалится / попадёт под чомпер /
* упрётся в неоткрытые ворота / шагнёт в дыру. */
if (tile == TILE_LOOSE || tile == TILE_CHOMPER || !tile_is_floor(tile)) {
can_guard_see_kid = 1;
} else if (tile == TILE_GATE &&
pop_gate_modif(col, Kid.curr_row) < 112) {
can_guard_see_kid = 1; /* ворота подняты не до конца */
}
} }
/* Видно, но страж не пойдёт: провалится / попадёт под чомпер /
* упрётся в неоткрытые ворота / шагнёт в дыру. */
if (tile == TILE_LOOSE || tile == TILE_CHOMPER || !tile_is_floor(tile)) {
can_guard_see_kid = 1;
} else if (tile == TILE_GATE &&
pop_gate_modif(tile_col, Kid.curr_row) < 112) {
can_guard_see_kid = 1; /* ворота подняты не до конца */
}
left_pos += TILE_SIZEX;
} }
} }
+32
View File
@@ -330,6 +330,38 @@ uint8_t pop_tile_at(int8_t col, int8_t row) __banked
return get_tile(col, row); return get_tile(col, row);
} }
/* Тайлы ОТРЕЗКА ряда одним вызовом — для луча видимости стража.
*
* Зачем: pop_tile_at объявлен __banked, а луч живёт в guards.c (банк 1) и
* звал его ПО ОДНОЙ КОЛОНКЕ. На сцене 11/15 (Кид в колонке 2, страж в 8)
* это девять трамплинов банк 1 -> банк 3 за кадр, и весь луч стоил 36 786
* тактов — 5,6 % работы кадра при том, что делает он девять чтений байта
* (замер 2026-08-19). Цена трамплина здесь та же, что уже измерена у
* pop_clip_char_top (8 892 такта ради одной проверки тайла).
*
* Колонки за пределами 0..9 разрешает сам get_tile (шов с соседней
* комнатой), поэтому диапазон отдаём как есть, без клипа.
*
* out обязан вмещать c1 - c0 + 1 байт; вызывающий даёт буфер на 12
* (колонки −1..10 плюс запас). */
void pop_row_tiles(int8_t row, int8_t c0, int8_t c1, uint8_t *out) __banked
{
int8_t col;
/* БЫСТРЫЙ ПУТЬ — отрезок целиком внутри комнаты. get_tile для ряда
* 0..2 и колонки 0..9 сводится ровно к `g_fg[row*10+col] & 0x1F`
* (см. его тело выше), а всё остальное там — разбор швов и краёв
* уровня. Идём указателем: иначе на каждую колонку заново считается
* row*10 + col. */
if (row >= 0 && row <= 2 && c0 >= 0 && c1 <= 9 && g_fg) {
const uint8_t *p = g_fg + (int)row * 10 + c0;
for (col = c0; col <= c1; col++)
*out++ = (uint8_t)(*p++ & 0x1F);
return;
}
for (col = c0; col <= c1; col++)
*out++ = get_tile(col, row);
}
/* can_bump_into_gate (seg004:373): ОТКРЫТЫЕ ворота проходимы — Kid не бампит и /* can_bump_into_gate (seg004:373): ОТКРЫТЫЕ ворота проходимы — Kid не бампит и
* идёт сквозь них (на шве — уходит в соседнюю комнату; внутри комнаты — просто * идёт сквозь них (на шве — уходит в соседнюю комнату; внутри комнаты — просто
* проходит по полу под поднятыми барами). ВАЖНО: ворота остаются тайлом 4 (в * проходит по полу под поднятыми барами). ВАЖНО: ворота остаются тайлом 4 (в
+4
View File
@@ -58,6 +58,10 @@ void pop_map_set_room(uint8_t room) __banked;
* (guards.c): он идёт по ряду между Кидом и стражем. */ * (guards.c): он идёт по ряду между Кидом и стражем. */
uint8_t pop_tile_at(int8_t col, int8_t row) __banked; uint8_t pop_tile_at(int8_t col, int8_t row) __banked;
/* Тайлы отрезка ряда c0..c1 одним банковым вызовом (см. тело в pop_map.c):
* луч видимости стража иначе платит по трамплину за КАЖДУЮ колонку. */
void pop_row_tiles(int8_t row, int8_t c0, int8_t c1, uint8_t *out) __banked;
/* determine_col (seg006:014D): Kid.curr_col = m7(dx_weight()). Наружу — для /* determine_col (seg006:014D): Kid.curr_col = m7(dx_weight()). Наружу — для
* pop_load_fram_det_col (pop_kid.c), порт load_fram_det_col. */ * pop_load_fram_det_col (pop_kid.c), порт load_fram_det_col. */
void pop_determine_col(void) __banked; void pop_determine_col(void) __banked;
+1
View File
@@ -367,6 +367,7 @@ int main(void)
pop_check_killed_shadow(); /* спецсобытие ур.12: смерть тени = смерть Кида */ pop_check_killed_shadow(); /* спецсобытие ур.12: смерть тени = смерть Кида */
pop_dbg_b6(); /* ЗАМЕР: три спецсобытия уровней сделаны */ pop_dbg_b6(); /* ЗАМЕР: три спецсобытия уровней сделаны */
pop_frame_timers(); /* timers (seg003:0735): вспышка слияния */ pop_frame_timers(); /* timers (seg003:0735): вспышка слияния */
pop_dbg_m9(); /* ЗАМЕР: pop_frame_timers сделан */
pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */ pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
pop_dbg_m14(); /* ЗАМЕР: спецсобытия + луч видимости */ pop_dbg_m14(); /* ЗАМЕР: спецсобытия + луч видимости */
pop_ctrl_tick(); /* ввод -> control(): смена seq */ pop_ctrl_tick(); /* ввод -> control(): смена seq */