Skip to main content

alpha2_render/
registry.rs

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
16/// Effects a host may register. The built-ins, a fixed set the build
17/// decides, do not count against it.
18const MAX_REGISTERED_EFFECTS: usize = 256;
19const MAX_WGSL_BODY_BYTES: usize = 32 * 1024;
20const MAX_WGSL_NESTING: usize = 64;
21/// Operator characters one statement may hold. A flat chain such as
22/// `x+x+...+x` has no delimiters to nest, but it is as deep as it is long, and
23/// naga lowers it by recursing once per operand: about 2,500 operands
24/// overflow a 1 MiB stack (a Windows main thread, wasm32) and abort the
25/// process instead of failing the registration. Engine bodies stay under 32.
26const MAX_WGSL_STATEMENT_OPERATORS: usize = 256;
27/// The identifier prefix of everything the generated wrapper declares.
28const RESERVED_PREFIX: &str = "alpha2_";
29const MAX_EFFECT_RESOURCES: usize = 8;
30/// The most named inputs an effect declares. Its source pair, resources, and
31/// inputs must also fit the sampled textures a device offers a shader stage
32/// (16 on WebGL2-class limits), which registration checks per device.
33pub const MAX_EFFECT_INPUTS: usize = 8;
34/// The binding of an effect's first named input: past every resource slot a
35/// definition may declare, so an input never moves another binding.
36#[allow(clippy::cast_possible_truncation)]
37const INPUT_BINDING_BASE: u32 = 5 + 2 * MAX_EFFECT_RESOURCES as u32;
38/// A resource's `ResourceMetadata_<name>` uniform: `domain_min` and
39/// `domain_max`, two `vec4f`.
40const RESOURCE_METADATA_BYTES: usize = 32;
41
42/// Scopes for every class of error that creating a shader module or a
43/// pipeline can raise.
44///
45/// Validation alone is not enough: `wgpu` hands an error only to a scope of
46/// its own class, and a backend compiler that rejects the code generated from
47/// valid WGSL (Metal's for MSL, DXC or FXC for HLSL, a GL driver or `ANGLE`
48/// for GLSL) reports an internal error, as a device out of memory reports its
49/// own class. Uncaught, either goes to the uncaptured-error handler and the
50/// effect registers with an invalid pipeline, which then invalidates every
51/// submission that draws it.
52struct 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    /// Closes the scopes now, in the reverse order of their pushes, and
64    /// returns what they caught when the device answers.
65    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/// A device's answer still to come. Browser WebGPU answers on its own event
80/// loop, so these are not `Send` there; natively they are.
81#[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
88/// A runtime effect whose shader module and pipeline exist on the device,
89/// with the device's verdict on them still to come: [`Renderer::compile_effect`]
90/// made it, [`PendingEffect::compiled`] waits for the verdict, and
91/// [`Renderer::register_compiled`] adds what passed.
92///
93/// It borrows nothing from the renderer, so waiting for a browser's shader
94/// compiler (a GPU process round trip) leaves the renderer free to render,
95/// to compile other effects, or to be dropped.
96pub 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    /// Waits for the device's verdict on the shader and the pipeline.
114    ///
115    /// # Errors
116    /// Returns the shader's diagnostics when it failed to compile, else the
117    /// errors creating its pipeline raised.
118    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        // A pipeline made from a shader that failed fails too; the shader's own
129        // diagnostics say why.
130        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
155/// A runtime effect the device compiled, ready for
156/// [`Renderer::register_compiled`] on the renderer that compiled it.
157pub 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    /// The identity the effect registers under.
174    #[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    /// Refuses `id` when it is registered already or the host's share of the
189    /// registry is full. A built-in is counted only once it is marked one, so
190    /// the built-ins registering never fill it.
191    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    /// Whether the body can read the frame time ([`body_reads_time`]).
213    reads_time: bool,
214    /// Made when a host's effect registers, its verdict awaited then; a
215    /// built-in's the first time it is drawn or warmed
216    /// ([`Renderer::effect_pipeline`]).
217    pipeline: std::sync::OnceLock<EffectPipeline>,
218}
219
220/// What a registered effect draws with on the device.
221#[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    /// Validates, compiles, and registers a runtime effect definition:
231    /// [`Self::compile_effect`], [`PendingEffect::compiled`], then
232    /// [`Self::register_compiled`].
233    ///
234    /// The returned future works on native and browser WebGPU backends because
235    /// compilation diagnostics are asynchronous in the WebGPU API. It holds
236    /// the renderer for as long as the device takes to answer; a host that
237    /// must use the renderer meanwhile calls the three steps itself.
238    ///
239    /// # Errors
240    /// Returns schema or WGSL diagnostics without mutating the registry.
241    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    /// Validates a runtime effect definition and creates its shader module
250    /// and pipeline, without waiting for the device's verdict on them.
251    ///
252    /// Everything is created, and every error scope pushed and popped, within
253    /// this call, so registrations that overlap never share a scope.
254    ///
255    /// # Errors
256    /// Returns schema diagnostics, or refuses an effect already registered or
257    /// a full registry, without touching the device.
258    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    /// Validates a built-in definition and registers it without touching the
287    /// device: its shader module and pipeline are made the first time it is
288    /// drawn or warmed ([`Self::warmup`]). Compiling every built-in at once
289    /// would cost each renderer seconds on a software adapter, most of them for
290    /// effects a project never draws; the engine's own shaders are checked on
291    /// every backend before they ship.
292    ///
293    /// # Errors
294    /// Returns schema diagnostics, or refuses an effect already registered or
295    /// a full registry.
296    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    /// Everything registering `definition` checks before it touches the
308    /// device, and the identity it registers under.
309    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    /// What a registered effect draws with, made now when it was not yet.
326    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    /// Adds an effect this renderer compiled.
340    ///
341    /// # Errors
342    /// Refuses an effect compiled on another device, one registered while it
343    /// compiled, and a registry that filled meanwhile.
344    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    /// Refuses parameters whose uniform this device cannot bind.
363    ///
364    /// Every parameter is one 16-byte slot of a single uniform binding, and a
365    /// device caps a binding's size (16 KiB on the WebGL2 limits every alpha2
366    /// device asks for). The pipeline compiles past the cap; it is the bind
367    /// group of each frame that fails, invalidating its submission, so the
368    /// registration refuses it instead.
369    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    /// Refuses a definition whose textures this device cannot bind in one
384    /// shader stage: the source pair, each resource, and each named input take
385    /// one sampled texture, and a device caps them (16 on the WebGL2 limits
386    /// every alpha2 device asks for). Past the cap the layout fails on the
387    /// device; this says why before it is asked.
388    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    /// Returns the generated host uniform layout for a registered effect.
404    #[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    /// Returns the immutable definition for a registered effect.
413    #[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    /// Whether what `id` draws can change with the frame time alone: whether
419    /// its body, or for a built-in drawn as several passes the body of any
420    /// of them, can read `time()` or `time_ticks()` ([`EffectRuntime::time`]). An
421    /// effect that is not registered answers `true`.
422    ///
423    /// The answer comes from the WGSL itself, so it cannot disagree with the
424    /// shader: a body that never names `time`, `time_ticks` or `runtime`, the
425    /// uniform they read, cannot reach the frame time, and every other body is
426    /// taken to read it. A caller may then pass any time to an effect that
427    /// answers `false` (the timeline passes zero), so its result is found
428    /// again in another frame from the same inputs and parameters.
429    ///
430    /// [`EffectRuntime::time`]: crate::EffectRuntime::time
431    #[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    /// Returns whether an exact effect version has been compiled.
445    #[must_use]
446    pub fn is_effect_registered(&self, id: &EffectId) -> bool {
447        self.effects.entries.contains_key(id)
448    }
449
450    /// Returns whether an effect may be referenced by a project document.
451    #[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    /// The identity a project's reference to effect `id` version `version`
457    /// draws with: the one it is registered under, when a project may
458    /// reference it ([`Self::is_effect_project_visible`]). Looking it up and
459    /// cloning it allocate nothing, so a frame names its effects without
460    /// copying their text.
461    #[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    /// Removes one compiled runtime effect and releases its GPU resources.
472    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    /// Removes every host-registered runtime effect.
484    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    /// Confirms that all requested effect versions are registered, and makes
492    /// the pipelines of the built-ins among them (and of the passes a built-in
493    /// draws as) not drawn yet, so a project's first frame does not wait for
494    /// their compiler. A host's effect compiled when it registered.
495    ///
496    /// # Errors
497    /// Returns a diagnostic naming the first unregistered effect.
498    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
538/// A registry entry for `definition`, its pipeline not made yet.
539fn 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        // At the sizes the generated structs take and each pass writes: a pipeline whose
600        // shader reads past them fails to compile instead of reading an arena's next slot.
601        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
624/// The binding of an effect's named input `index`, in declaration order.
625pub(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
710/// Whether an effect body can read the frame time. The prelude gives it
711/// only through `time()` and `time_ticks()`, which read the `runtime`
712/// uniform, and WGSL has no other way to reach any of them than by naming it,
713/// so a body in which none of the names occurs as a word cannot read it.
714/// Everything else counts as reading it, comments and a local that shadows
715/// the name included: a false answer would freeze a picture that should
716/// change, a true one only redraws it.
717///
718/// A word here is a run of letters, digits, and underscores. Every character
719/// that may follow an identifier in WGSL (space, punctuation, an operator,
720/// a comment) ends one, so every `time`, `time_ticks` or `runtime` token is
721/// such a word.
722pub(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        // Registration validated the body, which includes delimiting its loops, so this
821        // only falls back for a definition that skipped it.
822        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
865/// What the wrapper declares for each named input: its texture, its sampling
866/// helper, and its size. Empty without inputs, so an effect that declares none adds
867/// nothing to its module.
868fn 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    // What the wrapper declares for each resource, at module scope beside its own names:
943    // none may be one of those or another resource's.
944    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    // An input's names share the module scope with the resources'.
961    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
981/// The module-scope names [`build_shader`] declares for a named input called
982/// `name`. The name `src` is refused through them: `sample_src` is the
983/// prelude's.
984fn input_declarations_of(name: &str) -> [String; 3] {
985    [
986        format!("input_{name}"),
987        format!("sample_{name}"),
988        format!("size_{name}"),
989    ]
990}
991
992/// The module-scope names [`build_shader`] declares for a resource named
993/// `name`.
994fn 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    // Comments may say anything; only code is checked, for balance as for tokens.
1018    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
1088/// The identifiers in `code`: words that start with a letter or `_`, so the
1089/// digits and suffixes of a number are not one.
1090fn 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
1095/// Counts every character that can make an expression one level deeper:
1096/// operators, member access, and indexing. It overcounts (the `.` of `1.0`,
1097/// the `<` of `vec4<f32>`), which only lowers the effective limit.
1098const 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(&param.name) {
1120        return Err(ShaderDiagnostics::error(format!(
1121            "invalid WGSL parameter name {}",
1122            param.name
1123        )));
1124    }
1125    let compatible = matches!(
1126        (&param.param_type, &param.default, &param.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(&param.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(&param.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) = &param.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) = &param.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;