From 2c6f4e33c3f0291df4964cba854cbbace68ec577 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Wed, 15 Jul 2026 10:23:39 +0300 Subject: [PATCH] =?UTF-8?q?libc/irq:=20=D1=86=D0=B5=D0=BF=D0=BE=D1=87?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BA=D0=B0=D0=B4=D1=80=D0=BE=D0=B2=D1=8B=D1=85?= =?UTF-8?q?=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=87=D0=B8=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2=20+=20all-modes=20W1-remap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _irq_user → _irq_chain[4]+_irq_chain_n (W2-BSS); трамплин tr_frame проходит слоты (один тяжёлый сейв на всю цепь, пустой слот пропуск). API irq_chain_add (0/-1+ENOMEM) / irq_chain_remove(h); irq_install → обёртка chain_add (EBUSY исчез), irq_remove() рвёт всю цепь; refcount таблицы на первом/последнем слоте, мутации под IRQ_DISABLE. Лимит 4 кадровых + 1 CTC. All-modes: трамплин copy-safe (только jr/djnz + литерал jp 0x0038), _irq_table_ref копирует его в _irq_tramp_w2buf (W2) когда оригинал в W1 (small/huge), вектор → на копию; вокруг вызова хендлеров восстанавливает базовую W1-страницу (_irq_app_w1_page = IN 0xA2). Хендлер может лежать где угодно в плоском 0x4000-0xBFFF. Проверено в MAME (tests/irqtest, 2 хендлера): tiny/big/huge — chain h1=h2 → remove h2 → h1 жив/h2=0; huge = код в W1, remap работает. Follow-up: CTC в small/huge = EINVAL (нужна W2-копия _irq_ctc_tramp); small для мелких программ (BSS в W1) = irq_install EINVAL, safe. Доки: im2_isr_design (цепочка), sprite-api §9е (делитель поверх цепи), fast_ram §8. rpgprof: gfx_sprite_ysort в профиль (+230 Б baseline). Co-Authored-By: Claude Opus 4.8 --- docs/TODO.md | 44 +++++--- docs/fast_ram.md | 94 ++++++++++++++++ docs/im2_isr_design.md | 212 +++++++++++++++++++++++++++++++++++- docs/size_baseline.tsv | 4 +- docs/sprite-api-design.md | 34 +++--- examples/rpgwalk/rpgprof.c | 1 + libc/include/irq.h | 51 ++++++--- libc/irq/_irq.h | 24 +++- libc/irq/_irq_state.c | 45 ++++++-- libc/irq/_irq_table.c | 53 +++++++-- libc/irq/_irq_tramp.c | 130 ++++++++++++++-------- libc/irq/irq_chain_add.c | 36 ++++++ libc/irq/irq_chain_remove.c | 25 +++++ libc/irq/irq_ctc_install.c | 4 + libc/irq/irq_install.c | 14 +-- libc/irq/irq_remove.c | 16 ++- tests/irqtest/irqtest.c | 41 +++++++ 17 files changed, 700 insertions(+), 128 deletions(-) create mode 100644 libc/irq/irq_chain_add.c create mode 100644 libc/irq/irq_chain_remove.c diff --git a/docs/TODO.md b/docs/TODO.md index 8e483aa..3a0c2b6 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -5,26 +5,34 @@ ## Ближайшее -- [ ] **FPS-делитель (frame pacing)** — gfx_set_fps_div(n): логический +Порядок реализации связки: **сначала цепочка irq, потом FPS-делитель +поверх неё** (делитель — просто один слот цепи; так снимается EBUSY +на сосуществование со своим тиком приложения). + +- [x] **(1) irq: цепочка кадровых обработчиков** — СДЕЛАНО 2026-07-15. + Массив `isr_t _irq_chain[4]` + `_irq_chain_n` в W2-data (заменили + `_irq_user`); трамплин `tr_frame` проходит слоты (один тяжёлый сейв + на всю цепь, пустой слот пропускается); API `irq_chain_add` + (0/-1+ENOMEM), `irq_chain_remove(h)`; `irq_install` → обёртка + chain_add (EBUSY исчез), `irq_remove()` рвёт всю цепь; refcount + таблицы на первом/последнем слоте; мутации под IRQ_DISABLE. + All-modes W1-remap: трамплин копируется в W2 (`_irq_tramp_w2buf`, + copy-safe — только jr/djnz + литерал jp 0x0038), вокруг вызова + восстанавливает базовую W1-страницу (`_irq_app_w1_page` = IN 0xA2 + при install). ПРОВЕРЕНО в MAME (tests/irqtest, 2 хендлера): + tiny/big/huge — chain 97=97 → remove h2 → 48/0; **huge = код в W1, + remap работает** (цепочка тикает синхронно). Дизайн: + docs/im2_isr_design.md. + FOLLOW-UP: (а) CTC-трамплин пока требует код в W2 → в small/huge + `irq_ctc_install` = EINVAL (нужна такая же W2-копия _irq_ctc_tramp); + (б) small для мелких программ (BSS в W1) → irq_install EINVAL + (safe; снять резервом area _IM2 в верхней W2). +- [ ] **(2) FPS-делитель (frame pacing)** — gfx_set_fps_div(n): логический кадр = ровно n кадровых интервалов (50/25/16.7 fps), при переполнении слота — выравнивание на ближайший фронт; счётчик - кадров через irq_install (только tiny/big в первой версии). - ПОЛНЫЙ ПЛАН: docs/sprite-api-design.md §9е (обсуждён 2026-07-14). -- [ ] **irq: цепочка обработчиков** (дизайн 2026-07-14, реализация по - потребности — понадобится, когда в программе сойдутся fps-делитель - + свой тик/звук). Вместо одного слота (EBUSY) — фиксированный - массив `isr_t _irq_chain[IRQ_CHAIN_MAX /* 4 */]` в W2-data: - - трамплин вместо SMC-вызова одного handler'а обходит слоты - (load/or a/skip-NULL/call — пустой слот ~40Т, копейки на кадре); - - API: `irq_chain_add(h)` (0 / -1+ENOMEM когда слоты кончились), - `irq_chain_remove(h)` (по указателю; слот → NULL); - - совместимость: `irq_install(h)` становится обёрткой chain_add - (EBUSY исчезает), `irq_remove()` БЕЗ аргумента снимает ВСЮ цепь - и возвращает IM 1 — семантика exit/atexit сохранена; - - порядок вызова = порядок слотов; взаимный порядок хендлеров НЕ - гарантируется после remove/add (первая дыра) — хендлеры обязаны - быть независимы; дисциплина ISR прежняя (irq.h); - - CTC-цепочка (irq_ctc_*) — тем же паттерном, если понадобится. + кадров через irq_chain_add(_gfx_frame_isr) (только tiny/big в + первой версии). ПОЛНЫЙ ПЛАН: docs/sprite-api-design.md §9е + (обсуждён 2026-07-14; ограничение EBUSY снято цепочкой). - [ ] **П6/железо**: MAME-смоук всех тестов после libc-сплита (conio, ptime, stattest, mouse, gfx_demo/gfx_d16/gfx_text/gfx_mous — трогался asm акселератора); затем прогон на реальном Sprinter diff --git a/docs/fast_ram.md b/docs/fast_ram.md index a79b10b..17ce99e 100644 --- a/docs/fast_ram.md +++ b/docs/fast_ram.md @@ -193,3 +193,97 @@ _intWaitVsyncSys Если будем добавлять поддержку — делать это отдельным opt-in механизмом (по аналогии с banked-режимами), с обязательной обёрткой off/restore вокруг всех точек входа в систему. + +--- + +## 8. Разбор двух предложений по спрайтовому движку (обсуждение 2026-07-14) + +Итог двух обсуждений (эмуляция FastRAM в dev-MAME подтверждена +разработчиками MAME как полная). + +### 8.1. Предложение «W0/FastRAM как код-банк `__banked`» + +Идея: выделить 1-2 FastRAM-страницы в crt0, научиться размещать в них +`__banked`-код с целью W0 и контрактом «такой код не зовёт DSS/BIOS»; +критичным функциям указывать размещение в W0/FastRAM. + +**Framing правильный** (переиспользовать трамплин `__banked` + +контракт), но W0-банк — НЕ «ещё один bank id», а тяжелее по трём осям: + +1. **Своп W0 сносит всё окно 0 с RST-векторами** (ESTEX RST10, BIOS + RST8, IM1 RST38, RST0). Пока FastRAM в W0: сисколлы невозможны (это + и есть контракт — ок), но прерывание фатально (`#0038` → мусор + FastRAM) → обязателен `DI` на весь интервал. `DI` бьёт ISR-счётчик + кадров (см. FPS-делитель, [[sprite-engine-perf]]). +2. **Загрузку нельзя сделать как в `crt0_banked`** (там `ESTEX READ` + прямо в окно, строки 165-173): сам READ вектрится через W0. Нужен + 2-шаг — READ в DRAM-буфер, затем `DI` + FastRAM в W0 + **`LD`-копия** + (акселератор с FastRAM не работает!) + выкл. Плюс FastRAM затирается + запуском дочернего DSS-процесса — нужна перезагрузка. +3. **16 КБ окно против 64 КБ**: кросс-страничные вызовы внутри FastRAM + тянут вложенный W0-своп. Вызовы в W1/W2/W3 — свободны (те окна + замаплены). + +Плюс **пер-вызовная такса трамплина** (~40-60Т: DI/toggle/EI/restore) +съедает выигрыш именно на горячих inner-листьях, которые от FastRAM +выигрывают больше всего. Per-function `__banked` — неправильная +гранулярность. + +**Выгода** мала: FastRAM ускоряет только выборку инструкций. Из бюджета +спрайта 19.5К (blit 9.7 + heal 6.4 + тик 3.0) blit и heal — +акселератор/видео-ОЗУ, не выигрывают; выигрывает только тик+сорт по +fetch: ~15-20К/кадр ≈ +1 спрайт (3-4% бюджета в самом узком месте). + +**Рекомендация:** если вообще трогать — модель «горячий остров», а не +«функция-в-банке»: одна 16-КБ FastRAM-страница с самодостаточным +кластером (тик + сортировщик + их листья), вход один раз за кадр через +единственную обёртку `DI / своп-в / вызов / своп-обратно / EI`, внутри +острова обычные `call`, никаких RST. Убирает таксу, сводит связку с +прерываниями к одному DI-интервалу на кадр (мы и так HALT-ждём между +кадрами; ISR-счётчик кадров тикает во время HALT-ожидания ПОСЛЕ EI, а +не во время острова → совместимо с FPS-делителем, пока остров < 1 +кадра). + +### 8.2. Данные в FastRAM вместо кода? + +Вывод неожиданный: **для нашей нагрузки данные — более СЛАБЫЙ рычаг, +чем код.** Wait-state вставляется на КАЖДЫЙ доступ к DRAM, а на Z80 +большинство обращений к памяти — выборка инструкций, не операндов. Тик += IX-индексный код (`ld a,(ix+d)` — DD-префикс: 3 байта fetch + 1 байт +данных) → ¾ штрафа на fetch, ¼ на операнде. Значит `sprite_t` в FastRAM +убирает лишь ¼, а код тика — ¾. + +По данным движка: +- **Пиксели/атласы — жёсткое НЕТ** (их читает акселератор; с FastRAM не + работает → блит откатился бы на LD-циклы). +- **Массив `sprite_t` — не стоит:** тик его трогает, но выигрыш мал (см. + выше), а хуже того — его читают и heal/blit под акселератор, которым + нужна нормальная карта памяти. `sprite_t` в W0-FastRAM → весь + `sprite_update` под `DI` + непроверенный вопрос «работает ли + акселератор при W0=FastRAM». Не кандидат, пока это не подтверждено + артефактом в MAME. +- **Таблица Y-сорта + чисто-CPU скретч — да, но не отдельное решение:** + живут в той же 16-КБ странице «горячего острова» с кодом сортировщика + (сорт не трогает ни акселератор, ни сисколлы). Отдельного рычага + «данные» тут нет. + +**Единственный сильный независимый кейс для данных-в-FastRAM:** +CPU-bound алгоритм с большим LUT случайного доступа (fetch внутреннего +цикла крошечный, чтений таблицы — тьма). У движка такого нет (тяжёлая +графика = акселератор). Но если добавим CPU-side эффект (программный +per-pixel шейдинг, палитровый remap, софт-скейлер) — его LUT станет +главным кандидатом на FastRAM-данные. На будущее. + +**Ограничения именно для FastRAM-данных:** лежат в окне 0 +(#0000-#3FFF) → буфер под BIOS/ESTEX туда нельзя (нужно #4000-#BFFF); +стек остаётся в W2; не переживают дочерний DSS-процесс; заполняются +только `LD`-копией (акселератор мимо). + +### 8.3. Общий вердикт + +Оба варианта реализуемы, но бьют по 3-4% бюджета в самом узком месте +(тик+сорт fetch), мимо доминирующих blit/heal. Остаётся **последним +резервом** ([[fast_ram]] в памяти). Перед любой реализацией — дешёвый +бенч в dev-MAME: один и тот же CPU-loop в DRAM vs FastRAM по +`totalcycles`, чтобы измерить реальный коэффициент wait-state, который +эмулятор моделирует, и подтвердить, что овчинка стоит выделки. diff --git a/docs/im2_isr_design.md b/docs/im2_isr_design.md index 0c31b76..c9f89c1 100644 --- a/docs/im2_isr_design.md +++ b/docs/im2_isr_design.md @@ -397,7 +397,217 @@ User's ISR НЕ должен: - ~~CBL/COVOX prerequisite handler~~ — реализован, см. «Phase 2b» выше - ISA interrupt handler (для ZX-Bus карт) -- Multiple user handler chain (e.g. tick + sound) +- Multiple user handler chain (e.g. tick + sound) — ДИЗАЙН ниже + +## Цепочка кадровых обработчиков (план, 2026-07-14) + +Мотив: сейчас кадровый слот один (`_irq_user`), второй `irq_install` +даёт `EBUSY`. Как только в программе сойдутся ≥2 потребителя кадрового +прерывания (FPS-делитель `gfx_set_fps_div` + свой тик/звук приложения), +одного слота мало. Решение — фиксированный массив слотов, по которому +трамплин проходит на каждом кадре. + +### Модель + +- `isr_t _irq_chain[IRQ_CHAIN_MAX]` (`IRQ_CHAIN_MAX = 4`) + счётчик + занятости `uint8_t _irq_chain_n` — в `_irq_state.c` (W2-data; + ЗАМЕНЯЮТ нынешний `isr_t _irq_user`). +- Порядок вызова = порядок индексов 0..3. Взаимный порядок хендлеров + **НЕ гарантируется** после remove/add (переиспользуется первая дыра) + → хендлеры обязаны быть независимы. Наш `_gfx_frame_isr` (инкремент + байта) независим по построению. +- Дисциплина ISR прежняя (`irq.h`): без ESTEX/BIOS/gfx/банков/акселератора. + +### Трамплин (`_irq_tramp.c`, секция `_irq_frame`) + +Полный сейв теневого+индексного набора делается ОДИН раз (амортизируется +на всю цепь), затем цикл по слотам с пропуском NULL: + +``` +_irq_frame: + push bc/de/hl + ld a, (__irq_chain_n) ; ранний выход: цепь пуста → без сейва + or a, a + jr Z, _irq_pop3 + ... сейв shadow + IX/IY ... + ld hl, #_irq_chain + ld b, #IRQ_CHAIN_MAX +_irq_chain_loop: + ld e,(hl) / inc hl / ld d,(hl) / inc hl ; DE = слот + ld a,d / or a,e / jr Z, _irq_chain_skip ; NULL → пропуск + push bc / push hl + call _irq_call_de ; jp (de)-обёртка (как _irq_call_hl) + pop hl / pop bc +_irq_chain_skip: + djnz _irq_chain_loop + ... restore ... +_irq_pop3: + pop hl/de/bc +_irq_chain: ... jp DSS ... ; как сейчас +``` + +Пустой проход 4 слотов ≈ 4×(load+or+skip) ≈ 40Т — копейки на кадре. +Ранний выход по `_irq_chain_n==0` сохраняет текущую оптимизацию «нет +хендлера → без тяжёлого сейва» (трамплин может быть установлен ради +CBL/CTC при пустой кадровой цепи). + +### API (`irq.h`) + +```c +#define IRQ_CHAIN_MAX 4 +int irq_chain_add(isr_t h); /* 0 / -1+ENOMEM (слоты кончились) */ +void irq_chain_remove(isr_t h); /* снять ОДИН слот (по указателю) */ +``` + +Совместимость (без изменения существующих call-sites): +- `irq_install(h)` → тонкая обёртка `return irq_chain_add(h)`. `EBUSY` + исчезает (двойной install теперь легален); при 4 занятых — `ENOMEM`. +- `irq_remove()` (без аргумента) — снимает **ВСЮ** кадровую цепь и + возвращает IM 1. Семантика exit/atexit сохранена (exit рвёт всё перед + возвратом в DSS). +- `gfx_set_fps_div` использует `irq_chain_add`/`irq_chain_remove` + (ТАРГЕТНО свой `_gfx_frame_isr`), НЕ `irq_remove` — чтобы выключение + делителя не снесло собственный хендлер приложения. + +### Refcount таблицы + +`irq_chain_add`: если `_irq_chain_n == 0` (первый слот) → `_irq_table_ref()` +(ставит IM2). `irq_chain_remove`: если слот был последним (`_irq_chain_n` +уходит в 0) → `_irq_table_unref()` (возврат IM1). CBL/CTC-ссылки на +таблицу считаются отдельно, как сейчас. + +### Гонка чтения слота (ОБЯЗАТЕЛЬНО) + +Трамплин читает слот двумя байтовыми `ld`; запись указателя в +add/remove — 16-бит store. Порванное чтение (низкий байт новый, старший +старый) = прыжок в мусор. Поэтому мутации слота+счётчика в +`irq_chain_add`/`irq_chain_remove` ОБЯЗАНЫ идти под `IRQ_DISABLE()` / +`IRQ_ENABLE()`. Дёшево и обязательно. + +### Файлы (канон 1 модуль = 1 функция) + +- `_irq_state.c` — `_irq_user` → `_irq_chain[IRQ_CHAIN_MAX]` + `_irq_chain_n`. +- `_irq_tramp.c` — переписать секцию `_irq_frame` на цикл (выше). +- `_irq.h` — объявить `_irq_chain`, `_irq_chain_n`, `IRQ_CHAIN_MAX`. +- `irq_chain_add.c`, `irq_chain_remove.c` — НОВЫЕ. +- `irq_install.c` — обёртка над chain_add; `irq_remove.c` — teardown-all + (чистит все слоты + unref до нуля). +- `include/irq.h` — публичные объявления + документировать «порядок не + гарантирован, хендлеры независимы; двойной install легален; 4 слота». +- `tests/irqtest` — добавить кейс двух хендлеров (оба тикают свои + счётчики; remove одного не глушит другого; exit рвёт оба). + +### Ограничения (СНИМАЮТСЯ — см. «все режимы» ниже) + +- ~~Только tiny/big~~ — цель теперь **все режимы**, дизайн ниже. +- CTC-цепочка (`irq_ctc_*`) — тем же паттерном, ОТДЕЛЬНЫЙ массив + (вектор 0x06); делать по потребности, не сейчас. + +## Вариант «работает во всех режимах памяти» (план 2026-07-14) + +Постановка: IM2-прерывания должны работать и в small/huge, где CODE +лежит в W1. Нельзя просто «загнать всё в W2» — обработчик пишет +пользователь как обычный C, и он компилируется в `_CODE` (=W1 в +small/huge). Задача — разложить *путь прерывания* так, чтобы он был +корректен при ЛЮБОМ содержимом W1/W3 в момент прихода прерывания. + +### Что на пути прерывания и что реально требует W2 + +Прерывание фетчит и исполняет по цепочке: **вектор-таблица → трамплин → +обработчик(и) → chain на DSS 0x0038**. В момент прерывания стабильно +замаплены только W0 (ROM/DSS) и **W2** (там стек — DSS это требует, +окно не перемапливается). W1 перемапливается банками кода (big) и самим +DSS при iff=1 во время сисколлов (подтверждено артефактом). W3 — банки +(huge). + +Ревизия по компонентам: +1. **Вектор-таблица** — УЖЕ в W2 (`_irq_vec_buf`, BSS; BSS садится в W2 + во всех режимах). Ничего менять не надо. +2. **Трамплин** (`_irq_tramp`) — сейчас в `_CODE` (=W1 в small/huge). + ЕДИНСТВЕННОЕ, что валит проверку `tramp < 0x8000` в + `_irq_table_ref()`. Нужно сделать W2-резидентным. +3. **Обработчик пользователя** — обычный C в W1/W3. НЕ трогаем — решаем + через remap (см. ниже). +4. **Данные цепи** (`_irq_chain`, `_irq_chain_n`, …) — BSS → W2. ОК. + +### Ключевой приём: не двигать обработчик, а подсунуть ему страницу + +ВАЖНО (уточнение 2026-07-14): в small/huge программа видит **плоские +32 КБ 0x4000–0xBFFF** — две статические страницы (W1+W2 подряд), их +номера фиксированы на всё время работы. Обработчик может лежать ГДЕ +УГОДНО в этом диапазоне: в W1 (0x4000–0x7FFF), в W2 (0x8000–0xBFFF) +или даже НА СТЫКЕ через 0x8000. Значит не нужно знать, в каком окне +хендлер — нужно лишь на время его вызова сделать весь плоский регион +адресуемым. + +Обработчик оставляем на месте. Трамплин (в W2) ПЕРЕД вызовом каждого +хендлера восстанавливает базовую W1-страницу приложения — паттерн +`__banked`-трамплина (bank.s: `in a,(0xA2)` / set / call / restore). +W2 всегда = страница приложения (там стек, DSS не перемапливает), так +что достаточно вернуть только W1 — и весь плоский 0x4000–0xBFFF +корректен. Это работает, потому что: +- в small/huge **базовая W1-страница статична** (пользователь её не + переключает — модель памяти); её номер crt0 фиксирует один раз при + старте (`IN A,(0xA2)`) в W2-переменную `_irq_app_w1_page`; +- если прерывание пришло вне сисколла — W1 уже = базовая, set/restore + это no-op; если ВНУТРИ сисколла (DSS подменил W1) — трамплин ставит + базовую на время хендлера и возвращает DSS-страницу перед chain на + 0x0038, прерванный сисколл продолжается корректно. + +Единый «restore W1=base» покрывает все положения хендлера БЕЗ детекции: +- целиком в W1 → W1=base делает его код валидным; +- целиком в W2 → W1-restore безвреден (хендлер оттуда не фетчит), W2 + и так = приложение; +- на стыке 0x8000 → нижняя часть по W1=base, верхняя по W2=приложение + — обе корректны. + +Итог по режимам: +- **tiny/big**: базовый код/данные в W2 (всегда замаплен) → remap НЕ + нужен и ВРЕДЕН (в tiny/big нет осмысленной «базовой W1»; W1 — + банк-окно в big). Как сейчас. +- **small/huge**: плоские 32 КБ → трамплин ставит `_irq_app_w1_page` + в 0xA2 вокруг вызова. +Флаг «нужен remap W1» = (mode ∈ {small,huge}); crt0/sprinter-cc его +выставляет, трамплин честит. + +**Scope v1:** любые хендлеры в плоском образе приложения (не-banked) — +т.е. обычный C где угодно в 0x4000–0xBFFF; покрывает практически все +ISR (счётчик/звук-тик). `__banked`-хендлер (в W3 huge / отдельном +банке big) потребовал бы хранить его страницу per-слот и мапить в его +окно — отдельное расширение, не сейчас. + +### Как сделать трамплин W2-резидентным — два пути + +**(A) Резерв W2-области (проще, рекомендую для v1).** Трамплин +(и, при желании, таблица) — в отдельной area `_IM2` с абсолютным +адресом в верхней части W2 (напр. под стеком). Область НЕнулевая только +если слинкованы irq-модули (DCE) → цена платится лишь при использовании +IRQ; heap-top при слинкованном IRQ = `s__IM2`. В small/huge W2-часть +статического образа грузится crt0 как есть — код окажется в W2 без +рантайм-копирования. Правка `_irq_table_ref()`: снять проверку +`tramp < 0x8000` (трамплин теперь по построению в W2). + +**(B) Рантайм-эмит стаба в W2-BSS (без резерва области).** Скопировать +PIC-шаблон трамплина в BSS-буфер (W2) при первом install, пропатчив +абсолютные само-ссылки (SMC, как `_irq_dss_target` уже сейчас). Плюс: +никакой зарезервированной области, чисто DCE. Минус: трамплин надо +писать позиционно-независимым (внутренний control-flow только `jr`; +`call (hl)`-идиому инлайнить или патчить адрес хелпера при копии). +Оптимизация поверх (A), если резерв области жмёт бюджет small. + +Ключевой приём (remap базовой страницы) от выбора (A)/(B) не зависит. + +### Изменения к плану цепочки (выше) + +- `_irq_table_ref()`: убрать EINVAL-проверку `tramp/buf < 0x8000` после + переезда трамплина в W2 (таблица уже там). +- crt0 (все варианты): записать `_irq_app_w1_page = IN A,(0xA2)` при + старте (для small/huge; в tiny/big просто не используется). +- Трамплин `_irq_frame`: вокруг `call` хендлера — save/set/restore + 0xA2 под флагом remap; перед `jp 0x0038` вернуть прерванную W1. +- Тест: собрать `tests/irqtest` во ВСЕХ четырёх режимах; критично — + small/huge прогон с активным сисколлом в main (файловый цикл), чтобы + словить прерывание при DSS-подменённой W1. ## Альтернатива: отдельный memory mode "im2" diff --git a/docs/size_baseline.tsv b/docs/size_baseline.tsv index 7fc8065..1dc6a56 100644 --- a/docs/size_baseline.tsv +++ b/docs/size_baseline.tsv @@ -31,7 +31,7 @@ gfx_demo 4103 gfxbanks 6044 hello 4167 hello2 4244 -irqtest 5850 +irqtest 6527 ls 4835 malloc 4461 mdview 18878 @@ -41,7 +41,7 @@ mouse 4382 openenv 6124 palfile 5406 ptime 5744 -rpgprof 11852 +rpgprof 12082 rpgwalk 9501 rt_test 4892 seek 4188 diff --git a/docs/sprite-api-design.md b/docs/sprite-api-design.md index f352ae5..a8fab0e 100644 --- a/docs/sprite-api-design.md +++ b/docs/sprite-api-design.md @@ -900,19 +900,27 @@ Call-sites НЕ меняются: gfx_wait_vsync() один; n<=1 — СТАРЫ (данные; div=0/1 → старый путь; НЕ инициализировать). - common/_gfx_frame_isr.c — ISR: `_gfx_frame_tick++` (правила irq.h: без ESTEX/gfx/банков — здесь чистый инкремент). -- common/gfx_set_fps_div.c — сеттер: n>=2 → irq_install(_gfx_frame_isr) - (однократно; повторные вызовы только меняют div), n<=1 → - irq_remove (если ставили) + div=1. Возврат 0 / -1+errno - (EINVAL/EBUSY от irq_install). ТОЛЬКО этот модуль ссылается на - irq-механику и ISR — DCE: программа без сеттера не тянет ничего - (wait ссылается лишь на data-модуль). +- common/gfx_set_fps_div.c — сеттер: n>=2 → irq_chain_add(_gfx_frame_isr) + (ТАРГЕТНО свой слот; однократно, повторные вызовы только меняют div), + n<=1 → irq_chain_remove(_gfx_frame_isr) (если ставили) + div=1. + ВАЖНО: chain_remove, НЕ irq_remove — выключение делителя не должно + снести собственный хендлер приложения (см. цепочку в + docs/im2_isr_design.md). Возврат 0 / -1+errno (EINVAL — не tiny/big; + ENOMEM — 4 слота заняты). ТОЛЬКО этот модуль ссылается на irq-механику + и ISR — DCE: программа без сеттера не тянет ничего (wait ссылается + лишь на data-модуль). - gfx_wait_vsync.c — ветка `if (_gfx_fps_div >= 2)` перед текущим поллингом. ### Ограничения (задокументировать в gfx.h) 1. irq_install работает ТОЛЬКО в tiny/big; в small/huge вернёт EINVAL - → gfx_set_fps_div(2+) там недоступен (rpgprof — small!). ПРИЧИНА + → gfx_set_fps_div(2+) там недоступен (rpgprof — small!). СНИМАЕТСЯ + вариантом «все режимы» (docs/im2_isr_design.md «Вариант работает во + всех режимах памяти», 2026-07-14): трамплин → W2-резидентный, хендлер + остаётся в W1, трамплин восстанавливает статическую базовую W1-страницу + приложения вокруг вызова (паттерн __banked). Тогда rpgprof (small) + тоже получит делитель. ПРИЧИНА исходного ограничения (см. docs/im2_isr_design.md): IM2-таблица/трамплин/handler обязаны лежать по адресам, валидным в ЛЮБОЙ момент прихода прерывания — стабильно только W2 (стек обязан быть там по требованию DSS, окно @@ -929,12 +937,12 @@ Call-sites НЕ меняются: gfx_wait_vsync() один; n<=1 — СТАРЫ период кадра 896×320, дрейфа нет; фаза не привязана к лучу — разовая синхронизация поллингом фронта при установке). Делать по потребности. -2. Один слот user-ISR: приложение со СВОИМ irq_install получит EBUSY - от сеттера. Обход v1: приложение инкрементирует _gfx_frame_tick из - своего обработчика и выставляет _gfx_fps_div напрямую (внутренние - символы задокументировать); правильное решение — ЦЕПОЧКА - обработчиков в libc/irq (дизайн записан в docs/TODO.md 2026-07-14, - реализация по потребности). +2. ~~Один слот user-ISR (EBUSY)~~ — СНЯТО цепочкой обработчиков: сеттер + зовёт irq_chain_add, приложение со своим хендлером — тоже слот, + оба тикают. Дизайн цепочки: docs/im2_isr_design.md «Цепочка + кадровых обработчиков». Порядок реализации: цепочка → потом + делитель поверх неё. (Осталось лишь: при 4 занятых слотах сеттер + вернёт ENOMEM.) 3. W0-атласы + IM2: прерывание при замапленном атласе идёт через IM2-таблицу приложения (W2) → чейн на 0x0038 = ISR-стаб страницы атласа → DSS. Путь совместим по построению, но ПРОВЕРИТЬ в MAME diff --git a/examples/rpgwalk/rpgprof.c b/examples/rpgwalk/rpgprof.c index 83d2adb..ce46e3f 100644 --- a/examples/rpgwalk/rpgprof.c +++ b/examples/rpgwalk/rpgprof.c @@ -270,6 +270,7 @@ int main(void) if (gfx_pal_fload(0, "bard.pal") < 0) gfx_pal_fload(0, "a:\\bard.pal"); gfx_pal_sync(); + gfx_sprite_ysort(YSORT_Y); getdatetime(&dt); srand((unsigned)(dt.second * 77u + dt.minute)); diff --git a/libc/include/irq.h b/libc/include/irq.h index d63b654..67b24ef 100644 --- a/libc/include/irq.h +++ b/libc/include/irq.h @@ -1,19 +1,26 @@ /* - * irq.h — пользовательский обработчик кадрового прерывания (IM 2). + * irq.h — пользовательские обработчики кадрового прерывания (IM 2). * * Sprinter шлёт вектор 0xFF от трёх источников: кадр (50 Гц), * клавиатура (бит 0 порта 0x19), CBL/COVOX. Штатно DSS работает в - * IM 1 (обработчик 0x0038). irq_install() переключает CPU в IM 2 со - * своей таблицей и зовёт handler ТОЛЬКО на кадровых прерываниях; - * после него (и на всех прочих источниках) управление всегда - * передаётся штатному обработчику DSS на 0x0038 — клавиатура, - * SYSTIME и курсор мыши продолжают работать. irq_remove() - * возвращает IM 1/I как было (и вызывается автоматически из exit). + * IM 1 (обработчик 0x0038). Установка обработчика переключает CPU в + * IM 2 со своей таблицей и зовёт handler'ы ТОЛЬКО на кадровых + * прерываниях; после них (и на всех прочих источниках) управление + * всегда передаётся штатному обработчику DSS на 0x0038 — клавиатура, + * SYSTIME и курсор мыши продолжают работать. irq_remove() возвращает + * IM 1/I как было (и вызывается автоматически из exit). * - * ТРЕБОВАНИЕ ПАМЯТИ: код и данные программы должны быть в W2 - * (0x8000-0xBFFF) — режимы tiny и big. В small/huge (CODE в W1) - * irq_install вернёт -1/EINVAL: во время прерывания W1 может быть - * перемаплен. Дизайн: docs/im2_isr_design.md. + * ЦЕПОЧКА: можно поставить до IRQ_CHAIN_MAX независимых обработчиков + * (напр. свой тик + FPS-делитель gfx_set_fps_div). Порядок вызова = + * порядок слотов; после remove/add взаимный порядок НЕ гарантируется + * (переиспользуется первая дыра) — обработчики обязаны быть независимы. + * + * ПАМЯТЬ: работает во ВСЕХ режимах (tiny/small/big/huge) — трамплин + * при коде в W1 копируется в W2 и восстанавливает базовую W1-страницу + * вокруг вызова (docs/im2_isr_design.md). Единственное исключение: + * в small для очень мелких программ данные могут ещё лежать в W1 — + * тогда установка вернёт -1/EINVAL (перейти на huge или увеличить + * размер). Обработчик может быть обычной C-функцией где угодно. * * Правила для handler'а (вызывается ~50 раз/с, все регистры * сохраняются снаружи): @@ -29,13 +36,25 @@ typedef void (*isr_t)(void); -/* Установить обработчик кадрового прерывания. - * 0 — OK; -1 + errno: EBUSY (уже установлен), EINVAL (код/данные - * не в W2 — неподходящий memory mode). */ +/* Максимум одновременных кадровых обработчиков в цепочке. */ +#define IRQ_CHAIN_MAX 4 + +/* Добавить обработчик кадрового прерывания в цепочку. + * 0 — OK; -1 + errno: EINVAL (handler==NULL или данные не в W2), + * ENOMEM (все IRQ_CHAIN_MAX слотов заняты). */ +int irq_chain_add(isr_t handler); + +/* Снять ОДИН обработчик из цепочки (по указателю). Идемпотентно; + * освобождение последнего слота возвращает IM 1/I штатному DSS. */ +void irq_chain_remove(isr_t handler); + +/* Совместимость: irq_install == irq_chain_add (EBUSY больше не бывает — + * несколько обработчиков легальны; при заполнении слотов — ENOMEM). */ int irq_install(isr_t handler); -/* Снять обработчик и вернуть IM2-таблицу DSS. Безопасно звать - * повторно; вызывается автоматически при exit(). */ +/* Снять ВСЮ цепочку и вернуть IM2-таблицу DSS. Безопасно звать + * повторно; вызывается автоматически при exit(). Для точечного + * снятия — irq_chain_remove. */ void irq_remove(void); /* ---- CTC-таймер (Z84C015, вектор 0x06 — отдельный от кадрового) --- diff --git a/libc/irq/_irq.h b/libc/irq/_irq.h index 36d991b..cb303f3 100644 --- a/libc/irq/_irq.h +++ b/libc/irq/_irq.h @@ -29,12 +29,29 @@ /* Буфер под таблицу: 256 (выравнивание) + 257 (таблица). */ #define _IRQ_VEC_BUF_SIZE (256 + 257) +/* Буфер под W2-копию кадрового трамплина (small/huge — _irq_tramp в + * W1). С запасом над реальным размером трамплина (~150 Б). */ +#define _IRQ_TRAMP_BUF_SIZE 224 + extern uint8_t _irq_vec_buf[_IRQ_VEC_BUF_SIZE]; -extern isr_t _irq_user; /* кадровый handler; NULL = нет */ + +/* Цепочка кадровых обработчиков (см. _irq_state.c, docs/im2_isr_design). */ +extern isr_t _irq_chain[IRQ_CHAIN_MAX]; +extern uint8_t _irq_chain_n; /* число занятых слотов */ + extern isr_t _irq_ctc_user; /* CTC-handler; NULL = нет */ extern uint8_t _irq_old_i; /* I до захвата таблицы */ extern uint8_t _irq_refs; /* счётчик пользователей таблицы */ +/* W1-remap (all-modes): базовая W1-страница приложения + сохранённая + * страница на входе в прерывание; W2-копия трамплина. */ +extern uint8_t _irq_app_w1_page; +extern uint8_t _irq_saved_w1; +extern uint8_t _irq_tramp_w2buf[_IRQ_TRAMP_BUF_SIZE]; + +/* Порт-регистр окна W1 (страница). */ +#define _IRQ_W1_PORT 0xA2 + /* Хук CBL-насоса (libc/cbl регистрирует _cbl_pump). Косвенность * сознательная: прямая ссылка трамплина на cbl-модуль притянула бы * его (и 2 КБ кольца) в каждую irq-программу. NULL = CBL выключен, @@ -61,6 +78,11 @@ uint8_t *_irq_table(void); void _irq_tramp(void); void _irq_ctc_tramp(void); +/* Метка конца кадрового трамплина — для вычисления его размера при + * копировании в W2 (_irq_table_ref). Объявлена как массив, чтобы + * можно было взять адрес без вызова. */ +extern uint8_t _irq_tramp_end[]; + /* SMC-операнд `jp` на обработчик DSS (метка в _irq_tramp.c). */ extern uint16_t _irq_dss_target; diff --git a/libc/irq/_irq_state.c b/libc/irq/_irq_state.c index 6e5b385..2ba0eb1 100644 --- a/libc/irq/_irq_state.c +++ b/libc/irq/_irq_state.c @@ -1,14 +1,43 @@ /* - * _irq_state — данные IM2-модуля: буфер вектор-таблицы, указатели на - * user-handler'ы (кадровый и CTC), сохранённый регистр I, счётчик - * ссылок таблицы. Только данные. + * _irq_state — данные IM2-модуля. Только данные. + * + * Кадровый обработчик теперь ЦЕПОЧКА (_irq_chain[]) вместо одного слота + * (см. docs/im2_isr_design.md «Цепочка кадровых обработчиков»). + * + * Всё, к чему трамплин обращается ВО ВРЕМЯ прерывания, обязано жить в + * W2 (0x8000-0xBFFF) — единственное окно, которое DSS не перемапливает + * (там стек). В tiny/big/huge вся BSS садится в W2 (code-loc W2 либо + * data-loc 0x8000); в small для маленьких программ BSS может оказаться + * в W1 — тогда _irq_table_ref вернёт EINVAL (safe). Полное снятие + * этого ограничения (резерв area _IM2) — отдельный follow-up. */ #include "_irq.h" uint8_t _irq_vec_buf[_IRQ_VEC_BUF_SIZE]; -isr_t _irq_user; -isr_t _irq_ctc_user; -isr_t _irq_cbl_hook; -uint8_t _irq_old_i; -uint8_t _irq_refs; + +/* Цепочка кадровых обработчиков. Пустой слот = NULL. Порядок вызова = + * порядок индексов; после remove/add взаимный порядок НЕ гарантирован + * (переиспользуется первая дыра) — хендлеры обязаны быть независимы. */ +isr_t _irq_chain[IRQ_CHAIN_MAX]; +uint8_t _irq_chain_n; /* число занятых слотов (быстрый выход) */ + +isr_t _irq_ctc_user; /* CTC-handler; NULL = нет */ +isr_t _irq_cbl_hook; /* хук CBL-насоса; NULL = нет */ +uint8_t _irq_old_i; /* I до захвата таблицы */ +uint8_t _irq_refs; /* счётчик пользователей таблицы */ + +/* --- W1-remap для all-modes (см. трамплин) --------------------------- + * В small/huge базовый код приложения в W1 (статическая страница). + * Трамплин (в W2) вокруг вызова хендлеров восстанавливает эту страницу + * в W1, чтобы хендлер, лежащий где угодно в плоском 0x4000-0xBFFF, был + * адресуем; на выходе возвращает прочитанное на входе (что было — + * возможно DSS-страница, если прервали сисколл). */ +uint8_t _irq_app_w1_page; /* базовая W1-страница (IN 0xA2 при install) */ +uint8_t _irq_saved_w1; /* W1 на входе в прерывание (для restore) */ + +/* W2-копия трамплина: в small/huge сам _irq_tramp лежит в W1-коде и в + * момент прерывания может быть перемаплен — _irq_table_ref копирует его + * байты сюда (BSS=W2) и направляет вектор на копию. В tiny/big копия не + * нужна (трамплин уже в W2), буфер не используется. */ +uint8_t _irq_tramp_w2buf[_IRQ_TRAMP_BUF_SIZE]; diff --git a/libc/irq/_irq_table.c b/libc/irq/_irq_table.c index 2f13be1..2f27b79 100644 --- a/libc/irq/_irq_table.c +++ b/libc/irq/_irq_table.c @@ -1,10 +1,19 @@ /* * _irq_table — жизненный цикл общей IM2-таблицы (счётчик ссылок). * - * Первый ref: проверка W2-адресов, построение таблицы (заливка H + - * jp _irq_tramp по смещению H), I := H, IM 2, atexit-уборка. + * Первый ref: проверка W2-адреса БУФЕРА (данные обязаны быть в W2), + * запись базовой W1-страницы приложения, W2-размещение кадрового + * трамплина (прямо или копией — см. ниже), построение таблицы (заливка + * H + jp по смещению H), I := H, IM 2, atexit-уборка. * Последний unref: I := старое, IM 1 (штатный режим DSS — его * обработчик на 0x0038; см. docs/samples/sprinterIntLib.asm). + * + * All-modes (docs/im2_isr_design.md): вектор-таблица и данные цепи + * должны быть в W2 (это BSS — W2 в tiny/big/huge; в small для мелких + * программ может уехать в W1 → EINVAL, safe). Код трамплина в small/ + * huge лежит в W1 и в момент прерывания может быть перемаплен — если + * &_irq_tramp < 0x8000, копируем его байты в _irq_tramp_w2buf (BSS=W2) + * и направляем вектор туда. Трамплин написан copy-safe (см. его шапку). */ #include @@ -21,6 +30,18 @@ static uint8_t irq_get_i(void) __naked __endasm; } +/* Прочитать порт-регистр окна W1 (0xA2) — текущая физическая страница. + * В main-контексте (install) это статическая базовая страница + * приложения (small/huge); в tiny/big значение не используется по + * назначению, но remap безвреден. */ +static uint8_t irq_read_w1_page(void) __naked +{ + __asm + in a, (#_IRQ_W1_PORT) + ret + __endasm; +} + /* I := A, режим IM 2. */ static void irq_set_i_im2(uint8_t i_val) __naked { @@ -65,17 +86,28 @@ int _irq_table_ref(void) return 0; } - /* Трамплины и таблица обязаны жить в W2 (0x8000-0xBFFF): во время - * прерывания W1/W3 могут быть перемаплены. CODE вне W2 = - * неподходящий memory mode (small/huge). */ - uint16_t tramp = (uint16_t)&_irq_tramp; - uint16_t buf = (uint16_t)_irq_vec_buf; - if (tramp < 0x8000 || tramp >= 0xC000 || - buf < 0x8000 || buf + _IRQ_VEC_BUF_SIZE > 0xC000) { + /* Вектор-таблица и данные цепи обязаны жить в W2 (0x8000-0xBFFF): + * во время прерывания W1/W3 могут быть перемаплены. Это BSS — W2 + * во всех режимах, КРОМЕ small для мелких программ (данные ещё в + * W1) → EINVAL, safe. Код трамплина в W1 обрабатывается копией. */ + uint16_t buf = (uint16_t)_irq_vec_buf; + if (buf < 0x8000 || buf + _IRQ_VEC_BUF_SIZE > 0xC000) { errno = EINVAL; return -1; } + /* Базовая W1-страница приложения — для W1-remap в трамплине. */ + _irq_app_w1_page = irq_read_w1_page(); + + /* W2-адрес кадрового трамплина: если оригинал уже в W2 (tiny/big) — + * прямо; иначе (small/huge, код в W1) — копия в W2-буфер. */ + uint16_t tramp = (uint16_t)&_irq_tramp; + if (tramp < 0x8000) { + uint16_t n = (uint16_t)_irq_tramp_end - tramp; + memcpy(_irq_tramp_w2buf, (const void *)tramp, n); + tramp = (uint16_t)_irq_tramp_w2buf; + } + uint8_t *table = _irq_table(); uint8_t h = (uint8_t)((uint16_t)table >> 8); @@ -84,11 +116,10 @@ int _irq_table_ref(void) /* Старое I — только чтобы вернуть регистр при unref: DSS работает * в IM 1 (I=0x3F — наследие Spectrum ROM, НЕ таблица). */ _irq_old_i = irq_get_i(); - _irq_dss_target = 0x0038; memset(table, h, 257); uint8_t *stub = table + h; /* h < 0xC0 — с [0xFF] не пересекается */ - stub[0] = 0xC3; /* jp _irq_tramp */ + stub[0] = 0xC3; /* jp */ stub[1] = (uint8_t)(tramp & 0xFF); stub[2] = (uint8_t)(tramp >> 8); diff --git a/libc/irq/_irq_tramp.c b/libc/irq/_irq_tramp.c index f6b463b..4f8b9ca 100644 --- a/libc/irq/_irq_tramp.c +++ b/libc/irq/_irq_tramp.c @@ -1,23 +1,31 @@ /* - * _irq_tramp — вход IM2-прерывания (вектор 0xFF: кадр/клавиатура/CBL). + * _irq_tramp — вход IM2-прерывания (вектор 0xFF: кадр/клавиатура/CBL), + * версия с ЦЕПОЧКОЙ кадровых обработчиков и W1-remap для all-modes. * - * До выяснения источника трогаем только AF: - * - клавиатура (бит 0 порта 0x19 = SIO-A RR0 «Rx Character - * Available») уходит напрямую в DSS; - * - CBL (бит 7 порта 0xFE) проверяется ТОЛЬКО при зарегистрированном - * хуке _irq_cbl_hook: при выключенном CBL бит подтянут к 1 (MAME - * kbd_fe_r: data |= 0xE0) и проверка ложно срабатывала бы на - * каждом кадре. CBL-путь: полный сейв → насос → restore → - * EI/RETI, к DSS НЕ чейнится (личное прерывание); - * - кадровое: полный сейв → user-handler → restore → чейн. + * Разбор источника (трогаем только AF, пока не выяснили): + * - клавиатура (бит 0 порта 0x19 = SIO-A RR0) → сразу к DSS, БЕЗ + * remap (W1 не трогаем — прерванный контекст сохранён как есть); + * - CBL (бит 7 порта 0xFE) — только при зарегистрированном хуке + * _irq_cbl_hook (иначе бит подтянут к 1); личное прерывание, RETI + * без чейна к DSS; + * - кадровое: проход по _irq_chain[] — все ненулевые слоты по порядку. * - * Чейн — ВСЕГДА jp на обработчик DSS 0x0038 (DSS работает в IM 1; - * jp с interrupted-PC на стеке имитирует RST 38) — он делает свой - * save/restore и завершает EI/RETI: клавиатурный FIFO, SYSTIME и - * курсор мыши живут. + * W1-remap (docs/im2_isr_design.md «Вариант работает во всех режимах»): + * в small/huge базовый код приложения (и хендлеры) в W1, а сам трамплин + * во время прерывания исполняется из W2-КОПИИ (_irq_table_ref копирует + * этот код в _irq_tramp_w2buf, т.к. оригинал в W1 может быть перемаплен + * DSS). Перед вызовом хендлеров копия сохраняет текущую W1-страницу и + * ставит базовую страницу приложения (_irq_app_w1_page), после — + * возвращает сохранённую (перед chain к DSS / перед RETI). Единый + * «restore W1=base» покрывает хендлер в W1, в W2 и на стыке 0x8000. + * В tiny/big remap безвреден (хендлеры в W2, W1 всё равно восстановлен). * - * Операнд финального jp (_irq_dss_target) — SMC, патчится из - * _irq_table_ref; модуль в _CODE (RAM на Sprinter, W2 в tiny/big). + * COPY-SAFE: все внутренние переходы — jr/djnz (относительные); + * единственный call — к _irq_call_hl (исполняется при W1=base, значит + * оригинал в W1 достижим); оба jp — литерал 0x0038 (W0, всегда + * замаплен). Никаких абсолютных само-ссылок → байты копируются как + * есть, без релокации. ВНИМАНИЕ: не добавлять `jp <метка>` внутрь + * трамплина — сломает копию. */ #include "_irq.h" @@ -25,25 +33,35 @@ void _irq_tramp(void) __naked { __asm - push af - in a, (#0x19) - rrca ; бит 0 — клавиатурный байт принят? - jr C, _irq_chain + push af ; сохранить прерванный AF + in a, (#_IRQ_SIO_A) + rrca ; бит 0 — клавиатурный байт принят? + jr NC, tr_notkbd - ;; --- CBL? --- старший байт хука ненулевой у любого - ;; W2-адреса — для проверки «хук есть» хватает одного байта. + ;; --- клавиатура: прямо к DSS, без remap --- + pop af + .db 0xC3 ; jp nn + __irq_dss_target:: + .dw 0x0038 ; SMC-операбельно, но по умолчанию 0x0038 + + tr_notkbd: + ;; --- CBL? --- старший байт хука ненулевой у любого W2-адреса. ld a, (__irq_cbl_hook + 1) or a, a - jr Z, _irq_frame + jr Z, tr_frame in a, (#0xFE) - rlca ; бит 7 — запрос блока CBL? - jr NC, _irq_frame + rlca ; бит 7 — запрос блока CBL? + jr NC, tr_frame - ;; --- CBL: сейв, насос, restore, EI/RETI (без DSS) --- + ;; --- CBL: remap-скобка + хук + приватный RETI --- + in a, (#_IRQ_W1_PORT) ; сохранить прерванную W1 + ld (__irq_saved_w1), a + ld a, (__irq_app_w1_page) + out (#_IRQ_W1_PORT), a ; W1 = базовая страница приложения push bc push de push hl - .db 0x08 ; ex af,af (теневой AF) + .db 0x08 ; ex af,af (теневой AF) push af exx push bc @@ -60,26 +78,31 @@ void _irq_tramp(void) __naked pop bc exx pop af - .db 0x08 ; ex af,af (теневой AF) + .db 0x08 ; ex af,af pop hl pop de pop bc + ld a, (__irq_saved_w1) ; вернуть прерванную W1 + out (#_IRQ_W1_PORT), a pop af ei reti - _irq_frame: - ;; --- кадровое прерывание --- + tr_frame: + ;; --- кадровое: цепочка обработчиков --- + ld a, (__irq_chain_n) + or a, a + jr Z, tr_frame_exit ; нет хендлеров → к DSS без remap/сейва + + in a, (#_IRQ_W1_PORT) ; remap-скобка: сохранить + база + ld (__irq_saved_w1), a + ld a, (__irq_app_w1_page) + out (#_IRQ_W1_PORT), a + ;; полный сейв обоих наборов + индексные (хендлеры клобберят всё) push bc push de push hl - ld hl, (__irq_user) - ld a, h - or a, l - jr Z, _irq_pop3 ; handler не установлен - - ;; Сохранить теневой набор и индексные — user-код клобберит всё. - .db 0x08 ; ex af,af (теневой AF) + .db 0x08 push af exx push bc @@ -88,8 +111,25 @@ void _irq_tramp(void) __naked push ix push iy - ld hl, (__irq_user) + ;; проход по слотам: HL идёт по _irq_chain, B = счётчик слотов + ld hl, #__irq_chain + ld b, #IRQ_CHAIN_MAX + tr_loop: + ld e, (hl) + inc hl + ld d, (hl) + inc hl + ld a, d + or a, e + jr Z, tr_skip ; пустой слот + push bc ; сохранить счётчик + указатель цепи + push hl + ex de, hl ; HL = адрес хендлера call _irq_call_hl + pop hl + pop bc + tr_skip: + djnz tr_loop pop iy pop ix @@ -98,19 +138,19 @@ void _irq_tramp(void) __naked pop bc exx pop af - .db 0x08 ; ex af,af (теневой AF) - _irq_pop3: + .db 0x08 pop hl pop de pop bc - _irq_chain: + ld a, (__irq_saved_w1) ; вернуть прерванную W1 перед chain + out (#_IRQ_W1_PORT), a + tr_frame_exit: pop af - ;; Всегда к DSS (IM 1, обработчик 0x0038) — EI/RETI за ним. - .db 0xC3 ; jp nn - __irq_dss_target:: - .dw 0x0038 ; SMC: подтверждается в _irq_table_ref + .db 0xC3 ; jp 0x0038 (литерал — copy-safe) + .dw 0x0038 _irq_call_hl: jp (hl) + __irq_tramp_end:: __endasm; } diff --git a/libc/irq/irq_chain_add.c b/libc/irq/irq_chain_add.c new file mode 100644 index 0000000..75ecc39 --- /dev/null +++ b/libc/irq/irq_chain_add.c @@ -0,0 +1,36 @@ +/* + * irq_chain_add — добавить обработчик кадрового прерывания в цепочку. + * Возврат 0 / -1+errno: EINVAL (handler==NULL, или данные не в W2 — + * из _irq_table_ref), ENOMEM (все IRQ_CHAIN_MAX слотов заняты). + * + * Первый слот захватывает IM2-таблицу (_irq_table_ref). Порядок вызова + * хендлеров = порядок индексов; после remove/add взаимный порядок НЕ + * гарантирован — хендлеры обязаны быть независимы (см. irq.h). + */ + +#include +#include "_irq.h" + +int irq_chain_add(isr_t handler) +{ + if (!handler) { errno = EINVAL; return -1; } + + uint8_t i; + for (i = 0; i < IRQ_CHAIN_MAX; i++) + if (!_irq_chain[i]) break; + if (i == IRQ_CHAIN_MAX) { errno = ENOMEM; return -1; } + + /* Первый обработчик — захватить таблицу и включить IM2. Делаем ДО + * записи слота: при неудаче (EINVAL в small) состояние не тронуто. + * Пока слот не выставлен, chain_n==0 → трамплин на кадре просто + * чейнит к DSS. */ + if (_irq_chain_n == 0) + if (_irq_table_ref() != 0) + return -1; + + IRQ_DISABLE(); + _irq_chain[i] = handler; + _irq_chain_n++; + IRQ_ENABLE(); + return 0; +} diff --git a/libc/irq/irq_chain_remove.c b/libc/irq/irq_chain_remove.c new file mode 100644 index 0000000..f43d3b6 --- /dev/null +++ b/libc/irq/irq_chain_remove.c @@ -0,0 +1,25 @@ +/* + * irq_chain_remove — снять ОДИН обработчик кадрового прерывания из + * цепочки (по указателю). Освобождение последнего слота отпускает + * IM2-таблицу (_irq_table_unref → возврат IM 1, если больше никто не + * держит). Идемпотентно: неизвестный/NULL handler игнорируется. + */ + +#include "_irq.h" + +void irq_chain_remove(isr_t handler) +{ + if (!handler) return; + + for (uint8_t i = 0; i < IRQ_CHAIN_MAX; i++) { + if (_irq_chain[i] == handler) { + IRQ_DISABLE(); + _irq_chain[i] = 0; + _irq_chain_n--; + IRQ_ENABLE(); + if (_irq_chain_n == 0) + _irq_table_unref(); + return; + } + } +} diff --git a/libc/irq/irq_ctc_install.c b/libc/irq/irq_ctc_install.c index 1d375eb..9299d0d 100644 --- a/libc/irq/irq_ctc_install.c +++ b/libc/irq/irq_ctc_install.c @@ -42,6 +42,10 @@ int irq_ctc_install(isr_t handler, uint8_t div2, uint8_t div3) { if (!handler) { errno = EINVAL; return -1; } if (_irq_ctc_user) { errno = EBUSY; return -1; } + /* CTC-трамплин вектрится напрямую (table[0x06] = &_irq_ctc_tramp) в + * обход W2-копии кадрового пути — поэтому CTC пока требует код в W2 + * (tiny/big). All-modes для CTC (копия _irq_ctc_tramp) — follow-up. */ + if ((uint16_t)&_irq_ctc_tramp < 0x8000) { errno = EINVAL; return -1; } if (_irq_table_ref() != 0) return -1; uint16_t tramp = (uint16_t)&_irq_ctc_tramp; diff --git a/libc/irq/irq_install.c b/libc/irq/irq_install.c index f9dc491..52c5aae 100644 --- a/libc/irq/irq_install.c +++ b/libc/irq/irq_install.c @@ -1,17 +1,13 @@ /* - * irq_install — установить user-handler кадрового прерывания. - * Захватывает общую IM2-таблицу (_irq_table_ref) и включает кадровый - * путь трамплина. Детали и результаты отладки: docs/im2_isr_design.md. + * irq_install — обёртка над цепочкой (обратная совместимость). + * Теперь просто добавляет обработчик в цепь: EBUSY больше не бывает + * (несколько хендлеров легальны), при 4 занятых слотах — ENOMEM. + * Возврат/errno — как у irq_chain_add. */ -#include #include "_irq.h" int irq_install(isr_t handler) { - if (!handler) { errno = EINVAL; return -1; } - if (_irq_user) { errno = EBUSY; return -1; } - if (_irq_table_ref() != 0) return -1; - _irq_user = handler; - return 0; + return irq_chain_add(handler); } diff --git a/libc/irq/irq_remove.c b/libc/irq/irq_remove.c index 034a6ac..e62a23d 100644 --- a/libc/irq/irq_remove.c +++ b/libc/irq/irq_remove.c @@ -1,16 +1,24 @@ /* - * irq_remove — снять кадровый user-handler и отпустить IM2-таблицу + * irq_remove — снять ВСЮ кадровую цепочку и отпустить IM2-таблицу * (последний пользователь возвращает I/IM 1 — штатный режим DSS). - * Идемпотентно; вызывается и из atexit-уборки. + * Идемпотентно; вызывается и из atexit-уборки, поэтому обязано снимать + * всё разом (exit не знает про отдельные слоты). Для точечного снятия + * одного обработчика — irq_chain_remove. */ #include "_irq.h" void irq_remove(void) { - if (!_irq_user) return; + if (_irq_chain_n == 0) return; + IRQ_DISABLE(); - _irq_user = 0; + for (uint8_t i = 0; i < IRQ_CHAIN_MAX; i++) + _irq_chain[i] = 0; + _irq_chain_n = 0; IRQ_ENABLE(); + + /* Цепь держала РОВНО одну ссылку на таблицу (взята при первом + * слоте) — отпускаем её один раз. */ _irq_table_unref(); } diff --git a/tests/irqtest/irqtest.c b/tests/irqtest/irqtest.c index aa3e237..7a8e453 100644 --- a/tests/irqtest/irqtest.c +++ b/tests/irqtest/irqtest.c @@ -3,6 +3,8 @@ * * Проверяет: * 1. кадровый handler тикает ~50 Гц (сверка с time() за 3 секунды); + * 1b. цепочка: второй обработчик (irq_chain_add) тикает синхронно с + * первым; irq_chain_remove снимает только его, первый жив; * 2. клавиатура/SYSTIME живы при установленном handler'е (чейн к DSS); * 3. CTC-таймер (вектор 0x06): пресет vsync ~49 Гц ОДНОВРЕМЕННО с * кадровым; затем быстрый режим 875 кГц/(50*50) = 350 Гц; @@ -17,6 +19,7 @@ #include static volatile uint16_t ticks; +static volatile uint16_t ticks2; static volatile uint16_t ctc_ticks; static void on_tick(void) @@ -24,6 +27,11 @@ static void on_tick(void) ticks++; } +static void on_tick2(void) +{ + ticks2++; +} + static void on_ctc(void) { ctc_ticks++; @@ -86,6 +94,39 @@ int main(void) printf("ticks: %u in 3 s (~%u Hz, expect ~50)\n", got, got / 3); + /* --- Phase 1b: цепочка — второй кадровый обработчик --- */ + if (irq_chain_add(on_tick2) != 0) { + printf("chain_add failed: errno=%d\n", errno); + return 1; + } + puts("chain: 2nd handler added"); + { + IRQ_DISABLE(); + uint16_t a0 = ticks, b0 = ticks2; + IRQ_ENABLE(); + time_t tc = time(0); + while (time(0) < tc + 2) ; + IRQ_DISABLE(); + uint16_t da = ticks - a0, db = ticks2 - b0; + IRQ_ENABLE(); + printf("chain both: h1=%u h2=%u in 2 s (both ~100, must match)\n", + da, db); + } + /* Снять ТОЛЬКО второй — первый обязан продолжать тикать. */ + irq_chain_remove(on_tick2); + { + IRQ_DISABLE(); + uint16_t a0 = ticks, b0 = ticks2; + IRQ_ENABLE(); + time_t tc = time(0); + while (time(0) < tc + 1) ; + IRQ_DISABLE(); + uint16_t da = ticks - a0, db = ticks2 - b0; + IRQ_ENABLE(); + printf("chain after remove h2: h1=%u (>0) h2=%u (must be 0)\n", + da, db); + } + /* --- Phase 2: CTC-таймер одновременно с кадровым --- */ if (irq_ctc_install(on_ctc, IRQ_CTC_VSYNC_DIV2, IRQ_CTC_VSYNC_DIV3)) { printf("irq_ctc_install failed: errno=%d\n", errno);