Worker w-meshfmt-a2, third run (a1 paused by user, run 2 truncated after session limit report), 2026-09-23.
Tool: tools/probe_meshfmt.py, Outputs: re_probes/meshfmt/*.txt.
Results
modelcontains geometry fully and uncompressed: 3 LOD slots, float32 positions, 48-byte vertices, u16 indices. There is no GPU-specific format and no streamed LOD tails.- Parser and Writer are byte-exact: 22,400 / 22,400 models across all 27 Retail Archives (24 × base, dlc_1..3), plus 7,465 / 7,465
modelstream. For the sample from the task (3 archives), it is 1,610 / 1,610. - New geometry is writable. The box is located in
re_probes/meshfmt/box_chunk_8.resources(sha256221a3e00…792fd325, deterministic). Container hashes match: 1562/1562, +0x08/+0x20 OK. - Structurally valid after these checks, NOT tested at runtime.
What this run corrected compared to the report from Run 2
All commands were re-executed. Parser, Round-Trip, Negative Controls, and Naming Rules held with the same numbers. Two points were incorrect:
- The box had an incorrect material list entry. It wrote
(material, 0, 0, 3), i.e., a "Retail majority value". The three u32s are derived: (Surface-Index, smallest index, largest index of LOD 0). Surface areas tile their LOD-0 index range seamlessly, starting from about 32,766 vertices as two ranges. This applies to 20,623 / 20,624 Retail models with a list. The exception is a Skydome whose surface carriesmissing_material. Correct for the box is therefore(material, 0, 0, 23). The old box would have reported the vertex range 0..3 instead of 0..23 to the engine. Fixed.check_geometry()now checks this rule, and the old box serves as its own negative control. Consequently, the container has a new sha256 (old3389a0cf…, new221a3e00…). - "The box takes Retail majority values" was not accurate. Re-measured:
head(12 B → model+0x74) is an f32[3]. In 22,214 / 22,400 models it is (0, ±0, 0). The box writes zeros.head2(12 B → model+0xB0) is null exactly when all surfaces have only LOD 0: 22,335 / 22,344. These are LOD parameters. The box has only LOD 0 and therefore writes zeros, which complies with the rule.- The Surface
hashis never 0 in Retail, nor is it the crc32 or fnv1a of the material name. The loader reads it at[rbp-0x7C]and never reads it again: throughout the entire loader there are exactly 2 accesses, the zeroing and theleafor the read. The box writes 0, and the loader ignores the value. density= 1e30 (0x7149F2CA) at both values causes the engine to calculate density itself (0x82B08A compares, 0x82B0A4 sets). Only 4 of the 4,069 LOD-0 in the sample use this. The box uses it.
New additions: --loader checks 21 code facts, 3 .pdata boundaries, and the non-use of the hash directly in module.bin (25/25). --box-into now rejects Megatexture models and vertex-painted models instead of writing a broken container. The docstring sentence "Retail passes all checks" was false: only 569 / 1,610 Retail models pass the Authoring Checks. All failures concern counter-clockwise winding triangles or normals/tangents deviating by more than 0.02. The docstring has been corrected.
Loader (module.bin, Code-RE, --loader checks it afterwards)
| Function | .text (.pdata) | Evidence |
|---|---|---|
idStaticModel::LoadFromBinary |
0x59BEC0–0x59D0E9 | "bad numSurfaces" 0x59C07E, cmp [rsp+0x64],0x724c4d42 0x59C754, "Bad trailing magic" 0x59CF0A |
idTriangles::ReadFromFile |
0x82ABE0–0x82B0E6 | bad numVerts / numIndexes / vertexMask, each FatalError + int3; cmp eax,0x30 0x82AF31 |
| Vertex size | 0x51F290 (Leaf) | Bitfield read, 0x1801F → 48 |
| Static-Model-Stream-Loader | 0x5A0360–0x5A08D9 | staticmodelstream.cpp |
The complete byte layout with addresses is documented in the Docstring of tools/probe_meshfmt.py. Vertex 0x0001801F (the only format in 174,860 / 174,860 LODs): pos f32×3 | uv0 f32×2 | normal u8×4 | tangent u8×4 | colour u8×4 | uv1 f32×2 | u16×4. bounds == min/max of positions in 174,860 / 174,860 LODs. Triangles run clockwise against the normal (chunk_1: 1 224 382 cw to 532 ccw).
Engine Limits (otherwise FatalError or memory corruption; the parser rejects these cases): Stride ≠ 48; numLods > 3 (LOD pointers are at +0/+8/+0x10, +0x18 is a different field, 0x59CB07); missing LOD 0 (LOD 0 creates the surface, 0x59C7EB → 0x59C808); more than 7 Extra-Strings (Stack Array 7 × 0x130, 0x59C233/0x59C238); u16 indices. Rest bytes and short reads are not checked by the engine. The parser is intentionally stricter here.
Question 1: how model, modelAsset, and modelstream relate
- model is the geometry.
…lwo$static_instance=1$lodgroup=gis a placed mesh,maps/…/_combo/*.bmodelis a brush model._world.bmodelhas 0 surfaces, so there is no world blob. - modelAsset is a text declaration (renderLODs, collisionSetup), so only a description.
- modelstream is vertex paint per instance and contains no geometry: a u16 per vertex and LOD.
count== numVerts of the paired model in 6,135 / 6,135 pairs, byte-exact 7,465 / 7,465. - lodGroup (17 Decls) provides
lods.num = 3and path prefixes for the resource name.
Question 2: the Writer
python tools/probe_meshfmt.py --box-into SRC "NAME" OUT
The command replaces model NAME in SRC with a box that fills the LOD-0 bounds and adopts the material. Material list membership comes from the original. Then idclwrite.write() and finalize() run, and the result is read back. Example: chunk_8,
models/crates/metal_crate_02_70cm_100cm.lwo$static_instance=1$lodgroup=default
→ Box −0.531..0.529 × −0.53..0.533 × 0..0.692, 1,568 bytes, 24 V / 36 I, 12/12 cw. Only 1 data hash changes. The library function for custom meshes is build_box(material, lo, hi, listed=True) with write_model()/parse_model()/check_geometry().
Question 3: what a mesh additionally needs
- A
modelentry under the name formed by the engine:lower(model) + "$static_instance=1$lodgroup=" + g.gis the lodGroup with the longest path prefix, otherwisedefault. The rule applies to 19,205 / 19,205 Retail names; the "all default" check only covers 7,500. - A material that exists by name. If missing, there is a warning and the Default material (0x59C854). See w-matpipe.
- A placement via an Entity with
renderModelInfo.model(w-entitypipe). - Not required: modelAsset, modelstream, LOD 1/2 (in Retail they are absent in 9,794 / 64,815 surfaces), density (1e30 → the engine calculates it).
- Collision is separate (
cm, w-cmfmt). Upon replacement, the old form remains.
Checks (executed in this run, output in re_probes/meshfmt/)
| Command | Result |
|---|---|
--loader |
25/25 ok, exit 0 |
--selftest |
Box 1 499 B, 24 V / 36 I, 12/12 cw, List (…,0,0,23), exit 0 |
--verify |
1 610/1 610 parsed and identical, bounds 10 645/10 645, Streams 252/252, Pairs 176/176, List 1 374/1 374 |
--verify --all |
22 400/22 400, bounds 174 860/174 860, Streams 7 465/7 465, Pairs 6 135/6 135, List 20 623/20 624, head2 22 335/22 344 |
--negative |
12 Mutations × 60 Retail Models: 0 accepted. Round-Trip Control 0/60 identical. Box from a1 (wrap) and Box from Run 2 (List) are rejected |
--conventions / --names |
cw 1 224 382 : ccw 532 / 19 205 : 19 205, Control 7 500 |
probe_datahash / probe_idclhash on box_chunk_8 |
1562/1562 / +08 OK, +20 OK |
Same container without finalize() |
1561/1562, +0x20 BAD, each exit 1 |
--box-into on Megatexture or Vertex-Paint Model |
rejected, exit 1, no file |
Open / Risks
- Runtime unproven. Offline proof does not demonstrate that the engine renders the Box.
- The consumer of the material list (model+0xC0) has not been found. The rule is measured (20 623 / 20 624) and not read from code. Similarly open is the semantics of the head/head2 floats, from
unk_28,unk_a/b(Retail 0xFFFFFFFF/0 in 4 062 / 4 069) and the Normal byte 3. - Megatexture materials (
material+0x650 ≠ -1, Patch-Table at 0x59BC90) are not decoded.--box-intorejects them.