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