Найдено пользователем: после телепорта в перевёрнутом виде ниже поля мусор.
Спрайты, торчащие в обычном виде ВЫШЕ поля (полоса кладки у потолка), после
отражения торчат НИЖЕ и лезут в борт с полосой HP.
blit_b_clip при перевороте режет по обеим границам поля всегда, а не по
pop_t_clip_top; быстрый путь pop_blit_b отдаёт клипующему всё, что выходит
за поле.
Проверено в MAME: телепорт по комнатам в перевёрнутом виде — борта чистые,
комнаты (факелы, чомперы, пол) зеркальны. Переходы между комнатами в
перевёрнутом виде работают.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Найдено пользователем: поднимающаяся решётка анимировалась неправильно, а в
углу (0,0) оставался чёрный прямоугольник — заливки шли голым bar по
НЕПЕРЕВЁРНУТЫМ экранным координатам.
Формула переворота вынесена в pop_bg.h (POP_FLIP_TOP) — одна на весь порт,
и добавлен pop_bar_black: чёрная плита банком 0x50 с учётом переворота и с
пометкой области (bar идёт мимо pop_blit_b). На неё переведены все четыре
стирания слоя фона: решётка, полоса потолка, floor_bake, loose_bake_empty.
Проверено в MAME: перевёрнутая комната чистая, артефакта в углу нет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Найдено пользователем на первом прогоне зеркального фона: чомперы
анимировались неправильно. Причина — pop_heal_off: точечные перерисовки
(чомпер, пики, loose, ворота, полоса потолка) чистят фон по КОМНАТНОЙ
координате, и при перевороте heal стирал прямоугольник не там, где тайл
рисуется. Фикс в единой точке — та же формула FLIP_TOP, что у блита.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
II.3: pop_upside проведён в единственную точку тайлового слоя (pop_blit_b /
blit_b_clip): позиция через FLIP_TOP, блит — vflip-двойник. Этим зеркалятся
полная отрисовка комнаты, точечные редрои, trob-анимации, fore-слой и кладка.
В клипованном пути обрезка сверху экрана съедает нижние строки источника,
поэтому кусок пересчитывается как sy' = h - sy - dh.
MEM-COLD2 п.1: enter_room_side/enter_room — в банк 8 (куча 450 -> 1231 Б).
Рабочие массивы комнаты остались в _DATA, в банк уехал только код.
Проверено в MAME (уровень 9, чит U): комната перевёрнута целиком, пол
вверху, дверь и кладка вверх ногами. tests-host 6/6, size-check OK.
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>
Порт семи мест 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>
Порт 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>
Главный цикл спейсится тремя 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>
Порт 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.
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>