Пейсинг: подробный разбор счётчика кадров по лучу (условие точности, точки выборки, приёмка)
This commit is contained in:
@@ -285,3 +285,223 @@ NORMAL секундомером совпадает с живым SDLPoP на о
|
|||||||
2. Ветка `frozen` (`roomtest.c:320`) — какой темп ей нужен под делителем.
|
2. Ветка `frozen` (`roomtest.c:320`) — какой темп ей нужен под делителем.
|
||||||
3. Проверить, что при n=4/5 бит 5 всё ещё единственный источник фронта
|
3. Проверить, что при n=4/5 бит 5 всё ещё единственный источник фронта
|
||||||
(то есть `_cbl_port_ref` держится всё это время).
|
(то есть `_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()` с точностью до микросекунд (иначе поедет момент
|
||||||
|
свопа и появятся разрывы картинки).
|
||||||
|
|||||||
Reference in New Issue
Block a user