"Per-game audit of Skyrim (SE + LE) compatibility — BSTriShape packed geometry, BSLightingShaderProperty shader-type dispatch, NPC equip/FaceGen, multi-master load order"
Instal·la
npx skillscat add matiaszanolli/byroredux/claude-commands-audit-skyrim Instal·la des del registre de SkillsCat.
Skyrim Compatibility Audit (SE + LE)
Deep audit of ByroRedux readiness for Skyrim Special Edition (BSVER 100, BSA v105) and the 2011 Skyrim LE (BSVER 83, BSA v104).
Architecture: Orchestrator. Each dimension runs as a Task agent (max 3 concurrent).
Read .claude/commands/_audit-common.md (layout, game data, dedup, finding format) and .claude/commands/_audit-severity.md. This file carries only Skyrim-specific context.
Scope: Skyrim SE is the renderer control bench (Whiterun BanneredMare — cell load and rendering both work), so this is regression coverage plus Skyrim-specific risk, not readiness scoping. The skill owns Skyrim's data through the shared mechanisms; mechanism defects go to the owner: NIF block/dispatch parsing → /audit-nif; canonical material invariants → /audit-nifal; ESM walker / FormID remap / ESL / .STRINGS → /audit-esm (Dim 3 + 6); BSA/hkx/facegen reader discipline → /audit-parsers; .btr/.bto distant LOD, VWD culling (#3307) and the unconsumed .btt tree-LOD family (#4913 — a bespoke non-NIF format) → /audit-exterior; Scaleform AVM1 HUD + GameDelegate catalog (SKYRIM_SKYUI_METHODS, 142 entries, all Measured since 3df616c12 — #5274; skyrim_catalog_is_fully_measured pins 142/0) → /audit-ui; NPC spawn / AI-package / loot gameplay → /audit-gameplay; ragdoll / rapier stepping → /audit-physics (the live Skyrim SE corpse-rig broad-phase panic, #5488, fixed by the rapier 0.36 bump ac81a09f9 + the one-step quarantine cooldown 52de1bc0a); exterior grade → /audit-renderer Dim 11 (a Skyrim exterior crushed ~60% to exact black was the pre-exposure contrast pivot, fixed 3bcf6c8e8 #5482 with a tonemap::grade_contrast mirror — not distant LOD water, as that report first concluded); runtime baseline (.claude/audit-baselines/runtime/skyrim_se-WhiterunDragonsreach.tsv) → /audit-runtime. Skyrim-hosted gameplay gates with their own owners: Story Manager dispatch (#5366, closed — Phases 1–4 live; SNAM previous-sibling walk 846e4a6dd #5385, decline on uncatalogued condition functions 395d2d831 #5380; docs/smoke-tests/sm1-story-manager.sh), the ForceGreet procedure-tree dialect (#5367, closed; landed 287214103, docs/smoke-tests/dt2-skyrim-forcegreet.sh) and the LSCR model loading cover (byroredux/src/systems/loading_model.rs, docs/smoke-tests/p6-loading-model.sh; turntable runs on a cover-owned wall clock, #5306) → /audit-gameplay / /audit-scripting.
Live numbers (parse rate, bench FPS/entities) come from a fresh harness run or ROADMAP's compat matrix / Bench-of-record — never transcribe them here.
Game Context
| Aspect | State |
|---|---|
| NIF | v20.2.0.7; SE BSVER 100 (BSTriShape packed geometry), LE BSVER 83 (classic NiTriShape under Skyrim's shader + skin blocks — the SE gate does not exercise this path) |
| Archives | SE BSA v105 = LZ4 frame (lz4_flex::frame::FrameDecoder, crates/bsa/src/archive/extract.rs) — NOT lz4_flex::block (BA2/Starfield's codec). LE BSA v104 = zlib. Guard: synthetic_v105_block_codec_payload_is_rejected_by_frame_reader (crates/bsa/src/archive/tests.rs, #1558) |
| Releases | LE is selected by the files on disk, not a flag: [[profiles.skyrim_se.releases]] in assets/debug_profiles.toml → GameProfileEntry::for_data_dir (crates/core/src/ecs/game_profiles.rs); unnumbered Skyrim - Meshes.bsa / Textures.bsa |
| ESM | Unified esm/ walker; SE Skyrim.esm form v44 / HEDR 1.71, LE form v40 / HEDR 0.94 |
| Data | SE /mnt/data/SteamLibrary/steamapps/common/Skyrim Special Edition/Data/; LE in the Wine prefix (BYROREDUX_SKYRIMLE_DATA, see _audit-common.md) |
Parameters / Setup
--focus <dimensions> (default all 5). Setup: parse $ARGUMENTS; mkdir -p /tmp/audit/skyrim; gh issue list --repo matiaszanolli/ByroRedux --limit 200 --json number,title,state,labels > /tmp/audit/issues.json; confirm the Data dir exists, else note which dimensions lose real-data validation.
Dimensions (ordered by risk, highest first)
Dimension 1: BSTriShape Packed Geometry + SSE Skinned Reconstruction
Subagent: legacy-specialist
Paths: crates/nif/src/blocks/tri_shape/bs_tri_shape.rs, crates/nif/src/import/mesh/{bs_tri_shape,sse_recon,skin,tangent}.rs, crates/nif/src/blocks/mod.rs (dispatch)
First step: git log --since=<last report> --format='%h %cs %s' -- crates/nif/src/import/mesh crates/nif/src/blocks/tri_shape
Guards: sse_skin_geometry_reconstruction_tests.rs, bs_tri_shape_partition_remap_tests.rs, tangent_convention_tests.rs, alpha_flag_tests.rs; bs_lod_tri_shape_skyrim_consumes_ni_tri_shape_body_plus_3u32_trailer (crates/nif/src/blocks/tri_shape_skin_vertex_tests.rs, #838). Real-data (#[ignore]d — run explicitly): sse_skin_index_space_tests.rs::packed_sse_indices_match_partition_palette_expansion_on_real_data.
Checklist (what the guards cannot see):
- Packed bone indices are already global —
widen_packed_bone_indices(skin.rs) widens theBSTriShape/ SSE global-buffer[u8; 4]channel with no partition-palette remap; onlyNiSkinPartition's separatebone_indicesis partition-local. The old #613/#2577 remap corrupted 4,893 of 11,669 weighted lanes on SSE Draugr/body/hands — re-introducing it (or any second remap) is the regression; a finding proposing it is a false premise. The indices are also bounded: any lane ≥ the skin's own bone count (inline arrays, SSE global buffer, classicNiSkinData) declines the whole weight set to bind pose viadecline_unbounded_packed_indices— never clamps (cf9dec258, #5389; the warn is arm-neutral and the FO4BsSkinInstancearm is boundary-tested too, 656e2bdc3 #5524; pinned bybs_tri_shape_skin_with_out_of_range_bone_index_declines/bs_tri_shape_skin_last_bone_index_is_in_range). - SSE reconstruction (
try_reconstruct_sse_geometry, empty-inlineBSTriShape+ populatedNiSkinPartitionglobal buffer, #559/#638): positions/normals Z-up→Y-up converted; partition triangles are global-buffer indices (concatenate directly, ignorevertex_map); the on-disk "bitangent" triplet is routed as the Y-up tangent (∂P/∂U) so bodies do not read magenta/chrome.BSDynamicTriShape(facegen heads) hasVF_VERTEXclear on every real block — positions come from external data. BSLODTriShape≠BSMeshLODTriShape—BSLODTriShapeinheritsNiTriBasedGeomand routes toNiLodTriShape::parse;BSMeshLODTriShape(FO4) inheritsBSTriShape. Folding the former intoBsTriShapeover-reads every Skyrim tree LOD (#838).BSSubIndexTriShape(segments, Arc-shared) and the fourBsTriShapeKindvariants must not be confused.- Alpha-property cascade — gated on
alpha_property_consumed; skinned geometry inherits the parentNiAlphaPropertyexactly once. - Specialty blocks:
BsLagBoneController/BsProceduralLightningControllerhave dedicated parsers (without them a by-designblock_sizeWARN burst fires per mesh sweep);BSTreeNodewind bones;BSPackedCombined[Shared]GeomDataExtradistant batches;BSFadeNode/BSBlastNode/BSMultiBoundNodeunwrapped by the import walker.
Output:/tmp/audit/skyrim/dim_1.md
Dimension 2: Shader-Type Dispatch + Skyrim Material Slice (NIFAL)
Subagent: renderer-specialist
Paths: crates/nif/src/blocks/shader/{lighting,effect}.rs, crates/nif/src/blocks/shader_tests/skyrim.rs, crates/nif/src/import/material/{mod,dedicated_shader,shader_data}.rs, byroredux/src/material_translate.rs, crates/renderer/shaders/include/pbr.glsl
First step: git log --since=<last report> --format='%h %cs %s' -- crates/nif/src/blocks/shader crates/nif/src/import/material byroredux/src/material_translate.rs
Guards: shader_tests/skyrim.rs (one wire-level test per trailing-data type + parse_bs_lighting_remaining_no_trailing_shader_types_consume_nothing_extra, #4252); lighting_shader_pbr_tests.rs, emissive_source_tests.rs, shader_type_data_tests.rs (crates/nif/src/import/material/). Single-boundary / resolve-once invariants are /audit-nifal.
Checklist:
parse_shader_type_datadispatches numeric types 1/5/6/7/11/14/16 toEnvironmentMap/SkinTint/HairTint/ParallaxOcc/MultiLayerParallax/SparkleSnow/EyeEnvmap; every other type (0/2/3/4/8–10/12/13/15/17–20) reads no trailing data — confirm none over-reads. FO76 (BSShaderType155,parse_shader_type_data_fo76) uses different numbering (4 =Fo76SkinTintColor4, 5 = HairTint) — the two enums must not cross-contaminate; the FO4 arm adds fields only at BSVER 130–139.- Skyrim flag-bit positions differ from FO4; verify the Skyrim decode of decal / alpha-test / skinned / glow / facegen bits.
BSEffectShaderProperty: soft-falloff depth, greyscale texture, lighting influence, env-map min LOD, falloff angle/opacity. - Unauthored fields stay unauthored — Skyrim's
BSEffectShaderPropertyhas noenv_map_scalewire field, so import must not write one (#4393: #4250's fabricated 1.0 flipped every inline Skyrim effect shader into the "authored environment mapping" PBR arm); theenv_map_scale_consumedlatch must be set by the Skyrim+ dedicated writers (#4251). - PBR lobe unreachable for vanilla — the Disney lobe is gated on
MAT_FLAG_PBR_BSDF; vanilla Skyrim materials never author BGSM, so vanilla parse runs must set 0 instances (modded BGSM opting into PBR is the one legitimate path). EmissiveSource— SkyrimBSLightingShaderProperty.emissive_multiplemaps toLighting, notEffect(#1280).- Detail / tint inputs — vanilla Skyrim's blank detail map and alpha-less BC1
_sk.ddsskin tint used to darken every NPC face (#4422detail_neutral, #4423TINT_ALPHA_WEIGHT_BIT, GpuMaterial 432 B). A Skyrim NPC rendering near-black skin is these arms regressing. Also fixed and guarded: the_msntexel takes the source→renderer basis flip before the posed rotation (#3922,material_tests.rs::msn_basis_pin); Environment Map (kind 1) glass classifies as glass — the authored-dispatch guard inhelpers.rsis2..=20(#4392,environment_map_alchemy_glass_classifies_as_glass). - #1241 PBR scalars (
refraction_strength,lighting_effect_1/2for BSVER < FO4) flowMaterialInfo→ImportedMesh.material. - Known-open, don't re-file:
ImportedMaterial.shader_typenever crosses the NIFAL boundary — spawn reads it back from the raw tier (#4256).
Output:/tmp/audit/skyrim/dim_2.md
Dimension 3: NPC Equip + FaceGen (M41)
Subagent: general-purpose
Paths: byroredux/src/npc_spawn.rs + npc_spawn/, crates/plugin/src/equip.rs, crates/plugin/src/esm/records/{items.rs,misc/equipment.rs,actor/npc.rs} (ARMO / ARMA BODT/BOD2, NPC_.WNAM), byroredux/src/scene/nif_loader.rs (select_facegen_diffuse), crates/facegen/src/, byroredux/src/render/skinned.rs
First step: git log --since=<last report> --format='%h %cs %s' -- byroredux/src/npc_spawn crates/plugin/src/equip.rs crates/plugin/src/esm/records/items.rs crates/plugin/src/esm/records/misc/equipment.rs crates/plugin/src/esm/records/actor/npc.rs
Guards: docs/smoke-tests/m41-equip.sh skyrim — hard equip floor 6 on WhiterunBanneredMare (Inventory + EquipmentSlots on saadia, brenuin, mikael, sinmir, amaundmotierreend, hulda); crates/plugin/src/equip_template_tests.rs (#4086); npc_spawn/tests.rs; skyrim_armo_reads_bodt_body_template / parse_arma_reads_bodt_biped_flags (#5358); prebaked_npc_wnam_skin_overrides_the_race_default_and_follows_the_traits_terminal (#5359). Real-data (#[ignore]d): skyrim_bodt_body_templates_decode_their_biped_masks (crates/plugin/src/esm/records/tests.rs), skyrim_npc_wnam_skin_census_matches_the_byte_walk (crates/plugin/tests/parse_real_esm.rs).
Checklist:
- Body-template masks — Skyrim authors the biped mask as
BOD2or the pre-BOD2BODT(u32 mask + u8 general flags + pad, armor type only in the 12-byte form); both ARMO and ARMA decode either (479414ffe, #5358). EverySkyrim.esmARMA (766) and 10 ARMOs (creature skins, Draugr hairs/beards) authorBODT; pre-fix they readbiped_flags == 0, soreconcile_worn_gearstamped Draugr hair/beardNpcAppearanceHiddenon every cell return and load. The #3411 covered-bits gate inresolve_armor_meshesis therefore live on Skyrim (80 redundant same-slot addons across 75 ARMOs, mostlyKhajiitRaceVampirecirclets) — a finding premised on "Skyrim ARMA has no biped field / the gate is a no-op here" is stale;SkinNaked's complementary addons (#3357) still resolve whole. - Armor chain —
resolve_armor_mesh(resolve_armor_mesheson the multi-addon path) walks ARMO → ARMA → worn mesh; a Skyrim ARMA withMOD2but noMOD3(172 ofSkyrim.esm's 766 addons — shields, closed helms, Stormcloak boots) draws its male biped model on a female wearer, per the CK rule (d678dc578, #5510 — regression guard:skyrim_female_wearer_falls_back_to_the_male_biped_model); gated onGameKind::Skyrim, FO4+ keeps the strict reading since no rule is sourced there. Pre-fix those pieces vanished on females while their occupancy still hid the skin partitions beneath (footless female Stormcloaks);humanoid_body_pathsreturns&[]for Skyrim+ (theupperbody.nifpre-scan is Oblivion/FO3/FNV only). Body coverage: the intrinsic skin equips first as the lowest-priority layer (#2093) — the per-NPCNPC_.WNAMskin read off theResolvedNpcUse-Traits terminal, else the race defaultRACE.WNAM(edb5fbdfe, #5359: 664 vanilla records author it — Alduin, Draugr variant skins, Falmer, horses; pre-fix 442 actors wore the race default) — then a post-loop occupancy filter drops any queued mesh — including the skin's — whose slot a higher-priority OTFT/CNTO entry displaced (#2094). Gendered RACE skeleton paths are stored separately from body models. LVLI flattening (expand_leveled_form_id) is level-gated: single-pick (highest eligible) vs multi-pick; pre-fix, LVLI outfits spawned gearless. - Template chain (#4086) — absent TPLT fields resolve down the whole chain (
resolve_inherited_*/walk_inherited_records), not across its two ends; nested actor lists feed every inherited category, level-gate tie-order preserved, cycles share one budget and retain the shell. Spawn reads the chain throughResolvedNpc(#4457); a templated NPC's outfit (DOFT) and leveled-expansion level come from its TPLT terminals, not the placed record (#4812,prebaked_equip_state_reads_outfit_and_level_from_tplt_terminals). - FaceGen is pre-baked on Skyrim — heads come from
meshes\actors\character\facegendata\facegeom\<plugin>\<formid>.nif(BSDynamicTriShape) plus the per-NPCfacetintDDS; there is no runtime EGM/EGT morph on this path (thecrates/facegenEGM evaluator serves the runtime-recipe games;.egt/.trihave no consumer at all). The tint override applies only to the FaceTint (kind 4) head mesh (#4421) — Skyrim's kind-5 shapes are overlays with their own textures (gashes, Argonian hair, Orc tusks); keying on SkinTint replaces them with atlas fragments. - Prebaked-FaceGen miss — vanilla Skyrim ships no facegeom for the player record, so the miss is the player's normal path. Regression guard: fixed in 8c925ec54 (#5095), the miss routes through
PrebakedPhase::HeadParts, loading the NPC's authoredPNAMHDPT meshes in authored order (each root stampedPrebakedHeadPart); pinned bymissing_prebaked_facegen_falls_back_to_authored_head_parts/prebaked_head_fallback_paths_follow_pnam_order_and_skip_unresolvableanddocs/smoke-tests/p3-player-body.sh(player.bodyhead_parts=N). A headless player body is this arm regressing; only an NPC with neither facegeom nor PNAM parts stays headless. BSDismemberSkinInstancepartition data routes into the skinning pipeline. NPC pool composition (#4454) and the rest of spawn/AI belongs to/audit-gameplay.
Output:/tmp/audit/skyrim/dim_3.md
Dimension 4: Multi-Master Load Order + TES5 Cell-Load Regression
Scope split: /audit-esm owns FormID remap, ESL 0xFE space, tombstones and .STRINGS; this dimension owns Skyrim's data through them (record counts, DLC authoring, this title's masters).
Subagent: general-purpose
Paths: byroredux/src/cell_loader/load_order.rs, crates/plugin/src/esm/{reader,cell/}, crates/plugin/src/esm/cell/tests/integration.rs
First step: git log --since=<last report> --format='%h %cs %s' -- byroredux/src/cell_loader/load_order.rs crates/plugin/src/esm/reader.rs crates/plugin/src/esm/cell
Guards: parse_real_skyrim_esm (#[ignore]d; walks Skyrim.esm, finds SolitudeWinkingSkeever, 92-byte XCLL); ESL / tombstone / strings tests live under /audit-esm.
Checklist:
- DLC repro (engine launch — not while another instance runs):
cargo run -- --master Skyrim.esm --master Update.esm --esm Dawnguard.esm --cell Forelhost01 …(Dawnguard's real MAST list is[Skyrim.esm, Update.esm]; a single--master404s the second, #2583). Confirm cross-plugin REFRs land under merged global FormIDs, last-write-wins, unresolved REFRs name the missing plugin, DLC-owned names/dialogue resolve (not raw string IDs), and a DLC-deleted base REFR does not over-render. - Parallel walk, ordered fold (#3813) — plugins parse on the rayon pool but
merge_fromfolds in load order; last-write-wins must follow fold order, never completion order. Guard:load_order_parallel_tests.rs::parallel_walk_matches_the_inline_walk_on_a_localized_master_chain. - DIAL quest ownership — Skyrim authors the quest→topic link as DIAL
QNAM(noQSTI);parse_dialfeeds both intoquest_refs. Emptyquest_refson Skyrim topics is this arm regressing (every NPC topic unowned). Only guard is the#[ignore]d real-datams01_eltrys_authored_placement(crates/plugin/tests/parse_real_esm.rs). An override topic (Update.esm, DLC) folds its INFO children into the master's by FormID — not a whole-DialRecordreplace (168b81322, #5375; pre-fixUpdate.esmemptied wholeSkyrim.esmtopics); guardsdialogue_override_folds_infos_into_the_master_topic/dialogue_override_without_children_keeps_master_infos(crates/plugin/src/esm/records/index.rs). - TES5 compressed-record groups decompress and interiors render; the minimum interior set parses (CELL, REFR, STAT, LIGH, WEAP, ARMO + Skyrim LAND heightmap scale, LTEX, TXST, ADDN); NAVM, HDPT,
BSBehaviorGraphExtraDataparse without error (out of scope to consume).XRGDcorpse poses decode ontoPlacedRef::ragdoll_pose(260afc33f; ids not unique, positions parent-relative) but nothing poses the skeleton from them — open #5015, forward scope. - Control bench — Whiterun BanneredMare entity count + FPS vs ROADMAP's Bench-of-record. Skyrim ships real
bhkcollision, so entity count is flat across collider-gate changes; any entity drop, or an FPS drop at flat entities, is a regression.
Output:/tmp/audit/skyrim/dim_4.md
Dimension 5: Archives + Corpus Gates
Subagent: general-purpose
Paths: crates/bsa/src/archive/, crates/bsa/src/naming.rs, byroredux/src/asset_provider/archive.rs, crates/nif/tests/{parse_real_nifs,common/mod}.rs, crates/nif/src/corpus.rs
First step: BYROREDUX_SKYRIMSE_DATA=… cargo test -p byroredux-nif --test parse_real_nifs -- --ignored skyrim (parse_rate_skyrim_se, parse_rate_skyrim_le)
Guards: numeric_sibling_paths cases in byroredux/src/asset_provider/tests/archive_siblings.rs (siblings_skyrim_zero_start_offers_1_through_9).
Checklist:
- v105 header, hash table, folder-record size, embedded-name flag, compressed-flag priority (archive vs per-file — which wins on disagreement);
BsaArchive::extractdecides the empty-stream edge itself on the zlib and LZ4-frame arms (size header 0 + no stream → empty file, non-zero declared size + no stream →InvalidData; fa2af0e89, #5660/#5073 — the flate2 1.1.10UnexpectedEofbreak hit Oblivion/FO3/FNV placeholders, Skyrim shares the guard); verify a known-good mesh (sweetroll) and a full sweep ofSkyrim - Meshes0/1.bsa+Textures0–8. - Sibling auto-load — a zero-based
Foo0.bsapullsFoo1..Foo9, so distant-LOD diffuse inTextures7.bsaand.btrinTextures8.bsareach the loader; re-narrowing starves distant terrain (/audit-exterior) of textures. Those terrain-LOD diffuse atlases (all 9,326 X8R8G8B8), the tree-LOD atlases and the DLC1 cubemaps are uncompressed 32-bpp:parse_dds(crates/renderer/src/vulkan/dds.rs, mechanism →/audit-renderer) reads the four channel masks — exact BGRA zero-copies as B8G8R8A8, X8R8G8B8 CPU-expands with A=255 (cb6a5bf0b, #5378; pre-fix R/B swapped and alpha came from the X byte). Guards:parse_uncompressed_x8r8g8b8_expands_channels_and_forces_opaque,parse_uncompressed_a8r8g8b8_zero_copies_as_bgra. - Corpus gate — SE sweeps the two base mesh archives plus the account-dependent Creation Club / Anniversary set (
Game::optional_mesh_archives, present-only, kept out of the count-keyed baseline); the corpus counts.bto/.btr(NIF_ENTRY_EXTENSIONS) — filtering on.nifalone hid 10,662 distant-LOD files. Expected: 100% clean, 0 truncated / recovered / realignment WARNs; any WARN on a clean Skyrim corpus is a regression. Report the measured count (33,468 SE NIFs across 8 archives + 22,466 LE at 2026-09-29; ROADMAP /game-compatibility.mdrefreshed to these in e034de59a, #5098); every corpus filter routes throughis_nif_entry(#5259, source-scan guarded); LE has its own per-block baseline (crates/nif/tests/data/per_block_baselines/skyrim_le.tsv). - Real-data render trace: one creature (dragon skeleton / NPC head), one tree LOD, one
BSEffectShaderPropertymagic effect →import_nif_scene→translate_material→render/static_meshes.rs/render/skinned.rs; verify mesh count, material extraction, texture handle resolution.
Output:/tmp/audit/skyrim/dim_5.md
Phase 3: Merge
- Read
/tmp/audit/skyrim/dim_*.md; combine intodocs/audits/AUDIT_SKYRIM_<TODAY>.md:- Executive Summary — control bench state; regression coverage vs Skyrim-specific geometry/shader/equip risk.
- Dimension Findings — grouped by severity per dimension.
- Shader-Type Coverage Matrix — numeric type →
ShaderTypeDataarm × parse-complete / import-complete / render-complete (note types that map toNone). - Cell-Load Regression Status — TES5 cells through the unified walker; Whiterun entity count + FPS vs ROADMAP.
- Remove cross-dimension duplicates.
Suggest: /audit-publish docs/audits/AUDIT_SKYRIM_<TODAY>.md
(label every finding game:skyrim + legacy-compat, plus its own domain label.)