libc/irq: цепочка кадровых обработчиков + all-modes W1-remap
_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 <noreply@anthropic.com>
This commit is contained in:
+211
-1
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user