Files
Sprinter-SDCC/applications/PoP/docs/midtable_analysis.md
T
snark13 c31930dcad midtable: разобрано, насколько узко оригинал помечает передний слой
set_redraw_fore (seg007:0550) зовут ровно из трёх мест, и нигде нет
пометки «всё»: персонаж помечает прямоугольник своего футпринта
(redraw_at_char, seg003:0576; для Кида — объединение с прошлым кадром),
падающий кусок — соседа справа и второй тайл на границе рядов (draw_mob,
seg007:1147), анимация тайла — один тайл.

Значит пометка переднего слоя НЕ ШИРЕ нашего окна вокруг персонажа, и
полный порт objtable её не отнимает, а формализует.  Главный риск снят.

Побочно найден точный рецепт для MOB-CLIP-RIGHT: оригинал не рисует соседа
поверх куска и не полагается на clip.right — он помечает соседний тайл
парой set_redraw2 + set_redraw_fore, и порядок делает всё сам.  Это же
закрывает «плиту перед колонной».

Рекомендация по итогам: вариант A (полный порт).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 20:10:55 +03:00

276 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Слои отрисовки: как устроен оригинал и чего стоит порт
Разбор 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 объекта), окно переднего слоя не теряется.