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.
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.

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

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

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

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

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

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

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.
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
ljmps 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.
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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.
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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.
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:
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.
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:
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:
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:
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.MODmusic. - 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.