description: "Per-game audit of Skyrim SE compatibility — BSTriShape packed geometry, BSLightingShaderProperty shader-type dispatch, NPC equip/FaceGen, multi-master load order" argument-hint: "--focus <dimensions>"
Skyrim SE Compatibility Audit
Deep audit of ByroRedux readiness for The Elder Scrolls V: Skyrim Special Edition content.
Architecture: Orchestrator. Each dimension runs as a Task agent (max 3 concurrent).
See .claude/commands/_audit-common.md for project layout, game-data locations,
methodology, deduplication rules, and finding format. See
.claude/commands/_audit-severity.md for the severity scale. Do not duplicate
those here.
Why Skyrim is the hardest geometry case
Skyrim SE is the engine's renderer control bench — cell-load and rendering both work (Whiterun BanneredMare). So this audit is not readiness scoping; it is regression coverage plus the genuinely Skyrim-specific risk surface:
- BSTriShape packed geometry — half-float vertex pool with a
vertex_descbitfield, inline tangents, and a separate SSE skinned-reconstruction path that is uniquely prone to silent magenta/chrome corruption. BSLightingShaderPropertyshader-type dispatch — the trailing-field reader branches on ~18 numeric shader types; an off-by-one drops or over-reads geometry on a whole material class.- NPC equip + FaceGen — Whiterun ships 6 named equipped NPCs via M41 OTFT/LVLI; this is the only vanilla cell that exercises the full outfit chain.
- Multi-master load order — DLC interiors via
--masterneed cross-plugin FormID remap.
Dimensions below are ordered by that risk, highest first.
Game Context
Pull live numbers from ROADMAP.md (compat matrix + Bench-of-record) and
docs/feature-matrix.md rather than trusting any figure transcribed here —
benches refresh every /session-close.
| Aspect | State (cite ROADMAP, do not re-transcribe) |
|--------------|---------------------------------------------|
| NIF format | v20.2.0.7 (BSVER 83 / 100) |
| BSA format | v105 ✓ (LZ4 block compression) — crates/bsa/src/archive/ |
| ESM parser | Unified esm/ walker ✓ — Skyrim.esm cells parse (parse_real_skyrim_esm, finds SolitudeWinkingSkeever) |
| Parse rate | 100% clean on the Meshes0 sweep (cite ROADMAP compat matrix for the exact ratio) |
| Rendering | Cells + meshes ✓ — Whiterun BanneredMare is the renderer control bench (entity/FPS figures: ROADMAP Bench-of-record, currently R6a-stale-14) |
| NPC equip | 6 named NPCs equipped via M41 OTFT/LVLI (byroredux/src/npc_spawn.rs) |
| Reference data | /mnt/data/SteamLibrary/steamapps/common/Skyrim Special Edition/Data/ |
Known Specifics (verified against live code)
- BSTriShape — packed vertex pool keyed off a 64-bit
vertex_descbitfield (crates/nif/src/blocks/tri_shape/bs_tri_shape.rs,BsTriShapestruct).VF_*attribute bits select u16 half-precision positions/normals, optional skinning (VF_SKINNED), optional full precision. Per-vertex tangents ship inline in the packed blob whenVF_TANGENTS | VF_NORMALSare set (Skyrim convention; FO4+ shares the inline path — #795 / #796). BsTriShapeKinddisambiguates the five wire-distinct subclasses that share the oneBsTriShapeRust struct:Plain(BSTriShape),LOD(BSLODTriShape),MeshLOD(BSMeshLODTriShape),SubIndex(BSSubIndexTriShape, boxed segmentation payload, #404),Dynamic(BSDynamicTriShape — facegen heads).BSLODTriShapeis routed throughNiLodTriShape, NOTBsTriShape(#838). Per nif.xml,BSLODTriShapeinheritsNiTriBasedGeom(#SKY##SSE#) whileBSMeshLODTriShapeinheritsBSTriShape(#FO4#) — they look identical at the block name but have different bodies. The dispatch incrates/nif/src/blocks/mod.rssends"BSLODTriShape"toNiLodTriShape::parseand"BSMeshLODTriShape"toBsTriShape::parse_meshlod. Pre-#838 routing ofBSLODTriShapethrough BSTriShape over-read every Skyrim tree LOD. Audit guard: any proposal to "fold BSLODTriShape into BSTriShape" is a regression of #838.BSLightingShaderPropertylives incrates/nif/src/blocks/shader.rs(NOT incrates/nif/src/blocks/properties.rs, where it was historically assumed). The shader-type-specific trailing data is theShaderTypeDataenum — 9 Rust variants (None,EnvironmentMap,SkinTint,HairTint,ParallaxOcc,MultiLayerParallax,SparkleSnow,EyeEnvmap,Fo76SkinTint). The dispatch (parse_shader_type_data) maps ~18 numeric Skyrim/FO4BSLightingShaderTypevalues onto those variants (most fall through toNone). FO76 uses the distinctBSShaderType155numbering (parse_shader_type_data_fo76). There is noGlowShadervariant — glow (type 2) readsNonetrailing data.BSEffectShaderProperty— also incrates/nif/src/blocks/shader.rs:soft_falloff_depth,greyscale_texture,lighting_influence,env_map_min_lod, falloff start/stop angle+opacity.BsLagBoneController+BsProceduralLightningController(#837) — both have dedicated parsers (crates/nif/src/blocks/controller/). Without them a large by-designblock_sizeWARN burst fires per Meshes0 sweep.- BSTriShape
data_sizewarning gate (#836) — gated onnum_vertices != 0so the SSE skinned-body reconstruction path doesn't fire false-positive WARNs. BSBoneLODExtraDataparser landed (#614,crates/nif/src/blocks/extra_data.rs).- Other specialty blocks:
BsDismemberSkinInstance(dismemberment),BSPackedCombined[Shared]GeomDataExtra(distant LOD batches),BSTreeNode(SpeedTree wind bones), and theBSFadeNode/BSBlastNode/BSMultiBoundNodeNiNode subclasses unwrapped by the import walker.
Parameters (from $ARGUMENTS)
--focus <dimensions>: Comma-separated dimension numbers (e.g.,1,3). Default: all 7.
Phase 1: Setup
- Parse
$ARGUMENTS. mkdir -p /tmp/audit/skyrim.- Fetch dedup baseline:
gh issue list --repo matiaszanolli/ByroRedux --limit 200 --json number,title,state,labels > /tmp/audit/issues.json. - Confirm
Skyrim Special Edition/Data/exists; if not, note which dimensions lose real-data validation.
Phase 2: Launch Dimension Agents (parallel)
Dimension 1: BSTriShape Packed Geometry + SSE Skinned Reconstruction
Subagent: legacy-specialist
Entry points: crates/nif/src/blocks/tri_shape/bs_tri_shape.rs (BsTriShape parser, vertex_desc / VF_* flags, BsTriShapeKind), crates/nif/src/import/mesh/bs_tri_shape.rs (extract_bs_tri_shape / _local), crates/nif/src/import/mesh/sse_recon.rs (#559), crates/nif/src/import/mesh/tangent.rs
Checklist:
VF_*flag bits mapped correctly (VERTEX, UVS, UVS_2, NORMALS, TANGENTS, COLORS, SKINNED, FULL_PRECISION, EYE_DATA). Half-precision u16 → f32 decode is IEEE-754 binary16 correct.extract_bs_tri_shapehandles every flag combination; index stride (u16 vs u32) chosen correctly. Skinnedbone_indices/bone_weightsextraction matches the skinning pipeline.- SSE skinned-geometry reconstruction tangent path (
crates/nif/src/import/mesh/sse_recon.rs#559, tangent convention #1204 incrates/nif/src/import/mesh/tangent.rs): SSE skinned bodies ship geometry in a partition-remapped global buffer; confirm positions/normals are Z-up→Y-up converted AND the on-disk "bitangent" triplet is routed as the Y-up tangent (∂P/∂U) so reconstructed bodies don't read magenta/chrome (regression guard — mirrors thefeedback_chrome_means_missing_texturesfailure mode). - Alpha-property cascade gated on
alpha_property_consumed(#1201 / #1202): set incrates/nif/src/import/material/mod.rs(searchinfo.alpha_property_consumed = true), consulted at the two gate sites incrates/nif/src/import/material/dedicated_shader.rs(Skyrim+ dedicated-ref implicit-blend write) andcrates/nif/src/import/material/legacy_properties.rs(legacyNiAlphaPropertycascade) —walker.rsno longer contains either gate, only a stale comment referencing the field. Skinned geometry must inherit the parentNiAlphaPropertyexactly once. Pinned byalpha_flag_tests.rs. Output:/tmp/audit/skyrim/dim_1.md
Dimension 2: BSLightingShaderProperty / BSEffectShaderProperty Shader-Type Dispatch
Subagent: renderer-specialist
Entry points: crates/nif/src/blocks/shader.rs (BSLightingShaderProperty, BSEffectShaderProperty, ShaderTypeData, parse_shader_type_data / _fo4 / _fo76), crates/nif/src/blocks/shader_tests/ (split by era, #2056 — skyrim.rs for this audit), crates/nif/src/import/material/ (mod, walker, shader_data), crates/renderer/shaders/triangle.frag
Checklist:
- Every numeric Skyrim/FO4 shader type dispatches to the correct
ShaderTypeDataarm and reads the right trailing-field count (EnvironmentMap = env scale; SkinTint/HairTint = Color3; ParallaxOcc = max_passes + scale; MultiLayerParallax = inner-layer fields; SparkleSnow = 4 params; EyeEnvmap = eye cubemap + two reflection centers). Types with no trailing data (0/2/3/4/8–10/12–13/15/17–19) fall through toNone— confirm none of those silently over-read. - FO76 (
BSShaderType155,parse_shader_type_data_fo76) uses the different numeric mapping (type 4 =Fo76SkinTintColor4, type 5 = HairTint Color3) — guard the two enums don't cross-contaminate. - Flag bits 0–31 (decal / alpha-test / skinned / …) — Skyrim positions differ from FO4; verify the Skyrim decode.
BSEffectShaderProperty:soft_falloff_depth,greyscale_texture,lighting_influence,env_map_min_lod, falloff angle/opacity. Environment-map slot in the texture set; alpha mask threshold.- #1241 PBR scalars surfaced at import (
crates/nif/src/import/material/lighting_shader_pbr_tests.rs,crates/nif/src/import/types.rs): smoothness / IOR / specular_strength flow intoMaterialInfo. - Disney/Burley lobe pin (regression guard): the principled BRDF in
crates/renderer/shaders/include/pbr.glsl(#included bytriangle.frag) is gated onMAT_FLAG_PBR_BSDF(#define MAT_FLAG_PBR_BSDF 32uincrates/renderer/shaders/include/shader_constants.glsl; branch sites searchMAT_FLAG_PBR_BSDFininclude/lighting.glsl+include/pbr.glsl). Vanilla Skyrim LE/SSE materials don't author the BGSM PBR flag (BGSM is FO4+), so the lobe must stay unreachable for vanilla content — confirm vanilla parse runs set 0 instances of the flag on the Skyrim.esm material universe. Modded BGSM that explicitly opts into PBR is the one legitimate path that flips it. See/audit-nifalfor the canonical boundary that sets the flag (Dimension 7). Output:/tmp/audit/skyrim/dim_2.md
Dimension 3: NPC Equip + FaceGen (M41)
Subagent: general-purpose
Entry points: byroredux/src/npc_spawn.rs (M41 actor instantiation), crates/facegen/src/ (.tri/.egm/.egt morph + texture blend), byroredux/src/render/skinned.rs (skinning consumer for heads/bodies), crates/nif/src/import/mesh/sse_recon.rs
Checklist:
- The Whiterun BanneredMare 6 named NPCs (saadia, brenuin, mikael, sinmir, amaundmotierreend, hulda) each land
Inventory+EquipmentSlotsand spawn equipped (OTFT.items + LVLI dispatch). Guard that count + components don't regress. - Skyrim+
resolve_armor_meshwalks ARMO → ARMA → worn-mesh. Body coverage does NOT use the kf-eraupperbody.nifpre-scan (humanoid_body_pathsreturns&[]forSkyrim | Fallout4 | Fallout76 | Starfield— that mechanism is Oblivion/FO3NV-only). Instead: the race's default skin (RACE.WNAM) equips first as the lowest-priority layer (#2093), then a post-loop occupancy filter drops any queued armor mesh — including the skin's — whose inventory slot got displaced by a higher-priority OTFT/CNTO entry covering the same biped bit (#2094). Net effect: the skin's mesh survives for exactly the biped regions nothing else covers. - LVLI flattening (
expand_leveled_form_id) gated on actor level — single-pick (highest eligible) vs multi-pick. Pre-fix, default outfits referencing LVLI spawned with no gear. - FaceGen heads parse via
BSDynamicTriShape+ thefacegencrate, but expected visual fidelity is limited (no FaceGen runtime morph at render time) — confirm parse, not pixel match. BSDismemberSkinInstancepartition data routes into the skinning pipeline. Output:/tmp/audit/skyrim/dim_3.md
Dimension 4: Multi-Master Load Order + TES5 Cell-Load Regression
Subagent: general-purpose
Entry points: byroredux/src/cell_loader/load_order.rs (--master FormID remap), crates/plugin/src/esm/records/ (TES5 records share the unified parser — the per-game legacy stub was removed under #390), crates/plugin/src/esm/cell/ (CELL walker), crates/plugin/src/esm/cell/tests/integration.rs (parse_real_skyrim_esm), ROADMAP.md
Checklist:
- Repeatable
--master <path>(M46.0 / #561): each plugin's TES4 master_files header drives a per-plugin FormID remap so cross-plugin REFRs land under merged global FormIDs; last-write-wins on collision (canonical Bethesda load order). Unresolved REFRs name the missing plugin. Repro:cargo run -- --master Skyrim.esm --esm Dawnguard.esm --cell ForebearsHoldoutInt01. .STRINGSloader wired into the multi-plugin load path (db5bb149) — the localized-string table loader (crates/plugin/src/esm/strings_table.rs) must be invoked fromcell_loader/load_order.rsfor every loaded plugin, not just the active one; a regression that resolves strings off only the last--esmleaves DLC-owned names/dialogue as raw string IDs.- ESL / light-master FormID decode (#1554,
59d3f007) — TES4 record flag0x0200(Light Master / ESL) plugins share the0xFEtop-byte space;crates/plugin/src/esm/reader.rsdecodes their forms as0xFE00_0000 | ((sub & 0x0FFF) << 12) | (raw & 0x0FFF)(12-bit load-order sub-index + 12-bit object id), driven bylight_masteron the plugin. A regression that treats an ESL like a full master (top byte = load-order index) collapses every ESL form into the wrong space and unresolves its REFRs. - Deleted-REFR tombstones (0x20 flag) skipped (#1660,
2dc43106) —walkers.rs(RECORD_FLAG_DELETED = 0x0000_0020) drops a REFR/ACHR/ACRE carrying the header Deleted flag instead of merging it as a live placement; without this a DLC-deleted base REFR over-renders under--master. Keep themod.rsdoc comment in sync — it previously drifted stale after this fix (#1781) and was corrected; a future edit re-introducing "not captured by the parser yet" language is doc-rot, not a real gap. parse_real_skyrim_esmwalks realSkyrim.esm, findsSolitudeWinkingSkeever— guard the unified walker keeps parsing Skyrim cells.- TES5 compressed-record decompression (groups can be compressed; interiors render) stays green.
- Minimum interior-render record set parses: CELL, REFR, STAT, LIGH, WEAP, ARMO, plus Skyrim-specific LAND (heightmap scale), LTEX, TXST, ADDN.
- Out of scope but must parse without error: NAVM, HDPT (metadata),
BSBehaviorGraphExtraData. - Control-bench guard: Whiterun BanneredMare entity count + FPS vs the current ROADMAP Bench-of-record (R6a-stale-14). Skyrim ships real
bhkcollision, so entity count is flat across collider-gate changes — any drop in entity count or substantial FPS regression at the same entity count is a control-bench regression. Output:/tmp/audit/skyrim/dim_4.md
Dimension 5: BSA v105 (LZ4)
Subagent: general-purpose
Entry points: crates/bsa/src/archive/ (mod, open, extract, hash, tests)
Checklist:
- v105 header format; LZ4 block decompression via
lz4_flex::block— verify against a known-good Skyrim mesh (e.g. sweetroll). - Hash table layout vs v104; folder record size; embedded-name flag; compressed-file flag priority (archive-level vs per-file — which wins on disagreement).
- Full-archive extraction sweep:
Skyrim - Meshes0.bsa+Skyrim - Textures*.bsa(through Textures8) all extract without error. Zero-based sibling auto-load (821a425b) —asset_provider/archive.rs::open_with_numeric_siblingsnow auto-loads<stem>2.bsa..<stem>9.bsasiblings, so distant-LOD diffuse inTextures7.bsaand.btrmeshes inTextures8.bsadrag in from a zero-based base archive; a regression that re-narrows sibling discovery starves M35 distant terrain of its LOD textures. Output:/tmp/audit/skyrim/dim_5.md
Dimension 6: Specialty Blocks + Real-Data Rendering
Subagent: renderer-specialist
Entry points: crates/nif/src/blocks/mod.rs (NiLodTriShape / BsLagBoneController / BsProceduralLightningController dispatch), crates/nif/src/blocks/controller/, crates/nif/src/import/walk/, crates/nif/examples/nif_stats.rs, byroredux/src/render/static_meshes.rs, byroredux/src/render/skinned.rs
Checklist:
BSLODTriShape(Skyrim DLC tree LOD) routed throughNiLodTriShape, NOT BSTriShape (#838 regression guard).BSLODTriShapevsBSMeshLODTriShapevsBSSubIndexTriShape— distinct bodies, must not be confused.BsLagBoneController+BsProceduralLightningController(#837): dedicated parsers — without them a by-designblock_sizeWARN burst fires per Meshes0 sweep.BSTreeNodewind-bone list (SpeedTree);BSPackedCombined[Shared]GeomDataExtradistant-LOD batch layout; the import walker unwrapsBSFadeNode/BSBlastNode/BSMultiBoundNode.- M35 prebaked
.btrdistant-terrain LOD (9384d4c2, Skyrim+/FO4) —byroredux/src/cell_loader/terrain_lod_btr.rsloads prebaked.btrdistant-terrain meshes (wired fromcell_loader/terrain_lod.rs); confirm.btrquads parse + render at distance and their diffuse resolves through the zero-based sibling archives (Dim 5). A regression silently drops distant terrain to no-LOD. - Meshes0 sweep baseline: 100% clean / 0 truncated / 0 recovered / 0 realignment WARNs. Any audit observing realignment WARNs on a clean Skyrim Meshes0 corpus has hit a regression.
.btoobject LOD (Session 45 EXAL step 6,byroredux/src/cell_loader/object_lod.rs) — the.btrterrain-LOD counterpart for objects: prebaked per-quad macro-meshes streamed for both Skyrim and FO4 (GameKind::Skyrim | GameKind::Fallout4gate), level-4 quads only,OBJECT_LOD_RADIUS_CELLS = 16. Confirm quads load/unload with the ring and free their entities on exit (mirrorsLodBlock/ObjectLodBlocklifecycle).- VWD full-model culling still unwired (#1731,
175ebf2c) —FLAG_VISIBLE_WHEN_DISTANT(0x00010000) is now parsed and exposed viaRecordHeader::is_visible_when_distant(), butobject_lod.rs's own doc comment says consuming it to cull the full-detail model where a.btoLOD stand-in is shown is still a "future slice" (today object LOD only loads outside the full-detail ring, where no full model is resident, so no z-fight is currently possible by construction). Not a regression — forward scope; don't re-file as a new gap. - Real-data render trace: pick one creature (dragon skeleton / NPC head), one landscape (tree LOD), one magic effect (BSEffectShaderProperty). Trace each
import_nif_scene→material_translate::translate_material→byroredux/src/render/static_meshes.rs(static) /byroredux/src/render/skinned.rs(skinned) — verify mesh count, material extraction, texture handle resolution. Single-mesh smoke: rendermeshes\clutter\ingredients\sweetroll01.nifand confirm FPS stays in the ROADMAP-documented band. Output:/tmp/audit/skyrim/dim_6.md
Dimension 7: NIFAL Canonical Material Translation (Skyrim slice)
Subagent: renderer-specialist
Entry points: byroredux/src/material_translate.rs (translate_material), crates/core/src/ecs/components/material.rs (Material, Material::resolve_pbr, EmissiveSource, classify_pbr_keyword, PbrClassifierInputs), docs/engine/nifal.md
Checklist:
translate_materialis the single canonical boundary mapping the per-gameImportedMesh(BSLightingShaderProperty / BSEffectShaderPropertyMaterialInfo) into one ECSMaterial— no second translation path, no render-time fallback.Material.metalness/Material.roughnessare plain resolvedf32fields, seeded from BGSM/BGEM scalars or anf32::NANsentinel, then filled byMaterial::resolve_pbr, which delegates to the keyword classifierclassify_pbr_keyword. The old per-drawMaterial::classify_pbris deleted — any audit proposing render-time PBR classification is a regression of the canonical boundary.- Ordering at the boundary:
material.resolve_pbr()runs beforecrate::helpers::classify_glass_into_materialso forced-glass roughness wins over the keyword default. EmissiveSourcediscriminator (#1280):enum EmissiveSource { None, Material, Lighting, Effect }. SkyrimBSLightingShaderProperty.emissive_multipleroutes through theLightingvariant (genuine emissive scalar);Effectis the BSEffectShaderProperty diffuse-tint conflation. Verify Skyrim emissive maps toLighting, notEffect.- See
/audit-nifalfor the cross-game canonical-translation deep dive (no-fabrication / single-boundary / no-render-time-fallback invariants). Output:/tmp/audit/skyrim/dim_7.md
Phase 3: Merge
- Read all
/tmp/audit/skyrim/dim_*.mdfiles. - Combine into
docs/audits/AUDIT_SKYRIM_<TODAY>.mdwith structure:- Executive Summary — Skyrim SE is the renderer control bench (Whiterun BanneredMare, 6 equipped NPCs); both loose-mesh and cell rendering work. This audit is regression coverage + Skyrim-specific geometry/shader/equip risk.
- Dimension Findings — grouped by severity per dimension.
- Shader-Type Coverage Matrix — the
ShaderTypeDatavariants × parse-complete / import-complete / render-complete (note which numeric types map toNone). - Cell-Load Regression Status — TES5 cells parse through the unified
esm/cell/walker (compressed records decompress); Whiterun control-bench entity count + FPS vs the current ROADMAP Bench-of-record.
- Remove cross-dimension duplicates.
Suggest: /audit-publish docs/audits/AUDIT_SKYRIM_<TODAY>.md
TDD Red-Green-Refactor
Testing
Skill that guides Claude through the complete TDD cycle.
Web Accessibility Audit
Testing
Performs a comprehensive web accessibility audit following WCAG standards.
UAT Test Case Generator
Testing
Generates structured and comprehensive user acceptance test cases.