Симптом (нашёл пользователь сразу после L1-EXIT): при подъёме по лестнице за
дверью уровня силуэт Кида вылезал ПРАВЕЕ правого косяка проёма; по высоте
обрезка была корректна.
Причина — недопортированная половина clip_char (seg006:1231). Для кадров
двери оригинал ставит ДВА клипа, у нас был только первый:
obj_clip_top = leveldoor_ybottom + 1; // было
obj_clip_right = leveldoor_right; // не было
Отдельная ловушка: комментарий в SDLPoP говорит «frames 217..228», а КОД
проверяет >= frame_224_exit_stairs_8, то есть 224..228 — портировано по коду.
Fore-слоем это не лечится: створка и косяк уходят в оригинале целиком в
backtable (draw_leveldoor, все add_backtable), рисуются ПОД персонажем и
перекрыть его не могут. Единственный способ — срезать сам спрайт.
libbgi: gfx_blit_cols_part_w(..., uint8_t maxw) — обрезка СПРАВА у
колоночного блита. Для column-major это ровно уменьшение числа колонок, то
есть внутри ядра механизм уже был (так же клипается край экрана,
w = _bgi_maxx + 1 - x), наружу не выводился. Тело блита переехало туда,
gfx_blit_cols_part стал тонкой обёрткой (maxw=0) — тем же приёмом, каким
gfx_blit_cols уже обёрнут вокруг gfx_blit_cols_part. Работает и при flip:
первые maxw нарисованных колонок всегда ложатся в левую часть футпринта.
make size-check: роста нет.
PoP: pop_leveldoor_right / pop_leveldoor_ybottom (порт одноимённых глобалов)
пишет draw_leveldoor в pop_state — их читает clip_char из другого банка;
pop_clip_char_right() отдаёт границу, kid_draw превращает её в maxw и уводит
эти кадры с noclip-пути на общий. Прямоугольник heal (kid_lw) сужается тоже
— стираем ровно нарисованное.
Проверено в MAME: pop_leveldoor_right = 176, что есть ровно (draw_xh<<3)+48
для двери комнаты 9; pop_leveldoor_ybottom = 112 у закрытой створки и 69 у
поднятой — сходится с формулой оригинала. Отрисовку подтвердил пользователь
на живом подъёме.
Заодно: ROOMNAV остаётся включённым осознанно — это наш чит, которого в
оригинале не было, как и S/K/I; позже сведём в общий блок читов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
39 KiB
roomtest — доска текущих задач (обновлено 2026-08-01)
Не список багов (открытые — bug_list.md, закрытые с разбором
корней — bug_closed.md) и не план фаз
(../docs/PORT_PLAN.md, ../docs/layout_plan_v2.md, ../docs/levels_plan.md),
а то, что берём в работу сейчас и в каком порядке. Каждая запись: что
сделать, почему именно сейчас, чем подтверждать результат.
Открытых багов нет (bug_list.md): остались незакрытые
оптимизации T-1, T-2 и незаконченная
таблица обхода 24 комнат.
Правило проекта в силе: механику сверять с ../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.x114 → 147. Цена:_CODE+170 Б, кадровый бюджет не затронут (опрос стоит в простое). Ниже — полный протокол, как к этому пришли.ОСТАТОК (ручная проверка пользователем, 2026-08-01): «стало значительно лучше, но иногда при зажатом Shift стрелка всё-таки пропускается». Ощущение, не замер — счётчики на 35 нажатиях подряд потерь не показали, значит остаточная частота заметно ниже прежних ~15 %. Задача осознанно ОТЛОЖЕНА до финальной полировки всей программы (решение пользователя); сейчас клавиатура пригодна для работы.
Где именно осталась дыра — чтобы на полировке не начинать с нуля. Idle-хук покрывает простой, то есть ~2/3 кадра. Оставшаяся треть — это занятая фаза, и там DI-окно одного accel-прохода доходит до ~650 мкс при допуске FIFO ~300 мкс. Пачка байт, целиком попавшая в такое окно, всё ещё может потерять байт — ровно «иногда». Порядок действий, если вернёмся:
- Вернуть вызовы
kbd_raw_poll()между блитами занятой фазы (они бесплатны; сами по себе не помогали, но вместе с хуком закрывают именно этот зазор) и при необходимости внутрь тайловых цикловpop_bg— тогда слепым остаётся только тело одного блита.- Мерить тем же счётным методом (см. ниже), а не на ощупь: скриптовые нажатия ровнее человеческих, поэтому набирать выборку от 50 нажатий.
- Если и это не добьёт — остаются два рычага вне нашего кода: Scan Code Set 3 через BIOS
$EA(убирает «fake shift» в корне, но в MAME непроверяемо — обратный путь к клавиатуре не разведён) и общийm_irq_off_timerв драйвере MAME.
Симптом (пользователь, 2026-08-01). Залипаний почти нет, но при УДЕРЖИВАЕМОМ Shift первые один-два нажатия ← дают осторожные шаги, дальше нажатия ← не отрабатываются, пока Shift не отпустишь.
Рабочая гипотеза (механизм, а не догадка «что-то с клавиатурой»). Три известных факта складываются в одну картину:
- PS/2 Set 2, «fake shift». При зажатом Shift нажатие РАСШИРЕННОЙ
клавиши (стрелки —
E0-коды) обрамляется фиктивным отпусканием/нажатием шифта: нажатие ← шлётE0 F0 12+E0 6B= 5 байт (без шифта было бы 2), отпускание —E0 F0 6B+E0 12= 5 байт (было 3). То есть ровно в связке Shift+стрелка трафик удваивается. - Приёмный FIFO SIO — 3 байта. Пачка в 5 байт переживает только то,
что мы успеваем вычерпывать её по ходу. Потерянный make стрелки =
«нажатие не отработало»; потерянный break = залипание (его лечит
kbd_raw_sync, но ценой сброса всех немодификаторных клавиш). - Импульс 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 Гц). - Наши 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 стрелки.
План проверки — по шагам, каждый даёт артефакт:
- Счётчики в MAME: брейк на
_kbdraw_overrun(запись) и на веткеtr_kbd_drain— сколько overrun'ов за 10 с при «Shift зажат, тапаю ←» против «тапаю ← без Shift». Ожидание по гипотезе: с Shift кратно больше. - Замер максимального DI-окна кадра: брейкпоинты на
di/eiв_bgi_blit_cols_raw+{printf totalcycles; g}— получить реальную длину в тактах и в микросекундах. - Спайк «блит без DI».
docs/new/06-accel.md §6.6: новая прошивка допускает работу акселератора при EI (по приходу прерывания он отключается, поRETIвключается обратно); старая — нет. Собрать libbgi с убраннымdiв блит/heal-ядрах, прогнать roomtest в MAME: (а) не рушится ли картинка, (б) падает ли счётчик overrun из п.1. Если да — причина подтверждена, и дальше это вопрос «какая прошивка на живом железе» (по умолчанию оставить DI, режим без DI — опцией libbgi). - Независимо от п.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). - Побочно сюда же играет T-2 (idle-skip): не перерисовывать Кида, пока поза/координаты не менялись, — это минус heal+blit (то есть минус DI-окна) в самых спокойных кадрах, где как раз и тапают Shift+стрелку.
- Только если после 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):
- ✅ 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 — тогда опрос работает исключительно в той ситуации, ради которой заведён. - ✅ Перемерено в roomtest тем же счётным методом: 35/35, потерь нет.
- ⏳ Вызовы в занятую треть кадра НЕ возвращены: одного idle-хука хватило. Держать в уме, если на реальном железе или на более тяжёлых сценах (несколько стражей) потери появятся снова — накрыть блиты дешевле, чем изобретать что-то новое.
- ⏳ Ручная проверка пользователем — без неё этап не закрыт: скриптовые нажатия ровнее человеческих, и «залипания до отпускания Shift» они не воспроизводили с самого начала.
Куда смотреть дальше, если плотного опроса не хватит.
- Драйвер MAME — НЕ ТРОГАЕМ (решение пользователя: пересборка MAME на
его машине занимает часы). Для протокола, подозрение осталось:
sinclair/sprinter.cppдержит запрос от клавиатуры ровно 32 такта CPU, иm_irq_off_timer— один на два источника (irq_on()экрана заводит его же,irq_off()гасит разом обе линии). То есть кадровое прерывание способно обрезать клавиатурный импульс — правдоподобное объяснение «44 % пропущенных импульсов». - Реальное железо. Если п.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 Б суммарно (не плюс!):
_CODE25 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. Старт по данным уровня — СДЕЛАНО 2026-08-01
Старт и оба рестарта (смерть, выпадение из уровня) сведены в один
pop_start_level() — порт start_level + do_startpos + set_start_pos
(seg003): комната/тайл/направление берутся из pop_level_start_*, направление
инвертируется (~start_dir), поза входа — из tbl_entry_pose. У уровня 1
это «падение внутрь» плюс нажатие кнопки room5(0,2) — то самое, что
захлопывает решётку за спиной. Проверено: старт даёт room 1, col 0, падение
на row 1 — как по данным уровня.
#define ROOMNAV оставлен ВКЛЮЧЁННЫМ осознанно: это наш чит, которого в
оригинале не было, — как и S (выдать меч), K, I. Все они со временем
съедутся в общий блок читов, разрешаемый в настройках (решение 2026-08-01).
L1-EXIT. Выход с уровня (дверь уровня) — СДЕЛАНО 2026-08-01
Портированы: ветка двери уровня из up_pressed + go_up_leveldoor
(seg005:0482/0574) — в pop_leveldoor_enter() (pop_map.c, тайлы и
геометрия) и up_pressed() (pop_ctrl.c, только последовательность);
опкод 0xF1 END_LEVEL в play_seq теперь инкрементит pop_next_level
(порт next_level), а главный цикл по нему перезапускает уровень — ровно
та точка, куда levels_plan.md §2.2 подключит загрузку уровня 2.
Открытость двери проверяем по modifier >= 42 (ветка fix_exit_door), а не
по ванильному leveldoor_open: с ванильным условием можно войти в ещё
ползущую створку.
Грабли, которые стоили отдельного разбора: go_up_leveldoor сначала
писал Char.x/Char.direction, и оба присваивания молча терялись — окно
Char вокруг диспетчера возвращает назад ТОЛЬКО curr_seq и sword
(pop_savekid_state). Направление оставалось «вправо», а все DX
последовательности seq_70 отрицательные, поэтому Кид уходил ИЗ проёма влево.
Вывод на будущее: геометрию персонажа в этом порте меняет pop_map (пишет в
Kid), а не диспетчер.
✅ Косметика тоже закрыта: BUG-DOOR-CLIP (обрезка силуэта правым косяком
проёма) — недоставало второй половины clip_char (obj_clip_right) и
обрезки СПРАВА у колоночного блита. В libbgi добавлен
gfx_blit_cols_part_w(..., maxw); разбор — в bug_closed.md.
L1-TRIAGE. Ревизия багов — ЧАСТЬ 1 СДЕЛАНА 2026-08-01
✅ Три Critical'а прогнаны в MAME и закрыты (протокол с числами — в
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_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.
⏳ Осталось: закрыть таблицу обхода 24 комнат
(bug_list.md) — заполнена на
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-3 в bug_closed.md.
Отложено осознанно (не брать, пока не появится причина)
- Звук (CBL-эффекты, Фаза 5
PORT_PLAN.md) — геймплей не блокирует. - Таймер уровня / HUD времени / меню / сохранения — Фаза 6.
- T-1 (пики: перерисовка по причине) — отдаётся почти бесплатно после T-2, отдельно не окупается.
- Отключение мыши на время игры и замена PRNG —
../docs/ideas_backlog.md(оба дают доли процента кадра). - OPT-1 (хирургический редрой шва) — решено НЕ делать, стоимость
транзиентная; разбор в
bug_closed.md.