libbgi: idle-хук в ожидании кадра; им лечится потеря нажатий с Shift
Причина потерь (замеры — applications/PoP/roomtest/TASKS.md, KBD-1): при
зажатом Shift PS/2 обрамляет расширенный код «фиктивным шифтом», нажатие
стрелки становится 5 байтами вместо 2, а импульс запроса прерывания здесь
теряется примерно в 44 % случаев — трёхбайтовый FIFO SIO переполняется, и
байт пропадает ДО чтения порта. Лечится только плотным вычерпыванием: раз
в ~0.5 мс. Столько времени есть даром — при пейсинге «3 растровых кадра на
логический тик» процессор проводит ~42 мс из 60 в gfx_wait_vsync, крутя
опрос луча и больше ничего не делая.
- gfx_set_idle_hook(fn) — что вызывать, пока gfx_wait_vsync ждёт луч.
Состояние в отдельном data-модуле (_gfx_idle_state.c), чтобы не тянуть
сеттер в программы, которые хук не ставят.
- Лучевой цикл зовёт хук в обеих фазах. BC (счётчик таймаута)
сохраняется, косвенный вызов — push адреса возврата + jp (hl), так как
`call (hl)` в Z80 нет; без хука это ret по нулевому указателю, порядка
двух десятков тактов в цикле, который и так сжигает время.
- Путь FPS-делителя не затронут: там ожидание через HALT.
- roomtest вешает на хук kbd_raw_poll.
Проверка в MAME счётчиками (брейкпоинты с { b@ADDR = b@ADDR+1 ; g } на
чтении порта 0x18 и на установке make-бита): 35 нажатий Shift+Home → 35
make, ноль потерь; до фикса было 9 из 10. Боевой сценарий: четыре Shift+→
подряд дали четыре осторожных шага (Kid.x 114 -> 147). _CODE +170 Б,
кадровый бюджет не затронут.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -347,6 +347,11 @@ void _bgi_poly_edge(int x0, int y0, int x1, int y1);
|
||||
extern void _cbl_port_ref(void);
|
||||
extern void _cbl_port_unref(void);
|
||||
|
||||
/* Idle-хук ожидания кадра (common/_gfx_idle_state.c, ставится
|
||||
* gfx_set_idle_hook). NULL = нет; зовётся из лучевого цикла
|
||||
* gfx_wait_vsync. */
|
||||
extern void (*_gfx_idle_hook)(void);
|
||||
|
||||
/* ---- FPS-делитель (frame pacing) --------------------------------- *
|
||||
* Фоновый счётчик кадров через цепочку кадровых IRQ (libc/irq). Данные
|
||||
* в common/_gfx_fps_state.c; при _gfx_fps_div<=1 путь не активен и
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* _gfx_idle_state — указатель на idle-хук, который gfx_wait_vsync зовёт,
|
||||
* пока ждёт луч. Отдельный data-модуль: на него ссылаются и установщик
|
||||
* (gfx_set_idle_hook), и сам цикл ожидания, а тянуть один из-за другого
|
||||
* в программу, которая хук не ставит, ни к чему. Без инициализации
|
||||
* (crt0 зануляет _DATA — NULL = хука нет).
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
|
||||
void (*_gfx_idle_hook)(void);
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* gfx_set_idle_hook — что вызывать, пока gfx_wait_vsync ждёт луч.
|
||||
*
|
||||
* ЗАЧЕМ. Ожидание кадра — это самый длинный простой в кадре: у игры с
|
||||
* пейсингом «3 растровых кадра на логический тик» процессор проводит там
|
||||
* ~42 мс из 60, крутя `in a,(#0xFE)` и больше ничего не делая. Хук
|
||||
* позволяет занять это время полезным опросом, не отнимая ни такта у
|
||||
* полезной работы.
|
||||
*
|
||||
* Первый потребитель (и повод завести) — клавиатура: приёмный FIFO SIO
|
||||
* всего 3 байта, а одно нажатие расширенной клавиши при зажатом Shift
|
||||
* даёт пачку из 5 байт («fake shift»); если импульс запроса прерывания
|
||||
* потерян, байты копятся и один теряется. Лечится вычерпыванием по
|
||||
* опросу (`kbd_raw_poll` из <kbd_raw.h>) — но только ПЛОТНЫМ, раз в
|
||||
* ~0.5 мс, а не пару раз за кадр. Замер: applications/PoP/roomtest/
|
||||
* TASKS.md, задача KBD-1.
|
||||
*
|
||||
* ТРЕБОВАНИЯ К ХУКУ: он зовётся в тесном цикле, поэтому обязан быть
|
||||
* дешёвым и НЕ рисовать (окно W3 в этот момент принадлежит вызывающему,
|
||||
* а не графике) и не ждать сам. fn = 0 — снять хук.
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
|
||||
void gfx_set_idle_hook(void (*fn)(void))
|
||||
{
|
||||
_gfx_idle_hook = fn;
|
||||
}
|
||||
@@ -62,6 +62,7 @@ static void gfx_wait_vsync_beam(void)
|
||||
;; --- фаза 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
|
||||
@@ -75,6 +76,7 @@ static void gfx_wait_vsync_beam(void)
|
||||
;; --- фаза 2: дождаться bit5==0 (сам переход — начало кадра) ---
|
||||
ld bc, #0
|
||||
_gwv_wait0:
|
||||
call _gwv_idle
|
||||
in a, (#0xFE)
|
||||
bit 5, a
|
||||
jr z, _gwv_done
|
||||
@@ -83,6 +85,26 @@ static void gfx_wait_vsync_beam(void)
|
||||
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
|
||||
|
||||
@@ -150,6 +150,18 @@ void gfx_heal(int x, int y, int w, int h);
|
||||
* gfx_set_visible_page(hidden); // tear-free flip */
|
||||
void gfx_wait_vsync(void);
|
||||
|
||||
/* Что вызывать, ПОКА gfx_wait_vsync ждёт луч. Ожидание кадра — самый
|
||||
* длинный простой в кадре (при пейсинге «3 растровых кадра на логический
|
||||
* тик» это ~42 мс из 60), и хук занимает его, не отнимая тактов у
|
||||
* полезной работы. Повод — вычерпывание FIFO клавиатуры по опросу
|
||||
* (`kbd_raw_poll`, см. <kbd_raw.h>): трёхбайтовый приёмник переполняется
|
||||
* на пачках «fake shift», а плотность нужна ~раз в 0.5 мс, которую иначе
|
||||
* взять негде. Хук зовётся в тесном цикле: он обязан быть дешёвым, НЕ
|
||||
* рисовать (окно W3 сейчас не за графикой) и не ждать сам. fn = 0 —
|
||||
* снять. На путь FPS-делителя (gfx_set_fps_div n>=2) не влияет: там
|
||||
* ожидание идёт через HALT, и процессор не крутит цикл. */
|
||||
void gfx_set_idle_hook(void (*fn)(void));
|
||||
|
||||
/* FPS-делитель (frame pacing): логический кадр = РОВНО n кадровых
|
||||
* интервалов, независимо от плавания длительности рендера. n: 1 = 50 fps
|
||||
* (дефолт), 2 = 25, 3 = ~16.7, 4 = 12.5, ...; 0 трактуется как 1.
|
||||
|
||||
Reference in New Issue
Block a user