Analysis model: gpt-5.5 xhigh
Contagion by The Coexistence - Technical Dissection
Scope
This is a static and limited runtime dissection of Contagion by The
Coexistence, the third-place PC demo at The Party 1994. The interesting part is
not a single isolated trick. It is the whole delivery stack: a small real-mode
wrapper, a custom bitstream depacker, an XLINK linked-file archive, a PMODE/W LE
protected-mode payload, linked graphics/music/object assets, and two very tight
z-buffered triangle renderers.
Public references:
- Pouet production page: https://www.pouet.net/prod.php?which=1526
- Scene.org archive: https://files.scene.org/get/parties/1994/theparty94/demo/contagio.zip
- The Party 1994 results: https://archive.scene.org/pub/parties/1994/theparty94/info/results.txt
- Pouet The Party 1994 listing: https://www.pouet.net/party.php?when=1994&which=45
This is not a source reconstruction. The high-level scene script and every object format field are not fully named here. What is covered in detail is the outer packing/linking system, the PMODE/W memory layout, VGA/DPMI services, the object renderer dispatch, z-buffer clearing, framebuffer copying, palette upload, and the inner loops of the shaded and mapped triangle paths.
Offset notation:
CONTAGIO.EXE+0x...means a file offset in the released wrapper.MAIN.EXE+0x...means a file offset in the carved XLINKMAIN.EXEpayload.0x...code labels in the PMODE/W section areMAIN.EXEfile offsets unless otherwise stated.- Protected-mode code is 32-bit x86 unless the snippet clearly uses 16-bit registers or BIOS/DOS calls.
Examined Files
The archive contains:
CONTAGIO.DAT 166,355 bytes
CONTAGIO.EXE 2,344,058 bytes
CONTAGIO.NFO 5,840 bytes
FILE_ID.DIZ 623 bytes
TC.APP 7,511 bytes
TC.NFO 11,712 bytes
TCDISTS.TXT 3,433 bytes
Hashes:
746c81dc585dc6a412867a666aaa1de9cef0ab3a9e249a640cff2b7748b2881d contagio.zip
3682d3ee34090d16ff89293e1273f83fd1c31e8e67feee2797b728c998948f07 CONTAGIO.DAT
26c7a9f4076ec1bea37759033503f887af4831e1cce8420a989bb7690d6c9d9c CONTAGIO.EXE
cfad8e6e5e197f05002ee20a220584f4cf030420f2ec255af1de92367b2950ff CONTAGIO.NFO
abe3c6a89a838b8fa2be2d791bb1fb7f5789991030c9cd8da4fba060a712ac82 FILE_ID.DIZ
cfa032d5a37dff362577c17c3b97da374f6b45de4f3a9f66f9c9b4bb8bae2139 TC.APP
42e946edcefc796e0ddf3ef7629fbbce66b825c843af4c8c1c8a34529e565fe2 TC.NFO
0856439196f2e5a468bdc903874e54933768b9523571628c8ea9bb786980bec7 TCDISTS.TXT
The NFO identifies the release as 29 December 1994 at The Party 1994. Credits:
- Code: Jinx!, Han Solo, InspirE, Iron Eagle, Wicked Mystic
- Graphics: Friday, Stingray
- Music: Libertine
The requirement text asks for at least 580 KB of conventional memory and 2 MB
of extra memory, with a 486DX2/66 and VLB VGA recommended. The feature list
advertises Gouraud shading, Phong shading, environment mapping, Gouraud mapping,
and full-screen smooth voxel space. The code and asset names strongly support
the object-show part of that list: there are separate untextured and mapped
z-buffered render paths, 64 KB environment-map tables, 4 KB texture blocks,
large .BOB mesh/object blobs, and Mode 13h full-screen images.
Competition Position
The Party 1994 official result file lists the PC demo top three as:
1. Project Angel - Impact Studios
2. No! - Nooon
3. Contagion - The Coexistence
Pouet's party listing agrees: Contagion is listed as a PC demo for
MS-DOS/GUS, ranked third at The Party 1994. The archive's FILE_ID.DIZ has a
garbled placement string, but the external result sources and party listing are
consistent.
Runtime Observation
A short DOSBox-X run entered graphics mode and detected a Sound Blaster 16:
[sound card]: Sound blaster 16 at 0x220 using IRQ 7 and DMA channel 5
No payload files were extracted to disk. That matters because the large
CONTAGIO.EXE is not simply a self-extracting ZIP-style package. It is a
real-mode wrapper around an embedded linked-file archive, and it feeds the
runtime directly.
Outer MZ Wrapper
CONTAGIO.EXE begins as a tiny MZ executable with a huge overlay:
file size: 2,344,058 bytes
MZ header size: 32 bytes
load image: 2,114 bytes
overlay: 2,341,912 bytes
CS:IP: fff0:0100
SS:SP: 008f:0200
relocations: 1
The entry maps to file offset 0x20. It is a compact real-mode memory check,
copy/relocate stub, bitstream depacker, and far-return trampoline. There is no
PKLITE or LZEXE signature. The code is custom enough that the best description
is "small MZ depacker plus XLINK loader."
The startup first probes memory and can print:
Not enough memory$
After that it copies a relocated code block with rep movsw, switches the
working segment, and enters a bit-level LZ-style depacker.
Wrapper Depacker Inner Loop
The bit reservoir is held in BP, with DX counting down the 16 bits loaded
from SI:
CONTAGIO.EXE+0x096:
lodsw
xchg bp,ax
mov dx,0010h
The main token loop shifts one bit at a time. A clear branch copies a literal byte from the compressed stream; a set branch falls into match decoding:
literal_or_match:
jb match
literal:
movsb ; copy one literal byte to output
shr bp,1
dec dx
je reload_bits
jae literal_or_match
The exact branch sense is controlled by the carry coming out of shr bp,1.
When a match is selected, the depacker decodes an offset/length pair from the
bitstream and performs an overlapping copy from already-written output. The
copy itself is the classic LZ core:
CONTAGIO.EXE+0x13b:
push si
mov si,di
sub si,bx ; source = current output - match distance
push ds
push es
pop ds ; DS = output segment
rep movsb ; copy match bytes, supports overlap
pop ds
pop si ; restore compressed-stream pointer
Two small decode tables live near CONTAGIO.EXE+0x160 and +0x170. The long
length/end-marker path reaches CONTAGIO.EXE+0x17c. After decompression ends,
the same stub applies relocation fixups to the new image:
CONTAGIO.EXE+0x186:
pop bx
add bx,0010h
fixup_loop:
lodsw
xchg di,ax
add word ptr es:[di],bx
Finally it loads the recovered stack and transfer address and leaves through a far return:
lodsw
add ax,bx
mov ss,ax
lodsw
mov sp,ax
...
retf
So the wrapper has three jobs:
- Expand a small real-mode loader image.
- Relocate it for the actual load segment.
- Transfer control into that loader, which then knows about the linked XLINK archive in the huge overlay.
XLINK Linked Archive
The release uses The Coexistence's XLINK system. TC.NFO describes XLINK as a
data-EXE-file linker by Jinx!, released in October 1994, and CONTAGIO.EXE
contains a clear marker:
CONTAGIO.EXE+0x872: _____XLC@SRT\0\0\0
The linked-file table begins at CONTAGIO.EXE+0x889. Each record is 23 bytes:
15 bytes NUL-padded ASCII filename
4 bytes little-endian size
4 bytes little-endian offset from linked-data base
The data base is CONTAGIO.EXE+0x862, so the physical file offset is:
file_offset = 0x862 + record_offset
The extracted table has 43 records:
MAIN.EXE size=0x028bd3 file=0x000d80
MAINMOD.S3M size=0x08e4dc file=0x029953
PRE.PAL size=0x000300 file=0x0b7e2f
PRE.RAW size=0x00fa00 file=0x0b812f
TC2.BOB size=0x006930 file=0x0c7b2f
IS_BACK.PAL size=0x000300 file=0x0ce45f
IS_BACK.RAW size=0x00fa00 file=0x0ce75f
NAME.PAL size=0x000300 file=0x0de15f
NAME.RAW size=0x00fa00 file=0x0de45f
FACE.BOB size=0x004d4c file=0x0ede5f
NOS.PAL size=0x000300 file=0x0f2bab
GS2.BOB size=0x00bb58 file=0x0f2eab
LBLUE.PAL size=0x000300 file=0x0fea03
PR2.BOB size=0x0084a4 file=0x0fed03
RED.PAL size=0x000300 file=0x1071a7
RONNY.PAL size=0x000300 file=0x1074a7
RONNY3.BOB size=0x008878 file=0x1077a7
SPOT.PHG size=0x010000 file=0x11001f
INDYCA1.BOB size=0x020b8a file=0x12001f
INDYCAR.PAL size=0x000300 file=0x140ba9
MESH.PAL size=0x000300 file=0x140ea9
MESH.RAW size=0x00fa00 file=0x1411a9
VASE3.BOB size=0x0169ac file=0x150ba9
STONE.PAL size=0x000300 file=0x167555
STONE.TEX size=0x001000 file=0x167855
STO_TEX.PAL size=0x000300 file=0x168855
STO_TEX.TEX size=0x001000 file=0x168b55
ENVMAP3.BOB size=0x00d41c file=0x169b55
GREEN.PAL size=0x000300 file=0x176f71
NUMBERS.RAW size=0x00dac0 file=0x177271
MTORUS.BOB size=0x002410 file=0x184d31
MTORUS.ENV size=0x010000 file=0x187141
MTORUS.PAL size=0x000300 file=0x197141
DTORUS.BOB size=0x00481c file=0x197441
DTORUS.ENV size=0x010000 file=0x19bc5d
DTORUS.PAL size=0x000300 file=0x1abc5d
SPACE.BOB size=0x0140dc file=0x1abf5d
FIGHTER.PAL size=0x000300 file=0x1c0039
ABSP2.AMF size=0x013fe1 file=0x1c0339
SNDLOAD.EXE size=0x018a70 file=0x1d431a
FONTFIN2.PCX size=0x001b73 file=0x1ecd8a
CONTAGIO.TC2 size=0x04cde0 file=0x1ee8fd
TEST.TXT size=0x000d9d file=0x23b6dd
The asset organization is very literal:
.PALfiles are 768-byte VGA DAC palettes..RAWfiles are full-screen Mode 13h chunky pictures, usually 64,000 bytes..TEXfiles are 4 KB texture blocks..ENVand.PHGfiles are 64 KB lookup/map tables..BOBfiles are object/mesh blobs.MAINMOD.S3Mis the large Scream Tracker module.ABSP2.AMFis another music/sound resource.SNDLOAD.EXEis a separate DOS executable, likely the sound loader/helper.
CONTAGIO.DAT is itself an LE PMODE/W executable, but the XLINK table also
contains a carved MAIN.EXE of 166,867 bytes. The two are very close but not
identical; the linked MAIN.EXE has a larger tail and is the payload actually
referenced by the wrapper. The analysis below uses the carved MAIN.EXE.
PMODE/W LE Payload
The carved MAIN.EXE is an MZ executable with an LE header at 0x2090, built
for the PMODE/W style DOS extender:
MZ file size: 166,867 bytes
MZ header size: 64 bytes
MZ load image: 8,272 bytes
MZ overlay: 158,531 bytes
MZ CS:IP: 0000:005b
MZ SS:SP: 02c7:0100
LE header: 0x2090
LE object count: 2
LE page size: 0x1000
LE data pages offset: 0xaa00
LE entry object: 1
LE entry EIP: 0x16ef0
LE stack object: 2
LE stack ESP: 0x14aa0
The objects are:
object 1: vsize=0x1ae0f base=0x10000 flags=0x2045 pages=27
object 2: vsize=0x14aa0 base=0x30000 flags=0x2043 pages=4
The LE entry at file offset 0x218f0 is still WATCOM/PMODE runtime code. It
jumps over a visible runtime string:
MAIN.EXE+0x218f0:
jmp 0x21968
db "WATCOM C/C++32 Run-Time system..."
The application-level main is much earlier, at MAIN.EXE+0xaa10.
Application Main Routine
MAIN.EXE+0xaa10 is the top-level initialization and shutdown path. It calls
the WATCOM prolog helper, initializes several runtime systems, enters video
mode, installs selectors/buffers, runs major scene groups, then fades and
restores video:
MAIN.EXE+0xaa10:
push 00000028h
call 0x2187a ; WATCOM/runtime prolog
mov eax,00000004h
call 0xb4de ; init/control path
call 0xcefe
call 0xc9cc ; allocate 128 KB depth/aux buffer selector
call 0xd16c ; set mode 13h and create A000 selector
call 0xd1d5 ; redirect drawing to virtual screen selector
call 0xbde7 ; renderer/object system setup
call 0x20164 ; major scene group
call 0x2134a ; major scene group
call 0xb346 ; major scene/part sequence
call 0x2184e
fade_out:
mov eax,00000200h
call 0xcf6c
; palette fade driven from [0x3470] >> 5 and table at 0x1f841
call 0xcf3f
call 0xd1fc ; restore A000 selector as current target
call 0xca37 ; free depth/aux buffer selector
call 0xb860
This is a conventional protected-mode demo shape for late 1994: render into a linear 64,000-byte virtual screen, keep a linear z-buffer, then copy to VGA memory during a retrace-timed frame boundary.
Video And Selector Services
The video service code is small and direct.
MAIN.EXE+0xd16c enters Mode 13h and creates a DPMI selector for physical VGA
memory:
mov ax,0013h
int 10h
; create selector for physical A0000
call 0xd36c
mov [0x864],ax ; physical A000 selector
mov [0x866],ax ; current draw selector
mov dword [0x868],000a0000h
MAIN.EXE+0xd1d5 switches the current draw target to a virtual framebuffer.
When it succeeds, [0x866] is no longer the A000 selector; it points to the
off-screen 64,000-byte buffer and [0x868] becomes the corresponding linear
address.
MAIN.EXE+0xd1fc undoes that redirection:
; release virtual selector
mov ax,[0x864]
mov [0x866],ax
mov dword [0x868],000a0000h
The vertical retrace wait is the standard VGA input-status-port loop:
MAIN.EXE+0xd220:
mov dx,03dah
wait_not_retrace:
in al,dx
test al,08h
jne wait_not_retrace
wait_retrace:
in al,dx
test al,08h
je wait_retrace
ret
The clear-current-screen path writes 64,000 bytes as 16,000 dwords:
MAIN.EXE+0xd23e:
mov es,[0x866]
xor edi,edi
xor eax,eax
mov ecx,00003e80h
rep stosd
The physical VGA clear does the same against [0x864]. The framebuffer copy is
equally plain:
MAIN.EXE+0xd2bb:
push ds
push es
push edi
push esi
mov es,[0x864] ; VGA A000
xor edi,edi
mov ds,[0x866] ; virtual screen
xor esi,esi
mov ecx,00003e80h
rep movsd ; 64,000 bytes
pop esi
pop edi
pop es
pop ds
ret
Palette upload writes to DAC ports 0x3c8/0x3c9:
MAIN.EXE+0xd320:
cld
mov esi,[ebp+8] ; palette pointer
mov dx,03c8h
mov al,[ebp+0x10] ; first DAC index
out dx,al
inc dx
mov eax,[ebp+0xc] ; entry count
lea ecx,[eax+eax*2] ; count * 3 bytes
rep outsb
Depth Buffer Setup
MAIN.EXE+0xc9cc allocates 0x20000 bytes and creates a selector for it:
MAIN.EXE+0xc9cc:
mov eax,00020000h
call 0x220a3 ; allocate heap
mov [0x3474],eax
push dword [0x3474]
push 00020000h
call 0xd36c ; create selector
mov [0x3482],ax
0x20000 bytes is 128 KB. The z clear routine at MAIN.EXE+0xdb4d proves why:
it fills 64,000 16-bit depth slots, one per Mode 13h pixel.
MAIN.EXE+0xdb4d:
mov es,[0x3482]
xor edi,edi
mov ecx,00008000h
mov eax,ffffffffh
rep stosd
That is a two-byte z-buffer initialized to 0xffff for every pixel. The
triangle loops reject a candidate when its depth word is greater than or equal
to the stored word, so smaller depth wins.
Object Renderer Dispatch
The setup path at 0xbde7 calls several renderer/object initialization
routines:
MAIN.EXE+0xbde7:
call 0xb88d
call 0xbb04
call 0xbca6
The draw dispatcher at 0xbe5a walks object groups and face pointers. A flag
bit chooses between the two major triangle functions:
for each object_group:
for each face:
if ((object_flags & 2) == 0)
draw_shaded_triangle(face); // MAIN.EXE+0xdb6c
else
draw_mapped_triangle(face); // MAIN.EXE+0xe3d4
That split matches the asset set. Some .BOB objects are pure shaded meshes,
while the torus/environment/texture parts have map resources loaded into the
texture selector used by the second path.
Shaded Triangle Path
MAIN.EXE+0xdb6c is the untextured z-tested triangle routine. It starts by
stashing the face pointer and selecting the current screen and z-buffer:
MAIN.EXE+0xdb6c:
pusha
mov eax,[ebp+0x24] ; face/triangle pointer
mov [0x13488],eax
mov ebx,[0x13488]
mov eax,[ebx]
mov [0x14d8],eax
mov eax,[ebx+4]
mov [0x14dc],eax
mov fs,[0x866] ; virtual screen selector
mov gs,[0x3482] ; z-buffer selector
The first geometric test is a signed 2D cross product using projected vertex
coordinates at vertex offsets +0x24 and +0x26:
; ax = v0.y - v1.y
; dx = v2.x - v1.x
imul dx
mov cx,ax
; ax = v0.x - v1.x
; dx = v2.y - v1.y
imul dx
sub cx,ax
js reject
This culls one winding. If the face is visible, the routine computes screen bounds:
min_x starts at 320
max_x starts at -1
min_y starts at 200
max_y starts at -1
For each vertex pointer stored after the face header, it reads the projected x/y
words, updates the bounding box, and records the top vertex index in
[0x134be]. If min_y == max_y, the triangle is flat/empty and is rejected.
The bottom y coordinate is clamped to 199, so spans never step beyond the
64,000-byte framebuffer.
The edge walkers use fixed-point accumulators in globals:
[0x13490] left x, 16.16
[0x13494] right x, 16.16
[0x13498] left dx/dy
[0x1349c] right dx/dy
[0x134a4] left depth/interpolant accumulator
[0x134a8] right depth/interpolant accumulator
[0x134ac] left depth/interpolant step
[0x134b0] right depth/interpolant step
[0x134c8] left shade accumulator, 8.8-ish
[0x134ca] right shade accumulator, 8.8-ish
[0x134cc] left shade step
[0x134ce] right shade step
[0x1348c] current screen row base, y * 320
The routine walks the top-to-bottom polygon as two edges. For each scanline it orders left and right x, clips to the screen, calculates span length, and divides endpoint differences by width to get per-pixel shade and depth deltas. Then it enters the important loop.
Shaded Triangle Inner Loop
The tight span core begins at MAIN.EXE+0xdfc1:
MAIN.EXE+0xdfc1:
add eax,edx ; advance shade/fixed accumulator
adc bp,si ; advance depth high word
cmp bp,word ptr gs:[edi*2] ; compare against z-buffer pixel
jae skip_pixel
mov word ptr gs:[edi*2],bp ; accept depth
mov byte ptr fs:[edi],ah ; write 8-bit color/shade
skip_pixel:
inc edi
dec cx
jne 0xdfc1
The loop has exactly the structure you would expect from an optimized 1994 software z-buffer:
EDIis the linear Mode 13h pixel index:y * 320 + x.GS:[EDI*2]is the 16-bit z-buffer cell for that pixel.BPis the current depth word.AHis the current visible shade/color byte.EAXandEDXhold a packed fixed-point shade accumulator and its step.SIis the depth step contribution.JAErejects farther/equal pixels because the clear value is0xffffand lower depth values win.
The add plus adc pair is a compact way to keep two interpolants coupled to a
single fixed-point carry chain. It is not doing perspective-correct texture
math here; it is a fast linear span interpolator with a depth test and one byte
of output color.
After each scanline, both active edges and the row pointer advance:
add [0x13490],[0x13498] ; left x += left dx/dy
add [0x13494],[0x1349c] ; right x += right dx/dy
add [0x134a4],[0x134ac] ; left depth/interpolant
add [0x134a8],[0x134b0] ; right depth/interpolant
add [0x134c8],word [0x134cc]
add [0x134ca],word [0x134ce]
add [0x1348c],00000140h ; next scanline
inc word [0x134b4] ; y++
This is a scanline polygon filler with global edge state rather than a call-frame-local structure. That is common in Watcom-era demos because globals avoid stack traffic and produce simpler addressing.
Mapped Triangle Path
MAIN.EXE+0xe3d4 is the mapped path. It has the same broad shape as the shaded
triangle function: save face pointer, select screen/z selectors, backface cull,
find top/bottom, walk edges, then draw spans. The differences are in the face
records and the per-pixel payload.
This path selects a third selector:
mov fs,[0x866] ; virtual screen
mov gs,[0x3482] ; z-buffer
mov es,[0x3480] ; texture/environment map table
The vertex loop advances face entries by 8 bytes, not by only a pointer-sized slot, which indicates per-vertex mapping data stored beside the vertex reference. The state block has more interpolants:
[0x134dc] left x, 16.16
[0x134e0] right x, 16.16
[0x134e4] left dx/dy
[0x134e8] right dx/dy
[0x14ec] left map coordinate accumulator
[0x14f4] right map coordinate accumulator
[0x14fc] left second map coordinate accumulator
[0x1504] right second map coordinate accumulator
[0x14e8] left map step
[0x14f0] right map step
[0x14f8] left second map step
[0x1500] right second map step
[0x134f0] left depth/interpolant
[0x134f4] right depth/interpolant
[0x134f8] left depth/interpolant step
[0x134fc] right depth/interpolant step
[0x13514] left shade/add accumulator
[0x13516] right shade/add accumulator
[0x13518] left shade/add step
[0x1351a] right shade/add step
[0x13524] current span width counter
The span setup computes per-pixel deltas by dividing endpoint differences by
the width. Several values are packed by rotating halves through EAX, which is
a Watcom-friendly way of preparing two 16-bit fixed-point components inside a
32-bit register.
Mapped Triangle Inner Loop
The mapped pixel loop begins at MAIN.EXE+0xe9ab:
MAIN.EXE+0xe9ab:
add edx,esi
adc ebx,ecx
adc ebp,dword ptr [0x13520]
adc bh,00h
cmp bp,word ptr gs:[edi*2]
jae skip_pixel
mov word ptr gs:[edi*2],bp
mov al,byte ptr es:[bx]
add al,dh
mov byte ptr fs:[edi],al
skip_pixel:
inc edi
dec word ptr [0x13524]
jne 0xe9ab
This is the classic inner loop in Contagion. Per pixel it:
- Advances packed texture/map/shade/depth state with a carry chain.
- Uses
BPas the candidate z value. - Tests
BPagainstGS:[EDI*2]. - Stores the new z word if the pixel is nearer.
- Uses
BXas an 8-bit by 8-bit table index intoES:[BX]. - Adds
DHas a shade or lighting contribution. - Writes the final palette index to
FS:[EDI].
That mov al, es:[bx] is why the 64 KB .ENV and .PHG resources matter. A
full 16-bit BX address can index a 256x256 table directly. For environment
mapping, the two interpolated components become a lookup into a precomputed
shade/reflection map. For texture/Gouraud mapping, the same loop can treat
ES:[BX] as a texture-like table and add the Gouraud component from DH.
The loop is not perspective-correct in the modern sense. It is affine across the scanline, but it is fast: one table read, one z compare, one z store, one pixel store, and a carry-chain update. On a 486, keeping the framebuffer in a linear DPMI selector and writing A000 only once per frame is exactly the right tradeoff.
After each scanline, the mapped path advances all edge walkers:
add [0x134dc],[0x134e4] ; left x
add [0x134e0],[0x134e8] ; right x
add [0x14ec],[0x14e8] ; left map coordinate
add [0x14f4],[0x14f0] ; right map coordinate
add [0x14fc],[0x14f8] ; left second coordinate
add [0x1504],[0x1500] ; right second coordinate
add [0x134f0],[0x134f8] ; left depth/interpolant
add [0x134f4],[0x134fc] ; right depth/interpolant
add [0x13514],word [0x13518]
add [0x13516],word [0x1351a]
add [0x134d8],00000140h ; next screen row base
The important difference from the shaded path is that color is no longer just
AH. The mapped path derives color from a table lookup plus an additive shade
term. That is the code-level signature of the NFO's "environment mapping" and
"Gouraud mapping" claims.
High-Level Effect Reading
The NFO's feature names can be connected to actual data and loops:
- Gouraud shading: the
0xdb6cpath interpolates a shade byte and writes it directly after a z test. - Environment mapping: the
0xe3d4path uses a 16-bitBXtable index throughES:[BX], and the archive includes 64 KB.ENVfiles. - Gouraud mapping: the mapped loop adds
DHto the table result before storing the pixel. - Phong-like shading: the archive includes
SPOT.PHG, a 64 KB table. The loop shape is compatible with using a precomputed lighting table rather than doing per-pixel normal math. - Full-screen voxel space: this pass did not fully recover the voxel routine, but the top-level payload has separate major scene calls outside the two mesh triangle routines, and the NFO advertises it as a distinct full-screen part.
The phrase "Phong shading" in 1994 demo NFOs often means "uses a precomputed
normal/light lookup table that looks like Phong", not necessarily a physically
faithful per-pixel normal interpolation. SPOT.PHG being exactly 64 KB fits
that period-correct interpretation.
Audio Path
The linked resources contain both MAINMOD.S3M and ABSP2.AMF, plus the helper
executable SNDLOAD.EXE. Runtime detection in DOSBox-X found a Sound Blaster
16, even though Pouet labels the production as MS-DOS/GUS. The NFO and runtime
therefore point to multiple audio paths or at least a setup layer that can
detect SB hardware. I did not fully reverse SNDLOAD.EXE in this pass.
Why The Design Works
The system is pragmatic for a late-1994 DOS demo:
- XLINK avoids shipping dozens of loose files while still letting the code refer to file-like resource names.
- PMODE/W gives flat-ish 32-bit code and DPMI selectors without writing a full extender.
- Mode 13h keeps the final presentation simple: 320x200 chunky pixels and direct palette control.
- The virtual framebuffer avoids slow random writes to VGA memory during triangle rendering.
- The 128 KB z-buffer makes the object show visually robust; faces can be emitted without perfect painter sorting.
- The 64 KB lookup tables trade memory for speed, which is exactly right on a 486 with a few megabytes of RAM.
The two triangle loops are the center of the demo's renderer. The shaded path is minimal: interpolate shade/depth and store one byte if nearer. The mapped path adds a 16-bit table lookup and an additive shade term, giving texture, environment, or Phong-table effects while staying compact enough for a 486.
Remaining Unknowns
- I did not fully name every high-level scene routine called from
0x20164,0x2134a, and0xb346. - I did not fully decode the
.BOBmesh format. The renderer reveals enough to identify face pointers, projected coordinates, flags, and mapping fields, but not a complete standalone converter. - I did not deeply reverse
CONTAGIO.TC2; it looks MZ-like at the start but does not behave like a normal LE program. - I did not deeply reverse
SNDLOAD.EXE. - The voxel-space routine was not isolated in this pass. The mesh renderer is clear; the landscape/voxel part remains a good target for a second pass.