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