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 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(&registered.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(&parameter_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(&registered.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        // As for resources: a name the definition does not declare is a bad setting, and a
151        // declared one left unbound a missing input.
152        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: &registered.bind_group_layout,
177                entries: &entries,
178            });
179        // One reusable probe, never one per pass: a query set per pass exhausts driver counter
180        // buffers (Metal), and the failed allocation silently drops the whole submission.
181        let timing = self.pass_timing_epoch();
182        if timing.is_some() {
183            // Advance a prior timestamp mapping first, so one slow map cannot latch `busy`
184            // forever on explicitly polled backends.
185            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            &registered.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    /// The timing run the pass was encoded in.
216    epoch: u64,
217    /// What the pass drew: an effect, a transition, or a generator.
218    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    /// The graph node kind the pass draws, which names its timing.
284    pub(crate) kind: crate::RenderNodeKind,
285    pub(crate) source_a: &'a wgpu::TextureView,
286    pub(crate) source_b: Option<&'a wgpu::TextureView>,
287    /// The named inputs the node binds, by the names it gives them.
288    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
363fn 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/// The runtime uniform: resolution, `time()` in seconds, progress, ratio and
375/// the filter sample budget as floats, then the time's ticks as two words,
376/// low first (`time_ticks()`), which tell apart every frame `time()` cannot.
377#[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    // Rounded once, from the exact quotient.
381    #[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/// Reusable GPU timestamp instrumentation for effect passes.
433///
434/// Exactly one of these exists per renderer. Timing is diagnostic, so it is
435/// sampled opportunistically: while one measurement is in flight the next pass
436/// renders without timestamps and falls back to CPU submission timing rather
437/// than allocating more driver resources.
438#[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    /// Takes the probe for one pass, or reports that a read is still pending.
448    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    /// Returns the shared probe, creating it at most once.
457    fn timestamp_probe(&self) -> Option<&TimestampProbe> {
458        self.timestamp_probe
459            .get_or_init(|| {
460                // Created at most once, and only if a caller asked for timing.
461                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}