Skip to main content

khora_lanes/render_lane/shadows_lane/algo/
atlas_2d.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//! 2D shadow atlas — directional and spot lights.
16//!
17//! Owns the `texture_depth_2d_array` atlas the lane samples for non-point
18//! shadow casters. One layer per shadow-casting directional / spot light.
19//!
20//! The dimensions are passed in by the lane — different quality tiers
21//! (`StandardShadowsLane`, `LowResShadowsLane`, …) use different
22//! resolutions but the algorithm is identical.
23
24use std::borrow::Cow;
25use std::sync::RwLock;
26
27use khora_core::math::{Extent3D, Mat4};
28use khora_core::renderer::api::resource::{
29    CameraUniformData, ImageAspect, TextureDescriptor, TextureDimension, TextureId, TextureUsage,
30    TextureViewDescriptor, TextureViewDimension, TextureViewId,
31};
32use khora_core::renderer::api::util::dynamic_uniform_buffer::DynamicUniformRingBuffer;
33use khora_core::renderer::api::util::{SampleCount, TextureFormat};
34use khora_core::renderer::error::RenderError;
35use khora_core::renderer::GraphicsDevice;
36
37use super::pass::{build_draw_cmds, AttachmentPass};
38
39/// 2D atlas resources — owned by whichever shadows lane created it.
40pub struct Atlas2D {
41    /// Texture id (depth-only).
42    pub texture: RwLock<Option<TextureId>>,
43    /// `D2Array` view bound by the lit shader at
44    /// [`khora_data::render::shadow_bindings::binding::ATLAS_2D`].
45    pub view: RwLock<Option<TextureViewId>>,
46}
47
48impl Default for Atlas2D {
49    fn default() -> Self {
50        Self {
51            texture: RwLock::new(None),
52            view: RwLock::new(None),
53        }
54    }
55}
56
57impl Atlas2D {
58    /// Creates the depth atlas + array view at the requested dimensions.
59    /// Idempotent — call once at `on_initialize`.
60    ///
61    /// `resolution` is the per-layer side length in texels;
62    /// `max_lights` is the number of array layers (== number of shadow
63    /// casters this lane will accept per frame).
64    pub fn create(
65        &self,
66        device: &dyn GraphicsDevice,
67        resolution: u32,
68        max_lights: u32,
69        label: &str,
70    ) -> Result<(), RenderError> {
71        use crate::render_lane::util::lock::write_lock_render;
72
73        let texture = device
74            .create_texture(&TextureDescriptor {
75                label: Some(Cow::Owned(format!("{label} Texture"))),
76                size: Extent3D {
77                    width: resolution,
78                    height: resolution,
79                    depth_or_array_layers: max_lights,
80                },
81                mip_level_count: 1,
82                sample_count: SampleCount::X1,
83                dimension: TextureDimension::D2,
84                format: TextureFormat::Depth32Float,
85                usage: TextureUsage::DEPTH_STENCIL_ATTACHMENT | TextureUsage::TEXTURE_BINDING,
86                view_formats: Cow::Borrowed(&[]),
87            })
88            .map_err(RenderError::ResourceError)?;
89
90        let view = device
91            .create_texture_view(
92                texture,
93                &TextureViewDescriptor {
94                    label: Some(Cow::Owned(format!("{label} View"))),
95                    format: Some(TextureFormat::Depth32Float),
96                    dimension: Some(TextureViewDimension::D2Array),
97                    aspect: ImageAspect::DepthOnly,
98                    base_mip_level: 0,
99                    mip_level_count: Some(1),
100                    base_array_layer: 0,
101                    array_layer_count: Some(max_lights),
102                },
103            )
104            .map_err(RenderError::ResourceError)?;
105
106        *write_lock_render(&self.texture, "Atlas2D.texture")? = Some(texture);
107        *write_lock_render(&self.view, "Atlas2D.view")? = Some(view);
108        Ok(())
109    }
110
111    /// Returns the current view id, if any.
112    pub fn view_id(&self) -> Option<TextureViewId> {
113        self.view.read().ok().and_then(|g| *g)
114    }
115
116    /// Destroys the atlas resources. No-op if already destroyed.
117    pub fn destroy(&self, device: &dyn GraphicsDevice) {
118        if let Some(view) = self.view.write().ok().and_then(|mut g| g.take()) {
119            if let Err(e) = device.destroy_texture_view(view) {
120                log::warn!("Atlas2D: failed to destroy view: {:?}", e);
121            }
122        }
123        if let Some(texture) = self.texture.write().ok().and_then(|mut g| g.take()) {
124            if let Err(e) = device.destroy_texture(texture) {
125                log::warn!("Atlas2D: failed to destroy texture: {:?}", e);
126            }
127        }
128    }
129}
130
131/// Records one shadow pass for a directional or spot light.
132///
133/// Pushes the camera UBO into the camera ring, builds the draw command
134/// list, and returns an [`AttachmentPass`] ready to be replayed by
135/// [`super::pass::record_depth_pass`]. Returns `None` if the camera
136/// uniform push fails.
137#[allow(clippy::too_many_arguments)]
138pub fn collect_pass(
139    atlas_view: TextureViewId,
140    layer: u32,
141    light_pos: khora_core::math::Vec3,
142    view_proj: Mat4,
143    device: &dyn GraphicsDevice,
144    render_world: &khora_data::render::RenderWorld,
145    gpu_meshes: &khora_data::assets::Assets<khora_core::renderer::api::scene::GpuMesh>,
146    camera_ring: &mut DynamicUniformRingBuffer,
147    model_ring: &mut DynamicUniformRingBuffer,
148) -> Option<AttachmentPass> {
149    let camera_data = CameraUniformData {
150        view_projection: view_proj.to_cols_array_2d(),
151        camera_position: [light_pos.x, light_pos.y, light_pos.z, 1.0],
152    };
153    let camera_offset = match camera_ring.push(device, bytemuck::bytes_of(&camera_data)) {
154        Ok(off) => off,
155        Err(e) => {
156            log::error!("Atlas2D: failed to push camera uniform: {:?}", e);
157            return None;
158        }
159    };
160    let camera_bg = *camera_ring.current_bind_group();
161
162    let draw_cmds = build_draw_cmds(device, render_world, gpu_meshes, model_ring);
163
164    Some(AttachmentPass {
165        target_view: atlas_view,
166        base_array_layer: layer,
167        camera_bg,
168        camera_offset,
169        draw_cmds,
170    })
171}