1146c57544
kid_heal/pop_guard_heal звали gfx_heal всегда, хотя рисуют ровно тот прямоугольник, который блит в большинстве кадров кладёт noclip-ядром. Все три места heal (+ heal_off фона) сведены к общему pop_heal_fast. Замер в MAME, счётчики totalcycles на входах kid_heal и kid_tick (вся группа heal за кадр), комната 1, Кид стоит: клипающее ядро 26 200 тактов/кадр (149 кадров) noclip 15 848 тактов/кадр (239 кадров) −10 352 такта, −39.5 %. A/B в одном прогоне: вторая половина снята с пропатченным в памяти условием (jr nz → jr), то есть на той же геометрии. Размер СУММАРНО −362 Б: _CODE +17, BANK2 −116 (свободно 2708 — это тесный банк из рисков levels_plan §5), BANK3 −263, BANK4 без изменений. Грабли по дороге: первым заходом хелпер был static inline в pop_bg.h — SDCC 4.5 И встраивает тело (181 Б) в каждый вызов, И оставляет копию в каждом TU, который видит заголовок. pop_guard_heal раздулся с ~60 до 663 Б, итого +1091 Б в _CODE и +636 Б в банке стража. Отсюда pop_draw.c: обычная функция в резиденте W1, из банков это прямой call без трамплина. pop_room_clip_borders оставлен клипающим осознанно (320 не лезет в 8 бит, гейт border_dirty редкий) — причина записана в коде. Проверено визуально: ходьба, прыжок, спуск, позиция за решёткой шва (straddle — там работает клипающий фолбэк) — артефактов нет. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
437 lines
37 KiB
Markdown
437 lines
37 KiB
Markdown
# roomtest — доска текущих задач (обновлено 2026-08-01)
|
||
|
||
Не список багов (открытые — [`bug_list.md`](bug_list.md), закрытые с разбором
|
||
корней — [`bug_closed.md`](bug_closed.md)) и не план фаз
|
||
(`../docs/PORT_PLAN.md`, `../docs/layout_plan_v2.md`, `../docs/levels_plan.md`),
|
||
а то, **что берём в работу сейчас и в каком порядке**. Каждая запись: что
|
||
сделать, почему именно сейчас, чем подтверждать результат.
|
||
|
||
**Открытых багов нет** (ревизия 2026-08-01): в [`bug_list.md`](bug_list.md)
|
||
остались только незакрытые оптимизации [T-1](bug_list.md#t-1),
|
||
[T-2](bug_list.md#t-2) и незаконченная
|
||
[таблица обхода 24 комнат](bug_list.md#обход-всех-24-комнат-уровня-1).
|
||
Следующая порция багов придёт оттуда и из L1-PASS.
|
||
|
||
Правило проекта в силе: механику сверять с `../SDLPoP/src/` ДО кодинга;
|
||
диагноз платформы подтверждать артефактом (брейкпоинт/дамп/.asm), а не
|
||
гипотезой (memory `defer_unexplained_quirks`).
|
||
|
||
---
|
||
|
||
## P0 — делаем сейчас
|
||
|
||
*(KBD-1 закрыт до финальной полировки, CLIP-1 сделан — оба ниже. P0 пуст;
|
||
следующая в работе — P1, начиная с L1-START/L1-EXIT.)*
|
||
|
||
### KBD-1. Shift + стрелки: нажатия теряются — **ПРИЧИНА НАЙДЕНА, ФИКС ПРОВЕРЕН**
|
||
|
||
> **Итог (2026-08-01).** Причина — не наш код и не DI-окна графики: при
|
||
> зажатом Shift PS/2 удваивает трафик («fake shift»), а импульс запроса
|
||
> прерывания здесь теряется примерно в 44 % случаев, и трёхбайтовый FIFO
|
||
> SIO переполняется. Лечится ПЛОТНЫМ опросом: `kbd_raw_poll` повешен
|
||
> idle-хуком на ожидание кадра (`gfx_set_idle_hook`, новый API libbgi) —
|
||
> процессор всё равно проводит там ~42 мс из 60, крутя опрос луча.
|
||
>
|
||
> **Проверка в roomtest тем же счётным методом: 35 нажатий Shift+Home →
|
||
> 35 make, ноль потерь** (до фикса — 9 из 10). Боевой сценарий тоже:
|
||
> четыре Shift+→ подряд дали четыре осторожных шага, `Kid.x` 114 → 147.
|
||
> Цена: `_CODE` +170 Б, кадровый бюджет не затронут (опрос стоит в
|
||
> простое). Ниже — полный протокол, как к этому пришли.
|
||
>
|
||
> **ОСТАТОК (ручная проверка пользователем, 2026-08-01): «стало значительно
|
||
> лучше, но иногда при зажатом Shift стрелка всё-таки пропускается».**
|
||
> Ощущение, не замер — счётчики на 35 нажатиях подряд потерь не показали,
|
||
> значит остаточная частота заметно ниже прежних ~15 %. **Задача осознанно
|
||
> ОТЛОЖЕНА до финальной полировки всей программы** (решение пользователя);
|
||
> сейчас клавиатура пригодна для работы.
|
||
>
|
||
> **Где именно осталась дыра — чтобы на полировке не начинать с нуля.**
|
||
> Idle-хук покрывает простой, то есть ~2/3 кадра. Оставшаяся треть — это
|
||
> занятая фаза, и там DI-окно одного accel-прохода доходит до ~650 мкс при
|
||
> допуске FIFO ~300 мкс. Пачка байт, целиком попавшая в такое окно, всё
|
||
> ещё может потерять байт — ровно «иногда». Порядок действий, если
|
||
> вернёмся:
|
||
> 1. Вернуть вызовы `kbd_raw_poll()` между блитами занятой фазы (они
|
||
> бесплатны; сами по себе не помогали, но вместе с хуком закрывают
|
||
> именно этот зазор) и при необходимости внутрь тайловых циклов
|
||
> `pop_bg` — тогда слепым остаётся только тело одного блита.
|
||
> 2. Мерить тем же счётным методом (см. ниже), а не на ощупь: скриптовые
|
||
> нажатия ровнее человеческих, поэтому набирать выборку от 50 нажатий.
|
||
> 3. Если и это не добьёт — остаются два рычага вне нашего кода: Scan Code
|
||
> Set 3 через BIOS `$EA` (убирает «fake shift» в корне, но в MAME
|
||
> непроверяемо — обратный путь к клавиатуре не разведён) и общий
|
||
> `m_irq_off_timer` в драйвере MAME.
|
||
|
||
**Симптом (пользователь, 2026-08-01).** Залипаний почти нет, но при
|
||
УДЕРЖИВАЕМОМ Shift первые один-два нажатия ← дают осторожные шаги, дальше
|
||
нажатия ← не отрабатываются, пока Shift не отпустишь.
|
||
|
||
**Рабочая гипотеза (механизм, а не догадка «что-то с клавиатурой»).**
|
||
Три известных факта складываются в одну картину:
|
||
|
||
1. **PS/2 Set 2, «fake shift».** При зажатом Shift нажатие РАСШИРЕННОЙ
|
||
клавиши (стрелки — `E0`-коды) обрамляется фиктивным отпусканием/нажатием
|
||
шифта: нажатие ← шлёт `E0 F0 12` + `E0 6B` = **5 байт** (без шифта было
|
||
бы 2), отпускание — `E0 F0 6B` + `E0 12` = **5 байт** (было 3). То есть
|
||
ровно в связке Shift+стрелка трафик удваивается.
|
||
2. **Приёмный FIFO SIO — 3 байта.** Пачка в 5 байт переживает только то,
|
||
что мы успеваем вычерпывать её по ходу. Потерянный make стрелки =
|
||
«нажатие не отработало»; потерянный break = залипание (его лечит
|
||
`kbd_raw_sync`, но ценой сброса всех немодификаторных клавиш).
|
||
3. **Импульс IRQ клавиатуры в MAME живёт 32 такта CPU.**
|
||
`mame/sources/MAME/src/mame/sinclair/sprinter.cpp`: `on_kbd_data()`
|
||
выставляет `m_irqs->in_set<1>()` НА КАЖДЫЙ принятый байт (то есть старая
|
||
запись в `docs/TODO.md` «MAME не даёт per-byte INT» — неверна), но тут же
|
||
заводит `m_irq_off_timer` на 32 такта, а `irq_off()` снимает линию.
|
||
**Если в эти 32 такта мы под `DI` — прерывание пропало насовсем**, байт
|
||
остаётся в FIFO до следующего IRQ (следующий байт или кадровый 50 Гц).
|
||
4. **Наши DI-окна длинные.** Ядра акселератора держат `di` на ВЕСЬ блит
|
||
(`libbgi/bgi256/_bgi_blit_cols_raw.c:47` — «один DI на весь блит»);
|
||
порядок цены прохода — 13.6 К тактов (`libbgi/include/gfx.h`), это
|
||
сотни микросекунд, на порядки больше 32-тактового импульса.
|
||
|
||
Отсюда: **обе версии пользователя — про одно и то же.** Логика ввода
|
||
(`pop_ctrl.c`, порт `read_user_control`/`safe_step`) сверена с SDLPoP и
|
||
выглядит корректной: `safe_step()` ставит `control_forward = CONTROL_IGNORE`,
|
||
и это снимается в `read_user_control()` при ОТПУСКАНИИ стрелки — то есть
|
||
повторные тапы ← при зажатом Shift обязаны работать. Не работают они
|
||
потому, что до нас не доезжает либо make, либо break стрелки.
|
||
|
||
**План проверки — по шагам, каждый даёт артефакт:**
|
||
|
||
1. Счётчики в MAME: брейк на `_kbdraw_overrun` (запись) и на ветке
|
||
`tr_kbd_drain` — сколько overrun'ов за 10 с при «Shift зажат, тапаю ←»
|
||
против «тапаю ← без Shift». Ожидание по гипотезе: с Shift кратно больше.
|
||
2. Замер максимального DI-окна кадра: брейкпоинты на `di`/`ei` в
|
||
`_bgi_blit_cols_raw` + `{printf totalcycles; g}` — получить реальную длину
|
||
в тактах и в микросекундах.
|
||
3. **Спайк «блит без DI».** `docs/new/06-accel.md §6.6`: новая прошивка
|
||
допускает работу акселератора при EI (по приходу прерывания он
|
||
отключается, по `RETI` включается обратно); старая — нет. Собрать libbgi
|
||
с убранным `di` в блит/heal-ядрах, прогнать roomtest в MAME: (а) не
|
||
рушится ли картинка, (б) падает ли счётчик overrun из п.1. Если да —
|
||
причина подтверждена, и дальше это вопрос «какая прошивка на живом
|
||
железе» (по умолчанию оставить DI, режим без DI — опцией libbgi).
|
||
4. **Независимо от п.3 — `kbd_raw_poll()`.** Вычерпывание FIFO ОПРОСОМ
|
||
(порт `0x19` бит 0 → читать `0x18`, тот же декодер make/break, что в
|
||
трамплине) из главного цикла 2–4 раза за кадр между фазами `PROF()`.
|
||
Снимает зависимость от «поймали ли мы импульс IRQ» вообще, стоит сотни
|
||
тактов, графику не трогает. Реализация: вынести drain-цикл из
|
||
`libc/irq/_irq_tramp.c` в общий кусок либо продублировать в
|
||
`libc/kbd/kbd_raw_poll.c`; тело обязано идти под `DI` (гонка с ISR за
|
||
деструктивное чтение порта 0x18).
|
||
5. Побочно сюда же играет **[T-2](bug_list.md#t-2) (idle-skip)**: не
|
||
перерисовывать Кида, пока поза/координаты не менялись, — это минус
|
||
heal+blit (то есть минус DI-окна) в самых спокойных кадрах, где как раз
|
||
и тапают Shift+стрелку.
|
||
6. Только если после 3–4 симптом жив — копать логику
|
||
`control_shift2`/`CONTROL_IGNORE` против `seg005.c:374..390`.
|
||
|
||
**Критерий готовности:** при зажатом Shift десять тапов ← дают десять
|
||
осторожных шагов (проверка в MAME через `:kbd:ms_naturl:*` напрямую, НЕ
|
||
через `press_key` — тот дёргает обе клавиатуры, см. `docs/libc-reference.md`
|
||
`<kbd_raw.h>`).
|
||
|
||
---
|
||
|
||
### KBD-1: ЧТО ИЗМЕРЕНО (сессия 2026-08-01) — гипотеза про DI НЕ подтвердилась
|
||
|
||
**Методика.** Симптом «нажатие не отработало» переведён в счётчики, чтобы не
|
||
спорить с глазами. Нажимается **Home** — тоже расширенная клавиша (тот же
|
||
`E0`-префикс и тот же «fake shift», что у стрелок), но игрой игнорируется,
|
||
поэтому Кид стоит на месте и рельеф комнаты на результат не влияет.
|
||
Брейкпоинты с действием `{ b@ADDR = b@ADDR + 1 ; g }` (счёт без остановки
|
||
машины) в трёх точках: вход клавиатурной ветки трамплина, чтение порта 0x18
|
||
внутри drain-цикла, запись make-бита для кода `0x6C`. Скратч-байты — хвост
|
||
`ovr_tile[]` (в этом сценарии не используется).
|
||
|
||
**Симптом воспроизведён скриптом:** при зажатом Shift 10 нажатий → до
|
||
декодера дошло 9 make-байт. Без Shift потерь нет — ровно как сообщил
|
||
пользователь.
|
||
|
||
| Прогон | make дошло / нажато | overrun |
|
||
|--------|---------------------|---------|
|
||
| игра идёт, `kbd_raw_poll` ВКЛ | 9 / 10 | 3 |
|
||
| игра идёт, `kbd_raw_poll` ВЫКЛ (патч `ret` в точке входа) | 9 / 10 | 4 |
|
||
| игра ЗАМОРОЖЕНА клавишей «1» (блитов нет вообще, значит и длинных DI нет) | **8 / 10** | 6 |
|
||
|
||
**Вывод 1: наши DI-окна ни при чём.** В замороженном кадре, где блитов нет
|
||
и прерывания разрешены практически всё время, потерь НЕ меньше, а больше.
|
||
|
||
**Вывод 2: `kbd_raw_poll()` в текущей расстановке бесполезен** — 9/10 и с
|
||
ним, и без. Причина понятна задним числом: шесть вызовов стоят В ТЕХ ЖЕ
|
||
точках, где прерывания и так разрешены, то есть добавляют ровно то, что
|
||
трамплин сделал бы сам. Вызовы из `roomtest.c` убраны; сама функция в libc
|
||
оставлена — она корректна и нужна как заготовка под «плотный опрос» (см.
|
||
ниже), но в горячем цикле её держать не за что.
|
||
|
||
**Вывод 3 (главный): байт теряется НИЖЕ нашего кода.** Счётчик чтений порта
|
||
0x18: 5 нажатий Shift+Home должны дать ровно 50 байт (нажатие `E0 F0 12` +
|
||
`E0 6C`, отпускание `E0 F0 6C` + `E0 12` = по 10 на цикл). Насчитано **49**
|
||
— и ровно один make потерян. То есть до процессора байт не доехал вообще,
|
||
декодер тут ни при чём.
|
||
|
||
**Вывод 4: прерывание на байт теряется примерно в 44 % случаев.** На тех же
|
||
49 прочитанных байтах — только **28 входов** в клавиатурную ветку трамплина
|
||
(1.75 байта за вход). То есть больше сорока процентов импульсов запроса
|
||
не были обслужены, и байты копятся в трёхбайтовом FIFO вплотную к его
|
||
потолку; одна неудачная пауза — и байт потерян.
|
||
|
||
### KBD-1: ПОТОЛОК ПЛОТНОГО ОПРОСА ИЗМЕРЕН — приём лечит полностью
|
||
|
||
`tests/kbdpoll` — программа, которая не делает НИЧЕГО, кроме
|
||
`kbd_raw_poll()` в бесконечном цикле (ни графики, ни vsync, ни вывода:
|
||
любая работа разредила бы опрос и испортила замер). Это физический
|
||
максимум плотности. Тот же счётный метод, те же брейкпоинты-счётчики.
|
||
|
||
| Прогон | нажатий | make дошло | байт прочитано / ожидалось |
|
||
|--------|---------|-----------|-----------------------------|
|
||
| контроль: Shift зажат 4 с, нажатий нет | 0 | 0 | 0 (Shift сам ничего не шлёт — автоповтора у модификатора нет) |
|
||
| Shift + Home | **25** | **25** | **250 / 250** |
|
||
|
||
**Ни одного потерянного байта.** Для сравнения: в игре при шести вызовах
|
||
за кадр терялся 1 байт из 50. При такой частоте потерь вероятность
|
||
случайно получить ноль потерь на 250 байтах ≈ 0.6 %, так что результат не
|
||
совпадение.
|
||
|
||
**Вывод: опрос — рабочее решение, вопрос только в ПЛОТНОСТИ.** Нужно
|
||
опрашивать примерно раз в 0.5 мс (≈10 000 тактов), а шесть вызовов за
|
||
60-мс кадр давали один раз в 10 мс — в двадцать раз реже необходимого.
|
||
|
||
**Где взять частоту:** логический тик = 60 мс, из них ~18 мс занято
|
||
работой и **~42 мс процессор простаивает внутри `gfx_wait_vsync`**, опрашивая
|
||
луч. Опрос там стоит ноль и покрывает две трети периода с запасом по
|
||
плотности. Остаётся слепым только тело одного accel-блита под DI (до
|
||
~650 мкс) — разорвать его нельзя (см. «что НЕ делать»).
|
||
|
||
**Почему нужна именно такая частота (вопрос «PS/2 же не даёт больше 30
|
||
нажатий в секунду»).** Частота опроса определяется НЕ темпом нажатий, а
|
||
темпом байт ВНУТРИ одного нажатия и глубиной FIFO. Одно нажатие при
|
||
зажатом Shift — это 5 байт подряд (`E0 F0 12`, `E0 6C`), отпускание — ещё 5,
|
||
и клавиатура выдаёт их со скоростью провода: 11 бит на байт при ~10–16 кГц
|
||
= ~0.7–1.1 мс на байт. Воронка — 3 байта. Значит между двумя вычерпываниями
|
||
имеют право прийти максимум два байта, то есть вычерпывать надо не реже чем
|
||
раз в ~1.5–2 мс (0.5 мс взято с запасом). **Даже ОДНО нажатие в секунду
|
||
переполнит FIFO**, если в эти несколько миллисекунд его никто не разгребает.
|
||
Замер это подтверждает: 1.75 байта за одно вычерпывание — уже 58 % ёмкости.
|
||
В норме разгребает прерывание; опрос понадобился только потому, что ~44 %
|
||
импульсов здесь теряется.
|
||
|
||
**Альтернатива, которая убирает опрос совсем — уменьшить трафик, а не
|
||
ускорять разгребание.** BIOS `$EA` (`FN_KBD_OUT`, `docs/new/09-input.md`
|
||
§9.2) шлёт байт НА клавиатуру, то есть ей можно скомандовать:
|
||
- **Scan Code Set 3** — нет ни «fake shift», ни `E0`-префиксов: make = 1 байт,
|
||
break = 2. Нажатие с шифтом перестаёт превышать FIFO в принципе.
|
||
- либо хотя бы отключить typematic (`0xF5`/`0xF7`).
|
||
|
||
**Но проверить это в MAME НЕЛЬЗЯ:** в `sprinter.cpp` подключено только
|
||
направление клавиатура→SIO (`m_kbd->out_data_cb() → rxa_w`); обратный путь
|
||
(SIO→клавиатура) не разведён вовсе, так что команда просто уйдёт в никуда.
|
||
Плюс пришлось бы переписать все наши константы кодов под Set 3. Значит это
|
||
кандидат на «когда дойдём до реального железа», а не на сейчас.
|
||
|
||
**Что сделано по этому плану (2026-08-01):**
|
||
1. ✅ Idle-хук в libbgi: `gfx_set_idle_hook(fn)`, вызывается в цикле ожидания
|
||
луча внутри `gfx_wait_vsync`. Приложение ставит туда `kbd_raw_poll`.
|
||
Полезен не только нам — любой программе даёт «качать» что-то в ожидании
|
||
кадра. Осторожно с регистрами: цикл ждёт на BC-таймауте, вокруг вызова
|
||
нужен push/pop, а сам таймаут в итерациях станет длиннее по времени.
|
||
**Важно про цену: это НЕ новая нагрузка.** Опрос ставится ровно туда,
|
||
где процессор и так впустую крутит `in a,(#0xFE)` — 42 мс из 60. Полезной
|
||
работы не отнимается нисколько.
|
||
**Ограничитель области, если «постоянный опрос» всё равно не нравится:**
|
||
потери случаются ТОЛЬКО при зажатом модификаторе (замерено; без Shift
|
||
потерь нет). Значит хук можно взводить лишь пока нажат Shift/Ctrl/Alt —
|
||
тогда опрос работает исключительно в той ситуации, ради которой заведён.
|
||
2. ✅ Перемерено в roomtest тем же счётным методом: **35/35**, потерь нет.
|
||
3. ⏳ Вызовы в занятую треть кадра НЕ возвращены: одного idle-хука хватило.
|
||
Держать в уме, если на реальном железе или на более тяжёлых сценах
|
||
(несколько стражей) потери появятся снова — накрыть блиты дешевле, чем
|
||
изобретать что-то новое.
|
||
4. ⏳ Ручная проверка пользователем — без неё этап не закрыт: скриптовые
|
||
нажатия ровнее человеческих, и «залипания до отпускания Shift» они не
|
||
воспроизводили с самого начала.
|
||
|
||
**Куда смотреть дальше, если плотного опроса не хватит.**
|
||
1. **Драйвер MAME — НЕ ТРОГАЕМ** (решение пользователя: пересборка MAME на
|
||
его машине занимает часы). Для протокола, подозрение осталось:
|
||
`sinclair/sprinter.cpp` держит запрос от клавиатуры ровно **32 такта
|
||
CPU**, и `m_irq_off_timer` — **один на два источника** (`irq_on()` экрана
|
||
заводит его же, `irq_off()` гасит разом обе линии). То есть кадровое
|
||
прерывание способно обрезать клавиатурный импульс — правдоподобное
|
||
объяснение «44 % пропущенных импульсов».
|
||
2. **Реальное железо.** Если п.1 — чисто эмуляционный артефакт, на железе
|
||
проблемы может не быть вовсе. Проверять при первом прогоне на живом
|
||
Sprinter.
|
||
|
||
**Про совпадение кадрового и клавиатурного прерываний** (вопрос
|
||
пользователя, 2026-08-01). Документация Sprinter: оба приходят с вектором
|
||
`0FFh`, различать по биту приёма байта в порту клавиатуры — «не пришёл,
|
||
значит экран»; совпадение возможно, но «исключительно редкий случай»
|
||
(в новой версии обещают развести жёстче через ПЛМ). То есть наш трамплин
|
||
делает ровно предписанное. Известный побочный эффект: при совпадении мы
|
||
обслуживаем клавиатуру и `reti`, пропуская кадровую цепочку и DSS — на
|
||
потерю байт это не влияет (линия кадрового остаётся взведённой и вызывает
|
||
повторный вход), но кадровый тик может пропасть. Отдельная мелкая правка,
|
||
в KBD-1 не входит.
|
||
|
||
**Что НЕ делать (проверено, стоило времени):**
|
||
- **Снимать `di` в accel-ядрах libbgi нельзя.** Патч `di`→`nop` в
|
||
`_bgi_blit_cols_raw`/`_bgi_heal_rows_raw`/`_bgi_blit_rows_raw` прямо в
|
||
памяти **уронил машину в перезагрузку**. То есть режим «акселератор
|
||
работает при EI» из `docs/new/06-accel.md §6.6` в этой прошивке/эмуляции
|
||
недоступен — вопрос закрыт артефактом, а не рассуждением.
|
||
- Дробить DI-окна по колонкам смысла тоже нет: см. вывод 1.
|
||
|
||
---
|
||
|
||
### CLIP-1. Аудит блитов: где клип не нужен — **СДЕЛАНО 2026-08-01**
|
||
|
||
> **Итог.** Heal Кида и стража переведены на выбор ядра по тому же тесту,
|
||
> что давно стоит у блитов. Замер в MAME (счётчики `totalcycles` на входах
|
||
> `kid_heal` и `kid_tick`, то есть вся группа heal за кадр; комната 1, Кид
|
||
> стоит, стража нет):
|
||
>
|
||
> | путь | тактов на кадр | кадров в выборке |
|
||
> |------|----------------|------------------|
|
||
> | клипающее ядро (как было) | **26 200** | 149 |
|
||
> | noclip (стало) | **15 848** | 239 |
|
||
>
|
||
> **−10 352 такта на кадр, то есть −39.5 % с группы heal** (≈0.49 мс при
|
||
> ~21 МГц). A/B честный: оба замера сняты в ОДНОМ прогоне, вторая половина —
|
||
> с пропатченным в памяти условием (`jr nz` → `jr` в `pop_heal_fast`), то
|
||
> есть на той же геометрии и в той же сцене.
|
||
>
|
||
> **Размер: −362 Б суммарно** (не плюс!): `_CODE` 25 289 → 25 306 (+17),
|
||
> BANK2 13 792 → **13 676** (−116, свободно стало 2708 Б — это тот самый
|
||
> тесный банк из рисков `levels_plan.md` §5), BANK3 6512 → 6249 (−263),
|
||
> BANK4 без изменений.
|
||
>
|
||
> **Грабли, стоившие двух пересборок** (вынесено в память
|
||
> `sdcc-static-inline-double-cost`): первым заходом хелпер был `static
|
||
> inline` в `pop_bg.h` — и SDCC 4.5 И встроил его тело (181 Б) в каждое
|
||
> место вызова, И оставил отдельную копию в КАЖДОМ TU, который видит
|
||
> заголовок. `pop_guard_heal` раздулся с ~60 до 663 Б, итого +1091 Б в
|
||
> `_CODE` и +636 Б в банке стража. Лечится обычной функцией в одном
|
||
> резидентном модуле (`pop_draw.c`, W1 — из банков это прямой `call` без
|
||
> трамплина, как у `pop_sword_draw`).
|
||
>
|
||
> **Проверено визуально:** обычная ходьба, прыжок, спуск и позиция «за
|
||
> решёткой шва» (straddle — там как раз работает клипающий фолбэк) —
|
||
> артефактов и следов нет.
|
||
|
||
**Ниже — исходная постановка задачи (что и почему смотрели).**
|
||
|
||
|
||
**Зачем сейчас.** Кадр занят на ~86 %; подготовка клипающего варианта
|
||
стоит ~5.6 К тактов на вызов, а общее ядро против линейного — 13 288 против
|
||
4 617 тактов на спрайт 32×3 (`libbgi/include/gfx.h`). Это самая дешёвая
|
||
оставшаяся оптимизация: не переписывание логики, а выбор ядра.
|
||
|
||
**Что уже правильно** (шаблон, который надо распространить): блиты Кида,
|
||
стража, клинка и брызг спрашивают `pop_onscreen_cols()` и уходят в
|
||
`gfx_blit_cols_part_noclip`, иначе в клипающий вариант
|
||
(`pop_kid.c:86,591,676`, `pop_gdraw.c:105`).
|
||
|
||
**Что чинили:**
|
||
|
||
- ✅ `kid_heal()` и `pop_guard_heal()` звали `gfx_heal` — **всегда с
|
||
клипом**, хотя `gfx_heal_noclip` существует и `heal_off` в `pop_bg.c` им
|
||
уже пользовался. Это heal 2–4 прямоугольников КАЖДЫЙ кадр; замер общего
|
||
ядра — 11 658 тактов на heal 22×22. Теперь все три идут через общий
|
||
`pop_heal_fast` (`pop_draw.c` + `_pop_draw.h`).
|
||
- ⛔ `pop_room_clip_borders()` (`pop_bg.c`) — `gfx_heal(0,0,320,…)`:
|
||
**оставлен клипающим осознанно**. Полоса шириной 320 не лезет в 8-битный
|
||
параметр noclip-ядра, а бить её на два куска по 160 нет смысла: гейт
|
||
`border_dirty` пускает туда только в кадрах падения, и выигрыш подготовки
|
||
тонет в цене самих 320×28 пикселей. Причина записана прямо в коде, чтобы
|
||
не «оптимизировать» повторно.
|
||
|
||
**Что обязано остаться с клипом** (зафиксировано комментариями в коде):
|
||
- кромочные тайлы фона (`blit_b` в `pop_bg.c`) — тайл у края экрана режется
|
||
по построению;
|
||
- спрайты при straddle (`kid_render_dx = ∓140`, комната Кида ≠ отрисованной)
|
||
и при падении ниже поля — фолбэки в `kid_draw`/`pop_guard_draw`;
|
||
- борта поля (`pop_room_clip_borders`) — см. выше.
|
||
|
||
**Что уже было правильно** (шаблон, который и распространили): блиты Кида,
|
||
стража, клинка и брызг спрашивают `pop_onscreen_cols()` и уходят в
|
||
`gfx_blit_cols_part_noclip`. Отдельный случай — `pop_kid_img_blit`: noclip
|
||
БЕЗ проверки, потому что единственный вызывающий (полоса HP) рисует по
|
||
фиксированным координатам; это тоже помечено в коде.
|
||
|
||
---
|
||
|
||
## P1 — сразу после P0 (закрываем уровень 1 как ИГРУ, а не стенд)
|
||
|
||
### L1-START. Старт по данным уровня
|
||
`roomtest.c` жёстко стартует `START_ROOM 1 / COL 3 / ROW 0`, хотя
|
||
`pop_level_start_room()` / `pop_level_start_pos()` / `pop_level_start_dir()`
|
||
в `pop_level.h` уже реализованы и НИКЕМ не вызываются. Перевести старт и
|
||
респавн на них; `#define ROOMNAV` оставить, но выключенным по умолчанию.
|
||
|
||
### L1-EXIT. Выход с уровня (дверь уровня)
|
||
Сейчас: дверь открывается (`animate_leveldoor`), но войти в неё нельзя —
|
||
`up_pressed()` (`pop_ctrl.c:188`) не проверяет `tiles_16_level_door_left`, а
|
||
опкод `0xF1 END_LEVEL` в `play_seq` (`pop_kid.c:418`) пустой. Портировать
|
||
`up_pressed`-ветку + `go_up_leveldoor()` (`seg005.c:410..500`) и завести
|
||
`pop_next_level`, который взводит `END_LEVEL` (`seg006.c:662`). **Это же
|
||
первый шаг плана следующих уровней** — см. `../docs/levels_plan.md`.
|
||
|
||
### L1-TRIAGE. Ревизия багов — **ЧАСТЬ 1 СДЕЛАНА 2026-08-01**
|
||
|
||
✅ **Три Critical'а прогнаны в MAME и закрыты** (протокол с числами — в
|
||
[`bug_closed.md`](bug_closed.md), раздел «Проверено в MAME 2026-08-01»):
|
||
BUG-1 (провал на row 1 при переходе через открытые ворота) и BUG-2
|
||
(ping-pong при возврате) **не воспроизводятся**, BUG-3 (окклюзия climb-up на
|
||
кнопке) закрыт фиксом `tile_code_drawn` от 2026-07-28. Заодно снят неверный
|
||
диагноз BUG-1: репроекция Y при БОКОВОМ переходе — не наш пробел, а точное
|
||
поведение `goto_other_room` (`SDLPoP/src/seg002.c:390` меняет только `x`).
|
||
Список разделён на [`bug_list.md`](bug_list.md) (открытое) и
|
||
[`bug_closed.md`](bug_closed.md) (закрытое + разбор корней).
|
||
|
||
✅ **Косметика окклюзии тоже закрыта** — BUG-CEIL-1/2/3 и BUG-OCCL-1 были
|
||
починены кодом ещё в июле, а записи никто не снял: `ceil_over_kid_tile`
|
||
(`pop_bg.c:1275`), `pop_ceil_modif` + `pop_ceil_shake_draw`/`_bake_empty`
|
||
(`:547,815,827`), `bar` с `POP_YOFF+3` в `pop_room_redraw_seam_left` (`:793`),
|
||
разделение слоёв по `add_backtable` vs `ptr_add_table` в `overlay_mid_tile`
|
||
(`:1367`). Разбор — в [`bug_closed.md`](bug_closed.md).
|
||
|
||
⏳ **Осталось:** закрыть таблицу обхода 24 комнат
|
||
([`bug_list.md`](bug_list.md#обход-всех-24-комнат-уровня-1)) — заполнена на
|
||
5 строк из 24, инструмент (`ROOMNAV`) готов. Делать вместе с L1-PASS: это
|
||
единственный оставшийся источник новых багов уровня 1.
|
||
|
||
### L1-SPEED. Игра идёт быстрее оригинала (найдено 2026-08-01)
|
||
Сверка таймингов: оригинал — `BASE_FPS = 60` при `base_speed = 5` тиков на
|
||
логический кадр (`SDLPoP/src/types.h:1373`, `data.h:869`) = **83.3 мс**, в бою
|
||
`fight_speed = 6` = **100 мс**. У нас `roomtest.c` ждёт **три** `gfx_wait_vsync()`
|
||
= 60 мс, и отдельной скорости боя нет — то есть примерно **+39 % к скорости
|
||
эталона**. Соответствие: 4 ожидания (80 мс) обычно, 5 (100 мс) в бою.
|
||
Условие «делать ПОСЛЕ CLIP-1» **снято** — CLIP-1 закрыт (см. P0). Проверка —
|
||
секундомером по одинаковому отрезку рядом с живым SDLPoP, не «на глаз».
|
||
|
||
### L1-PASS. Сквозное прохождение уровня 1
|
||
От старта до двери уровня одним заходом: подбор меча, страж, кнопки/ворота,
|
||
пики, loose-полы, зелье, падения. Это приёмка этапа 1 и одновременно
|
||
регресс-база для уровня 2.
|
||
|
||
Совмещать с [таблицей обхода 24 комнат](bug_list.md#обход-всех-24-комнат-уровня-1):
|
||
известные баги закрыты, значит новые придут только отсюда. Точки, где стоит
|
||
смотреть внимательно, — уже закрытая косметика окклюзии (потолок при прыжке
|
||
вверх, шов при анимации решётки, грани дальней колонны) и подъём на
|
||
тайл-кнопку: см. оговорку к BUG-3 в [`bug_closed.md`](bug_closed.md).
|
||
|
||
---
|
||
|
||
## Отложено осознанно (не брать, пока не появится причина)
|
||
|
||
- **Звук** (CBL-эффекты, Фаза 5 `PORT_PLAN.md`) — геймплей не блокирует.
|
||
- **Таймер уровня / HUD времени / меню / сохранения** — Фаза 6.
|
||
- **[T-1](bug_list.md#t-1)** (пики: перерисовка по причине) — отдаётся почти
|
||
бесплатно после [T-2](bug_list.md#t-2), отдельно не окупается.
|
||
- **Отключение мыши на время игры** и **замена PRNG** —
|
||
`../docs/ideas_backlog.md` (оба дают доли процента кадра).
|
||
- **OPT-1** (хирургический редрой шва) — решено НЕ делать, стоимость
|
||
транзиентная; разбор в [`bug_closed.md`](bug_closed.md).
|