khora_lanes/render_lane/
shadow_pass_lane.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Shadow pass lane implementation - handles depth rendering for shadows.
16
17use super::RenderWorld;
18use khora_core::renderer::{
19    api::{
20        command::{
21            BindGroupLayoutId, LoadOp, Operations, RenderPassDepthStencilAttachment,
22            RenderPassDescriptor, StoreOp,
23        },
24        core::RenderContext,
25        pipeline::RenderPipelineId,
26        resource::{CameraUniformData, SamplerId, TextureId, TextureViewId},
27        scene::{GpuMesh, ModelUniforms},
28        util::dynamic_uniform_buffer::DynamicUniformRingBuffer,
29    },
30    traits::CommandEncoder,
31    GraphicsDevice,
32};
33use khora_data::assets::Assets;
34use std::sync::RwLock;
35
36/// A rendering lane dedicated to producing shadow maps.
37///
38/// It renders the scene from the perspective of shadow-casting lights
39/// into a depth texture (shadow map or atlas).
40pub struct ShadowPassLane {
41    /// The render pipeline for depth-only rendering.
42    pub pipeline: RwLock<Option<RenderPipelineId>>,
43    /// Layout for the shadow camera uniform.
44    pub camera_layout: RwLock<Option<BindGroupLayoutId>>,
45    /// Layout for the model uniform.
46    pub model_layout: RwLock<Option<BindGroupLayoutId>>,
47    /// The shadow atlas texture (depth array).
48    pub atlas_texture: RwLock<Option<TextureId>>,
49    /// The view into the shadow atlas.
50    pub atlas_view: RwLock<Option<TextureViewId>>,
51    /// Comparison sampler for PCF.
52    pub shadow_sampler: RwLock<Option<SamplerId>>,
53    /// Stores calculated shadow matrices and atlas indices for the main pass.
54    /// Mapping: Light Index -> (Shadow Matrix, Atlas Index)
55    pub shadow_results: RwLock<std::collections::HashMap<usize, (khora_core::math::Mat4, i32)>>,
56    /// Dynamic ring buffer for the shadow camera (light view-projection) uniforms.
57    /// Uses dynamic offsets so each light can have its own camera data in the same frame.
58    pub camera_ring: RwLock<Option<DynamicUniformRingBuffer>>,
59    /// Dynamic ring buffer for per-mesh model uniforms.
60    pub model_ring: RwLock<Option<DynamicUniformRingBuffer>>,
61}
62
63impl Default for ShadowPassLane {
64    fn default() -> Self {
65        Self {
66            pipeline: RwLock::new(None),
67            camera_layout: RwLock::new(None),
68            model_layout: RwLock::new(None),
69            atlas_texture: RwLock::new(None),
70            atlas_view: RwLock::new(None),
71            shadow_sampler: RwLock::new(None),
72            shadow_results: RwLock::new(std::collections::HashMap::new()),
73            camera_ring: RwLock::new(None),
74            model_ring: RwLock::new(None),
75        }
76    }
77}
78
79impl ShadowPassLane {
80    /// Creates a new `ShadowPassLane`.
81    pub fn new() -> Self {
82        Self::default()
83    }
84
85    /// Calculates the view-projection matrix for a given light's shadow map.
86    fn calculate_shadow_view_proj(
87        &self,
88        light: &super::ExtractedLight,
89        view: &super::ExtractedView,
90    ) -> khora_core::math::Mat4 {
91        use khora_core::math::{Mat4, Vec3, Vec4};
92
93        match &light.light_type {
94            khora_core::renderer::light::LightType::Directional(_) => {
95                // --- CSM logic ---
96                // 1. Calculate Frustum Corners in World Space
97                let inv_view_proj = view.view_proj.inverse().unwrap_or(Mat4::IDENTITY);
98                let mut corners = Vec::with_capacity(8);
99                for x in &[-1.0, 1.0] {
100                    for y in &[-1.0, 1.0] {
101                        for z in &[0.0, 1.0] {
102                            let pt = inv_view_proj * Vec4::new(*x, *y, *z, 1.0);
103                            corners.push(pt.truncate() / pt.w);
104                        }
105                    }
106                }
107
108                // 2. Light View Matrix
109                let light_dir = light.direction.normalize();
110                let up = if light_dir.y.abs() > 0.99 {
111                    Vec3::Z
112                } else {
113                    Vec3::Y
114                };
115
116                // Center light view on frustum center
117                let mut center = Vec3::ZERO;
118                for p in &corners {
119                    center = center + *p;
120                }
121                center = center / 8.0;
122
123                let light_view =
124                    Mat4::look_at_rh(center, center + light_dir, up).unwrap_or(Mat4::IDENTITY);
125
126                // 3. Find Frustum AABB in Light Space
127                let mut min = Vec3::new(f32::MAX, f32::MAX, f32::MAX);
128                let mut max = Vec3::new(f32::MIN, f32::MIN, f32::MIN);
129                for p in corners {
130                    let p_ls = light_view * Vec4::from_vec3(p, 1.0);
131                    min.x = min.x.min(p_ls.x);
132                    max.x = max.x.max(p_ls.x);
133                    min.y = min.y.min(p_ls.y);
134                    max.y = max.y.max(p_ls.y);
135                    min.z = min.z.min(p_ls.z);
136                    max.z = max.z.max(p_ls.z);
137                }
138
139                // 4. Create Ortho Projection
140                // Z-range should be large enough to encapsulate casters outside view
141                let z_padding = 100.0;
142                let light_proj = Mat4::orthographic_rh_zo(
143                    min.x,
144                    max.x,
145                    min.y,
146                    max.y,
147                    min.z - z_padding,
148                    max.z + z_padding,
149                );
150
151                light_proj * light_view
152            }
153            khora_core::renderer::light::LightType::Spot(sl) => {
154                let light_dir = light.direction.normalize();
155                let up = if light_dir.y.abs() > 0.99 {
156                    Vec3::Z
157                } else {
158                    Vec3::Y
159                };
160                let view = Mat4::look_at_rh(light.position, light.position + light_dir, up)
161                    .unwrap_or(Mat4::IDENTITY);
162
163                let proj = Mat4::perspective_rh_zo(sl.outer_cone_angle * 2.0, 1.0, 0.1, sl.range);
164                proj * view
165            }
166            khora_core::renderer::light::LightType::Point(_) => {
167                // Point lights need 6 passes (cubemap)
168                Mat4::IDENTITY
169            }
170        }
171    }
172}
173
174impl khora_core::lane::Lane for ShadowPassLane {
175    fn strategy_name(&self) -> &'static str {
176        "ShadowPass"
177    }
178
179    fn lane_kind(&self) -> khora_core::lane::LaneKind {
180        khora_core::lane::LaneKind::Shadow
181    }
182
183    fn estimate_cost(&self, ctx: &khora_core::lane::LaneContext) -> f32 {
184        let render_world =
185            match ctx.get::<khora_core::lane::Slot<crate::render_lane::RenderWorld>>() {
186                Some(slot) => slot.get_ref(),
187                None => return 1.0,
188            };
189        let gpu_meshes = match ctx.get::<std::sync::Arc<
190            std::sync::RwLock<
191                khora_data::assets::Assets<khora_core::renderer::api::scene::GpuMesh>,
192            >,
193        >>() {
194            Some(arc) => arc,
195            None => return 1.0,
196        };
197        self.estimate_shadow_cost(render_world, gpu_meshes)
198    }
199
200    fn on_initialize(
201        &self,
202        ctx: &mut khora_core::lane::LaneContext,
203    ) -> Result<(), khora_core::lane::LaneError> {
204        let device = ctx
205            .get::<std::sync::Arc<dyn khora_core::renderer::GraphicsDevice>>()
206            .ok_or(khora_core::lane::LaneError::missing(
207                "Arc<dyn GraphicsDevice>",
208            ))?;
209        self.on_gpu_init(device.as_ref())
210            .map_err(|e| khora_core::lane::LaneError::InitializationFailed(Box::new(e)))
211    }
212
213    fn execute(
214        &self,
215        ctx: &mut khora_core::lane::LaneContext,
216    ) -> Result<(), khora_core::lane::LaneError> {
217        use khora_core::lane::{LaneError, Slot};
218
219        // Phase 1: Render shadow maps
220        {
221            let device = ctx
222                .get::<std::sync::Arc<dyn khora_core::renderer::GraphicsDevice>>()
223                .ok_or(LaneError::missing("Arc<dyn GraphicsDevice>"))?
224                .clone();
225            let gpu_meshes = ctx
226                .get::<std::sync::Arc<
227                    std::sync::RwLock<
228                        khora_data::assets::Assets<khora_core::renderer::api::scene::GpuMesh>,
229                    >,
230                >>()
231                .ok_or(LaneError::missing("Arc<RwLock<Assets<GpuMesh>>>"))?
232                .clone();
233            let encoder = ctx
234                .get::<Slot<dyn khora_core::renderer::traits::CommandEncoder>>()
235                .ok_or(LaneError::missing("Slot<dyn CommandEncoder>"))?
236                .get();
237            let render_world = ctx
238                .get::<Slot<crate::render_lane::RenderWorld>>()
239                .ok_or(LaneError::missing("Slot<RenderWorld>"))?
240                .get_ref();
241            let color_target = ctx
242                .get::<khora_core::lane::ColorTarget>()
243                .ok_or(LaneError::missing("ColorTarget"))?
244                .0;
245            let depth_target = ctx
246                .get::<khora_core::lane::DepthTarget>()
247                .ok_or(LaneError::missing("DepthTarget"))?
248                .0;
249            let clear_color = ctx
250                .get::<khora_core::lane::ClearColor>()
251                .ok_or(LaneError::missing("ClearColor"))?
252                .0;
253
254            let render_ctx = khora_core::renderer::api::core::RenderContext::new(
255                &color_target,
256                Some(&depth_target),
257                clear_color,
258            );
259
260            self.render_shadows(
261                render_world,
262                device.as_ref(),
263                encoder,
264                &render_ctx,
265                &gpu_meshes,
266            );
267        }
268
269        // Phase 2: Patch lights with shadow data
270        {
271            let render_world = ctx
272                .get::<Slot<crate::render_lane::RenderWorld>>()
273                .ok_or(LaneError::missing("Slot<RenderWorld>"))?
274                .get();
275            let shadow_results = self.get_shadow_results();
276            for (i, (matrix, index)) in shadow_results.iter() {
277                if let Some(light) = render_world.lights.get_mut(*i) {
278                    light.shadow_view_proj = *matrix;
279                    light.shadow_atlas_index = Some(*index);
280                }
281            }
282        }
283
284        // Phase 3: Store shadow resources for render lanes
285        if let Some(view) = self.get_atlas_view() {
286            ctx.insert(khora_core::lane::ShadowAtlasView(view));
287        }
288        if let Some(sampler) = self.get_shadow_sampler() {
289            ctx.insert(khora_core::lane::ShadowComparisonSampler(sampler));
290        }
291
292        Ok(())
293    }
294
295    fn on_shutdown(&self, ctx: &mut khora_core::lane::LaneContext) {
296        if let Some(device) = ctx.get::<std::sync::Arc<dyn khora_core::renderer::GraphicsDevice>>()
297        {
298            self.on_gpu_shutdown(device.as_ref());
299        }
300    }
301
302    fn as_any(&self) -> &dyn std::any::Any {
303        self
304    }
305
306    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
307        self
308    }
309}
310
311impl ShadowPassLane {
312    fn render_shadows(
313        &self,
314        render_world: &RenderWorld,
315        device: &dyn GraphicsDevice,
316        encoder: &mut dyn CommandEncoder,
317        _render_ctx: &RenderContext,
318        gpu_meshes: &RwLock<Assets<GpuMesh>>,
319    ) {
320        use khora_core::renderer::api::{
321            command::BindGroupId, resource::BufferId, util::IndexFormat,
322        };
323
324        let pipeline = if let Some(p) = *self.pipeline.read().unwrap() {
325            p
326        } else {
327            return;
328        };
329
330        let atlas_view = if let Some(v) = *self.atlas_view.read().unwrap() {
331            v
332        } else {
333            return;
334        };
335
336        // Acquire mutable access to ring buffers
337        let mut camera_lock = self.camera_ring.write().unwrap();
338        let camera_ring = match camera_lock.as_mut() {
339            Some(r) => r,
340            None => {
341                log::warn!("ShadowPassLane: camera_ring not initialized");
342                return;
343            }
344        };
345        camera_ring.advance();
346
347        let mut model_lock = self.model_ring.write().unwrap();
348        let model_ring = match model_lock.as_mut() {
349            Some(r) => r,
350            None => {
351                log::warn!("ShadowPassLane: model_ring not initialized");
352                return;
353            }
354        };
355        model_ring.advance();
356
357        let gpu_meshes_guard = gpu_meshes.read().unwrap();
358
359        let mut shadow_results = self.shadow_results.write().unwrap();
360        shadow_results.clear();
361
362        let mut next_atlas_index = 0;
363
364        /// Pre-collected draw command for one mesh within a shadow pass.
365        struct ShadowDrawCmd {
366            model_bg: BindGroupId,
367            model_offset: u32,
368            vertex_buffer: BufferId,
369            index_buffer: BufferId,
370            index_count: u32,
371            index_format: IndexFormat,
372        }
373
374        /// All data needed to execute one light's shadow pass.
375        struct LightPass {
376            atlas_index: i32,
377            camera_bg: BindGroupId,
378            camera_offset: u32,
379            draw_cmds: Vec<ShadowDrawCmd>,
380        }
381
382        let mut light_passes: Vec<LightPass> = Vec::new();
383
384        for (i, light) in render_world.lights.iter().enumerate() {
385            let shadow_enabled = match &light.light_type {
386                khora_core::renderer::light::LightType::Directional(l) => l.shadow_enabled,
387                khora_core::renderer::light::LightType::Point(l) => l.shadow_enabled,
388                khora_core::renderer::light::LightType::Spot(l) => l.shadow_enabled,
389            };
390
391            if !shadow_enabled {
392                continue;
393            }
394
395            // 1. Calculate Shadow View-Projection
396            let shadow_view_proj = if let Some(view) = render_world.views.first() {
397                self.calculate_shadow_view_proj(light, view)
398            } else {
399                khora_core::math::Mat4::IDENTITY
400            };
401
402            // Store result for main pass consumption
403            let atlas_index = next_atlas_index;
404            next_atlas_index += 1;
405            shadow_results.insert(i, (shadow_view_proj, atlas_index));
406
407            // 2. Push camera (light VP) uniform for this light
408            let camera_data = CameraUniformData {
409                view_projection: shadow_view_proj.to_cols_array_2d(),
410                camera_position: [light.position.x, light.position.y, light.position.z, 1.0],
411            };
412
413            let camera_offset = match camera_ring.push(device, bytemuck::bytes_of(&camera_data)) {
414                Ok(off) => off,
415                Err(e) => {
416                    log::error!("ShadowPassLane: Failed to push camera uniform: {:?}", e);
417                    continue;
418                }
419            };
420            let camera_bg = *camera_ring.current_bind_group();
421
422            // 3. Pre-collect per-mesh draw commands
423            let mut draw_cmds = Vec::with_capacity(render_world.meshes.len());
424
425            for mesh in &render_world.meshes {
426                if let Some(gpu_mesh) = gpu_meshes_guard.get(&mesh.cpu_mesh_uuid) {
427                    let model_mat = mesh.transform.to_matrix();
428                    let normal_mat = if let Some(inv) = model_mat.inverse() {
429                        inv.transpose()
430                    } else {
431                        continue;
432                    };
433
434                    let model_uniforms = ModelUniforms {
435                        model_matrix: model_mat.to_cols_array_2d(),
436                        normal_matrix: normal_mat.to_cols_array_2d(),
437                    };
438
439                    let model_offset = match model_ring
440                        .push(device, bytemuck::bytes_of(&model_uniforms))
441                    {
442                        Ok(off) => off,
443                        Err(e) => {
444                            log::error!("ShadowPassLane: Failed to push model uniform: {:?}", e);
445                            continue;
446                        }
447                    };
448
449                    draw_cmds.push(ShadowDrawCmd {
450                        model_bg: *model_ring.current_bind_group(),
451                        model_offset,
452                        vertex_buffer: gpu_mesh.vertex_buffer,
453                        index_buffer: gpu_mesh.index_buffer,
454                        index_count: gpu_mesh.index_count,
455                        index_format: gpu_mesh.index_format,
456                    });
457                }
458            }
459
460            light_passes.push(LightPass {
461                atlas_index,
462                camera_bg,
463                camera_offset,
464                draw_cmds,
465            });
466        }
467
468        // Drop write guards before beginning render passes (avoids holding
469        // locks longer than necessary; ring buffers are no longer mutated).
470        drop(model_lock);
471        drop(camera_lock);
472
473        // 4. Execute all render passes
474        for lp in &light_passes {
475            let depth_attachment = RenderPassDepthStencilAttachment {
476                view: &atlas_view,
477                depth_ops: Some(Operations {
478                    load: LoadOp::Clear(1.0),
479                    store: StoreOp::Store,
480                }),
481                stencil_ops: None,
482                base_array_layer: lp.atlas_index as u32,
483            };
484
485            let mut pass = encoder.begin_render_pass(&RenderPassDescriptor {
486                label: Some("Shadow Pass"),
487                color_attachments: &[],
488                depth_stencil_attachment: Some(depth_attachment),
489            });
490
491            pass.set_pipeline(&pipeline);
492            pass.set_bind_group(0, &lp.camera_bg, &[lp.camera_offset]);
493
494            for cmd in &lp.draw_cmds {
495                pass.set_bind_group(1, &cmd.model_bg, &[cmd.model_offset]);
496                pass.set_vertex_buffer(0, &cmd.vertex_buffer, 0);
497                pass.set_index_buffer(&cmd.index_buffer, 0, cmd.index_format);
498                pass.draw_indexed(0..cmd.index_count, 0, 0..1);
499            }
500        }
501    }
502
503    fn estimate_shadow_cost(
504        &self,
505        render_world: &RenderWorld,
506        _gpu_meshes: &RwLock<Assets<GpuMesh>>,
507    ) -> f32 {
508        // Cost depends on number of shadow-casting lights and meshes
509        let shadow_lights = render_world
510            .lights
511            .iter()
512            .filter(|l| match &l.light_type {
513                khora_core::renderer::light::LightType::Directional(dl) => dl.shadow_enabled,
514                khora_core::renderer::light::LightType::Point(pl) => pl.shadow_enabled,
515                khora_core::renderer::light::LightType::Spot(sl) => sl.shadow_enabled,
516            })
517            .count();
518        (shadow_lights as f32) * (render_world.meshes.len() as f32) * 0.001
519    }
520
521    fn get_shadow_results(
522        &self,
523    ) -> std::collections::HashMap<usize, (khora_core::math::Mat4, i32)> {
524        self.shadow_results.read().unwrap().clone()
525    }
526
527    fn get_atlas_view(&self) -> Option<khora_core::renderer::api::resource::TextureViewId> {
528        *self.atlas_view.read().unwrap()
529    }
530
531    fn get_shadow_sampler(&self) -> Option<khora_core::renderer::api::resource::SamplerId> {
532        *self.shadow_sampler.read().unwrap()
533    }
534
535    fn on_gpu_init(
536        &self,
537        device: &dyn GraphicsDevice,
538    ) -> Result<(), khora_core::renderer::error::RenderError> {
539        use crate::render_lane::shaders::SHADOW_PASS_WGSL;
540        use khora_core::renderer::api::{
541            command::{
542                BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, BufferBindingType,
543            },
544            core::{ShaderModuleDescriptor, ShaderSourceData},
545            pipeline::enums::{CompareFunction, PrimitiveTopology, VertexFormat, VertexStepMode},
546            pipeline::state::{DepthBiasState, StencilFaceState},
547            pipeline::{
548                DepthStencilStateDescriptor, MultisampleStateDescriptor, PipelineLayoutDescriptor,
549                PrimitiveStateDescriptor, RenderPipelineDescriptor, VertexAttributeDescriptor,
550                VertexBufferLayoutDescriptor,
551            },
552            util::{SampleCount, ShaderStageFlags, TextureFormat},
553        };
554        use std::borrow::Cow;
555
556        // 1. Bind Group Layouts
557        let camera_layout = device
558            .create_bind_group_layout(&BindGroupLayoutDescriptor {
559                label: Some("shadow_camera_layout"),
560                entries: &[BindGroupLayoutEntry {
561                    binding: 0,
562                    visibility: ShaderStageFlags::VERTEX,
563                    ty: BindingType::Buffer {
564                        ty: BufferBindingType::Uniform,
565                        has_dynamic_offset: true,
566                        min_binding_size: None,
567                    },
568                }],
569            })
570            .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
571
572        let model_layout = device
573            .create_bind_group_layout(&BindGroupLayoutDescriptor {
574                label: Some("shadow_model_layout"),
575                entries: &[BindGroupLayoutEntry {
576                    binding: 0,
577                    visibility: ShaderStageFlags::VERTEX,
578                    ty: BindingType::Buffer {
579                        ty: BufferBindingType::Uniform,
580                        has_dynamic_offset: true,
581                        min_binding_size: None,
582                    },
583                }],
584            })
585            .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
586
587        // 2. Pipeline
588        let shader_module = device
589            .create_shader_module(&ShaderModuleDescriptor {
590                label: Some("shadow_pass_shader"),
591                source: ShaderSourceData::Wgsl(Cow::Borrowed(SHADOW_PASS_WGSL)),
592            })
593            .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
594
595        let pipeline_layout = device
596            .create_pipeline_layout(&PipelineLayoutDescriptor {
597                label: Some(Cow::Borrowed("Shadow Pass Pipeline Layout")),
598                bind_group_layouts: &[camera_layout, model_layout],
599            })
600            .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
601
602        let vertex_layout = VertexBufferLayoutDescriptor {
603            array_stride: 32, // pos(12) + norm(12) + uv(8)
604            step_mode: VertexStepMode::Vertex,
605            attributes: Cow::Owned(vec![VertexAttributeDescriptor {
606                format: VertexFormat::Float32x3,
607                offset: 0,
608                shader_location: 0,
609            }]),
610        };
611
612        let pipeline_desc = RenderPipelineDescriptor {
613            label: Some(Cow::Borrowed("Shadow Pass Pipeline")),
614            layout: Some(pipeline_layout),
615            vertex_shader_module: shader_module,
616            vertex_entry_point: Cow::Borrowed("vs_main"),
617            fragment_shader_module: None,
618            fragment_entry_point: None,
619            color_target_states: Cow::Borrowed(&[]),
620            vertex_buffers_layout: Cow::Owned(vec![vertex_layout]),
621            primitive_state: PrimitiveStateDescriptor {
622                topology: PrimitiveTopology::TriangleList,
623                ..Default::default()
624            },
625            depth_stencil_state: Some(DepthStencilStateDescriptor {
626                format: TextureFormat::Depth32Float,
627                depth_write_enabled: true,
628                depth_compare: CompareFunction::Less,
629                stencil_front: StencilFaceState::default(),
630                stencil_back: StencilFaceState::default(),
631                stencil_read_mask: 0,
632                stencil_write_mask: 0,
633                bias: DepthBiasState {
634                    constant: 2, // Slope-scale depth bias
635                    slope_scale: 2.0,
636                    clamp: 0.0,
637                },
638            }),
639            multisample_state: MultisampleStateDescriptor {
640                count: SampleCount::X1,
641                mask: !0,
642                alpha_to_coverage_enabled: false,
643            },
644        };
645
646        let pipeline = device
647            .create_render_pipeline(&pipeline_desc)
648            .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
649
650        *self.pipeline.write().unwrap() = Some(pipeline);
651        *self.camera_layout.write().unwrap() = Some(camera_layout);
652        *self.model_layout.write().unwrap() = Some(model_layout);
653
654        // 3. Ring Buffers
655        use khora_core::renderer::api::util::dynamic_uniform_buffer::{
656            DEFAULT_MAX_ELEMENTS, MIN_UNIFORM_ALIGNMENT,
657        };
658
659        let camera_ring = DynamicUniformRingBuffer::new(
660            device,
661            camera_layout,
662            0,
663            std::mem::size_of::<CameraUniformData>() as u32,
664            16, // max shadow-casting lights per frame
665            MIN_UNIFORM_ALIGNMENT,
666            "Shadow Camera Ring",
667        )
668        .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
669
670        let model_ring = DynamicUniformRingBuffer::new(
671            device,
672            model_layout,
673            0,
674            std::mem::size_of::<ModelUniforms>() as u32,
675            DEFAULT_MAX_ELEMENTS,
676            MIN_UNIFORM_ALIGNMENT,
677            "Shadow Model Ring",
678        )
679        .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
680
681        *self.camera_ring.write().unwrap() = Some(camera_ring);
682        *self.model_ring.write().unwrap() = Some(model_ring);
683
684        // 4. Shadow Atlas Allocation
685        use khora_core::math::Extent3D;
686        use khora_core::renderer::api::resource::{
687            AddressMode, FilterMode, ImageAspect, MipmapFilterMode, SamplerDescriptor,
688            TextureDescriptor, TextureDimension, TextureUsage, TextureViewDescriptor,
689            TextureViewDimension,
690        };
691
692        let atlas_size = 2048;
693        let atlas_layers = 4; // Placeholder for MAX_SHADOW_CASTERS
694
695        let atlas = device
696            .create_texture(&TextureDescriptor {
697                label: Some(Cow::Borrowed("Shadow Atlas")),
698                size: Extent3D {
699                    width: atlas_size,
700                    height: atlas_size,
701                    depth_or_array_layers: atlas_layers,
702                },
703                mip_level_count: 1,
704                sample_count: SampleCount::X1,
705                dimension: TextureDimension::D2,
706                format: TextureFormat::Depth32Float,
707                usage: TextureUsage::DEPTH_STENCIL_ATTACHMENT | TextureUsage::TEXTURE_BINDING,
708                view_formats: Cow::Borrowed(&[]),
709            })
710            .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
711
712        let atlas_view = device
713            .create_texture_view(
714                atlas,
715                &TextureViewDescriptor {
716                    label: Some(Cow::Borrowed("Shadow Atlas View")),
717                    format: Some(TextureFormat::Depth32Float),
718                    dimension: Some(TextureViewDimension::D2Array),
719                    aspect: ImageAspect::DepthOnly,
720                    base_mip_level: 0,
721                    mip_level_count: Some(1),
722                    base_array_layer: 0,
723                    array_layer_count: Some(atlas_layers),
724                },
725            )
726            .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
727
728        let sampler = device
729            .create_sampler(&SamplerDescriptor {
730                label: Some(Cow::Borrowed("Shadow Sampler")),
731                address_mode_u: AddressMode::ClampToEdge,
732                address_mode_v: AddressMode::ClampToEdge,
733                address_mode_w: AddressMode::ClampToEdge,
734                mag_filter: FilterMode::Linear,
735                min_filter: FilterMode::Linear,
736                mipmap_filter: MipmapFilterMode::Nearest,
737                lod_min_clamp: 0.0,
738                lod_max_clamp: 1.0,
739                compare: Some(CompareFunction::LessEqual),
740                anisotropy_clamp: 1,
741                border_color: None,
742            })
743            .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
744
745        *self.atlas_texture.write().unwrap() = Some(atlas);
746        *self.atlas_view.write().unwrap() = Some(atlas_view);
747        *self.shadow_sampler.write().unwrap() = Some(sampler);
748
749        Ok(())
750    }
751
752    fn on_gpu_shutdown(&self, device: &dyn GraphicsDevice) {
753        // Destroy ring buffers
754        if let Some(ring) = self.camera_ring.write().unwrap().take() {
755            ring.destroy(device);
756        }
757        if let Some(ring) = self.model_ring.write().unwrap().take() {
758            ring.destroy(device);
759        }
760
761        // Destroy pipeline
762        if let Some(pipeline) = self.pipeline.write().unwrap().take() {
763            if let Err(e) = device.destroy_render_pipeline(pipeline) {
764                log::warn!("ShadowPassLane: Failed to destroy pipeline: {:?}", e);
765            }
766        }
767
768        // Destroy bind group layouts
769        if let Some(layout) = self.camera_layout.write().unwrap().take() {
770            if let Err(e) = device.destroy_bind_group_layout(layout) {
771                log::warn!("ShadowPassLane: Failed to destroy camera layout: {:?}", e);
772            }
773        }
774        if let Some(layout) = self.model_layout.write().unwrap().take() {
775            if let Err(e) = device.destroy_bind_group_layout(layout) {
776                log::warn!("ShadowPassLane: Failed to destroy model layout: {:?}", e);
777            }
778        }
779
780        // Destroy atlas texture, view, and sampler
781        if let Some(view) = self.atlas_view.write().unwrap().take() {
782            if let Err(e) = device.destroy_texture_view(view) {
783                log::warn!("ShadowPassLane: Failed to destroy atlas view: {:?}", e);
784            }
785        }
786        if let Some(texture) = self.atlas_texture.write().unwrap().take() {
787            if let Err(e) = device.destroy_texture(texture) {
788                log::warn!("ShadowPassLane: Failed to destroy atlas texture: {:?}", e);
789            }
790        }
791        if let Some(sampler) = self.shadow_sampler.write().unwrap().take() {
792            if let Err(e) = device.destroy_sampler(sampler) {
793                log::warn!("ShadowPassLane: Failed to destroy shadow sampler: {:?}", e);
794            }
795        }
796    }
797}