Пейсинг: подробный разбор счётчика кадров по лучу (условие точности, точки выборки, приёмка)
This commit is contained in:
@@ -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()` с точностью до микросекунд (иначе поедет момент
|
||||
свопа и появятся разрывы картинки).
|
||||
|
||||
Reference in New Issue
Block a user