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:

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.

Dope runtime sequence GIF showing the major visual sections

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

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

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

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

Dope runtime frame at 03:40.000 showing the fire-cloud spiral

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

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

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:

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:

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.