1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
//! `AwsmRenderer` public setters / getters for shadow state.
//!
//! These wrap the per-`Shadows` API so callers don't have to hold a
//! mutable borrow of two fields at once, and so the lights buffer
//! (which bakes the shadow descriptor index into `LightPacked.row4.z`
//! at pack time) can be marked dirty in the same call that mutates
//! the source-of-truth shadow params.
use crate::{
lights::{AwsmLightError, Light, LightKey},
shadows::{
config::ShadowsConfig,
error::AwsmShadowError,
light_shadow::{LightShadowParams, MeshShadowFlags},
},
AwsmRenderer,
};
impl AwsmRenderer {
/// Compiles the shadow caster + EVSM pipelines if at least one
/// shadow-casting light is registered AND the pipelines aren't
/// already compiled. Idempotent and cheap when nothing to do.
///
/// Block B.1 + B.2 lazy-compile entry point. Cold-boot leaves
/// shadow pipelines uncompiled even when `features.shadows == true`.
/// The first shadow-caster (added via [`Self::insert_light`] with a
/// casting `LightShadowParams`, [`Self::set_light_shadow_params`],
/// or [`Self::update_light_shadow`] that flips `cast` on) makes
/// the pipelines necessary — call this then `.await` before the
/// next `render()` to guarantee shadows draw that frame. If the
/// caller forgets, the dispatch sites in
/// `shadows::render_pass::record` and `dispatch_evsm` issue a
/// one-shot `warn_pipeline_not_compiled` and silently skip the
/// pass that frame; subsequent frames also skip until pipelines
/// land.
pub async fn ensure_shadow_pipelines_compiled(&mut self) -> crate::error::Result<()> {
if !self.shadows.any_active() || self.shadows.pipelines_compiled() {
return Ok(());
}
let crate::pipelines::Pipelines {
render: render_pipelines,
compute: compute_pipelines,
} = &mut self.pipelines;
self.shadows
.ensure_pipelines_compiled(
&self.gpu,
&self.shaders,
&self.pipeline_layouts,
render_pipelines,
compute_pipelines,
)
.await?;
Ok(())
}
/// Replaces the renderer-wide shadow config. Player / runtime
/// equivalent of the editor's "shadows" inspector — load the
/// `ShadowsConfig` from disk (via `awsm_renderer_scene` → `into()`
/// or a custom build pipeline) and push it in at startup.
///
/// Resource-shaped fields (`atlas_size`, `max_point_shadows`,
/// `point_shadow_resolution`, `evsm_atlas_size`) are baked into
/// `Shadows::new` at construction time, so changing them at
/// runtime requires recreating the renderer. The other tunables
/// (SSCS toggle, blur radius, exponent, debug overlay) take
/// effect on the next `render()` call.
pub fn set_shadows_config(&mut self, config: ShadowsConfig) {
self.shadows.set_config(config);
}
/// Returns the current renderer-wide shadow config.
pub fn shadows_config(&self) -> &ShadowsConfig {
self.shadows.config()
}
/// Sets a light's shadow parameters. Pass
/// `LightShadowParams { cast: false, .. }` to disable shadows for a
/// specific light while keeping the light itself. Takes effect on
/// the next `render()` call.
pub fn set_light_shadow_params(
&mut self,
key: LightKey,
params: LightShadowParams,
) -> Result<(), AwsmShadowError> {
self.shadows.params.insert(key, params);
// The light's `shadow_index` is baked into `LightPacked.row4.z`
// at pack time via the `shadow_index_for` callback in
// `Lights::write_gpu`. Changing shadow params can change that
// index (cast=false → SHADOW_INDEX_NONE, or a freshly assigned
// descriptor_base when shadows toggle on), so the cached pack
// must be invalidated even though the light itself didn't move.
self.lights.mark_punctual_dirty();
Ok(())
}
/// Returns the current shadow parameters for a light, or `None` if
/// the light has never had shadow params set.
pub fn light_shadow_params(&self, key: LightKey) -> Option<&LightShadowParams> {
self.shadows.params.get(key)
}
/// Inserts a light and (optionally) its authored shadow params in
/// one transaction. Pass `Some(LightShadowParams { cast: true, .. })`
/// to enable shadows immediately; pass `None` (or
/// `Some(LightShadowParams::default())`) for an unshadowed light
/// — callers can still register shadow params later via
/// [`Self::set_light_shadow_params`], but threading them through
/// here makes the common "insert a casting light" path one call,
/// and prevents the two-step pattern from being interrupted by a
/// frame that sees the light without its shadow registration.
pub fn insert_light(
&mut self,
light: Light,
shadow_params: Option<LightShadowParams>,
) -> std::result::Result<LightKey, AwsmLightError> {
let key = self.lights.insert(light)?;
if let Some(params) = shadow_params {
self.shadows.params.insert(key, params);
}
Ok(key)
}
/// Removes a light AND every piece of shadow state keyed on it:
/// the authored shadow params, the cube-pool slot cache (and the
/// slot's owner field), and any throttle history. Without this
/// coordinated removal, `params` would keep a stale entry with
/// `cast = true` forever — `caster_count` / `any_active` would
/// stay nonzero, and `write_gpu`'s per-frame caster-AABB sweep
/// would keep running for a light that no longer exists.
pub fn remove_light(&mut self, key: LightKey) {
self.shadows.on_light_removed(key);
self.lights.remove(key);
}
/// Removes every light, dropping all per-light shadow state in
/// lockstep (params / cube slots / throttle history). The
/// shadow-clear runs first so a subsequent per-frame
/// `Shadows::write_gpu` doesn't see lights for which the slotmap
/// entry is already gone.
pub fn clear_lights(&mut self) {
self.shadows.clear_all_lights();
self.lights.clear();
}
/// Mutates a light in place, detecting and recovering from a
/// **kind change** (Directional ↔ Point ↔ Spot). Without this guard
/// a Directional→Point flip would leave the previous cube-pool
/// allocation (or lack of one) attached to the new kind: a point
/// light without a cube slot, or a directional light still owning
/// one. Detect the discriminant change and re-run the shadow
/// side's add/remove handshake.
///
/// `params` are preserved across the kind flip so the user's
/// `cast = true` survives.
pub fn update_light<F: FnOnce(&mut Light)>(
&mut self,
key: LightKey,
f: F,
) -> Result<(), AwsmLightError> {
let prev_kind = match self.lights.get(key) {
Some(light) => light.kind_discriminant(),
None => return Ok(()),
};
// Stash the authored shadow params before we touch shadow state.
let saved_params = self.shadows.params.get(key).cloned();
self.lights.update(key, f);
let new_kind = match self.lights.get(key) {
Some(light) => light.kind_discriminant(),
None => return Ok(()),
};
if new_kind != prev_kind {
// Drop every shadow-side per-light record (cube slot, throttle,
// record list) and reinstate the saved params under the new
// kind. Next `Shadows::write_gpu` allocates fresh views.
self.shadows.on_light_removed(key);
if let Some(params) = saved_params {
self.shadows.params.insert(key, params);
}
self.lights.mark_punctual_dirty();
}
Ok(())
}
/// Mutates a light's shadow params in place. Convenience over the
/// get-clone-mutate-set pattern.
pub fn update_light_shadow<F: FnOnce(&mut LightShadowParams)>(
&mut self,
key: LightKey,
f: F,
) -> Result<(), AwsmShadowError> {
if let Some(params) = self.shadows.params.get_mut(key) {
f(params);
// See `set_light_shadow_params` — the baked `shadow_index`
// in the lights buffer must be reconciled.
self.lights.mark_punctual_dirty();
Ok(())
} else {
Err(AwsmShadowError::UnknownLight)
}
}
/// Sets a mesh's shadow flags. Takes effect on the next `render()`.
pub fn set_mesh_shadow_flags(
&mut self,
key: crate::meshes::MeshKey,
flags: MeshShadowFlags,
) -> Result<(), AwsmShadowError> {
let mesh = self
.meshes
.get_mut(key)
.map_err(|_| AwsmShadowError::UnknownMesh)?;
let receive_changed = mesh.receive_shadows != flags.receive;
let cast_changed = mesh.cast_shadows != flags.cast;
mesh.cast_shadows = flags.cast;
mesh.receive_shadows = flags.receive;
// §B: a cast-flag flip changes the shadow caster set without changing the
// mesh count, so bump the caster-set revision — otherwise a cached view
// whose view-projection didn't drift would keep a stale shadow.
if cast_changed {
self.shadows.bump_shadow_caster_revision();
}
// `cast_shadows` is read CPU-side by the shadow render pass at
// draw time — no GPU state to update. `receive_shadows` is
// packed into `MaterialMeshMeta.receive_shadows` and read by
// the lighting shader; patch it in place so the GPU buffer
// doesn't keep the stale value.
if receive_changed {
self.meshes
.meta
.set_receive_shadows(key, flags.receive)
.map_err(|_| AwsmShadowError::UnknownMesh)?;
}
// Mirror the flag flip into the spatial index so per-view shadow
// filters see the latest `cast_shadows` / `receive_shadows`.
self.sync_spatial_for_mesh(key);
Ok(())
}
/// Returns the current shadow flags for a mesh.
pub fn mesh_shadow_flags(&self, key: crate::meshes::MeshKey) -> MeshShadowFlags {
match self.meshes.get(key) {
Ok(mesh) => MeshShadowFlags {
cast: mesh.cast_shadows,
receive: mesh.receive_shadows,
},
Err(_) => MeshShadowFlags::default(),
}
}
}