Files
Sprinter-SDCC/applications/PoP/docs/size_optimization_plan.md
T
snark13 86d7615841 PoP: roomtest — объекты/обломки/переходы комнат + арт
Порт Prince of Persia (applications/PoP/roomtest): развитие уровня,
объекты (loose-полы/обломки), переходы между комнатами, фон-упаковка;
планы (room_model/size_optimization), bug_list, pop_trob.
Арт third_party/16x16-RPG-characters.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:38:23 +03:00

20 KiB
Raw Blame History

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) 0x41000xAB6F
_HOME 227 Б 0xAB6F
_DATA 3 449 Б (0x0D79) 0xAC780xB9F1
_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).