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}