From f8493a4c04541386b0b437a5a93e2908e0d146a8 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Wed, 19 Aug 2026 12:37:06 +0300 Subject: [PATCH] =?UTF-8?q?P2b:=20=D0=BB=D1=83=D1=87=20=D0=B2=D0=B8=D0=B4?= =?UTF-8?q?=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B8=20=D1=81=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D0=B6=D0=B0=2036=20786=20->=2013=20002=20(-65=20%)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Замер отделил луч от 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 --- applications/PoP/roomtest/guards.c | 113 +++++++++++++-------------- applications/PoP/roomtest/pop_map.c | 32 ++++++++ applications/PoP/roomtest/pop_map.h | 4 + applications/PoP/roomtest/roomtest.c | 1 + 4 files changed, 91 insertions(+), 59 deletions(-) diff --git a/applications/PoP/roomtest/guards.c b/applications/PoP/roomtest/guards.c index b2cd844..d3a7143 100644 --- a/applications/PoP/roomtest/guards.c +++ b/applications/PoP/roomtest/guards.c @@ -56,39 +56,16 @@ static uint8_t tile_is_floor(uint8_t t) } } -/* Тайл в ряду Кида по X-координате персонажа (порт get_tile_at_kid, - * seg003:761): xpos − 7 делится на ширину тайла с округлением вниз. */ -static int8_t tile_col; /* колонка последнего tile_at_kid */ +/* get_tile_at_kid (seg003:761) СНЯТ вместе с переходом луча на колонки: + * единственным его вызывающим был этот луч, а он больше не работает с + * X-координатами (разбор — в теле pop_check_can_guard_see_kid ниже). + * Таблица POP_TILE_DIV осталась в резиденте, ею пользуется физика. */ -static uint8_t tile_at_kid(int16_t xpos) -{ - /* get_tile_div_mod_m7 (seg006:697): колонка = floor((xpos − 7 − 58)/14), - * СРАЗУ в координатах комнаты (58 = x_bump[FIRST_ONSCREEN_COLUMN]). - * Оригинал берёт её из tile_div_tbl — и мы теперь тоже: резидентная - * POP_TILE_DIV[] это ровно она (индекс = xpos, значение = floor((xpos−58)/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); -} +/* Буфер тайлов луча — file-scope, а не локальный массив: локальный уезжает + * в стековый кадр, и каждое чтение из него стоит `-n(ix)` (19 тактов + * номинала, ~46 с wait-state'ами). Тот же приём, что у draw_tile и + * pop_blit_b. Размер — колонки −1..10 плюс запас. */ +static uint8_t row_t[12]; /* check_can_guard_see_kid (seg003:688) — луч видимости по ряду. * Результат в 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 { uint8_t kid_frame = Kid.frame; - int16_t left_pos, right_pos; uint8_t tile; /* МЫШЬ Кида «не видит» (seg003:0965): иначе он выхватит меч на зверька, @@ -122,34 +98,53 @@ void pop_check_can_guard_see_kid(void) __banked } can_guard_see_kid = 2; - left_pos = pop_x_bump[Kid.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX; - right_pos = pop_x_bump[Guard.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX; - if (left_pos > right_pos) { - int16_t t = left_pos; left_pos = right_pos; right_pos = t; - } - /* Чомпер стоит у ЛЕВОГО края тайла — свой тайл он не перекрывает. */ - if (tile_at_kid(left_pos) == TILE_CHOMPER) - left_pos += TILE_SIZEX; - /* Ворота — у ПРАВОГО края, поэтому крайний правый тайл не считается. */ - if (tile_at_kid(right_pos) == TILE_GATE) - right_pos -= TILE_SIZEX; + /* ПО КОЛОНКАМ, а не по X-координатам. + * + * Оригинал (и SDLPoP, seg003:688) идёт по x с шагом TILE_SIZEX и на + * каждом шаге переводит x в колонку — потому что x у него уже под рукой. + * Но начальные x — это РОВНО центры тайлов персонажей + * (x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX), а перевод обратно + * даёт ту же колонку: floor((58 + col*14 - 58) / 14) == col. Значит + * весь проход эквивалентен обходу колонок, и ни одна x-координата тут не + * нужна: уходят 16-битный шаг, 16-битное сравнение и индексация таблицы + * POP_TILE_DIV на каждой итерации. + * + * Тайлы отрезка забираем ОДНИМ банковым вызовом. Раньше на каждую + * колонку шёл свой 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) { - tile = tile_at_kid(left_pos); - /* Сквозь это не видно вовсе. */ - if (tile == TILE_WALL || tile == TILE_DOORTOP_FLOOR || tile == TILE_DOORTOP) { - can_guard_see_kid = 0; - return; + if (lcol > rcol) { int8_t t = lcol; lcol = rcol; rcol = t; } + lcol0 = lcol; /* БАЗА индексации буфера — фиксируем + * ДО сдвигов ниже: row_t[0] это + * тайл lcol0, и сдвиг lcol/rcol на + * чомпере и воротах её менять не + * должен. */ + 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; } } diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index ebe95d7..bcd1d71 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -330,6 +330,38 @@ uint8_t pop_tile_at(int8_t col, int8_t row) __banked 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 не бампит и * идёт сквозь них (на шве — уходит в соседнюю комнату; внутри комнаты — просто * проходит по полу под поднятыми барами). ВАЖНО: ворота остаются тайлом 4 (в diff --git a/applications/PoP/roomtest/pop_map.h b/applications/PoP/roomtest/pop_map.h index 6269868..3b03845 100644 --- a/applications/PoP/roomtest/pop_map.h +++ b/applications/PoP/roomtest/pop_map.h @@ -58,6 +58,10 @@ void pop_map_set_room(uint8_t room) __banked; * (guards.c): он идёт по ряду между Кидом и стражем. */ 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()). Наружу — для * pop_load_fram_det_col (pop_kid.c), порт load_fram_det_col. */ void pop_determine_col(void) __banked; diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index aead726..d97db23 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -367,6 +367,7 @@ int main(void) pop_check_killed_shadow(); /* спецсобытие ур.12: смерть тени = смерть Кида */ pop_dbg_b6(); /* ЗАМЕР: три спецсобытия уровней сделаны */ pop_frame_timers(); /* timers (seg003:0735): вспышка слияния */ + pop_dbg_m9(); /* ЗАМЕР: pop_frame_timers сделан */ pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */ pop_dbg_m14(); /* ЗАМЕР: спецсобытия + луч видимости */ pop_ctrl_tick(); /* ввод -> control(): смена seq */