1b4fbeaa6b
Логический кадр = ровно 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>
108 lines
4.2 KiB
C
108 lines
4.2 KiB
C
/*
|
||
* 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;
|
||
}
|