From 72797e1be8ff6ed39a89845633055b098c089342 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Mon, 10 Aug 2026 20:23:43 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D0=BD=D0=B2=D0=B5=D0=BD=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=BF=D0=BE=20.asm:=20=D1=83=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D1=8B=20=D1=82=D1=80=D0=B8,=20=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20=D1=80=D0=B0=D0=B7?= =?UTF-8?q?=D0=BE=D0=B1=D1=80=D0=B0=D0=BD=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Обход всех сгенерированных .asm (awk по `call __div/__mod/__mul` с привязкой к строке исходника) нашёл 31 вызов в 9 модулях. Три из них были в горячем пути: - pop_cdraw.c calc_screen_x_coord: `x * 8 / 7` -> __divsint, 2 400 тактов на ПЕРСОНАЖА КАЖДЫЙ КАДР (два вызова при живом сопернике). Заменено тождеством 8x/7 == x + x/7 плюс таблица DIV7[256] в банке 4 — резидент не тронут, обычный диапазон (obj_x 0..252 при x_bump 58..184) покрыт целиком, деление осталось только хвостом для шва (render_dx = ∓140). - pop_guard.c guard_col_from_x: /14 и %14 звались БЕЗУСЛОВНО, мимо POP_TILE_DIV — единственное 16-битное деление без подключённой таблицы. - pop_trob.c animate_chomper: `tp / 10` на чомпера каждый кадр, при том что TP_ROW/TP_COL лежали в этом же файле, но ниже по тексту. Таблицы подняты выше чомперов. Остальные 25 оставлены осознанно и расписаны в TASKS_OPEN.md: хвосты за таблицей (x вне 0..255 = персонаж в соседней комнате), намеренный медленный хвост pop_y_to_row, недостижимая ветка pop_rnd_fit и холодные места (вход стража, старт уровня, читы, имя файла, отладочный HUD). В банках 4, 6, 7 теперь ноль __div*. Тождество 8x/7 закреплено тестом geom_mul8div7_identity (перебор −420..700), таблица DIV7 сверена с x//7. tests-host: [geom] 840 -> 1961, все 5 наборов прошли. Co-Authored-By: Claude Opus 5 --- applications/PoP/roomtest/TASKS_OPEN.md | 48 +++++++++++++++++++ applications/PoP/roomtest/pop_cdraw.c | 47 ++++++++++++++++-- applications/PoP/roomtest/pop_guard.c | 15 ++++-- applications/PoP/roomtest/pop_trob.c | 23 +++++---- applications/PoP/roomtest/tests-host/t_geom.c | 13 +++++ 5 files changed, 129 insertions(+), 17 deletions(-) diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md index e43ab18..463c219 100644 --- a/applications/PoP/roomtest/TASKS_OPEN.md +++ b/applications/PoP/roomtest/TASKS_OPEN.md @@ -435,6 +435,54 @@ wait-state'ы, и замеренная стоимость выходит **≈ 2 (перебор −400..400 против исходной формулы) — вызовы разбросаны по трём банкам, и соблазн написать деление «по месту» возвращается. +### Полная инвентаризация делений 2026-08-10 + +Способ (повторяемый одной командой из `.sprinter-cc-roomtest/`): + +``` +awk '/^;[a-z_0-9]+\.[ch]:[0-9]+:/{s=$0} /^\tcall\t__(div|mod|mul)/{printf "%-22s %-12s %s\n",FILENAME,$2,s}' *.asm +``` + +Найден **31 вызов в 9 модулях**. Прибрано три места, остальное разобрано и +осознанно оставлено. + +Убрано: + +| место | что было | почему стоило | +|---|---|---| +| `pop_cdraw.c` `calc_screen_x_coord` (2 вызова) | `x * 8 / 7` = `__divsint`, **2 400 тактов НА ПЕРСОНАЖА КАЖДЫЙ КАДР** | последнее деление в горячем пути; ~4 800/кадр при живом сопернике | +| `pop_guard.c` `guard_col_from_x` | `/14` + `%14` **безусловно**, мимо `POP_TILE_DIV` | единственное 16-битное деление, у которого таблица вообще не была подключена | +| `pop_trob.c` `animate_chomper` | `tp / 10` = `__divuchar` на чомпера каждый кадр | таблицы `TP_ROW`/`TP_COL` уже лежали в ЭТОМ ЖЕ файле, но ниже по тексту — чомперы их не видели | + +Приём для `×8/7`: тождество **`8x/7 == x + x/7`** (7x делится нацело, поэтому +усечение обеих частей совпадает — и для отрицательных с трункейтом к нулю +тоже). Дальше `x/7` — таблица `DIV7[256]` в банке 4 (резидент не тронут). +Диапазон покрыт не «частый», а весь обычный: `x_bump[5..14] = 58..184`, то +есть `obj_x = 2*fwd − 116` у персонажа в отрисованной комнате лежит в 0..252; +хвост нужен только на шве (`render_dx = ∓140`). Тождество проверено тестом +`geom_mul8div7_identity`, сама таблица — сверкой с `x//7` питоном. + +Оставлено сознательно (НЕ трогать, это не забытые места): + +- **Хвосты за таблицей** — `guards.c:84`, `pop_bg.c:497`, `roomtest.c:163`, + `pop_map.c:412`: срабатывают только при x вне 0..255, то есть когда + персонаж в соседней комнате. Убирать их — это расширять `POP_TILE_DIV` до + −140..395 (+280 Б резидента) ради редкого пути. +- **`pop_geom.c:21`** — намеренный медленный хвост `pop_y_to_row` (см. выше + про подъём деления SDCC). +- **`pop_geom.c:52`** — `v % n` в `pop_rnd_fit` для не-степени двойки: + оригинал зовёт `prandom(1)/(255)/(0xFF)`, все три идут веткой с маской, + сюда управление не приходит вовсе. +- **Холодные, раз на комнату/уровень/событие**: `pop_guard.c:266/268/269` + (вход стража), `pop_map.c:310` (пробуждение скелета), `pop_trob.c` дверь + уровня, `roomtest.c:473/663/887` (читы и старт), `pop_level.c:131/132` + (имя файла уровня), `roomtest.c:976/977` (отладочный HUD номера комнаты). + Каждое — единицы вызовов за секунды игры; таблицы под них только раздули + бы код. + +Итог: **в горячем пути делений не осталось**. В банках 4, 6, 7 — ноль +`__div*`; всё, что видно в списке выше, либо за быстрым путём, либо холодное. + **Профиль работы за логический кадр СЕЙЧАС (409 956 тактов + ~15 600 ввод):** | блок | тактов | % растрового кадра | diff --git a/applications/PoP/roomtest/pop_cdraw.c b/applications/PoP/roomtest/pop_cdraw.c index 4be8d7d..55b7773 100644 --- a/applications/PoP/roomtest/pop_cdraw.c +++ b/applications/PoP/roomtest/pop_cdraw.c @@ -110,6 +110,46 @@ void pop_guard_free(void) __banked g_ok = 0; } +/* ---- calc_screen_x_coord (seg008:2602) БЕЗ деления ------------------- * + * + * Логическая X (280 шир = 28px/тайл) -> экранная (320 = 32px/тайл): ×8/7. + * SDLPoP пишет это как `logical_x*320/280`, у нас SDCC разворачивал в + * `__divsint` — 2 400 тактов на вызов, и зовётся оно на КАЖДОГО персонажа + * КАЖДЫЙ кадр (последнее деление, остававшееся в горячем пути). + * + * Тождество: 8x/7 == x + x/7. Оно ТОЧНОЕ в целых, потому что 7x делится + * нацело: floor((7x+x)/7) = x + floor(x/7); для отрицательных с усечением + * к нулю — тоже (проверено перебором в tests-host). + * + * Таблица кроет 0..255 — это ВЕСЬ обычный случай, а не «частый»: + * x_bump[5..14] = 58..184, то есть у персонажа в отрисованной комнате + * obj_x = 2*fwd − 116 лежит в 0..252. Медленный хвост остаётся для шва, + * где render_dx уводит X на ∓140. */ +static const uint8_t DIV7[256] = { + 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, + 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, + 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, + 6, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 9, + 9, 9, 9, 9, 9, 9,10,10,10,10,10,10,10,11,11,11, + 11,11,11,11,12,12,12,12,12,12,12,13,13,13,13,13, + 13,13,14,14,14,14,14,14,14,15,15,15,15,15,15,15, + 16,16,16,16,16,16,16,17,17,17,17,17,17,17,18,18, + 18,18,18,18,18,19,19,19,19,19,19,19,20,20,20,20, + 20,20,20,21,21,21,21,21,21,21,22,22,22,22,22,22, + 22,23,23,23,23,23,23,23,24,24,24,24,24,24,24,25, + 25,25,25,25,25,25,26,26,26,26,26,26,26,27,27,27, + 27,27,27,27,28,28,28,28,28,28,28,29,29,29,29,29, + 29,29,30,30,30,30,30,30,30,31,31,31,31,31,31,31, + 32,32,32,32,32,32,32,33,33,33,33,33,33,33,34,34, + 34,34,34,34,34,35,35,35,35,35,35,35,36,36,36,36, +}; + +static int scr_x(int x) +{ + if ((unsigned int)x < 256u) return x + DIV7[x]; + return x * 8 / 7; +} + /* Добавить накладной спрайт (клинок / брызги) в overlay-прямоугольник * страницы. Оба мелкие и в одном кадре встречаются редко — их объединение * дешевле третьего heal-слота. */ @@ -314,7 +354,7 @@ static void cd_splash(pop_cdraw_t *s, uint8_t who, const atlas_t *pages, } else { qy -= (who == POP_CH_KID) ? 15 : 11; } - qx = (qx + fw) * 8 / 7; /* calc_screen_x_coord */ + qx = scr_x(qx + fw); /* calc_screen_x_coord */ img = (const uint8_t *)atlas_image(ap, aidx); gfx_w0_map(ap->page); w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); @@ -395,8 +435,9 @@ void pop_char_draw(uint8_t who) __banked s->fpx = fp_x; s->fpy = obj_y; /* seg008 calc_screen_x_coord: логическая X (280 шир = 28px/тайл) -> * экран (320 шир = 32px/тайл), ×320/280 = ×8/7. БЕЗ этого персонаж - * уезжает влево от тайлов тем сильнее, чем правее. */ - obj_x = obj_x * 8 / 7; + * уезжает влево от тайлов тем сильнее, чем правее. Через scr_x — + * таблицей, без __divsint (см. шапку DIV7). */ + obj_x = scr_x(obj_x); page = (uint8_t)(fr->image >> 3); idx = (uint8_t)(fr->image & 7); diff --git a/applications/PoP/roomtest/pop_guard.c b/applications/PoP/roomtest/pop_guard.c index 2e3557d..1d90f88 100644 --- a/applications/PoP/roomtest/pop_guard.c +++ b/applications/PoP/roomtest/pop_guard.c @@ -12,6 +12,7 @@ #include "pop_kid.h" /* Char/play_seq — интерпретатор общий */ #include "pop_map.h" /* hitp_* — HP Кида */ #include "pop_state.h" /* pop_immortal — чит бессмертия */ +#include "pop_tile.h" /* POP_TILE_DIV — колонка по X без деления */ #include "kid_data.h" pop_char_t Guard; @@ -233,10 +234,16 @@ uint8_t pop_guard_follow(uint8_t new_room, uint8_t side) * floor((xpos − 7 − 58) / 14), где 58 = pop_x_bump[FIRST_ONSCREEN_COLUMN]. */ static int8_t guard_col_from_x(uint8_t xpos) { - int16_t x = (int16_t)xpos - 7 - pop_x_bump[FIRST_ONSCREEN_COLUMN]; - int16_t col = x / TILE_SIZEX; - if (x % TILE_SIZEX < 0) --col; - return (int8_t)col; + /* Таблицей (резидентная POP_TILE_DIV: −58 уже внутри значений) — тот же + * приём, что в tile_at_kid/get_tile_div_mod. Здесь он кроет ПОЧТИ всё: + * xpos — байт, так что медленный хвост остаётся только для xpos < 7. */ + if (xpos >= 7) return POP_TILE_DIV[xpos - 7]; + { + int16_t x = (int16_t)xpos - 7 - pop_x_bump[FIRST_ONSCREEN_COLUMN]; + int16_t col = x / TILE_SIZEX; + if (x % TILE_SIZEX < 0) --col; + return (int8_t)col; + } } void pop_guard_enter(uint8_t room) diff --git a/applications/PoP/roomtest/pop_trob.c b/applications/PoP/roomtest/pop_trob.c index be03f92..7d66f32 100644 --- a/applications/PoP/roomtest/pop_trob.c +++ b/applications/PoP/roomtest/pop_trob.c @@ -198,6 +198,17 @@ static uint8_t bubble_next_frame(uint8_t curr) return (next >= 8) ? 1 : next; } +/* Разбор номера тайла 0..29 на ряд/колонку без деления (`tp / 10`, `tp % 10` + * — это __divuchar/__moduchar, а в animate_chomper они звались на каждого + * чомпера каждый кадр). Определены ЗДЕСЬ, а не перед pop_process_trobs, где + * лежали раньше: чомперы выше по файлу и оригинала не видели. */ +static const uint8_t TP_ROW[30] = { + 0,0,0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,1,1, 2,2,2,2,2,2,2,2,2,2 +}; +static const uint8_t TP_COL[30] = { + 0,1,2,3,4,5,6,7,8,9, 0,1,2,3,4,5,6,7,8,9, 0,1,2,3,4,5,6,7,8,9 +}; + /* animate_chomper (seg007:0448). Модификатор: младшие 7 бит — фаза 1..N, * старший — «перемололо кого-то» (кровь). Фаза крутится по кругу до * POP_CHOMPER_SPEED (у оригинала 15), то есть челюсти смыкаются раз в N @@ -217,7 +228,7 @@ static void animate_chomper(uint8_t *m, int8_t *type, frame = (uint8_t)((*m & 0x7F) + 1); if (frame > POP_CHOMPER_SPEED) frame = 1; *m = (uint8_t)(blood | frame); - if ((room != drawn_room || tp / 10 != Kid.curr_row || + if ((room != drawn_room || (int8_t)TP_ROW[tp] != Kid.curr_row || (Kid.alive >= 0 && blood == 0)) && frame >= 6) *type = -1; } @@ -467,14 +478,6 @@ static uint8_t torch_tick; #define TORCH_ANIM_NOW 1 #endif -/* Разбор номера тайла 0..29 на ряд/колонку без деления. */ -static const uint8_t TP_ROW[30] = { - 0,0,0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,1,1, 2,2,2,2,2,2,2,2,2,2 -}; -static const uint8_t TP_COL[30] = { - 0,1,2,3,4,5,6,7,8,9, 0,1,2,3,4,5,6,7,8,9, 0,1,2,3,4,5,6,7,8,9 -}; - void pop_process_trobs(uint8_t cur_room) __banked { uint8_t i, w = 0; @@ -608,7 +611,7 @@ void pop_process_trobs(uint8_t cur_room) __banked * непрозрачны, поэтому wipe не нужен. */ /* Створку рисует ПРАВАЯ половина двери; последний кадр анимации * помечаем на обе страницы, иначе дверь дрожит через кадр. */ - if (tp % 10 + 1 < 10) + if (TP_COL[tp] < 9) pop_set_redraw((uint8_t)(tp + 1), POP_RD_LEVELDOOR, (uint8_t)(type < 0 ? 2 : 1)); } diff --git a/applications/PoP/roomtest/tests-host/t_geom.c b/applications/PoP/roomtest/tests-host/t_geom.c index fc0f6df..5276c43 100644 --- a/applications/PoP/roomtest/tests-host/t_geom.c +++ b/applications/PoP/roomtest/tests-host/t_geom.c @@ -72,6 +72,18 @@ TC_TEST(geom_y_to_row_matches_formula) } } +/* calc_screen_x_coord (seg008:2602) считается в pop_cdraw.c таблицей DIV7 по + * тождеству 8x/7 == x + x/7 (7x делится нацело, поэтому усечение обеих частей + * совпадает). Само тождество и проверяем — оно и есть неочевидная часть; + * таблица DIV7 сгенерирована как x/7 и живёт в банке 4, сюда не линкуется. */ +TC_TEST(geom_mul8div7_identity) +{ + int16_t x; + /* obj_x = 2*fwd − 116; со сдвигом шва (∓140) уходит в −406..684. */ + for (x = -420; x <= 700; x++) + TC_EQ((int16_t)(x * 8 / 7), (int16_t)(x + x / 7)); +} + /* ---- PRNG: сверка asm-LCG с эталонной формулой ---------------------- */ #if POP_PRANDOM_EXACT @@ -154,6 +166,7 @@ int main(void) TC_RUN(geom_yland_rows); TC_RUN(geom_y_to_row); TC_RUN(geom_y_to_row_matches_formula); + TC_RUN(geom_mul8div7_identity); #if POP_PRANDOM_EXACT TC_RUN(geom_lcg_matches_reference); TC_RUN(geom_lcg_seed_zero);