/* * 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 #include #include #include 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; }