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