1use std::collections::HashMap;
23use std::sync::RwLock;
24
25use khora_core::renderer::api::{
26 command::BindGroupLayoutId,
27 pipeline::RenderPipelineId,
28 resource::{CameraUniformData, SamplerId},
29 scene::{GpuMesh, ModelUniforms},
30 util::dynamic_uniform_buffer::DynamicUniformRingBuffer,
31};
32use khora_core::renderer::{traits::CommandEncoder, GraphicsDevice};
33use khora_data::assets::Assets;
34use khora_data::render::{RenderWorld, ShadowEntry};
35
36use super::atlas_2d::Atlas2D;
37use super::atlas_cube::AtlasCube;
38use super::pass;
39
40pub struct ShadowsLaneState {
48 pub pipeline: RwLock<Option<RenderPipelineId>>,
50 pub camera_layout: RwLock<Option<BindGroupLayoutId>>,
52 pub model_layout: RwLock<Option<BindGroupLayoutId>>,
54
55 pub atlas_2d: Atlas2D,
57 pub atlas_cube: AtlasCube,
59
60 pub shadow_sampler: RwLock<Option<SamplerId>>,
62
63 pub shadow_results: RwLock<HashMap<usize, ShadowEntry>>,
65
66 pub camera_ring: RwLock<Option<DynamicUniformRingBuffer>>,
69 pub model_ring: RwLock<Option<DynamicUniformRingBuffer>>,
71}
72
73impl Default for ShadowsLaneState {
74 fn default() -> Self {
75 Self {
76 pipeline: RwLock::new(None),
77 camera_layout: RwLock::new(None),
78 model_layout: RwLock::new(None),
79 atlas_2d: Atlas2D::default(),
80 atlas_cube: AtlasCube::default(),
81 shadow_sampler: RwLock::new(None),
82 shadow_results: RwLock::new(HashMap::new()),
83 camera_ring: RwLock::new(None),
84 model_ring: RwLock::new(None),
85 }
86 }
87}
88
89impl ShadowsLaneState {
90 #[allow(clippy::too_many_arguments)]
94 pub fn init_gpu(
95 &self,
96 device: &dyn GraphicsDevice,
97 pipeline_system: &dyn khora_core::renderer::traits::PipelineSystem,
98 atlas_2d_resolution: u32,
99 atlas_2d_max_lights: u32,
100 cube_face_resolution: u32,
101 cube_max_lights: u32,
102 label_prefix: &str,
103 ) -> Result<(), khora_core::renderer::error::RenderError> {
104 use crate::render_lane::util::lock::write_lock_render;
105 use khora_core::renderer::api::pipeline::enums::CompareFunction;
106 use khora_core::renderer::api::resource::{
107 AddressMode, FilterMode, MipmapFilterMode, SamplerDescriptor,
108 };
109 use std::borrow::Cow;
110
111 let camera_layout = pipeline_system.inline_layout(
115 device,
116 SHADOW_CAMERA_LAYOUT_LABEL,
117 &shadow_camera_layout_entries(),
118 )?;
119 let model_layout = pipeline_system.inline_layout(
120 device,
121 SHADOW_MODEL_LAYOUT_LABEL,
122 &shadow_model_layout_entries(),
123 )?;
124
125 let pipeline = pipeline_system.pipeline(device, &shadow_pipeline_spec())?;
128
129 *write_lock_render(&self.pipeline, "ShadowsLaneState.pipeline")? = Some(pipeline);
130 *write_lock_render(&self.camera_layout, "ShadowsLaneState.camera_layout")? =
131 Some(camera_layout);
132 *write_lock_render(&self.model_layout, "ShadowsLaneState.model_layout")? =
133 Some(model_layout);
134
135 use khora_core::renderer::api::util::dynamic_uniform_buffer::{
137 DEFAULT_MAX_ELEMENTS, MIN_UNIFORM_ALIGNMENT,
138 };
139 let camera_ring_capacity = atlas_2d_max_lights + cube_max_lights * 6;
140 let camera_ring = DynamicUniformRingBuffer::new(
141 device,
142 camera_layout,
143 0,
144 std::mem::size_of::<CameraUniformData>() as u32,
145 camera_ring_capacity,
146 MIN_UNIFORM_ALIGNMENT,
147 "Shadow Camera Ring",
148 )
149 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
150 let model_ring = DynamicUniformRingBuffer::new(
151 device,
152 model_layout,
153 0,
154 std::mem::size_of::<ModelUniforms>() as u32,
155 DEFAULT_MAX_ELEMENTS,
156 MIN_UNIFORM_ALIGNMENT,
157 "Shadow Model Ring",
158 )
159 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
160 *write_lock_render(&self.camera_ring, "ShadowsLaneState.camera_ring")? = Some(camera_ring);
161 *write_lock_render(&self.model_ring, "ShadowsLaneState.model_ring")? = Some(model_ring);
162
163 let label_2d = format!("{label_prefix} Atlas 2D");
165 let label_cube = format!("{label_prefix} Atlas Cube");
166 self.atlas_2d
167 .create(device, atlas_2d_resolution, atlas_2d_max_lights, &label_2d)?;
168 self.atlas_cube
169 .create(device, cube_face_resolution, cube_max_lights, &label_cube)?;
170
171 let sampler = device
173 .create_sampler(&SamplerDescriptor {
174 label: Some(Cow::Borrowed("Shadow Sampler")),
175 address_mode_u: AddressMode::ClampToEdge,
176 address_mode_v: AddressMode::ClampToEdge,
177 address_mode_w: AddressMode::ClampToEdge,
178 mag_filter: FilterMode::Linear,
179 min_filter: FilterMode::Linear,
180 mipmap_filter: MipmapFilterMode::Nearest,
181 lod_min_clamp: 0.0,
182 lod_max_clamp: 1.0,
183 compare: Some(CompareFunction::LessEqual),
184 anisotropy_clamp: 1,
185 border_color: None,
186 })
187 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
188 *write_lock_render(&self.shadow_sampler, "ShadowsLaneState.shadow_sampler")? =
189 Some(sampler);
190
191 log::info!(
192 "{label_prefix}: atlases initialized — 2D({}×{}, {} layers), Cube({}×{}, {} cubes = {} face layers)",
193 atlas_2d_resolution,
194 atlas_2d_resolution,
195 atlas_2d_max_lights,
196 cube_face_resolution,
197 cube_face_resolution,
198 cube_max_lights,
199 cube_max_lights * 6,
200 );
201
202 Ok(())
203 }
204
205 #[allow(clippy::too_many_arguments)] pub fn render(
211 &self,
212 atlas_2d_max_lights: u32,
213 cube_max_lights: u32,
214 strategy_label: &str,
215 render_world: &RenderWorld,
216 shadow_view: Option<&khora_data::flow::ShadowView>,
217 device: &dyn GraphicsDevice,
218 encoder: &mut dyn CommandEncoder,
219 gpu_meshes: &std::sync::RwLock<Assets<GpuMesh>>,
220 ) {
221 use super::atlas_2d as atlas_2d_mod;
222 use super::atlas_cube as atlas_cube_mod;
223 use khora_core::math::Mat4;
224 use khora_data::flow::ShadowMatrices;
225
226 let pipeline = if let Some(p) =
227 *crate::lock_or_log!(self.pipeline.read(), "ShadowsLaneState.pipeline")
228 {
229 p
230 } else {
231 return;
232 };
233
234 let atlas_2d_view = if let Some(v) = self.atlas_2d.view_id() {
235 v
236 } else {
237 return;
238 };
239 let cube_face_views = self.atlas_cube.face_views_snapshot();
240
241 let mut camera_lock =
242 crate::lock_or_log!(self.camera_ring.write(), "ShadowsLaneState.camera_ring");
243 let camera_ring = match camera_lock.as_mut() {
244 Some(r) => r,
245 None => {
246 log::warn!("{strategy_label}: camera_ring not initialized");
247 return;
248 }
249 };
250 camera_ring.advance();
251
252 let mut model_lock =
253 crate::lock_or_log!(self.model_ring.write(), "ShadowsLaneState.model_ring");
254 let model_ring = match model_lock.as_mut() {
255 Some(r) => r,
256 None => {
257 log::warn!("{strategy_label}: model_ring not initialized");
258 return;
259 }
260 };
261 model_ring.advance();
262
263 let gpu_meshes_guard =
264 crate::lock_or_log!(gpu_meshes.read(), "ShadowsLaneState.gpu_meshes");
265
266 let mut shadow_results = crate::lock_or_log!(
267 self.shadow_results.write(),
268 "ShadowsLaneState.shadow_results"
269 );
270 shadow_results.clear();
271
272 let mut next_atlas_2d_index: u32 = 0;
273 let mut next_cube_layer: u32 = 0;
274 let mut passes: Vec<pass::AttachmentPass> = Vec::new();
275
276 for (i, light) in render_world.lights.iter().enumerate() {
277 let Some(matrices) = shadow_view.and_then(|sv| sv.matrices.get(&i)) else {
278 continue;
279 };
280
281 match matrices {
282 ShadowMatrices::Single(view_proj) => {
283 if next_atlas_2d_index >= atlas_2d_max_lights {
284 log::warn!(
285 "{strategy_label}: 2D atlas full ({} / {}), dropping shadow for light {}",
286 next_atlas_2d_index,
287 atlas_2d_max_lights,
288 i
289 );
290 continue;
291 }
292 let layer = next_atlas_2d_index;
293 next_atlas_2d_index += 1;
294
295 shadow_results.insert(
296 i,
297 ShadowEntry::Atlas2D {
298 view_proj: *view_proj,
299 atlas_index: layer as i32,
300 },
301 );
302
303 if let Some(p) = atlas_2d_mod::collect_pass(
304 atlas_2d_view,
305 layer,
306 light.position,
307 *view_proj,
308 device,
309 render_world,
310 &gpu_meshes_guard,
311 camera_ring,
312 model_ring,
313 ) {
314 passes.push(p);
315 }
316 }
317 ShadowMatrices::Cube(face_view_projs) => {
318 if next_cube_layer >= cube_max_lights {
319 log::warn!(
320 "{strategy_label}: cube atlas full ({} / {}), dropping shadow for point light {}",
321 next_cube_layer,
322 cube_max_lights,
323 i
324 );
325 continue;
326 }
327 let cube_layer = next_cube_layer;
328 next_cube_layer += 1;
329
330 let far_plane = match &light.light_type {
331 khora_core::renderer::light::LightType::Point(p) => p.range,
332 _ => continue,
333 };
334
335 let face_view_projs_arr: [Mat4; 6] = **face_view_projs;
336 shadow_results.insert(
337 i,
338 ShadowEntry::Cube {
339 face_view_projs: Box::new(face_view_projs_arr),
340 cube_array_index: cube_layer as i32,
341 light_pos: light.position,
342 far_plane,
343 },
344 );
345
346 let cube_passes = atlas_cube_mod::collect_passes(
347 &cube_face_views,
348 cube_layer,
349 light.position,
350 &face_view_projs_arr,
351 device,
352 render_world,
353 &gpu_meshes_guard,
354 camera_ring,
355 model_ring,
356 );
357 passes.extend(cube_passes);
358 }
359 }
360 }
361
362 drop(model_lock);
363 drop(camera_lock);
364
365 for ap in &passes {
366 pass::record_depth_pass(encoder, &pipeline, ap);
367 }
368 }
369
370 pub fn shadow_bindings(&self) -> Option<khora_data::render::ShadowGpuBindings> {
374 super::bindings::build_bindings(
375 &self.atlas_2d,
376 &self.atlas_cube,
377 self.shadow_sampler.read().ok().and_then(|g| *g)?,
378 )
379 }
380
381 pub fn shutdown(&self, device: &dyn GraphicsDevice) {
384 if let Some(ring) = self.camera_ring.write().ok().and_then(|mut g| g.take()) {
385 ring.destroy(device);
386 }
387 if let Some(ring) = self.model_ring.write().ok().and_then(|mut g| g.take()) {
388 ring.destroy(device);
389 }
390 let _ = self.pipeline.write().map(|mut g| g.take());
394 let _ = self.camera_layout.write().map(|mut g| g.take());
395 let _ = self.model_layout.write().map(|mut g| g.take());
396 self.atlas_2d.destroy(device);
397 self.atlas_cube.destroy(device);
398 if let Some(sampler) = self.shadow_sampler.write().ok().and_then(|mut g| g.take()) {
399 if let Err(e) = device.destroy_sampler(sampler) {
400 log::warn!("ShadowsLaneState: failed to destroy sampler: {:?}", e);
401 }
402 }
403 }
404}
405
406const SHADOW_CAMERA_LAYOUT_LABEL: &str = "shadow_camera_layout";
410const SHADOW_MODEL_LAYOUT_LABEL: &str = "shadow_model_layout";
412
413fn shadow_camera_layout_entries() -> Vec<khora_core::renderer::api::command::BindGroupLayoutEntry> {
416 use khora_core::renderer::api::command::{
417 BindGroupLayoutEntry, BindingType, BufferBindingType,
418 };
419 use khora_core::renderer::api::util::ShaderStageFlags;
420 vec![BindGroupLayoutEntry {
421 binding: 0,
422 visibility: ShaderStageFlags::VERTEX,
423 ty: BindingType::Buffer {
424 ty: BufferBindingType::Uniform,
425 has_dynamic_offset: true,
426 min_binding_size: None,
427 },
428 }]
429}
430
431fn shadow_model_layout_entries() -> Vec<khora_core::renderer::api::command::BindGroupLayoutEntry> {
434 shadow_camera_layout_entries()
435}
436
437fn shadow_pipeline_spec() -> khora_core::renderer::api::pipeline::PipelineSpec {
440 use khora_core::renderer::api::pipeline::enums::{
441 CompareFunction, PrimitiveTopology, VertexFormat, VertexStepMode,
442 };
443 use khora_core::renderer::api::pipeline::state::{DepthBiasState, StencilFaceState};
444 use khora_core::renderer::api::pipeline::{
445 DepthStencilStateDescriptor, LayoutSpec, MultisampleStateDescriptor, PipelineSpec,
446 PrimitiveStateDescriptor, ShaderVariantKey, VertexAttributeDescriptor,
447 VertexBufferLayoutDescriptor,
448 };
449 use khora_core::renderer::api::util::{SampleCount, TextureFormat};
450 use std::borrow::Cow;
451
452 PipelineSpec {
453 label: "Shadow Pass Pipeline",
454 shader: "khora::pipelines::shadow_pass",
455 variant: ShaderVariantKey::empty(),
456 bind_group_layouts: vec![
457 LayoutSpec::Inline {
458 label: SHADOW_CAMERA_LAYOUT_LABEL,
459 entries: Cow::Owned(shadow_camera_layout_entries()),
460 },
461 LayoutSpec::Inline {
462 label: SHADOW_MODEL_LAYOUT_LABEL,
463 entries: Cow::Owned(shadow_model_layout_entries()),
464 },
465 ],
466 vertex_buffers: vec![VertexBufferLayoutDescriptor {
467 array_stride: 32,
468 step_mode: VertexStepMode::Vertex,
469 attributes: Cow::Owned(vec![VertexAttributeDescriptor {
470 format: VertexFormat::Float32x3,
471 offset: 0,
472 shader_location: 0,
473 }]),
474 }],
475 vs_entry: "vs_main",
476 fs_entry: None,
477 primitive: PrimitiveStateDescriptor {
478 topology: PrimitiveTopology::TriangleList,
479 ..Default::default()
480 },
481 depth_stencil: Some(DepthStencilStateDescriptor {
482 format: TextureFormat::Depth32Float,
483 depth_write_enabled: true,
484 depth_compare: CompareFunction::Less,
485 stencil_front: StencilFaceState::default(),
486 stencil_back: StencilFaceState::default(),
487 stencil_read_mask: 0,
488 stencil_write_mask: 0,
489 bias: DepthBiasState {
490 constant: 2,
491 slope_scale: 2.0,
492 clamp: 0.0,
493 },
494 }),
495 color_targets: vec![],
496 multisample: MultisampleStateDescriptor {
497 count: SampleCount::X1,
498 mask: !0,
499 alpha_to_coverage_enabled: false,
500 },
501 }
502}