1use std::sync::Arc;
2use std::sync::atomic::{AtomicBool, Ordering};
3
4use crate::{
5 EffectId, EffectInputs, EffectKind, EffectParams, EffectResources, EffectRuntime, GraphError,
6 ParamSpec, ParamType, ParamValue, RenderError, RenderGraph, RenderSize, RenderedTexture,
7 Renderer,
8 uniform::{
9 PARAM_SLOT_BYTES, RUNTIME_UNIFORM_WORDS, UniformArenaLease, f32_bytes, param_uniform_bytes,
10 },
11};
12
13impl Renderer {
14 pub fn render_generator(
22 &self,
23 id: &EffectId,
24 size: RenderSize,
25 params: &EffectParams,
26 runtime: EffectRuntime,
27 ) -> Result<RenderedTexture, RenderError> {
28 self.render_generator_with_resources(id, size, params, &EffectResources::new(), runtime)
29 }
30
31 pub fn render_generator_with_resources(
38 &self,
39 id: &EffectId,
40 size: RenderSize,
41 params: &EffectParams,
42 resources: &EffectResources,
43 runtime: EffectRuntime,
44 ) -> Result<RenderedTexture, RenderError> {
45 if self
46 .effect_definition(id)
47 .ok_or(RenderError::UnknownEffect)?
48 .kind
49 != EffectKind::Generator
50 {
51 return Err(RenderError::InvalidGraph);
52 }
53 let mut graph = RenderGraph::new(size);
54 let output = graph.add_generator_with_resources(
55 id.clone(),
56 params.clone(),
57 resources.clone(),
58 runtime,
59 );
60 graph
61 .set_output(output)
62 .map_err(GraphError::into_render_error)?;
63 self.execute_graph(&graph)
64 .map_err(GraphError::into_render_error)
65 }
66
67 #[allow(clippy::too_many_lines)]
68 pub(crate) fn encode_effect_pass(
69 &self,
70 encoder: &mut wgpu::CommandEncoder,
71 pass: EffectPass<'_>,
72 uniforms: &mut UniformArenaLease,
73 ) -> Result<Option<PendingEffectTiming>, RenderError> {
74 let EffectPass {
75 id,
76 kind,
77 source_a,
78 source_b,
79 size,
80 params,
81 resources,
82 runtime,
83 output,
84 } = pass;
85 let registered = self.effects.get(id).ok_or(RenderError::UnknownEffect)?;
86 let source_b = match (registered.definition.inputs, source_b) {
87 (EffectInputs::None | EffectInputs::One, _) => source_a,
88 (EffectInputs::Two, Some(source)) => source,
89 (EffectInputs::Two, None) => return Err(RenderError::MissingEffectInput),
90 };
91 validate_runtime(runtime)?;
92 let parameter_bytes = encode_params(®istered.definition.params, params)?;
93 if resources.iter().any(|(name, _)| {
94 !registered
95 .definition
96 .resources
97 .iter()
98 .any(|spec| spec.name == name)
99 }) {
100 return Err(RenderError::InvalidEffectParams);
101 }
102 let parameter_uniform = uniforms.write(¶meter_bytes);
103 let runtime_uniform = uniforms.write(&runtime_bytes(
104 size,
105 runtime,
106 self.quality_controls().max_filter_samples,
107 ));
108 let mut entries = vec![
109 wgpu::BindGroupEntry {
110 binding: 0,
111 resource: wgpu::BindingResource::TextureView(source_a),
112 },
113 wgpu::BindGroupEntry {
114 binding: 1,
115 resource: wgpu::BindingResource::TextureView(source_b),
116 },
117 wgpu::BindGroupEntry {
118 binding: 2,
119 resource: wgpu::BindingResource::Sampler(®istered.sampler),
120 },
121 wgpu::BindGroupEntry {
122 binding: 3,
123 resource: uniforms.binding(parameter_uniform),
124 },
125 wgpu::BindGroupEntry {
126 binding: 4,
127 resource: uniforms.binding(runtime_uniform),
128 },
129 ];
130 for (index, spec) in registered.definition.resources.iter().enumerate() {
131 let id = resources
132 .get(&spec.name)
133 .ok_or(RenderError::MissingEffectResource)?;
134 let resource = self
135 .effect_resources
136 .get(id)
137 .ok_or(RenderError::MissingEffectResource)?;
138 if resource.kind != spec.kind {
139 return Err(RenderError::EffectResourceKindMismatch);
140 }
141 entries.push(wgpu::BindGroupEntry {
142 binding: u32::try_from(index).unwrap_or(0) * 2 + 5,
143 resource: wgpu::BindingResource::TextureView(&resource.view),
144 });
145 entries.push(wgpu::BindGroupEntry {
146 binding: u32::try_from(index).unwrap_or(0) * 2 + 6,
147 resource: resource.metadata.as_entire_binding(),
148 });
149 }
150 let bind_group = self
151 .gpu
152 .device()
153 .create_bind_group(&wgpu::BindGroupDescriptor {
154 label: Some("alpha2 runtime effect bind group"),
155 layout: ®istered.bind_group_layout,
156 entries: &entries,
157 });
158 let timing = self.pass_timing_epoch();
161 if timing.is_some() {
162 let _ = self.gpu.device().poll(wgpu::PollType::Poll);
165 }
166 let probe = timing
167 .and_then(|epoch| Some((epoch, self.timestamp_probe()?)))
168 .filter(|(_, probe)| probe.acquire());
169 encode_pass(
170 encoder,
171 ®istered.pipeline,
172 &bind_group,
173 output,
174 probe.map(|(_, probe)| &probe.queries),
175 );
176 if let Some((_, probe)) = probe {
177 encoder.resolve_query_set(&probe.queries, 0..2, &probe.resolve, 0);
178 encoder.copy_buffer_to_buffer(&probe.resolve, 0, &probe.readback, 0, 16);
179 }
180 Ok(probe.map(|(epoch, probe)| PendingEffectTiming {
181 epoch,
182 label: kind.pass_label(),
183 readback: probe.readback.clone(),
184 busy: Arc::clone(&probe.busy),
185 diagnostics: self.diagnostics.clone(),
186 timestamp_period: self.gpu.queue().get_timestamp_period(),
187 gpu: self.gpu.clone(),
188 armed: true,
189 }))
190 }
191}
192
193pub(crate) struct PendingEffectTiming {
194 epoch: u64,
196 label: &'static str,
198 readback: wgpu::Buffer,
199 busy: Arc<AtomicBool>,
200 diagnostics: crate::diagnostics::SharedDiagnostics,
201 timestamp_period: f32,
202 gpu: alpha2_gpu::GpuContext,
203 armed: bool,
204}
205
206impl PendingEffectTiming {
207 pub(crate) fn map_after_submit(mut self) {
208 let mapped = self.readback.clone();
209 let busy = Arc::clone(&self.busy);
210 let diagnostics = self.diagnostics.clone();
211 let timestamp_period = self.timestamp_period;
212 let epoch = self.epoch;
213 let label = self.label;
214 self.gpu.map_async(
215 &self.readback.slice(..),
216 wgpu::MapMode::Read,
217 move |result| {
218 if result.is_ok() {
219 if let Ok(bytes) = mapped.slice(..).get_mapped_range() {
220 if bytes.len() >= 16 {
221 let start =
222 u64::from_ne_bytes(bytes[0..8].try_into().unwrap_or([0; 8]));
223 let end = u64::from_ne_bytes(bytes[8..16].try_into().unwrap_or([0; 8]));
224 let elapsed = end.saturating_sub(start);
225 #[allow(
226 clippy::cast_possible_truncation,
227 clippy::cast_precision_loss,
228 clippy::cast_sign_loss
229 )]
230 let nanoseconds = (elapsed as f64 * f64::from(timestamp_period)) as u64;
231 crate::diagnostics::record_pass(
232 &diagnostics,
233 epoch,
234 label,
235 std::time::Duration::from_nanos(nanoseconds),
236 crate::TimingSource::GpuTimestamp,
237 );
238 }
239 drop(bytes);
240 }
241 mapped.unmap();
242 }
243 busy.store(false, Ordering::Release);
244 },
245 );
246 self.armed = false;
247 let _ = self.gpu.device().poll(wgpu::PollType::Poll);
248 }
249}
250
251impl Drop for PendingEffectTiming {
252 fn drop(&mut self) {
253 if self.armed {
254 self.busy.store(false, Ordering::Release);
255 }
256 }
257}
258
259#[derive(Copy, Clone)]
260pub(crate) struct EffectPass<'a> {
261 pub(crate) id: &'a EffectId,
262 pub(crate) kind: crate::RenderNodeKind,
264 pub(crate) source_a: &'a wgpu::TextureView,
265 pub(crate) source_b: Option<&'a wgpu::TextureView>,
266 pub(crate) size: RenderSize,
267 pub(crate) params: &'a EffectParams<'a>,
268 pub(crate) resources: &'a EffectResources,
269 pub(crate) runtime: EffectRuntime,
270 pub(crate) output: &'a wgpu::TextureView,
271}
272
273pub(crate) fn encode_params(
274 schema: &[ParamSpec],
275 supplied: &EffectParams,
276) -> Result<Vec<u8>, RenderError> {
277 if supplied
278 .names()
279 .any(|name| !schema.iter().any(|param| param.name == name))
280 {
281 return Err(RenderError::InvalidEffectParams);
282 }
283 let mut bytes = vec![0; param_uniform_bytes(schema.len())];
284 for (spec, slot) in schema
285 .iter()
286 .zip(bytes.as_chunks_mut::<PARAM_SLOT_BYTES>().0)
287 {
288 let value = supplied.get(&spec.name).unwrap_or(&spec.default);
289 encode_value(spec, value, slot)?;
290 }
291 Ok(bytes)
292}
293
294fn encode_value(
295 spec: &ParamSpec,
296 value: &ParamValue,
297 output: &mut [u8],
298) -> Result<(), RenderError> {
299 match (spec.param_type, value) {
300 (ParamType::Float, ParamValue::Float(value)) if valid_scalar(spec, *value) => {
301 output[..4].copy_from_slice(&value.to_ne_bytes());
302 }
303 (ParamType::Vec2, ParamValue::Vec2(values))
304 if values.iter().all(|value| valid_scalar(spec, *value)) =>
305 {
306 output[..4].copy_from_slice(&values[0].to_ne_bytes());
307 output[4..8].copy_from_slice(&values[1].to_ne_bytes());
308 }
309 (ParamType::Vec4, ParamValue::Vec4(values))
310 if values.iter().all(|value| valid_scalar(spec, *value)) =>
311 {
312 output.copy_from_slice(&f32_bytes(*values));
313 }
314 (ParamType::Color, ParamValue::Color(values))
315 if values
316 .iter()
317 .all(|value| value.is_finite() && (0.0..=1.0).contains(value)) =>
318 {
319 output.copy_from_slice(&f32_bytes(*values));
320 }
321 (ParamType::Enum, ParamValue::Enum(value))
322 if usize::try_from(*value).is_ok_and(|index| index < spec.enum_values.len()) =>
323 {
324 output[..4].copy_from_slice(&value.to_ne_bytes());
325 }
326 (ParamType::Bool, ParamValue::Bool(value)) => {
327 output[..4].copy_from_slice(&u32::from(*value).to_ne_bytes());
328 }
329 _ => return Err(RenderError::InvalidEffectParams),
330 }
331 Ok(())
332}
333
334fn valid_scalar(spec: &ParamSpec, value: f32) -> bool {
335 value.is_finite()
336 && spec.min.is_none_or(|minimum| value >= minimum)
337 && spec.max.is_none_or(|maximum| value <= maximum)
338}
339
340fn validate_runtime(runtime: EffectRuntime) -> Result<(), RenderError> {
341 if !runtime.progress.is_finite()
342 || !(0.0..=1.0).contains(&runtime.progress)
343 || !runtime.ratio.is_finite()
344 || runtime.ratio <= 0.0
345 {
346 return Err(RenderError::InvalidEffectParameter);
347 }
348 Ok(())
349}
350
351#[allow(clippy::cast_precision_loss)]
355fn runtime_bytes(size: RenderSize, runtime: EffectRuntime, max_filter_samples: u32) -> Vec<u8> {
356 let ticks = runtime.time.ticks().cast_unsigned();
357 #[allow(clippy::cast_possible_truncation)]
359 let seconds = (runtime.time.ticks() as f64 / alpha2_time::TICKS_PER_SECOND as f64) as f32;
360 let values: [f32; 8] = [
361 size.width as f32,
362 size.height as f32,
363 seconds,
364 0.0,
365 runtime.progress,
366 runtime.ratio,
367 max_filter_samples as f32,
368 0.0,
369 ];
370 #[allow(clippy::cast_possible_truncation)]
371 let words: [u32; RUNTIME_UNIFORM_WORDS - 8] = [ticks as u32, (ticks >> 32) as u32, 0, 0];
372 let mut bytes = f32_bytes(values);
373 bytes.extend(words.iter().flat_map(|word| word.to_ne_bytes()));
374 bytes
375}
376
377fn encode_pass(
378 encoder: &mut wgpu::CommandEncoder,
379 pipeline: &wgpu::RenderPipeline,
380 bind_group: &wgpu::BindGroup,
381 output: &wgpu::TextureView,
382 timestamp_queries: Option<&wgpu::QuerySet>,
383) {
384 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
385 label: Some("alpha2 runtime effect pass"),
386 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
387 view: output,
388 depth_slice: None,
389 resolve_target: None,
390 ops: wgpu::Operations {
391 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
392 store: wgpu::StoreOp::Store,
393 },
394 })],
395 depth_stencil_attachment: None,
396 timestamp_writes: timestamp_queries.map(|query_set| wgpu::RenderPassTimestampWrites {
397 query_set,
398 beginning_of_pass_write_index: Some(0),
399 end_of_pass_write_index: Some(1),
400 }),
401 occlusion_query_set: None,
402 multiview_mask: None,
403 });
404 pass.set_pipeline(pipeline);
405 pass.set_bind_group(0, bind_group, &[]);
406 pass.draw(0..3, 0..1);
407}
408
409#[derive(Debug)]
416pub(crate) struct TimestampProbe {
417 pub(crate) queries: wgpu::QuerySet,
418 pub(crate) resolve: wgpu::Buffer,
419 pub(crate) readback: wgpu::Buffer,
420 pub(crate) busy: Arc<AtomicBool>,
421}
422
423impl TimestampProbe {
424 pub(crate) fn acquire(&self) -> bool {
426 self.busy
427 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
428 .is_ok()
429 }
430}
431
432impl Renderer {
433 fn timestamp_probe(&self) -> Option<&TimestampProbe> {
435 self.timestamp_probe
436 .get_or_init(|| {
437 if !self.gpu.capabilities().timestamp_queries {
439 return None;
440 }
441 let queries = self
442 .gpu
443 .device()
444 .create_query_set(&wgpu::QuerySetDescriptor {
445 label: Some("alpha2 effect timestamps"),
446 ty: wgpu::QueryType::Timestamp,
447 count: 2,
448 });
449 let resolve = self.gpu.device().create_buffer(&wgpu::BufferDescriptor {
450 label: Some("alpha2 effect timestamp resolve"),
451 size: 16,
452 usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
453 mapped_at_creation: false,
454 });
455 let readback = self.gpu.device().create_buffer(&wgpu::BufferDescriptor {
456 label: Some("alpha2 effect timestamp readback"),
457 size: 16,
458 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
459 mapped_at_creation: false,
460 });
461 Some(TimestampProbe {
462 queries,
463 resolve,
464 readback,
465 busy: Arc::new(AtomicBool::new(false)),
466 })
467 })
468 .as_ref()
469 }
470}