Notre avis
Audit approfondi de la correction d'un moteur de rendu Vulkan, couvrant le ray tracing, l'indexation des buffers, les structures GPU, la synchronisation, la mémoire et les passes de shaders, en s'appuyant sur des docs de référence et une discipline de vérification sans conjecture.
Points forts
- Couverture complète des dimensions à haut risque du moteur, des structures d'accélération aux passes de denoiseur.
- Orchestration de jusqu'à trois agents concurrents par dimension pour la productivité.
- Vérification stricte : symboles plutôt que numéros de ligne, affirmations confirmées par grep, docs autoritatives comme référence.
- Reformulation des problèmes résolus en gardes de régression pour éviter la dérive.
Limites
- Lié au dépôt ByroRedux et à ses docs internes (shader-pipeline, memory-budget) et au suivi de tickets.
- Évite volontairement de proposer des changements Vulkan non validables par cargo test, laissant certains problèmes de rendu non vérifiés.
- Nécessite un token GitHub valide et l'accès au dépôt pour la déduplication des tickets.
À utiliser lors d'un audit de correction d'un moteur Vulkan et de sa conformité à l'architecture documentée, notamment avant une sortie ou après des refactorisations.
Ne pas utiliser pour une revue de code générale, pour des projets hors de cette structure de code spécifique, ou lorsque des propositions correctives Vulkan concrètes sont attendues.
Analyse de sécurité
SûrThe skill outlines a read-only audit workflow that inspects the renderer codebase, checks documentation, and runs grep/GitHub CLI queries—none of which are destructive or data-exfiltrating. No obfuscated or externally-sourced payloads, no commands that could compromise the system.
Aucun point d'attention détecté
Exemples
Audit the Vulkan renderer with --focus 1,2,3 --depth deep, prioritizing acceleration structures, SSBO indexing, and GPU-struct layout.Run a renderer audit with --focus 4,5 --depth shallow to check synchronization and memory patterns.Perform a deep renderer audit focusing on ray tracing and denoiser correctness: --focus 1,6,7 --depth deep.description: "Deep audit of the Vulkan renderer — pipeline, sync, memory, shaders, ray tracing, denoiser" argument-hint: "--focus <dimensions> --depth shallow|deep"
Renderer Audit
Audit the Vulkan renderer for correctness across the full pipeline: ray tracing (BLAS/TLAS, ray queries, shadows, reflections, GI, glass refraction), SSBO/UBO indexing, GPU-struct layout, synchronization, GPU memory, deferred indirect lighting (G-buffer, SVGF), denoiser/composite, and the per-feature passes (TAA, skinning, caustics, water, volumetrics, bloom, material table).
Architecture: Orchestrator. Each dimension runs as a Task agent (max 3 concurrent).
See .claude/commands/_audit-common.md for project layout, the Key Reference
Docs table, methodology, dedup, context rules, and the finding format.
See .claude/commands/_audit-severity.md for the severity scale — the
RT/SSBO/GPU-struct/denoiser rows there set the floors used below.
Do NOT restate the GPU-struct byte layouts, descriptor bindings, G-buffer
formats, or submission order here — docs/engine/shader-pipeline.md is the
authoritative, code-verified reference. docs/engine/memory-budget.md is
authoritative for VRAM/RAM ceilings, LRU thresholds, and deferred-destroy depth.
Audit against those docs; if the code diverges from the doc, that divergence is
itself a finding (or the doc is stale — note which).
Verification discipline (No-Guessing)
- Symbols, not line numbers. Anchor every finding on a symbol
(
fn/struct/const/test name), notfile:NN— line anchors rot on every refactor. Confirm bygrep; if a claim is unconfirmable, drop it. - Backticked
.extpaths must resolve now (the_audit-validate.shgate). note thatbyroredux/src/render/is a directory, not arender.rsfile (it split out post-#1115);scene.rs/systems.rs/cell_loader.rsare likewise thin dispatchers over sibling dirs. - Recast resolved issues as regression guards — phrase as "verify X still holds / hasn't drifted", not "X is broken".
- No speculative Vulkan changes. Per the user's standing guidance, do NOT
propose render-pass / pipeline / barrier edits whose failure modes are
invisible to
cargo test. Frame such findings as "needs RenderDoc verification" and stop at the observation. - Bench numbers rot. Do not hard-code FPS/ms; cite ROADMAP.md Bench-of-record (currently flagged stale — R6a-stale-15 gates any FPS claim).
Parameters (from $ARGUMENTS)
--focus <dimensions>: Comma-separated dimension numbers (e.g.,1,3,7). Default: all.--depth shallow|deep:shallow= check patterns only;deep= trace data flow and validate invariants. Default:deep.
Extra Per-Finding Fields
- Dimension: AS Correctness | SSBO/Indexing | GPU-Struct Layout | Sync/Barriers | Memory/Lifecycle | Ray Queries | Denoiser/Composite | TAA | Skinning | Camera-Relative Precision | NIFAL Material | Material Table | Caustics | Water | Volumetrics | Bloom | Disney BSDF | Soft Shadows | Sky/Weather | Tangent-Space | Pipeline/RenderPass | Debug/Telemetry | Cornell Harness | Light Animation
Phase 1: Setup
- Parse
$ARGUMENTSfor--focus,--depth. mkdir -p /tmp/audit/renderer.- Dedup baseline:
gh issue list --repo matiaszanolli/ByroRedux --limit 200 --json number,title,state,labels > /tmp/audit/renderer/issues.json. - Scan
docs/audits/for prior renderer reports. - Read
docs/engine/shader-pipeline.md+docs/engine/memory-budget.mdfirst — they pin almost everything below.
Phase 2: Launch Dimension Agents
Dimensions are ordered by renderer risk: AS/SSBO indexing and GPU-struct layout corrupt every frame silently (CRITICAL/HIGH floors); sync and memory leaks compound; denoiser/shader correctness is mostly visual.
CRITICAL tier — silent whole-frame corruption
Dimension 1: Acceleration Structures (BLAS/TLAS correctness)
Entry points: crates/renderer/src/vulkan/acceleration/ (blas_static.rs, blas_skinned.rs, tlas.rs, predicates.rs, constants.rs, types.rs), crates/renderer/src/vulkan/context/resources.rs (build_blas_for_mesh).
Severity floor: wrong geometry/address in AS = CRITICAL; missing build→read barrier = HIGH.
Checklist:
- BLAS build geometry: vertex format
R32G32B32_SFLOATat offset 0, index typeUINT32,OPAQUEflag, correct prefer-trace/build flags per buffer class. - Build-flag constants are stable:
STATIC_BLAS_FLAGS(FAST_TRACE | ALLOW_COMPACTION),SKINNED_BLAS_FLAGS(FAST_BUILD | ALLOW_UPDATE— deliberate, see memory-budget.md),UPDATABLE_AS_FLAGS(FAST_TRACE | ALLOW_UPDATE). Pinned by tests inacceleration/; drift = Vulkan-version-rev breakage (regression guard, #1144/#1196). BlasEntry.built_flagsrecords BUILD-time flags; refit must assert the same set (VUID-03667). Mismatch surfaces as validation, not silent corruption (regression guard, #1145).instance_custom_indexencoding == draw-command index used for SSBO lookup — this is the load-bearing AS/SSBO contract (CRITICAL). It is a 24-bit field;MAX_INSTANCES = 0x40000stays under1 << 24, pinned by the const-assert inscene_buffer/constants.rs.- TLAS build/update decision keys on the
last_blas_addressesdevice-address sequence only. UPDATE mode requires matching geometry + instance count — verify padded/unused instance slots don't break it. - Transform: column-major
mat4→ 3×4 row-majorVkTransformMatrixKHR.TRIANGLE_FACING_CULL_DISABLEon all instances (two-sided meshes). - Empty TLAS valid from frame 0 (no validation errors before any geometry).
- Device-address queries require
SHADER_DEVICE_ADDRESSusage on the source buffer. - LRU/shrink wiring (regression guards):
shrink_tlas_scratch_to_fituses TLAS-calibrated slack matchingtlas_instance_should_shrink(predicates.rs), called at the END OFdraw_frame(context/draw.rs), NOT at cell-unload (#1226 / REN-LOW L-2 — cell-unload calls the differentshrink_blas_scratch_to_fit; matchesdocs/engine/memory-budget.md); the threemissing_blascause-counters (skinned/rigid/ssbo_evicted) all increment and surface only through the rate-limited (once/sec)log::warn!inbuild_tlas_instances— there is NOmem.statscommand, and no registered command reads them (#1228 / REN-LOW L-1); post-TLASrt_flagpatch indraw_framekeeps cell-load frames from rendering RT-disabled (#1227). Two known, documented-not-fixed correctness gaps live here (#1793): a permanently-missing rigid BLAS has no recovery path (no per-frame build primitive exists), and a synchronous multi-cell burst (--grid) can false-evict a not-yet-drawn entry via the sharedframe_counterbump — both gated behindstatic_blas_bytes > budget, unreachable on the 12 GB dev card. Recast, don't re-report as new. - Deferred BLAS destruction (regression guard, #a476b256).
drop_blas/evict_unused_blas(blas_static.rs) and the skinned drop (blas_skinned.rs) push theVkAccelerationStructureKHR+ backing buffers ontopending_destroy_blas(deferred,DEFAULT_COUNTDOWNframes) instead of destroying immediately — an eviction or unload must not free an AS the in-flight frame's ray queries still read (use-after-free → CRITICAL). The shutdown path drainspending_destroy_blassynchronously. Regression = a re-introduced immediatedestroy_acceleration_structureat the eviction/drop site. Output:/tmp/audit/renderer/dim_1.md
Dimension 2: SSBO/Index plumbing & RT ray queries (shader)
Entry points: crates/renderer/shaders/triangle.frag + its #included crates/renderer/shaders/include/raytrace.glsl / include/lighting.glsl (all rayQueryEXT), crates/renderer/shaders/water.frag.
Severity floor: SSBO index mismatch = CRITICAL; ray self-intersection / wrong tMin = HIGH.
Checklist:
instance_custom_index(NOTgl_InstanceID) indexesGpuInstance[];materials[instance.material_id], vertex/index SSBOs (Set 1 bindings 8/9 per shader-pipeline.md) use the same offsets the Rust upload writes.- Shadow rays: origin = surface world pos with normal/tMin bias, direction toward light,
TerminateOnFirstHit,CommittedIntersectionNone→ 0/1. Disk/cone jitter geometry correct (point/spot concentric disk, directional angular cone). - Reflection rays: normal-biased origin,
reflect(viewDir, N)sign, metalness/roughness gate consistent with PBR intent, barycentric UV interp from vertex SSBO, descriptor-valid texture lookup. - 1-bounce GI: cosine-weighted hemisphere with correct tangent-basis, distance cutoff, miss → sky/ambient fill with no NaN/inf.
- Glass / IOR refraction:
- Roughness-spread basis via Frisvad orthonormal basis (not
cross(N, up), which degenerates vertical) — verify tangent/bitangent unit length (#820). - Window-portal demote on coincident glass to break the IOR self-passthrough infinite loop (#789).
GLASS_RAY_BUDGET(fromshader_constants.glsl) cap wired; the budgetatomicAddovershoots unconditionally by design (#1438) — that's documented, not a bug; verify the doc comment is intact and no CPU reads the counter.- Interior miss falls back to cell-ambient, not open-sky tint (no daylight leak in dungeons).
DBG_VIZ_GLASS_PASSTHRUviz still wired at the diagnostic-state setup + the two refraction-loop viz-write branches.- Thin-glass gate (regression guard, #883f57cd).
MAT_FLAG_THIN_GLASS(bit 11) forces non-occluding glass (open window panes, display-case fronts) onto the zero-ray Fresnel/framebuffer-transmission path:glassIORAllowed = isGlass && !isThinGlass && rtEnabled && !isWindow && rtLOD < RT_LOD_IOR. Occluding/thick glass (bottles, canopies) keeps the full Snell/RT path. BGEM classification pinned bybgem_uses_thin_glass_behavior/closed_bgem_glass_does_not_select_thin_surface_behavior/legacy_bgem_effect_cards_do_not_become_glass(asset_provider/tests.rs) — thin only for non-occluding transmissive shells, never plain closed BGEM glass or effect cards.
- Roughness-spread basis via Frisvad orthonormal basis (not
- RT gating:
sceneFlags.x > 0.5checked before every ray query; TLAS binding is the correct descriptor (Set 1, Binding 2). - Interleaved-gradient noise seeded by frame counter — deterministic per-pixel-per-frame so TAA can converge (no true RNG).
- ReSTIR-DI spatial reuse (regression guard, #d523b9b3). The shadow-reservoir spatial pass in
triangle.fragrejects a neighbour reservoir on a 25° geometric-normal cone (SPATIAL_NORMAL_COS = 0.906, Bitterli 2020 §5) BEFORE combining — the neighbour's geometric normal is octEncode→packSnorm2x16-packed into the reservoirpad0at write time (no normal-history texture, reservoir stays 32 B). It uses the GEOMETRIC normal (fragNormalEffective), not the normal-mapped shading N (so the cone doesn't over-reject on bumpy detail). Gated byDBG_DISABLE_SPATIALfor A/B; stale/uninit reservoirs decode to a degenerate normal the gate rejects. Regression: dropping the normal-cone test (re-opens cross-corner shadow bleed), packing the shading N, or growing the reservoir struct. - ReSTIR-DI surface-identity tag now uses the stable surface ID (regression guard, #883f57cd). The reservoir's surface tag is
uint surfaceId = inst.surfaceId & RESERVOIR_SURFACE_MASK(mask0x3FFFFF), replacing the oldfragInstanceIndex + 1— so spatial-reuse validity survives per-frame draw-order/batch reordering instead of going stale whenever the draw list re-sorts. Test:restir_history_uses_stable_surface_id_not_instance_order(gpu_instance_layout_tests.rs). - BC1 punch-through alpha (regression guard, #ae285062): a pure-blend mesh whose BC1/DXT1 diffuse decodes index-3 texels as
a==0(an RGB-fidelity encoder choice, NOT transparency) must NOT leak into blend-discard / decalWeight / finalAlpha.triangle.fragpinstexColor.a = 1.0whenINSTANCE_FLAG_DIFFUSE_ALPHA(bit 8) is clear and no alpha test is active. The CPU bit is set indraw.rsfromformat_has_alpha(which excludesBC1_RGBA). Regression = a BC1-blend mesh speckling/pinholing again. Output:/tmp/audit/renderer/dim_2.md
Dimension 3: GPU-struct layout (lockstep with shaders)
Entry points: crates/renderer/src/vulkan/scene_buffer/gpu_types.rs, scene_buffer/constants.rs, crates/renderer/src/vulkan/material.rs, the layout-pin tests in scene_buffer/ (gpu_instance_layout_tests.rs, material_hash_tests.rs, instance_hash_tests.rs) and material.rs.
Severity floor: #[repr(C)] GPU struct drifting from its shader struct = HIGH (silent per-instance/per-material corruption).
Checklist:
- Sizes pinned by tests — confirm they hold and match shader-pipeline.md:
GpuInstance= 128 B (gpu_instance_is_128_bytes_std430_compatible; grew 112→128 with theskinned_vertex_address+_reservedvec4 slot, #2219),GpuCamera= 336 B (gpu_camera_is_336_bytes— grew 320→336 with therender_originvec4, #markarth-precision / #1492),GpuMaterial= 348 B (gpu_material_size_is_348_bytes; NOTE the size grew 260→…→300 via #804/#1249/#1250, then 300→348 on 2026-07-27 (1d94eb24) when the twelve common supplemental texture roles landed — the_348_test name is current;_300_and_260_are both gone). The role indices are the newest and largest single growth in this struct's history: re-check the GLSL mirror incrates/renderer/shaders/include/bindings.glslfield-by-field, not just the size. GpuInstance.surface_id(u32, offset 108, regression guard #883f57cd) repurposes the old_pad_albedopadding into a stable per-entity surface identity (draw_cmd.entity_id.wrapping_add(1)— 0 reserved for background/synthetic), used by TAA/SVGF disocclusion and ReSTIR-DI reuse to survive per-frame draw-order reshuffling. Size is 128 B since #2219. Pinned bygpu_instance_field_offsets_match_shader_contract(asserts offset 108, plusskinned_vertex_addressat 112 and_reservedat 120) plusrestir_history_uses_stable_surface_id_not_instance_order/gbuffer_history_uses_stable_surface_id_but_caustics_keep_draw_lookup.- Per-field offset pins:
gpu_material_field_offsets_match_shader_contractasserts every named field offset across all vec4 slots (#806) — a size-only pin can't catch within-vec4 reorders. Any added field needs a matching offset assertion plus updates to the Rust struct AND the GLSLstruct GpuMaterial(only declared incrates/renderer/shaders/include/bindings.glsl,#included bytriangle.frag). - All
GpuMaterialfields are scalar f32/u32 — never[f32;3](std430 vec3 alignment would desync the byte-hash dedup). Named pad fields explicitly zeroed (no uninit bytes feeding Hash/Eq). struct GpuInstanceis declared once incrates/renderer/shaders/include/bindings.glsl(pulled intotriangle.fragvia#include) and hand-mirrored in 4 standalone shaders — verify lockstep viagrep -rl "struct GpuInstance" crates/renderer/shaders/→include/bindings.glsl,triangle.vert,ui.vert,water.vert,caustic_splat.comp(5 declaration sites;ui.vert/water.vertreading wrong offsets is the recurring trap, #785/#1498). Every mirror must carry thesurfaceIdfield (renamed from the old albedo pad, #883f57cd) —grep -L surfaceIdacross the 5 sites should return nothing. Perfeedback_shader_struct_sync.md.- Flag constants are the single source of truth in
crates/renderer/src/shader_constants_data.rs, emitted intocrates/renderer/shaders/include/shader_constants.glsland#included — never hand-written shader-side:INSTANCE_FLAG_*(NON_UNIFORM_SCALEbit 0,ALPHA_BLENDbit 1,CAUSTIC_SOURCEbit 2,TERRAIN_SPLATbit 3,FLAT_SHADINGbit 7,DIFFUSE_ALPHAbit 8 — the BC1 punch-through gate, #ae285062),MATERIAL_KIND_*,MAT_FLAG_*(bits 0–9, includingPBR_BSDFbit 5,TRANSLUCENCYbit 6,MODEL_SPACE_NORMALSbit 7,TRANSLUCENCY_THICK_OBJECTbit 8,TRANSLUCENCY_MIX_ALBEDObit 9 — all canonical post-#1357, theBGSM_*prefix is gone; plusTHIN_GLASSbit 11 — bit 10 unused/reserved — gating occluding-vs-non-occluding glass, #883f57cd), and theDBG_*bits (currently 24,0x1…0x800000, value-pinned by the shared catalog,8eaade44— the count grew with the Session-49 ReSTIR/SVGF/FSR additions; readDBG_BITSrather than trusting any figure quoted here). - Capacity constants match memory-budget.md:
MAX_INSTANCES = 0x40000,MAX_MATERIALS = 16384(scene_buffer/constants.rs),MAX_INDIRECT_DRAWS = MAX_INSTANCES. Over-cap material intern returns id 0 + one-shotwarn!(no SSBO-index corruption, #797); upload truncates tomin(intern_count, MAX_MATERIALS). Output:/tmp/audit/renderer/dim_3.md
HIGH tier — sync, memory, leaks, denoiser correctness
Dimension 4: Synchronization & barriers
Entry points: crates/renderer/src/vulkan/context/draw.rs (draw_frame), sync.rs, context/resize.rs. Cross-check the submission order in shader-pipeline.md.
Checklist (flag invisible-failure-mode items as needs RenderDoc):
- Semaphore/fence lifecycle: signal-before-wait, no double-signal, per-frame fence waited before command-buffer reuse,
images_in_flighttracking. render_finishedis per-swapchain-image, indexed byimage_index(not per-frame) — per-frame signalling fires VUID-vkQueueSubmit-pSignalSemaphores-00067 whenMAX_FRAMES_IN_FLIGHT> swapchain image count (regression guard,548c1b69).- AS build → fragment read barrier (
AS_WRITE → AS_READ, build stage → fragment stage); skin compute write → BLAS refit → fragment read chain (Dim 9). - AS-build INPUT barrier access flag (regression guard, #507945d8). Vertex/index/instance build inputs are read with
SHADER_READat theACCELERATION_STRUCTURE_BUILDstage — NOTACCELERATION_STRUCTURE_READ_KHR(that flag is for reading an AS structure, not its build inputs). Applies to the instance-buffer-copy → TLAS-build barrier (tlas.rs) and the skinned-vertex compute-write → BLAS-build barrier (draw.rs). The wrong flag is a RAW hazard surfaced by sync-validation (~40 hazards/frame on--cornellpre-fix); turn validation on viaBYRO_VALIDATION(release) to confirm. - G-buffer attachment transitions between render pass and the compute consumers (SVGF/TAA/SSAO); caustic-accum atomic-add → SHADER_READ.
- egui render pass supplies its own incoming dependency after composite's outgoing
dstStage = NONE(explicit EXTERNAL dependency, #1433) — missing it is a WAR hazard on the swapchain image. - Swapchain recreate: all in-flight work waited and resources destroyed before rebuild.
Output:
/tmp/audit/renderer/dim_4.md
Dimension 5: GPU memory & resource lifecycle
Entry points: crates/renderer/src/vulkan/buffer.rs, allocator.rs, scene_buffer/, acceleration/memory.rs, crates/renderer/src/vulkan/context/mod.rs (Drop), context/resize.rs. Cross-check ceilings in memory-budget.md.
Severity floor: any per-frame leak = HIGH.
Checklist:
- gpu-allocator memory-type correctness (
CpuToGpuvsGpuOnly); buffers/images destroyed before allocator; allocator dropped before device; no leakedVkDeviceMemoryon shutdown. AllocatorResourceECS-ordering: must be removed from theWorldBEFOREVulkanContext::drop()— the allocator holds a liveArc<Device>; aWorldoutliving the context fires the allocator Drop against a destroyed device (use-after-free). Verify the drop/remove ordering inmain.rs, and that it survives a panic-unwind path (#1406). Allocator-independent destroys are hoisted out of the allocator-guarded Drop block (#1483).- BLAS scratch high-water-mark reuse never shrinks mid-life (verify no use-after-free); of the two shrink fns, only
shrink_blas_scratch_to_fitruns at cell-unload (cell_loader/unload.rs, pluscontext/resize.rs);shrink_tlas_to_fitandshrink_tlas_scratch_to_fitboth run at the end ofdraw_frame. Slack constants from memory-budget.md (REN-LOW L-2). - Deferred BLAS-scratch destruction (regression guard, #1782). The shared
blas_scratch_bufferretired on grow/shrink routes throughpending_destroy_scratch: DeferredDestroyQueue<GpuBuffer>(deferred, mirrorspending_destroy_blas) instead of an immediate free — the immediate-destroy at these cell-unload/streaming-worker sites was a GPU use-after-free (a just-submitted frame's skinned-BLAS refit/first-sight build may still read the old address). NOTE:build_skinned_blas_batched_on_cmd's own grow-destroy stays immediate by design (runs after that frame's own fence wait) — don't "fix" it to match. - TLAS resize calls
device_wait_idle()beforeallocator.free()of the old allocation (tlas.rs) — absence opens a use-after-destroy window under resize-while-build-in-flight (latent, #1390). - Vertex/index pool growth: soft cap
warn!, hard cap error (check_pool_growth);NifImportRegistryLRU cap (BYRO_NIF_CACHE_MAX, default 2048) bounds scene count. - Deferred-destroy countdown =
MAX_FRAMES_IN_FLIGHTframes, ticked after the in-flight fence wait (memory-budget.md); BGSM/failed-path caches half-evict on overflow (#1430). - Reverse-order teardown of all
VulkanContextfields; per-subsystemdestroy()forAccelerationManager,SvgfPipeline,GBuffer,CompositePipeline,Ssao,WaterCausticAccum,EguiPass,GpuPerFrameTimers,TextureRegistry; framebuffers before render pass, image views before swapchain, device last. Output:/tmp/audit/renderer/dim_5.md
Dimension 6: NIFAL material canonical translation
Entry points: byroredux/src/material_translate.rs (translate_material — the single ImportedMesh → Material boundary), crates/core/src/ecs/components/material.rs (Material, resolve_pbr, EmissiveSource), the particle slice crates/nif/src/import/walk/mod.rs (extract_emitter_params/extract_emitter_rate) → byroredux/src/systems/particle.rs (apply_emitter_params) → byroredux/src/render/particles.rs (emit_particles). Spec: docs/engine/nifal.md. See also /audit-nifal.
Severity floor: wrong/divergent Material out of translate_material = HIGH (one boundary, all-game blast radius, no per-draw fallback to mask it).
Checklist:
- Single boundary:
translate_materialhas exactly two callers —byroredux/src/scene/nif_loader.rs(loose NIF) andbyroredux/src/cell_loader/spawn.rs(REFR placement). A thirdMaterial {…}literal downstream is a translation leak. Material::metalness/roughnessare plain resolvedf32(notOption);resolve_pbrruns once at translate (NaN-sentinel →classify_pbr_keyword, then clampmetalness 0..1,roughness 0.04..1), idempotent (resolve_pbr_is_idempotent). No per-frame re-classification anywhere.EmissiveSource(None/Material/Lighting/Effect) resolved at translate; the renderer reads the resolvedemissive_mult, not the raw per-game property (Effect = diffuse-tint multiplier conflated into emissive — drift mis-tints FO4+ glow).- No per-game branch between
MaterialandMaterialTable::intern— per-game quirks resolve here, never in the renderer (feedback_format_translation.md). - Particle slice: authored emitter rate/size override the preset but NOT color (
apply_emitter_params_overrides_kinematics_and_size_not_color); render assembly readsParticleEmitterpost-overlay. Output:/tmp/audit/renderer/dim_6.md
Dimension 7: Material table (R1 dedup)
Entry points: crates/renderer/src/vulkan/material.rs (MaterialTable::intern), scene_buffer/upload.rs, byroredux/src/render/mod.rs (build_render_data), byroredux/src/render/static_meshes.rs.
Upstream: the interned Materials come from Dim 6 (NIFAL) — a corrupt GpuMaterial may be a translate-side bug.
Checklist:
internproduces stablematerial_ids within a frame; identical materials collapse to one entry. Over-cap returns id 0 + warn-once, surfaced via thectx.scratchcommand (#7823eb59/#797 — NOTmem.stats/mem, neither of which exists; REN-LOW L-1/L-6). Per-frame SSBO sized tomin(intern_count, MAX_MATERIALS).- Hash/Eq treat
GpuMaterialas raw bytes (depends on the Dim-3 scalar-fields + zeroed-pad invariant). - Dedup-ratio telemetry surfaced (unique vs placement count) — a drop in hit-rate is a finding even if correctness holds (#780).
- Import-side scalars feeding the table (regression guards): BSLightingShaderProperty smoothness/IOR/specular into
MaterialInfofor the Disney lobe, BGSM smoothness normalized once (no double-apply, #1241); WaterShaderProperty + bare BSShaderProperty produce distinct entries (no dedup collapse with glass/opaque, #1243/#1244);HasModelSpaceNormalsrouted for direct-TXST REFRs (#972). - Identity invariant: N copies of one material render byte-identical pre/post dedup. No per-instance field remains in
GpuInstance/DrawCommandthat should now live inGpuMaterial(R1 Phase 6 closeout). - Particle color-fade quantization (regression guard, #1795).
emit_particles(byroredux/src/render/particles.rs) snaps the color LERP's fade parameter to 32 steps (quantize_fade,COLOR_FADE_STEPS) before hashing intoGpuMaterial— the size LERP stays continuous. A continuous fade defeatedmaterial_hashdedup (~97%→~1 material/particle). Regression = color read back off the rawtfraction, which reinflates per-particle material churn. Output:/tmp/audit/renderer/dim_7.md
Dimension 8: Denoiser & composite
Entry points: crates/renderer/src/vulkan/svgf.rs, composite.rs, crates/renderer/shaders/svgf_temporal.comp, composite.frag.
Severity floor: SVGF using wrong motion vectors = HIGH; ghosting / wrong tone-map order = MEDIUM.
Checklist:
- SVGF history ping-pong (read prev, write current); reprojection motion vectors match the vertex-shader output; mesh-ID disocclusion rejection prevents ghosting — for opaque draws the packed ID is now the stable surface ID (entity-based, survives per-frame draw reordering, #883f57cd) while alpha-blended draws still pack the current-frame instance index (caustic lookup needs live draw order, see Dim 11's mesh-ID bullet); blend α clamped, first-frame uses current (no garbage history); dispatch covers exactly the image (ceil division); per-frame history descriptor swap.
- Firefly rejection hoisted ahead of the
hasHistorybranch (regression guard,48906670) — verify the clamp applies on the no-history path too. - Composite reassembly: direct + SVGF-denoised indirect + albedo (+ TAA-resolved HDR when TAA on), ACES tone-map is NOT in
composite.frag— it lives inpresentation.frag(aces(), applied tograded * params.exposure), so composite emits linear HDR and bloom is added there, still upstream of the tone-map (Dim 16). Fog applied to direct only, not indirect. SSAO modulates indirect only. - Alpha-blend aux-MRT alpha lanes are no longer hardcoded to 1.0 (regression guard, #883f57cd).
triangle.fragnow writesauxiliaryAlpha = isAlphaBlend ? finalAlpha : 1.0into bothoutRawIndirect.aandoutAlbedo.a(effect/emissive early-outs and framebuffer-transmission/RT-terminus glass exits follow the same pattern) so the blend pipeline can preserve the opaque receiver's indirect/albedo when transmission is unresolved. Do not assume alpha≡1 on these MRTs for blended fragments when touching composite. - Caustic accumulator (
R32_UINT) sampled viausampler2D, divided by the fixed-point scale, added to direct (never the SVGF-denoised indirect — double-count guard, Dims 14/15). - Composite writes the offscreen HDR scene image (
HDR_FORMAT, final layoutSHADER_READ_ONLY_OPTIMAL), NOT the swapchain — the FSR upscale then thepresentation.rspass consume it, and presentation is what owns the swapchain attachment and itsPRESENT_SRC_KHRfinal layout (Dim 23). Output:/tmp/audit/renderer/dim_8.md
Dimension 9: GPU skinning compute + BLAS refit (M29)
Entry points: crates/renderer/src/vulkan/skin_compute.rs, crates/renderer/shaders/skin_vertices.comp, skin_palette.comp, acceleration/blas_skinned.rs, byroredux/src/render/skinned.rs, byroredux/src/render/bone_palette_overflow_tests.rs.
Checklist:
VERTEX_STRIDE_FLOATS = 26(104 B/vertex, since the[f32;4]tangent lane #783/M-NORMALS and thecd2b5fe4colourvec3→vec4widening) is defined incrates/renderer/src/shader_constants_data.rs, consumed byskin_compute.rsviause crate::shader_constants::VERTEX_STRIDE_FLOATS(NOT a hardcoded25), pinned againstsize_of::<Vertex>()by the assert inskin_compute.rs— drift corrupts every skinned vertex.skin_palette.comp(palette =bone_world × bind_inverse, GPU-side) pre-dispatches beforeskin_vertices.comp; both share a 64-wide workgroup; dispatch(vertex_count + 63) / 64.SkinPushConstants(vertex_offset/count, bone_offset) matches the GLSL push-constant struct, ≤ 128 B.- Skinned output buffer usage flags are
STORAGE_BUFFER+ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_KHR.VERTEX_BUFFERis deliberately ABSENT:b99ae91e("Fix #681 (MEM-2-6)") removed it because M29.3 raster still inline-skins intriangle.vertand nothing binds the slot buffer as a VBO. Correct as-is — re-add only alongside a Phase-3 raster bind path (REN-LOW L-13). - COMPUTE → AS-BUILD → FRAGMENT barrier scopes correct; refit (UPDATE mode) matches original BUILD geometry/vertex count; skinned BLAS pinned against LRU eviction while in flight; refit-count rebuild threshold per memory-budget.md. Scratch-serialize barrier dst mask (regression guard, #1790).
record_scratch_serialize_barrier(blas_skinned.rs) usesACCELERATION_STRUCTURE_WRITE_KHR | ACCELERATION_STRUCTURE_READ_KHR, not WRITE-only — an UPDATE-mode refit readssrcAccelerationStructure, and a first-sight frame's BUILD-then-refit in the same command buffer was an unmade-visible RAW hazard (confirmed by validation layer). A narrowed mask back to WRITE-only is the regression. - Bone-palette overflow guard fires (
Once-gated warn) at the cap — silent truncation pastMAX_TOTAL_BONESwas the M29 regression, pinned bybone_palette_overflow_tests.rs. Output:/tmp/audit/renderer/dim_9.md
Dimension 10: Camera-relative render origin & f32 precision (#1495/#1496)
Entry points: crates/renderer/src/vulkan/scene_buffer/gpu_types.rs (GpuCamera.render_origin), byroredux/src/render/camera.rs, crates/renderer/shaders/triangle.vert / triangle.frag, byroredux/src/cell_loader/references/mod.rs (RT_ABSOLUTE_PRECISION_CEILING). Spec: shader-pipeline.md "Coordinate Spaces & Precision".
Severity floor: a path mixing the two conventions = HIGH (large-world precision corruption).
Checklist:
- Two conventions, never mixed. Raster runs render-origin-relative (
viewProj × worldPos_relkeeps full f32 at large offsets); RT stays absolute (TLAS transforms, skinned BLAS, ray origins/lighting/fog reconstructed asworldPos_rel + render_origin). - Rigid
GpuInstance.modeltranslation rebased on CPU; skinned path rebases blended bone-palette translation by−render_originintriangle.vert(#1486). triangle.vertemitsfragWorldPosRel(location 3) relative;triangle.fragreconstructs absolute at top ofmain()(#1496) sodFdx/dFdyconsumers (flat-shading normal,perturbNormalTBN,parallaxDisplaceUV, rtLOD footprint) see small magnitudes. Verify no derivative consumer was moved back to the absolute varying.RT_ABSOLUTE_PRECISION_CEILING = 2^20 = 1_048_576—references.rsdebug_assert!s loaded-cell max|coord|stays under it viaworldspace_extent_over_rt_ceiling(unit-tested). Any new absolute-space shader consumer inherits this ceiling.- DoF: degenerate
focus_distguarded (#1525);GpuCameradoc accuracy (#1526). Output:/tmp/audit/renderer/dim_10.md
MEDIUM tier — per-feature passes, shader correctness, visual
Dimension 11: Pipeline state & render pass / G-buffer
Entry points: crates/renderer/src/vulkan/pipeline.rs, descriptors.rs, context/helpers.rs (create_render_pass), gbuffer.rs.
Severity floor: G-buffer format mismatch (shader output vs attachment) = HIGH.
Checklist (formats/bindings are in shader-pipeline.md — audit the match, don't restate the table):
- Vertex input matches
crates/renderer/src/vertex.rs(binding/location/format/offset); push-constant ranges match shader declarations; dynamic viewport/scissor (and dynamicCULL_MODEfor water two-sided) set each frame. - G-buffer pipeline writes all eight color attachments (locations 0–7: HDR, normal, motion, mesh_id, raw_indirect, albedo, FSR reactive mask, FSR transparency & composition mask; depth is attachment 8); composite inputs match G-buffer + denoiser outputs; SSAO/cluster-cull compute descriptor layouts correct.
- Mesh-ID encoding:
R32_UINT, bit 31 (0x80000000) =ALPHA_BLEND_NO_HISTORY(SVGF skip). Bits 0–30 changed meaning (not layout) under #883f57cd —gbuffer.rs::MESH_ID_FORMATdoc now reads "stable surface ID / alpha draw lookup": opaque draws packinst.surfaceId & 0x7FFFFFFF(stable across per-frame depth-sort/batch reordering), alpha-blended draws still pack the current-frame instance index + 1 (caustic source lookup needs live draw order, not a stable identity). Encoded shader-side intriangle.frag(meshIdBase = alphaBlendFrag ? sortedInstanceId : stableSurfaceId,outMeshID = meshIdBase | (alphaBlendFrag ? 0x80000000u : 0u)); runtime overrun is a one-shotwarn!+ clamp indraw_frame/upload_instances(NOT adebug_assert!— moved off the assert to avoid leaking the in-flight cmd buffer on unwind, #956/#992). - Render-pass load/store ops, layout transitions, subpass dependencies cover all stage/access masks; G-buffer images created
SAMPLED(SVGF/composite read). Flag barrier/dependency changes as needs-RenderDoc. - Pipeline cache: header pre-validated against the device before handoff; mismatch → warning + empty cache, no crash (
context/helpers.rs, SAFE-11/#91). Output:/tmp/audit/renderer/dim_11.md
Dimension 12: Command buffer recording
Entry points: crates/renderer/src/vulkan/context/draw.rs.
Checklist:
- Reset-before-record, begin/end balanced (command buffer + render pass), AS build recorded outside the render pass, SVGF/compute after RP end and before composite, then the fixed
record_post_passestail (svgf → caustic splat → volumetrics → taa → ssao → bloom → composite → FSR upscale → presentation), then egui, then optional screenshot copy. - Per-draw: depth bias for decals, pipeline/descriptor bind, push constants, indexed draw; batch coalescing groups draws by texture/descriptor.
- Counter independence (regression guards):
DrawCommandinput count vs post-batch GPU draw count are separate metrics, both surfaced (#1258); blend-pipeline cache-hit fast path exists at the per-draw bind site (#1259); off-frustum draws skipGpuInstance.flagsassembly without dropping state on frustum-border visible draws (#1260); cell-loader REFR spawn attaches a per-entitySceneFlagsfrom the NIF rootNiAVObject.flags(SceneFlags::from_nif(cached.root_flags)) for parity with the loose-NIF loader (#1235). - Two-sided blend split keys on material kind, not depth state (regression guard, #1804 →
883f57cd→ #2165).needs_two_sided_blend_split(draw.rs) isis_blend && b.two_sided && b.order_dependent_glass, whereorder_dependent_glassis set at batch formation fromis_refractive_glass. Both earlier spellings were wrong in opposite directions:&& b.z_write(#1804) excluded the FO4 BGEM glass that motivated the split (commonlyz_write: false), and dropping the limb entirely (883f57cd) re-included every two-sided blended particle batch, whose FRONT-cull pass rasterizes zero fragments and which then falls out of indirect grouping (#2165). Guarded bysplits_when_glass_and_z_write_false/splits_when_blended_two_sided_glass_and_z_writeplus the negative pairdoes_not_split_two_sided_blended_particles/does_not_split_non_glass_regardless_of_z_write(all indraw.rs; the old inverted-assertion name splits_when_z_write_false is gone) — a re-addedz_writeterm OR a droppedorder_dependent_glassterm is the regression. Output:/tmp/audit/renderer/dim_12.md
Dimension 13: TAA (M37.5)
Entry points: crates/renderer/src/vulkan/taa.rs, crates/renderer/shaders/taa.comp, Halton jitter assembly (halton fn + the (jx, jy) block in draw_frame) in crates/renderer/src/vulkan/context/draw.rs.
Checklist:
- Halton(2,3) jitter advances per frame (no seam), applied in NDC pixel units; un-jittered projection retained for motion-vector reconstruction.
- Per-frame-in-flight history slot (no aliasing); reprojection samples motion with linear/dilated filter (point causes edge wobble); 3×3 YCoCg neighborhood clamp on the history sample; mesh-ID disocclusion discards stale history; first-frame /
should_force_history_resetforces α = 1.0 with no garbage read. - History weight is a flat
alpha = 0.1(taa.comp) plus a per-pixel surface-consistency disocclusion test — octahedral-decodeddot(currNormal, prevNormal) < 0.85rejects history across a surface change. #1497'sstatic_frames-driven progressive alpha FLOOR was deleted bye5d02f83; that hazard cannot recur, so do not re-report it. Regression guard:taa_comp_keeps_history_bounded_and_rejects_unstable_surfaces(taa.rs). - History images in
GENERAL(no per-frame UNDEFINED);validate_set_layout(reflect.rs) fires and matches Rust bindings; composite samples TAA output only when TAA on; disable path skips the dispatch entirely. Output:/tmp/audit/renderer/dim_13.md
Dimension 14: Caustic splat (#321)
Entry points: crates/renderer/src/vulkan/caustic.rs, crates/renderer/shaders/caustic_splat.comp, composite consumption in composite.frag.
Checklist:
- Per-FIF
caustic_accum(R32_UINT,STORAGE|SAMPLED|TRANSFER_DST); cleared viavkCmdClearColorImagebefore dispatch; HOST→COMPUTE + CLEAR→COMPUTE barriers before dispatch; COMPUTE→FRAGMENT before composite sample; stays inGENERAL. - Accumulation via
imageAtomicAddon u32 fixed-point (no float race); fixed-point scale inCausticParamsmatches the composite divide. - Source-pixel selection reads the material flag from
materials[material_id](post-R1), usingINSTANCE_FLAG_CAUSTIC_SOURCEmacro (not a hex literal, #1234). Output added to direct only. caustic_splat.comp"water-side caustic is the water shader's responsibility" comment matches the livewater.fragimpl, not a stub (Dim 15). Output:/tmp/audit/renderer/dim_14.md
Dimension 15: Water (M38) + water-side caustics
Entry points: crates/renderer/src/vulkan/water.rs, crates/renderer/src/vulkan/water_caustic.rs (WaterCausticAccum), crates/renderer/shaders/water.vert/water.frag, byroredux/src/cell_loader/water.rs, byroredux/src/systems/water.rs (submersion_system). Shared water components live in crates/core/src/ecs/components/water.rs.
Checklist:
- WaterPlane spawned from interior/exterior cell water records (height/extent match); vertex displacement bounded, no NaN, no Z-fight at shoreline; Fresnel base ~0.02 (do NOT reuse glass IOR 1.5); RT reflect/refract (IOR ~1.33) miss → sky/backdrop with fog (not black/magenta).
submersion_systemflipsSubmersionStateat the water plane with no per-frame strobe; cell unload despawns water cleanly (no leaked BLAS vs post-unload TLAS); water doesn't cast opaque shadows; two-sided via dynamicCULL_MODE; sort key places water perbyroredux/src/render/sort_key_tests.rsordering; distinctGpuMaterialentry (no dedup collapse with glass).- Procedural-noise precision bound marked for absolute-world UVs (regression guard, #1502).
- Water-side caustic synthesis (regression guards):
sun_directionplumbed throughGpuCameraand uploaded each frame (not stale-from-init);WaterCausticAccumlifecycle (per-FIFR32_UINT, GENERAL/TRANSFER_DST/GENERAL, reverse-orderdestroy) lives inwater_caustic.rswhilewater.rsowns only the descriptor set/layout/pool;water.fragactually writes the accumulator viaimageAtomicAdd; composite samples it into direct lighting (#1210 Phases A–E / #1255–#1257). Output:/tmp/audit/renderer/dim_15.md
Dimension 16: Volumetrics (M55) & bloom (M58)
Entry points: crates/renderer/src/vulkan/volumetrics.rs, bloom.rs, crates/renderer/shaders/volumetrics_inject.comp, volumetrics_integrate.comp, bloom_downsample.comp, bloom_upsample.comp, composite consumption in composite.frag.
Checklist — volumetrics:
- Froxel grid is DERIVED, not fixed:
froxel_extent(render_extent, config)=render_extent.{width,height}.div_ceil(froxel_xy_divisor)×froxel_z_slices(defaults 12 / 64 inupscaling.rs::VolumetricsConfig, so 160×90×64 at 1080p native and smaller under any FSR preset — it keys on RENDER extent, deliberately downstream of the FSR preset query). Dispatch covers exactly the grid at the injectlocal_size(WORKGROUP_X/Y/Z= 8³); per-FIF buffer is RGBA16F (8 B/froxel, no cross-frame WAR); inject does a singleTerminateOnFirstHitshadow ray per froxel; integrate multiplies transmittance across the walk; HGgclamped to (−0.999, 0.999). - Gate
VOLUMETRIC_OUTPUT_CONSUMED(#928): if composite drops the sample, the dispatch must be skipped (not dispatched + ignored). Interior cells (no sun) produce neutral non-NaN output; resize rebinds both volumetric and composite descriptors (#905).
Checklist — bloom:
- 5 down-mips + 4 up-mips,
B10G11R11_UFLOATthroughout (no R16G16B16A16 mid-chain); 4-tap bilinear down (weights sum 1.0), additive up (no [0,1] clamp); per-FIF mip chain (cross-frame WAR gated by fence — do NOT reintroduce the redundant pre-barriers removed in #931). - Bloom added before ACES tone-map (HDR add); intensity constant is
BLOOM_INTENSITY(0.15) inshader_constants_data.rsemitted intoinclude/shader_constants.glsl— NOT hand-written incomposite.frag; 5 down-mips + 4 up-mips isBLOOM_MIP_COUNT/BLOOM_MIP_COUNT - 1(bloom.rs); source is the un-tone-mapped HDR (NOT the TAA output — descriptor-binding regression pattern); disable path short-circuits both dispatch and composite addition. Output:/tmp/audit/renderer/dim_16.md
Dimension 17: Disney BSDF / PBR gating (#1248–#1254) + soft shadows
Entry points: crates/renderer/shaders/include/pbr.glsl (Disney lobe fn definitions) + crates/renderer/shaders/include/lighting.glsl (gate + call sites; triangle.frag #includes both), crates/renderer/src/vulkan/material.rs (Disney preset constructors), byroredux/src/render/sky.rs (sun_angular_radius).
Checklist — Disney (symbol-anchored; flag bits live in shader_constants_data.rs, NOT hand-declared in the shader — verify, this was the #1357 migration):
- Gate is
MAT_FLAG_PBR_BSDF(bit 5) only — grepMAT_FLAG_PBR_BSDFgate sites incrates/renderer/shaders/include/lighting.glsl+include/pbr.glsland confirm each lights the Disney lobe (no FNV/FO3/Skyrim legacy path tripping it). FNV/FO3/Oblivion: zero materials set the flag → lobe unreachable. FO4/FO76/Starfield: BGSM is canonical → lobe is the expected path (regression is a BGSM falling back to Lambert). dielectricF0FromIor(eta)derives F0 from per-material IOR (not hardcoded 0.04), with input-domain clamp guardingeta ≤ 0(#1248/#1253). Per-material IOR now has explicit canonical sources (#41eedfe1):Material::iordefaults toDEFAULT_DIELECTRIC_IOR = 1.5(generic dielectric, F0≈0.04); glass classification instead appliesGLASS_SURFACE_BEHAVIOR(roughness 0.10, metalness 0.0, ior 1.45) viaMaterial::apply_surface_behavior, which must NOT overwrite authoredtexture_path/normal_map/glow_map/uv_scale/alpha (regression guard:glass_behavior_preserves_authored_map_overlay,crates/core/src/ecs/components/material.rs).distributionGGXAniso(NdotH, HdotX, HdotY, ax, ay)MUST degenerate exactly to isotropic GGX whenax == ay(legacy-compat contract, #1250);deriveAxAy(roughness, anisotropic, …)clampsanisotropicto [0,1] (half-axis convention per GLSL-PathTracer, NOT Disney-2012[-1,1]) — verify nosqrt(<0)atanisotropic = 1.0(#1254).disneyDiffuseSplit(...)returns split lobes (Burley retro + sheen + Hanrahan-Krueger SSS); sheen is additive, NOT divided by π (#1249/#1252).- Clustered-light path rescales diffuse and sheen together (regression guard, #2243/
c4cb2614).shadowableLightRadiance(lighting.glsl) keeps the legacy non-/πLambert convention and must scale the whole Disney lobe byPI—diffuseBrdf = (dd.diffuse + dd.sheen) * PI * (1.0 - metalness)— not diffuse alone; scaling onlydd.diffuseleft sheen weighted π× too low relative to diffuse compared to the normalized direct-sun path (triangle.frag, which keeps(dd.diffuse + dd.sheen) * (1.0 - metalness)). Regression:disney_sheen_keeps_its_relative_weight_across_direct_light_paths(gpu_instance_layout_tests.rs) reverting to a diffuse-only* PI. - DALC bounded-path escape converts irradiance to radiance (regression guard, #2244/
c4cb2614).pathEnvironmentRadiance(lighting.glsl) samplessampleDalcCube— an authored irradiance cube — and must divide byPIbefore returning it as environment radiance for a path escaping the TLAS (sampleDalcCube(rayDir) * (1.0 / PI)); the raw, unconverted sample over-brightens Skyrim DALC-lit GI misses by π×. Regression:bounded_path_converts_dalc_irradiance_to_environment_radiance(gpu_instance_layout_tests.rs) reverting to a baresampleDalcCubereturn. #1147 Phase 2bsiblings fire independently:MAT_FLAG_TRANSLUCENCY(bit 6) → SSS, modulated byMAT_FLAG_TRANSLUCENCY_THICK_OBJECT(bit 8) /MAT_FLAG_TRANSLUCENCY_MIX_ALBEDO(bit 9);MAT_FLAG_MODEL_SPACE_NORMALS(bit 7, set by #972) → model-space sampling. No spurious cross-activation.- Disney preset constructors in
material.rsmatch documented values (cross-ref GLSL-PathTracer perreference_glsl_pathtracer.md).
Checklist — soft shadows (M-LIGHT):
sun_angular_radiusships inGpuCamera; shipping default0.020rad (sky-params assert caps < 0.10) — drift changes shadow softness globally.- Single-tap stochastic cone sample around the sun, deterministic per-pixel-per-frame (no true RNG that breaks TAA history);
TerminateOnFirstHit; TAA absorbs the noise (YCoCg clamp tolerance allows convergence). - Interior fill (
radius < 0.0→isInteriorFill) bypasses the cone sample; disocclusion single-sample fallback is not black. Output:/tmp/audit/renderer/dim_17.md
Dimension 18: Sky / weather / exterior lighting (M33/M34)
Entry points: byroredux/src/systems/weather.rs, byroredux/src/scene/world_setup.rs (apply_worldspace_weather, ensure_game_time), byroredux/src/components/game_time.rs (GameTimeRes), byroredux/src/render/sky.rs, crates/plugin/src/esm/records/weather.rs, crates/renderer/shaders/triangle.frag (sky gradient + cloud + fog). See also /audit-exal.
Checklist:
weather_systemadvances game time monotonically (GameTimeRes::tick); sun arc from CLMT TNAM hours (not hardcoded); TOD color easing matches legacy; weather fade blends AFTER the TOD lookup (WeatherTransitionRes); all 4 cloud layers active with world-XY parallax scaled by TOD wind.- Sky gradient (zenith→horizon) from active TOD palette in the non-RT miss-fill, consistent with the GI miss "sky fill" (Dim 2); fog applied to direct only (Dim 8); interior fill at 0.6× ambient with
radius = −1(unshadowed), gating RT shadow on!isInteriorFill(symbol-anchored, #1200). - Disabled-WTHR fallback is neutral (no NaN / pitch-black); cell transition does not strobe TOD (palette is per-worldspace + global clock, not per-cell).
- Game clock survives worldspace transitions (regression guard,
7a851ab9).GameTimeResis the single persistent clock driving TOD (day + hour + rate, save/load round-tripped);apply_worldspace_weatherreads the live hour viabootstrap_game_hourand re-installs the resource only throughensure_game_time(insert-if-absent), never an unconditional reset. Regression: a worldspace load re-seedingGameTimeResunconditionally, snapping an in-progress session's hour/day back to the process default — pinned bybootstrap_hour_prefers_the_persistent_live_clock/insert_procedural_fallback_resources_preserves_advanced_game_time(world_setup.rs). Output:/tmp/audit/renderer/dim_18.md
Dimension 19: Tangent-space & normal maps (M-NORMALS)
Entry points: crates/nif/src/import/mesh/tangent.rs, crates/nif/src/import/mesh/bs_tri_shape.rs, crates/nif/src/blocks/tri_shape/bs_tri_shape.rs (VF_TANGENTS), crates/renderer/shaders/include/material_sampling.glsl (perturbNormal).
Checklist:
- Oblivion/FO3/FNV: per-vertex tangents from
NiBinaryExtraData"Tangent space …" — Bethesda's "tangent" is∂P/∂Vand "bitangent" is∂P/∂U(theCalcTangentSpaceswap). The decoder must read the bitangent half intoVertex.tangent.xyzand derive the sign from the tangent half (handedness regression #786). - FO4+ BSTriShape inline tangents when
VF_TANGENTS | VF_NORMALSset (packed-vertex loop) — distinct from Skyrim, not gated on the wrong BSVER (#795/#796). - Synthesized fallback (
synthesize_tangents) produces unit-length tangents + consistent signs when the blob is missing/malformed. - Sign convention
B = bitangent_sign * cross(N, T)reconstructed fromVertex.tangent.w, consistent across all three import paths; Z-up→Y-up conversion applied to tangent xyz in lockstep with the normal (no path converting N but not T). perturbNormaldefault-on (#787/#788);DBG_BYPASS_NORMAL_MAP(0x10) runtime opt-out still recognized; theDBG_*catalog (24 entries) pinned in lockstep (Dim 3).- "Chrome posterized walls" is the magenta-checker placeholder × a correctly-loaded normal map — per
feedback_chrome_means_missing_textures.md, runtex.missingbefore recommending any tangent-space fix. Output:/tmp/audit/renderer/dim_19.md
Dimension 20: Debug overlay & GPU telemetry
Entry points: crates/renderer/src/vulkan/egui_pass.rs (EguiPass), crates/renderer/src/vulkan/gpu_timers.rs (GpuPerFrameTimers), crates/debug-ui/, wired in context/draw.rs + context/mod.rs.
Checklist:
- egui pass uses
loadOp = LOAD+initialLayout = PRESENT_SRC_KHRand is recorded after composite; supplies its own incoming dependency (Dim 4, #1433); framebuffers recreated on resize;Option<EguiPass>taken +destroy()d before device teardown; disabled (= None) path skips the dispatch with no layout drift. - GPU timers: one
VkQueryPoolper FIF slot;cmd_reset_query_poolbefore re-recording brackets; results readMAX_FRAMES_IN_FLIGHTbehind; driver-absent (timestamp_supported == false) →new()returnsOk(None), no unwrap in the draw path; skipped passes omit their bracket (no bogus interval). dispatches_skippedis a skin-coverage counter (skin_compute.rs, incremented indraw.rswhen the bone palette is unchanged), surfaced via console telemetry (NOTmem.stats, which does not exist — REN-LOW L-1) — NOT aGpuPerFrameTimersfield. Issued dispatches = total − skipped (#1194). Output:/tmp/audit/renderer/dim_20.md
Dimension 21: Cornell-box RT harness
Entry points: byroredux/src/cornell.rs (setup_cornell_scene, --cornell flag), mat.* console commands in byroredux/src/commands/scene.rs.
Checklist:
- The harness is a self-contained RT material/lighting reference scene (no on-disk game data) — verify it still builds a valid TLAS and renders without the asset pipeline, so it stays usable for bisecting glass/GI/caustic regressions (the Session 47 arc).
mat.*live commands drive material params at runtime for A/B verification — confirm they round-trip intoGpuMaterialvia the sameMaterialTablepath as game content (no Cornell-only material shortcut that would invalidate it as a reference).- Known confound (per memory): metalness-vs-lighting and the glass-stipple / IGN refraction jitter on opaque glass are open observations, not harness bugs — don't re-report them as new.
Output:
/tmp/audit/renderer/dim_21.md
Dimension 22: Light animation canonical translation (flicker/pulse)
Entry points: byroredux/src/systems/light_anim.rs (canonical_light_animation_flags), crates/core/src/ecs/components/light.rs (LightFlicker.animation_flags), attach site byroredux/src/cell_loader/references/mod.rs.
Severity floor: wrong per-game flag decode = MEDIUM (visual-only; feeds LightSource intensity into the light buffer, no crash/corruption risk).
Checklist:
canonical_light_animation_flags(game, source_flags)is the per-game→shared-behavior boundary for animation (mirrors the NIFAL translate pattern, Dim 6) — FO4 and FO76 mask raw LIGH flags toFLICKER | PULSEonly, dropping raw bit0x400(the Shadow-Spotlight flag, NOT a slow-pulse animation); Starfield masks to0(its restructuredDAT2subrecord has no named Flags field in SF1Edit, so no bit meaning is positively evidenced — #2251); every other game masks to the fullSHARED_LIGHT_ANIMATION_MASK(FLICKER | FLICKER_SLOW | PULSE | PULSE_SLOW). It now has a deliberate sibling,canonical_light_shadow_flags(#2250 / REN-D22-01), applying the same match-arm pattern toLIGHT_FLAG_SHADOW_MASK(0x400/0x800/0x1000) — audit the two as a mirrored pair; a per-game divergence added to one and not the other is the drift to look for. Regression guards (byroredux/src/systems/light_anim.rs):shadow_spotlight_bit_never_leaks_into_animation_on_any_game(the consolidated, any-game successor to the FO4-only fallout4_shadow_spotlight_is_not_slow_pulse, now removed),fallout4_real_flicker_and_pulse_map_to_shared_behavior.LightFlicker.animation_flagsholds the translated value;animate_lights_system/flicker_intensitymust readanimation_flags, never the rawLightSource.flags— a caller reading raw flags reintroduces the FO4 shadow-spotlight-flickers-like-a-torch bug. Output:/tmp/audit/renderer/dim_22.md
Dimension 23: FSR 3.1 upscaler & presentation chain (FSR plan phases 1–7)
Entry points: crates/fsr3-sys/src/lib.rs (vendored FidelityFX SDK FFI), crates/renderer/src/vulkan/frame_upscaler.rs, crates/renderer/src/vulkan/upscaling.rs, crates/renderer/src/vulkan/presentation.rs, crates/renderer/src/vulkan/exposure.rs, crates/renderer/shaders/presentation.frag, the frame-tail split in crates/renderer/src/vulkan/context/post_passes.rs. Docs: docs/engine/fsr3-upscaler-integration-plan.md, docs/engine/fsr3-troubleshooting.md. Shader generation: scripts/generate-fsr3-vulkan-shaders.sh.
Severity floor: a layout/barrier error here is CRITICAL — FSR Quality is the engine default since phase 7, so this is the default render path, not an optional feature. A wrong reactive/T&C mask is MEDIUM (visual-only).
Why this dimension exists: FSR was the bulk of Sessions 60–61 and had no owning dimension until 2026-07-27. Do not assume prior renderer audits covered it.
Checklist:
- FFI safety —
crates/fsr3-sysis a real live FFI boundary (unlikecxx-bridge's placeholder). Everyunsafe fnneeds a# Safetycontract stating who owns the pointed-to memory and for how long. Cross-check with/audit-safetyDimension 1 rather than duplicating the finding. - Resource-state contract — FSR requires specific image layouts on every input (color, depth, motion vectors, reactive mask, transparency & composition mask) and on the output.
BYRO_VALIDATION=1validated this clean over 900 frames; a change to the frame tail can silently break it because layout errors are invisible tocargo test. Treat any barrier/layout edit here as needing a validation-layer run, not reasoning — this is exactly the "speculative Vulkan fix" trap. - Dispatch-failure fallback — the fallback path exists and is telemetry-reported. Verify it actually renders (not a black frame) and that
BYRO_FSR_FORCE_DISPATCH_FAIL=1still exercises it; a fallback that regressed to a hang or blank output is worse than no fallback. - Runtime preset switching —
r.upscaler/--upscalerswitch presets live. Confirm resources are re-created (not reused at the wrong render resolution) and that the swap can't land mid-frame. native-aais expected to be a net loss on every scene (−9 % … +4 %) — it reconstructs at full output resolution. It exists to separate reconstruction quality from upscaling quality. Do not report its cost as a performance bug.- Jitter / motion-vector agreement — FSR consumes the same Halton jitter and motion vectors as TAA (Dim 13). Verify one jitter source, not two; a divergence shows up as smearing that is easy to misattribute to the denoiser (Dim 8).
- Exposure —
exposure.rsfeeds FSR's auto-exposure. Confirm the value handed to FSR matches what composite/tone-map uses, or reconstruction fights the tone mapper. - The FP32 permutation is unexercised — it needs a GPU without
shaderFloat16and the dev box has one. Carried scope, not a finding; note it as untested rather than reporting it as a defect. - Bench-harness stability —
scripts/fsr-bench-matrix.sh+scripts/fsr_bench_report.pymust stay byte-stable across commits for cross-commit comparisons to mean anything. Flag any edit to either that wasn't itself re-benched. Output:/tmp/audit/renderer/dim_23.md
Phase 3: Merge
- Read all
/tmp/audit/renderer/dim_*.md. - Combine into
docs/audits/AUDIT_RENDERER_<TODAY>.md:- Executive Summary — findings by severity, pipeline areas affected.
- RT Pipeline Assessment — BLAS/TLAS + SSBO indexing + ray-query safety + denoiser stability.
- GPU-Struct & Memory Assessment — layout pins, leaks, lifecycle/teardown.
- Findings — grouped by severity (CRITICAL first), deduplicated.
- Prioritized Fix Order — correctness → safety → optimization.
- Needs-RenderDoc — sync/barrier findings deferred for capture-based verification.
- Remove cross-dimension duplicates.
Phase 4: Cleanup
rm -rf /tmp/audit/renderer.- Inform the user the report is ready.
- Suggest:
/audit-publish docs/audits/AUDIT_RENDERER_<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.