Analysis model: gpt-5.5 xhigh

# Dope by Complex - Technical Dissection

## Scope

This is a static reverse-engineering pass over **Dope** by Complex, released
for MS-DOS on 15 April 1995. Demozoo lists it as the 1st-place entry in the
The Gathering 1995 PC Demo competition and credits Jmagic for code, Jugi for
graphics/music/objects, Phantom for music replay, Reward for graphics, and
M. Hakulinen for additional 3D objects.

Public references:

- Demozoo production: https://demozoo.org/productions/57/
- Pouet production: https://www.pouet.net/prod.php?which=37
- Scene.org party archive entry: https://files.scene.org/view/parties/1995/thegathering95/demo/clx_dope.zip
- Direct archive URL used here: https://archive.scene.org/pub/parties/1995/thegathering95/demo/clx_dope.zip

This pass does not claim source-level reconstruction. It identifies the loader
format, unpacks the RNC-compressed program stages, recovers the original stage
entry points from the depacker variable blocks, and dissects selected inner
loops. Effect names below are sometimes inferred from code shape and from the
release text, because the stage binaries do not contain friendly part names.

## Examined Archive

The archive used here is `clx_dope.zip` from the The Gathering 1995 party
path.

```text
98be62edf7d7523e73fe14d678bd9fa3eb3f434a31bfc88ae384dda7bbd65fea  clx_dope.zip
```

Files:

| File | Size | SHA-256 |
| --- | ---: | --- |
| `DOPE.EXE` | 584,133 | `0c54549d741858ecddd23ac4a92d32b0d4823a537a45dcf1722613dcd6de97d0` |
| `DOPE.MOD` | 957,334 | `b2440ad6d713b38d5ca4c21a4a4ab4ab974e2f951175291769191c39ff7d93ce` |
| `DOPE.TXT` | 1,093 | `a3045224ea46c5d175ece5205ebf5864fa2665dc9ddf4f6f70186c31f2d11357` |
| `FILE_ID.DIZ` | 198 | `3d8c82774e198b8bd7c438c944f513b0c52a955fc970ab0c860a73e37b00793e` |

`DOPE.TXT` states the target requirements as 550 KB of base memory, 600 KB of
extended memory, and 512 KB of GUS RAM for music. It also says the demo ran out
of time and therefore lacks the planned end scroller. The listed objects are
the credits part, a 7800-poly object, environment map object, shadow volume
object, and world domination object.

That matches the executable shape: the demo is built as a stage chain. The main
file is small on disk because every program stage is RNC-compressed, while the
large visual data and object tables expand into separate in-memory MZ load
images.

## Observed Runtime Timeline

Timing pass added on 18 June 2026. The archive above was run in DOSBox-X
2026.01.02 under a local Xvfb display and recorded with an external `ffmpeg`
`x11grab` capture at 30 fps, 1280x800. The DOSBox-X run used `machine=svga_s3`,
`cycles=auto` (DOSBox-X switched to max cycles), `gus=true`, and
`ultradir=C:\ULTRASND`. Timing zero is the start of that external capture, just
before the DOSBox-X window begins executing `DOPE.EXE`.

Hard visual handoffs below were checked at 0.25-second spacing around the cut.
Longer label and camera changes were checked on one-second contact sheets. The
recording length is 303.766 seconds; the final row is the visible state at the
300-second emulator cap, not a verified natural exit.

| Time | Visual state | Recreation notes |
| ---: | --- | --- |
| `00:00.00` | Black DOSBox-X window after launch. | Use this as timing zero; the demo does not draw custom graphics immediately. |
| `00:02.00` - `00:03.50` | Tiny loader/status text near the upper-left. | Text-mode prelude while the runtime brings up the demo. |
| `00:04.75` | First purple chrome logo sliver appears at the right edge. | Start the horizontal logo sweep here. |
| `00:05.00` - `00:19.75` | Purple chrome `Complex Media` logo sweeps left across black. | Large cropped bitmap/text object; each frame is mostly black plus the moving logo. |
| `00:20.00` - `00:42.50` | The sweep continues as the logo palette/asset shifts toward orange `Complex Labs`; the tail exits left. | Same staging idea, but the visible word fragment and palette change. By `00:42.25` only a tiny orange shard remains. |
| `00:42.75` | Blue stone-slab credit frame appears. | Hard handoff from logo sweep to the credit/tableau part. |
| `00:49.00` | `jmagic` / `code` credit appears between the slabs. | Text is centered between the moving blue slabs. |
| `00:58.00` | `jugi` / `music` credit replaces the coder credit. | Same slab framing, different text payload. |
| `01:06.00` | `jugi` / `graphics` credit appears. | The slabs continue sliding while text swaps. |
| `01:12.00` | The quoted `"dope"` title appears. | Final title beat of the credit setup. |
| `01:27.50` | The credit slabs give way to the first 3D object scene. | Bottom floor/line art and a small central emblem become visible. |
| `01:27.75` | The 3D composition is fully established. | Use this as the first stable frame of the blue object part. |
| `01:34.00` | `strong BLUR motion` label is visible in the upper-left. | Label overlays the same moving blue object scene. |
| `01:40.00` - `02:26.75` | Blue object/blur sequence: close passes, smeared object surfaces, and camera pushes through the geometry. | This is the long motion-blur object section; the camera repeatedly clips close to the model, then pulls back. |
| `02:27.25` | White flash/fade begins at the end of the blur section. | Transition out of the blue object part. |
| `02:27.50` | Brief near-black frame. | Separator frame before the next scene. |
| `02:27.75` | 7800-poly object appears on a purple starfield. | Start the starfield object section here. |
| `02:50.00` | `7800 PHONG PHYSICS` label is visible in the upper-left. | Same object, now explicitly labeled as the high-poly/phong object part. |
| `03:10.00` - `03:17.75` | The high-poly object fills the frame and slides off to the left. | Final close pass of the 7800-poly object. |
| `03:18.25` | The object is gone; only the starfield remains. | Empty starfield bridge. |
| `03:22.00` | First faint orange fire spiral appears over the starfield. | Start the fire/clouds accumulation here. |
| `03:40.00` | `FIRE CLOUDS` label is visible. | The orange spiral has become the dominant visual. |
| `04:21.25` | Fractal/landscape plane begins to overlay the fire spiral. | First visible grey ground/scape element. |
| `04:35.00` | `FRACTAL SCAPE` label is visible. | Fire spiral remains behind the grey fractal landscape. |
| `04:52.50` | Checker/dome texture first becomes visible under the spiral. | Start of the final checker overlay. |
| `04:52.75` | Checker overlay is clearly readable. | Use this as the practical handoff into the final visual. |
| `04:55.00` - `05:03.77` | Checker/dome texture with the orange spiral still rotating over it. | This was still visible when the 300-second emulator cap ended the run. |

## Runtime Captures

A follow-up capture on 19 June 2026 used DOSBox-X 2026.01.02 internal
`mpegts-h264` capture from the same `clx_dope.zip` archive. The run kept GUS
enabled, routed SDL audio to the dummy driver, removed the earlier Escape key
injection, and let the demo run to a hard emulator cap. The resulting stream is
720x400 video with 336.099822 seconds of duration. Timestamps below are capture
stream timestamps.

Six frames are useful because they hit distinct visual states rather than
nearby variants of the same effect.

The sequence GIF below is assembled from those six follow-up frames. It is not
a continuous clip, but it gives a quick section index for the later code notes:
credit/title slabs, blue blur object, 7800-poly object, fire clouds, fractal
scape, and checker/dome environment mapping.

![Dope runtime sequence GIF showing the major visual sections](images/dope-runtime-sequence.gif)

At `01:12.000`, the quoted title beat closes the credit-slab setup:


![Dope runtime frame at 01:12.000 showing the quoted title between blue slabs](images/dope-runtime-01-12-000.png)

At `01:34.000`, the `phong BLUR motion` label is visible over the blue object
section:


![Dope runtime frame at 01:34.000 showing the blur-motion object section](images/dope-runtime-01-34-000.png)

At `02:50.000`, the `7800 PHONG polys` object is established on the purple
starfield:


![Dope runtime frame at 02:50.000 showing the 7800 Phong-polys object over a starfield](images/dope-runtime-02-50-000.png)

At `03:40.000`, the fire-cloud spiral is the dominant visual:


![Dope runtime frame at 03:40.000 showing the fire-cloud spiral](images/dope-runtime-03-40-000.png)

At `04:35.000`, the fractal-scape plane is visible under the fire spiral:


![Dope runtime frame at 04:35.000 showing the fractal scape under the fire spiral](images/dope-runtime-04-35-000.png)

At `05:00.000`, the checker/dome environment-mapping section has taken over:


![Dope runtime frame at 05:00.000 showing the checker environment-mapping section](images/dope-runtime-05-00-000.png)

The existing runtime frames and sequence GIF were not regenerated, cropped,
padded, resized, or touched in this concordance pass.

## Runtime-To-Code Concordance

The six runtime frames are a section index for the stage-chain architecture.
They do not identify every RNC stage with certainty, but they do tie the
visible show to the loader, the unpacked stage entries, the 3D object pipeline,
and the framebuffer/palette loops described below.

```text
runtime evidence                         code evidence
01:12 credit/title slab beat             stage-chain setup, stage 1 VGA/raster work
01:34 blue blur-motion object            object-stage frame pacing and work-buffer blit
02:50 7800 Phong-polys starfield object  fixed-point matrix, projection, polygon ordering
03:40 fire-cloud spiral                  additive/tile overlay and palette-heavy staging
04:35 fractal scape under fire           stage data/resource chain plus framebuffer restore
05:00 checker/dome environment mapping   stage 5 nibble matte/residue loop
sequence GIF section index               eight RNC-packed MZ stages and stage handoffs
```

The loader-to-stage bridge is exact. `DOPE.EXE` is a front MZ image plus seven
overlay MZ images, each RNC method-1 compressed. The depacker tail restores the
original `SS:SP`, relocates the original far entry stored at `CS:0000`, and
`ljmp`s to it. That explains why the captured runtime is a sequence of hard
visual sections rather than one monolithic loop: the demo is literally running
a chain of unpacked programs.

The `01:12.000` credit/title frame belongs to the opening VGA/raster phase
rather than to the later 3D object renderer. Stage 1 enters through the recovered
entry, waits for retrace, sets BIOS mode `13h`, reprograms the Sequencer/CRTC
for unchained VGA, clears `A000h` with 32-bit stores, repacks interleaved image
planes in the `0x0c5c` builder, and drives palette ramps through `0x0c00` and
`0x0c24`. That matches a slab/logo/title presentation where palette and image
data dominate.

The object frames at `01:34.000` and `02:50.000` connect to the fixed-point 3D
stage family. Stage 7 shows the complete pipeline: sine/cosine table lookup at
`0x410`/`0x810`, matrix builder `0x1ed6`, vertex transform/projection at
`0x1c90`, object-record scanning at `0x2fca`, and bit/radix-style polygon
ordering at `0x3005`. The projection constants center output around `160,100`,
which matches the 320x200 object framing in the runtime captures.

The blur/object sections also fit the work-buffer discipline. The frame blitter
at `0x308c` waits on a frame flag, copies the prepared work buffer to `A000h`
with `rep movsd`, and immediately restores that work buffer from a clean
background segment. That model explains the captured object scenes: destructive
object drawing can be complex, because every frame starts again from the
background instead of individually erasing polygons.

The fire-cloud and fractal-scape frames are the least safely nameable at exact
stage granularity, but their code family is visible. The stage images contain
RIX resource markers and dense data islands, while stage 7's `0x3146` additive
tile loop adds 128x128 dword source material into a work buffer. That kind of
additive composition is consistent with the accumulated orange fire/cloud
visuals over a stable background, though the exact stage-to-label assignment
still remains partly inferred.

The `05:00.000` checker/dome environment-mapping frame has the clearest
framebuffer-processing hook outside the generic 3D path. Stage 5's `0x31e2`
reads the current `A000h` image, masks every dword with `0F0F0F0Fh`, writes the
residue back to VGA, and copies it into secondary buffers. That low-nibble
residue trick is a plausible support path for the checker/dome/environment-map
look because it preserves cheap per-pixel state without expensive full-color
math.

The audio/runtime side is also part of the concordance. Stage 0 loads
`DOPE.MOD`, initializes the ProTracker-for-GUS path, and transfers samples to
GUS RAM. That lets the visual stages spend their frame time on VGA copies,
matrix math, palette/resource staging, and object ordering instead of software
mixing a 28-channel module.

## Top-Level File Layout

`DOPE.EXE` is an MZ file, but the normal MZ image is only the first packed
stage.

```text
MZ header size:            32 bytes
MZ packed image size:  11,338 bytes
file size:            584,133 bytes
overlay size:         572,795 bytes
relocation entries:         0
packed entry:          0294:000c
```

The key signature is `RNC\1`. There are eight RNC method-1 blocks:

| Stage | Packed MZ offset | RNC offset | Packed bytes | Unpacked image bytes | Original entry |
| ---: | ---: | ---: | ---: | ---: | --- |
| 0 | `0x000000` | `0x000020` | 10,551 | 25,570 | `0000:03d4` |
| 1 | `0x002c4a` | `0x002c6a` | 51,014 | 213,658 | `0010:0bd5` |
| 2 | `0x00f64c` | `0x00f66c` | 58,000 | 449,862 | `0010:376c` |
| 3 | `0x01dbfa` | `0x01dc1a` | 52,802 | 374,042 | `0010:2bec` |
| 4 | `0x02ad50` | `0x02ad70` | 179,914 | 485,072 | `0010:1caa` |
| 5 | `0x056f22` | `0x056f42` | 72,451 | 386,842 | `0010:3089` |
| 6 | `0x068d44` | `0x068d64` | 76,685 | 446,560 | `0010:38e8` |
| 7 | `0x07bbe2` | `0x07bc02` | 76,430 | 457,550 | `0010:3194` |

The packed MZ header entry does not point to the original program. It points to
the RNC depacker stub after the compressed block. The original far jump target
is stored in the stub variable area at runtime `CS:0000`.

The depacker tail does this after expansion:

```asm
mov si, [cs:0004]       ; original SP
mov di, [cs:0006]       ; original SS, relative segment
add di, dx              ; relocate SS
add [cs:0002], dx       ; relocate original CS in far pointer
sub dx, 0010h
mov es, dx
mov ds, dx
xor ax, ax
xor bx, bx
cli
mov sp, si
mov ss, di
sti
ljmp far [cs:0000]      ; original IP:CS, now relocated
```

That is why the recovered entry for stage 1 is `0010:0bd5`, not the packed
header entry `0c75:000c`.

## Stage Chain Interpretation

The stages are independent packed MZ load images. Stage 0 is the main DOS
runtime and music loader. Stages 1 through 7 are visual programs in the overlay.

Observable stage traits:

| Stage | Static evidence | Likely role |
| ---: | --- | --- |
| 0 | CDPI/DPMI/VCPI strings, `dope.mod`, ProTracker for GUS strings | Runtime, memory setup, GUS replay setup |
| 1 | Compact VGA setup, `RIX3`, simple palette/raster loops | Intro/logo/routine-box style setup |
| 2 | Large image/object tables, `RIX3` and `RiX5`, repeated debug text | Object/effect stage |
| 3 | Similar object-stage code and RIX resources | Object/effect stage |
| 4 | Largest compressed stage, high nonzero data density, RIX resources | Heavy object stage, likely one of the large 3D objects |
| 5 | Nibble-mask A000 loop and 3D/palette code | Environment-map or shadow-style stage |
| 6 | Palette marker `palette1`, object-stage renderer | Object/effect stage |
| 7 | Final-stage text, 3D matrix/project/blit routines | Late/final 3D object stage |

The exact mapping to "7800 polys", "environment map", "shadow volume", and
"world domination" needs a runtime visual trace to be fully certain. The
binary clearly contains the separate object stages, but it does not label them
with those names.

## Stage 0: DOS Runtime And Protected-Mode Probe

Stage 0 starts at unpacked offset `0x03d4`. It is a 16-bit real-mode program
that freely uses 386 operand-size/address-size prefixes.

The first part records real-mode bases and then probes the environment:

```asm
cli
cld
push cs
pop  ds
call 0366h
mov ax, es
movzx eax, ax
shl eax, 4
mov [0017f0h], eax      ; ES physical base
...
mov ebx, 0000017eh
shl ebx, 4
mov [0017f8h], ebx      ; local physical base
...
mov ax, 1687h
int 2fh                 ; DPMI installation check
or ax, ax
je 0685h                ; DPMI path found
...
mov ax, 4300h
int 2fh                 ; XMS/HIMEM style check
cmp al, 80h
je 0baeh
```

The loader's environment strings include:

```text
386 or better not detected!!!
System is already in V86 mode, and no VCPI found!!!
DPMI host is not 32bit!!!
Couldn't enter 32bit protected mode!!!
```

This is not a DOS/4GW or PMODE/W header. The string says `CDPI v0.2`, and the
code is a small custom life-support layer that decides how to reach a usable
32-bit-capable execution environment.

### A20 Probe

The A20 test is compact and direct:

```asm
xor ax, ax
mov fs, ax
dec ax
mov gs, ax              ; FS:0000 and GS:0010 alias only if A20 is off
call 0500h
je  04d7h
in  al, 92h             ; fast A20 gate
or  al, 02h
out 92h, al
call 0500h
je  04d7h
...
mov al, d1h
out 64h, al             ; keyboard-controller fallback
...
mov al, dfh
out 60h, al
```

The actual alias test:

```asm
mov al, fs:[0000]
mov ah, al
not al
mov gs:[0010], al
cmp ah, fs:[0000]
mov fs:[0000], ah
ret
```

If writing through `FFFF:0010` changes `0000:0000`, A20 is still masked. The
loader first tries port `0x92`, then the keyboard controller command path, and
only then reports failure.

## RNC Depacker Inner Loop

Every packed MZ image carries a similar depacker after its RNC data. Stage 0's
stub is easiest to see at file offsets around `0x2960..0x2c4a`.

The RNC1 header gives three Huffman tables. The stub allocates three 0x80-byte
tables on the stack:

```asm
sub sp, 001ah
mov bp, sp
sub sp, 0180h
mov ax, sp
mov [bp+08], ax
add ax, 0080h
mov [bp+0a], ax
add ax, 0080h
mov [bp+0c], ax
...
call build_table_for_literals
call build_table_for_offsets
call build_table_for_lengths
```

The table builder at `0x2b6d` reads code lengths and emits decode records. The
interesting part is the bit-reversal used for canonical Huffman codes:

```asm
mov al, 01h
xor bx, bx
mov dx, 8000h
...
cmp ss:[si], al         ; code length equals current length?
jne next_symbol
mov ax, 1
shl ax, cl
dec ax
stosw                   ; code mask
...
xor ax, ax
rcr bx, 1
rcl ax, 1               ; reverse bits into AX
loop bit_reverse
stosw                   ; reversed code
```

The bit reader at `0x2b17` maintains a 32-bit-ish split buffer in
`[bp+18]`/`[bp+16]` and a bit count in `[bp+15]`. It masks the requested low
bits, refills from the compressed stream when not enough bits remain, and
stores the advanced state back.

Main expansion alternates literal runs and matches:

```asm
decode literal_count via table [bp+08]
jcxz no_literals
rep movsb               ; copy literal bytes from packed stream to output

; refresh bit reservoir from the packed stream
mov cl, [bp+15]
mov ax, [si]
...
mov [bp+18], ax

decode match_offset via table [bp+0a]
decode match_length via table [bp+0c]
add cx, 2
...
mov dx, si              ; save compressed source pointer
mov si, di
sub si, ax              ; source = output - distance
mov ds, [bp+02]         ; output segment
rep movsb               ; LZ copy
mov ds, [bp+00]         ; restore packed-input segment
mov si, dx
```

That is standard RNC1 logic: Huffman-coded literal length, raw literal copy,
Huffman-coded back-reference distance and length, then an LZ copy from already
expanded output.

The stub also has segment rollover logic after each block:

```asm
mov bx, di
and di, 000fh
add di, 8000h
shr bx, 4
mov ax, es
add ax, bx
sub ax, 0800h
mov es, ax

mov bx, si
and si, 000fh
shr bx, 4
mov ax, ds
add ax, bx
mov ds, ax
```

So the depacker treats input and output as far pointers and periodically
renormalizes them. That lets a small 16-bit routine expand images much larger
than one 64 KB segment.

## Music Path

`DOPE.MOD` is an external 28-channel ProTracker-style module:

```text
module magic: 28CH
song length: 124
file size:   957,334 bytes
```

The sample table contains loop and percussion names such as `loop2.1`,
`ping.Leadi`, `Basso Sinidist`, `808clap2.pat`, `909rcym1.pat`,
`GrooveLoop`, and `HardPlate`.

Stage 0 contains these runtime strings:

```text
Initializing music replay...
Dumping samples to GUS...
dope.mod
ProTracker for GUS replay routine
ProTracker for GUS - Gravis UltraSound driver
ULTRASND
```

The release requirement for 512 KB of GUS RAM is therefore literal: stage 0
loads the 28-channel module, initializes a ProTracker-derived GUS replay
routine, and transfers samples into the card memory instead of software-mixing
the whole soundtrack into the framebuffer loop.

## Stage 1: VGA Mode And Opening Raster Work

Stage 1's original entry is unpacked offset `0x0cd5`. It waits for retrace,
sets mode 13h, then reprograms VGA registers.

```asm
mov dx, 03dah
in  al, dx
test al, 08h
jne wait_not_vblank
in  al, dx
test al, 08h
je  wait_vblank

mov ax, 0013h
int 10h

mov dx, 03c4h
mov ax, 0604h
out dx, ax              ; sequencer memory mode: unchain/odd-even control

mov dx, 03d4h
mov ax, 0014h
out dx, ax
mov ax, e317h
out dx, ax              ; CRTC mode control/protect bits

mov dx, 03c2h
mov al, e3h
out dx, al              ; misc output

mov dx, 03c4h
mov ax, 0f02h
out dx, ax              ; enable all VGA planes
```

Then it clears 64 KB of VGA memory using 32-bit stores in real mode:

```asm
push 0a000h
pop es
xor di, di
mov cx, 4000h
rep stosd               ; 16,384 dwords = 65,536 bytes
```

This is the usual "386 real mode" style: the CPU is still in real mode, but
the code uses operand-size prefix `66h` for dword throughput.

### Interleaved Plane/Image Builder

The loop at `0x0c5c` is a useful example of how the opening image data is
repacked:

```asm
push ds
mov cx, 00c3h           ; 195 rows
row:
  push cx
  mov cx, 0040h         ; 64 groups per row
col:
  mov bl, [si+08]
  mov bh, [si+0c]
  shl ebx, 16
  mov bl, [si+00]
  mov bh, [si+04]
  mov es:[di], ebx      ; four packed bytes in one dword write
  mov bx, ds
  inc bx
  mov ds, bx            ; next source plane/segment slice
  add di, 4
  dec cx
  jne col
  add di, 50h           ; destination row stride/padding
  pop cx
  dec cx
  jne row
pop ds
ret
```

Each inner iteration reads four bytes at offsets `0,4,8,12`, packs them into
`EBX`, and stores one dword. It then increments `DS`, which means the source is
not a flat byte stream; the source planes are spread across adjacent segments.
The destination advances linearly in 4-byte cells with padding after each row.

### Palette Ramp Builder

Stage 1 also builds palette ramps in RAM before writing them to the DAC.
Routine `0x0c00` takes a base intensity in `AH`, a signed slope in `DX`, clamps
the slope to `0..0x100`, and writes 64 entries:

```asm
test dx, dx
jns positive
xor dx, dx
positive:
shr dx, 4
cmp dx, 0100h
jb  ok
mov dx, 0100h
ok:
mov cx, 0040h
loop:
  mov cs:[bx], ah
  inc bx
  cmp ax, 3f00h
  ja  saturated
  add ax, dx
saturated:
  dec cx
  jne loop
ret
```

The DAC writer at `0x0c24` combines two 64-entry ramps with saturation:

```asm
mov dx, 03c8h
xor al, al
out dx, al
inc dx                  ; 03c9h
...
mov bl, cs:[ramp_index_a]
mov al, cs:[ramp_a+bx]
mov bl, cs:[ramp_index_b]
add al, cs:[ramp_b+bx]
cmp al, 3fh
jb no_sat
mov al, 3fh
no_sat:
out dx, al
```

That gives cheap palette fades without touching the framebuffer.

## Timer Hook And Frame Pacing

The object stages carry a repeated timer setup pattern. Stage 7's version is
around `0x1b8a`.

It saves the old timer interrupt vector:

```asm
push 0
pop es
mov eax, es:[0020h]     ; INT 08h vector
mov cs:[1a27h], eax
mov ax, cs
shl eax, 16
mov ax, 1a47h
mov es:[0020h], eax     ; install local timer handler
```

It then reads the PIT and writes a new divisor:

```asm
mov al, 30h
out 43h, al
mov al, bl
out 40h, al
mov al, bh
out 40h, al
mov word cs:[1a2d], 0
```

The cleanup path restores the old vector and reprograms the PIT back through
the saved old handler path. This is why several stages wait on a memory flag
instead of directly polling retrace in every loop.

## 3D Object Stage: Matrix Builder

Stage 7 has a clear fixed-point 3D path. It first derives sine/cosine values
from tables at offsets `0x410` and `0x810`:

```asm
mov si, [1dd0]          ; angle X
and si, 0ffeh
movsx eax, word [si+0410h]
movsx ebx, word [si+0810h]
mov [1d94], eax
mov [1da0], ebx
...
```

Then routine `0x1ed6` builds a 3x3 rotation matrix in fixed point. The code is
full of patterns like this:

```asm
mov eax, [1da4]
imul dword [1da8]
mov ebx, [1d98]
imul dword [1d94]
sar ebx, 0fh
imul ebx, [1d9c]
sub eax, ebx
sar eax, 0fh
mov [1dac], eax
```

The shift by 15 bits is the fixed-point normalization. The matrix elements are
stored at `0x1dac..0x1dcc` and are consumed by the vertex transformer.

## 3D Object Stage: Vertex Transform And Projection

The vertex routine begins at `0x1c90`. The input model is addressed through
`FS`; output work arrays are addressed through `GS`.

The first few instructions convert an index into a 6-byte vertex pointer:

```asm
shl si, 1
lea si, [esi + esi*2]   ; si = index * 6
add si, fs:[0008h]      ; add vertex base
```

Then it loads signed 16-bit `x,y,z`:

```asm
movsx eax, word fs:[si+0]
movsx ebx, word fs:[si+2]
movsx ecx, word fs:[si+4]
```

The row dot products are straight fixed-point multiply/adds:

```asm
imul eax, [1dac]
imul ebx, [1db0]
imul ecx, [1db4]
add eax, ebx
add eax, ecx            ; transformed coordinate row
```

The projection path later adds translation and divides by depth:

```asm
mov ebp, [1b80]         ; transformed z
mov eax, [198a]
shl eax, 8
sub ebp, eax            ; depth denominator

mov eax, [1b78]         ; transformed x
imul dword [198e]       ; scale
idiv ebp
sar eax, 3
adc ax, 00a0h           ; screen x around 160
mov gs:[si+6d62h], ax

mov eax, [1b7c]         ; transformed y
imul dword [198e]
idiv ebp
lea eax, [eax + eax*2]  ; y * 3
sar eax, 5
adc ax, 0064h           ; screen y around 100
mov gs:[si+6d60h], ax
```

This is a conventional software 3D projection, but implemented in 16-bit code
with 32-bit registers and explicit segment registers. The center constants
`160` and `100` match a 320x200 target.

The per-vertex loop increments `SI` by four because projected output records
are dword-indexed. It stops when the current vertex index reaches the model
vertex count at `FS:[0000]`.

## 3D Object Stage: Object Records And Polygon Ordering

Routine `0x2fca` scans records beginning at `0x1810`. Each record is `0x3a`
bytes. A zero word terminates the list; `0xffff` skips a slot.

```asm
mov si, 1810h
scan:
  cmp word [si], 0
  je done
  cmp word [si], 0ffffh
  je next
  push si
  mov bx, si
  call 2f0bh            ; process object record
  pop si
next:
  add si, 003ah
  jmp scan
```

`0x2f0b` loads the model segment into `FS`, copies object-local translation,
rotation, and scale fields into global working variables, builds the matrix,
transforms vertices, and calls face/polygon routines.

After face collection, routine `0x3005` performs an in-place bit/radix-style
ordering pass over dword polygon records:

```asm
mov bp, 4
mov edx, 00200000h
outer:
  mov si, 0
  mov di, 1f40h
  mov bx, [1978]        ; tail pointer
scan:
  mov eax, [si]
  test ax, ax
  js end_scan
  add si, bp
  test eax, edx
  je front
  mov [bx], eax         ; bit set: move to tail side
  sub bx, bp
  jmp scan
front:
  mov [di], eax         ; bit clear: keep front side
  add di, bp
  jmp scan
...
add edx, edx
jae outer
```

This is not a comparison sort. It partitions by successive bit masks, which is
cheap for fixed-point depth/priority keys and predictable on a 486/Pentium.

## 3D Object Stage: Overlay Add Loop

Stage 7's `0x3146` adds a source tile into the work buffer. Its caller computes
`DI` from sine tables, then calls this routine repeatedly to add bright marks
or overlays at moving positions.

```asm
push 37eah
pop ds
push 13b4h
pop es
xor si, si
mov ch, 80h             ; 128 rows
row:
  mov cl, 20h           ; 32 dwords = 128 pixels per row
col:
  mov eax, [si]
  add es:[di], eax      ; additive 4-pixel write
  add si, 4
  add di, 4
  dec cl
  jne col
  add di, 80h           ; skip to next destination row in 320-byte stride
  dec ch
  jne row
ret
```

The add is dword-wide. It is not checking carries or clipping here; the caller
chooses positions, and the palette/data representation makes additive
composition cheap.

## 3D Object Stage: Frame Blit And Background Restore

The blitter at `0x308c` waits for a frame flag, optionally scrolls/copies a
strip, then moves the work buffer to VGA memory:

```asm
push [cs:19d4]
pop ds                  ; work buffer segment
push 0a000h
pop es
wait:
  mov ecx, [cs:1996]
  test ecx, ecx
  je wait
mov dword [cs:1996], 0
...
mov si, 0140h
xor di, di
mov cx, 3ed0h
rep movsd               ; 16,080 dwords = 64,320 bytes to A000
```

The copy starts from offset `0x0140`, so it skips a 320-byte guard/top line and
copies exactly 64,320 bytes, one 320x201-ish block minus the guard math used by
the stage's work buffer. Immediately after the VGA copy, the routine restores
the work buffer from a background segment:

```asm
push ds
pop es                  ; ES = work buffer
push 5ab4h
pop ds                  ; DS = clean background
xor si, si
mov di, 0140h
mov cx, 3ed0h
rep movsd
ret
```

That means object drawing is destructive into a work buffer, and each frame
starts from a clean background rather than erasing individual polygons.

## Stage 5: Nibble Matte / Residue Loop

Stage 5 has a compact framebuffer-processing loop at `0x31e2`:

```asm
push 0a000h
pop ds                  ; current VGA page
push 3ecfh
pop es                  ; copy target
push 2edfh
pop fs                  ; secondary target
xor si, si
mov di, 0310h
mov cx, 3e80h           ; 16,000 dwords = 64,000 bytes
loop:
  mov eax, [si]
  and eax, 0f0f0f0fh    ; keep low nibble of every pixel
  mov [si], eax         ; write matte back to A000
  mov fs:[si+0140h], eax
  stosd                 ; also copy to ES:DI
  add si, 4
  dec cx
  jne loop
```

This is a classic demoscene compromise: four pixels are processed at a time,
and the effect relies on palette/nibble encoding instead of expensive per-pixel
math. Keeping only low nibbles turns the existing screen into a masked residue
buffer that can be reused by the stage's later palette and object code.

## RIX Resources And Data Layout

The stage images contain multiple `RIX3`, `RIX3F`, `RIX3@`, and one `RiX5`
marker. They are not executable signatures; they mark image/resource payloads
inside the unpacked images. The distribution therefore combines:

- RNC-compressed MZ program images.
- External `DOPE.MOD` music.
- Embedded RIX-style image resources in the stage images.
- Large object/face/table data in the same expanded stage memory.

This helps explain why many unpacked stages start with long zero ranges and
then have dense data islands. The MZ image contains both code and large BSS-like
working areas, not a tidy code/data section split.

## What The Code Is Optimizing For

Dope's core trick is not one exotic instruction. It is the combination of:

- Small RNC-packed stages to keep disk footprint reasonable.
- Direct real-mode VGA programming instead of a general graphics API.
- 386 dword copies/stores inside 16-bit code.
- Segment registers used as cheap base pointers for model data, work buffers,
  VGA memory, and transformed arrays.
- Fixed-point matrix math with precomputed sine/cosine tables.
- Radix-style polygon ordering instead of comparison sorting.
- Palette and nibble tricks where possible, so per-pixel arithmetic stays
  shallow.
- GUS hardware playback, avoiding a huge software audio mixer inside the
  visual frame loop.

The result is very 1995: big object-show staging, lots of precomputed data,
direct VGA/GUS hardware assumptions, and carefully chosen inner loops that are
small enough for a 486/Pentium-era cache but still push much more data than a
1992-style single real-mode effect.

## Limits Of This Pass

The RNC-expanded code is available and the representative loops above are real
disassembly. The runtime timeline is based on one DOSBox-X/Xvfb capture, with
hard cuts checked at 0.25-second spacing. It is still not a source-level
reconstruction, and the stage-to-effect names remain partly inferred from code
shape, release text, and the observed visuals.
