148 lines
7.4 KiB
Markdown
148 lines
7.4 KiB
Markdown
# 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 страницы
|
||
перед стартом воспроизведения.
|