Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

PVO File Format

Pre-computed Visibility Octree. Binary spatial data format located in the /maps directory alongside .RGM (scene) and .WLD (terrain) files.

5 PVO files exist, each corresponding to a game level: CATACOMB, CAVERNS, DRINT, ISLAND, PALACE.

For a complete reference parser with pseudocode, see PVO Parser.

Purpose

PVO files store pre-computed visibility data used for geometry culling at runtime. Instead of calculating which polygons are visible from the camera every frame, the game looks up the camera position in the octree and retrieves a pre-built list of visible group IDs.

The runtime lookup works as follows: take the camera’s world-space position, walk the octree from root to leaf by comparing coordinates against each node’s center, then collect the polygon indices stored in that leaf’s PLST entries (which reference ranges in the MLST table). Only those polygons are submitted for rendering — everything else is skipped.

The data is generated offline by placing a virtual camera at every point on a uniform 256-unit grid across the level, running a visibility query at each point, and baking the results into the octree. See Generation Process for details.

Overall Structure

The file uses the same section-framing as RGM: 4-byte ASCII tag + 4-byte big-endian data length.

[OCTH — header]
[OCTR — octree node records]
[PLST — leaf polygon-list records]
[MLST — master polygon index table]
[END  — footer]

Each section is framed as:

OffsetSizeTypeEndianNameDescription
0x004[u8; 4]tagASCII section tag
0x044u32BEdata_lengthPayload size in bytes (0 for END )

Section layout

FileOCTHOCTRPLSTMLSTENDTotal
CATACOMB@0x00 (52)@0x3C (256,091)@0x3E89F (827,861)@0x108A7C (110,788)@0x123B48 (0)1,194,832
CAVERNS@0x00 (52)@0x3C (271,546)@0x424FE (900,844)@0x11E3F2 (33,630)@0x126758 (0)1,206,112
DRINT@0x00 (52)@0x3C (300,688)@0x496D4 (994,987)@0x13C587 (81,074)@0x150241 (0)1,376,841
ISLAND@0x00 (52)@0x3C (213,134)@0x340D2 (1,053,725)@0x1354F7 (216,378)@0x16A239 (0)1,483,329
PALACE@0x00 (52)@0x3C (48,125)@0xBC41 (224,647)@0x429D0 (108,922)@0x5D352 (0)381,786

A sixth section tag PTCH exists in the executable’s string table but is not present in shipped files. The engine writes and loads PTCH sections through dedicated save/load paths (the pvopatchsave console command triggers a write).

Best-effort runtime characterization from engine behavior:

  • PTCH section length is serialized as patch_count * 6 bytes.
  • The loader allocates and reads 6-byte records from PTCH.
  • Patch application expands record data into MLST object-id lists used by PVO visibility checks.

Per-record layout (resolved from add/remove/apply behavior):

OffsetSizeTypeEndianNameDescription
0x004u32LEoctr_node_indexOCTR-node index ((node_ptr - octr_base) / 5) identifying which octree node receives the patch object id.
0x042u16LEobject_indexObject index appended to the runtime-visible MLST id list for that node.

Engine behavior:

  • Add patch: writes octr_node_index at record+0 and object_index at record+4.
  • Delete patch: matches/removes records by the same pair (octr_node_index, object_index).
  • Apply patch: scans records matching current octr_node_index and appends object_index values into the runtime visibility-id buffer.

OCTH Section — Header (52 bytes payload)

OffsetSizeTypeEndianNameDescription
0x004[u8; 4]magicOCTH
0x044u32BEheader_data_sizeAlways 52 (0x34).
0x084u32LEdepthAlways 10. Maximum octree depth.
0x0C4u32LEtotal_nodesTotal octree node count. Equals leaf_nodes + interior_nodes.
0x104u32LEleaf_nodesLeaf node count. Equals total_nodes - interior_nodes.
0x144u32LEmlst_polygon_countTotal entries in the MLST polygon index table. Invariant: mlst_polygon_count * 2 == MLST data_length.
0x184u32LEreservedAlways 0.
0x1C4u32LEcell_sizeRoot cell half-extent. Power of 2: 16384 (4 files) or 8192 (PALACE).
0x204i32LEcenter_xOctree root center X coordinate.
0x244i32LEcenter_yOctree root center Y coordinate.
0x284i32LEcenter_zOctree root center Z coordinate.
0x2C16reservedAlways zero (4 × u32).

Node count relationship

total_nodes = leaf_nodes + interior_nodes where interior_nodes equals the number of 0xFFFFFFFF leaf_ref values in the OCTR section:

Filetotal_nodesleaf_nodesinterior_nodes
CATACOMB23,28716,7876,500
CAVERNS25,69419,6186,076
DRINT29,11222,7176,395
ISLAND23,15418,9714,183
PALACE5,1134,1051,008

mlst_polygon_count confirmation

Filemlst_polygon_countMLST data_lengthcount × 2 == length
CATACOMB55,394110,788yes
CAVERNS16,81533,630yes
DRINT40,53781,074yes
ISLAND108,189216,378yes
PALACE54,461108,922yes

Center coordinates and extents

Filecenter_xcenter_ycenter_zcell_size
CATACOMB35,584-11,52029,95216,384
CAVERNS27,392-9,98421,50416,384
DRINT23,808-13,05633,02416,384
ISLAND35,584-16,38436,60816,384
PALACE31,488-6,14430,7208,192

The octree root spans [center - cell_size, center + cell_size] on each axis.

OCTR Section — Octree Node Records

Serialized octree nodes written sequentially. Each node is a variable-length record addressed by byte offset within the section.

Node record format

OffsetSizeTypeNameDescription
01u8child_maskBit field. Bit i set = child i is present (octants 0..7).
14u32leaf_refByte offset into the PLST section. 0xFFFFFFFF = no leaf data (interior-only node).
54 × nu32[n]child_refsOne entry per set bit in child_mask, low bit first. Each is a byte offset into the OCTR section pointing to a child node. 0xFFFFFFFE = uninitialized-child sentinel (see below).

Record size = 5 + popcount(child_mask) * 4

Possible sizes: 5, 9, 13, 17, 21, 25, 29, 33, 37 bytes.

Octant assignment

The 3-bit octant index encodes spatial position relative to the node center:

bit 0 (value 1) = z > center_z
bit 1 (value 2) = y > center_y
bit 2 (value 4) = x > center_x

Octant 0 = (x-, y-, z-)    Octant 4 = (x+, y-, z-)
Octant 1 = (x-, y-, z+)    Octant 5 = (x+, y-, z+)
Octant 2 = (x-, y+, z-)    Octant 6 = (x+, y+, z-)
Octant 3 = (x-, y+, z+)    Octant 7 = (x+, y+, z+)

Common child_mask patterns

PatternBinaryMeaning
0x0000000000Leaf node, no children
0x3300110011Children in octants 0,1,4,5 (one face)
0xCC11001100Children in octants 2,3,6,7 (opposite face)
0xAA10101010Children in octants 1,3,5,7 (axis-aligned half)
0x5501010101Children in octants 0,2,4,6 (other half)
0xFF11111111All 8 children present

Child and leaf sentinel values

Two sentinel values appear in octree records:

  • 0xFFFFFFFF in the leaf_ref field marks interior-only nodes — nodes with children but no directly associated polygon list. The count of these values equals interior_nodes from the header. In runtime traversal code, 0xFFFFFFFF also serves as the null terminator that ends octree walks.
  • 0xFFFFFFFE in child_refs marks an uninitialized/placeholder child node. During runtime octree traversal, this value indicates an unpopulated slot; when traversal visits it, the slot is overwritten with the current position. This is distinct from a null child (0xFFFFFFFF) and from a valid child offset.

PLST Section — Leaf Polygon-List Records

Serialized leaf data written sequentially. This is the largest section in every file. Each leaf describes which polygon groups are visible from an octree cell.

Leaf record format

OffsetSizeTypeNameDescription
01u8entry_countNumber of entries in this leaf.
16 × nentriesArray of entry_count entries (see below).

Record size = 1 + 6 * entry_count

Each entry:

OffsetSizeTypeNameDescription
02u16countNumber of polygon indices in this sub-list.
24u32mlst_startStarting index into the MLST array.

Each entry references a contiguous slice: mlst[mlst_start .. mlst_start + count].

Constraint: mlst_start + count <= mlst_polygon_count.

Entries within a leaf represent distinct polygon groups. The full visible set for a leaf is the union of all its entry slices. Multiple leaves may share the same MLST ranges.

leaf_ref values in OCTR are byte offsets into this section, pointing to the start of a leaf record.

MLST Section — Master Polygon Index Table

A flat array of u16 visibility group indices.

  • Length: mlst_polygon_count * 2 bytes.

This table is the master list of visibility groups referenced by the octree. PLST entries reference contiguous ranges within this table.

Index semantics

Each u16 value is a placed-object visibility ID, not an individual face index. The indices form a dense, zero-based sequential range with no gaps. The visibility check at runtime uses two lookup paths based on the index value:

  • Indices 0 .. MPSO_count-1: direct MPSO record index. The runtime multiplies the index by 66 (0x42 = MPSO record size) and adds the MPSO array base to get the placed-object record.
  • Indices MPSO_count .. N-1: secondary table index. The runtime subtracts MPSO_count and uses the result as an index into a separate pointer table. This table holds additional static objects loaded at runtime (e.g. via LoadStatic script commands, MPRP rope chains, or other non-MPSO visibility targets).
FileTotal IDsMPSO rangeSecondary rangeMPSO objectsSecondary count
CATACOMB5910–500501–59050190
CAVERNS2330–203204–23220429
DRINT2840–257258–28325826
ISLAND1,7040–1,6901,691–1,7031,69113
PALACE2890–262263–28826326

The MPSO record size (66 bytes = 0x42) appears as the multiplier in visibility lookups.

8 bytes: END (4 ASCII bytes) followed by 0x00000000 (4 zero bytes). Data length is 0.

Generation Process

PVO files are generated by iterating a uniform 3D grid over the world bounding box:

  1. Compute world bounding box from level geometry.
  2. Iterate a uniform 3D grid at 256-unit spacing.
  3. At each grid point, run a visibility query to determine which polygons are visible.
  4. Insert the visible polygon set as a leaf into the octree.
  5. Prune single-child branches, then write the file.

Debug console commands

CommandDescription
pvoi / pvotreeinfoDisplay PVO tree statistics
pvoa / pvoaddpatchAdd object to PVO visibility patch
pvod / pvodeletepatchRemove object from PVO patch
pvoonoffToggle PVO visibility system on/off
pvos / pvopatchsaveSave PVO patches to PTCH section
pvol / pvotreeloadLoad PVO tree from file

Secondary Visibility Table

MLST indices >= MPSO_count reference a secondary pointer table built at runtime. This table is populated by iterating the placed object list and collecting objects whose visibility flag (offset +0x7a in the runtime object struct) is non-zero.

The visibility flag is set by SOUP script functions during object initialization. Objects that receive this flag — such as dynamically loaded static models — become trackable by the PVO system alongside the primary MPSO-based objects.

StepDescription
1Count placed objects with visibility flag set → secondary_count
2Allocate secondary_count × 4 bytes for pointer array
3Iterate placed objects; for each with flag +0x7a != 0, append pointer to array
4At runtime, MLST index - MPSO_count indexes into this array

External References