"Deep audit of NIF parser — stream position, version gating, block dispatch coverage, geometry handoff"
Install
npx skillscat add matiaszanolli/byroredux/claude-commands-audit-nif Install via the SkillsCat registry.
NIF Parser Audit
Audit the NIF binary-format parser (crates/nif/src/) for byte-accurate correctness
across the Oblivion → Starfield version span. Real game data is used when available.
The recurring failure mode is stream-position drift: a block over/under-reads its
payload, the consumed-byte count diverges from the header block_sizes entry, and either the
reconciliation masks it (parse "succeeds" with silent corruption) or — on Oblivion-era files
with no block_sizes table — every later block is misaligned and the scene truncates.
Dimensions are ordered by that risk.
Architecture: Orchestrator. Each dimension runs as a Task agent (max 3 concurrent).
Read .claude/commands/_audit-common.md and .claude/commands/_audit-severity.md for
shared protocol (layout, game-data locations, dedup, finding format). Do not duplicate them.docs/engine/nif-parser.md is the code-verified reference (module map, version handling,
per-game coverage matrix, block coverage) — prefer it over re-deriving. The authoritative
block spec is /mnt/data/src/reference/nifxml/nif.xml (reference_nifxml); Gamebryo 2.3
source is the tiebreaker.
Not in scope: BSA/BA2/CSG archive-reader discipline and the FO4 previs header
(/audit-parsers); per-game material translation and the canonical tier (/audit-nifal);
SpeedTree .spt (/audit-speedtree); Havok→Rapier behaviour (/audit-physics — only the
constraint CInfo decode is a NIF seam).
Parameters (from $ARGUMENTS)
--focus <dimensions>: comma-separated numbers (e.g.1,2). Default: all 6.--game <name>:fnv,fo3,skyrim,oblivion,fo4,fo76,starfield. Default: all detected.--corpus <path>: directory of extracted.niffiles for bulk testing.
Extra Per-Finding Fields
- Dimension: Stream Position | Version Gating | Block Dispatch Coverage | Geometry Handoff | Collision/Shader Parsing | Allocation Hygiene
- Game Affected: which
NifVariant(s), citing thebsverband.
Phase 1: Setup
- Parse
$ARGUMENTS;mkdir -p /tmp/audit/nif; fetch the dedup baseline (_audit-common.md). - Note which game-data directories exist. Skim
docs/engine/nif-parser.md§ "Per-game NIF
coverage" so findings are measured against current clean/recoverable rates. cargo test -p byroredux-nif(default lane); record the pass count.- Corpus gates are opt-in (
#[ignore], need installed data):parse_real_nifs,per_block_baselines,block_coverage_baselines,oblivion_stream_drift_corpus,constraint_drift_corpus,normal_synthesis_corpusundercrates/nif/tests/. They run
nightly per title in.github/workflows/real-data-gates.yml(BYROREDUX_REQUIRE_GAME_DATA=1,
so an absent corpus is a failed job). Locally: one title at a time,BYROREDUX_<GAME>_DATA=<Data dir> cargo test -p byroredux-nif --test per_block_baselines <game> -- --ignored --nocapture.
Still true 2026-10-10: the workflow parses since d98769436 (#4619, closed), but nobyroredux-game-dataself-hosted runner is registered (repo runner count 0) — every scheduled
run queues and is cancelled at 24 h, so no nightly result exists (the corpus jobs now run--no-fail-fast, fa2af0e89, #5660, so one red test binary no longer hides the ones after it). A green defaultcargo testsays nothing about parse rates; run the gate locally.
Phase 2: Dimensions
Dimension 1: Stream Position Integrity (PRIMARY)
Paths: crates/nif/src/{lib,stream,header,scene}.rs, every blocks/**/*.rs parse()
First step: git log --since=<last report> --format='%h %s' -- crates/nif/src/blocks crates/nif/src/lib.rs
Flow: parse_nif → parse_nif_with_options → dispatch_blocks (block loop, no_block_sizes
branch) with parse_header / finalize_scene as sibling phases.
Guards: the NifScene::recovered_by_guess zero-assert in the corpus walk
(tests/common, surfaced by parse_real_nifs: a size-cache/oblivion_skip_sizes skip is
inferred, not file-declared — absolute zero, not a rate; a size-cache skip that lands
inside the failed block's own consumed bytes is refused, #3926);every_tail_capturing_block_reports_it_and_parse_nif_records_it (blocks/shader_tests/starfield.rs;
source-shape scan of blocks/shader/*.rs + blocks/node.rs: every struct declaring astarfield_tail field overrides opaque_tail_len; a tail-capturing block in a file not in
its list is invisible to it); blocks/controller/sequence_pre_10_1_0_106_tests.rs;tests/oblivion_stream_drift_corpus.rs (no-block_sizes detector has zero false positives).
Checklist (what the guards cannot see):
- Each block consumes exactly its
block_sizesentry. A finding is a block whose drift the
reconciliation is silently absorbing, not the reconciliation itself. - Oblivion-era files (v20.0.0.4/5 and the v10.x NetImmerse family) ship no
block_sizes:
one wrong field cascades. Audit those parsers for unconditional reads that assume a
later-game layout; version-gated trailing fields must be guarded, never always-read. - Bool width:
read_bool(version-dependent 1 or 4 bytes) vsread_byte_bool(always 1) must
match the nif.xml type per field — a phantom/missing bool is the classic 1-/3-byte drift. - An opaque tail captured to
block_sizeis invisible todrift_histogram(consumed ==
declared by construction). It needs its own telemetry:NiObject::opaque_tail_lenfeedingNifScene::opaque_tail_histogram(#2625). Regression = a new tail-capturing block that
doesn't wire it. Also confirm capture-to-block_sizeis used, never a hardcoded length
(StarfieldBSLightingShaderPropertytail is 0 B on most content — 480,757
of 483,837 blocks — with a 30 B tail on the remaining 3,080). - Pre-10.1.0.106
NiSequence/ControlledBlock/NiControllerSequence: exactly one of
eachuntil="10.1.0.103"/since="10.1.0.106"pair is present for any version (a read
that fires both, or drops theuntilhalf, loses text keys and binds no channel);
below-bandcycle_typedefault isCYCLE_CLAMP = 2, andimport_sequence
(crates/nif/src/anim/sequence.rs) collapses a non-finite/non-positive duration to0.0. NiDynamicEffectcarries two affected-nodes groups (until="4.0.0.2"andsince="10.1.0.0"); both must be read (NiLight/NiTextureEffect, no anchor on files
that old).- Starfield
BSGeometryMeshData's meshlet/cull trailer isMeshTrailer::OptionalAtEndonly for a
standalone.meshbody (parse_from_bytes); an inline (flag 0x200) body reads itRequired
(#4269) —remaining() == 0says nothing inside a NIF stream. - Starfield
BSFaceGenNiNodehas its own type with an opaquestarfield_tail(aliasing it
to plainNiNode::parseunder-reads 2 B on every instance);BSWeakReferenceNodedeclines
an unfittable payload intostarfield_tailrather than overrunning. - Regression pins (verify still fixed, do not re-report): Oblivion v10.x family (#1509,
#1337, #1329, #1310/#1301/#1302 — method: extract →trace_block→ byte-decode; memory
nif_v10x_stride_drift_resolved); Starfield shader over-read (#1510: gate FO76-tail fields
onbsver < STARFIELD, since#BS_F76#is stream 155 only); root-NiNode / inline-type-name
truncation recovers rather than hard-Err(#688/#698). - If a corpus is available: report consumed-vs-
block_sizemismatches by block type with
counts; any non-zero on a known type is drift; separately anyno_block_sizestruncation.
Output:/tmp/audit/nif/dim_1.md
Dimension 2: Version Gating (highest report yield)
Paths: crates/nif/src/{version,shader_flags}.rs, all stream.bsver() / stream.version() sites in blocks/
First step: git log --since=<last report> --format='%h %s' -- crates/nif/src/version.rs crates/nif/src/shader_flags.rs, then for every parser changed since, compare each since=/until=/vercond in nif.xml with the gate written; grep -rn 'V10_1_0_106\|V10_1_0_103' crates/nif/src/blocks shows raw uses of constants a NifVersion helper already encodes.
Guard: the detect_* tests in version.rs; crates/nif/src/version_literal_tests.rs (72769b95a, #5119) fails on any bare NifVersion(0x…) literal or bare decimal bsver comparison in production src/ outside version.rs. The scan blanks each #[cfg(test)] module individually (a first-occurrence cut once hid every later production item, #5258). Nothing guards a named constant used where a NifVersion helper already encodes the gate.
Checklist:
NifVariant::detectcovers every(version, user_version, user_version_2)combination of
the seven titles + the FO3-dev edge case.impl NifVariantis minimal (detect,bsver);
the live named-helper surface is onNifVersion(has_mopp_offset,has_havok_strips_scale,has_object_group_id,has_skin_data_partition_ref,has_skin_data_vertex_weights_flag,has_keyframe_controller_data,has_quat_transform_trs_valid,has_interp_controller_manager_controlled,uses_old_rigid_body_layout,has_ni_sequence_prologue,has_controller_sequence_fields) —
verify againstversion.rsbefore citing (it has been pruned and grown). A new parser
that hardcodes a raw version literal for a feature a helper covers is the regression; a
new helper needs a consumer in the same change (dead helpers were pruned). Doctrine:docs/engine/nif-parser.md"Version handling".bsverbands are named constants (OBLIVION = 11,FO3_FNV = 34,RIGID_BODY_FLAGS16 = 76,SKYRIM_LE = 83,SKYRIM_SE = 100,FALLOUT4 = 130,FO4_DLC_UPPER = 140,FO76 = 155,STARFIELD = 172, …): check operator and constant (off-by-one band membership silently
corrupts a neighbouring game). FO76 (155) and Starfield (172+) differ — keep apart.shader_flags.rs: FO3/FNVBSShaderFlags+BSShaderFlags2u32 pair (fo3nv_f1/f2) vs
Skyrim+ single-word storage (skyrim_slsf1/2,fo4_slsf1/2) and the FO76/Starfieldbs_shader_crc32arrays; each game reads its own storage shape. Import reads them viais_decal_from_legacy_shader_flags/is_decal_from_modern_shader_flags/is_two_sided_from_modern_shader_flags(crates/nif/src/import/material/mod.rs).- Oblivion specifics: u16 flags below
FLAGS_U32_THRESHOLD = 26; inline strings belowSTRING_TABLE_THRESHOLD;NiPSysEmitterrouted through the nif.xml gate (bsver < 26);NiTextureEffect's embeddedNiDynamicEffectbase gatedbsver < FALLOUT4.
Output:/tmp/audit/nif/dim_2.md
Dimension 3: Block Dispatch Coverage
Paths: crates/nif/src/blocks/mod.rs, crates/nif/tests/{per_block,block_coverage}_baselines.rs, crates/nif/tests/data/, crates/nif/src/corpus.rs, crates/nif/src/kfm.rs
First step: cargo run -p byroredux-nif --release --example nif_stats -- <archive-or-dir> --tsv — but do not diff it directly against tests/data/per_block_baselines/<game>.tsv: the two TSVs use different keys (#4265 — nif_stats keys parsed on the dispatched Rust struct name, the baselines on the header-advertised wire name), so they legitimately disagree on alias-collapsed families. Use nif_stats --tsv for the unknown/parsed split per struct, and the per_block_baselines gate itself (--unknown-only for the short view) for wire-name comparison against the checked-in baselines
Checklist:
- Dispatch is a hand-written
match type_nameinparse_block_inner(theimpl_ni_object!
macro only generates trait impls). Count top-level arms fresh — nestedmatches
(type_name_staticRTTI arms,type_name_arc_hint) inflate a naivegrep -c '=>'; never
quote a stale number or copy one fromdocs/engine/nif-parser.md§ "Block coverage". - From a corpus or archive listing, enumerate block types that fall through to
NiUnknown;
count per game; flag any that cascade (a missing block on Oblivion truncates the scene → Dim 1). - A block that parses but is dropped downstream is a Dim 4/5 finding, not coverage.
- Baseline harness invariants: baselines key on wire RTTI via
header.block_type_indices(record_scene_blocks,tests/common/mod.rs), not the parsed
struct name (several arms parse multiple wire types into one struct —BhkRigidBody.is_t,NiPSysBlock.original_type); the corpus definition is shared incorpus.rs
(NIF_ENTRY_EXTENSIONSincludes.bto/.btr— renamed NIFs; a second private copy of
the rule is the regression; every corpus filter routes throughcorpus::is_nif_entry, pinned by
a source scan, #5259); the gates walk every mesh-bearing archive
(open_all_mesh_archivesall-or-nothing,open_optional_mesh_archivespresent-only — a
present-but-unopenable archive panics and an all-absent corpus fails, #5232;run_all_meshes_gatewith a per-archivelimit);parse_real_nifsasserts a per-archive
clean-rate floor (min_clean, 0.995) plus recoverable 100% — truncation counts as
recoverable, so the floor is what catches silent clean-rate collapse. New coverage
findings should land as a baseline-test extension;BYROREDUX_REGEN_BASELINES=1
regenerates after an intentional change.run_baselinecompares each TSV's owntotal=header
(baseline_corpus_total) against the walked corpus before the per-type comparator, so an
install that grew or shrank fails naming the corpus drift rather than reading as parser loss
(#4628). Known-open harness gaps (dedup, do not re-file): the #5260total_blockspin
measures parser output, so a truncation regression reads as "corpus drift" (#5438);open_mesh_archiveand the Oblivion drift corpus still treat present-but-unopenable as
absent (#5439); the #5259 scan only matches theends_with(".nif")spelling, sonif_stats'
directory mode still walks.nifonly (#5440). kfm.rs(KFM binary catalog, v1.2.0.0–2.2.0.0, transcribed fromNiKFMTool::ReadBinary)
has no engine consumer today — audit for version-gate fidelity andallocate_vecbounds
only (itsread_cstringbounds thei32length against remaining bytes andMAX_SINGLE_ALLOC_BYTES, #4631); "unused" is not a finding.
Output:/tmp/audit/nif/dim_3.md
Dimension 4: Geometry Extraction & Import Handoff
Paths: crates/nif/src/import/{mod,types,transform,coord,precombine,units}.rs, import/mesh/, import/walk/, import/material/{walker,dedicated_shader}.rs
First step: git log --since=<last report> --format='%h %s' -- crates/nif/src/import/mesh crates/nif/src/import/walk
This is the parse → ECS handoff; per-game material classification is /audit-nifal.
Guards: import/mesh/sse_skin_index_space_tests.rs (#[ignore], needs Skyrim SE data),bs_tri_shape_partition_remap_tests.rs, sse_skin_geometry_reconstruction_tests.rs,tangent_convention_tests.rs, tests/normal_synthesis_corpus.rs (ignored), import/walk/tests.rs.
Checklist:
- All
NiAVObjectfields via the.av.*sub-struct. Coordinate conversion (Z-up → Y-up,coord.rs) applied consistently to positions, normals, rotations.compose_transforms
neutralises an overflowed (finite-input, non-finite-product) result through the #4549
sanitiser (#4633,compose_transforms_overflowing_products_are_neutralized). - Walker bounds:
NiNode.childrenholds rawBlockRefs never verified to form a tree, so
all five scene-graph walkers (hierarchical, flat, and the lights / texture-effect /
particle-emitter satellites) thread one per-walk visited bitset (new_visited) throughrevisit_guard(import/walk/mod.rs): a repeat reference is skipped and depth is capped atMAX_NIF_NODE_DEPTH(128). #1269's depth cap alone left self-reference / diamond fan-out
exponential (fixed in cf8730adc, #5485; pinned by the*_walker_survives_a_self_reference
family +walkers_survive_a_mutual_cycleinimport/walk/tests.rs). A new walker that
recurses without the guard is the regression; vanilla has zero multi-parent children. - Per-game geometry path: classic
NiTriShape(Oblivion/FO3/FNV) vs Skyrim SE+ packed-halfBSTriShapevs StarfieldBSGeometry(bulk-read viaread_u16_array+ unpack; import
extractor inimport/mesh/bs_geometry.rs). Each decodes its own stride and index format. - Tangents: FO4+
BSTriShapeinline whenVF_TANGENTS | VF_NORMALSset;
Oblivion/FO3/FNVNiBinaryExtraData"Tangent space (binormal & tangent vectors)" — must
honor the[tangents…, bitangents…]swap (the field holds ∂P/∂V, #786); Starfield UDEC3tangents_rawwith W normalized to ±1 at import (#2246); otherwisetangent::synthesize_tangents(Mikkelsen). Distinct paths — don't cross-wire. - Skyrim SE skinning index spaces (#3355/#3360, then the 2026-09-17 correction): the
NiSkinPartition::Trianglesare already global vertex indices on theStream() == 100
band (do not push them throughvertex_map); the packedBSTriShape/ SSE global-buffer
bone indices already address the skin's bone list and are only widened
(widen_packed_bone_indices); only the separateNiSkinPartition.bone_indiceschannel is
partition-local. Regression = re-applying a partition palette to the packed channel (the
deleted remap_bs_tri_shape_bone_indices behaviour) orvertex_mapto triangles.sse_recon::try_reconstruct_sse_geometryandskin::*consume the right counts.
Every skin producer declines the whole weight set (bind pose, never a clamp) when any bone
index >= the skin's own bone count: StarfieldBSGeometryinconvert_bs_geometry_skin_weights
(#4268, 0abc86c8b), and theBSTriShapeinline + SSE global-buffer packed channel and classicNiSkinDataviadecline_unbounded_packed_indices(import/mesh/skin.rs; fixed in cf9dec258,
#5389 — #4268's "siblings already bound" premise was false). The palette bound inrender/skinned.rsis a producer-upheld invariant, so a new producer without the decline is
the regression. #5524 (fixed in 656e2bdc3) gave the FO4BsSkinInstancearm its decline +bone_count - 1boundary tests (mesh/bs_tri_shape_partition_remap_tests.rs), made the decline
warning arm-neutral, and restoredwiden_packed_bone_indices' doc comment. - Particle emitters:
NiPSysEmitter/NiPSysEmitterCtlr/NiPSysEmitterCtlrData/NiPSysGrowFadeModifierare typed (blocks/particle.rs); params flowextract_emitter_params/extract_emitter_rate(import/walk/emitter.rs) →apply_emitter_params(byroredux/src/systems/particle.rs, overrides kinematics + size,
not colour). The rate walk is scoped to the system's own controller chain; when a siblingNiPSys*controller parses to the opaqueNiPSysBlockmarker (which discardsnext_controller_ref) the fallback resolves bybase.target_refto the ctlr targeting the
system whosecontroller_refequals the chain head — per-instance exact, never a
whole-scene first-match, and gated on a non-NULL chain head (#4467; opt-in gatereal_archive_torch_meshes_surface_particle_emitters). The legacy tier follows the ctlr's ownNiPSysEmitterCtlr.data_ref(until 10.1.0.103, #4560); an unlinked data block is attributed
to nobody. Both walkers skip APP_CULLED / editor-marker particle systems
(particle_system_is_culled, #4561) unless aNiVisController— found on the chain or bytarget_ref— un-hides them. Regression = a hardcoded preset replacing an authored birth
rate, or the fallback claiming another system's ctlr. - Starfield units (
import/units.rs):bsver >= FO76_STARFIELD_BOUNDARYcontent is authored
in meters and scaled once to 70 units/m (length_scale) across meshes, hierarchy, emitters,
force fields and animation;BSGeometryvertices also divide outHAVOK_SCALE(69.969).
Rotations, weights and authored scales stay dimensionless; pre-Starfield imports are
bit-identical (import/tests/units.rs). A second scale site downstream is the regression. - FO4 precombined geometry (
import/precombine.rs, M49) reusesdecode_bs_vertex_stream
withfull_precision = false(PSG positions are half even when the descriptor sets
full-precision); container read isbyroredux_bsa::CsgArchive(/audit-parsers); specdocs/engine/fo4-csg-format.md. - FO4 model-space normals + alpha-test:
dedicated_shader.rsORsModel_Space_Normals
(F4SF1 bit 12, or theMODELSPACENORMALSCRC forbsver >= 132) andAlpha_Test
(F4SF2 bit 25) intoMaterialInfo; parsed-but-dropped bits render object-space normals as
tangent-space. The NIF flag ranks below the later BGSM merge.
Output:/tmp/audit/nif/dim_4.md
Dimension 5: Collision & Shader Block Parsing
Paths: crates/nif/src/blocks/collision/, crates/nif/src/blocks/shader/, crates/nif/src/import/collision/
First step: cargo test -p byroredux-nif -- dispatch_coverage_tests bhk_ hk_packed
Guards: import::collision::dispatch_coverage_tests::every_dispatched_bhk_shape_has_resolve_arm
(a new bhk*Shape dispatch arm needs a resolve_shape_inner downcast_ref arm — else it
parses then silently drops collision; nif_shape_dispatch_resolve_parity; the scan reads only theresolve_shape_inner body, #5573; a body's per-leaf Havok surface materials are pushed byresolve_shape_into in the same walk as the shape, #5550, and a packed-strips table is chained
from the data block's sub_parts, #5541);import::collision::dispatch_tests::havok_motion_type_maps_full_enum;blocks/collision/hk_packed_ni_tri_strips_data_tests.rs;tests/constraint_drift_corpus.rs (opt-in; drift must be a known motor-tail value).
Checklist:
bhk*field-for-field: rigid-body flag width (uses_old_rigid_body_layout,RIGID_BODY_FLAGS16 = 76,RIGID_BODY_EXTRA_FLOATS = 9); MOPP offset / strips scale gated
(has_mopp_offset,has_havok_strips_scale); FO3+hkSubPartDatais decoded (filter +
material), neverskip(12).bhkRigidBodytakes the CInfo2014 layout atbsver >= FALLOUT4,
a deliberate widening of nif.xml's== 130(which sends FO76/Starfield to CInfo2010); no
vanilla content decides it (0bhkRigidBodyon FO4/FO76/Starfield) — absver > 130body in
the wild is the evidence to re-check (#4627). The PHYSAL per-game seam is only the constraint CInfo decode.- Constraint CInfo: typed decoders exist for hinge (into
LimitedHingeCInfo), limited
hinge (the Oblivion layout authors noPerp Axis In B1; it is derived asPerpB2 × AxisBfrom
nif.xml's own identity, never zeroed — 526cb07d9, #5537, pinned byoblivion_limited_hinge_derives_perp_b1_from_perp_b2_cross_axis_b), prismatic, ragdoll, ball-and-socket, stiff-spring and the ball-socket chain
(BhkConstraintData;BhkBreakableConstraintand malleable wrappers decode the inner
CInfo).bhkGenericConstrainthas no dispatch arm (nif.xml gives only its name), so it
lands asNiUnknownwith an exactblock_sizeskip. There is no drift-suppression list
any more (#4626 removed is_havok_constraint_stub / stubbed_drift_histogram); any
constraint drift lands in the realdrift_histogram— suppression is the mechanism that
hidbhkHingeConstraint's +128, so do not reintroduce it. By-design
residuals are pinned bycorpus::is_known_constraint_motor_tail_drift(1/18/19/26;
malleable +4; the three no-motor types exactly 0). Decoded is not imported: the ragdoll
importer (import/collision/ragdoll.rs) still declines ball-and-socket / spring / chain
until a canonical joint kind exists (docs/engine/physal.md). BSLightingShaderProperty::parseis a thinbsverdispatcher →parse_skyrim
(83–129) /parse_fo4(130–154) /parse_fo76_plus(≥155); each reads only its own field
set; per-shader_typetrailing count (0–7) matches nif.xml. Starfield capturesstarfield_tailtoblock_size. The material-reference stub gate is!name.is_empty()
forbsver >= STARFIELD(stubs are.matnames; the only suffix-less ones are the bareMaterials\directory, not content hashes — #4439) but the suffix-awareis_material_referencefor FO76 (155..171 — the stopcond isbsver >= FO76, not the 152FO76_SF2_CRCSshader-flag threshold; theeffect.rscomment was corrected in 2be7c7c0b,
#5082);BSEffectShaderPropertyandBSLightingShaderProperty::parse_fo76_plusmust stay in lockstep (parse_bs_effect_starfield_suffixless_name_stubs).- Starfield shader-type translation is keyed at the parser boundary
(parse_with_sizeroutes everybsver >= 155throughparse_fo76_plus), not the
slot-table layout tag;normalize_shader_typemasks types 4/5 — a Starfield FaceTint (3)
must not reach the slot table as Skyrim Parallax (#3364). BhkNPCollisionObjectis "approximated", not "decoded": the FO4/FO76/StarfieldBhkSystemBinaryblob does not resolve to aCollisionShape.blocks::collision::havok_packfile::parse_havok_packfiledecodes the container (header,
sections, three fixup tables → typed object graph); what stays opaque ishknpCompressedMeshShapeData's bit-packed layout (docs/engine/physal.md). Do not report
it as decoded.summarize_collision_authoringscans every collision block and feedsCachedNifImport.collision_authoring(byroredux/src/cell_loader/nif_import_registry.rs);examine_collision_kindkeepsNone / Classic / NewPhysicsStub / Phantom / Unrecognised
distinct (a Skyrim+ phantom must not be force-translated as a rigid body). A per-game
detail (rawbsver, block-type string) escapingimport/collision/is a finding →
cross-link/audit-nifal.hkMotionTypebyte → canonicalMotionType(havok_motion_type):1..=5 | 8Dynamic,6Keyframed,7Static,9CharacterKinematic, else Static; the zero-mass "Dynamic"
reclassification lives beside it — see/audit-physics, do not re-derive.
Output:/tmp/audit/nif/dim_5.md
Dimension 6: Allocation Hygiene (PERF)
Paths: crates/nif/src/stream.rs, blocks/**/*.rs callers, crates/nif/src/anim/bspline.rs, crates/nif/tests/heap_allocation_bounds*.rs, byroredux/src/streaming/pre_parse.rs (pre_parse_cell)
First step: cargo test -p byroredux-nif --features dhat-heap --test heap_allocation_bounds (CI job nif-heap-allocation-bounds runs the three heap files, each its own process)
Guards: the dhat-gated heap_allocation_bounds.rs (a harness = false sequential main since
#5050 — libtest's own threads polluted its exact total_blocks == 1 pin; single node, FO4 packed vertices,
SSE geometry+particle, skin blocks), heap_allocation_bounds_geometry.rs,heap_allocation_bounds_import.rs; stream.rs unit tests
(allocate_vec_sized_*, allocate_vec_min_bytes_uses_the_supplied_minimum_not_size_of);blocks/tri_shape_tangent_presize_tests.rs + the SSE-recon sibling (tangent Vec pre-sized
exactly for a VF_TANGENTS | VF_NORMALS descriptor and not otherwise, both packed decoders, #4617).
Checklist:
allocate_vec/allocate_vec_sized/allocate_vec_min_bytesare#[must_use]; a call
that only bound-checks and drops the Vec is a no-op. Bulk arrays go throughread_pod_vec<T>
(single allocation); allocate-then-loop-fill is the regression.read_pod_vecis bounded by
the crate-localunsafe trait AnyBitPatternwith an explicit per-type impl list
(stream.rs;bytemuckis not a dependency — check before reporting it) and a
big-endiancompile_error!gate.allocate_vec_min_bytes's minimum must be the smallest legitimate on-disk encoding of one
emitted element, notsize_ofof the in-memory type (#3918): a generated element (e.g.
de-stripped triangles, ≥ 2 B each on disk, 6 B in memory) with asize_of-derived bound
rejects valid data near EOF and fails silently downstream (a demotedNiUnknownNiSkinPartitionun-hides skin partitions → NPCs render bare skin through armor). Known
siblings over-demand by 12–18% (InterpBlendItem20 vs 17,NiAgdDataStream28 vs 25).- Per-block loop counters use the
entry().get_mut() / insertsplit, notor_insert(name.to_string()); block names are internedArc<str>;ragdoll.rsusesallocate_vec(not the oldcheck_alloc). pre_parse_cell: each stream-pool task extracts (archive read + inflate) and parses its own
input, admitted first againstSTREAM_PARSE_INPUT_BYTES(64 MiB) by the archive-declared size
(mesh_declared_size) so nothing is allocated before admission; cells with < 8 fresh inputs
keep a serial fast path. A coordinator-side extract-all barrier returning is the regression
(its doc comment was corrected to the per-task extract in 3deda6bb7, #5063). Known-open #5442:
the #5092streaming.rssplit dropped #1171's compile-timePartialNifImport: Send
assertion (still enforced at the channel send).NifStreamcaps any single file-driven allocation (256 MB); confirm new readers route
through the capped helpers, not rawvec![0; n].- B-spline resampling (
anim/bspline.rs) is import-side allocation the stream cap never
sees: each interpolator span is intersected with the owningNiControllerSequence's span,
capped per channel atBSPLINE_MAX_SAMPLES_PER_CHANNEL(108 k = 30 Hz × 1 h), and charged
against oneBsplineSamplingbudget (BSPLINE_IMPORT_KEY_BUDGET, 1 M keys) perimport_kf
pass (import_embedded_animationsgets its own, with no sequence span); a spent budget takes
the static-pose fallback with a warn, the degraded channels counted and logged once per pass
(BsplineSampling::degraded/log_degraded, a076ca7fb #5577) (fixed in 0c4d4af08, #5486; pins inanim/tests/bspline.rs). The sequence clamp only decides which times are emitted:BsplineWindow
(reserve()) keeps the interpolator's own span as theudomain (6fef6bd57, #5572), and only a
finite, positive-width sequence span clamps (usable_sequence_span, 356a74b0f, #5575 — the
pre-10.1.0.106f32::MAX/f32::MINdefaults must not). Sampling from the interpolator's ownstop_timeunclamped, or a per-channelBsplineSampling::new(), is the regression — the allocation abort it allows is not caught byparse_one_nif'scatch_unwind.
Output:/tmp/audit/nif/dim_6.md
Phase 3: Merge
- Read all
/tmp/audit/nif/dim_*.md; combine intodocs/audits/AUDIT_NIF_<TODAY>.md:- Executive Summary — clean/recoverable rate per game (vs the
nif-parser.mdmatrix),
stream-position mismatches, critical coverage gaps. - Block Type Coverage Matrix — block types × games (parsed / skipped / NiUnknown).
- Findings — by severity (
_audit-severity.mdNIF rows: hard parse failure = HIGH;
a mismatch theblock_sizereconciliation covers = MEDIUM). - Prioritized Fix Order — rendering-blocking blocks, then animation, then collision.
- Executive Summary — clean/recoverable rate per game (vs the
- Remove cross-dimension duplicates; material translation →
/audit-nifal.
Suggest: /audit-publish docs/audits/AUDIT_NIF_<TODAY>.md