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:
2026-07-15 10:36:37 +03:00
parent 2c6f4e33c3
commit 1b4fbeaa6b
11 changed files with 290 additions and 8 deletions
+42
View File
@@ -0,0 +1,42 @@
/*
* gfx_set_fps_div — задать делитель кадровой частоты (frame pacing).
* n: логический кадр = РОВНО n кадровых интервалов (1=50 fps дефолт,
* 2=25, 3=~16.7, 4=12.5, ...; 0 трактуется как 1). gfx_wait_vsync()
* при n>=2 переключается со лучевого поллинга на счётчиковый путь
* (docs/sprite-api-design.md §9е).
*
* n>=2: однократно ставит _gfx_frame_isr в ЦЕПОЧКУ кадровых IRQ
* (irq_chain_add — свой слот, не мешает собственному хендлеру
* приложения); повторные вызовы только меняют делитель.
* n<=1: снимает ТОЛЬКО свой слот (irq_chain_remove, НЕ irq_remove —
* чтобы не снести хендлер приложения) и возвращает старый путь.
*
* Возврат 0 / -1+errno: EINVAL (данные не в W2 — неподходящий memory
* mode, из irq_chain_add), ENOMEM (все слоты цепи заняты). ТОЛЬКО этот
* модуль ссылается на irq-механику и _gfx_frame_isr — программа без
* вызова сеттера не линкует ни цепочку, ни ISR (DCE).
*/
#include "../_bgi.h"
#include <irq.h>
int gfx_set_fps_div(uint8_t n)
{
if (n < 2) {
/* Выключение: снять свой слот, если ставили. */
if (_gfx_fps_div >= 2) {
irq_chain_remove(_gfx_frame_isr);
_gfx_fps_div = 1;
}
return 0;
}
/* Первое включение — занять слот цепи под свой ISR. */
if (_gfx_fps_div < 2) {
if (irq_chain_add(_gfx_frame_isr) != 0)
return -1; /* errno уже выставлен (EINVAL/ENOMEM) */
/* База отсчёта — с текущего тика, чтобы первый wait не «опоздал». */
_gfx_fps_last = _gfx_frame_tick;
}
_gfx_fps_div = n;
return 0;
}