Skip to main content

alpha2_render/
registry.rs

1use std::collections::{HashMap, HashSet};
2use std::fmt::Write;
3
4use alpha2_gpu::{CapabilityTier, GpuContext};
5
6use crate::effect::EffectKey;
7use crate::loop_bound::{BUDGET, MAX_LOOP_ITERATIONS, blank_comments, bound_for_loops};
8use crate::uniform::{PARAM_SLOT_BYTES, RUNTIME_UNIFORM_WORDS, param_uniform_bytes};
9
10use crate::{
11    DiagnosticSeverity, EffectDef, EffectId, EffectKind, EffectResourceKind, InputSpec, ParamSpec,
12    ParamType, ParamUnit, ParamValue, Renderer, ResourceSpec, ShaderDiagnostic, ShaderDiagnostics,
13    UniformField, UniformLayout,
14};
15
16/// Effects a host may register; built-ins do not count.
17const MAX_REGISTERED_EFFECTS: usize = 256;
18const MAX_WGSL_BODY_BYTES: usize = 32 * 1024;
19const MAX_WGSL_NESTING: usize = 64;
20/// Operator characters one statement may hold.
21///
22/// naga recurses once per operand of a flat chain like `x+x+...+x`; about 2,500 operands
23/// overflow a 1 MiB stack (Windows main thread, wasm32) and abort the process. Engine bodies
24/// stay under 32. DXC needs more at this limit, hence `D3D12_COMPILE_STACK`.
25const MAX_WGSL_STATEMENT_OPERATORS: usize = 256;
26/// Identifier prefix of everything the generated wrapper declares.
27const RESERVED_PREFIX: &str = "alpha2_";
28const MAX_EFFECT_RESOURCES: usize = 8;
29/// Most named inputs an effect declares. Source pair, resources and inputs must also fit the
30/// device's sampled textures per stage (16 on WebGL2-class limits), checked at registration.
31pub const MAX_EFFECT_INPUTS: usize = 8;
32/// First named input's binding, after every possible resource slot so inputs never shift other
33/// bindings.
34#[allow(clippy::cast_possible_truncation)]
35const INPUT_BINDING_BASE: u32 = 5 + 2 * MAX_EFFECT_RESOURCES as u32;
36/// `ResourceMetadata_<name>`: `domain_min` and `domain_max`, two `vec4f`.
37const RESOURCE_METADATA_BYTES: usize = 32;
38
39/// Scopes for every error class shader-module or pipeline creation can raise.
40///
41/// `wgpu` routes an error only to a scope of its class. A backend compiler rejecting code
42/// generated from valid WGSL reports an internal error, and OOM its own class; uncaught, either
43/// leaves an invalid pipeline that invalidates every submission drawing it.
44struct CreationErrorScopes([wgpu::ErrorScopeGuard; 3]);
45
46impl CreationErrorScopes {
47    fn push(device: &wgpu::Device) -> Self {
48        Self([
49            device.push_error_scope(wgpu::ErrorFilter::Internal),
50            device.push_error_scope(wgpu::ErrorFilter::OutOfMemory),
51            device.push_error_scope(wgpu::ErrorFilter::Validation),
52        ])
53    }
54
55    /// Pops the scopes in reverse push order and returns what they caught.
56    fn pop(self) -> ScopeErrors {
57        let [internal, out_of_memory, validation] = self.0;
58        let validation = validation.pop();
59        let out_of_memory = out_of_memory.pop();
60        let internal = internal.pop();
61        Box::pin(async move {
62            [validation.await, out_of_memory.await, internal.await]
63                .into_iter()
64                .flatten()
65                .collect()
66        })
67    }
68}
69
70/// Stack size for D3D12 effect pipeline compiles. DXC recurses per operand and needs about
71/// 1.5 MiB at [`MAX_WGSL_STATEMENT_OPERATORS`], more than a Windows main thread has.
72#[cfg(not(target_arch = "wasm32"))]
73const D3D12_COMPILE_STACK: usize = 8 << 20;
74
75/// Runs `make` on a `D3D12_COMPILE_STACK` thread on D3D12, or the calling thread elsewhere.
76/// Error scopes are per thread, so `make` pushes and pops its own.
77#[cfg(not(target_arch = "wasm32"))]
78fn on_compile_stack<T: Send>(gpu: &GpuContext, make: impl FnOnce() -> T + Send) -> T {
79    if gpu.capabilities().backend != "Dx12" {
80        return make();
81    }
82    std::thread::scope(|scope| {
83        let compile = std::thread::Builder::new()
84            .name("alpha2 shader compile".to_owned())
85            .stack_size(D3D12_COMPILE_STACK)
86            .spawn_scoped(scope, make)
87            .expect("the shader compile thread starts");
88        compile
89            .join()
90            .unwrap_or_else(|panic| std::panic::resume_unwind(panic))
91    })
92}
93
94#[cfg(target_arch = "wasm32")]
95fn on_compile_stack<T>(_gpu: &GpuContext, make: impl FnOnce() -> T) -> T {
96    make()
97}
98
99/// Not `Send` in a browser, where the device answers on its own event loop.
100#[cfg(not(target_arch = "wasm32"))]
101type Answer<T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send>>;
102#[cfg(target_arch = "wasm32")]
103type Answer<T> = std::pin::Pin<Box<dyn std::future::Future<Output = T>>>;
104
105type ScopeErrors = Answer<Vec<wgpu::Error>>;
106
107/// A runtime effect created on the device, awaiting the device's verdict.
108///
109/// Made by [`Renderer::compile_effect`], awaited with [`PendingEffect::compiled`], added by
110/// [`Renderer::register_compiled`]. It borrows nothing from the renderer, so the renderer stays
111/// usable while a browser's shader compiler answers.
112pub struct PendingEffect {
113    compiled: CompiledEffect,
114    compilation: Answer<wgpu::CompilationInfo>,
115    shader_errors: ScopeErrors,
116    pipeline_errors: ScopeErrors,
117}
118
119impl std::fmt::Debug for PendingEffect {
120    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        formatter
122            .debug_struct("PendingEffect")
123            .field("id", &self.compiled.id)
124            .finish_non_exhaustive()
125    }
126}
127
128impl PendingEffect {
129    /// Waits for the device's verdict on the shader and pipeline.
130    ///
131    /// # Errors
132    /// The shader's diagnostics if it failed, else the pipeline creation errors.
133    pub async fn compiled(self) -> Result<CompiledEffect, ShaderDiagnostics> {
134        let mut diagnostics = compilation_diagnostics(self.compilation.await);
135        for error in self.shader_errors.await {
136            diagnostics.messages.push(ShaderDiagnostic {
137                severity: DiagnosticSeverity::Error,
138                message: error.to_string(),
139                line: None,
140                column: None,
141            });
142        }
143        // A pipeline from a failed shader fails too; report the shader's diagnostics.
144        if diagnostics
145            .messages
146            .iter()
147            .any(|message| message.severity == DiagnosticSeverity::Error)
148        {
149            return Err(diagnostics);
150        }
151        let errors = self.pipeline_errors.await;
152        if !errors.is_empty() {
153            return Err(ShaderDiagnostics {
154                messages: errors
155                    .into_iter()
156                    .map(|error| ShaderDiagnostic {
157                        severity: DiagnosticSeverity::Error,
158                        message: error.to_string(),
159                        line: None,
160                        column: None,
161                    })
162                    .collect(),
163            });
164        }
165        Ok(self.compiled)
166    }
167}
168
169/// A compiled runtime effect, ready for [`Renderer::register_compiled`] on the same renderer.
170pub struct CompiledEffect {
171    device: wgpu::Device,
172    id: EffectId,
173    effect: RegisteredEffect,
174}
175
176impl std::fmt::Debug for CompiledEffect {
177    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178        formatter
179            .debug_struct("CompiledEffect")
180            .field("id", &self.id)
181            .finish_non_exhaustive()
182    }
183}
184
185impl CompiledEffect {
186    /// The identity the effect registers under.
187    #[must_use]
188    pub const fn id(&self) -> &EffectId {
189        &self.id
190    }
191}
192
193#[derive(Debug, Default)]
194pub(crate) struct EffectRegistry {
195    entries: HashMap<EffectId, RegisteredEffect>,
196    builtins: HashSet<EffectId>,
197    internal: HashSet<EffectId>,
198}
199
200impl EffectRegistry {
201    /// Refuses `id` when already registered or the host's share is full. Built-ins do not count.
202    fn check_room(&self, id: &EffectId) -> Result<(), ShaderDiagnostics> {
203        if self.entries.contains_key(id) {
204            return Err(ShaderDiagnostics::error(format!(
205                "effect {} version {} is already registered",
206                id.as_str(),
207                id.version()
208            )));
209        }
210        if self.entries.len() - self.builtins.len() >= MAX_REGISTERED_EFFECTS {
211            return Err(ShaderDiagnostics::error(format!(
212                "effect registry is full (maximum {MAX_REGISTERED_EFFECTS})"
213            )));
214        }
215        Ok(())
216    }
217}
218
219#[derive(Debug)]
220pub(crate) struct RegisteredEffect {
221    pub(crate) definition: EffectDef,
222    pub(crate) uniform_layout: UniformLayout,
223    reads_time: bool,
224    /// Worst-case per-pixel cost of one invocation.
225    pub(crate) cost: crate::effect_cost::EffectCost,
226    /// Created at registration for host effects, and on first draw or warmup for built-ins.
227    pipeline: std::sync::OnceLock<EffectPipeline>,
228}
229
230#[derive(Debug)]
231pub(crate) struct EffectPipeline {
232    _shader: wgpu::ShaderModule,
233    pub(crate) bind_group_layout: wgpu::BindGroupLayout,
234    pub(crate) pipeline: wgpu::RenderPipeline,
235    pub(crate) sampler: wgpu::Sampler,
236}
237
238impl Renderer {
239    /// Validates, compiles and registers a runtime effect: [`Self::compile_effect`],
240    /// [`PendingEffect::compiled`], then [`Self::register_compiled`].
241    ///
242    /// Holds the renderer until the device answers; a host that needs the renderer meanwhile calls
243    /// the three steps itself.
244    ///
245    /// # Errors
246    /// Returns schema or WGSL diagnostics without mutating the registry.
247    pub async fn register_effect(
248        &mut self,
249        definition: EffectDef,
250    ) -> Result<EffectId, ShaderDiagnostics> {
251        let compiled = self.compile_effect(definition)?.compiled().await?;
252        self.register_compiled(compiled)
253    }
254
255    /// Validates a runtime effect and creates its shader module and pipeline without waiting for
256    /// the verdict.
257    ///
258    /// All error scopes open and close within this call, so overlapping registrations never share
259    /// one.
260    ///
261    /// # Errors
262    /// Schema diagnostics, a duplicate id or a full registry, without touching the device.
263    pub fn compile_effect(
264        &self,
265        definition: EffectDef,
266    ) -> Result<PendingEffect, ShaderDiagnostics> {
267        let id = self.check_registration(&definition)?;
268        let device = self.gpu.device();
269        let shader_scope = CreationErrorScopes::push(device);
270        let shader = effect_shader(device, &definition);
271        let compilation: Answer<wgpu::CompilationInfo> = Box::pin(shader.get_compilation_info());
272        let shader_errors = shader_scope.pop();
273        let format = self.intermediate_format();
274        let (pipeline, pipeline_errors) = on_compile_stack(&self.gpu, || {
275            let pipeline_scope = CreationErrorScopes::push(device);
276            let pipeline = effect_pipeline(device, &definition, shader, format);
277            (pipeline, pipeline_scope.pop())
278        });
279        Ok(PendingEffect {
280            compiled: CompiledEffect {
281                device: device.clone(),
282                id,
283                effect: RegisteredEffect {
284                    pipeline: std::sync::OnceLock::from(pipeline),
285                    ..registered_effect(definition)
286                },
287            },
288            compilation,
289            shader_errors,
290            pipeline_errors,
291        })
292    }
293
294    /// Validates and registers a built-in without touching the device; its pipeline is made on
295    /// first draw or [`Self::warmup`]. Compiling every built-in up front costs seconds on a
296    /// software adapter.
297    ///
298    /// # Errors
299    /// Schema diagnostics, a duplicate id or a full registry.
300    pub(crate) fn register_lazily(
301        &mut self,
302        definition: EffectDef,
303    ) -> Result<EffectId, ShaderDiagnostics> {
304        let id = self.check_registration(&definition)?;
305        self.effects
306            .entries
307            .insert(id.clone(), registered_effect(definition));
308        Ok(id)
309    }
310
311    /// Pre-device registration checks; returns the identity to register under.
312    fn check_registration(&self, definition: &EffectDef) -> Result<EffectId, ShaderDiagnostics> {
313        validate_definition(definition)?;
314        self.validate_parameter_uniform(definition)?;
315        self.validate_texture_bindings(definition)?;
316        if definition.tier_min == CapabilityTier::TierA
317            && self.gpu.capabilities().tier == CapabilityTier::TierB
318        {
319            return Err(ShaderDiagnostics::error(
320                "effect requires capability tier A on a tier B adapter",
321            ));
322        }
323        let id = EffectId::new(definition.id.clone(), definition.version);
324        self.effects.check_room(&id)?;
325        Ok(id)
326    }
327
328    /// The effect's pipeline, created now if needed.
329    pub(crate) fn effect_pipeline<'a>(&self, effect: &'a RegisteredEffect) -> &'a EffectPipeline {
330        effect.pipeline.get_or_init(|| {
331            crate::diagnostics::timed_pipeline_creation(
332                &self.diagnostics,
333                || effect.definition.id.as_str().to_owned(),
334                || {
335                    let device = self.gpu.device();
336                    let shader = effect_shader(device, &effect.definition);
337                    let format = self.intermediate_format();
338                    on_compile_stack(&self.gpu, || {
339                        effect_pipeline(device, &effect.definition, shader, format)
340                    })
341                },
342            )
343        })
344    }
345
346    /// Adds an effect this renderer compiled.
347    ///
348    /// # Errors
349    /// Refuses an effect from another device, one registered meanwhile, or a full registry.
350    pub fn register_compiled(
351        &mut self,
352        compiled: CompiledEffect,
353    ) -> Result<EffectId, ShaderDiagnostics> {
354        if compiled.device != *self.gpu.device() {
355            return Err(ShaderDiagnostics::error(format!(
356                "effect {} version {} was compiled on another GPU device",
357                compiled.id.as_str(),
358                compiled.id.version()
359            )));
360        }
361        self.effects.check_room(&compiled.id)?;
362        self.effects
363            .entries
364            .insert(compiled.id.clone(), compiled.effect);
365        Ok(compiled.id)
366    }
367
368    /// Refuses parameter uniforms some alpha2 runtime cannot bind.
369    ///
370    /// Each parameter is one 16-byte slot. The cap is the WebGL2 limit every alpha2 device
371    /// requests, so an effect that registers anywhere registers everywhere. Past it the pipeline
372    /// still compiles but every frame's bind group fails.
373    fn validate_parameter_uniform(&self, definition: &EffectDef) -> Result<(), ShaderDiagnostics> {
374        let limit = self
375            .limits
376            .max_uniform_buffer_binding_size
377            .min(wgpu::Limits::downlevel_webgl2_defaults().max_uniform_buffer_binding_size);
378        let bytes = u64::try_from(param_uniform_bytes(definition.params.len())).unwrap_or(u64::MAX);
379        if bytes > limit {
380            let most = limit / u64::try_from(PARAM_SLOT_BYTES).unwrap_or(u64::MAX);
381            return Err(ShaderDiagnostics::error(format!(
382                "effect declares {} parameters, a {bytes}-byte uniform past the {limit}-byte \
383                 limit every alpha2 runtime binds (at most {most} parameters)",
384                definition.params.len()
385            )));
386        }
387        Ok(())
388    }
389
390    /// Refuses definitions needing more sampled textures per stage than the WebGL2 limit (16) every
391    /// alpha2 device requests: source pair, each resource and each named input take one.
392    fn validate_texture_bindings(&self, definition: &EffectDef) -> Result<(), ShaderDiagnostics> {
393        let textures = 2 + definition.resources.len() + definition.inputs.len();
394        let limit =
395            usize::try_from(self.limits.max_sampled_textures_per_shader_stage.min(
396                wgpu::Limits::downlevel_webgl2_defaults().max_sampled_textures_per_shader_stage,
397            ))
398            .unwrap_or(usize::MAX);
399        if textures > limit {
400            return Err(ShaderDiagnostics::error(format!(
401                "effect samples {textures} textures (its source pair, {} resources, and {} \
402                 named inputs), past this device's {limit} per shader stage",
403                definition.resources.len(),
404                definition.inputs.len()
405            )));
406        }
407        Ok(())
408    }
409
410    /// The generated host uniform layout of a registered effect.
411    #[must_use]
412    pub fn effect_uniform_layout(&self, id: &EffectId) -> Option<&UniformLayout> {
413        self.effects
414            .entries
415            .get(id)
416            .map(|entry| &entry.uniform_layout)
417    }
418
419    /// The definition of a registered effect.
420    #[must_use]
421    pub fn effect_definition(&self, id: &EffectId) -> Option<&EffectDef> {
422        self.effects.entries.get(id).map(|entry| &entry.definition)
423    }
424
425    /// Whether `id`'s output can change with frame time alone; `true` for unknown effects.
426    ///
427    /// Derived from the WGSL of every pass: a body that never names `time`, `time_ticks` or
428    /// `runtime` cannot read it. Callers may pass any time to an effect answering `false`, so its
429    /// result can be reused across frames.
430    #[must_use]
431    pub fn effect_reads_time(&self, id: &EffectId) -> bool {
432        if let Some(passes) = crate::gaussian_blur::expanded_passes(id.as_str()) {
433            return passes
434                .iter()
435                .any(|pass| self.effect_reads_time(&EffectId::from_static(pass, 1)));
436        }
437        self.effects
438            .entries
439            .get(id)
440            .is_none_or(|entry| entry.reads_time)
441    }
442
443    /// Whether an exact effect version is registered.
444    #[must_use]
445    pub fn is_effect_registered(&self, id: &EffectId) -> bool {
446        self.effects.entries.contains_key(id)
447    }
448
449    /// Whether a project document may reference the effect.
450    #[must_use]
451    pub fn is_effect_project_visible(&self, id: &EffectId) -> bool {
452        self.effects.entries.contains_key(id) && !self.effects.internal.contains(id)
453    }
454
455    /// The registered identity a project reference to `id`@`version` draws with, if the project
456    /// may reference it. Allocation-free.
457    #[must_use]
458    pub fn project_effect_id(&self, id: &str, version: u32) -> Option<&EffectId> {
459        let key: &dyn EffectKey = &(id, version);
460        self.effects
461            .entries
462            .get_key_value(key)
463            .map(|(id, _)| id)
464            .filter(|id| !self.effects.internal.contains(*id))
465    }
466
467    /// Whether `id` is an engine built-in rather than a host-registered runtime effect.
468    #[must_use]
469    pub fn is_builtin_effect(&self, id: &EffectId) -> bool {
470        self.effects.builtins.contains(id)
471    }
472
473    /// Removes one runtime effect and releases its GPU resources.
474    pub fn unregister_effect(&mut self, id: &EffectId) -> bool {
475        if self.effects.builtins.contains(id) {
476            return false;
477        }
478        let removed = self.effects.entries.remove(id).is_some();
479        if removed {
480            self.clear_result_cache_for(crate::CacheInvalidation::Effects);
481        }
482        removed
483    }
484
485    /// Removes every host-registered runtime effect.
486    pub fn clear_effects(&mut self) {
487        self.effects
488            .entries
489            .retain(|id, _| self.effects.builtins.contains(id));
490        self.clear_result_cache_for(crate::CacheInvalidation::Effects);
491    }
492
493    /// Confirms the effects are registered and creates pending built-in pipelines (including their
494    /// passes) so the first frame does not wait on the compiler.
495    ///
496    /// # Errors
497    /// Names 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`, pipeline not yet created.
539pub(crate) fn registered_effect(definition: EffectDef) -> RegisteredEffect {
540    RegisteredEffect {
541        uniform_layout: uniform_layout(&definition.params),
542        reads_time: body_reads_time(&definition.wgsl_body),
543        cost: crate::effect_cost::invocation_cost(&blank_comments(&definition.wgsl_body)),
544        definition,
545        pipeline: std::sync::OnceLock::new(),
546    }
547}
548
549fn effect_shader(device: &wgpu::Device, definition: &EffectDef) -> wgpu::ShaderModule {
550    device.create_shader_module(wgpu::ShaderModuleDescriptor {
551        label: Some(&definition.id),
552        source: wgpu::ShaderSource::Wgsl(build_shader(definition).into()),
553    })
554}
555
556fn effect_pipeline(
557    device: &wgpu::Device,
558    definition: &EffectDef,
559    shader: wgpu::ShaderModule,
560    format: wgpu::TextureFormat,
561) -> EffectPipeline {
562    let bind_group_layout = runtime_bind_group_layout(
563        device,
564        &definition.params,
565        &definition.resources,
566        &definition.inputs,
567    );
568    let pipeline = runtime_pipeline(device, &bind_group_layout, &shader, format);
569    let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
570        label: Some("alpha2 runtime effect sampler"),
571        address_mode_u: wgpu::AddressMode::ClampToEdge,
572        address_mode_v: wgpu::AddressMode::ClampToEdge,
573        mag_filter: wgpu::FilterMode::Linear,
574        min_filter: wgpu::FilterMode::Linear,
575        ..Default::default()
576    });
577    EffectPipeline {
578        _shader: shader,
579        bind_group_layout,
580        pipeline,
581        sampler,
582    }
583}
584
585fn runtime_bind_group_layout(
586    device: &wgpu::Device,
587    params: &[ParamSpec],
588    resources: &[ResourceSpec],
589    inputs: &[InputSpec],
590) -> wgpu::BindGroupLayout {
591    let mut entries = vec![
592        runtime_texture_entry(0, wgpu::TextureViewDimension::D2),
593        runtime_texture_entry(1, wgpu::TextureViewDimension::D2),
594        wgpu::BindGroupLayoutEntry {
595            binding: 2,
596            visibility: wgpu::ShaderStages::FRAGMENT,
597            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
598            count: None,
599        },
600        // Bind at exact struct sizes so a shader reading past them fails to compile instead of
601        // reading the next arena slot.
602        runtime_uniform_entry(3, param_uniform_bytes(params.len())),
603        runtime_uniform_entry(4, RUNTIME_UNIFORM_WORDS * size_of::<f32>()),
604    ];
605    for (index, resource) in resources.iter().enumerate() {
606        let binding = u32::try_from(index).unwrap_or(0) * 2 + 5;
607        let dimension = match resource.kind {
608            EffectResourceKind::Lut3d => wgpu::TextureViewDimension::D3,
609        };
610        entries.push(runtime_texture_entry(binding, dimension));
611        entries.push(runtime_uniform_entry(binding + 1, RESOURCE_METADATA_BYTES));
612    }
613    for index in 0..inputs.len() {
614        entries.push(runtime_texture_entry(
615            input_binding(index),
616            wgpu::TextureViewDimension::D2,
617        ));
618    }
619    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
620        label: Some("alpha2 runtime effect layout"),
621        entries: &entries,
622    })
623}
624
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.
711///
712/// `time()` and `time_ticks()` read the `runtime` uniform, and WGSL can reach none of them
713/// without naming it, so a body lacking all three as words cannot. Anything else counts as
714/// reading it (comments and shadowing included): a false `true` only redraws. A word is a run
715/// of letters, digits and underscores.
716pub(crate) fn body_reads_time(body: &str) -> bool {
717    body.split(|character: char| !(character.is_alphanumeric() || character == '_'))
718        .any(|word| matches!(word, "time" | "time_ticks" | "runtime"))
719}
720
721pub(crate) fn build_shader(definition: &EffectDef) -> String {
722    let param_fields = if definition.params.is_empty() {
723        "    _padding: vec4f,\n".to_owned()
724    } else {
725        definition
726            .params
727            .iter()
728            .fold(String::new(), |mut output, param| {
729                writeln!(
730                    output,
731                    "    {}: {},",
732                    param.name,
733                    wgsl_type(param.param_type)
734                )
735                .expect("writing to a String cannot fail");
736                output
737            })
738    };
739    let uniform_fields =
740        (0..definition.params.len().max(1)).fold(String::new(), |mut output, index| {
741            writeln!(output, "    slot_{index}: vec4u,").expect("writing to a String cannot fail");
742            output
743        });
744    let param_values = if definition.params.is_empty() {
745        "vec4f(0.0)".to_owned()
746    } else {
747        definition
748            .params
749            .iter()
750            .enumerate()
751            .map(|(index, param)| uniform_value(index, param.param_type))
752            .collect::<Vec<_>>()
753            .join(", ")
754    };
755    let call = match definition.kind {
756        EffectKind::Effect => "effect(input.uv, sample_src(input.uv))",
757        EffectKind::Transition => "alpha2_transition(input.uv)",
758        EffectKind::Generator => "generate(input.uv)",
759    };
760    let transition_wrapper = match definition.kind {
761        EffectKind::Effect | EffectKind::Generator => "",
762        EffectKind::Transition => {
763            r"fn alpha2_transition(uv: vec2f) -> vec4f {
764    if (progress() <= 0.0) { return from_color(uv); }
765    if (progress() >= 1.0) { return to_color(uv); }
766    return transition(uv);
767}"
768        }
769    };
770    let resource_declarations = resource_declarations(&definition.resources);
771    let input_declarations = input_declarations(&definition.inputs);
772    format!(
773        r"@group(0) @binding(0) var source_a: texture_2d<f32>;
774@group(0) @binding(1) var source_b: texture_2d<f32>;
775@group(0) @binding(2) var source_sampler: sampler;
776struct EffectParams {{
777{param_fields}}};
778struct EffectParamUniforms {{
779{uniform_fields}}};
780@group(0) @binding(3) var<uniform> param_uniforms: EffectParamUniforms;
781var<private> params: EffectParams;
782var<private> {BUDGET}: u32;
783struct RuntimeValues {{ resolution_time: vec4f, transition: vec4f, ticks: vec4u, }};
784@group(0) @binding(4) var<uniform> runtime: RuntimeValues;
785{resource_declarations}{input_declarations}
786
787fn sample_src(uv: vec2f) -> vec4f {{ return textureSampleLevel(source_a, source_sampler, uv, 0.0); }}
788fn from_color(uv: vec2f) -> vec4f {{ return textureSampleLevel(source_a, source_sampler, uv, 0.0); }}
789fn to_color(uv: vec2f) -> vec4f {{ return textureSampleLevel(source_b, source_sampler, uv, 0.0); }}
790fn resolution() -> vec2f {{ return runtime.resolution_time.xy; }}
791fn time() -> f32 {{ return runtime.resolution_time.z; }}
792fn time_ticks() -> vec2u {{ return runtime.ticks.xy; }}
793fn progress() -> f32 {{ return runtime.transition.x; }}
794fn ratio() -> f32 {{ return runtime.transition.y; }}
795fn max_filter_samples() -> f32 {{ return runtime.transition.z; }}
796fn load_params() -> EffectParams {{ return EffectParams({param_values}); }}
797
798{body}
799
800{transition_wrapper}
801
802struct VertexOutput {{ @builtin(position) position: vec4f, @location(0) uv: vec2f, }};
803@vertex fn alpha2_vs(@builtin(vertex_index) index: u32) -> VertexOutput {{
804    var p = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
805    var u = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));
806    return VertexOutput(vec4f(p[index], 0.0, 1.0), u[index]);
807}}
808@fragment fn alpha2_fs(input: VertexOutput) -> @location(0) vec4f {{
809    params = load_params();
810    {BUDGET} = {MAX_LOOP_ITERATIONS}u;
811    return {call};
812}}
813",
814        // Registration already delimited loops; this covers definitions that skipped it.
815        body =
816            bound_for_loops(&definition.wgsl_body).unwrap_or_else(|_| definition.wgsl_body.clone()),
817        resource_declarations = resource_declarations,
818        input_declarations = input_declarations,
819        transition_wrapper = transition_wrapper,
820    )
821}
822
823fn resource_declarations(resources: &[ResourceSpec]) -> String {
824    resources
825        .iter()
826        .enumerate()
827        .map(|(index, resource)| match resource.kind {
828            EffectResourceKind::Lut3d => format!(
829                "struct ResourceMetadata_{} {{ domain_min: vec4f, domain_max: vec4f, }};\n\
830                 @group(0) @binding({}) var resource_{}: texture_3d<f32>;\n\
831                 @group(0) @binding({}) var<uniform> resource_{}_metadata: ResourceMetadata_{};\n\
832                 fn sample_{}(rgb: vec3f) -> vec3f {{\n\
833                     let edge = f32(textureDimensions(resource_{}).x);\n\
834                     let normalized = clamp((rgb - resource_{}_metadata.domain_min.xyz) /\n\
835                         (resource_{}_metadata.domain_max.xyz - resource_{}_metadata.domain_min.xyz),\n\
836                         vec3f(0.0), vec3f(1.0));\n\
837                     let coord = (normalized * (edge - 1.0) + 0.5) / edge;\n\
838                     return textureSampleLevel(resource_{}, source_sampler, coord, 0.0).rgb;\n\
839                 }}",
840                resource.name,
841                index * 2 + 5,
842                resource.name,
843                index * 2 + 6,
844                resource.name,
845                resource.name,
846                resource.name,
847                resource.name,
848                resource.name,
849                resource.name,
850                resource.name,
851                resource.name,
852            ),
853        })
854        .collect::<Vec<_>>()
855        .join("\n")
856}
857
858/// Per-input wrapper declarations: texture, sampling helper and size. Empty without inputs.
859fn input_declarations(inputs: &[InputSpec]) -> String {
860    inputs
861        .iter()
862        .enumerate()
863        .fold(String::new(), |mut output, (index, input)| {
864            let name = &input.name;
865            write!(
866                output,
867                "\n@group(0) @binding({binding}) var input_{name}: texture_2d<f32>;\n\
868                 fn sample_{name}(uv: vec2f) -> vec4f {{ return textureSampleLevel(input_{name}, source_sampler, uv, 0.0); }}\n\
869                 fn size_{name}() -> vec2f {{ return vec2f(textureDimensions(input_{name})); }}",
870                binding = input_binding(index),
871            )
872            .expect("writing to a String cannot fail");
873            output
874        })
875}
876
877fn uniform_value(index: usize, param_type: ParamType) -> String {
878    match param_type {
879        ParamType::Float => format!("bitcast<f32>(param_uniforms.slot_{index}.x)"),
880        ParamType::Vec2 => format!("bitcast<vec2f>(param_uniforms.slot_{index}.xy)"),
881        ParamType::Vec4 | ParamType::Color => {
882            format!("bitcast<vec4f>(param_uniforms.slot_{index})")
883        }
884        ParamType::Enum | ParamType::Bool => format!("param_uniforms.slot_{index}.x"),
885    }
886}
887
888fn wgsl_type(param_type: ParamType) -> &'static str {
889    match param_type {
890        ParamType::Float => "f32",
891        ParamType::Vec2 => "vec2f",
892        ParamType::Vec4 | ParamType::Color => "vec4f",
893        ParamType::Enum | ParamType::Bool => "u32",
894    }
895}
896
897fn validate_definition(definition: &EffectDef) -> Result<(), ShaderDiagnostics> {
898    if !valid_identifier(&definition.id) {
899        return Err(ShaderDiagnostics::error(
900            "effect id must contain only ASCII letters, digits, '-', '_', or '.'",
901        ));
902    }
903    if definition.version == 0 {
904        return Err(ShaderDiagnostics::error("effect version must be non-zero"));
905    }
906    validate_wgsl_body(&definition.wgsl_body)?;
907    if !definition.inputs.is_empty() && definition.kind != EffectKind::Effect {
908        return Err(ShaderDiagnostics::error(
909            "only effects declare named inputs; a transition samples from and to",
910        ));
911    }
912    if definition.inputs.len() > MAX_EFFECT_INPUTS {
913        return Err(ShaderDiagnostics::error(format!(
914            "effect declares more than {MAX_EFFECT_INPUTS} named inputs"
915        )));
916    }
917    let mut names = std::collections::HashSet::new();
918    for param in &definition.params {
919        validate_param(param)?;
920        if !names.insert(param.name.as_str()) {
921            return Err(ShaderDiagnostics::error(format!(
922                "duplicate parameter name {}",
923                param.name
924            )));
925        }
926    }
927    if definition.resources.len() > MAX_EFFECT_RESOURCES {
928        return Err(ShaderDiagnostics::error(format!(
929            "effect declares more than {MAX_EFFECT_RESOURCES} resources"
930        )));
931    }
932    let mut resource_names = std::collections::HashSet::new();
933    // Resource declarations share module scope with the wrapper's names and each other.
934    let mut declared = std::collections::HashSet::new();
935    for resource in &definition.resources {
936        let generated = resource_declarations_of(&resource.name);
937        if !valid_wgsl_identifier(&resource.name)
938            || names.contains(resource.name.as_str())
939            || !resource_names.insert(resource.name.as_str())
940            || !generated
941                .iter()
942                .all(|name| valid_wgsl_identifier(name) && declared.insert(name.clone()))
943        {
944            return Err(ShaderDiagnostics::error(format!(
945                "invalid or duplicate effect resource name {}",
946                resource.name
947            )));
948        }
949    }
950    // An input's names share the module scope with the resources'.
951    let mut input_names = std::collections::HashSet::new();
952    for input in &definition.inputs {
953        let generated = input_declarations_of(&input.name);
954        if !valid_wgsl_identifier(&input.name)
955            || names.contains(input.name.as_str())
956            || resource_names.contains(input.name.as_str())
957            || !input_names.insert(input.name.as_str())
958            || !generated
959                .iter()
960                .all(|name| valid_wgsl_identifier(name) && declared.insert(name.clone()))
961        {
962            return Err(ShaderDiagnostics::error(format!(
963                "invalid or duplicate effect input name {}",
964                input.name
965            )));
966        }
967    }
968    Ok(())
969}
970
971/// Module-scope names [`build_shader`] declares for input `name`. This is also what refuses
972/// `src`, since `sample_src` is the prelude's.
973fn input_declarations_of(name: &str) -> [String; 3] {
974    [
975        format!("input_{name}"),
976        format!("sample_{name}"),
977        format!("size_{name}"),
978    ]
979}
980
981/// Module-scope names [`build_shader`] declares for resource `name`.
982fn resource_declarations_of(name: &str) -> [String; 4] {
983    [
984        format!("resource_{name}"),
985        format!("resource_{name}_metadata"),
986        format!("sample_{name}"),
987        format!("ResourceMetadata_{name}"),
988    ]
989}
990
991fn validate_wgsl_body(body: &str) -> Result<(), ShaderDiagnostics> {
992    if body.len() > MAX_WGSL_BODY_BYTES {
993        return Err(ShaderDiagnostics::error(format!(
994            "WGSL body exceeds {MAX_WGSL_BODY_BYTES} bytes"
995        )));
996    }
997    if body
998        .chars()
999        .any(|character| character.is_control() && !matches!(character, '\n' | '\r' | '\t'))
1000    {
1001        return Err(ShaderDiagnostics::error(
1002            "WGSL body contains unsupported control characters",
1003        ));
1004    }
1005    // Only code is checked; comments may contain anything.
1006    let code = blank_comments(body);
1007    if let Some(identifier) = identifiers(&code).find(|word| word.starts_with(RESERVED_PREFIX)) {
1008        return Err(ShaderDiagnostics::error(format!(
1009            "WGSL identifier {identifier} is reserved: names starting with {RESERVED_PREFIX} belong to the generated wrapper"
1010        )));
1011    }
1012    if code.split([';', '{', '}']).any(|statement| {
1013        statement.bytes().filter(|byte| is_operator(*byte)).count() > MAX_WGSL_STATEMENT_OPERATORS
1014    }) {
1015        return Err(ShaderDiagnostics::error(format!(
1016            "a WGSL statement has more than {MAX_WGSL_STATEMENT_OPERATORS} operators; split it with `let`"
1017        )));
1018    }
1019    let mut delimiters = Vec::new();
1020    for character in code.chars() {
1021        match character {
1022            '{' | '(' | '[' => {
1023                delimiters.push(character);
1024                if delimiters.len() > MAX_WGSL_NESTING {
1025                    return Err(ShaderDiagnostics::error(format!(
1026                        "WGSL nesting exceeds {MAX_WGSL_NESTING} levels"
1027                    )));
1028                }
1029            }
1030            '}' | ')' | ']' => {
1031                let matches = matches!(
1032                    (delimiters.pop(), character),
1033                    (Some('{'), '}') | (Some('('), ')') | (Some('['), ']')
1034                );
1035                if !matches {
1036                    return Err(ShaderDiagnostics::error(
1037                        "WGSL body has unbalanced delimiters",
1038                    ));
1039                }
1040            }
1041            _ => {}
1042        }
1043    }
1044    if !delimiters.is_empty() {
1045        return Err(ShaderDiagnostics::error(
1046            "WGSL body has unbalanced delimiters",
1047        ));
1048    }
1049
1050    let forbidden = [
1051        "@binding",
1052        "@compute",
1053        "@group",
1054        "atomic",
1055        "enable",
1056        "loop",
1057        "override",
1058        "var<storage",
1059        "var<uniform",
1060        "var<workgroup",
1061        "while",
1062    ];
1063    for token in code.split(|character: char| {
1064        !(character.is_ascii_alphanumeric() || matches!(character, '_' | '@' | '<'))
1065    }) {
1066        if forbidden.contains(&token) {
1067            return Err(ShaderDiagnostics::error(format!(
1068                "WGSL token {token} is not allowed in a runtime effect"
1069            )));
1070        }
1071    }
1072    bound_for_loops(body)?;
1073    crate::call_budget::check_invocation_calls(&code)?;
1074    crate::effect_cost::check_invocation_cost(&code).map(drop)
1075}
1076
1077/// Identifiers in `code`; words starting with a letter or `_`, so number suffixes are excluded.
1078fn identifiers(code: &str) -> impl Iterator<Item = &str> {
1079    code.split(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
1080        .filter(|word| word.starts_with(|first: char| first.is_ascii_alphabetic() || first == '_'))
1081}
1082
1083/// Characters that can deepen an expression. Overcounts (`1.0`, `vec4<f32>`), which only
1084/// lowers the effective limit.
1085const fn is_operator(byte: u8) -> bool {
1086    matches!(
1087        byte,
1088        b'+' | b'-'
1089            | b'*'
1090            | b'/'
1091            | b'%'
1092            | b'&'
1093            | b'|'
1094            | b'^'
1095            | b'!'
1096            | b'~'
1097            | b'<'
1098            | b'>'
1099            | b'='
1100            | b'.'
1101            | b'['
1102    )
1103}
1104
1105fn validate_param(param: &ParamSpec) -> Result<(), ShaderDiagnostics> {
1106    if !valid_wgsl_identifier(&param.name) {
1107        return Err(ShaderDiagnostics::error(format!(
1108            "invalid WGSL parameter name {}",
1109            param.name
1110        )));
1111    }
1112    let compatible = matches!(
1113        (&param.param_type, &param.default, &param.unit),
1114        (
1115            ParamType::Float,
1116            ParamValue::Float(_),
1117            ParamUnit::Normalized | ParamUnit::Degrees
1118        ) | (ParamType::Vec2, ParamValue::Vec2(_), ParamUnit::Normalized)
1119            | (ParamType::Vec4, ParamValue::Vec4(_), ParamUnit::Normalized)
1120            | (ParamType::Color, ParamValue::Color(_), ParamUnit::Color)
1121            | (ParamType::Enum, ParamValue::Enum(_), ParamUnit::Enum)
1122            | (ParamType::Bool, ParamValue::Bool(_), ParamUnit::Bool)
1123    );
1124    if !compatible {
1125        return Err(ShaderDiagnostics::error(format!(
1126            "parameter {} has incompatible type, default, and unit",
1127            param.name
1128        )));
1129    }
1130    if !finite_value(&param.default)
1131        || param.min.is_some_and(|value| !value.is_finite())
1132        || param.max.is_some_and(|value| !value.is_finite())
1133        || param
1134            .step
1135            .is_some_and(|value| !value.is_finite() || value <= 0.0)
1136        || matches!((param.min, param.max), (Some(min), Some(max)) if min > max)
1137    {
1138        return Err(ShaderDiagnostics::error(format!(
1139            "parameter {} has an invalid numeric range",
1140            param.name
1141        )));
1142    }
1143    if !matches!(
1144        param.param_type,
1145        ParamType::Float | ParamType::Vec2 | ParamType::Vec4
1146    ) && (param.min.is_some() || param.max.is_some() || param.step.is_some())
1147    {
1148        return Err(ShaderDiagnostics::error(format!(
1149            "only float, vec2, or vec4 parameter {} may declare a numeric range",
1150            param.name
1151        )));
1152    }
1153    if matches!(
1154        param.param_type,
1155        ParamType::Float | ParamType::Vec2 | ParamType::Vec4
1156    ) && !value_in_range(&param.default, param.min, param.max)
1157    {
1158        return Err(ShaderDiagnostics::error(format!(
1159            "parameter {} default is outside its numeric range",
1160            param.name
1161        )));
1162    }
1163    if let ParamValue::Color(values) = &param.default
1164        && !values.iter().all(|value| (0.0..=1.0).contains(value))
1165    {
1166        return Err(ShaderDiagnostics::error(format!(
1167            "color parameter {} default must be inside 0..=1",
1168            param.name
1169        )));
1170    }
1171    if param.param_type == ParamType::Enum && param.enum_values.is_empty() {
1172        return Err(ShaderDiagnostics::error(format!(
1173            "enum parameter {} requires named values",
1174            param.name
1175        )));
1176    }
1177    if let ParamValue::Enum(value) = &param.default
1178        && usize::try_from(*value).map_or(true, |index| index >= param.enum_values.len())
1179    {
1180        return Err(ShaderDiagnostics::error(format!(
1181            "enum parameter {} default is out of range",
1182            param.name
1183        )));
1184    }
1185    Ok(())
1186}
1187
1188fn value_in_range(value: &ParamValue, min: Option<f32>, max: Option<f32>) -> bool {
1189    let in_range = |value: &f32| {
1190        min.is_none_or(|minimum| *value >= minimum) && max.is_none_or(|maximum| *value <= maximum)
1191    };
1192    match value {
1193        ParamValue::Float(value) => in_range(value),
1194        ParamValue::Vec2(values) => values.iter().all(in_range),
1195        ParamValue::Vec4(values) => values.iter().all(in_range),
1196        _ => true,
1197    }
1198}
1199
1200fn finite_value(value: &ParamValue) -> bool {
1201    match value {
1202        ParamValue::Float(value) => value.is_finite(),
1203        ParamValue::Vec2(values) => values.iter().all(|value| value.is_finite()),
1204        ParamValue::Vec4(values) | ParamValue::Color(values) => {
1205            values.iter().all(|value| value.is_finite())
1206        }
1207        ParamValue::Enum(_) | ParamValue::Bool(_) => true,
1208    }
1209}
1210
1211fn valid_identifier(value: &str) -> bool {
1212    !value.is_empty()
1213        && value
1214            .bytes()
1215            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
1216}
1217
1218fn valid_wgsl_identifier(value: &str) -> bool {
1219    let mut bytes = value.bytes();
1220    bytes
1221        .next()
1222        .is_some_and(|byte| byte.is_ascii_alphabetic() || byte == b'_')
1223        && bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
1224        && !value.starts_with(RESERVED_PREFIX)
1225        && !matches!(
1226            value,
1227            "params"
1228                | "resolution"
1229                | "time"
1230                | "time_ticks"
1231                | "progress"
1232                | "ratio"
1233                | "sample_src"
1234                | "from_color"
1235                | "to_color"
1236                | "load_params"
1237                | "max_filter_samples"
1238                | "source_a"
1239                | "source_b"
1240                | "source_sampler"
1241                | "param_uniforms"
1242                | "runtime"
1243                | "EffectParams"
1244        )
1245}
1246
1247fn compilation_diagnostics(info: wgpu::CompilationInfo) -> ShaderDiagnostics {
1248    ShaderDiagnostics {
1249        messages: info
1250            .messages
1251            .into_iter()
1252            .map(|message| {
1253                let severity = match message.message_type {
1254                    wgpu::CompilationMessageType::Error => DiagnosticSeverity::Error,
1255                    wgpu::CompilationMessageType::Warning => DiagnosticSeverity::Warning,
1256                    wgpu::CompilationMessageType::Info => DiagnosticSeverity::Info,
1257                };
1258                ShaderDiagnostic {
1259                    severity,
1260                    message: message.message,
1261                    line: message.location.map(|location| location.line_number),
1262                    column: message.location.map(|location| location.line_position),
1263                }
1264            })
1265            .collect(),
1266    }
1267}
1268
1269#[cfg(test)]
1270#[path = "registry_tests.rs"]
1271mod tests;