1use 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
36pub struct ShadowPassLane {
41 pub pipeline: RwLock<Option<RenderPipelineId>>,
43 pub camera_layout: RwLock<Option<BindGroupLayoutId>>,
45 pub model_layout: RwLock<Option<BindGroupLayoutId>>,
47 pub atlas_texture: RwLock<Option<TextureId>>,
49 pub atlas_view: RwLock<Option<TextureViewId>>,
51 pub shadow_sampler: RwLock<Option<SamplerId>>,
53 pub shadow_results: RwLock<std::collections::HashMap<usize, (khora_core::math::Mat4, i32)>>,
56 pub camera_ring: RwLock<Option<DynamicUniformRingBuffer>>,
59 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 pub fn new() -> Self {
82 Self::default()
83 }
84
85 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 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 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 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 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 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 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 {
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 {
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 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 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 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 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 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 let atlas_index = next_atlas_index;
404 next_atlas_index += 1;
405 shadow_results.insert(i, (shadow_view_proj, atlas_index));
406
407 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 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(model_lock);
471 drop(camera_lock);
472
473 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 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 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 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, 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: 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 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, 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 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; 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 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 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 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 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}