Каталог атласа теперь читается из W0 вместо переключения W3 — минус 408
на кадре при ожидании минус 5 400. Цена блита 16 107 -> 16 005.
Причина записана: 672 такта atlas_image — это почти целиком вызов
функции и арифметика idx*8, а не переключение окна; после правки работа
переехала в статью «каталог + шапка + клип» (2 694), а сам gfx_w0_map
стоит всего 324.
Отсюда понижена оценка непробованной части P4 (один map на группу
блитов): потолок ~2 600 за кадр, а не 10 000.
Бюджет: лёгкая 438 570, тяжёлая 602 574. До цели 8 570 и 172 574.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
atlas_image ради двух байт записи каталога переключает W3 туда и обратно,
хотя вызывающий сразу после этого мапит ту же страницу в W0 — и каталог
там доступен по тому же смещению. Новый atlas_image_w0 (libbgi) читает
его из W0; в pop_blit_b порядок стал «сначала gfx_w0_map, потом каталог».
ОЖИДАНИЕ НЕ ОПРАВДАЛОСЬ. По раскладке блита atlas_image стоил 672 такта,
и я рассчитывал снять их целиком: 8 блитов зелёной фазы это 5 400 за кадр.
Фактически цена блита 16 107 -> 16 005 (-102), на кадре -408.
Причина: 672 — это почти целиком вызов функции и арифметика idx*8, а не
переключение окна. Замер после правки: gfx_w0_map 324, «каталог + шапка +
клип» 2 694 — работа просто переехала из одной статьи в другую.
Правку оставляю: она не вредит, убирает лишнее переключение W3 и делает
контракт честнее (страница мапится один раз). Но как способ снять
накладные блита она не работает — фиксированная часть 6 765 -> 6 663.
Замеры: лёгкая позиция 438 978 -> 438 570; тяжёлая 602 574 (прошлый замер
617 487 снят до P16, поэтому напрямую не сравним).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fore-проход Кида в тяжёлой позиции (89 268) разложен зондами:
вход + set_clip + char_footprint 10 872
арифметика границ окна 3 786
шов ворот + overlay-цикл 3 294
ЦИКЛ fore_tile ПО ТАЙЛАМ 67 854 76 %
gate_over_char + хвост 3 462
Счётчик показал, что цикл обходит ВСЕГО 4 тайла, и 3 из них реально
рисуют. То есть 67 854 — не перебор лишних тайлов и не проверки, а цена
самих блитов переднего слоя: около четырёх блитов по ~16 000, где 6 765
на каждом — фиксированная накладная.
Значит отдельной оптимизации fore-прохода почти нет: срезать можно цену
блита (P4, ~27 000 из 67 854), char_footprint из физики (P10) и слияние
двух трамплинов в банк 2 (~4 000).
P4 поднят в очереди: он бьёт и по fore-проходу (4 блита), и по зелёной
фазе (8 блитов) — то есть работает и в динамике, и в статике.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Записаны, чтобы не повторять, и с разбором причины.
1. cd_sig_same блоком (сравнение 10 байт циклом вместо 13 сравнений
полей): по листингу короче (1939 -> 1290), на машине хуже
438 978 -> 450 426. Сумма тактов по листингу считает инструкцию один
раз, а тело цикла исполняется десять раз.
2. cd_touch_pb (пометка «для блита» из file-scope вместо четырёх
аргументов): 438 978 -> 442 242. В зелёной фазе блиты идут пакетным
путём, где нужны все четыре значения, а в регистрах они дешевле, чем
чтение из статиков.
3. Обёртка pop_cd_hit_slot поверх pop_cd_hit — 1799 против 1318 тактов;
помогло только когда сравнение переехало внутрь.
Общий урок записан там же: короткий листинг не равно быстрый код, и
снятие аргументов со стека помогает не всегда.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
P16 (цианные проверки) — минус 25 818 тремя правками: снимок без
построения структуры, guard_over_kid только когда кого-то рисуем,
проверка слота без пяти аргументов. Записан и отрицательный результат
внутри третьей: обёртка, которая внутри всё равно звала pop_cd_hit с
пятью аргументами, сделала хуже.
P14 уточнён: fore-проход не «62 778…89 000», а от 4 122 (персонаж
пропущен) до 117 570 (труп Кида в челюстях — широкий кадр в тайле с
передним слоем). Значит в бою он будет ближе к сотне тысяч.
Бюджет лёгкой позиции: 801 768 -> 438 978. До цели 430 000 осталось
8 978 — одна правка.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pop_cd_hit принимает (p, x0, y0, x1, y1) — три последних идут стеком, и
функция целиком уезжает в IX-фрейм: 45 % её тактов на `-n(ix)` (asm).
А зовут её из cd_quiet до восьми раз за кадр.
Новый pop_cd_hit_slot(who, p) берёт координаты прямо из pop_cd, а само
сравнение вынесено в hit_rect с file-scope аргументами. Первая попытка —
обёртка, которая внутри всё равно звала pop_cd_hit — не дала ничего
(1799 Z80 вместо 1318, то есть стало хуже), и это записано здесь, чтобы
не повторять: снимать аргументы со стека нужно у ТОГО, кто их читает.
asm на путь «спрайт + накладной»: было 1799 + 2x1318 = 4435 тактов Z80,
стало 1221 + 2x1009 = 3239 (-27 %).
Замер 11/15, лёгкая позиция: синяя 219 894 -> 218 052, циан 41 280 ->
39 438, работа кадра 442 662 -> 438 978.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Разложил остаток цианной фазы (44 007 на трёх вызовах):
pop_loose_mob_draw 978 гейт mobs_live работает
guard_over_kid 14 424 трамплин в банк 8 + два objtile_at_char
pop_char_skip_mask 28 605 трамплин в банк 4 + два cd_quiet
guard_over_kid отвечает на вопрос «кто рисуется поверх кого», а он не имеет
смысла, когда не рисуется никто. Перенёс вызов ПОСЛЕ pop_char_skip_mask и
сделал условным: при skip == 3 оба слота тихие, и порядок не нужен.
Перестановка безопасна: обе функции только читают, и читают разное —
skip_mask снимок cd_sig, guard_over_kid габариты pop_cd прошлого кадра.
Замер 11/15, лёгкая позиция: циан 55 257 -> 41 280, работа кадра
456 780 -> 442 662.
Проверено в MAME: статика чистая, в бою (Кид сближается и бьёт стража)
персонажи перекрываются правильно, порядок не сломался.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cd_sig_make СТРОИТ структуру из тринадцати полей в стековом кадре (то есть
через -n(ix)), и только потом шёл побайтовый цикл сравнения. А зовётся
проверка четыре раза за кадр: pop_char_skip_mask дважды, и в ней по два
слота.
Новый cd_sig_same сравнивает поля прямо с источником, с ранним выходом на
первом расхождении — у двигающегося персонажа это обычно первое же поле.
cd_sig_make остался: он нужен pop_char_draw, чтобы снимок записать.
Замер 11/15, лёгкая позиция: участок «mob_draw + guard_over_kid +
skip_mask» 47 883 -> 43 875, циан 59 265 -> 55 257, синяя 223 902 ->
219 753, работа кадра 464 796 -> 456 780.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Вопрос пользователя после боя в комнате 15: Кид вытеснил стража вправо
(из-за края торчал только меч), убил — труп не появился ни в 15, ни в
соседней справа.
Это не наш баг, а сложение трёх механизмов оригинала: физика стража
работает только в полосе x 44..211, поэтому комнату он не менял;
мёртвый за Кидом не идёт (follow_guard требует alive < 0), и leave_guard
сохраняет его в прежнюю комнату; а из чужой комнаты страж не рисуется
вовсе — при Guard.room != drawn_room оригинал гасит слот (seg000:422).
Труп остаётся приписан комнате 15 с guards_x за правым краем: при
возврате восстанавливается там же, то есть вне видимого поля.
Живьём в SDLPoP сценарий не воспроизводился — вывод из чтения кода, о чём
в записи сказано прямо.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Лёгкая: 628 542 -> 464 796 (-163 746), персонажи не рисуются вовсе.
Тяжёлая: 758 358 -> 617 487 (-140 871), рисуется только Кид — он
действительно стоит под пламенем, а страж нет. Пятирастровые кадры в
тяжёлой позиции исчезли (было 27 %).
Итог восьми позиций: 801 768 -> 464 796 в лёгкой (-42 %). До цели
430 000 осталось 35 000 в лёгкой и 187 000 в тяжёлой.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Две правки, обе про ложные срабатывания пропуска отрисовки персонажа.
1. МЕТКА: вместо «маска колонок по 32 px на ТРИ ряда по 63 px» теперь на
каждую колонку хранится диапазон затронутых y (ymin/ymax, 40 байт на обе
страницы). Прежняя гранулярность склеивала касания внутри ряда: пламя
факела занимает y 33..50, клинок стоящего стража — y 59..65, между ними
девять пикселей зазора, а метка считала слот задетым.
2. ПРОВЕРКА: cd_quiet сверяет с меткой спрайт и накладной (клинок, брызги)
ДВУМЯ ОТДЕЛЬНЫМИ прямоугольниками, а не объединённым bbox. Объединение
включает пустой угол между ними, и он ловил касания, которых нет: спрайт
стража лежит в колонке 8, клинок уходит в колонку 7 на y 59..65, пламя
метит колонку 7 на y 33..50 — прямоугольник «спрайт + клинок»
(x 241..284, y 46..84) цеплял метку углом.
Без второй правки первая почти ничего не дала (632 676 против 628 542 до
неё): объединённый bbox продолжал ловить ложное пересечение.
Замер 11/15:
фаза до P15 после
синяя 259 050 223 902 (heal тоже перестал платить)
зелёная 181 494 181 494
циан 194 262 59 406
работа 632 676 464 796
Проверено в MAME: в статике картинка чистая, в динамике (пробежка, бой,
переход в соседнюю комнату) хвостов и просвечивания нет. Хост-тесты
зелёные.
Заодно найден и исправлен собственный баг первой редакции: обе страницы
обновлялись по условию, проверяющему только страницу 0, и после
pop_cd_clear(0) метка страницы 1 переставала расти. Плюс pop_cd_init:
пустая колонка обозначается ymin = 255, а нули от crt0 читались бы как
«затронута строка 0».
У ОРИГИНАЛА такой метки нет вовсе: и Apple II (FRAMEADV.S RedBlockFast,
шесть буферов по блокам), и SDLPoP (set_redraw_fore) метят целыми тайлами,
но им это не мешает — персонаж у них рисуется каждый кадр безусловно.
Пропуск неизменившегося персонажа — наша добавка, поэтому и точность метки
нужна выше оригинальной.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Пользователь поймал ошибку в моём расчёте, глядя на экран: страж целиком
правее пламени, пересекаться может только меч.
Проверка по памяти машины подтвердила и уточнила:
страж, спрайт 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>
Пользователь передвинул Кида на один осторожный шаг вправо (x = 106
вместо 99, колонка та же) — его спрайт начал пересекаться с тайлом (0,3),
где одновременно чомпер и пламя факела.
фаза лёгкая тяжёлая
синяя 259 500 257 520
зелёная 181 068 180 870
циан 187 761 319 842 (+132 081)
работа 628 542 758 358
Период кадра: 4 растра в 335 кадрах, 5 растров в 123 (27 %). Это уже не
«стабильно медленно», а рывки.
Куда ушли 132 тысячи: pop_char_draw(KID) 204 -> 54 738 и fore-проход
Кида 4 356 -> 93 486. То есть Кид из «пропущен» превращается в
полноценного персонажа за ~144 000 — столько же, сколько страж.
Отсюда новая позиция P14: fore-проход персонажа, 62 778 у стража и
~89 000 у Кида, вместе около 152 000 = 20 % работы кадра. Это самая
дорогая единичная статья. У Кида он дороже потому, что в его футпринте
лежит чомпер со своим передним слоем.
P3 переведён в «частично сбылось»: выигрыш держится только пока персонаж
не подошёл к анимированному тайлу, а в игре он подходит постоянно.
Итог семи закрытых позиций: 801 768 -> 628 542, то есть -22 %. До цели
430 000 остаётся снять 199 000 в лёгкой позиции и 328 000 в тяжёлой, а
всё оставшееся в реестре даёт порядка 100 000. Арифметика не сходится —
в реестр записаны три возможных решения (P13, осознанное расхождение с
оригиналом, принять 4 растра), выбор за пользователем.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Циан 188 004 не двигался ни от P1, ни от P5, ни от P2b — разложил его
зондами.
Хорошая новость: Кид УЖЕ пропускается (204 такта на pop_char_draw), то
есть надежда P3 сбылась после P1 — метка от чомпера до него больше не
дотягивается. Страж же перерисовывается каждый кадр честно: пламя
правого факела (0,7) рисуется в ячейке (0,8), где он стоит, и реально
накрывает ему голову (пламя занимает y 5..22, страж 12..62).
Отрисовка стража — 148 302:
pop_char_fore (2 трамплина в банк 2 + обход тайлов) 62 778 42 %
клинок (sword_draw + overlay_add + clip_add) 27 522 19 %
блит спрайта + clip_char_right 20 982 14 %
загрузка кадра и геометрия 12 696 9 %
pop_clip_char_top (трамплин банк 4 -> банк 3) 8 658 6 %
снимок прямоугольника + cd_clip_add 7 890 5 %
gfx_w0_unmap + cd_sig_make 4 968 3 %
вход + cd_heal 2 946 2 %
Единственная явно лишняя статья — трамплин clip_char_top, и снять его
непросто: функции нужны get_tile и таблицы деления из банка 3, перенос в
резидент вернёт тот же трамплин внутрь. Остальное — работа, которую
персонаж действительно делает.
Зонды переставлены с уже закрытых замеров (loose_tick, физика) внутрь
pop_cdraw; оснастка снимается позицией P12, когда оптимизация закончится.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Замер отделил луч от pop_frame_timers: таймеры со всеми тремя
спецсобытиями уровней стоят 1 962, луч — 36 786, то есть 5,6 % работы
кадра на девять чтений байта.
Причина оказалась НЕ в алгоритме. Сверка трёх референсов:
SDLPoP (seg003:688) — идёт по x с шагом 14 и на каждом шаге переводит x
в колонку делением. Причём сам SDLPoP признаёт в комментарии, что
«DOS PoP does this: tile_div_tbl[xpos]» — то есть оригинал брал
таблицу, а порт заменил её на / и %, потому что на 32 битах так проще.
Apple II (MISC.S CHECKALERT) — тот же алгоритм байт в байт, но перевод
x -> блок через таблицу BlockTable[x]. Ровно то, что у нас уже было
сделано (POP_TILE_DIV, 2026-08-10).
mininim — другая архитектура (тайловые позиции, своя механика), для
сравнения реализации не годится.
То есть алгоритмически мы уже были на уровне Apple II, а платили за
другое: pop_tile_at объявлен __banked, луч живёт в guards.c (банк 1), и
на КАЖДУЮ колонку шёл трамплин банк 1 -> банк 3. На сцене 11/15 (Кид в
колонке 2, страж в 8) это девять трамплинов за кадр.
Сделано:
1. луч переведён на КОЛОНКИ вместо x-координат. Это эквивалентно:
начальные x — ровно центры тайлов персонажей, а обратный перевод даёт
ту же колонку (floor((58 + col*14 - 58)/14) == col). Ушли 16-битный
шаг, 16-битное сравнение и индексация таблицы на каждой итерации;
2. тайлы отрезка забираются ОДНИМ банковым вызовом (pop_row_tiles)
вместо девяти;
3. внутри pop_row_tiles — быстрый путь для отрезка целиком внутри
комнаты: get_tile при ряде 0..2 и колонке 0..9 сводится ровно к
g_fg[row*10+col] & 0x1F, идём указателем;
4. буфер тайлов — file-scope, а не локальный массив (иначе каждое
чтение это -n(ix)).
Замер по шагам: 36 786 -> 24 048 (колонки + один вызов) -> 13 002
(быстрый путь + буфер). Синяя фаза 283 215 -> 259 500, работа кадра
654 990 -> 628 542, то есть -26 448 при ожидании -30 000.
Кэш-гейт «пересчитывать только при смене позиции» НЕ понадобился:
расхождения с оригиналом нет, луч считается каждый кадр, как и должен.
Поведение проверено в MAME: страж в боевой стойке, но не идёт — между ним
и Кидом чомпер, то есть can_guard_see_kid = 1 («видит, но не пойдёт»).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Разбор pop_phys_tick (61 266 тактов на НЕПОДВИЖНОМ Киде) зондами по
звеньям kid_phys:
check_collisions 33 846 55 %
хвост (spike/spiked/chomped/knock/leave/save) 16 188 26 %
check_press 4 140
check_action 2 622
loadkid_and_opp 2 148
determine_col 1 182
fall_accel+fall_speed 582
bump_into_opponent 198
Внутри check_collisions: три coll_row (сканирование рядов) — 23 256,
подготовка окна 3 240, set_char_collision 1 788, обход пересечения 5 562.
Сгенерированный asm coll_scan показал 322 такта Z80 на ПУСТУЮ колонку
(с wait-state'ами 773 — ровно замеренные 750), из них 137 (43 %) —
обращения через IX-фрейм, и четыре 16-битные операции на колонку там,
где от колонки зависит один операнд.
Сделано:
1. вся арифметика цикла в 8 битах. scan_left = x_bump[col+5] + TILE_MIDX
при колонках окна -2..11 лежит в [37, 233], wall_dl в [-1, 10],
wall_dr в [0, 13] — суммы в [36, 246], переполниться не могут.
Границы персонажа приводятся к 8 битам с клипом, и клип точен: порог
ниже 37 означает «условие не выполнится никогда», выше 233 — «всегда».
2. dst снят с IX-фрейма в file-scope (scan_dst).
ОТРИЦАТЕЛЬНЫЙ РЕЗУЛЬТАТ, не повторять: предпосчёт таблиц порогов по типу
стены (thr_l[6]/thr_r[6] на кадр) сделал ХУЖЕ — check_collisions
33 846 -> 36 570, синяя фаза +10 269. Колонок в окне четыре-пять, а типов
стен пять: кэша получилось больше, чем потребления.
Проверено на кодогенерации: register на параметре-указателе SDCC 4.5 z80
проигнорировал (asm байт в байт), а file-scope дал 607 -> 454 такта.
Итог: check_collisions 33 846 -> 30 360 (-10 %), работа кадра
657 882 -> 654 990. Крупной статьи в физике нет: остаток размазан по
десятку честных проверок.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Позиция заводилась с НЕПОЛНЫМ диагнозом. Я приписал 190 260 тактов
пометке от факела (пламя лежит в ячейке правого соседа, то есть поверх
чомпера, и запекается каждый кадр). Правка по этому диагнозу не дала
ничего: 769 002 против 768 684.
Зонд pop_dbg_kind показал факт: все 312 перерисовок прогона — вид
POP_RD_CHOMP, полная, и ни одной от факела. Собственная пометка чомпера
просто перебивала пометку соседа.
Настоящая причина нашлась сверкой с animate_chomper (seg007:0448).
Оригинал заканчивает её так:
if ((curr_modifier & 0x7F) < 6) redraw_at_trob();
то есть перерисовывает чомпер только пока фаза меньше 6 — пять кадров из
пятнадцати. Это не оптимизация оригинала, а следствие таблицы поз:
chomper_fram1 = {3,2,0,1,4,3,3}, и с фазы 5 до конца круга поза одна и та
же. Мы метили тайл каждый кадр, пока trob жив, а живёт он всё время, пока
Кид в том же ряду — то есть платили полный draw_tile плюс heal 32x64 за
неизменную картинку в двух третях кадров.
Сделано:
1. пометка только при фазе < 6; на фазе 5 — обе страницы дабл-буфера
(она последняя рисуемая, её поза обязана лечь на обе; вторую страницу
пометка догоняет в кадре фазы 6, где поза та же — CHOMP_FRAM1[6] == 3);
2. новый вид POP_RD_CHOMP_ANIM -> pop_chomp_anim_draw: три блита графики
чомпера поверх свежего пламени, без heal и без остальных слоёв — порт
ветки redraw_frames_anim (seg008:0211), где оригинал делает ровно
draw_tile_anim_topright / draw_tile_anim_right / draw_tile_anim и
никакого wipe;
3. приоритет полной перерисовки над anim в pop_set_redraw: у оригинала
это два независимых счётчика и full побеждает, а у нас вид один на
тайл, и без проверки исход решал бы порядок trob'ов в списке.
Обе половины работают — замер даёт 40 % полных перерисовок и 60 % лёгких.
Работа 768 684 -> 657 882 (медиана), зелёная 294 510 -> 183 420. В 40 %
кадров цена прежняя: там поза реально меняется, это честная работа.
Циан не сдвинулся ни на такт, то есть надежда P3 (Кид перестанет будиться
каждый кадр) пока не оправдалась — метки продолжают его будить.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Синяя фаза (286 518) разложена зондами на 12 участков, process_trobs
(89 784) — на три.
Гипотеза, с которой я входил в замер, ОТВЕРГНУТА. Я ждал, что дорого
обходятся банковые трамплины на спецсобытиях уровней — по аналогии с
pop_clip_char_top, где трамплин ради одной проверки стоит 8 892. На деле
три спецсобытия (skel, mouse, killed_shadow) вместе стоят 1 650: они
гейтятся внутри и на уровне 11 выходят сразу.
Настоящие статьи синей:
физика двух Char 105 246 (61 266 Кид + 43 980 страж)
heal двух Char 67 734
луч видимости стража до 37 032 (вместе с frame_timers)
pop_ctrl_tick 18 648
логика стража 19 932
Физика съедает 13,7 % работы кадра при том, что ОБА персонажа стоят и кадр
позы не меняется. Цена измерена, причина нет — это отдельная позиция P2a.
Луч видимости считается каждый кадр, хотя никто не двигался: гейт по смене
позиции/комнаты — позиция P2b, ждём −30 000. heal отдельной правки не
требует, он уйдёт вместе с P1/P3.
process_trobs: префетч кодов тайлов с маппингом окна 0 — 11 058, обход
самих trob'ов ~43 000 (pop_trob_modif зовётся банковым вызовом на КАЖДЫЙ
trob, хотя комната одна), два факела ~36 000. Цена одного pop_pot_b
измерена отдельно: 17 346, и это единственная группа в распределении —
значит в кадре его зовут только факелы. Пиксели пламени 16x18 — 1 716,
то есть 10 % цены.
Заодно посчитано, достижим ли период 3 растра: снять надо 338 000, а сумма
ВСЕХ известных позиций даёт 357 000, из которых 160 000 держатся на одной
(P1). Цель достижима, но без запаса.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Замер 11/15 показал, что loose-механика берёт 28 872 такта в комнате, где
не анимируется ни одна плита и не летит ни один кусок. Раскладка зондами
m9..m12: два цикла по тайлам 9 852, обход 14 слотов mob 12 090, поиск
куска над головой Кида 5 868 (там ещё и банковый трамплин).
Два гейта:
loose_any (статик pop_map.c) — «идёт ли анимация плит». Ставят пять мест
записи ненулевой фазы: make_loose_fall, ветка потолка в check_press,
do_knock для обоих рядов и восстановление фазы из room_modif при входе в
комнату. Снимает его сам цикл, по факту прохода, в котором не осталось
ни одной живой фазы.
pop_mob_busy (резидент pop_state.c) — «занят ли слот падающего куска»
(active или дочистка clean). Ставит mob_alloc, снимает обход по факту
пустой таблицы. В резиденте, а не в pop_room.c, потому что читает его
pop_map из банка 3, а писучие статики банкового модуля наружу не видны.
Гейт отвечает не на «есть ли в комнате плиты», а на «идёт ли анимация»: у
лежащей плиты-потолка фаза нулевая, и крутить нечего (вопрос пользователя).
Асимметрия намеренная — ложная единица стоит одного холостого прохода,
ложный ноль стоит застывшей навсегда плиты, поэтому взвод стоит рядом с
КАЖДОЙ записью, а снятие только по факту пустого прохода.
Стало: 132 / 996 / 546, вся функция 28 872 -> 2 760. На кадре работа
801 768 -> 767 928. Ожидание по реестру было -28 000.
Покрытие: новый phys_loose_gate_survives_room_change на пятое место взвода
(фаза восстановлена входом в комнату) — единственное, которое не прогонял
ни один тест, и дающее самый тихий отказ. Мутационная проверка: со снятым
взводом тест падает (фаза 3 вместо 4).
Заодно отладочный старт сразу в целевую комнату: make ROOM=15 POS=2
(дефолт), roomtest стартует в 11/15 с Кидом в (0,2). kid_init ставит
x = x_bump[col] + TILE_SIZEX, а это левая граница СЛЕДУЮЩЕЙ колонки — с неё
физика относила Кида в тайл чомпера, и он погибал на старте (найдено
пользователем). Сдвиг внутрь на 2: колонку определяет весовая точка кадра,
поэтому число снято замером, а не выведено геометрией.
План работ между сессиями — docs/perf_registry.md §4: очередь позиций со
статусами, текущий бюджет сцены, рецепт её воспроизведения и метод замера.
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>
Порт правила оригинала, разобранного в 272cf8f. y_to_row_mod4 даёт −1 и
для куска выше потолка, и для ушедшего ниже комнаты; get_tilepos_nominus
сводит оба в тайл 30, а объекты тайла 30 рисуются в redraw_needed_tiles
ПЕРВЫМИ, до всего обхода тайлов.
Что сделано:
- defer = 0 для таких кусков: они под всем, включая Кида. Раньше
сравнение рядов читало −1 как «обходится последним» = «поверх всего»;
- оверлею отдаётся ориентир 3 («раньше любого ряда 2,1,0») вместо сырого
−1 — гейт other_overlay_tile перестал отбрасывать возврат соседа, из-за
чего тело плиты не возвращалось и оставался только её торец из
переднего слоя;
- в набор перекрываемых тайлов добавлена СВОЯ клетка (только для этого
случая: у куска в обычном ряду объект вливается в midtable после частей
своего тайла, и перерисовывать её нельзя).
Отладочная обвязка разбора (журнал решений оверлея, маска перекрывающих
тайлов) снята; счётчик перерисовок за кадр в pop_redraw_needed оставлен —
он дешёвый и пригодится для HEAL-WIDTH.
ЗАМЕР 13/23, 3032 кадра, против тега mob-order-B-start:
работа 888 984 -> 913 848 (+24 864)
синяя 159 804 -> 159 810
зелёная 427 242 -> 440 418 (+13 176)
циан 378 864 -> 393 000 (+14 136)
Период кадра не изменился: 4 растра в 23 кадрах, 5 в двух.
Зелёная вышла за растровый кадр (440 418 против 430 000). Детализация:
pop_loose_tick 185 826, из них pop_loose_mob_tick 168 180; тробы +
redraw_needed 337 800. Разбор и план возврата тактов — HEAL-WIDTH.
Визуальная проверка комнаты 14 за пользователем: поймать кадр с куском
снимками мне не удалось.
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>
Правый конец падающего куска лежал поверх соседней плиты. У оригинала
мид-оверлей — draw_tile2(), и его последний вызов draw_loose(0) рисует
передний торец плиты; у loose bottom_id = 0, поэтому больше его не рисует
никто, и в overlay_mid_tile торца не было вовсе.
Заодно снят вопрос про клип объекта: add_mob_to_objtable ставит куску
clip.right = 40, но клип применяется только при chtab_flip_clip[chtab_id],
а для chtab_6_environment там 0 — поле игнорируется и в оригинале. Значит
«клипа нет» у нас верно, MOB-CLIP-RIGHT закрывается как несуществующий.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Уровень 11 комната 14: плита ряда 2 уходит в комнату снизу на первом же
move_loose (спавн y=191 при границе ряда 188), а у нас кусок в чужой комнате
не рисовался вовсе — плита исчезала мгновенно. Оригинал (seg007:13E5)
рисует его ещё три кадра, выглядывающим из нижней кромки (+192), и
симметрично из комнаты сверху (−189).
У куска появилась экранная координата draw_y; по ней идут отрисовка, heal,
порядок относительно Кида, пометки соседа и оверлей, отбор в проходе — по
ней же, а не по комнате. Ссылки вверх/вниз кэшируются.
Цена (13/23): зелёная 419 562 -> 427 242, работа 880 272 -> 888 984.
Первый вариант стоил втрое дороже (445 248) из-за безусловной пометки по
прошлой нарисованной позиции — у куска из соседней комнаты она в 192
пикселях, и объединение растягивалось на весь экран; гейт вернул 18 000.
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>
Инструмент: pop_dbg_kidobj печатает то же, что вывод, добавленный
пользователем в SDLPoP add_kid_to_objtable — tilepos/frame/act/col/cols/rows
(+ наша комната). Считает по set_char_collision и set_objtile_at_char.
Выключен по умолчанию (DBG_KIDOBJ 0), включается одним define; вывод
забирает брейкпоинт MAME на резидентном pop_dbg_trap.
Сверка подтвердила фикс a498255: окно перехода совпало кадр в кадр, за бой
на уровне 1 комната сменилась один раз, на уровне 8 у шва 24/18 — четыре
раза на 387 кадров боя, и все четыре на РАЗРЕШЁННЫХ кадрах (170/164/170/165).
Смен на запрещённых кадрах во всей трассе нет.
Разбор целиком — BUGS_CLOSED.md#seam-fight-flicker, включая невыясненное
расхождение поля cols.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Пользователь: при бое на шве (Кид в 18, страж в 24) комната перерисовывается
то одна, то другая — драться невозможно.
leave_room запрещает уход не только на развороте, подъёме-с-зацепа и
вставании из приседа, но и на всей боевой анимации: кадры 150..162 и
166..168. У нас были только первые три условия, поэтому отступающий и
наступающий Кид пересекал границу почти каждый кадр.
Оригинал пускает смену комнаты только на «легальном» кадре вроде 170.
Известный побочный эффект — Trick 35 «retreat without leaving the room»;
в SDLPoP его чинит FIX_RETREAT_WITHOUT_LEAVING_ROOM, по умолчанию
выключенный, так что портируем оригинал.
Сторона стража проверена отдельно и расхождений не дала: play_guard_frame
(seg000:0F48) ухода из комнаты не содержит вовсе, страж меняет комнату
только через follow_guard (есть, pop_guard_follow) и check_guard_fallout
(есть, pop_guard_fallout).
Банк 3 +10 Б, резидент без изменений. Host-тесты 5106 проверок чисто.
Сам бой у шва не воспроизводился — проверка за пользователем.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Уровень 8 комната 24: после первого же выхода Кида страж телепортировался
в колонку 0, а ряд 0 там пустой в колонках 1..3 — страж падал с ряда 0 на
ряд 2 и разбивался.
leave_guard (seg002:02F5) кладёт в guards_tile get_tilepos(0, row), то есть
обнуляет колонку НАМЕРЕННО: позицию по горизонтали несёт guards_x, туда же
она и пишется. enter_guard берёт x оттуда всегда. Мы же считали x из
tile % 10 (то есть из нуля), а запомненную брали только у трупа.
Заодно портирован pos_guards (seg003:0913): при загрузке уровня guards_x
пересчитывается из колонки тайла, а файловое значение выбрасывается — в
комнате 24 уровня 8 там 255.
Проверено в MAME: три входа подряд дают одну и ту же позицию (x 156, ряд 0),
страж стоит на полу. Host-тесты 5106 проверок без расхождений.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Постановка пользователя: ручной проход уровня с логом фаз + комната/тайл,
дальше оптимизация конкретной комнаты. Записано вместе с блокером —
брейкпоинтами это делать нельзя (эмуляция падает в проценты от реального
времени, играть невозможно); варианты: тап на порт бордюра в Lua-мосте
(не привязан к адресам кода) или самозамер программой в растрах (работает
и на железе).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2435 кадров сцены каскада. Максимумы по секциям: работа 880 170,
синяя 159 774, зелёная 419 520, циан 380 244 — против 878 550 / 158 880 /
419 526 / 379 488 у ec1f384. Все четыре в пределах шума прогона, зелёная
совпала до 6 тактов. Распределение периода совпало кадр в кадр: 4 растра
в 24 кадрах, 5 в одном, остальные 3.
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 начинается падением; разбившись на кнопке открытия решётки
(комната 3, тайл 2,1), Кид в оригинале открывает решётку НАСОВСЕМ, а у нас
она закрывалась обратно. Чинить пришлось три места.
1. pop_phys_tick выходил по pop_kid_dead, то есть check_press до трупа не
доходил вовсе. У оригинала play_kid_frame гейтится только Char.room
!= 0. Кнопка получала одно нажатие — в кадре смерти, потому что флаг
ставит land() уже внутри цепочки. «Труп не шевелится» держит внутренний
выход в kid_phys, он остался.
2. Портирован died_on_button: открывалка → пол + связь дёргается типом
«щебень» (открыть насовсем), прочие кнопки → TILE_STUCK. Ветки по
Char.alive в check_press не было вовсе.
3. Рестарт уровня восстанавливал только foretable, а died_on_button
оставляет таймер связи нажатым (trob кнопки умирает сразу — тайл уже
пол). Остаток переживал респавн, и кнопка рисовалась нажатой с первого
кадра. pop_level_reset_tiles теперь возвращает и LINKMAP.
Проверено в MAME по всему циклу: смерть → FF (открыта навсегда), респавн →
кнопка цела и не нажата, второе падение → снова ломается.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Кид гибнет на кнопке открытия решётки (уровень 7, комната 3, тайл 2,1) —
в оригинале решётка открыта насовсем, у нас отжимается. Мало того, что
died_on_button (seg007:776) не портирован: pop_phys_tick целиком выходит
по pop_kid_dead, так что check_press до трупа не доходит вовсе. У
оригинала play_kid_frame гейтится только Char.room != 0.
Фикса пока нет — запись в BUGS_OPEN.
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>
XOR-приём оригинала требует чтения видео-ОЗУ (нельзя) и несовместим с
0xFF-прозрачностью; план — отдельный атлас тени.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Флаг «no blue» лежит в нулевом бите модификатора стены В ФАЙЛЕ уровня, а
отрисовка (как и оригинал) ищет его в СТАРШЕМ: load_alter_mod переводит
их сдвигом на 7. Ветку стен мы пропускали намеренно — связи кладки
считаются по соседям, — но noblue из соседей не выводится.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Тень на уровне 6 стояла на двух страницах дабл-буфера в разных позах.
Отрисовка брала image из кэша kid_frame/pop_gframe, который наполняет тик,
а снимок пропуска кадра писала по Char.frame — расхождение застревало
навсегда, потому что снимок совпадал и страница больше не перерисовывалась.
Кадр теперь грузит сама отрисовка, как в оригинале (add_*_to_objtable →
load_fram_det_col, seg008:22F0/2324). Разбор — BUGS_CLOSED.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Уровень 4: в анимации ухода на следующий уровень контур Кида обрезался не
правой гранью портала, а раньше — брался клип подземного проёма, а
дворцовый шире.
draw_leveldoor считал кромку как xh*8 + 48, без дворцовой поправки. В
оригинале строкой ниже стоит (seg008:1429):
if (custom->tbl_level_type[current_level]) leveldoor_right += 8;
Значение читает clip_char как правую границу клипа персонажа — отсюда
ранняя обрезка. Расхождение было осознанным и отложенным: в коде стоял
комментарий «+8 у palace-уровней — на уровне 1 не применяется», дворцовых
уровней тогда в порту не было. tbl_level_type[4] = 1, там и проявилось.
pop_palace выставляет pop_bg_load из того же tbl_level_type, что читает
оригинал, так что эквивалент дословный.
Попутно найдено и НЕ починено (заведено отдельным багом
LEVELDOOR-STARTROOM-WIPE): в той же функции оригинал в СТАРТОВОЙ комнате
кладёт затирающий прямоугольник вместо лестницы, со своей дворцовой/
подземной разницей 48/39 и сдвигом 2 px, а мы рисуем марш 144 безусловно.
Видно только при приподнятой створке входной двери, поэтому на обходах
уровней 1-4 не попалось.
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>
Замер 418 кадров, зонды A/C/D/E (база модуля roomtest 0x42AD — совпала с
прошлой сборкой, фикс ушёл в банк).
работа синяя зелёная циан
af189a1 916 458 142 830 546 900 270 510
40f0d46 873 930 158 874 417 630 379 482
ec1f384 878 550 158 880 419 526 379 488
Фиксы второго уровня на бюджет не повлияли: +4 620 работы и +1 896 зелёной
— шум прогона. Период: 3 растра в 392 кадрах, 4 в 24, 5 в одном, то есть за
бюджет вылезает только сам каскад.
Синяя и циан в бюджете 400 000; зелёная 419 526 — 1,05x цели и ниже
растрового кадра 430 000. Остаток на потом: раскол draw_tile на узкие части
и идея G8 (инвалидация соседнего тайла полосой 28 px вместо целых 60).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>