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    /// [`RenderError`] for an unknown or non-generator effect or mismatched parameters;
17    /// [`RenderError::MissingEffectResource`] when it declares a resource (use
18    /// [`Renderer::render_generator_with_resources`]).
19    pub fn render_generator(
20        &self,
21        id: &EffectId,
22        size: RenderSize,
23        params: &EffectParams,
24        runtime: EffectRuntime,
25    ) -> Result<RenderedTexture, RenderError> {
26        self.render_generator_with_resources(id, size, params, &EffectResources::new(), runtime)
27    }
28
29    /// Draws a registered generator at `size` with named external resources.
30    ///
31    /// # Errors
32    /// [`RenderError`] for an unknown or non-generator effect, mismatched parameters, or a resource
33    /// not bound to a registered one of its kind.
34    pub fn render_generator_with_resources(
35        &self,
36        id: &EffectId,
37        size: RenderSize,
38        params: &EffectParams,
39        resources: &EffectResources,
40        runtime: EffectRuntime,
41    ) -> Result<RenderedTexture, RenderError> {
42        if self
43            .effect_definition(id)
44            .ok_or(RenderError::UnknownEffect)?
45            .kind
46            != EffectKind::Generator
47        {
48            return Err(RenderError::InvalidGraph);
49        }
50        let mut graph = RenderGraph::new(size);
51        let output = graph.add_generator_with_resources(
52            id.clone(),
53            params.clone(),
54            resources.clone(),
55            runtime,
56        );
57        graph
58            .set_output(output)
59            .map_err(GraphError::into_render_error)?;
60        self.execute_graph(&graph)
61            .map_err(GraphError::into_render_error)
62    }
63
64    /// Encodes a registered effect's pass, split into bands (each its own command buffer) when the
65    /// plan says it exceeds a submission's budget.
66    pub(crate) fn encode_effect_pass(
67        &self,
68        encoder: &mut crate::renderer::RenderEncoder,
69        pass: EffectPass<'_>,
70        uniforms: &mut UniformArenaLease,
71    ) -> Result<Option<PendingEffectTiming>, RenderError> {
72        let registered = self
73            .effects
74            .get(pass.id)
75            .ok_or(RenderError::UnknownEffect)?;
76        self.encode_registered_pass(encoder, registered, pass, uniforms)
77    }
78
79    #[allow(clippy::too_many_lines)]
80    pub(crate) fn encode_registered_pass(
81        &self,
82        encoder: &mut crate::renderer::RenderEncoder,
83        registered: &crate::registry::RegisteredEffect,
84        pass: EffectPass<'_>,
85        uniforms: &mut UniformArenaLease,
86    ) -> Result<Option<PendingEffectTiming>, RenderError> {
87        let EffectPass {
88            id: _,
89            kind,
90            source_a,
91            source_b,
92            inputs,
93            size,
94            params,
95            resources,
96            runtime,
97            output,
98        } = pass;
99        let drawn = self.effect_pipeline(registered);
100        let source_b = match (registered.definition.kind, source_b) {
101            (EffectKind::Transition, Some(source)) => source,
102            (EffectKind::Transition, None) => return Err(RenderError::MissingEffectInput),
103            (EffectKind::Effect | EffectKind::Generator, _) => source_a,
104        };
105        validate_runtime(runtime)?;
106        let parameter_bytes = encode_params(&registered.definition.params, params)?;
107        if resources.iter().any(|(name, _)| {
108            !registered
109                .definition
110                .resources
111                .iter()
112                .any(|spec| spec.name == name)
113        }) {
114            return Err(RenderError::InvalidEffectParams);
115        }
116        let parameter_uniform = uniforms.write(&parameter_bytes);
117        let runtime_uniform = uniforms.write(&runtime_bytes(
118            size,
119            runtime,
120            self.quality_controls().max_filter_samples,
121        ));
122        let mut entries = vec![
123            wgpu::BindGroupEntry {
124                binding: 0,
125                resource: wgpu::BindingResource::TextureView(source_a),
126            },
127            wgpu::BindGroupEntry {
128                binding: 1,
129                resource: wgpu::BindingResource::TextureView(source_b),
130            },
131            wgpu::BindGroupEntry {
132                binding: 2,
133                resource: wgpu::BindingResource::Sampler(&drawn.sampler),
134            },
135            wgpu::BindGroupEntry {
136                binding: 3,
137                resource: uniforms.binding(parameter_uniform),
138            },
139            wgpu::BindGroupEntry {
140                binding: 4,
141                resource: uniforms.binding(runtime_uniform),
142            },
143        ];
144        for (index, spec) in registered.definition.resources.iter().enumerate() {
145            let id = resources
146                .get(&spec.name)
147                .ok_or(RenderError::MissingEffectResource)?;
148            let resource = self
149                .effect_resources
150                .get(id)
151                .ok_or(RenderError::MissingEffectResource)?;
152            if resource.kind != spec.kind {
153                return Err(RenderError::EffectResourceKindMismatch);
154            }
155            entries.push(wgpu::BindGroupEntry {
156                binding: u32::try_from(index).unwrap_or(0) * 2 + 5,
157                resource: wgpu::BindingResource::TextureView(&resource.view),
158            });
159            entries.push(wgpu::BindGroupEntry {
160                binding: u32::try_from(index).unwrap_or(0) * 2 + 6,
161                resource: resource.metadata.as_entire_binding(),
162            });
163        }
164        // An undeclared name is a bad setting; a declared but unbound one is a missing input.
165        if inputs.iter().any(|(name, _)| {
166            !registered
167                .definition
168                .inputs
169                .iter()
170                .any(|spec| spec.name == *name)
171        }) {
172            return Err(RenderError::InvalidEffectParams);
173        }
174        for (index, spec) in registered.definition.inputs.iter().enumerate() {
175            let (_, view) = inputs
176                .iter()
177                .find(|(name, _)| *name == spec.name)
178                .ok_or(RenderError::MissingEffectInput)?;
179            entries.push(wgpu::BindGroupEntry {
180                binding: crate::registry::input_binding(index),
181                resource: wgpu::BindingResource::TextureView(view),
182            });
183        }
184        let bind_group = self
185            .gpu
186            .device()
187            .create_bind_group(&wgpu::BindGroupDescriptor {
188                label: Some("alpha2 runtime effect bind group"),
189                layout: &drawn.bind_group_layout,
190                entries: &entries,
191            });
192        let bands = encoder.place_pass(
193            registered.cost,
194            u64::from(size.width) * u64::from(size.height),
195            true,
196        );
197        if bands > 1 {
198            // Bands read only the pass's sources, never its target, so pixels match an unbanded
199            // pass.
200            for band in 0..bands {
201                if band > 0 {
202                    encoder.next_band();
203                }
204                let (top, rows) = crate::submission_planner::band_rows(band, bands, size.height);
205                if rows > 0 {
206                    encode_pass(
207                        encoder,
208                        &drawn.pipeline,
209                        &bind_group,
210                        output,
211                        None,
212                        Some((band, top, size.width, rows)),
213                    );
214                }
215            }
216            return Ok(None);
217        }
218        // One reusable probe: a query set per pass exhausts Metal counter buffers, and the failed
219        // allocation silently drops the whole submission.
220        let timing = self.pass_timing_epoch();
221        if timing.is_some() {
222            // Advance a pending timestamp map first so one slow map cannot latch `busy` on polled
223            // backends.
224            let _ = self.gpu.device().poll(wgpu::PollType::Poll);
225        }
226        let probe = timing
227            .and_then(|epoch| Some((epoch, self.timestamp_probe()?)))
228            .filter(|(_, probe)| probe.acquire());
229        encode_pass(
230            encoder,
231            &drawn.pipeline,
232            &bind_group,
233            output,
234            probe.map(|(_, probe)| &probe.queries),
235            None,
236        );
237        if let Some((_, probe)) = probe {
238            encoder.resolve_query_set(&probe.queries, 0..2, &probe.resolve, 0);
239            encoder.copy_buffer_to_buffer(&probe.resolve, 0, &probe.readback, 0, 16);
240        }
241        Ok(probe.map(|(epoch, probe)| PendingEffectTiming {
242            epoch,
243            label: kind.pass_label(),
244            readback: probe.readback.clone(),
245            busy: Arc::clone(&probe.busy),
246            diagnostics: self.diagnostics.clone(),
247            timestamp_period: self.gpu.queue().get_timestamp_period(),
248            gpu: self.gpu.clone(),
249            armed: true,
250        }))
251    }
252}
253
254pub(crate) struct PendingEffectTiming {
255    epoch: u64,
256    label: &'static str,
257    readback: wgpu::Buffer,
258    busy: Arc<AtomicBool>,
259    diagnostics: crate::diagnostics::SharedDiagnostics,
260    timestamp_period: f32,
261    gpu: alpha2_gpu::GpuContext,
262    armed: bool,
263}
264
265impl PendingEffectTiming {
266    pub(crate) fn map_after_submit(mut self) {
267        let mapped = self.readback.clone();
268        let busy = Arc::clone(&self.busy);
269        let diagnostics = self.diagnostics.clone();
270        let timestamp_period = self.timestamp_period;
271        let epoch = self.epoch;
272        let label = self.label;
273        self.gpu.map_async(
274            &self.readback.slice(..),
275            wgpu::MapMode::Read,
276            move |result| {
277                if result.is_ok() {
278                    if let Ok(bytes) = mapped.slice(..).get_mapped_range() {
279                        if bytes.len() >= 16 {
280                            let start =
281                                u64::from_ne_bytes(bytes[0..8].try_into().unwrap_or([0; 8]));
282                            let end = u64::from_ne_bytes(bytes[8..16].try_into().unwrap_or([0; 8]));
283                            let elapsed = end.saturating_sub(start);
284                            #[allow(
285                                clippy::cast_possible_truncation,
286                                clippy::cast_precision_loss,
287                                clippy::cast_sign_loss
288                            )]
289                            let nanoseconds = (elapsed as f64 * f64::from(timestamp_period)) as u64;
290                            crate::diagnostics::record_pass(
291                                &diagnostics,
292                                epoch,
293                                label,
294                                std::time::Duration::from_nanos(nanoseconds),
295                                crate::TimingSource::GpuTimestamp,
296                            );
297                        }
298                        drop(bytes);
299                    }
300                    mapped.unmap();
301                }
302                busy.store(false, Ordering::Release);
303            },
304        );
305        self.armed = false;
306        let _ = self.gpu.device().poll(wgpu::PollType::Poll);
307    }
308}
309
310impl Drop for PendingEffectTiming {
311    fn drop(&mut self) {
312        if self.armed {
313            self.busy.store(false, Ordering::Release);
314        }
315    }
316}
317
318#[derive(Copy, Clone)]
319pub(crate) struct EffectPass<'a> {
320    pub(crate) id: &'a EffectId,
321    /// The node kind, which names the pass's timing.
322    pub(crate) kind: crate::RenderNodeKind,
323    pub(crate) source_a: &'a wgpu::TextureView,
324    pub(crate) source_b: Option<&'a wgpu::TextureView>,
325    pub(crate) inputs: &'a [(&'a str, &'a wgpu::TextureView)],
326    pub(crate) size: RenderSize,
327    pub(crate) params: &'a EffectParams<'a>,
328    pub(crate) resources: &'a EffectResources,
329    pub(crate) runtime: EffectRuntime,
330    pub(crate) output: &'a wgpu::TextureView,
331}
332
333pub(crate) fn encode_params(
334    schema: &[ParamSpec],
335    supplied: &EffectParams,
336) -> Result<Vec<u8>, RenderError> {
337    if supplied
338        .names()
339        .any(|name| !schema.iter().any(|param| param.name == name))
340    {
341        return Err(RenderError::InvalidEffectParams);
342    }
343    let mut bytes = vec![0; param_uniform_bytes(schema.len())];
344    for (spec, slot) in schema
345        .iter()
346        .zip(bytes.as_chunks_mut::<PARAM_SLOT_BYTES>().0)
347    {
348        let value = supplied.get(&spec.name).unwrap_or(&spec.default);
349        encode_value(spec, value, slot)?;
350    }
351    Ok(bytes)
352}
353
354fn encode_value(
355    spec: &ParamSpec,
356    value: &ParamValue,
357    output: &mut [u8],
358) -> Result<(), RenderError> {
359    match (spec.param_type, value) {
360        (ParamType::Float, ParamValue::Float(value)) if valid_scalar(spec, *value) => {
361            output[..4].copy_from_slice(&value.to_ne_bytes());
362        }
363        (ParamType::Vec2, ParamValue::Vec2(values))
364            if values.iter().all(|value| valid_scalar(spec, *value)) =>
365        {
366            output[..4].copy_from_slice(&values[0].to_ne_bytes());
367            output[4..8].copy_from_slice(&values[1].to_ne_bytes());
368        }
369        (ParamType::Vec4, ParamValue::Vec4(values))
370            if values.iter().all(|value| valid_scalar(spec, *value)) =>
371        {
372            output.copy_from_slice(&f32_bytes(*values));
373        }
374        (ParamType::Color, ParamValue::Color(values))
375            if values
376                .iter()
377                .all(|value| value.is_finite() && (0.0..=1.0).contains(value)) =>
378        {
379            output.copy_from_slice(&f32_bytes(*values));
380        }
381        (ParamType::Enum, ParamValue::Enum(value))
382            if usize::try_from(*value).is_ok_and(|index| index < spec.enum_values.len()) =>
383        {
384            output[..4].copy_from_slice(&value.to_ne_bytes());
385        }
386        (ParamType::Bool, ParamValue::Bool(value)) => {
387            output[..4].copy_from_slice(&u32::from(*value).to_ne_bytes());
388        }
389        _ => return Err(RenderError::InvalidEffectParams),
390    }
391    Ok(())
392}
393
394fn valid_scalar(spec: &ParamSpec, value: f32) -> bool {
395    value.is_finite()
396        && spec.min.is_none_or(|minimum| value >= minimum)
397        && spec.max.is_none_or(|maximum| value <= maximum)
398}
399
400pub(crate) fn validate_runtime(runtime: EffectRuntime) -> Result<(), RenderError> {
401    if !runtime.progress.is_finite()
402        || !(0.0..=1.0).contains(&runtime.progress)
403        || !runtime.ratio.is_finite()
404        || runtime.ratio <= 0.0
405    {
406        return Err(RenderError::InvalidEffectParameter);
407    }
408    Ok(())
409}
410
411/// Runtime uniform: resolution, `time()` seconds, progress, ratio and filter budget as floats,
412/// then the tick count as two words, low first (`time_ticks()`).
413#[allow(clippy::cast_precision_loss)]
414fn runtime_bytes(size: RenderSize, runtime: EffectRuntime, max_filter_samples: u32) -> Vec<u8> {
415    let ticks = runtime.time.ticks().cast_unsigned();
416    // Rounded once from the exact quotient.
417    #[allow(clippy::cast_possible_truncation)]
418    let seconds = (runtime.time.ticks() as f64 / alpha2_time::TICKS_PER_SECOND as f64) as f32;
419    let values: [f32; 8] = [
420        size.width as f32,
421        size.height as f32,
422        seconds,
423        0.0,
424        runtime.progress,
425        runtime.ratio,
426        max_filter_samples as f32,
427        0.0,
428    ];
429    #[allow(clippy::cast_possible_truncation)]
430    let words: [u32; RUNTIME_UNIFORM_WORDS - 8] = [ticks as u32, (ticks >> 32) as u32, 0, 0];
431    let mut bytes = f32_bytes(values);
432    bytes.extend(words.iter().flat_map(|word| word.to_ne_bytes()));
433    bytes
434}
435
436/// Encodes one full-target draw, or with `band` (index, top row, width, rows) one band; the
437/// first band clears the target, later ones load it.
438fn encode_pass(
439    encoder: &mut wgpu::CommandEncoder,
440    pipeline: &wgpu::RenderPipeline,
441    bind_group: &wgpu::BindGroup,
442    output: &wgpu::TextureView,
443    timestamp_queries: Option<&wgpu::QuerySet>,
444    band: Option<(u32, u32, u32, u32)>,
445) {
446    let load = match band {
447        Some((index, ..)) if index > 0 => wgpu::LoadOp::Load,
448        _ => wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
449    };
450    let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
451        label: Some("alpha2 runtime effect pass"),
452        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
453            view: output,
454            depth_slice: None,
455            resolve_target: None,
456            ops: wgpu::Operations {
457                load,
458                store: wgpu::StoreOp::Store,
459            },
460        })],
461        depth_stencil_attachment: None,
462        timestamp_writes: timestamp_queries.map(|query_set| wgpu::RenderPassTimestampWrites {
463            query_set,
464            beginning_of_pass_write_index: Some(0),
465            end_of_pass_write_index: Some(1),
466        }),
467        occlusion_query_set: None,
468        multiview_mask: None,
469    });
470    pass.set_pipeline(pipeline);
471    pass.set_bind_group(0, bind_group, &[]);
472    if let Some((_, top, width, rows)) = band {
473        pass.set_scissor_rect(0, top, width, rows);
474    }
475    pass.draw(0..3, 0..1);
476}
477
478/// Reusable GPU timestamp instrumentation for effect passes, one per renderer.
479///
480/// While one measurement is in flight, later passes fall back to CPU timing instead of
481/// allocating more driver resources.
482#[derive(Debug)]
483pub(crate) struct TimestampProbe {
484    pub(crate) queries: wgpu::QuerySet,
485    pub(crate) resolve: wgpu::Buffer,
486    pub(crate) readback: wgpu::Buffer,
487    pub(crate) busy: Arc<AtomicBool>,
488}
489
490impl TimestampProbe {
491    /// Takes the probe for one pass; `false` while a read is pending.
492    pub(crate) fn acquire(&self) -> bool {
493        self.busy
494            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
495            .is_ok()
496    }
497}
498
499impl Renderer {
500    fn timestamp_probe(&self) -> Option<&TimestampProbe> {
501        self.timestamp_probe
502            .get_or_init(|| {
503                if !self.gpu.capabilities().timestamp_queries {
504                    return None;
505                }
506                let queries = self
507                    .gpu
508                    .device()
509                    .create_query_set(&wgpu::QuerySetDescriptor {
510                        label: Some("alpha2 effect timestamps"),
511                        ty: wgpu::QueryType::Timestamp,
512                        count: 2,
513                    });
514                let resolve = self.gpu.device().create_buffer(&wgpu::BufferDescriptor {
515                    label: Some("alpha2 effect timestamp resolve"),
516                    size: 16,
517                    usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
518                    mapped_at_creation: false,
519                });
520                let readback = self.gpu.device().create_buffer(&wgpu::BufferDescriptor {
521                    label: Some("alpha2 effect timestamp readback"),
522                    size: 16,
523                    usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
524                    mapped_at_creation: false,
525                });
526                Some(TimestampProbe {
527                    queries,
528                    resolve,
529                    readback,
530                    busy: Arc::new(AtomicBool::new(false)),
531                })
532            })
533            .as_ref()
534    }
535}