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:

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:

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:

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:

  1. Expand a small real-mode loader image.
  2. Relocate it for the actual load segment.
  3. 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:

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:

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:

  1. Advances packed texture/map/shade/depth state with a carry chain.
  2. Uses BP as the candidate z value.
  3. Tests BP against GS:[EDI*2].
  4. Stores the new z word if the pixel is nearer.
  5. Uses BX as an 8-bit by 8-bit table index into ES:[BX].
  6. Adds DH as a shade or lighting contribution.
  7. 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:

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:

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