DRAW-CHAR: отрисовка одна на всех Char; разгрузка банка 2 (90.4% -> 72.9%)

DRAW-CHAR.  Отрисовка персонажа сведена к одному набору функций над Char —
как физика после GUARD-PHYS.  В оригинале add_kid_to_objtable (seg008:22F0) и
add_guard_to_objtable (seg008:2324) имеют идентичное тело и различаются
окном (loadkid/loadshad), набором спрайтов и типом объекта, а
redraw_at_char/redraw_at_char2 гейтов по charid не имеют вовсе.

  pop_gdraw.c -> pop_cdraw.c: pop_char_draw/heal/fore(who), слот
  POP_CH_KID / POP_CH_OPP; состояние слотов pop_cd[] в _DATA — читается из
  любого банка без трамплина.  Проход окклюзии тоже один
  (pop_fore_over_char), pop_fore_over_kid больше нет.

Починилось само (расхождения, которые и были ценой дублирования): у
соперника не было clip_char; у Кида не было клипа полем 192 и ветки брызг
«мёртв/падение»; char_width_half СТРАЖА считался по спрайту КИДА.

Замер: _CODE 24 881 -> 20 524 (куча 2023 -> 6333), BANK2 -265, итого -3.2 КБ.
Проверено пользователем в MAME; циан-полоса профиля подросла — оптимизация
заведена отдельной задачей DRAW-COST.

MEM-BANK2, шаг 1: общие «листья» слоя фона в РЕЗИДЕНТ (pop_tile.c/.h).
Ограничение платформы: писучие данные банка лежат в _DATA и видны всем, а
const-таблицы — в странице банка, из другого банка их не прочитать; трамплин
же выбирается объявлением, то есть __banked на листе бьёт и по горячим
вызывающим (654 такта).  W1 замаплено всегда — оттуда обе половины зовут
листья прямым call и читают таблицы напрямую.

MEM-BANK2, шаг 2: дедуп внутри банка.  wall_pattern 808 -> 394 и wall_rnd
786 -> 654: четыре ветки по виду стены отличались только набором кусков и
числами в одной серии prandom — сведены к таблицам WP_PARTS и WR_RULE,
порядок вызовов prandom сохранён дословно.

Заодно: kid_seq_off больше не static const в kid_data.h (230 Б мёртвой копии
в каждом из 9 модулей) — генератор pop_extract_kid_data.py отдаёт
макро-инициализатор, массив определяет один pop_kid.c.

Итог: BANK2 14 815 -> 11 942 (72.9 %, свободно 4442 Б), _CODE 22 556,
куча 4301 Б.  tests-host зелёные (65/39/53/1723/1); в MAME комната 1
совпала с дорефакторным снимком попиксельно (0 из 227 520), комната 3 —
та же раскладка кладки.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 13:25:53 +03:00
parent 6673279cef
commit bbf91d10ee
28 changed files with 1593 additions and 1471 deletions
+107 -60
View File
@@ -1,4 +1,4 @@
# roomtest — доска ОТКРЫТЫХ задач (обновлено 2026-08-07)
# roomtest — доска ОТКРЫТЫХ задач (обновлено 2026-08-08)
Что берём в работу сейчас и в каком порядке. Каждая запись: что сделать,
почему именно сейчас, чем подтверждать результат.
@@ -12,9 +12,18 @@
**Состояние на 2026-08-07:** пользователь прогнал ВСЕ комнаты уровней 1 и 2 —
крупных багов нет. Приёмки [L1-PASS](TASKS_CLOSED.md#l1-pass) и
[L2-PASS](TASKS_CLOSED.md#l2-pass) закрыты; с прогона открыты три записи по
уровню 2 (цвет стража, брызги по стражу, чит `+`/`` в бою) — все в
[`bug_list.md`](bug_list.md). Уровень 3 играется, но приёмки не было: сначала
чомперы и скелет.
уровню 2, из них цвет стража и брызги уже закрыты (см.
[`bug_closed.md`](bug_closed.md)); открытым остался чит `+`/`` в бою.
Уровень 3: **скелет сделан и проверен в MAME 2026-08-07** (бой, падение в
пропасть, окклюзия), чомперов ещё нет.
**Разгрузка банка 2 сделана 2026-08-08** ([MEM-BANK2](#mem-bank2)): 90.4 % →
**72.9 %, свободно 4442 Б** — чомперам места хватает.
**DRAW-CHAR сделана и ПРОВЕРЕНА 2026-08-08** (протокол и замеры —
[`TASKS_CLOSED.md`](TASKS_CLOSED.md#draw-char)): отрисовка теперь одна на
всех `Char`. Банку 2 она дала всего +265 Б — место под чомперов дала уже
[MEM-BANK2](#mem-bank2).
Правило проекта в силе: механику сверять с `../SDLPoP/src/` ДО кодинга;
диагноз платформы подтверждать артефактом (брейкпоинт/дамп/.asm), а не
@@ -40,9 +49,11 @@
| # | Задача | Что | Блокирует |
|---|--------|-----|-----------|
| 1 | [GUARD-PHYS](#guard-phys) | физика стража = физика Кида (одна над `Char`) | **ядро сделано 2026-08-07**; открыт живой сценарий в MAME |
| 2 | [L3-CHOMP](#l3-chomp) | чомперы (5 шт) | прохождение ур. 3 |
| 3 | [L3-SKEL](#l3-skel) | скелет (единственный противник ур. 3) | прохождение ур. 3 |
| 4 | [L3-PASS](#l3-pass) | приёмка уровня 3 (обход комнат) | закрытие цели |
| | [DRAW-CHAR](TASKS_CLOSED.md#draw-char) | отрисовка одна на всех `Char`**закрыта и проверена 2026-08-08** | — |
| | [MEM-BANK2](#mem-bank2) | разгрузка банка 2 — **сделана 2026-08-08**: 90.4 % → 72.9 %, свободно 4442 Б | — |
| 2 | [L3-CHOMP](#l3-chomp) | **СЛЕДУЮЩАЯ**: чомперы (5 шт) | прохождение ур. 3 |
| — | [L3-SKEL](#l3-skel) | скелет ур. 3 — **сделан 2026-08-07**, ждёт финальной приёмки | — |
| 3 | [L3-PASS](#l3-pass) | приёмка уровня 3 (обход комнат) | закрытие цели |
| — | [L1-SPEED](#l1-speed) | игра на ~39 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент |
| — | [TUNE-1](#tune-1) | параметры движка → cfg-файл (сейчас `pop_tune.h`) | отладка таймингов и моды; берётся по мере надобности |
@@ -173,6 +184,66 @@
скриншотах пользователя). Обратная проверка: если в соседней комнате СВОЙ
живой страж — переход не происходит.
### <a id="mem-bank2"></a>MEM-BANK2. Разгрузка банка 2 — **шаги 1-2 сделаны 2026-08-08**
Банк 2 (`pop_bg.c`) упирался в потолок: 90.8 % ещё до чомперов. Разбор по
символам показал, что он держит ДВЕ разные вещи — горячий fore-проход (каждый
кадр) и холодную отрисовку тайлов (вход в комнату, точечные перерисовки), —
а общие «листья» (`blit_b`, `tile_code`, `tile_table`, `wall_pattern`) нужны
обеим.
**Ключевое ограничение платформы** (записано в memory
`sdcc_banked_call_rules`): писучие данные банка лежат в `_DATA`/W2 и видны
всем, а `const`-таблицы — в СТРАНИЦЕ банка, и из другого банка их не
прочитать вовсе. Плюс трамплин выбирается ОБЪЯВЛЕНИЕМ: пометить лист
`__banked` — значит заставить платить и горячих вызывающих (654 такта на
круг).
**Шаг 1 — общие листья в РЕЗИДЕНТ** (`pop_tile.c` + внутренний `pop_tile.h`).
W1 замаплен всегда, поэтому и горячая половина, и будущая холодная зовут их
прямым `call` и читают таблицы напрямую — без дублей и без трамплинов.
Уехали: `blit_b`, `env_b/wall_b/fore_b/pot_b`, `tile_code/tile_mod/
tile_code_drawn/wall_modifier`, `heal_off`, `get_loose_frame`,
`potion_flask`, `pop_fore_set_clip`, `pop_room_set_below/above` + таблицы
`tile_table`, `COL_XH`, `WALL_FRAM_*`, `SPIKES_FRAM_LEFT`,
`LOOSE_FRAM_LEFT/BOTTOM`. Заодно три функции перестали быть `__banked` —
минус трамплин на кадр.
**Шаг 2 — дедуп внутри банка.** `wall_pattern` (808 → 394) и `wall_rnd`
(786 → 654): четыре ветки по виду стены (SWS/SWW/WWS/WWW) отличались ТОЛЬКО
набором кусков и числами в одной и той же серии `prandom` — сведены к
таблицам `WP_PARTS` и `WR_RULE`. Порядок вызовов `prandom` сохранён
дословно (иначе разъедется раскладка кладки).
**Грабля, пойманная замером:** после выноса `tile_table` из `static const` в
глобальную SDCC перестала сворачивать `tile_table[10].fore_x/fore_y` в
константы и стала читать их в рантайме — `potion_flask` раздулся 115 → 265 Б.
Лечится макросами, которыми инициализируется та же запись таблицы (одно место
правки, дублирования данных нет): 265 → 230.
**Замер (ALLOCS=3000):**
```
было после ш.1 после ш.2
BANK2 14 815 12 460 11 942 (90.4 % -> 72.9 %, свободно 4442 Б)
_CODE 20 064 22 591 22 556 (куча 6793 -> 4301 Б)
```
С релизным `ALLOCS=100000` банк 2 будет ещё примерно на 500-600 Б меньше
(замер до разгрузки: 14 815 -> 14 176).
**Проверено:** `tests-host` зелёные; в MAME комната 1 уровня 1 после дедупа
совпала с дорефакторным снимком **попиксельно (0 различий из 227 520)**,
комната 3 (сплошная кладка, все четыре вида стены и марки) — та же раскладка
кирпича.
**Что осталось в запасе, если места снова не хватит** (не делаем, пока нет
причины): вынести ХОЛОДНУЮ половину (`draw_tile` 1800 + точечные
перерисовки 1438 + падающие плиты 1898 ≈ 5.1 КБ) в свободный банк 7. Теперь
это дёшево: листья уже в резиденте, а `wall_pattern`/`wall_rnd`/марки/
`draw_gate_back` нужны обеим половинам — им понадобится тонкая `__banked`-
обёртка (тело остаётся непомеченным, горячие вызывающие платят ноль).
### <a id="l3-chomp"></a>L3-CHOMP. Чомперы — механика уровня 3
**Где:** 5 штук, комнаты 5(тайл 4), 16(23,24,25), 22(26). Дальше они почти
@@ -183,10 +254,10 @@
коллизия и смерть Кида в сомкнутых челюстях (`seg004`/`seg006`); отрисовка
кадра по модификатору (`draw_tile_anim`, ветка `tiles_18_chomper`).
**Риск:** банк 2 (`pop_bg`) занят на **90.8 %, свободно 1503 Б** (замер
2026-08-07, было 2213 Б) — считать
место ДО кодинга (`levels_plan.md` §5.1), иначе повторится «банк 2 упёрся
в потолок» (коммит 2f3e854). Свободные номера банков есть (5+).
**Место в банке 2 расчищено** ([MEM-BANK2](#mem-bank2), 2026-08-08): 72.9 %,
**свободно 4442 Б** (было 1074 Б). Считать место ДО кодинга всё равно
обязательно (`levels_plan.md` §5.1), и если не хватит — в запасе вынос
холодной половины `pop_bg` в банк 7, там расписано как.
**Ассеты УЖЕ упакованы (2026-08-07):** `pop_pack_bg.py` кладёт в атлас весь
набор кадров чомпера явно (`CHOMPER_BOT_IDS` 101-105, `CHOMPER_TOP_IDS`
@@ -301,60 +372,36 @@ offset 22790, **240 байт** = 5 палитр × 16 цветов × 3 байт
таблицу нельзя передавать прямо из банка (0xC000+), надо копировать в
W1/W2 (см. [BUG-GUARD-COLOR-1](bug_closed.md#bug-guard-color-1)).
### <a id="draw-char"></a>DRAW-CHAR. Отрисовка — ОДНА на всех Char, как физика после GUARD-PHYS
### <a id="draw-cost"></a>DRAW-COST. Стоимость fore-прохода после DRAW-CHAR
**Почему заведено (пользователь, 2026-08-07).** После GUARD-PHYS физика стала
общей над `Char`, а ОТРИСОВКА так и осталась продублированной: `kid_draw` и
`pop_guard_draw` считают `load_frame_to_obj`, флип и heal-прямоугольники
каждая по-своему, окклюзия — двумя проходами (`pop_fore_over_kid` /
`pop_fore_over_char`). Общее вынесено лишь частично (`char_footprint`,
`other_overlay_tile`, `pop_sword_draw`), а СПИСКИ ВЫЗОВОВ разные.
**Наблюдение пользователя на приёмке 2026-08-08:** циан-полоса профиля
(спрайты + fore) выросла — «в пределах, но оптимизация отдельной задачей».
**В оригинале она общая — проверено.** `seg008` держит два входа с
ИДЕНТИЧНЫМ телом:
**Отчего именно.** Раньше перебор тайлов у Кида шёл строго по футпринту
кадра (`char_x_left/right`, seg006:1021), а он УЖЕ спрайта; расширение
перебора окном спрайта (клинок и брызги уходят за габарит кадра) было
только у соперника. DRAW-CHAR сделала его общим — то есть у Кида теперь на
колонку-другую больше `fore_tile` за кадр. Это не регрессия «лишней
работы», а недостающая ранее корректность: тайл, который спрайт задевает,
обязан вернуть свой передний слой поверх него.
```
add_kid_to_objtable (seg008:22F0) add_guard_to_objtable (seg008:2324)
loadkid() loadshad()
load_fram_det_col() load_fram_det_col()
load_frame_to_obj() load_frame_to_obj()
stuck_lower() stuck_lower()
set_char_collision() set_char_collision()
set_objtile_at_char() set_objtile_at_char()
redraw_at_char() redraw_at_char()
redraw_at_char2() redraw_at_char2()
clip_char() clip_char()
add_objtable(0) add_objtable(1 тень / 2 страж)
```
**Куда копать, когда возьмёмся** (сначала МЕРИТЬ, потом резать —
[`pop_fore_layer_cost`](../../../docs) уже показывал 78 % кадра до кэша
кладки):
1. расширять перебор ТОЛЬКО по накладным спрайтам (клинок/брызги), а не по
всему окну: габарит самого кадра уже покрыт футпринтом + правилом меча;
2. кэш «в этом тайле fore-слоя нет вовсе» — половина колонок пустые, а за
каждую платим `tile_in_fclip` + разбор кладки;
3. считать окно клипа в тайловых координатах один раз, а не сравнивать
пиксельные габариты в двух циклах.
Различаются ТОЛЬКО окно (`Kid`/`Guard`), тип объекта и спецкейс тени у
зеркала (уровень 4). То есть та же схема, что `play_kid_frame` /
`play_guard_frame` у физики.
### <a id="draw-char"></a>DRAW-CHAR — **ЗАКРЫТА И ПРОВЕРЕНА 2026-08-08**
**Цена дублирования уже видна.** Скелет, падающий в пропасть (ур. 3),
рисовался ПОВЕРХ верхней грани пола соседней колонки: проход
`other_overlay_tile` (порядок midtable — «тайлы позже персонажа ложатся
поверх него») вызывался только у Кида. Симптом закрыт 2026-08-07
переносом прохода в `pop_fore_over_char`, но корень — именно дублирование.
Раньше это не всплывало потому, что ОБЫЧНЫЙ страж в пропасть не падает: его
физика гейтится `x ∈ [44,211)`, а провалившегося убирает
`check_guard_fallout`. Тем же путём пойдут тень (ур. 4/5/6/12), визирь
(13), толстяк (12) — расхождение будет повторяться на каждом.
**Что сделать:** свести к одному набору функций над `Char`, оставив
параметрами ровно то, что различается в оригинале:
- окно (`loadkid`/`loadshad` — уже есть);
- набор атласов и таблица кадров (`kid*` / `GUARD` / `SKEL` — уже
выбирается в `pop_guard_load`);
- слот heal-прямоугольников (у каждого персонажа свой, по страницам
дабл-буфера);
- тип объекта для порядка (Kid / тень / страж).
**Не портированы вовсе и относятся сюда же:** `stuck_lower`, `redraw_at_char2`,
`clip_char` (см. memory `pop_clip_char_todo`).
**Чем подтверждать:** падение скелета в пропасть с разных X (сегодняшний
сценарий), бой стража на ур. 1-2 без регрессий, `tests-host` зелёные.
Отрисовка сведена к одному набору функций над `Char` (`pop_cdraw.c`), проход
окклюзии — один на всех (`pop_fore_over_char`). Разбор, таблица «было →
стало», пять починенных расхождений и замеры — в
[`TASKS_CLOSED.md`](TASKS_CLOSED.md#draw-char). Оттуда же список того, что
надо потрогать вживую при приёмке уровней.
### <a id="l3-pass"></a>L3-PASS. Приёмка уровня 3 — обход всех комнат