diff --git a/applications/PoP/docs/room_model_plan.md b/applications/PoP/docs/room_model_plan.md index 5605a9c..fa2b083 100644 --- a/applications/PoP/docs/room_model_plan.md +++ b/applications/PoP/docs/room_model_plan.md @@ -78,7 +78,9 @@ drawn_room с `curr_col=-1/10` + снапшоты соседей `g_lcol/g_rcol` ### S4. Полировка - Окклюзия/ceiling у шва при straddle, BUG-OCCL-1 (глубина), правый край. -## Связанные баги (`../roomtest/bug_list.md` — все открыты) +## Связанные баги — все ЗАКРЫТЫ (`../roomtest/bug_closed.md`) BUG-CEIL-1 (руки при прыжке вверх), BUG-CEIL-2 (loose в потолке), BUG-CEIL-3 (потолок над анимируемыми воротами), BUG-OCCL-1 (тень дальней -колонны). Memory: `pop_seam_room_model`. +колонны) — починены без полной straddle-модели. То есть S4 «полировка +окклюзии» осталась актуальной только для окклюзии У ШВА при straddle. +Memory: `pop_seam_room_model`. diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index 79a45f5..3157683 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -21,7 +21,7 @@ EXTRA_FLAGS ?= --gfx 256 --bank 2=pop_bg.c --bank 4=pop_gdraw.c --bank 3=pop_map # MAME = время фазы в растровых строках. PROF ?= 1 PROF_FLAGS := -DPROF_BORDER=$(PROF) -EXTRA_SRCS := pop_state.c pop_kid.c pop_ctrl.c pop_level.c pop_trob.c pop_redraw.c pop_geom.c pop_guard.c +EXTRA_SRCS := pop_state.c pop_draw.c pop_kid.c pop_ctrl.c pop_level.c pop_trob.c pop_redraw.c pop_geom.c pop_guard.c BG_DIR := $(CURDIR)/../poc/res/bg KID_DIR := $(CURDIR)/../poc/res/kid @@ -71,4 +71,4 @@ $(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py cd $(TC) && python3 pop_extract_kid_data.py -$(EXAMPLE).exe: guards.c pop_guard.c pop_guard.h pop_gdraw.c pop_state.c pop_state.h pop_cheat.h pop_bg.c pop_bg.h pop_geom.c pop_geom.h pop_redraw.c pop_redraw.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA) +$(EXAMPLE).exe: guards.c pop_guard.c pop_guard.h pop_gdraw.c pop_draw.c _pop_draw.h pop_state.c pop_state.h pop_cheat.h pop_bg.c pop_bg.h pop_geom.c pop_geom.h pop_redraw.c pop_redraw.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA) diff --git a/applications/PoP/roomtest/TASKS.md b/applications/PoP/roomtest/TASKS.md index 529e0e6..6c44ca8 100644 --- a/applications/PoP/roomtest/TASKS.md +++ b/applications/PoP/roomtest/TASKS.md @@ -6,11 +6,11 @@ а то, **что берём в работу сейчас и в каком порядке**. Каждая запись: что сделать, почему именно сейчас, чем подтверждать результат. -**Открытые баги** (все Medium/косметика, Critical'ов нет): -[BUG-CEIL-1](bug_list.md#bug-ceil-1) · [BUG-CEIL-3](bug_list.md#bug-ceil-3) · -[BUG-OCCL-1](bug_list.md#bug-occl-1) · -[BUG-CEIL-2](bug_list.md#bug-ceil-2) (отложен) · -оптимизации [T-1](bug_list.md#t-1), [T-2](bug_list.md#t-2). +**Открытых багов нет** (ревизия 2026-08-01): в [`bug_list.md`](bug_list.md) +остались только незакрытые оптимизации [T-1](bug_list.md#t-1), +[T-2](bug_list.md#t-2) и незаконченная +[таблица обхода 24 комнат](bug_list.md#обход-всех-24-комнат-уровня-1). +Следующая порция багов придёт оттуда и из L1-PASS. Правило проекта в силе: механику сверять с `../SDLPoP/src/` ДО кодинга; диагноз платформы подтверждать артефактом (брейкпоинт/дамп/.asm), а не @@ -20,7 +20,8 @@ ## P0 — делаем сейчас -*(KBD-1 закрыт до финальной полировки — см. ниже; следующая в работе — CLIP-1.)* +*(KBD-1 закрыт до финальной полировки, CLIP-1 сделан — оба ниже. P0 пуст; +следующая в работе — P1, начиная с L1-START/L1-EXIT.)* ### KBD-1. Shift + стрелки: нажатия теряются — **ПРИЧИНА НАЙДЕНА, ФИКС ПРОВЕРЕН** @@ -284,7 +285,43 @@ --- -### CLIP-1. Аудит блитов: где клип не нужен +### CLIP-1. Аудит блитов: где клип не нужен — **СДЕЛАНО 2026-08-01** + +> **Итог.** Heal Кида и стража переведены на выбор ядра по тому же тесту, +> что давно стоит у блитов. Замер в MAME (счётчики `totalcycles` на входах +> `kid_heal` и `kid_tick`, то есть вся группа heal за кадр; комната 1, Кид +> стоит, стража нет): +> +> | путь | тактов на кадр | кадров в выборке | +> |------|----------------|------------------| +> | клипающее ядро (как было) | **26 200** | 149 | +> | noclip (стало) | **15 848** | 239 | +> +> **−10 352 такта на кадр, то есть −39.5 % с группы heal** (≈0.49 мс при +> ~21 МГц). A/B честный: оба замера сняты в ОДНОМ прогоне, вторая половина — +> с пропатченным в памяти условием (`jr nz` → `jr` в `pop_heal_fast`), то +> есть на той же геометрии и в той же сцене. +> +> **Размер: −362 Б суммарно** (не плюс!): `_CODE` 25 289 → 25 306 (+17), +> BANK2 13 792 → **13 676** (−116, свободно стало 2708 Б — это тот самый +> тесный банк из рисков `levels_plan.md` §5), BANK3 6512 → 6249 (−263), +> BANK4 без изменений. +> +> **Грабли, стоившие двух пересборок** (вынесено в память +> `sdcc-static-inline-double-cost`): первым заходом хелпер был `static +> inline` в `pop_bg.h` — и SDCC 4.5 И встроил его тело (181 Б) в каждое +> место вызова, И оставил отдельную копию в КАЖДОМ TU, который видит +> заголовок. `pop_guard_heal` раздулся с ~60 до 663 Б, итого +1091 Б в +> `_CODE` и +636 Б в банке стража. Лечится обычной функцией в одном +> резидентном модуле (`pop_draw.c`, W1 — из банков это прямой `call` без +> трамплина, как у `pop_sword_draw`). +> +> **Проверено визуально:** обычная ходьба, прыжок, спуск и позиция «за +> решёткой шва» (straddle — там как раз работает клипающий фолбэк) — +> артефактов и следов нет. + +**Ниже — исходная постановка задачи (что и почему смотрели).** + **Зачем сейчас.** Кадр занят на ~86 %; подготовка клипающего варианта стоит ~5.6 К тактов на вызов, а общее ядро против линейного — 13 288 против @@ -296,30 +333,32 @@ `gfx_blit_cols_part_noclip`, иначе в клипающий вариант (`pop_kid.c:86,591,676`, `pop_gdraw.c:105`). -**Что чинить:** +**Что чинили:** -- `kid_heal()` (`pop_kid.c:613,615`) и `pop_guard_heal()` - (`pop_gdraw.c:65,66`) зовут `gfx_heal` — **всегда с клипом**, хотя - `gfx_heal_noclip` существует и `pop_bg.c:147` им уже пользуется по тому же - тесту. Это heal 2–4 прямоугольников КАЖДЫЙ кадр; замер общего ядра — - 11 658 тактов на heal 22×22. Тест onscreen у нас уже посчитан рядом. -- `pop_room_clip_borders()` (`pop_bg.c:1193,1194`) — `gfx_heal(0,0,320,…)`: - noclip требует w,h ≤ 255, значит либо два куска по 160, либо оставить как - есть (зовётся по гейту, только в кадрах падения — проверить, что гейт - действительно редкий, прежде чем трогать). +- ✅ `kid_heal()` и `pop_guard_heal()` звали `gfx_heal` — **всегда с + клипом**, хотя `gfx_heal_noclip` существует и `heal_off` в `pop_bg.c` им + уже пользовался. Это heal 2–4 прямоугольников КАЖДЫЙ кадр; замер общего + ядра — 11 658 тактов на heal 22×22. Теперь все три идут через общий + `pop_heal_fast` (`pop_draw.c` + `_pop_draw.h`). +- ⛔ `pop_room_clip_borders()` (`pop_bg.c`) — `gfx_heal(0,0,320,…)`: + **оставлен клипающим осознанно**. Полоса шириной 320 не лезет в 8-битный + параметр noclip-ядра, а бить её на два куска по 160 нет смысла: гейт + `border_dirty` пускает туда только в кадрах падения, и выигрыш подготовки + тонет в цене самих 320×28 пикселей. Причина записана прямо в коде, чтобы + не «оптимизировать» повторно. -**Что обязано остаться с клипом** (зафиксировать в комментарии, чтобы потом -не «оптимизировать» повторно): -- кромочные тайлы фона `pop_bg.c:132` — тайл у края экрана режется по - построению; +**Что обязано остаться с клипом** (зафиксировано комментариями в коде): +- кромочные тайлы фона (`blit_b` в `pop_bg.c`) — тайл у края экрана режется + по построению; - спрайты при straddle (`kid_render_dx = ∓140`, комната Кида ≠ отрисованной) - и при падении ниже поля — фолбэки `pop_kid.c:594,681`, `pop_gdraw.c:107`. + и при падении ниже поля — фолбэки в `kid_draw`/`pop_guard_draw`; +- борта поля (`pop_room_clip_borders`) — см. выше. -**Порядок работы:** (1) выписать полный список вызовов -`gfx_blit*`/`gfx_heal*` в roomtest с ответом «кто гарантирует on-screen»; -(2) перевести то, что можно, на noclip по существующему тесту; (3) замерить -кадр полосами бордюра ДО/ПОСЛЕ (`PROF()` уже в `roomtest.c`) и брейкпоинтом -на конкретной функции — числом, а не «стало плавнее»; (4) `make size-check`. +**Что уже было правильно** (шаблон, который и распространили): блиты Кида, +стража, клинка и брызг спрашивают `pop_onscreen_cols()` и уходят в +`gfx_blit_cols_part_noclip`. Отдельный случай — `pop_kid_img_blit`: noclip +БЕЗ проверки, потому что единственный вызывающий (полоса HP) рисует по +фиксированным координатам; это тоже помечено в коде. --- @@ -351,13 +390,17 @@ BUG-1 (провал на row 1 при переходе через открыты Список разделён на [`bug_list.md`](bug_list.md) (открытое) и [`bug_closed.md`](bug_closed.md) (закрытое + разбор корней). +✅ **Косметика окклюзии тоже закрыта** — BUG-CEIL-1/2/3 и BUG-OCCL-1 были +починены кодом ещё в июле, а записи никто не снял: `ceil_over_kid_tile` +(`pop_bg.c:1275`), `pop_ceil_modif` + `pop_ceil_shake_draw`/`_bake_empty` +(`:547,815,827`), `bar` с `POP_YOFF+3` в `pop_room_redraw_seam_left` (`:793`), +разделение слоёв по `add_backtable` vs `ptr_add_table` в `overlay_mid_tile` +(`:1367`). Разбор — в [`bug_closed.md`](bug_closed.md). + ⏳ **Осталось:** закрыть таблицу обхода 24 комнат ([`bug_list.md`](bug_list.md#обход-всех-24-комнат-уровня-1)) — заполнена на -5 строк из 24, инструмент (`ROOMNAV`) готов. Открытая косметика окклюзии -([BUG-CEIL-1](bug_list.md#bug-ceil-1), -[BUG-CEIL-3](bug_list.md#bug-ceil-3), -[BUG-OCCL-1](bug_list.md#bug-occl-1)) остаётся Medium — трогать после -L1-PASS, обход комнат может добавить к ней новые случаи. +5 строк из 24, инструмент (`ROOMNAV`) готов. Делать вместе с L1-PASS: это +единственный оставшийся источник новых багов уровня 1. ### L1-SPEED. Игра идёт быстрее оригинала (найдено 2026-08-01) Сверка таймингов: оригинал — `BASE_FPS = 60` при `base_speed = 5` тиков на @@ -365,21 +408,19 @@ L1-PASS, обход комнат может добавить к ней новы `fight_speed = 6` = **100 мс**. У нас `roomtest.c` ждёт **три** `gfx_wait_vsync()` = 60 мс, и отдельной скорости боя нет — то есть примерно **+39 % к скорости эталона**. Соответствие: 4 ожидания (80 мс) обычно, 5 (100 мс) в бою. -**Делать ПОСЛЕ CLIP-1**: замедление кадра спрячет проблемы бюджета вместо -того, чтобы их показать. Проверка — секундомером по одинаковому отрезку -рядом с живым SDLPoP, не «на глаз». +Условие «делать ПОСЛЕ CLIP-1» **снято** — CLIP-1 закрыт (см. P0). Проверка — +секундомером по одинаковому отрезку рядом с живым SDLPoP, не «на глаз». ### L1-PASS. Сквозное прохождение уровня 1 От старта до двери уровня одним заходом: подбор меча, страж, кнопки/ворота, пики, loose-полы, зелье, падения. Это приёмка этапа 1 и одновременно регресс-база для уровня 2. -Заодно проверить на живом проходе открытую косметику окклюзии — -[BUG-CEIL-1](bug_list.md#bug-ceil-1), -[BUG-CEIL-3](bug_list.md#bug-ceil-3), -[BUG-OCCL-1](bug_list.md#bug-occl-1): именно здесь станет видно, мешают ли -они играть или это придирки на стоп-кадре. И подъём на тайл-кнопку — см. -оговорку к BUG-3 в [`bug_closed.md`](bug_closed.md). +Совмещать с [таблицей обхода 24 комнат](bug_list.md#обход-всех-24-комнат-уровня-1): +известные баги закрыты, значит новые придут только отсюда. Точки, где стоит +смотреть внимательно, — уже закрытая косметика окклюзии (потолок при прыжке +вверх, шов при анимации решётки, грани дальней колонны) и подъём на +тайл-кнопку: см. оговорку к BUG-3 в [`bug_closed.md`](bug_closed.md). --- @@ -389,9 +430,6 @@ L1-PASS, обход комнат может добавить к ней новы - **Таймер уровня / HUD времени / меню / сохранения** — Фаза 6. - **[T-1](bug_list.md#t-1)** (пики: перерисовка по причине) — отдаётся почти бесплатно после [T-2](bug_list.md#t-2), отдельно не окупается. -- **[BUG-CEIL-2](bug_list.md#bug-ceil-2)** (loose-плита в потолке из комнаты - сверху) — требует персистентного per-room modifier соседей, это Фаза P0 из - `../docs/gates_spikes_plan.md`. - **Отключение мыши на время игры** и **замена PRNG** — `../docs/ideas_backlog.md` (оба дают доли процента кадра). - **OPT-1** (хирургический редрой шва) — решено НЕ делать, стоимость diff --git a/applications/PoP/roomtest/_pop_draw.h b/applications/PoP/roomtest/_pop_draw.h new file mode 100644 index 0000000..712418b --- /dev/null +++ b/applications/PoP/roomtest/_pop_draw.h @@ -0,0 +1,47 @@ +/* + * _pop_draw.h — внутренний заголовок слоя отрисовки: выбор ядра libbgi + * (клипающее / линейное «noclip») по одному и тому же тесту для блита и + * для heal. + * + * ПОЧЕМУ ОТДЕЛЬНЫЙ ФАЙЛ, А НЕ pop_bg.h. Обе функции — `static inline`, а + * SDCC 4.5 оставляет тело такой функции в КАЖДОМ TU, который видит + * объявление, даже если тот её не зовёт (memory + * `sdcc_inline_codegen_findings`). pop_bg.h включают почти все модули PoP, + * то есть из общего заголовка эти ~100 Б размножились бы десятком мёртвых + * копий (замер CLIP-1, 2026-08-01: +1091 Б в _CODE и +636 Б в банке + * стража — за код, который там никто не вызывает). Здесь их видят ровно + * три файла, которые реально рисуют: pop_bg.c, pop_kid.c, pop_gdraw.c. + */ +#ifndef POP_DRAW_H +#define POP_DRAW_H + +#include +#include + +/* Спрайт целиком на экране И укладывается в 8-битные параметры noclip- + * примитивов libbgi? Условие входа в gfx_blit_cols_part_noclip: клипающий + * вариант платит ~5.6 К тактов подготовки на КАЖДЫЙ вызов, независимо от + * того, вылезает край или нет. */ +static inline uint8_t pop_onscreen_cols(int x, int y, uint16_t w, uint16_t h) +{ + return (uint8_t)(x >= 0 && y >= 0 && w < 256 && h < 256 && + x + (int)w <= 320 && y + (int)h <= 256); +} + +/* Стереть прямоугольник (heal из ОЗУ-копии) тем же приёмом, что и блит: + * целиком на экране → линейное ядро без клипа. Условие входа у + * gfx_heal_noclip ровно то же, что у gfx_blit_cols_part_noclip, поэтому + * тест один — общее ядро gfx_heal платит за клип и 16-бит, а не за пиксели + * (замер: 11 658 тактов на heal 22×22). + * + * НЕ inline (тело — pop_draw.c, резидент W1): SDCC 4.5 встраивал бы его в + * каждое место вызова по 181 Б И оставлял мёртвую копию в каждом TU — см. + * шапку pop_draw.c с замером. Зовётся 5 раз за кадр, цена вызова тонет в + * стоимости самого heal. + * + * w/h тут int, а не uint16_t: вызывающие считают их вычитанием (h − skip), и + * отрицательный результат обязан быть no-op, а не превратиться в огромный + * unsigned. */ +void pop_heal_fast(int x, int y, int w, int h); + +#endif diff --git a/applications/PoP/roomtest/bug_closed.md b/applications/PoP/roomtest/bug_closed.md index af680bb..9b531f1 100644 --- a/applications/PoP/roomtest/bug_closed.md +++ b/applications/PoP/roomtest/bug_closed.md @@ -84,6 +84,72 @@ BUG-3 — дубль пункта «спуск с кнопки (room8, кром --- +## Косметика окклюзии — закрыта кодом, список отставал (сверено 2026-08-01) + +Четыре записи от 2026-07-22 висели как открытые Medium. Каждая описывала +недостающий кусок порта; каждый из них с тех пор написан, но записи никто не +снял. Ниже — что именно закрывает каждую. + +### BUG-CEIL-1. Прыжок вверх: руки Kid рисуются ПОВЕРХ потолка — **ЗАКРЫТ** + +**Был симптом:** при прыжке вверх (SEQ up, кадры 67..79) руки/голова Kid +заходили в полосу кладки у потолка (row −1) и рисовались ПОВЕРХ неё. + +**Чем закрыт:** `ceil_over_kid_tile()` (`pop_bg.c:1275`) — порт «нижняя грань +потолка и кадр плиты-потолка идут в FOREtable», т.е. поверх персонажа. +Зовётся из `pop_fore_over_kid` (`pop_bg.c:1575`) и из fore-прохода стража +(`:1607`). Комментарий на месте прямо называет причину: «иначе руки +прыгающего Kid лезут на кромку потолка». + +### BUG-CEIL-2. Тряска/разбитие loose-плиты в потолке (row −1) — **ЗАКРЫТ** + +**Был симптом:** loose-плита в ряду 2 верхнего соседа (room5 (2,5) → потолок +room6 над (0,5)) при прыжках Kid на (0,5) не тряслась и не разбивалась — +loose-состояние соседней комнаты не тянулось. + +**Чем закрыт:** отдельное состояние плиты-потолка `pop_ceil_modif[10]` +(`pop_bg.c:547`, ведёт `pop_map`) + пара `pop_ceil_shake_draw()` / +`pop_ceil_bake_empty()` (`pop_bg.c:815,827`): дрожание рисуется на текущей +странице поверх фона, а провал «запекается» в ОЗУ-копию, чтобы heal его +сохранял. В полосе у потолка виден только НИЗ плиты (`draw_tile_aboveroom`), +всё выше режется клипом `POP_YOFF` — как в оригинале. + +Из этого следует, что и запись «требует персистентного per-room modifier +соседей, это Фаза P0 gates_spikes_plan» больше не верна: понадобился не общий +механизм, а один массив на 10 байт под конкретный случай. + +### BUG-CEIL-3. Анимация ворот стирает потолок над ними — **ЗАКРЫТ** + +**Был симптом:** при анимации решётки шва полоса кладки у потолка НАД +воротами пропадала — чёрный `bar` по col0 стирал ряд −1, а redraw его не +восстанавливал. + +**Чем закрыт:** `pop_room_redraw_seam_left()` (`pop_bg.c:793`) больше не +трогает полосу потолка — `bar` идёт с `POP_YOFF + 3`, а не с `POP_YOFF`: +низ полосы кладки на room-space y=2, бары ворот начинаются с y=3 +(`gate_top_y = dby − 62`). Приятный побочный эффект: отпала необходимость +перерисовывать `draw_tile(-1,0)` на КАЖДОМ кадре анимации решётки — то есть +фикс не только косметический, но и вдвое дешевле прежнего. + +### BUG-OCCL-1. Тень дальней колонны перекрывает Kid — **ЗАКРЫТ** + +**Был симптом:** Kid у (0,4)-(0,5) частично перекрыт тёмной штриховкой — +боковой гранью ДАЛЬНЕЙ колонны, которая окклюдить персонажа не должна +(over-occlusion fore-слоя, рисовавшего fore футпринт-тайлов без учёта +глубины). + +**Чем закрыт:** разделение слоёв по признаку из оригинала, а не по нашим +соображениям о глубине (`overlay_mid_tile`, `pop_bg.c:1367`). Ключ, +подтверждённый трассой оригинала: `draw_tile_right`, `draw_tile_anim_right` и +`draw_loose` кладут спрайты через `add_backtable` НАПРЯМУЮ, минуя +`ptr_add_table` — значит правая грань левого соседа («шахматка» столба 93, +blueline, грани пик/loose соседа, кадр loose) **всегда** рисуется ПОД +персонажем. Через `ptr_add_table` (→ midtable при `draw_other_overlay`) идут +только «floor B» 42 при левом соседе-поле, `draw_tile_base`, +`draw_tile_anim` (свои пики) и `draw_tile_bottom`. + +--- + ## BUG-SEAM-PINGPONG (#4). Пинг-понг drawn_room у шва с закрытыми воротами — **РЕШЁН 2026-07-22** Настоящий корень найден потиковой трассой ЖИВОГО SDLPoP 1.23 diff --git a/applications/PoP/roomtest/bug_list.md b/applications/PoP/roomtest/bug_list.md index 25e38fc..6f6a860 100644 --- a/applications/PoP/roomtest/bug_list.md +++ b/applications/PoP/roomtest/bug_list.md @@ -8,73 +8,23 @@ odd-pixel арифметика `char_x`, подстановка тайла на Приоритеты работ — в [`TASKS.md`](TASKS.md), а не здесь. Правило проекта: механику сверять с `../SDLPoP/src/` ДО кодинга. -Ревизия списка: **2026-08-01** (задача L1-TRIAGE). BUG-1, BUG-2 и BUG-3 -стояли как Critical с 2026-07-21 — все три прогнаны в MAME и закрыты, -протокол в [`bug_closed.md`](bug_closed.md). Critical'ов сейчас нет. +Ревизия списка: **2026-08-01** (задача L1-TRIAGE). Закрыты и перенесены в +архив семь записей: BUG-1, BUG-2, BUG-3 (стояли как Critical с 2026-07-21, +прогнаны в MAME) и вся косметика окклюзии BUG-CEIL-1/2/3, BUG-OCCL-1 (была +починена кодом ещё в июле, список просто отстал — чем именно закрыта каждая, +расписано в [`bug_closed.md`](bug_closed.md)). -| ID | что | тяжесть | -|----|-----|---------| -| [BUG-CEIL-1](#bug-ceil-1) | прыжок вверх: руки Kid поверх потолка | Medium | -| [BUG-CEIL-3](#bug-ceil-3) | анимация ворот стирает потолок над ними | Medium | -| [BUG-OCCL-1](#bug-occl-1) | тень дальней колонны перекрывает Kid | Medium | -| [BUG-CEIL-2](#bug-ceil-2) | loose-плита в потолке не трясётся | Medium, отложен | +**Открытых багов сейчас нет** — только две незакрытые оптимизации и +незаконченная ревизия комнат: + +| ID | что | тип | +|----|-----|-----| | [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | | [T-2](#t-2) | Кид перерисовывается в покое | оптимизация | | [обход 24 комнат](#обход-всех-24-комнат-уровня-1) | таблица заполнена на 5 строк из 24 | ревизия | ---- - - -## BUG-CEIL-1. Прыжок вверх: руки Kid рисуются ПОВЕРХ потолка — **Medium** - -**Симптом (2026-07-22):** при прыжке вверх (SEQ up, кадры 67..79) руки/голова -Kid заходят в полосу кладки у потолка (row -1) и рисуются ПОВЕРХ неё — потолок -должен окклюдить руки (как передняя грань). - -**Где:** `pop_bg.c` — полоса потолка (`draw_tile(-1)`) сейчас только фон -(запечена в pop_room_draw), не окклюдер поверх Kid. Нужен ceiling-overlay в -`pop_fore_over_kid` (перерисовать срез полосы над Kid поверх спрайта), по -аналогии с fore-тайлами. Инфраструктура `g_clip_top`/`gfx_blit_part` уже есть. - - -## BUG-CEIL-3. Анимация ворот стирает потолок над ними — **Medium** - -**Симптом (2026-07-22):** при опускании/поднятии решётки шва (анимация openness) -полоса кладки у потолка НАД воротами пропадает (см. потолок над левым швом). - -**Где:** `pop_bg.c` `pop_room_redraw_seam_left` (и `pop_button_redraw`/ -`pop_spike_redraw` — общий класс): чёрный `bar` по col0 row0 стирает потолочную -полосу (row-1), а redraw рисует только сам гейт, не восстанавливая полосу. -Нужно: при перерисовке шва/тайла у потолка перерисовать и срез потолочной полосы -(draw_tile(-1,col) с g_clip_top) над затронутыми колонками. - - -## BUG-OCCL-1. Тень дальней колонны перекрывает Kid в (0,4)-(0,5) — **Medium** - -**Симптом (2026-07-22):** Kid, стоя/прыгая у (0,4)-(0,5), частично перекрыт -тёмной штриховкой — это ТЕНЬ/боковая грань ДАЛЬНЕЙ колонны (сзади по глубине), -которая НЕ должна окклюдить персонажа. Over-occlusion fore-слоя. - -**Где:** `pop_bg.c` `pop_fore_over_kid`/`fore_tile` — рисует fore футпринт-тайлов -без учёта глубины (какая грань РЕАЛЬНО перед Kid). Порт seg003 redraw_at_char -окклюдит только передние грани; дальняя колонна (за Kid) не должна. Сверить с -SDLPoP redraw_at_char (какие тайлы/грани идут в fore vs back по y-сортировке). - - -## BUG-CEIL-2. Тряска/разбитие loose-плиты в потолке (row -1) не работает — **Medium, ОТЛОЖЕН** - -**Симптом (2026-07-22):** loose-плита в ряду 2 верхнего соседа (room5 (2,5) → -потолок room6 над (0,5)) при прыжках Kid на (0,5) НЕ трясётся и не разбивается. - -**Где:** верхний сосед не «живой» — `above_fg` берётся из статики уровня, -loose-состояние соседней комнаты не тянем (per-room loose/modif не персистятся). -check_press/knock работают только по текущей комнате. Нужно: (а) пробой -loose сверху через границу комнаты, (б) персистентное loose-состояние соседа + -его отрисовка в полосе. Пока полоса рисует loose статически (кадр покоя). - -**Почему отложен:** требует персистентного per-room modifier соседей — это -фаза P0 из [`../docs/gates_spikes_plan.md`](../docs/gates_spikes_plan.md), а не -точечный фикс. См. «Отложено осознанно» в [`TASKS.md`](TASKS.md). +Это НЕ значит «багов нет» — значит, что известные найдены и закрыты, а +следующая порция придёт из обхода 24 комнат и сквозного прохождения (L1-PASS). --- diff --git a/applications/PoP/roomtest/pop_bg.c b/applications/PoP/roomtest/pop_bg.c index 3b94a3a..55ed518 100644 --- a/applications/PoP/roomtest/pop_bg.c +++ b/applications/PoP/roomtest/pop_bg.c @@ -14,6 +14,7 @@ #include #include #include "pop_bg.h" +#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */ #include "pop_state.h" #include "pop_geom.h" #include "pop_level.h" /* pop_doorlink2 — состояние нажатия кнопки */ @@ -135,18 +136,12 @@ static void blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) gfx_w0_unmap(); } -/* heal в room-space (со сдвигом POP_YOFF) — все heal слоя фона/loose. */ +/* heal в room-space (со сдвигом POP_YOFF) — все heal слоя фона/loose. + * Выбор ядра (noclip / клипающее) — общий с Кидом и стражем, pop_heal_fast + * в pop_bg.h. */ static void heal_off(int x, int y, int w, int h) { - int sy = y + POP_YOFF; - /* Без клипа — линейным ядром (~2.9×, как gfx_blit_noclip): общий - * gfx_heal платит за клип/16-бит/split, а не за пиксели (замер: - * 11 658 тактов на heal 22×22). */ - if (x >= 0 && sy >= 0 && w > 0 && h > 0 && w < 256 && h < 256 && - x + w <= 320 && sy + h <= 256) - gfx_heal_noclip(x, sy, (uint8_t)w, (uint8_t)h); - else - gfx_heal(x, sy, w, h); + pop_heal_fast(x, y + POP_YOFF, w, h); } /* Область, куда в этом кадре рисовался ОВЕРЛЕЙ (не футпринт fore, а @@ -1190,6 +1185,11 @@ void pop_room_clip_borders(void) __banked { if (!border_dirty) return; border_dirty--; + /* ОСТАЁТСЯ клипающим осознанно (CLIP-1, 2026-08-01): полоса шириной 320 + * не лезет в 8-битный параметр noclip-ядра, а разбивать её на два куска + * по 160 нет смысла — гейт border_dirty пускает сюда только в кадрах + * падения/выхода за поле, и выигрыш (~2×5.6 К тактов подготовки) там + * тонет в цене самих 320×28 пикселей. Не «оптимизировать» повторно. */ gfx_heal(0, 0, 320, POP_YOFF); /* верхний борт */ gfx_heal(0, POP_CLIP_BOTTOM, 320, 256 - POP_CLIP_BOTTOM);/* статус-полоса + нижний борт */ } diff --git a/applications/PoP/roomtest/pop_bg.h b/applications/PoP/roomtest/pop_bg.h index 05e72ca..0f8445a 100644 --- a/applications/PoP/roomtest/pop_bg.h +++ b/applications/PoP/roomtest/pop_bg.h @@ -25,18 +25,10 @@ /* Экранный y нижней границы поля (ниже — статус-полоса + борт → клип). */ #define POP_CLIP_BOTTOM (POP_YOFF + POP_PLAYFIELD_H) -/* Спрайт целиком на экране И укладывается в 8-битные параметры noclip- - * примитивов libbgi? Условие входа в gfx_blit_cols_part_noclip: клипающий - * вариант платит ~5.6 К тактов подготовки на КАЖДЫЙ вызов, независимо от - * того, вылезает край или нет. - * static inline, а не чистый inline: SDCC оставляет мёртвое тело в каждом TU - * (memory sdcc_inline_codegen_findings), но здесь это ~30 байт на два TU — - * дешевле риска неразрешённой ссылки. */ -static inline uint8_t pop_onscreen_cols(int x, int y, uint16_t w, uint16_t h) -{ - return (uint8_t)(x >= 0 && y >= 0 && w < 256 && h < 256 && - x + (int)w <= 320 && y + (int)h <= 256); -} +/* Выбор ядра libbgi (pop_onscreen_cols / pop_heal_fast) переехал в + * `_pop_draw.h` — его включают только три рисующих модуля. Из общего + * заголовка SDCC размножал мёртвые копии этих inline'ов по всем TU: см. + * шапку `_pop_draw.h`. */ /* Загрузить 7 атласов фона. Звать ДО initgraph (как poc.c — atlas_load * трогает W3; проверенный порядок). Палитру pop_bg.pal грузит ПРИЛОЖЕНИЕ diff --git a/applications/PoP/roomtest/pop_draw.c b/applications/PoP/roomtest/pop_draw.c new file mode 100644 index 0000000..e78802b --- /dev/null +++ b/applications/PoP/roomtest/pop_draw.c @@ -0,0 +1,29 @@ +/* + * pop_draw.c — выбор ядра libbgi для heal: линейное «noclip», если + * прямоугольник целиком на экране, иначе общее клипающее. Объявление и + * мотивация — `_pop_draw.h`. + * + * ПОЧЕМУ ОТДЕЛЬНЫЙ РЕЗИДЕНТНЫЙ МОДУЛЬ, а не inline в заголовке. Замер + * CLIP-1 (2026-08-01), SDCC 4.5: `static inline` тут обходится втридорога — + * компилятор И встраивает тело в каждый вызов (181 Б на место), И оставляет + * отдельную мёртвую копию в каждом TU. У стража это раздуло + * `pop_guard_heal` с ~60 до 663 Б. Пяти местам вызова это стоило бы ~1.4 КБ + * ради экономии ~50 тактов на вызов — при том что BANK2 занят на 86 %. + * + * Резидент W1 (_CODE), а не банк: сюда зовут из ДВУХ разных банков + * (pop_bg.c → BANK2, pop_gdraw.c → BANK4) и из резидента (pop_kid.c). W1 + * замаплено всегда, поэтому из банка это обычный `call` без трамплина — + * тот же приём, что у pop_sword_draw. + */ +#include +#include +#include "_pop_draw.h" + +void pop_heal_fast(int x, int y, int w, int h) +{ + if (w <= 0 || h <= 0) return; + if (pop_onscreen_cols(x, y, (uint16_t)w, (uint16_t)h)) + gfx_heal_noclip(x, y, (uint8_t)w, (uint8_t)h); + else + gfx_heal(x, y, w, h); +} diff --git a/applications/PoP/roomtest/pop_gdraw.c b/applications/PoP/roomtest/pop_gdraw.c index 8e60450..1946be5 100644 --- a/applications/PoP/roomtest/pop_gdraw.c +++ b/applications/PoP/roomtest/pop_gdraw.c @@ -17,6 +17,7 @@ #include "pop_guard.h" #include "pop_kid.h" /* pop_sword_draw — клинок общий с Кидом */ #include "pop_bg.h" /* POP_YOFF, pop_fore_over_char */ +#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */ #include "pop_map.h" /* hitp_curr/hitp_max — HP Кида */ #include "kid_data.h" @@ -59,11 +60,15 @@ void pop_guard_free(void) __banked g_ok = 0; } +/* Стирание прошлого кадра стража и его клинка. pop_heal_fast — по той же + * причине, что в kid_heal: стираем ровно то, что рисовали, а рисовали в + * большинстве кадров noclip-ядром. Два прямоугольника, а не объединение + * (см. комментарий к sx_l выше). */ void pop_guard_heal(void) __banked { uint8_t p = gfx_get_draw_page() & 1; - if (gvalid[p]) gfx_heal(gx_l[p], gy_l[p] + POP_YOFF, gw_l[p], gh_l[p]); - if (svalid[p]) gfx_heal(sx_l[p], sy_l[p] + POP_YOFF, sw_l[p], sh_l[p]); + if (gvalid[p]) pop_heal_fast(gx_l[p], gy_l[p] + POP_YOFF, gw_l[p], gh_l[p]); + if (svalid[p]) pop_heal_fast(sx_l[p], sy_l[p] + POP_YOFF, sw_l[p], sh_l[p]); gvalid[p] = svalid[p] = 0; } diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c index ee59a90..cac64fb 100644 --- a/applications/PoP/roomtest/pop_kid.c +++ b/applications/PoP/roomtest/pop_kid.c @@ -12,6 +12,7 @@ #include "pop_kid.h" #include "pop_guard.h" /* Guard/pop_gframe/CHARID_* — интерпретатор общий */ #include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */ +#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */ #include "pop_map.h" /* pop_clip_char_top (clip_char) */ #include "pop_geom.h" #include "kid_data.h" @@ -108,7 +109,9 @@ void pop_kid_img_blit(uint8_t image, int x, int y) h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); /* Атласы Кида хранятся COLUMN-MAJOR (transpose_cols в pop_pack_kid — * ради бесплатного горизонтального флипа), поэтому блит обязан быть - * колоночным. Row-major gfx_blit_noclip выдал бы марку транспонированной. */ + * колоночным. Row-major gfx_blit_noclip выдал бы марку транспонированной. + * noclip БЕЗ проверки (CLIP-1): единственный вызывающий — полоса HP + * (pop_gdraw.c), у неё координаты фиксированные и заведомо на экране. */ if (w && h) gfx_blit_cols_part_noclip(x, y, img, 0, 0, 0); gfx_w0_unmap(); } @@ -605,14 +608,20 @@ void kid_draw_splash(void) gfx_w0_unmap(); } -/* Восстановить фон из теневой копии на месте ПРЕДЫДУЩЕГО кадра Kid. */ +/* Восстановить фон из теневой копии на месте ПРЕДЫДУЩЕГО кадра Kid. + * + * pop_heal_fast, а не голый gfx_heal: прямоугольник тут — это ровно тот + * прямоугольник, который kid_draw нарисовал noclip-ядром, то есть в + * подавляющем большинстве кадров он заведомо на экране. Клипающий путь + * остаётся для straddle (kid_render_dx = ∓140) и падения ниже поля — + * там координата реально уходит за экран. */ void kid_heal(void) { uint8_t p = gfx_get_draw_page(); - if (kid_lvalid[p]) - gfx_heal(kid_lx[p], kid_ly[p] + POP_YOFF, kid_lw[p], kid_lh[p]); /* +YOFF: центрирование */ + if (kid_lvalid[p]) /* +YOFF: центрирование */ + pop_heal_fast(kid_lx[p], kid_ly[p] + POP_YOFF, kid_lw[p], kid_lh[p]); if (kid_ovalid[p]) - gfx_heal(kid_ox[p], kid_oy[p] + POP_YOFF, kid_ow[p], kid_oh[p]); + pop_heal_fast(kid_ox[p], kid_oy[p] + POP_YOFF, kid_ow[p], kid_oh[p]); kid_lvalid[p] = kid_ovalid[p] = 0; }