Skip to main content

khora_lanes/render_lane/shadows_lane/algo/
atlas_cube.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//! Cube shadow atlas — point lights.
16//!
17//! Owns the `texture_depth_cube_array` atlas the lane samples for
18//! omnidirectional shadow casters. Six layers per shadow-casting point
19//! light (one per cube face).
20//!
21//! The dimensions are passed in by the lane — different quality tiers
22//! pick different `face_resolution` and `max_cubes`; the algorithm is
23//! identical.
24
25use std::borrow::Cow;
26use std::sync::RwLock;
27
28use khora_core::math::{Extent3D, Mat4};
29use khora_core::renderer::api::resource::{
30    CameraUniformData, ImageAspect, TextureDescriptor, TextureDimension, TextureId, TextureUsage,
31    TextureViewDescriptor, TextureViewDimension, TextureViewId,
32};
33use khora_core::renderer::api::util::dynamic_uniform_buffer::DynamicUniformRingBuffer;
34use khora_core::renderer::api::util::{SampleCount, TextureFormat};
35use khora_core::renderer::error::RenderError;
36use khora_core::renderer::GraphicsDevice;
37
38use super::pass::{build_draw_cmds, AttachmentPass};
39
40/// Cube atlas resources — owned by whichever shadows lane created it.
41pub struct AtlasCube {
42    /// Underlying 2D-array texture id.
43    pub texture: RwLock<Option<TextureId>>,
44    /// `CubeArray` view bound by the lit shader at
45    /// [`khora_data::render::shadow_bindings::binding::ATLAS_CUBE`].
46    pub view: RwLock<Option<TextureViewId>>,
47    /// Per-face 2D views used as depth render targets when rasterising
48    /// each cube face. Indexed `cube_layer * 6 + face_index` (with
49    /// `face_index` matching [`khora_core::math::CubeFace::index`]).
50    pub face_views: RwLock<Vec<TextureViewId>>,
51}
52
53impl Default for AtlasCube {
54    fn default() -> Self {
55        Self {
56            texture: RwLock::new(None),
57            view: RwLock::new(None),
58            face_views: RwLock::new(Vec::new()),
59        }
60    }
61}
62
63impl AtlasCube {
64    /// Creates the cube depth atlas + cube-array view + per-face views at
65    /// the requested dimensions. Idempotent — call once at
66    /// `on_initialize`.
67    pub fn create(
68        &self,
69        device: &dyn GraphicsDevice,
70        face_resolution: u32,
71        max_cubes: u32,
72        label: &str,
73    ) -> Result<(), RenderError> {
74        use crate::render_lane::util::lock::write_lock_render;
75
76        let cube_layers = max_cubes * 6;
77
78        let texture = device
79            .create_texture(&TextureDescriptor {
80                label: Some(Cow::Owned(format!("{label} Texture"))),
81                size: Extent3D {
82                    width: face_resolution,
83                    height: face_resolution,
84                    depth_or_array_layers: cube_layers,
85                },
86                mip_level_count: 1,
87                sample_count: SampleCount::X1,
88                dimension: TextureDimension::D2,
89                format: TextureFormat::Depth32Float,
90                usage: TextureUsage::DEPTH_STENCIL_ATTACHMENT | TextureUsage::TEXTURE_BINDING,
91                view_formats: Cow::Borrowed(&[]),
92            })
93            .map_err(RenderError::ResourceError)?;
94
95        let view = device
96            .create_texture_view(
97                texture,
98                &TextureViewDescriptor {
99                    label: Some(Cow::Owned(format!("{label} View"))),
100                    format: Some(TextureFormat::Depth32Float),
101                    dimension: Some(TextureViewDimension::CubeArray),
102                    aspect: ImageAspect::DepthOnly,
103                    base_mip_level: 0,
104                    mip_level_count: Some(1),
105                    base_array_layer: 0,
106                    array_layer_count: Some(cube_layers),
107                },
108            )
109            .map_err(RenderError::ResourceError)?;
110
111        let mut face_views = Vec::with_capacity(cube_layers as usize);
112        for layer in 0..cube_layers {
113            let face_view = device
114                .create_texture_view(
115                    texture,
116                    &TextureViewDescriptor {
117                        label: Some(Cow::Owned(format!("{label} Face View [{}]", layer))),
118                        format: Some(TextureFormat::Depth32Float),
119                        dimension: Some(TextureViewDimension::D2),
120                        aspect: ImageAspect::DepthOnly,
121                        base_mip_level: 0,
122                        mip_level_count: Some(1),
123                        base_array_layer: layer,
124                        array_layer_count: Some(1),
125                    },
126                )
127                .map_err(RenderError::ResourceError)?;
128            face_views.push(face_view);
129        }
130
131        *write_lock_render(&self.texture, "AtlasCube.texture")? = Some(texture);
132        *write_lock_render(&self.view, "AtlasCube.view")? = Some(view);
133        *write_lock_render(&self.face_views, "AtlasCube.face_views")? = face_views;
134        Ok(())
135    }
136
137    /// Returns the cube-array view id, if any.
138    pub fn view_id(&self) -> Option<TextureViewId> {
139        self.view.read().ok().and_then(|g| *g)
140    }
141
142    /// Returns a clone of the per-face view list (cheap — just `Vec<TextureViewId>`).
143    pub fn face_views_snapshot(&self) -> Vec<TextureViewId> {
144        self.face_views
145            .read()
146            .map(|g| g.clone())
147            .unwrap_or_default()
148    }
149
150    /// Destroys the atlas resources. No-op if already destroyed.
151    pub fn destroy(&self, device: &dyn GraphicsDevice) {
152        if let Ok(mut face_views) = self.face_views.write() {
153            for view in face_views.drain(..) {
154                if let Err(e) = device.destroy_texture_view(view) {
155                    log::warn!("AtlasCube: failed to destroy face view: {:?}", e);
156                }
157            }
158        }
159        if let Some(view) = self.view.write().ok().and_then(|mut g| g.take()) {
160            if let Err(e) = device.destroy_texture_view(view) {
161                log::warn!("AtlasCube: failed to destroy view: {:?}", e);
162            }
163        }
164        if let Some(texture) = self.texture.write().ok().and_then(|mut g| g.take()) {
165            if let Err(e) = device.destroy_texture(texture) {
166                log::warn!("AtlasCube: failed to destroy texture: {:?}", e);
167            }
168        }
169    }
170}
171
172/// Records six shadow passes for one point light (one per cube face).
173#[allow(clippy::too_many_arguments)]
174pub fn collect_passes(
175    cube_face_views: &[TextureViewId],
176    cube_layer: u32,
177    light_pos: khora_core::math::Vec3,
178    face_view_projs: &[Mat4; 6],
179    device: &dyn GraphicsDevice,
180    render_world: &khora_data::render::RenderWorld,
181    gpu_meshes: &khora_data::assets::Assets<khora_core::renderer::api::scene::GpuMesh>,
182    camera_ring: &mut DynamicUniformRingBuffer,
183    model_ring: &mut DynamicUniformRingBuffer,
184) -> Vec<AttachmentPass> {
185    let mut passes = Vec::with_capacity(6);
186    for (face_idx, face_vp) in face_view_projs.iter().enumerate() {
187        let face_view_index = (cube_layer as usize) * 6 + face_idx;
188        let Some(face_view) = cube_face_views.get(face_view_index).copied() else {
189            log::error!(
190                "AtlasCube: missing cube face view {} (cube_layer={}, face={})",
191                face_view_index,
192                cube_layer,
193                face_idx
194            );
195            continue;
196        };
197
198        let camera_data = CameraUniformData {
199            view_projection: face_vp.to_cols_array_2d(),
200            camera_position: [light_pos.x, light_pos.y, light_pos.z, 1.0],
201        };
202        let camera_offset = match camera_ring.push(device, bytemuck::bytes_of(&camera_data)) {
203            Ok(off) => off,
204            Err(e) => {
205                log::error!("AtlasCube: failed to push cube face uniform: {:?}", e);
206                continue;
207            }
208        };
209        let camera_bg = *camera_ring.current_bind_group();
210
211        let draw_cmds = build_draw_cmds(device, render_world, gpu_meshes, model_ring);
212        passes.push(AttachmentPass {
213            // The depth attachment path recreates the render target from the
214            // source texture + this layer index (it ignores the view's own
215            // layer), so it MUST be the absolute cube-array layer — passing 0
216            // would collapse every face of every cube into layer 0. Matches the
217            // 2D atlas, which passes its real cascade layer.
218            target_view: face_view,
219            base_array_layer: face_view_index as u32,
220            camera_bg,
221            camera_offset,
222            draw_cmds,
223        });
224    }
225    passes
226}