diff --git a/applications/PoP/roomtest/TASKS.md b/applications/PoP/roomtest/TASKS.md index 0a91c44..aa2749a 100644 --- a/applications/PoP/roomtest/TASKS.md +++ b/applications/PoP/roomtest/TASKS.md @@ -13,7 +13,20 @@ ## 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).** Залипаний почти нет, но при УДЕРЖИВАЕМОМ Shift первые один-два нажатия ← дают осторожные шаги, дальше @@ -183,8 +196,8 @@ Плюс пришлось бы переписать все наши константы кодов под Set 3. Значит это кандидат на «когда дойдём до реального железа», а не на сейчас. -**Что делать (в порядке зависимостей):** -1. Idle-хук в libbgi: `gfx_set_idle_hook(fn)`, вызывается в цикле ожидания +**Что сделано по этому плану (2026-08-01):** +1. ✅ Idle-хук в libbgi: `gfx_set_idle_hook(fn)`, вызывается в цикле ожидания луча внутри `gfx_wait_vsync`. Приложение ставит туда `kbd_raw_poll`. Полезен не только нам — любой программе даёт «качать» что-то в ожидании кадра. Осторожно с регистрами: цикл ждёт на BC-таймауте, вокруг вызова @@ -196,9 +209,14 @@ потери случаются ТОЛЬКО при зажатом модификаторе (замерено; без Shift потерь нет). Значит хук можно взводить лишь пока нажат Shift/Ctrl/Alt — тогда опрос работает исключительно в той ситуации, ради которой заведён. -2. Вернуть вызовы в занятую треть кадра (они бесплатны, просто сами по себе - ничего не решали). -3. Перемерить тем же счётным методом уже в roomtest: цель — 25/25. +2. ✅ Перемерено в roomtest тем же счётным методом: **35/35**, потерь нет. +3. ⏳ Вызовы в занятую треть кадра НЕ возвращены: одного idle-хука хватило. + Держать в уме, если на реальном железе или на более тяжёлых сценах + (несколько стражей) потери появятся снова — накрыть блиты дешевле, чем + изобретать что-то новое. +4. ⏳ Ручная проверка пользователем — без неё этап не закрыт: скриптовые + нажатия ровнее человеческих, и «залипания до отпускания Shift» они не + воспроизводили с самого начала. **Куда смотреть дальше, если плотного опроса не хватит.** 1. **Драйвер MAME — НЕ ТРОГАЕМ** (решение пользователя: пересборка MAME на diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 3dce59f..7969d4d 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -57,22 +57,32 @@ const uint8_t n_banks = 4; /* Режим читов (pop_cheat.h): на время разработки включаем в main. */ uint8_t pop_cheats; -/* ПОТЕРЯ НАЖАТИЙ ПРИ ЗАЖАТОМ Shift — НЕ ЧИНИТСЯ ИЗ ЭТОГО ЦИКЛА. +/* ПОТЕРЯ НАЖАТИЙ ПРИ ЗАЖАТОМ Shift — лечится ПЛОТНЫМ опросом клавиатуры. * - * Здесь стояли шесть вызовов kbd_raw_poll() (вычерпать FIFO клавиатуры - * после каждой тяжёлой фазы кадра). Убраны: замерено, что они не меняют - * НИЧЕГО — 9 дошедших make-байт из 10 нажатий и с ними, и без них. Они - * стояли ровно там, где прерывания и так разрешены, то есть добавляли то, - * что трамплин делает сам. + * При зажатом Shift PS/2 обрамляет расширенный код «фиктивным шифтом», и + * одно нажатие стрелки превращается в 5 байт вместо 2 (отпускание — тоже + * 5). Приёмный FIFO SIO — 3 байта, а импульс запроса прерывания здесь + * теряется примерно в 44 % случаев, поэтому байты копятся и один из + * полусотни пропадает ДО чтения порта: нажатие «не срабатывает». * - * Настоящая причина лежит НИЖЕ нашего кода: при зажатом Shift PS/2 шлёт на - * расширенную клавишу вдвое больше байт («fake shift»), а импульс запроса - * прерывания пропускается примерно в 44 % случаев — байты копятся в - * трёхбайтовом FIFO SIO и один из полусотни теряется ДО чтения порта. - * Длина наших DI-окон ни при чём: в замороженном кадре, где блитов нет - * вообще, потерь БОЛЬШЕ, а не меньше. + * Что НЕ помогает (замерено, TASKS.md/KBD-1): редкий опрос (несколько + * вызовов за кадр — они попадают туда, где прерывания и так разрешены, и + * лишь дублируют трамплин) и укорачивание DI-окон графики (в кадре вообще + * без блитов потерь БОЛЬШЕ). Снимать `di` в accel-ядрах нельзя — машина + * уходит в перезагрузку. * - * Полный протокол замеров и куда копать дальше — 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 стоит, ждёт ввода. */ #define SEQ_STAND 2 @@ -317,6 +327,7 @@ int main(void) puts("kbd_raw open failed"); return 1; } + gfx_set_idle_hook(kbd_idle); /* вычерпывать FIFO, пока ждём кадр */ while (!kbd_raw_down(KBD_ESC)) { /* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один @@ -583,6 +594,7 @@ int main(void) } } + gfx_set_idle_hook(0); /* снять хук ДО закрытия raw-канала */ pop_ctrl_close(); closegraph(); pop_bg_free(); diff --git a/libbgi/_bgi.h b/libbgi/_bgi.h index 10f6ba7..1067b64 100644 --- a/libbgi/_bgi.h +++ b/libbgi/_bgi.h @@ -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 путь не активен и diff --git a/libbgi/common/_gfx_idle_state.c b/libbgi/common/_gfx_idle_state.c new file mode 100644 index 0000000..8abd1bf --- /dev/null +++ b/libbgi/common/_gfx_idle_state.c @@ -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); diff --git a/libbgi/common/gfx_set_idle_hook.c b/libbgi/common/gfx_set_idle_hook.c new file mode 100644 index 0000000..bb40f01 --- /dev/null +++ b/libbgi/common/gfx_set_idle_hook.c @@ -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` из ) — но только ПЛОТНЫМ, раз в + * ~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; +} diff --git a/libbgi/common/gfx_wait_vsync.c b/libbgi/common/gfx_wait_vsync.c index 8043c21..0e87f75 100644 --- a/libbgi/common/gfx_wait_vsync.c +++ b/libbgi/common/gfx_wait_vsync.c @@ -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 diff --git a/libbgi/include/gfx.h b/libbgi/include/gfx.h index 90f9683..8467707 100644 --- a/libbgi/include/gfx.h +++ b/libbgi/include/gfx.h @@ -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`, см. ): трёхбайтовый приёмник переполняется + * на пачках «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.