Files
Sprinter-SDCC/libbgi/common/gfx_wait_vsync.c
T
snark13 1b4fbeaa6b 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>
2026-07-15 10:36:37 +03:00

117 lines
5.0 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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:
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:
in a, (#0xFE)
bit 5, a
jr z, _gwv_done
dec bc
ld a, b
or c
jr nz, _gwv_wait0
;; таймаут и здесь переходим к тому же фолбэку
_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();
}