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