matiaszanolli

Exterior Audit — EXAL / SKYAL / WATAL / Ground Cover

"Deep audit of the exterior layers EXAL / SKYAL / WATAL + ground cover — translation boundary, terrain + LOD, ground-cover pipeline, sky bake / clouds / SH, weather + sun, water translation"

matiaszanolli 28 3 Actualitzat fa 1 setm.
GitHub

Instal·la

npx skillscat add matiaszanolli/byroredux/claude-commands-audit-exterior

Instal·la des del registre de SkillsCat.

SKILL.md

Exterior Audit — EXAL / SKYAL / WATAL / Ground Cover

Read _audit-common.md and _audit-severity.md for shared protocol.

Audits the outdoors: per-game ESM environment data -> one canonical form -> terrain, LOD, ground cover, sky, weather/sun, water. Cite the code-verified specs: docs/engine/exal.md, exal-groundcover.md, exal-trees.md, skyal.md, watal.md; VRAM in docs/engine/memory-budget.md § Sky and Ground Cover. Doctrine mirrors /audit-nifal: single-boundary (one translate_* site per category), no-fabrication (a new constant cites a source or is a documented engine choice), no-leak, no-render-time-fallback (no GameKind/version branch downstream of the boundary, Rust or GLSL). Orchestrator: each dimension is a Task agent (max 3 concurrent).

Not owned here: water shading/sync, sky consumption in triangle.frag/raytrace.glsl, GPU-struct lockstep, FSR -> /audit-renderer (Dim 8, 10, 3, 11); buoyancy -> /audit-physics Dim 5; byte-level WTHR/CLMT/WATR/LTEX/GRAS/LAND decode -> /audit-esm; .spt -> /audit-speedtree; streaming cost -> /audit-performance; NIF materials -> /audit-nifal.

Severity: _audit-severity.md applies; a wrong/divergent canonical value out of an EXAL translate_* is HIGH (all-game blast radius, no render-time fallback).

Parameters: --focus <dims> (default all 7); --game <name> restricts Dim 1, 2, 5, 6; --depth shallow|deep (deep = trace one worldspace ESM -> ECS -> GPU; default).
Extra per-finding fields: Dimension (the dimension name below); Tier Violated (single-boundary | no-fabrication | no-leak | no-render-time-fallback | n/a); Game Affected.

Phase 1: Setup

  1. mkdir -p /tmp/audit/exterior; gh issue list --repo matiaszanolli/ByroRedux --limit 200 --json number,title,state,labels > /tmp/audit/issues.json.
  2. Read the newest docs/audits/AUDIT_EXTERIOR_*.md; scope to dimensions whose Paths: changed (git log --since=<report date> --format='%h %cs %s' -- <Paths>).
  3. A Guard: counts only while live: grep -B3 'fn <name>' <file> shows a plain #[test] (not #[ignore]) still asserting its name. Aim at what guards cannot see.
  4. Cargo tests only; no windowed engine launch (feedback_no_parallel_engine_launch).

Phase 2: Dimensions

Dimension 1: EXAL boundary discipline

Paths: byroredux/src/env_translate/ (mod.rs PNAM walk, lod.rs, climate.rs, water.rs, sky_weather.rs, fallback.rs; split from one file by #5584), byroredux/src/groundcover_translate.rs, byroredux/src/fog.rs, byroredux/src/scene/world_setup.rs (orchestration only), byroredux/src/cell_loader/exterior.rs, byroredux/src/app_step.rs (per-tick XCCM override call)
First step: grep -rn 'translate_sky\|translate_weather\|translate_exterior_cell_lighting\|procedural_fallback_' byroredux/src | grep -v -E 'env_translate/|test' — callers are scene/world_setup.rs (incl. install_interior_outdoor_defaults: a direct---cell interior boot installs the canonical outdoor sky once, weather_system advances it; render/sky.rs rebuilds nothing since 387bb9d9c, #4902), systems/weather.rs, cornell.rs; a SkyParamsRes { / WeatherDataRes { literal outside env_translate/, components.rs and #[cfg(test)] is a second site (the let &WeatherDataRes { in systems/weather.rs is #4733's destructure).

  • Sole producers: translate_sky, translate_weather (carries the JNAM table with canonical [0, 1] alphas so the seed frame equals the steady-state sampler, #5344, ad280e17c; resolves IMGS itself from an ImageSpaceSources bundle, #4416; caller-side patch removed in 2eab265fe, #4914, guard translate_weather_resolves_the_exterior_image_space_itself), translate_exterior_cell_lighting, procedural_fallback_*, resolve_exterior_climate (-> resolve_worldspace_climate) / resolve_cell_climate / resolve_default_weather, translate_lod_water, translate_terrain_lod_textures; bulk --grid, streaming and XCCM overrides all install through apply_environment. No-climate is an explicit canonical default, not a render-loop branch. The interior-boot default is marked ProvisionalOutdoorEnvironment so the first real weather snaps instead of cross-fading (only_a_seen_weather_cross_fades). Regression guard: the cross-fade promotes image_space (#4901, d093f194b, image_space_is_promoted_when_the_transition_completes) — the #4481 unpromoted-field class has recurred five times, so check every new WeatherDataRes field against promote_weather_transition_target.
  • GameKind only as table-shaped match returning data (terrain_lod_layout, default_water_for_worldspace, object_lod_scheme, DefaultLandTexture::for_game, combined_lod_supported, resolve_exterior_climate's rung table; parse-boundary arms such as the wrld.rs stamp below are allowed). Flag game == in logic and any game token in sky* / clouds / groundcover_* GLSL.
  • Climate resolution is one boundary call from build_exterior_world_context: resolve_exterior_climate (env_translate/climate.rs, #5423, b3e679dba). Rungs: (1) WRLD CNAM up the PNAM-gated WNAM chain (resolve_worldspace_climate); (2) the centre cell's XCLR -> REGN CNAM (region_climate_for_center, orthogonal-neighbour probe, b4497ec1d; its test still lives in cell_loader/exterior.rs), mod-only: REGN CNAM is xEdit-undefined and vanilla authors none in any of the four census games (#5421, bd052048a; the "Oblivion-era regions author one" claim was retired from exal.md and the resolve_exterior_climate doc by #5519, 65cadde2f); (3) Oblivion only, oblivion_climate_rungs (#5388, 217bcbc9f): the chain's authored CNAM, then the CS <key>Climate name along the whole WNAM chain (named_climate), then a logged richest-climate last resort. Oblivion children inherit by a parse-time stamp of the LAND/LOD/WATER/CLIMATE PNAM bits (crates/plugin/src/esm/cell/wrld.rs, #5374, 67afa4b95); water, climate and (via PNAM 0x02 "Use LOD Data") the distant LOD rings consume it (no parent LAND outside the child's footprint, open #5505; the rings key on lod_source_worldspace / ExteriorWorldContext::lod_worldspace_key() since #5496, 2ca57c4bc, guard installed_fo3_statesman_roof_inherits_wasteland_lod + the synthetic chain tests), and the Oblivion inherit-all is that parse-time PNAM stamp walked by the one inherit_up_chain (the flag-less inherit_all_up_chain twin was deleted by #5516, 65cadde2f; the surviving pin is oblivion_child_gets_the_inherit_all_stamp_and_eras_stay_real). TES5/FO4 XCCM is a REGN reference consumed as CLMT; it falls back to the worldspace climate (open #5463).
  • Default weather is one rule for the worldspace resolve and the per-cell XCCM re-resolve (resolve_default_weather, #5424, 302394f94): highest non-negative-chance resolvable WTHR, else for climates with only WTHS-referencing WSLT rows (ClimateRecord.seasonal_weathers) the authored DefaultWeather WTHR stand-in (chance i32::MAX; 424dfe0e1, #5363). WTHS's reflection schema decodes since a5484faf6 (#5364 closed), but its instance values are uninterpreted, so the stand-in stays. Guard wths_only_climate_resolves_the_defaultweather_stand_in; its "both callers share the rule" half is a source_scan pin since #5517 (65cadde2f): cell_loader/exterior.rs and scene/world_setup.rs must call resolve_default_weather and neither may name default_weather_by_edid. A cell's XCCM override is resolved once per grid cell, success or failure (climate_override_grid, #5529, 2fd7ee270).
  • Sentinels are not leaks (skyrim_dalc_per_tod: None, no default water, sunlight_dimmer 1.0 without HNAM); grep a parsed field's consumers before calling it "parked". Constants cite a source or are documented engine choices: SUN_SOUTH_TILT (no authored latitude, exal.md §9 Q1), cloud coverage 0.86/0.80/0.70/0.40/0.55 (skyal.md §2.3).
  • fog.rs (exterior half): the near/far ramp becomes a FogMedium once via fit_legacy_fog_extinction; runtime never rebuilds a ramp. Particle fire/smoke volumes there -> /audit-renderer Dim 8.
  • Guard: material_translate.rs::every_exterior_spawner_inserts_a_boundary_material walks SPAWNER_ROOTS recursively and, since #4856, counts boundary calls (translate_texture_only_material*, translate_material, insert_object_lod_submesh_material) against , MeshHandle( inserts per enclosing function. Blind to other insert idioms and to a translate hoisted into a caller. Its stripper removes only inline mod x { test blocks (#4917, 5fe978b1d, stripper_keeps_production_after_out_of_line_test_module_declarations); rebuild_lod_water_mesh's mesh swap is an explicit exemption (d1ec1c57f). env.health (commands/env_health.rs) gates non-finite/negative/non-unit inputs, incl. WeatherSkyState and the image-space slots with a field-list completeness scan (#4916, a55a2b906); the interior/exterior check fires only for exterior lighting under a non-exterior sky (#4915, exterior_lighting_under_a_non_exterior_sky_is_caught).

Dimension 2: Terrain, splatting, LTEX/GNAM

Paths: byroredux/src/cell_loader/terrain.rs, terrain_seam.rs, crates/renderer/shaders/include/terrain_sample.glsl
First step: cargo test -p byroredux terrain

  • spawn_terrain_mesh + build_cell_splat_layers are the one LAND translate site (33x33 grid; FO76/Starfield ship no LAND). Splat: <= TERRAIN_SPLAT_LAYERS (8, one named constant host + GLSL, #5113/#5173) lanes, coverage-aware truncation, BTXT feather; optional normal/specular roles use handle 0 = "no contribution" (never the checker) and upload linear. Per-lane cover_affinity is keyed from the LTEX editor ID (cover_affinity_key, #4899), never the TXST diffuse path. The BTXT base is a cover input too: base_cover_affinity (tile) / the cell record carry it and byroGcAffinity composes base-then-lanes in the diffuse loop's order (#4903, 177774890; groundcover_affinity_composes_the_base_in_diffuse_loop_order). The BTXT feather covers internal quadrant edges only; cell edges stay hard at vanilla parity by decision (#4905, btxt_transition_cell_edges_stay_hard_vanilla_parity): not a finding.
  • LTEX.GNAM (an array since #4642, carried through merge_from since #4904) -> authored_grass_for_splat_layers (GPU lane order) -> TerrainTile.authored_grass, read by nothing yet (the scatter by design; the model tier uses climate, #4906).
  • Guard: gpu_instance_layout_tests.rs::gpu_terrain_tile_is_176_bytes, shader_contract_tests.rs::named_splat_offset_reads_keep_their_unorm_recovery, terrain_splat_tests.rs. Unseen: >8 real layers, a DLC overriding one LAND tile.

Dimension 3: Ground-cover pipeline

Paths: crates/renderer/src/vulkan/groundcover.rs (+ groundcover/construct.rs, groundcover/frame.rs, split by #5089), groundcover_models.rs (Phase C model tier), groundcover_stats.rs, groundcover_bench.rs (opt-in §11.1 measurement harness), crates/renderer/shaders/groundcover_*, crates/renderer/shaders/include/groundcover_*.glsl, crates/renderer/src/shader_constants_data.rs (GROUNDCOVER_*), crates/core/src/ecs/components/groundcover.rs, byroredux/src/render/groundcover.rs
First step: cargo test -p byroredux-renderer groundcover && cargo test -p byroredux groundcover

  • Determinism: seeds are frame-invariant integer hashes (only the tier cross-fade uses the frame serial); compaction is in candidate order, never an atomic race. Guard: groundcover_seed_hashes_are_integer_and_frame_invariant.
  • Sync (#4293): counter clear = zero fill, then ordered seed fills (counter_seed_fills_are_sequenced_after_the_zero_fill); source-shape only, so hazards need a BYRO_VALIDATION=1 run.
  • Budget: GROUNDCOVER_MAX_CHUNKS 256 x GROUNDCOVER_MAX_BLADES_PER_CHUNK 16,384 x 16 B = 64 MiB arena (64.6 MiB total, allocated on every RT device). The model tier adds 8.3 MiB (groundcover_model_resident_bytes). Guard: memory_budget_ledgers_the_sky_and_ground_cover_owners (both rows), chunk_cap_covers_every_chunk_in_reach.
  • LOD tiers: blade / ribbon (projected-height crossover, not distance) / clump card / always-on terrain detail layer. Guard: tiered_indirect_streams_preserve_fixed_blade_slabs.
  • Lifecycle (#4307): recreate_draw_pipelines runs in resize's format_changed arm. Guard: context/resize.rs::a_surface_format_change_rebuilds_every_main_pass_pipeline.
  • FSR (#4297): opaque ribbons write reactive 0 / T&C 0 with previous-pose motion vectors; only the stochastic hand-off writes bounded (<=0.9) reactive (§12.14). Guard: blade_motion_and_fsr_mask_contract_stay_material_driven (shader-text only).
  • Boundary: per-game data enters only via groundcover_translate.rs (layer_affinity: nograss/asphalt suppression beats any positive keyword; climate palette; resolve_wind*); GroundCoverDimmer (HNAM grass dimmer) rides weather_system. Blade ground colour is the terrain's own byroTerrainSplatAlbedo chain, BTXT base included (#4907, blade_ground_colour_uses_the_shared_terrain_splat_chain). Not findings: GRAS is not translated into blades (§12.12 census); REGN RDGS/RDOT is out of scope (§10, #3301). A new bare literal in groundcover_* GLSL is a finding; the blade-shaping set is named in shader_constants_data.rs and registered uncited in §12.12 (#4378, fd0cd577c / 4fc8ab8b2).
  • Authored-model tier (§12.12 Phase C, #4413): GRAS -> resolve_authored_cover (one caller, scene/world_setup.rs) -> AuthoredCover -> groundcover_models.comp, drawn as indexed-indirect shapes. Guard: model_timer_encloses_all_phases_and_stats_copy (#4866). Candidates sit on one world lattice across chunk borders (#4919, model_candidates_tile_one_world_lattice_across_chunk_borders); over the instance cap each record keeps whole plants, nearest chunks first (#4920/#5176, over_budget_grant_keeps_the_nearest_chunks_plants), surfaced as stats GCModels demanded/emitted and zeroed on nothing-to-place frames (#5220); rigid by documented decision (#4921, §12.12); blend-only GRAS shapes excluded at template spawn (blend_only_cover_shape, #4924). Record selection weighs climate x authored density over placeable records with one union accept rate 1 - PI(1 - d_i) (render/groundcover.rs, 1505b4404; 3e6d94ff0 was the earlier saturating bake). Known-open: not localised per lane LTEX->GNAM (#4906's remaining half; wrong species per region, DLC bleed).
  • Known-open: Phase 4 RT proxy gated on need (§5 Stage 2).

Dimension 4: Sky, weather, sun

Paths: crates/renderer/src/vulkan/{sky_cube,sky_dome,cloud_noise}.rs (+ sky_cube/), crates/renderer/shaders/{sky_cube,sky_prefilter,sky_irradiance}.comp, crates/renderer/shaders/include/{sky,clouds,sky_sh,sky_cube_direction,medium_transport}.glsl, byroredux/src/systems/weather.rs, byroredux/src/render/sky.rs
First step: cargo test -p byroredux-renderer sky_ && cargo test -p byroredux weather; GPU (prefilter mips + SH): cargo test -p byroredux-renderer gpu_filter_preserves_constant_radiance --lib -- --ignored

  • Bake: every frame before the geometry pass, from the same CompositeParams as the background; ready flag exterior_sky_tint.w = pipeline presence. Guard: every_sky_cube_consumer_gates_on_the_ready_flag.
  • Prefilter/SH: 8 mips, 256 GGX samples per texel; 9-coefficient SH (SKY_SH_COEFFICIENTS, set 1 binding 21; workgroup widths and the SH count come from the generated header since #5620, fff5ed9a9) replaces the ambient fallback and is deliberately unoccluded, each consumer owns its occlusion (sky_visibility_pin).
  • Clouds: one cloud_march for background and bake (composite_does_not_carry_its_own_copy_of_the_sky, forbidden set derived from the live sky.glsl + clouds.glsl declarations and scanning sky_cube.comp too since #5180); height from radial altitude (the_cloud_density_profile_uses_radial_altitude); adaptive bounded march (the_cloud_march_steps_adaptively); each WTHR layer gets the mip its own tile scale needs (#4230: every_cloud_layer_samples_with_its_own_lod); sun term = compute_directional_upload illuminance.
  • Weather -> sun: translate_weather carries HNAM sunlight_dimmer, weather_system multiplies the sampled sun colour (steady state + cross-fade, #4481). Guards: sunlight_dimmer_translates_from_the_hnam_block, consumer_multiplies_sunlight_and_grass_dimmers_without_a_transition, promotion_carries_the_dimmers_and_the_steady_state_uses_them. The froxel medium's sun is sun_illuminance on both arms of volumetric_sun (#4909, overcast_dims_the_portal_sun); the cross-fade blends the two climates' TNAM breakpoints for the sun arc (#4926, sun_arc_eases_between_the_two_climates_arcs) and publishes them as SkyParamsRes::tod_hours for water/time (#5179, the_published_breakpoints_ease_with_the_crossfade). Direct-sun exp(-2.5*coverage): render/directional_upload_tests.rs::exterior_cloud_coverage_attenuates_the_direct_key. TOD / sun arc / cross-fade / DALC easing: systems/weather.rs tests.
  • Clock + interior: GameTimeRes is re-installed only by ensure_game_time (insert-if-absent). Interior SkyParams keep the cell's own fields; the outdoor sky rides separate lanes only: exterior_zenith_color (-> GpuCamera::exterior_sky_tint, window-portal escape, #3323), portal_outdoor_sky (bake + background through openings), portal_sun_* (volumetric apertures, #4839), interior_show_sky. Never zenith_color (#2226): render/sky.rs::stale_exterior_sky_params_res_does_not_leak_into_interior. Night detail gates on the drawn palette (sky_lower.w), not the room's weather flag (#4908, night_sky_details_follow_the_outdoor_palette_not_the_weather_flag); pack_sky_dome is the one SkyDome packer for composite and interior cube (#4925, interior_sky_cube_packs_the_outdoor_palette_like_an_exterior_composite); the lane is specified in skyal.md § interior outdoor-sky lane (#4927).
  • Sun disc / sunset: sky_radiance floors the disc luminance at 1.8x the local sky behind it (hue-preserving, never attenuates; c60405083); build_tod_keys carries a (sunset_end, TOD_SUNSET) hold key so the palette stays SUNSET through the sun's whole descent (56786698b, tod_keys_hold_sunset_through_the_suns_descent). Both shape golden-hour captures and are recorded in skyal.md (#5425, 4e32d33ec); the disc floor is a literal in sky.glsl that composite/sky_cube.comp share.
  • Documented, do not re-file (skyal.md §2.3-§3, 2026-09-14/18): cloud-type taxonomy, noise frequencies, atmosphere LUT, positional cloud shadows are open; coverage constants and multi-scatter a=b=c=0.5 are engine choices.

Dimension 5: Water translation (WATAL)

Paths: byroredux/src/env_translate/water.rs (water arm), byroredux/src/cell_loader/water.rs, byroredux/src/streaming/mod.rs (spawn_lod_water / refresh_lod_water), byroredux/src/material_translate.rs (attach_mesh_water, water_volume_from_phantom), crates/core/src/ecs/components/water.rs, byroredux/src/systems/water.rs, byroredux/src/commands/water.rs
First step: cargo test -p byroredux water

  • Two composition sites: ESM WATR/XCWT -> resolve_water_material; NIF water shader -> attach_mesh_water -> water_material_from_mesh. Any other production WaterMaterial { .. } outside #[cfg(test)] is a third.
  • Frames: WATR noise-layer angles go through watr_angle_to_engine_xz (#4727), scoped per game since 0d113b35a (#4910): Skyrim/FO4 convert (FO76 tentatively), Oblivion and FO3/FNV/Starfield read un-rotated (frames OPEN in watal.md); guard wind_angle_conversion_is_scoped_per_game. NAM0-filled layers skip it (#4900). Scroll: compose_flow_scrolls is the one composer for the WATR arm and the REFR XWCU merge (#4911, xwcu_current_recomposes_the_pattern_scroll; calm WATR promoted under an authored current, #5183); flowing kinds damp weather transport to FLOWING_WATER_WEATHER_TRANSPORT and the Rapids third layer reads the baked scroll, not BU/s (#4929, 37cc637e9, flowing_kinds_damp_the_weather_transport_but_calm_keeps_it).
  • SENTINEL vs AUTHORED per game is watal.md §4's table. Guard: resolve_water_material_sentinels_are_game_invariant (sweeps every GameKind since #5184). Byte offsets (Oblivion DATA shift, Skyrim DNAM tail) are decode -> /audit-esm; check promotion: each decoded field reaches WaterMaterial / WaterFlow or is open in watal.md §2. WaterNormalEncoding is set in the parser (FO3/FNV = OffsetNoise) and honoured on every normal layer in water.frag (source guard in vulkan/water.rs).
  • Heights: Oblivion Z=0 when NAM2 resolves (a child takes its parent's, #5374), other games WRLD DNAM; XCLW tri-state (an_absent_xclw_stays_absent); LOD water NAM3/NAM4 is render-only (lod_water_is_render_only_and_cannot_create_false_submersion); distant water is one quad per cell at that cell's effective XCLW height (dry skipped, LAND-occluded dropped, #5243), its holes only where cells are actually resident, not a radius_unload disc (#5244/#5334, e38ff49d2, distant_water_holes_only_actual_residency; the plane rebuilds on residency change, coalesced to 250 ms, from a spawn-time cell projection, #5289; a failed or emptied rebuild records its attempt, #5339). The mesh's height and form are one boundary rule, resolve_distant_water (env_translate/lod.rs): inherit height = the full-detail default (default_water_for_worldspace, not NAM4, #5337), form = NAM3 else the full-detail NAM2, which is how Oblivion gets distant water at all (#5335; oblivion_gets_distant_water_from_the_full_detail_default).
  • Mesh water depth: authored bhkSimpleShapePhantom bounds beat the named 4 x radius heuristic (authored_phantom_volume_replaces_the_radius_heuristic). Currents: WATR NAM0 velocity, REFR XWCU entry 0 overrides (reference_current_overrides_the_watr_current), CELL XWCU is not a velocity; flow bounded (flow_speed_ladder_is_ordered_and_bounded). The wave tuning (WATER_WAVE_*) and the scroll prep (WaterMaterial::wave_scrolls) live once in crates/core/src/ecs/components/water.rs (#5582, 7cf2bd086): water.vert (via the generated header), the render upload and the CPU crest sampler all read them, so a retyped literal in any of the three is a finding; the flowing-kind weather damping is the one WaterKind::weather_scroll_for for renderer, camera submersion, buoyancy and the player (#5354, cff1f9c42).
  • Hazard-water damage has one owner, water_damage_system (#5548, c28571563); splash/ripple events land on the contact's published waterline (#5562). Physics stays game-invariant (grep -rn GameKind crates/physics/src is empty). Open (watal.md): water-walking, freezing, Skyrim DNAM tail, FO76/Starfield scope.

Dimension 6: Distant LOD and trees

Paths: byroredux/src/cell_loader/{terrain_lod,terrain_lod_btr,object_lod,placement_lod,lod_bands,lod_support,lod_coverage,legacy_lod_index}.rs, byroredux/src/streaming_helpers.rs, crates/bsa/examples/probe_lod_corpus.rs
First step: cargo test -p byroredux lod; with game data, cargo run -p byroredux-bsa --example probe_lod_corpus (counts all three LOD families).

  • One partitioning ring: select_lod_quads descends a 4/8/16/32 quadtree so a quad emits itself or recurses, never both. Objects use coarsen_to_available (#3502); terrain keeps descending (synthesis always covers). Guard: partition_never_covers_a_cell_twice, missing_baked_asset_subdivides_instead_of_holing, coarsened_selection_is_still_a_partition.
  • Schemes are data tables: terrain_lod_layout (Oblivion FormID-keyed, FO3/FNV editor-ID quadtree, Skyrim+FO4 .btr, FO76/Starfield none); object_lod_scheme (BakedBto Skyrim+FO4+FO76 (#4488), FalloutLegacyBlocks FO3/FNV, Starfield none; Oblivion DistantLOD\*.lod -> _far.nif via placement_lod_supported). FO3/FNV ship zero distantlod\*.lod, but "FO3/FNV have no distant-object LOD" is a false premise (#3321). Regression guard #5222 (b7987d813): FO3/FNV quads sit on per-worldspace lattices (26 of 29 FO3 worldspace/level sets are off (0,0), no single origin explains them), so the legacy object + terrain rings no longer derive an origin: LegacyLodQuadIndex (cell_loader/legacy_lod_index.rs, scanned once per streaming state from the archive name tables) feeds select_authored_lod_quads (lod_bands.rs; ring touch by footprint, too-near yields to finer authored levels, seam suppression, terrain completes uncovered ring ground with level-4 synth tiles). worldspace_lod_grid_origin still returns (0,0) for non-combined games and only serves the synth/placement grids. Pins: authored_selection_reaches_off_lattice_quads, authored_level_8_only_selection_fills_its_near_band, authored_selection_partitions_between_levels plus --ignored real-data tests in legacy_lod_index_tests.rs; live FO3/FNV visual verification is still owed. Before selection, prune_same_level_overlaps keeps each level's majority lattice-residue class, dropping leftover-generation quads that overlap on one level (six FO3 worldspaces; FNV and both terrain tables pass through; #5387, fbf1bed74). Guards: same_level_leftover_generation_quads_are_pruned, and the --ignored washmontop_level8_spans_multiple_residues_and_all_select no-overlap pin. Count ties go to the class covering more of the LOD source worldspace's authored exterior cells (lod_source_cell_exists), only a coverage tie falling back to residue order (#5520, a9577fd69, residue_count_tie_breaks_on_authored_cell_coverage); which lattice the shipped game loads stays open (exal.md). Both legacy rings and the index scan gate on the scheme tables (TerrainLodLayout::FalloutLegacy / ObjectLodScheme::FalloutLegacyBlocks), not GameKind (#5423, b3e679dba); the scan fires when EITHER table selects the legacy family (should_scan_legacy_lod_quads in streaming_helpers.rs, #5518, 5a549e664, legacy_lod_scan_gate_keys_on_either_family_table) and the object ring warns when the index is missing. A PNAM 0x02 child owns no quads: every distant-ring key (index, archive paths, .btr, generated textures, band bounds) resolves on lod_worldspace_key() while full-detail residency stays on the child (#5496).
  • .btr is quad-local (btr_local_to_world), .bto world-absolute; a block is .btr or synthesized, never both (z-fight). lod_coverage.rs: overlap > 0 = double draw. Legacy (Oblivion/FO3/FNV) quad textures carry their image orientation from the boundary (TranslatedTerrainLodTexture, #4898); .btr and legacy quads sample with the authored CLAMP (#4912, 235a90ba2, btr_terrain_lod_resolve_uses_the_authored_clamp); only the tiled base-LTEX fallback stays WRAP by design.
  • VWD: flag parsed (#1731) -> stamp_visible_when_distant -> resident_vwd_refr_cells (locks hoisted, #3142). Open (2026-09-29): full-model culling #3307. LOD entities carry IsLodTerrain, no BLAS, materials via the Dim 1 boundary.
  • Trees: near-field geometry is design-only (exal-trees.md re-scoped 2026-09-07, #3808: .spt carries no geometry); vanilla Skyrim .bto bakes no trees: the tree tier is the .btt + .lst + treelod-atlas family, registered (tree_lod_supported / tree_lod_archive_path; layout census-verified 62a31d6de) but unconsumed, so distant Skyrim forests render empty (known-open #4913). .btt records carry REFR FormIDs (matters for #3307).

Dimension 7: Acceptance gates and harness

Paths: scripts/renderer-eval-groundcover.sh, docs/smoke-tests/{m-exteriors,w1-water-traversal,m34-day-night}.sh
First step: read docs/smoke-tests/README.md (--bench-hold -> byro-dbg attach).

  • Unit guards cannot see GPU output; a sky/cloud/ground-cover/water claim needs a captured frame (skyal.md §4: fixed camera, --render-debug-mode composite_term separates sky assembly from bloom/tonemap, capture twice for the noise floor).
  • m-exteriors.sh <game> static|boundary|soak|cycle|water = cross-game readiness matrix (incl. WATR provenance, waterline delta); w1-water-traversal.sh = WATAL traversal; m34-day-night.sh = Skyrim sun response. All need Vulkan + game data.
  • renderer-eval-groundcover.sh enforces the washout precheck (#4482, groundcover_reference_capture_washout): luminance sd under 0.039 (env override BYROREDUX_GC_WASHOUT_SD) fails a gc-backlit-* frame (the only ground-framing poses), WARNs frontlit ones; missing telemetry fails (#4509). The committed backlit references (sd ~0.01, 2026-09-20) are the veil: confirm a re-mint before trusting an A/B. 3bcf6c8e8 (#5482, renderer-owned) moved exposure ahead of the contrast grade and added a shadow toe for every game, so frames from before 2026-10-09 (those references, and the 0.039 line's calibration) are not A/B-comparable with new ones.
  • --render-debug-mode water_term / water_refl paint non-water pixels a 0.08 grey, so their frame mean is not water radiance (#5482 first misread a grade crush as zero-radiance Skyrim LOD water this way); composite_term shows the pre-grade scene.
  • "Chrome" surfaces: tex.missing first (feedback_chrome_means_missing_textures). The harnesses run scripts/check-shader-artifacts.sh before capture (#4738); a rebuilt .spv may still not relink (feedback_spv_rebuild_staleness).

Phase 3: Merge and Report

  1. Write docs/audits/AUDIT_EXTERIOR_<TODAY>.md: Executive Summary (findings by severity; verdict per tier invariant); Per-Category Matrix (terrain / sky / weather-sun / water / ground cover / LOD x the four invariants, boundary fn cited); Findings; Known-Open Register (dated items above, with what this pass changed).
  2. Dedup per _audit-common.md § Deduplication (/tmp/audit/issues.json, prior docs/audits/AUDIT_*).
  3. rm -rf /tmp/audit/exterior; tell the user the report is ready and suggest /audit-publish docs/audits/AUDIT_EXTERIOR_<TODAY>.md (labels: terrain-exterior, plus water, shaders, doc-rot; game:* if title-specific).