Skip to main content

khora_lanes/render_lane/shadows_lane/
mod.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 lanes for [`ShadowAgent`].
16//!
17//! Per the SAA lane model, each lane in this module is a **distinct
18//! quality strategy**, all implementing the same shadow domain:
19//!
20//! - [`StandardShadowsLane`] — full quality (2048² atlas + 512² cube)
21//! - [`MediumShadowsLane`]   — same algorithm, half resolution (1024² + 256²)
22//! - [`LowResShadowsLane`]   — same algorithm, quarter resolution (512² + 128²)
23//!
24//! All produce identical output types (`ShadowGpuBindings` +
25//! `ShadowEntries`); lit consumer lanes never need to know which one
26//! ran. The agent picks one per frame via `apply_budget`.
27//!
28//! Shared infrastructure (atlas creation, pass recording, draw-cmd
29//! building) lives in [`algo`] as free functions that take dimensions
30//! as parameters — never as a config struct injected by the agent.
31
32pub mod algo;
33mod low_res;
34mod medium;
35mod standard;
36
37pub use low_res::{LowResShadowsLane, STRATEGY_NAME as LOW_RES_STRATEGY_NAME};
38pub use medium::{MediumShadowsLane, STRATEGY_NAME as MEDIUM_STRATEGY_NAME};
39pub use standard::{StandardShadowsLane, STRATEGY_NAME as STANDARD_STRATEGY_NAME};
40
41use khora_core::lane::{LaneContext, LaneError, Ref, Slot};
42use khora_core::renderer::api::scene::GpuMesh;
43use khora_core::renderer::{traits::CommandEncoder, GraphicsDevice};
44use khora_data::assets::Assets;
45use khora_data::render::RenderWorld;
46
47use algo::ShadowsLaneState;
48
49/// Cost estimate shared by both strategies — roughly `casters × meshes`
50/// with point lights weighted 6× (six cube passes). Each lane scales
51/// this further to reflect its own quality.
52pub(crate) fn cost_estimate(render_world: &RenderWorld) -> f32 {
53    let mut cube_passes = 0u32;
54    for light in &render_world.lights {
55        let (enabled, is_point) = match &light.light_type {
56            khora_core::renderer::light::LightType::Directional(l) => (l.shadow_enabled, false),
57            khora_core::renderer::light::LightType::Spot(l) => (l.shadow_enabled, false),
58            khora_core::renderer::light::LightType::Point(l) => (l.shadow_enabled, true),
59        };
60        if enabled {
61            cube_passes += if is_point { 6 } else { 1 };
62        }
63    }
64    (cube_passes as f32) * (render_world.meshes.len() as f32) * 0.001
65}
66
67/// Shared `Lane::execute` body — each concrete strategy delegates here
68/// with its own `(atlas_2d_max_lights, cube_max_lights)` constants and
69/// `strategy_label` for diagnostics. Eliminates duplication while
70/// keeping each lane's quality intrinsic to its own type.
71pub(crate) fn execute_shared(
72    state: &ShadowsLaneState,
73    atlas_2d_max_lights: u32,
74    cube_max_lights: u32,
75    strategy_label: &'static str,
76    ctx: &mut LaneContext,
77) -> Result<(), LaneError> {
78    // Phase 1: Render shadow maps.
79    {
80        let device = ctx
81            .get::<std::sync::Arc<dyn GraphicsDevice>>()
82            .ok_or(LaneError::missing("Arc<dyn GraphicsDevice>"))?
83            .clone();
84        let gpu_meshes = ctx
85            .get::<std::sync::Arc<std::sync::RwLock<Assets<GpuMesh>>>>()
86            .ok_or(LaneError::missing("Arc<RwLock<Assets<GpuMesh>>>"))?
87            .clone();
88        let encoder = ctx
89            .get::<Slot<dyn CommandEncoder>>()
90            .ok_or(LaneError::missing("Slot<dyn CommandEncoder>"))?
91            .get();
92        let render_world = ctx
93            .get::<Ref<RenderWorld>>()
94            .ok_or(LaneError::missing("Ref<RenderWorld>"))?
95            .get();
96        let shadow_view = ctx
97            .get::<Ref<khora_data::flow::ShadowView>>()
98            .map(|r| r.get());
99
100        state.render(
101            atlas_2d_max_lights,
102            cube_max_lights,
103            strategy_label,
104            render_world,
105            shadow_view,
106            device.as_ref(),
107            encoder,
108            &gpu_meshes,
109        );
110    }
111
112    // Phase 2: Publish a single `ShadowFrame` slot into the per-frame
113    // `OutputDeck`. This is the **only** cross-lane channel — no
114    // `LaneContext::insert`, no `FrameContext` hoist, no shared
115    // resource. Lit consumer lanes read `deck.slot::<ShadowFrame>()`.
116    if let Some(deck_slot) = ctx.get::<Slot<khora_core::lane::OutputDeck>>() {
117        let deck = deck_slot.get();
118        let frame = deck.slot::<khora_core::renderer::api::shadow::ShadowFrame>();
119        frame.bindings = state.shadow_bindings();
120        if let Ok(results) = state.shadow_results.read() {
121            frame.entries.0.clear();
122            for (i, entry) in results.iter() {
123                frame.entries.insert(*i, entry.clone());
124            }
125        }
126    }
127
128    Ok(())
129}