Add OPL4 playback and analyzer support

This commit is contained in:
2026-09-15 14:59:29 +03:00
commit 1686838e03
553 changed files with 50524 additions and 0 deletions
+380
View File
@@ -0,0 +1,380 @@
# Wild Commander — Обзор Системы Плагинов
> Версия документа: 2.0 — дополнена по результатам анализа исходников
> Источники: `inc/wc_api.h`, `asm/wc_api.s`, `asm/crt0.s`, `DiskRef/Help.txt`, `DiskRef/WC_History.txt`
## Введение
Wild Commander (WC) — файловый менеджер с системой плагинов для платформы
**TS-Configuration** (ZX Spectrum Evo). Выполняет роль основной оболочки и
загрузчика для работы с FAT32-дисками, SD-картами и IDE.
WC запускается из BootROM / vDOS и предоставляет:
- Двухпанельный интерфейс (80 × 25/30/36 TXT-режим)
- FAT32 (длинные и короткие имена до 255 символов)
- Систему плагинов (.WMF) с горячей загрузкой/выгрузкой
- Поддержку устройств: SD (Z-Controller), IDE NEMO, IDE SMUC, NeoGS SD
- Встроенный просмотрщик (F3): TXT, HEX, TGA, SCR, Gigascreen, MGS — до 1 МБ
- Мультиязычность (ENG / RUS, переключение CTRL+SHIFT)
### Версии
| Компонент | Версия | Примечание |
|-----------|--------|------------|
| WC | 1.10 (03.08.2025) | Последняя из `WC_History.txt` |
| Plugin System | **#10** | Формат заголовка `.WMF` |
| Идентификатор | `WildCommanderMDL` | 16 байт, offset +16 в заголовке |
## Общая концепция
Плагины WC (.WMF файлы) — модульные расширения, выполняемые в адресном
пространстве Z80. Поддерживают до **64 страниц** (1 МБ) кода/данных.
Типичные области применения:
- Просмотр файлов различных форматов (BMP, GIF, ILBM, SCR)
- Воспроизведение музыки и видео (VGM, MOD, WAV, MP3/GS)
- Редактирование файлов (TXT Editor с подсветкой синтаксиса)
- Работа с ROM/Flash (ROM_PROG)
- Монтирование образов дисков (MOUNTER: TRD, TAP, FDI)
- Утилиты (часы, скринсейвер, компилятор, дамп памяти)
## Типы запуска плагинов
При запуске плагина, в регистр **A** передается тип вызова:
| Код | Константа (C) | Описание |
|-----|---------------|----------|
| #00 | `WC_PCOND_EXT_ONLY` | По расширению файла (ENTER / F3) |
| #01 | `WC_PCOND_ON_LOAD` | При первой загрузке плагина (ENTER) |
| #02 | `WC_PCOND_TIMER` | По таймеру скринсейвера + меню F10 |
| #03 | `WC_PCOND_F10` | Из меню запуска плагинов (F10) |
| #04 | `WC_PCOND_VIEWER` | Из меню вьювера (SHIFT+F3 / SHIFT+ENTER) |
| #05 | `WC_PCOND_F10_VIEW` | F10 + SHIFT+F3 |
| #11 | `WC_PCOND_F10_F2` | F10 + F2 |
| #12 | `WC_PCOND_F2` | По нажатию F2 |
| #13 | `WC_PCOND_F10_F4` | F10 + F4 |
| #14 | `WC_PCOND_F4` | По нажатию F4 |
| #15 | `WC_PCOND_F10_V_F4` | F10 + SHIFT+F3 + F4 |
> **Примечание:** Все плагины также вызываются по расширению, если расширения
> указаны в блоке расширений заголовка (offset +64). ENTER и F3 разделены
> начиная с WC v1.00 — флаг +63 определяет, какой из них запускает плагин.
## Входные параметры при запуске
WC делает `CALL #8000`. В `crt0.s` (SDCC) входные регистры сохраняются
в глобальные переменные автоматически.
```asm
; Входные параметры:
A - тип вызова (см. таблицу выше)
[DE,HL] - длина файла в байтах (DE=hi16, HL=lo16)
BC - адрес строки с именем файла (для типов #00, #04, #14)
формат: name(1-255), 0 (длинное FAT имя)
IX - адрес структуры активной панели
; Альтернативные регистры (только для #00,#04,#12,#14):
A' - номер расширения (0-31) для типа #00
DE' - количество файлов/каталогов в панели (включая "..")
HL' - номер объекта из панели (от 1)
```
В SDCC-плагине (`crt0.s` автоматически):
```c
extern uint8_t wc_file_ext; // ← A' (номер расширения)
extern char * wc_file_name; // ← BC (указатель на имя)
extern uint32_t wc_file_size; // ← [DE,HL] (32-бит размер)
```
## Коды возврата из плагина
При выходе из плагина регистр **A** определяет действие WC:
| A | Константа (C) | Действие |
|---|---------------|----------|
| 0 | `WC_EXIT_ESC` | Просто выход (ESC) |
| 1 | `WC_EXIT_UNRECOGNIZED` | Файл не опознан → передать другому плагину (тип #00) |
| 2 | `WC_EXIT_NEXT` | Перейти на следующий файл (тип #00) |
| 3 | `WC_EXIT_RELOAD_DIR` | Перечитать каталог |
| 4 | `WC_EXIT_PREV` | Перейти на предыдущий файл (тип #00) |
В SDCC-плагине: `wc_exit_code = 0; return 0;` — `crt0.s` считывает
`wc_exit_code` в A перед `RET` обратно в WC.
> **Код 2/4 (NEXT/PREV):** WC вызывает тот же плагин повторно с другим
> файлом. `crt0.s` заново обнуляет BSS (`_DATA`) и копирует инициализаторы
> (`_INITIALIZER → _INITIALIZED`), поэтому все `static` переменные сбросятся.
## Системные адреса
> Источник: `src_sdcc/inc/wc_api.h`, раздел «СИСТЕМНЫЕ ПЕРЕМЕННЫЕ WC»
| Адрес | Константа | Размер | Описание |
|---------------|--------------|--------|---------------------------------------------------|
| `#6000` | `WC_SYS_PG0` | 1 | Текущая страница в окне #0000 |
| `#6001` | `WC_SYS_PG4` | 1 | Текущая страница в окне #4000 |
| `#6002` | `WC_SYS_PG8` | 1 | Текущая страница в окне #8000 |
| `#6003` | `WC_SYS_PGC` | 1 | Текущая страница в окне #C000 |
| `#6004` | `WC_SYS_ABT` | 1 | 1 = был нажат ESC |
| `#6005` | `WC_SYS_ENT` | 1 | 1 = был нажат ENTER |
| `#6006` | `WC_SYS_FUN` | 3 | **Точка входа WC API** (JP к диспетчеру) |
| `#6009` | `WC_SYS_TMN` | 2 | Таймер INT (uint16_t, ++/кадр ≈50 Гц) |
| `#600B` | `WC_SYS_FEP` | 1 | Страница FAT-движка в окне 0 |
| `#600C` | `WC_SYS_XPP` | 1 | Сохранённая страница |
| `#600D` | `WC_SYS_CNFV`| 1 | ЦАП: 0=PWM, 1=3bit, 2=4bit, 3=5bit, 7=VDAC2/FT812 |
| `#600E` | `WC_SYS_HEI` | 1 | Высота TXT-экрана: 25 / 30 / 36 |
| `#600F` | `WC_SYS_TXTMD`| 1 | Текущий видеорежим WC (0=TXT) |
| `#6010` | `WC_SYS_INIFLG`| 1 | Флаг загрузки INI-конфигурации |
| `#6011` | `WC_SYS_RSQBC`| 2 | RSQ буфер-счётчик |
| `#6013` | `WC_SYS_RSQB2`| 2 | RSQ буфер 2 |
| `#6015` | `WC_SYS_SDBSF`| 1 | Состояние SD1 драйвера |
| `#6016` | `WC_SYS_SDBSF2`| 1 | Состояние SD2 драйвера |
| `#6017` | `WC_SYS_F3ENTFL`| 1 | Флаг F3/ENTER для файловых операций |
| `#6018` | `WC_SYS_FEP2`| 2 | FAT engine page extra |
| `#6020` | `WC_SYS_PLRESAD`| 8 | Резидент перехода: A=страница, HL=адрес → JP(HL) |
| `#6028` | `WC_SYS_PLRESCALL`| 8 | Резидент вызова: A=страница, HL=адрес → CALL(HL) |
| `#8000` | — | — | **Точка входа плагина** (WC делает CALL #8000) |
Чтение из C:
```c
#define WC_SYS_ABT (*(volatile uint8_t*)0x6004)
#define WC_SYS_TMN (*(volatile uint16_t*)0x6009)
#define WC_SYS_HEI (*(volatile uint8_t*)0x600E)
// Или через макросы wc_api.h:
uint8_t h = wc_get_height(); // 25/30/36
uint16_t t = wc_get_timer(); // кадровый счётчик
uint8_t esc = wc_get_abt(); // 1 = ESC нажат
```
## Вызов API функций
Все функции WC вызываются через точку доступа #6006:
```asm
WLD EQU #6006
; Пример вызова функции PRWOW (вывод окна)
PRWOW LD A,1
JP WLD
; Пример вызова ESC (проверка нажатия ESC)
ESC LD A,#17
JP WLD
```
**Регистровые соглашения WC API:**
- Вход: **A** = номер функции, остальные параметры в регистрах (см. справочник)
- Некоторые функции используют **A'** (альтернативный A через `EX AF,AF'`)
- **Портит**: AF, BC, DE, HL, IX
- **Сохраняет**: IY, AF', BC', DE', HL', SP
> ⚠️ **IX сохраняется** диспетчером P8000, но **AF, BC, DE, HL портятся**
> при каждом вызове `CALL #6006`.
---
## Карта памяти (физические страницы)
> Источник: `wc_api.h` — полная таблица `WC_PAGE_*`
| Страницы | Константа | Назначение |
|----------|-----------|------------|
| `#00` | `WC_PAGE_MTXPG` | Основной текстовый экран (TXT) |
| `#01` | `WC_PAGE_FONT0` | Шрифт основного TXT + Hrust/MegaLZ |
| `#02` | `WC_PAGE_CODE` | Код WC (ядро) |
| `#05` | `WC_PAGE_FAT_ENGINE` | FAT движок + стек + данные |
| `#08` | `WC_PAGE_BUFPG` | 2-й текстовый экран / буфер |
| `#09` | `WC_PAGE_FONT1` | Шрифт для 2-го TXT |
| `#0B–#13` | `WC_PAGE_CAT_LEFT` | Каталог левой панели (9 стр.) |
| `#14–#1C` | `WC_PAGE_CAT_RIGHT` | Каталог правой панели (9 стр.) |
| `#1D` | `WC_PAGE_RESB` | Буфер сохранения окон (RESB) |
| `#1E` | `WC_PAGE_CPPG1` | Позиции левой панели |
| `#1F` | `WC_PAGE_CPPG2` | Позиции правой панели |
| **#20–#5F** | **`WC_PAGE_TVBPG`** | **Мегабуфер (64 стр. = 1 МБ)**, он же видео: |
| `#20–#2F` | `WC_PAGE_GV1` | — видеобуфер 1 (16 стр.) |
| `#30–#3F` | `WC_PAGE_GV2` | — видеобуфер 2 |
| `#40–#4F` | `WC_PAGE_GV3` | — видеобуфер 3 |
| `#50–#5F` | `WC_PAGE_GV4` | — видеобуфер 4 |
| `#60` | `WC_PAGE_PLHPG` | Заголовки плагинов |
| **#61–#A0** | **`WC_PAGE_PLGPG`** | **Код/данные плагинов (до 64 стр.)** |
| `#A1–#BF` | `WC_PAGE_TAP_START` | TAP-область (31 стр. = 496 КБ) |
| `#C0–#E7` | `WC_PAGE_VDOS_RAM` | vDOS RAM-диск (40 стр. = 640 КБ) |
| `#E8–#EF` | `WC_PAGE_VDOS_FREE` | Свободные страницы |
| `#F0` | `WC_PAGE_PNLPG` | Поток левой панели |
| `#F1` | `WC_PAGE_PNL_RIGHT` | Поток правой панели |
| `#F2` | `WC_PAGE_INIPG` | INI-поток (плагин #00) |
| `#F3` | `WC_PAGE_STRPG` | Поток плагина #01 |
| `#F4–#F7` | — | SPG loader, boot pages (5,2), panel paths |
| `#F8–#FB` | `WC_PAGE_VROM_START` | Виртуальный ROM (4 стр.) |
| `#FC–#FF` | — | vDOS: таблицы, драйверы, трапы |
> **Плагин видит:** свои заказанные страницы (0..N) через `wc_mngc_pl()` /
> `wc_mng0_pl()` / `wc_mng8_pl()`. Физическая нумерация скрыта — WC
> маппит логическую страницу N плагина в `WC_PAGE_PLGPG + N`.
---
## Поддерживаемые устройства хранения
| Код | Константа | Устройство |
|-----|-----------|------------|
| 0 | `WC_DEV_SD_ZC` | SD-карта (Z-Controller) |
| 1 | `WC_DEV_IDE_NEMO_M` | IDE Nemo — Master |
| 2 | `WC_DEV_IDE_NEMO_S` | IDE Nemo — Slave |
| 3 | `WC_DEV_SD_NGS` | SD-карта (NeoGS) |
| 4 | `WC_DEV_IDE_SMUC_M` | IDE SMUC — Master |
| 5 | `WC_DEV_IDE_SMUC_S` | IDE SMUC — Slave |
| 6 | `WC_DEV_SD2_ZC` | Вторая SD (Z-Controller SD2) |
---
## Работа со страницами памяти
### Логическая нумерация
Плагин запрашивает N страниц в заголовке (offset +34). WC выделяет их
из пула `#61–#A0`. Плагин обращается по **логическому** номеру 0..N−1.
### Окна CPU и функции маппинга
| Окно | Адреса | Функция ASM | Функция C | Примечание |
|------|--------|-------------|-----------|------------|
| Win 0 | `#0000–#3FFF` | `MNG0_PL` (#4E) | `wc_mng0_pl(page)` | ⚠ Данные для WC API нельзя класть сюда |
| Win 2 | `#8000–#BFFF` | `MNG8_PL` (#4F) | `wc_mng8_pl(page)` | Код плагина — обычно стартовая страница |
| Win 3 | `#C000–#FFFF` | `MNGC_PL` (#00) | `wc_mngc_pl(page)` | ⚠ Сбрасывается после КАЖДОГО вызова WC API |
Специальные значения для `MNGC_PL`:
- `#FF` — шрифтовая страница (нельзя писать в `#E000–#FFFF`!)
- `#FE` — первый текстовый экран (с панелями WC)
### Резидент перехода (#6020)
Переключает страницу плагина и делает **безвозвратный** JP:
```c
// C: никогда не вернётся в вызывающий код
void wc_resident_jump(uint8_t page, uint16_t addr);
```
```asm
; ASM: A = страница (0..N-1), HL = адрес (#8000–#BFFF)
LD A,1
LD HL,#8000
CALL #6020 ; → WC переключает Win2 на стр.1, JP (HL)
```
### Резидент вызова (#6028)
Вызывает код в другой странице плагина **с возвратом**:
```c
void wc_resident_call(uint8_t page, uint16_t addr);
```
```asm
; ASM: A = страница, HL = адрес. Сохраняет всё кроме A,HL.
LD A,2
LD HL,#A000
CALL #6028 ; → WC переключает, CALL (HL), возвращает стр. назад
```
> Добавлено в WC v1.09 (31.07.2024). До этого многостраничные плагины
> требовали прямой OUT в порт Page2 (#12AF).
---
## Важные замечания
1. **При входе в плагин может быть любой экран!** Обязательно вызывайте
`GEDPL` (#0F) / `wc_gedpl()` для переключения на текстовый режим.
2. **Вызывать GEDPL лучше после анализа файла**, когда убедились что файл
подходит для плагина (и вернули `wc_exit_code = 1` если нет).
3. **Обязательно сохраняйте IX** при входе в плагин, если планируете
использовать файловые функции.
4. **⚠ Win3 (#C000) сбрасывается после каждого вызова WC API!**
Внутри `CALL #6006` WC переключает окно `#C000` на текстовую страницу
и **не восстанавливает** его. Если плагин работал через `mngcvpl()` /
`mngc_pl()`, он **обязан** повторить маппинг после любого UI-вызова
(`gedpl`, `prwow`, `txtpr`, `rresb`, `load512` и т.д.).
5. **GEDPL затирает #1000–#1447 в окне 0** — нельзя хранить данные в этой области.
6. **Буферы и строки для файловых функций** (`load512`, `fentry`, `findnext`,
и т.д.) **обязаны** быть в `#8000–#FFFF`. WC ремапит `#0000–#7FFF`
под FAT-движок внутри каждого файлового вызова.
7. **FindNext портит #0000–#1FFF** на странице драйвера — не хранить
данные там при итерации по каталогу.
8. **WC API портит регистры:** AF, BC, DE, HL, IX. Сохраняет: IY, AF',
BC', DE', HL', SP. Это верно для **всех** функций через `CALL #6006`.
9. **Стек не настраивается** — плагин использует стек WC (в Win 1, `#4000–#7FFF`).
Стек WC достаточно глубокий для обычных плагинов, но рекурсия требует осторожности.
---
## Горячие клавиши WC (для пользователя)
> Источник: `DiskRef/Help.txt`
| Клавиша | Действие |
|---------|----------|
| ENTER | Войти в каталог / запустить .WMF / просмотреть файл |
| F3 | Встроенный просмотрщик (TXT/HEX/TGA/SCR/MGS, до 1 МБ) |
| SHIFT+F3/SHIFT+ENTER | Меню выбора вьювера (плагин с `WC_PCOND_VIEWER`) |
| F2 | Пользовательское меню (из `wc.mnu`) |
| F4 | Редактирование / выбор текстового редактора |
| F5 | Копирование отмеченных файлов в другую панель |
| F6 | Переименование файла/каталога |
| F7 | Создание каталога |
| F8 / DEL | Удаление отмеченных файлов |
| F9 | Настройки INI (авто-создаёт каталог WC на загрузочном устройстве) |
| F10 | Меню запуска/удаления плагинов |
| TAB | Переключение между панелями |
| SPACE / INSERT | Отметить файл/каталог |
| * | Инвертировать выделение |
| ALT+F1/F2 | Выбор диска для левой/правой панели |
| CTRL+F3 | Сортировка по имени |
| CTRL+F4 | Сортировка по расширению |
| CTRL+F6 | Сортировка по размеру |
| CTRL+R | Обновить каталог |
| CTRL+A | Выделить все объекты |
| CTRL+SHIFT | Переключение ENG/RUS |
---
## Примеры плагинов
В каталоге `DiskRef/WC/` находятся скомпилированные плагины WC:
| Файл | Назначение |
|------|------------|
| `GSPLAYER.WMF` | Плеер для GS (MP3, MOD, WAV) |
| `WPLAYER.WMF` | Музыкальный плеер с плейлистами |
| `VGMPLAY.WMF` | VGM плеер (OPL3/AY/SAA) — этот проект |
| `TXTEDIT.WMF` | Текстовый редактор с подсветкой синтаксиса |
| `BMPV.WMF` | Просмотрщик BMP |
| `GIFVIEW.WMF` | Просмотрщик GIF |
| `ILBMVIEW.WMF` | Просмотрщик IFF/ILBM |
| `FTVIEW.WMF` | Просмотрщик FT-шрифтов |
| `MOUNTER.WMF` | Монтирование TRD/TAP/FDI образов |
| `ROM_PROG.WMF` | Программатор ROM/Flash |
| `SCRSAVER.WMF` | Скринсейвер (тип вызова `WC_PCOND_TIMER`) |
| `CLOCK.WMF` | Часы |
| `SETIME.WMF` | Установка времени |
| `COMPILER.WMF` | Компилятор |
| `RE.WMF` | Ассемблер/отладчик |
---
## Ссылки на другие разделы документации
- [02_Plugin_Header_Structure.md](02_Plugin_Header_Structure.md) — формат 512-байтного заголовка
- [03_WC_API_Reference.md](03_WC_API_Reference.md) — полный справочник функций
- [04_TSConfig_Platform.md](04_TSConfig_Platform.md) — аппаратная платформа, порты, регистры
- [06_Example_Plugin.md](06_Example_Plugin.md) — примеры плагинов на ASM и SDCC C
+311
View File
@@ -0,0 +1,311 @@
# Структура заголовка плагина (512 байт)
> Источники: `inc/wc_api.h` (константы `WC_PLUGIN_*`, `WC_PCOND_*`),
> `DiskRef/WC_History.txt` (история изменений формата)
## Общая структура
Заголовок плагина имеет фиксированный размер **512 байт** и содержит всю
необходимую информацию для загрузки и запуска плагина системой WC.
**Текущая версия формата:** `#10` (`WC_PLUGIN_VERSION`), идентификатор: `WildCommanderMDL`.
Формат неоднократно менялся (WC v0.56, v1.00, v1.02, v1.06, v1.09) —
старые плагины несовместимы с новой версией.
## Детальная структура
| Offset | Размер | Описание |
|--------|--------|----------|
| +0 | 16 | Резерв (reserved) |
| +16 | 16 | Идентификатор: "WildCommanderMDL" |
| +32 | 1 | Версия формата (#10) |
| +33 | 1 | Резерв |
| +34 | 1 | Количество страниц для плагина |
| +35 | 1 | Номер страницы для #8000 (от 0) |
| +36 | 12 | Блоки данных (6 x 2 байта) |
| +48 | 15 | Резерв |
| +63 | 1 | Флаги для блока расширений |
| +64 | 96 | Блок расширений (32 x 3 байта) |
| +160 | 1 | #00 (маркер окончания расширений) |
| +161 | 4 | Максимальный размер файла |
| +165 | 32 | Имя плагина |
| +197 | 1 | Тип условий вызова плагина |
| +198 | 6 | Текст для F4 или F2 |
| +204 | 24 | Текст для меню вьювера |
| +228 | 32 | Резерв |
| +260 | 252 | Структура параметров INI |
## Детальное описание полей
### Количество страниц (+34)
Указывает, сколько 16 КБ страниц памяти будет выделено плагину.
Максимум: **64 страницы** (`WC_PLUGIN_MAX_PAGES`) = 1 МБ.
Страницы выделяются из пула `#61–#A0` (физические), плагин обращается
по логическому номеру 0..N−1 через `wc_mngc_pl()` / `wc_mng0_pl()` / `wc_mng8_pl()`.
Часть страниц может использоваться как буфер, который **сохраняется
между повторными запусками** плагина (NEXT/PREV) — код плагина WC не
перезагружает, если плагин уже загружен.
### Номер стартовой страницы (+35)
Номер страницы (от 0), которая будет включена в окно #8000 при запуске плагина. Нумерация начинается с 0.
### Блоки данных (+36, 12 байт)
Описывает до 6 блоков данных плагина:
```
Для каждого блока (2 байта):
Байт 0: Номер страницы (от 0) для размещения блока
Байт 1: Размер блока (x512 байт)
```
Блоки данных следуют сразу за заголовком плагина (512 байт).
Пример:
```asm
DB 0,16 ; Блок 0: код плагина, 16x512 = 8КБ
DB 1,32 ; Блок 1: данные плеера, 32x512 = 16КБ
DB 2,9 ; Блок 2: дополнительный плеер, 9x512 = 4.5КБ
DB 0,0 ; Остальные блоки не используются
DB 0,0
DB 0,0
```
### Флаги блока расширений (+63)
Определяет, какая клавиша запускает плагин по расширению:
```
BIT 0: Режим запуска
0 - запуск и по ENTER, и по F3
1 - см. BIT 1
BIT 1 (если BIT 0 = 1):
0 - запуск только по ENTER
1 - запуск только по F3
```
| Биты [1:0] | Поведение |
|------------|------------------------------------------|
| `00` | ENTER и F3 — оба запускают плагин |
| `01` | Только ENTER (F3 = встроенный вьювер WC) |
| `11` | Только F3 (ENTER = вход в файл как данные) |
> Разделение ENTER и F3 добавлено в WC v1.00 (октябрь 2019).
### Блок расширений (+64, 96 байт)
Содержит до 32 расширений файлов (по 3 байта каждое).
**Важно:** Расширения указываются **ТОЛЬКО БОЛЬШИМИ БУКВАМИ!**
Примеры:
```asm
DB "TXT" ; Текстовые файлы
DB "MOD" ; MOD музыка
DB "WAV" ; WAV аудио
DB "MP3" ; MP3 аудио
DB "VGM" ; VGM музыка
; ... до 32 расширений
DS 27*3 ; Остальные заполняем нулями
```
Блок можно забить нулями, если не нужна инициация по расширениям.
### Максимальный размер файла (+161, 4 байта)
Определяет максимальный размер файла, который плагин может обработать. Файлы больше этого размера плагину не передаются.
Примеры:
```asm
DW #FFFF,#FFFF ; Без ограничений
DW #6000,#0000 ; Максимум 24КБ
```
### Имя плагина (+165, 32 байта)
Текстовое имя плагина, которое показывается в меню.
```asm
DB "VGM Player for OPL3 v1.0 "
```
### Тип условий вызова (+197)
Определяет, при каких условиях плагин должен вызываться (помимо расширений):
| Код | Константа (C) | Условие вызова |
|-----|---------------|----------------|
| #00 | `WC_PCOND_EXT_ONLY` | Только по расширению |
| #01 | `WC_PCOND_ON_LOAD` | Сразу после загрузки (при ENTER) |
| #02 | `WC_PCOND_TIMER` | По таймеру скринсейвера + из меню F10 |
| #03 | `WC_PCOND_F10` | Из меню F10 |
| #04 | `WC_PCOND_VIEWER` | Из меню вьювера (SHIFT+F3) |
| #05 | `WC_PCOND_F10_VIEW` | F10 + SHIFT+F3 |
| #11 | `WC_PCOND_F10_F2` | F10 + F2 |
| #12 | `WC_PCOND_F2` | По F2 |
| #13 | `WC_PCOND_F10_F4` | F10 + F4 |
| #14 | `WC_PCOND_F4` | По F4 |
| #15 | `WC_PCOND_F10_V_F4` | F10 + SHIFT+F3 + F4 |
**Примечание:** Все плагины с указанными расширениями также вызываются
по расширению. Тип вызова расширяет набор способов запуска.
### Текст для F2/F4 (+198, 6 байт)
Если тип вызова #11/#12 - текст для подсказки F2
Если тип вызова #13/#14/#15 - текст для подсказки F4
```asm
DB "Edit " ; Для редактора
DB "Play " ; Для плеера
```
### Текст для меню вьювера (+204, 24 байта)
Текст, который выводится в меню выбора вьювера (при типах #04/#05/#15):
```asm
DB "Play VGM on OPL3 "
```
## Структура параметров INI (+260, 252 байта)
> Добавлено в WC v1.09 (31.07.2024)
Позволяет плагину принимать параметры из INI-файла WC (`wc.ini`).
Параметры отображаются в меню F9 → настройки плагина.
### Формат записи
```
+0: 1 байт - длина ПОЛНОЙ структуры (включая этот байт) или #00 (конец)
+1: 1 байт - default_option (1-based номер опции по умолчанию)
+2: N байт - "parameter_name" (ASCII, без терминатора)
+2+N: 1 байт - количество опций (1-10)
+3+N: M байт - "option1",0, "option2",0, ...
... следующая запись или #00
```
Максимум 8 параметров, каждый до 10 опций.
### Пример
```asm
PAR1:
DB PAR1END-PAR1 ; Длина структуры
DB 1 ; Опция по умолчанию = 1
DB "opl_port" ; Имя параметра
DB 2 ; Количество опций
DB "C4",0 ; Опция 1
DB "C0",0 ; Опция 2
PAR1END:
PAR2:
DB PAR2END-PAR2
DB 1
DB "freq"
DB 3
DB "683",0
DB "1367",0
DB "2734",0
PAR2END:
DB 0 ; Конец списка параметров
```
### Чтение параметров в плагине
```c
// wc_prm_pl(param_num) — param_num с 0
// Возвращает: номер опции (1-based), 0xFF если параметр не задан
uint8_t opt = wc_prm_pl(0); // прочитать 1-й параметр
if (opt == 0xFF) opt = 1; // значение по умолчанию
```
```asm
; ASM: A' = номер параметра (0-based)
EXA
LD A,0 ; Параметр #0
CALL WLD
EXA
LD A,#53 ; PRM_PL
CALL WLD
; Z=не задан, NZ: A=номер опции (1-based)
```
В INI-файле (`wc.ini`) параметры записываются по длинному имени плагина:
```ini
[VGM Player v0.5]
opl_port=C4
freq=683
```
## Пример полного заголовка
```asm
ORG #6000
; +0: Резерв
DS 16
; +16: Идентификатор
DB "WildCommanderMDL"
; +32: Версия
DB #10
; +33: Резерв
DB 0
; +34: Количество страниц
DB 2
; +35: Стартовая страница
DB 0
; +36: Блоки данных
DB 0,8 ; Код: 4КБ
DB 1,16 ; Данные: 8КБ
DB 0,0
DB 0,0
DB 0,0
DB 0,0
; +48: Резерв
DS 15
; +63: Флаги расширений
DB 0
; +64: Расширения
DB "VGM"
DB "VGZ"
DS 30*3
; +160: Конец расширений
DB 0
; +161: Максимальный размер
DW #FFFF,#FFFF
; +165: Имя плагина
DB "VGM Player v1.0 "
; +197: Тип вызова
DB #00 ; По расширению
; +198-228: Резерв/меню
DS 6
DS 24
DS 32
; +260: Параметры INI
DB 0 ; Нет параметров
ORG #8000,#6200
; Здесь начинается код плагина
```
File diff suppressed because it is too large Load Diff
+658
View File
@@ -0,0 +1,658 @@
# TSConfig Platform Reference
> Источники: `inc/tsconf.h` (532 стр.), `inc/wc_api.h` (системные константы),
> [TSConfig Wiki](https://github.com/tslabs/zx-evo)
## Обзор платформы
TS-Configuration (TSConfig) — расширенная конфигурация на базе **ZX Evolution**
(Большой Спектрум / ZX Evo), реализованная в FPGA (Altera/Xilinx).
| Параметр | Значение |
|-----------|----------|
| CPU | Z80 @ 3.5 / 7 / 14 / 28 МГц |
| RAM | до 4096 КБ (256 страниц × 16 КБ) |
| ROM | до 512 КБ (32 страницы) |
| Видео | до 360×288, 256 цветов, TXT 80/90 столбцов |
| Спрайты | До 85 на строку, 8×8–64×64, 3 плоскости |
| Тайлы | 2 плоскости, 8×8 |
| DMA | Аппаратный контроллер (RAM↔RAM, RAM↔SPI/IDE, FILL, CRAM, SFILE) |
| Кэш | 512 байт, по-оконно |
| Звук | AY-3-8910 (TurboSound: 2 чипа), OPL3 (YMF262), SAA1099 ×2, HUS |
| Хранилище | SD (Z-Controller, NeoGS), IDE (NEMO, SMUC), FAT32 |
| Совместимость | Pentagon-128/512/1024, порт #7FFD |
## Память
### Страничная адресация
Процессор Z80 адресует 64 КБ памяти, разделенные на 4 окна по 16 КБ:
| Окно | Адреса CPU | Регистр страницы | R/W | Reset |
|------|------------|------------------|-----|-------|
| 0 | #0000..#3FFF | Page0 | W | #00 |
| 1 | #4000..#7FFF | Page1 | W | #05 |
| 2 | #8000..#BFFF | Page2 | R/W | #02 |
| 3 | #C000..#FFFF | Page3 | R/W | #00 |
**Доступно:**
- RAM: до 4096 КБ (256 страниц по 16 КБ)
- ROM: до 512 КБ (32 страницы по 16 КБ)
### Регистры управления памятью
Доступ к регистрам через порты **#nnAF**, где nn - номер регистра.
#### MemConfig (регистр #07)
```
Биты:
7:6 - LCK128[1:0] - режим порта #7FFD
5:4 - резерв
3 - W0_RAM - выбор ROM/RAM в окне 0
2 - !W0_MAP - режим маппинга окна 0
1 - W0_WE - разрешение записи в окно 0
0 - ROM128 - выбор страниц маппинга
```
#### Page0..3 (регистры #10-#13)
8-битные регистры, содержащие номер страницы для соответствующего окна.
## Порты ввода-вывода
### Схема адресации портов
TSConfig использует порты с адресом **#nnAF**, где:
- Младшие 8 бит (A0-A7) = #AF
- Старшие 8 бит (A8-A15) = номер регистра (nn)
### Базовые регистры TSConfig
> Источник: `src_sdcc/inc/tsconf.h` — `#define TSCONF_PORT(reg) (((uint16_t)(reg) << 8) | 0xAF)`
| Порт | Константа | R/W | Описание |
|---------|----------------------------|-----|-------------------------------------|
| `#00AF` | `TSCONF_PORT_VCONFIG` | RW | VConfig: видеорежим (VM, RRES) |
| `#01AF` | `TSCONF_PORT_VPAGE` | W | VPage: страница видеобуфера |
| `#02AF` | `TSCONF_PORT_GXOFFSL` | W | GFX X offset (low) |
| `#03AF` | `TSCONF_PORT_GXOFFSH` | W | GFX X offset (high) |
| `#04AF` | `TSCONF_PORT_GYOFFSL` | W | GFX Y offset (low) |
| `#05AF` | `TSCONF_PORT_GYOFFSH` | W | GFX Y offset (high) |
| `#06AF` | `TSCONF_PORT_TSCONFIG` | W | TSConfig: tile/sprite engine |
| `#07AF` | `TSCONF_PORT_PALSEL` | W | PalSel: выбор палитры |
| `#0FAF` | `TSCONF_PORT_BORDER` | W | Border: цвет бордюра (биты 2:0) |
| `#10AF` | `TSCONF_PORT_PAGE0` | **W** | **Page0: страница окна #0000** |
| `#11AF` | `TSCONF_PORT_PAGE1` | **W** | **Page1: страница окна #4000** |
| `#12AF` | `TSCONF_PORT_PAGE2` | **RW** | **Page2: страница окна #8000** |
| `#13AF` | `TSCONF_PORT_PAGE3` | **RW** | **Page3: страница окна #C000** |
| `#14AF` | `TSCONF_PORT_FMADDR` | W | Font Maps |
| `#15AF` | `TSCONF_PORT_TMPAGE` | W | TileMap page |
| `#16AF` | `TSCONF_PORT_T0GPAGE` | W | Tile plane 0 gfx page |
| `#17AF` | `TSCONF_PORT_T1GPAGE` | W | Tile plane 1 gfx page |
| `#18AF` | `TSCONF_PORT_SGPAGE` | W | Sprite gfx page |
| `#20AF` | `TSCONF_PORT_SYSCONFIG` | W | **SysConfig: CPU freq + кэш** |
| `#21AF` | `TSCONF_PORT_MEMCONFIG` | W | MemConfig: режим памяти |
| `#22AF` | `TSCONF_PORT_HSINT` | W | FRAME-INT: горизонт. позиция |
| `#23AF` | `TSCONF_PORT_VSINTL` | W | FRAME-INT: вертик. позиция (low) |
| `#24AF` | `TSCONF_PORT_VSINTH` | W | FRAME-INT: вертик. позиция (high) + auto-inc |
| `#29AF` | `TSCONF_PORT_FDDVIRT` | W | FDD виртуализация |
| `#2AAF` | `TSCONF_PORT_INTMASK` | W | **INTMask: маска прерываний** |
| `#2BAF` | `TSCONF_PORT_CACHECONFIG` | W | **CacheConfig: кэш по окнам** |
#### Порты тайловых смещений
| Порт | Описание |
|------|----------|
| `#40AF`/`#41AF` | Tile plane 0: X offset (lo/hi) |
| `#42AF`/`#43AF` | Tile plane 0: Y offset (lo/hi) |
| `#44AF`/`#45AF` | Tile plane 1: X offset (lo/hi) |
| `#46AF`/`#47AF` | Tile plane 1: Y offset (lo/hi) |
#### Порты DMA
| Порт | Константа | Описание |
|------|-----------|----------|
| `#19AF` | `TSCONF_PORT_DMASADDRL` | Source addr low |
| `#1AAF` | `TSCONF_PORT_DMASADDRH` | Source addr high |
| `#1BAF` | `TSCONF_PORT_DMASADDRX` | Source page (extended) |
| `#1CAF` | `TSCONF_PORT_DMADADDRL` | Dest addr low |
| `#1DAF` | `TSCONF_PORT_DMADADDRH` | Dest addr high |
| `#1EAF` | `TSCONF_PORT_DMADADDRX` | Dest page (extended) |
| `#25AF` | `TSCONF_PORT_DMAWPDEV` | Wait Port device |
| `#26AF` | `TSCONF_PORT_DMALEN` | Burst length |
| `#27AF` | `TSCONF_PORT_DMACTRL` | Control (W) / Status (R) |
| `#28AF` | `TSCONF_PORT_DMANUM` | Bursts num (low) |
| `#2CAF` | `TSCONF_PORT_DMANUMH` | Bursts num (high) |
| `#2DAF` | `TSCONF_PORT_DMAWPADDR` | Wait Port addr |
> ⚠️ **Частая ошибка:** Page0–Page3 = `#10AF`–`#13AF`, SysConfig = `#20AF`.
> Не путать с VConfig (`#00AF`) или VPage (`#01AF`)!
### Порты OPL3 — YMF262
**OPL3 чип располагается по портам #C4–#C7:**
| Порт | Константа | Назначение |
|-------|-----------|------------|
| `#C4` | `TSCONF_PORT_OPL3_ADDR0` | Bank 0: регистр адреса |
| `#C5` | `TSCONF_PORT_OPL3_DATA0` | Bank 0: данные |
| `#C6` | `TSCONF_PORT_OPL3_ADDR1` | Bank 1: регистр адреса |
| `#C7` | `TSCONF_PORT_OPL3_DATA1` | Bank 1: данные |
**Регистр NEW (Bank 1, reg 0x05):**
- `NEW=1` (0x01) → OPL3 mode: биты L/R в регистрах `C0–C8` управляют маршрутизацией звука
- `NEW=0` (0x00) → OPL2-compat: L+R всегда активны (обязательно для файлов OPL2/OPL1!)
```c
// inc/opl3.h
void opl3_write_b0(uint8_t reg, uint8_t data); // Bank 0 (#C4/#C5)
void opl3_write_b1(uint8_t reg, uint8_t data); // Bank 1 (#C6/#C7)
void opl3_init(void); // сброс + NEW=1
// Для OPL2/OPL1 VGM: после opl3_init() нужно:
opl3_write_b1(0x05, 0x00); // NEW=0 — иначе тишина!
```
### Порты AY-3-8910 / YM2149
| Порт | Константа | Назначение |
|---------|-----------|------------|
| `#FFFD` | `TSCONF_PORT_AY_ADDR` | Регистр адреса / чтение (`OUT/IN`) |
| `#BFFD` | `TSCONF_PORT_AY_DATA` | Запись данных |
**TurboSound (два AY):** второй чип выбирается через порт `#FFFD`:
- `OUT (#FFFD), #FF` (= `TSCONF_AY_TS_CHIP0`) → чип 0 (основной)
- `OUT (#FFFD), #FE` (= `TSCONF_AY_TS_CHIP1`) → чип 1
**Частоты AY** (управление через `wc_turbopl(WC_TURBO_AY, param)`):
| param | Частота | Примечание |
|-------|---------|------------|
| 0 | 1,750,000 Гц | Стандарт TSConfig / Pentagon |
| 1 | 1,773,400 Гц | NTSC-кварц (315/88 МГц ÷ 2) |
| 2 | 3,500,000 Гц | Удвоенная |
| 3 | 3,546,800 Гц | Удвоенная NTSC |
### Порты SAA1099 — MultiSound card
| Чип | Данные | Адрес | Константы |
|------|--------|-------|-----------|
| SAA1 | `#00FF` | `#01FF` | `TSCONF_PORT_SAA_DATA/ADDR` |
| SAA2 | `#00FE` | `#01FE` | `TSCONF_PORT_SAA2_DATA/ADDR` |
> Некоторые источники указывают SAA2 по портам `#02FF`/`#03FF` — это
> зависит от реализации MultiSound платы. В `tsconf.h` используются
> `#00FE`/`#01FE`.
**Пример записи в регистр OPL3:**
```asm
; Запись в регистр банка 0
LD BC,#C4 ; Порт адреса
LD A,reg ; Номер регистра
OUT (C),A
LD B,#C5 ; Порт данных
LD A,value ; Значение
OUT (C),A
; Запись в регистр банка 1
LD BC,#C6 ; Порт адреса банка 1
LD A,reg ; Номер регистра
OUT (C),A
LD B,#C7 ; Порт данных банка 1
LD A,value ; Значение
OUT (C),A
```
## Процессор
### Частоты CPU
| Режим | Частота | SysConfig [1:0] | Константа |
|--------|---------|-----------------|----------|
| 00 | 3.5 МГц | `00` | `TSCONF_CPU_3_5MHZ` |
| 01 | 7.0 МГц | `01` | `TSCONF_CPU_7MHZ` |
| 10 | 14.0 МГц | `10` | `TSCONF_CPU_14MHZ` |
| 11 | 28.0 МГц | `11` | (non-standard, не все FPGA) |
> В WC API `wc_turbopl(WC_TURBO_CPU, param)` поддерживает
> param 0–3 (3.5/7/14/28 МГц).
Управление через регистр **SysConfig #20AF** (биты 1:0):
```c
// tsconf.h
#define TSCONF_CPU_3_5MHZ 0x00
#define TSCONF_CPU_7MHZ 0x01
#define TSCONF_CPU_14MHZ 0x02
#define TSCONF_CPU_MASK 0x03
#define TSCONF_CACHE_EN 0x04 // бит 2 — глобальный кэш
// Convenience macros:
tsconf_set_cpu(TSCONF_CPU_14MHZ); // только CPU
tsconf_set_cpu_cache(TSCONF_CPU_14MHZ, TSCONF_CACHE_EN); // CPU + кэш
```
### Кэш
512 байт кэш-памяти для ускорения работы на 14/28 МГц.
**Глобально:** бит 2 регистра **SysConfig** (`TSCONF_CACHE_EN`).
**По-оконно:** регистр **CacheConfig #2BAF**:
| Константа | Значение | Описание |
|-----------|----------|----------|
| `TSCONF_CACHE_EN_0000` | 0x01 | Кэш для #0000–#3FFF |
| `TSCONF_CACHE_EN_4000` | 0x02 | Кэш для #4000–#7FFF |
| `TSCONF_CACHE_EN_8000` | 0x04 | Кэш для #8000–#BFFF |
| `TSCONF_CACHE_EN_C000` | 0x08 | Кэш для #C000–#FFFF |
| `TSCONF_CACHE_ALL` | 0x0F | Все окна |
| `TSCONF_CACHE_NONE` | 0x00 | Кэш выключен |
```c
tsconf_set_cache(TSCONF_CACHE_EN_8000 | TSCONF_CACHE_EN_C000);
```
**Важно:** После DMA операций необходима **инвалидация кэша**:
```asm
; Инвалидация: 512 байт LDIR на себя
LD HL,#FE00
LD DE,#FE00
LD BC,#0200
LDIR
```
## Прерывания
### Источники прерываний
TSConfig поддерживает несколько источников INT:
| Источник | Бит | IM2 вектор | Константа |
|----------|-----|------------|-----------|
| FRAME | 0 | `#FF` (`TSCONF_IVEC_FRAME`) | `TSCONF_INT_FRAME` |
| LINE | 1 | `#FD` (`TSCONF_IVEC_LINE`) | `TSCONF_INT_LINE` |
| DMA | 2 | `#FB` (`TSCONF_IVEC_DMA`) | `TSCONF_INT_DMA` |
| WTP | 3 | — | `TSCONF_INT_WTP` |
### Регистр маски прерываний (INTMask #2AAF)
```
Биты:
3 - WTP (Wait Port) interrupt enable
2 - DMA interrupt enable
1 - LINE interrupt enable
0 - FRAME interrupt enable
Reset: #01 (только FRAME включен)
```
```c
tsconf_set_intmask(TSCONF_INT_FRAME | TSCONF_INT_LINE);
```
### Позиционирование FRAME-INT
```c
// Задать вертикальную строку и горизонтальную позицию
tsconf_set_frame_int(vline, hpos);
// Отключить
tsconf_disable_frame_int();
// Авто-инкремент VCNT (для множественных INT в кадре):
// VSINTH[7:4] = auto-increment value
#define TSCONF_VSINTH_INC(n) ((n) << 4)
```
---
## MemConfig (#21AF) — Конфигурация памяти
| Константа | Значение | Описание |
|-----------|----------|----------|
| `TSCONF_MEM_ROM128` | 0x01 | Выбор банка ROM (= бит 4 порта #7FFD) |
| `TSCONF_MEM_W0_WE` | 0x02 | Разрешить запись в окно #0000 |
| `TSCONF_MEM_W0_MAP_DIS` | 0x04 | Отключить маппинг через Page0 |
| `TSCONF_MEM_W0_RAM` | 0x08 | RAM вместо ROM в окне #0000 |
| `TSCONF_MEM_LCK128_512K` | 0x00 | Pentagon-512 режим |
| `TSCONF_MEM_LCK128_128K` | 0x40 | Pentagon-128 режим |
| `TSCONF_MEM_LCK128_AUTO` | 0x80 | Авто-определение |
| `TSCONF_MEM_LCK128_1M` | 0xC0 | Расширенный 1 МБ |
---
## TSU — Tile/Sprite Engine (#06AF)
| Константа | Значение | Описание |
|-----------|----------|----------|
| `TSCONF_TS_EXT` | 0x01 | TSU 360×288 (extended) |
| `TSCONF_T0Z_EN` | 0x04 | Tile[0] visible (plane 0) |
| `TSCONF_T1Z_EN` | 0x08 | Tile[0] visible (plane 1) |
| `TSCONF_T0_EN` | 0x20 | Tile layer 0 enable |
| `TSCONF_T1_EN` | 0x40 | Tile layer 1 enable |
| `TSCONF_S_EN` | 0x80 | Sprite layers enable |
---
## HUS — Hurricane Sound Engine
> Порты `#80AF`–`#87AF` (есть не на всех FPGA-прошивках)
HUS — аппаратный звуковой движок TSConfig для воспроизведения
PCM-сэмплов из RAM с DMA.
| Порт | Константа | Описание |
|------|-----------|----------|
| `#80AF` | `TSCONF_PORT_HUSCTRL` | Control (W) / Status (R) |
| `#81AF` | `TSCONF_PORT_HUSSAMPRATE` | Sample Rate: 1,750,000 / (SR+1) |
| `#82AF` | `TSCONF_PORT_HUSTICKRATEL` | Tick Rate (low) |
| `#83AF` | `TSCONF_PORT_HUSTICKRATEH` | Tick Rate (high) |
| `#84AF`–`#87AF` | `TSCONF_PORT_HUSRLD0`–`3` | Reload ch 0–31 |
**Биты статуса (чтение #80AF):**
| Константа | Бит | Описание |
|-----------|-----|----------|
| `TSCONF_HUS_ACT` | 0 | 1 = HUS активен |
| `TSCONF_HUS_TICK` | 1 | 1 = tick event |
| `TSCONF_HUS_HALF` | 6 | 1 = полбуфера |
| `TSCONF_HUS_EMPTY` | 7 | 1 = буфер пуст |
**Управление (запись #80AF):**
| Константа | Значение | Описание |
|-----------|----------|----------|
| `TSCONF_HUS_PAUSE` | 0x02 | Пауза воспроизведения |
| `TSCONF_HUS_RESET` | 0x80 | Сброс HUS |
### ISR плагина — собственный таймер
Плагин может зарегистрировать собственный ISR через
`wc_int_pl(WC_INT_PLUGIN)` + `wc_int_pl_handler(addr)`.
В VGM-плеере ISR настраивается через **перехват** вектора IM2
(FRAME INT, вектор `#5BFF`) с высокой частотой вызова.
POS_TABLE задаёт позиции INT-триггеров в кадре:
| Вариант | ISR_FREQ | T-states/тик | Тиков/кадр | VGM_SAMPLE_SHIFT | Семплов/тик |
|---------|----------|-------------|------------|------------------|---------|
| Quarter | **683 Гц** | 5120 T | 14 | 6 (44100/64) | 64 |
| Half | 1367 Гц | 2560 T | 28 | 5 (44100/32) | 32 |
| Full | 2734 Гц | 1280 T | 56 | 4 (44100/16) | 16 |
Конфигурация варианта хранится в `inc/variant_cfg.h`:
```c
// variant_cfg.h (текущий вариант)
#define ISR_FREQ 683
#define ISR_TICKS_PER_FRAME 14 // 683/50≈14
#define VGM_SAMPLE_SHIFT 6 // 44100 >> 6 = 689 ≈ 683
#define VGM_SAMPLE_MASK 0x3Fu
#define VGM_FILL_CMD_BUDGET 32 // команд за один yield
#define VARIANT_POS_ENTRIES 14 // записей в pos_table
#define VARIANT_POS_STEP 5120 // T-states между INT
```
### Механизм pos_table
`pos_table` — кольцевой буфер позиций INT в кадре.
Каждая запись: 5 байт `[VSINTH, VSINTL, HSINT, next_ptr_lo, next_ptr_hi]`.
ISR выводит 3 байта через OUTI (порты VSINTH/VSINTL/HSINT),
затем переходит по next_ptr к следующей записи.
Генерируется скриптом `scripts/gen_pos_table.js`.
### Частота прерываний TSConfig (FRAME)
- FRAME: зависит от видео режима
- 50 Гц (312 строк PAL)
- 60 Гц (262 строки NTSC)
- ~48.8 Гц (320 строк Pentagon)
## DMA Контроллер
TSConfig имеет аппаратный DMA для быстрого копирования/заполнения данных.
### Режимы DMA (регистр DMACTRL #27AF)
| Константа | Значение | Описание |
|-----------|----------|----------|
| `TSCONF_DMA_RAM_RAM` | 0x09 | RAM → RAM копирование |
| `TSCONF_DMA_BLT1_RAM` | 0x89 | RAM → RAM с пропуском нулей (transparency) |
| `TSCONF_DMA_BLT2_RAM` | 0x31 | Blitter с aдером |
| `TSCONF_DMA_FILL` | 0x21 | Заполнение RAM |
| `TSCONF_DMA_RAM_CRAM` | 0xA1 | RAM → CRAM (палитра, RGB555) |
| `TSCONF_DMA_RAM_SFILE` | — | RAM → Sprite File |
| `TSCONF_DMA_SPI_RAM` | 0x11 | SPI → RAM (чтение SD) |
| `TSCONF_DMA_RAM_SPI` | 0x91 | RAM → SPI (запись SD) |
| `TSCONF_DMA_IDE_RAM` | 0x19 | IDE → RAM |
| `TSCONF_DMA_RAM_IDE` | 0x99 | RAM → IDE |
| `TSCONF_DMA_FDD_RAM` | 0x29 | FDD → RAM |
| `TSCONF_DMA_RAM_WTP` | 0xB9 | RAM → Wait Port |
| `TSCONF_DMA_WTP_RAM` | 0x39 | Wait Port → RAM |
### Флаги DMA
| Константа | Значение | Описание |
|-----------|----------|----------|
| `TSCONF_DMA_ASZ_256` | 0x00 | Выравнивание 256 байт |
| `TSCONF_DMA_ASZ_512` | 0x04 | Выравнивание 512 байт |
| `TSCONF_DMA_D_ALGN` | 0x08 | Выравнивание приёмника |
| `TSCONF_DMA_S_ALGN` | 0x10 | Выравнивание источника |
| `TSCONF_DMA_ACT` | 0x80 | DMA active (бит статуса при чтении) |
### Wait Port Device (#25AF)
| Константа | Значение | Описание |
|-----------|----------|----------|
| `TSCONF_DMAWP_GLUCLOCK` | 0x00 | GluClock |
| `TSCONF_DMAWP_COMPORT` | 0x01 | COM-порт |
### Основные регистры DMA
> Порты DMA описаны выше в таблице портов. Основные:
| Регистр | Порт | Описание |
|---------|------|----------|
| DMA_SAL/H/X | `#19AF`/`#1AAF`/`#1BAF` | Адрес + страница источника |
| DMA_DAL/H/X | `#1CAF`/`#1DAF`/`#1EAF` | Адрес + страница приёмника |
| DMA_LEN | `#26AF` | Размер бёрста (N) |
| DMA_NUM/H | `#28AF`/`#2CAF` | Количество бёрстов (T) |
| DMA_CTRL | `#27AF` | Режим + запуск (W) / статус (R) |
### Пример DMA через WC API (C)
```c
// Копирование 4 КБ из стр. 0 плагина (#8000) в стр. 1 (#8000)
wc_dmapl(WC_DMA_INIT_SD); // потребует B,C,HL,DE в регистрах
wc_dmapl(WC_DMA_SET_TN); // B=бёрсты, C=размер бёрста
wc_dmapl(WC_DMA_RUN_RAM); // запуск RAM→RAM с ожиданием
```
---
## Использование портов из SDCC
`tsconf.h` объявляет все порты как `__sfr __banked` переменные для
прямого ввода/вывода без `__asm`:
```c
// Примеры:
sfr_page2 = 5; // OUT (#12AF), 5
sfr_sysconfig = TSCONF_CPU_14MHZ; // OUT (#20AF), 2
sfr_border = 2; // OUT (#0FAF), 2
sfr_opl3_addr0 = 0x20; // OUT (#00C4), 0x20
sfr_opl3_data0 = 0xFF; // OUT (#00C5), 0xFF
// Чтение:
uint8_t page = sfr_page2; // IN A,(#12AF)
```
Полный список: `sfr_vconfig`, `sfr_vpage`, `sfr_gxoffsl/h`, `sfr_gyoffsl/h`,
`sfr_tsconfig`, `sfr_palsel`, `sfr_border`, `sfr_page0`–`sfr_page3`,
`sfr_sysconfig`, `sfr_memconfig`, `sfr_intmask`, `sfr_cacheconfig`,
`sfr_dmasaddrh/l/x`, `sfr_dmadaddrh/l/x`, `sfr_dmalen`, `sfr_dmactrl`,
`sfr_dmanum/h`, `sfr_opl3_addr0/data0/addr1/data1`, `sfr_ay_addr/data`,
`sfr_saa_addr/data`, `sfr_saa2_addr/data`.
## Видео система
### Видео режимы (VConfig #00AF)
| Режим | Константа | Разрешения | Цвета |
|-------|-----------|------------|-------|
| ZX | `TSCONF_VM_ZX` (0x00) | 256×192 | 16 (атрибуты) |
| 16c | `TSCONF_VM_16C` (0x01) | 256..360 | 16 (4 bpp) |
| 256c | `TSCONF_VM_256C` (0x02) | 256..360 | 256 (8 bpp) |
| TXT | `TSCONF_VM_TXT` (0x03) | 80×25, 80×30, 90×36 | 256 |
**Разрешения:**
| Константа | Значение | Пиксели |
|-----------|----------|---------|
| `TSCONF_RRES_256x192` | 0x00 | 256×192 |
| `TSCONF_RRES_320x200` | 0x40 | 320×200 |
| `TSCONF_RRES_320x240` | 0x80 | 320×240 |
| `TSCONF_RRES_360x288` | 0xC0 | 360×288 |
**Прочие биты VConfig:**
| Константа | Бит | Описание |
|-----------|-----|----------|
| `TSCONF_VC_FT_EN` | 2 | FT812 enable (VDAC2) |
| `TSCONF_VC_GFXOVR` | 3 | GFX over TSU |
| `TSCONF_VC_NOTSU` | 4 | Отключить TSU graphics |
| `TSCONF_VC_NOGFX` | 5 | Отключить main graphics |
**Чтение VConfig (статус):**
| Константа | Значение | Описание |
|-----------|----------|----------|
| `TSCONF_VDVER_2BIT_PWM` | 0x00 | 2-бит ЦАП (ШИМ) |
| `TSCONF_VDVER_3BIT` | 0x01 | 3-бит |
| `TSCONF_VDVER_4BIT` | 0x02 | 4-бит |
| `TSCONF_VDVER_5BIT` | 0x03 | 5-бит |
| `TSCONF_VDVER_VDAC2` | 0x07 | VDAC2 / FT812 |
| `TSCONF_STATUS_PWR_UP` | 0x40 | Після включення |
### Палитра
256 ячеек палитры, формат RGB555 (15 бит цвета).
### Спрайты
- До 85 спрайтов на строку
- Размеры: 8x8 до 64x64 пикселей
- До 3 плоскостей спрайтов
- До 16 палитр на строку
## Совместимость с Pentagon-128
### Порт #7FFD
```
Биты:
7:6 - дополнительные биты страницы (512K/1024K)
5 - LOCK (блокировка переключения)
4 - ROM128 (выбор ROM)
3 - SCR (экран 5 или 7)
2:0 - номер страницы для #C000
```
### Режимы совместимости (MemConfig [7:6])
| LCK128 | Режим | Описание |
|--------|-------|----------|
| 00 | Pentagon-512 | `TSCONF_MEM_LCK128_512K` |
| 01 | Pentagon-128 | `TSCONF_MEM_LCK128_128K` |
| 10 | Авто | `TSCONF_MEM_LCK128_AUTO` |
| 11 | 1024 КБ | `TSCONF_MEM_LCK128_1M` |
---
## FDD виртуализация (#29AF)
TSConfig позволяет виртуализировать FDD-приводы (для TRD/TAP):
| Константа | Значение | Описание |
|-----------|----------|----------|
| `TSCONF_FDD_A` | 0x01 | Привод A |
| `TSCONF_FDD_B` | 0x02 | Привод B |
| `TSCONF_FDD_C` | 0x04 | Привод C |
| `TSCONF_FDD_D` | 0x08 | Привод D |
| `TSCONF_FDD_VG_OPEN` | 0x80 | Открыть VG93 |
---
## Файловая система
WC работает через FAT32:
- **SD-карты**: Z-Controller (основной), NeoGS SD
- **IDE**: NEMO (Master/Slave), SMUC (Master/Slave)
- **Длинные имена**: до 255 символов (с WC v0.73)
- **Каталоги**: >4095 записей (с WC v1.05)
---
## Полезные ссылки
- TSConfig спецификация: https://github.com/tslabs/zx-evo
- Регистры TSConfig: [src_sdcc/inc/tsconf.h](../src_sdcc/inc/tsconf.h)
- WC API: [src_sdcc/inc/wc_api.h](../src_sdcc/inc/wc_api.h)
- ISR конфигурация: [src_sdcc/inc/variant_cfg.h](../src_sdcc/inc/variant_cfg.h)
## Пример инициализации для плагина
```asm
; Базовая инициализация для музыкального плагина
INIT_PLUGIN:
; Сохранить текущее состояние
LD A,(#6002) ; Страница в окне 2
PUSH AF
LD A,(#6003) ; Страница в окне 3
PUSH AF
; Восстановить TXT режим
LD A,#0F
CALL WLD ; GEDPL
; Установить частоту CPU
LD B,#00 ; Z80 frequency
LD C,%01 ; 7 MHz
LD A,#0E
CALL WLD ; TURBOPL
; Настроить прерывания
LD HL,INT_HANDLER
EXA
LD A,#FF
CALL WLD ; INT_PL
EXA
; Инициализация OPL3
CALL INIT_OPL3
RET
; Обработчик прерываний
INT_HANDLER:
PUSH AF, BC, DE, HL
; Ваш код обработки музыки
CALL UPDATE_MUSIC
POP HL, DE, BC, AF
EI
RETI
; Инициализация OPL3
INIT_OPL3:
; Сброс OPL3
LD BC,#C4
XOR A
OUT (C),A
LD B,#C5
OUT (C),A
; Дополнительная инициализация...
RET
```
+293
View File
@@ -0,0 +1,293 @@
# VGM Format Reference
> **Источник истины:** `src_sdcc/inc/vgm.h`, `src_sdcc/lib/vgm.c`
## Обзор формата VGM
VGM (Video Game Music) — формат хранения логов регистров звуковых чипов.
### Основные характеристики
- Расширение файла: **.vgm** (raw) или **.vgz** (gzip-сжатый)
- Содержит команды записи в регистры звуковых чипов
- Поддерживаемые плагином чипы: **YMF262 (OPL3), YM3812 (OPL2), YM3526 (OPL1), Y8950, AY-3-8910/YM2149/YM2203, SAA1099**
- Частота семплирования фиксирована: **44100 Гц**
## Структура заголовка VGM (ключевые смещения)
> Все поля — little-endian 32 бит, если не указано иное.
> Минимальная поддерживаемая версия: **1.00** (`0x00000100`).
| Offset | Размер | Название | Описание | С версии |
|--------|--------|------------------|------------------------------------------------|----------|
| `0x00` | 4 | Ident | Сигнатура `"Vgm "` (0x56 0x67 0x6D 0x20) | 1.00 |
| `0x04` | 4 | EoF offset | Расстояние до EOF от позиции 0x04 | 1.00 |
| `0x08` | 4 | Version | BCD: `0x0150`=v1.50, `0x0151`=v1.51 | 1.00 |
| `0x0C` | 4 | SN76489 clock | Тактовая частота SN76489 (0 = нет) | 1.00 |
| `0x10` | 4 | YM2413 clock | Тактовая частота YM2413 / OPLL (0 = нет) | 1.00 |
| `0x14` | 4 | GD3 offset | Смещение GD3-тега от `0x14` (0 = нет тега) | 1.00 |
| `0x18` | 4 | Total samples | Полная длина трека в семплах (÷44100 = сек) | 1.00 |
| `0x1C` | 4 | Loop offset | Смещение точки петли от `0x1C` (0 = нет) | 1.00 |
| `0x20` | 4 | Loop samples | Длина тела петли в семплах | 1.00 |
| `0x24` | 4 | Rate | Кадровая частота (50/60 Гц, информативно) | 1.01 |
| `0x34` | 4 | **VGM data offset** | Смещение VGM-данных от `0x34`; 0 → данные с `0x40` | **1.50** |
| `0x44` | 4 | YM2203 clock | **YM2203 (OPN/PSG)** (0 = нет) | 1.51 |
| `0x50` | 4 | **YM3812 clock** | **YM3812 (OPL2)** — AdLib/SoundBlaster | **1.51** |
| `0x54` | 4 | **YM3526 clock** | **YM3526 (OPL1)** — ISA AdLib | **1.51** |
| `0x58` | 4 | **Y8950 clock** | **Y8950 (OPL+ADPCM)** — MSX-Audio | **1.51** |
| `0x5C` | 4 | **YMF262 clock** | **YMF262 (OPL3)** — SB Pro 2 / TSConfig | **1.51** |
| `0x60` | 4 | YMF278B clock | YMF278B (OPL4) (0 = нет) | 1.51 |
| `0x74` | 4 | AY8910 clock | **AY-3-8910 / YM2149** (0 = нет) | 1.51 |
| `0xC8` | 4 | SAA1099 clock | **SAA1099** — SAM Coupé (0 = нет) | 1.71 |
> ⚠️ **Частая ошибка в документации:** смещения чипов в диапазоне `0x50`–`0x5C` — не `0x5C`/`0x60`/`0x64`/`0x68`.
> Используйте константы: `VGM_OFF_YM3812=0x50`, `VGM_OFF_YM3526=0x54`, `VGM_OFF_Y8950=0x58`, `VGM_OFF_YMF262=0x5C`.
### Смещение данных VGM
```
Если заголовок[0x34] (VGM data offset) == 0:
данные начинаются с абсолютного адреса 0x40 (старые файлы, < v1.50)
Иначе:
данные начинаются с 0x34 + заголовок[0x34]
```
Типичное значение для v1.51+: `VGM_data_offset = 0x4C`, данные с `0x80`.
Реализация в `vgm_parse_header()`: хранит готовый адрес начала данных в `vgm_data_start`.
### Bit 30 поля clock — флаг dual-chip
Бит 30 любого clock-поля (`clock & 0x40000000`) означает **два экземпляра** чипа (`VGM_CF_DUAL`).
Реальная тактовая частота = `clock & 0x3FFFFFFF`.
### Обнаружение чипов — `vgm_parse_header()`
Приоритет определения `vgm_chip_type`: **YMF262 > YM3812 > YM3526/Y8950**:
```c
// vgm.h — константы
#define VGM_OFF_YM3812 0x50 // OPL2
#define VGM_OFF_YM3526 0x54 // OPL1
#define VGM_OFF_Y8950 0x58 // OPL+ADPCM
#define VGM_OFF_YMF262 0x5C // OPL3
#define VGM_OFF_AY8910 0x74 // AY-3-8910 / YM2149
#define VGM_OFF_YM2203 0x44 // OPN (PSG-часть = AY)
#define VGM_OFF_SAA1099 0xC8 // SAA1099
// Значения vgm_chip_type
#define VGM_CHIP_NONE 0
#define VGM_CHIP_OPL 1 // YM3526 или Y8950
#define VGM_CHIP_OPL2 2 // YM3812
#define VGM_CHIP_OPL3 3 // YMF262
```
## VGM-команды — полная таблица
| Код | Байт | Параметры | Описание |
|--------------|------|-----------|-----------------------------------------------|
| `0x55` | 3 | `rr dd` | YM2203 write (PSG-часть, reg 0–15) |
| `0x5A` | 3 | `rr dd` | YM3812 (OPL2) write |
| `0x5B` | 3 | `rr dd` | YM3526 (OPL1) write |
| `0x5C` | 3 | `rr dd` | Y8950 (OPL+ADPCM) write |
| `0x5E` | 3 | `rr dd` | YMF262 (OPL3) Bank 0 write |
| `0x5F` | 3 | `rr dd` | YMF262 (OPL3) Bank 1 write |
| `0x61` | 3 | `lo hi` | Задержка: `(hi<<8)\|lo` семплов |
| `0x62` | 1 | — | Задержка 735 семплов (1/60 с) |
| `0x63` | 1 | — | Задержка 882 семпла (1/50 с) |
| `0x66` | 1 | — | End of data → конец потока / loop |
| `0x67` | var | — | Data block (PCM и пр.) — пропускается |
| `0x70`–`0x7F`| 1 | — | Задержка 1–16 семплов (`(cmd & 0x0F) + 1`) |
| `0xA0` | 3 | `rr dd` | AY8910 write; бит 7 `rr` = чип 2 |
| `0xBD` | 3 | `rr dd` | SAA1099 write; бит 7 `rr` = чип 2 |
| `0xD0` | 4 | `pp aa dd`| YMF278B (OPL4) write, порт `pp` |
Прочие команды пропускаются по таблице длин VGM spec (размер 1, 2, или 3 байта).
### `0xD0` — YMF278B (OPL4) write
```
0xD0 pp aa dd
```
- `pp` бит 7 — выбор чипа (dual-chip); игнорируется, на ZXM-MoonSound один чип.
- `pp & 0x7F` — порт:
- `0` — FM Bank 0 (те же регистры/порты #C4-#C5, что и `0x5E` для OPL3)
- `1` — FM Bank 1 (те же регистры/порты #C6-#C7, что и `0x5F` для OPL3)
- `2` — wave-часть чипа (табличный синтез), порты #7E (адрес) / #7F (данные)
- `aa` — номер регистра, `dd` — значение.
Чтобы чип реально принимал запись в порт 2 (wave-часть), в Bank 1
регистре `0x05` должен быть установлен бит NEW2 (бит 1), в дополнение к
обычному биту NEW (бит 0) — см. `CMDBLK_INIT_OPL4` в `cmdblocks.asm`.
Без NEW2 запись в порты #7E/#7F реальным чипом YMF278B игнорируется.
---
## Масштабирование задержек
VGM sample rate = **44100 Гц**, ISR rate = **1367 Гц** (= 44100 / 32):
```c
// inc/isr.h
#define VGM_SAMPLE_SHIFT 5 // делитель = 2^5 = 32
#define VGM_SAMPLE_MASK 0x1Fu // маска остатка
```
Алгоритм `vgm_fill_buffer()`:
- Накапливает семплы задержек
- При накоплении ≥ 32 → конвертирует `samples >> 5` в ISR-тики → команда `CMD_WAIT`
- Задержки < 32 семплов (< 0.73 мс) **теряются** (rounded down, это нормально)
```
1 секунда = 44100 семплов = 1378 ISR-тиков ≈ 1367 Гц
1 фрейм (50Hz) = 882 семпла = 27 ISR-тиков
1 фрейм (60Hz) = 735 семплов = 22 ISR-тика
```
---
## GD3 — метаданные трека
GD3-тег содержит Unicode-строки (UCS-2 LE, нуль-терминированные):
| № | Содержимое |
|---|--------------------|
| 1 | Track title (EN) |
| 2 | Track title (JP) |
| 3 | Game name (EN) |
| 4 | Game name (JP) |
| 5 | System name (EN) |
| 6 | System name (JP) |
| 7 | Author (EN) |
| 8 | Author (JP) |
| 9 | Release date |
|10 | VGM pack author |
|11 | Notes |
В плагине читаются только **английские** поля (нечётные), конвертируются в ASCII: символы > 127 → `'?'`.
Макс. длина поля: `VGM_GD3_LEN = 48` байт.
```c
// inc/vgm.h
extern char vgm_gd3_track[VGM_GD3_LEN];
extern char vgm_gd3_game[VGM_GD3_LEN];
extern char vgm_gd3_system[VGM_GD3_LEN];
extern char vgm_gd3_author[VGM_GD3_LEN];
```
---
## Петля (loop)
```c
// inc/vgm.h
extern uint16_t vgm_loop_addr; // адрес точки петли в окне #C000 (0 = нет)
extern uint8_t vgm_loop_page; // VPL-страница точки петли
```
Если `loop_offset (0x1C) != 0`:
- Абсолютное смещение в файле = `0x1C + loop_offset`
- `vgm_rewind_to_loop()` переставляет `vgm_read_ptr` / `vgm_cur_page` на точку петли
---
## VGZ — gzip-сжатый VGM
Файл `.vgz` — стандартный gzip. Схема обработки в плагине:
```
1. Загрузить .vgz в мегабуфер (VPL-страницы выше #20)
2. copy_pages_to_tap(n) — скопировать во временную TAP-область (#A1–#BF, 31 стр.)
3. inflate_vgz(src, dst) — распаковать (inflate.asm) обратно в мегабуфер
4. Работать с VGM как обычно
```
Макс. размер распакованного VGM через TAP: 31 × 16 КБ = **496 КБ**.
```c
void copy_pages_to_tap(uint8_t n_pages);
uint8_t inflate_vgz(uint8_t src_page, uint8_t dst_page);
// возвращает: 0 = успех, 1 = ошибка (не gzip или CRC-ошибка)
```
---
## OPL2 vs OPL3 — проблема L/R output
**Проблема:** `opl3_init()` устанавливает `NEW=1` (OPL3 mode).
В OPL3 mode биты 4–5 регистров `C0–C8` управляют маршрутизацией L/R (`L=1, R=1` для звука).
Файлы OPL2/OPL1 (команды `0x5A`/`0x5B`) пишут `C0` без битов L/R → **тишина**.
**Решение в `start_playback()` (main.c):**
```c
opl3_init(); // NEW=1 по умолчанию
if (vgm_chip_type != VGM_CHIP_OPL3) {
opl3_write_b1(0x05, 0x00); // NEW=0: OPL2-совместимый режим
}
// В OPL2-compat звук всегда идёт на оба канала L+R
```
---
## Пример парсинга заголовка (SDCC C)
```c
#include "inc/vgm.h"
#include "inc/wc_api.h"
// Загрузить первую страницу файла в окно #C000 и разобрать заголовок
wc_mngcvpl(0); // Переключить Win3 (#C000) на VPL-страницу 0
wc_load512(0xC000, 32); // Загрузить 16 КБ с диска
uint8_t rc = vgm_parse_header();
switch (rc) {
case VGM_OK:
// vgm_chip_type → VGM_CHIP_OPL / OPL2 / OPL3
// vgm_chip_count → кол-во обнаруженных чипов
// vgm_version → 0x0151 для v1.51
// vgm_loop_addr → 0 = нет петли
break;
case VGM_ERR_HEADER:
// Нет сигнатуры "Vgm " или заголовок повреждён
break;
case VGM_ERR_NOOPL:
// Файл не содержит поддерживаемого чипа (OPL/AY/SAA)
break;
}
```
---
## Полезные константы (из `vgm.h`)
```c
// Смещения clock-полей в заголовке (relative byte offsets)
#define VGM_OFF_SN76489 0x0C
#define VGM_OFF_YM2413 0x10
#define VGM_OFF_YM2203 0x44 // OPN
#define VGM_OFF_YM3812 0x50 // OPL2 ← основной
#define VGM_OFF_YM3526 0x54 // OPL1
#define VGM_OFF_Y8950 0x58 // MSX-Audio
#define VGM_OFF_YMF262 0x5C // OPL3 ← основной
#define VGM_OFF_YMF278B 0x60 // OPL4
#define VGM_OFF_AY8910 0x74 // AY-3-8910 / YM2149
#define VGM_OFF_SAA1099 0xC8 // SAA1099
// Коды результата vgm_parse_header()
#define VGM_OK 0
#define VGM_ERR_HEADER 1
#define VGM_ERR_NOOPL 2
// ISR / VGM sample rate mapping
#define VGM_SAMPLE_SHIFT 5 // 44100 / 32 ≈ 1367 Гц (ISR_FREQ)
```
---
## Ссылки
- VGM Format Specification: https://vgmrips.net/wiki/VGM_Specification
- YMF262 (OPL3) Datasheet: Yamaha YMF262 Application Manual
- Архив VGM-треков: https://vgmrips.net/
+475
View File
@@ -0,0 +1,475 @@
# Пример простого плагина для WC
## Минимальный плагин "Hello World"
Этот пример показывает базовую структуру плагина для Wild Commander.
```asm
;---------------------------------------
; Hello World Plugin
; Простейший пример плагина для WC
;---------------------------------------
ORG #6000
; Константы WC API
WLD EQU #6006 ; Точка вызова API
ABT EQU #6004 ; Флаг ESC
ENT EQU #6005 ; Флаг ENTER
;---------------------------------------
; ЗАГОЛОВОК ПЛАГИНА (512 байт)
;---------------------------------------
; +0: Резерв (16 байт)
DS 16
; +16: Идентификатор (16 байт)
DB "WildCommanderMDL"
; +32: Версия формата
DB #10
; +33: Резерв
DB 0
; +34: Количество страниц
DB 1 ; Плагин занимает 1 страницу
; +35: Стартовая страница
DB 0 ; Запускаться будет со страницы 0
; +36: Блоки данных (6 x 2 байта)
DB 0,1 ; Блок 0: код, 1x512 байт
DB 0,0
DB 0,0
DB 0,0
DB 0,0
DB 0,0
; +48: Резерв (15 байт)
DS 15
; +63: Флаги расширений
DB 0 ; Стандартный режим
; +64: Блок расширений (32 x 3 байта)
DS 32*3 ; Не используем расширения
; +160: Конец блока расширений
DB 0
; +161: Максимальный размер файла (4 байта)
DW #FFFF,#FFFF ; Без ограничений
; +165: Имя плагина (32 байта)
DB "Hello World Plugin "
; +197: Тип условий вызова
DB #03 ; Из меню (F10)
; +198: Текст для F2/F4 (6 байт)
DS 6
; +204: Текст для меню вьювера (24 байта)
DS 24
; +228: Резерв (32 байта)
DS 32
; +260: Параметры INI (252 байта)
DB 0 ; Нет параметров
DS 251
;---------------------------------------
; КОД ПЛАГИНА
;---------------------------------------
ORG #8000,#6200
;---------------------------------------
; Точка входа плагина
;---------------------------------------
PLUGIN:
; Сохранить входные параметры
LD (SAVE_IX),IX
LD (SAVE_HL),HL
LD (SAVE_DE),DE
LD (SAVE_BC),BC
LD (SAVE_A),A
; Восстановить текстовый режим
CALL GEDPL
; Показать окно
LD IX,WINDOW
CALL PRWOW
; Вывести текст
LD HL,TEXT
LD DE,#0102 ; Y=1, X=2
LD BC,12 ; Длина
CALL PRSRW
; Применить цвет
EXA
LD A,%00001111 ; Синий на белом
CALL PRIAT
EXA
; Главный цикл
MAIN_LOOP:
EI
HALT ; Ждем прерывание
; Проверка ESC
CALL ESC_KEY
JR Z,MAIN_LOOP
; Выход
LD IX,WINDOW
CALL RRESB ; Стереть окно
XOR A ; A=0: просто выход
RET
;---------------------------------------
; WC API функции
;---------------------------------------
PRWOW: ; Вывод окна
LD A,1
JP WLD
RRESB: ; Стирание окна
LD A,2
JP WLD
PRSRW: ; Печать строки
LD A,3
JP WLD
PRIAT: ; Установка цвета
EXA
LD A,4
JP WLD
ESC_KEY: ; Проверка ESC
LD A,#17
JP WLD
GEDPL: ; Восстановление TXT режима
LD A,#0F
JP WLD
;---------------------------------------
; Данные
;---------------------------------------
; Структура окна
WINDOW:
DB 0 ; Флаги
DB 0 ; Маска курсора
DB 255 ; X (центр)
DB 255 ; Y (центр)
DB 30 ; Ширина
DB 5 ; Высота
DB %01111111 ; Цвет (белый на синем)
DB 0 ; Резерв
DW #0000 ; Буфер
DB 0,0 ; Разделители
; Текст
TEXT:
DB "Hello World!"
; Сохраненные регистры
SAVE_A: DB 0
SAVE_BC: DW 0
SAVE_DE: DW 0
SAVE_HL: DW 0
SAVE_IX: DW 0
END
```
## Компиляция
Используйте sjasm для компиляции:
```batch
sjasm hello.asm hello.wmf
```
## Пояснения к коду
### Структура
1. **Заголовок (#6000-#61FF)** - 512 байт метаданных
2. **Код (#8000+)** - исполняемый код плагина
### Ключевые моменты
1. **GEDPL обязателен!** Вызывайте его в начале, чтобы восстановить текстовый режим.
2. **Структура окна** должна быть в памяти плагина (#8000-#BFFF).
3. **Главный цикл** должен включать:
- `EI` - разрешить прерывания
- `HALT` - ждать прерывание (экономит энергию)
- Проверку клавиш
4. **Выход** через `RET` с кодом в A.
## Пример с файлом
```asm
;---------------------------------------
; Text Viewer Plugin - просмотр текстовых файлов
;---------------------------------------
ORG #6000
WLD EQU #6006
; ... Заголовок плагина ...
; +64: Расширения
DB "TXT" ; .txt файлы
DB "DOC" ; .doc файлы
DS 30*3
; +197: Тип вызова
DB #00 ; По расширению
ORG #8000,#6200
;---------------------------------------
; Точка входа
;---------------------------------------
PLUGIN:
; Проверить тип вызова
CP #00 ; По расширению?
JR Z,VIEW_FILE
; Другой тип вызова - не наш файл
LD A,1 ; Файл не опознан
RET
VIEW_FILE:
; Сохранить параметры
LD (FILESIZE),HL
LD (FILESIZE+2),DE
LD (FILENAME),BC
LD (PANEL_IX),IX
; Восстановить TXT режим
CALL GEDPL
; Загрузить начало файла
LD HL,BUFFER
LD B,16 ; 16 блоков = 8KB
CALL LOAD512
; Показать окно
LD IX,WINDOW
CALL PRWOW
; Вывести текст
LD HL,BUFFER
LD DE,#0101
LD BC,1000
CALL PRSRW
; Цикл просмотра
VIEW_LOOP:
EI
HALT
CALL ESC_KEY
JR Z,VIEW_LOOP
; Выход
LD IX,WINDOW
CALL RRESB
XOR A
RET
;---------------------------------------
; WC API
;---------------------------------------
LOAD512: LD A,#30
JP WLD
PRWOW: LD A,1
JP WLD
RRESB: LD A,2
JP WLD
PRSRW: LD A,3
JP WLD
ESC_KEY: LD A,#17
JP WLD
GEDPL: LD A,#0F
JP WLD
;---------------------------------------
; Данные
;---------------------------------------
WINDOW:
DB 0
DB 0
DB 0 ; X=0
DB 0 ; Y=0
DB 80 ; На весь экран
DB 25
DB %01111111
DB 0
DW #0000
DB 0,0
BUFFER: DS 8192 ; Буфер для файла
FILENAME: DW 0
FILESIZE: DS 4
PANEL_IX: DW 0
END
```
## Следующий шаг
Изучите примеры в каталоге PlugRef/:
- **wild_player/** - полноценный музыкальный плеер
- **gsplayer/** - плеер для GS
- **txt_editor/** - редактор текста
---
## Минимальный плагин на SDCC C
> Компилятор: SDCC 4.x, ключи `--opt-code-size -mz80 --sdcccall 1`
> Источник соглашений: `src_sdcc/inc/wc_api.h`
```c
/*
* hello_plugin.c — минимальный WC-плагин на SDCC C
*
* Сборка:
* sdcc --opt-code-size -mz80 --sdcccall 1 --code-loc 0x8000 \
* --data-loc 0xB800 -I inc hello_plugin.c wc_api.s crt0.s
* makebin -s 16384 hello_plugin.ihx hello_plugin.bin
* pack hello_plugin.bin HELLO.WMF # добавить заголовок WMF
*/
#include "inc/wc_api.h"
#include "inc/types.h"
/* ─────────────────────────────────────────────────
* DATA: размещается в #B800+ (--data-loc 0xB800)
* ──────────────────────────────────────────────── */
/* Окно в секции DATA (адрес в #8000–#BFFF — обязательно!) */
static wc_window_t g_win;
/* Буфер вывода */
static char g_buf[64];
/*
* wc_exit_code — специальная переменная: WC читает A при
* выходе из main(). Значения: 0=ESC, 2=след. файл, 3=reload dir.
*/
uint8_t wc_exit_code;
/* ─────────────────────────────────────────────────
* CODE: размещается в #8000+ (--code-loc 0x8000)
* ──────────────────────────────────────────────── */
int main(void) {
/* 1. Восстановить дисплей WC (TXT-режим, панели)
* ⚠ Затирает #1000–#1447 в окне 0 */
wc_gedpl();
/* 2. Настроить окно */
g_win.type = WC_WIN_SINGLE; /* однолинейная рамка */
g_win.cur_mask = 0x07;
g_win.x = 255; /* 255 = по центру */
g_win.y = 255;
g_win.width = 44;
g_win.height = 6;
g_win.color = WC_COLOR(WC_BLACK, WC_BRIGHT_WHITE);
g_win.buf_addr = 0x0000; /* 0 = автосохранение фона в RESB */
/* 3. Нарисовать окно */
wc_prwow(&g_win);
/* 4. Вывести текст (разметка WC: 0x0E = центрирование) */
wc_txtpr(&g_win, "\x0eHello from C plugin!", 1, 1);
wc_txtpr(&g_win, "\x0e\x03Press ESC to exit", 3, 1);
/* 5. Главный цикл */
while (1) {
if (wc_key_esc()) break;
if (wc_key_enter()) break;
/* пример: цикл ожидания клавиши без блокировки */
/* uint8_t k = wc_kbscn(WC_KBSCN_NORMAL); */
}
/* 6. Убрать окно и восстановить дисплей */
wc_rresb(&g_win);
wc_gedpl();
/* 7. Код выхода */
wc_exit_code = 0; /* 0 = нормальный выход */
return 0;
}
```
### Полезные паттерны
**Загрузка файла в мегабуфер:**
```c
void load_file_to_megabuf(void) {
uint8_t page = 0;
uint8_t rc;
wc_stream(WC_STREAM_ROOT); // открыть файловый поток
// wc_fentry() уже вызван WC перед стартом плагина
do {
/* ⚠ После любого wc_* Win3 сбрасывается — переключаем снова */
wc_mngcvpl(page);
rc = wc_load512(0xC000, 32); // читать 16 КБ
page++;
} while (rc == 0 && page < 64);
/* rc == WC_EOF (0x0F) = конец файла, rc != 0 && != 0x0F = ошибка */
}
```
**Установка собственного ISR:**
```c
extern void my_isr(void); // ISR написан на ASM в asm/isr.s
void setup_isr(void) {
wc_int_pl(WC_INT_PLUGIN); // режим плагина
wc_int_pl_handler((uint16_t)my_isr); // задать адрес
/* Теперь my_isr() вызывается ~1367 раз/сек */
}
void restore_isr(void) {
wc_int_pl(WC_INT_DISABLE_ALL); // вернуть управление WC
}
```
**Переключение CPU на 14 МГц:**
```c
wc_turbopl(WC_TURBO_CPU, 0x02); // 14 МГц
// ...работа...
wc_turbopl(WC_TURBO_RESTORE, 0); // восстановить
```
+147
View File
@@ -0,0 +1,147 @@
# TODO: Потоковая (streaming) распаковка inflate
**Дата:** 2026-03-28
**Статус:** Идея / Анализ завершён, реализация отложена
## Цель
Заменить текущую полную предварительную распаковку VGZ → мегабуфер
на потоковую (чанковую) распаковку с прерываниями EI, что даст:
- Мгновенный старт воспроизведения (не ждать 2–10 сек)
- Снятие ограничения 1 МБ (мегабуфер) на размер raw VGM
- Возможность играть VGZ файлы > 496 КБ (TAP) через кольцевой буфер
## Ключевые находки
### 1. ISR не использует Win 0 / Win 1 / Win 3
ISR-хэндлер и все данные (cmd_buf_a/b, isr_read_ptr, pos_ptr) живут
исключительно в Win 2 (0x8000–0xBFFF, _CODE + _DATA). Это означает:
**Win 0, Win 1, Win 3 можно свободно переключать при EI.**
### 2. Раскладка окон во время inflate-чанка
| Win | Адреса | Содержимое | Кто использует |
|-----|-----------------|---------------------------|--------------------------|
| 0 | 0x0000–0x3FFF | Source VGZ page (ротация) | inflate: чтение HL' |
| 1 | 0x4000–0x7FFF | Decode buffer (PAGE_DECODE)| inflate: запись DE |
| 2 | 0x8000–0xBFFF | Player code + ISR + DATA | **НЕ ТРОГАЕМ**, ISR тут |
| 3 | 0xC000–0xFFFF | Inflate code + Huffman | inflate: исполнение |
Win 2 сейчас занят decode buffer (#EA) во время inflate.
Для streaming нужно перенести decode buffer из Win 2 в Win 1.
### 3. Self-modifying code = бесплатное сохранение состояния
Все переменные inflate (`src_page`, `dst_page`, `dst`, `interim_dst`,
`this_was_last_block`, Huffman таблицы ~10 КБ) — self-modifying code
на inflate-странице (Win 3). При unmap/remap страницы значения
автоматически сохраняются. Стек (SP = 0xFFF0, адреса возврата
inflate_core → blocktype2 → decode_tableloop) тоже на inflate-странице.
**Нужно сохранять только CPU-регистры (18 байт):**
- AF, BC, DE, HL (8 байт main regs)
- AF', BC', DE', HL' (8 байт alt regs — bit-stream state!)
- IX (2 байта)
- SP хранится в C-переменной caller
### 4. DMA вместо LDIR для page flush
Текущий `copy_decoded_page` (LDIR 16 КБ) = **344064 T** ≈ 49 мс @ 7 МГц.
DMA RAM→RAM (TSCONF_DMA_RAM_RAM = 0x09): **~8000–16000 T** (20–40× быстрее).
Адресация: 22-bit [page(8)][H(6)][L(7)][0].
DMA копирует страницу→страницу без участия CPU и без маппинга окон.
Двойной буфер DMA: пока DMA копирует страницу N, inflate пишет в страницу N+1.
Не нужно ждать флаг TSCONF_DMA_ACT.
### 5. Backref: DMA для cross-page, LDIR для intra-page
~80–90% backrefs внутри текущего буфера → LDIR из Win 1 в Win 1.
Cross-page backrefs (distance > current_offset): DMA из физической
страницы history → физическую страницу decode buffer. Окна не трогаем.
Исключение: overlapping backrefs (distance < length, паттерн "repeat")
DMA не гарантирует побайтную семантику → fallback: temp remap Win 0 = history,
LDIR, restore Win 0 = src.
### 6. Overflow 258 байт — решение для Win 1
Если decode buffer в Win 1 (0x4000–0x7FFF), overflow LDIR пойдёт
в Win 2 (player code). **Нельзя.**
Решение: split-copy перед LDIR. Проверить DE + BC > 0x8000?
Если да — LDIR до границы, DMA flush, сброс DE = 0x4000, LDIR остаток.
Overhead: ~30 T на backref (проверка).
### 7. Yield point: decode_tableloop
Единственная разумная точка yield — вершина `decode_tableloop`.
Добавить yield_counter (self-mod var): dec → jp z, inflate_yield.
Caller задаёт количество 8-символьных батчей за чанк.
### 8. Оценка бюджета
| Параметр | Значение |
|----------------------------------|--------------------------|
| Потребление VGM (типичный) | ~2.5 КБ/сек (50 б/кадр) |
| Скорость inflate @ 7 МГц | ~50–100 КБ/сек |
| Нужный duty cycle | **2.5–5% CPU** |
| Overhead контекста (вход+выход) | ~600 T / 71680 T = 0.8% |
| Запас | 10–20× |
### 9. Кольцевой буфер выхода
```
Megabuffer pages: [P0][P1][P2][P3][P4][P5][P6][P7] ... ring
write_ptr → inflate пишет (producer)
read_ptr → player читает (consumer)
```
- Старт воспроизведения после первых 2–4 готовых страниц
- Нет ограничения 1 МБ: ring buffer кольцуется
- Max VGM raw: 496 КБ × 3 (сжатие) ≈ 1.5 МБ
## Контекст переключения (pseudo-code)
```asm
inflate_enter:
; Save Win 0/1/3 page numbers (from C vars or sys vars)
; OUT #10AF = src_page ; Win 0 = source VGZ
; OUT #11AF = PAGE_DECODE ; Win 1 = decode buffer
; OUT #13AF = inflate_page ; Win 3 = inflate code
; Restore saved CPU regs (18 bytes from save area on inflate page)
; JP resume_point
inflate_yield:
; Save CPU regs to save area on inflate page
; ld sp, (caller_sp)
; Restore Win 0/1/3 from saved values
; ret to main loop
```
## План реализации
1. [ ] Перенести decode buffer из Win 2 в Win 1
2. [ ] Добавить split-copy проверку для LDIR overflow в Win 1
3. [ ] Реализовать yield/resume в decode_tableloop
4. [ ] Заменить LDIR page flush на DMA
5. [ ] Реализовать DMA backref для cross-page
6. [ ] Добавить кольцевой буфер (producer/consumer) в main.c
7. [ ] Добавить inflate_enter / inflate_yield в inflate_call.s
8. [ ] Тестирование с файлами разных размеров и степеней сжатия
## Риски
- makeHuffmanTable: может занять десятки тысяч T-states (непредсказуемо).
При yield внутри него нужен глубокий стек save. Альтернатива:
не yield'ить во время построения таблиц, только во время decode.
- DMA overlap с ISR: ISR не использует DMA, конфликтов нет.
- Первый блок может быть dynamic Huffman → долгое построение таблиц перед
первыми декодированными данными. Mitigation: буферизовать 2–4 страницы
перед стартом воспроизведения.