Files
Sprinter-SDCC/applications/PoP/docs/size_optimization_plan.md
T
snark13 c127a4b0d2 PoP: факелы и зелья (chtab_1) + быстрый блит фона gfx_blit_noclip
Графика chtab_1 (flame/sword/potion) — новый атлас pop_pot.atl:
- склянка зелья берётся из chtab_1 (seg008 draw_tile_fore: id 12 малая /
  13 большая при типах 2..4), а НЕ chtab_6 id 12 — res212 в VDUNGEON нет,
  каскад падал на VPALACE и рисовал кусок дворцовой декорации («мусорный
  объект» в комнате 5);
- пакуем chtab_1 целиком (id 1..23): пламя, склянки, кадры пузырька;
- MONO-блит (method_3_blit_mono, seg009:3040) красит спрайт цветом из
  ОБЩЕЙ 16-цветной палитры экрана — она добавлена в 0x30..0x3F, палитра
  chtab_1 в 0x40..0x4F; пузырёк пакуется силуэтом цвета 12 (красный),
  его маска — цвета 0;
- в env-атлас добавлено основание факела (env 146, seg008:489 — рисует
  правый сосед); в статический render_room оно не попадало.

Анимация (seg007 animate_torch/animate_potion + seg000:0B12
anim_tile_modif): при входе в комнату факелам и зельям задаётся случайная
стартовая фаза и заводится trob; факел — get_torch_frame, пламя в ячейке
ПРАВОГО соседа (xh = draw_xh+1, y = draw_main_y−40); зелье —
bubble_next_frame по младшим 3 битам модификатора (старшие 5 = тип, при
загрузке уровня modif <<= 3, seg009).  Пламя и пузырёк не запекаются в
фон: heal своей области + кадр поверх.  Скорость пламени /2 — наш
логический кадр короче игрового тика оригинала.

Скорость отрисовки (профиль в MAME, кадр = 430 080 тактов):
- замер показал, что цена блита почти НЕ зависит от размера — 13 288
  тактов на спрайт 32×3 против 4 617 у линейного спрайтового ядра без
  клипа; платим за проход gfx_blit → gfx_blit_part → _gfx_blit_full;
- в libbgi добавлен gfx_blit_noclip() — блит без клипа в ТЕКУЩЕМ банке
  (putsprite не годится: навязывает GFX_BANK_SPRITE, фону нужен
  TRANSPARENT ради ОЗУ-копии); pop_bg.blit_b уходит на него, когда
  спрайт целиком на экране и не нужен g_clip_top → ~2.9× на блит;
- W3-скобку ставит САМА libbgi: из модуля с --w3 её вызывать нельзя —
  после _bgi_begin окно W3 занято видеобанком и код вызывающего исчезает
  (проявлялось белым экраном);
- редрой шва больше не перерисовывает полосу потолка (bar начинается с
  POP_YOFF+3) — это удваивало стоимость блока при анимации решётки;
- docs/size_optimization_plan.md §7–§8: замеры, сделанное и запас
  (батчинг W3-скобки, решётка одним спрайтом, лишние блиты).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 20:53:04 +03:00

26 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).

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.

8. Скорость отрисовки: замеры и запас (2026-07-27)

Профилирование в MAME (маркеры в порт 0xFE + wpiset … totalcycles, приём из memory mame_mcp_bridge). Кадр Sprinter = 430 080 тактов.

Стоимость блита почти НЕ зависит от размера — платим за проход по цепочке gfx_blit → gfx_blit_part → _gfx_blit_full (16-битная арифметика, клип, пересчёт src, нарезка полос >256), а не за пиксели:

путь (спрайт 32×3) тактов
gfx_blit (общее ядро, с клипом) 13 288
линейное спрайтовое ядро без клипа (putsprite при gfx_sprite_clip(0)) 4 617

Отсюда draw_tile(0,0) тайла шва (9 блитов) стоил 183 690 тактов = 43 % кадра; сам bar — только 13 308.

СДЕЛАНО (шаг 1): в libbgi добавлен gfx_blit_noclip() (common/gfx_blit_noclip.c, прототип в include/gfx.h) — блит без клипа в ТЕКУЩЕМ банке через линейное ядро; pop_bg.blit_b уходит на него, когда спрайт целиком на экране и не нужен g_clip_top. Выигрыш ~2.9× на каждом фоновом блите (подтверждено в MAME). ВАЖНО: W3-скобку (_bgi_begin/_bgi_end) ставит САМА libbgi — вызывать её из модуля, собранного с --w3, нельзя: после _bgi_begin окно W3 занято видеобанком и код вызывающего исчезает из адресного пространства (проверено: белый экран).

ЗАПАС (шаг 2), когда перестанет хватать бюджета кадра:

  1. Батчинг W3-скобки — одна _bgi_begin/_bgi_end на весь draw_tile вместо скобки на блит; нужен публичный batch-API в libbgi (как у спрайтового движка). Осторожно: между begin/end стоит DI — длинная серия задержит кадровое прерывание.
  2. Решётка ворот одним спрайтомdraw_gate_back рисует бары по одному (env 52, до 7 блитов). Сгенерировать в атласе «столб решётки» (повтор бара на высоту тайла) и выводить одним gfx_blit_part с обрезкой по фазе gate_bot_y & 7: 7 блитов → 1.
  3. Не перерисовывать статичные части шва — грань ворот (env 47, 26×62), пол (41) и кромка (43) при анимации решётки не меняются; если стирать только полосу баров, уйдут ещё 3 блита из 9.
  4. См. также §7 (лишние блиты, которых нет в оригинале).