Skip to main content

alpha2_render/
runtime_effect.rs

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    /// Draws a registered generator at `size`.
15    ///
16    /// # Errors
17    /// Returns [`RenderError`] when the generator is unknown, is not a
18    /// generator, or supplied parameters do not match the registered schema;
19    /// [`RenderError::MissingEffectResource`] when it declares a resource,
20    /// which [`Renderer::render_generator_with_resources`] binds.
21    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    /// Draws a registered generator at `size` with named external resources.
32    ///
33    /// # Errors
34    /// Returns [`RenderError`] when the generator is unknown, is not a
35    /// generator, supplied parameters do not match the registered schema, or
36    /// a declared resource is not bound to a registered one of its kind.
37    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(&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        let bind_group = self
151            .gpu
152            .device()
153            .create_bind_group(&wgpu::BindGroupDescriptor {
154                label: Some("alpha2 runtime effect bind group"),
155                layout: &registered.bind_group_layout,
156                entries: &entries,
157            });
158        // One reusable probe, never one per pass: a query set per pass exhausts driver counter
159        // buffers (Metal), and the failed allocation silently drops the whole submission.
160        let timing = self.pass_timing_epoch();
161        if timing.is_some() {
162            // Advance a prior timestamp mapping first, so one slow map cannot latch `busy`
163            // forever on explicitly polled backends.
164            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            &registered.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    /// The timing run the pass was encoded in.
195    epoch: u64,
196    /// What the pass drew: an effect, a transition, or a generator.
197    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    /// The graph node kind the pass draws, which names its timing.
263    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/// The runtime uniform: resolution, `time()` in seconds, progress, ratio and
352/// the filter sample budget as floats, then the time's ticks as two words,
353/// low first (`time_ticks()`), which tell apart every frame `time()` cannot.
354#[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    // Rounded once, from the exact quotient.
358    #[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/// Reusable GPU timestamp instrumentation for effect passes.
410///
411/// Exactly one of these exists per renderer. Timing is diagnostic, so it is
412/// sampled opportunistically: while one measurement is in flight the next pass
413/// renders without timestamps and falls back to CPU submission timing rather
414/// than allocating more driver resources.
415#[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    /// Takes the probe for one pass, or reports that a read is still pending.
425    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    /// Returns the shared probe, creating it at most once.
434    fn timestamp_probe(&self) -> Option<&TimestampProbe> {
435        self.timestamp_probe
436            .get_or_init(|| {
437                // Created at most once, and only if a caller asked for timing.
438                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}