mirror of
https://github.com/Tolik-Trek/DOOM2.git
synced 2026-06-15 09:01:34 +03:00
ИИ описание и генератор d2_table
This commit is contained in:
parent
f753609d4f
commit
2fb38cb3ef
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.claude/settings.json
|
||||||
BIN
Bin/d2_fram.bin
BIN
Bin/d2_fram.bin
Binary file not shown.
524
INFO/doom-аксель.md
Normal file
524
INFO/doom-аксель.md
Normal file
@ -0,0 +1,524 @@
|
|||||||
|
# Руководство по «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`).
|
||||||
249
INFO/doom-энжин.md
Normal file
249
INFO/doom-энжин.md
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
# DOOM2 / Sprinter Sp2000 — справочник по движку
|
||||||
|
|
||||||
|
Краткий технический справочник по архитектуре оригинального движка
|
||||||
|
DOOM2 для Sprinter Sp2000 (CPU Z84C15 @ 21 МГц, FPGA-акселератор).
|
||||||
|
Документ описывает то, что **уже работает в репозитории**: форматы
|
||||||
|
файлов-ассетов, ключевые таблицы в памяти, поведение основных
|
||||||
|
процедур рейкастера. Деталями акселератора и порта SCALE занимается
|
||||||
|
отдельный документ [doom-аксель.md](doom-аксель.md).
|
||||||
|
|
||||||
|
Кодировка исходников `D2_FRAM.asm`, `DOOM2.asm` — **CP866**. При
|
||||||
|
правках использовать `iconv` (см. [cp866-edit-workflow в памяти]).
|
||||||
|
|
||||||
|
## 1. Память и слоты
|
||||||
|
|
||||||
|
| Адрес | Кто | Описание |
|
||||||
|
|--------------|---------|------------------------------------------------|
|
||||||
|
| `#0000..#1FFF` | SLOT0 (RAM) | Код `D2_FRAM.asm` (`ORG #1000`) |
|
||||||
|
| `#2000..#23FF` | SLOT0 (RAM) | `TABLE_W` — относ. карта (4 квадранта × 16×16) |
|
||||||
|
| `#4000..#7FFF` | SLOT1 | RAM-диск/файлы (страница 16 КБ): `d2_table.tbl`, `map_wall.res`, текстуры стен и т.п. |
|
||||||
|
| `#8000..#BFFF` | SLOT2 | Код `DOOM2.asm` (`ORG #8000`) |
|
||||||
|
| `#A000..#AFFF` | RAM | `TABLE_X` (`table_x.tbl` — SCALE-LUT) |
|
||||||
|
| `#C000..#FFFF` | Screen | Экран (двойная буферизация через `SCREEN_1`) |
|
||||||
|
|
||||||
|
Подключение страниц: `OUT (SLOT1),A` (`#5E`) — выбор страницы под
|
||||||
|
`#4000..#7FFF`, `OUT (SLOT3),A` (`#7E`) — режим экрана / wall-режим.
|
||||||
|
|
||||||
|
## 2. Карта `Bin/map_wall.res`
|
||||||
|
|
||||||
|
Файл = **256 КБ = 16 страниц по 16 КБ**. Загружается через
|
||||||
|
`RAMBlkIDs.map_wall`; `TABLE_WALL` (DOOM2.asm) хранит список реальных
|
||||||
|
страниц блока (17 байт).
|
||||||
|
|
||||||
|
### 2.1 Раскладка страницы 0 (карта)
|
||||||
|
|
||||||
|
Внутри первой 16-КБ страницы — **четыре слоя карты** по 4 КБ каждый.
|
||||||
|
В адресном пространстве CPU (после `OUT (SLOT1),map_wall_page_0`):
|
||||||
|
|
||||||
|
| Адрес CPU | Слой | Назначение |
|
||||||
|
|-----------|--------------------|------------|
|
||||||
|
| `#4000..#4FFF` | «декоративный» | визуальный слой (стены/двери, ASCII-коды) |
|
||||||
|
| `#5000..#5FFF` | **видимость** | то, что трассируется лучом (читает `TRACE`) |
|
||||||
|
| `#6000..#6FFF` | альтернативный | (см. RECALC ниже) |
|
||||||
|
| `#7000..#7FFF` | **коллизия** | проходимость для игрока/монстров (`MAP_PLACE`) |
|
||||||
|
|
||||||
|
Все слои имеют **одинаковую** клеточную раскладку:
|
||||||
|
* строка = 62 клетки + `0x0D 0x0A` (CR+LF) = **64 байта**;
|
||||||
|
* в коде используется адрес `H = (R >> 2) | base_section`,
|
||||||
|
`L = ((R & 3) << 6) | C` ⇒ ячейка `(R, C)` лежит по адресу
|
||||||
|
`base + R*64 + C`.
|
||||||
|
|
||||||
|
### 2.2 Кодировка клеток (ASCII в редакторе уровней)
|
||||||
|
|
||||||
|
| ASCII | Hex | Что |
|
||||||
|
|-------|-------|------------------|
|
||||||
|
| ` ` | `#20` | пусто |
|
||||||
|
| `0..>` | `#30..#3E` | стена 0..14 (индекс в `TABLE_WALL[1+id]`) |
|
||||||
|
| `S` | `#53` | старт игрока |
|
||||||
|
| `M` | `#4D` | монстр |
|
||||||
|
| `N` | `#4E` | факел (огни) |
|
||||||
|
| `O` | `#4F` | бочка |
|
||||||
|
| `P` | `#50` | огонь BFG |
|
||||||
|
|
||||||
|
Полный список — в `RECALC_MAP` (`DOOM2.asm`, `CALL Z,START_POS / MONSTR_POS / …`).
|
||||||
|
|
||||||
|
### 2.3 `RECALC_MAP` — преобразование уровня
|
||||||
|
|
||||||
|
Один раз после загрузки уровня. Сканирует все 4 слоя:
|
||||||
|
* В слоях `#4xxx, #5xxx, #6xxx`: ASCII → **номер страницы текстуры
|
||||||
|
стены** (через `TABLE_WALL[1 + (ascii - '0')]`); пробел/нестенки →
|
||||||
|
`#00` (в `#5xxx`) или `#FF` (в `#4xxx, #6xxx`).
|
||||||
|
* В слое `#7xxx`: ASCII → `#5F` (стена/препятствие) или `#00`
|
||||||
|
(проходимо).
|
||||||
|
* Заодно регистрирует стартовую позицию, монстров и т.п. через
|
||||||
|
`START_POS`, `MONSTR_POS*` (вызовы `CALL Z,…` по коду клетки).
|
||||||
|
|
||||||
|
После `RECALC_MAP` слой `#5xxx` готов для `TRACE`/`MAKE_MAP`,
|
||||||
|
слой `#7xxx` — для `MAP_PLACE` (проверка коллизий).
|
||||||
|
|
||||||
|
## 3. Относительная карта `TABLE_W`
|
||||||
|
|
||||||
|
`MAKE_MAP` (D2_FRAM.asm) строит вокруг игрока **4 квадрантные карты
|
||||||
|
16×16 = 256 байт каждая** в основном ОЗУ:
|
||||||
|
|
||||||
|
```
|
||||||
|
TABLE_W EQU #2000 ; квадрант 0 (источник: +X = +C)
|
||||||
|
TABLE_W+#100 ; квадрант 1 (поворот 90°)
|
||||||
|
TABLE_W+#200 ; квадрант 2 (поворот 180°)
|
||||||
|
TABLE_W+#300 ; квадрант 3 (поворот 270°)
|
||||||
|
```
|
||||||
|
|
||||||
|
* Каждый квадрант = пред-повёрнутая копия мировой карты `#5xxx`
|
||||||
|
вокруг игрока, чтобы трассировка читала одной 8-битной арифметикой
|
||||||
|
(`LD A,(DE)`, где `D` — старший байт квадранта, `E` — `lo`-байт
|
||||||
|
из таблицы `d2_table.tbl`).
|
||||||
|
* В пределах квадранта 16×16 (адрес `cy * 16 + cx`) игрок находится
|
||||||
|
в верхнем-левом углу относительной системы — реальные пред-повороты
|
||||||
|
делают так, что «вперёд» соответствует положительному направлению
|
||||||
|
обхода независимо от того, куда игрок смотрит.
|
||||||
|
* Дальность ограничена 16-битной адресацией одной страницы 256 байт
|
||||||
|
⇒ **16 клеток** в каждую сторону. Расширение до 24 потребовало бы
|
||||||
|
16-битной адресации в горячем цикле (см. историю провалившейся
|
||||||
|
попытки в git-логе ветки `beta` до этого коммита).
|
||||||
|
|
||||||
|
## 4. Формат `Bin/d2_table.tbl`
|
||||||
|
|
||||||
|
**512 КБ = 32 страницы × 16384 = 512 записей × 32 байта** на страницу.
|
||||||
|
|
||||||
|
Страницы — пары `(EVEN, ODD)`, **по одной паре на CORNER**
|
||||||
|
(субпозицию игрока в клетке, 0..15). Адресуются через `TABLE_TRACE`
|
||||||
|
(массив 33 байта в `DOOM2.asm`): чётный байт пары = номер страницы
|
||||||
|
`PLACE_L` (offset-страница), нечётный = `PLACE_L1/L2` (projection).
|
||||||
|
|
||||||
|
### 4.1 Запись 32 байта = две половины по 16 (бит 4 регистра L)
|
||||||
|
|
||||||
|
**EVEN-страница (offset / трассировка):**
|
||||||
|
* `lo[0..15]` — **низкий байт** адреса клетки в квадрантной карте
|
||||||
|
`TABLE_W` на каждом из 16 шагов луча (DDA-путь). Опорный луч
|
||||||
|
«прямо вперёд» = `1, 2, …, 16`.
|
||||||
|
* `hi[0..15]` = `0`.
|
||||||
|
|
||||||
|
**ODD-страница (projection):**
|
||||||
|
* `lo[0..15]` — **код высоты** на каждом шаге = индекс в
|
||||||
|
`table_x.tbl` (см. §5). Перспективный закон: ближе → больше
|
||||||
|
(полноэкранно), дальше → меньше.
|
||||||
|
* `hi[0..15]` — **u-координата** столбца текстуры стены
|
||||||
|
(+16 на субпозицию = «тир» в адресации страницы стены).
|
||||||
|
|
||||||
|
### 4.2 Адресация записи в кадре
|
||||||
|
|
||||||
|
Внутри страницы запись адресуется регистрами `(H, L)`:
|
||||||
|
* `H ∈ #40..#7F` (6 бит) — **крупный угол** внутри квадранта
|
||||||
|
(`(ANGLE_M >> 7) & #3F`). 64 значения покрывают 90°.
|
||||||
|
* `L_top3 ∈ {0, 32, 64, …, 224}` — **подгруппа из 8 экранных
|
||||||
|
колонок** (`L & #E0`, шаг `ADD A,32`).
|
||||||
|
* Вместе: 64 × 8 = 512 = страница. За кадр движок проходит
|
||||||
|
**40 H × 8 L = 320 экранных лучей** (FOV 60° из квадранта 90°).
|
||||||
|
|
||||||
|
Квадрант (0..3) выбирается старшими битами `ANGLE_M` через
|
||||||
|
`LD D,A` (`D` — старший байт страницы `TABLE_W`).
|
||||||
|
|
||||||
|
### 4.3 Закон проекции (реверс)
|
||||||
|
|
||||||
|
* `N = K / perp_dist`, `K ≈ 315`;
|
||||||
|
* `perp = d_eucl * cos(ray_ang - view_ang)` (фиш-ай-фикс);
|
||||||
|
* экран 256 пикс., `N > 256` → полноэкранно + клип через `table_x`
|
||||||
|
(буфер-offset);
|
||||||
|
* `height_code`:
|
||||||
|
* `A < 128` → частичная стена, `N = 256 - 2A` пикс.;
|
||||||
|
* `A ≥ 128` → полноэкранная стена с клипом по 6-битному
|
||||||
|
буфер-offset (`A - 128 = 0..63`).
|
||||||
|
|
||||||
|
> **Бит-в-бит репликация оригинала невозможна** — fixed-point-формат
|
||||||
|
> инструмента-генератора утерян. Резерв: `Resources/tools/gen_d2_table/`
|
||||||
|
> печёт формат-совместимую копию `Build/d2_table.gen.tbl` из
|
||||||
|
> задокументированной математики (без претензии на бинарное
|
||||||
|
> совпадение с `Bin/d2_table.tbl`).
|
||||||
|
|
||||||
|
## 5. `Bin/table_x.tbl` — SCALE-LUT
|
||||||
|
|
||||||
|
256-байтовая таблица в RAM `#A000` (низкий байт коэффициента
|
||||||
|
порта SCALE) + парная `#A100` (старший байт). Полный 16-битный
|
||||||
|
коэффициент = `(B << 8) | L`, где:
|
||||||
|
|
||||||
|
* `bits[15..10]` — смещение в 256-байтовом буфере акселератора
|
||||||
|
(0..63), используется при полноэкранных стенах для клипа;
|
||||||
|
* `bits[9..0]` — fixed-point **2.8** шага чтения буфера на каждый
|
||||||
|
выводимый пиксель (целая часть 0..3, дробная /256).
|
||||||
|
* Коэффициент `#0100` = `step = 1.000` = **масштаб 1:1**.
|
||||||
|
|
||||||
|
Подробнее про порт SCALE, схему `OUT (BC),r` и расшифровку битов —
|
||||||
|
[doom-аксель.md](doom-аксель.md).
|
||||||
|
|
||||||
|
`height_code` из `d2_table.tbl` (см. §4.1) — это **индекс в
|
||||||
|
`table_x.tbl`**: `TABLE_X[height_code]` (низкий байт) +
|
||||||
|
`TABLE_X[height_code + #100]` (старший байт) дают коэффициент SCALE,
|
||||||
|
который выгружается в порт `#C7` парой `LD B,(HL) / OUT (C),L`
|
||||||
|
(см. `TRACE_CONT`).
|
||||||
|
|
||||||
|
## 6. Алгоритм `TRACE` (упрощённо)
|
||||||
|
|
||||||
|
Из `D2_FRAM.asm`:
|
||||||
|
|
||||||
|
```
|
||||||
|
TRACE:
|
||||||
|
SLOT3 := #50 ; экран mode
|
||||||
|
SCALE := #0100 ; 1:1 (BC=#0100, OUT (C),C)
|
||||||
|
|
||||||
|
; ---- небо (sky scroll по углу) ----
|
||||||
|
LD HL,(ANGLE_M); ADD HL,HL; …
|
||||||
|
LD C,80
|
||||||
|
SKY_LOOP_1: ACC_CopyBlock + ACC_CopyScreenBlock ×4 (4 пикс. колонки)
|
||||||
|
|
||||||
|
; ---- веер лучей ----
|
||||||
|
EXX; LD DE,(SCREEN_1); LD C,0; EXX
|
||||||
|
LD HL,ANGLE_M; D := TABLE_W/256 + quadrant; H := #40..#7F (угол)
|
||||||
|
LD B,40 ; 40 групп × 8 колонок = 320 лучей
|
||||||
|
|
||||||
|
TRACE_NEXT_:
|
||||||
|
EXX; LD HL,(PLACE_L); … EXX
|
||||||
|
PLACE_L+1: LD A,(TABLE_TRACE+…); OUT (SLOT1),A ; subpos page (offsets)
|
||||||
|
LD (CONT_PAGE),A
|
||||||
|
|
||||||
|
TRACE_LOOP:
|
||||||
|
REPT 16
|
||||||
|
LD E,(HL) ; offset байт текущего шага
|
||||||
|
LD A,(DE) ; клетка в относ. карте (`TABLE_W`)
|
||||||
|
AND A
|
||||||
|
JP NZ,TRACE_CONT ; стенка → рисовать
|
||||||
|
INC L ; следующий шаг луча
|
||||||
|
ENDR
|
||||||
|
JP TRACE_EMPTY ; 16 шагов пусто → пол/потолок
|
||||||
|
|
||||||
|
NEXT_ANGLE:
|
||||||
|
INC DE (alt-DE) ; ↑ экранная колонка
|
||||||
|
L := (L & #E0) + 32 ; следующий луч в подгруппе 8
|
||||||
|
(если L переполнен) INC H; если H ≥ #80 — новая группа (квадрант)
|
||||||
|
DJNZ ...
|
||||||
|
```
|
||||||
|
|
||||||
|
`TRACE_CONT` (нечётная страница `PLACE_L1/2`) читает `height_code` и
|
||||||
|
`u_col`, подгружает 64-байтовый столбец текстуры стены, выставляет
|
||||||
|
SCALE из `table_x.tbl` и рисует столбец через `ACC_CopyScreenBlock`.
|
||||||
|
|
||||||
|
## 7. CORNER (субпозиция игрока)
|
||||||
|
|
||||||
|
`PRECALC_PLACE` вычисляет 4-битный CORNER (0..15) из:
|
||||||
|
* `X_COORD+1 & 3` — субпозиция X внутри клетки (2 бита);
|
||||||
|
* `Y_COORD+1 & 3` — субпозиция Y внутри клетки (2 бита);
|
||||||
|
* квадранта взгляда (`BIT 6,H` / `BIT 7,H`) — XOR/swap-nibble
|
||||||
|
для канонизации (одна таблица переиспользуется на 4 поворота).
|
||||||
|
|
||||||
|
CORNER служит индексом в `TABLE_TRACE`:
|
||||||
|
`PLACE_L = TABLE_TRACE + 2*CORNER` — пара
|
||||||
|
`(offset-страница, projection-страница)` в `d2_table.tbl`. Меняется
|
||||||
|
при пересечении границы клетки.
|
||||||
|
|
||||||
|
## 8. Генератор `Resources/tools/gen_d2_table/`
|
||||||
|
|
||||||
|
* `gen_d2_table.asm` — обёртка под sjasmplus (Lua-блок, Z80-код не
|
||||||
|
производится).
|
||||||
|
* `gen_d2_table.lua` — Lua-генератор: 16 CORNER × (EVEN+ODD) ×
|
||||||
|
512 записей × 32 байта = 524288 байт в `Build/d2_table.gen.tbl`.
|
||||||
|
Использует supercover-DDA + `K=315 / perp` для проекции.
|
||||||
|
* Запуск из корня проекта:
|
||||||
|
```
|
||||||
|
sjasmplus --nologo Resources/tools/gen_d2_table/gen_d2_table.asm
|
||||||
|
```
|
||||||
|
* Выход формат-совместим, **но не бит-в-бит** с `Bin/d2_table.tbl`
|
||||||
|
(оригинальный fixed-point утерян).
|
||||||
19
Resources/tools/gen_d2_table/gen_d2_table.asm
Normal file
19
Resources/tools/gen_d2_table/gen_d2_table.asm
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
; =====================================================================
|
||||||
|
; Запуск генератора d2_table.tbl под sjasmplus (Lua 5.5), без эмулятора.
|
||||||
|
;
|
||||||
|
; sjasmplus --nologo Resources/tools/gen_d2_table/gen_d2_table.asm
|
||||||
|
;
|
||||||
|
; (запускать из КОРНЯ проекта — пути в .lua относительные)
|
||||||
|
; Z80-кода не производит: вся работа в Lua-блоке. Сравнение
|
||||||
|
; результата с Bin/d2_table.tbl печатается в консоль.
|
||||||
|
; =====================================================================
|
||||||
|
DEVICE NONE
|
||||||
|
|
||||||
|
LUA PASS1
|
||||||
|
local d = "Resources/tools/gen_d2_table/gen_d2_table.lua"
|
||||||
|
local chunk, err = loadfile(d)
|
||||||
|
if not chunk then sj.error("Lua load: " .. tostring(err)) return end
|
||||||
|
chunk()
|
||||||
|
ENDLUA
|
||||||
|
|
||||||
|
END
|
||||||
178
Resources/tools/gen_d2_table/gen_d2_table.lua
Normal file
178
Resources/tools/gen_d2_table/gen_d2_table.lua
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
-- =====================================================================
|
||||||
|
-- Генератор d2_table.tbl (рейкастер DOOM2 / Sprinter Sp2000)
|
||||||
|
-- 16-шаговая ближняя зона, формат-совместимый с Bin/d2_table.tbl.
|
||||||
|
--
|
||||||
|
-- Запуск:
|
||||||
|
-- sjasmplus --nologo Resources/tools/gen_d2_table/gen_d2_table.asm
|
||||||
|
-- (из КОРНЯ проекта; эмулятор не нужен)
|
||||||
|
--
|
||||||
|
-- Вход: нет.
|
||||||
|
-- Выход: Build/d2_table.gen.tbl
|
||||||
|
-- (Bin/d2_table.tbl — канонический рабочий ассет, не трогаем).
|
||||||
|
--
|
||||||
|
-- Документация формата и математики: INFO/doom-энжин.md.
|
||||||
|
--
|
||||||
|
-- ФОРМАТ (как в оригинале):
|
||||||
|
-- * 524288 байт = 32 страницы по 16384 (PAGE).
|
||||||
|
-- * Страницы парами CORNER=0..15: чётная = «offset», нечётная = «projection».
|
||||||
|
-- * Каждая страница = 512 записей по 32 байта (REC).
|
||||||
|
-- * Запись (32 б):
|
||||||
|
-- EVEN: byte[0..15] = низкий байт адреса DDA-клетки в отн.карте
|
||||||
|
-- TABLE_W (квадрант 16×16, страйд 1, base=#2000+QUAD*256);
|
||||||
|
-- byte[16..31] = 0.
|
||||||
|
-- ODD : byte[0..15] = код высоты (индекс в table_x.tbl);
|
||||||
|
-- byte[16..31] = текстурная u-координата столбца стены.
|
||||||
|
-- * Адресация записи в кадре: H ∈ #40..#7F (6 бит = крупный угол
|
||||||
|
-- внутри квадранта), L_top3 ∈ {0,32,…,224} (3 бита = подгруппа
|
||||||
|
-- колонок); вместе 64×8 = 512 = страница. За кадр движок проходит
|
||||||
|
-- 40 H × 8 L = 320 экранных лучей (60°/90° квадранта).
|
||||||
|
--
|
||||||
|
-- МАТЕМАТИКА (см. INFO/doom-энжин.md §«Закон проекции»):
|
||||||
|
-- * supercover-DDA, tie tx<=ty -> X (как в оригинале).
|
||||||
|
-- * перспектива по перпендикулярной дистанции: N = K / perp, K=315;
|
||||||
|
-- perp = d_eucl * cos(ray_ang - view_ang) (fisheye-фикс).
|
||||||
|
-- * height_code = index в table_x.tbl, грубо A = clamp((256-N)/2, 0..127).
|
||||||
|
-- * u_col = round(u_fraction * 64) & 63.
|
||||||
|
--
|
||||||
|
-- CORNER (0..15) кодирует субпозицию игрока в клетке (4×4).
|
||||||
|
-- Запись (record_idx 0..511) кодирует «виртуальный экранный угол»
|
||||||
|
-- внутри квадранта: ray_ang = (record_idx + 0.5)/512 * (pi/2).
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
local PAGES = 32
|
||||||
|
local PAGE = 16384
|
||||||
|
local REC = 32
|
||||||
|
local RECS = PAGE // REC -- 512
|
||||||
|
local STEPS = 16
|
||||||
|
local SUBPOS = 16
|
||||||
|
local GRID = 16 -- TABLE_W квадрант = 16×16
|
||||||
|
local K_PROJ = 315.0
|
||||||
|
local FOV_R = math.pi / 3.0 -- 60°
|
||||||
|
local SCR_W = 320
|
||||||
|
local QUAD = math.pi / 2.0 -- 90°
|
||||||
|
|
||||||
|
local OUT = "Build/d2_table.gen.tbl"
|
||||||
|
|
||||||
|
-- Субпозиция CORNER=0..15 -> (sx, sy) в [0..1] (углы 1/8 от стенки).
|
||||||
|
local function corner_xy(c)
|
||||||
|
local sx = (c % 4) / 4.0 + 1.0 / 8.0
|
||||||
|
local sy = (c // 4) / 4.0 + 1.0 / 8.0
|
||||||
|
return sx, sy
|
||||||
|
end
|
||||||
|
|
||||||
|
-- supercover-DDA, 16 шагов. Возвращает массив { {cx,cy,d_eucl,u}, ... }.
|
||||||
|
-- Ось +X = «вперёд» (вид игрока). cx,cy — клеточные координаты относит.
|
||||||
|
-- стартовой клетки (в которой игрок).
|
||||||
|
local function trace(sx, sy, ang)
|
||||||
|
local dx = math.cos(ang); if math.abs(dx) < 1e-9 then dx = (dx < 0 and -1e-9 or 1e-9) end
|
||||||
|
local dy = math.sin(ang); if math.abs(dy) < 1e-9 then dy = (dy < 0 and -1e-9 or 1e-9) end
|
||||||
|
local x, y = sx, sy
|
||||||
|
local cx, cy = math.floor(x), math.floor(y)
|
||||||
|
local out = {}
|
||||||
|
for _ = 1, STEPS do
|
||||||
|
local nx = cx + (dx > 0 and 1 or 0)
|
||||||
|
local ny = cy + (dy > 0 and 1 or 0)
|
||||||
|
local tx = (nx - x) / dx
|
||||||
|
local ty = (ny - y) / dy
|
||||||
|
local u
|
||||||
|
if tx <= ty then
|
||||||
|
x = nx; y = y + dy * tx; cx = cx + (dx > 0 and 1 or -1)
|
||||||
|
u = y - math.floor(y)
|
||||||
|
else
|
||||||
|
y = ny; x = x + dx * ty; cy = cy + (dy > 0 and 1 or -1)
|
||||||
|
u = x - math.floor(x)
|
||||||
|
end
|
||||||
|
local de = math.sqrt((x - sx)^2 + (y - sy)^2)
|
||||||
|
out[#out + 1] = { cx, cy, de, u }
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
-- offset байта = (cy & (GRID-1)) * GRID + (cx & (GRID-1))
|
||||||
|
-- (вне квадранта 16×16 «оборачиваем» по 8-битной арифметике
|
||||||
|
-- движка; реальный движок попадает в нулевые ячейки relmap).
|
||||||
|
local function off_byte(cx, cy)
|
||||||
|
local oc = cx % GRID
|
||||||
|
local oy = cy % GRID
|
||||||
|
if oc < 0 then oc = oc + GRID end
|
||||||
|
if oy < 0 then oy = oy + GRID end
|
||||||
|
return (oy * GRID + oc) & 0xFF
|
||||||
|
end
|
||||||
|
|
||||||
|
local function height_code(N)
|
||||||
|
if N <= 256 then
|
||||||
|
local A = (256 - N) // 2
|
||||||
|
if A < 0 then A = 0 end
|
||||||
|
if A > 127 then A = 127 end
|
||||||
|
return A
|
||||||
|
end
|
||||||
|
-- full-screen с клипом: A = 128 + 6-битный буфер-offset (как в оригинале).
|
||||||
|
local over = (N - 256) // 8
|
||||||
|
if over > 63 then over = 63 end
|
||||||
|
return 128 + over
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ucol(uf)
|
||||||
|
local v = math.floor(uf * 64.0 + 0.5)
|
||||||
|
return v & 63
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Кадр движка читает 320 экранных лучей подряд из 512 записей. Поэтому
|
||||||
|
-- виртуальный угол луча в кадре = view_ang + atan2(cam_x, halfproj),
|
||||||
|
-- где cam_x шагает по 320. Но в таблице 512 записей покрывают 90° квадранта,
|
||||||
|
-- так что одной записи соответствует ray_ang = ((idx + 0.5)/512) * 90°
|
||||||
|
-- (без поправки fisheye-камеры — она применяется через perp ниже).
|
||||||
|
local function ray_angle(record_idx)
|
||||||
|
return (record_idx + 0.5) / RECS * QUAD
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gen_pair(corner)
|
||||||
|
local sx, sy = corner_xy(corner)
|
||||||
|
local even = {}; for i = 1, PAGE do even[i] = 0 end
|
||||||
|
local odd = {}; for i = 1, PAGE do odd[i] = 0 end
|
||||||
|
-- view_ang = 0 (ось +X — вперёд); MAKE_MAP в движке поворачивает уровень
|
||||||
|
-- под квадрант. Луч ray_ang отсчитывается от +X.
|
||||||
|
for r = 0, RECS - 1 do
|
||||||
|
local ang = ray_angle(r)
|
||||||
|
local path = trace(sx, sy, ang)
|
||||||
|
local base = r * REC
|
||||||
|
for k = 1, STEPS do
|
||||||
|
local cx, cy, de, u = path[k][1], path[k][2], path[k][3], path[k][4]
|
||||||
|
-- EVEN: offset байт в relmap (TABLE_W) для шага k.
|
||||||
|
even[base + k] = off_byte(cx, cy)
|
||||||
|
-- ODD : код высоты + текстурная u.
|
||||||
|
local perp = de * math.cos(ang - 0.0) -- view_ang=0 -> perp=de*cos(ang)
|
||||||
|
if perp < 0.05 then perp = 0.05 end
|
||||||
|
local N = K_PROJ / perp
|
||||||
|
odd[base + k] = height_code(N) & 0xFF
|
||||||
|
odd[base + STEPS + k] = ucol(u)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return even, odd
|
||||||
|
end
|
||||||
|
|
||||||
|
local out = {}
|
||||||
|
for i = 1, PAGES * PAGE do out[i] = 0 end
|
||||||
|
|
||||||
|
for c = 0, SUBPOS - 1 do
|
||||||
|
local even, odd = gen_pair(c)
|
||||||
|
local eb = (c * 2) * PAGE
|
||||||
|
local ob = (c * 2 + 1) * PAGE
|
||||||
|
for i = 1, PAGE do
|
||||||
|
out[eb + i] = even[i]
|
||||||
|
out[ob + i] = odd[i]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local t = {}
|
||||||
|
for i = 1, #out do t[i] = string.char(out[i] & 0xFF) end
|
||||||
|
local w = io.open(OUT, "wb"); w:write(table.concat(t)); w:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
print(string.format("[gen_d2_table] %s размер=%d б = %d страниц × %d б",
|
||||||
|
OUT, PAGES * PAGE, PAGES, PAGE))
|
||||||
|
print(string.format("[gen_d2_table] CORNER=0..15, 16 шагов, %d записей × %d б на страницу",
|
||||||
|
RECS, REC))
|
||||||
|
print("[gen_d2_table] NB: формат-совместим, но не бит-в-бит с Bin/d2_table.tbl")
|
||||||
|
print("[gen_d2_table] (оригинальный fixed-point утерян; см. INFO/doom-энжин.md).")
|
||||||
Loading…
Reference in New Issue
Block a user