# Руководство по «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` (≈стр. 165–176): ```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` (≈стр. 165–176), `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`).