Files
Sprinter-SDCC/libbgi/common/gfx_wait_vsync.c
T
Александр Петров 4b498d171b 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>
2026-08-01 16:05:24 +03:00

139 lines
6.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:
call _gwv_idle ; занять простой (см. gfx_set_idle_hook)
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:
call _gwv_idle
in a, (#0xFE)
bit 5, a
jr z, _gwv_done
dec bc
ld a, b
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
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();
}