/*
 * gfx_wait_vsync — ждать переход бита 5 порта 0xFE из 1 в 0: выход из
 * нижнего blank/border (Y > 256 — см. MAME sprinter.cpp kbd_fe_r:
 * `vpos() >= BORDER_TOP+SCREEN_YSIZE`) — начало нового кадра, идеальный
 * момент для свопа страниц без разрывов.  Это чтение РЕАЛЬНОГО
 * состояния луча (не событие/прерывание), поэтому не подвержено
 * путанице с другими источниками вектора 0xFF (клавиатура, CBL, CTC) —
 * подробнее см. memory/gfx_wait_vsync_redesign.
 *
 * Бит доступен ТОЛЬКО пока включён cbl_mode() (bit7 порта 0x004E) —
 * см. libc/cbl/_cbl_port.c: если приложение уже играет через
 * cbl_open(), берём бит бесплатно (порт не трогаем); иначе
 * лениво занимаем bit7 "немым" кодом частоты через _cbl_port_ref()
 * при первом вызове, освобождаем в atexit.
 *
 * Fallback: если ни разу не увидели ожидаемое состояние за разумное
 * число попыток (иное железо, бит ведёт себя не как в MAME) —
 * откатываемся на одно кадровое прерывание (halt), чтобы не
 * зависнуть намертво.
 *
 * FPS-ДЕЛИТЕЛЬ (docs/sprite-api-design.md §9е): при _gfx_fps_div>=2
 * (выставлен gfx_set_fps_div) идём СЧЁТЧИКОВЫМ путём — ждём, пока
 * фоновый ISR насчитает n кадровых фронтов от прошлого выхода.  В
 * отличие от поллинга это ловит фронты, прошедшие ВО ВРЕМЯ рендера
 * (elapsed>=n → выравнивание на ближайший фронт, без накопления фазы).
 */

#include "../_bgi.h"
#include <sprinter_exit.h>

static uint8_t vsync_ready;

static void vsync_cleanup(void)
{
    _cbl_port_unref();
}

/* EI + HALT: разбудит ЛЮБОЕ прерывание (кадр/клавиатура/CBL), вызывающий
 * цикл перепроверит счётчик.  EI обязателен (как в fallback ниже). */
static void gfx_frame_halt(void) __naked
{
    __asm
        ei
        halt
        ret
    __endasm;
}

/* Лучевой поллинг (n<=1) — выделен в отдельную функцию: её тело
 * заканчивается непрозрачным __asm-блоком, поэтому в НЕЙ не должно быть
 * переходов «через asm» (SDCC не эмитит эпилог-метку за таким блоком).
 * Ранний return делителя живёт в gfx_wait_vsync (чистый C). */
static void gfx_wait_vsync_beam(void)
{
    if (!vsync_ready) {
        vsync_ready = 1;
        _cbl_port_ref();
        atexit(vsync_cleanup);
    }

    __asm
        ;; --- фаза 1: дождаться bit5==1 (гарантированно в blank) ---
        ld    bc, #0             ; ~65536 попыток на фазу — таймаут
    _gwv_wait1:
        call  _gwv_idle          ; занять простой (см. gfx_set_idle_hook)
        in    a, (#0xFE)
        bit   5, a
        jr    nz, _gwv_have1
        dec   bc
        ld    a, b
        or    c
        jr    nz, _gwv_wait1
        jr    _gwv_fallback      ; bit5 ни разу не встал за таймаут
    _gwv_have1:

        ;; --- фаза 2: дождаться bit5==0 (сам переход — начало кадра) ---
        ld    bc, #0
    _gwv_wait0:
        call  _gwv_idle
        in    a, (#0xFE)
        bit   5, a
        jr    z, _gwv_done
        dec   bc
        ld    a, b
        or    c
        jr    nz, _gwv_wait0
        ;; таймаут и здесь — переходим к тому же фолбэку
        jr    _gwv_fallback

        ;; --- вызов idle-хука ------------------------------------- *
        ;; BC (счётчик таймаута) сохраняем: хук — обычная C-функция и
        ;; клобберит всё, кроме IX.  Косвенный вызов через push адреса
        ;; возврата + jp (hl): `call (hl)` в Z80 нет.  Цена холостого
        ;; прохода — около двух десятков тактов в цикле, который и так
        ;; сжигает время впустую.
    _gwv_idle:
        ld    hl, (__gfx_idle_hook)
        ld    a, h
        or    l
        ret   z                  ; хук не поставлен
        push  bc
        ld    de, #_gwv_idle_ret
        push  de                 ; адрес возврата для jp (hl)
        jp    (hl)
    _gwv_idle_ret:
        pop   bc
        ret

    _gwv_fallback:
        ei
        halt                     ; аварийный путь: одно кадровое прерывание

    _gwv_done:
    __endasm;
}

void gfx_wait_vsync(void)
{
    /* --- счётчиковый путь делителя (n>=2) --- */
    if (_gfx_fps_div >= 2) {
        uint8_t n = _gfx_fps_div;
        if ((uint8_t)(_gfx_frame_tick - _gfx_fps_last) >= n) {
            /* Слот истёк (рендер занял >= n интервалов) — ближайший
             * фронт, чтобы не копить фазовую ошибку. */
            uint8_t t = _gfx_frame_tick;
            while (_gfx_frame_tick == t)
                gfx_frame_halt();
        } else {
            while ((uint8_t)(_gfx_frame_tick - _gfx_fps_last) < n)
                gfx_frame_halt();
        }
        _gfx_fps_last = _gfx_frame_tick;
        return;
    }

    /* --- n<=1: обычный лучевой поллинг (поведение бит-в-бит прежнее) --- */
    gfx_wait_vsync_beam();
}
