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:
Александр Петров
2026-08-01 16:05:24 +03:00
parent b56f2b4582
commit 4b498d171b
7 changed files with 125 additions and 19 deletions
+10
View File
@@ -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);
+27
View File
@@ -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;
}
+22
View File
@@ -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