# roomtest — план оптимизации по размеру + переход на huge/banking Статус: **план для отдельной сессии** (2026-07-21). Документ самодостаточный (рассчитан на старт с пустого контекста). Цель — освободить место: сейчас `applications/PoP/roomtest` в режиме `small` почти упёрся в потолок 32 КБ. Правило проекта (`applications/PoP/CLAUDE.md`): механику/раскладку памяти сверять с исходником и с memory (`sprinter_memory_modes`, `sdcc_banking`, `bank_local_data_pattern`, `pop_banking_architecture`). Перед оптимизацией — `make size-check`-подобный замер до/после (здесь — руками по `.map`). --- ## 0. Как мерить - Сборка: `cd applications/PoP/roomtest && make roomtest.exe` (режим `small`, `--gfx 256`). Карта символов — `.sprinter-cc-roomtest/roomtest.map` (адреса сдвигаются при каждой пересборке!). - Размеры областей — из `.map` (`_CODE`, `_DATA`, `_BSS`). - Вклад модулей в `_CODE` — атрибуция диапазонов между символами по модулю (скрипт-однострочник на python в истории; группировать символы `.map` по 3-й колонке-модулю и суммировать `addr[i+1]-addr[i]`). - MAME-проверка после изменений раскладки ОБЯЗАТЕЛЬНА (режимы памяти — типовой источник «молча ломается», см. `sprinter_memory_modes`). ## 1. ТЕКУЩЕЕ СОСТОЯНИЕ (замер 2026-07-21) Режим `small` = единое пространство **W1+W2 = 0x4000..0xBFFF (32 КБ)**; CODE с 0x4100, DATA/BSS/heap цепляются ЗА CODE автоматически (`--data-loc 0` = linker chains), стек — вверху W2. | Область | Размер | Диапазон | |---------|--------|----------| | `_CODE` | ~27 250 Б (0x6A6F) | 0x4100–0xAB6F | | `_HOME` | 227 Б | 0xAB6F | | `_DATA` | 3 449 Б (0x0D79) | 0xAC78–0xB9F1 | | `_BSS` | 290 Б | | **Образ ≈ 31.2 КБ; до верха W2 (0xBFFF) остаётся ≈ 1.3 КБ на кучу+стек.** Куча в roomtest почти не используется (атласы/уровень — в EMM-страницах), но запас критично мал. ### Вклад модулей в _CODE (по .map, приблизительно) ``` 7003 pop_bg (вся отрисовка тайлов/слоёв/wall_pattern) 6326 pop_kid (из них ~3745 Б — СТАТ. ТАБЛИЦЫ kid_data.h, см. ниже) 3762 pop_map (коллизия/физика/пики) 1455 pop_trob (кнопки/ворота/пики-каркас) 1224 pop_level (загрузка уровня, doorlink) 914 roomtest (главный цикл) ~7000 libc/libbgi (gfx_blit*, atlas_load, kbd_raw, open/read, irq, div/mul…) ``` ### Крупные СТАТИЧЕСКИЕ данные (сейчас в _CODE как `const`) - **`kid_data.h` — самый большой кусок, ~3.7 КБ**, живёт в _CODE (атрибутируется pop_kid): - `kid_seqtbl[2310]` — байткод последовательностей (play_seq). - `kid_frames[241]` × 5 Б = 1205 Б — таблица кадров (image,dx,dy,flags,sword). - `kid_seq_off[115]` × 2 Б = 230 Б — смещения seq. - `pop_bg`: `tile_table[31]`×12 = 372 Б + ~20 мелких const-таблиц (COL_XH, WALL_FRAM_*, SPIKES_FRAM_{RIGHT,LEFT,FORE}, LOOSE_FRAM_*, DOOR_FRAM_SLICE, BLUELINE_*, LPOS/RPOS, FLOOR_LEFT_OVERLAY) — суммарно ~0.5–0.7 КБ. - `pop_map`: `x_bump[20]`, `y_land[5]`, `wall_dl/dr`, `dir_front/behind` — ~100 Б. - В `_DATA` (W2, не CODE): `room_modif[24][30]`=720 Б + копии LINKLOC/LINKMAP=512 Б (pop_trob/pop_level) + рабочие массивы roomtest. --- ## 2. ПУТЬ A — оптимизация КОДА (без смены модели) 1. **Компиляторные флаги** (`bin/sprinter-cc`): попробовать `--opt-code-size` у SDCC и подобрать `--max-allocs` (сейчас дефолт 100000; меньше = мельче код, но медленнее компиляция; см. `mdview2_size_budget` — там `--max-allocs` давал −1.4 КБ). Замерить каждый модуль отдельно. 2. **Дедуп подстановки нажатой кнопки**: логика `opener→floor / closer→stuck` по таймеру связи ПРОДУБЛИРОВАНА в `draw_tile` и `fore_tile` (pop_bg.c). Вынести в `static inline`/helper `subst_pressed_button(code,mod)`. 3. **wall_pattern / prandom** (pop_bg): 32-битный LCG (`unsigned long`) — пользователь не любит 32-бит (см. `avoid_32bit_arith_z80`); но это PRNG оригинала (нужен для совпадения раскладки стен) — трогать осторожно, только если найдётся 16-битный эквивалент, дающий ТУ ЖЕ последовательность. 4. **Ревизия дублей**: `y_to_row` определён в pop_bg И pop_map; мелкие геометрические хелперы дублируются — свести в один internal-модуль. 5. `/simplify`-проход по последним правкам Фазы B (pop_trob/pop_bg). Ожидаемый выигрыш пути A: единицы–первые сотни байт на пункт; в сумме, оптимистично, ~1–2 КБ. Недостаточно как единственная мера. --- ## 3. ПУТЬ B — вынос СТАТ. ДАННЫХ в EMM-страницы (с атласами / с level) **Идея (по замечанию пользователя):** EMM-страницы атласов и уровня использованы лишь частично (страница 16 КБ, данных меньше), в «хвосте» — свободное место. Часть `const`-таблиц можно хранить ТАМ, а не в _CODE/_DATA, если таблица читается ИМЕННО ТОГДА, когда нужная страница уже в W0. **Механика W0:** атласы блитятся из W0 (`_gfx_w0_state`: `_gfx_w0_cur` — спрайт-страница в W0; ISR-стаб `_gfx_w0_isr` возвращает её после прерывания). Уровень (pop_level) маппит свою страницу в W0 на время извлечения (`gfx_w0_map`/`gfx_w0_unmap`). → пока страница в W0, CPU может читать и данные из неё по адресам 0x0000..0x3FFF. **Категоризация таблиц по W0-контексту (задача сессии — уточнить по каждой):** - **(a) Читается, когда в W0 АТЛАС** → хранить в свободном хвосте атлас-страницы. Кандидаты — таблицы, которые нужны В МОМЕНТ блита конкретного атласа. ГРАБЛИ: `draw_tile` читает `tile_table`/`COL_XH` ДО блита (чтобы решить, какой спрайт/куда) — в этот момент в W0 может быть ДРУГАЯ страница (DSS/предыдущий атлас). Т.е. большинство draw-таблиц читаются ВНЕ W0-атлас-контекста → «в лоб» не переносятся. Нужен аудит КАЖДОГО чтения: гарантирована ли нужная страница в W0 в этот тик. - **(b) Читается, когда в W0 LEVEL** → хранить с уровнем (в его странице; там ~13.7 КБ свободно из 16). Кандидаты: константы декода doorlink, разбор комнат — всё, что pop_level делает под `gfx_w0_map(lvl_page)`. - **(c) Нужна и там, и там** → дублировать в обеих страницах ЛИБО оставить резидентной (если дубли дороже экономии). - **(d) Читается в чистой ЛОГИКЕ (W0 не важен)** → перенос требует ЯВНОГО `gfx_w0_map` на каждое чтение (дорого, особенно в горячих циклах) → как правило оставить резидентной. **Отдельно `kid_data.h` (3.7 КБ — самый жирный кандидат):** - `kid_frames`/`kid_seqtbl` читаются в `play_seq` (ЧИСТАЯ логика, каждый тик) И в `kid_draw` (блит из kid-атласа, kid-страница в W0). Т.е. частично (a), частично (d). Перенос всей таблицы в kid-атлас-страницу заставит `play_seq` делать `gfx_w0_map` на каждый шаг байткода → замерить стоимость (может убить бюджет спрайтов, см. `sprite_engine_perf`). Вариант: держать в EMM отдельной страницей данных Kid и маппить один раз на кадр вокруг kid_tick+kid_draw. - Это самый большой одиночный выигрыш (−3.7 КБ из _CODE), но и самый рискованный по скорости — приоритетный к ПРОТОТИПИРОВАНИЮ и замеру. **Паттерн переноса writable/const данных в банк/страницу:** см. memory `bank_local_data_pattern` (--codeseg/--constseg/--dataseg BANKn + trampoline-fix + mkexe -p 0) и `sdcc_static_storage_gotcha`. ### 3.1 Свободное место в страницах (замер 2026-07-21, страница = 16384 Б) ``` BG-атласы: размер свободно pop_env0.atl 10578 5806 pop_env1.atl 12449 3935 <- САМАЯ ТЕСНАЯ из bg pop_env2.atl 10798 5586 pop_env3.atl 5032 11352 <- много места pop_env4.atl 8498 7886 pop_wall.atl 11543 4841 pop_fore.atl 7763 8621 Kid-атласы (28 стр): free min=6161 max=15452 avg=9722 Level (res2001.bin): данные 2305, свободно ~13823 (16384 − 0x100 стаб − 2305) ``` **Выводы по вместимости:** - **Макс. данных в ОДНОМ атлас-банке = свободный хвост ЭТОЙ страницы** (см. таблицу). Связывающее ограничение — самая тесная нужная страница (env1 = 3935 Б; не перегружать её). - Если страница будет маппиться в **W0** — минус ~0x100 Б на ISR-стаб (как level). Атлас-страницы стаб УЖЕ содержат (atlas_load патчит) → данные класть в хвост ПОСЛЕ атласа. - **`kid_data.h` (3.7 КБ) влезает в kid-страницу** (min free 6161) или в отдельную выделенную страницу данных Kid — предпочтительно отдельную (маппить раз на кадр, не конфликтуя с kid-атласами блита). - **Level-таблицы** — вагон места в level-странице (~13.8 КБ). - **BG draw-таблицы** (~0.7 КБ) влезут в env3/fore/env4 (много free), НО см. граблю W0-контекста в §3(a) — читаются ли они, когда нужная страница в W0. - **Выделенная страница ТОЛЬКО под данные** (не делить с атласом) = до ~16 КБ (−0x100 стаб при W0-маппинге). EMM-бюджет это позволяет (см. `sprinter_emm_budget`: 215/3440 КБ free на старте). - **Принудительно уменьшать макс. атлас (репак мельче) — КРАЙНИЙ случай:** это резко поднимет число атлас-банков (сейчас 5 env-страниц адресуются как id>>5; дробление ломает эту адресацию и множит страницы). Сначала использовать СУЩЕСТВУЮЩИЙ свободный хвост и отдельные data-страницы. --- ## 4. ПУТЬ C — переход на huge (banked code) ### 4.1 Что такое huge сейчас (`bin/sprinter-cc`, `runtime/crt0_banked`) - `--memory huge`: `MODE_CODE_LOC=0x4100`, **`MODE_DATA_LOC=0x8000` (ФИКС.)**, banked code в W3. crt0_banked, как crt0_small, авто-детектит W2. Помечено `[TODO]` — не обкатано. - Отличие от small: small цепляет DATA сразу за CODE (`--data-loc 0`); huge ФИКСИРУЕТ DATA на 0x8000. ### 4.2 ТРЕБОВАНИЕ (по пользователю): huge должен переносить DATA динамически Сейчас huge жёстко кладёт DATA на 0x8000. Если РЕЗИДЕНТНЫЙ CODE вылезет за 0x8000 (W1 = только 0x4000..0x7FFF ≈ 16 КБ; резидент > 16 КБ лезет в W2) → коллизия с DATA. **Надо научить huge класть DATA динамически ЗА резидентным CODE (как small: `--data-loc 0` + crt0 считает старт), а не на фикс 0x8000.** Тогда huge = «small-раскладка резидента (W1+W2, DATA за CODE) + ДОП. код в банках W3». Это первый пункт работ по huge. ### 4.3 КОНФЛИКТ: графика тоже хочет W3 (ключевой риск) `pop_banking_architecture` прямо говорит: **графику нельзя в W3** (блиты/атласы используют окна; см. §4.5). Поэтому в банки W3 можно выносить ТОЛЬКО НЕ-графические блоки, и такой банк НЕ должен во время своего исполнения держать графику в W3. Если W3-банкованная функция ЗОВЁТ графику (которой нужен W3), трамплин обязан сохранить/восстановить банк вокруг вызова (проверить, что banking-ABI это делает — `sdcc_banking`). Альтернатива без этого риска — **big + BANK_W1** (банк кода в W1, не W3), рекомендованная в `pop_banking_architecture` именно из-за W3-графики. Сессия должна выбрать: huge(W3) с аккуратным save/restore ИЛИ big(BANK_W1). ### 4.4 Какие блоки МОЖНО вынести (не работают с графикой напрямую) Замер graphics-ref по модулям (grep `gfx_|blit|env_b|wall_b|fore_b|setfillstyle| bar(|GFX_BANK|initgraph`): ``` pop_bg.c : 83 — РЕЗИДЕНТ (вся отрисовка) roomtest.c : 23 — РЕЗИДЕНТ (главный цикл + флип страниц) pop_level.c : 17 — использует gfx_w0_map (W0, не W3-блиты) — ПОГРАНИЧНЫЙ pop_kid.c : 12 — kid_draw = графика; НО play_seq — чистая логика (можно split) pop_ctrl.c : 0 — КАНДИДАТ В БАНК (ввод/диспетчер control) pop_map.c : 0 — КАНДИДАТ В БАНК (коллизия/физика, ~3.8 КБ) — лучший по объёму pop_trob.c : 0 — КАНДИДАТ В БАНК (кнопки/ворота/пики-логика) ``` - **Лучшие кандидаты в W3-банк(и): pop_map + pop_trob + pop_ctrl** (нет прямой графики; вместе ~5.3 КБ CODE). Освобождают резидент → он влезает в W1. - **Осторожно с межбанковыми вызовами:** pop_map/pop_trob ЗОВУТ pop_bg (перерисовка loose/пик/кнопок/шва) и pop_kid (play_seq/kid_set_seq). Это кросс-банк вызовы через трамплин (`sdcc_banking`: стек +3 байта, виртуальный 24-битный адрес). Правило `pop_banking_architecture`: «один файл = один банк = прямые вызовы», main резидентен. Проверить, что трамплин сохраняет W3 вокруг вызова в графический pop_bg (см. §4.3). - **pop_kid split** (по желанию): вынести play_seq/seqtbl-интерпретатор (логика + таблицы kid_data.h) в банк, оставить kid_draw/kid_heal резидентными. Даёт и −код, и −данные из резидента, но требует аккуратного разделения TU (1 функция = 1 модуль, см. `libc_one_function_per_module`). - **pop_level: пограничный** — не блитит, но маппит уровень в W0; банковать можно, если W0-логика совместима с трамплином (проверить ISR-стаб взаимодействие). ### 4.5 Почему графику нельзя в W3 (контекст) Блиттер держит спрайт-страницу атласа в **W0** (`_gfx_w0_state`, `_gfx_w0_isr`). Ускоритель/адресация видео — отдельная тема (см. `sprinter_accelerator`, `sprinter_graphics`). W3 в banked-раскладке — окно кода-банка; смешивать с окном, которое графика перемапливает, нельзя без save/restore. Детально — `pop_banking_architecture`, `graphics_constraints`. --- ## 5. РЕКОМЕНДУЕМЫЙ ПОРЯДОК РАБОТ (для след. сессии) 1. **Замер-базлайн** (CODE/DATA/BSS + per-module) — зафиксировать до. 2. **Путь A** дешёвые пункты (флаги, дедуп кнопки, дедуп y_to_row) — быстрый −1..2 КБ. 3. **huge §4.2**: научить huge класть DATA динамически (как small) — инфраструктурный пререквизит, без него банкинг не даст гибкости. Обкатать в MAME на текущем резиденте (пока без выноса — просто huge-раскладка = small + пустой W3). 4. **huge §4.4**: вынести pop_map (+pop_trob, +pop_ctrl) в W3-банк(и); проверить кросс-банк вызовы в pop_bg (§4.3) в MAME. ЛИБО выбрать big+BANK_W1. 5. **Путь B** (по остатку нужды): прототип выноса `kid_data.h` в EMM-страницу Kid с маппингом раз на кадр; замерить скорость (`sprite_engine_perf`). Затем аудит draw-таблиц по W0-контексту (§3 a/b/c/d). ## 6. Ссылки - `bin/sprinter-cc` (§162+ — резолв memory-mode → CODE_LOC/DATA_LOC). - `runtime/crt0_small.*`, `runtime/crt0_banked.*`, `runtime/bank.s`. - memory: `sprinter_memory_modes`, `memory_modes_implemented`, `setwin2_for_w2_alloc`, `sdcc_banking`, `bank_local_data_pattern`, `pop_banking_architecture`, `avoid_32bit_arith_z80`, `libc_one_function_per_module`, `sprite_engine_perf`, `mdview2_size_budget`. - `applications/PoP/roomtest/bug_list.md` — открытые баги Фазы B (не блокируют оптимизацию, но держать в уме при рефакторе pop_map/pop_bg). --- ## 7. Лишние блиты в горячем пути (добавлено 2026-07-27) Найдено при разборе окклюзии по эталону SDLPoP: **наш «передний слой» рисовал спрайты, которых в оригинале там нет** — это и артефакты, и лишняя работа каждый кадр. Исправлено: `fore_tile` (вызывается для КАЖДОГО тайла футпринта Kid, обычно 2–4 за кадр) рисовал ещё и `bottom_id` — переднюю кромку пола; в оригинале `draw_tile_fore` (seg008:690) добавляет только `add_foretable`-часть, а `bottom` идёт через `draw_tile_bottom` в backtable (ПОД персонажем). Итог: −2..4 блита за кадр, `_CODE` −388 Б, ушла «тень» у основания колонны. **Что проверить тем же методом (по одному вопросу к каждому месту: а есть ли этот спрайт в оригинале в ЭТОЙ таблице?):** 1. `pop_room_draw`/`draw_tile` — вызовы на входе в комнату не критичны по скорости, но по ним стоит сверить состав слоёв (backtable vs foretable). 2. `overlay_mid_tile` — сейчас точный порт midtable-части `draw_tile2`; проверить, не рисуем ли `base_id` там, где оригинал его не рисует (loose: base=0, потому что кадр плиты идёт через `draw_loose` в backtable). 3. `pop_loose_mob_tick` — перерисовка соседнего тайла (`draw_tile(mob_row, mob_col+1)`) КАЖДЫЙ кадр падения: в оригинале это `set_redraw_full` на один кадр; можно ограничить только тайлом, который реально пересекается с куском. 4. `pop_ceil_shake_draw` — heal 64×8 + два `draw_tile(-1,·)` на кадр тряски; проверить, нужен ли второй тайл (правую грань loose в полосе потолка оригинал не рисует вовсе — `draw_tile_aboveroom` без `draw_tile_anim_right`). 5. `fore_only_tile` для полосы потолка: вызывается для всех колонок габарита, а оригинал (`redraw_needed_above`) — только для колонок с флагом `redraw_frames_above`; сузить до колонок, реально задетых спрайтом. 6. `wall_pattern` внутри fore/overlay — тяжёлая (PRNG + до 4 блитов); проверить, не зовём ли её там, где оригинал ограничивается `wall_fram_main`.