From a25ce5886917a06736c8a1e1443bc859c6552960 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Sat, 8 Aug 2026 18:39:00 +0300 Subject: [PATCH] =?UTF-8?q?DRAW-COST=20=D1=88=D0=B0=D0=B3=201:=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=BF=D1=83=D1=81=D0=BA=20=D0=BD=D0=B5=D0=B8=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B8=D0=B2=D1=88=D0=B5=D0=B3=D0=BE=D1=81?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=B5=D1=80=D1=81=D0=BE=D0=BD=D0=B0=D0=B6=D0=B0?= =?UTF-8?q?=20=E2=80=94=20210%=20->=20116%=20=D0=BA=D0=B0=D0=B4=D1=80?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Персонаж, у которого с прошлой отрисовки ЭТОЙ страницы дабл-буфера не изменился ни один вход отрисовки, а фон в его прямоугольнике не трогали, уже нарисован правильно: 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. --- applications/PoP/roomtest/CLAUDE.md | 4 +- applications/PoP/roomtest/TASKS_OPEN.md | 113 ++++++++++++++---------- applications/PoP/roomtest/pop_bg.c | 3 + applications/PoP/roomtest/pop_cdraw.c | 113 +++++++++++++++++++++++- applications/PoP/roomtest/pop_cdraw.h | 41 +++++++++ applications/PoP/roomtest/pop_room.c | 15 +++- applications/PoP/roomtest/pop_tile.c | 27 ++++++ applications/PoP/roomtest/roomtest.c | 27 ++++-- 8 files changed, 283 insertions(+), 60 deletions(-) diff --git a/applications/PoP/roomtest/CLAUDE.md b/applications/PoP/roomtest/CLAUDE.md index d9b883a..ae3ad6d 100644 --- a/applications/PoP/roomtest/CLAUDE.md +++ b/applications/PoP/roomtest/CLAUDE.md @@ -60,12 +60,12 @@ Kid — `kid_data.h` (генерится `pop_extract_kid_data.py`). | Файл | Роль (порт SDLPoP) | |------|--------------------| | `roomtest.c` | Главный цикл: дабл-буфер (2 страницы + флип на vsync), tick→draw порядок слоёв. | -| `pop_tile.c/.h` | Общие ЛИСТЬЯ слоя фона — РЕЗИДЕНТ W1: блит куска атласа, чтение тайла комнаты, `tile_table`. Резидент потому, что их зовут обе половины слоя фона из разных банков, а `const`-таблицы банка из чужого банка не видны. | +| `pop_tile.c/.h` | Общие ЛИСТЬЯ слоя фона — РЕЗИДЕНТ W1: блит куска атласа, чтение тайла комнаты, `tile_table`, метка «фон трогали вот здесь» (`pop_cd_touch`). Резидент потому, что их зовут обе половины слоя фона из разных банков, а `const`-таблицы банка из чужого банка не видны. | | `pop_bg.c/.h` | ГОРЯЧАЯ половина слоя фона (банк 2, каждый кадр): fore-окклюзия поверх ЛЮБОГО персонажа (`seg003` redraw_at_char/char2 — один проход на всех Char), оверлеи кромки, кладка стены (`wall_pattern`), клип. `pop_bg.h` — публичный API ВСЕГО слоя (обеих половин). | | `pop_room.c` | ХОЛОДНАЯ половина слоя фона (банк 7, раз на комнату/событие): полная отрисовка комнаты (`seg008` draw_tile), точечные перерисовки тайлов (пики, ворота, кнопки, дверь уровня, дрожащие плиты), падающие куски loose (`seg007` mob), загрузка атласов. | | `_pop_bg.h` | Внутренний контракт между половинами: общее состояние + три тонкие `__banked`-обёртки. Там же расписано, почему рез прошёл именно здесь и сколько стоит. | | `pop_kid.c/.h` | Анимация/движение: интерпретатор seqtbl `play_seq` + frame_table (`seg006`), окна `Char` (loadkid/loadshad), атласы Кида. | -| `pop_cdraw.c/.h` | ОТРИСОВКА персонажей — одна на всех Char (порт `add_kid_to_objtable`/`add_guard_to_objtable`, `seg008:22F0/2324`): спрайт, clip_char, брызги, клинок, heal; + полоса HP и палитры соперника. | +| `pop_cdraw.c/.h` | ОТРИСОВКА персонажей — одна на всех Char (порт `add_kid_to_objtable`/`add_guard_to_objtable`, `seg008:22F0/2324`): спрайт, clip_char, брызги, клинок, heal; + полоса HP и палитры соперника. Там же ПРОПУСК неизменившегося кадра (`pop_char_skip_mask`): персонаж, у которого с прошлой отрисовки этой страницы ничего не изменилось и фон под ним не трогали, не перерисовывается вовсе. | | `pop_ctrl.c/.h` | Управление: диспетчер `control()` (`seg005`) + ввод `read_user_control` (`seg006`) поверх ``. | | `pop_map.c/.h` | Коллизия с картой + физика падения/приземления/стены + loose-полы (`seg005/006/007`). | diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md index 7a20d1c..e75fc5c 100644 --- a/applications/PoP/roomtest/TASKS_OPEN.md +++ b/applications/PoP/roomtest/TASKS_OPEN.md @@ -55,7 +55,7 @@ | 2 | [L3-CHOMP](#l3-chomp) | **СЛЕДУЮЩАЯ**: чомперы (5 шт) | прохождение ур. 3 | | — | [L3-SKEL](#l3-skel) | скелет ур. 3 — **сделан 2026-08-07**, ждёт финальной приёмки | — | | 3 | [L3-PASS](#l3-pass) | приёмка уровня 3 (обход комнат) | закрытие цели | -| — | [DRAW-COST](#draw-cost) | **кадр не укладывается в бюджет: ~210 % кадрового периода в ПОКОЕ** (комн. 3), 140 % без трупа стража (комн. 2) | плавность на ВСЕХ уровнях | +| — | [DRAW-COST](#draw-cost) | шаг 1 сделан (пропуск неизменившегося персонажа): в покое 210 % -> **116 %** кадрового периода; дальше узкое место — ЛОГИКА (60 % на тик стоящих персонажей) | плавность на ВСЕХ уровнях | | — | [L1-SPEED](#l1-speed) | игра на ~39 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент | | — | [TUNE-1](#tune-1) | параметры движка → cfg-файл (сейчас `pop_tune.h`) | отладка таймингов и моды; берётся по мере надобности | @@ -318,53 +318,73 @@ W1/W2 (см. [BUG-GUARD-COLOR-1](bug_closed.md#bug-guard-color-1)). ### DRAW-COST. Кадр НЕ УКЛАДЫВАЕТСЯ в бюджет — нужна оптимизация -> **ЗАМЕР ПОЛЬЗОВАТЕЛЯ 2026-08-08 (после MEM-BANK2).** Уровень 1, комната 3, -> страж УБИТ, Кид СТОИТ (то есть это НЕ худший случай — ни боя, ни движения): -> -> | полоса | фаза | доля кадрового периода | -> |---|---|---| -> | синяя (`PROF(2)`) | ввод + heal + логика | ~80 % | -> | зелёная (`PROF(4)`) | слой фона (loose/ловушки) | ~20 % | -> | циан (`PROF(6)`) | спрайты + fore-проход | **~110 %** | -> -> **Итого ~210 % — больше ДВУХ кадровых периодов на кадр.** Это много; в -> покое должно оставаться место под бой, движение и вторую фигуру. -> Приоритет задачи поднят: это уже не косметика профиля. -> -> **Контрольный замер там же: комната 2** — та же по устройству, но БЕЗ тела -> стража: 60 % / 20 % / 60 % = **~140 %**. +> **ШАГ 1 СДЕЛАН 2026-08-08: пропуск неизменившегося персонажа.** Комната +> 1.3, труп стража, Кид стоит: было **210 %** кадрового периода, стало +> **116 %** (500 772 такта при бюджете 430 000). Отрисовка перестала быть +> узким местом: персонажи в покое не рисуются ВООБЩЕ (ноль вызовов +> `pop_heal_fast` за кадр), весь фон — ДВА блита факелов (44 136 тактов). +> Механизм и почему метка позиционная — в шапке `pop_cdraw.h`. -### Первый след: ТЕЛО УБИТОГО СТРАЖА стоит ~70 % кадра +**Исходные замеры пользователя (полосы бордюра, до шага 1).** Уровень 1, +Кид СТОИТ — то есть НЕ худший случай, ни боя, ни движения: -Комнаты 2 и 3 отличаются ровно одним — трупом, и он даёт +20 % синей и -+50 % циана. Причина видна прямо в коде и НЕ требует замера: ни -`roomtest.c`, ни `pop_cdraw.c` не смотрят на `Char.alive`. Мёртвый страж -сохраняет `charid != 0`, поэтому КАЖДЫЙ кадр честно проходит весь путь -живого персонажа — `pop_char_heal(OPP)` (стереть), `pop_char_draw(OPP)` -(блит + clip_char + брызги + клинок), `pop_char_fore(OPP)` (перебор тайлов -футпринта с разбором кладки). При этом труп НЕ МЕНЯЕТСЯ: его кадр -постоянен до выхода из комнаты. +| комната | синяя (ввод+heal+логика) | зелёная (фон) | циан (спрайты) | итого | +|---|---|---|---|---| +| 3, страж УБИТ | ~80 % | ~20 % | ~110 % | **~210 %** | +| 2, стража НЕТ | ~60 % | ~20 % | ~60 % | ~140 % | -**Что напрашивается** (порядок — от дешёвого к дорогому, каждое МЕРИТЬ): -1. запечь труп в фон один раз, как делают loose-плиты (`pop_floor_bake`) — - тогда heal сам его восстанавливает, а per-frame путь исчезает целиком. - Сверить с SDLPoP: там мёртвый страж остаётся обычной записью objtable, но - у нас нет их системы «перерисовать только грязные тайлы», и запечка — наш - штатный ответ на «статичный элемент поверх фона» (см. `pop_loose_floors`); -2. если запечка не годится (труп надо стирать при выходе/рестарте) — - минимум пропустить `pop_char_fore(OPP)` для мёртвого: передние грани - вокруг неподвижного тела тоже не меняются. +Разница ровно в теле убитого стража: +20 % синей и +50 % циана. Труп +сохраняет `charid != 0`, поэтому каждый кадр честно проходил весь путь +живого персонажа (heal → спрайт+clip_char+брызги+клинок → fore-проход), хотя +его кадр постоянен до выхода из комнаты. -Проверять на той же паре комнат 2/3 — разница должна схлопнуться. +**Что сделано (шаг 1).** Не спецкейс «мёртвый», а общее правило: у каждой +страницы дабл-буфера свой снимок ВХОДОВ отрисовки слота; совпал снимок, +спрайт этой страницы цел и фон в его прямоугольнике не трогали — heal, блит +и fore-проход пропускаются целиком. Покрывает и труп, и стоящего Кида, и +ждущего стража. Детали контракта — `pop_cdraw.h`, реализация — +`pop_char_skip_mask` / `cd_quiet` в `pop_cdraw.c`, метка фона — +`pop_cd_touch` в резидентном `pop_tile.c`. + +Грабли, на которые наступили по дороге: сначала метка была ФЛАГОМ «фон +трогали хоть где-то» — и выигрыш оказался ровно нулевым, потому что факелы +анимируются каждый кадр и гасили пропуск для всех персонажей сразу (замер: +597 684 такта, как без оптимизации). Метка обязана быть ПОЗИЦИОННОЙ. + +**Остаточный эффект от объединения прямоугольников.** Метка одна на +страницу — объединение всех правок фона. В комнате 1 два факела дают +прямоугольник x 40…88, а стоящий там Кид занимает x 32…44: пересечение 4 px, +и он перерисовывается каждый кадр (140 % против 94 %, если отойти). Если +понадобится — держать не один прямоугольник, а 3-4 отдельных и мержить +только при переполнении. + +**Куда идти дальше — теперь это ЛОГИКА, а не отрисовка.** Разбивка работы +кадра брейкпоинтами (комната 3, труп, Кид стоит; всего 500 772 такта): + +| участок | тактов | % кадра | +|---|---:|---:| +| ввод + `check_skel` + луч видимости + `ctrl_tick` + heal | 65 862 | 15 % | +| `kid_tick` + физика + страж + боёвка | **260 022** | **60 %** | +| `loose_tick` + `process_trobs` | 119 562 | 28 % | +| `redraw_needed` + шов + спрайты + метка | 55 152 | 13 % | +| — из них два блита факелов | 44 136 | 10 % | + +1. **60 % на тик персонажей** при том, что оба СТОЯТ — первый кандидат. + Смотреть `pop_phys_tick`/`pop_guard_phys_tick` (банк 3) и `pop_guard_tick` + (банк 1): сколько там работы, которую неподвижный персонаж делать не + обязан, и сколько стоит трамплин на каждом шаге. +2. **28 % на `loose_tick` + `process_trobs`** в комнате БЕЗ единой ловушки — + явно перебор; разобрать, что там сканируется каждый кадр (список trob, + `pop_trob_modif` соседа для шва, `pop_room_link`). +3. Запечь труп в фон (как щебень: тело в фоне + fore-часть поверх Кида) — + тогда бесплатным станет и проход Кида ПО телу, который сейчас снимает + пропуск по правилу «слоты рядом». Предложено пользователем 2026-08-08. Мерить в ЭТОЙ точке (комната 3, страж убит, Кид стоит) — она воспроизводима и даёт нижнюю границу; худший случай (бой + бег + падающая плита) считать отдельно. Метод — memory `z80_profiling_method` (брейкпоинты с -`totalcycles`), полосы бордюра дают только раскладку по фазам. - -Обратить внимание: **синие 80 % при стоящем Киде** — это подозрительно -много для «ввод + heal + логика», heal там наверняка чистит больше, чем -нужно. Разобрать синюю фазу отдельно от циана. +`totalcycles`); полосы бордюра показывают только одну картинку из периода и +годятся лишь для раскладки по фазам. **Наблюдение пользователя на приёмке DRAW-CHAR 2026-08-08:** циан-полоса профиля (спрайты + fore) выросла — тогда «в пределах». @@ -377,15 +397,10 @@ W1/W2 (см. [BUG-GUARD-COLOR-1](bug_closed.md#bug-guard-color-1)). работы», а недостающая ранее корректность: тайл, который спрайт задевает, обязан вернуть свой передний слой поверх него. -**Куда копать, когда возьмёмся** (сначала МЕРИТЬ, потом резать — -[`pop_fore_layer_cost`](../../../docs) уже показывал 78 % кадра до кэша -кладки): -1. расширять перебор ТОЛЬКО по накладным спрайтам (клинок/брызги), а не по - всему окну: габарит самого кадра уже покрыт футпринтом + правилом меча; -2. кэш «в этом тайле fore-слоя нет вовсе» — половина колонок пустые, а за - каждую платим `tile_in_fclip` + разбор кладки; -3. считать окно клипа в тайловых координатах один раз, а не сравнивать - пиксельные габариты в двух циклах. +**Если fore-проход снова станет узким местом** (сейчас он в покое не +выполняется вовсе): расширять перебор ТОЛЬКО по накладным спрайтам +(клинок/брызги), а не по всему окну; кэш «в этом тайле fore-слоя нет +вовсе»; считать окно клипа в тайловых координатах один раз. ### DRAW-CHAR — **ЗАКРЫТА И ПРОВЕРЕНА 2026-08-08** diff --git a/applications/PoP/roomtest/pop_bg.c b/applications/PoP/roomtest/pop_bg.c index 79a7ccc..660ba8e 100644 --- a/applications/PoP/roomtest/pop_bg.c +++ b/applications/PoP/roomtest/pop_bg.c @@ -52,6 +52,9 @@ void pop_fore_heal(void) __banked if (!(ov_pending & (1 << pg))) return; pop_heal_off(ov_x0, ov_y0, ov_x1 - ov_x0, ov_y1 - ov_y0); ov_pending = (uint8_t)(ov_pending & ~(1 << pg)); + /* Восстановленный прямоугольник шире персонажа и запросто накрывает + * СОСЕДА — пометить область (DRAW-COST): heal идёт мимо pop_blit_b. */ + pop_cd_touch(ov_x0, ov_y0 + POP_YOFF, ov_x1 - ov_x0, ov_y1 - ov_y0); } diff --git a/applications/PoP/roomtest/pop_cdraw.c b/applications/PoP/roomtest/pop_cdraw.c index c8b58e2..e9632f3 100644 --- a/applications/PoP/roomtest/pop_cdraw.c +++ b/applications/PoP/roomtest/pop_cdraw.c @@ -156,8 +156,12 @@ static void cd_clip_add(pop_cdraw_t *s, int x, int y, uint16_t w, uint16_t h) /* Стирание прошлого кадра персонажа и его накладных. pop_heal_fast, а не * голый gfx_heal: стираем ровно то, что рисовали, а рисовали в подавляющем * большинстве кадров noclip-ядром; клипающий путь остаётся для straddle и - * выхода за поле. */ -void pop_char_heal(uint8_t who) __banked + * выхода за поле. + * + * Тело — static: pop_char_draw зовёт его же «страховкой» (см. ниже), а + * трамплин выбирается ОБЪЯВЛЕНИЕМ, и вызов через `__banked`-имя стоил бы + * 654 такта внутри собственного банка (memory sdcc_banked_call_rules). */ +static void cd_heal(uint8_t who) { pop_cdraw_t *s = &pop_cd[who]; uint8_t p = gfx_get_draw_page() & 1; @@ -166,6 +170,101 @@ void pop_char_heal(uint8_t who) __banked s->valid[p] = s->ovalid[p] = 0; } +void pop_char_heal(uint8_t who) __banked { cd_heal(who); } + +/* ---- Пропуск неизменившегося кадра (DRAW-COST; контракт — pop_cdraw.h) -- */ + +/* Снимок ВХОДОВ отрисовки слота. Кадр анимации (kid_frame/pop_gframe) + * однозначно определяется полем frame, поэтому его самого в снимке нет; + * тайлы вокруг персонажа (clip_char, fore-проход) в снимок тоже не входят — + * их изменение ловит pop_cd_dirty. */ +typedef struct { + uint8_t frame, x, y, action, sword, charid, room, hurt; + int8_t dir, ccol, crow; + int dx; /* render_dx: straddle-смещение */ +} cd_sig_t; + +static cd_sig_t cd_sig[POP_CH_N][2]; + +static void cd_sig_make(uint8_t who, cd_sig_t *g) +{ + const pop_char_t *ch = (who == POP_CH_KID) ? &Kid : &Guard; + g->frame = ch->frame; g->x = ch->x; g->y = ch->y; + g->action = ch->action; g->sword = ch->sword; g->charid = ch->charid; + g->room = ch->room; + g->hurt = (uint8_t)((who == POP_CH_KID) ? pop_kid_hurt : pop_guard_hurt); + g->dir = ch->direction; + g->ccol = ch->curr_col; g->crow = ch->curr_row; + g->dx = pop_cd[who].render_dx; +} + +/* Габарит слота на странице: спрайт + накладные (клинок/брызги). y — + * КОМНАТНЫЙ (как в pop_cd), перевод в экранный делает вызывающий. */ +static void cd_bbox(uint8_t who, uint8_t p, int *x0, int *y0, int *x1, int *y1) +{ + const pop_cdraw_t *s = &pop_cd[who]; + *x0 = s->x[p]; *y0 = s->y[p]; + *x1 = *x0 + (int)s->w[p]; *y1 = *y0 + (int)s->h[p]; + if (s->ovalid[p]) { + if (s->ox[p] < *x0) *x0 = s->ox[p]; + if (s->oy[p] < *y0) *y0 = s->oy[p]; + if (s->ox[p] + (int)s->ow[p] > *x1) *x1 = s->ox[p] + (int)s->ow[p]; + if (s->oy[p] + (int)s->oh[p] > *y1) *y1 = s->oy[p] + (int)s->oh[p]; + } +} + +/* «Тихий» слот: спрайт этой страницы цел (heal его не трогал), фон в его + * прямоугольнике не трогали, и все входы отрисовки те же, что в момент, + * когда этот кадр на эту страницу и рисовали. valid[p] здесь ключевой: он + * снимается heal'ом, поэтому решение остаётся согласованным, сколько раз его + * ни перепроверяй за кадр. */ +static uint8_t cd_quiet(uint8_t who, uint8_t p) +{ + cd_sig_t g; + const uint8_t *a, *b; + uint8_t i; + if (!pop_cd[who].valid[p]) return 0; + if (pop_cd_dirty & (1 << p)) { /* фон правили — но задели ли нас? */ + int x0, y0, x1, y1; + cd_bbox(who, p, &x0, &y0, &x1, &y1); + y0 += POP_YOFF; y1 += POP_YOFF; /* метка фона — в ЭКРАННЫХ */ + if (x0 < pop_cd_dx1[p] && pop_cd_dx0[p] < x1 && + y0 < pop_cd_dy1[p] && pop_cd_dy0[p] < y1) return 0; + } + cd_sig_make(who, &g); + a = (const uint8_t *)&g; b = (const uint8_t *)&cd_sig[who][p]; + for (i = 0; i < sizeof(cd_sig_t); i++) + if (*a++ != *b++) return 0; + return 1; +} + +/* Запас вокруг габарита соседа: за кадр персонаж проходит заметно меньше + * (бег — ~11 экранных px), а прыжок с падением укладывается сюда же. */ +#define CD_MARGIN 32 + +uint8_t pop_char_skip_mask(void) __banked +{ + uint8_t p = gfx_get_draw_page() & 1; + uint8_t q0 = cd_quiet(POP_CH_KID, p); + uint8_t q1 = cd_quiet(POP_CH_OPP, p); + if (q0 && q1) return 3; /* оба стоят — пересечься нечему */ + if (q0 || q1) { + /* Один перерисовывается: его heal (и fore-проход по его окну) может + * задеть «тихого» соседа и выесть из него кусок. Прямоугольники + * рядом — перерисовываем ОБОИХ. */ + uint8_t st = (uint8_t)(q0 ? POP_CH_KID : POP_CH_OPP); + uint8_t mv = (uint8_t)(st ^ 1); + if (pop_cd[mv].valid[p]) { + int ax0, ay0, ax1, ay1, bx0, by0, bx1, by1; + cd_bbox(st, p, &ax0, &ay0, &ax1, &ay1); + cd_bbox(mv, p, &bx0, &by0, &bx1, &by1); + if (ax0 < bx1 + CD_MARGIN && bx0 < ax1 + CD_MARGIN && + ay0 < by1 + CD_MARGIN && by0 < ay1 + CD_MARGIN) return 0; + } + } + return (uint8_t)((q0 ? 1 : 0) | (q1 ? 2 : 0)); +} + void pop_char_set_render_dx(uint8_t who, int dx) __banked { pop_cd[who].render_dx = dx; @@ -243,6 +342,12 @@ void pop_char_draw(uint8_t who) __banked uint16_t w, h, vis_w; int obj_x, obj_y, fp_x, fwd, top, bx, skip, rows, ct, cr; + /* Страховка пропуска (DRAW-COST): главный цикл мог решить, что слот + * «тихий», и heal не звать — а тик после этого персонажа всё-таки + * сдвинул. Здесь прошлый кадр уже точно надо стереть. Если heal в этом + * кадре был, вызов бесплатен: valid/ovalid уже сняты. */ + cd_heal(who); + s->cw = 0; /* окно fore-клипа: пусто, пока не нарисовали */ s->fpw = 0; /* габарит кадра: нечего перекрывать */ @@ -360,6 +465,10 @@ void pop_char_draw(uint8_t who) __banked } } gfx_w0_unmap(); + /* Что именно нарисовано на ЭТОЙ странице — снимок для пропуска + * следующих кадров (DRAW-COST). Только на полном пути: ранние выходы + * оставляют valid[dp] снятым, и cd_quiet отсеет слот и без снимка. */ + cd_sig_make(who, &cd_sig[who][dp]); } void pop_char_fore(uint8_t who) __banked diff --git a/applications/PoP/roomtest/pop_cdraw.h b/applications/PoP/roomtest/pop_cdraw.h index d433c6d..166cd3c 100644 --- a/applications/PoP/roomtest/pop_cdraw.h +++ b/applications/PoP/roomtest/pop_cdraw.h @@ -90,4 +90,45 @@ void pop_char_fore(uint8_t who) __banked; /* Straddle-смещение спрайта (см. render_dx). */ void pop_char_set_render_dx(uint8_t who, int dx) __banked; +/* ---- Пропуск НЕИЗМЕНИВШЕГОСЯ кадра (DRAW-COST) ---------------------- * + * heal + спрайт + fore-проход стоят ~50 % кадрового периода НА ПЕРСОНАЖА + * (замер пользователя 2026-08-08: комната 1.3 с телом убитого стража — 210 % + * кадра, соседняя 1.2 без тела — 140 %). Платили мы это и за то, что не + * менялось: труп лежит неподвижно до выхода из комнаты, стоящий Кид — пока + * игрок не нажал клавишу, а перерисовывались они каждый кадр. + * + * Оптимизация: у каждой страницы дабл-буфера свой снимок ВХОДОВ отрисовки + * слота (поза, координаты, направление, меч, брызги, straddle). Если снимок + * совпал, спрайт этой страницы не стёрт и фона под ним никто не трогал — + * нужный кадр на странице УЖЕ нарисован, и heal/draw/fore пропускаются + * целиком. + * + * `pop_cd_touch` — «фон трогали ВОТ ЗДЕСЬ»: слой фона копит ПРЯМОУГОЛЬНИК + * своих правок по страницам дабл-буфера, и слот перерисовывается, только + * если тот задел его самого. Флага «трогали хоть что-то» не хватило: + * факелы анимируются КАЖДЫЙ кадр, и один факел в комнате обнулял пропуск + * для всех персонажей сразу (замер 2026-08-08: комната 1.1, Кид стоит — + * 597 684 такта, то есть ровно как без оптимизации). + * + * Метку ставит сам блит фона (`pop_blit_b`, по признаку «идёт не + * fore-проход») плюс те немногие места, что стирают фон голым `bar` или + * своим heal (падающий кусок, оверлей fore-heal). Координаты — ЭКРАННЫЕ + * (с POP_YOFF). Прямоугольник копится сразу для ОБЕИХ страниц: правку фона + * почти всегда повторяют на второй, а лишняя перерисовка персонажа дешевле + * лишнего состояния. Снимает метку главный цикл — после того, как + * персонажи на этой странице отрисованы. + * + * Резидент (pop_tile.c): зовёт и банковый слой фона, и резидентные листья, + * а трамплин на КАЖДЫЙ кусок фона стоил бы дороже самой метки. */ +extern uint8_t pop_cd_dirty; /* биты страниц: прямоугольник взведён */ +extern int pop_cd_dx0[2], pop_cd_dy0[2], pop_cd_dx1[2], pop_cd_dy1[2]; +void pop_cd_touch(int x, int y, int w, int h); +#define POP_CD_TOUCH_ALL() pop_cd_touch(0, 0, 320, 256) + +/* Кого в этом кадре можно НЕ перерисовывать: бит 0 — Кид, бит 1 — соперник. + * Звать ДВАЖДЫ: перед heal (стирать нечего) и перед отрисовкой (тик мог + * персонажа всё-таки сдвинуть — тогда бит снимется сам, а pop_char_draw + * сотрёт прошлый кадр за нас). */ +uint8_t pop_char_skip_mask(void) __banked; + #endif diff --git a/applications/PoP/roomtest/pop_room.c b/applications/PoP/roomtest/pop_room.c index b9085e6..58f4fbc 100644 --- a/applications/PoP/roomtest/pop_room.c +++ b/applications/PoP/roomtest/pop_room.c @@ -335,6 +335,7 @@ void pop_spike_redraw(int row, int col) __banked * оставались бы; чёрная плита стирает старые бары в VRAM+ОЗУ-копию. */ void pop_room_redraw_seam_left(void) __banked { + pop_cd_touch(0, POP_YOFF + 3, 32, 62); /* bar мимо pop_blit_b (DRAW-COST) */ gfx_set_bank(GFX_BANK_NORMAL); setfillstyle(SOLID_FILL, BLACK); /* Полосу кладки У ПОТОЛКА (ряд −1) НЕ трогаем: её низ на room-space y=2, @@ -369,6 +370,7 @@ void pop_ceil_shake_draw(int col) __banked void pop_ceil_bake_empty(int col) __banked { int x = POP_COL_XH[col] * 8; + pop_cd_touch(x, POP_YOFF, 64, CEIL_BAND_H + 1); /* bar мимо pop_blit_b */ gfx_set_bank(GFX_BANK_NORMAL); setfillstyle(SOLID_FILL, BLACK); bar(x, POP_YOFF, x + 63, POP_YOFF + CEIL_BAND_H); /* стереть плиту в ОЗУ-копии */ @@ -441,6 +443,7 @@ void pop_potion_draw(int row, int col, uint8_t modif) __banked void pop_floor_bake(int row, int col) __banked { int x = POP_COL_XH[col] * 8; + pop_cd_touch(x, POP_YOFF + 63 * row + 26, 60, 39); /* bar мимо pop_blit_b */ int yb = POP_YOFF + 63 * row; /* Wipe СВОЕЙ ячейки + правой грани (заходит в соседа) + перерисовка пола. * НИЗ строго yb+64 (≈ низ передней грани пола): ниже — СТЕНА ряда ниже @@ -490,6 +493,7 @@ void pop_gate_redraw(int row, int col) __banked if (col + 1 > 9 || row < 0 || row > 2) return; x = POP_COL_XH[col + 1] * 8; yt = POP_YOFF + 63 * row; + pop_cd_touch(x, yt + 3, 32, 62); /* bar мимо pop_blit_b (DRAW-COST) */ gfx_set_bank(GFX_BANK_NORMAL); setfillstyle(SOLID_FILL, BLACK); bar(x, yt + 3, x + 31, yt + 64); @@ -521,6 +525,8 @@ void pop_loose_bake_empty(int row, int col) __banked * снова, и на второй странице дабл-буфера его не было → мерцание. * Тот же урок уже учтён в pop_floor_bake. */ yb += POP_YOFF; + /* bar идёт мимо pop_blit_b — пометить область самим (DRAW-COST). */ + pop_cd_touch(x, yb, 72, POP_YOFF + 63 * row + 66 - yb); gfx_set_bank(GFX_BANK_NORMAL); setfillstyle(SOLID_FILL, BLACK); bar(x, yb, x + 39, POP_YOFF + 63 * row + 65); /* сам тайл: +40 (кусок пола заходит вправо; сосед перерисуется) */ @@ -716,8 +722,15 @@ static void mob_tick_one(mob_t *m, uint8_t pg) void pop_loose_mob_tick(void) __banked { uint8_t pg = gfx_get_draw_page() & 1, i; - for (i = 0; i < MOB_MAX; i++) + for (i = 0; i < MOB_MAX; i++) { + /* Летящий кусок и его дочистка ходят по чужим пикселям СВОИМ heal'ом, + * мимо pop_blit_b — пока он в кадре, персонажей пропускать нельзя + * (DRAW-COST). Взводим ДО тика: кусок может исчезнуть прямо в нём, а + * стереть себя ему всё равно надо будет. */ + if (mobs[i].active || mobs[i].clean) + pop_cd_touch(MOB_X0(mobs[i].x), mobs[i].y - 27 + POP_YOFF, MOB_W, 64); mob_tick_one(&mobs[i], pg); + } } static void mob_render(mob_t *m, uint8_t pg) diff --git a/applications/PoP/roomtest/pop_tile.c b/applications/PoP/roomtest/pop_tile.c index ef3394c..da87c89 100644 --- a/applications/PoP/roomtest/pop_tile.c +++ b/applications/PoP/roomtest/pop_tile.c @@ -11,6 +11,7 @@ #include #include #include "pop_bg.h" /* POP_YOFF */ +#include "pop_cdraw.h" /* POP_CD_TOUCH — фон тронут, персонажей перерисовать */ #include "pop_tile.h" #include "pop_level.h" /* pop_doorlink2 — подстановка нажатой кнопки */ #include "_pop_draw.h" /* pop_heal_fast */ @@ -88,6 +89,27 @@ uint8_t pop_loose_frame(uint8_t m) return m; } +/* ---- «Фон трогали ВОТ ЗДЕСЬ» (DRAW-COST; контракт — pop_cdraw.h) ----- */ +uint8_t pop_cd_dirty; +int pop_cd_dx0[2], pop_cd_dy0[2], pop_cd_dx1[2], pop_cd_dy1[2]; + +void pop_cd_touch(int x, int y, int w, int h) +{ + uint8_t p; + for (p = 0; p < 2; p++) { + if (!(pop_cd_dirty & (1 << p))) { + pop_cd_dx0[p] = x; pop_cd_dy0[p] = y; + pop_cd_dx1[p] = x + w; pop_cd_dy1[p] = y + h; + } else { + if (x < pop_cd_dx0[p]) pop_cd_dx0[p] = x; + if (y < pop_cd_dy0[p]) pop_cd_dy0[p] = y; + if (x + w > pop_cd_dx1[p]) pop_cd_dx1[p] = x + w; + if (y + h > pop_cd_dy1[p]) pop_cd_dy1[p] = y + h; + } + } + pop_cd_dirty = 3; +} + /* ---- Блит куска атласа ---------------------------------------------- */ void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) { @@ -137,6 +159,11 @@ void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) } else { gfx_blit_part(dx, dy, img, sx, sy, dw, dh); /* край экрана — с клипом */ } + /* Кусок ФОНА лёг туда, где может лежать уже нарисованный персонаж — + * пометить область, чтобы его перерисовали (DRAW-COST, pop_cdraw.h). + * Признак «это не fore-проход поверх персонажа» — окно клипа: его + * ставит только он. */ + if (!pop_t_fclip_on) pop_cd_touch(dx, dy, dw, dh); } gfx_w0_unmap(); } diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index dbb95d6..4939e7b 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -464,6 +464,7 @@ int main(void) * по умолчанию (без мерцания); SPACE переключает в однобуфер для * отладки багов рисования (edge по sp_prev, чтоб не дёргать). */ uint8_t front = 0, back, dbuf = 1, sp_prev = 0, frozen = 0; + uint8_t skip = 0; /* маска «слот перерисовывать не надо» */ uint16_t dead_frames = 0; #ifdef ROOMNAV uint8_t nav_prev = 0; @@ -645,8 +646,13 @@ int main(void) pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */ pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */ pop_ctrl_tick(); /* ввод -> control(): смена seq */ - pop_char_heal(POP_CH_KID); /* стереть прошлый кадр ЭТОЙ страницы */ - pop_char_heal(POP_CH_OPP); /* и прошлый кадр соперника */ + /* Кто в этом кадре не изменился с прошлой отрисовки ЭТОЙ страницы — + * тот на ней уже нарисован правильно: ни стирать, ни рисовать заново + * не надо (DRAW-COST, контракт в pop_cdraw.h). Решение + * перепроверяется перед самой отрисовкой — тик ещё впереди. */ + skip = pop_char_skip_mask(); + if (!(skip & 1)) pop_char_heal(POP_CH_KID); /* стереть прошлый кадр ЭТОЙ страницы */ + if (!(skip & 2)) pop_char_heal(POP_CH_OPP); /* и прошлый кадр соперника */ pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */ PROF(2); /* логика (тот же синий) */ kid_tick(); /* play_seq: следующий кадр */ @@ -819,10 +825,14 @@ int main(void) * персонажа (см. guard_over_kid). Отрисовка одна на всех Char * (pop_cdraw.c), различает их только слот. */ uint8_t g_after = guard_over_kid(); - if (!g_after) { pop_char_draw(POP_CH_OPP); pop_char_fore(POP_CH_OPP); } + /* Пересчёт после тика: слот, за который heal не платили, мог + * всё-таки сдвинуться — тогда бит снимется, а прошлый кадр + * сотрёт сам pop_char_draw (страховка cd_heal). */ + skip = pop_char_skip_mask(); + if (!g_after && !(skip & 2)) { pop_char_draw(POP_CH_OPP); pop_char_fore(POP_CH_OPP); } PROF(6); /* кадр Кида (тот же циан) */ - pop_char_draw(POP_CH_KID); /* спрайт + брызги + клинок */ - if (g_after) { pop_char_draw(POP_CH_OPP); pop_char_fore(POP_CH_OPP); } + if (!(skip & 1)) pop_char_draw(POP_CH_KID); /* спрайт + брызги + клинок */ + if (g_after && !(skip & 2)) { pop_char_draw(POP_CH_OPP); pop_char_fore(POP_CH_OPP); } /* Брызги урона рисует сама отрисовка персонажа (порядок objtable: * сразу после него и до клинка), а флаги снимаем здесь: соперника * могло не оказаться в отрисованной комнате — тогда брызги просто @@ -840,7 +850,12 @@ int main(void) * pop_char_fore(KID) заодно возвращает окно Кида (при «соперник * поверх» в нём остался бы соперник, и передние грани у Кида * отсеклись бы целиком — он рисовался бы поверх столбов). */ - pop_char_fore(POP_CH_KID); + if (!(skip & 1)) pop_char_fore(POP_CH_KID); + /* Персонажи на этой странице приведены в порядок — след «фон трогали» + * для неё снят. ПОСЛЕ отрисовки, а не до: слой фона этого же кадра + * взводит бит и обязан быть учтён. Всё, что рисуется ниже + * (mob поверх Кида, борта), взводит его заново само. */ + pop_cd_dirty = (uint8_t)(pop_cd_dirty & ~(1 << back)); pop_room_clip_borders(); /* почистить борта — только в кадрах падения (гейт) */ /* Пейсинг 3 кадра, затем tear-free флип на vblank: draw уже