Skip to content

feat: IW3 d3dbsp dumping / loading#834

Open
michaeloliverx wants to merge 35 commits into
Laupetin:mainfrom
michaeloliverx:iw3-d3dbsp-dumping
Open

feat: IW3 d3dbsp dumping / loading#834
michaeloliverx wants to merge 35 commits into
Laupetin:mainfrom
michaeloliverx:iw3-d3dbsp-dumping

Conversation

@michaeloliverx

Copy link
Copy Markdown
Contributor

[WIP] This needs cleanup and review, especially around structure/readability, but the core IW3 d3dbsp loader/dumper path is now working and producing linker_pc-compatible output.

image image

Scope

IW3 raw .d3dbsp files use IBSP version 22 with named/typed chunks. The current loader/dumper work targets this v22 CoD4 Mod Tools format.

Radiant still has internal raw BSP/world loading code embedded in the executable. IW3xRadiant exposes that path, and the stock asserts/sanity checks have been useful for validating generated BSPs. Also the CoD4 decomp still has raw bsp loading code: https://github.com/SwagSoftware/KisakCOD/blob/1d8ff8d579187354ff595cce9477f2f78d5f2382/src/qcommon/cm_load_obj.cpp#L343

Current status

My primary testing map has been mp_backlot because the devs gave the full map source for it in the mod tools. The original cod4map.exe BSP is not expected to be byte-identical to the dumped fixed point. linker_pc consumes and normalizes parts of the source BSP into runtime assets, then OAT dumps the canonical post-link representation.

  • An OAT dumped mp_backlot.d3dbsp and linked again by linker_pc works
  • An OAT linked mp_backlot with an original cod4map.exe produced d3dbsp loaded from disk works
  • An OAT dumped d3dbsp from many stockmaps loads into IW3xRadiant successfully

Given the same source .d3dbsp produced by cod4map.exe, the dumped raw BSP output is now byte-identical between the stock linker and OAT:

stock d3dbsp -> linker_pc -> OAT unlink
stock d3dbsp -> OAT link  -> OAT unlink

Both dumped BSPs have 35 chunks and all 35 / 35 are byte-identical:

0  MATERIALS
1  LIGHTMAPS
2  LIGHTGRID_ENTRIES
3  LIGHTGRID_COLORS
4  PLANES
5  BRUSHSIDES
6  BRUSHSIDE_EDGE_COUNTS
7  BRUSHEDGES
8  BRUSHES
9  LAYERED_TRI_SOUPS
10 LAYERED_VERTS
11 LAYERED_INDICES
25 CELLS
27 NODES
28 LEAFS
29 LEAFBRUSHES
31 COLLISIONVERTS
32 COLLISIONTRIS
33 COLLISION_EDGE_WALKABLE
34 COLLISIONBORDERS
35 COLLISIONPARTITIONS
36 COLLISIONAABBS
37 MODELS
39 ENTITIES
41 REFLECTION_PROBES
43 PRIMARY_LIGHTS
44 LIGHTGRID_HEADER
45 LIGHTGRID_ROWS
47 SIMPLE_TRI_SOUPS
48 SIMPLE_VERTS
49 SIMPLE_INDICES
51 SIMPLE_AABBTREES
52 LIGHT_REGION_COUNTS
53 LIGHT_REGION_HULLS
54 LIGHT_REGION_AXE

Source BSP vs Dumped BSP

16 chunks byte-identical
13 chunks same-size byte diffs
6 chunks size diffs
4 source chunks omitted from dumped fixed point

Byte-identical source chunks:

2  LIGHTGRID_ENTRIES
3  LIGHTGRID_COLORS
4  PLANES
5  BRUSHSIDES
6  BRUSHSIDE_EDGE_COUNTS
7  BRUSHEDGES
31 COLLISIONVERTS
32 COLLISIONTRIS
33 COLLISION_EDGE_WALKABLE
34 COLLISIONBORDERS
36 COLLISIONAABBS
44 LIGHTGRID_HEADER
45 LIGHTGRID_ROWS
52 LIGHT_REGION_COUNTS
53 LIGHT_REGION_HULLS
54 LIGHT_REGION_AXES

Source chunks not emitted in the dumped fixed point:

19 PORTALVERTS
24 LAYERED_AABBTREES
26 PORTALS
42 VERTEX_LAYER_DATA

These chunks are also omitted by the linker_pc-origin dump, so this is not an OAT-only omission.

Notes on the omitted chunks:

19 PORTALVERTS / 26 PORTALS
    Raw portal/PVS-style world portal data emitted by cod4map.exe.
    The linked runtime GfxWorld does not preserve enough source portal lump
    data to dump these byte-for-byte, and the canonical fixed-point BSP omits
    them.

24 LAYERED_AABBTREES
    Source layered AABB tree data. The fixed-point BSP uses the simple
    AABB-tree path for this map, so the layered tree lump is not emitted.

42 VERTEX_LAYER_DATA
    Extra payload for the layered geometry path. For the simple/unlayered
    path, linker_pc uses a small dummy runtime vertex-layer buffer instead of
    preserving the raw lump.

Resources

AI Disclaimer

AI assistance was heavily used during reverse engineering and implementation, especially for the loader. The output has been validated against linker_pc and runtime/Radiant loading tests described above.

OAT linked + unlinked `.d3dbsp` can be read by IW3xRadiant viewer
This preserves lightdef names, reads the disk exponent field, derives cosHalfFovExpanded like the linker, and uses the stock sun-light index convention.
Also mirrors linker_pc angle/quaternion conversion behaviour for dynamic entity poses.
Use the loaded leaf cluster value directly and recover firstLeafBrush from the packed leafbrush cursor when OAT-built fastfiles contain copied per-node leafbrush arrays.

This keeps static model visibility intact when dumping BSPs from OAT-built fastfiles.
Recover raw leafbrush offsets when leaf brush nodes contain copied per-node arrays instead of pointers into the shared leafbrush list, preserving byte-identical BSP dumps after OAT round trips.
Rebuild clipmap leafbrush nodes, box hull data, visibility fallback, material content masking, and collision partition pointers from raw BSP data.

Also improve d3dbsp dumping of raw material contents, leafbrush ranges, and submodel brush/AABB ranges for more stable linker_pc-compatible round trips.
OAT-built fastfiles from raw d3dbsp data now load into game with no crashes. World is not rendering at this time.
LUMP_MODELS = 37 is now byte identical with this change
This makes dumped d3dbsp lumps DRAWINDICES and UNLAYERED_DRAWINDICES byte exact compared to a linker_pc-built ff.
This makes dumped d3dbsp lumps DRAWVERTS and SIMPLE_VERTS byte exact compared to a linker_pc-built ff.
Recompute linker-style drawSurf sort keys for BSP world materials before sorting surfaces, including pixel-constant tie breakers and camera-region fallbacks for referenced techsets.

This makes dumped d3dbsp TRIANGLES and SIMPLE_TRI_SOUPS lumps byte-exact
compared to a linker_pc-built fastfile.
Use linker_pc-compatible misc_model key lookup, vector/scale parsing, angle matrix intermediates, and inverse-scale packing.

This makes dumped d3dbsp lump 39 ENTITIES byte-identical between linker_pc-built and OAT-built fastfiles for the same source BSP.
Reconstruct static model primary light data and AABB tree handling closely enough for OAT-built and linker_pc-built fastfiles from the same source d3dbsp to dump byte-identical raw d3dbsp output.

Also emits a canonical decal-split SIMPLE_AABBTREES lump instead of preserving unstable runtime zero-padding.
Improve the triangle key hash used for coincident surface lookup so 32-bit MSVC builds do not collapse dense map geometry into huge unordered_map buckets.

Also store only the first triangle key per surface, matching the linker_pc decal classification behaviour while avoiding unnecessary per-surface key vectors.
@Nikolai2390

Copy link
Copy Markdown
  1. After testing the IW3 D3DBSP export, i noticed that "dyn_model" entities are currently missing from the exported d3dbsp files.

  2. As a separate feature suggestion, it could be useful to support map specific entity overrides. For example, if a .map file exists in {mapname}/maps/override/, its entities could be used in place of the dumped entities when generating the D3DBSP.

Reconstruct dyn_model entity blocks from clipMap
@michaeloliverx
michaeloliverx marked this pull request as ready for review June 24, 2026 10:55
@michaeloliverx

Copy link
Copy Markdown
Contributor Author
  1. After testing the IW3 D3DBSP export, i noticed that "dyn_model" entities are currently missing from the exported d3dbsp files.

Missed those, added now :)

image

@Laupetin Laupetin added the enhancement New feature or request label Jun 27, 2026
@michaeloliverx

Copy link
Copy Markdown
Contributor Author

Relates to #532

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants