khora_lanes/render_lane/shadows_lane/low_res.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//! Low-resolution shadows lane.
16//!
17//! Same algorithm as [`super::StandardShadowsLane`] (CSM / spot / cube
18//! point), but with smaller atlases — quarter the per-side resolution
19//! everywhere. Same output contract: produces a
20//! [`khora_data::render::ShadowGpuBindings`] bundle plus
21//! [`khora_data::render::ShadowEntries`]; lit consumer lanes never need
22//! to know which strategy ran.
23//!
24//! `ShadowAgent` selects this lane when GORNA reports a tight time /
25//! VRAM budget (`StrategyId::LowPower`).
26
27use std::any::Any;
28
29use khora_core::lane::{Lane, LaneContext, LaneError, LaneKind, Ref};
30use khora_core::renderer::GraphicsDevice;
31use khora_data::render::RenderWorld;
32
33use super::algo::ShadowsLaneState;
34
35/// Stable strategy name advertised to the agent / GORNA.
36pub const STRATEGY_NAME: &str = "LowResShadows";
37
38/// Low-resolution shadows: 512² × 4-layer 2D atlas + 128² × 4-cube cube
39/// atlas. Same algorithm and output types as
40/// [`super::StandardShadowsLane`]; just lower-resolution textures —
41/// VRAM ≈ 1.5 + 0.25 MiB instead of 64 + 24 MiB.
42#[derive(Default)]
43pub struct LowResShadowsLane {
44 state: ShadowsLaneState,
45}
46
47impl LowResShadowsLane {
48 /// 2D depth atlas resolution (per layer).
49 pub const ATLAS_2D_RESOLUTION: u32 = 512;
50 /// Number of directional / spot shadow casters per frame.
51 pub const ATLAS_2D_MAX_LIGHTS: u32 = 4;
52 /// Cube atlas per-face resolution.
53 pub const CUBE_FACE_RESOLUTION: u32 = 128;
54 /// Number of point shadow casters per frame.
55 pub const CUBE_MAX_LIGHTS: u32 = 4;
56
57 /// Creates a new lane in its uninitialised state.
58 pub fn new() -> Self {
59 Self::default()
60 }
61}
62
63impl Lane for LowResShadowsLane {
64 fn strategy_name(&self) -> &'static str {
65 STRATEGY_NAME
66 }
67
68 fn lane_kind(&self) -> LaneKind {
69 LaneKind::Shadow
70 }
71
72 fn estimate_cost(&self, ctx: &LaneContext) -> f32 {
73 let render_world = match ctx.get::<Ref<RenderWorld>>() {
74 Some(slot) => slot.get(),
75 None => return 0.5,
76 };
77 // Roughly 1/16th the per-texel cost of Standard at the same scene
78 // (16× fewer texels per layer), capped so GORNA always sees this
79 // strategy as cheaper.
80 (super::cost_estimate(render_world) * 0.0625).max(0.05)
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 "LowResShadows",
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}