# Слои отрисовки: как устроен оригинал и чего стоит порт Разбор 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) стоит: ```c 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 — ТОЛЬКО в окне вокруг персонажа ``` Отличия, из которых растут все три оставшихся дефекта: 1. **Объект не знает своего тайла.** Глубина «кусок против Кида» считается отдельной формулой (`pop_room.c`, поле `defer`), а «кусок против КУСКА ТАЙЛА» не считается вовсе — куски тайлов рисуются раньше всех объектов. 2. **Передний слой считается только вокруг персонажа.** Это наша оптимизация (memory `pop_fore_layer_cost`: полный проход стоил 78 % кадра). Падающая плита в чужом углу комнаты передних частей тайлов поверх себя не получает — отсюда «плита перед колонной». 3. **Оверлей кромки идёт после персонажей** и потому безусловно поверх всех, тогда как у оригинала он в 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_bg` 2, `pop_cdraw` 4, `pop_room` 7) плюс резидент. Единый обход тайлов с объектами внутри означает, что цикл обхода зовёт код из всех трёх — надо проверить, что не упрёмся в границы банков и трамплины. * **Объём.** Это не правка, а этап: сопоставимо с тем, что делалось для 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[]` ставит ровно одна функция — `set_redraw_fore` (seg007:0550), и зовут её из трёх мест. Ни в одном нет «пометить всё». **Персонаж — `redraw_at_char` (seg003:0576).** Помечается ПРЯМОУГОЛЬНИК футпринта: ```c for (tile_row = x_top_row; tile_row <= char_bottom_row; ++tile_row) for (tile_col = x_col_left; tile_col <= x_col_right; ++tile_col) set_redraw_fore(get_tilepos(tile_col, tile_row), 1); ``` с двумя уточнениями: при вынутом мече прямоугольник расширяется на колонку в сторону клинка, а для КИДА берётся объединение с футпринтом ПРОШЛОГО кадра (`prev_char_*`) — чтобы освободившиеся тайлы тоже вернули свои передние части. **Падающий кусок — `draw_mob` (seg007:~1147).** Каждый кадр помечается СОСЕД СПРАВА (`++tile_col`), и второй тайл, если кусок висит на границе рядов: ```c ++tile_col; tilepos = get_tilepos(tile_col, tile_row); set_redraw2(tilepos, 1); set_redraw_fore(tilepos, 1); top_row = y_to_row_mod4(ypos - 18); if (top_row != tile_row) { ... то же для top_row ... } add_mob_to_objtable(ypos); ``` **Анимация тайла — `draw_trob` (seg007:01E6):** один тайл. **Вывод: пометка переднего слоя в оригинале НЕ ШИРЕ нашего окна.** Она пообъектная — футпринт персонажа и 1-2 тайла на кусок. Значит полный порт objtable **не отнимает** нашу оптимизацию fore-окна, а формализует её: вместо «окно вокруг персонажа» будет «тайлы, помеченные объектами», что как минимум не шире, а для одиночного куска заметно уже. Риск, вокруг которого крутилась вся оценка, снят. ### 7.3 Побочный результат: готовый рецепт для MOB-CLIP-RIGHT `draw_mob` даёт точный ответ на вопрос, как оригинал прячет правую часть куска за соседним полом: он НЕ рисует сосед поверх куска (наша подпорка) и НЕ полагается только на `clip.right`. Он помечает соседний тайл СРАЗУ двумя пометками — `set_redraw2` (фон) и `set_redraw_fore` (передний слой). Дальше порядок делает всё сам: фон соседа рисуется ДО куска, его передние части — ПОСЛЕ. Это же закрывает и «плиту перед колонной»: передние части соседнего тайла (колонна) ложатся поверх куска, потому что тайл помечен. **Рекомендация после разбора: вариант A (полный порт).** Оба возражения против него сняты замерами и этим разбором — сортировка внутри тайла бесплатна (максимум 2 объекта), окно переднего слоя не теряется.