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:
@@ -13,7 +13,20 @@
|
|||||||
|
|
||||||
## P0 — делаем сейчас
|
## P0 — делаем сейчас
|
||||||
|
|
||||||
### KBD-1. Shift + стрелки: нажатия теряются — определить причину
|
### KBD-1. Shift + стрелки: нажатия теряются — **ПРИЧИНА НАЙДЕНА, ФИКС ПРОВЕРЕН**
|
||||||
|
|
||||||
|
> **Итог (2026-08-01).** Причина — не наш код и не DI-окна графики: при
|
||||||
|
> зажатом Shift PS/2 удваивает трафик («fake shift»), а импульс запроса
|
||||||
|
> прерывания здесь теряется примерно в 44 % случаев, и трёхбайтовый FIFO
|
||||||
|
> SIO переполняется. Лечится ПЛОТНЫМ опросом: `kbd_raw_poll` повешен
|
||||||
|
> idle-хуком на ожидание кадра (`gfx_set_idle_hook`, новый API libbgi) —
|
||||||
|
> процессор всё равно проводит там ~42 мс из 60, крутя опрос луча.
|
||||||
|
>
|
||||||
|
> **Проверка в roomtest тем же счётным методом: 35 нажатий Shift+Home →
|
||||||
|
> 35 make, ноль потерь** (до фикса — 9 из 10). Боевой сценарий тоже:
|
||||||
|
> четыре Shift+→ подряд дали четыре осторожных шага, `Kid.x` 114 → 147.
|
||||||
|
> Цена: `_CODE` +170 Б, кадровый бюджет не затронут (опрос стоит в
|
||||||
|
> простое). Ниже — полный протокол, как к этому пришли.
|
||||||
|
|
||||||
**Симптом (пользователь, 2026-08-01).** Залипаний почти нет, но при
|
**Симптом (пользователь, 2026-08-01).** Залипаний почти нет, но при
|
||||||
УДЕРЖИВАЕМОМ Shift первые один-два нажатия ← дают осторожные шаги, дальше
|
УДЕРЖИВАЕМОМ Shift первые один-два нажатия ← дают осторожные шаги, дальше
|
||||||
@@ -183,8 +196,8 @@
|
|||||||
Плюс пришлось бы переписать все наши константы кодов под Set 3. Значит это
|
Плюс пришлось бы переписать все наши константы кодов под Set 3. Значит это
|
||||||
кандидат на «когда дойдём до реального железа», а не на сейчас.
|
кандидат на «когда дойдём до реального железа», а не на сейчас.
|
||||||
|
|
||||||
**Что делать (в порядке зависимостей):**
|
**Что сделано по этому плану (2026-08-01):**
|
||||||
1. Idle-хук в libbgi: `gfx_set_idle_hook(fn)`, вызывается в цикле ожидания
|
1. ✅ Idle-хук в libbgi: `gfx_set_idle_hook(fn)`, вызывается в цикле ожидания
|
||||||
луча внутри `gfx_wait_vsync`. Приложение ставит туда `kbd_raw_poll`.
|
луча внутри `gfx_wait_vsync`. Приложение ставит туда `kbd_raw_poll`.
|
||||||
Полезен не только нам — любой программе даёт «качать» что-то в ожидании
|
Полезен не только нам — любой программе даёт «качать» что-то в ожидании
|
||||||
кадра. Осторожно с регистрами: цикл ждёт на BC-таймауте, вокруг вызова
|
кадра. Осторожно с регистрами: цикл ждёт на BC-таймауте, вокруг вызова
|
||||||
@@ -196,9 +209,14 @@
|
|||||||
потери случаются ТОЛЬКО при зажатом модификаторе (замерено; без Shift
|
потери случаются ТОЛЬКО при зажатом модификаторе (замерено; без Shift
|
||||||
потерь нет). Значит хук можно взводить лишь пока нажат Shift/Ctrl/Alt —
|
потерь нет). Значит хук можно взводить лишь пока нажат Shift/Ctrl/Alt —
|
||||||
тогда опрос работает исключительно в той ситуации, ради которой заведён.
|
тогда опрос работает исключительно в той ситуации, ради которой заведён.
|
||||||
2. Вернуть вызовы в занятую треть кадра (они бесплатны, просто сами по себе
|
2. ✅ Перемерено в roomtest тем же счётным методом: **35/35**, потерь нет.
|
||||||
ничего не решали).
|
3. ⏳ Вызовы в занятую треть кадра НЕ возвращены: одного idle-хука хватило.
|
||||||
3. Перемерить тем же счётным методом уже в roomtest: цель — 25/25.
|
Держать в уме, если на реальном железе или на более тяжёлых сценах
|
||||||
|
(несколько стражей) потери появятся снова — накрыть блиты дешевле, чем
|
||||||
|
изобретать что-то новое.
|
||||||
|
4. ⏳ Ручная проверка пользователем — без неё этап не закрыт: скриптовые
|
||||||
|
нажатия ровнее человеческих, и «залипания до отпускания Shift» они не
|
||||||
|
воспроизводили с самого начала.
|
||||||
|
|
||||||
**Куда смотреть дальше, если плотного опроса не хватит.**
|
**Куда смотреть дальше, если плотного опроса не хватит.**
|
||||||
1. **Драйвер MAME — НЕ ТРОГАЕМ** (решение пользователя: пересборка MAME на
|
1. **Драйвер MAME — НЕ ТРОГАЕМ** (решение пользователя: пересборка MAME на
|
||||||
|
|||||||
@@ -57,22 +57,32 @@ const uint8_t n_banks = 4;
|
|||||||
/* Режим читов (pop_cheat.h): на время разработки включаем в main. */
|
/* Режим читов (pop_cheat.h): на время разработки включаем в main. */
|
||||||
uint8_t pop_cheats;
|
uint8_t pop_cheats;
|
||||||
|
|
||||||
/* ПОТЕРЯ НАЖАТИЙ ПРИ ЗАЖАТОМ Shift — НЕ ЧИНИТСЯ ИЗ ЭТОГО ЦИКЛА.
|
/* ПОТЕРЯ НАЖАТИЙ ПРИ ЗАЖАТОМ Shift — лечится ПЛОТНЫМ опросом клавиатуры.
|
||||||
*
|
*
|
||||||
* Здесь стояли шесть вызовов kbd_raw_poll() (вычерпать FIFO клавиатуры
|
* При зажатом Shift PS/2 обрамляет расширенный код «фиктивным шифтом», и
|
||||||
* после каждой тяжёлой фазы кадра). Убраны: замерено, что они не меняют
|
* одно нажатие стрелки превращается в 5 байт вместо 2 (отпускание — тоже
|
||||||
* НИЧЕГО — 9 дошедших make-байт из 10 нажатий и с ними, и без них. Они
|
* 5). Приёмный FIFO SIO — 3 байта, а импульс запроса прерывания здесь
|
||||||
* стояли ровно там, где прерывания и так разрешены, то есть добавляли то,
|
* теряется примерно в 44 % случаев, поэтому байты копятся и один из
|
||||||
* что трамплин делает сам.
|
* полусотни пропадает ДО чтения порта: нажатие «не срабатывает».
|
||||||
*
|
*
|
||||||
* Настоящая причина лежит НИЖЕ нашего кода: при зажатом Shift PS/2 шлёт на
|
* Что НЕ помогает (замерено, TASKS.md/KBD-1): редкий опрос (несколько
|
||||||
* расширенную клавишу вдвое больше байт («fake shift»), а импульс запроса
|
* вызовов за кадр — они попадают туда, где прерывания и так разрешены, и
|
||||||
* прерывания пропускается примерно в 44 % случаев — байты копятся в
|
* лишь дублируют трамплин) и укорачивание DI-окон графики (в кадре вообще
|
||||||
* трёхбайтовом FIFO SIO и один из полусотни теряется ДО чтения порта.
|
* без блитов потерь БОЛЬШЕ). Снимать `di` в accel-ядрах нельзя — машина
|
||||||
* Длина наших DI-окон ни при чём: в замороженном кадре, где блитов нет
|
* уходит в перезагрузку.
|
||||||
* вообще, потерь БОЛЬШЕ, а не меньше.
|
|
||||||
*
|
*
|
||||||
* Полный протокол замеров и куда копать дальше — TASKS.md, KBD-1. */
|
* Что помогает: опрос раз в ~0.5 мс. Столько времени берётся из ожидания
|
||||||
|
* кадра — процессор всё равно крутит там ~42 мс из 60, ничего не делая.
|
||||||
|
* Поэтому kbd_raw_poll вешается idle-хуком на gfx_wait_vsync (см.
|
||||||
|
* kbd_idle ниже); полезной работы это не отнимает. Потолок приёма
|
||||||
|
* подтверждён отдельно (tests/kbdpoll: 25 нажатий → 25, 250 байт из 250). */
|
||||||
|
|
||||||
|
/* Обёртка ради типа: хук — void(*)(void), а kbd_raw_poll возвращает
|
||||||
|
* «было ли что вычерпано». Приводить указатель кастом не хочется. */
|
||||||
|
static void kbd_idle(void)
|
||||||
|
{
|
||||||
|
kbd_raw_poll();
|
||||||
|
}
|
||||||
|
|
||||||
/* seq_2_stand (types.h seqids) — Kid стоит, ждёт ввода. */
|
/* seq_2_stand (types.h seqids) — Kid стоит, ждёт ввода. */
|
||||||
#define SEQ_STAND 2
|
#define SEQ_STAND 2
|
||||||
@@ -317,6 +327,7 @@ int main(void)
|
|||||||
puts("kbd_raw open failed");
|
puts("kbd_raw open failed");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
gfx_set_idle_hook(kbd_idle); /* вычерпывать FIFO, пока ждём кадр */
|
||||||
|
|
||||||
while (!kbd_raw_down(KBD_ESC)) {
|
while (!kbd_raw_down(KBD_ESC)) {
|
||||||
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
|
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
|
||||||
@@ -583,6 +594,7 @@ int main(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gfx_set_idle_hook(0); /* снять хук ДО закрытия raw-канала */
|
||||||
pop_ctrl_close();
|
pop_ctrl_close();
|
||||||
closegraph();
|
closegraph();
|
||||||
pop_bg_free();
|
pop_bg_free();
|
||||||
|
|||||||
@@ -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_ref(void);
|
||||||
extern void _cbl_port_unref(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) --------------------------------- *
|
/* ---- FPS-делитель (frame pacing) --------------------------------- *
|
||||||
* Фоновый счётчик кадров через цепочку кадровых IRQ (libc/irq). Данные
|
* Фоновый счётчик кадров через цепочку кадровых IRQ (libc/irq). Данные
|
||||||
* в common/_gfx_fps_state.c; при _gfx_fps_div<=1 путь не активен и
|
* в 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) ---
|
;; --- фаза 1: дождаться bit5==1 (гарантированно в blank) ---
|
||||||
ld bc, #0 ; ~65536 попыток на фазу — таймаут
|
ld bc, #0 ; ~65536 попыток на фазу — таймаут
|
||||||
_gwv_wait1:
|
_gwv_wait1:
|
||||||
|
call _gwv_idle ; занять простой (см. gfx_set_idle_hook)
|
||||||
in a, (#0xFE)
|
in a, (#0xFE)
|
||||||
bit 5, a
|
bit 5, a
|
||||||
jr nz, _gwv_have1
|
jr nz, _gwv_have1
|
||||||
@@ -75,6 +76,7 @@ static void gfx_wait_vsync_beam(void)
|
|||||||
;; --- фаза 2: дождаться bit5==0 (сам переход — начало кадра) ---
|
;; --- фаза 2: дождаться bit5==0 (сам переход — начало кадра) ---
|
||||||
ld bc, #0
|
ld bc, #0
|
||||||
_gwv_wait0:
|
_gwv_wait0:
|
||||||
|
call _gwv_idle
|
||||||
in a, (#0xFE)
|
in a, (#0xFE)
|
||||||
bit 5, a
|
bit 5, a
|
||||||
jr z, _gwv_done
|
jr z, _gwv_done
|
||||||
@@ -83,6 +85,26 @@ static void gfx_wait_vsync_beam(void)
|
|||||||
or c
|
or c
|
||||||
jr nz, _gwv_wait0
|
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:
|
_gwv_fallback:
|
||||||
ei
|
ei
|
||||||
|
|||||||
@@ -150,6 +150,18 @@ void gfx_heal(int x, int y, int w, int h);
|
|||||||
* gfx_set_visible_page(hidden); // tear-free flip */
|
* gfx_set_visible_page(hidden); // tear-free flip */
|
||||||
void gfx_wait_vsync(void);
|
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 кадровых
|
/* FPS-делитель (frame pacing): логический кадр = РОВНО n кадровых
|
||||||
* интервалов, независимо от плавания длительности рендера. n: 1 = 50 fps
|
* интервалов, независимо от плавания длительности рендера. n: 1 = 50 fps
|
||||||
* (дефолт), 2 = 25, 3 = ~16.7, 4 = 12.5, ...; 0 трактуется как 1.
|
* (дефолт), 2 = 25, 3 = ~16.7, 4 = 12.5, ...; 0 трактуется как 1.
|
||||||
|
|||||||
Reference in New Issue
Block a user