SprPoP: автономное приложение, выделенное из roomtest
Порт PoP переехал в applications/SprPoP — приложение, которое собирается само: код, оригинальные данные, конверторы ресурсов и сборка внутри одной папки. Наружу знает единственный путь — корень тулчейна (SPRINTER_ROOT, по умолчанию ../..). applications/PoP/roomtest ЗАМОРОЖЕНА и остаётся архивом закрытых задач, багов и исполненных планов. Скопировано из applications/PoP/roomtest@4b74478. Перенос проверен побайтово: собранный sprpop.exe совпал с roomtest.exe того же коммита, все 39 дисковых ресурсов и все 16 генерируемых заголовков — тоже, host- тесты зелёные (15/15). Раскладка: src/ рукописный C (roomtest.c -> sprpop.c) gen/ генерируемые заголовки, в репозитории assets/orig/ оригинальные данные игры, вне репозитория (копирайт) assets/packed/ то, что ложится на диск, в раскладке диска tools/ конверторы; все пути — в одном tools/paths.py build/ выход: exe, каталоги ресурсов, hdd/, промежуточные atl/ Сборка ресурсов: assets/packed и gen — версионируемые ВХОДЫ, а не то, что пересчитывается каждым make. Автоматика построена на ОТСУТСТВИИ файла, а не на таймстемпах: git не хранит времена, и в свежем клоне сравнение по времени превращалось бы в лотерею. Недостающий ресурс или заголовок чинится сам, рекурсивным вызовом в ветку генерации. Музыка собирается из любого из четырёх наборов записей (make music-mp3, music-mt32, ...); набор входит в имя stamp'а, поэтому смена набора сама делает музыку устаревшей. Длины реплик больше не захардкожены: упаковщик печатает их в gen/pop_music_ticks.h, и шкала сцены выражена через них — иначе mt32 (реплики на 6% длиннее) молча ломал катсцену. Тулчейн: в app.mk два обратносовместимых крючка (SRC_DIR/BUILD_DIR), HDD_IMG стал ?=; команда сборки roomtest не изменилась. Корневой make host-tests переключён на SprPoP. Подгонка тайминга катсцены с принцессой (PV_MAGIC_LEAD): сцена render-bound и идёт ~49 тиков/с вместо 60, из-за чего кода реплики приходила раньше молнии. Это обход, а не лечение; разбор с замерами — docs/BUGS_OPEN.md, записи SND-PACE-DEAD, PV-RENDER-BOUND, MUS-LEFT-TEAR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,532 @@
|
||||
# SprPoP — план v2: размер кода и раскладка по окнам/банкам/страницам
|
||||
|
||||
> **Замер 2026-08-08, после MEM-BANK2 (актуальная сборка, ALLOCS=3000).**
|
||||
> `_CODE` 22 556 Б, данные 4 375, куча 4 301. Банки: 1 (`guards.c`) 2 289,
|
||||
> 2 (`pop_bg.c`, ГОРЯЧАЯ половина фона) 5 872, 3 (`pop_map.c`) 8 483,
|
||||
> 4 (`pop_cdraw.c`) 5 664, 5 (`pop_ctrl.c`) 2 040, 6 (`pop_trob.c`) 2 889,
|
||||
> 7 (`pop_room.c`, ХОЛОДНАЯ половина фона) 6 035 — все из 16 384.
|
||||
>
|
||||
> Слой фона разрезан надвое по ЧАСТОТЕ ВЫЗОВА (`TASKS_CLOSED.md#mem-bank2`):
|
||||
> общие листья — в резидент `pop_tile.c` (W1 замаплен всегда, таблицы видны
|
||||
> обеим половинам), горячий fore-проход остался в банке 2, холодная отрисовка
|
||||
> комнаты и точечные перерисовки уехали в банк 7. Стык — три тонкие
|
||||
> `__banked`-обёртки, чтобы трамплин платил только холодный путь.
|
||||
> **Банк 2: 90.4 % -> 35.8 %.** Резидент вырос на 2 КБ (листья) — куча
|
||||
> 6 333 -> 4 301 Б, это плата за то, что таблицы должны быть видны из двух
|
||||
> банков.
|
||||
>
|
||||
> Число банков дублируется: `--bank N=` в Makefile И `const uint8_t n_banks`
|
||||
> в `sprpop.c`. Расхождение — не ошибка сборки, а зависание до первого
|
||||
> кадра.
|
||||
>
|
||||
> **Замер 2026-08-01 (историческая отметка).** `_CODE` 25 119 Б, `_DATA` 3 709,
|
||||
> куча ~2.4 КБ. Банки: 1 (`guards.c`) 1 896, 2 (`pop_bg.c`) 13 792,
|
||||
> 3 (`pop_map.c`) 6 331, 4 (отрисовка стража) 2 236 — все из 16 384.
|
||||
> **Резидента `--w3` больше нет**: отрисовка уехала в банк 2, и это сняло
|
||||
> главное ограничение резидента (из банка его было не достать) — банк→банк
|
||||
> работает, трамплин сохраняет страницу окна на стеке. Отрисовка стража
|
||||
> вынесена из банка 2 в собственный банк 4, потому что банк 2 подошёл к
|
||||
> потолку (16 021 из 16 384) — коммит `2f3e854`.
|
||||
>
|
||||
> **Свободного места в банке 2 теперь 10.5 КБ**, в банке 7 — 10.3 КБ; туда
|
||||
> просятся чомперы, зеркало и второй тайлсет (palace). Прежде чем начинать
|
||||
> `levels_plan.md` §3 — всё равно посчитать, куда это ляжет. Следующий
|
||||
> свободный номер банка — 8, гранулярность — файл.
|
||||
>
|
||||
> Из плана ниже **не сделаны шаги 5 (данные: `room_modif`, `dl1/dl2`,
|
||||
> `_kbdraw_down`; потенциал ~1.5 КБ) и 7 (дедуп `draw_tile`, отложен по
|
||||
> решению пользователя)**.
|
||||
|
||||
Статус: **план для отдельной сессии**, составлен 2026-07-29 по свежему замеру.
|
||||
Заменял `size_optimization_plan.md` (v1, 2026-07-21) — тот удалён 2026-08-01
|
||||
как полностью перекрытый этим документом. Документ самодостаточный
|
||||
— рассчитан на старт с пустого контекста.
|
||||
|
||||
Повод: перед стражами и боёвкой (новый код ~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
|
||||
SprPoP 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 КБ
|
||||
SprPoP : 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, SprPoP 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-SprPoP/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, SprPoP,
|
||||
pop_kid — то есть из главного цикла на каждом кадре; плюс он сам держит
|
||||
`tile_table` и всю отрисовку. Банк дал бы трамплин на каждый блит.
|
||||
- **Нельзя банковать `pop_map`, пока `pop_map` зовёт `pop_bg`** (R2). Сейчас
|
||||
зовёт: `pop_loose_tick` и компания (~30 вызовов графики).
|
||||
- **Нельзя тащить `kid_frames` в EMM «в лоб»**: он читается несколько раз за
|
||||
кадр из коллизии (`kid_cur_dx`/`kid_cur_flags` → `dx_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-часть SprPoP?
|
||||
pop_ctrl (ввод/диспетчер)
|
||||
SprPoP (главный цикл)
|
||||
```
|
||||
|
||||
Почему так:
|
||||
|
||||
- **`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 (+ проверка кодов в SprPoP) | 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-SprPoP/*.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/SprPoP/TASKS_OPEN.md` — что из этого берётся в работу сейчас.
|
||||
|
||||
---
|
||||
|
||||
## 9. Скорость отрисовки: замеры и запас
|
||||
|
||||
Перенесено из удалённого `size_optimization_plan.md` §8 (замер 2026-07-27) —
|
||||
единственная его часть, которая не была перекрыта этим документом.
|
||||
Профилирование в MAME: маркеры в порт 0xFE + `wpiset … totalcycles` (приём из
|
||||
memory `mame_mcp_bridge`); в самом `sprpop.c` для этого уже стоят полосы
|
||||
бордюра `PROF()`. Кадр Sprinter = **430 080 тактов**.
|
||||
|
||||
**Стоимость блита почти НЕ зависит от размера** — платим за проход по цепочке
|
||||
`gfx_blit → gfx_blit_part → _gfx_blit_full` (16-битная арифметика, клип,
|
||||
пересчёт src, нарезка полос >256), а не за пиксели:
|
||||
|
||||
| путь (спрайт 32×3) | тактов |
|
||||
|---|---|
|
||||
| `gfx_blit` (общее ядро, с клипом) | 13 288 |
|
||||
| линейное ядро без клипа | 4 617 |
|
||||
|
||||
Отсюда `draw_tile(0,0)` тайла шва (9 блитов) стоил **183 690 тактов = 43 %
|
||||
кадра**; сам `bar` — только 13 308.
|
||||
|
||||
**Сделано:** `gfx_blit_noclip()` в libbgi, фоновые блиты `pop_bg` уходят на
|
||||
него, когда спрайт целиком на экране (~2.9×, подтверждено в MAME). Позже
|
||||
тем же приёмом закрыты спрайты персонажей (`gfx_blit_cols_part_noclip`).
|
||||
**Не закрыт heal** — задача CLIP-1 в `../PoP/SprPoP/TASKS_CLOSED.md`.
|
||||
|
||||
**ВАЖНО:** W3-скобку (`_bgi_begin`/`_bgi_end`) ставит САМА libbgi — вызывать
|
||||
её из модуля, собранного с `--w3`, нельзя: после `_bgi_begin` окно W3 занято
|
||||
видеобанком и код вызывающего исчезает из адресного пространства (проверено:
|
||||
белый экран).
|
||||
|
||||
**Запас, когда перестанет хватать бюджета кадра:**
|
||||
|
||||
1. **Батчинг W3-скобки** — одна `_bgi_begin`/`_bgi_end` на весь `draw_tile`
|
||||
вместо скобки на блит; нужен публичный batch-API в libbgi.
|
||||
**Осторожно, и это стало важнее, чем было:** между begin/end стоит `DI`,
|
||||
длинная серия задержит кадровое прерывание — а по разбору KBD-1
|
||||
(`../PoP/SprPoP/TASKS_CLOSED.md`) длинные DI-окна и есть причина потери байт
|
||||
клавиатуры. Батчинг эту проблему УХУДШИТ, если делать его вслепую.
|
||||
2. **Решётка ворот одним спрайтом** — `draw_gate_back` рисует бары по одному
|
||||
(до 7 блитов). Сгенерировать в атласе «столб решётки» и выводить одним
|
||||
`gfx_blit_part` с обрезкой по фазе `gate_bot_y & 7`: 7 блитов → 1.
|
||||
3. **Не перерисовывать статичные части шва** — грань ворот, пол и кромка при
|
||||
анимации решётки не меняются (см. OPT-1 в `../PoP/SprPoP/BUGS_CLOSED.md` —
|
||||
решено не делать, стоимость транзиентная).
|
||||
4. **T-1 / T-2** (`../BUGS_OPEN.md`) — перерисовка пик по причине и
|
||||
idle-skip Кида: самый большой оставшийся резерв, потому что убирает работу
|
||||
целиком, а не удешевляет её.
|
||||
Reference in New Issue
Block a user