29 KiB
Руководство по «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.
Принцип работы автомата:
- команда-триггер (
LD r,r) переводит аксель в нужный режим; - следующая команда обращения к памяти (
LD A,(HL),LD (DE),A,LD (HL),A,PUSH, …) выполняется уже «через аксель» и обрабатывает весь блок, а не один байт; - команда
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)
ACC_SetBlockSize
LD A,0 ; 0 трактуется как 256 байт
ACC_Off
Длину берёт значение, перенесённое следующей командой загрузки, в каком угодно регистре:
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-буфер:
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):
;----[открыть порты масштабирования]----
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.
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) задают
стартовое смещение чтения внутри этого буфера. Типовой цикл
«текстура → экран»:
-
Заливка буфера (без масштаба):
ACC_SetBlockSize LD A,#40 ; 64 байта исходной текстуры ACC_CopyBlock LD A,(HL) ; читать 64 байта из (HL) в буфер акселя ACC_Off ; столбец текстуры теперь в ОЗУ акселяЧтение в буфер всегда немасштабированное (комментарий «взять немасштабированно!» в
TRACE_CONT). -
Задать масштаб — 16-битный коэффициент (старший байт в
B, младший — данныеOUT,C= 0); см. §5.2/§5.3. -
Задать длину выводимого блока (
ACC_SetBlockSize, число экранных пикселей по вертикали). -
Вывод в экран с масштабом:
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
; 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):
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
OUT (PORT_Y),A ; стартовая строка
ACC_SetBlockSize
LD (DE),A ; число пикселей (если ещё не задано)
ACC_FillScreenOneByte
LD (DE),A ; A = цвет, заполнить столбец
ACC_Off
7.3. Очистка графического экрана стеком — DOOM2.asm CLEAR_GRAF_SCR
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
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
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
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).