Спрайты: у падающего куска obj_id = 10 (add_mob_to_objtable, seg007:1170), и три части берутся из таблиц по этому индексу — 70/74/72, а не 41/43/42. Мы рисовали плиту В ПОКОЕ, отсюда «цельная ровная» вместо двух половин со сдвигом правой на пиксель. Подпорка «перерисовать соседний тайл поверх куска» убрана: она тащила на плиту чужой узор и окно, да ещё стоила по одной полной отрисовке тайла на кусок за кадр. Клип объекта (clip.right = 40) НЕ портирован — единицы поля не выяснены, буквальные 40 пикселей срезают задний угол плиты. Цена отказа — правая грань куска и передние части чужих тайлов его не перекрывают (MOB-CLIP-RIGHT, BUGS_OPEN.md). Коридор heal сужен по высоте 32 -> 24: реальный след спрайта 16 px. Ширина оставлена 64 — из неё используются 62, экономить нечего; заодно в комментарии зафиксировано, что «выравнивание блока акселератора» ничем не подтверждается и требует проверки артефактом. docs/midtable_analysis.md — разбор слоёв оригинала и замеры: - логический кадр 1 289 526 тактов; - pop_redraw_needed 978 тактов (0,08 %); - fore-проход 128 778 тактов (10 %) на ОДНОГО персонажа, но выполняется лишь на 4 % кадров — гасит пропуск неизменившегося персонажа; - максимум объектов на одном тайле 2 (пара из loose_fall), значит сортировка внутри тайла бесплатна. Вывод: единственный риск порта objtable — сохранится ли окно клипа fore-прохода; до разбора redraw_frames_fore выбирать вариант рано. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
14 KiB
Слои отрисовки: как устроен оригинал и чего стоит порт
Разбор 2026-08-13, по ../SDLPoP/src/seg008.c. Повод — семь дефектов
падающих плит на уровне 13, из которых три оказались не багами кода, а
следствием того, что у нас нет слоя, в котором объекты и куски тайлов
сортируются между собой. Решение по этому документу ещё не принято.
1. Как это работает в оригинале
1.1 Три таблицы, а не «слои»
draw_tables (seg008:1373) рисует ровно в таком порядке:
restore_peels();
draw_wipes(0);
draw_table(0); // BACKTABLE
draw_table(3); // MIDTABLE
draw_wipes(1);
draw_table(1); // FORETABLE
Это грубое разделение на три уровня глубины. Куда попадёт кусок тайла,
решает переменная ptr_add_table, которую вызывающий переставляет перед
draw_tile*: по умолчанию add_backtable, в оверлее кромки —
add_midtable (draw_other_overlay, seg008:1499), а add_foretable
вызывается явно и точечно.
1.2 Объекты живут НЕ в таблицах, а в objtable — и привязаны к ТАЙЛУ
Персонажи, падающие куски, мечи, брызги попадают в objtable, и у каждой
записи есть поле tilepos — тайл, которому объект принадлежит.
Ключевое: объекты рисуются не отдельным проходом после фона, а ВНУТРИ
обхода тайлов. В redraw_needed_tile (seg008:207) стоит:
if (tile_object_redraw[tilepos]) {
if (tile_object_redraw[tilepos] == 0xFF)
draw_objtable_items_at_tile(tilepos - 1);
draw_objtable_items_at_tile(tilepos);
tile_object_redraw[tilepos] = 0;
}
if (redraw_frames_fore[tilepos]) draw_tile_fore();
То есть на каждом тайле: сначала его фоновые куски, потом объекты ЭТОГО тайла, потом его передние куски.
1.3 Порядок глубины складывается из ТРЁХ независимых механизмов
| механизм | что даёт |
|---|---|
| порядок обхода тайлов: ряды 2, 1, 0, колонки 0..9 (seg008:129) | тайл, обойдённый позже, рисуется поверх |
сортировка объектов ВНУТРИ одного тайла (sort_curr_objs, seg008:1553) |
кто из объектов одного тайла поверх кого |
| три таблицы back/mid/fore | грубая глубина для кусков тайлов |
Сортировка внутри тайла (compare_curr_objs, seg008:1572) — пузырьком, и
правил в ней три:
объект типа 1 (ТЕНЬ) — всегда первым;
оба объекта — падающие плиты (0x80): y1 < y2 → по УБЫВАНИЮ y;
любая другая пара: y1 > y2 → по ВОЗРАСТАНИЮ y.
Обратный порядок для пары плит — не описка: две плиты из loose_fall летят
в 6 пикселях друг от друга, и верхняя обязана лечь поверх нижней.
1.4 Что из этого следует
«Сортируемый midtable» — неточное имя. Глобальной сортировки среднего слоя в оригинале нет. Есть привязка объекта к тайлу и сортировка внутри тайла; всё остальное решает порядок обхода. Это принципиально дешевле общей сортировки: объектов на один тайл обычно 1-2.
2. Что делаем мы
Наш кадр — жёсткая последовательность проходов, без привязки объектов к тайлам:
фон: pop_loose_tick (физика кусков) -> pop_process_trobs -> pop_redraw_needed
-> редрой шва
объекты: pop_loose_mob_draw (куски ПОД Kid, отсортированы по y между собой)
pop_char_draw(OPP/KID) (порядок задаёт guard_over_kid)
pop_loose_mob_draw_over (куски ПОВЕРХ Kid)
перед: pop_fore_over_char — ТОЛЬКО в окне вокруг персонажа
Отличия, из которых растут все три оставшихся дефекта:
- Объект не знает своего тайла. Глубина «кусок против Кида» считается
отдельной формулой (
pop_room.c, полеdefer), а «кусок против КУСКА ТАЙЛА» не считается вовсе — куски тайлов рисуются раньше всех объектов. - Передний слой считается только вокруг персонажа. Это наша
оптимизация (memory
pop_fore_layer_cost: полный проход стоил 78 % кадра). Падающая плита в чужом углу комнаты передних частей тайлов поверх себя не получает — отсюда «плита перед колонной». - Оверлей кромки идёт после персонажей и потому безусловно поверх всех, тогда как у оригинала он в midtable и сортируется.
3. Что затрагивает порт
| участок | объём правки |
|---|---|
pop_room.c — куски |
привязать к тайлу, убрать defer, убрать собственную сортировку |
pop_cdraw.c — персонажи |
то же: объект вместо слота, привязка к тайлу |
pop_bg.c — overlay_mid_tile, fore_only_tile, ceil_over_kid_tile |
вызов из обхода тайлов, а не из отдельного прохода |
pop_redraw.c — пометки |
добавить «на этом тайле есть объект» (порт tile_object_redraw) |
roomtest.c — главный цикл |
вместо трёх проходов один: обход тайлов с объектами внутри |
окно fore-клипа (pop_t_fclip_*) |
смысл меняется: клип по тайлу, а не по персонажу |
Плюс новая структура objtable и её сортировка — но маленькая, на тайл.
4. Плюсы
- Уходят разом MOB-CLIP-RIGHT, MID-OVERLAY-LAYER и «плита перед колонной»: все три — следствие отсутствия привязки к тайлу, а не самостоятельные баги.
- Уходят подпорки. Перерисовка соседнего тайла поверх куска,
defer, ручная сортировка кусков, отдельный проходdraw_over— всё это заменяется одним механизмом. - Совпадение с оригиналом по построению. Дальше любой вопрос «что поверх чего» решается чтением seg008, а не экспериментом в MAME.
- Возможный выигрыш по кадру. Сейчас fore-проход считает окно вокруг персонажа и всё равно перебирает до девяти тайлов; при привязке к тайлу передние части рисуются только там, где реально есть помеченный объект. Но это НАДО ЗАМЕРИТЬ, а не обещать.
5. Минусы и риски
- Риск регресса широкий. Трогается порядок отрисовки ВСЕГО: Кид, соперник, меч, брызги, зеркало, куски, оверлеи, полоса потолка. Уровни 1-11 приняты и держатся на текущем порядке.
- Наша оптимизация fore-окна может не пережить порт в прежнем виде.
Она даёт 3.2x на самом дорогом проходе (memory
pop_fore_layer_cost). Если привязка к тайлу заставит рисовать передние части шире — можно потерять больше, чем выиграть. - Дабл-буфер. У оригинала один экран с dirty-rect, у нас две страницы со своими копиями фона и heal. Пометка «на тайле есть объект» обязана быть счётчиком страниц, как остальные наши пометки, — иначе объект перерисуется на одной странице и не перерисуется на другой.
- Банки. Отрисовка размазана по трём банкам (
pop_bg2,pop_cdraw4,pop_room7) плюс резидент. Единый обход тайлов с объектами внутри означает, что цикл обхода зовёт код из всех трёх — надо проверить, что не упрёмся в границы банков и трамплины. - Объём. Это не правка, а этап: сопоставимо с тем, что делалось для fore-слоя.
6. Развилки
A. Полный порт — objtable с привязкой к тайлу, сортировка внутри тайла, объекты внутри обхода. Максимально близко к оригиналу, максимальный риск и объём.
B. Частичный: только привязать КУСКИ к тайлам. Персонажей оставить как есть. Закрывает MOB-CLIP-RIGHT и «плиту перед колонной», не трогает проверенный порядок персонажей. Дешевле и безопаснее; MID-OVERLAY-LAYER остаётся.
C. Отложить до этапа BG-ONCE и делать вместе — там всё равно пересматриваются слои, и два пересмотра подряд дороже одного.
Рекомендация: B или C. Вариант A целиком оправдан только если мы одновременно берёмся за BG-ONCE — тогда это один пересмотр слоёв вместо двух, и замер кадра делается один раз.
7. ЗАМЕРЫ (сделаны 2026-08-13, уровень 13)
Метод — маркеры-пустышки в РЕЗИДЕНТЕ вокруг измеряемого вызова плюс
брейкпоинты с printf totalcycles (memory z80_profiling_method). В
банковый код брейкпоинт ставить нельзя: 0xC000 — общее окно всех банков.
| что | такты | доля логического кадра |
|---|---|---|
| логический кадр целиком | 1 289 526 | 100 % |
pop_redraw_needed |
978 | 0,08 % |
| fore-проход, ОДИН персонаж | 128 778 | 10 % |
Плюс два счётчика за прогон ~229 логических кадров:
- fore-проход вызван 10 раз — на 96 % кадров он не выполняется вовсе
(пропуск неизменившегося персонажа,
pop_char_skip_mask). Средняя цена по кадру выходит ~0,4 %, но КАК ТОЛЬКО персонаж движется — платим все 10 % каждый кадр, и при двух персонажах это ~20 %; - максимум объектов на одном тайле = 2 (комната 16, пара из
loose_fall: плита сбивает плиту, дальше летят обе). В комнате 23, где гряда падает в пустоту, максимум 1.
7.1 Что эти числа меняют в оценке
Сортировка внутри тайла — бесплатна. Два объекта, пузырёк на два элемента. Возражение против варианта A, которое закладывалось в §5, снято.
pop_redraw_needed можно не считать вовсе. 978 тактов против 128 778 у
fore-прохода — соотношение 131 к 1.
Единственный настоящий риск порта — окно клипа fore-прохода. Если привязка объектов к тайлам заставит рисовать передние части шире нынешнего окна вокруг персонажа, мы потеряем 10 % кадра, и потеряем их НА ДВИЖЕНИИ, когда бюджет и так самый напряжённый.
7.2 Что осталось выяснить ДО выбора варианта
Как оригинал отсекает лишнюю работу переднего слоя: redraw_frames_fore[tilepos]
рисует передние части ТОЛЬКО у помеченных тайлов (seg008:214). Надо понять,
насколько узко эта пометка ставится — если не шире нашего окна, вариант A
безопасен по кадру; если шире, остаёмся на B.
Обновлённая рекомендация: решение упирается в §7.2, и до его разбора выбирать между A и B преждевременно. B остаётся безопасным запасным вариантом.