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:
| Function | Declaration | Purpose |
|---|---|---|
g3_set_interp_points | 3D.H:324 | Gives the interpreter a vertex output array |
g3_draw_polygon_model | 3D.H:328 | Main interpreter: execute model bytecode |
g3_init_polygon_model | 3D.H:331 | One-time init pass: validate and translate colors |
g3_draw_morphing_model | 3D.H:334 | Morphing 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) pointersEAX= pointer to animation angles (vms_angvecarray)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
| Value | Name | Handler | Operand Layout | Size (bytes) | Description |
|---|---|---|---|---|---|
| 0 | OP_EOF | L215 | (none) | 2 | End of stream; executes ret |
| 1 | OP_DEFPOINTS | L218 | n:word, then n x vms_vector (12 bytes each) | 4 + 12n | Define & rotate vertices into point array |
| 2 | OP_FLATPOLY | L245 | See below | Variable | Draw flat-shaded polygon |
| 3 | OP_TMAPPOLY | L310 | See below | Variable | Draw texture-mapped polygon |
| 4 | OP_SORTNORM | L415 | normal vms_vector, point vms_vector, front_ofs word, back_ofs word | 32 | BSP-sort node: draw children in back-to-front order |
| 5 | OP_RODBM | L456 | bitmap# word, top_point vms_vector, top_width fix, bot_point vms_vector, bot_width fix | 36 | Draw a rod/cylinder billboard |
| 6 | OP_SUBCALL | L480 | subobj# word, offset vms_vector, subobj_ofs word | 20 | Instance and draw a child subobject |
| 7 | OP_DEFP_START | L231 | n:word, start:word, padding word, then n x vms_vector | 8 + 12n | Like OP_DEFPOINTS but with explicit start index |
| 8 | OP_GLOW | L300 | glow_index word | 4 | Set 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):
- Translates flat polygon colors from 15-bit RGB to the closest palette entry (L553-557)
- Tracks the highest texture number referenced by any
OP_TMAPPOLY(L574-577) - Validates face vertex counts (must be >= 3) in debug builds (L545-546)
- Recursively follows
OP_SORTNORMbranches andOP_SUBCALLoffsets
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:
-
Vertex source:
morph_defpointsreads vertex positions from themorph_pointsarray (passed viaEBXat entry) instead of from the bytecode stream. It still skips past the bytecode’s vertex data to advanceEBPcorrectly. -
Triangle fan decomposition:
morph_flatpolyandmorph_tmappolydecompose n-gons into triangle fans, callingg3_check_and_draw_poly/g3_check_and_draw_tmapfor each triangle. This is because morphing can make coplanar vertices non-coplanar, so triangulation ensures valid rendering. The standard handlers pass full n-gons tog3_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:
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.do_morph_frame()advances each vertex position per frame. When a submodel finishes morphing, its children are activated.draw_morph_object()callsdraw_model()which recursively renders submodels, callingg3_draw_morphing_modelwith the interpolatedmorph_vecsas 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:
| ID | Constant | Purpose |
|---|---|---|
'RDHO' | ID_OHDR | Object header: n_models, radius, bounding box |
'JBOS' | ID_SOBJ | Subobject: parent, normals, offset, radius, bytecode offset |
'SNUG' | ID_GUNS | Gun mount points and directions |
'MINA' | ID_ANIM | Animation frame angles |
'ATDI' | ID_IDTA | Interpreter Data — the bytecode blob |
'RTXT' | ID_TXTR | Texture filename list |
6.3 The Critical Chunk: ID_IDTA
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:
- Validated by
g3_init_polygon_model()(POLYOBJ.C:717) - 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
| Technique | Description | Used by Descent? |
|---|---|---|
| (a) Native x86 machine code | Model data literally contains MOV, JMP, CALL instructions executed by the CPU | No |
| (b) Custom bytecode interpreted by x86 assembly | Model data contains domain-specific opcodes; a hand-written x86 asm interpreter executes them | Yes |
| (c) Compiled sprites | 2D pixel data compiled to sequences of MOV [dest], immediate x86 instructions for fast blitting | No |
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 (viaOP_SUBCALLwithcall_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
| File | Path | Role |
|---|---|---|
| INTERP.ASM | 3D/INTERP.ASM | Bytecode interpreter (998 lines) |
| POLYOBJ.C | MAIN/POLYOBJ.C | POF loading, model rendering entry |
| POLYOBJ.H | MAIN/POLYOBJ.H | polymodel struct definition |
| MORPH.C | MAIN/MORPH.C | Morphing animation logic |
| MORPH.H | MAIN/MORPH.H | morph_data struct definition |
| 3D.H | LIB/3D.H | 3D API declarations + Watcom pragmas |
| DRAW.ASM | 3D/DRAW.ASM | Low-level polygon drawing |
| POINTS.ASM | 3D/POINTS.ASM | 3D->2D point projection |
| CLIPPER.ASM | 3D/CLIPPER.ASM | Polygon clipping |
| INSTANCE.ASM | 3D/INSTANCE.ASM | Transform stack (push/pop) |