The Claim

“Fun fact: Descent didn’t use 3D file formats per se. Instead, 3D models were compiled as x86 assembler doing draw commands.” — @praeclarum, 2025

Verdict (TL;DR)

The tweet is partially right about the spirit, but technically wrong about the mechanism. Descent’s 3D models are not “compiled as x86 assembler.” They are a custom bytecode — a stream of 16-bit opcodes with inline operand data — that is interpreted at runtime by a hand-written x86 assembly language interpreter. The model data contains no x86 machine instructions whatsoever. It is a domain-specific virtual machine for 3D rendering, where the “program” is the model and the “CPU” is an opcode dispatch loop in INTERP.ASM.


1. The Interpreter Engine

The entire polygon model interpreter lives in a single file: 3D/INTERP.ASM (998 lines of x86-386 assembly, Watcom/MASM syntax).

1.1 Entry Points

There are four public entry points, declared in LIB/3D.H:

FunctionDeclarationPurpose
g3_set_interp_points3D.H:324Gives the interpreter a vertex output array
g3_draw_polygon_model3D.H:328Main interpreter: execute model bytecode
g3_init_polygon_model3D.H:331One-time init pass: validate and translate colors
g3_draw_morphing_model3D.H:334Morphing variant with alternate vertex source

All four use Watcom register-based calling conventions, specified by #pragma aux directives at 3D.H:386-389:

#pragma aux g3_draw_polygon_model "*" parm [esi] [edi] [eax] [edx] [ebx] value [al] modify exact [];

Register parameter mapping for g3_draw_polygon_model:

  • ESI = pointer to model bytecode (model_data)
  • EDI = pointer to array of bitmap (texture) pointers
  • EAX = pointer to animation angles (vms_angvec array)
  • EDX = lighting value (fixed-point)
  • EBX = pointer to glow values array
  • Returns: AL (boolean — 1 = drew, 0 = off-screen)

1.2 The Program Counter

The EBP register serves as the program counter through the model bytecode stream.

At INTERP.ASM:196, immediately upon entry:

g3_draw_polygon_model:
    pushm   eax,ebx,ecx,edx,esi,edi,ebp
    mov     ebp,esi             ; ebp = interp ptr (PROGRAM COUNTER)

From this point on, EBP always points to the current position in the bytecode stream. Each opcode handler reads its operands at fixed offsets from EBP, then advances EBP past the operand data before dispatching the next opcode.


2. The Opcode Dispatch Mechanism

2.1 The Dispatch Table

The dispatch table is defined at INTERP.ASM:107-116:

opcode_table    dd  op_eof          ;0 = eof
    dd  op_defpoints    ;1 = defpoints
    dd  op_flatpoly     ;2 = flat-shaded polygon
    dd  op_tmappoly     ;3 = texture-mapped polygon
    dd  op_sortnorm     ;4 = sort by normal
    dd  op_rodbm        ;5 = rod bitmap
    dd  op_subcall      ;6 = call a subobject
    dd  op_defp_start   ;7 = defpoints with start
    dd  op_glow         ;8 = glow value for next poly
n_opcodes = ($-opcode_table)/4      ; = 9

This is an array of 9 dword (32-bit) code addresses in the .DATA segment. It is not read-only — this is critical for the morphing system (see Section 5).

2.2 The next Macro (Jump Dispatch)

Defined at INTERP.ASM:162-171:

next    macro
    xor     ebx,ebx
    mov     bx,[ebp]                    ; load 16-bit opcode from [EBP]
    mov     ebx,opcode_table[ebx*4]     ; index into dispatch table
    jmp     ebx                         ; jump to handler (tail-call)
    endm

This is used when the current handler has finished and control should not return — the handler ends with next and execution flows directly to the next opcode’s handler. The opcode is a 16-bit unsigned integer (a word), zero-extended to 32 bits, then used as an index into opcode_table.

2.3 The call_next Macro (Call Dispatch)

Defined at INTERP.ASM:174-183:

call_next   macro
    xor     ebx,ebx
    mov     bx,[ebp]                    ; load 16-bit opcode from [EBP]
    mov     ebx,opcode_table[ebx*4]     ; index into dispatch table
    call    ebx                         ; CALL handler (returns via RET)
    endm

This is used when the caller needs to regain control after the sub-stream finishes — specifically by OP_SORTNORM (which draws two sub-streams in order) and OP_SUBCALL (which draws a child subobject then returns). The sub-stream ends when its OP_EOF handler executes ret (line 215).

2.4 Debug Bounds Check

In debug builds (ifndef NDEBUG), the macros check ebx against n_opcodes before the table lookup (lines 166-168):

    cmp     ebx,n_opcodes
    break_if ge,'Invalid opcode'

2.5 No JIT, No Self-Modifying Code (for model data)

There is zero evidence of runtime x86 code generation from model data anywhere in this codebase. The model data is pure data — opcodes and operands — never executed by the CPU. The jmp ebx in the next macro jumps to fixed handler addresses compiled into the interpreter, not to addresses inside the model data.


3. The Opcode Set

3.1 Opcode Reference Table

ValueNameHandlerOperand LayoutSize (bytes)Description
0OP_EOFL215(none)2End of stream; executes ret
1OP_DEFPOINTSL218n:word, then n x vms_vector (12 bytes each)4 + 12nDefine & rotate vertices into point array
2OP_FLATPOLYL245See belowVariableDraw flat-shaded polygon
3OP_TMAPPOLYL310See belowVariableDraw texture-mapped polygon
4OP_SORTNORML415normal vms_vector, point vms_vector, front_ofs word, back_ofs word32BSP-sort node: draw children in back-to-front order
5OP_RODBML456bitmap# word, top_point vms_vector, top_width fix, bot_point vms_vector, bot_width fix36Draw a rod/cylinder billboard
6OP_SUBCALLL480subobj# word, offset vms_vector, subobj_ofs word20Instance and draw a child subobject
7OP_DEFP_STARTL231n:word, start:word, padding word, then n x vms_vector8 + 12nLike OP_DEFPOINTS but with explicit start index
8OP_GLOWL300glow_index word4Set glow override for next texture-mapped polygon

3.2 Detailed Operand Layouts

OP_FLATPOLY (opcode 2)

Offset  Size   Field
0       word   opcode (2)
2       word   nv (vertex count)
4       vec3   normal_point (12 bytes)
16      vec3   normal_vector (12 bytes)
28      word   color (15-bit RGB, translated during init)
30      nv*w   vertex index list (word per vertex, padded to even count)

Advance formula (L294-296):

    and     ecx,0fffffffeh      ; round nv down to even
    inc     ecx                 ; +1 for padding
    lea     ebp,30[ebp+ecx*2]  ; skip header + padded vertex list

OP_TMAPPOLY (opcode 3)

Offset  Size   Field
0       word   opcode (3)
2       word   nv (vertex count)
4       vec3   normal_point (12 bytes)
16      vec3   normal_vector (12 bytes)
28      word   bitmap_number
30      nv*w   vertex index list (word per vertex, padded)
30+pad  nv*12  UVL list (u:fix, v:fix, l:fix per vertex)

Advance formula (L401-411):

    ; skip past vertex list (padded)
    lea     ebp,30[ebp+ebx*2]
    ; skip past UVL data (nv * 12 bytes)
    mov     eax,ecx
    sal     ecx,1
    add     ecx,eax
    sal     ecx,2               ; ecx = nv * 12
    add     ebp,ecx

OP_SORTNORM (opcode 4)

Offset  Size   Field
0       word   opcode (4)
2       word   (unused)
4       vec3   normal_vector (12 bytes)
16      vec3   normal_point (12 bytes)
28      word   front_offset (relative to this opcode)
30      word   back_offset (relative to this opcode)

This implements a BSP tree node inline in the bytecode. Based on which side of the plane the viewer is on, it draws the two child sub-streams in the correct order (L415-453):

op_sortnorm:
    lea     esi,16[ebp]         ; point on plane
    lea     edi,4[ebp]          ; plane normal
    call    g3_check_normal_facing
    jng     sortnorm_not_facing
    ; facing: draw BACK first, then FRONT
    push    ebp
    mov     ax,30[ebp]          ; back offset
    add     ebp,eax
    call_next                   ; draw back sub-stream
    mov     ebp,[esp]
    mov     ax,28[ebp]          ; front offset
    add     ebp,eax
    call_next                   ; draw front sub-stream
    pop     ebp
    lea     ebp,32[ebp]
    next

OP_SUBCALL (opcode 6)

Offset  Size   Field
0       word   opcode (6)
2       word   subobject_number
4       vec3   position_offset (12 bytes)
16      word   subobject_code_offset (relative to this opcode)
18      word   (padding)

This creates a coordinate frame instance (translation + rotation from animation angles), recursively interprets the subobject’s bytecode, then pops the instance (L480-509):

op_subcall:
    mov     ax,2[ebp]           ; subobject number
    mov     edi,anim_angles     ; get animation angles
    ; ... index into angles array ...
    lea     esi,4[ebp]          ; position offset
    call    g3_start_instance_angles    ; push transform
    push    ebp
    mov     ax,16[ebp]          ; subobject bytecode offset
    add     ebp,eax
    call_next                   ; interpret subobject
    pop     ebp
    call    g3_done_instance    ; pop transform
    lea     ebp,20[ebp]
    next

4. The Initialization Pass

Before a model is ever rendered, g3_init_polygon_model walks the entire bytecode stream linearly. This is done once at load time via load_polygon_model at POLYOBJ.C:717.

The init pass (INTERP.ASM:524-658):

  1. Translates flat polygon colors from 15-bit RGB to the closest palette entry (L553-557)
  2. Tracks the highest texture number referenced by any OP_TMAPPOLY (L574-577)
  3. Validates face vertex counts (must be >= 3) in debug builds (L545-546)
  4. Recursively follows OP_SORTNORM branches and OP_SUBCALL offsets

Note that this pass uses ESI (not EBP) as the walk pointer, and dispatches via a linear chain of cmp/jne comparisons rather than the opcode table — it’s a completely separate traversal from the rendering interpreter.


5. The Morphing System

The morphing effect (used for robot spawn-in animations) is one of the most elegant parts of the architecture. It reuses the exact same bytecode but swaps out 4 of the 9 opcode handlers at runtime.

5.1 Dispatch Table Hot-Patching

At g3_draw_morphing_model (INTERP.ASM:962-992):

g3_draw_morphing_model:
    pushm   eax,ebx,ecx,edx,esi,edi,ebp
    mov     bitmap_ptr,edi
    mov     anim_angles,eax
    mov     morph_points,ebx        ; alternate vertex positions!
    mov     model_light,edx
    mov     ebp,esi

    ;; SAVE original handler addresses onto the stack
    push    opcode_table[1*4]       ; save OP_DEFPOINTS handler
    push    opcode_table[2*4]       ; save OP_FLATPOLY handler
    push    opcode_table[3*4]       ; save OP_TMAPPOLY handler
    push    opcode_table[7*4]       ; save OP_DEFP_START handler

    ;; REPLACE with morphing variants
    mov     opcode_table[1*4],offset morph_defpoints
    mov     opcode_table[2*4],offset morph_flatpoly
    mov     opcode_table[3*4],offset morph_tmappoly
    mov     opcode_table[7*4],offset morph_defp_start

    call_next                       ; run interpreter with swapped handlers

    ;; RESTORE original handlers
    pop     opcode_table[7*4]
    pop     opcode_table[3*4]
    pop     opcode_table[2*4]
    pop     opcode_table[1*4]

    popm    eax,ebx,ecx,edx,esi,edi,ebp
    ret

This is runtime polymorphism at the machine code level — the dispatch table itself is mutated, so the next and call_next macros transparently dispatch to the morphing handlers without any changes to the dispatch logic or the bytecode.

5.2 What the Morphing Handlers Do Differently

The morphing handlers (INTERP.ASM:711-955) differ from the standard handlers in two key ways:

  1. Vertex source: morph_defpoints reads vertex positions from the morph_points array (passed via EBX at entry) instead of from the bytecode stream. It still skips past the bytecode’s vertex data to advance EBP correctly.

  2. Triangle fan decomposition: morph_flatpoly and morph_tmappoly decompose n-gons into triangle fans, calling g3_check_and_draw_poly/g3_check_and_draw_tmap for each triangle. This is because morphing can make coplanar vertices non-coplanar, so triangulation ensures valid rendering. The standard handlers pass full n-gons to g3_draw_poly/g3_draw_tmap.

5.3 The C-Side Morphing Logic

The morphing animation is managed in MAIN/MORPH.C with data structures in MAIN/MORPH.H:

// MORPH.H:67-80
typedef struct morph_data {
    object *obj;                            // object being morphed
    vms_vector morph_vecs[MAX_VECS];        // current vertex positions (interpolating)
    vms_vector morph_deltas[MAX_VECS];      // per-frame movement vectors
    fix morph_times[MAX_VECS];              // remaining time per vertex
    int submodel_active[MAX_SUBMODELS];     // 0=off, 1=morphing, 2=static
    int n_morphing_points[MAX_SUBMODELS];
    int submodel_startpoints[MAX_SUBMODELS];
    int n_submodels_active;
    // ... saved object state ...
} morph_data;

Flow:

  1. morph_start() projects all vertices onto the surface of a bounding box, then computes delta vectors and times so each vertex linearly interpolates from the box surface to its true position.
  2. do_morph_frame() advances each vertex position per frame. When a submodel finishes morphing, its children are activated.
  3. draw_morph_object() calls draw_model() which recursively renders submodels, calling g3_draw_morphing_model with the interpolated morph_vecs as the alternate vertex source.

6. The POF File Format

6.1 File Structure

POF files are loaded in read_model_file() at POLYOBJ.C:257-449.

POF File Layout:
+--------+--------------------------------------------------+
| Offset | Field                                            |
+--------+--------------------------------------------------+
| 0      | Magic: 'OPSP' (4 bytes)                          |
| 4      | Version: short (6-8 supported)                   |
| 6+     | Chunk stream (repeating: id, len, data)          |
+--------+--------------------------------------------------+

Magic check at POLYOBJ.C:275:

if (id!='OPSP')
    Error("Bad ID in model file <%s>",filename);

Version check at POLYOBJ.C:280-281:

#define PM_COMPATIBLE_VERSION 6
#define PM_OBJFILE_VERSION 8

6.2 Chunk Types

Defined at POLYOBJ.C:239-244:

IDConstantPurpose
'RDHO'ID_OHDRObject header: n_models, radius, bounding box
'JBOS'ID_SOBJSubobject: parent, normals, offset, radius, bytecode offset
'SNUG'ID_GUNSGun mount points and directions
'MINA'ID_ANIMAnimation frame angles
'ATDI'ID_IDTAInterpreter Data — the bytecode blob
'RTXT'ID_TXTRTexture filename list

6.3 The Critical Chunk: ID_IDTA

At POLYOBJ.C:418-426:

case ID_IDTA:       // Interpreter data
    pm->model_data = malloc(len);
    pm->model_data_size = len;
    pof_cfread(pm->model_data,1,len,model_buf);
    break;

This is the raw bytecode blob — malloc’d and copied directly from the file into polymodel.model_data. No transformation, no compilation, no code generation. It is subsequently:

  1. Validated by g3_init_polygon_model() (POLYOBJ.C:717)
  2. Passed directly to g3_draw_polygon_model() at render time (POLYOBJ.C:590)

6.4 The polymodel Structure

Defined at POLYOBJ.H:143-161:

typedef struct polymodel {
    int n_models;                                   // submodel count
    int model_data_size;                            // bytecode size in bytes
    ubyte *model_data;                              // pointer to bytecode
    int submodel_ptrs[MAX_SUBMODELS];               // byte offsets into model_data
    vms_vector submodel_offsets[MAX_SUBMODELS];     // position offsets
    vms_vector submodel_norms[MAX_SUBMODELS];       // separation plane normals
    vms_vector submodel_pnts[MAX_SUBMODELS];        // separation plane points
    fix submodel_rads[MAX_SUBMODELS];               // bounding radii
    ubyte submodel_parents[MAX_SUBMODELS];          // parent links
    vms_vector submodel_mins[MAX_SUBMODELS];        // bounding box mins
    vms_vector submodel_maxs[MAX_SUBMODELS];        // bounding box maxs
    vms_vector mins,maxs;                           // overall bounding box
    fix rad;                                        // overall radius
    ubyte n_textures;
    ushort first_texture;
    ubyte simpler_model;                            // LOD chain
} polymodel;

The submodel_ptrs[] array holds byte offsets into model_data — these are the entry points for each subobject’s bytecode sub-stream, used by OP_SUBCALL and by direct rendering of debris pieces (POLYOBJ.C:607).


7. Data Flow: From Disk to Pixels

  POF file on disk
       |
       v
  read_model_file()                    [POLYOBJ.C:257]
  +-- Parse chunk headers
  +-- ID_IDTA chunk -> malloc + memcpy -> polymodel.model_data
  +-- ID_SOBJ chunks -> submodel_ptrs[], offsets, normals, etc.
  +-- ID_OHDR -> n_models, radius, bounds
       |
       v
  g3_init_polygon_model()              [INTERP.ASM:515]
  +-- Walk bytecode linearly (ESI as cursor)
  +-- Translate 15bpp colors -> palette indices
  +-- Track highest_texture_num
  +-- Validate vertex counts
       |
       v
  (model is ready for rendering)
       |
       v
  draw_polygon_model()                 [POLYOBJ.C:532]
  +-- LOD selection (simpler_model chain)
  +-- Build texture_list[] from bitmap indices
  +-- g3_start_instance_matrix()
  +-- g3_set_interp_points(robot_points)
  +-- g3_draw_polygon_model(model_data, texture_list, anim_angles, light, glow)
       |
       v
  Interpreter loop                     [INTERP.ASM:193]
  +-- EBP <- model_data pointer
  +-- call_next -> dispatch first opcode
  |   +-- OP_DEFPOINTS -> rotate vertices into robot_points[]
  |   +-- OP_FLATPOLY -> backface cull, g3_draw_poly()
  |   +-- OP_TMAPPOLY -> backface cull, lighting calc, g3_draw_tmap()
  |   +-- OP_SORTNORM -> BSP ordering, call_next twice
  |   +-- OP_SUBCALL -> instance push, call_next, instance pop
  |   +-- OP_GLOW -> set glow override for next poly
  |   +-- OP_EOF -> ret (end of stream)
  +-- Returns to draw_polygon_model()
       |
       v
  g3_done_instance()

8. Addressing the Tweet’s Claim

8.1 Three Techniques to Distinguish

TechniqueDescriptionUsed by Descent?
(a) Native x86 machine codeModel data literally contains MOV, JMP, CALL instructions executed by the CPUNo
(b) Custom bytecode interpreted by x86 assemblyModel data contains domain-specific opcodes; a hand-written x86 asm interpreter executes themYes
(c) Compiled sprites2D pixel data compiled to sequences of MOV [dest], immediate x86 instructions for fast blittingNo

8.2 What the Tweet Gets Right

  • The model files are not standard 3D file formats (not .OBJ, not .3DS, etc.) — they are a proprietary format (POF) that embeds an executable bytecode program.
  • The model data is indeed “doing draw commands” — the bytecode encodes drawing operations (flat polygon, textured polygon, rod bitmap) as opcodes.
  • This design is strikingly like a compiled program — it has a program counter, opcodes, operands, conditional branches (via OP_SORTNORM), and subroutine calls (via OP_SUBCALL with call_next/ret).

8.3 What the Tweet Gets Wrong

  • The models are not “compiled as x86 assembler.” They contain no x86 machine instructions. The opcodes (0-8) are a custom instruction set, not x86 opcodes.
  • The interpreter is written in x86 assembly, but the model data is platform-independent bytecode. You could write a C interpreter for the same format (and indeed, Descent source ports like DXX-Rebirth do exactly this).
  • The phrase “compiled as x86 assembler” conflates the interpreter’s implementation language (x86 ASM) with the format of the data it interprets (custom bytecode).

8.4 A More Accurate Statement

Descent’s 3D models are stored as a custom bytecode — a stream of opcodes encoding draw commands (define points, draw polygon, sort by normal, call subobject). At runtime, a hand-written x86 assembly interpreter walks the bytecode using EBP as a program counter, dispatching each opcode through a jump table. The models are not x86 machine code; they are programs for a domain-specific virtual machine that happens to be implemented in x86 assembly.


9. Comparison with “Compiled Sprites”

9.1 What Are Compiled Sprites?

“Compiled sprites” (also called “compiled bitmaps”) are a real technique from 1990s DOS game development. The idea: take a 2D sprite’s pixel data and compile it into a sequence of x86 MOV instructions that directly write pixels to the frame buffer. Instead of:

for each pixel (x,y) in sprite:
    if pixel != transparent:
        framebuffer[y * pitch + x] = pixel_color

You generate actual x86 code like:

mov byte ptr es:[di+0],  0x1A    ; pixel at (0,0)
mov byte ptr es:[di+1],  0x2B    ; pixel at (1,0)
; skip (2,0) -- transparent
mov byte ptr es:[di+3],  0x0F    ; pixel at (3,0)
add di, 320                       ; next row
mov byte ptr es:[di+0],  0x1A    ; pixel at (0,1)
; ...

This eliminates the per-pixel transparency check and loop overhead. The sprite is the code. Libraries like Allegro (via get_compiled_sprite()) and games like Jazz Jackrabbit used this technique.

9.2 Does Descent Use Compiled Sprites?

No. A thorough search of the codebase found no evidence of runtime x86 code generation for sprites, bitmaps, or any other purpose. Descent uses:

  • Pre-compiled specialized inner loops in the TEXMAP/ directory — multiple hand-written assembly texture mappers optimized for different scenarios (perspective vs. linear interpolation, 8-bit vs. 16-bit color, lighting modes). These are statically compiled at build time, not generated from sprite data.
  • Runtime selection among these pre-compiled variants based on distance, lighting settings, and color depth.
  • For 2D blitting, standard bitmap copy routines in 2D/.

The key difference: in compiled sprites, the data becomes code. In Descent, the data stays data and the code interprets it.


10. Architectural Diagram

+-----------------------------------------------------------+
|                    POF File (disk)                        |
|  +------+ +------+ +------+ +------+ +-----------------+  |
|  | OHDR | | SOBJ | | GUNS | | ANIM | |      IDTA       |  |
|  |header| | xN   | |mounts| |frames| |   (bytecode)    |  |
|  +------+ +------+ +------+ +------+ +-----------------+  |
+--------------------------------+--------------------------+
                                 | read_model_file()
                                 v
+-----------------------------------------------------------+
|              polymodel (in memory)                        |
|  model_data --> [bytecode blob: opcodes + operands]       |
|  submodel_ptrs[] --> offsets into model_data              |
|  submodel_offsets[], norms[], parents[], etc.             |
+--------------------------------+--------------------------+
                                 | g3_draw_polygon_model()
                                 v
+-----------------------------------------------------------+
|           INTERP.ASM Bytecode Interpreter                 |
|                                                           |
|  EBP (Program Counter) --> model_data                     |
|                                                           |
|  +--------------------------------------+                 |
|  | opcode_table[9]:                     |                 |
|  |  [0] op_eof          -> ret          |                 |
|  |  [1] op_defpoints    -> rotate vecs  |                 |
|  |  [2] op_flatpoly     -> draw_poly    |                 |
|  |  [3] op_tmappoly     -> draw_tmap    |                 |
|  |  [4] op_sortnorm     -> BSP sort     |                 |
|  |  [5] op_rodbm        -> rod bitmap   |                 |
|  |  [6] op_subcall      -> instance     |                 |
|  |  [7] op_defp_start   -> rotate vecs  |                 |
|  |  [8] op_glow         -> set glow     |                 |
|  +--------------------------------------+                 |
|                                                           |
|  Dispatch: mov bx,[ebp]                                   |
|            mov ebx,opcode_table[ebx*4]                    |
|            jmp ebx          (next macro)                  |
|        or: call ebx         (call_next macro)             |
|                                                           |
|  Morphing: hot-patch entries [1],[2],[3],[7] with         |
|            morph_* handlers, then restore after           |
+-----------------------------------------------------------+

11. Summary

“Were Descent’s 3D models compiled as x86 assembler doing draw commands?”

No. The models are a custom bytecode — a flat stream of 16-bit opcodes (9 total) with inline operand data — stored in POF files and interpreted at runtime by a dispatch loop written in x86 assembly. The model data contains zero x86 instructions. The interpreter uses EBP as a program counter, indexes a 9-entry jump table to find the handler for each opcode, and each handler advances EBP past its operand data before dispatching the next opcode. It is a textbook threaded bytecode interpreter, implemented in assembly for performance.

The tweet captures the spirit — the models are indeed structured as executable programs encoding draw commands, making them closer to code than to a traditional mesh format. But the letter is wrong: the models are bytecode for a custom virtual machine, not x86 machine code. The interpreter is x86 assembly; the data it interprets is not.


Appendix: Key File Index

FilePathRole
INTERP.ASM3D/INTERP.ASMBytecode interpreter (998 lines)
POLYOBJ.CMAIN/POLYOBJ.CPOF loading, model rendering entry
POLYOBJ.HMAIN/POLYOBJ.Hpolymodel struct definition
MORPH.CMAIN/MORPH.CMorphing animation logic
MORPH.HMAIN/MORPH.Hmorph_data struct definition
3D.HLIB/3D.H3D API declarations + Watcom pragmas
DRAW.ASM3D/DRAW.ASMLow-level polygon drawing
POINTS.ASM3D/POINTS.ASM3D->2D point projection
CLIPPER.ASM3D/CLIPPER.ASMPolygon clipping
INSTANCE.ASM3D/INSTANCE.ASMTransform stack (push/pop)