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

29 KiB
Raw Permalink Blame History

Руководство по «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)

    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 (≈стр. 165176):

;----[открыть порты масштабирования]----
    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.000bit[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. Заливка буфера (без масштаба):

        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. Вывод в экран с масштабом:

        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-буфер).
  • SCALE16-битный коэффициент: старший байт едет в регистре B (на адрес порта A8..A15), младший — данные OUT; C = 0 (выбор порта). Не путать с «одним байтом». Для следующей линии с другой высотой коэффициент нужно переустановить.
  • Значение SCALE вычислимо: coeff = (offset << 10) | round(step*256), где step — формат 2.8. table_x.tbl — лишь удобный прекалк (плоскость #A000B, плоскость #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.asmTRACE/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).