1. Architectural Role & Engine Purpose
The QVM (Innerloop Virtual Machine) file format executes all mission logic, AI behavior state machines, guard alert responses, alarm networks, security cameras, weapon firing logic, and in-engine cutscenes in Project I.G.I.: I'm Going In (2000).
The Innerloop VM was derived from / inspired by the Quake virtual machine, but it is not directly related
to it: QVM uses its own LOOP container, its own 49-opcode instruction set, and its own
registrar-based native bridging. Despite the shared inspiration, nothing about the bytecode is Quake-compatible.
Innerloop Studios designed a stack-based virtual machine executing bytecode version VM v8.5 (encoded as major version 8, minor version 5). The binary payload is self-contained with its own symbol and string pools, allowing O(1) table lookups and deterministic execution on 32-bit x86 hardware.
2. Binary File Layout & Memory Map Diagram
The QVM file uses a contiguous 32-bit layout where the 60-byte header defines explicit byte offsets and lengths for all five payload sections:
3. Engine Pipeline & Architecture Data Flow Diagram
At runtime, the QVM subsystem operates as an integrated virtual machine embedded directly into the native
game loop of IGI.EXE. The script engine dispatches bytecode on every simulation tick:
4. Architectural Mechanics & Engine Implementation Details
The QVM virtual machine uses a stack-oriented evaluation model. Instructions push values onto a 32-bit operand stack and consume them during arithmetic, relational, and control flow operations:
- Stack Width: The operand stack is composed of 32-bit slots. Both 32-bit integers and 32-bit IEEE 754 floating-point values share the same stack slots without runtime tagging.
- O(1) Symbol Resolution: Identifiers and string literals are referenced via 0-based indices into the
ITableandSTablearrays. During script execution, string lookups require zero hashing or runtime scanning. - Native Function Bridging: When the bytecode executes a native call (
CALLopcode0x18), it indexes into an internal native registration table inIGI.EXE. Arguments are popped directly from the VM operand stack and passed into native x86 stdcall/cdecl C functions. - Subroutine Control Flow: Script logic flows through
BRA(0x14), conditionalBF(0x15) /BT(0x16) branches,JSR(0x17) andRET(0x13).CALL(0x18) stores a 32-bit count followed by that many 32-bit targets, which is how both script subroutines and native engine methods are reached.
5. Binary Header Specification (60 Bytes)
| Offset | Size | Type | Field | Description |
|---|---|---|---|---|
0x00 | 4 | char[4] | signature | Magic FourCC: "LOOP" (0x4C4F4F50 in little-endian) |
0x04 | 4 | uint32_t | ver_major | Major version: strictly 8 |
0x08 | 4 | uint32_t | ver_minor | Minor version: strictly 5 (VM v8.5) |
0x0C | 4 | uint32_t | of_itable | Byte offset to Identifier Table (ITable) |
0x10 | 4 | uint32_t | of_ivalue | Byte offset to Identifier String Pool |
0x14 | 4 | uint32_t | sz_itable | Size of Identifier Table in bytes |
0x18 | 4 | uint32_t | sz_ivalue | Size of Identifier String Pool in bytes |
0x1C | 4 | uint32_t | of_stable | Byte offset to String Table (STable) |
0x20 | 4 | uint32_t | of_svalue | Byte offset to String Value Pool |
0x24 | 4 | uint32_t | sz_stable | Size of String Table in bytes |
0x28 | 4 | uint32_t | sz_svalue | Size of String Value Pool in bytes |
0x2C | 4 | uint32_t | of_ctable | Byte offset to Bytecode Section (CTable) |
0x30 | 4 | uint32_t | sz_ctable | Size of Bytecode Section in bytes |
0x34 | 4 | uint32_t | unknown_1 | Purpose unknown; not used by the parser (read and stored) |
0x38 | 4 | uint32_t | unknown_2 | Purpose unknown; not used by the parser (read and stored) |
6. C++ Struct Definitions
// QVM Header Definition (60 Bytes)
#pragma pack(push, 1)
struct QvmHeader {
char signature[4]; // "LOOP"
uint32_t ver_major; // 8
uint32_t ver_minor; // 5
uint32_t of_itable; // Offset to Identifier Table
uint32_t of_ivalue; // Offset to Identifier Strings
uint32_t sz_itable; // Size of Identifier Table
uint32_t sz_ivalue; // Size of Identifier String Pool
uint32_t of_stable; // Offset to String Table
uint32_t of_svalue; // Offset to String Values
uint32_t sz_stable; // Size of String Table
uint32_t sz_svalue; // Size of String Value Pool
uint32_t of_ctable; // Offset to Bytecode Section
uint32_t sz_ctable; // Size of Bytecode Section
uint32_t unknown_1; // Unknown, unused by parser
uint32_t unknown_2; // Unknown, unused by parser
};
static_assert(sizeof(QvmHeader) == 60, "QvmHeader must be 60 bytes");
#pragma pack(pop)
7. VM Instruction Set (49 Opcodes)
The VM executes 49 distinct opcodes encoded as single bytes (0x00 – 0x30):
| Opcode | Mnemonic | Operand | Description |
|---|---|---|---|
0x00 | BRK | None | Break / debug trap |
0x01 | NOP | None | No operation |
0x02 | PUSH | uint32 (4B) | Push 32-bit integer immediate onto operand stack |
0x03 | PUSHB | uint8 (1B) | Push 8-bit integer immediate |
0x04 | PUSHW | uint16 (2B) | Push 16-bit word immediate |
0x05 | PUSHF | float32 (4B) | Push 32-bit float immediate |
0x06 | PUSHA | uint32 (4B) | Push address (4-byte code offset) |
0x07 | PUSHS | uint32 (4B) | Push string (4-byte string-pool index) |
0x08 | PUSHSI | uint32 (4B) | Push string-index immediate |
0x09 | PUSHSIB | uint8 (1B) | Push string-index immediate, 8-bit |
0x0A | PUSHSIW | uint16 (2B) | Push string-index immediate, 16-bit |
0x0B | PUSHI | uint32 (4B) | Push integer immediate (4-byte value) |
0x0C | PUSHII | uint32 (4B) | Push indirect integer |
0x0D | PUSHIIB | uint8 (1B) | Push indirect integer, 8-bit |
0x0E | PUSHIIW | uint16 (2B) | Push indirect integer, 16-bit |
0x0F | PUSH0 | None | Push constant zero |
0x10 | PUSH1 | None | Push constant one |
0x11 | PUSHM | None | Push marked/multiple stack values |
0x12 | POP | None | Pop and discard top of operand stack |
0x13 | RET | None | Return from subroutine, restore caller IP |
0x14 | BRA | int32 (4B) | Unconditional branch to target code offset |
0x15 | BF | int32 (4B) | Branch if top of stack is false |
0x16 | BT | int32 (4B) | Branch if top of stack is true |
0x17 | JSR | uint32 (4B) | Jump to subroutine (4-byte code offset) |
0x18 | CALL | uint32 count + count<02D7> int32 | Call script subroutine or native engine method |
0x19 | ADD | None | Integer addition: pop B, pop A, push (A + B) |
0x1A | SUB | None | Integer subtraction: pop B, pop A, push (A - B) |
0x1B | MUL | None | Integer multiplication: pop B, pop A, push (A * B) |
0x1C | DIV | None | Integer division: pop B, pop A, push (A / B) |
0x1D | SHL | None | Bitwise shift left: pop B, pop A, push (A << B) |
0x1E | SHR | None | Bitwise shift right: pop B, pop A, push (A >> B) |
0x1F | AND | None | Bitwise AND: pop B, pop A, push (A & B) |
0x20 | OR | None | Bitwise OR: pop B, pop A, push (A | B) |
0x21 | XOR | None | Bitwise XOR: pop B, pop A, push (A ^ B) |
0x22 | LAND | None | Logical AND: pop B, pop A, push (A && B) |
0x23 | LOR | None | Logical OR: pop B, pop A, push (A || B) |
0x24 | EQ | None | Relational equal: pop B, pop A, push (A == B) |
0x25 | NE | None | Relational not equal: pop B, pop A, push (A != B) |
0x26 | LT | None | Relational less than: pop B, pop A, push (A < B) |
0x27 | LE | None | Relational less or equal: pop B, pop A, push (A <= B) |
0x28 | GT | None | Relational greater than: pop B, pop A, push (A > B) |
0x29 | GE | None | Relational greater or equal: pop B, pop A, push (A >= B) |
0x2A | ASSIGN | None | Assign top of stack to referenced variable |
0x2B | PLUS | None | Unary plus: pop A, push (+A) |
0x2C | MINUS | None | Unary minus: pop A, push (-A) |
0x2D | INV | None | Bitwise invert: pop A, push (~A) |
0x2E | NOT | None | Logical NOT: pop A, push (!A) |
0x2F | BLK | None | Block scope marker |
0x30 | ILLEGAL | None | Illegal/undefined opcode (parse boundary) |