Симптом: через несколько комнат живого прохода перезагружался DSS. pop_trob_modif() возвращает указатель на room_modif[24][30], а зовут её из банков 2, 3, 7 и резидента. После переноса массив лежит по 0xC000+ в странице банка 6, но разыменовывает указатель ЧУЖОЙ код — когда замаплена его собственная страница. Значит чтение и запись идут поверх кода соседнего банка. Анализ Def/Ref такое не ловит: снаружи нет ссылки на символ, есть ссылка на функцию, которая отдаёт его адрес. Условий для кандидата два, и второе проверяется только чтением кода — ни один указатель на статику не должен уходить наружу. pop_room оба условия проходит (_mobs не читает никто; bake_copy статическая; atlas_load(&pop_env[i]) берёт адрес глобала из резидентного pop_tile.c). Остаётся −1620 Б: данные в W2 6774 -> 5157, свободно 129 -> 1746 Б. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.5 KiB
Бюджет резидента W1/W2 и как его освобождать
Резидент huge-режима — это окно 0x4100..0xBB00 (стек с 0xBB00): код в W1,
данные в W2, между концом данных и стеком остаётся куча. Всё, что туда не
влезло, обязано жить в банках.
Как СМОТРЕТЬ, а не гадать
Атрибуция по карте (roomtest.map) ВРЁТ: file-static SDCC в карту не
попадает, и «дырка» между двумя именованными символами приписывается
предыдущему модулю целиком. На этом легко получить «у pop_bg 1529 байт
данных», когда на самом деле у него 289.
Правильный источник — объектные файлы: в .rel есть строки
A <area> size <n> flags <f> с ТОЧНЫМ размером каждой области модуля.
# по каждому .rel: _DATA/_BSS/_INITIALIZED = резидентные данные модуля,
# _BANKn = его код в банке
re.match(r'A\s+(\S+)\s+size\s+([0-9A-Fa-f]+)\s+flags', line)
Кто на кого ссылается — оттуда же: S <sym> Def... — модуль определяет
символ, S <sym> Ref... — ссылается на него.
Замер 2026-08-20 (до правки)
Данные банковых модулей, лежавшие в РЕЗИДЕНТЕ:
| модуль | данные в W2 | разделяемые символы |
|---|---|---|
pop_room (банк 7) |
1620 | нет (_mobs не читает никто) |
pop_trob (банк 6) |
929 | НЕТ ВООБЩЕ |
pop_bg (банк 2) |
289 | pop_bg_obj_col/row читает pop_room |
pop_map (банк 3) |
227 | hitp_*, pop_upside*, pop_loose_modif, … — читают 6 модулей |
pop_cdraw (банк 4) |
175 | pop_cd читают 4 модуля |
Что сделано: данные банка — в страницу банка
sprinter-cc --bank-data=SRC (повторяемый) кладёт писучие данные ОДНОГО
банкового модуля в его же 16-КБ страницу. Выборочность принципиальна:
глобал, который читает соседний банк или резидент, обязан остаться
замапленным всегда, иначе чтение придёт из чужой страницы.
Включено для pop_room.c. Результат:
| было | стало | |
|---|---|---|
| данные в W2 | 6774 | 5157 |
| свободно до стека | 129 | 1746 |
| BANK7 | 12684 | 14301 (87 %) |
Проверено в MAME: уровень 1, комнаты 1 и 2 — фон, факелы, решётки, проваливающиеся полы и переход между комнатами работают.
ГЛАВНАЯ ЛОВУШКА: указатель на банковую статику
Первый заход включал ещё и pop_trob.c — по Def/Ref он выглядел ИДЕАЛЬНЫМ
кандидатом (экспортируемых данных нет вообще). Через несколько комнат
живого прохода перезагрузился DSS.
Причина: pop_trob_modif() ВОЗВРАЩАЕТ УКАЗАТЕЛЬ на room_modif[24][30], а
зовут её из банков 2, 3, 7 и резидента. После переноса массив лежит по
0xC000+ в странице банка 6, но разыменовывает указатель чужой код — когда
замаплена ЕГО страница. То есть чтение и запись идут по тем же смещениям
поверх кода соседнего банка. Отсюда и уход системы.
Анализ Def/Ref этого не видит: снаружи нет ссылки на символ, есть ссылка на функцию, которая отдаёт его адрес. Поэтому условий для кандидата ДВА, и второе проверяется только чтением кода:
- ни один чужой модуль не Ref его глобалы (по
.rel); - ни один указатель на его статику не уходит наружу — ни возвратом, ни через out-параметр, ни записью в общий глобал.
У pop_room второе выполнено: единственные &-выражения наружу —
atlas_load(&pop_env[i]), но pop_env определён в pop_tile.c, то есть в
резиденте; bake_copy статическая.
Внутри своей страницы указатель безопасен всегда: любой вызов резидентной
функции, которая перемапливает W3 (atlas_load, pop_page_read),
восстанавливает окно перед возвратом.
Два условия, без которых это молча ломается
- Нулевая инициализация. crt0 зануляет только резидентный
_DATA, до банковых страниц он не дотягивается, аmkexeпо умолчанию забивает пустоты образа0xFF. По правилам проекта file-scope переменные никто не инициализирует явно (crt0 же зануляет) — значит вся банковая статика поднялась бы с0xFF. Поэтомуsprinter-ccпри любом--bank-dataСАМ добавляетmkexe -p 0. _INITIALIZEDостаётся в резиденте.--datasegпереименовывает только_DATA; инициализированные переменные и их_INITIALIZERживут как раньше, и gsinit копирует их без участия банков. Уpop_roomтаких 3 байта — они и остались в W2.
Что осталось на столе
malloc— 287 Б резидента, который никто не зовёт: его тянетcbl_open, гдеmalloc/freeстоят в мёртвой веткеCBL_UNDERRUN_SILENCE. Вынести аллокацию буфера тишины в отдельный модуль libc — и линкер выбросит malloc у всех приложений наCBL_UNDERRUN_APP.pop_bg289 Б — уедут, если вынестиpop_bg_obj_col/row(2 байта!) в общий резидентный модуль состояния.pop_tile— 5972 Б кода в резиденте, самый крупный жилец. Он там потому, что его листья зовут обе половины слоя фона из РАЗНЫХ банков. Резать имеет смысл только по этому признаку: что зовут из одного банка — туда и переносить.- BANK7 после правки на 87 %: следующий кандидат на разгрузку —
pop_redraw.c(981 Б) в другой банк.