From 2fb38cb3efa0226558057a68a4165826d549a599 Mon Sep 17 00:00:00 2001 From: Tolik <85737314+Tolik-Trek@users.noreply.github.com> Date: Thu, 21 May 2026 01:32:55 +1000 Subject: [PATCH] =?UTF-8?q?=D0=98=D0=98=20=D0=BE=D0=BF=D0=B8=D1=81=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B8=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80?= =?UTF-8?q?=D0=B0=D1=82=D0=BE=D1=80=20d2=5Ftable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + Bin/d2_fram.bin | Bin 2211 -> 0 bytes INFO/doom-аксель.md | 524 ++++++++++++++++++ INFO/doom-энжин.md | 249 +++++++++ Resources/tools/gen_d2_table/gen_d2_table.asm | 19 + Resources/tools/gen_d2_table/gen_d2_table.lua | 178 ++++++ 6 files changed, 971 insertions(+) create mode 100644 .gitignore delete mode 100644 Bin/d2_fram.bin create mode 100644 INFO/doom-аксель.md create mode 100644 INFO/doom-энжин.md create mode 100644 Resources/tools/gen_d2_table/gen_d2_table.asm create mode 100644 Resources/tools/gen_d2_table/gen_d2_table.lua diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8373264 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.claude/settings.json diff --git a/Bin/d2_fram.bin b/Bin/d2_fram.bin deleted file mode 100644 index d53ee8a7362a04a91df2c1e5c391b7413ee68b3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2211 zcmY*aeQevt6+eoyYzLM^eOk_pnvy@js$OFVTW4q~P1jzG%vfw_Kr$d*JZ#;xB8}B$ z?<UqoS!DZCrs4L(xCd#1OnF$p#n$8lZo)o$Mf9SWZPVB3TLTipYbwKRgC87B#@2 zPI`PK&-r~a+k6C=s;sX|EExz00{y{Cl=L9t$Kk7N`75JCETIeTQ z8{eW991sspsfk+hMRLkEwUFS2W1>K8n(_xx8(2>5>zg%RR8DbTIA{XkKTtOzEUVz; z378t=YCc7D{2uU~CDZIu!V4=j&3967)mXZP`! z-Ba*)|5;~mZc>p2CVpPh>8bcT z5(K&9(jE5{WP~Qe9OwLs)7U$n$9`p3!4R&wHi6aoY2ySbh*44yj+O+EAVk~O>S46> zdP-Cau)PCfK}w*Hxxc-4jrrG_F$zB!!7+@t=Er}p=8gT3c3@tC)b@Yk9^ai@%;VcuH@DS-3_I+2iz`sL%1TYFKXV@f zwBxI7hPbviaxnaR372r}0K^h}?S1-7V(ziF^hoS`8ZjSJchOe+R%YNw<}~*%N%VF# z{l(7EyYv1^A(ru1^07HT*$9hJO_~c0EaO>P)alr)zfX)E_4j9&jB%`V?&Ihl{S*iX zxRzp1k3OPJxDY;K#|K-Yb4=Gf7-xdWNNnMs+3+kyTtaCtm{*6!&C>8UcWWPL!+SK+ z8VQFWXb=}|U!Q24=*un*QLDyyA}#dyXEkUEYUheLsk?nAHqaE0q$FdV|BIsClxxP} zH~mE;k-5UQRwHlvWrWB8!L%ajL2S*%I2h5}+s*usA9_8N`4}agtl_WL%HInRQ%&?{ zHv~)H&HT6el#|^8;|}O40A+6tr81vA1U*FD0t6@tq8AWVlEjB@_b&(9)O8uEkKDQ>p`=it!}fdq5Yzo$0kSK{F9nM1 zE}n{A49FBhB|aDXW1vh+yd5;Fu}cBD(xpsXuY}CoT-~g4+vZ)4GwHuIJL~bWAURvi z``h}q>F2&~zQTROJkAZ9N4dS`94CbQtFf_Q=r1+6h9eiH$chBbROOpQc^SaqynHqg zx5Ku9NY(V5o$EnuxLpL4h}^Cd``_5djpSwox`9%7$!Bn@O3- zY??q#5GWH=cAK5DBYV?s+8@{(bi>x58mQ51n2iSwpa&o{K|z6mZom=~mLh=I?6e(- zhQ~??DE`jJ>?OT587W9Oj4nbm6t6>K@mtOffy*eT*!f#s@! z+z%ZHntN9UCU8@y9CjtkwSx3g(r(W#Ev1xuB=^63O)bF9HFyBhzK}cQR8DX<7wW*j zprZw;xCO-w+v8X4c&^2hLtP%h1_l1lHD5X1>FGka>uNO!%_o+y=+ND*KzPxC(+>S^ z3Dm#(=v-&{T<3KVO&Ncy*TZ{q8q>A!)Z5|W?({!c8CGg z5pGxcv)_3B-Naq1GRzNh3>Iy8F+`qooVJ+)xTQ8gq>6GK*J?Mf#m~H2WSx-xGq08h zLOg Конфигурация `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`). diff --git a/INFO/doom-энжин.md b/INFO/doom-энжин.md new file mode 100644 index 0000000..1c0e1ff --- /dev/null +++ b/INFO/doom-энжин.md @@ -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 утерян). diff --git a/Resources/tools/gen_d2_table/gen_d2_table.asm b/Resources/tools/gen_d2_table/gen_d2_table.asm new file mode 100644 index 0000000..36ffac3 --- /dev/null +++ b/Resources/tools/gen_d2_table/gen_d2_table.asm @@ -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 diff --git a/Resources/tools/gen_d2_table/gen_d2_table.lua b/Resources/tools/gen_d2_table/gen_d2_table.lua new file mode 100644 index 0000000..89f0ad0 --- /dev/null +++ b/Resources/tools/gen_d2_table/gen_d2_table.lua @@ -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).")