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