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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user