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 qui guide Claude a travers le cycle TDD complet.
Audit d'Accessibilité Web
Testing
Réalise un audit d'accessibilité web complet selon les normes WCAG.
Générateur de Tests UAT
Testing
Génère des cas de test d'acceptation utilisateur structurés et complets.