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>
This commit is contained in:
@@ -17,6 +17,12 @@
|
||||
* число попыток (иное железо, бит ведёт себя не как в MAME) —
|
||||
* откатываемся на одно кадровое прерывание (halt), чтобы не
|
||||
* зависнуть намертво.
|
||||
*
|
||||
* FPS-ДЕЛИТЕЛЬ (docs/sprite-api-design.md §9е): при _gfx_fps_div>=2
|
||||
* (выставлен gfx_set_fps_div) идём СЧЁТЧИКОВЫМ путём — ждём, пока
|
||||
* фоновый ISR насчитает n кадровых фронтов от прошлого выхода. В
|
||||
* отличие от поллинга это ловит фронты, прошедшие ВО ВРЕМЯ рендера
|
||||
* (elapsed>=n → выравнивание на ближайший фронт, без накопления фазы).
|
||||
*/
|
||||
|
||||
#include "../_bgi.h"
|
||||
@@ -29,7 +35,22 @@ static void vsync_cleanup(void)
|
||||
_cbl_port_unref();
|
||||
}
|
||||
|
||||
void gfx_wait_vsync(void)
|
||||
/* 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;
|
||||
@@ -70,3 +91,26 @@ void gfx_wait_vsync(void)
|
||||
_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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user