Обе сборки прогнаны полным циклом (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>
tile_at_kid (guards.c) считала колонку честным / и %, хотя резидентная
POP_TILE_DIV — это и есть tile_div_tbl оригинала, и остальной порт давно на
неё переведён. У SDCC z80 пара / и % над int это __divsint плюс __modsint,
который внутри снова зовёт __divsint — ~5 400 тактов на вызов.
Зовут её в ЦИКЛЕ по колонкам между стражем и Кидом
(check_can_guard_see_kid, seg003:761). Когда Кид у шва, его curr_col = −1,
луч тянется через всю комнату: замер дал ВОСЕМЬ пар делений за кадр,
около 43 000 тактов = 10 % растрового кадра, в фазе логики. После фикса
таких вызовов не остаётся.
Как ловилось: брейкпоинт на __divsint с печатью адреса возврата дал
ret=C033 восемь раз за кадр; остановка на нём с dasm при замапленном банке
показала HL−65 / ld de,#14 / call __divsint по смещению 0x24 банка 1.
Снята и ложная тревога из прошлого коммита: пролог pop_char_fore на шве НЕ
разбухает до 134 730 — это была ошибка зонда (адрес fore_tile в банке
совпадает с кодом других банков, в интервал попадали чужие срабатывания).
Чистый замер: пролог 16 950, как и в середине комнаты. Урок записан в
«Как мерить» в docs/perf_backlog.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Три шага пункта 0 из docs/perf_backlog.md.
1. Ранний выход из wall_pattern_palace по окну fore-клипа: узор целиком
лежит в x [xh*8, xh*8+32), y [dmy-59, dby], и если окно его не задевает
— возврат до первой заливки. Замер: от входа в узор до конца всего
прохода 6 055 тактов вместо ~60 000 на тайл.
2. Отсев КАЖДОГО декаля (wp_blit) по реальному габариту. pop_blit_b тоже
отсеивает до маппинга страницы, но по заведомо большему 64x64 — куски
кладки высотой 3..12 px он пропускал и платил полный
atlas_image + gfx_w0_map (~9 760 тактов на блит), чтобы там обнаружить,
что рисовать нечего. Размеры сняты из каталогов атласов и заданы верхней
границей по группе.
3. То же для ПОДЗЕМЕЛЬЯ: ранний выход wall_pattern (габарит выше — левая
марка уходит на dby+POP_YOFF-67) плюс wp_blit на RNDBLOCK, обоих
разделителях и обеих марках.
Замер (уровень 4, комната 18, Кид сдвигается читом ] по пикселю, skip
выключен, шесть тайлов в fore-окне, три из них — дворцовая стена):
тайл стены ~12 700 вместо 60 000-74 000, fore-проход целиком 70 681 вместо
171 693, работа за кадр 419 839, период 3 растровых кадра вместо 4.
Уровень 1 (подземелье) проверен снимком — кладка, марки, разделители на
месте.
Остаток в проходе — пролог pop_char_fore 17 208 тактов (пункт 1 backlog'а).
Отдельно записано: на ШВЕ пролог разбухает до 134 730, причина не разобрана.
BUG-CHEAT-FIGHT-1 (заведён 2026-08-07) закрыт по своему же плану: ветка
ROOMNAV после kid_init/pop_kid_hp_reset гасит состояние схватки
(Kid.sword = SWORD_0_SHEATHED, holding_sword, offguard, guard_refrac).
Меч в инвентаре не теряется. Это болезнь чита: в оригинале телепорта между
комнатами нет и в режим боя без соперника попасть нечем.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ОПТИМИЗАЦИЯ. Метка «фон трогали» была одним union-прямоугольником на
страницу, и три факела комнаты склеивались в полосу x 40..280 на всю
комнату — Кид, стоящий между крайними факелами, терял пропуск перерисовки
и каждый кадр платил полным fore-проходом. Теперь это маска тайлов
(uint16_t pop_cd_dmask[2][3]: бит = колонка, слово = ряд, набор = страница),
проверка — три AND через резидентный pop_cd_hit. Гранулярность тайла — это
гранулярность оригинала (redraw_frames_anim[tilepos], set_wipe;
подтайловое уточнение там только по высоте, wipe_heights). Замер на (1,7):
циан 233 515 -> 24 781, работа за кадр 517 609 -> 306 553, период 4 -> 3
растровых кадра.
BUG-LOOSE-BUTTON-1. Упавшая плита не нажимала кнопку. Три слоя: порт
loose_land не звал trigger_button вовсе; pop_room_col_landing считала
площадкой только чистый пол, а у оригинала их семь (пол, пика, обе кнопки,
зелье, оба факела); сигнал приходил в момент ОТРЫВА плиты, из-за чего
ворота начинали открываться, пока она ещё в воздухе. Нажатие в оригинале
ОДНО, но с button_type = tiles_14_debris — это «открыть НАСОВСЕМ»
(modifier 0xFF), и кнопка съедается. Посадка в комнате снизу переехала на
новый сигнал pop_loose_exit (взводит mob_tick_one, когда кусок ушёл ниже
поля).
BUG-GATE-FF-1. 0xFF был перегружен: сторожевое «тайла нет» в gate_modif и
живое «открыто навсегда» из trigger_gate. gate_passable заворачивал Кида в
воротах, нарисованных открытыми. Мёртвая ветка убрана.
BUG-TORCH-CHOMP-1. Чомпер (0,7) комнаты 23 healит x 224..255 / y 30..93 и
стирал пламя факела (0,6), которое рисуется в ячейке правого соседа. Фикс —
запекать пламя (GFX_BANK_NORMAL): у факела все девять кадров на общем
канвасе 16x18 и непрозрачны, протухнуть в ОЗУ-копии нечему, а heal чомпера
сам возвращает огонь и кладёт челюсти поверх — z-порядок как в оригинале.
Пузырёк зелья так нельзя (ползёт вверх, нужен heal) — остаётся в SPRITE.
Заведено открытым: died_on_button (seg007:776) не портирован — нужен тайл
tiles_5_stuck в атласе.
Проверено пользователем в MAME (уровень 4: плита 16(1,1) -> кнопка 17(0,1)
-> ворота 23(0,9); комната 23 с чомпером и факелом); make -C tests-host —
все 5 наборов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BUG-GUARD-IX-1 — «зависание» в бою со стражем. Не зависание: главный цикл
крутился, а отладочная локаль frozen сама становилась ненулевой. Корень —
у main затирался IX (0xBFFA -> 0xBF00), и все его локали адресовали живой
стековый мусор. Затирал check_chomped_guard: coll_row() пишет по
flags + scan_off, длину берёт из win_lo/win_hi, а scan_off выставляла
только coll_scan_prepare() из пути Кида. Ряд стража писался по смещению
Кида длиной стража и при Киде у правого края комнаты вылезал за flags[13]
— прямо в сохранённый IX. Фикс: coll_scan_prepare() в начале
get_row_collision_data(); заодно чинится расчёт (scan_left0 задаёт x
колонок, чомпер-коллизия стража считалась по координатам Кида).
На уровне 1 не проявлялось: бой идёт левее середины, запись оставалась
внутри массива — данные были неверны молча.
BUG-GATE-SEAM-ROW1 — решётка в шве не анимировалась. Плита комнаты 1
открывает ворота комнаты 8 в (1,9), видимые через левый шов, а
change-driven редрой смотрел только m[9] (ряд 0) и перерисовывал жёстко
draw_tile(0,0). На уровне 1 та же связка работала лишь потому, что
решётка соседа стояла в (0,9). Фикс: сигнатура по всем трём рядам,
маска изменившихся рядов в seam_rows (переживает оба кадра дабл-буфера),
pop_room_redraw_seam_left(rows) перерисовывает только помеченные. Плюс
ряд выше (changed | changed>>1): верх решётки (draw_tile_anim_topright,
seg008:0568) рисует тайл над-справа от ворот, без этого чёрный
треугольник над ними оставался статичным.
Проверено пользователем в MAME (бой в комнате 18; анимация решётки в
стартовой комнате), детекторы IX висели без починки и не сработали;
make -C tests-host — все 5 наборов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Разбивка логического кадра брейкпоинтами (уровень 4, Кид неподвижно на
(1,7)): работа 517 609 тактов = 120% растрового кадра, период 4 кадра.
Циан (PROF(6)) — 233 515 = 54% растрового кадра, из них
pop_char_fore(KID) = 171 693.
Внутри fore-прохода шесть тайлов, и два из них — СТЕНА ряда 2 под ногами
Кида — стоят 74 310 и 65 553. Дворцовая кладка на тайл: 6 wpp_fill
(~3 100) + 5 pop_wall_b (~9 760) ≈ 60 000. Окно клипа в этот момент
x 229..241, y 106..147 (прочитано из pop_t_fclip_*), верхний кусок узора
стоит на y=157 — не пересекается вовсе, тайл (2,6) промахивается и по x.
То есть 139 863 такта за кадр рисуют ноль пикселей; снятие уводит период
с 4 растровых кадров на 3.
Записано пунктом 0 в docs/perf_backlog.md с тремя вариантами лечения.
Состав узора сверен с SDLPoP (seg008.c:1943) — порт дословный.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Симптом (наблюдение пользователя): кирпичи дворцовой кладки правильного
цвета, а швы между ними — нет.
Корень шире, чем швы. init_game_main (seg000:164) подменяет ДВЕ записи
16-цветной палитры сразу после загрузки:
// (blood, hurt flash) #E00030 = red
set_pal(12, 0x38, 0x00, 0x0C);
// (palace wall pattern) #C09850 = light brown
set_pal( 6, 0x30, 0x26, 0x14);
Подтверждено дампом палитры живого SDLPoP: PAL[6] = 48,38,20,
PAL[12] = 56,0,12. У нас в таблице VGA16 стояли стандартные VGA-цвета
(42,21,0) и (63,21,21). Цветом 6 рисуются швы дворцовой кладки
(blitters_46h_mono_6), цветом 12 — кровь чомпера и вспышка урона, так что
промах был не только в стенах.
Заодно исправлена вспышка урона в roomtest.c: было flash_bg(255,85,85)
(стандартный brightred), стало (224,0,48).
Проверка: ряд стен уровня 4 теперь совпадает с эталоном SDLPoP по всем
восьми цветам и их количествам один в один (5333/4013/3110/2065/1785/1372/
1075/447). Остаточное различие значений — только наше масштабирование
6->8 бит: (v*255)/63 против v*4 у SDLPoP, то есть (194,153,80) против
(192,152,80).
ПОПРАВКА к f107711: там записано, будто рисунок кладки не совпадает с
оригиналом из-за замены PRNG. Это неверно — POP_PRANDOM_EXACT по умолчанию
1, работает ассемблерный LCG оригинала, и совпадение счётчиков цветов это
подтверждает. Расхождения по PRNG нет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Во дворце тело стены — не кирпичи-спрайты, а шесть СПЛОШНЫХ ЗАЛИВОК плюс
пять моно-разделителей поверх (seg008:1946). Порт целиком:
- gen_palace_wall_colors (seg000:1942): 3 ряда × 4 подряда × 11 колонок = 132
цвета, сид = номер комнаты, подряды 1/3 из 0x61..0x64, подряды 0/2 из
0x66..0x69, соседние по горизонтали не повторяются. Одиннадцать колонок,
а не десять: заливки 3 и 5 берут цвет СЛЕДУЮЩЕЙ колонки. Пересчёт на
смене комнаты — там же, где сбрасывается кэш кладки (wall_pattern_reset).
Таблица не static: writable-данные банка живут в _DATA/W2.
- Геометрия заливок дословно из add_wipetable(layer, left, bottom, height,
width): прямоугольник = x..x+width-1, (bottom-height+1)..bottom.
- Пять prandom(2) на тайл кэшируются так же, как подземельные решения
(wp_a/wp_b переиспользуются — наборы в одной комнате не сосуществуют).
Сохранён квирк порядка: при which_part == 0 разыгрывается ОДНО значение, и
нижний разделитель берёт ПЕРВОЕ из серии, а не пятое.
- Заливки режутся по окну fore-клипа: иначе легли бы поверх областей, которые
в этом кадре никто не восстанавливает. Вне fore-прохода — pop_cd_touch,
потому что bar идёт мимо pop_blit_b.
- wall_fram_bottom / wall_fram_main во дворце НЕ рисуются (seg008:576, 711) —
и в горячей половине слоя (pop_bg.c), и в холодной (pop_room.c).
- Упаковщик: дворцовые wall-id 3..17 пакуются силуэтом в цвете 6 общей
16-цветной палитры (blitters_46h_mono_6). В подземелье те же id —
обычные кирпичи, поэтому mono только у паласного набора.
ИЗВЕСТНОЕ РАСХОЖДЕНИЕ: рисунок цветов не совпадает с SDLPoP попиксельно,
потому что наш prandom — 16-битный xorshift, а не LCG оригинала (замена
сделана раньше по бюджету кадра, prng_alternatives.md). Совпадают
геометрия, диапазоны цветов и правило «соседние не повторяются».
Проверено в MAME: уровень 4 — песочный мрамор с разделителями, структурно
как эталон SDLPoP; уровень 1 не изменился. tests-host 5/5.
Остаётся расхождение по двери уровня (мы заполняем проём плетёнкой целиком,
оригинал рисует несколько кусков лестницы) — отдельным шагом.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порт семи мест seg008, расходящихся по tbl_level_type, плюс общая дыра
порта, которая на дворце стала видна.
Паласные ветки (все — «в подземелье этого нет»):
- doortop_fram_top / doortop_fram_bot (seg008:413, 506): декоративная панель
над воротами. У шва она и есть тот «ковёр», которого не хватало.
- stripe_id соседа слева (seg008:486): орнаментная лента под окнами. Она
непрерывная, потому что stripe_id = 145 у пола, кнопок, зелья, loose,
чомпера и меча; без неё лента шла кусками (только blueline).
- полоска на стене id 84 (seg008:510), при (modifier & 0x80) == 0.
- blueline_fram3: условие `num == !!level_type` — в подземелье пропускается
num==0, в паласе num==1 (seg008:501).
- левая половина кнопки-opener без пола (id 148) — только подземелье
(seg008:628).
- склянка зелья: id += 2 во дворце (seg008:747).
- remove_loose возвращает ТИП УРОВНЯ, и он ложится модификатором пустой
клетки от упавшей плиты (seg007:846/1083).
Потерянный вывод (НЕ паласное расхождение, просто заметили здесь):
draw_tile_anim_topright (seg008:0568) не был портирован вовсе — верх ворот,
который рисует тайл НАД ними: маска 68 (mono, чёрным) + door_fram_top
[(modifier>>2) % 8] = 60..67. Ids 60..68 в атлас не паковались. Симптом —
чёрный клин над воротами; нашёлся сравнением с эталоном SDLPoP
(--screenshot) и трассой add_backtable.
Флаг тайлсета pop_palace вынесен в резидент (pop_tile.c): по нему расходятся
ветки в банке 7 (полная отрисовка), банке 2 (fore-проход) и банке 3
(модификатор пустой клетки) — читается напрямую, без трамплина.
Известное расхождение: модификатора ряда СНИЗУ у нас нет (pop_t_below —
только fg), поэтому паласная панель над воротами в комнате снизу не
рисуется. Помечено в коде.
tests-host: 5/5 (добавлен include-путь до pop_bg_atlas.h и стаб pop_palace).
Проверено в MAME: уровень 4 совпадает с эталоном SDLPoP в рядах 0-1
попиксельно (кроме фазы пламени); уровень 1 не изменился.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Симптом (эталон SDLPoP против нашего кадра, уровень 4): дворцовая геометрия
рисовалась подземельными красками — сине-серые арки вместо песочных,
бирюзовая дверь уровня вместо кремовой, сланцевый пол вместо
коричнево-розового. Бирюза и зелень — это dungeon-слоты 0x5E (0,117,76) и
0x5F (0,165,157).
Причина в порядке старта: атласы (и вместе с ними палитра тайлсета) грузятся
ДО initgraph, потому что тот снимает DSS-страницу W0. А kid.pal — ЕДИНАЯ
игровая палитра, собранная из VDUNGEON (pop_pack_kid.py build_palette), —
читается ПОСЛЕ initgraph и затирает слоты 0x50..0x6F.
pop_bg_pal_apply() возвращает 32 записи текущего набора; зовётся сразу за
gfx_pal_sync(). На смене уровня палитра по-прежнему едет внутри
pop_bg_load — там initgraph давно позади.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порт tbl_envir_ki[tbl_level_type[level]] (seg000:1108): оригинал под один и
тот же набор id грузит РАЗНЫЙ .DAT — VDUNGEON или VPALACE.
Упаковщик (toolchain/pop_pack_bg.py):
- аргумент набора: `pop_pack_bg.py dungeon|palace`. Каскад каталогов —
сначала свой набор, потом чужой фолбэком (в распакованном data/ res230/
231/348 есть только в VDUNGEON, два десятка — только в VPALACE).
- ОБА набора пакуются по одному объединению id, поэтому раскладка
id -> (страница, idx) общая и заголовок один: коду достаточно подменить
имена файлов.
- PALACE_ENV_IDS: 78/80/82 (doortop_fram_bot), 81/83 (doortop_fram_top),
84 (полоска стены), 145 (stripe_id) — их рисует только палас, render_room
про них не знает.
- ENV_SHIFT 5 -> 4: с паласными кусками страница 2 переваливала за 16 КБ
(16 996). Цена — 10 страниц EMM на набор вместо 5, при 215 свободных.
- *tile.pal: 32 записи (env 0x50..0x5F + wall 0x60..0x6F) на набор. Полная
kid.pal не трогается — Кид, страж, меч и зелья в других слотах.
Движок:
- pop_level_type() (tbl_level_type, SDLPoP data.h:840): дворцовые уровни
4, 5, 6, 10, 11, 14. Живёт в pop_level.c, потому что по типу расходятся
не только атласы, но и ветки отрисовки seg008, кладка стены и модификатор
пустой клетки от упавшей плиты (remove_loose, seg007:0EB8).
- pop_bg_load(set): no-op при том же наборе, при смене выгружает старый
(иначе текут 12 EMM-страниц) и правит 32 записи палитры в ОБЕ страницы
дабл-буфера. Зовётся на старте и на границе уровня, не в кадре.
- Путь к атласу склеивается на месте (bg_path): двадцать строк-имён в банке
— лишние полкилобайта.
Проверено в MAME: уровень 1 (подземелье) рисуется как прежде; уровень 4
(-DFIRST_LEVEL=4) — дворцовые арки, окна, пол, решётчатая дверь уровня,
Кид не перекрашен. Стены пока чёрные: паласный wall_pattern — шаг C.
tests-host: 5/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>
Сближение с SDLPoP, замером НЕ подтвердилось — фиксирую как есть.
- FORE_ANY[32]: есть ли у кода тайла хоть что-то в переднем слое. Порт
ранней проверки оригинала `if (tile_table[curr_tile].fore_id == 0) return;`
из начала ветки default в draw_tile_fore (seg008:0D15), расширенной нашими
спецслучаями (стена 20, пики 2, чомпер 0x12, нижняя грань loose 11 — они
рисуются мимо fore_id). Была в самом конце цепочки сравнений.
- ft_code/ft_x/ft_dmy: контекст тайла считается один раз на тайл, как глобалы
curr_tile/draw_xh/draw_main_y у load_curr_and_left_tile. Код тайла читался
дважды (fore_tile и снова fore_only_tile), координаты — в каждом листе.
Замер (Кид на 2,4 в щебне, MAME): pop_fore_over_char 58 764 -> 58 866, то
есть в пределах шума. Ранний выход не срабатывает — в футпринте Кида тайлы
почти всегда С передним слоем; снятое второе чтение кода съедено проверкой
FORE_ANY и записью контекста. Отрисовка не изменилась: щебёнка блитится теми
же параметрами (x=150 y=208 sx=22 w=10 h=2).
Где время на самом деле: ~16 000 тактов фиксированных накладных на КАЖДЫЙ
блит фона независимо от размера (atlas_image + gfx_w0_map + чтение w/h через
окно 0 + арифметика клипа + unmap). Блит щебёнки 10×2 обходится в 21 168,
факел 16×22 — в 31 000 при самом gfx_blit_noclip 8 706.
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>
Оригинал (seg004:0004) индексирует флаги перекрытия колонкой ВНУТРИ
разрешённой комнаты и хранит рядом её номер, поэтому решётка комнаты 8
остаётся в своём слоте и после перехода 8->6: переход флага 0->1 виден,
bumped() срабатывает. У нас индекс — колонка отрисованной комнаты, тот же
тайл менял слот, и enter_room вынужден был выбрасывать историю целиком —
на кадре входа бампа не было, и Кид с разбега уходил сквозь закрытые ворота.
Вариант B (сдвиг вместо тега комнаты): при БОКОВОМ переходе история не
выбрасывается, а перенумеровывается на 10 слотов. check_leave двигает x
ровно на ∓140 = 10 тайлов, координата грани едет на те же 140 вместе с
габаритом Кида — сами флаги инвариантны, меняется только номер слота.
Сдвигаются curr/above/below (prev на следующем кадре всё равно перезапишет
move_coll_to_prev), освободившиеся слоты = 3 «уже перекрывал».
Вверх/вниз и прочие входы в комнату — по-прежнему полная инвалидация.
Дословный вариант A (10 слотов + массив номеров комнат) не взят: он тянет
за собой сужение окна перебора колонок, то есть отказ от FIX_COLL_FLAGS.
Заведён docs/impl_diff.md — список осознанных расхождений с SDLPoP; правило
«фиксировать расхождение» в обоих CLAUDE.md теперь указывает туда.
tests-host: все 5 наборов прошли. Приёмка в MAME впереди.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Сценарий: ур.1 комн.8, ряд 0, x=170, лицом вправо, решётка шва закрыта —
разбежаться вправо и не отпускать. Кид проходит сквозь решётку.
Корень (две трассы, наша и оригинала, стартовая позиция совпала до пикселя):
смена комнаты в этой точке ШТАТНАЯ и у нас, и в оригинале — leave_room вправо
блокируют только doortop. Оригинал держит Кида бампом СРАЗУ ПОСЛЕ перехода,
потому что check_collisions хранит флаги по паре (колонка в СВОЕЙ комнате,
номер комнаты) и сравнивает prev/curr только при совпадении номеров: решётка
комнаты 8 и до, и после перехода лежит в слоте 9 с room=8, история не рвётся.
У нас индекс — колонка относительно отрисованной комнаты, при переходе все
индексы уезжают на 10, поэтому enter_room зовёт pop_coll_invalidate, тот
подавляет бамп на кадре входа, а дальше перехода флага 0->1 уже не будет.
Что делать — порт индексации оригинала (10 слотов по колонке своей комнаты +
массив её номера); тогда pop_coll_invalidate не нужен вовсе. Осторожно: это
сердце коллизии из BUG-SEAM-PINGPONG.
SDLPoP инструментирован для сверки: POP_TRACE=1 включает покадровую печать
Char + обе пары краёв, плюс маркеры BUMPED и LEAVE. В bug_list записана и
команда сборки на macOS (штатный Makefile требует pkg-config, которого нет).
073a6e0 вернул в safe_step(distance == 0) оригинальный seq_39 — а это шаг на
ОДИННАДЦАТЬ пикселей, который останавливает только бамп. У ворот шва
(ур. 1, комн. 6) наш бамп срабатывает не на том же кадре, что у оригинала
(своя история — BUG-SEAM-PINGPONG), и Кид с разбега ПРОБЕГАЛ сквозь
закрытую решётку между комнатами 8 и 6. Мелким шагом упирался нормально —
то есть промах именно в длине шага, а не в самом бампе.
Теперь ветка разделена по препятствию:
ЧОМПЕР — оригинальный seq_39: бампа там нет по определению (is_obstacle
требует modif == 2), и шаг обязан пройти целиком, иначе Кид топчется на
1 px и не успевает между челюстями;
СТЕНА И ВОРОТА — прежний «шаг-1»: осознанное приближение, проверенное
трассой живого SDLPoP в BUG-SEAM-PINGPONG. Останется таким, пока наш
бамп не сойдётся с оригиналом покадрово.
Симптом (уровень 3, комната 22): после спуска с уступа Кид проваливался
СКВОЗЬ пол; при подъёме проскакивало лишнее движение вперёд с прыжком вверх.
Корень. play_seq держит W0 замапленным на страницу данных Кида весь цикл и
читает seqtbl прямо через окно (макрос SEQ). Вызов pop_start_chompers,
поставленный в dc0bd47 внутрь обработки SEQ_UP/SEQ_DOWN, лезет за тайлами
уровня: pop_level_access_begin/end — это ровно gfx_w0_map/unmap. После
возврата цикл продолжал читать байткод из закрытого окна, то есть исполнял
мусор.
Фикс: SEQ_UP/SEQ_DOWN только взводят флаг, а pop_start_chompers зовётся
после выхода из цикла, когда W0 уже размаплен. Эффект тот же — трасса
кадра не меняется, чомперы заводятся в том же кадре.
Заодно убрана вложенность того же рода внутри самого pop_start_chompers:
start_anim_chomper получает mod параметром, а не зовёт pop_trob_modif —
тот при первом обращении к комнате сам перемапливает W0 под её bg.
Остальные точки вызова (start_fall, land, enter_room) проверены: там окно
W0 не открыто.
safe_step при distance == 0: возвращена ветка оригинала (seg005:0604) —
seq_39 (шаг 11) вместо нашего «шага-1». Для СТЕНЫ результат прежний: первый
же dx(1) даёт бамп, ровно как в трассе живого SDLPoP из BUG-SEAM-PINGPONG.
Для ЧОМПЕРА бампа нет (в разомкнутой фазе он не препятствие), и оригинал
уносит Кида на все 11 px — а мы шагали на один. Это и был «микрошаг вместо
нормального короткого шага» перед челюстями.
ВНИМАНИЕ на приёмке ур.1: это тот самый safe_step из BUG-SEAM-PINGPONG —
проверить комнату 6, осторожный шаг вплотную к воротам шва.
BUG-CHOMP-JUMP-1 (низкий, маловоспроизводим, на пререлиз): прыжок с места
вплотную к чомперу иногда даёт кадр с отступом назад. В запись сложено всё,
что выяснено: seq_3_standing_jump состоит ТОЛЬКО из положительных dx (значит
отступ даёт bumped, а не анимация); is_obstacle для чомпера у нас совпадает
с оригиналом; прямая трасса (UP+RIGHT одновременно) отката не показала —
главная гипотеза в порядке нажатий (↑ раньше → уводит в up_pressed с
выравниванием x). Там же метод ловли.
T-2 (idle-skip) закрыт — сделан шире, чем формулировался, как DRAW-COST
шаг 1 (a25ce58).
forward_pressed (seg005:0577) исключает чомпер из правила «у стены шагаем,
а не бежим»: `edge_type == EDGE_TYPE_WALL && curr_tile2 != tiles_18_chomper
&& distance < 8`. У нас исключения не было, а wall_type(18) = 3 — чомпер
считается стеной, — поэтому из позиции вплотную к челюстям Кид сначала
делал safe_step на 1-2 px и только потом бежал. Лишние кадры шага не дают
проскочить между челюстями (поймано на приёмке, комната 3.22).
Добавлен pop_edge_tile() — порт curr_tile2 после get_edge_distance.
control_turning не трогаем: у нас он ванильный, а второе такое исключение
в SDLPoP сидит под фиксом fix_turn_running_near_wall.
Две ошибки в одном месте. 1) Кадры 106..110 и передняя кровь 119..123
упакованы в pop_fore.atl (FORE_ENV_IDS в pop_pack_bg.py), а блитились через
pop_env_b — id уходил в env-страницу 3 по индексу 10, где записи нет, и
передний слой не рисовался вовсе: Кид, стоящий ЗА челюстями, был виден
целиком. 2) В draw_tile была только backtable-часть; у оригинала фронт
чомпера рисует отдельная ветка draw_tile_fore (через таблицу он не идёт —
у записи 0x12 fore_id нулевой), поэтому передние зубья появлялись лишь там,
где по тайлу прошёлся fore-проход персонажа.
Порт SDLPoP:
animate_chomper / start_chompers / start_anim_chomper /
next_chomper_timing (seg007) -> pop_trob.c;
draw_tile_anim + draw_tile_fore, ветка tiles_18_chomper (seg008) ->
pop_room.c (низ/кровь/верх, backtable) и pop_bg.c (передний слой);
check_chomped_kid / check_chomped_guard / chomped (seg004) -> pop_map.c.
Состояние — в room_modif, как у пик и ворот: младшие 7 бит фаза 1..N,
старший бит «перемололо кого-то» (кровь остаётся на тайле навсегда).
Номер позы chomper_fram1 и передние куски лежат в РЕЗИДЕНТЕ (pop_tile.c):
их читают обе половины слоя фона, а const-таблица банка из чужого банка
не видна.
start_chompers зовётся там же, где в оригинале: SEQ_UP/SEQ_DOWN в play_seq
(pop_kid.c), start_fall и land (pop_map.c), вход в комнату (roomtest.c).
Поэтому чомперы щёлкают только пока персонаж в ИХ ряду — так в оригинале.
check_chomped_guard у оригинала отдельное тело (страж не проходит через
check_collisions). У нас та же формула уже есть в get_row_collision_data,
поэтому флаги ряда считаются во ВРЕМЕННЫЙ массив: coll_curr/above/below —
это кадр Кида, из них move_coll_to_prev берёт прошлые флаги, затирание
сломало бы ему бамп.
Период смыкания — POP_CHOMPER_SPEED в pop_tune.h (15, как в оригинале).
Заодно: устаревшая заглушка pop_fore_set_clip в tests-host была __banked,
хотя функция давно переехала в резидент — всплыло при пересборке.
Ассеты уже были упакованы (pop_pack_bg.py, 2026-08-07). Банк 6: 20.0 %,
банк 7: 39.7 %, банк 3: 53.8 %. tests-host зелёные (5 наборов).
Зубья в MAME рисуются; анимация и смерть — на ручной приёмке.
Персонаж, у которого с прошлой отрисовки ЭТОЙ страницы дабл-буфера не
изменился ни один вход отрисовки, а фон в его прямоугольнике не трогали,
уже нарисован правильно: 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>
pop_bg.c разрезан по ЧАСТОТЕ вызова, а не по размеру:
pop_bg.c (банк 2) горячее fore-проход, оверлеи, кладка, клип
pop_room.c (банк 7) холодное draw_tile, точечные перерисовки, mob,
загрузка атласов
Стык — три тонкие __banked-обёртки (wall_pattern, wall_pattern_reset,
draw_gate_back): тела остаются непомеченными, поэтому горячий fore-проход,
зовущий wall_pattern до девяти раз за кадр, платит ноль, а трамплин
достаётся только холодному пути — ~40 вызовов на вход в комнату, 26 000
тактов = 0.06 кадра РАЗОВО. Общее состояние (pop_loose_modif, pop_ceil_modif,
obj_row/obj_col) писучее, лежит в _DATA/W2 и видно обеим половинам.
Заодно удалена мёртвая potion_bubble (169 Б).
Грабля: n_banks объявляет само приложение (roomtest.c), а не sprinter-cc.
Восьмой банк без правки константы линкуется молча, _bank_pages[7] остаётся
0xFF, и программа встаёт намертво до первого кадра. Диагноз снят дампом
_bank_pages из MAME.
Замер (ALLOCS=3000): BANK2 11 942 -> 5 872, BANK7 6 035; _CODE и куча не
тронуты (22 556 / 4 301).
Проверено: tests-host зелёные; построчная сверка pop_bg.c+pop_room.c против
дорефакторного pop_bg.c — ни одной строки логики не пропало; 8 комнат в MAME
до/после совпали попиксельно по активному экрану (различия только в фазе
анимации факелов и кадре Кида); живой прогон с переходами комнат, боем и
воротами сверен контрольным запуском HEAD-бинаря.
DRAW-COST поднят в приоритете: замер пользователя (ур.1 комната 3, страж
убит, Кид стоит) — синяя 80%, зелёная 20%, циан 110%, итого ~210 %
кадрового периода В ПОКОЕ.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DRAW-CHAR. Отрисовка персонажа сведена к одному набору функций над Char —
как физика после GUARD-PHYS. В оригинале add_kid_to_objtable (seg008:22F0) и
add_guard_to_objtable (seg008:2324) имеют идентичное тело и различаются
окном (loadkid/loadshad), набором спрайтов и типом объекта, а
redraw_at_char/redraw_at_char2 гейтов по charid не имеют вовсе.
pop_gdraw.c -> pop_cdraw.c: pop_char_draw/heal/fore(who), слот
POP_CH_KID / POP_CH_OPP; состояние слотов pop_cd[] в _DATA — читается из
любого банка без трамплина. Проход окклюзии тоже один
(pop_fore_over_char), pop_fore_over_kid больше нет.
Починилось само (расхождения, которые и были ценой дублирования): у
соперника не было clip_char; у Кида не было клипа полем 192 и ветки брызг
«мёртв/падение»; char_width_half СТРАЖА считался по спрайту КИДА.
Замер: _CODE 24 881 -> 20 524 (куча 2023 -> 6333), BANK2 -265, итого -3.2 КБ.
Проверено пользователем в MAME; циан-полоса профиля подросла — оптимизация
заведена отдельной задачей DRAW-COST.
MEM-BANK2, шаг 1: общие «листья» слоя фона в РЕЗИДЕНТ (pop_tile.c/.h).
Ограничение платформы: писучие данные банка лежат в _DATA и видны всем, а
const-таблицы — в странице банка, из другого банка их не прочитать; трамплин
же выбирается объявлением, то есть __banked на листе бьёт и по горячим
вызывающим (654 такта). W1 замаплено всегда — оттуда обе половины зовут
листья прямым call и читают таблицы напрямую.
MEM-BANK2, шаг 2: дедуп внутри банка. wall_pattern 808 -> 394 и wall_rnd
786 -> 654: четыре ветки по виду стены отличались только набором кусков и
числами в одной серии prandom — сведены к таблицам WP_PARTS и WR_RULE,
порядок вызовов prandom сохранён дословно.
Заодно: kid_seq_off больше не static const в kid_data.h (230 Б мёртвой копии
в каждом из 9 модулей) — генератор pop_extract_kid_data.py отдаёт
макро-инициализатор, массив определяет один pop_kid.c.
Итог: BANK2 14 815 -> 11 942 (72.9 %, свободно 4442 Б), _CODE 22 556,
куча 4301 Б. tests-host зелёные (65/39/53/1723/1); в MAME комната 1
совпала с дорефакторным снимком попиксельно (0 из 227 520), комната 3 —
та же раскладка кладки.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Скелет (L3-SKEL, ассеты + механика):
- pop_pack_guard.py получил параметр набора (GUARD/SKEL): атлас скелета
poc/res/skel/g0..g3.atl (28 кадров), палитра — из его res750.pal (на ур. 3
curr_guard_color = 0, оригинал палитру не подменяет);
- pop_guard_load выбирает набор по tbl_guard_type и перезагружается ПРИ СМЕНЕ
УРОВНЯ (load_lev_spr, seg000:1092) — без этого скелет рисовался атласом
стража и был невидим;
- load_frame: charid_4_skeleton идёт по таблице стража (seg006:529), тень —
только в кадрах 150..189. Пока ветка была одна (charid_2_guard), скелет
получал image из таблицы Кида (180 при 28 спрайтах) и не рисовался;
- check_skel (seg002:1042), ветка charid_4 в enter_guard, возрождение в
комнате 3 при падении (seg002:252), autocontrol_skeleton;
- leveldoor_open (seg007:456) — новый флаг, сбрасывается стартом уровня.
Цвета стражей (BUG-GUARD-COLOR-1, закрыт):
- все 7 палитр res10.bin -> pop_guard_pal.h, заливка 16 слотов по
guards_color комнаты перед отрисовкой (set_chtab_palette, seg003:257).
Проверено в MAME: ур. 2 комн. 11 = цвет 1, комн. 7 = цвет 3, полоса HP
меняется вместе со стражем. Грабля: gfx_pal_load отдаёт указатель в BIOS,
а тот читает только #4000-#BFFF — таблицу из банка копируем в стек.
Кэш соседних комнат (BUG-SWORD-GHOST-1, закрыт):
- pop_map кэширует fg соседей слева/справа ЦЕЛИКОМ и резолвит col -10..19.
Было -2..11, дальше мнимая стена: луч видимости упирался в неё (страж
после follow_guard в col 12), Кид прятал меч посреди боя и не мог достать
обратно. +48 байт W2.
Окклюзия соперника:
- pop_fore_over_char получил проход other_overlay_tile (порядок midtable,
seg008:1B06) и расширение перебора объединённым прямоугольником
«персонаж + клинок + брызги» — падающий скелет больше не рисуется поверх
кладки и верхней грани пола;
- клип полем 192 строк (reset_obj_clip, seg006:0507) для спрайта, клинка
(общий pop_sword_draw) и брызг — спрайт не залезает на полосу HP;
- ROOMNAV после смерти Кида делает честный pop_start_level: телепорт
«оживлял» мёртвого мимо старта уровня, оставляя живого скелета рядом с
вернувшейся кучей костей.
Ассеты чомпера (под L3-CHOMP): весь набор кадров в атласе явным списком
(101-105 низ, 111-113 верх, 106-110 фронт, 114-123 кровь mono-силуэтом) —
render_room анимированные тайлы пропускает, и в атласе не было ни одного.
Число EMM-страниц не изменилось.
Тесты: tests-host все 5 наборов зелёные, t_char вырос до 65 проверок
(резолв колонок за краем комнаты, возрождение скелета); в testkit добавлен
гард «код наехал на данные» (DATA_LOC).
Доски: TASKS.md разнесён на TASKS_OPEN/TASKS_CLOSED, закрытые баги с
разбором корней — в bug_closed.md; заведены DRAW-CHAR (отрисовка одна на всех
Char, как физика после GUARD-PHYS) и L3-COLOR (зелёная кладка уровня 3:
level_var_palettes = ресурс 20, есть в MSDOS/PRINCE.DAT).
В roomtest.c временно оставлен автостоп на падении соперника (отладка
падений скелета) — помечен ВРЕМЕННО.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Точки вызова стоят и в обеих ветках check_bumped, поэтому регрессия
проявится не в зацепе, а в обычном ударе о стену с зажатым Shift.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L3-CHKP (чекпойнт уровня 3). Флаг взводится, когда Кид уходит ВЛЕВО ИЗ
комнаты 7, а do_startpos по нему подменяет старт на комнату 2, тайл (0,6),
лицом влево и снимает loose-плиту (7,0,4). Тонкость, на которой я сначала
ошибся: level3_set_chkp (seg002:0665) вызван из leave_room ДО
goto_other_room, поэтому `Char.room == 7` — это комната, ИЗ которой уходят,
а не в которую входят. Поймал пользователь прогоном в SDLPoP: смерть В
комнате 7 вернула его в стартовую 9, а плита осталась цела. Проверено в
MAME: вход в 7 флаг не ставит, уход влево — ставит; респавн в комнате 2;
после обычной смерти (без чекпойнта) плиты восстанавливаются как раньше.
Зацеп ПРЯМО В ПРЫЖКЕ (check_grab_run_jump, seg006:1228) — портирован за
выключателем POP_ENABLE_JUMP_GRAB. Это НЕ ваниль: в оригинале зацепиться
можно только в начале падения (кадры 102..105), то есть Shift приходится
жать уже в полёте; у SDLPoP это enable_jump_grab, и работает он лишь при
включённых fixes-and-enhancements. Три точки вызова как у оригинала:
check_action и обе ветки check_bumped (зацеп за верх стены вместо удара).
pop_tune.h — настраиваемые константы в одном месте (аналог
custom_options_type SDLPoP): чекпойнт, выключатель зацепа и отладочная
крутилка POP_DBG_GATE_HOLD (сколько кадров решётка держится поднятой;
оригинал 5, потолок 30 — таймер связи пятибитный, 31 = «заклинено»).
Задача TUNE-1 в TASKS.md: читать это из ini рядом с exe.
СБОРКА. --max-allocs-per-node снижен со 100000 (дефолт sprinter-cc) до
3000 (дефолт SDCC) через `make ALLOCS=...`, а pop_trob.c уехал в БАНК 6 —
на 3000 резидент иначе не влезает (замер: конец _HOME 0xBC69 при стеке с
0xBB00). Итог: сборка с нуля 1:48 вместо >10 минут, куча 2751 Б вместо
2298. Релизная сборка — make ALLOCS=100000; сравнивать занятость банков
можно только при одинаковом ALLOCS.
Отдельно (вне git, SDLPoP в .gitignore): из референса вычищена вся наша
отладка DBG-GRAB — трасса JMP, GRAB try/probe/fail/OK/skip, автоскриншоты
seg003, печати DBG mob/mid/overlay/kidobj и счётчик dbg_shots.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Оба фикса подтверждены игрой, записи с разбором корней переехали в
bug_closed.md. BUG-GATE-PASS-1 остался ждать сценария, но его оговорка
про BUG-GATEMOD-1 обновлена (тот закрыт). BUG-SPIKE-1 понижен до низкого
приоритета: маловоспроизводим, смертельность пик подтверждена замером.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверено в игре: комната 11 уровня 2, страж выталкивает Кида в 22, Кид
возвращается бегом — страж оборачивается и достаёт меч. Разбор корня
(is_guard_notice не взводился ни в одном из пяти мест оригинала) переехал
в bug_closed.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Запись переведена в «ждёт сценария»: чистый пробег по убранным пикам
убивает (трасса по кадрам в записи), а наблюдавшееся «нет урона» — это
уже выдвинутые пики, безвредные для бегущего и в оригинале. Открытым
остаётся только визуальное расхождение со скриншотом SDLPoP.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Watchpoint на модификатор пики (ур.2 к.6, тайл 13) на чистом пробеге:
modif=1 — кадр 11 (беговой), x=112, col=2
modif=2 — кадр 12 (беговой), x=117, col=3 (Кид на тайле, h=2)
modif=3 — кадр 177 (frame_177_spiked) (напоролся)
То есть check_spike_below/check_spiked/is_spike_harmful и тайминг
выдвижения верны, «раннего» триггера нет. Настоящий корень: пики
ЗАЛИПАЮТ выдвинутыми — пока габарит Кида накрывает колонку, каждый кадр
start_anim_spike переставляет отрицательный модификатор обратно в 0x8F.
Выдвинутые пики (h=1) для бегущего безвредны по правилам оригинала, отсюда
обе жалобы: пробег не убивает и острия остаются на экране.
Открытый вопрос сузился до 2–3 пикселей: код start_anim_spike совпадает с
оригиналом дословно, значит на скриншоте SDLPoP Кид стоит чуть левее и
колонку пики не задевает. План закрытия — в записи.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Четыре разбора с прогонов пользователя; три бага закрыты, четвёртый
(BUG-SPIKE-1, пики) заведён с замером и гипотезой.
BUG-JUMPWALL-1 (Critical) — недолетевший прыжок проходил СКВОЗЬ стену.
check_collisions держал флаги перекрытия ОДНОГО ряда, а оригинал
(seg004:0004) — трёх, и move_coll_to_prev (seg004:00DF) берёт «прошлые»
флаги из нужного. Наш prev=3 («уже перекрывал») подавлял бамп ровно на
кадре смены ряда, а в падении ряд меняется почти каждый кадр — переход 0→1
на стене приходился как раз на него. Порт трёх рядов дословно.
Воспроизведено и закрыто на харнессе (новый набор t_wall: свип по 20
стартовым X, 4 давали проход сквозь кладку); 1723 трассы t_phys НЕ
изменились — правка поведение-сохраняющая. Живьём подтвердил пользователь.
BUG-GUARD-DEAF-1 (Major) — is_guard_notice не взводился НИГДЕ, поэтому
неактивный страж не оборачивался на Кида за спиной никогда. Портированы
все пять мест оригинала: опкод SOUND в play_seq (звуки 0..2), bumped_sound,
мягкое/среднее приземление, обрушенная плита, щелчок кнопки. Ждёт
игровой проверки боем в комнате 11 уровня 2.
BUG-SEAM-WEDGE-1 — клин кладки в пустом (2,0). Сосед угла снизу-слева
лежит в комнате по диагонали (room_BL); мы безусловно считали его стеной,
оригинал (load_rowbelow, seg008:368) — только когда такой комнаты нет.
Ряд «снизу» стал 11-байтным: [10] = тайл (0,9) диагональной комнаты.
BUG-LOOSE-3 — чёрный бар под упавшей плитой-потолком. pop_ceil_bake_empty
стирал полосу и восстанавливал только два тайла ряда −1, а в полосу лезет
графика соседа слева и верхушки ряда 0. Теперь перерисовываются ряды −1 и
0, колонки col−1..col+1. Проверено попиксельной сверкой с эталонной
перерисовкой: 0 различий.
Плюс карта связности комнат уровней 1–3 (TASKS.md): на ур. 2 недостижимых
нет, на ур. 3 это 23 и 24 — те же односторонние ссылки, что дали 13/18/24
на уровне 1, только комнаты полностью пустые.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Три правки едут вместе намеренно: n_banks и --bank обязаны меняться
атомарно, иначе промежуточный коммит — зависание (см. ниже).
MEM-BANK5. CODE и DATA делят одно 32-КБ пространство W1+W2, поэтому
килобайт кода, уехавший в банк, — это килобайт, доступный данным.
Кандидат выбран не по размеру, а по частоте вызова: диспетчер управления
дёргается раз в кадр на персонажа и горячих банк→банк переходов не
создаёт (в отличие от pop_level, чей pop_level_tile зовётся из банка 2 на
КАЖДЫЙ тайл).
_CODE 26 780 -> 24 662 Б (−2 118)
куча 180 -> 2 298 Б
банк 5 2 211 / 16 384 (13.5 %)
Шина control_* (8 глобалов) переехала в pop_state.c. Сегодня она уцелела
бы и в pop_ctrl.c — банки собираются без --bank-data, их писучие данные
остаются в общем _DATA, — но это флаг сборки, а не свойство кода, а шину
трогают уже три банка: 5 пишет с клавиатуры, 1 подаёт синтетический ввод
ИИ (autocontrol_*, seg002), 3 читает через pop_ctrl_shift_held.
Заодно pop_ctrl.c наконец включает собственный заголовок — раньше
объявления жили прямо в нём.
ГРАБЛИ, на которые наступили (стоили дольше самой задачи): n_banks в
roomtest.c захардкожен, и его надо править вместе с числом --bank. С
n_banks=4 и пятым банком crt0 выделил четыре страницы, _bank_pages[5]
остался нулём, и первый же вызов pop_ctrl_init() через трамплин
отобразил в W3 страницу 0 и прыгнул на 0xC874 в мусор — исполнение
забрело в дисковый код DSS и осталось крутить чтение секторов. Симптом:
загрузка ресурсов проходит целиком (open=45 — все атласы), комната и Кид
успевают нарисоваться из enter_room, а HP и номер комнаты уже нет, и
kid_tick не вызывается ни разу. Ровно предупреждение из шапки
runtime/bank.s. Сверку n_banks с реальным максимальным индексом банка
записал в docs/TODO.md (Auto-banking) — это должно быть ошибкой сборки.
DBG-CHEATS: [ (0x54) и ] (0x5B) двигают Кида на пиксель (seg000:1828),
по фронту нажатия, под pop_cheats. Нужны потому, что мост MAME теряет
нажатия при быстрой отправке и подогнать Кида в позу скриптом нельзя —
на это упёрлись BUG-LOOSE-2 и BUG-GATE-PASS-1.
ROOMNAV больше не зовёт pop_trob_reset: reset обнуляет room_seen, то есть
чит ОТМАТЫВАЛ МИР (открытые/закрытые ворота, нажатые кнопки). Навигация
обязана только телепортировать.
Проверено в MAME: старт уровня 1 рисуется полностью (комната, Кид, HP,
номер), бег вправо и падение на второй ряд отрабатывают, ] даёт x+1 и
[ даёт x−1 по одному нажатию, Shift+→ — осторожный шаг (x 131 -> 142,
колонка 4 -> 5) и при удержании 90 кадров не срывается в бег (x 142 ->
151), то есть pop_ctrl_shift_held работает через границу банк 3 -> банк 5.
Наборы под ucsim: geom 39, grab 53, phys 1723 — все зелёные.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BUG-FALL-SWORD-1 — start_fall (seg006:1044) был портирован не целиком:
не хватало трёх веток выбора последовательности и уборки меча в ножны.
Из-за seq_7 с set_fall(1,15) (дрейф 1 px/кадр) Кид с мечом уезжал примерно
на тайл вбок; в оригинале это seq_81_fightfall — падение строго вниз.
Сверено по логу SDLPoP: кадры 102..105 дают x = 155,157,159,160.
BUG-RJUMP-1 — run_jump (seg005:0AA8) не выравнивал Кида по кромке пола
перед толчком, а был заглушкой «полировка K3». Суммарный dx seq_4 —
62 px при тайле 14, то есть провал ровно в три тайла берётся ТОЛЬКО с
кромки: без выравнивания Кид не перепрыгивал его никогда. Порт —
pop_run_jump_align() в pop_map (беззнаковое сравнение оригинала = «сдвиг
не попал в [-8,-1]»). На харнессе: было — толчок с x=165, кадр 44 в
колонке 3 (провал); стало — 5 кадров добега, толчок с x=149, кадр 44 даёт
x=87 col=1 row=1. Остальные 8 сценариев не изменились.
BUG-GATEMOD-1 — load_alter_mod (seg008:198E) был портирован только для
зелий, поэтому ворота с bg=1 («Open» по спецификации DAT, табл. 8)
стартовали закрытыми. Добавлены ветки gate (1 -> 188) и loose. Ветка
wall намеренно НЕ портируется: связи стен наш pop_bg считает по типам
соседей в момент отрисовки. На уровнях 1-3 таких ворот всего двое
(ур. 1 комн. 5 (0,9); ур. 2 комн. 13 (1,5)) — у остальных bg=2, а 2 и 0
ведут себя одинаково.
Убран временный трассировщик pop_dbg_trace/pop_dbg_draw; pop_dbg_trap()
оставлен как многоразовый инструмент.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверять физику Кида глазами в MAME дорого и ненадёжно: ошибка почти
всегда не в одной функции, а в РАСХОЖДЕНИИ ТРАЕКТОРИИ через несколько
кадров. Харнесс гоняет тот же кадр, что и главный цикл
(pop_ctrl_tick -> kid_tick -> pop_phys_tick -> pop_loose_tick), и
сравнивает трассу состояния с эталоном.
- scene.c/.h — раннер: комната + стартовая поза + скрипт ввода -> трасса;
sc_kid_at_x задаёт точный X (исход часто зависит от фазы внутри тайла).
- stubs.c/.h — libc/libbgi/соседние модули; read() реально отдаёт
kid_data.bin (иначе kdat_ok=0 и play_seq молчит — трасса замирает).
- t_phys.c — 9 характеризующих сценариев, 1723 сверки (golden/).
- t_grab.c — окно зацепа: существует, достижимо коротким шагом, не
зависит от рисунка нажатий.
- record_golden.py — снятие эталона по одному сценарию за прогон.
- testkit/host-tests.mk — CODE_LOC настраиваемый, EXTRA_INC/EXTRA_CFLAGS.
Именно харнесс дал доказательство, что физика зацепа у нас верна, и тем
самым перевёл поиск BUG-GRAB-1 на клавиатуру.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Отличие от остальных записей раздела «НЕ БАГИ»: там картинка кривая и
совпадает с оригиналом лишь потому, что оригинал сам так рисует (порядок
midtable). Здесь же спрайт перекрывает кромку ровно настолько, насколько
персонаж за неё зашёл — геометрия и рендер согласованы.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Пользователь проверил в SDLPoP v1.24: оригинал падает с той же позиции,
по той же траектории и с тем же видом кадра падения (голова/руки поверх
кромки пола). Кадры совпадают один в один.
Механика записана с числами: у стоек с мечом weight_x = 13-14 против 3 у
обычной стойки, поэтому при взгляде влево точка веса уезжает на 13 px
вправо от Char.x, и кромку персонаж переступает раньше, чем выглядит.
Замер: x=151 -> dx_weight=164 -> колонка 7 (дыра) вместо 6 (пол).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порт seg003:0430 «If char is holding sword, it makes redraw-area bigger»:
при Char.sword >= sword_2_drawn футпринт расширяется на одну колонку в
сторону взгляда (вправо при dir>=0, влево иначе). У нас char_footprint
этого не делал, и клинок торчал на колонку дальше области, где
перерисовываются передние грани: меч оставался поверх столба, а его след
— на фоне.
char_footprint получил параметр sword; pop_fore_over_char — тоже (страж
машет мечом ровно так же). Ветка Кида берёт Kid.sword, ветка стража —
Guard.sword.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cur_frame — один глобал на всех персонажей (как в оригинале), владелец —
тот, кто последним прошёл load_frame. Последним в кадре тикает страж,
поэтому к моменту control() Кида там лежал кадр СТРАЖА, а через
kid_cur_dx/dy/flags по нему считается вся геометрия управления:
dx_weight -> determine_col -> distance_to_edge_weight -> get_edge_distance
-> выбор ветки в check_jump_up.
Оригинал зовёт load_fram_det_col() (seg006:0144) сразу после
loadkid/loadshad и ДО control() — play_kid_frame (seg000:1211) и
play_guard_frame (seg000:1248). У нас этого не было.
Замерено брейкпоинтом на pop_jump_up_seq: Kid x=156 col=6 кадр 15
(dx=0 weight_x=3) при кадре стража image17 (dx=-1 weight_x=8) дал
curr_col=7 и distance=2 вместо 6 и 10 — то есть jump_up_plain (вернулось
A=28, пустой прыжок) вместо «шаг назад на x=160 + зацеп». Предсказание
по кадру стража совпало с намеренным до единицы.
Отсюда же плавающее поведение: кадр стража меняется каждый тик, distance
Кида скакал через порог 6 — то прыжок, то попытка зацепа с неверной X.
И «голова Кида поверх плиты (1,6)» — не баг отрисовки, а следствие позы,
которой в оригинале в этом месте не бывает.
Фикс: pop_load_fram_det_col() (pop_kid.c) + вызовы в pop_ctrl_tick и
pop_guard_tick. determine_col — только на ветке Кида: у нас он
существует лишь для него (pop_map работает с Kid, а не с Char).
Разбор с числами — bug_closed.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>