DRAW-COST шаг 1: пропуск неизменившегося персонажа — 210% -> 116% кадра

Персонаж, у которого с прошлой отрисовки ЭТОЙ страницы дабл-буфера не
изменился ни один вход отрисовки, а фон в его прямоугольнике не трогали,
уже нарисован правильно: 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.
This commit is contained in:
2026-08-08 18:39:00 +03:00
parent fd54bc78c0
commit a25ce58869
8 changed files with 283 additions and 60 deletions
+2 -2
View File
@@ -60,12 +60,12 @@ Kid — `kid_data.h` (генерится `pop_extract_kid_data.py`).
| Файл | Роль (порт SDLPoP) | | Файл | Роль (порт SDLPoP) |
|------|--------------------| |------|--------------------|
| `roomtest.c` | Главный цикл: дабл-буфер (2 страницы + флип на vsync), tick→draw порядок слоёв. | | `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_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_room.c` | ХОЛОДНАЯ половина слоя фона (банк 7, раз на комнату/событие): полная отрисовка комнаты (`seg008` draw_tile), точечные перерисовки тайлов (пики, ворота, кнопки, дверь уровня, дрожащие плиты), падающие куски loose (`seg007` mob), загрузка атласов. |
| `_pop_bg.h` | Внутренний контракт между половинами: общее состояние + три тонкие `__banked`-обёртки. Там же расписано, почему рез прошёл именно здесь и сколько стоит. | | `_pop_bg.h` | Внутренний контракт между половинами: общее состояние + три тонкие `__banked`-обёртки. Там же расписано, почему рез прошёл именно здесь и сколько стоит. |
| `pop_kid.c/.h` | Анимация/движение: интерпретатор seqtbl `play_seq` + frame_table (`seg006`), окна `Char` (loadkid/loadshad), атласы Кида. | | `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`) поверх `<kbd_raw.h>`. | | `pop_ctrl.c/.h` | Управление: диспетчер `control()` (`seg005`) + ввод `read_user_control` (`seg006`) поверх `<kbd_raw.h>`. |
| `pop_map.c/.h` | Коллизия с картой + физика падения/приземления/стены + loose-полы (`seg005/006/007`). | | `pop_map.c/.h` | Коллизия с картой + физика падения/приземления/стены + loose-полы (`seg005/006/007`). |
+64 -49
View File
@@ -55,7 +55,7 @@
| 2 | [L3-CHOMP](#l3-chomp) | **СЛЕДУЮЩАЯ**: чомперы (5 шт) | прохождение ур. 3 | | 2 | [L3-CHOMP](#l3-chomp) | **СЛЕДУЮЩАЯ**: чомперы (5 шт) | прохождение ур. 3 |
| — | [L3-SKEL](#l3-skel) | скелет ур. 3 — **сделан 2026-08-07**, ждёт финальной приёмки | — | | — | [L3-SKEL](#l3-skel) | скелет ур. 3 — **сделан 2026-08-07**, ждёт финальной приёмки | — |
| 3 | [L3-PASS](#l3-pass) | приёмка уровня 3 (обход комнат) | закрытие цели | | 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 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент | | — | [L1-SPEED](#l1-speed) | игра на ~39 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент |
| — | [TUNE-1](#tune-1) | параметры движка → cfg-файл (сейчас `pop_tune.h`) | отладка таймингов и моды; берётся по мере надобности | | — | [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)).
### <a id="draw-cost"></a>DRAW-COST. Кадр НЕ УКЛАДЫВАЕТСЯ в бюджет — нужна оптимизация ### <a id="draw-cost"></a>DRAW-COST. Кадр НЕ УКЛАДЫВАЕТСЯ в бюджет — нужна оптимизация
> **ЗАМЕР ПОЛЬЗОВАТЕЛЯ 2026-08-08 (после MEM-BANK2).** Уровень 1, комната 3, > **ШАГ 1 СДЕЛАН 2026-08-08: пропуск неизменившегося персонажа.** Комната
> страж УБИТ, Кид СТОИТ (то есть это НЕ худший случай — ни боя, ни движения): > 1.3, труп стража, Кид стоит: было **210 %** кадрового периода, стало
> > **116 %** (500 772 такта при бюджете 430 000). Отрисовка перестала быть
> | полоса | фаза | доля кадрового периода | > узким местом: персонажи в покое не рисуются ВООБЩЕ (ноль вызовов
> |---|---|---| > `pop_heal_fast` за кадр), весь фон — ДВА блита факелов (44 136 тактов).
> | синяя (`PROF(2)`) | ввод + heal + логика | ~80 % | > Механизм и почему метка позиционная — в шапке `pop_cdraw.h`.
> | зелёная (`PROF(4)`) | слой фона (loose/ловушки) | ~20 % |
> | циан (`PROF(6)`) | спрайты + fore-проход | **~110 %** |
>
> **Итого ~210 % — больше ДВУХ кадровых периодов на кадр.** Это много; в
> покое должно оставаться место под бой, движение и вторую фигуру.
> Приоритет задачи поднят: это уже не косметика профиля.
>
> **Контрольный замер там же: комната 2** — та же по устройству, но БЕЗ тела
> стража: 60 % / 20 % / 60 % = **~140 %**.
### Первый след: ТЕЛО УБИТОГО СТРАЖА стоит ~70 % кадра **Исходные замеры пользователя (полосы бордюра, до шага 1).** Уровень 1,
Кид СТОИТ — то есть НЕ худший случай, ни боя, ни движения:
Комнаты 2 и 3 отличаются ровно одним — трупом, и он даёт +20 % синей и | комната | синяя (ввод+heal+логика) | зелёная (фон) | циан (спрайты) | итого |
+50 % циана. Причина видна прямо в коде и НЕ требует замера: ни |---|---|---|---|---|
`roomtest.c`, ни `pop_cdraw.c` не смотрят на `Char.alive`. Мёртвый страж | 3, страж УБИТ | ~80 % | ~20 % | ~110 % | **~210 %** |
сохраняет `charid != 0`, поэтому КАЖДЫЙ кадр честно проходит весь путь | 2, стража НЕТ | ~60 % | ~20 % | ~60 % | ~140 % |
живого персонажа — `pop_char_heal(OPP)` (стереть), `pop_char_draw(OPP)`
(блит + clip_char + брызги + клинок), `pop_char_fore(OPP)` (перебор тайлов
футпринта с разбором кладки). При этом труп НЕ МЕНЯЕТСЯ: его кадр
постоянен до выхода из комнаты.
**Что напрашивается** (порядок — от дешёвого к дорогому, каждое МЕРИТЬ): Разница ровно в теле убитого стража: +20 % синей и +50 % циана. Труп
1. запечь труп в фон один раз, как делают loose-плиты (`pop_floor_bake`) — сохраняет `charid != 0`, поэтому каждый кадр честно проходил весь путь
тогда heal сам его восстанавливает, а per-frame путь исчезает целиком. живого персонажа (heal → спрайт+clip_char+брызги+клинок → fore-проход), хотя
Сверить с SDLPoP: там мёртвый страж остаётся обычной записью objtable, но его кадр постоянен до выхода из комнаты.
у нас нет их системы «перерисовать только грязные тайлы», и запечка — наш
штатный ответ на «статичный элемент поверх фона» (см. `pop_loose_floors`);
2. если запечка не годится (труп надо стирать при выходе/рестарте) —
минимум пропустить `pop_char_fore(OPP)` для мёртвого: передние грани
вокруг неподвижного тела тоже не меняются.
Проверять на той же паре комнат 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, страж убит, Кид стоит) — она воспроизводима Мерить в ЭТОЙ точке (комната 3, страж убит, Кид стоит) — она воспроизводима
и даёт нижнюю границу; худший случай (бой + бег + падающая плита) считать и даёт нижнюю границу; худший случай (бой + бег + падающая плита) считать
отдельно. Метод — memory `z80_profiling_method` (брейкпоинты с отдельно. Метод — memory `z80_profiling_method` (брейкпоинты с
`totalcycles`), полосы бордюра дают только раскладку по фазам. `totalcycles`); полосы бордюра показывают только одну картинку из периода и
годятся лишь для раскладки по фазам.
Обратить внимание: **синие 80 % при стоящем Киде** — это подозрительно
много для «ввод + heal + логика», heal там наверняка чистит больше, чем
нужно. Разобрать синюю фазу отдельно от циана.
**Наблюдение пользователя на приёмке DRAW-CHAR 2026-08-08:** циан-полоса **Наблюдение пользователя на приёмке DRAW-CHAR 2026-08-08:** циан-полоса
профиля (спрайты + fore) выросла — тогда «в пределах». профиля (спрайты + fore) выросла — тогда «в пределах».
@@ -377,15 +397,10 @@ W1/W2 (см. [BUG-GUARD-COLOR-1](bug_closed.md#bug-guard-color-1)).
работы», а недостающая ранее корректность: тайл, который спрайт задевает, работы», а недостающая ранее корректность: тайл, который спрайт задевает,
обязан вернуть свой передний слой поверх него. обязан вернуть свой передний слой поверх него.
**Куда копать, когда возьмёмся** (сначала МЕРИТЬ, потом резать — **Если fore-проход снова станет узким местом** (сейчас он в покое не
[`pop_fore_layer_cost`](../../../docs) уже показывал 78 % кадра до кэша выполняется вовсе): расширять перебор ТОЛЬКО по накладным спрайтам
кладки): (клинок/брызги), а не по всему окну; кэш «в этом тайле fore-слоя нет
1. расширять перебор ТОЛЬКО по накладным спрайтам (клинок/брызги), а не по вовсе»; считать окно клипа в тайловых координатах один раз.
всему окну: габарит самого кадра уже покрыт футпринтом + правилом меча;
2. кэш «в этом тайле fore-слоя нет вовсе» — половина колонок пустые, а за
каждую платим `tile_in_fclip` + разбор кладки;
3. считать окно клипа в тайловых координатах один раз, а не сравнивать
пиксельные габариты в двух циклах.
### <a id="draw-char"></a>DRAW-CHAR — **ЗАКРЫТА И ПРОВЕРЕНА 2026-08-08** ### <a id="draw-char"></a>DRAW-CHAR — **ЗАКРЫТА И ПРОВЕРЕНА 2026-08-08**
+3
View File
@@ -52,6 +52,9 @@ void pop_fore_heal(void) __banked
if (!(ov_pending & (1 << pg))) return; if (!(ov_pending & (1 << pg))) return;
pop_heal_off(ov_x0, ov_y0, ov_x1 - ov_x0, ov_y1 - ov_y0); pop_heal_off(ov_x0, ov_y0, ov_x1 - ov_x0, ov_y1 - ov_y0);
ov_pending = (uint8_t)(ov_pending & ~(1 << pg)); 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);
} }
+111 -2
View File
@@ -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, а не /* Стирание прошлого кадра персонажа и его накладных. pop_heal_fast, а не
* голый gfx_heal: стираем ровно то, что рисовали, а рисовали в подавляющем * голый gfx_heal: стираем ровно то, что рисовали, а рисовали в подавляющем
* большинстве кадров noclip-ядром; клипающий путь остаётся для straddle и * большинстве кадров 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]; pop_cdraw_t *s = &pop_cd[who];
uint8_t p = gfx_get_draw_page() & 1; 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; 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 void pop_char_set_render_dx(uint8_t who, int dx) __banked
{ {
pop_cd[who].render_dx = dx; pop_cd[who].render_dx = dx;
@@ -243,6 +342,12 @@ void pop_char_draw(uint8_t who) __banked
uint16_t w, h, vis_w; uint16_t w, h, vis_w;
int obj_x, obj_y, fp_x, fwd, top, bx, skip, rows, ct, cr; 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->cw = 0; /* окно fore-клипа: пусто, пока не нарисовали */
s->fpw = 0; /* габарит кадра: нечего перекрывать */ s->fpw = 0; /* габарит кадра: нечего перекрывать */
@@ -360,6 +465,10 @@ void pop_char_draw(uint8_t who) __banked
} }
} }
gfx_w0_unmap(); 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 void pop_char_fore(uint8_t who) __banked
+41
View File
@@ -90,4 +90,45 @@ void pop_char_fore(uint8_t who) __banked;
/* Straddle-смещение спрайта (см. render_dx). */ /* Straddle-смещение спрайта (см. render_dx). */
void pop_char_set_render_dx(uint8_t who, int dx) __banked; 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 #endif
+14 -1
View File
@@ -335,6 +335,7 @@ void pop_spike_redraw(int row, int col) __banked
* оставались бы; чёрная плита стирает старые бары в VRAM+ОЗУ-копию. */ * оставались бы; чёрная плита стирает старые бары в VRAM+ОЗУ-копию. */
void pop_room_redraw_seam_left(void) __banked 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); gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK); setfillstyle(SOLID_FILL, BLACK);
/* Полосу кладки У ПОТОЛКА (ряд −1) НЕ трогаем: её низ на room-space y=2, /* Полосу кладки У ПОТОЛКА (ряд −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 void pop_ceil_bake_empty(int col) __banked
{ {
int x = POP_COL_XH[col] * 8; 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); gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK); setfillstyle(SOLID_FILL, BLACK);
bar(x, POP_YOFF, x + 63, POP_YOFF + CEIL_BAND_H); /* стереть плиту в ОЗУ-копии */ 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 void pop_floor_bake(int row, int col) __banked
{ {
int x = POP_COL_XH[col] * 8; 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; int yb = POP_YOFF + 63 * row;
/* Wipe СВОЕЙ ячейки + правой грани (заходит в соседа) + перерисовка пола. /* Wipe СВОЕЙ ячейки + правой грани (заходит в соседа) + перерисовка пола.
* НИЗ строго yb+64 (≈ низ передней грани пола): ниже — СТЕНА ряда ниже * НИЗ строго yb+64 (≈ низ передней грани пола): ниже — СТЕНА ряда ниже
@@ -490,6 +493,7 @@ void pop_gate_redraw(int row, int col) __banked
if (col + 1 > 9 || row < 0 || row > 2) return; if (col + 1 > 9 || row < 0 || row > 2) return;
x = POP_COL_XH[col + 1] * 8; x = POP_COL_XH[col + 1] * 8;
yt = POP_YOFF + 63 * row; 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); gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK); setfillstyle(SOLID_FILL, BLACK);
bar(x, yt + 3, x + 31, yt + 64); 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. */ * Тот же урок уже учтён в pop_floor_bake. */
yb += POP_YOFF; 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); gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK); setfillstyle(SOLID_FILL, BLACK);
bar(x, yb, x + 39, POP_YOFF + 63 * row + 65); /* сам тайл: +40 (кусок пола заходит вправо; сосед перерисуется) */ 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 void pop_loose_mob_tick(void) __banked
{ {
uint8_t pg = gfx_get_draw_page() & 1, i; 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); mob_tick_one(&mobs[i], pg);
}
} }
static void mob_render(mob_t *m, uint8_t pg) static void mob_render(mob_t *m, uint8_t pg)
+27
View File
@@ -11,6 +11,7 @@
#include <gfx.h> #include <gfx.h>
#include <sprite.h> #include <sprite.h>
#include "pop_bg.h" /* POP_YOFF */ #include "pop_bg.h" /* POP_YOFF */
#include "pop_cdraw.h" /* POP_CD_TOUCH — фон тронут, персонажей перерисовать */
#include "pop_tile.h" #include "pop_tile.h"
#include "pop_level.h" /* pop_doorlink2 — подстановка нажатой кнопки */ #include "pop_level.h" /* pop_doorlink2 — подстановка нажатой кнопки */
#include "_pop_draw.h" /* pop_heal_fast */ #include "_pop_draw.h" /* pop_heal_fast */
@@ -88,6 +89,27 @@ uint8_t pop_loose_frame(uint8_t m)
return 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) 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 { } else {
gfx_blit_part(dx, dy, img, sx, sy, dw, dh); /* край экрана — с клипом */ 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(); gfx_w0_unmap();
} }
+21 -6
View File
@@ -464,6 +464,7 @@ int main(void)
* по умолчанию (без мерцания); SPACE переключает в однобуфер для * по умолчанию (без мерцания); SPACE переключает в однобуфер для
* отладки багов рисования (edge по sp_prev, чтоб не дёргать). */ * отладки багов рисования (edge по sp_prev, чтоб не дёргать). */
uint8_t front = 0, back, dbuf = 1, sp_prev = 0, frozen = 0; uint8_t front = 0, back, dbuf = 1, sp_prev = 0, frozen = 0;
uint8_t skip = 0; /* маска «слот перерисовывать не надо» */
uint16_t dead_frames = 0; uint16_t dead_frames = 0;
#ifdef ROOMNAV #ifdef ROOMNAV
uint8_t nav_prev = 0; uint8_t nav_prev = 0;
@@ -645,8 +646,13 @@ int main(void)
pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */ pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */
pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */ pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
pop_ctrl_tick(); /* ввод -> control(): смена seq */ 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) */ pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */
PROF(2); /* логика (тот же синий) */ PROF(2); /* логика (тот же синий) */
kid_tick(); /* play_seq: следующий кадр */ kid_tick(); /* play_seq: следующий кадр */
@@ -819,10 +825,14 @@ int main(void)
* персонажа (см. guard_over_kid). Отрисовка одна на всех Char * персонажа (см. guard_over_kid). Отрисовка одна на всех Char
* (pop_cdraw.c), различает их только слот. */ * (pop_cdraw.c), различает их только слот. */
uint8_t g_after = guard_over_kid(); 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); /* кадр Кида (тот же циан) */ PROF(6); /* кадр Кида (тот же циан) */
pop_char_draw(POP_CH_KID); /* спрайт + брызги + клинок */ if (!(skip & 1)) pop_char_draw(POP_CH_KID); /* спрайт + брызги + клинок */
if (g_after) { pop_char_draw(POP_CH_OPP); pop_char_fore(POP_CH_OPP); } if (g_after && !(skip & 2)) { pop_char_draw(POP_CH_OPP); pop_char_fore(POP_CH_OPP); }
/* Брызги урона рисует сама отрисовка персонажа (порядок objtable: /* Брызги урона рисует сама отрисовка персонажа (порядок objtable:
* сразу после него и до клинка), а флаги снимаем здесь: соперника * сразу после него и до клинка), а флаги снимаем здесь: соперника
* могло не оказаться в отрисованной комнате — тогда брызги просто * могло не оказаться в отрисованной комнате — тогда брызги просто
@@ -840,7 +850,12 @@ int main(void)
* pop_char_fore(KID) заодно возвращает окно Кида (при «соперник * 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(); /* почистить борта — только в кадрах падения (гейт) */ pop_room_clip_borders(); /* почистить борта — только в кадрах падения (гейт) */
/* Пейсинг 3 кадра, затем tear-free флип на vblank: draw уже /* Пейсинг 3 кадра, затем tear-free флип на vblank: draw уже