libbgi: FPS-делитель gfx_set_fps_div(n) поверх цепочки кадровых IRQ

Логический кадр = ровно n кадровых интервалов (1=50/2=25/3=~16.7 fps);
при переполнении слота — выравнивание на ближайший фронт (без дрейфа
фазы, в отличие от наивного «жди n фронтов»).

Механика: фоновый счётчик _gfx_frame_tick инкрементит _gfx_frame_isr,
поставленный в СВОЙ слот цепи (irq_chain_add); gfx_set_fps_div(1) снимает
только этот слот (irq_chain_remove), не трогая хендлер приложения.
gfx_wait_vsync: ветка n>=2 (счётчик + halt) перед лучевым поллингом;
поллинг вынесен в static gfx_wait_vsync_beam (функция с хвостовым __asm
не должна иметь переходов через asm — SDCC не эмитит эпилог-метку;
ранний return делителя в чистом-C gfx_wait_vsync).

Файлы: common/_gfx_fps_state.c (данные), _gfx_frame_isr.c (ISR),
gfx_set_fps_div.c (сеттер, единственная ссылка на irq-механику → DCE).
Работает tiny/big/huge (цепочка all-modes); small для мелких программ
= EINVAL.

Проверено MAME (tests/fpsdiv): n=1/2/3 → 20/40/60 кадров на 20 wait'ов
(drift=0); n=2 с рендер-заглушкой ~1 кадр → период держится 2
(поглощение перерасхода, наивный путь дал бы ~60); huge идентично;
small = EINVAL graceful.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 10:36:37 +03:00
parent 2c6f4e33c3
commit 1b4fbeaa6b
11 changed files with 290 additions and 8 deletions
+107
View File
@@ -0,0 +1,107 @@
/*
* fpsdiv — тест FPS-делителя (frame pacing, docs/sprite-api-design.md §9е).
*
* Проверяет:
* 1. коэффициент: gfx_set_fps_div(n) → gfx_wait_vsync() ждёт РОВНО n
* кадровых интервалов; 20 wait'ов копят ровно 20·n кадров (точное
* значение = отсутствие дрейфа фазы);
* 2. поглощение перерасхода: при n=2 с рендер-заглушкой ~1 кадр
* период держится 2 (wait добирает до границы слота), а не уезжает
* в 3 — то, чего наивный «жди n фронтов подряд» не умеет;
* 3. сосуществование со СВОИМ хендлером приложения: тест сам держит
* слот цепи (on_frame), делитель добавляет второй — оба тикают,
* gfx_set_fps_div(1) снимает ТОЛЬКО свой слот.
*
* Графический режим НЕ инициализируется: счётчиковый путь gfx_wait_vsync
* трогает лишь _gfx_frame_tick + halt (видео не нужно). Собственный
* счётчик кадров (on_frame) меряет реальные фронты независимо от делителя.
*/
#include <stdio.h>
#include <gfx.h>
#include <irq.h>
#include <errno.h>
static volatile uint16_t frames; /* реальные кадры (наш слот цепи) */
static void on_frame(void)
{
frames++;
}
/* Атомарное чтение счётчика, разделяемого с ISR. */
static uint16_t rd(void)
{
uint16_t v;
IRQ_DISABLE();
v = frames;
IRQ_ENABLE();
return v;
}
/* Занятая пауза без ESTEX — «рендер». */
static void busy(uint16_t loops)
{
while (loops--) {
__asm nop __endasm;
}
}
int main(void)
{
/* Свой слот цепи — независимый счётчик реальных кадров. */
if (irq_chain_add(on_frame) != 0) {
printf("chain_add(on_frame): errno=%d\n", errno);
return 1;
}
/* --- 1. коэффициент n=1/2/3 --- */
for (uint8_t n = 1; n <= 3; n++) {
if (gfx_set_fps_div(n) != 0) {
printf("set_fps_div(%u): errno=%d\n", n, errno);
return 1;
}
gfx_wait_vsync(); /* выровняться на границу слота */
uint16_t f0 = rd();
for (uint8_t i = 0; i < 20; i++)
gfx_wait_vsync();
uint16_t df = rd() - f0;
printf("n=%u: %u frames / 20 waits (expect %u; drift=%d)\n",
n, df, (unsigned)(20 * n), (int)df - (int)(20 * n));
}
/* --- 2. поглощение перерасхода при n=2 --- */
gfx_set_fps_div(2);
gfx_wait_vsync();
/* Откалибровать заглушку так, чтобы «рендер» пересекал ~1 фронт. */
uint16_t spin = 2000;
for (;;) {
uint16_t a = rd();
busy(spin);
if (rd() - a >= 1) break; /* заняли >= 1 интервала */
spin = (uint16_t)(spin + spin / 2 + 1000);
if (spin > 60000u) break;
}
printf("stub spin=%u (~1 frame of render)\n", spin);
gfx_wait_vsync(); /* чистая граница слота */
uint16_t p0 = rd();
for (uint8_t i = 0; i < 20; i++) {
busy(spin); /* рендер ~1 кадр */
gfx_wait_vsync(); /* слот n=2 должен добрать до 2 */
}
uint16_t pd = rd() - p0;
printf("n=2 +render: %u frames / 20 waits (expect 40; naive~60)\n", pd);
/* --- 3. снять ТОЛЬКО слот делителя, наш on_frame обязан жить --- */
gfx_set_fps_div(1);
uint16_t g0 = rd();
busy(40000); /* дать нескольким кадрам пройти */
uint16_t gd = rd() - g0;
printf("after fps_div(1): on_frame still ticks: %u (>0)\n", gd);
irq_chain_remove(on_frame);
puts("fpsdiv done.");
return 0;
}