1use std::collections::{HashMap, HashSet};
2use std::fmt::Write;
3
4use alpha2_gpu::{CapabilityTier, GpuContext};
5
6use crate::effect::EffectKey;
7use crate::loop_bound::{BUDGET, MAX_LOOP_ITERATIONS, blank_comments, bound_for_loops};
8use crate::uniform::{PARAM_SLOT_BYTES, RUNTIME_UNIFORM_WORDS, param_uniform_bytes};
9
10use crate::{
11 DiagnosticSeverity, EffectDef, EffectId, EffectKind, EffectResourceKind, InputSpec, ParamSpec,
12 ParamType, ParamUnit, ParamValue, Renderer, ResourceSpec, ShaderDiagnostic, ShaderDiagnostics,
13 UniformField, UniformLayout,
14};
15
16const MAX_REGISTERED_EFFECTS: usize = 256;
18const MAX_WGSL_BODY_BYTES: usize = 32 * 1024;
19const MAX_WGSL_NESTING: usize = 64;
20const MAX_WGSL_STATEMENT_OPERATORS: usize = 256;
26const RESERVED_PREFIX: &str = "alpha2_";
28const MAX_EFFECT_RESOURCES: usize = 8;
29pub const MAX_EFFECT_INPUTS: usize = 8;
32#[allow(clippy::cast_possible_truncation)]
35const INPUT_BINDING_BASE: u32 = 5 + 2 * MAX_EFFECT_RESOURCES as u32;
36const RESOURCE_METADATA_BYTES: usize = 32;
38
39struct CreationErrorScopes([wgpu::ErrorScopeGuard; 3]);
45
46impl CreationErrorScopes {
47 fn push(device: &wgpu::Device) -> Self {
48 Self([
49 device.push_error_scope(wgpu::ErrorFilter::Internal),
50 device.push_error_scope(wgpu::ErrorFilter::OutOfMemory),
51 device.push_error_scope(wgpu::ErrorFilter::Validation),
52 ])
53 }
54
55 fn pop(self) -> ScopeErrors {
57 let [internal, out_of_memory, validation] = self.0;
58 let validation = validation.pop();
59 let out_of_memory = out_of_memory.pop();
60 let internal = internal.pop();
61 Box::pin(async move {
62 [validation.await, out_of_memory.await, internal.await]
63 .into_iter()
64 .flatten()
65 .collect()
66 })
67 }
68}
69
70#[cfg(not(target_arch = "wasm32"))]
73const D3D12_COMPILE_STACK: usize = 8 << 20;
74
75#[cfg(not(target_arch = "wasm32"))]
78fn on_compile_stack<T: Send>(gpu: &GpuContext, make: impl FnOnce() -> T + Send) -> T {
79 if gpu.capabilities().backend != "Dx12" {
80 return make();
81 }
82 std::thread::scope(|scope| {
83 let compile = std::thread::Builder::new()
84 .name("alpha2 shader compile".to_owned())
85 .stack_size(D3D12_COMPILE_STACK)
86 .spawn_scoped(scope, make)
87 .expect("the shader compile thread starts");
88 compile
89 .join()
90 .unwrap_or_else(|panic| std::panic::resume_unwind(panic))
91 })
92}
93
94#[cfg(target_arch = "wasm32")]
95fn on_compile_stack<T>(_gpu: &GpuContext, make: impl FnOnce() -> T) -> T {
96 make()
97}
98
99#[cfg(not(target_arch = "wasm32"))]
101type Answer<T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send>>;
102#[cfg(target_arch = "wasm32")]
103type Answer<T> = std::pin::Pin<Box<dyn std::future::Future<Output = T>>>;
104
105type ScopeErrors = Answer<Vec<wgpu::Error>>;
106
107pub struct PendingEffect {
113 compiled: CompiledEffect,
114 compilation: Answer<wgpu::CompilationInfo>,
115 shader_errors: ScopeErrors,
116 pipeline_errors: ScopeErrors,
117}
118
119impl std::fmt::Debug for PendingEffect {
120 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 formatter
122 .debug_struct("PendingEffect")
123 .field("id", &self.compiled.id)
124 .finish_non_exhaustive()
125 }
126}
127
128impl PendingEffect {
129 pub async fn compiled(self) -> Result<CompiledEffect, ShaderDiagnostics> {
134 let mut diagnostics = compilation_diagnostics(self.compilation.await);
135 for error in self.shader_errors.await {
136 diagnostics.messages.push(ShaderDiagnostic {
137 severity: DiagnosticSeverity::Error,
138 message: error.to_string(),
139 line: None,
140 column: None,
141 });
142 }
143 if diagnostics
145 .messages
146 .iter()
147 .any(|message| message.severity == DiagnosticSeverity::Error)
148 {
149 return Err(diagnostics);
150 }
151 let errors = self.pipeline_errors.await;
152 if !errors.is_empty() {
153 return Err(ShaderDiagnostics {
154 messages: errors
155 .into_iter()
156 .map(|error| ShaderDiagnostic {
157 severity: DiagnosticSeverity::Error,
158 message: error.to_string(),
159 line: None,
160 column: None,
161 })
162 .collect(),
163 });
164 }
165 Ok(self.compiled)
166 }
167}
168
169pub struct CompiledEffect {
171 device: wgpu::Device,
172 id: EffectId,
173 effect: RegisteredEffect,
174}
175
176impl std::fmt::Debug for CompiledEffect {
177 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 formatter
179 .debug_struct("CompiledEffect")
180 .field("id", &self.id)
181 .finish_non_exhaustive()
182 }
183}
184
185impl CompiledEffect {
186 #[must_use]
188 pub const fn id(&self) -> &EffectId {
189 &self.id
190 }
191}
192
193#[derive(Debug, Default)]
194pub(crate) struct EffectRegistry {
195 entries: HashMap<EffectId, RegisteredEffect>,
196 builtins: HashSet<EffectId>,
197 internal: HashSet<EffectId>,
198}
199
200impl EffectRegistry {
201 fn check_room(&self, id: &EffectId) -> Result<(), ShaderDiagnostics> {
203 if self.entries.contains_key(id) {
204 return Err(ShaderDiagnostics::error(format!(
205 "effect {} version {} is already registered",
206 id.as_str(),
207 id.version()
208 )));
209 }
210 if self.entries.len() - self.builtins.len() >= MAX_REGISTERED_EFFECTS {
211 return Err(ShaderDiagnostics::error(format!(
212 "effect registry is full (maximum {MAX_REGISTERED_EFFECTS})"
213 )));
214 }
215 Ok(())
216 }
217}
218
219#[derive(Debug)]
220pub(crate) struct RegisteredEffect {
221 pub(crate) definition: EffectDef,
222 pub(crate) uniform_layout: UniformLayout,
223 reads_time: bool,
224 pub(crate) cost: crate::effect_cost::EffectCost,
226 pipeline: std::sync::OnceLock<EffectPipeline>,
228}
229
230#[derive(Debug)]
231pub(crate) struct EffectPipeline {
232 _shader: wgpu::ShaderModule,
233 pub(crate) bind_group_layout: wgpu::BindGroupLayout,
234 pub(crate) pipeline: wgpu::RenderPipeline,
235 pub(crate) sampler: wgpu::Sampler,
236}
237
238impl Renderer {
239 pub async fn register_effect(
248 &mut self,
249 definition: EffectDef,
250 ) -> Result<EffectId, ShaderDiagnostics> {
251 let compiled = self.compile_effect(definition)?.compiled().await?;
252 self.register_compiled(compiled)
253 }
254
255 pub fn compile_effect(
264 &self,
265 definition: EffectDef,
266 ) -> Result<PendingEffect, ShaderDiagnostics> {
267 let id = self.check_registration(&definition)?;
268 let device = self.gpu.device();
269 let shader_scope = CreationErrorScopes::push(device);
270 let shader = effect_shader(device, &definition);
271 let compilation: Answer<wgpu::CompilationInfo> = Box::pin(shader.get_compilation_info());
272 let shader_errors = shader_scope.pop();
273 let format = self.intermediate_format();
274 let (pipeline, pipeline_errors) = on_compile_stack(&self.gpu, || {
275 let pipeline_scope = CreationErrorScopes::push(device);
276 let pipeline = effect_pipeline(device, &definition, shader, format);
277 (pipeline, pipeline_scope.pop())
278 });
279 Ok(PendingEffect {
280 compiled: CompiledEffect {
281 device: device.clone(),
282 id,
283 effect: RegisteredEffect {
284 pipeline: std::sync::OnceLock::from(pipeline),
285 ..registered_effect(definition)
286 },
287 },
288 compilation,
289 shader_errors,
290 pipeline_errors,
291 })
292 }
293
294 pub(crate) fn register_lazily(
301 &mut self,
302 definition: EffectDef,
303 ) -> Result<EffectId, ShaderDiagnostics> {
304 let id = self.check_registration(&definition)?;
305 self.effects
306 .entries
307 .insert(id.clone(), registered_effect(definition));
308 Ok(id)
309 }
310
311 fn check_registration(&self, definition: &EffectDef) -> Result<EffectId, ShaderDiagnostics> {
313 validate_definition(definition)?;
314 self.validate_parameter_uniform(definition)?;
315 self.validate_texture_bindings(definition)?;
316 if definition.tier_min == CapabilityTier::TierA
317 && self.gpu.capabilities().tier == CapabilityTier::TierB
318 {
319 return Err(ShaderDiagnostics::error(
320 "effect requires capability tier A on a tier B adapter",
321 ));
322 }
323 let id = EffectId::new(definition.id.clone(), definition.version);
324 self.effects.check_room(&id)?;
325 Ok(id)
326 }
327
328 pub(crate) fn effect_pipeline<'a>(&self, effect: &'a RegisteredEffect) -> &'a EffectPipeline {
330 effect.pipeline.get_or_init(|| {
331 crate::diagnostics::timed_pipeline_creation(
332 &self.diagnostics,
333 || effect.definition.id.as_str().to_owned(),
334 || {
335 let device = self.gpu.device();
336 let shader = effect_shader(device, &effect.definition);
337 let format = self.intermediate_format();
338 on_compile_stack(&self.gpu, || {
339 effect_pipeline(device, &effect.definition, shader, format)
340 })
341 },
342 )
343 })
344 }
345
346 pub fn register_compiled(
351 &mut self,
352 compiled: CompiledEffect,
353 ) -> Result<EffectId, ShaderDiagnostics> {
354 if compiled.device != *self.gpu.device() {
355 return Err(ShaderDiagnostics::error(format!(
356 "effect {} version {} was compiled on another GPU device",
357 compiled.id.as_str(),
358 compiled.id.version()
359 )));
360 }
361 self.effects.check_room(&compiled.id)?;
362 self.effects
363 .entries
364 .insert(compiled.id.clone(), compiled.effect);
365 Ok(compiled.id)
366 }
367
368 fn validate_parameter_uniform(&self, definition: &EffectDef) -> Result<(), ShaderDiagnostics> {
374 let limit = self
375 .limits
376 .max_uniform_buffer_binding_size
377 .min(wgpu::Limits::downlevel_webgl2_defaults().max_uniform_buffer_binding_size);
378 let bytes = u64::try_from(param_uniform_bytes(definition.params.len())).unwrap_or(u64::MAX);
379 if bytes > limit {
380 let most = limit / u64::try_from(PARAM_SLOT_BYTES).unwrap_or(u64::MAX);
381 return Err(ShaderDiagnostics::error(format!(
382 "effect declares {} parameters, a {bytes}-byte uniform past the {limit}-byte \
383 limit every alpha2 runtime binds (at most {most} parameters)",
384 definition.params.len()
385 )));
386 }
387 Ok(())
388 }
389
390 fn validate_texture_bindings(&self, definition: &EffectDef) -> Result<(), ShaderDiagnostics> {
393 let textures = 2 + definition.resources.len() + definition.inputs.len();
394 let limit =
395 usize::try_from(self.limits.max_sampled_textures_per_shader_stage.min(
396 wgpu::Limits::downlevel_webgl2_defaults().max_sampled_textures_per_shader_stage,
397 ))
398 .unwrap_or(usize::MAX);
399 if textures > limit {
400 return Err(ShaderDiagnostics::error(format!(
401 "effect samples {textures} textures (its source pair, {} resources, and {} \
402 named inputs), past this device's {limit} per shader stage",
403 definition.resources.len(),
404 definition.inputs.len()
405 )));
406 }
407 Ok(())
408 }
409
410 #[must_use]
412 pub fn effect_uniform_layout(&self, id: &EffectId) -> Option<&UniformLayout> {
413 self.effects
414 .entries
415 .get(id)
416 .map(|entry| &entry.uniform_layout)
417 }
418
419 #[must_use]
421 pub fn effect_definition(&self, id: &EffectId) -> Option<&EffectDef> {
422 self.effects.entries.get(id).map(|entry| &entry.definition)
423 }
424
425 #[must_use]
431 pub fn effect_reads_time(&self, id: &EffectId) -> bool {
432 if let Some(passes) = crate::gaussian_blur::expanded_passes(id.as_str()) {
433 return passes
434 .iter()
435 .any(|pass| self.effect_reads_time(&EffectId::from_static(pass, 1)));
436 }
437 self.effects
438 .entries
439 .get(id)
440 .is_none_or(|entry| entry.reads_time)
441 }
442
443 #[must_use]
445 pub fn is_effect_registered(&self, id: &EffectId) -> bool {
446 self.effects.entries.contains_key(id)
447 }
448
449 #[must_use]
451 pub fn is_effect_project_visible(&self, id: &EffectId) -> bool {
452 self.effects.entries.contains_key(id) && !self.effects.internal.contains(id)
453 }
454
455 #[must_use]
458 pub fn project_effect_id(&self, id: &str, version: u32) -> Option<&EffectId> {
459 let key: &dyn EffectKey = &(id, version);
460 self.effects
461 .entries
462 .get_key_value(key)
463 .map(|(id, _)| id)
464 .filter(|id| !self.effects.internal.contains(*id))
465 }
466
467 pub fn unregister_effect(&mut self, id: &EffectId) -> bool {
469 if self.effects.builtins.contains(id) {
470 return false;
471 }
472 let removed = self.effects.entries.remove(id).is_some();
473 if removed {
474 self.clear_result_cache_for(crate::CacheInvalidation::Effects);
475 }
476 removed
477 }
478
479 pub fn clear_effects(&mut self) {
481 self.effects
482 .entries
483 .retain(|id, _| self.effects.builtins.contains(id));
484 self.clear_result_cache_for(crate::CacheInvalidation::Effects);
485 }
486
487 pub fn warmup(&self, effects: &[EffectId]) -> Result<(), ShaderDiagnostics> {
493 for effect in effects {
494 let Some(registered) = self.effects.get(effect) else {
495 return Err(ShaderDiagnostics::error(format!(
496 "effect {} version {} is not registered",
497 effect.as_str(),
498 effect.version()
499 )));
500 };
501 self.effect_pipeline(registered);
502 for pass in crate::gaussian_blur::expanded_passes(effect.as_str()).unwrap_or_default() {
503 if let Some(registered) = self.effects.get(&EffectId::from_static(pass, 1)) {
504 self.effect_pipeline(registered);
505 }
506 }
507 }
508 Ok(())
509 }
510}
511
512impl EffectRegistry {
513 pub(crate) fn get(&self, id: &EffectId) -> Option<&RegisteredEffect> {
514 self.entries.get(id)
515 }
516
517 pub(crate) fn len(&self) -> usize {
518 self.entries.len()
519 }
520
521 pub(crate) fn mark_builtin(&mut self, id: EffectId) {
522 debug_assert!(self.entries.contains_key(&id));
523 self.builtins.insert(id);
524 }
525
526 pub(crate) fn mark_internal(&mut self, id: EffectId) {
527 debug_assert!(self.entries.contains_key(&id));
528 self.internal.insert(id);
529 }
530}
531
532pub(crate) fn registered_effect(definition: EffectDef) -> RegisteredEffect {
534 RegisteredEffect {
535 uniform_layout: uniform_layout(&definition.params),
536 reads_time: body_reads_time(&definition.wgsl_body),
537 cost: crate::effect_cost::invocation_cost(&blank_comments(&definition.wgsl_body)),
538 definition,
539 pipeline: std::sync::OnceLock::new(),
540 }
541}
542
543fn effect_shader(device: &wgpu::Device, definition: &EffectDef) -> wgpu::ShaderModule {
544 device.create_shader_module(wgpu::ShaderModuleDescriptor {
545 label: Some(&definition.id),
546 source: wgpu::ShaderSource::Wgsl(build_shader(definition).into()),
547 })
548}
549
550fn effect_pipeline(
551 device: &wgpu::Device,
552 definition: &EffectDef,
553 shader: wgpu::ShaderModule,
554 format: wgpu::TextureFormat,
555) -> EffectPipeline {
556 let bind_group_layout = runtime_bind_group_layout(
557 device,
558 &definition.params,
559 &definition.resources,
560 &definition.inputs,
561 );
562 let pipeline = runtime_pipeline(device, &bind_group_layout, &shader, format);
563 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
564 label: Some("alpha2 runtime effect sampler"),
565 address_mode_u: wgpu::AddressMode::ClampToEdge,
566 address_mode_v: wgpu::AddressMode::ClampToEdge,
567 mag_filter: wgpu::FilterMode::Linear,
568 min_filter: wgpu::FilterMode::Linear,
569 ..Default::default()
570 });
571 EffectPipeline {
572 _shader: shader,
573 bind_group_layout,
574 pipeline,
575 sampler,
576 }
577}
578
579fn runtime_bind_group_layout(
580 device: &wgpu::Device,
581 params: &[ParamSpec],
582 resources: &[ResourceSpec],
583 inputs: &[InputSpec],
584) -> wgpu::BindGroupLayout {
585 let mut entries = vec![
586 runtime_texture_entry(0, wgpu::TextureViewDimension::D2),
587 runtime_texture_entry(1, wgpu::TextureViewDimension::D2),
588 wgpu::BindGroupLayoutEntry {
589 binding: 2,
590 visibility: wgpu::ShaderStages::FRAGMENT,
591 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
592 count: None,
593 },
594 runtime_uniform_entry(3, param_uniform_bytes(params.len())),
597 runtime_uniform_entry(4, RUNTIME_UNIFORM_WORDS * size_of::<f32>()),
598 ];
599 for (index, resource) in resources.iter().enumerate() {
600 let binding = u32::try_from(index).unwrap_or(0) * 2 + 5;
601 let dimension = match resource.kind {
602 EffectResourceKind::Lut3d => wgpu::TextureViewDimension::D3,
603 };
604 entries.push(runtime_texture_entry(binding, dimension));
605 entries.push(runtime_uniform_entry(binding + 1, RESOURCE_METADATA_BYTES));
606 }
607 for index in 0..inputs.len() {
608 entries.push(runtime_texture_entry(
609 input_binding(index),
610 wgpu::TextureViewDimension::D2,
611 ));
612 }
613 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
614 label: Some("alpha2 runtime effect layout"),
615 entries: &entries,
616 })
617}
618
619pub(crate) fn input_binding(index: usize) -> u32 {
620 INPUT_BINDING_BASE + u32::try_from(index).unwrap_or(0)
621}
622
623fn runtime_texture_entry(
624 binding: u32,
625 view_dimension: wgpu::TextureViewDimension,
626) -> wgpu::BindGroupLayoutEntry {
627 wgpu::BindGroupLayoutEntry {
628 binding,
629 visibility: wgpu::ShaderStages::FRAGMENT,
630 ty: wgpu::BindingType::Texture {
631 sample_type: wgpu::TextureSampleType::Float { filterable: true },
632 view_dimension,
633 multisampled: false,
634 },
635 count: None,
636 }
637}
638
639fn runtime_uniform_entry(binding: u32, bytes: usize) -> wgpu::BindGroupLayoutEntry {
640 wgpu::BindGroupLayoutEntry {
641 binding,
642 visibility: wgpu::ShaderStages::FRAGMENT,
643 ty: wgpu::BindingType::Buffer {
644 ty: wgpu::BufferBindingType::Uniform,
645 has_dynamic_offset: false,
646 min_binding_size: u64::try_from(bytes)
647 .ok()
648 .and_then(std::num::NonZeroU64::new),
649 },
650 count: None,
651 }
652}
653
654fn runtime_pipeline(
655 device: &wgpu::Device,
656 bind_group_layout: &wgpu::BindGroupLayout,
657 shader: &wgpu::ShaderModule,
658 format: wgpu::TextureFormat,
659) -> wgpu::RenderPipeline {
660 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
661 label: Some("alpha2 runtime effect pipeline layout"),
662 bind_group_layouts: &[Some(bind_group_layout)],
663 immediate_size: 0,
664 });
665 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
666 label: Some("alpha2 runtime effect pipeline"),
667 layout: Some(&layout),
668 vertex: wgpu::VertexState {
669 module: shader,
670 entry_point: Some("alpha2_vs"),
671 buffers: &[],
672 compilation_options: wgpu::PipelineCompilationOptions::default(),
673 },
674 fragment: Some(wgpu::FragmentState {
675 module: shader,
676 entry_point: Some("alpha2_fs"),
677 targets: &[Some(format.into())],
678 compilation_options: wgpu::PipelineCompilationOptions::default(),
679 }),
680 primitive: wgpu::PrimitiveState::default(),
681 depth_stencil: None,
682 multisample: wgpu::MultisampleState::default(),
683 multiview_mask: None,
684 cache: None,
685 })
686}
687
688pub(crate) fn uniform_layout(params: &[ParamSpec]) -> UniformLayout {
689 let fields = params
690 .iter()
691 .enumerate()
692 .map(|(index, param)| UniformField {
693 name: param.name.clone(),
694 offset: u32::try_from(index).expect("parameter count fits u32") * 16,
695 size: 16,
696 })
697 .collect::<Vec<_>>();
698 UniformLayout {
699 size: u32::try_from(param_uniform_bytes(params.len())).expect("parameter uniform fits u32"),
700 fields,
701 }
702}
703
704pub(crate) fn body_reads_time(body: &str) -> bool {
711 body.split(|character: char| !(character.is_alphanumeric() || character == '_'))
712 .any(|word| matches!(word, "time" | "time_ticks" | "runtime"))
713}
714
715pub(crate) fn build_shader(definition: &EffectDef) -> String {
716 let param_fields = if definition.params.is_empty() {
717 " _padding: vec4f,\n".to_owned()
718 } else {
719 definition
720 .params
721 .iter()
722 .fold(String::new(), |mut output, param| {
723 writeln!(
724 output,
725 " {}: {},",
726 param.name,
727 wgsl_type(param.param_type)
728 )
729 .expect("writing to a String cannot fail");
730 output
731 })
732 };
733 let uniform_fields =
734 (0..definition.params.len().max(1)).fold(String::new(), |mut output, index| {
735 writeln!(output, " slot_{index}: vec4u,").expect("writing to a String cannot fail");
736 output
737 });
738 let param_values = if definition.params.is_empty() {
739 "vec4f(0.0)".to_owned()
740 } else {
741 definition
742 .params
743 .iter()
744 .enumerate()
745 .map(|(index, param)| uniform_value(index, param.param_type))
746 .collect::<Vec<_>>()
747 .join(", ")
748 };
749 let call = match definition.kind {
750 EffectKind::Effect => "effect(input.uv, sample_src(input.uv))",
751 EffectKind::Transition => "alpha2_transition(input.uv)",
752 EffectKind::Generator => "generate(input.uv)",
753 };
754 let transition_wrapper = match definition.kind {
755 EffectKind::Effect | EffectKind::Generator => "",
756 EffectKind::Transition => {
757 r"fn alpha2_transition(uv: vec2f) -> vec4f {
758 if (progress() <= 0.0) { return from_color(uv); }
759 if (progress() >= 1.0) { return to_color(uv); }
760 return transition(uv);
761}"
762 }
763 };
764 let resource_declarations = resource_declarations(&definition.resources);
765 let input_declarations = input_declarations(&definition.inputs);
766 format!(
767 r"@group(0) @binding(0) var source_a: texture_2d<f32>;
768@group(0) @binding(1) var source_b: texture_2d<f32>;
769@group(0) @binding(2) var source_sampler: sampler;
770struct EffectParams {{
771{param_fields}}};
772struct EffectParamUniforms {{
773{uniform_fields}}};
774@group(0) @binding(3) var<uniform> param_uniforms: EffectParamUniforms;
775var<private> params: EffectParams;
776var<private> {BUDGET}: u32;
777struct RuntimeValues {{ resolution_time: vec4f, transition: vec4f, ticks: vec4u, }};
778@group(0) @binding(4) var<uniform> runtime: RuntimeValues;
779{resource_declarations}{input_declarations}
780
781fn sample_src(uv: vec2f) -> vec4f {{ return textureSampleLevel(source_a, source_sampler, uv, 0.0); }}
782fn from_color(uv: vec2f) -> vec4f {{ return textureSampleLevel(source_a, source_sampler, uv, 0.0); }}
783fn to_color(uv: vec2f) -> vec4f {{ return textureSampleLevel(source_b, source_sampler, uv, 0.0); }}
784fn resolution() -> vec2f {{ return runtime.resolution_time.xy; }}
785fn time() -> f32 {{ return runtime.resolution_time.z; }}
786fn time_ticks() -> vec2u {{ return runtime.ticks.xy; }}
787fn progress() -> f32 {{ return runtime.transition.x; }}
788fn ratio() -> f32 {{ return runtime.transition.y; }}
789fn max_filter_samples() -> f32 {{ return runtime.transition.z; }}
790fn load_params() -> EffectParams {{ return EffectParams({param_values}); }}
791
792{body}
793
794{transition_wrapper}
795
796struct VertexOutput {{ @builtin(position) position: vec4f, @location(0) uv: vec2f, }};
797@vertex fn alpha2_vs(@builtin(vertex_index) index: u32) -> VertexOutput {{
798 var p = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
799 var u = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));
800 return VertexOutput(vec4f(p[index], 0.0, 1.0), u[index]);
801}}
802@fragment fn alpha2_fs(input: VertexOutput) -> @location(0) vec4f {{
803 params = load_params();
804 {BUDGET} = {MAX_LOOP_ITERATIONS}u;
805 return {call};
806}}
807",
808 body =
810 bound_for_loops(&definition.wgsl_body).unwrap_or_else(|_| definition.wgsl_body.clone()),
811 resource_declarations = resource_declarations,
812 input_declarations = input_declarations,
813 transition_wrapper = transition_wrapper,
814 )
815}
816
817fn resource_declarations(resources: &[ResourceSpec]) -> String {
818 resources
819 .iter()
820 .enumerate()
821 .map(|(index, resource)| match resource.kind {
822 EffectResourceKind::Lut3d => format!(
823 "struct ResourceMetadata_{} {{ domain_min: vec4f, domain_max: vec4f, }};\n\
824 @group(0) @binding({}) var resource_{}: texture_3d<f32>;\n\
825 @group(0) @binding({}) var<uniform> resource_{}_metadata: ResourceMetadata_{};\n\
826 fn sample_{}(rgb: vec3f) -> vec3f {{\n\
827 let edge = f32(textureDimensions(resource_{}).x);\n\
828 let normalized = clamp((rgb - resource_{}_metadata.domain_min.xyz) /\n\
829 (resource_{}_metadata.domain_max.xyz - resource_{}_metadata.domain_min.xyz),\n\
830 vec3f(0.0), vec3f(1.0));\n\
831 let coord = (normalized * (edge - 1.0) + 0.5) / edge;\n\
832 return textureSampleLevel(resource_{}, source_sampler, coord, 0.0).rgb;\n\
833 }}",
834 resource.name,
835 index * 2 + 5,
836 resource.name,
837 index * 2 + 6,
838 resource.name,
839 resource.name,
840 resource.name,
841 resource.name,
842 resource.name,
843 resource.name,
844 resource.name,
845 resource.name,
846 ),
847 })
848 .collect::<Vec<_>>()
849 .join("\n")
850}
851
852fn input_declarations(inputs: &[InputSpec]) -> String {
854 inputs
855 .iter()
856 .enumerate()
857 .fold(String::new(), |mut output, (index, input)| {
858 let name = &input.name;
859 write!(
860 output,
861 "\n@group(0) @binding({binding}) var input_{name}: texture_2d<f32>;\n\
862 fn sample_{name}(uv: vec2f) -> vec4f {{ return textureSampleLevel(input_{name}, source_sampler, uv, 0.0); }}\n\
863 fn size_{name}() -> vec2f {{ return vec2f(textureDimensions(input_{name})); }}",
864 binding = input_binding(index),
865 )
866 .expect("writing to a String cannot fail");
867 output
868 })
869}
870
871fn uniform_value(index: usize, param_type: ParamType) -> String {
872 match param_type {
873 ParamType::Float => format!("bitcast<f32>(param_uniforms.slot_{index}.x)"),
874 ParamType::Vec2 => format!("bitcast<vec2f>(param_uniforms.slot_{index}.xy)"),
875 ParamType::Vec4 | ParamType::Color => {
876 format!("bitcast<vec4f>(param_uniforms.slot_{index})")
877 }
878 ParamType::Enum | ParamType::Bool => format!("param_uniforms.slot_{index}.x"),
879 }
880}
881
882fn wgsl_type(param_type: ParamType) -> &'static str {
883 match param_type {
884 ParamType::Float => "f32",
885 ParamType::Vec2 => "vec2f",
886 ParamType::Vec4 | ParamType::Color => "vec4f",
887 ParamType::Enum | ParamType::Bool => "u32",
888 }
889}
890
891fn validate_definition(definition: &EffectDef) -> Result<(), ShaderDiagnostics> {
892 if !valid_identifier(&definition.id) {
893 return Err(ShaderDiagnostics::error(
894 "effect id must contain only ASCII letters, digits, '-', '_', or '.'",
895 ));
896 }
897 if definition.version == 0 {
898 return Err(ShaderDiagnostics::error("effect version must be non-zero"));
899 }
900 validate_wgsl_body(&definition.wgsl_body)?;
901 if !definition.inputs.is_empty() && definition.kind != EffectKind::Effect {
902 return Err(ShaderDiagnostics::error(
903 "only effects declare named inputs; a transition samples from and to",
904 ));
905 }
906 if definition.inputs.len() > MAX_EFFECT_INPUTS {
907 return Err(ShaderDiagnostics::error(format!(
908 "effect declares more than {MAX_EFFECT_INPUTS} named inputs"
909 )));
910 }
911 let mut names = std::collections::HashSet::new();
912 for param in &definition.params {
913 validate_param(param)?;
914 if !names.insert(param.name.as_str()) {
915 return Err(ShaderDiagnostics::error(format!(
916 "duplicate parameter name {}",
917 param.name
918 )));
919 }
920 }
921 if definition.resources.len() > MAX_EFFECT_RESOURCES {
922 return Err(ShaderDiagnostics::error(format!(
923 "effect declares more than {MAX_EFFECT_RESOURCES} resources"
924 )));
925 }
926 let mut resource_names = std::collections::HashSet::new();
927 let mut declared = std::collections::HashSet::new();
929 for resource in &definition.resources {
930 let generated = resource_declarations_of(&resource.name);
931 if !valid_wgsl_identifier(&resource.name)
932 || names.contains(resource.name.as_str())
933 || !resource_names.insert(resource.name.as_str())
934 || !generated
935 .iter()
936 .all(|name| valid_wgsl_identifier(name) && declared.insert(name.clone()))
937 {
938 return Err(ShaderDiagnostics::error(format!(
939 "invalid or duplicate effect resource name {}",
940 resource.name
941 )));
942 }
943 }
944 let mut input_names = std::collections::HashSet::new();
946 for input in &definition.inputs {
947 let generated = input_declarations_of(&input.name);
948 if !valid_wgsl_identifier(&input.name)
949 || names.contains(input.name.as_str())
950 || resource_names.contains(input.name.as_str())
951 || !input_names.insert(input.name.as_str())
952 || !generated
953 .iter()
954 .all(|name| valid_wgsl_identifier(name) && declared.insert(name.clone()))
955 {
956 return Err(ShaderDiagnostics::error(format!(
957 "invalid or duplicate effect input name {}",
958 input.name
959 )));
960 }
961 }
962 Ok(())
963}
964
965fn input_declarations_of(name: &str) -> [String; 3] {
968 [
969 format!("input_{name}"),
970 format!("sample_{name}"),
971 format!("size_{name}"),
972 ]
973}
974
975fn resource_declarations_of(name: &str) -> [String; 4] {
977 [
978 format!("resource_{name}"),
979 format!("resource_{name}_metadata"),
980 format!("sample_{name}"),
981 format!("ResourceMetadata_{name}"),
982 ]
983}
984
985fn validate_wgsl_body(body: &str) -> Result<(), ShaderDiagnostics> {
986 if body.len() > MAX_WGSL_BODY_BYTES {
987 return Err(ShaderDiagnostics::error(format!(
988 "WGSL body exceeds {MAX_WGSL_BODY_BYTES} bytes"
989 )));
990 }
991 if body
992 .chars()
993 .any(|character| character.is_control() && !matches!(character, '\n' | '\r' | '\t'))
994 {
995 return Err(ShaderDiagnostics::error(
996 "WGSL body contains unsupported control characters",
997 ));
998 }
999 let code = blank_comments(body);
1001 if let Some(identifier) = identifiers(&code).find(|word| word.starts_with(RESERVED_PREFIX)) {
1002 return Err(ShaderDiagnostics::error(format!(
1003 "WGSL identifier {identifier} is reserved: names starting with {RESERVED_PREFIX} belong to the generated wrapper"
1004 )));
1005 }
1006 if code.split([';', '{', '}']).any(|statement| {
1007 statement.bytes().filter(|byte| is_operator(*byte)).count() > MAX_WGSL_STATEMENT_OPERATORS
1008 }) {
1009 return Err(ShaderDiagnostics::error(format!(
1010 "a WGSL statement has more than {MAX_WGSL_STATEMENT_OPERATORS} operators; split it with `let`"
1011 )));
1012 }
1013 let mut delimiters = Vec::new();
1014 for character in code.chars() {
1015 match character {
1016 '{' | '(' | '[' => {
1017 delimiters.push(character);
1018 if delimiters.len() > MAX_WGSL_NESTING {
1019 return Err(ShaderDiagnostics::error(format!(
1020 "WGSL nesting exceeds {MAX_WGSL_NESTING} levels"
1021 )));
1022 }
1023 }
1024 '}' | ')' | ']' => {
1025 let matches = matches!(
1026 (delimiters.pop(), character),
1027 (Some('{'), '}') | (Some('('), ')') | (Some('['), ']')
1028 );
1029 if !matches {
1030 return Err(ShaderDiagnostics::error(
1031 "WGSL body has unbalanced delimiters",
1032 ));
1033 }
1034 }
1035 _ => {}
1036 }
1037 }
1038 if !delimiters.is_empty() {
1039 return Err(ShaderDiagnostics::error(
1040 "WGSL body has unbalanced delimiters",
1041 ));
1042 }
1043
1044 let forbidden = [
1045 "@binding",
1046 "@compute",
1047 "@group",
1048 "atomic",
1049 "enable",
1050 "loop",
1051 "override",
1052 "var<storage",
1053 "var<uniform",
1054 "var<workgroup",
1055 "while",
1056 ];
1057 for token in code.split(|character: char| {
1058 !(character.is_ascii_alphanumeric() || matches!(character, '_' | '@' | '<'))
1059 }) {
1060 if forbidden.contains(&token) {
1061 return Err(ShaderDiagnostics::error(format!(
1062 "WGSL token {token} is not allowed in a runtime effect"
1063 )));
1064 }
1065 }
1066 bound_for_loops(body)?;
1067 crate::call_budget::check_invocation_calls(&code)?;
1068 crate::effect_cost::check_invocation_cost(&code).map(drop)
1069}
1070
1071fn identifiers(code: &str) -> impl Iterator<Item = &str> {
1073 code.split(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
1074 .filter(|word| word.starts_with(|first: char| first.is_ascii_alphabetic() || first == '_'))
1075}
1076
1077const fn is_operator(byte: u8) -> bool {
1080 matches!(
1081 byte,
1082 b'+' | b'-'
1083 | b'*'
1084 | b'/'
1085 | b'%'
1086 | b'&'
1087 | b'|'
1088 | b'^'
1089 | b'!'
1090 | b'~'
1091 | b'<'
1092 | b'>'
1093 | b'='
1094 | b'.'
1095 | b'['
1096 )
1097}
1098
1099fn validate_param(param: &ParamSpec) -> Result<(), ShaderDiagnostics> {
1100 if !valid_wgsl_identifier(¶m.name) {
1101 return Err(ShaderDiagnostics::error(format!(
1102 "invalid WGSL parameter name {}",
1103 param.name
1104 )));
1105 }
1106 let compatible = matches!(
1107 (¶m.param_type, ¶m.default, ¶m.unit),
1108 (
1109 ParamType::Float,
1110 ParamValue::Float(_),
1111 ParamUnit::Normalized | ParamUnit::Degrees
1112 ) | (ParamType::Vec2, ParamValue::Vec2(_), ParamUnit::Normalized)
1113 | (ParamType::Vec4, ParamValue::Vec4(_), ParamUnit::Normalized)
1114 | (ParamType::Color, ParamValue::Color(_), ParamUnit::Color)
1115 | (ParamType::Enum, ParamValue::Enum(_), ParamUnit::Enum)
1116 | (ParamType::Bool, ParamValue::Bool(_), ParamUnit::Bool)
1117 );
1118 if !compatible {
1119 return Err(ShaderDiagnostics::error(format!(
1120 "parameter {} has incompatible type, default, and unit",
1121 param.name
1122 )));
1123 }
1124 if !finite_value(¶m.default)
1125 || param.min.is_some_and(|value| !value.is_finite())
1126 || param.max.is_some_and(|value| !value.is_finite())
1127 || param
1128 .step
1129 .is_some_and(|value| !value.is_finite() || value <= 0.0)
1130 || matches!((param.min, param.max), (Some(min), Some(max)) if min > max)
1131 {
1132 return Err(ShaderDiagnostics::error(format!(
1133 "parameter {} has an invalid numeric range",
1134 param.name
1135 )));
1136 }
1137 if !matches!(
1138 param.param_type,
1139 ParamType::Float | ParamType::Vec2 | ParamType::Vec4
1140 ) && (param.min.is_some() || param.max.is_some() || param.step.is_some())
1141 {
1142 return Err(ShaderDiagnostics::error(format!(
1143 "only float, vec2, or vec4 parameter {} may declare a numeric range",
1144 param.name
1145 )));
1146 }
1147 if matches!(
1148 param.param_type,
1149 ParamType::Float | ParamType::Vec2 | ParamType::Vec4
1150 ) && !value_in_range(¶m.default, param.min, param.max)
1151 {
1152 return Err(ShaderDiagnostics::error(format!(
1153 "parameter {} default is outside its numeric range",
1154 param.name
1155 )));
1156 }
1157 if let ParamValue::Color(values) = ¶m.default
1158 && !values.iter().all(|value| (0.0..=1.0).contains(value))
1159 {
1160 return Err(ShaderDiagnostics::error(format!(
1161 "color parameter {} default must be inside 0..=1",
1162 param.name
1163 )));
1164 }
1165 if param.param_type == ParamType::Enum && param.enum_values.is_empty() {
1166 return Err(ShaderDiagnostics::error(format!(
1167 "enum parameter {} requires named values",
1168 param.name
1169 )));
1170 }
1171 if let ParamValue::Enum(value) = ¶m.default
1172 && usize::try_from(*value).map_or(true, |index| index >= param.enum_values.len())
1173 {
1174 return Err(ShaderDiagnostics::error(format!(
1175 "enum parameter {} default is out of range",
1176 param.name
1177 )));
1178 }
1179 Ok(())
1180}
1181
1182fn value_in_range(value: &ParamValue, min: Option<f32>, max: Option<f32>) -> bool {
1183 let in_range = |value: &f32| {
1184 min.is_none_or(|minimum| *value >= minimum) && max.is_none_or(|maximum| *value <= maximum)
1185 };
1186 match value {
1187 ParamValue::Float(value) => in_range(value),
1188 ParamValue::Vec2(values) => values.iter().all(in_range),
1189 ParamValue::Vec4(values) => values.iter().all(in_range),
1190 _ => true,
1191 }
1192}
1193
1194fn finite_value(value: &ParamValue) -> bool {
1195 match value {
1196 ParamValue::Float(value) => value.is_finite(),
1197 ParamValue::Vec2(values) => values.iter().all(|value| value.is_finite()),
1198 ParamValue::Vec4(values) | ParamValue::Color(values) => {
1199 values.iter().all(|value| value.is_finite())
1200 }
1201 ParamValue::Enum(_) | ParamValue::Bool(_) => true,
1202 }
1203}
1204
1205fn valid_identifier(value: &str) -> bool {
1206 !value.is_empty()
1207 && value
1208 .bytes()
1209 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
1210}
1211
1212fn valid_wgsl_identifier(value: &str) -> bool {
1213 let mut bytes = value.bytes();
1214 bytes
1215 .next()
1216 .is_some_and(|byte| byte.is_ascii_alphabetic() || byte == b'_')
1217 && bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
1218 && !value.starts_with(RESERVED_PREFIX)
1219 && !matches!(
1220 value,
1221 "params"
1222 | "resolution"
1223 | "time"
1224 | "time_ticks"
1225 | "progress"
1226 | "ratio"
1227 | "sample_src"
1228 | "from_color"
1229 | "to_color"
1230 | "load_params"
1231 | "max_filter_samples"
1232 | "source_a"
1233 | "source_b"
1234 | "source_sampler"
1235 | "param_uniforms"
1236 | "runtime"
1237 | "EffectParams"
1238 )
1239}
1240
1241fn compilation_diagnostics(info: wgpu::CompilationInfo) -> ShaderDiagnostics {
1242 ShaderDiagnostics {
1243 messages: info
1244 .messages
1245 .into_iter()
1246 .map(|message| {
1247 let severity = match message.message_type {
1248 wgpu::CompilationMessageType::Error => DiagnosticSeverity::Error,
1249 wgpu::CompilationMessageType::Warning => DiagnosticSeverity::Warning,
1250 wgpu::CompilationMessageType::Info => DiagnosticSeverity::Info,
1251 };
1252 ShaderDiagnostic {
1253 severity,
1254 message: message.message,
1255 line: message.location.map(|location| location.line_number),
1256 column: message.location.map(|location| location.line_position),
1257 }
1258 })
1259 .collect(),
1260 }
1261}
1262
1263#[cfg(test)]
1264#[path = "registry_tests.rs"]
1265mod tests;