1
0
mirror of https://github.com/Tolik-Trek/DOOM2.git synced 2026-06-15 09:01:34 +03:00
DOOM2/INFO/doom-аксель.md

525 lines
29 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Руководство по «doom-акселю» Sprinter (z84c15)
Расширенная версия аппаратного акселератора Sprinter, использующая
встроенный в ПЛИС блок **аппаратного растяжения/сжатия линий** (масштабатор).
Документ составлен по результатам анализа исходников проекта DOOM2
(`DOOM2.asm`, `D2_FRAM.asm`, `Shared_Includes/macroses/accelerator.z80`,
`Shared_Includes/constants/SP2000.inc`, `Bin/table_x.tbl`).
> Конфигурация `Config_ID.Sp97_DOOM EQU #FFF9`, тип акселератора `3`
> (`SP2000.inc`, строки ~1573, 1611). Это не «обычный» аксель из
> `accelerator.z80`, а его надстройка с портом масштабирования `SCALE`.
---
## 1. Что это такое
«doom-аксель» — это автомат в ПЛИС, который перехватывает на шине Z80
определённые **недокументированные команды-«пустышки»** (`LD r,r`) и команды
обращения к памяти, идущие сразу за ними. По этим командам он:
* копирует/заполняет блоки памяти и видеопамяти быстрее, чем это делает
процессор покомандно;
* **на лету масштабирует линию** (растягивает или сжимает по вертикали)
при выводе в графический экран — это и есть «doom»-расширение.
Ключевой приём DOOM: вертикальный столбец текстуры стены (64 тексела)
аппаратно растягивается/сжимается в столбец произвольной экранной высоты за
один проход, без таблиц переходов и без покомандного цикла на Z80.
Принцип работы автомата:
1. команда-триггер (`LD r,r`) переводит аксель в нужный **режим**;
2. **следующая** команда обращения к памяти (`LD A,(HL)`, `LD (DE),A`,
`LD (HL),A`, `PUSH`, …) выполняется уже «через аксель» и обрабатывает
**весь блок**, а не один байт;
3. команда `ACC_Off` (`LD B,B`) возвращает аксель в обычный режим.
Между шагом 1 и шагом 2 можно ставить любые команды, **не обращающиеся к
памяти данных** (загрузка регистров константами, работа с портами, `OUT`,
`IN`, арифметика). Именно так в коде между триггером и «рабочей» командой
успевают задать порт `SCALE`, `PORT_Y`, страницу видео-ОЗУ и т.д.
### 1.1. Что именно «слушает» аксель
Аксель физически подключён к **шине данных и шине адреса** процессора.
* Команда-триггер распознаётся по сигналу **`M1`** (цикл выборки опкода):
как только по `M1` с шины считывается код одной из команд активации —
аксель переходит в соответствующий режим.
* Дальше, **в зависимости от режима**, он ловит:
* `ACC_SetBlockSize` → значение на **шине данных** (длину берёт из байта,
который пересылает следующая команда загрузки);
* команды пересылки/заливки (работа с буфером акселя) → адрес на
**шине адреса** (по нему он сам прогоняет блок по памяти).
### 1.2. ВАЖНО: аксель работает только с SIMM-памятью
Аксель умеет обрабатывать данные **только из физической динамической
SIMM-памяти** компьютера. **Fast-RAM** — это быстрый статический **кэш**,
который как источник/приёмник блочной пересылки акселя **не работает**.
Fast-RAM **не «прибита» к нулевому окну** — это переключаемый кэш
(подробно — `Shared_Includes/constants/SP2000.inc`, метка `FastRAM`):
* `IN A,(FastRAM.ON = #FB)`**включить** кэш (он появляется в нулевом
окне `#0000#3FFF`);
* `IN A,(FastRAM.OFF = #7B)` — **выключить** (в нулевом окне снова
ПЗУ/обычная память);
* `OUT (FastRAM.SLOT0 = #5C)` — выбор страницы кэша, `bit0..1`
(работает только при `SYS_PORT.ROM`).
То есть статический кэш присутствует в окне `#0000#3FFF` **только пока
включён** через `#FB`. Если в этот момент адресовать туда блочную
пересылку акселя — она **не сработает** (там не SIMM). Когда кэш выключен
(`#7B`), то же окно — обычная SIMM, и аксель с ним работает нормально.
Правило: операнд-источник/приёмник блочной пересылки не должен попадать
на **включённый** статический кэш.
Нюансы (важны для размещения кода):
* **Управлять** акселем из кода, исполняемого в Fast-RAM, **можно** — при
условии, что сама пересылка адресует SIMM (например, текстуры в страницах
SIMM и видео-ОЗУ в окне `#C000`).
* `ACC_SetBlockSize` из Fast-RAM работает **полноценно**, потому что длина
снимается с **шины данных** процессора, а не читается из памяти.
Именно поэтому в DOOM рендер (`D2_FRAM.asm`, `ORG #1000`, нулевое окно)
перебрасывается в Fast-RAM (`DOOM2.asm`: `IN A,(FastRAM.ON)``LDIR`
код по `#1000`) и работает быстро, но **все** блочные пересылки/заливки
акселя адресуют SIMM: текстуры стен в страницах SIMM, экран в окне
`#C000`. Буфер строкового Z-буфера и т.п. тоже лежат в SIMM.
---
## 2. Команды-триггеры (макросы `accelerator.z80`)
Все триггеры — это легальные опкоды Z80, которые без акселя являются
безвредными NOP-ами (`LD r,r`). Аксель ловит их по коду на шине.
| Макрос | Команда | Опкод | Назначение |
|---|---|---|---|
| `ACC_Off` | `LD B,B` | `#40` | выключить аксель (обязательно после каждой операции) |
| `ACC_SetBlockSize` | `LD D,D` | `#52` | след. загрузка задаёт длину блока (0 → 256) |
| `ACC_FillOneByte` | `LD C,C` | `#49` | заполнить блок одним байтом (линейные адреса) |
| `ACC_FillScreenOneByte`| `LD E,E` | `#5B` | заполнить вертикальную линию экрана одним байтом |
| `ACC_CopyBlock` | `LD L,L` | `#6D` | копирование блока через буфер акселя (линейно) |
| `ACC_CopyScreenBlock` | `LD A,A` | `#7F` | копирование блока в экран **верт. линиями + масштаб** |
| `ACC_DoubleByte` | `LD H,H` | `#64` | дублирование байта в (Addr&#FE) и (Addr&#FE)+1 |
| `ACC_Halt` | `HALT` | `#76` | зарезервировано (ведёт себя как `LD B,B`) |
Важные следствия:
* `ACC_Off` нужен после **каждой** акселерированной операции. Без него
следующее обращение к памяти будет интерпретировано акселем.
* Опкоды `#40/#7F/#6D/...` — это `LD B,B`, `LD A,A`, `LD L,L`. Не вставляйте
«настоящие» `LD A,A`/`LD B,B` в горячий код рядом — они тоже триггеры.
---
## 3. Длина блока (`ACC_SetBlockSize`)
```asm
ACC_SetBlockSize
LD A,0 ; 0 трактуется как 256 байт
ACC_Off
```
Длину берёт **значение, перенесённое следующей командой загрузки**, в каком
угодно регистре:
```asm
ACC_SetBlockSize
LD B,#80 ; блок = 128
ACC_Off
ACC_SetBlockSize
LD C,119 ; блок = 119 (длина спрайта оружия, D2_FRAM.asm)
ACC_Off
```
Особый приём (`D2_FRAM.asm`, метка `HIGH_1`): запись `LD (DE),A` в режиме
`ACC_SetBlockSize` **одновременно** задаёт длину блока (из `A`) **и**
выполняет реальную запись `A` по адресу `(DE)`. В DOOM так одной командой
одновременно задаётся высота стены и пишется маркер в строковый Z-буфер:
```asm
ACC_SetBlockSize
LD (DE),A ; A = длина блока И запись в LINE-Z-Buffer
ACC_Off
```
Установленная длина **сохраняется** до следующего `ACC_SetBlockSize`, её не
нужно задавать перед каждой операцией.
---
## 4. Графический экран и `PORT_Y`
* `PORT_Y EQU #C4` (внешний `#89`) — **вертикальная** координата (номер
строки) точки на графическом экране.
* **Горизонталь** задаётся адресом в окне `#C000#FFFF` (1 байт = 1 пиксель,
режим 8 бит/точку).
* Страница видео-ОЗУ выбирается через `SLOT3` (в DOOM: `#58` — выводимый
кадр, `#50` — служебная страница/Z-буфер, `#5C` — рабочая).
* В проекте два экранных буфера: `ScreenStartAddress = #C040` и
`ScreenStartAddress + #0140`; переключение — порт `SCREEN_SWITCH`
(`RGMOD`, `#C9`).
«Экранные» режимы акселя (`ACC_*Screen*`) при чтении/записи инкрементируют
**не адрес, а `PORT_Y`** — то есть обрабатывают **вертикальную линию**
(столбец) пикселей при фиксированном адресе-горизонтали. Это основа вывода
стен/пола/неба в DOOM.
---
## 5. Порт масштабирования `SCALE` — главное «doom»-расширение
### 5.1. Включение
Внутренний регистр акселя `ACEX.SCALE = #C7` (он же `PORT_SCALE = #FC`).
Перед использованием его нужно «открыть» через BIOS-функцию управления
дешифратором портов `DCP_CONFIG (#F4)`. Дешифрация порта масштабирования
выполняется по схеме **DCP (карта портов)** — её полное описание см. в
`Shared_Includes/Docs/SP2000.PDF` (раздел про карту портов / DCP).
Практически: порт выбирается, когда **младший байт адреса порта = 0**, а
**вся пара `BC` несёт 16-битный коэффициент** (см. §5.2). Из `DOOM2.asm`
(≈стр. 165176):
```asm
;----[открыть порты масштабирования]----
LD A,1
LD HL, %0000'0100'0000'0000 ; значение
LD DE, %1111'1110'0110'1111 ; маска (0 = изменяемый бит) = #FE6F
LD BC, ACEX.SCALE*256 + BIOS.DCP_CONFIG ; B=#C7 (порт), C=#F4 (ф-ция)
RST ToBIOS
; Активация необратима до RESET
LD BC,#0100 ; коэффициент #0100 ...
OUT (C),C ; ... = 2.8-шаг 1.000 -> масштаб 1:1
```
> После активации обратной дороги нет — выключается только сбросом.
> Внимание: «масштаб 1:1» — это коэффициент **`#0100`**, а не `0`.
### 5.2. Как писать в `SCALE` (16-битный коэффициент)
В порт масштабирования пишется **16-битное число** по штатной схеме Z80
`OUT (C),r`: **старший байт** коэффициента кладётся в регистр `B` (он
выходит на линии адреса `A8..A15`), **младший байт** — это данные `OUT`
(содержимое записываемого регистра). `C` держим = 0 — это выбор порта
SCALE по DCP.
```asm
LD B, coeff_hi ; старший байт коэффициента -> A8..A15
LD C, 0 ; младший байт адреса порта = 0 -> выбор SCALE (DCP)
LD A, coeff_lo ; младший байт коэффициента
OUT (C),A ; 16-битный коэффициент = B*256 + A
```
Варианты из реального кода:
* `LD BC,#0100 : OUT (C),C` — коэффициент `#0100` (данные = `C` = 0,
старший байт `B` = 1). Это и есть **1:1**.
* `LD B,(hl_hi) … LD C,0 … OUT (C),L` (`TRACE_CONT`) — коэффициент
`B*256 + L`, где `B` взят из старшей плоскости таблицы, `L` — из младшей.
Важно: в форме `OUT (C),C` данные равны `C`, поэтому младший байт там
всегда 0; для произвольного младшего байта берите другой регистр
(`A`/`L`/…), а `C` оставляйте = 0 (выбор порта).
### 5.3. Раскладка битов коэффициента
16-битный коэффициент `coeff = B*256 + (младший байт)`:
| Биты | Поле | Смысл |
|---|---|---|
| `bit[15..10]` (6 бит) | смещение в буфере | стартовая позиция чтения в 256-байтовом буфере акселя, 0..63 (вертикальный клиппинг/центровка текстуры у близких и полноэкранных стен) |
| `bit[9..0]` (10 бит) | шаг **2.8** | дробный шаг чтения по буферу на каждый выходной пиксель: 2 бита целой части, 8 бит дробной (значение `/256`) |
* `шаг = 1.000``bit[9..0] = 256` ⟺ коэффициент `#0100` при нулевом
смещении — копирование **1:1** (именно его пишет DOOM как «масштаб 1:1»).
* `шаг < 1.0` — **растяжение** (исходный тексел повторяется на нескольких
выходных пикселях);
* `шаг > 1.0` — **сжатие** (часть текселей пропускается);
* `шаг = 0` — предельное растяжение (один тексел на всю линию).
Коэффициент **вычислим напрямую**:
```
coeff = (offset << 10) | round(step * 256) ; step в формате 2.8
```
Таблица `Bin/table_x.tbl` (грузится в `TABLE_X = #A000`, размер `#0800`) —
это лишь заранее посчитанный LUT по индексу высоты столбца. Парные плоскости
по 256 байт: плоскость `#A000` = **старший байт** коэффициента (→ `B`),
плоскость `#A100` = **младший байт** (→ данные `OUT`). Проверенные значения
(`coeff = #A000[idx]*256 + #A100[idx]`):
| idx | `#A000`→B | `#A100`→data | coeff | смещение `b15..10` | шаг 2.8 `b9..0` |
|---:|---:|---:|---:|---:|---:|
| 1 | 0 | 64 | `#0040` | 0 | 0.250 |
| 32 | 0 | 85 | `#0055` | 0 | 0.332 |
| 64 | 0 | 128 | `#0080` | 0 | 0.500 |
| 96 | 1 | 0 | `#0100` | 0 | **1.000 (1:1)** |
| 160 | 32 | 48 | `#2030` | 8 | 0.188 |
| 192 | 64 | 32 | `#4020` | 16 | 0.125 |
| 224 | 96 | 16 | `#6010` | 24 | 0.0625 |
| 255 | 124 | 0 | `#7C00` | 31 | 0.000 |
Разбор по биту 7 индекса: индексы `0..127` — стена частичной высоты
(смещение в буфере = 0), индексы `128..255` — стена «на весь экран»
(растёт смещение в буфере: 0,8,16,24,28,31 — текстура поднимается/клиппится
по вертикали). См. `D2_FRAM.asm`, метки `TRACE_CONT`, `HIGH_1`, `WALL`.
---
## 6. Буфер-линия акселя
У акселя есть собственное ОЗУ-буфер на одну линию/блок размером **ровно
256 байт**. Старшие 6 бит коэффициента (`bit[15..10]`, см. §5.3) задают
стартовое смещение чтения внутри этого буфера. Типовой цикл
«текстура → экран»:
1. **Заливка буфера (без масштаба):**
```asm
ACC_SetBlockSize
LD A,#40 ; 64 байта исходной текстуры
ACC_CopyBlock
LD A,(HL) ; читать 64 байта из (HL) в буфер акселя
ACC_Off ; столбец текстуры теперь в ОЗУ акселя
```
Чтение в буфер всегда **немасштабированное** (комментарий
«взять немасштабированно!» в `TRACE_CONT`).
2. **Задать масштаб** — 16-битный коэффициент (старший байт в `B`,
младший — данные `OUT`, `C` = 0); см. §5.2/§5.3.
3. **Задать длину выводимого блока** (`ACC_SetBlockSize`, число экранных
пикселей по вертикали).
4. **Вывод в экран с масштабом:**
```asm
OUT (PORT_Y),A ; стартовая строка
ACC_CopyScreenBlock
LD (DE),A ; буфер -> столбец экрана (DE = X), масштаб
ACC_Off
```
Аксель читает 64 тексела из буфера, применяет дробный шаг `SCALE` и
пишет `N` пикселей в столбец, инкрементируя `PORT_Y`.
---
## 7. Готовые рецепты из проекта
### 7.1. Столбец стены DOOM (растяжение/сжатие) — `D2_FRAM.asm`
```asm
; A = код высоты столбца (индекс в TABLE_X)
LD H,TABLE_X/256 ; #A0
LD L,A
LD B,(HL) ; СТАРШИЙ байт 16-битного коэффициента -> B
INC H ; #A1
LD L,(HL) ; МЛАДШИЙ байт коэффициента -> L
OUT (C),L ; SCALE = B*256+L (C уже = 0: выбор порта)
; ... буфер столбца текстуры уже залит ACC_CopyBlock-ом ...
HIGH_1: ; стена не на весь экран
ACC_SetBlockSize
LD (DE),A ; высота + маркер в LINE-Z-Buffer
ACC_Off
NEG
OUT (PORT_Y),A ; позиция начала пола
; ... заливка пола сплошным цветом (см. 7.2) ...
LD A,L
NEG
OUT (PORT_Y),A ; вернуться к началу стены
LD A,L
ADD A,A ; экранная высота стены = 2*L
ACC_SetBlockSize
LD (DE),A
ACC_CopyScreenBlock
LD (DE),A ; растянуть/сжать столбец в экран!
ACC_Off
```
Полноэкранный вариант (код высоты ≥ 128, бит 7 = 1):
```asm
XOR A
OUT (PORT_Y),A
ACC_SetBlockSize
LD (DE),A ; 256 строк (0 -> 256) + Z-buffer
ACC_Off
LD A,#58
OUT (SLOT3),A ; страница видео-ОЗУ
ACC_CopyScreenBlock
LD (DE),A ; весь столбец 256 px за раз
ACC_Off
```
### 7.2. Заливка вертикали сплошным цветом (пол/потолок) — `D2_FRAM.asm`
```asm
OUT (PORT_Y),A ; стартовая строка
ACC_SetBlockSize
LD (DE),A ; число пикселей (если ещё не задано)
ACC_FillScreenOneByte
LD (DE),A ; A = цвет, заполнить столбец
ACC_Off
```
### 7.3. Очистка графического экрана стеком — `DOOM2.asm CLEAR_GRAF_SCR`
```asm
LD A,#50
OUT (SLOT3),A
XOR A
OUT (PORT_Y),A
LD SP,ScreenStartAddress + 640
LD B,640/4
ACC_SetBlockSize
LD E,0 ; блок = 256
LD D,E
LOOP_CLS:
ACC_FillScreenOneByte
PUSH DE ; PUSH тоже «обращение к памяти» -> заливка
PUSH DE
ACC_Off
DJNZ LOOP_CLS
```
### 7.4. Очистка строкового Z-буфера — `D2_FRAM.asm CLEAR_Z_BUFER`
```asm
ACC_SetBlockSize
LD A,0 ; 256
ACC_FillOneByte
LD (DE),A ; линейная заливка нулём
INC D
ACC_SetBlockSize
LD B,#40 ; ещё 64 байта
ACC_FillOneByte
LD (DE),A
ACC_Off
```
### 7.5. Копирование картинки 320×256 в экран — `D2_FRAM.asm SET_PICTURE`
```asm
ld bc,#0100
OUT (C),C ; масштаб 1:1
ACC_SetBlockSize
LD A,0 ; 256
ACC_Off
OUT (PORT_Y),A
LOOP_PG:
ACC_CopyBlock
LD A,(HL) ; 256 байт из (HL) в буфер
ACC_CopyScreenBlock
LD (DE),A ; столбец экрана (256 px, без масштаба)
ACC_Off
INC DE ; следующий столбец (X+1)
INC H ; следующие 256 байт источника
DJNZ LOOP_PG
```
### 7.6. Горизонтальный спрайт без масштаба (оружие) — `D2_FRAM.asm WEAPON_OUT`
```asm
ACC_SetBlockSize
LD C,119 ; длина спрайта
ACC_Off
W_OUT_L:
LD A,#44
SUB H
OUT (PORT_Y),A
OUT (C),C ; масштаб 1:1, начать с 0
ACC_CopyBlock
LD A,(HL) ; строка спрайта -> буфер
ACC_Off
OUT (C),C
ACC_CopyBlock
LD (DE),A ; буфер -> экран
ACC_Off
INC H
BIT 7,H
JR Z,W_OUT_L
```
---
## 8. Типовая последовательность (шпаргалка)
```
[OUT (SLOT3),video_page] ; выбрать страницу видео-ОЗУ
ACC_SetBlockSize / LD r,len / - ; длина блока (0 = 256)
ACC_CopyBlock / LD A,(HL) / OFF ; заполнить буфер акселя (немасштаб.)
LD B,hi / LD C,0 / LD A,lo / OUT (C),A ; 16-битный коэффициент B*256+lo
; (#0100 = 2.8-шаг 1.0 = 1:1)
ACC_SetBlockSize / LD r,out_len/- ; сколько пикселей выводить
OUT (PORT_Y),y_start ; стартовая строка
ACC_CopyScreenBlock / LD (DE),A /OFF; вывод столбца с масштабом
ACC_Off ; ОБЯЗАТЕЛЬНО
```
---
## 9. Подводные камни
* **Только SIMM-память.** Блочные пересылки/заливки акселя должны
адресовать динамическую SIMM-память. Fast-RAM — переключаемый
статический кэш (появляется в нулевом окне `#0000#3FFF` только пока
включён через `IN A,(#FB)`); как источник/приёмник пересылки он
**не работает**. Управлять акселем из кода в Fast-RAM можно, а
`ACC_SetBlockSize` из Fast-RAM работает полноценно (длина — с шины
данных). См. §1.2.
* **Всегда `ACC_Off`** после операции, иначе ближайшее обращение к памяти
«съест» аксель и разрушит данные.
* Между триггером и рабочей командой **нельзя обращаться к памяти данных**
(можно: `LD r,const`, `OUT`, `IN`, арифметика, `EXX`, `EX AF,AF'`).
* В горячем коде не используйте «настоящие» `LD A,A`, `LD B,B`, `LD H,H`
и т.п. — это команды акселя.
* `SCALE` активируется один раз и навсегда (до RESET); порт декодируется по
младшему байту адреса = 0 — не выводите туда ничего «случайно»
(любой `OUT (C),r` при `C=0`).
* Длина блока `0` означает **256**, а не «ничего».
* В `ACC_SetBlockSize` команда-запись (`LD (DE),A`) и задаёт длину, и
реально пишет байт — учитывайте побочную запись (DOOM использует её
специально под Z-буфер).
* `SCALE` — **16-битный** коэффициент: старший байт едет в регистре `B`
(на адрес порта `A8..A15`), младший — данные `OUT`; `C` = 0 (выбор
порта). Не путать с «одним байтом». Для следующей линии с другой
высотой коэффициент нужно переустановить.
* Значение `SCALE` **вычислимо**: `coeff = (offset << 10) | round(step*256)`,
где `step` — формат 2.8. `table_x.tbl` — лишь удобный прекалк (плоскость
`#A000``B`, плоскость `#A100` → данные `OUT`).
* «Масштаб 1:1» — это коэффициент **`#0100`** (2.8-шаг = 1.000), **не `0`**.
Перед операциями без масштаба ставьте `LD BC,#0100 : OUT (C),C`, иначе
картинка/спрайт исказятся. Коэффициент `0` = предельное растяжение.
---
## 10. Карта сущностей
| Имя | Значение | Где |
|---|---|---|
| `PORT_Y` | `#C4` (внеш. `#89`) | вертикальная координата |
| `SCALE`/`PORT_SCALE` | `#C7` / `#FC`, внеш. `#XX00` | порт масштаба; 16-бит `B:data`, `b15..10`=смещение в буфере, `b9..0`=2.8-шаг |
| `RGMOD`/`SCREEN_SWITCH` | `#C9` | переключение экранных страниц |
| `SLOT3` | окно `#C000#FFFF` | выбор страницы видео-ОЗУ |
| `BIOS.DCP_CONFIG` | `#F4` | открытие порта `SCALE` |
| `ACEX.SCALE` | `#C7` | внутр. номер порта масштаба |
| `TABLE_X` | `#A000`, разм. `#0800` | таблица коэффициентов (`table_x.tbl`) |
| `ScreenStartAddress` | `#C040` (+`#0140` 2-й буфер) | старт экрана |
| `Config_ID.Sp97_DOOM` | `#FFF9`, тип акселя `3` | идентификатор конфигурации |
Ключевые места в коде: `D2_FRAM.asm``TRACE`/`TRACE_CONT`/`HIGH_1`
(растеризация стен с масштабом), `WALL` (спрайты-монстры),
`CLEAR_Z_BUFER`, `SET_PICTURE`, `WEAPON_OUT`, `SKY_LOOP_1`;
`DOOM2.asm` — активация `SCALE` (≈стр. 165176), `CLEAR_GRAF_SCR`,
`INIT_TABLE` (загрузка `table_x.tbl`).
Дешифрация порта масштабирования (схема DCP / карта портов) — подробно в
`Shared_Includes/Docs/SP2000.PDF` (раздел про карту портов / DCP);
конкретная инициализация декодера — вызов `BIOS.DCP_CONFIG` в `DOOM2.asm`
(маска `#FE6F`, значение `#0400`, порт `ACEX.SCALE #C7`).