Files
Sprinter-SDCC/applications/PoP/docs/layout_plan_v2.md
T
snark13 af5f0a4638 PoP roomtest: отрисовка стража в резидент W3 + fore-окклюзия + off-by-one спрайта
Разгрузка W1/W2 перед ИИ стражей (вариант 2 из двух обсуждённых).

1. pop_guard.c разделён по окнам: состояние/логика (Guard, HP, enter,
   kill, load_frame) остаются в W1/W2 — их обязан видеть банк guards.c;
   ОТРИСОВКА уехала в новый pop_gdraw.c, собираемый как --w3 (резидент).
   Правило границы: резидент = только то, что рисует и зовётся
   исключительно из главного цикла.  Кадр стража стал глобальным
   (pop_gframe): заполняет логика, читает резидент.

2. Страж не окклюдировался передними гранями тайлов — рисовался поверх
   столба.  В оригинале любой Char это запись midtable, а foretable
   рисуется после всех midtable (draw_tile_fore, seg008:690), т.е. столб
   перекрывает всех одинаково.  Футпринт персонажа выделен из
   pop_fore_over_kid в char_footprint(), поверх него добавлен
   pop_fore_over_char() — слой fore + полоса потолка, без оверлеев поз
   виса/полёта/подъёма (у стража их нет; появятся — портируем
   redraw_at_char2 общим кодом, а не догадками).

3. Упаковщик стража: тот же off-by-one, что уже ловили у Kid.
   load_chtab_from_file(id_chtab_5_guard, 750) даёт images[0] = res751,
   а рисование индексирует images[frame.image] — значит image=N это
   res(751+N), а не res(750+N).  Из-за сдвига frame_166_stand_inactive
   рисовался как res767 (выпад) вместо res768 (стойка).

Проверено в MAME: страж в комнатах 3 и 21 стоит в правильной позе;
окклюзия подтверждена патчем Guard.x в живой сессии — при заходе за
столб спрайт корректно срезается его передней гранью.

Память: _CODE 26 149 -> 25 703, куча W2 805 -> 1245 Б, резидент W3
11 656 -> 12 819 (свободно 3565 Б), банк 1 236/16384 Б.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 23:14:21 +03:00

32 KiB
Raw Blame History

roomtest — план v2: размер кода и раскладка по окнам/банкам/страницам

Статус: план для отдельной сессии, составлен 2026-07-29 по свежему замеру. Заменяет size_optimization_plan.md (v1, 2026-07-21): часть его пунктов уже сделана, часть опиралась на неверную модель банкинга. Документ самодостаточный — рассчитан на старт с пустого контекста.

Повод: перед стражами и боёвкой (новый код ~5–8 КБ) надо понять, куда он поместится, и заранее развести код так, чтобы банкованные модули не упёрлись в ограничения окна W3.


СТАТУС ВЫПОЛНЕНИЯ (обновлено 2026-07-29, коммит ecf5ecf)

Шаг Статус Факт
1. kid_data.h → EMM-страница + load_frame/cur_frame сделан _CODE −3 247 Б; попутно найден и обойдён баг кодогенерации SDCC (см. ниже)
2. pop_geom.c (дедуп геометрии + PRNG) сделан 41 Б _CODE, −11 Б W3; ценность — не байты, а bank-safe слой
3. pop_map без вызовов графики сделан (фазы 1a/1b) через пометки перерисовки, см. ниже
4. Разгрузка/перебалансировка W3 сделан резидент = pop_bg + pop_gdraw (отрисовка стража, 2026-07-29); W3 14 376 → 12 819 (свободно 3 565 Б)
5. Данные (room_modif, dl1/dl2, _kbdraw_down) не начат потенциал ~1.5 КБ
6. Контракт банка стражей + пробник пробник сделан tests/w3bankgfx — модель подтверждена в MAME, см. ниже
7. Дедуп семейства draw_tile отложен по решению пользователя «мороки много, выгода не так велика»

Замер сейчас против замера §1: _CODE 24 881 → 24 421, куча W2 2 076 → 2 592 Б, W3-резидент 14 376 → 11 632 (свободно 2 008 → 4 752 Б). Сумма кода упала на ~3.2 КБ (данные Kid уехали в EMM), остальное — перераспределение.

Замер 2026-07-29 (после стража). Появление стража съело кучу до 805 Б; разгрузка — вынос ОТРИСОВКИ стража в резидент (pop_gdraw.c, --w3), логика и состояние остались в W1/W2, чтобы банк guards.c их видел (R2). Итог: _CODE 26 149 → 25 703, куча 805 → 1 245 Б, W3-резидент 11 656 → 12 819 (свободно 4 728 → 3 565 Б), банк 1 — 236 / 16 384 Б. Граница «что резидент» теперь формулируется одним правилом: резидент = только то, что рисует и зовётся исключительно из главного цикла; всё, что может понадобиться банку, остаётся в W1/W2.

Что сделано вместо §5.3 (вынос loose в W3)

Вместо переноса кода между окнами выбран (по обсуждению с пользователем) порт архитектуры оригинала: логика ставит пометку, отрисовка идёт отдельным проходом — set_redraw_* (seg007) + redraw_needed (seg008:0178). Появился pop_redraw.c/.h; pop_trob и pop_map больше не рисуют тайлы. Наши самодельные счётчики (spike_rest, button_rest, ldoor_rest, loose_bake, loose_rest, ceil_rest, ceil_bake, land_bake) удалены — их роль (вторая страница дабл-буфера) взял счётчик страниц в пометке.

Два исключения остались (обе — функции ТОЛЬКО главного цикла, звать из банка нельзя):

  • pop_process_trobs — пламя факела и пузырёк зелья (покадровый оверлей);
  • pop_loose_tick — падающий кусок (mob): spawn/tick/pos. В оригинале это отдельная подсистема (mobs + draw_moving), разделение на логику и отрисовку — задел следующей фазы.

Пробник банка (2026-07-29): модель ПОДТВЕРЖДЕНА

tests/w3bankgfx (huge + --w3 res.c + --bank 1=bank1.c, графика 256):

  • банк рисует примитивом libbgi НАПРЯМУЮ — работает; страница W3 внутри банка до блита, после блита и после возврата из вызванной им W1/W2-функции одна и та же (0xF0), резидент — 0xF3. То есть _bgi_begin/_bgi_end корректно возвращают ИМЕННО банковую страницу (правило R4);
  • вызов W1/W2-функции из банка работает, и она тоже может рисовать;
  • резидент W3 жив и вызывается после возврата из банка (R3).

Дополнительно выяснено (важно для стражей): писучие статики __banked-модуля линкуются В СТРАНИЦУ БАНКА (0x1C000+) — снаружи их не прочитать, из W1/W2 по 0xC000 видна резидентная страница. Значит всё состояние банкованного кода (позиции стражей, таймеры боя) обязано жить в W1/W2 как обычные глобалы, а банк — только код.

Ещё одна мина, найденная там же: инлайновый in a,(#0xE2) посреди тела функции затирает A, куда SDCC уже положил параметр (у нас из-за этого цвет заливки стал номером страницы, и «резидент не рисовал»). Читать порт отдельной __naked-функцией.

Найденная по дороге ловушка компилятора

(const T *)КОНСТАНТА + var*K SDCC 4.5 может собрать неверно: умножение делает в 16 битах, а потом берёт только младший байт (ld c,l / inc b). Кадры Kid с индексом ≥ 52 читались из чужой строки таблицы, у бега/шага пропадал FRAME_NEEDS_FLOOR и персонаж проваливался сквозь пол. Лечение — считать адрес в uint16_t и кастовать один раз. Тот же паттерн в pop_level.c компилируется ПРАВИЛЬНО, т.е. полагаться на «у соседа работает» нельзя. Подробности: memory sdcc_z80_const_ptr_index_bug.


0. Что уже сделано из v1 (не повторять)

  • --opt-code-size и --max-allocs 100000 уже включены по умолчанию в bin/sprinter-cc (v1 §2.1 закрыт, выигрыш получен).
  • Лишние блиты переднего слоя убраны (v1 §7 п.0): fore_tile больше не рисует bottom_id, _CODE 388 Б.
  • gfx_blit_noclip в libbgi (v1 §8 шаг 1): фоновые блиты в 2.9× дешевле.
  • --w3 как резидент окна 3 реализован и обкатан (memory w3_resident_code).

1. ЗАМЕР (сборка 2026-07-29, коммит 1214785)

Команда: --memory small --gfx 256 --w3 pop_trob.c pop_map.c --w3 pop_bg.c.

1.1 Окна

Область Занято Свободно Примечание
W1+W2 _CODE 24 881 Б 0x4100…0xA231
W1+W2 _HOME+_GSINIT+_DATA+_BSS ~4 240 Б до 0xB2E4
W1+W2 куча 0 (никто не malloc'ит) 2 076 Б 0xB2E4…0xBB00
W1+W2 стек 1 279 Б 0xBB00…0xBFFE
W3 резидент 14 376 Б 2 008 Б 0xC000…0xF828
EMM-страницы 37 атласов + 1 уровень ~215 страниц свободно sprinter_emm_budget

Итого запаса до стены: ≈ 4 КБ (2 КБ в W1/W2 + 2 КБ в W3). Стражи туда не влезут.

1.2 Код по модулям (точно, из .rel)

W1/W2 (_CODE 24 881):            W3 резидент (_W3CODE 14 376):
  pop_kid      6 963               pop_bg     11 643
  pop_map      6 157               pop_trob    2 733
  roomtest     2 583
  pop_level    1 425
  pop_ctrl     1 145
  crt0           333
  libc+libbgi ~6 275

1.3 Крупнейшие функции/данные (из .lst)

pop_bg   : draw_tile 3080, other_overlay_tile 1146, wall_pattern 944,
           mob_render 720, mob_tick_one 661, overlay_mid_tile 498,
           fore_only_tile 409, climb_overlay_tile 391, tile_table 371
pop_map  : check_loose_fall_on_kid 674, check_bumped 567, jump_up_or_grab 413,
           get_tile 266, do_knock 243, check_press 238, check_leave 212
pop_kid  : kid_seqtbl 2310 + kid_frames 1205 + kid_seq_off 230 = 3745 Б ДАННЫХ
           (в _CODE!), собственно кода ~3.2 КБ
roomtest : enter_room+main ~2.1 КБ
pop_level: room_bg_ptr 1084 (+ 515 Б таблиц LINKLOC/LINKMAP в _DATA)

1.4 _DATA (3 710 Б)

pop_trob 963  (room_modif[24][30] = 720 + trobs + rest-массивы)
pop_level 515 (копии LINKLOC/LINKMAP уровня)
pop_map 163, roomtest 141, pop_kid 130, pop_bg 115, pop_ctrl 13
libc: _irq_state 818, _kbdraw_state 515, _gfx_pal_buf 256, прочее ~200

1.5 Находки замера (мелкие, но чинить)

  1. --w3 берёт ОДИН файл на флаг. В Makefile написано --w3 pop_trob.c pop_map.c --w3 pop_bg.c, и это значит «W3 = pop_trob и pop_bg», а pop_map.c компилируется как обычный исходник в W1/W2. Судя по .sprinter-cc-roomtest/w3_pop_map.rel (устаревший артефакт), когда-то pop_map был в W3. Решить осознанно (см. §4) и записать явно: --w3 pop_trob.c --w3 pop_bg.c.
  2. libc тянет _irq_state 818 Б + _kbdraw_state 515 Б в _DATA. __irq_vec_buf (513 Б) — таблица векторов IM2; __kbdraw_down (512 Б) — битмап клавиш на 512 скан-кодов. Оба можно ужать (см. §5.4), это ~0.7 КБ в самом дефицитном окне.

2. ПРАВИЛА ПЛАТФОРМЫ, ОТ КОТОРЫХ ПЛЯШЕТ РАСКЛАДКА

Это главное, что изменилось по сравнению с v1: модель банкинга уточнена по bin/sprinter-cc (справка --w3/--bank) и по коду libbgi.

(R1) Резидент W3 (--w3) и банки W3 (--bank) делят одно окно. Резидент лежит на своей странице 0xC000…0xFFFF; трамплин на время вызова __banked подменяет страницу W3 на банковую и возвращает резидентную назад.

(R2) Из банка резидент W3 НЕДОСТИЖИМ — и транзитивно тоже. Пока исполняется банк, резидентной страницы в адресном пространстве нет. Значит нельзя не только bank → pop_bg(), но и bank → pop_map() → pop_bg(). Это ключевое ограничение при выборе, что делать банком.

(R3) Резидент → банк работает (через трамплин в W1), резидент → W1/W2 — тоже.

(R4) Графические примитивы libbgi звать можно откуда угодно. _bgi_begin читает текущую страницу W3 из порта 0xE2, а _bgi_end её возвращает — то есть скобка корректна и из банка, и из резидента. Нельзя только одно: звать _bgi_begin/_bgi_end ИЗ кода, который сам лежит в W3 (после подмены страницы исчезнет исполняемый код — проверено, белый экран). Поэтому pop_bg (резидент W3) обязан пользоваться готовыми примитивами (gfx_blit*, bar, …), а батчинг скобки на весь draw_tile (v1 §8 шаг 1) для него невозможен без переноса самого draw_tile в W1/W2.

(R5) --w3 кладёт в W3 код И rodata модуля (--codeseg/--constseg W3CODE), а писучие статики оставляет в _DATA (W2). То есть const-таблицы переносятся в W3 бесплатно вместе с модулем (так уже лежит tile_table 371 Б).

(R6) Данные в EMM-странице читаются, только пока страница в окне. gfx_w0_map(page) / gfx_w0_unmap() — окно W0 (0x0000…0x3FFF), первые 0x100 занимает ISR-стаб. Так уже работает pop_level. Цена — пара OUT на маппинг, поэтому годится для «пачками», а не для чтения по байту в горячем цикле.


3. ЧТО ДЕЛАТЬ НЕЛЬЗЯ (анти-паттерны, чтобы не потерять время)

  • Нельзя банковать pop_bg. Он вызывается из pop_map, pop_trob, roomtest, pop_kid — то есть из главного цикла на каждом кадре; плюс он сам держит tile_table и всю отрисовку. Банк дал бы трамплин на каждый блит.
  • Нельзя банковать pop_map, пока pop_map зовёт pop_bg (R2). Сейчас зовёт: pop_loose_tick и компания (~30 вызовов графики).
  • Нельзя тащить kid_frames в EMM «в лоб»: он читается несколько раз за кадр из коллизии (kid_cur_dx/kid_cur_flagsdx_weight, char_x_forward_edge, …). Нужен кэш кадра (см. §5.1) — иначе маппинг страницы окажется в горячем пути.
  • Нельзя «причёсывать» семейство draw_tile ради экономии, не имея пиксельного теста. Мы неделю выравнивали слои по SDLPoP; любой рефактор этой зоны проверять диффом страниц (заморозка кадра клавишей 1 + сравнение VRAM обеих страниц, приём из memory mame_mcp_bridge).

4. ЦЕЛЕВАЯ РАСКЛАДКА

Принцип: W3-резидент = «толстая графика, которую зовёт только главный цикл»; W1/W2 = ядро, которое должно быть достижимо ОТОВСЮДУ (включая банки); банки = новая холодная логика (стражи, боёвка, будущие уровни).

W1/W2 (всегда отображено)          W3 резидент (стр. 0xC000)      Банки W3
──────────────────────────         ─────────────────────────      ─────────
libc + libbgi                      pop_bg   (отрисовка тайлов)    guards.c
pop_kid  (интерпретатор+рисование)  pop_trob (анимации тайлов)     fight.c
pop_map  (коллизия/физика/предметы) pop_loose.c (loose+потолок)    debug/roomnav
pop_geom (общая геометрия/тайлы)    enter_room-часть roomtest?
pop_ctrl (ввод/диспетчер)
roomtest (главный цикл)

Почему так:

  • pop_map остаётся в W1/W2 — его зовут и главный цикл, и (в будущем) банк стражей; в W3 его класть нельзя именно из-за R2. Для этого из него надо вынести графическую часть (loose/потолок) — она уезжает в W3 к pop_bg (§5.3). После выноса pop_map становится чистой логикой без единого вызова графики — тот самый bank-safe API.
  • pop_kid остаётся в W1/W2: play_seq/kid_set_seq/Kid нужны и стражам (у стражей ТА ЖЕ seqtbl), а kid_draw зовёт только libbgi (R4).
  • pop_trob остаётся резидентом: его зовёт только главный цикл, и он сам зовёт pop_bg — идеальный житель W3.
  • Банк стражей не зовёт ничего из W3. Рисование стражей — либо через libbgi напрямую (R4), либо (лучше) резидентный guard_draw() в W1/W2 рядом с kid_draw, а банк только считает состояние. Тот же приём мы уже используем для pop_item_taken/pop_loose_fell/pop_ceil_fell: банк выставляет флаг — резидент рисует.

5. ПЛАН РАБОТ

Порядок выбран так, чтобы каждый шаг был проверяем отдельно и давал место следующему.

Шаг 1. kid_data.h (3 745 Б) → EMM-страница + порт load_frameсамый большой выигрыш

Сейчас kid_seqtbl (2310) + kid_frames (1205) + kid_seq_off (230) лежат в _CODE окна W1/W2 — это 15 % всего дефицитного пространства.

Как переносить:

  1. pop_extract_kid_data.py дополнительно пишет kid_data.bin (те же три таблицы подряд, фиксированные смещения).
  2. Грузим её в отдельную EMM-страницу тем же способом, что уровень (pop_level_load — готовый образец), хэндл держим в pop_kid.
  3. Порт load_frame (seg006) и глобала cur_frame — в оригинале ровно так и сделано: раз за тик кадр копируется в структуру, а весь остальной код читает cur_frame, а не таблицу. У нас kid_cur_dx()/kid_cur_flags() станут чтением из cur_frame (5 байт в _DATA).
  4. play_seq оборачивается в один gfx_w0_map(kid_data_page)unmap на вызов (в тике, не в отрисовке — конфликта с атласом в W0 нет).

Выигрыш: 3 745 Б из W1/W2, цена — один маппинг страницы за тик и 5 байт _DATA. Дополнительный бонус: load_frame/cur_frame — шаг К СХОДСТВУ с оригиналом, а не отход от него.

Риск: сломать play_seq (сердце анимации). Проверка: прогон по комнатам с эталонными позами (вис, подтягивание, прыжки, подъём меча).

Шаг 2. Модуль pop_geom.c — дедуп + bank-safe фундамент

Сейчас продублировано между модулями:

что где сколько
y_to_row/y_to_row_mod4 pop_bg + pop_map 2 копии
char_dx_forward pop_kid + pop_map 2 копии
x_bump[20] pop_kid (uint8) + pop_map (int16) 20 + 40 Б, РАЗНЫЕ типы
y_land[5] pop_kid + pop_map 10 + 10 Б
tile_is_floor pop_map (+ проверка кодов в roomtest) 2 места
32-битный LCG prandom pop_bg (prandom) + pop_trob (trob_prandom) 2 копии по ~60 Б + 2 сида

Собрать в один W1/W2-модуль pop_geom.c: таблицы x_bump/y_land/dir_front/ dir_behind, y_to_row, char_dx_forward, get_tile_div_mod(_m7), tile_is_floor, prandom. Выигрыш прямой — сотни байт (оценка 250–400 Б), но главное — это и есть тот «чистый» API, который потом сможет звать банк (R2): вся геометрия оказывается в W1/W2 по определению.

Осторожно: prandom у pop_bg и pop_trob — РАЗНЫЕ последовательности с разными сидами (стены vs фазы факелов). Объединять функцию можно, сиды — нет: передавать сид указателем/по индексу, иначе поедет раскладка кладки.

Шаг 3. Вынести loose/потолок из pop_map в W3

pop_map — единственный модуль W1/W2, который зовёт графику, и делает это ровно в одном логическом блоке: pop_loose_tick + check_press + do_knock + fell_on_your_head + check_loose_fall_on_kid + плита-потолок (~1.4–2 КБ).

Вынести их в pop_loose.c, собираемый --w3 рядом с pop_bg/pop_trob. Тогда:

  • pop_map = чистая логика (bank-safe, R2 соблюдён);
  • W1/W2 худеет ещё на ~1.5–2 КБ;
  • W3 растёт на столько же — а место там появится после шага 4.

Шаг 4. Перебалансировка резидента W3

После шага 3 в W3 будет тесно (14.4 + 2 ≈ 16.4 КБ > 16 КБ). Разгружаем:

  1. wall_pattern (944 Б) + mob_render/mob_tick_one (1381 Б) — кандидаты на переезд в W1/W2: их зовёт только pop_bg/pop_loose, но сами они уже пользуются только libbgi (R4), значит из W1/W2 работают и остаются достижимыми из банка.
  2. tile_table и мелкие const-таблицы pop_bg (371 + ~300 Б) можно унести в EMM-страницу уровня (там ~13.8 КБ свободно) — но только если чтение происходит под уже замапленной страницей. Сейчас draw_tile читает tile_table ВНЕ W0-контекста → потребуется явный маппинг на тайл. Не делать раньше замера: 30 тайлов на входе в комнату × map/unmap — терпимо, а вот в покадровых редроях (пики/loose/кнопка) — уже горячий путь.
  3. Если и этого мало — enter_room (~2.1 КБ, зовётся только при смене комнаты) переносится в резидент W3 или в БАНК (он вызывается из главного цикла = резидента, значит банк допустим по R3).

Шаг 5. Данные

  1. room_modif[24][30] = 720 Б (pop_trob, _DATA). Нужен произвольный доступ каждый кадр (анимации, ворота) — в EMM не годится. Но 24 комнаты × 30 байт хранятся ЦЕЛИКОМ, хотя одновременно живут modif'ы только текущей комнаты и соседей по швам. Вариант: хранить полный массив в EMM-странице уровня, а в _DATA держать кэш на 2–3 комнаты (свою + левого/правого соседа) с записью обратно при смене комнаты. Выигрыш ~600 Б, цена — аккуратность на швах (кнопка в одной комнате открывает ворота в другой). Делать последним — это самая «тонкая» правка по семантике.
  2. dl1[256]+dl2[256] = 512 Б (pop_level, _DATA — копии LINKLOC/ LINKMAP уровня) — читаются при нажатии кнопки и при отрисовке нажатой кнопки. Кандидат на чтение прямо из страницы уровня (она и так маппится) — но проверить, что pop_doorlink2 не зовётся из отрисовки в тот момент, когда в W0 атлас. Выигрыш ~500 Б.
  3. _kbdraw_down[512] 512 Б (libc): проверено — это байт на скан-код (libc/kbd/_kbdraw_state.c), хотя комментарий называет его битовой картой. Упаковка в биты даёт −448 Б, но добавляет сдвиг/маску в ISR-трамплин и в kbd_raw_down. Трогать осторожно: raw-клавиатура уже дважды была источником залипаний (memory kbd_raw_fifo_drain, kbd_overrun_wipe_modifiers) — правку сопровождать прогоном docs/kbd-games.
  4. __irq_vec_buf 513 Б (libc IM2): таблица векторов обязана быть выровнена и полна — не трогать.

Шаг 6. Контракт банка стражей (проектируется ДО написания кода)

Когда дойдём до стражей:

  • guards.c собирается --bank 1=guards.c, режим huge (или big с BANKED=W1, если W3 окажется тесен для трамплинов).
  • Банк зовёт только: pop_map (чистая логика после шага 3), pop_kid (play_seq, kid_set_seq, cur_frame), pop_geom, libc/libbgi.
  • Банк НЕ зовёт: pop_bg, pop_trob, pop_loose (резидент W3) — ни прямо, ни через промежуточные функции. Нужна отрисовка — выставляет флаг, рисует резидент (идиома pop_item_taken).
  • Первым делом — пробник (tests/ или --bank на пустышке): банк зовёт pop_map-функцию, та зовёт libbgi-примитив; убедиться в MAME, что скобка W3 корректно возвращает банковую страницу (R4) — это проверка модели, а не веры в неё.

Шаг 7. Мелкий дедуп в pop_bg (после того, как появится тест страниц)

  • Пять функций-редроев (pop_spike_redraw, pop_loose_shake_draw, pop_floor_bake, pop_button_redraw, pop_leveldoor_redraw) отличаются только прямоугольником heal, банком и набором тайлов — свести к одному параметризованному хелперу (оценка −150…250 Б).
  • env_b/wall_b/fore_b/pot_b — четыре одинаковых обёртки над blit_b (оставить: экономия единицы байт, читаемость дороже).
  • overlay_mid_tile / fore_only_tile / climb_overlay_tile / draw_tile — общая структура «взять code/lcode, посчитать x/dmy/dby, разобрать слои». Тут экономия потенциально сотни байт, но это та самая зона риска из §3 — только с пиксельным диффом до/после и по одному слою за раз.

6. Ожидаемый итог

Шаг W1/W2 W3 Риск
1. kid_data → EMM + load_frame 3 745 средний (сердце анимации)
2. pop_geom (дедуп) 250…400 низкий
3. loose → W3 1 500…2 000 +1 500…2 000 низкий (перенос как есть)
4. разгрузка W3 (wall_pattern, mob) +2 300 2 300 низкий
5. данные (room_modif, LINKLOC, kbd) 1 000…1 600 средний/высокий
7. дедуп редроев pop_bg 150…250 средний

Суммарно: W1/W2 освобождается ~4.5–6 КБ, W3 остаётся примерно в нынешнем объёме, но становится «правильно заполненным» — в нём только то, что банк никогда не позовёт. Плюс открывается путь к банкам: стражи и боёвка получают до 16 КБ на банк, не трогая резидент.


7. Как мерить и проверять (обязательно к каждому шагу)

  1. До/после по .rel — точные размеры на модуль: for f in .sprinter-cc-roomtest/*.rel; do grep '^A ' $f; done (области _CODE/_W3CODE/_DATA). Итоги окон печатает сам sprinter-cc.
  2. Функции — из .lst (метки _name: и адреса), скрипт в истории этой сессии; полезно ловить «функция распухла после рефактора».
  3. MAME: любой перенос кода между окнами/страницами — это класс «молча ломается» (sprinter_memory_modes). Минимум: комната 1 (loose), 12 (вис/подтягивание), 15 (меч), 9 (дверь уровня), 6 (кнопка/ворота).
  4. Пиксельный дифф для правок отрисовки: заморозить кадр (1), сравнить обе страницы дабл-буфера через vram (см. memory mame_mcp_bridge) и/или сверить с эталонным рендером render_room.py.
  5. Скорость — после шагов 1 и 4 замерить кадр маркерами в порт 0xFE (приём из v1 §8), чтобы маппинг страницы за тик не съел бюджет.

8. Ссылки

  • bin/sprinter-cc — справка по --w3, --bank, --memory, --memory-manual.
  • runtime/crt0_banked.s, runtime/bank.s — трамплины и захват резидентной страницы W3.
  • libbgi/common/_bgi_begin.c / _bgi_end.c — механика скобки W3 (R4).
  • memory: w3_resident_code, pop_banking_architecture, sdcc_banking, bank_local_data_pattern, sprinter_memory_modes, memory_modes_implemented, sprinter_emm_budget, mame_mcp_bridge, avoid_32bit_arith_z80, libc_one_function_per_module.
  • applications/PoP/docs/size_optimization_plan.md — v1 (замер 2026-07-21, раздел §8 про скорость отрисовки актуален и не дублируется здесь).