Звуки 24..43 в наборе оцифровки пустые — в оригинале это Adlib-музыка, и
в digisnd её нет. На этом и построено подключение: pop_sfx_play для звука
с нулевой длиной кладёт номер в pop_mus_req, а разбирает заявку
pop_music_service() — один вызов на кадр из любого цикла (игра, интро,
катсцена). Всё чтение с диска живёт там.
Ждать полной загрузки джингла нельзя — это фриз посреди игры. Поэтому
pop_music_stream читает первую страницу (33 мс) и сразу пускает трек: она
звучит 1,5 с, а следующая читается те же 33 мс. Остальные доливаются по
одной за кадр.
Расставлено по местам оригинала: смерть (24/28/32), вступление первого
уровня и тень шестого (25), сцены перед уровнями (27/35/40), встреча с
Джафаром (29), зелья (30/33/39), время вышло (36), меч и смерть стража
(37), смерть Джафара (43), конец уровня (41/32), встреча с принцессой
(26). Темы «раз за уровень» сбрасывает pop_music_level_start.
Упакован 21 трек (2,4 МБ). Не озвучен только финал: won — 115 с и 78
страниц EMM, ему нужен кольцевой стриминг (задача MUS-WON на доске).
Проверено в MAME: вступительная тема первого уровня отыгрывается на
старте (курсор дошёл до конца, id 25). Host-тесты: 15 наборов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Добавлены треки 54 (тема титров), 50/53/52 (три реплики сцены с
принцессой и Джафаром) — заставка звучит целиком, как в оригинале.
pop_music: два слота EMM. Реплики идут встык, паузы под загрузку нет,
поэтому следующая читается в НЕ играющий слот, а play подменяет
резидентную таблицу страниц. Своей копии таблицы слот не держит — её
хранит блок EMM. Плюс постраничная загрузка (load_begin/load_step):
страница стоит 33 мс, четверть логического кадра сцены, и подкачка
посреди анимации не видна.
Тайминги: все длительности сцен — в тиках оригинала (60 Гц), перевод в
кадры луча делает POP_T60. Пока сцены были немыми, разбег в 20 % был
незаметен; с музыкой реплика кончалась раньше картинки. Переведены
титры, история, хвост после PV и пейсинг самой PV-сцены.
Паузы, которые оригинал отмеряет концом сэмпла, теперь равны реальной
длине наших записей: конец m50 на тике 846, вход Джафара 1046, уход
2073, конец сцены 2500 (было 1959). Fade перед PV сдвинут так, чтобы
полная темнота наступала в тот же момент трека, что у SDLPoP: их fade
длиннее нашего на 123 тика, наш полосовой переход длиннее на 16.
Финальная сцена: убраны песочные часы — end_sequence_anim не трогает
hourglass_state, а reset_cutscene его обнулил. Уровень пройден, время
больше не идёт.
Проверено в MAME: цепочка 54 -> 55 -> 50 -> 53 -> 52 отыгрывается
целиком, интро доходит до демо-режима. Host-тесты: 15 наборов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
menu_settings_plan.md:
- §10 переписан: выбран Вариант A — текстовые строки + собственный
растровый рендерер в новом банке; референс SDLPoP (hc_small_font /
hc_font — один рендерер, два шрифта); шрифт как ассет из паковщика,
прототип MS4 — системный CP866 ZG; двуязычность eng/rus через пару
(таблица строк CP866, файл шрифта)
- Restart Level / Restart Game выполняются сразу, без подтверждения
(§3, §8, из §11 убраны диалоги RESTART *?)
- §13 MS4: текстовый рендерер + два шрифта; §14: host-тест рендерера
quicksave_plan.md: статус «РЕАЛИЗОВАНО и проверено в MAME»
(v0.6-pop-quicksave), документ оставлен справочником по формату 'POPQ'
TASKS_OPEN/TASKS_CLOSED: запись QSAVE переехала в закрытые с полным
протоколом; docs/README.md аннотации обновлены
Замеры по ассетам: 31 эффект уже 8-битным PCM на 11 000 Гц (у CBL есть
10 937,5 — расхождение 0,6 %, формат сэмпла совпадает байт в байт, то
есть конверсии нет вовсе), 103 941 Б = 6,3 EMM-страницы.
Вся музыка есть нотами PC-спикера — 7 КБ на 57 звуков, и нота там задана
в ГЕРЦАХ напрямую (проверено по speaker_callback), а не делителем PIT,
как кажется по числам. MIDI разбирать не нужно.
Отдельный таймер не нужен: секвенсор музыки двигает CBL-callback раз в
11,7 мс, а короче 12 мс во всей музыке 2 ноты из 1469.
Пользователь поймал ошибку в моём расчёте, глядя на экран: страж целиком
правее пламени, пересекаться может только меч.
Проверка по памяти машины подтвердила и уточнила:
страж, спрайт x 257..284 y 18..56
страж, клинок x 241..261 y 31..37
пламя факела x 232..247 y 5..22
Ошибок было две. Первая: координаты пламени я взял по предположению
«факел в колонке 7», а он в колонке 6 (пламя рисуется в ячейке правого
соседа). Вторая, содержательная: ФИЗИЧЕСКОГО ПЕРЕКРЫТИЯ НЕТ ВООБЩЕ — по x
клинок и пламя пересекаются, но по y между ними девять пикселей зазора.
Настоящая причина: pop_cd_touch хранит метку как маску КОЛОНОК по 32 px на
ТРИ ряда по 63 px (cd_row_of). Пламя (y 5..22) и клинок (y 31..37)
попадают в один ряд 0 и одну колонку 7 — cd_quiet считает слот задетым.
148 302 такта, 23 % кадра, за ложную тревогу.
Решение стало проще и точнее: хранить на колонку диапазон y вместо номера
ряда (10 x 2 байта x 2 страницы = 40 байт). Расчётом проверено, что это
спасает стража и НЕ спасает Кида в тяжёлой позиции — там перекрытие
настоящее, и он честно перерисовывается. Вариант с 8-пиксельными полосами
тоже работает, 16-пиксельные уже нет.
Прежние предложения (частичная перерисовка по пересечению, обрезка фона под
персонажем) записаны как НЕ НУЖНЫЕ: они решали задачу, которой нет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Постановка пользователя: проверять, нужна ли отрисовка стража, когда он
не двигается. Если движется — лишние ~150 000 тактов приемлемы: в
оригинале во время боя число физических кадров на логический тоже растёт
на единицу.
Замер чтением pop_cd из памяти машины показал, насколько цена
несоразмерна поводу:
страж x 257..284, y 18..56 28 x 39
пламя (0,7) x 264..279, y 5..22 16 x 18
пересечение x 264..279, y 18..22 16 x 5
То есть пламя задевает страже только макушку — 80 пикселей, — а
перерисовывается он целиком за 148 302 такта (85 524 спрайт с клинком и
снимком + 62 778 fore-проход), это 23 % работы кадра. Пересечение при
этом настоящее: дело не в грубости маски меток, проверено числами.
В задаче записаны два варианта: A — частичная перерисовка только
пересечения (безопаснее, укладывается в контракт pop_cd), B — не рисовать
фон там, где он всё равно перекрыт неподвижным персонажем (дешевле, но
обрезанное пламя попадёт в ОЗУ-копию и heal вернёт дыру, когда персонаж
сдвинется).
Заодно уточнено, чем НЕ является P13 (вопрос пользователя): это не
перерисовка комнаты заново каждый кадр — такой вариант стоил бы порядка
3 000 000 тактов, семь растровых кадров, и оригинал так тоже не делает.
Разница в цене ПОСЕЩЕНИЯ тайла: у нас fore_tile сразу блитит, у оригинала
add_*table только кладёт запись, а рисует один draw_table в конце.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Новая целевая сцена: уровень 11 комната 15 — два факела, чомпер, страж.
В отличие от 13/23 (разовый пик на каскаде плит) здесь дорога САМА
статика: Кид и страж стоят, а кадр стоит 801 768 тактов = 1,86
растрового кадра, период 4 растра во всех 866 интервалах прогона.
Фазы: синяя 293 238 (heal 141 048 + логика 152 190), зелёная 320 916
(loose_tick 28 872 + process_trobs 92 448 + redraw_needed 190 260),
циан 187 758. Впечатление «циан ~150 % кадра» не подтвердилось: за 867
кадров разброс циана 174 такта, это 0,44 растра.
Главная находка — 190 260 тактов на ОДИН тайл (pop_dbg_rdmax_tot = 1).
Пламя факела запекается в ячейке правого соседа, то есть поверх чомпера,
и process_trobs метит соседа (порт set_redraw_anim_right). Оригинал на
такую пометку рисует ТОЛЬКО слой anim, мы же отвечаем heal 32x64 плюс
полный draw_tile — со всеми слоями, которых пламя не касалось.
Заодно разложена цена одного блита фона (брейкпоинты на резидентных
адресах внутри pop_blit_b, temp0 на входе, 1603 блита): фиксированная
накладная 6 126 тактов на ЛЮБОЙ блит — пролог с IX-фреймом 810,
atlas_image 672, w0_map с чтением шапки 2 400, cd_touch 2 069, unmap 175.
У самого дешёвого блита это 59 % цены, у пламени 16x18 пиксели тянут
лишь 12 %. Причины ровно те, на которые указал пользователь:
16-битные аргументы там, где хватает 8 бит, и адресация через IX.
perf_registry.md сводит в один отсортированный список всё отложенное из
perf_green_phase (G1-G9), perf_cyan_phase (C1-C7), perf_backlog (1-7),
HEAL-WIDTH и сегодняшние находки — с пометкой замер/модель/гипотеза.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Уровень 11 комната 14 проверена пользователем визуально после варианта B —
порядок падающего куска, соседней плиты и Кида корректен. На 10 и 12
багов не найдено.
HEAL-WIDTH и G8 сведены как две половины одной темы: G8 про ширину ЗАПЕЧКИ
соседнего тайла (60 вместо 28 нужных, плюс draw_tile соседа дважды на
пометку), HEAL-WIDTH про ширину HEAL'ов (64 вместо фактических 58/57).
Оговорка из G8 перенесена: 60 = 32 свой тайл + 28 собственный свес, для
запечки самого тайла это минимум, сужать можно только пометку СОСЕДА.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
СИМПТОМ (пользователь, ур.11 к.14). Кусок, отвалившийся от плиты нижнего
ряда, рисуется ПОВЕРХ соседней трясущейся плиты; при этом передний торец
соседа лежит поверх куска — порядок противоречив в разных частях
перекрытия.
КОРЕНЬ. draw_mob (seg007:13E5) считает ряд объекта как
y_to_row_mod4(y) = (y+60)/63 % 4 - 1. Из-за % 4 ряд 3 (кусок ушёл ниже
комнаты) и ряд −1 (кусок у потолка) дают ОДНО значение −1. Оригинал
прогоняет его через get_tilepos -> get_tilepos_nominus и получает тайл 30,
а объекты с тайлом 30 рисуются в redraw_needed_tiles ПЕРВЫМИ, до всего
обхода тайлов. Мы же передаём сырой −1 в мид-оверлей как «тайл объекта», и
гейт `row > pop_bg_obj_row` читает его как «объект в последнем ряду обхода»,
то есть «объект поверх всего», и отбрасывает возврат соседа. Один и тот же
−1 у нас значит «сверху», у оригинала — «снизу».
Торец при этом виден потому, что приходит из ДРУГОГО слоя: draw_loose кладёт
loose_fram_bottom в backtable и foretable, минуя ptr_add_table, — тело плиты
обязан вернуть мид-оверлей, а его и выключает гейт.
ЧТО ПРОВЕРЕНО ЗАМЕРОМ (журнал решений в pop_dbg_ovl/pop_dbg_pass, зонды
ВРЕМЕННЫЕ и будут сняты):
- на застывшем кадре: слот draw_y=194, r=−1, rt=2, оверлей позван, внутри
отбрасывается гейтом;
- пропуск оверлея на последнем кадре полёта ЗАКОНЕН: габарит куска уже
ниже габарита тайла, перекрывать нечего;
- в 13/23 кусок перекрывают 2-4 тайла (накопленно за полёт), включая СВОЮ
клетку, — то есть «сосед справа» покрытие не исчерпывает;
- перерисовок тайлов за кадр в 13/23: максимум 6, в покое 0.
ТУПИКИ, чтобы не ходить второй раз. Клип объекта тут ни при чём:
add_mob_to_objtable ставит clip.right = 40, но клип применяется только при
chtab_flip_clip[chtab_id], а для chtab_6_environment там 0 — поле
игнорируется и в оригинале. Пункт MOB-CLIP-RIGHT закрывается как
несуществующий. Добавлять торец плиты в мид-оверлей тоже не надо: в
foretable он уже кладётся из fore_tile.
ВЫБРАН ВАРИАНТ B: классифицировать ряд объекта (вне 0..2 = корзина 30),
отдавать оверлею ориентир «раньше любого тайла» и расширить набор
перекрываемых тайлов на СВОЮ клетку. Переносить проход отрисовки не нужно —
heal при этом не участвует, работа та же (оверлей = два блита в окне клипа),
разница с узким вариантом A всего один-два оверлея на кусок.
Заодно записана ОБЯЗАТЕЛЬНАЯ задача HEAL-WIDTH: ширины точечных heal'ов
взяты по клеткам (64), а фактический след плиты — 58/57 (замерено по
атласам: верх 32, правая грань 26 в подземелье и 25 во дворце).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Уровень 9 (зелье инверсии) — багов не найдено.
Регресс 13/23, 2701 кадр: работа 880 272, синяя 159 822, зелёная 419 562,
циан 380 202 — против 880 170 / 159 774 / 419 520 / 380 244 у 3bcaf51.
Разброс ±100 тактов на 880 000 (0,01 %), циан даже в минус. Период совпал
кадр в кадр: 4 растра в 24 кадрах, 5 в одном. Host-тесты 5106 проверок
без расхождений.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Найдено и закрыто по дороге: GUARD-RESPAWN-COL0 (c40ae3f) — страж при
возврате в комнату телепортировался в колонку 0 и падал насмерть;
SEAM-FIGHT-FLICKER (a498255) — бой у шва перерисовывал комнату
туда-обратно, поведение сверено с SDLPoP покадрово.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Постановка пользователя: ручной проход уровня с логом фаз + комната/тайл,
дальше оптимизация конкретной комнаты. Записано вместе с блокером —
брейкпоинтами это делать нельзя (эмуляция падает в проценты от реального
времени, играть невозможно); варианты: тап на порт бордюра в Lua-мосте
(не привязан к адресам кода) или самозамер программой в растрах (работает
и на железе).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Найдено и закрыто по дороге: SPIKE-BAKED (980d48c), DIED-ON-BUTTON
(6feab5d). Регресс после них: сцена 13/23 (каскад плит) отрабатывает
штатно, host-тесты 5106 проверок без расхождений — включая 1730 трасс
физики, которых касалось снятие раннего выхода для трупа.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Уровень 7, комната 19. Кнопка в (0,5) при нажатии зовёт pop_floor_bake,
а тот перерисовывает и правого соседа — пики (0,6) — в банке, который
пишет в ОЗУ-копию. Кадр брался живой, и выдвинутая пика оставалась в
фоне: дальше heal возвращал её каждый кадр. В (0,7) чисто, потому что
окно клипа запечки кончается на x=219, а колонка 7 начинается с 224.
Флаг pop_t_bake_rest («в фон кладём покой») для этого и был, но читал его
только pop_loose_frame. Кадр пик считался по месту в ЧЕТЫРЁХ слоях.
Добавлен pop_spike_frame — порт get_spike_frame (SDLPoP seg008:08A0),
которого у нас не было, — и все четыре слоя переведены на него; заодно
позу покоя в запечке стал отдавать pop_chomp_pose.
Одна точка лечит все пять путей запечки, включая вход в комнату.
Резидент +28 Б, скорость не затронута.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Обход после оптимизации фаз: третий уровень прошёл без правок — в
отличие от первого и второго, чинить ничего не пришлось.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Пересмотр по вопросу пользователя. Первая редакция плана рекомендовала
EMM-страницу — ошибка: взвешивала скорость и недооценивала главный
сценарий.
EMM-страница не переживает рестарт программы, а именно рестарт — тот
случай, ради которого QuickSave и нужен: сцену каскада плит на 13/23
воспроизводит ТОЛЬКО ESC → запуск заново (perf_l13_room23.md §1). Снимок
в ОЗУ там не помогает вовсе.
Доводы за EMM при перепроверке оказались слабыми: лимит манипуляторов DSS
ни при чём (один файл, гард _fd_guard и так стоит), а экономия на пути к
файлу — одна строка. Разница в скорости некритична: 1,9 КБ на HDD не
заметны на фоне полной перерисовки комнаты при загрузке.
Добавлен шаг QS0 — проверить, что D: вообще пишется из-под MAME: если
образ только на чтение, это меняет весь план, поэтому идёт первым.
Критерий приёмки задачи: сохранить, выйти, запустить заново, загрузить —
и оказаться там же.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Только изучение и план, кода нет.
Первое, что выяснилось: в оригинале 1989 года быстрого сохранения НЕТ
вовсе — это enhancement SDLPoP (seg000.c, USE_QUICKSAVE, F6/F9). Значит
искать в Apple II / MSDOS нечего, и повторяем мы не букву, а устройство.
Что берём у SDLPoP: плоский снимок с ОДНИМ обходом на запись и на чтение
(#define process(x)); совместимость держится строкой версии и ничем больше;
клавиша только взводит флаг, работа идёт между кадрами; состояние отрисовки
не сохраняется вовсе — комната перерисовывается с нуля.
Чем наш случай тяжелее: уровень в EMM-странице, room_modif/trobs — static в
банковом pop_trob.c, ГСЧ у нас ТРИ (pop_t_seed, trob_seed, pop_fight_seed),
и дабл-буфер требует перерисовать после загрузки ОБЕ страницы.
Снимок ≈1,9 КБ, поэтому основной носитель — EMM-страница (мгновенно, мимо
DSS и его лимита манипуляторов), файл вынесен в необязательный шаг QS6.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Обход уровней после оптимизации фаз: уровни 1 и 2 пройдены, критичных
багов не осталось. Найденное по дороге закрыто и перечислено в шапке
TASKS_OPEN.md, чтобы регресс-база была видна одним взглядом.
README раньше читы не перечислял вовсе. Теперь у бессмертия явно записано
главное ограничение — работает ТОЛЬКО с мечом в руке — и почему оно не
косметическое: кадры seq_74 все с мечом, и подмена смерти на эту анимацию
оставляла Кида в стойке при sword == 0, где у control() нет ни одной ветки.
На физику (падения/пики/чомперы) бессмертие действует всегда.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Контрольный замер перед оптимизацией (задача OPT-BLIT требовала начать с
него, а не с правок). Сцена: старт уровня 13, комната 23, шесть плит
сыплются с потолка вразнобой.
Пик логического кадра — 1 437 150 тактов при растровом кадре 430 000, то
есть 5-6 растровых кадров вместо трёх. По секциям (бюджет 400 000):
синяя 142 830 (в норме), зелёная до 805 000, циан до 632 000.
Что нашлось (всё подтверждено зондами, не гипотезы):
RDA_CEIL дрожащая плита-потолок 48 658 x до 6 = 292 000
RDA_CEIL_GONE запечь колодец 251 023 x до 2 = 619 000
RD_FLOOR щебень на месте посадки 198 259 x до 2 = 397 000
Внутри RDA_CEIL: heal 8 491 + draw_tile 39 029, а блитов в этом draw_tile
всего 1,17 на тайл (15-16 тыс.) — то есть ~23 700 тактов чистых накладных.
Подсчёт asm сходится до процентов: у draw_tile 26-байтовый стековый кадр и
513 обращений (ix), при ~46 замеренных тактах на обращение это те же
~23 600. У blit_b_clip — 22 Б кадра и 211 (ix).
Разбор одного pop_blit_b (157 замеров быстрого пути): atlas_image+w0_map
1 086, ядро libbgi 10 422, pop_cd_touch 4 502 (28%!), unmap+возврат 264.
В mob_render эти три pop_cd_touch к тому же ИЗБЫТОЧНЫ — коридор куска уже
помечен одним вызовом в pop_loose_mob_tick: 81 000 тактов в кадре впустую.
Итог по пиковому кадру: блиты 488 100 (34%), из них «железный» минимум
пикселей ~150 000; остальные ~1 150 000 (80% кадра) — наши накладные.
Узкое место не передача пикселей, а 16-битная арифметика в IX-кадрах.
Ответ на вопрос про uint8_t: просканированы все 109 .atl и наборы
TITLE/PV — максимум по игровому кадру 56x63 (kid3), фон 48x63, спрайты
комнаты принцессы 49x60. Шире 255 только восемь полноэкранных подложек
титров/сюжета (три 320x200 и пять 256..272), и все рисуются раз на экран.
Значит горячий путь переводится на байтовые габариты целиком, а подложкам —
отдельный банк / прямой libbgi (решение пользователя).
Документы (решение пользователя — вести работу по фазам, чтобы результаты
не теряись между сессиями):
docs/perf_l13_room23.md сцена, рецепт воспроизведения, зонды, канал clog,
сводка по кадрам, габариты спрайтов
docs/perf_green_phase.md зелёная фаза: раскладка, позиции G1..G6, журнал
docs/perf_cyan_phase.md циан: раскладка, позиции C1..C7, журнал
Отдельно записан рецепт воспроизведения: сцена повторяется ТОЛЬКО
перезапуском программы (ESC → зонды → roomtest). Возврат в комнату не
годится — упавшая плита уходит в страницу уровня насовсем; рестарт уровня
не годится — гряда доваливается заочно через trob комнаты 17, пока телепорт
уносит Кида; запись отладчиком на бегущей машине затирается тем же trob.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Точки на цепочку вызовов (libbgi живёт в резиденте W1, адреса однозначны —
пересборка не нужна) разложили постоянные накладные блита:
pop_blit_b ДО вызова ядра ....... 6 683 <- НАШ код, 77% накладных
gfx_blit_noclip + _bgi_begin
+ _gfx_blit_sprite_noclip ..... 1 938
пролог _bgi_blit_rows_raw ....... 457
строчный цикл ................... 389/строку (= 198 + 5,96*32, сходится
с регрессией)
эпилог + _bgi_end + возврат ..... 1 355
«вне цикла» ..................... 8 725 при ЛЮБОЙ высоте (h=9..60)
Причина в pop_blit_b, подтверждена чтением .asm: `w`/`h` объявлены
uint16_t, 16-битные значения не влезли в регистры, и SDCC увёл функцию в
14-байтовый стековый кадр (`ld iy,#-14 / add iy,sp / ld sp,iy`), после чего
`w = img[0] | (img[1] << 8)` развернулось в ДВА ДЕСЯТКА IX-относительных
пересылок между ячейками -7..-13 кадра.
Правка: габарит читается БАЙТАМИ. Корректно по построению — обе ветки и
так требовали w<256 && h<256 (эти проверки теперь убраны как тождественные),
а кадры атласов не крупнее 32x63; формат .atl допускает больше, такой кадр
уходит на общий путь (blit_b_oversize).
Замер A/B на той же детерминированной сцене, те же выборки:
gfx_blit_noclip 13 679 -> 11 530 (-16%)
blit_b_clip 18 853 -> 15 340 (-19%)
Стековый кадр 14 -> 6 байт, _CODE -42 Б. Тайл ~107 000 -> ~95 000.
Все 8 наборов tests-host проходят, комната 23 в MAME рисуется корректно.
Плюс TASKS_OPEN.md: OPT-BLIT — руководство на следующую сессию (где ещё
uint16->uint8, как искать IX-спиллы по asm, что НЕ делать, и грабли с
несколькими экземплярами MAME на один error.log).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Замер зелёного блока (комната 23 уровня 13, MAME, такты эмулятора) показал,
что 86% его стоимости в ПОКОЕ — это pop_loose_tick, который не делает ничего.
Причина — кодоген SDCC, подтверждена чтением .asm. С `int pos` и записью
pop_loose_modif[pos] компилятор держал счётчик в IX-фрейме, каждую итерацию
заново складывал 16-битный адрес элемента, клал его в локал и тут же
вычитывал обратно парами `pop bc / pop hl / push hl / push bc`. 40 холостых
итераций (30 тайлов + 10 потолков) стоили 27 936 тактов. То же в
pop_loose_mob_tick: запись mobs[i] заставляла умножать i на sizeof(mob_t)=15
заново под КАЖДОЕ поле (.active/.clean/.x/.y), 14 пустых слотов — 35 790.
Правка — обход указателем, счётчик uint8_t, пустые слоты отсеиваются в
вызывающем цикле (а не гардом внутри mob_tick_one, до которого надо ещё
дойти). Холостая итерация стала `ld a,(de) / or a,a / jp Z` — три
инструкции вместо дюжины с обращениями к памяти.
Результат (такты MAME, холостой кадр):
циклы по тайлам 27 936 -> 9 852 (2,8x)
pop_loose_mob_tick 35 790 -> 7 416 (4,8x)
pop_loose_tick 70 866 -> 24 384 (2,9x)
ЗЕЛЁНЫЙ БЛОК 82 242 -> 35 760 (2,3x)
Банки ужались: BANK3 -90 Б, BANK7 -74 Б.
Все 8 наборов tests-host проходят; комната 23 в MAME рисуется корректно.
Плюс ВРЕМЕННАЯ оснастка замера (маркеры m9..m16, pop_dbg_kind) — она же
показала, что пик зелёного блока сидит НЕ в тряске плит, а в запекании
тайлов; разбор продолжается, оснастку снять перед закрытием темы.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Уровень 12 (тень) — порт seg002/seg006:
- check_shadow: подъём тени в комнате 15 по условию «меч подобран»,
init_shad_12, вход падением (seq 7);
- autocontrol_shadow_level12: ждать Кида, бой, сближение, СЛИЯНИЕ;
- общий урон (ранил тень — ранил себя) и check_killed_shadow
(убил тень — убил себя);
- таймер вспышки слияния 42 -> -1, sword_disappears при уходе из
комнаты 18, появление плит в комнатах 2/13 после слияния.
Переход 12 -> 13 бесшовный: двери у уровня нет, он кончается фактом
попадания в комнату 23 (tbl_seamless_exit); флаг pop_seamless не даёт
сбросить HP. Чит навигации по комнатам этот триггер придерживает —
иначе комнату 23 двенадцатого уровня не посмотреть в принципе.
Уровень 13 (Джафар): guard_notice_timer (фора после встречи),
on_guard_killed + Jaffar_exit, check_fall_flo (гряда плит сверху) и три
исключения loose-механики. Плюс спрайты визиря VIZIER.DAT — без них он
рисовался обычным стражем; заодно цвет из данных комнаты применяется
только к обычному стражу, как в оригинале.
Падающие плиты — семь дефектов, найденных прогонами:
- MOB_MAX 4 -> 14: check_fall_flo роняет шесть плит разом, лишние
терялись без щебня;
- одиночные сигналы посадки/провала больше не затираются в кадре;
- честный loose_fall: сбитая плита рождается в том же кадре, от места
удара, с половинной скоростью;
- при снятии плиты метится и сосед справа (висел передний торец);
- heal и отрисовка кусков разнесены на два прохода;
- куски рисуются ПОСЛЕ фона, порядок между собой — по убыванию y
(compare_curr_objs для пары 0x80 сортирует наоборот);
- wall_pattern убран из ceil_over_kid_tile: у оригинала узор из
draw_tile_bottom идёт в фон, а не в передний слой.
Хост-тесты: два новых набора, t_shadow (45 проверок) и t_jaffar (44).
TK_ROOMS 8 -> 24 — спецсобытия адресуют комнаты по реальным номерам.
Осознанные расхождения (docs/impl_diff.md): нет мигания Кида спрайтами
тени при слиянии, чит навигации не запускает бесшовный переход, чит K
убивает через штатный путь смерти.
Открытым остаётся разбор слоёв: MOB-CLIP-RIGHT, MID-OVERLAY-LAYER,
GUARD-FALLOUT-VICTORY, BG-ONCE (BUGS_OPEN.md / TASKS_OPEN.md).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Вход в комнату: одна отрисовка в скрытую страницу, показ флипом, вторая
страница — gfx_copy_page(DIRECT) вместо повторной отрисовки. Процесс
рисования тайлов больше не виден, обе страницы синхронны (два снимка подряд
идентичны). Видимую страницу главный цикл теперь перечитывает у железа в
начале кадра: её меняют и вход в комнату, и переворот — оба из банка.
MEM-COLD2 п.2: guard_over_kid с хелперами (objtile_at_char/char_x_left_of/
tile_div_mod) — в банк 8. Куча 1005 -> 1574 Б, цена — один трамплин на кадр.
tests-host 6/6, size-check OK, в MAME проверены старт уровня 9, переходы
между комнатами и отрисовка стража.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Разведка железа первым шагом (смена Port_Y между burst-триггерами и
адресация двух страниц в W3), ядро копии с реверсом Y, vflip-блиты для
row-major, gfx_copy_rect экран-экран; в приложении — состояние, единая точка
пересчёта Y, фон через pop_tile.c, зеркальные кадры персонажей, клип.
Побочной задачей ENTER-ROOM-FAST на том же кирпиче.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Момент переключения зелья переворота делаем не перерисовкой комнаты, а двумя
построчными проходами акселератора (A->B с реверсом Y, B->A без): accel
читает ОЗУ-копию, поэтому копируется чистый фон и ложится и в видео, и в
копию приёмника — heal остаётся без изменений. Бюджет 768 burst-строк
(0.2-0.3 логического кадра) против почти миллиона тактов у перерисовки.
Отдельным пунктом: деления HP переезжают из column-major атласов в
const-массивы резидента, что убирает pop_kid_img_blit (334 Б) и делает
страницы kid27/g0 однородными для кэша перевёрнутых кадров.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Уровни 10 и 11 не приносят ни новых тайлов, ни спецсобытий (зелья только
heal / +max HP). Уровень 9 — зелья типа 4 (переворот) в комнатах 7 и 10.
Записан разбор механики оригинала и разбор цены переворота для наших двух
раскладок спрайтов (row-major бесплатно, column-major требует копий).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Спецсобытие уровня 8 (SDLPoP seg003:0545/0ABA, seg002:07EB): когда дверь
уровня открыта, а Кид остаётся в комнате 16, через 150 кадров приходит мышь,
пробегает справа налево по верхнему ряду, наступает на кнопку (0,7) —
решётка (0,3) открывается — и убегает.
Ассетов не потребовалось: кадры 186..188 идут по таблице Кида, их спрайты
(images 130..132) уже лежат в kid16.atl, отрисовка подхватывает мышь сама.
- guards.c: pop_check_mouse, autocontrol_mouse, ветки мыши в
autocontrol_opponent / play_guard / check_can_guard_see_kid;
- pop_leveldoor_open стал word, как в оригинале (в него же считается
задержка — байт переполнился бы через 255 кадров);
- leave_guard больше не записывает тень и мышь в данные уровня (seg002:02F5);
- полосы HP у мыши нет (seg000:1159);
- tests-host/t_mouse: 17 проверок, единственный набор с guards.c.
Разгрузка резидента (куча в huge упала до 879 Б): pop_start_level,
find_start_level_door, чит-навигация по комнатам и лейбл номера уехали в
roomtest_cold.c (--bank 8, n_banks = 8). _CODE 25653 -> 24825, куча -> 1707 Б;
кадровый путь не тронут.
Проверено: tests-host 6/6, make size-check OK, живьём в MAME (мышь появилась,
нажала кнопку, решётка поехала вверх, мышь ушла).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L7-FEATHER: тип 3 (уровень 7, комната 1) больше не пустой TODO.
- pop_map.c: pop_feather — счётчик кадров эффекта (POP_FEATHER_FRAMES = 225,
ванильный порог do_timers seg003:0517; привязку к звуку взять неоткуда).
fall_accel — ускорение 1 / потолок 4 (seg006:057C) вместо 3 / 33.
proc_get_object case 3: взвести эффект + зелёная вспышка на 3 кадра.
- pop_kid.c: опкод JMP_IF_FEATHER (0xF7) больше не пропускает адрес
безусловно — под пером прыгает по нему, то есть seqtbl уводит падение и
удар в ветки stepfloat/bumpfloat (плавные кадры, без урона).
- roomtest.c: pop_flash_red -> pop_flash_color (жёлтый/красный/ЗЕЛЁНЫЙ);
сброс pop_feather в pop_start_level (seg003:189).
- Цвет пузырька по ТИПУ зелья (seg008:652), чего у нас не было вовсе:
3/4 зелёный, 5/6 СИНИЙ, остальные красный. Mono-блиттера с параметром
цвета в libbgi нет, поэтому цвет запекается при упаковке: pop_pack_bg.py
кладёт те же 7 кадров ещё дважды (id 30..36 зелёные, 40..46 синие),
pop_potion_draw выбирает набор. Атлас 23 -> 37 спрайтов (+423 Б).
- Синее зелье «−HP» приведено к оригиналу (seg006:1892): своей вспышки не
ставит (красный кадр даёт общий flash_if_hurt — иначе экран красился
дважды), а на уровне зелий забирает ПОЛОВИНУ запаса HP.
Такое зелье стоит уже на пройденном уровне 2 (комната 13, тайл (1,3)),
а также ур.8 комн.2 и весь ур.15 — до сих пор оно было красным.
Тесты: phys_feather_fall_is_slow_and_harmless (обычное падение с двух рядов
разгоняется и стоит HP, под пером скорость <= 4 и HP целое); tests-host
5/5 (1727 в [phys]), size-check OK.
Расхождения с ванилью — в docs/impl_diff.md (перо ловит только Кида;
синее зелье без своей вспышки).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Уровень 7, комната 14: спуск с ряда 0 на ряд 2 через зацеп (повис на кромке
кнопки (0,2), отпустил, взялся за (2,2)) не работал ни при какой фазе и
задержке — Кид пролетал мимо и разгонялся до fall_y = 33, после чего окно
зацепа закрыто уже по скорости.
Корень: do_fall не давал curr_row выйти за 2, а check_grab целится в тайл
ряда curr_row-1 — то есть ряд 2 своей комнаты становится целью только при
curr_row == 3. В оригинале (seg005:0030) inc_curr_row безусловный, а
get_tile для ряда 3 уходит по links.down (find_room_of_tile, seg006:005D).
- pop_map.c do_fall: inc_curr_row теперь 2 -> 3; ветка «достиг y_land»
гейтится по curr_row <= 2 (при ряде 3 ни in_wall, ни land звать нельзя —
тайлов своей комнаты там нет, а до y_land[4] дело не доходит: комнату
меняет check_leave_below на y >= 211).
- pop_map.c check_action: восстановлена ветка ACT_MIDAIR (кадры 102..105,
seg006:0619) — первые четыре кадра падения, где fall_y ещё не разогнан,
зацеп не работал вовсе. Отсюда же «иногда цепляется, иногда нет».
- tests-host/t_grab.c: регресс grab_below_room_edge_window_exists — сцена
комнаты 14 + переход в 15 по pop_fell_out. До фикса ни одного зацепа,
после — окно из 8 фаз X при задержках 0..5 кадров; проверяется и
играбельность (зацеп при «отпустил и сразу зажал Shift»).
- roomtest.c + pop_guard.h: чит-навигация ставит Кида на верхний ряд в
комнате 14 уровня 7 (GRAB_DOWN_LEVEL/ROOM) — снизу этот спуск не проверить.
Общее правило (снизу вверх) не тронуто: в верхнем ряду пола чаще нет и Кид
проваливается сразу после телепорта.
Проверено: tests-host 5/5 (55 в [grab]), size-check OK, живьём в MAME —
Кид повис на кромке (2,2): frame=91 y=55 row=0 room=15.
Доски: GRAB-BELOW-ROOM в BUGS_CLOSED, GRAB-KBD-TIMING в BUGS_OPEN (отложен
пользователем до готовности всех уровней), план L7-FEATHER в TASKS_OPEN.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. L6-SHADOW (seg002:0090 + seg002:1064). Тень встаёт при каждом входе в
комнату 1 уровня 6 и делает ОДИН осторожный шаг (Shift+вперёд) в тот
кадр, когда Кид прыгает к решётке (Kid.frame == 43, Kid.x < 128): встаёт
на closer (1,1) и роняет решётку (1,2), за порожек которой Кид цепляется.
do_init_shad переписан под таблицы init_shad_5/6 — первые 7 полей Char,
как memcpy(&Char, source, 7) у оригинала.
Проверено в MAME: «Кид прыгнул, зацепился, Тень сделал шаг, решётка
упала». Тем самым закрыт остаток GUARD-PHYS — нажатие плиты НЕ-Кидом
работает.
2. BUG-BELOWROW-WALL. При отсутствующей комнате снизу load_rowbelow
(seg008:368) подставляет tiles_0_empty колонкам 1..9 и tiles_20_wall
только левому краю; у нас стеной забивались все 11 позиций, и её
topright лез жёлтыми треугольниками под пустые тайлы нижнего ряда
(видно в шахте падения, комната 3).
Комнаты 1 и 3 уровня 6 сверены с картой SDLPoP: расхождения только силуэты
персонажей. tests-host 5/5, size-check чист.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порт seg002: check_shadow (0064), do_init_shad (0000), do_auto_moves (1089),
autocontrol_shadow_level5 (1157) + таблица shad_drink_move (data.h:866).
Механика. При входе в комнату 24 уровня 5, если зелье (кол 3, ряд 0) ещё
на месте, слот соперника занимает ТЕНЬ — и занимает его ВМЕСТО стража:
enter_guard в этой комнате оригинал не зовёт НИКОГДА (в данных страж есть,
guards_tile[23] = 8, но check_shadow уходит в return до него — в том числе
когда зелье уже выпито). Поэтому pop_check_shadow вернула 1 = «событие
обработано, стража не поднимать», и вызов стоит ПЕРЕД pop_guard_enter.
Тень стоит, пока не откроются ворота комнаты (openness >= 80), затем
проигрывает ЗАПИСЬ ходов: подойти, взять, выпить, развернуться, уйти за
левый край. do_auto_moves — тот же движок, что у демо-режима заставки;
тонкость выбора записи (при достигнутом времени индекс сдвигается, а ход
берётся из ещё не сдвинутой записи) портирована дословно.
Три вещи, найденные по дороге:
1. ГЕЙТ ВЫКЛЮЧЕННОГО ПЕРСОНАЖА. play_guard_frame (seg000:1248) начинается
с `if (Guard.direction != dir_56_none)`, и clear_char выключает
персонажа именно так — он не трогает charid. У нас гейт был только по
charid, поэтому ушедшая за край тень продолжала тикать: do_auto_moves за
концом таблицы отдаёт последнюю запись {0x31,1} = «вперёд», тень
разворачивалась, вбегала обратно, пробегала верхний ряд и падала в проём
(наблюдение пользователя в MAME). Гейт добавлен и в pop_guard_tick, и в
pop_guard_phys_tick.
2. ЭФФЕКТ ЗЕЛЬЯ — ТОЛЬКО КИДУ. proc_get_object (seg006:16CB) начинается с
`if (Char.charid != charid_0_kid || ...) return`: предмет забирает любой
персонаж, а эффект достаётся только Киду. На этом стоит всё спецсобытие
— тень пьёт, зелье исчезает и не достаётся никому. Без проверки
hitp_delta уходил бы Киду, то есть тень его ЛЕЧИЛА бы.
3. ВЕТКА ТЕНИ в check_guard_fallout (seg002:0241): исчезает, только если
реально падает (action == 4). Была помечена в коде как «появится с
L3-SKEL» — на самом деле она про тень.
Чит ROOMNAV: телепорт ставил Кида в первый попавшийся пол, то есть почти
всегда (0,0). Теперь ищет пол С МАКСИМАЛЬНЫМ Y (ряд 2 -> 1 -> 0, по
просьбе пользователя): в комнате 24 старая посадка была вплотную к тени, и
вместо сцены кражи начиналась схватка, которой там быть не должно.
Отладочный тумблер POP_DBG_SHADOW_NOWAIT (pop_tune.h, по умолчанию 0) —
посмотреть сцену, не проходя уровень до кнопки.
Проверено в MAME (уровень 5, комната 24): при закрытых воротах тень стоит
за левым краем и не видна; с тумблером — подошла, выпила (зелье исчезло),
развернулась, ушла за край, выключилась и НЕ вернулась. tests-host 5/5,
make size-check OK. Цена: резидент +102 Б (24637 -> 24739), банк 1
+368 Б, банк 3 +11 Б.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Доска отставала от кода на три задачи — планировать по ней было нельзя.
Сверка проведена грепом по исходникам, а не по записям:
- L4-MIRROR ЗАКРЫТА: шаги 1-5 сделаны и проверены пользователем в MAME
(зеркало в атласе, постановка тайла, отражение, прыжок сквозь зеркало с
рождением тени, левый клип тени). Протокол с разбором решений — в архиве;
- L3-CHOMP и L3-SKEL закрыты ещё 2026-08-08/07 (коммиты dc0bd47, 4d4323f,
db4106a, 1461ed5), на доске значились как предстоящие;
- тайлсет palace (шаг 2 levels_plan) в коде есть целиком — pop_bg_load(type),
pal_*.atl, дворцовый wall_pattern, решётки 25-29 и в tile_table, и в
коллизии (tile_is_floor совпадает с seg006:0628);
- в GUARD-PHYS остаток пересобран по факту: check_chomped_guard сделан,
скелет в check_guard_fallout сделан, ветки ТЕНИ нет — она уехала в L5-SHADOW.
Приёмки: по решению пользователя уровни 1-4 приняты SMOKE-тестами, полные
обходы всех комнат делаются по готовности ВСЕХ уровней — L3-PASS/L4-PASS как
отдельные задачи отменены, вместо них политика приёмок в архиве.
Новая цель — уровень 5. Инвентарь res2005.bin: НИ ОДНОГО нового тайла, всё
портировано на уровнях 1-4. Единственная новая механика — спецсобытие «тень
крадёт зелье» (комната 24): заведена задача L5-SHADOW с портом по SDLPoP
(check_shadow / do_init_shad / do_auto_moves + shad_drink_move /
autocontrol_shadow_level5 + ветка тени в check_guard_fallout), включая
готовые константы и то, что у нас уже есть под это.
Заведён MIRROR-FG-STALE (низкий): place_mirror пишет тайл в данные уровня, но
не в снимок room_fg, по которому работает коллизия — если зеркало поставлено,
пока игрок В комнате 4, оно невидимо для коллизии (тень не родится). В
обычном прохождении недостижимо: дверь выхода в другой комнате. Записан
точный сценарий воспроизведения читом ROOMNAV и фикс на несколько строк.
Правило «в _OPEN только незакрытое» теперь выполняется буквально:
- bug_list.md → BUGS_OPEN.md, bug_closed.md → BUGS_CLOSED.md (ссылки
обновлены во всех документах и в комментарии pop_trob.c);
- из TASKS_OPEN убраны блоки закрытых задач (L3-CHOMP, L3-SKEL, L3-PASS,
L4-MIRROR, DRAW-CHAR), справка по связности комнат уехала в архив;
- из BUGS_OPEN убраны 8 строк таблицы закрытых багов, закрытый T-2 (уехал в
BUGS_CLOSED) и раздел «уровень 3 — неначатые задачи» (обе записи закрыты);
сводная таблица пересобрана по реально открытым записям.
Все внутренние ссылки проверены скриптом: битых якорей 0. make size-check
OK (65 программ), tests-host 5/5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
NOT. У акселератора нет режима «инвертировать буфер»: буфер меняется
только на ЧТЕНИИ и только опкодами AND/OR/XOR (HL) (драйвер MAME,
update_accel_buffer), а `CPL` автомат вообще не распознаёт — инвертируется
регистр CPU, не буфер. Зато ~src = src XOR #FF, поэтому NOT собирается из
уже имеющегося: буфер := src, XOR-burst по константному блоку единиц
(common/_bgi_ones256.c), запись. Ядра _bgi_blit_{rows,cols}_not_raw.c;
между burst'ами меняется HL, поэтому каждая смена — под СТОПом (иначе fetch
операнда перезапустит burst). В колоночном варианте второй OUT Port_Y не
нужен: op-burst идёт по блоку единиц горизонтально, а Port_Y шагает только
вертикальный.
Наружу — тем же op-параметром (GFX_OP_NOT), диспетчер один раз на вызов, в
цикл по полосам/колонкам не заходит. putimage лишился попиксельного пути
ЦЕЛИКОМ: все пять операций BGI идут через акселератор и клиппируются
одинаково. tests/accop дополнен T9/T10 (NOT строками и колонками) —
10/10 PASS в MAME; tests/bgi_img P1/P2 по-прежнему PASS.
tests/convbench (новый) — замер побайтной конвертации атласа
«прозрачный #FF -> 0x00» (источник для XOR-блита): 145.3 такта/байт, то
есть 2.5× от 59 номинальных T-states цикла. 0.11 с на страницу 16 КБ,
3.2 с на все 28 страниц, 1.3 с по фактическому объёму данных (186 КБ).
applications/PoP/docs/shadow_render.md — вид Тени (два блиттера OR+XOR)
ОТЛОЖЕН по решению пользователя: пока рисуем обычной копией атласами Кида.
В документе собрано, почему в лоб не выходит (XOR несовместим с
прозрачностью #FF; операция читает ОЗУ-копию, поэтому два прохода
оригинала вырождаются в один XOR — нужен однопроходный композит
s | bg ^ s(сдвиг)), замер стоимости источника и четыре варианта. Ключ к
выбору — список кадров, которыми тень реально пользуется; снимать по факту,
когда пойдут уровни 5/6/12.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порт seg003:0798..08A9 + seg004:0239 + seg002:081D/1131 + seg006:1945.
- is_obstacle (pop_map.c): ветка зеркала — Кид, кадры бегового прыжка
39..43, направление ВЛЕВО -> modif = 0x56, pop_jumped_mirror = -1,
препятствия нет (пролетает насквозь).
- mirror_image / jump_through_mirror / pop_check_mirror (pop_map.c):
отражённый Char уходит в слот Guard как CHARID_1_SHADOW, guardhp =
hitp_max, у Кида hitp_curr = 1. savekid НЕ делается — как в оригинале,
отражается только копия. Полосы HP перерисует pop_hp_draw сам.
- pop_check_mirror() зовётся из главного цикла ПЕРЕД отрисовкой персонажей
(в оригинале — первая строка draw_people, seg008:228A).
- autocontrol_shadow + autocontrol_shadow_level4 + clear_char (guards.c):
тень идёт СВОЕЙ веткой целиком, к стражьему ИИ не сводится — на уровне 4
она не дерётся, а бежит влево и при x < 80 исчезает.
АТЛАС ТЕНИ — вскрылось при чтении seg006:0532. Тень вне боевых кадров
150..189 ходит по таблице КИДА, и image оттуда индексирует спрайты Кида,
а не стража. Выбор атласа в pop_cdraw шёл по СЛОТУ, то есть тень
рисовалась бы спрайтами стража. Условие вынесено в
pop_frame_tbl_is_guard() (pop_kid.c) — его теперь читают и load_frame, и
отрисовка, разъехаться не могут. Заодно из pop_load_fram_det_col выделен
pop_load_frame() без determine_col: jump_through_mirror берёт ось
отражения из curr_col, и пересчёт колонки по x там был бы вреден.
Константы MIRROR_* и DIR_56_NONE переехали в pop_guard.h — нужны и
постановке тайла (банк 6), и ИИ тени (банк 1).
Звука sound_45_jump_through_mirror в порте нет, пропущен.
Шаг 5 (клип тени слева от зеркала) НЕ сделан и оказался не однострочником:
в pop_cdraw есть клип сверху/снизу/справа, левого нет вовсе — нужен новый
примитив либо срез исходных колонок. Расписано в TASKS_OPEN.
Цена: _CODE +18 Б, банк 1 2311 -> 2367, банк 3 10328 -> 10551,
банк 4 8243 -> 8267. tests-host: все 5 наборов прошли.
НЕ ПРОВЕРЕНО В MAME — сценарий проверки записан в TASKS_OPEN.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Задача заведена активной на доске по решению пользователя (palace был
отложен 2026-08-04, но уровень 4 уже гоняется в MAME и зеркало —
единственное, что мешает пройти его сюжетно). Механика целиком сверена по
SDLPoP, таблица соответствий в TASKS_OPEN.
Шаг 1 — АТЛАС. tile_table[0x0D] = база 75, фронт 77 (наша таблица
совпадает с SDLPoP байт в байт). Тайла 13 НЕТ НИ В ОДНОМ уровне
статически: перебор всех 15 res200N.bin даёт ноль попаданий (санити
разбора: ур.1 без чомпера, ур.3 с 18, ур.4 с паласными 25..29). Значит
render_room эти id не увидит и на месте зеркала был бы чёрный провал —
грабли memory pop_atlas_dynamic_ids. Добавлены MIRROR_ENV_IDS = {75,77} в
pop_pack_bg.py, 77 ещё и в FORE_ENV_IDS. Оба набора переупакованы:
fore 17 -> 18 спрайтов, все страницы EMM в пределах 16 КБ.
Шаг 2 — ПОСТАНОВКА. place_mirror() в pop_trob.c по переходу
pop_leveldoor_open 0/2 -> 1 (условие оригинала, seg007:0457 — иначе тайл
ставился бы заново каждый кадр открытой двери). Пишет тайл 13 в комнату 4,
колонку 4, ряд 0; если комната уже на экране — POP_RD_FLOOR на обе
страницы. Банк 6 3450 -> 3518.
НЕ ПРОВЕРЕНО В MAME: нужно нажать плиту выхода на уровне 4 и дойти до
комнаты 4. Отдельный вопрос к проверке — не устареет ли g_fg коллизии,
если игрок окажется в комнате 4 в момент постановки.
Дальше по плану: 4 (прыжок сквозь + рождение тени), 5 (клип тени),
3 (отражение — косметика, самое дорогое).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Обе сборки прогнаны полным циклом (make hdd -> рестарт MAME -> уровень 1),
сцена «комната 1, Кид стоит, соперника нет», скриншоты идентичны. Фазы
сняты брейкпоинтами на out (_io_border), a, медиана по 60 кадрам:
спрайты 129 568 -> 127 510 (-2 058)
работа/кадр 391 258 -> 389 221 (-2 037)
остальные фазы совпали такт в такт
Счётчик делений (bp на __divsint/__modsint/__divuchar/__moduchar с
печатью адреса возврата):
комната 1, только Кид : 1,00 __divsint/кадр (возврат 0xD3AA = pop_cdraw) -> 0
комната 3, бой стража : 2,01 __divsint/кадр -> 0
Экономия ровно в фазе спрайтов и ровно на стоимость одного вызова
(2 058 тактов против документированной оценки ~2 400).
Честные оговорки записаны в TASKS_OPEN: период цикла как был 3 растровых
кадра, так и остался (выигрыш ушёл в запас, 40 800 вместо 38 700);
остальные правки в этой сцене не срабатывают; тайминги комнаты 3
несравнимы между прогонами (живой страж + pop_char_skip_mask), оттуда взят
только счётчик делений.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Гипотеза «двухбайтная индексация съест выигрыш от сложения» не
подтвердилась. Собраны ОБА варианта, такты посчитаны по сгенерированному
asm (хвост после проверки границ):
int16_t готовое: add hl,hl / add hl,de / ld e,(hl) / inc hl / ld d,(hl)
= 132 такта, 2 304 байта
int8_t x/7: add hl,bc / ld a,(hl) / ld l,a / rlca / sbc a,a /
ld h,a / add hl,de = 131 такт, 1 152 байта
Расширение знака плюс 16-битное сложение стоят ровно столько же, сколько
лишний add hl,hl при двухбайтном индексе, а обращений к памяти на одно
меньше — под wait-state'ами Sprinter (2,4x номинала именно на обращениях
к ОЗУ) байтовый вариант ещё чуть выгоднее номинала.
Банк 4: 9 394 -> 8 243 из 16 384 (свободно 8 141 вместо 6 990) — запас под
рост pop_cdraw, о котором и был вопрос.
Тест переименован в geom_mul8div7_table_rules и проверяет ОБА правила
генерации таблицы: тождество 8x/7 == x + x/7 и усечение к нулю.
tests-host: [geom] 1992 -> 3144, все 5 наборов прошли.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Первая версия крыла только 0..255 байтовой таблицей x/7 по тождеству
8x/7 == x + x/7. Это было мимо: obj_x = 2*fwd - 116 уходит в минус, как
только fwd < 58 (левее x_bump[5]) — то есть у ЛЕВОЙ КРОМКИ комнаты, и там
мы продолжали звать __divsint.
Границы взяты из данных, а не на глаз: kid_data.bin даёт dx кадров Кида
-5..+10, стража -2..+10; при Char.x типа uint8_t и render_dx из
{-140,0,+140} полный диапазон obj_x = -416..695. SCRX[1152] кроет
-448..703 — деление стало недостижимым, оставлено страховкой.
Хранится ГОТОВОЕ значение (int16_t), а не x/7: байтовая таблица вдвое
меньше, но со знаковыми значениями требует расширения знака плюс
16-битного сложения — те же такты, что лишний add hl,hl при 2-байтном
индексе. Кодоген проверен: индекс полный 16-битный (грабли
sdcc_z80_const_ptr_index_bug обойдены отдельной uint16_t-переменной),
~130 тактов номинала против ~1 000 у __divsint.
Банк 4: 7 336 -> 9 394 из 16 384 (свободно 6 990). Таблица сверена
питоном обратно из .c (1 152 записи), правило генерации «усечение к нулю»
закреплено тестом geom_mul8div7_trunc_to_zero на целевом компиляторе.
tests-host: [geom] 1961 -> 1992, все 5 наборов прошли.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Обход всех сгенерированных .asm (awk по `call __div/__mod/__mul` с
привязкой к строке исходника) нашёл 31 вызов в 9 модулях. Три из них
были в горячем пути:
- pop_cdraw.c calc_screen_x_coord: `x * 8 / 7` -> __divsint, 2 400 тактов
на ПЕРСОНАЖА КАЖДЫЙ КАДР (два вызова при живом сопернике). Заменено
тождеством 8x/7 == x + x/7 плюс таблица DIV7[256] в банке 4 — резидент
не тронут, обычный диапазон (obj_x 0..252 при x_bump 58..184) покрыт
целиком, деление осталось только хвостом для шва (render_dx = ∓140).
- pop_guard.c guard_col_from_x: /14 и %14 звались БЕЗУСЛОВНО, мимо
POP_TILE_DIV — единственное 16-битное деление без подключённой таблицы.
- pop_trob.c animate_chomper: `tp / 10` на чомпера каждый кадр, при том
что TP_ROW/TP_COL лежали в этом же файле, но ниже по тексту. Таблицы
подняты выше чомперов.
Остальные 25 оставлены осознанно и расписаны в TASKS_OPEN.md: хвосты за
таблицей (x вне 0..255 = персонаж в соседней комнате), намеренный
медленный хвост pop_y_to_row, недостижимая ветка pop_rnd_fit и холодные
места (вход стража, старт уровня, читы, имя файла, отладочный HUD).
В банках 4, 6, 7 теперь ноль __div*. Тождество 8x/7 закреплено тестом
geom_mul8div7_identity (перебор −420..700), таблица DIV7 сверена с x//7.
tests-host: [geom] 840 -> 1961, все 5 наборов прошли.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mob_tick_one (927) и mob_render (976/977) считали `(y+60)/63 % 4 - 1`
вручную, хотя pop_y_to_row — точный эквивалент этой формулы на всём
int16_t (включая усечение деления к нулю для отрицательных). В asm это
были три пары __divsint+__modsint, ~16 200 тактов (3,8 % кадра) — только
пока кусок плиты в полёте, то есть в самом тяжёлом кадре.
В банке 7 теперь ноль __divsint. Эквивалентность закреплена тестом
geom_y_to_row_matches_formula: перебор −400..400 против исходной формулы
(вызовы разбросаны по трём банкам, соблазн написать деление «по месту»
возвращается). tests-host: [geom] 39 -> 840, все 5 наборов прошли.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Клипованный путь вынесен в отдельную функцию blit_b_clip: под его девять
16-битных локалей SDCC заводит кадр IX, и за этот кадр платили ВСЕ блиты
фона, включая те, где клипа нет вовсе (весь фон вне fore-прохода — факелы,
зелья, перерисовка тайлов, у них pop_t_fclip_on == 0). Быстрый путь идёт
сразу в gfx_blit_noclip.
Замер: pop_torch_draw 41 778 -> 31 218 тактов на факел (часть разницы —
прошлая правка pop_cd_touch; чистый вклад этой ~6 300 на блит). Поведение
не изменилось: клипованная ветка перенесена дословно.
docs/perf_backlog.md — отложенные идеи с измеренной ценой (футпринт из
физики 11 574, размеры ленты из каталога атласа, один map/unmap на группу
блитов, единый проход по тайлам как redraw_needed_tiles, objtable,
отложенные таблицы back/mid/fore) плюс раздел «как мерить»: wait-state'ы
дают 2,4x к справочным тактам, кадр 430 000, адреса символов меняются
после каждой пересборки, сцена между сессиями не воспроизводится.
Там же — что уже проверено и НЕ сработало.
tests-host: 5/5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Главный цикл спейсится тремя gfx_wait_vsync, поэтому работа сверх 430 000
тактов стоит сразу целый лишний растровый кадр. Было 470 964, стало
~425 600 — игра быстрее на треть (16,7 логических кадров/с против 12,5).
- pop_y_to_row: цепочка сравнений вместо (y+60)/63%4-1. ВАЖНО: медленный
хвост вынесен в ОТДЕЛЬНУЮ функцию — SDCC видит одинаковое выражение в двух
ветках и поднимает деление в вершину, быстрые возвраты не спасают.
- col_from_x (pop_bg) и get_tile_div_mod (pop_map) — общие резидентные
таблицы POP_TILE_DIV/POP_TILE_MOD в pop_tile.c (const банка из чужого
банка не читается).
- pop_fore_over_char: расширение окна считается арифметикой, а не перебором
10 колонок и 3 рядов (условие монотонно -> границы). Формулы сверены с
прежним перебором перебором значений, расхождений нет.
- pop_cd_touch: цикл по страницам развёрнут, x+w/y+h считаются один раз.
Зовётся с каждого блита фона, стоил 6 846 тактов.
- process_trobs: tp/10 и tp%10 у факелов — таблицей.
- Пустой слот соперника (стража на сцене нет, на странице ничего не
нарисовано) считается «тихим»: ни heal, ни вход в pop_char_draw, ни
fore-проход.
Приём для поиска делений: брейкпоинт на __divsint/__divuint/__divuchar с
печатью адреса возврата (printf "%04X", w@(sp)).
Профиль остатка — TASKS_OPEN.md#draw-cost. tests-host: 5/5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- move_coll_to_prev -> memcpy (LDIR): цикл на C пересчитывал адрес
назначения через слот кадра IX и обходился в 5 514 тактов на 14 байт.
- Окно перебора режется на НЕПРЕРЫВНЫЕ пробеги (комната слева / своя /
справа), по каждому идёт coll_scan с шагающим указателем. Прежний
«быстрый путь для окна внутри комнаты» не срабатывал почти никогда: Кид
в колонке 0 даёт окно с −1, и всегда шёл медленный сбор во временный
буфер с тернарником на колонку (1 340 тактов на колонку).
- Пролог ряда (координата грани, длина окна, смещение слота) вынесен из
тела ряда на кадр; базы соседних рядов — те же ±10 без пересчёта.
check_collisions 44 022 -> 38 334, физика Кида 67 518 -> 63 102, работа за
логический кадр 470 964 -> 466 560 (бюджет растрового кадра 430 000).
Замер итерации: пустая колонка 750 тактов, колонка-стена ~1 700.
tests-host: все 5 наборов прошли.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Замерено брейкпоинтами в MAME (уровень 1 комната 1, Кид стоит у факела).
Калибровка, без которой цифры не сходятся: такт totalcycles != номинальный
T-такт Z80, wait-state'ы ОЗУ Sprinter дают ~2,4x (get_tile 574 против 1422).
1. Окно перебора коллизии — как у оригинала (left_checked_col..right_checked_col,
seg004:0047), было: все 14 колонок каждый кадр. Признак годности слота у
нас дешевле оригинального: не массив номеров комнат с очисткой, а границы
окна, которые move_coll_to_prev переносит в prev вместе с флагами; бамп
считается по пересечению двух окон. check_chomped_flags тоже ограничен
окном, иначе протухшие слоты дают фантомный перемол.
2. get_tile_div_mod — таблицами tile_div_tbl/tile_mod_tbl (seg006:702), было
/14 и %14. SDCC разворачивал это в __divsint + __modsint, а __modsint
внутри зовёт __divsint ещё раз: 5 400 тактов на вызов, 13 вызовов за
кадр = 16 % кадрового периода на «в какой колонке точка».
3. get_row_collision_data: ряд разрешается один раз на весь перебор (было —
get_tile на каждую колонку, 1 422 такта), грань идёт шагом TILE_SIZEX как
в оригинале, wall_type таблицей вместо switch.
Итог: check_collisions 60 888 -> 42 750, физика Кида 100 578 -> 67 518,
синяя полоса ~60 % -> ~30 % кадрового периода. Профиль остатка и следующие
цели (отрисовка Кида 47 %, process_trobs 21 %) — в TASKS_OPEN.md#draw-cost.
tests-host: все 5 наборов прошли.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Персонаж, у которого с прошлой отрисовки ЭТОЙ страницы дабл-буфера не
изменился ни один вход отрисовки, а фон в его прямоугольнике не трогали,
уже нарисован правильно: heal, блит и fore-проход пропускаются целиком.
Не спецкейс «мёртвый страж», а общее правило — покрывает и труп, и
стоящего Кида, и ждущего стража.
Механизм: снимок входов по страницам (pop_cdraw.c, cd_sig/cd_quiet) +
позиционная метка «фон трогали вот здесь» (pop_cd_touch в резидентном
pop_tile.c, зовёт сам pop_blit_b). Решение перепроверяется перед
отрисовкой, а pop_char_draw страхуется собственным heal — если тик всё-таки
сдвинул персонажа, прошлый кадр стирается там. Слоты рядом (32 px) —
перерисовываем оба, иначе heal соседа выест кусок из «тихого».
Метка обязана быть ПОЗИЦИОННОЙ: с флагом «фон трогали хоть где-то» выигрыш
был ровно нулевым — факелы анимируются каждый кадр и гасили пропуск для
всех сразу (597 684 такта, как без оптимизации).
Замеры (MAME, брейкпоинты по totalcycles, бюджет кадра 430 000):
комн. 1.3, труп стража, Кид стоит: 210 % -> 116 % (500 772 такта),
ноль вызовов pop_heal_fast за кадр, весь фон — 2 блита (44 136);
комн. 1.1, Кид стоит вдали от факелов: 404 112 (94 %), цикл 4 -> 3 кадра.
Узкое место сместилось на ЛОГИКУ: 60 % кадра уходит на тик персонажей,
которые СТОЯТ, ещё 28 % — на loose_tick + process_trobs в комнате без
единой ловушки. Разбивка и план — TASKS_OPEN.md#draw-cost.
Комнаты 2 и 3 отличаются ровно телом убитого стража, и оно даёт +20% синей
и +50% циана, то есть ~70% кадрового периода. Причина видна в коде: ни
roomtest.c, ни pop_cdraw.c не смотрят на Char.alive — мёртвый страж каждый
кадр проходит весь путь живого (heal + блит + clip_char + брызги + клинок +
перебор тайлов fore), хотя его кадр постоянен до выхода из комнаты.
Кандидат на фикс — запечь труп в фон, как loose-плиты.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>