Графика 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>
26 KiB
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 — оптимизация КОДА (без смены модели)
- Компиляторные флаги (
bin/sprinter-cc): попробовать--opt-code-sizeу SDCC и подобрать--max-allocs(сейчас дефолт 100000; меньше = мельче код, но медленнее компиляция; см.mdview2_size_budget— там--max-allocsдавал −1.4 КБ). Замерить каждый модуль отдельно. - Дедуп подстановки нажатой кнопки: логика
opener→floor / closer→stuckпо таймеру связи ПРОДУБЛИРОВАНА вdraw_tileиfore_tile(pop_bg.c). Вынести вstatic inline/helpersubst_pressed_button(code,mod). - wall_pattern / prandom (pop_bg): 32-битный LCG (
unsigned long) — пользователь не любит 32-бит (см.avoid_32bit_arith_z80); но это PRNG оригинала (нужен для совпадения раскладки стен) — трогать осторожно, только если найдётся 16-битный эквивалент, дающий ТУ ЖЕ последовательность. - Ревизия дублей:
y_to_rowопределён в pop_bg И pop_map; мелкие геометрические хелперы дублируются — свести в один internal-модуль. /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. РЕКОМЕНДУЕМЫЙ ПОРЯДОК РАБОТ (для след. сессии)
- Замер-базлайн (CODE/DATA/BSS + per-module) — зафиксировать до.
- Путь A дешёвые пункты (флаги, дедуп кнопки, дедуп y_to_row) — быстрый −1..2 КБ.
- huge §4.2: научить huge класть DATA динамически (как small) — инфраструктурный пререквизит, без него банкинг не даст гибкости. Обкатать в MAME на текущем резиденте (пока без выноса — просто huge-раскладка = small + пустой W3).
- huge §4.4: вынести pop_map (+pop_trob, +pop_ctrl) в W3-банк(и); проверить кросс-банк вызовы в pop_bg (§4.3) в MAME. ЛИБО выбрать big+BANK_W1.
- Путь 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 Б, ушла «тень» у основания колонны.
Что проверить тем же методом (по одному вопросу к каждому месту: а есть ли этот спрайт в оригинале в ЭТОЙ таблице?):
pop_room_draw/draw_tile— вызовы на входе в комнату не критичны по скорости, но по ним стоит сверить состав слоёв (backtable vs foretable).overlay_mid_tile— сейчас точный порт midtable-частиdraw_tile2; проверить, не рисуем лиbase_idтам, где оригинал его не рисует (loose: base=0, потому что кадр плиты идёт черезdraw_looseв backtable).pop_loose_mob_tick— перерисовка соседнего тайла (draw_tile(mob_row, mob_col+1)) КАЖДЫЙ кадр падения: в оригинале этоset_redraw_fullна один кадр; можно ограничить только тайлом, который реально пересекается с куском.pop_ceil_shake_draw— heal 64×8 + дваdraw_tile(-1,·)на кадр тряски; проверить, нужен ли второй тайл (правую грань loose в полосе потолка оригинал не рисует вовсе —draw_tile_aboveroomбезdraw_tile_anim_right).fore_only_tileдля полосы потолка: вызывается для всех колонок габарита, а оригинал (redraw_needed_above) — только для колонок с флагомredraw_frames_above; сузить до колонок, реально задетых спрайтом.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), когда перестанет хватать бюджета кадра:
- Батчинг W3-скобки — одна
_bgi_begin/_bgi_endна весьdraw_tileвместо скобки на блит; нужен публичный batch-API в libbgi (как у спрайтового движка). Осторожно: между begin/end стоитDI— длинная серия задержит кадровое прерывание. - Решётка ворот одним спрайтом —
draw_gate_backрисует бары по одному (env 52, до 7 блитов). Сгенерировать в атласе «столб решётки» (повтор бара на высоту тайла) и выводить однимgfx_blit_partс обрезкой по фазеgate_bot_y & 7: 7 блитов → 1. - Не перерисовывать статичные части шва — грань ворот (env 47, 26×62), пол (41) и кромка (43) при анимации решётки не меняются; если стирать только полосу баров, уйдут ещё 3 блита из 9.
- См. также §7 (лишние блиты, которых нет в оригинале).