Skip to main content

alpha2_render/
runtime_effect.rs

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    /// Draws a registered generator at `size`.
14    ///
15    /// # Errors
16    /// Returns [`RenderError`] when the generator is unknown, is not a
17    /// generator, or supplied parameters do not match the registered schema;
18    /// [`RenderError::MissingEffectResource`] when it declares a resource,
19    /// which [`Renderer::render_generator_with_resources`] binds.
20    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    /// Draws a registered generator at `size` with named external resources.
31    ///
32    /// # Errors
33    /// Returns [`RenderError`] when the generator is unknown, is not a
34    /// generator, supplied parameters do not match the registered schema, or
35    /// a declared resource is not bound to a registered one of its kind.
36    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(&registered.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(&parameter_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        // As for resources: a name the definition does not declare is a bad setting, and a
152        // declared one left unbound a missing input.
153        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        // One reusable probe, never one per pass: a query set per pass exhausts driver counter
181        // buffers (Metal), and the failed allocation silently drops the whole submission.
182        let timing = self.pass_timing_epoch();
183        if timing.is_some() {
184            // Advance a prior timestamp mapping first, so one slow map cannot latch `busy`
185            // forever on explicitly polled backends.
186            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    /// The timing run the pass was encoded in.
217    epoch: u64,
218    /// What the pass drew: an effect, a transition, or a generator.
219    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    /// The graph node kind the pass draws, which names its timing.
285    pub(crate) kind: crate::RenderNodeKind,
286    pub(crate) source_a: &'a wgpu::TextureView,
287    pub(crate) source_b: Option<&'a wgpu::TextureView>,
288    /// The named inputs the node binds, by the names it gives them.
289    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/// The runtime uniform: resolution, `time()` in seconds, progress, ratio and
376/// the filter sample budget as floats, then the time's ticks as two words,
377/// low first (`time_ticks()`), which tell apart every frame `time()` cannot.
378#[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    // Rounded once, from the exact quotient.
382    #[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/// Reusable GPU timestamp instrumentation for effect passes.
434///
435/// Exactly one of these exists per renderer. Timing is diagnostic, so it is
436/// sampled opportunistically: while one measurement is in flight the next pass
437/// renders without timestamps and falls back to CPU submission timing rather
438/// than allocating more driver resources.
439#[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    /// Takes the probe for one pass, or reports that a read is still pending.
449    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    /// Returns the shared probe, creating it at most once.
458    fn timestamp_probe(&self) -> Option<&TimestampProbe> {
459        self.timestamp_probe
460            .get_or_init(|| {
461                // Created at most once, and only if a caller asked for timing.
462                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}