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(
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 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 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(®istered.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(¶meter_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 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 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 let timing = self.pass_timing_epoch();
221 if timing.is_some() {
222 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 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#[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 #[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
436fn 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#[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 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}