From 61b8d80275cdc1de29c704cfaeaab8c16056b167 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Wed, 19 Aug 2026 21:55:05 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D0=B9=D1=81=D0=B8=D0=BD=D0=B3:=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D1=80=D0=BE=D0=B1=D0=BD=D1=8B=D0=B9=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B7=D0=B1=D0=BE=D1=80=20=D1=81=D1=87=D1=91=D1=82=D1=87?= =?UTF-8?q?=D0=B8=D0=BA=D0=B0=20=D0=BA=D0=B0=D0=B4=D1=80=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D0=BB=D1=83=D1=87=D1=83=20(=D1=83=D1=81=D0=BB?= =?UTF-8?q?=D0=BE=D0=B2=D0=B8=D0=B5=20=D1=82=D0=BE=D1=87=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8,=20=D1=82=D0=BE=D1=87=D0=BA=D0=B8=20=D0=B2=D1=8B?= =?UTF-8?q?=D0=B1=D0=BE=D1=80=D0=BA=D0=B8,=20=D0=BF=D1=80=D0=B8=D1=91?= =?UTF-8?q?=D0=BC=D0=BA=D0=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- applications/PoP/docs/frame_pacing_plan.md | 220 +++++++++++++++++++++ 1 file changed, 220 insertions(+) diff --git a/applications/PoP/docs/frame_pacing_plan.md b/applications/PoP/docs/frame_pacing_plan.md index a860d23..249cefc 100644 --- a/applications/PoP/docs/frame_pacing_plan.md +++ b/applications/PoP/docs/frame_pacing_plan.md @@ -285,3 +285,223 @@ NORMAL секундомером совпадает с живым SDLPoP на о 2. Ветка `frozen` (`roomtest.c:320`) — какой темп ей нужен под делителем. 3. Проверить, что при n=4/5 бит 5 всё ещё единственный источник фронта (то есть `_cbl_port_ref` держится всё это время). + + +--- + +# 9. Предлагаемый вариант подробно: программный счётчик кадров по лучу + +Дополнение от 2026-08-19 по запросу: как именно получается **точное** +число пройденных кадровых интервалов. Акселератор рассматриваем только +в нынешнем виде — с DI/EI (режим «акселератор с EI» из новой прошивки +из рассмотрения снят). + +## 9.1. Сигнал и его геометрия + +Единственный источник — **бит 5 порта `0xFE`**. Точная семантика по +исходнику MAME (`sprinter.cpp`, `kbd_fe_r`): + +```c +data |= 0xe0; +data ^= 0x40; +if (cbl_mode()) { + data &= ~0xa0; /* гасит биты 5 и 7 */ + data |= (vpos >= BORDER_TOP + SCREEN_YSIZE) << 5; + data |= ... & 0x80; /* бит 7 — CBL */ +} +``` + +То есть **бит 5 = 1 ровно тогда, когда луч ниже картинки** +(`vpos >= 16 + 256 = 272`), и это ЧТЕНИЕ ПОЛОЖЕНИЯ ЛУЧА, а не событие: +ни прерывания, ни защёлки, ни очереди — его невозможно «потерять», +можно только не посмотреть. + +Важное следствие из той же строки: вне `cbl_mode` бит читается как 1 +всегда (его выставляет `data |= 0xe0` и уже ничто не гасит). Поэтому +счётчик обязан работать только при взведённом `_cbl_port_ref()` — том +самом, который сейчас лениво взводит `gfx_wait_vsync`. + +Геометрия кадра (320 строк × 896 пикселей при 14 МГц): + +| | строк | мс | тактов CPU (21 МГц) | +|---|---:|---:|---:| +| бит 5 = 1 (нижний бланк) | 48 | 3,07 | **64 512** | +| бит 5 = 0 (картинка + верхний бордер) | 272 | 17,41 | 365 568 | +| кадр целиком | 320 | 20,48 | 430 080 | + +Границей кадра берём **фронт 1→0** — это `vpos = 0`, ровно то же +событие, которого ждёт сегодняшний `gfx_wait_vsync`. Значит момент +свопа страниц не меняется: до начала картинки остаётся верхний бордер, +16 строк ≈ 1 мс запаса, как и сейчас. + +## 9.2. Счётчик + +```c +static uint8_t beam_prev; /* бит 5 на прошлой выборке */ +static uint8_t frame_tick; /* счётчик кадров, разностная арифметика */ + +/* ~20 T-состояний тела + вызов; в тактах MAME ≈ 120 на выборку */ +void pop_beam_sample(void) +{ + uint8_t b = in_fe() & 0x20; + if (beam_prev && !b) frame_tick++; /* фронт 1→0 = начало кадра */ + beam_prev = b; +} +``` + +Вся арифметика ожидания — разностная по модулю 256, wrap безопасен +(тот же приём, что в существующем `_gfx_fps_state`). + +## 9.3. Почему счёт ТОЧНЫЙ (условие и запас) + +Утверждение: **если между соседними выборками проходит меньше 64 512 +тактов, то каждый фронт 1→0 будет засчитан ровно один раз.** + +Доказательство прямое. Пусть максимальный зазор между выборками +Δ < 64 512. Окно «бит 5 = 1» длится 64 512 тактов, то есть длиннее Δ, +значит в него попадает хотя бы одна выборка → `beam_prev` обязательно +станет 1 внутри каждого бланка. Окно «бит 5 = 0» длится 365 568 — тем +более содержит выборку → сразу после бланка `beam_prev` перейдёт в 0 и +даст ровно один инкремент. Двойной счёт невозможен: инкремент +происходит только на переходе 1→0, а `beam_prev` тут же обновляется. + +Условие ОДНО и оно про зазор, а не про нагрузку, не про DI, не про +прерывания. Отсюда все свойства варианта. + +**Какой запас по факту.** Самый длинный неделимый кусок кода без +возможности выборки — одно DI-окно акселератора, то есть один вызов +`_bgi_blit_rows_raw`. Он по контракту режется вызывающим на чанки +**≤16 строк**; при ширине 32 это ≈ 6 200 тактов, при полной высоте +спрайта 63 строки самый дорогой замеренный блит целиком — 32 073. +Даже если мерить самым грубым образом (одна выборка на целый блит, +а не на чанк), зазор вдвое меньше окна бланка. + +## 9.4. Где ставить выборки + +Правило простое: **выборка обязана стоять так, чтобы ни один путь +исполнения не давал зазора длиннее 64 512 тактов.** По фазам: + +- **Зелёная и циан** (436 494 и 382 770 тактов на 13/23) состоят из + блитов и хилов. Достаточно одной выборки на вызов наших обёрток + `pop_blit_b`, `pop_heal_off`, `pop_heal_fast` — но НЕ только их: + прямые вызовы `gfx_blit_*` / `gfx_heal*` разбросаны по шести файлам + (`pop_cdraw.c`, `pop_draw.c`, `pop_kdraw.c`, `pop_room.c`, + `pop_state.c`, `pop_tile.c`). Точный набор точек определяем НЕ + рассуждением, а замером (см. 9.7): ставим в обёртки, меряем худший + зазор, добавляем точки только там, где замер их требует. +- **Синяя** (149 106 тактов) — блитов нет вообще, это 2,3 окна бланка. + Нужны явные точки: после ввода, после физики, после `pop_process_trobs`, + после mob-тика. Ставятся на границах, которые и так размечены + зондами `pop_dbg_m*`. + +Чего заведомо НЕ хватит: выборок только в главном цикле. Один +`pop_floor_bake` — 179 914 тактов, почти три окна бланка. + +## 9.5. Ожидание — тот же примитив + +Ожидание фронта и есть плотная выборка, поэтому оно сливается со +счётчиком, а опрос клавиатуры остаётся ровно таким же плотным, как +сегодня: + +```c +static void wait_edge(void) +{ + uint8_t t = frame_tick; + do { + pop_beam_sample(); + kbd_raw_poll(); /* то, что сейчас висит idle-хуком */ + } while (frame_tick == t); +} +``` + +`gfx_set_idle_hook` при этом больше не нужен — опрос зовётся прямо. +Обязателен аварийный выход по счётчику попыток (как в нынешнем +`gfx_wait_vsync`): на железе, где бит ведёт себя иначе, цикл не должен +виснуть насмерть. + +## 9.6. Пейсинг целиком + +```c +uint8_t k = (uint8_t)(frame_tick - anchor); /* фронтов съел рендер */ +if (k >= n) { + wait_edge(); /* опоздали — выравниваемся на ближайший */ +} else { + do { wait_edge(); } while ((uint8_t)(frame_tick - anchor) < n); +} +anchor = frame_tick; /* якорь по ФАКТУ, фаза не копится */ +flip_page(); +``` + +`anchor = frame_tick`, а не `anchor += n` — сознательно: догонять +пропущенное время нельзя, иначе после тяжёлого кадра игра рванёт +вперёд. Это же правило заложено в исходном дизайне делителя +(«выравнивание на ближайший фронт, без накопления фазовой ошибки»). + +Поведение по случаям: + +| работа W (растров) | период | комментарий | +|---|---|---| +| W ≤ n | ровно n | цель задачи | +| n < W ≤ n+1 | ceil(W) | подтормаживает ровно настолько, насколько не успели | +| вход в комнату, W ≫ n | ceil(W) + 1 | ветка «опоздали»: один фронт, без растягивания | +| загрузка уровня, файловые операции | ceil(W) + 1…n | выборок нет вовсе → k занижен; худшее — n лишних растров ОДИН раз | + +Последняя строка — единственный случай, где счёт неточен, и он +безобиден: во время `ESTEX`-вызова выбирать нечего, а ошибка живёт один +кадр, потому что якорь переставляется по факту. + +## 9.7. Как это доказывается, а не декларируется + +Инструмент уже построен и проверен на нынешнем коде (§4): брейкпоинт на +следующей инструкции пишет `temp3 = totalcycles`, второй с условием +`(totalcycles - temp3) > 0x9D800` останавливает машину. + +Для приёмки он ставится **на инструкцию инкремента `frame_tick`**. +Если хоть один фронт пропущен, зазор между инкрементами станет два +растра и детектор остановит машину. Критерий: **10 000 кадров без +единого срабатывания**, включая смену комнаты и смерть Кида. + +Дополнительно, в отладочной сборке — перекрёстная проверка со счётчиком +кадровых прерываний (слот цепочки, один INC): прерывания теряются, луч +не должен, значит `frame_tick` обязан идти НЕ МЕДЛЕННЕЕ `irq_tick`. +Расхождение в другую сторону = пропущенная выборка. + +Напоминание о методике: **счёт попаданий брейкпоинтом на этом драйвере +недостоверен** (WAIT-линия, инструкция пересчитывается) — только +детектор разрыва. И литералы в отладчике MAME шестнадцатеричные. + +## 9.8. Цена + +| статья | тактов | +|---|---:| +| одна выборка (с вызовом) | ≈ 120 | +| ~60 выборок за логический кадр | ≈ 7 200 | +| доля от бюджета при n=3 (1 290 240) | **0,6 %** | + +В горячих местах (`pop_blit_b`) выборку можно заинлайнить и снять цену +вызова. + +## 9.9. Чем это лучше счётчика прерываний + +| | счётчик кадровых IRQ | счётчик по лучу | +|---|---|---| +| теряет тик под `di` акселератора | да, фазозависимо 0…3 % | нет — читается положение луча | +| теряет тик, если IRQ съела клавиатурная/CBL-ветка трамплина | да (приватный RETI) | нет | +| ломается от добавления звука | да (ещё один источник, `irqack` гасит все входы мержера) | нет | +| поведение на полной перерисовке | 3 тика подряд мимо | считает все | +| условие корректности | никакого — не в нашей власти | зазор выборок < 64 512 тактов, проверяется артефактом | +| риск залипнуть в плохой фазе и ровно потерять 25 % скорости | есть | нет | + +## 9.10. Что осталось проверить зондами до кодирования + +1. **Худший зазор между выборками** при размещении «только в обёртках» + — сколько точек реально нужно добавить. Это же число решает, нужна + ли выборка в синей фазе в четырёх местах или в двух. +2. **Владение `cbl_mode`**: сейчас бит 5 доступен потому, что + `gfx_wait_vsync` взвёл `_cbl_port_ref()` при первом вызове. Свой + ожидатель обязан взвести его сам — значит примитив логичнее держать + в libbgi (там доступен `_cbl_port_ref`), а не в приложении. +3. **Ветка `frozen`** (`roomtest.c:320`) — какой темп ей нужен. +4. Совпадает ли момент возврата `wait_edge()` с нынешним возвратом + `gfx_wait_vsync()` с точностью до микросекунд (иначе поедет момент + свопа и появятся разрывы картинки).