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