diff --git a/applications/PoP/docs/resident_budget.md b/applications/PoP/docs/resident_budget.md new file mode 100644 index 0000000..8ac8fcc --- /dev/null +++ b/applications/PoP/docs/resident_budget.md @@ -0,0 +1,85 @@ +# Бюджет резидента W1/W2 и как его освобождать + +Резидент huge-режима — это окно `0x4100..0xBB00` (стек с 0xBB00): код в W1, +данные в W2, между концом данных и стеком остаётся куча. Всё, что туда не +влезло, обязано жить в банках. + +## Как СМОТРЕТЬ, а не гадать + +Атрибуция по карте (`roomtest.map`) ВРЁТ: file-static SDCC в карту не +попадает, и «дырка» между двумя именованными символами приписывается +предыдущему модулю целиком. На этом легко получить «у pop_bg 1529 байт +данных», когда на самом деле у него 289. + +Правильный источник — объектные файлы: в `.rel` есть строки +`A size flags ` с ТОЧНЫМ размером каждой области модуля. + +```python +# по каждому .rel: _DATA/_BSS/_INITIALIZED = резидентные данные модуля, +# _BANKn = его код в банке +re.match(r'A\s+(\S+)\s+size\s+([0-9A-Fa-f]+)\s+flags', line) +``` + +Кто на кого ссылается — оттуда же: `S Def...` — модуль определяет +символ, `S 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` и `pop_trob.c` — единственных модулей, у которых +НИ ОДИН символ не Ref снаружи. Результат: + +| | было | стало | +|---|---:|---:| +| данные в W2 | 6774 | **4228** | +| свободно до стека | 129 | **2675** | +| BANK6 | 4199 | 5128 (31 %) | +| BANK7 | 12684 | 14301 (87 %) | + +**Проверено в MAME**: уровень 1, комнаты 1 и 2 — фон, факелы, решётки, +проваливающиеся полы и переход между комнатами работают. + +### Два условия, без которых это молча ломается + +1. **Нулевая инициализация.** crt0 зануляет только резидентный `_DATA`, до + банковых страниц он не дотягивается, а `mkexe` по умолчанию забивает + пустоты образа `0xFF`. По правилам проекта file-scope переменные никто + не инициализирует явно (crt0 же зануляет) — значит вся банковая статика + поднялась бы с `0xFF`. Поэтому `sprinter-cc` при любом `--bank-data` + САМ добавляет `mkexe -p 0`. +2. **`_INITIALIZED` остаётся в резиденте.** `--dataseg` переименовывает + только `_DATA`; инициализированные переменные и их `_INITIALIZER` живут + как раньше, и gsinit копирует их без участия банков. У `pop_room` таких + 3 байта — они и остались в W2. + +## Что осталось на столе + +- `malloc` — **287 Б** резидента, который никто не зовёт: его тянет + `cbl_open`, где `malloc`/`free` стоят в мёртвой ветке + `CBL_UNDERRUN_SILENCE`. Вынести аллокацию буфера тишины в отдельный + модуль libc — и линкер выбросит malloc у всех приложений на + `CBL_UNDERRUN_APP`. +- `pop_bg` 289 Б — уедут, если вынести `pop_bg_obj_col/row` (2 байта!) в + общий резидентный модуль состояния. +- `pop_tile` — **5972 Б кода** в резиденте, самый крупный жилец. Он там + потому, что его листья зовут обе половины слоя фона из РАЗНЫХ банков. + Резать имеет смысл только по этому признаку: что зовут из одного банка — + туда и переносить. +- BANK7 после правки на 87 %: следующий кандидат на разгрузку — + `pop_redraw.c` (981 Б) в другой банк. diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index 184b93c..ef785da 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -35,7 +35,16 @@ MEMORY ?= huge # small-раскладка + банки кода в W # ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том # же ALLOCS — иначе сравниваются не правки, а уровни оптимизации. ALLOCS ?= 3000 -EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 7=pop_room.c --bank 4=pop_cdraw.c --bank 4=pop_kdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c --bank 7=pop_redraw.c --bank 8=roomtest_cold.c --bank 8=pop_level_cold.c --bank 8=pop_kboot.c --bank 8=pop_guard_cold.c --bank 8=pop_shadow.c --bank 8=pop_sfx_cold.c $(PROF_FLAGS) +# Писучие данные ЭТИХ банковых модулей живут в их же странице, а не в +# резиденте W1/W2 — это 2549 Б, которые иначе съедала бы база. Список +# короткий не по лени: у остальных банковых модулей часть глобалов читают +# соседние банки и резидент (hitp_*, pop_upside, pop_loose_modif, pop_cd), +# и такие данные ОБЯЗАНЫ остаться замапленными всегда. Проверять новичка в +# списке — по объектным файлам: символ должен быть Def только в своём .rel и +# нигде не Ref (см. ../docs/resident_budget.md). +BANK_DATA := --bank-data=pop_room.c --bank-data=pop_trob.c + +EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 7=pop_room.c --bank 4=pop_cdraw.c --bank 4=pop_kdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c --bank 7=pop_redraw.c --bank 8=roomtest_cold.c --bank 8=pop_level_cold.c --bank 8=pop_kboot.c --bank 8=pop_guard_cold.c --bank 8=pop_shadow.c --bank 8=pop_sfx_cold.c $(BANK_DATA) $(PROF_FLAGS) # Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по # умолчанию, пока идёт работа с производительностью; make PROF=0 выключает. # Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте diff --git a/bin/sprinter-cc b/bin/sprinter-cc index b3b84f7..a2d4f38 100755 --- a/bin/sprinter-cc +++ b/bin/sprinter-cc @@ -54,6 +54,10 @@ # no writable data belongs in W3 (code + rodata only). From a # __banked context the resident W3 code is unreachable (but # resident -> __banked via the W1 trampoline is fine). +# --bank-data=SRC писучие данные ТОЛЬКО этого банкового модуля — в его +# страницу (repeatable). Безопасно лишь если ни один +# другой модуль не ссылается на его глобалы: проверяется +# по .rel (Def/Ref). Включает mkexe -p 0 (zero-init). # --mkexe FLAG extra mkexe flag (repeatable; e.g. --mkexe -p --mkexe 0) # --max-allocs N SDCC --max-allocs-per-node (default: 100000 — smaller/ # faster code at the cost of compile time; pass a lower @@ -111,7 +115,8 @@ MKEXE_EXTRA=() BANK_SPECS=() # entries like "1=engine.c" W3_SPECS=() # entries like "mod.c" — резидентные модули окна W3 (--w3) USER_DEFS=() # -DFOO / -DFOO=bar — пробрасываются в sdcc как есть -BANK_LOCAL_DATA=0 # 1 (--bank-data): писучие данные банков — В СТРАНИЦУ банка +BANK_LOCAL_DATA=0 # 1 (--bank-data): писучие данные ВСЕХ банков — в страницу банка +BANK_DATA_SRCS=() # --bank-data=src.c: только эти модули (выборочно) W3_RELS=() # заполняется при компиляции W3-модулей W3_LD_FLAGS=() # -Wl-b_W3CODE=0xC000, если есть --w3 USER_SET_MEMORY="" # непусто, если --memory задан явно (для --w3 авто-small) @@ -148,6 +153,7 @@ while [[ $# -gt 0 ]]; do -D*) USER_DEFS+=("$1"); shift;; --bank) BANK_SPECS+=("$2"); shift 2;; --bank-data) BANK_LOCAL_DATA=1; shift;; + --bank-data=*) BANK_DATA_SRCS+=("${1#*=}"); shift;; --mkexe) MKEXE_EXTRA+=("$2"); shift 2;; --max-allocs) MAX_ALLOCS="$2"; shift 2;; --gfx) GFX_MODE="$2"; shift 2;; @@ -436,8 +442,17 @@ if [[ "$CRT0_TYPE" == "banked" ]]; then # глобал банкового модуля лёг по 0x0001C000): это экономит # W1/W2, но требует ручного контроля видимости, поэтому # включается явно через --bank-data. + # Выборочный режим (--bank-data=src.c) точнее глобального: у + # большинства банковых модулей часть глобалов ЧИТАЮТ соседние + # банки и резидент, и такие данные обязаны остаться в W2. + # Проверять это надо по объектным файлам (кто Def, кто Ref), а + # не на глаз. bank_data_flags=() - [[ $BANK_LOCAL_DATA -eq 1 ]] && bank_data_flags=(--dataseg "BANK${bank_n}") + bank_data_want=$BANK_LOCAL_DATA + for bd in "${BANK_DATA_SRCS[@]}"; do + [[ "$(basename "$bd")" == "$(basename "$bank_src")" ]] && bank_data_want=1 + done + [[ $bank_data_want -eq 1 ]] && bank_data_flags=(--dataseg "BANK${bank_n}") run "$SDCC" "${CC_FLAGS[@]}" \ --codeseg "BANK${bank_n}" --constseg "BANK${bank_n}" \ "${bank_data_flags[@]}" \ @@ -494,6 +509,16 @@ MK_PREFIX=() [[ $VERBOSE -eq 1 ]] && MK_PREFIX+=(-v) [[ $BANK_W1 -eq 1 ]] && MK_PREFIX+=(-B 0x4000) [[ $W3_RESIDENT_HUGE -eq 1 ]] && MK_PREFIX+=(-W) +# Данные банка в его же странице обязаны стартовать НУЛЯМИ: crt0 зануляет +# только резидентный _DATA, до банковых страниц он не дотягивается. Даром +# это даёт паддинг образа нулём вместо дефолтного 0xFF — иначе вся банковая +# статика поднимется с 0xFF, а по правилам проекта её никто не инициализирует +# явно (crt0 зануляет _DATA, см. memory/sdcc_static_storage_gotcha). +if [[ $BANK_LOCAL_DATA -eq 1 || ${#BANK_DATA_SRCS[@]} -gt 0 ]]; then + mk_has_pad=0 + for f in "${MKEXE_EXTRA[@]}"; do [[ "$f" == "-p" ]] && mk_has_pad=1; done + [[ $mk_has_pad -eq 0 ]] && MK_PREFIX+=(-p 0) +fi MK_PREFIX+=("${MKEXE_EXTRA[@]}") MK_PREFIX+=(-L "$LOAD_ADDR" -E "$ENTRY_ADDR" -S "$STACK_ADDR" -o "$OUT") run "$MKEXE" "${MK_PREFIX[@]}" "$IHX"