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:
2026-07-15 10:23:39 +03:00
parent 6b6986d9b6
commit 2c6f4e33c3
17 changed files with 700 additions and 128 deletions
+26 -18
View File
@@ -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), при кадр = ровно n кадровых интервалов (50/25/16.7 fps), при
переполнении слота — выравнивание на ближайший фронт; счётчик переполнении слота — выравнивание на ближайший фронт; счётчик
кадров через irq_install (только tiny/big в первой версии). кадров через irq_chain_add(_gfx_frame_isr) (только tiny/big в
ПОЛНЫЙ ПЛАН: docs/sprite-api-design.md §9е (обсуждён 2026-07-14). первой версии). ПОЛНЫЙ ПЛАН: docs/sprite-api-design.md §9е
- [ ] **irq: цепочка обработчиков** (дизайн 2026-07-14, реализация по (обсуждён 2026-07-14; ограничение EBUSY снято цепочкой).
потребности — понадобится, когда в программе сойдутся 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_*) — тем же паттерном, если понадобится.
- [ ] **П6/железо**: MAME-смоук всех тестов после libc-сплита (conio, - [ ] **П6/железо**: MAME-смоук всех тестов после libc-сплита (conio,
ptime, stattest, mouse, gfx_demo/gfx_d16/gfx_text/gfx_mous — ptime, stattest, mouse, gfx_demo/gfx_d16/gfx_text/gfx_mous —
трогался asm акселератора); затем прогон на реальном Sprinter трогался asm акселератора); затем прогон на реальном Sprinter
+94
View File
@@ -193,3 +193,97 @@ _intWaitVsyncSys
Если будем добавлять поддержку — делать это отдельным opt-in механизмом Если будем добавлять поддержку — делать это отдельным opt-in механизмом
(по аналогии с banked-режимами), с обязательной обёрткой off/restore вокруг всех (по аналогии с 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, который
эмулятор моделирует, и подтвердить, что овчинка стоит выделки.
+211 -1
View File
@@ -397,7 +397,217 @@ User's ISR НЕ должен:
- ~~CBL/COVOX prerequisite handler~~ — реализован, см. «Phase 2b» выше - ~~CBL/COVOX prerequisite handler~~ — реализован, см. «Phase 2b» выше
- ISA interrupt handler (для ZX-Bus карт) - 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 КБ 0x40000xBFFF** — две статические страницы (W1+W2 подряд), их
номера фиксированы на всё время работы. Обработчик может лежать ГДЕ
УГОДНО в этом диапазоне: в W1 (0x40000x7FFF), в W2 (0x80000xBFFF)
или даже НА СТЫКЕ через 0x8000. Значит не нужно знать, в каком окне
хендлер — нужно лишь на время его вызова сделать весь плоский регион
адресуемым.
Обработчик оставляем на месте. Трамплин (в W2) ПЕРЕД вызовом каждого
хендлера восстанавливает базовую W1-страницу приложения — паттерн
`__banked`-трамплина (bank.s: `in a,(0xA2)` / set / call / restore).
W2 всегда = страница приложения (там стек, DSS не перемапливает), так
что достаточно вернуть только W1 — и весь плоский 0x40000xBFFF
корректен. Это работает, потому что:
- в 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" ## Альтернатива: отдельный memory mode "im2"
+2 -2
View File
@@ -31,7 +31,7 @@ gfx_demo 4103
gfxbanks 6044 gfxbanks 6044
hello 4167 hello 4167
hello2 4244 hello2 4244
irqtest 5850 irqtest 6527
ls 4835 ls 4835
malloc 4461 malloc 4461
mdview 18878 mdview 18878
@@ -41,7 +41,7 @@ mouse 4382
openenv 6124 openenv 6124
palfile 5406 palfile 5406
ptime 5744 ptime 5744
rpgprof 11852 rpgprof 12082
rpgwalk 9501 rpgwalk 9501
rt_test 4892 rt_test 4892
seek 4188 seek 4188
1 # Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
31 gfxbanks
32 hello
33 hello2
34 irqtest
35 ls
36 malloc
37 mdview
41 openenv
42 palfile
43 ptime
44 rpgprof
45 rpgwalk
46 rt_test
47 seek
+21 -13
View File
@@ -900,19 +900,27 @@ Call-sites НЕ меняются: gfx_wait_vsync() один; n<=1 — СТАРЫ
(данные; div=0/1 → старый путь; НЕ инициализировать). (данные; div=0/1 → старый путь; НЕ инициализировать).
- common/_gfx_frame_isr.c — ISR: `_gfx_frame_tick++` (правила irq.h: - common/_gfx_frame_isr.c — ISR: `_gfx_frame_tick++` (правила irq.h:
без ESTEX/gfx/банков — здесь чистый инкремент). без ESTEX/gfx/банков — здесь чистый инкремент).
- common/gfx_set_fps_div.c — сеттер: n>=2 → irq_install(_gfx_frame_isr) - common/gfx_set_fps_div.c — сеттер: n>=2 → irq_chain_add(_gfx_frame_isr)
(однократно; повторные вызовы только меняют div), n<=1 → (ТАРГЕТНО свой слот; однократно, повторные вызовы только меняют div),
irq_remove (если ставили) + div=1. Возврат 0 / -1+errno n<=1 → irq_chain_remove(_gfx_frame_isr) (если ставили) + div=1.
(EINVAL/EBUSY от irq_install). ТОЛЬКО этот модуль ссылается на ВАЖНО: chain_remove, НЕ irq_remove — выключение делителя не должно
irq-механику и ISR — DCE: программа без сеттера не тянет ничего снести собственный хендлер приложения (см. цепочку в
(wait ссылается лишь на data-модуль). 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_wait_vsync.c — ветка `if (_gfx_fps_div >= 2)` перед текущим
поллингом. поллингом.
### Ограничения (задокументировать в gfx.h) ### Ограничения (задокументировать в gfx.h)
1. irq_install работает ТОЛЬКО в tiny/big; в small/huge вернёт EINVAL 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 обязаны (см. docs/im2_isr_design.md): IM2-таблица/трамплин/handler обязаны
лежать по адресам, валидным в ЛЮБОЙ момент прихода прерывания — лежать по адресам, валидным в ЛЮБОЙ момент прихода прерывания —
стабильно только W2 (стек обязан быть там по требованию DSS, окно стабильно только W2 (стек обязан быть там по требованию DSS, окно
@@ -929,12 +937,12 @@ Call-sites НЕ меняются: gfx_wait_vsync() один; n<=1 — СТАРЫ
период кадра 896×320, дрейфа нет; фаза не привязана к лучу — разовая период кадра 896×320, дрейфа нет; фаза не привязана к лучу — разовая
синхронизация поллингом фронта при установке). Делать по синхронизация поллингом фронта при установке). Делать по
потребности. потребности.
2. Один слот user-ISR: приложение со СВОИМ irq_install получит EBUSY 2. ~~Один слот user-ISR (EBUSY)~~ — СНЯТО цепочкой обработчиков: сеттер
от сеттера. Обход v1: приложение инкрементирует _gfx_frame_tick из зовёт irq_chain_add, приложение со своим хендлером — тоже слот,
своего обработчика и выставляет _gfx_fps_div напрямую (внутренние оба тикают. Дизайн цепочки: docs/im2_isr_design.md «Цепочка
символы задокументировать); правильное решение — ЦЕПОЧКА кадровых обработчиков». Порядок реализации: цепочка → потом
обработчиков в libc/irq (дизайн записан в docs/TODO.md 2026-07-14, делитель поверх неё. (Осталось лишь: при 4 занятых слотах сеттер
реализация по потребности). вернёт ENOMEM.)
3. W0-атласы + IM2: прерывание при замапленном атласе идёт через 3. W0-атласы + IM2: прерывание при замапленном атласе идёт через
IM2-таблицу приложения (W2) → чейн на 0x0038 = ISR-стаб страницы IM2-таблицу приложения (W2) → чейн на 0x0038 = ISR-стаб страницы
атласа → DSS. Путь совместим по построению, но ПРОВЕРИТЬ в MAME атласа → DSS. Путь совместим по построению, но ПРОВЕРИТЬ в MAME
+1
View File
@@ -270,6 +270,7 @@ int main(void)
if (gfx_pal_fload(0, "bard.pal") < 0) if (gfx_pal_fload(0, "bard.pal") < 0)
gfx_pal_fload(0, "a:\\bard.pal"); gfx_pal_fload(0, "a:\\bard.pal");
gfx_pal_sync(); gfx_pal_sync();
gfx_sprite_ysort(YSORT_Y);
getdatetime(&dt); getdatetime(&dt);
srand((unsigned)(dt.second * 77u + dt.minute)); srand((unsigned)(dt.second * 77u + dt.minute));
+35 -16
View File
@@ -1,19 +1,26 @@
/* /*
* irq.h — пользовательский обработчик кадрового прерывания (IM 2). * irq.h — пользовательские обработчики кадрового прерывания (IM 2).
* *
* Sprinter шлёт вектор 0xFF от трёх источников: кадр (50 Гц), * Sprinter шлёт вектор 0xFF от трёх источников: кадр (50 Гц),
* клавиатура (бит 0 порта 0x19), CBL/COVOX. Штатно DSS работает в * клавиатура (бит 0 порта 0x19), CBL/COVOX. Штатно DSS работает в
* IM 1 (обработчик 0x0038). irq_install() переключает CPU в IM 2 со * IM 1 (обработчик 0x0038). Установка обработчика переключает CPU в
* своей таблицей и зовёт handler ТОЛЬКО на кадровых прерываниях; * IM 2 со своей таблицей и зовёт handler ТОЛЬКО на кадровых
* после него (и на всех прочих источниках) управление всегда * прерываниях; после них (и на всех прочих источниках) управление
* передаётся штатному обработчику DSS на 0x0038 — клавиатура, * всегда передаётся штатному обработчику DSS на 0x0038 — клавиатура,
* SYSTIME и курсор мыши продолжают работать. irq_remove() * SYSTIME и курсор мыши продолжают работать. irq_remove() возвращает
* возвращает IM 1/I как было (и вызывается автоматически из exit). * IM 1/I как было (и вызывается автоматически из exit).
* *
* ТРЕБОВАНИЕ ПАМЯТИ: код и данные программы должны быть в W2 * ЦЕПОЧКА: можно поставить до IRQ_CHAIN_MAX независимых обработчиков
* (0x8000-0xBFFF) — режимы tiny и big. В small/huge (CODE в W1) * (напр. свой тик + FPS-делитель gfx_set_fps_div). Порядок вызова =
* irq_install вернёт -1/EINVAL: во время прерывания W1 может быть * порядок слотов; после remove/add взаимный порядок НЕ гарантируется
* перемаплен. Дизайн: docs/im2_isr_design.md. * (переиспользуется первая дыра) — обработчики обязаны быть независимы.
*
* ПАМЯТЬ: работает во ВСЕХ режимах (tiny/small/big/huge) — трамплин
* при коде в W1 копируется в W2 и восстанавливает базовую W1-страницу
* вокруг вызова (docs/im2_isr_design.md). Единственное исключение:
* в small для очень мелких программ данные могут ещё лежать в W1 —
* тогда установка вернёт -1/EINVAL (перейти на huge или увеличить
* размер). Обработчик может быть обычной C-функцией где угодно.
* *
* Правила для handler'а (вызывается ~50 раз/с, все регистры * Правила для handler'а (вызывается ~50 раз/с, все регистры
* сохраняются снаружи): * сохраняются снаружи):
@@ -29,13 +36,25 @@
typedef void (*isr_t)(void); typedef void (*isr_t)(void);
/* Установить обработчик кадрового прерывания. /* Максимум одновременных кадровых обработчиков в цепочке. */
* 0 — OK; -1 + errno: EBUSY (уже установлен), EINVAL (код/данные #define IRQ_CHAIN_MAX 4
* не в W2 — неподходящий memory mode). */
/* Добавить обработчик кадрового прерывания в цепочку.
* 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); int irq_install(isr_t handler);
/* Снять обработчик и вернуть IM2-таблицу DSS. Безопасно звать /* Снять ВСЮ цепочку и вернуть IM2-таблицу DSS. Безопасно звать
* повторно; вызывается автоматически при exit(). */ * повторно; вызывается автоматически при exit(). Для точечного
* снятия — irq_chain_remove. */
void irq_remove(void); void irq_remove(void);
/* ---- CTC-таймер (Z84C015, вектор 0x06 — отдельный от кадрового) --- /* ---- CTC-таймер (Z84C015, вектор 0x06 — отдельный от кадрового) ---
+23 -1
View File
@@ -29,12 +29,29 @@
/* Буфер под таблицу: 256 (выравнивание) + 257 (таблица). */ /* Буфер под таблицу: 256 (выравнивание) + 257 (таблица). */
#define _IRQ_VEC_BUF_SIZE (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 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 isr_t _irq_ctc_user; /* CTC-handler; NULL = нет */
extern uint8_t _irq_old_i; /* I до захвата таблицы */ extern uint8_t _irq_old_i; /* I до захвата таблицы */
extern uint8_t _irq_refs; /* счётчик пользователей таблицы */ 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-насоса (libc/cbl регистрирует _cbl_pump). Косвенность
* сознательная: прямая ссылка трамплина на cbl-модуль притянула бы * сознательная: прямая ссылка трамплина на cbl-модуль притянула бы
* его (и 2 КБ кольца) в каждую irq-программу. NULL = CBL выключен, * его (и 2 КБ кольца) в каждую irq-программу. NULL = CBL выключен,
@@ -61,6 +78,11 @@ uint8_t *_irq_table(void);
void _irq_tramp(void); void _irq_tramp(void);
void _irq_ctc_tramp(void); void _irq_ctc_tramp(void);
/* Метка конца кадрового трамплина — для вычисления его размера при
* копировании в W2 (_irq_table_ref). Объявлена как массив, чтобы
* можно было взять адрес без вызова. */
extern uint8_t _irq_tramp_end[];
/* SMC-операнд `jp` на обработчик DSS (метка в _irq_tramp.c). */ /* SMC-операнд `jp` на обработчик DSS (метка в _irq_tramp.c). */
extern uint16_t _irq_dss_target; extern uint16_t _irq_dss_target;
+37 -8
View File
@@ -1,14 +1,43 @@
/* /*
* _irq_state — данные IM2-модуля: буфер вектор-таблицы, указатели на * _irq_state — данные IM2-модуля. Только данные.
* user-handler'ы (кадровый и CTC), сохранённый регистр I, счётчик *
* ссылок таблицы. Только данные. * Кадровый обработчик теперь ЦЕПОЧКА (_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" #include "_irq.h"
uint8_t _irq_vec_buf[_IRQ_VEC_BUF_SIZE]; uint8_t _irq_vec_buf[_IRQ_VEC_BUF_SIZE];
isr_t _irq_user;
isr_t _irq_ctc_user; /* Цепочка кадровых обработчиков. Пустой слот = NULL. Порядок вызова =
isr_t _irq_cbl_hook; * порядок индексов; после remove/add взаимный порядок НЕ гарантирован
uint8_t _irq_old_i; * (переиспользуется первая дыра) — хендлеры обязаны быть независимы. */
uint8_t _irq_refs; 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];
+41 -10
View File
@@ -1,10 +1,19 @@
/* /*
* _irq_table — жизненный цикл общей IM2-таблицы (счётчик ссылок). * _irq_table — жизненный цикл общей IM2-таблицы (счётчик ссылок).
* *
* Первый ref: проверка W2-адресов, построение таблицы (заливка H + * Первый ref: проверка W2-адреса БУФЕРА (данные обязаны быть в W2),
* jp _irq_tramp по смещению H), I := H, IM 2, atexit-уборка. * запись базовой W1-страницы приложения, W2-размещение кадрового
* трамплина (прямо или копией — см. ниже), построение таблицы (заливка
* H + jp <W2-трамплин> по смещению H), I := H, IM 2, atexit-уборка.
* Последний unref: I := старое, IM 1 (штатный режим DSS — его * Последний unref: I := старое, IM 1 (штатный режим DSS — его
* обработчик на 0x0038; см. docs/samples/sprinterIntLib.asm). * обработчик на 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 <string.h> #include <string.h>
@@ -21,6 +30,18 @@ static uint8_t irq_get_i(void) __naked
__endasm; __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. */ /* I := A, режим IM 2. */
static void irq_set_i_im2(uint8_t i_val) __naked static void irq_set_i_im2(uint8_t i_val) __naked
{ {
@@ -65,17 +86,28 @@ int _irq_table_ref(void)
return 0; return 0;
} }
/* Трамплины и таблица обязаны жить в W2 (0x8000-0xBFFF): во время /* Вектор-таблица и данные цепи обязаны жить в W2 (0x8000-0xBFFF):
* прерывания W1/W3 могут быть перемаплены. CODE вне W2 = * во время прерывания W1/W3 могут быть перемаплены. Это BSS — W2
* неподходящий memory mode (small/huge). */ * во всех режимах, КРОМЕ small для мелких программ (данные ещё в
uint16_t tramp = (uint16_t)&_irq_tramp; * W1) → EINVAL, safe. Код трамплина в W1 обрабатывается копией. */
uint16_t buf = (uint16_t)_irq_vec_buf; uint16_t buf = (uint16_t)_irq_vec_buf;
if (tramp < 0x8000 || tramp >= 0xC000 || if (buf < 0x8000 || buf + _IRQ_VEC_BUF_SIZE > 0xC000) {
buf < 0x8000 || buf + _IRQ_VEC_BUF_SIZE > 0xC000) {
errno = EINVAL; errno = EINVAL;
return -1; 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 *table = _irq_table();
uint8_t h = (uint8_t)((uint16_t)table >> 8); uint8_t h = (uint8_t)((uint16_t)table >> 8);
@@ -84,11 +116,10 @@ int _irq_table_ref(void)
/* Старое I — только чтобы вернуть регистр при unref: DSS работает /* Старое I — только чтобы вернуть регистр при unref: DSS работает
* в IM 1 (I=0x3F — наследие Spectrum ROM, НЕ таблица). */ * в IM 1 (I=0x3F — наследие Spectrum ROM, НЕ таблица). */
_irq_old_i = irq_get_i(); _irq_old_i = irq_get_i();
_irq_dss_target = 0x0038;
memset(table, h, 257); memset(table, h, 257);
uint8_t *stub = table + h; /* h < 0xC0 — с [0xFF] не пересекается */ uint8_t *stub = table + h; /* h < 0xC0 — с [0xFF] не пересекается */
stub[0] = 0xC3; /* jp _irq_tramp */ stub[0] = 0xC3; /* jp <W2-трамплин> */
stub[1] = (uint8_t)(tramp & 0xFF); stub[1] = (uint8_t)(tramp & 0xFF);
stub[2] = (uint8_t)(tramp >> 8); stub[2] = (uint8_t)(tramp >> 8);
+82 -42
View File
@@ -1,23 +1,31 @@
/* /*
* _irq_tramp — вход IM2-прерывания (вектор 0xFF: кадр/клавиатура/CBL). * _irq_tramp — вход IM2-прерывания (вектор 0xFF: кадр/клавиатура/CBL),
* версия с ЦЕПОЧКОЙ кадровых обработчиков и W1-remap для all-modes.
* *
* До выяснения источника трогаем только AF: * Разбор источника (трогаем только AF, пока не выяснили):
* - клавиатура (бит 0 порта 0x19 = SIO-A RR0 «Rx Character * - клавиатура (бит 0 порта 0x19 = SIO-A RR0) → сразу к DSS, БЕЗ
* Available») уходит напрямую в DSS; * remap (W1 не трогаем — прерванный контекст сохранён как есть);
* - CBL (бит 7 порта 0xFE) проверяется ТОЛЬКО при зарегистрированном * - CBL (бит 7 порта 0xFE) — только при зарегистрированном хуке
* хуке _irq_cbl_hook: при выключенном CBL бит подтянут к 1 (MAME * _irq_cbl_hook (иначе бит подтянут к 1); личное прерывание, RETI
* kbd_fe_r: data |= 0xE0) и проверка ложно срабатывала бы на * без чейна к DSS;
* каждом кадре. CBL-путь: полный сейв → насос → restore → * - кадровое: проход по _irq_chain[] — все ненулевые слоты по порядку.
* EI/RETI, к DSS НЕ чейнится (личное прерывание);
* - кадровое: полный сейв → user-handler → restore → чейн.
* *
* Чейн — ВСЕГДА jp на обработчик DSS 0x0038 (DSS работает в IM 1; * W1-remap (docs/im2_isr_design.md «Вариант работает во всех режимах»):
* jp с interrupted-PC на стеке имитирует RST 38) — он делает свой * в small/huge базовый код приложения (и хендлеры) в W1, а сам трамплин
* save/restore и завершает EI/RETI: клавиатурный FIFO, SYSTIME и * во время прерывания исполняется из 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, патчится из * COPY-SAFE: все внутренние переходы — jr/djnz (относительные);
* _irq_table_ref; модуль в _CODE (RAM на Sprinter, W2 в tiny/big). * единственный call — к _irq_call_hl (исполняется при W1=base, значит
* оригинал в W1 достижим); оба jp — литерал 0x0038 (W0, всегда
* замаплен). Никаких абсолютных само-ссылок → байты копируются как
* есть, без релокации. ВНИМАНИЕ: не добавлять `jp <метка>` внутрь
* трамплина — сломает копию.
*/ */
#include "_irq.h" #include "_irq.h"
@@ -25,21 +33,31 @@
void _irq_tramp(void) __naked void _irq_tramp(void) __naked
{ {
__asm __asm
push af push af ; сохранить прерванный AF
in a, (#0x19) in a, (#_IRQ_SIO_A)
rrca ; бит 0 клавиатурный байт принят? rrca ; бит 0 клавиатурный байт принят?
jr C, _irq_chain jr NC, tr_notkbd
;; --- CBL? --- старший байт хука ненулевой у любого ;; --- клавиатура: прямо к DSS, без remap ---
;; W2-адреса для проверки «хук есть» хватает одного байта. pop af
.db 0xC3 ; jp nn
__irq_dss_target::
.dw 0x0038 ; SMC-операбельно, но по умолчанию 0x0038
tr_notkbd:
;; --- CBL? --- старший байт хука ненулевой у любого W2-адреса.
ld a, (__irq_cbl_hook + 1) ld a, (__irq_cbl_hook + 1)
or a, a or a, a
jr Z, _irq_frame jr Z, tr_frame
in a, (#0xFE) in a, (#0xFE)
rlca ; бит 7 запрос блока CBL? rlca ; бит 7 запрос блока CBL?
jr NC, _irq_frame 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 bc
push de push de
push hl push hl
@@ -60,26 +78,31 @@ void _irq_tramp(void) __naked
pop bc pop bc
exx exx
pop af pop af
.db 0x08 ; ex af,af (теневой AF) .db 0x08 ; ex af,af
pop hl pop hl
pop de pop de
pop bc pop bc
ld a, (__irq_saved_w1) ; вернуть прерванную W1
out (#_IRQ_W1_PORT), a
pop af pop af
ei ei
reti 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 bc
push de push de
push hl push hl
ld hl, (__irq_user) .db 0x08
ld a, h
or a, l
jr Z, _irq_pop3 ; handler не установлен
;; Сохранить теневой набор и индексные user-код клобберит всё.
.db 0x08 ; ex af,af (теневой AF)
push af push af
exx exx
push bc push bc
@@ -88,8 +111,25 @@ void _irq_tramp(void) __naked
push ix push ix
push iy 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 call _irq_call_hl
pop hl
pop bc
tr_skip:
djnz tr_loop
pop iy pop iy
pop ix pop ix
@@ -98,19 +138,19 @@ void _irq_tramp(void) __naked
pop bc pop bc
exx exx
pop af pop af
.db 0x08 ; ex af,af (теневой AF) .db 0x08
_irq_pop3:
pop hl pop hl
pop de pop de
pop bc pop bc
_irq_chain: ld a, (__irq_saved_w1) ; вернуть прерванную W1 перед chain
out (#_IRQ_W1_PORT), a
tr_frame_exit:
pop af pop af
;; Всегда к DSS (IM 1, обработчик 0x0038) EI/RETI за ним. .db 0xC3 ; jp 0x0038 (литерал copy-safe)
.db 0xC3 ; jp nn .dw 0x0038
__irq_dss_target::
.dw 0x0038 ; SMC: подтверждается в _irq_table_ref
_irq_call_hl: _irq_call_hl:
jp (hl) jp (hl)
__irq_tramp_end::
__endasm; __endasm;
} }
+36
View File
@@ -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 <errno.h>
#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;
}
+25
View File
@@ -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;
}
}
}
+4
View File
@@ -42,6 +42,10 @@ int irq_ctc_install(isr_t handler, uint8_t div2, uint8_t div3)
{ {
if (!handler) { errno = EINVAL; return -1; } if (!handler) { errno = EINVAL; return -1; }
if (_irq_ctc_user) { errno = EBUSY; 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; if (_irq_table_ref() != 0) return -1;
uint16_t tramp = (uint16_t)&_irq_ctc_tramp; uint16_t tramp = (uint16_t)&_irq_ctc_tramp;
+5 -9
View File
@@ -1,17 +1,13 @@
/* /*
* irq_install — установить user-handler кадрового прерывания. * irq_install — обёртка над цепочкой (обратная совместимость).
* Захватывает общую IM2-таблицу (_irq_table_ref) и включает кадровый * Теперь просто добавляет обработчик в цепь: EBUSY больше не бывает
* путь трамплина. Детали и результаты отладки: docs/im2_isr_design.md. * (несколько хендлеров легальны), при 4 занятых слотах — ENOMEM.
* Возврат/errno — как у irq_chain_add.
*/ */
#include <errno.h>
#include "_irq.h" #include "_irq.h"
int irq_install(isr_t handler) int irq_install(isr_t handler)
{ {
if (!handler) { errno = EINVAL; return -1; } return irq_chain_add(handler);
if (_irq_user) { errno = EBUSY; return -1; }
if (_irq_table_ref() != 0) return -1;
_irq_user = handler;
return 0;
} }
+12 -4
View File
@@ -1,16 +1,24 @@
/* /*
* irq_remove — снять кадровый user-handler и отпустить IM2-таблицу * irq_remove — снять ВСЮ кадровую цепочку и отпустить IM2-таблицу
* (последний пользователь возвращает I/IM 1 — штатный режим DSS). * (последний пользователь возвращает I/IM 1 — штатный режим DSS).
* Идемпотентно; вызывается и из atexit-уборки. * Идемпотентно; вызывается и из atexit-уборки, поэтому обязано снимать
* всё разом (exit не знает про отдельные слоты). Для точечного снятия
* одного обработчика — irq_chain_remove.
*/ */
#include "_irq.h" #include "_irq.h"
void irq_remove(void) void irq_remove(void)
{ {
if (!_irq_user) return; if (_irq_chain_n == 0) return;
IRQ_DISABLE(); 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_ENABLE();
/* Цепь держала РОВНО одну ссылку на таблицу (взята при первом
* слоте) — отпускаем её один раз. */
_irq_table_unref(); _irq_table_unref();
} }
+41
View File
@@ -3,6 +3,8 @@
* *
* Проверяет: * Проверяет:
* 1. кадровый handler тикает ~50 Гц (сверка с time() за 3 секунды); * 1. кадровый handler тикает ~50 Гц (сверка с time() за 3 секунды);
* 1b. цепочка: второй обработчик (irq_chain_add) тикает синхронно с
* первым; irq_chain_remove снимает только его, первый жив;
* 2. клавиатура/SYSTIME живы при установленном handler'е (чейн к DSS); * 2. клавиатура/SYSTIME живы при установленном handler'е (чейн к DSS);
* 3. CTC-таймер (вектор 0x06): пресет vsync ~49 Гц ОДНОВРЕМЕННО с * 3. CTC-таймер (вектор 0x06): пресет vsync ~49 Гц ОДНОВРЕМЕННО с
* кадровым; затем быстрый режим 875 кГц/(50*50) = 350 Гц; * кадровым; затем быстрый режим 875 кГц/(50*50) = 350 Гц;
@@ -17,6 +19,7 @@
#include <irq.h> #include <irq.h>
static volatile uint16_t ticks; static volatile uint16_t ticks;
static volatile uint16_t ticks2;
static volatile uint16_t ctc_ticks; static volatile uint16_t ctc_ticks;
static void on_tick(void) static void on_tick(void)
@@ -24,6 +27,11 @@ static void on_tick(void)
ticks++; ticks++;
} }
static void on_tick2(void)
{
ticks2++;
}
static void on_ctc(void) static void on_ctc(void)
{ {
ctc_ticks++; ctc_ticks++;
@@ -86,6 +94,39 @@ int main(void)
printf("ticks: %u in 3 s (~%u Hz, expect ~50)\n", got, got / 3); 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-таймер одновременно с кадровым --- */ /* --- Phase 2: CTC-таймер одновременно с кадровым --- */
if (irq_ctc_install(on_ctc, IRQ_CTC_VSYNC_DIV2, IRQ_CTC_VSYNC_DIV3)) { if (irq_ctc_install(on_ctc, IRQ_CTC_VSYNC_DIV2, IRQ_CTC_VSYNC_DIV3)) {
printf("irq_ctc_install failed: errno=%d\n", errno); printf("irq_ctc_install failed: errno=%d\n", errno);