khora_lanes/render_lane/shadows_lane/medium.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//! Medium-resolution shadows lane.
16//!
17//! Same algorithm as [`super::StandardShadowsLane`] (CSM / spot / cube
18//! point), with half the per-side resolution everywhere — the middle
19//! rung between Standard and [`super::LowResShadowsLane`]. Same output
20//! contract: produces a [`khora_data::render::ShadowGpuBindings`] bundle
21//! plus [`khora_data::render::ShadowEntries`]; lit consumer lanes never
22//! need to know which strategy ran.
23//!
24//! `ShadowAgent` selects this lane for `StrategyId::Balanced`, giving
25//! GORNA a genuine intermediate trade-off instead of a binary
26//! Standard/LowRes choice.
27
28use std::any::Any;
29
30use khora_core::lane::{Lane, LaneContext, LaneError, LaneKind, Ref};
31use khora_core::renderer::GraphicsDevice;
32use khora_data::render::RenderWorld;
33
34use super::algo::ShadowsLaneState;
35
36/// Stable strategy name advertised to the agent / GORNA.
37pub const STRATEGY_NAME: &str = "MediumShadows";
38
39/// Medium-resolution shadows: 1024² × 4-layer 2D atlas + 256² × 4-cube
40/// cube atlas. Same algorithm and output types as
41/// [`super::StandardShadowsLane`]; half the per-side resolution —
42/// VRAM ≈ 16 + 6 MiB instead of 64 + 24 MiB.
43#[derive(Default)]
44pub struct MediumShadowsLane {
45 state: ShadowsLaneState,
46}
47
48impl MediumShadowsLane {
49 /// 2D depth atlas resolution (per layer).
50 pub const ATLAS_2D_RESOLUTION: u32 = 1024;
51 /// Number of directional / spot shadow casters per frame.
52 pub const ATLAS_2D_MAX_LIGHTS: u32 = 4;
53 /// Cube atlas per-face resolution.
54 pub const CUBE_FACE_RESOLUTION: u32 = 256;
55 /// Number of point shadow casters per frame.
56 pub const CUBE_MAX_LIGHTS: u32 = 4;
57
58 /// Creates a new lane in its uninitialised state.
59 pub fn new() -> Self {
60 Self::default()
61 }
62}
63
64impl Lane for MediumShadowsLane {
65 fn strategy_name(&self) -> &'static str {
66 STRATEGY_NAME
67 }
68
69 fn lane_kind(&self) -> LaneKind {
70 LaneKind::Shadow
71 }
72
73 fn estimate_cost(&self, ctx: &LaneContext) -> f32 {
74 let render_world = match ctx.get::<Ref<RenderWorld>>() {
75 Some(slot) => slot.get(),
76 None => return 0.75,
77 };
78 // Quarter the per-texel cost of Standard at the same scene (4× fewer
79 // texels per layer), floored so GORNA still sees real work.
80 (super::cost_estimate(render_world) * 0.25).max(0.1)
81 }
82
83 fn on_initialize(&self, ctx: &mut LaneContext) -> Result<(), LaneError> {
84 let device = ctx
85 .get::<std::sync::Arc<dyn GraphicsDevice>>()
86 .ok_or(LaneError::missing("Arc<dyn GraphicsDevice>"))?
87 .clone();
88 let pipeline_system = ctx
89 .get::<std::sync::Arc<dyn khora_core::renderer::traits::PipelineSystem>>()
90 .ok_or(LaneError::missing("Arc<dyn PipelineSystem>"))?
91 .clone();
92 self.state
93 .init_gpu(
94 device.as_ref(),
95 pipeline_system.as_ref(),
96 Self::ATLAS_2D_RESOLUTION,
97 Self::ATLAS_2D_MAX_LIGHTS,
98 Self::CUBE_FACE_RESOLUTION,
99 Self::CUBE_MAX_LIGHTS,
100 "MediumShadows",
101 )
102 .map_err(|e| LaneError::InitializationFailed(Box::new(e)))
103 }
104
105 fn execute(&self, ctx: &mut LaneContext) -> Result<(), LaneError> {
106 super::execute_shared(
107 &self.state,
108 Self::ATLAS_2D_MAX_LIGHTS,
109 Self::CUBE_MAX_LIGHTS,
110 STRATEGY_NAME,
111 ctx,
112 )
113 }
114
115 fn on_shutdown(&self, ctx: &mut LaneContext) {
116 if let Some(device) = ctx.get::<std::sync::Arc<dyn GraphicsDevice>>() {
117 self.state.shutdown(device.as_ref());
118 }
119 }
120
121 fn as_any(&self) -> &dyn Any {
122 self
123 }
124
125 fn as_any_mut(&mut self) -> &mut dyn Any {
126 self
127 }
128}