# 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 утерян).