4b498d171b
Причина потерь (замеры — 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>
139 lines
6.0 KiB
C
139 lines
6.0 KiB
C
/*
|
||
* 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();
|
||
}
|