NIF Parser Audit

VerifiedCaution

Deep audit of the Rust NIF binary parser across the Oblivion → Starfield span: stream-position drift, version gating, block dispatch coverage, and geometry handoff.

Sby Skills Guide Bot
DevelopmentAdvanced
11/6/2026
Claude Code
#nif-parser#binary-formats#rust#game-modding#code-audit

Recommended for

Automated summary

Audits the Rust NIF binary-format parser for byte-accurate behavior across Bethesda game versions, focusing on stream-position drift and block coverage.

Strengths

  • Structures the audit into six risk-ordered dimensions, with stream-position drift as the primary concern.
  • Supports focus, game, and corpus parameters, with version gating spanning Oblivion through Starfield.
  • Uses real game data and opt-in corpus tests to validate parse rates.
  • Grounds findings in code-verified documentation and the authoritative nif.xml specification.

Limitations

  • Scope is deliberately limited to the NIF parser; archive readers, materials, SpeedTree, and physics are explicitly out of scope.
  • Corpus gates are opt-in and apparently not automated, so a green default cargo test does not prove parsing quality.
  • Depends on shared audit protocol files, local game-data paths, and external references being available.
When to use it

Use when auditing or debugging byte-level NIF parsing correctness across Bethesda game versions, especially stream misalignment or block-dispatch gaps.

When not to use it

Do not use for archive readers, material translation, SpeedTree, or physics behavior, or when the NIF parser code or local game data is unavailable.

Security analysis

Caution
Quality score82/100

This is a long, repo-specific audit playbook. I found no malicious payloads: no curl|sh, no deletion, no credential/secret handling, no encoded blobs, nothing that disables safety mechanisms. It does drive shell commands, cargo builds/tests, and repo writes for legitimate auditing purposes, and it defers some behavior to sibling instruction files and an absolute reference path that are not part of the submitted content, so a human should confirm those exist and are benign. Rated caution rather than safe on tool-surface grounds, not on intent.

Findings
  • •No destructive, exfiltration, or obfuscation behavior: the skill only runs local developer tooling (`mkdir -p /tmp/audit/nif`, `git log`, `cargo test`, `cargo run --example nif_stats`) and writes markdown reports under `/tmp/audit/nif/` and `docs/audits/`.
  • •Behavior is delegated to files not included in this submission: `.claude/commands/_audit-common.md`, `.claude/commands/_audit-severity.md`, `docs/engine/nif-parser.md`, and the absolute path `/mnt/data/src/reference/nifxml/nif.xml`. Those transitively control output format and data locations and cannot be vetted here.
  • •`cargo test`/`cargo run` compile and execute code from the target repository, and the skill relies on environment variables (`BYROREDUX_<GAME>_DATA`, `BYROREDUX_REQUIRE_GAME_DATA`) pointing at installed game data — legitimate for the task but a broad execution surface with no declared `allowed-tools` allowlist.
  • •Mentions `BYROREDUX_REGEN_BASELINES=1`, which rewrites checked-in test baselines; misuse could silently mask parser regressions. The text does warn to investigate deltas before regenerating, which mitigates this.
  • •Instructs the agent to execute shell commands (`mkdir -p /tmp/audit/nif`, `cargo test`, `cargo run ... --example nif_stats`, `git log`) and to spawn up to 3 concurrent Task agents; all are legitimate build/audit operations but require shell/tool access in the target repo.
  • •May mutate repository state: `BYROREDUX_REGEN_BASELINES=1` regenerates checked-in baseline TSVs and Phase 3 writes `docs/audits/AUDIT_NIF_<TODAY>.md`, so runs are not purely read-only.
  • •Reads arbitrary user-supplied corpus paths (`--corpus <path>`) and external reference path `/mnt/data/src/reference/nifxml/nif.xml`; no validation or restriction on those paths is described.
  • •References a repo-local `/audit-publish` follow-up command, delegating further action to another skill.
How this analysis is made

Examples

Oblivion stream drift audit
/audit-nif --focus 1 --game oblivion --corpus /games/oblivion/Data/Meshes
Skyrim version gating and block coverage
Audit the NIF parser for Skyrim, focusing on version gating and block dispatch coverage. Use the extracted meshes in /data/skyrim/meshes.
Fallout New Vegas stream and geometry checks
/audit-nif --focus 1,4 --game fnv --corpus /games/fnv/Data/Meshes

description: "Deep audit of NIF parser — stream position, version gating, block dispatch coverage, geometry handoff" argument-hint: "--focus <dimensions> --game <fnv|fo3|skyrim|oblivion|fo4|fo76|starfield> --corpus <path>"

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 .nif files 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 the bsver band.

Phase 1: Setup

  1. Parse $ARGUMENTS; mkdir -p /tmp/audit/nif; fetch the dedup baseline (_audit-common.md).
  2. 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.
  3. cargo test -p byroredux-nif (default lane); record the pass count.
  4. 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_corpus, np_ragdoll_corpus under crates/nif/tests/. They are wired 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. The workflow parses since d98769436 (#4619, closed), but no byroredux-game-data self-hosted runner is registered, so the lane is manual-only (workflow_dispatch; the nightly schedule was removed in fde8feec2, #5905) — no automated result exists (the corpus jobs run --no-fail-fast, fa2af0e89, #5660, so one red test binary no longer hides the ones after it). A green default cargo test says 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 a starfield_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; every sizeless Oblivion NIF must land on its root footer via parse_nif_with_options_and_consumed, eight measured stale-tail LOD files allowlisted — 6ad0c0214, #5782; extraction / parse failures are counted and fail the sweep, 7869a93fd #5781; a present-but-unopenable Oblivion DLC archive fails, c2d3914bf #5780). Checklist (what the guards cannot see):

  • Each block consumes exactly its block_sizes entry. 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) vs read_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_size is invisible to drift_histogram (consumed == declared by construction). It needs its own telemetry: NiObject::opaque_tail_len feeding NifScene::opaque_tail_histogram (#2625). Regression = a new tail-capturing block that doesn't wire it. Also confirm capture-to-block_size is used, never a hardcoded length (Starfield BSLightingShaderProperty tail 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 each until="10.1.0.103" / since="10.1.0.106" pair is present for any version (a read that fires both, or drops the until half, loses text keys and binds no channel); below-band cycle_type default is CYCLE_CLAMP = 2, and import_sequence (crates/nif/src/anim/sequence.rs) collapses a non-finite/non-positive duration to 0.0.
  • NiDynamicEffect carries two affected-nodes groups (until="4.0.0.2" and since="10.1.0.0"); both must be read (NiLight/NiTextureEffect, no anchor on files that old).
  • Starfield BSGeometryMeshData's meshlet/cull trailer is MeshTrailer::OptionalAtEnd only for a standalone .mesh body (parse_from_bytes); an inline (flag 0x200) body reads it Required (#4269) — remaining() == 0 says nothing inside a NIF stream.
  • Starfield BSFaceGenNiNode has its own type with an opaque starfield_tail (aliasing it to plain NiNode::parse under-reads 2 B on every instance); BSWeakReferenceNode declines an unfittable payload into starfield_tail rather 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 on bsver < 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_size mismatches by block type with counts; any non-zero on a known type is drift; separately any no_block_sizes truncation. 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::detect covers every (version, user_version, user_version_2) combination of the seven titles + the FO3-dev edge case. impl NifVariant is minimal (detect, bsver); the live named-helper surface is on NifVersion (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 against version.rs before 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".
  • bsver bands 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/FNV BSShaderFlags + BSShaderFlags2 u32 pair (fo3nv_f1/f2) vs Skyrim+ single-word storage (skyrim_slsf1/2, fo4_slsf1/2) and the FO76/Starfield bs_shader_crc32 arrays; each game reads its own storage shape. Import reads them via is_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 below STRING_TABLE_THRESHOLD; NiPSysEmitter routed through the nif.xml gate (bsver < 26); NiTextureEffect's embedded NiDynamicEffect base gated bsver < 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_name in parse_block_inner (the impl_ni_object! macro only generates trait impls). Count top-level arms fresh — nested matches (type_name_static RTTI arms, type_name_arc_hint) inflate a naive grep -c '=>'; never quote a stale number or copy one from docs/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 in corpus.rs (NIF_ENTRY_EXTENSIONS includes .bto/.btr — renamed NIFs; a second private copy of the rule is the regression; every corpus filter routes through corpus::is_nif_entry, pinned by a source scan, #5259); the gates walk every mesh-bearing archive (open_all_mesh_archives all-or-nothing, open_optional_mesh_archives present-only — a present-but-unopenable archive panics and an all-absent corpus fails, #5232; run_all_meshes_gate with a per-archive limit); parse_real_nifs asserts 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_baseline compares each TSV's own total= 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). The #5260 file-count pin: diagnose_corpus (tests/block_coverage_baselines.rs) compares the NIF file count against that same total= header, so a total_blocks move on a matching install reports as a parser delta (fewer / more — investigate before regenerating), and only a file-count mismatch reads as corpus drift (92ad8247d, #5438). open_present_archive (tests/common/mod.rs) owns the present-but-unopenable panic for open_mesh_archive, open_all_mesh_archives and the Oblivion drift corpus, which now resolves its directory through game_data_dir (5f414d681, #5439; open_optional_mesh_archives alone still logs and continues, by design). nif_stats' directory mode routes through corpus::is_nif_entry and the #5259 scan also rejects eq_ignore_ascii_case("nif") and == / != "nif" compares (ffa85a0b4, #5440; pinned by the_corpus_scan_knows_both_hand_rolled_spellings). Regression guards, not open gaps.
  • kfm.rs (KFM binary catalog, v1.2.0.0–2.2.0.0, transcribed from NiKFMTool::ReadBinary) has no engine consumer today — audit for version-gate fidelity and allocate_vec bounds only (its read_cstring bounds the i32 length against remaining bytes and MAX_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 NiAVObject fields 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.children holds raw BlockRefs 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) through revisit_guard (import/walk/mod.rs): a repeat reference is skipped and depth is capped at MAX_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_cycle in import/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-half BSTriShape vs Starfield BSGeometry (bulk-read via read_u16_array + unpack; import extractor in import/mesh/bs_geometry.rs). Each decodes its own stride and index format.
  • Tangents: FO4+ BSTriShape inline when VF_TANGENTS | VF_NORMALS set; Oblivion/FO3/FNV NiBinaryExtraData "Tangent space (binormal & tangent vectors)" — must honor the [tangents…, bitangents…] swap (the field holds ∂P/∂V, #786); Starfield UDEC3 tangents_raw with W normalized to ±1 at import (#2246); otherwise synthesis — Lengyel accumulation + Gram-Schmidt (nifly CalcTangentSpace, not MikkTSpace, d81f7501f #5963) in the single producer synthesize_tangents_yup; the Z-up synthesize_tangents is a wrapper that converts its inputs and calls it (89a0b6ccd, #5793 — a second accumulation copy is the regression). Distinct paths — don't cross-wire.
  • Skyrim SE skinning index spaces (#3355/#3360, then the 2026-09-17 correction): the NiSkinPartition::Triangles are already global vertex indices on the Stream() == 100 band (do not push them through vertex_map); the packed BSTriShape / SSE global-buffer bone indices already address the skin's bone list and are only widened (widen_packed_bone_indices); only the separate NiSkinPartition.bone_indices channel is partition-local. Regression = re-applying a partition palette to the packed channel (the deleted remap_bs_tri_shape_bone_indices behaviour) or vertex_map to triangles. sse_recon::try_reconstruct_sse_geometry and skin::* 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: Starfield BSGeometry in convert_bs_geometry_skin_weights (#4268, 0abc86c8b), and the BSTriShape inline + SSE global-buffer packed channel and classic NiSkinData via decline_unbounded_packed_indices (import/mesh/skin.rs; fixed in cf9dec258, #5389 — #4268's "siblings already bound" premise was false). The palette bound in render/skinned.rs is a producer-upheld invariant, so a new producer without the decline is the regression. #5524 (fixed in 656e2bdc3) gave the FO4 BsSkinInstance arm its decline + bone_count - 1 boundary tests (mesh/bs_tri_shape_partition_remap_tests.rs), made the decline warning arm-neutral, and restored widen_packed_bone_indices' doc comment.
  • Particle emitters: NiPSysEmitter/NiPSysEmitterCtlr/NiPSysEmitterCtlrData/ NiPSysGrowFadeModifier are typed (blocks/particle.rs); params flow extract_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 sibling NiPSys* controller parses to the opaque NiPSysBlock marker (which discards next_controller_ref) the fallback resolves by base.target_ref to the ctlr targeting the system whose controller_ref equals the chain head — per-instance exact, never a whole-scene first-match, and gated on a non-NULL chain head (#4467; opt-in gate real_archive_torch_meshes_surface_particle_emitters). The legacy tier follows the ctlr's own NiPSysEmitterCtlr.data_ref (until 10.1.0.103, #4560); an unlinked data block is attributed to nobody. The geometry APP_CULLED + live-NiVisController gate lives once in shape_is_culled (node arms: node_is_culled; ef78a0eec, #5792, source-count guard in import/tests/app_culled_visibility.rs — a per-arm copy is the regression). Both walkers skip APP_CULLED / editor-marker particle systems (particle_system_is_culled, #4561) unless a NiVisController — found on the chain or by target_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_BOUNDARY content is authored in meters and scaled once to 70 units/m (length_scale) across meshes, hierarchy, emitters, force fields and animation; BSGeometry vertices also divide out HAVOK_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) reuses decode_bs_vertex_stream with full_precision = false (PSG positions are half even when the descriptor sets full-precision); container read is byroredux_bsa::CsgArchive (/audit-parsers); spec docs/engine/fo4-csg-format.md.
  • FO4 model-space normals + alpha-test: dedicated_shader.rs ORs Model_Space_Normals (F4SF1 bit 12, or the MODELSPACENORMALS CRC for bsver >= 132) and Alpha_Test (F4SF2 bit 25) into MaterialInfo; 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 the resolve_shape_inner body, #5573; a body's per-leaf Havok surface materials are pushed by resolve_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; decoded_constraints_consume_their_whole_block — drift must be exactly 0 on Oblivion, FO3, FNV, Skyrim LE and SE); tests/np_ragdoll_corpus.rs (opt-in FO4: every ragdoll blob decodes with consistent joint pivots, every body's shape volume × density equals its mass). 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+ hkSubPartData is decoded (filter + material), never skip(12). bhkRigidBody takes the CInfo2014 layout at bsver >= FALLOUT4, a deliberate widening of nif.xml's == 130 (which sends FO76/Starfield to CInfo2010); no vanilla content decides it (0 bhkRigidBody on FO4/FO76/Starfield) — a bsver > 130 body 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 no Perp Axis In B1; it is derived as PerpB2 × AxisB from nif.xml's own identity, never zeroed — 526cb07d9, #5537, pinned by oblivion_limited_hinge_derives_perp_b1_from_perp_b2_cross_axis_b), prismatic, ragdoll, ball-and-socket, stiff-spring and the ball-socket chain (BhkConstraintData; BhkBreakableConstraint and malleable wrappers decode the inner CInfo). bhkGenericConstraint has no dispatch arm (nif.xml gives only its name), so it lands as NiUnknown with an exact block_size skip. There is no drift-suppression list any more (#4626 removed is_havok_constraint_stub / stubbed_drift_histogram); any constraint drift lands in the real drift_histogram — suppression is the mechanism that hid bhkHingeConstraint's +128, so do not reintroduce it. The trailing bhkConstraintMotorCInfo (type byte + 0/25/18/17 B) of FO3+ Ragdoll / LimitedHinge / Prismatic is now consumed — in the bare dispatch and the Malleable wrapper (motor, then Strength f32), leniently, via the one BhkBreakableConstraint::consume_motor table; the breakable wrapper consumes it exactly once (a071ed217, #5960). The old motor-tail allowlist (is_known_constraint_motor_tail_drift) is deleted — any non-zero constraint drift is a finding. 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::parse is a thin bsver dispatcher → parse_skyrim (83–129) / parse_fo4 (130–154) / parse_fo76_plus (≥155); each reads only its own field set; per-shader_type trailing count (0–7) matches nif.xml (parse_shader_type_data_fo4 handles only the FO4-specific types and delegates every "same as Skyrim" arm to parse_shader_type_data, b99db0f11 #5737 — a duplicated arm is the regression). Starfield captures starfield_tail to block_size. The material-reference stub gate is !name.is_empty() for bsver >= STARFIELD (stubs are .mat names; the only suffix-less ones are the bare Materials\ directory, not content hashes — #4439) but the suffix-aware is_material_reference for FO76 (155..171 — the stopcond is bsver >= FO76, not the 152 FO76_SF2_CRCS shader-flag threshold; the effect.rs comment was corrected in 2be7c7c0b, #5082); BSEffectShaderProperty and BSLightingShaderProperty::parse_fo76_plus must stay in lockstep (parse_bs_effect_starfield_suffixless_name_stubs).
  • Starfield shader-type translation is keyed at the parser boundary (parse_with_size routes every bsver >= 155 through parse_fo76_plus), not the slot-table layout tag; normalize_shader_type masks types 4/5 — a Starfield FaceTint (3) must not reach the slot table as Skyrim Parallax (#3364).
  • BhkNPCollisionObject is "approximated", not "decoded": the FO4/FO76/Starfield BhkSystemBinary blob does not resolve to a CollisionShape. blocks::collision::havok_packfile::parse_havok_packfile decodes the container (header, sections, three fixup tables → typed object graph); what stays opaque is hknpCompressedMeshShapeData's bit-packed layout (docs/engine/physal.md). Do not report it as decoded. The FO4 skeleton ragdoll blob is decoded (blocks/collision/np_ragdoll.rs, decode_np_ragdoll: hknpRagdollData capsule / sphere / hknpConvexPolytopeShape / hknpDynamicCompoundShape bodies, ragdoll + limited-hinge joints, hknpBreakableConstraintData unwrapped — e1bd40bcf, #5015 (closed); fc4a15104, #6067 (open: 64 joints whose bind-pose pivots disagree are only characterised)) into the classic RagdollCInfo / LimitedHingeCInfo records; its offsets were recovered from the corpus (no SDK) and are pinned by independent physical checks, so a layout claim needs a corpus measurement, not nif.xml. summarize_collision_authoring scans every collision block and feeds CachedNifImport.collision_authoring (byroredux/src/cell_loader/nif_import_registry.rs); examine_collision_kind keeps None / Classic / NewPhysicsStub / Phantom / Unrecognised distinct (a Skyrim+ phantom must not be force-translated as a rigid body). A per-game detail (raw bsver, block-type string) escaping import/collision/ is a finding → cross-link /audit-nifal.
  • hkMotionType byte → canonical MotionType (havok_motion_type): 1..=5 | 8 Dynamic, 6 Keyframed, 7 Static, 9 CharacterKinematic, 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_bytes are #[must_use]; a call that only bound-checks and drops the Vec is a no-op. Bulk arrays go through read_pod_vec<T> (single allocation); allocate-then-loop-fill is the regression. read_pod_vec is bounded by the crate-local unsafe trait AnyBitPattern with an explicit per-type impl list (stream.rs; bytemuck is not a dependency — check before reporting it) and a big-endian compile_error! gate.
  • allocate_vec_min_bytes's minimum must be the smallest legitimate on-disk encoding of one emitted element, not size_of of the in-memory type (#3918): a generated element (e.g. de-stripped triangles, ≥ 2 B each on disk, 6 B in memory) with a size_of-derived bound rejects valid data near EOF and fails silently downstream (a demoted NiUnknown NiSkinPartition un-hides skin partitions → NPCs render bare skin through armor). Known siblings over-demand by 12–18% (InterpBlendItem 20 vs 17, NiAgdDataStream 28 vs 25).
  • Per-block loop counters use the entry().get_mut() / insert split, not or_insert(name.to_string()); block names are interned Arc<str>; ragdoll.rs uses allocate_vec (not the old check_alloc).
  • pre_parse_cell: each stream-pool task extracts (archive read + inflate) and parses its own input, admitted first against STREAM_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). Regression guard: #1171's compile-time PartialNifImport: Send assertion (restored beside the struct in byroredux/src/streaming/mod.rs, 7de61c2c2, #5442, after the #5092 split dropped it).
  • NifStream caps any single file-driven allocation (256 MB); confirm new readers route through the capped helpers, not raw vec![0; n].
  • B-spline resampling (anim/bspline.rs) is import-side allocation the stream cap never sees: each interpolator span is intersected with the owning NiControllerSequence's span, capped per channel at BSPLINE_MAX_SAMPLES_PER_CHANNEL (108 k = 30 Hz × 1 h), and charged against one BsplineSampling budget (BSPLINE_IMPORT_KEY_BUDGET, 1 M keys) per import_kf pass (import_embedded_animations gets 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 in anim/tests/bspline.rs). The sequence clamp only decides which times are emitted: BsplineWindow (reserve()) keeps the interpolator's own span as the u domain (6fef6bd57, #5572), and only a finite, positive-width sequence span clamps (usable_sequence_span, 356a74b0f, #5575 — the pre-10.1.0.106 f32::MAX/f32::MIN defaults must not). Sampling from the interpolator's own stop_time unclamped, or a per-channel BsplineSampling::new(), is the regression — the allocation abort it allows is not caught by parse_one_nif's catch_unwind. Output: /tmp/audit/nif/dim_6.md

Phase 3: Merge

  1. Read all /tmp/audit/nif/dim_*.md; combine into docs/audits/AUDIT_NIF_<TODAY>.md:
    • Executive Summary — clean/recoverable rate per game (vs the nif-parser.md matrix), stream-position mismatches, critical coverage gaps.
    • Block Type Coverage Matrix — block types × games (parsed / skipped / NiUnknown).
    • Findings — by severity (_audit-severity.md NIF rows: hard parse failure = HIGH; a mismatch the block_size reconciliation covers = MEDIUM).
    • Prioritized Fix Order — rendering-blocking blocks, then animation, then collision.
  2. Remove cross-dimension duplicates; material translation → /audit-nifal.

Suggest: /audit-publish docs/audits/AUDIT_NIF_<TODAY>.md

From the file to a first result

The download contains exactly the content displayed on this page. This copy may differ from the latest version in the source repository.

View the source and supporting files

This download contains SKILL.md only. Any scripts, references, templates and dependencies are not included. Get the required files from the source, preserving their directory structure, before continuing.

1. Download the file

Download SKILL.md

From the root of a test project, save the file as .skill-downloads/nif-parser-audit-mvhm5lzl/SKILL.md, or use the command below. This folder is for inspecting the skill before installation.

Command for macOS/Linux or WSL: requires a POSIX shell and curl. On Windows without WSL, use the download button and create the folder manually.

mkdir -p '.skill-downloads/nif-parser-audit-mvhm5lzl' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/nif-parser-audit-mvhm5lzl/download' --output '.skill-downloads/nif-parser-audit-mvhm5lzl/SKILL.md.part' && mv '.skill-downloads/nif-parser-audit-mvhm5lzl/SKILL.md.part' '.skill-downloads/nif-parser-audit-mvhm5lzl/SKILL.md'

2. Check prerequisites

Read SKILL.md before using it. If it references scripts or external services, inspect them and prepare the required access in your test environment.

No recognized prerequisites section in this file. This does not mean it is self-contained: libraries, tools and access requirements may be mentioned elsewhere in the content or source repository.

The platforms on this page are catalog indications. The paths below follow tool documentation; they do not prove this skill has been run in those tools. No minimum tool version is confirmed for this listing.

3. Load the skill

This file does not provide a usable name/description header for this native procedure. It remains available to download and inspect; use the manual method below.

Manual method or another tool: attach SKILL.md to a conversation, or open it in an agent that can read files. Include any required supporting resources. No universal folder is recognized by every tool.

4. Try a first task

Use a short, synthetic input: an API endpoint, a small document or an interface brief. In a new conversation, attach the file or give the agent its path, then adapt this request:

Read .skill-downloads/nif-parser-audit-mvhm5lzl/SKILL.md and apply its instructions to this example: [describe a test input]. Identify any missing files, dependencies or access. Produce the result requested by the skill without inventing data or running unreviewed scripts.

Expected result: output matching the skill instructions, or a precise list of missing prerequisites. A read confirmation alone does not validate a first use. If an external resource is unavailable, that path remains untested.

Related skills