Preface: “Descent’s models were x86 assembler”
“Fun fact: Descent didn’t use 3D file formats per se. Instead, 3D models were compiled as x86 assembler doing draw commands.” — @praeclarum, 2025
It’s a great observation, and there’s a real kernel of truth to it — though the full story is even more interesting. The tweet doesn’t specify which Descent it’s referring to, and the answer changes quite a bit depending on which generation you look at.
What Descent 1 & 2 actually did (1994-1996). The original Descent stored 3D models in POF (Parallax Object Format) files as a stream of custom bytecode opcodes — not native x86 machine code, but something close in spirit. Each model was essentially a small program written in a 9-opcode domain-specific language:
| Opcode | Value | Action |
|---|---|---|
OP_EOF | 0 | End of model data |
OP_DEFPOINTS | 1 | Define vertex list (rotate into view space) |
OP_FLATPOLY | 2 | Draw a flat-shaded polygon |
OP_TMAPPOLY | 3 | Draw a texture-mapped polygon |
OP_SORTNORM | 4 | BSP-style front/back sort by normal facing |
OP_RODBM | 5 | Draw a cylindrical rod bitmap |
OP_SUBCALL | 6 | Call into a subobject (with offset + animation angles) |
OP_DEFP_START | 7 | Define points with explicit starting index |
OP_GLOW | 8 | Set glow value for next polygon |
This bytecode was interpreted at runtime by a hand-written x86 assembly dispatcher (3D/INTERP.ASM in the Descent 1 source). The ebp register served as the program counter, a next macro read the 16-bit opcode and jumped through a dispatch table, and each handler advanced ebp past its operand data before calling next again. For morphing models (robot death animations), Descent even hot-patched the dispatch table at runtime to swap in alternative handlers — polymorphism at the assembly level.
So the models really were bytecode programs, not data files — @praeclarum is onto something genuinely cool there. The nuance is that the bytecode wasn’t native x86 machine code, but a custom interpreted format. It’s easy to see how the two get conflated, especially since “compiled sprites” — a genuine 90s technique (used in Allegro, Jazz Jackrabbit, and others) where 2D sprite pixels were literally emitted as x86 MOV instructions — were a real and well-documented thing from the same era.
What Descent 3 did (1999). Descent 3, whose source code this document covers, completely abandoned the bytecode interpreter model. The vestigial ID_IDTA (“Interpreter Data”) chunk ID is still defined at polymodel.cpp:632:
#define ID_IDTA 'ATDI' // Interpreter data
But no code ever reads it. Descent 3 replaced the interpreter with what the codebase calls “new style” models — conventional vertex/face arrays stored in OOF/POF chunks and rendered through a standard algorithmic pipeline:
RenderPolygonModel()
└─ RenderSubmodel() // per-subobject transform + lighting
└─ RenderSubmodelFace() // iterate faces, setup UVs, call GPU
└─ rend_DrawPolygon3D() // submit to OpenGL
The ASSERT(pm->new_style == 1) guard at polymodel.cpp:1276 enforces this — legacy interpreter-format models cannot even be loaded into the Descent 3 renderer.
In summary: the tweet points at something genuinely fascinating about Descent’s heritage. The fine print is that the models were custom bytecode rather than native x86, and that Descent 3 moved on to traditional geometry data structures — so the technique was really a Descent 1 & 2 era thing. But the core insight — that these models were closer to programs than to data files — is spot on, and it’s a wonderful example of 90s engine ingenuity.
1. Executive Summary
1.1 Document Purpose and Scope
This document provides a comprehensive architectural reference for the three foundational runtime subsystems of the Descent3 game engine:
| Subsystem | Type | Era | Status |
|---|---|---|---|
| D3X | Register-based bytecode VM | 1997-98 | Legacy (ISA preserved, interpreter lost in open-source release) |
| OSIRIS | Native DLL/SO module system | Dec 1998+ | Production (active scripting system) |
| Renderer | OpenGL abstraction layer | 1997-present | Active (modernized to GLSL #version 150 core) |
1.2 Historical Context: D3X to OSIRIS Evolution
The D3X bytecode VM was the original scripting system designed by Samir (Parallax Software) in mid-1997. It provided a custom register machine with 52 opcodes, vector-float registers, and a dual-stack execution model. Scripts were compiled to .d3x bytecode files tagged "D3X5".
In December 1998, Jeff replaced D3X with OSIRIS (Object-oriented Scripting for Interactive Routines, Ingame Scenarios). OSIRIS moved from interpreted bytecode to compiled native DLLs/SOs, eliminating interpretation overhead entirely. The event model concepts from D3X — events, object references (ME, IT), and script types (object/trigger/level) — were carried forward into OSIRIS.
The Dallas visual scripting tool generated C++ source code that compiled into OSIRIS DLL modules, using a library of 300+ predefined action (a*) and query (q*) functions.
1.3 System Relationship Overview
Main Frame Tick] --> ED[Event Dispatcher
Osiris_CallEvent] GL --> RP[Rendering Pipeline
RenderMine / RenderFrame] GL --> PT[Osiris_ProcessTimers] ED --> CS[Custom Script DLL] ED --> LS[Level Script DLL] ED --> MS[Mission Script DLL] ED --> DS[Default Script DLL] CS -->|CallInstanceEvent| DF[Dallas Functions
300+ a*/q* actions] LS -->|CallInstanceEvent| DF MS -->|CallInstanceEvent| DF DF -->|aRoomSetFog, aPortalRenderSet| RendAPI[Renderer API
rend_* functions] DF -->|aObjSetShields, aAIGoalGotoRoom| GS[Game State
Objects, AI, Physics] DF -->|MSafe_CallFunction| NET[Multiplayer
Network Layer] GS --> RP RP --> G3[3D Library
g3_DrawPoly, g3_RotatePoint] G3 --> RD[rend_DrawPolygon3D] RD --> GPU[GPU Abstraction
gpu_RenderPolygon] GPU --> OGL[OpenGL 3.2 Core
glDrawArrays] subgraph "Legacy (Unused)" D3X[D3X Bytecode VM
52 opcodes, 32 registers] end style D3X fill:#888,stroke:#555,color:#fff
2. D3X Bytecode Virtual Machine (Legacy)
Status: The D3X ISA is fully preserved in
lib/d3x_op.h. The interpreter implementation was not included in the open-source release. This section documents the architecture as designed.
2.1 Instruction Set Architecture
2.1.1 Register File
D3X uses a register-based architecture with 32 total registers divided into two banks:
| Bank | Registers | Count | Purpose |
|---|---|---|---|
| VF (Vector-Float) | VF0-VF7 (+ 8 extended) | 16 | General-purpose: integers, floats, and 3D vectors |
| AD (Address) | AD0, AD1 | 2 | Indirect addressing; AD0 typically accesses local stack variables |
| (Reserved) | 14 | Padding to MAX_D3X_REGS = 32 |
Register Map (32 slots):
┌──────────────────────────────┬──────┬──────┬────────────────┐
│ VF0-VF7 + VF8-VF15 (16 reg) │ AD0 │ AD1 │ Reserved (14) │
│ VFREG_START = 0 │ 16 │ 17 │ 18 .. 31 │
│ General Purpose │ Addr │ Addr │ (unused) │
└──────────────────────────────┴──────┴──────┴────────────────┘
2.1.2 Opcode Catalog
D3X defines 52 opcodes across 7 categories:
52 total)) Memory
8 opcodes LOAD_ABS LOAD_ADI LOAD_IMM LOAD_PARM STORE_ABS STORE_ADI STORE_PARM BREAK Arithmetic
15 opcodes ADD / SUB / MUL DIV / MOD ADDI AND / OR BAND / BOR BANDI / BORI NEG / NOT / ABS Comparison
6 opcodes EQU / NEQ LT / LTE GT / GTE Vector
7 opcodes VEX / VEY / VEZ XEV / YEV / ZEV SCALEV Exec Buffer
3 opcodes EPUSH / EPOP PCALL Call Stack
6 opcodes CALL / RET CPUSH / CPOP TOCSP / FROMCSP Control Flow
3 opcodes JUMP_ABS JUMP_NCOND DEFER
Complete Opcode Table:
| Value | Mnemonic | Encoding | Description |
|---|---|---|---|
| 0 | BREAK | — | Debug breakpoint |
| 1 | LOAD_ABS | ri | rx ← mem[ABSOLUTE] |
| 2 | LOAD_ADI | aii | rx ← mem[ADx + IMM] |
| 3 | LOAD_IMM | ri | rx ← IMM (immediate int or float) |
| 4 | LOAD_PARM | ri | rx ← parameter[IMM] |
| 5 | STORE_ABS | ri | mem[ABSOLUTE] ← rx |
| 6 | STORE_ADI | aii | mem[ADx + IMM] ← rx |
| 7 | STORE_PARM | ri | parameter[IMM] ← rx |
| 10 | ADD | rr | rd ← rd + rs |
| 11 | SUB | rr | rd ← rd - rs |
| 12 | MUL | rr | rd ← rd * rs |
| 13 | DIV | rr | rd ← rd / rs |
| 14 | MOD | rr | rd ← rd % rs |
| 15 | AND | rr | rd ← rd && rs (logical) |
| 16 | OR | rr | rd ← rd || rs (logical) |
| 17 | NEG | rr | rd ← -rd |
| 18 | NOT | rr | rd ← !rd |
| 19 | ABS | rr | rd ← abs(rd) |
| 20 | EQU | rr | rd ← (rd == rs) |
| 21 | NEQ | rr | rd ← (rd != rs) |
| 22 | LT | rr | rd ← (rd < rs) |
| 23 | LTE | rr | rd ← (rd <= rs) |
| 24 | GT | rr | rd ← (rd > rs) |
| 25 | GTE | rr | rd ← (rd >= rs) |
| 26 | BOR | rr | rd ← rd | rs (bitwise) |
| 27 | BAND | rr | rd ← rd & rs (bitwise) |
| 28 | VEX | rr | rd ← vs.x (extract X from vector) |
| 29 | VEY | rr | rd ← vs.y (extract Y from vector) |
| 30 | VEZ | rr | rd ← vs.z (extract Z from vector) |
| 31 | XEV | rr | vd.x ← rs (set X of vector) |
| 32 | YEV | rr | vd.y ← rs (set Y of vector) |
| 33 | ZEV | rr | vd.z ← rs (set Z of vector) |
| 34 | ADDI | ri | rd ← rd + IMM |
| 35 | BANDI | ri | rd ← rd & IMM |
| 36 | BORI | ri | rd ← rd | IMM |
| 40 | EPUSH | rr | Push register to execution buffer |
| 41 | EPOP | rr | Pop from execution buffer to register |
| 42 | PCALL | ra | Call predefined external function |
| 43 | CALL | ra | Call script subroutine at ABSOLUTE address |
| 44 | CPUSH | rr | Push register to call stack |
| 45 | CPOP | rr | Pop from call stack to register |
| 46 | TOCSP | rr | Transfer ADx to call stack pointer |
| 47 | FROMCSP | rr | Transfer call stack pointer to ADx |
| 48 | RET | — | Return from CALL (pops address from call stack) |
| 49 | DEFER | ri | Defer execution: 0=end, 1=default handler |
| 50 | JUMP_ABS | ra | Unconditional jump to ABSOLUTE address |
| 51 | JUMP_NCOND | ra | Jump to ABSOLUTE if register == 0 |
2.1.3 Instruction Encoding
Each instruction is encoded as a tD3XInstruction struct with a 1-byte opcode followed by a union of 4 operand formats:
(1 byte)"] --> FMT{Format?} FMT -->|ri| RI_D["d
reg 1B"] RI_D --> RI_IMM["imm
int|float 4B"] RI_IMM -.- RI_SZ["6 bytes total"] FMT -->|aii| AII_D["d
dest 1B"] AII_D --> AII_A["a
addr 1B"] AII_A --> AII_IMM["imm
uint16 2B"] AII_IMM -.- AII_SZ["5 bytes total"] FMT -->|ra| RA_ABS["abs
uint16 2B"] RA_ABS --> RA_R["r
reg 1B"] RA_R -.- RA_SZ["4 bytes total"] FMT -->|rr| RR_D["d
dest 1B"] RR_D --> RR_S["s
src 1B"] RR_S -.- RR_SZ["3 bytes total"] style RI_SZ fill:none,stroke:none style AII_SZ fill:none,stroke:none style RA_SZ fill:none,stroke:none style RR_SZ fill:none,stroke:none
| Format | Fields | Size | Used by |
|---|---|---|---|
| ri | d (reg), imm (int|float) | 6 bytes | LOAD_IMM, LOAD_ABS, STORE_ABS, LOAD_PARM, STORE_PARM, ADDI, BANDI, BORI, DEFER |
| aii | d (dest reg), a (addr reg), imm (uint16 offset) | 5 bytes | LOAD_ADI, STORE_ADI |
| ra | abs (uint16 address), r (reg, 0xFF=unused) | 4 bytes | JUMP_ABS, JUMP_NCOND, CALL, PCALL |
| rr | d (dest), s (src) | 3 bytes | All arithmetic, comparison, vector, stack ops |
2.2 Program Structure
2.2.1 File Format
D3X script files are identified by the tag "D3X5". Each file contains:
- A header with the D3X tag
- A program map (
tD3XPMapentries) indexing named scripts - The bytecode instruction stream
2.2.2 Program Map Entry (tD3XPMap)
struct tD3XPMap {
char name[32]; // Script name (MAX_D3XID_NAME)
uint16_t ip; // Entry point: instruction pointer
uint16_t mem; // Memory required by this script
uint16_t type; // Script binding type
uint16_t parms; // Number of parameters
};
| Value | Constant | Description |
|---|---|---|
| 0 | REF_OBJTYPE | Bound to a specific game object (robot, door, powerup, etc.); receives events when that object is interacted with |
| 1 | REF_TRIGTYPE | Bound to a trigger volume; fires when a player or object enters the designated area in the level |
| 2 | REF_LEVELTYPE | Level-wide script that handles global events (level start/end, goals, cinematics) not tied to any single object |
| 3 | REF_GAMEMODE | Game mode script that controls multiplayer rule logic (scoring, team assignment, win conditions) for a specific mode |
| Value | Constant | Description |
|---|---|---|
| 0 | PARMTYPE_NUMBER | Integer or float |
| 1 | PARMTYPE_VECTOR | 3D vector (x, y, z) |
| 2 | PARMTYPE_REF | Object reference handle |
| 3 | PARMTYPE_STRREF | String reference |
2.3 Execution Model
2.3.1 Dual-Stack Architecture
D3X uses two independent stacks:
| Stack | Push/Pop | Purpose |
|---|---|---|
| Execution Buffer | EPUSH/EPOP | Passing arguments to external function calls (PCALL) |
| Call Stack | CPUSH/CPOP | Return addresses, local variables, subroutine frames |
The TOCSP and FROMCSP instructions transfer the address register (ADx) to/from the call stack pointer, enabling frame pointer manipulation for local variable access.
2.3.2 Event Arguments
When a script is invoked, the call stack is pre-populated with standard arguments at fixed offsets:
| Offset | Constant | Description |
|---|---|---|
| 0 | SCRARG_EVENT | Event type being handled (e.g., collision, damage, timer — identifies why this script was invoked) |
| 1 | SCRARG_ME | Handle of “ME” — the object this script is attached to (the robot, door, or powerup that owns this script) |
| 2 | SCRARG_ITTYPE | Type code of “IT” — the other object involved in the interaction (player, weapon, another robot, etc.) |
| 3 | SCRARG_IT | Handle of “IT” — the interacting object (e.g., the player who collided with ME, or the weapon that damaged ME) |
| 4-11 | EVTARG_* | 8 event-specific argument slots (carry extra data like damage amount, room number, or timer ID depending on event type) |
| 12+ | SCRSTACK_START | Start of script-defined local variables (script’s own working memory, allocated per the mem field in the program map) |
2.3.3 Control Flow
- CALL/RET: Standard subroutine mechanism. CALL pushes the return address onto the call stack; RET pops it.
- PCALL: Calls a predefined engine function by index. Arguments are passed via the execution buffer (EPUSH before PCALL, results EPOP after).
- DEFER: Terminates script execution.
DEFER_END(0) stops completely;DEFER_DEFAULT(1) defers to the default event handler. - JUMP_NCOND: Conditional branch — jumps to the absolute address if the specified register contains zero.
3. OSIRIS Module System (Modern Scripting)
3.1 Architecture Overview
OSIRIS replaced D3X with compiled native code modules. Scripts are written in C++, compiled to platform-specific shared libraries (.dll/.so/.dylib), and loaded at runtime.
Key source files:
Descent3/osiris_dll.h— Module loader APIscripts/osiris_common.h— Event definitions, shared structuresscripts/osiris_import.h— Engine function pointer importsmodule/module.h— Cross-platform DLL/SO abstraction
3.1.1 Module Loading
The module struct in module/module.h provides cross-platform dynamic loading:
mod_LoadModule(module*, filename, flags) → Load DLL/SO
mod_GetSymbol(module*, name, parmbytes) → Resolve function pointer
mod_FreeModule(module*) → Unload
Loading Flags:
| Flag | Value | Description |
|---|---|---|
MODF_LAZY | — | Resolve function pointers on demand — only look up each symbol when it is first called, not at load time |
MODF_NOW | — | Resolve all function pointers immediately at load time; fails fast if any expected symbol is missing |
MODF_GLOBAL | — | Make this module’s symbols visible to other modules, allowing cross-module function calls |
3.1.2 Module Slot Management
OSIRIS manages up to 64 simultaneously loaded modules (MAX_LOADED_MODULES). Each slot tracks:
- Reference count (multiple objects may share a module)
- Module flags:
OSIMF_INUSE,OSIMF_LEVEL,OSIMF_DLLELSEWHERE,OSIMF_INTEMPDIR,OSIMF_NOUNLOAD - Three dedicated slots: Level module, Game module, Mission module
3.1.3 Module Types
| Type | Loader | Lifecycle |
|---|---|---|
| Level | Osiris_LoadLevelModule() | Loaded when a level begins, unloaded when it ends; contains scripts for that level’s rooms, triggers, puzzles, and cinematics |
| Game | Osiris_LoadGameModule() | Persists across the entire game session; provides shared logic like the default object behaviors and the GuideBot AI companion |
| Mission | Osiris_LoadMissionModule() | Custom per-mission scripts for add-on content; allows third-party missions to define unique gameplay mechanics and event responses |
3.2 Module Interface Contract
Every OSIRIS module must export 9 functions:
| Export | Signature | Purpose |
|---|---|---|
InitializeDLL | (tOSIRISModuleInit*) → char | Called once at load: receives the engine’s 256-slot function pointer table so the script can call back into the engine |
ShutdownDLL | () → void | Called when the module is unloaded; frees any resources the script allocated during its lifetime |
GetGOScriptID | (name, is_door) → int | Given a game object’s name (e.g., “TubeWindA”), returns the script ID that should handle its events, or -1 if unrecognized |
CreateInstance | (id) → void* | Allocates per-object memory for a script instance (e.g., tracking an individual robot’s custom state like patrol waypoints) |
DestroyInstance | (id, ptr) → void | Frees the per-object instance memory when the object is removed from the game world |
CallInstanceEvent | (id, ptr, event, data) → int16_t | Main event handler: receives gameplay events (collision, damage, timer, etc.) and returns flags controlling chain behavior |
GetTriggerScriptID | (name) → int | Given a trigger volume’s name, returns the script ID that should handle enter/exit events for that trigger |
GetCOScriptList | (scripts, params) → int | Enumerates all custom object scripts this module provides, so the engine knows which objects this module can handle |
SaveRestoreState | (file, saving) → int | Serializes or deserializes the module’s global state to/from a save-game file, preserving progress across save/load cycles |
3.2.1 Initialization: tOSIRISModuleInit
struct tOSIRISModuleInit {
int32_t *fp[256]; // MAX_MODULEFUNCS function pointers
char **string_table; // Localized strings
int32_t string_count;
int32_t module_identifier;
bool module_is_static; // Don't unload when refcount=0
char *script_identifier; // Unique ID for OMMS
uint32_t game_checksum; // Struct compatibility check
};
The game_checksum is computed by multiplying struct sizes (sizeof(object), sizeof(player), etc.) with prime numbers. Modules compare this against their compile-time value to detect ABI mismatches.
3.3 Event System
3.3.1 Event Taxonomy
OSIRIS defines 35+ event types in osiris_common.h:
| Category | Events | When These Fire |
|---|---|---|
| Lifecycle | EVT_CREATED (0x104), EVT_DESTROY (0x105) | When an object is first spawned into the world, or when it is about to be removed (killed, exploded, or cleaned up) |
| Per-Frame | EVT_INTERVAL (0x100), EVT_AI_FRAME (0x101) | Every game frame — used for continuous behaviors like monitoring conditions, updating custom animations, or polling object state |
| Interaction | EVT_DAMAGED (0x102), EVT_COLLIDE (0x103), EVT_USE (0x107) | When an object takes damage from a weapon, physically collides with another object, or is activated/used by the player (e.g., a switch) |
| AI Notifications | EVT_AI_NOTIFY (0x110), EVT_AI_INIT (0x111), plus 11 child events: EVT_AIN_OBJKILLED, EVT_AIN_SEEPLAYER, EVT_AIN_GOALCOMPLETE, EVT_AIN_MELEE_HIT, EVT_AIN_MOVIE_START, etc. | When AI-controlled robots detect players, complete navigation goals, land melee attacks, have their targets killed, or enter cinematics |
| Timer | EVT_TIMER (0x106), EVT_TIMERCANCEL (0x11A) | When a script-created countdown expires (for delayed actions like timed doors or staged explosions), or when a timer is cancelled early |
| Level | EVT_CHANGESEG (0x115), EVT_LEVELSTART, EVT_LEVELEND | When an object moves from one room to another, or at level load/unload — used for room-based triggers and level initialization/teardown |
| Persistence | EVT_SAVESTATE (0x117), EVT_RESTORESTATE (0x118), EVT_MEMRESTORE (0x119) | During save/load game — scripts serialize their custom state (puzzle progress, counters, flags) so gameplay resumes correctly after load |
| Doors | EVT_DOOR_ACTIVATE (0x125), EVT_DOOR_CLOSE (0x126) | When a door begins opening (activated by player, key, or script) or finishes closing — used for locked-door puzzles and ambush triggers |
| Goals | EVT_LEVEL_GOAL_COMPLETE (0x128), EVT_ALL_LEVEL_GOALS_COMPLETE (0x129), EVT_LEVEL_GOAL_ITEM_COMPLETE (0x12A) | When the player completes mission objectives — used to trigger cinematics, unlock new areas, or advance the mission narrative |
| Player | EVT_PLAYER_MOVIE_START/END (0x12B/C), EVT_PLAYER_RESPAWN (0x12D), EVT_PLAYER_DIES (0x12E) | When a cinematic sequence begins/ends, when a player respawns after death (multiplayer), or when a player is killed |
| Matcen | EVT_MATCEN_CREATE (0x124) | When a matcen (material center, i.e., enemy spawning facility) produces a new robot or object into the world |
| Misc | EVT_CHILD_DIED (0x127) | When an object that was spawned by or attached to this object is destroyed — lets parents react to children being killed |
Each event carries its data through the tOSIRISEventInfo union, which contains a type-specific struct plus the common me_handle and extra_info fields.
3.3.2 Event Chain Dispatch
Osiris_CallEvent() participant Custom as Custom Script participant Level as Level Script participant Mission as Mission Script participant Default as Default Script Engine->>Custom: CallInstanceEvent(event, data) Custom-->>Engine: return flags alt CONTINUE_CHAIN set Engine->>Level: CallInstanceEvent(event, data) Level-->>Engine: return flags alt CONTINUE_CHAIN set Engine->>Mission: CallInstanceEvent(event, data) Mission-->>Engine: return flags alt CONTINUE_CHAIN set Engine->>Default: CallInstanceEvent(event, data) Default-->>Engine: return flags end end end Note over Engine: Return CONTINUE_DEFAULT
to run built-in behavior
Return flags:
| Flag | Value | Meaning |
|---|---|---|
CONTINUE_CHAIN | 0x0100 | Pass the event to the next script in the chain (custom → level → mission → default); if omitted, no further scripts see this event |
CONTINUE_DEFAULT | 0x0001 | Allow the engine’s built-in behavior to run (e.g., apply damage, play death animation); if omitted, the script fully overrides the default action |
3.3.3 Event Masking
Events can be selectively enabled/disabled by category:
#define OEM_OBJECTS 0x01 // Object events
#define OEM_TRIGGERS 0x02 // Trigger events
#define OEM_LEVELS 0x04 // Level events
Osiris_EnableEvents(OEM_OBJECTS | OEM_TRIGGERS);
Osiris_DisableEvents(OEM_LEVELS);
Create events can be suppressed during game loading/demo playback via Osiris_DisableCreateEvents().
3.4 Dallas Action/Query System
The Dallas visual scripting tool generates C++ code that calls predefined engine functions. These are declared in scripts/DallasFuncs.h and organized by category:
Action Functions (a* prefix)
| Category | Description | Key Functions |
|---|---|---|
| Portal/Room | Control room-level environment effects: forcefields at doorways, fog/wind atmosphere, hazard damage, wall textures, lighting | aPortalRenderSet, aPortalBreakGlass, aRoomSetFog, aRoomChangeFog, aRoomSetWind, aRoomChangeWind, aRoomSetDamage, aRoomSetFaceTexture, aRoomSetLightingStrobe/Flicker/Pulse, aRoomSetFuelcen, aRoomFogSetState |
| Objects | Manipulate individual game objects: set health/energy, apply damage, kill or hide objects, play animations, toggle visibility (ghosting makes objects intangible), apply visual effects like sparking or mesh deformation | aObjSetShields, aObjSetEnergy, aObjApplyDamage, aObjKill, aObjDestroy, aObjPlayAnim, aObjGhostSet, aObjHide, aObjMakeInvuln, aObjDeform, aObjSpark, aObjSetVelocity, aObjGravityEnable, aObjSetLightingDist/Color, aObjSetMovementType, aObjFireWeapon, aObjSaveHandle |
| AI | Control robot behavior: set awareness state, field of vision, targets, team allegiance, navigation goals (go to room, follow path, pick up object), and movement speed | aAISetState, aAISetFOV, aAISetTarget, aAISetTeam, aAISetMode, aAISetMaxSpeed, aAIFlags, aAIGoalGotoRoom, aAIGoalGotoObject, aAIGoalFollowPath/Simple, aAIGoalPickUpObject, aAIClearGoal, aAIGoalSetCircleDistance |
| Sound | Play audio effects: 2D UI sounds, positional 3D sounds attached to objects, streaming music/voiceover, volume control | aSoundPlay2D, aSoundPlay2DObj, aSoundPlayObject, aSoundPlaySteaming/Obj, aSoundVolumeObj, aSoundStopObj |
| Weather | Toggle environmental weather effects visible to the player: rain, snow, and lightning storms with configurable bolt effects | aRainTurnOn/Off, aSnowTurnOn/Off, aLightningTurnOn/Off, aLightningCreate, aLightningCreateGunpoints |
| Cinematics | Trigger in-game cutscenes: camera paths, text overlays, letterbox framing, and multi-track sequences with timed camera cuts | aCinematicSimple, aCinematicIntro, aCinematicStop, aComplexCinematicStart/End/Text/Track/CameraOnPath/CameraAtPoint/ScreenMode |
| Doors | Control door state: lock/unlock (requiring keys), force open/close, set position along its travel, or stop mid-movement | aDoorLockUnlock, aDoorActivate, aDoorSetPos, aDoorStop |
| Inventory | Add or remove items from a player’s inventory (powerups, keys, quest items) | aAddObjectToInventory/Named, aRemoveObjectFromInventory |
| Timers | Schedule delayed actions: create countdowns on objects or the level itself, cancel pending timers, display timer on HUD | aSetObjectTimer, aSetLevelTimer, aCancelTimer, aTimerShow |
| Level Flow | Control mission progression: end the level (success or failure), trigger the exit flythrough sequence, fade to white on exit | aEndLevel, aFailLevel, aStartEndlevelSequence/Path, aFadeWhiteAndEndlevel |
| Player | Modify player capabilities: disable/enable movement controls, grant keys, toggle invisibility (cloaking), strip all weapons and energy back to the basic laser | aTogglePlayerObjControl/AllControls, aObjectPlayerGiveKey, aCloakObject, aUnCloakObject, aCloakAllPlayers, aStripWeaponsEnergy |
| HUD/Messages | Display text to the player: on-screen messages, colored alerts, persistent game messages, and waypoint markers (HUD arrows guiding the player to objectives) | aShowHUDMessage/Obj, aShowColoredHUDMessage/Obj, aAddGameMessage, aSetWaypoint |
| Goals | Manage mission objectives: mark goals as completed/failed, enable or disable goals dynamically, set display priority and completion text | aGoalCompleted, aGoalFailed, aGoalEnableDisable, aGoalSetPriority, aGoalSetCompletionMessage |
| Misc | Miscellaneous: set script-local flags/variables for tracking puzzle state, change background music region, enable custom death sequences, physically attach objects together, emit continuous particle streams (spew), apply screen shake, control matcen (enemy spawner) production rates, toggle available ships | aUserFlagSet, aUserVarSet/Inc/Dec/Add/Sub, aMusicSetRegion/All, aSetScriptedDeath, aAttachObject/Existing, aUnAttachObject, aTurnOnSpew/Off, aMiscViewerShake, aMiscShakeArea, aMatcenSetState/EnableState/Values, aTriggerSetState, aEnableShip/DisableShip |
Query Functions (q* prefix)
| Category | Description | Key Functions |
|---|---|---|
| Object State | Check properties of any game object: whether it exists/is alive, its health/energy, current room, distance to other objects, line-of-sight visibility, and what type of object it is (player, robot, powerup, etc.) | qObjExists, qObjIsPlayer, qObjIsPlayerWeapon, qObjIsPlayerOrPlayerWeapon, qObjIsType, qObjShields, qObjEnergy, qObjShieldsOriginal, qObjRoom, qObjOnTerrain, qObjType, qObjParent, qObjAnimFrame, qObjGetDistance, qObjGetLightingDist, qObjDamage, qObjCanSeeObj/Advanced, qObjCanSeePlayer/Advanced |
| User Variables | Read back script-local state: retrieve the current value of numbered variables or boolean flags used to track puzzle/level progress | qUserVarValue, qUserVarValueInt, qUserFlag |
| Door/Portal | Check door and portal status: whether a door is locked, can be opened, its current position along its travel, or whether a portal’s forcefield is active | qDoorLocked, qDoorOpenable, qDoorGetPos, qPortalIsOn |
| Room | Query room environment state: whether fog is active, whether any player is currently inside, and the room’s hazard damage rate | qRoomFogOn, qRoomHasPlayer, qRoomGetDamage |
| AI | Query robot AI state: proximity to current target, whether the robot is aware of threats, what it’s currently targeting, and its speed limit | qAICloseToTarget, qAIIsObjectAware, qAIGetTarget, qAIQueryMaxSpeed |
| Inventory | Check whether a specific item is in a player’s inventory (used for key-gate puzzles and item-dependent progression) | qHasObjectInInventory |
| Goals | Check mission objective status: whether all primary goals are done, whether a specific goal is enabled, completed, or failed | qGoalPrimariesComplete, qGoalEnabled, qGoalCompleted, qGoalFailed |
| Math | Arithmetic helpers for Dallas visual scripts (which lack native math operators): add, subtract, multiply, convert types, compute percentages | qMathAddFloat, qMathSubFloat, qMathMulFloat, qMathIntToFloat, qMathAddPercent, qMathSubPercent, qMathAddInt, qMathSubInt, qMathPercentage |
| Misc | General-purpose queries: random number generation, find nearest player, get frame delta time, check difficulty setting, count objects of a type, check ship availability, read trigger state, check if player is cloaked (invisible) | qRandomChance, qRandomValue, qPlayerClosest, qFrametime, qGetDifficulty, qObjCountTypeID, qIsShipEnabled, qTriggerGetState, qVirusInfected, qNegativeLight, qObjectCloakTime, qObjectPosition |
User State: Scripts maintain up to 25 user variables (MAX_USER_VARS) via the user_var variant type (float or int32), plus boolean flags and 50 spew handles (MAX_SPEW_HANDLES), and 20 saved object handles.
3.5 Memory Management
OSIRIS provides three tiers of memory management:
3.5.1 Auto-Save Memory
Scripts allocate memory via Osiris_AllocateMemory(tOSIRISMEMCHUNK*). The engine automatically saves this memory to disk on game save and fires EVT_MEMRESTORE with the restored pointer on load.
struct tOSIRISMEMCHUNK {
tOSIRISSCRIPTID my_id; // Script identity (type + handle)
uint16_t id; // Chunk identifier
int32_t size; // Allocation size
};
Osiris_AllocateMemory] --> B[Engine tracks
chunk by script ID] B --> C[Returns void*
to script] end subgraph "Game Save" D[Osiris_SaveMemoryChunks] --> E[Write all chunks
to CFILE] end subgraph "Game Load" F[Osiris_RestoreMemoryChunks] --> G[Reallocate memory
from saved data] G --> H[Fire EVT_MEMRESTORE
with new pointer] H --> I[Script updates
its pointer] end C --> D E -.->|Save file| F
3.5.2 OMMS (Osiris Mission Memory System)
For cross-script shared state, OMMS provides reference-counted global memory:
Osiris_InitOMMS() -- Initialize at mission start
OMMS_Malloc(size, unique_id) -- Allocate shared block
OMMS_Attach(handle) / OMMS_Detach() -- Increment/decrement refcount
OMMS_Find(unique_id) -- Look up by unique ID
OMMS_Free(handle) -- Mark for deletion
Memory is only freed when both OMMS_Free has been called AND the reference count reaches zero.
3.5.3 Script State Persistence
Scripts handle EVT_SAVESTATE and EVT_RESTORESTATE to manually serialize custom data through CFILE read/write functions (CFReadInt, CFWriteString, etc.).
3.6 Timer System
struct tOSIRISTIMER { // https://github.com/DescentDevelopers/Descent3/blob/main/scripts/osiris_common.h#L1174
uint16_t flags; // OTF_REPEATER, OTF_TRIGGER, OTF_LEVEL, OTF_CANCELONDEAD
int32_t id; // User-defined ID (passed back in EVT_TIMER)
int32_t repeat_count; // -1 for infinite repeat
union {
int32_t object_handle; // Recipient of EVT_TIMER
int32_t trigger_number; // If OTF_TRIGGER set
};
int32_t object_handle_detonator; // If OTF_CANCELONDEAD: auto-cancel if this object dies
float timer_interval; // Seconds between signals
};
Osiris_ProcessTimers() is called every frame, scanning active timers and firing EVT_TIMER when intervals elapse. EVT_TIMERCANCEL is sent when a timer is cancelled (either explicitly or by detonator object death).
3.7 Multiplayer Safety Layer
Network-synchronized operations go through the MSafe system, which ensures all clients execute the same game state changes.
Architecture: MSafe_CallFunction(type, msafe_struct*) dispatches one of 100+ operation types defined by MSAFE_* constants. The msafe_struct is a large universal parameter block containing fields for rooms, objects, weather, sound, doors, triggers, inventory, and more.
Categories (from osiris_common.h):
| Base | Category | Operations |
|---|---|---|
| 0 | Room | Change wall textures, set wind direction/speed, configure fog color/density, set pulsing/strobing/flickering lights, apply environmental hazard damage, toggle portal forcefields, enable refueling (shield/energy restore), break glass |
| 20 | Object | Set health (shields) and energy, change light emission color, scale movement/recharge/weapon speeds, apply damage, grant weapons, emit particle streams (spew), make intangible (ghost/unghost), reposition, set velocity, toggle invisibility (cloak), fire weapons, add spark effects, ignite on fire |
| 100 | Weather | Toggle rain, snow, and lightning storm effects; create individual lightning bolts between points |
| 110 | Sound | Play non-positional (2D) audio, play positional audio attached to objects, play streaming audio (voiceover/music), stop sounds, adjust volume |
| 130 | Matcen | (reserved for matcen/enemy spawner operations — not yet implemented) |
| 150 | Misc | Display HUD messages, set waypoint navigation markers, trigger level end, show popup camera views, post game messages, change background music region, enable/disable selectable ships, update mission goals, rename the GuideBot companion, manage timers, control HUD item visibility |
| 170 | Door | Lock or unlock doors, force doors open, set door position along travel, stop door movement, query whether a door can be opened |
| 190 | Trigger | Enable or disable trigger volumes (the invisible regions that fire events when entered) |
| 200 | Inventory | Add items to player inventory by type or ID, check for specific items, count held items, remove items, query inventory capacity, add by direct object handle |
| 210 | Countermeasure | Manage player countermeasures (deployable decoys that distract homing missiles): add, count, check availability, remove, query capacity |
| 250 | Weapon | Check whether a player has a specific weapon, grant new weapons to a player |
3.8 Netgame DLL System
Multiplayer game modes (Anarchy, CTF, Coop, Entropy, Hoard, MonsterBall, RoboAnarchy, Team Anarchy) are implemented as separate DLLs using the same OSIRIS module interface. The engine passes a game_api structure containing:
- Data pointers to engine arrays (Objects, Rooms, Players)
- ~450 function pointers for engine callbacks
- ~50 variable pointers
- The standard
tOSIRISModuleInitfor OSIRIS integration
4. Renderer Abstraction Layer
4.1 Architecture Overview
The renderer provides a hardware-agnostic API through ~50 rend_* functions declared in lib/renderer.h. The current implementation targets OpenGL 3.2 core profile via renderer/HardwareOpenGL.cpp.
Backend enumeration (renderer_type):
enum renderer_type {
RENDERER_OPENGL = 2, // Active
RENDERER_DIRECT3D = 3, // Legacy (in legacy/renderer/)
RENDERER_GLIDE = 4, // Unused
RENDERER_NONE = 5, // Stub/dedicated server
};
Key source files:
| File | Purpose |
|---|---|
lib/renderer.h | Public API: all rend_* function declarations |
renderer/HardwareInternal.h | Internal types: PosColorUVVertex, PosColorUV2Vertex, color_array, tex_array |
renderer/HardwareOpenGL.cpp | OpenGL backend: init, texture cache, GPU render functions |
renderer/HardwareBaseGPU.cpp | Shared GPU logic: state machine, DeterminePointColor, rend_DrawPolygon2D/3D |
renderer/HardwareDraw.cpp | 3D library: g3_DrawPoly, g3_DrawLine, g3_DrawBitmap |
renderer/ShaderProgram.h | Modern GLSL infrastructure: ShaderProgram<V>, OrphaningVertexBuffer |
renderer/shaders/vertex.glsl | Vertex shader: MVP transform |
renderer/shaders/fragment.glsl | Fragment shader: dual-texture, fog, gamma |
4.2 State Machine
The renderer maintains global state via the rendering_state struct:
struct rendering_state {
int8_t initted;
int8_t cur_bilinear_state; // Texture filtering on/off
int8_t cur_zbuffer_state; // Z-buffer on/off
texture_type cur_texture_type; // TT_FLAT through TT_PERSPECTIVE_SPECIAL
color_model cur_color_model; // CM_MONO or CM_RGB
light_state cur_light_state; // LS_NONE/GOURAUD/PHONG/FLAT_GOURAUD
int8_t cur_alpha_type; // AT_ALWAYS through AT_LIGHTMAP_BLEND_SATURATE
wrap_type cur_wrap_type; // WT_WRAP, WT_CLAMP, WT_WRAP_V
int cur_alpha; // Constant alpha value (0-255)
ddgr_color cur_color; // Flat color for LS_FLAT_GOURAUD
int8_t cur_texture_quality; // 0=none, 1=linear, 2=perspective
int clip_x1, clip_x2, clip_y1, clip_y2; // Viewport clip region
int screen_width, screen_height;
};
Alpha blending modes (15 total, defined in lib/renderer.h):
| Value | Constant | Formula | Visual Effect |
|---|---|---|---|
| 0 | AT_ALWAYS | Alpha = 1.0 (fully opaque) | No transparency; surface is completely solid and opaque |
| 1 | AT_CONSTANT | Alpha = cur_alpha / 255 | Uniform transparency across the entire surface, like tinted glass; used for fading UI and movie overlays |
| 2 | AT_TEXTURE | Alpha from texture alpha channel | Per-pixel transparency from the texture image; creates detailed silhouettes with smooth edges |
| 3 | AT_CONSTANT_TEXTURE | texture_alpha * constant | Texture transparency modulated by a global fade; used for fading decals, HUD gauges, scorch marks |
| 4 | AT_VERTEX | Alpha from per-vertex p3_a | Smooth transparency gradients across a polygon; used for smoke trails fading from opaque to transparent |
| 5 | AT_CONSTANT_VERTEX | vertex_alpha * constant | Vertex gradients with overall fade control; used for HUD elements with per-corner fading |
| 6 | AT_TEXTURE_VERTEX | texture_alpha * vertex_alpha | Textured transparency with per-vertex gradients; for complex particle shapes that fade at edges |
| 7 | AT_CONSTANT_TEXTURE_VERTEX | texture * constant * vertex | Maximum flexibility: texture detail + vertex gradients + overall fade; for cinematic particle effects |
| 8 | AT_LIGHTMAP_BLEND | dest * src (multiplicative) | Darkens surfaces by multiplying colors; applies baked lighting and shadow maps to walls and terrain |
| 9 | AT_SATURATE_TEXTURE | Saturate to white | Additive glow that brightens toward white; used for explosions, weapon fire, energy auras, light halos |
| 12 | AT_SATURATE_VERTEX | Saturate with vertex alpha | Additive glow with smooth vertex gradients; for expanding blast rings that fade at their edges |
| 13 | AT_SATURATE_CONSTANT_VERTEX | Constant * vertex saturation | Additive glow with vertex gradients and overall brightness control; for colored light rings |
| 14 | AT_SATURATE_TEXTURE_VERTEX | Texture * vertex saturation | Additive glow preserving texture detail with vertex fading; for textured blast waves and energy effects |
| 32 | AT_SPECULAR | Specular blending | Shiny reflective highlights on glossy materials (polished metal, plastic); bright spots where light hits |
| 33 | AT_LIGHTMAP_BLEND_SATURATE | Additive lightmap blend | Brightens surfaces additively; used for specular highlight maps and intense light reflections on walls |
4.3 Rendering Pipeline
nv vertices, bitmap handle] A --> B2[g3_DrawBitmap
billboard sprite] A --> B3[g3_DrawLine
wireframe] end subgraph "3D Library (HardwareDraw.cpp)" B --> C{Texture Quality?} C -->|quality == 0| D[gpu_DrawFlatPolygon3D
Solid color only] C -->|quality > 0| E{Overlay Type?} E -->|OT_NONE| F[rend_DrawPolygon3D
Single texture] E -->|OT_BLEND| G[rend_DrawMultitexturePolygon3D
Base + Lightmap] end subgraph "GPU Abstraction (HardwareBaseGPU.cpp)" F --> H[Build PosColorUVVertex array] G --> I[Build PosColorUV2Vertex array] H --> J[For each vertex:
DeterminePointColor] I --> K[For each vertex:
DeterminePointColor + lightmap UVs] J --> L[gpu_BindTexture slot 0] K --> M[gpu_BindTexture slot 0 + slot 1] L --> N[gpu_RenderPolygon] M --> O[gpu_SetMultitextureBlendMode true] O --> P[gpu_RenderPolygonUV2] end subgraph "OpenGL Backend (HardwareOpenGL.cpp)" N --> Q[shader.addVertexData
OrphaningVertexBuffer] P --> Q Q --> R[glDrawArrays
GL_TRIANGLE_FAN] end
Frame lifecycle:
rend_StartFrame(x1, y1, x2, y2, clear_flags)— Set viewport, optionally clear Z-buffer- Game issues draw calls (
g3_DrawPoly,rend_DrawScaledBitmap, etc.) rend_EndFrame()— Finalize framerend_Flip()— Swap front/back buffers (SDL3SDL_GL_SwapWindow)
Color determination (DeterminePointColor):
alpha = gpu_Alpha_multiplier * gpu_Alpha_factor // see HardwareBaseGPU.cpp:63
if (ATF_VERTEX flag set): alpha *= pnt->p3_a
if (LS_FLAT_GOURAUD): color = cur_color (flat)
else if (LS_NONE): color = white (1,1,1)
else if (CM_MONO): color = (p3_l, p3_l, p3_l) // monochromatic intensity
else: color = (p3_r, p3_g, p3_b) // per-vertex RGB
4.4 Shader System
4.4.1 GLSL Shaders (OpenGL 3.2 Core, #version 150)
Vertex Shader (renderer/shaders/vertex.glsl):
in vec3 in_pos;
in vec4 in_color;
in vec2 in_uv0;
in vec2 in_uv1;
uniform mat4 u_modelview;
uniform mat4 u_projection;
void main() {
vertex_modelview_pos = u_modelview * vec4(in_pos, 1);
gl_Position = u_projection * vertex_modelview_pos;
// Pass through: vertex_color, vertex_uv0, vertex_uv1
}
Fragment Shader (renderer/shaders/fragment.glsl):
uniform sampler2D u_texture0, u_texture1;
uniform int u_texture_enable; // Bitfield: bit 0 = tex0, bit 1 = tex1
uniform bool u_fog_enable;
uniform vec4 u_fog_color;
uniform float u_fog_start, u_fog_end;
uniform float u_gamma;
void main() {
// Dual-texture multiply (branchless disable via max with inverted enable)
out_color = vertex_color
* max(texture(u_texture0, vertex_uv0), vec4(float(!bool(u_texture_enable & 1))))
* max(texture(u_texture1, vertex_uv1), vec4(float(!bool(u_texture_enable & 2))));
// Distance-based fog
float fog_factor = clamp((fog_end - length(modelview_pos)) / (fog_end - fog_start), 0, 1);
out_color = out_color * fog_factor + (1 - fog_factor) * u_fog_color;
// Gamma correction
out_color.rgb = pow(out_color.rgb, vec3(1.0 / u_gamma));
}
Key design: Texture enable/disable is branchless — disabled textures sample vec4(1) via max() with an inverted enable flag, avoiding GPU branching overhead.
4.4.2 ShaderProgram<V> Template
template <typename V>
struct ShaderProgram {
ShaderProgram(vertex_src, fragment_src, attribs);
void Use() / Unuse(); // Bind/unbind program
size_t addVertexData(begin, end); // Stream vertices to VBO
void setUniformMat4f(name, matrix); // Set 4x4 matrix uniform
void setUniform1i/1f/4fv(name, value); // Set scalar/vector uniforms
private:
OrphaningVertexBuffer<V> vbo_; // Streaming vertex buffer
unordered_map<string, GLint> uniform_cache_; // Cached uniform locations
};
4.5 Vertex Buffer Management
4.5.1 Vertex Formats (defined in HardwareInternal.h)
struct PosColorUVVertex { // Single-texture polygon
vector pos; // 3D position (12 bytes)
color_array color; // RGBA float (16 bytes)
tex_array uv; // UV + r,w (16 bytes)
}; // Total: 44 bytes/vertex
struct PosColorUV2Vertex { // Multi-texture polygon (base + lightmap)
vector pos; // 3D position
color_array color; // RGBA float
tex_array uv0; // Primary texture coords
tex_array uv1; // Lightmap texture coords
}; // Total: 60 bytes/vertex
4.5.2 OrphaningVertexBuffer
The OrphaningVertexBuffer<V> implements the buffer orphaning streaming pattern:
Ring Buffer (kVertexCount = 64K vertices = 1 << 16): // see ShaderProgram.h:140
┌──────────────────────────────────────────────────────┐
│ ████████████ used │ ← next write ─── free space ───→ │
└──────────────────────────────────────────────────────┘
↑ nextVertex_
When nextVertex_ + newCount >= 64K:
→ glBufferData(GL_ARRAY_BUFFER, size, nullptr, GL_STREAM_DRAW) // Orphan
→ nextVertex_ = 0 // Wrap to start
GL_STREAM_DRAWusage hint for frequently-updated dataglMapBufferRangewithGL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BITfor zero-copy streaming- Buffer orphaning avoids GPU stalls by discarding the old buffer when it fills up
4.6 Texture Cache System
handle, map_type, slot] --> B{map_type?} B -->|MAP_TYPE_BITMAP| C[Lookup OpenGL_bitmap_remap] B -->|MAP_TYPE_LIGHTMAP| D[Lookup OpenGL_lightmap_remap] C --> E{Mapped?} D --> E E -->|No, value = 65535| F[Allocate GL texture
opengl_MakeTextureObject] E -->|Yes| G{Changed?} F --> H[Upload texture data] G -->|BF_CHANGED or BF_BRAND_NEW| H G -->|Clean| I[Skip upload] H --> J{Pixel Format?} J -->|16-bit 1555| K[opengl_Translate_table
Convert to RGBA] J -->|16-bit 4444| L[opengl_4444_translate_table
Convert to RGBA] J -->|Packed| M[opengl_packed_Translate_table
GL_UNSIGNED_SHORT_5_5_5_1] K --> N[glTexImage2D / glTexSubImage2D] L --> N M --> N I --> O[Bind to texture unit] N --> O O --> P{Already bound?} P -->|OpenGL_last_bound == handle| Q[Skip redundant bind] P -->|Different| R[glBindTexture
GL_TEXTURE_2D]
Tracking arrays (defined in HardwareOpenGL.cpp):
OpenGL_bitmap_remap[MAX_BITMAPS]— Maps game bitmap handles to GL texture IDsOpenGL_lightmap_remap[MAX_LIGHTMAPS]— Maps lightmap handles to GL texture IDsOpenGL_bitmap_states[MAX_BITMAPS]— Tracks dirty state per bitmapOpenGL_lightmap_states[MAX_LIGHTMAPS]— Tracks dirty state per lightmapOpenGL_last_bound[2]— Last bound texture per slot (avoids redundantglBindTexturecalls)
Lightmap UV scaling: Lightmap texture coordinates are scaled by width / square_res and height / square_res to account for power-of-two padding.
4.7 Platform Integration
- SDL3: Window creation, GL context management, input handling, buffer swap
- OpenGL 3.2 Core Profile:
SDL_GL_CONTEXT_MAJOR_VERSION = 3,SDL_GL_CONTEXT_MINOR_VERSION = 2 - FBO Resolution Scaling: Renders to an offscreen FBO at the configured resolution, then blits to the window at display resolution
- Dynamic GL Loading:
dyna_gl.hwraps all GL function pointers (loaded via SDL3’sSDL_GL_GetProcAddress), enabling runtime GL library selection via-gllibrarycommand-line flag
5. Cross-Cutting: Memory Architecture
| Subsystem | Memory Model | Allocation | Persistence |
|---|---|---|---|
| D3X | Stack-based | tD3XPMap.mem per-script | N/A (legacy) |
| OSIRIS Auto-Save | Engine-managed heap | Osiris_AllocateMemory | Auto-saved/restored with game saves |
| OSIRIS OMMS | Ref-counted shared heap | OMMS_Malloc | Per-mission lifetime |
| OSIRIS Script Instance | Script-allocated | CreateInstance returns void* | Script manages via SaveRestoreState |
| Renderer Texture Cache | GPU-side | GL texture objects via remap tables | Lifetime of texture/lightmap data |
| Renderer VBO | GPU-side ring buffer | OrphaningVertexBuffer 64K vertices | Per-frame streaming (ephemeral) |
| Module Loading | OS DLL/SO allocator | mod_LoadModule | Until mod_FreeModule |
6. Cross-Cutting: Performance Characteristics
6.1 Renderer Performance Tracking
struct tRendererStats {
int poly_count; // Polygons drawn this frame
int vert_count; // Vertices processed this frame
int texture_uploads; // Textures uploaded to GPU this frame
};
Internal counters (not exposed via API):
OpenGL_sets_this_frame[10]— State change counts by category
6.2 Optimization Strategies
| Strategy | Implementation | Benefit |
|---|---|---|
| Redundant state avoidance | Check cur_* before setting GL state | Skips expensive GPU driver calls when the state is already set correctly (e.g., don’t re-enable what’s enabled) |
| Texture dirty tracking | BF_CHANGED/BF_BRAND_NEW flags | Only re-uploads texture image data to the GPU when it has actually been modified, saving bus bandwidth |
| Last-bound cache | OpenGL_last_bound[2] per slot | Tracks which texture is already active on each texture unit; skips the bind call if it’s already the right one |
| Buffer orphaning | glBufferData(nullptr) on overflow | When the vertex ring buffer fills up, discards it and starts fresh instead of waiting for the GPU to finish |
| Unsynchronized mapping | GL_MAP_UNSYNCHRONIZED_BIT | Lets the CPU write new vertices into the buffer without waiting for the GPU to finish reading old ones |
| Branchless shader | max(sample, inverted_enable) | Disables a texture by forcing its sample to white (multiply identity) instead of using an if/else branch on GPU |
| Alpha multiplier cache | gpu_Alpha_multiplier precomputed | Pre-calculates the combined alpha factor once when the blend mode changes, instead of recomputing it per vertex |
6.3 OSIRIS vs D3X Performance
OSIRIS scripts execute as native code — no interpretation overhead. The per-event cost is:
- One function pointer indirection per script in the chain (
CallInstanceEvent) - Up to 4 chain links per event (custom → level → mission → default)
Osiris_ProcessTimers()scans all active timers linearly each frame
D3X (when it was active) had per-instruction interpretation overhead from the decode-dispatch loop, making OSIRIS significantly faster for complex scripts.
7. Cross-Cutting: Debugging & Diagnostics
7.1 OSIRIS Debug Capabilities
| Feature | Control | Purpose |
|---|---|---|
| Debug messages | Show_osiris_debug flag | Toggle verbose event logging |
| Object tracking | OSIRISDEBUG compile flag | tRefObj linked list per module tracks all bound objects |
| Object dump | Osiris_DumpLoadedObjects() | Write all bound objects to file |
| Event masking | Osiris_EnableEvents/DisableEvents | Selectively suppress event categories |
| Create suppression | Osiris_DisableCreateEvents() | Suppress object creation events during game load/demo playback |
7.2 Renderer Diagnostics
| Feature | Location | Purpose |
|---|---|---|
| Frame statistics | rend_GetStatistics() | Poly count, vert count, texture uploads |
| State change counts | OpenGL_sets_this_frame[10] | Track redundant state changes |
| GL info logging | opengl_GetInformation() | Log vendor, renderer, version strings |
| Error reporting | rend_GetErrorMessage()/SetErrorMessage() | Last renderer error string |
7.3 Checksum Validation
Osiris_CreateGameChecksum() computes a checksum from critical struct sizes multiplied by prime numbers. This detects ABI mismatches between the engine and script DLLs at load time, preventing crashes from struct layout changes.
8. Appendices
8.1 Complete D3X Opcode Reference (lib/d3x_op.h)
| Value | Mnemonic | Format | Operands |
|---|---|---|---|
| 0 | BREAK | — | (none) |
| 1 | LOAD_ABS | ri | d, imm (absolute address) |
| 2 | LOAD_ADI | aii | d, a (addr reg), imm (offset) |
| 3 | LOAD_IMM | ri | d, imm (int or float literal) |
| 4 | LOAD_PARM | ri | d, imm (parameter index) |
| 5 | STORE_ABS | ri | d (source reg), imm (absolute address) |
| 6 | STORE_ADI | aii | d (source), a (addr reg), imm (offset) |
| 7 | STORE_PARM | ri | d (source), imm (parameter index) |
| 10 | ADD | rr | d (dest += src), s |
| 11 | SUB | rr | d, s |
| 12 | MUL | rr | d, s |
| 13 | DIV | rr | d, s |
| 14 | MOD | rr | d, s |
| 15 | AND | rr | d, s (logical) |
| 16 | OR | rr | d, s (logical) |
| 17 | NEG | rr | d (negate) |
| 18 | NOT | rr | d (logical not) |
| 19 | ABS | rr | d (absolute value) |
| 20 | EQU | rr | d, s |
| 21 | NEQ | rr | d, s |
| 22 | LT | rr | d, s |
| 23 | LTE | rr | d, s |
| 24 | GT | rr | d, s |
| 25 | GTE | rr | d, s |
| 26 | BOR | rr | d, s (bitwise or) |
| 27 | BAND | rr | d, s (bitwise and) |
| 28 | VEX | rr | d (float), s (vector) — extract X |
| 29 | VEY | rr | d, s — extract Y |
| 30 | VEZ | rr | d, s — extract Z |
| 31 | XEV | rr | d (vector), s (float) — set X |
| 32 | YEV | rr | d, s — set Y |
| 33 | ZEV | rr | d, s — set Z |
| 34 | ADDI | ri | d, imm (add immediate) |
| 35 | BANDI | ri | d, imm (bitwise and immediate) |
| 36 | BORI | ri | d, imm (bitwise or immediate) |
| 40 | EPUSH | rr | d (push to exec buffer) |
| 41 | EPOP | rr | d (pop from exec buffer) |
| 42 | PCALL | ra | abs (function index) |
| 43 | CALL | ra | abs (script address) |
| 44 | CPUSH | rr | d (push to call stack) |
| 45 | CPOP | rr | d (pop from call stack) |
| 46 | TOCSP | rr | d (ADx → call stack pointer) |
| 47 | FROMCSP | rr | d (call stack pointer → ADx) |
| 48 | RET | — | Return from CALL |
| 49 | DEFER | ri | imm: 0=end, 1=default handler |
| 50 | JUMP_ABS | ra | abs (target address) |
| 51 | JUMP_NCOND | ra | abs (target), r (condition reg, jump if 0) |
8.2 OSIRIS Event Type Reference (scripts/osiris_common.h)
| Value | Event | Data Struct | Trigger Description |
|---|---|---|---|
| 0x100 | EVT_INTERVAL | tOSIRISEVTINTERVAL (frame_time, game_time) | Every game frame; for continuous monitoring, custom animations, or periodic state checks |
| 0x101 | EVT_AI_FRAME | tOSIRISIEVTAIFRAME | Every frame for AI objects; used for custom robot behavior that runs alongside the AI |
| 0x102 | EVT_DAMAGED | tOSIRISEVTDAMAGED (damage, it_handle, weapon_handle, damage_type) | When this object takes damage; carries the amount, source weapon, and damage type |
| 0x103 | EVT_COLLIDE | tOSIRISEVTCOLLIDE (it_handle) | When this object physically touches another object (player hits wall, robot bumps player) |
| 0x104 | EVT_CREATED | tOSIRISEVTCREATED | When this object is first spawned into the game world (level load or runtime creation) |
| 0x105 | EVT_DESTROY | tOSIRISEVTDESTROY (is_dying) | When this object is about to be removed; is_dying distinguishes death from cleanup |
| 0x106 | EVT_TIMER | tOSIRISEVTTIMER (id, game_time) | When a script-created countdown expires; id identifies which timer fired |
| 0x107 | EVT_USE | tOSIRISEVTUSE (it_handle) | When a player activates/uses this object (pressing a switch, picking up an item) |
| 0x110 | EVT_AI_NOTIFY | tOSIRISEVTAINOTIFY (notify_type, it_handle, goal_num, goal_uid) | General AI notification; notify_type specifies which sub-event (see 0x11B-0x123 below) |
| 0x111 | EVT_AI_INIT | tOSIRISEVTAIINIT | When AI is first initialized on this object; for setting up initial goals and behaviors |
| 0x115 | EVT_CHANGESEG | tOSIRISEVTCHANGESEG (room_num) | When this object moves from one room to another; carries the new room number |
| 0x117 | EVT_SAVESTATE | tOSIRISEVTSAVESTATE (fileptr) | During game save; script writes its custom state (puzzle progress, flags) to the file |
| 0x118 | EVT_RESTORESTATE | tOSIRISEVTRESTORESTATE (fileptr) | During game load; script reads back its saved state to resume where the player left off |
| 0x119 | EVT_MEMRESTORE | tOSIRISEVTMEMRESTORE (id, memory_ptr) | After game load; engine provides the restored auto-save memory pointer for the script |
| 0x11A | EVT_TIMERCANCEL | tOSIRISEVTTIMERCANCEL (handle, detonated) | When a timer is cancelled; detonated indicates if it was auto-cancelled by object death |
| 0x11B | EVT_AIN_OBJKILLED | tOSIRISEVTAINOTIFY | AI notification: the robot’s current target or an observed object was killed |
| 0x11C | EVT_AIN_SEEPLAYER | tOSIRISEVTAINOTIFY | AI notification: the robot has spotted a player within its field of vision |
| 0x11D | EVT_AIN_WHITOBJECT | tOSIRISEVTAINOTIFY | AI notification: the robot has collided with another object while navigating |
| 0x11E | EVT_AIN_GOALCOMPLETE | tOSIRISEVTAINOTIFY | AI notification: the robot has reached its navigation destination or completed its AI goal |
| 0x11F | EVT_AIN_GOALFAIL | tOSIRISEVTAINOTIFY | AI notification: the robot’s current navigation goal could not be completed (path blocked) |
| 0x120 | EVT_AIN_MELEE_HIT | tOSIRISEVTAINOTIFY | AI notification: the robot’s melee (close-range) attack successfully connected with a target |
| 0x121 | EVT_AIN_MELEE_ATTACK_FRAME | tOSIRISEVTAINOTIFY | AI notification: the robot’s animation has reached the melee strike frame (for timing FX) |
| 0x122 | EVT_AIN_MOVIE_START | tOSIRISEVTAINOTIFY | AI notification: an in-game cinematic has begun; robot may need to pause or play a role |
| 0x123 | EVT_AIN_MOVIE_END | tOSIRISEVTAINOTIFY | AI notification: the cinematic has ended; robot can resume normal behavior |
| 0x124 | EVT_MATCEN_CREATE | tOSIRISEVTMATCENCREATE (it_handle, id) | A matcen (enemy spawning facility) has produced a new object; carries the spawned object |
| 0x125 | EVT_DOOR_ACTIVATE | tOSIRISEVTDOORACTIVATE | A door has been activated (started opening), by player, key, or script command |
| 0x126 | EVT_DOOR_CLOSE | tOSIRISEVTDOORCLOSE | A door has finished closing; used to trigger ambushes or lock-behind-you sequences |
| 0x127 | EVT_CHILD_DIED | tOSIRISEVTCHILDDIED (it_handle) | A child object (spawned by or attached to this object) was destroyed |
| 0x128 | EVT_LEVEL_GOAL_COMPLETE | tOSIRISEVTLEVELGOALCOMPLETE (level_goal_index) | A specific mission objective was completed; index identifies which goal |
| 0x129 | EVT_ALL_LEVEL_GOALS_COMPLETE | tOSIRISEVTALLLEVELGOALSCOMPLETE | All mission objectives for this level have been completed; typically triggers the exit |
| 0x12A | EVT_LEVEL_GOAL_ITEM_COMPLETE | tOSIRISEVTLEVELGOALITEMCOMPLETE (level_goal_index) | A sub-item within a multi-part goal was completed (e.g., one of several reactors destroyed) |
| 0x12B | EVT_PLAYER_MOVIE_START | tOSIRISEVTPLAYERMOVIESTART | A cinematic sequence has begun playing for this player; controls may be disabled |
| 0x12C | EVT_PLAYER_MOVIE_END | tOSIRISEVTPLAYERMOVIEEND | The cinematic sequence has ended; player controls are restored |
| 0x12D | EVT_PLAYER_RESPAWN | tOSIRISEVTPLAYERRESPAWN (it_handle) | A player has respawned after dying (multiplayer); used to reset per-player script state |
| 0x12E | EVT_PLAYER_DIES | tOSIRISEVTPLAYERDIES (it_handle) | A player has been killed; used for score tracking, triggering failure conditions, or death FX |
8.3 Renderer Alpha Mode Reference (lib/renderer.h)
| Value | Constant | Alpha Multiplier | Blend Factors | OpenGL Blend Function | Typical Usage |
|---|---|---|---|---|---|
| 0 | AT_ALWAYS | 1.0 | No blending | Blending disabled | Solid opaque geometry (walls, floors) |
| 1 | AT_CONSTANT | cur_alpha / 255 | constant | SRC_ALPHA, ONE_MINUS_SRC_ALPHA | Uniform fade: movie overlays, UI transitions |
| 2 | AT_TEXTURE | 1.0 | texture alpha | ONE, ZERO | Per-pixel cutouts from texture: windows, grates |
| 3 | AT_CONSTANT_TEXTURE | cur_alpha / 255 | texture * constant | SRC_ALPHA, ONE_MINUS_SRC_ALPHA | Fading decals, HUD gauges, scorch marks on walls |
| 4 | AT_VERTEX | 1.0 | per-vertex alpha | SRC_ALPHA, ONE_MINUS_SRC_ALPHA | Smoke trails, soft-edged particles |
| 5 | AT_CONSTANT_VERTEX | cur_alpha / 255 | vertex * constant | SRC_ALPHA, ONE_MINUS_SRC_ALPHA | HUD elements with gradient fading |
| 6 | AT_TEXTURE_VERTEX | 1.0 | texture * vertex | SRC_ALPHA, ONE_MINUS_SRC_ALPHA | Complex particles with texture and edge fading |
| 7 | AT_CONSTANT_TEXTURE_VERTEX | cur_alpha / 255 | texture * constant * vertex | SRC_ALPHA, ONE_MINUS_SRC_ALPHA | Maximum-control transparency: cinematic particles |
| 8 | AT_LIGHTMAP_BLEND | cur_alpha / 255 | dest * src (multiplicative) | DST_COLOR, ZERO | Baked shadow/lighting maps darkening surfaces |
| 9 | AT_SATURATE_TEXTURE | cur_alpha / 255 | saturate to white (additive) | SRC_ALPHA, ONE | Explosions, weapon fire glow, energy auras |
| 12 | AT_SATURATE_VERTEX | 1.0 | vertex saturation (additive) | SRC_ALPHA, ONE | Expanding blast rings, terrain glow effects |
| 13 | AT_SATURATE_CONSTANT_VERTEX | cur_alpha / 255 | constant * vertex saturation | SRC_ALPHA, ONE | Colored light rings with brightness control |
| 14 | AT_SATURATE_TEXTURE_VERTEX | 1.0 | texture * vertex saturation | SRC_ALPHA, ONE | Textured blast waves, energy discharge effects |
| 32 | AT_SPECULAR | 1.0 | specular | (special handling) | Shiny highlights on glossy metal/plastic materials |
| 33 | AT_LIGHTMAP_BLEND_SATURATE | cur_alpha / 255 | additive lightmap | SRC_ALPHA, ONE | Specular highlight maps, bright reflections on walls |