Порт PoP переехал в applications/SprPoP — приложение, которое собирается само: код, оригинальные данные, конверторы ресурсов и сборка внутри одной папки. Наружу знает единственный путь — корень тулчейна (SPRINTER_ROOT, по умолчанию ../..). applications/PoP/roomtest ЗАМОРОЖЕНА и остаётся архивом закрытых задач, багов и исполненных планов. Скопировано из applications/PoP/roomtest@4b74478. Перенос проверен побайтово: собранный sprpop.exe совпал с roomtest.exe того же коммита, все 39 дисковых ресурсов и все 16 генерируемых заголовков — тоже, host- тесты зелёные (15/15). Раскладка: src/ рукописный C (roomtest.c -> sprpop.c) gen/ генерируемые заголовки, в репозитории assets/orig/ оригинальные данные игры, вне репозитория (копирайт) assets/packed/ то, что ложится на диск, в раскладке диска tools/ конверторы; все пути — в одном tools/paths.py build/ выход: exe, каталоги ресурсов, hdd/, промежуточные atl/ Сборка ресурсов: assets/packed и gen — версионируемые ВХОДЫ, а не то, что пересчитывается каждым make. Автоматика построена на ОТСУТСТВИИ файла, а не на таймстемпах: git не хранит времена, и в свежем клоне сравнение по времени превращалось бы в лотерею. Недостающий ресурс или заголовок чинится сам, рекурсивным вызовом в ветку генерации. Музыка собирается из любого из четырёх наборов записей (make music-mp3, music-mt32, ...); набор входит в имя stamp'а, поэтому смена набора сама делает музыку устаревшей. Длины реплик больше не захардкожены: упаковщик печатает их в gen/pop_music_ticks.h, и шкала сцены выражена через них — иначе mt32 (реплики на 6% длиннее) молча ломал катсцену. Тулчейн: в app.mk два обратносовместимых крючка (SRC_DIR/BUILD_DIR), HDD_IMG стал ?=; команда сборки roomtest не изменилась. Корневой make host-tests переключён на SprPoP. Подгонка тайминга катсцены с принцессой (PV_MAGIC_LEAD): сцена render-bound и идёт ~49 тиков/с вместо 60, из-за чего кода реплики приходила раньше молнии. Это обход, а не лечение; разбор с замерами — docs/BUGS_OPEN.md, записи SND-PACE-DEAD, PV-RENDER-BOUND, MUS-LEFT-TEAR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
23 KiB
ЗЕЛЁНАЯ фаза (слой фона) — анализ и оптимизация
Рабочий документ: живёт между сессиями. Внизу журнал правок — каждая
запись с замером до/после. Сцена, рецепт воспроизведения и зонды —
perf_l13_room23.md; там же ответ про 8-битные
габариты. Парная фаза — perf_cyan_phase.md (её цель
достигнута).
Границы фазы в sprpop.c: от PROF(4) (строка 450) до PROF(6)
(строка 620). Содержимое: pop_loose_tick, pop_process_trobs,
pop_redraw_needed, шов, смена уровня, сигналы провалов, вспышка.
Бюджет: ≤ 400 000 тактов (растровый кадр = 430 000).
| состояние | было (c312e4a) |
стало (af189a1) |
|---|---|---|
| покой в комнате 23 | 35 760 | 35 760 |
| дрожат 6 плит-потолков | 366 000 | 335 400 |
| пик каскада | 805 000 | 546 900 |
ЦЕЛЬ ФАЗЫ НЕ ДОСТИГНУТА: 546 900 против 400 000 (1,37×). Что осталось
сделать и почему это именно раскол draw_tile — §3.
1. Раскладка ПОСЛЕ правок (замер af189a1)
Пик — кадры 24-28 (посадки плит), больше не кадры провалов.
| участок | покой | дрожь | пик |
|---|---|---|---|
pop_loose_tick |
36 234 | 36 234 | 156 762 |
pop_process_trobs |
1 050 | 1 050 | 1 050 |
pop_redraw_needed |
924 | 288 800 | 380 568 |
| хвост (шов, смена уровня, сигналы, вспышка) | 9 336 | 9 336 | 10 776 |
pop_loose_tick изнутри на пике: два цикла по тайлам 9 852,
pop_loose_mob_tick 154 074 (heal шести летящих кусков), остальное мелочь.
Цена одной перерисовки: было → стало
| вид | было | стало | чем |
|---|---|---|---|
RDA_CEIL — дрожащая плита-потолок |
48 785 | 43 536 | G2 + G3 |
RDA_CEIL_GONE — запечь колодец |
251 335 | 138 318 | G1 (клип полосы) + G2 + G3 |
RD_FLOOR — щебень на месте посадки |
198 805 | 179 914 | G2 + G3 + снятая двойная пометка |
Штук за кадр: RDA_CEIL до 6, RDA_CEIL_GONE до 2, RD_FLOOR до 2.
Детальный профиль RD_FLOOR (179 914) — главная оставшаяся статья
Снят зондами по каждому блиту (pop_dbg_b1/b5) и по рамкам
(pop_bar_black, pop_cd_batch_begin/end):
| участок | такты |
|---|---|
вход pop_floor_bake + gfx_set_bank |
2 382 |
pop_bar_black 60×39 (включая pop_cd_touch 4 502) |
~15 500 |
контекст draw_tile #1 (5 чтений тайлов, 63*row, индексация таблицы) |
13 584 |
| 4 блита тайла #1 | 50 718 |
диспетчер между блитами #1 (все if (code == …)) |
11 388 |
контекст draw_tile #2 |
13 584 |
| 4 блита тайла #2 | ~54 000 |
| диспетчер между блитами #2 | 11 388 |
| хвост | 6 474 |
Итого: 105 500 — сами блиты (реальные пиксели), 74 400 — накладные, из
которых 27 168 контекст двух draw_tile и 22 776 их диспетчер.
2. Что сделано (с чем сравнивать)
G1. Окно клипа для точечной перерисовки — −90 000
pop_t_win_set(x, ytop, w, h) / pop_t_win_clear() в pop_tile.c: ставит
уже существующее окно pop_t_fclip_* на прямоугольник, который перерисовка
восстанавливает. Работает в обе стороны — и предфильтр pop_blit_b
отсеивает куски мимо окна ДО atlas_image/gfx_w0_map, и blit_b_clip
режет остальные по нему.
Где сработало: pop_ceil_bake_empty — куски ряда 0 высотой 63 px рисовались
целиком, хотя восстановить надо девять строк полосы. Блит 21 447 → 7 619,
вся перерисовка 251 335 → 138 318.
Где НЕ сработало — см. §4, отрицательные результаты.
Побочно: пока окно стоит, pop_blit_b не ставит пометку «фон трогали»
(признак fore-прохода), поэтому вызывающий обязан пометить прямоугольник сам.
В pop_ceil_shake_draw добавлен явный pop_cd_touch на область heal'а; в
pop_ceil_bake_empty и pop_floor_bake метит pop_bar_black, а лишний
второй вызов на ту же область снят.
G2. Контекст тайла — file-scope, а не локали draw_tile — −55 000
Порт load_curr_and_left_tile (seg008:0339): у оригинала это
curr_tile/curr_modifier/draw_xh/draw_main_y/draw_bottom_y —
переменные модуля, а не локали.
Причина в кодогене: в draw_tile 57 вызовов, и каждое живое через вызов
значение SDCC спиливал в стековый кадр — 26 байт кадра и 513 обращений
-N(ix) (при ~46 замеренных тактах на обращение это ~23 600, что и
намерено). Стало 33 обращения, кадра нет, банк 7 −703 Б.
G3. blit_b_clip — байтовый габарит + file-scope
Два шага, и важен порядок наблюдений:
- Байтового габарита ОДНОГО НЕ ХВАТИЛО.
sx/sy/dw/dh→uint8_t(корректно: кадры атласов ≤ 56×63) дало 211 → 173 обращения, а 22-байтовый кадр остался: значений, живых через шесть вызовов ядер libbgi, всё равно больше, чем регистров у Z80. - Решило вынесение из локалей (
bc_*): 51 обращение, кадр 22 → 12 Б.
Клипованный блит 14 088 → 11 848. Заодно blit_b_oversize больше не ходит
через blit_b_clip (там теперь байтовый габарит) — рисует напрямую
gfx_blit_part; это путь под полноэкранные подложки интро/финала.
G4. Мелочи
pop_blit_b: аргументы в file-scope (третий и дальше SDCC передаёт стеком, каждое чтение шло через-N(ix)) — 76 → 11 обращений.pop_loose_mob_tick: пометки всех кусков ОДНИМ пакетом (pop_cd_batch_begin/end) — было по 4 502 такта на кусок. 176 772 → 168 600.- Коридор heal куска — по фактической высоте СОБРАННОГО композита (было 24 строки константой, стало 20). 168 600 → 156 762.
3. Что осталось: раскол draw_tile (позиция G5)
Оставшийся разрыв: −147 000. Он весь в двух местах.
G5. Расколоть draw_tile на узкие части, как в оригинале
Ожидание: −50 000 … −60 000.
У оригинала draw_tile (seg008:01C7) — это девять независимых вызовов:
draw_tile_floorright, draw_tile_anim_topright, draw_tile_right,
draw_tile_anim_right, draw_tile_bottom, draw_loose, draw_tile_base,
draw_tile_anim, draw_tile_fore. Для ряда −1 он зовёт шесть из них
(draw_tile_aboveroom, seg008:01F2), для полосы у потолка — те же шесть плюс
draw_tile_wipe(3) (redraw_needed_above, seg008:02C1).
У нас всё это — ветки if (row >= 0) ВНУТРИ одной функции, то есть контекст
(13 584) и диспетчер (11 388) оплачиваются целиком всегда. Расколов, каждая
точечная перерисовка сможет звать только нужные части:
pop_floor_bake: вместо второго полногоdraw_tile(row, col+1)— только его правую грань и базу;pop_ceil_shake_draw/pop_ceil_bake_empty: дословныйdraw_tile_aboveroom;pop_loose_shake_draw,pop_spike_redraw,pop_gate_redraw— то же.
Риск средний: у draw_tile собрано много инвариантов (BUG-LOOSE-3,
BUG-LATTICE-DOORTOP, BUG-SEAM-WEDGE-1), проверять придётся прогонами всех
уровней. Поэтому делать отдельным заходом, а не хвостом другой правки.
G6. Меньше блитов в RD_FLOOR
Ожидание: неизвестно, надо мерить. 105 500 из 179 914 — это 7,6 блита,
и они рисуют настоящие пиксели. Сократить можно только сократив то, что
восстанавливается: бар сейчас 60×39 от yb+26, а плита занимает по вертикали
меньше (её куски: левая грань POP_LOOSE_FRAM_LEFT 32×13 на dmy = yb+62,
низ POP_LOOSE_FRAM_BOTTOM 32×3 на dby = yb+65, правая грань в соседе
26×16 на dby−1). То есть плита живёт в yb+47 .. yb+65, а бар начинается с
yb+26 — 21 лишняя строка сверху.
Проверять осторожно: бар заодно стирает и то, что рисует ДРУГИЕ куски тайла
(орнаментная лента stripe_id на dmy−27 = yb+35 попадает как раз в
«лишнюю» часть). Сузишь бар — надо убедиться, что ничего не осталось.
G7. heal летящих кусков — 154 074 (28 % фазы)
Шесть кусков × ~25 700: сам heal 64×20 (по модели ~19 700) + пакетная
пометка + накладные mob_tick_one (16-байтовый кадр, 99 обращений (ix)).
Сам heal у предела железа — это 1 280 пикселей на кусок, оптимизировать
нечего, кроме площади. Площадь уже подрезана до габарита композита.
Остаётся mob_tick_one (~4 500 на кусок = 27 000 на кадр) — то же лечение
file-scope, что у draw_tile.
G8. Пометка соседа — узкой полосой, а не полным тайлом (идея пользователя)
Ожидание: заметное, но не мерено. Взять ПОСЛЕ обхода всех уровней (решение пользователя 2026-08-17: пока идёт отлов багов слоёв, каждая правка добавляет переменных в картину).
Когда плита (1,8) падает, помечаются ДВА тайла:
| пометка | что делает |
|---|---|
(1,8) → RD_LOOSE_GONE |
бар 40 на своём x, бар 32 на соседе, draw_tile(1,8) + draw_tile(1,9) |
(1,9) → RD_FLOOR |
бар 60 на x соседа, draw_tile(1,9) ЕЩЁ РАЗ |
То есть сосед перезапекается ЦЕЛИКОМ и ПОВТОРНО, хотя потревожили у него
только левые 28 пикселей — там, куда свисает правая грань упавшего тайла.
draw_tile(1,9) при этом вызывается дважды на одну пометку.
Что такое эти числа (чтобы не сузить лишнего):
- 60 = 32 свой тайл + 28 СОБСТВЕННЫЙ свес. Правая грань пола (кадр 42,
26 px) рисуется в клетке соседа с
x+32, занимаяx+32..x+57. Для запечки САМОГО тайла 60 уже минимальны — сужать их нельзя; - сузить можно только тот случай, когда тайл помечен ПОТОМУ ЧТО ИЗМЕНИЛСЯ ЕГО ЛЕВЫЙ СОСЕД: тогда нужна полоса 28 px у левого края, а не весь тайл.
Почему выигрыш не символический: pop_floor_bake стоит 179 914 тактов,
из них 105 500 — сами блиты. Узкая полоса срезала бы и площадь бара
(60×39 → 28×39), и часть блитов — окно клипа там теперь стоит обязательным
(см. §4), так что отсев достаётся даром.
Условия, из-за которых это не «просто уменьшить число»:
pop_floor_bake— ОБЩАЯ функция: её же зовут кнопка (pop_button_redraw), зеркало, подобранный предмет и щебень на месте посадки. Там меняется сам тайл и 60 нужны целиком. Значит нужен отдельный вход (напр.pop_floor_bake_edge(row, col)) или параметр-прямоугольник — именно под пометку «изменился мой левый сосед».- Прежде чем выкидывать вторую пометку целиком, сверить ВЕРТИКАЛЬНЫЕ
диапазоны:
pop_loose_bake_emptyкроет63*row+46 .. +65(20 строк), аpop_floor_bake—yb+26 .. yb+64(39 строк). То есть сосед покрыт ВТОРЫМ баром не полностью, и просто снять пометку нельзя. - Ширина полосы = свес ЛЕВОГО тайла, а он зависит от типа тайла (у loose это
8 px по комментарию в
pop_loose_bake_empty, у пола 26). Брать по максимуму (28) — безопасно.
G9. Снять временную оснастку
Шесть pop_dbg_b1..b6 внутри pop_blit_b — ~400 такта на блит; при 15
блитах зелёной это 6 000 на кадр. Плюс pop_dbg_kind/m16 (2 вызова на
перерисовку) и pop_dbg_m5..m15. Снимать ПОСЛЕ окончания оптимизации: без
них не мерить.
4. Копия второй страницы — и почему она ТРЕБУЕТ окна клипа
Точечные запечки ставятся с pages = 2, срабатывают два кадра подряд (по разу
на страницу дабл-буфера) и оба раза считают одно и то же. После первого раза
нужный прямоугольник уже лежит в ОЗУ-копии первой страницы, и его можно
скопировать: gfx_copy_page берёт источником ОЗУ-копию НЕактивной страницы
(то есть ЧИСТЫЙ фон — спрайты рисуются банком без тени и в копию не попадают),
а приёмник обновляет и в видео-ОЗУ, и в ОЗУ-копии. Идея пользователя: тот же
приём, что при перевороте экрана (зелёное зелье), только без зеркала.
| полная запечка | копия | |
|---|---|---|
| щебень / кнопка (60×39) | 179 914 | ~35 000 |
| колодец полосы потолка (64×9) | 138 318 | ~17 500 |
ДВА УСЛОВИЯ КОРРЕКТНОСТИ. Оба нарушались и оба дали видимые баги.
- Запечка обязана быть ОГРАНИЧЕНА копируемым прямоугольником.
draw_tileрисует тайлы ЦЕЛИКОМ, то есть пишет ШИРЕ бара; копия переносит ровно бар, и всё, что легло вне него, на второй странице остаётся прежним — страницы расходятся, это видно как МЕРЦАНИЕ через кадр. У полосы потолка окно стояло с самого начала (G1), уpop_floor_bake— нет, и он мерцал торцами полов, плит и кнопок (найдено пользователем 2026-08-17: уровень 1, комната 6, Кид на кнопке (0,2)). Поэтому вpop_floor_bakeокно теперь стоит КАК УСЛОВИЕ КОРРЕКТНОСТИ, хотя по скорости само по себе убыточно (см. §5) — снимать его нельзя. - Копия годится только если содержимое тайла между двумя кадрами не
изменилось. У анимированного тайла (кнопка с идущим таймером связи)
пометка обновляется КАЖДЫЙ кадр и картинка каждый раз другая. Поэтому
pop_set_redraw/pop_set_redraw_aboveгасят слот копии при ПЕРЕпометке (pop_bake_slot_reset*).
Плюс слот bake_pg/bake_pg_above помнит, НА КАКОЙ странице сделана первая
запечка: копируем только если первая была на ДРУГОЙ странице и дабл-буфер
включён. Это покрывает переплетение двух запечек в одном кадре, однобуфер
(чит SPACE) и смену комнаты (pop_bake_forget).
5. Отрицательные результаты — НЕ повторять
Окно клипа в pop_floor_bake — по СКОРОСТИ проверено ТРИ раза, каждый раз хуже
Но оно всё равно стоит на месте: без него ломается копия второй страницы (§4). Ниже — только про скорость самого окна.
| попытка | было | стало |
|---|---|---|
| до G3 | 187 266 | 198 279 |
| после G3 | 182 124 | 188 460 |
после pop_blit_b file-scope, с детальным зондом |
179 914 | 188 417 |
Третья попытка объяснила причину: клипованный путь стоит +1 500 такта на КАЖДОМ из 7,6 блитов (+11 400), а режет он только редкие высокие куски — в трассе такие нашлись (34 878 → 25 872 и 28 818 → 24 090, всего −13 700), но в среднем по 12 перерисовкам их нет. Запись стоит в коде.
Прочее (проверено раньше)
- Не откладывать запекание на другой кадр — запечка пишет ОЗУ-копию, из которой восстанавливает heal; отложенная даёт призрак плиты на месте дыры.
- Не батчить смежные колонки в
pop_ceil_shake_draw— плиты стартуют со случайными задержками, в кадре дрожат разрозненные колонки, пробег почти всегда длиной в одну. - Не ускорять передачу пикселей — предел железа (3+3 такта на байт,
memory
blit_cost_model). В пиковом кадре «железный» минимум всех блитов ≈150 000 из 916 458. LOOSE-SHAKE-RUNS(пометки по сменам кадра): наивный вариант выигрыша НЕ даёт — пять смен × две страницы = те же десять перерисовок. Работает только версия «пары и тройки», ~20 % и только на дрожащих плитах; оценка 2026-08-13, не перемерена. Подробности —TASKS_OPEN.md#loose-shake-runs.
6. Журнал правок
| дата | что сделано | зелёная: покой / дрожь / пик | коммит |
|---|---|---|---|
| 2026-08-17 | базовый замер | 35 760 / 366 000 / 805 000 | c312e4a |
| 2026-08-17 | G2 контекст тайла в file-scope | — / — / 747 954 | a3c473d |
| 2026-08-17 | G1 окно клипа в pop_ceil_bake_empty |
— / — / 663 250 | a3c473d |
| 2026-08-17 | G3 blit_b_clip байты + file-scope |
— / 335 400 / 575 730 | a3c473d |
| 2026-08-17 | pop_blit_b file-scope; пакетная пометка кусков |
— / — / 557 706 | b2da0b8 |
| 2026-08-17 | коридор heal по высоте композита | 35 760 / 335 400 / 546 900 | 9a50ab2 |
| 2026-08-17 | копия второй страницы вместо второй запечки | — / — / 423 558 | 5ef721e |
| 2026-08-17 | mob_tick_one в file-scope; снята оснастка из горячих путей |
35 760 / 326 130 / 414 456 | 18ee60e |
| 2026-08-17 | фикс мерцания: окно клипа в pop_floor_bake как условие корректности копии |
замер после фикса — ниже | 35b7cd5 |
| 2026-08-17 | замер после фиксов уровня 1 (мерцание торцов, потолочный fore, сосед под плитой, блеск меча) | 35 760 / — / 417 630 | 40f0d46 |
| 2026-08-17 | регресс после фиксов уровня 2 (чёрные бары, чит бессмертия) — в пределах шума | — / — / 419 526 | ec1f384 |