Skip to main content

lumen_engine/gpu/
compiler.rs

1use std::collections::HashMap;
2
3use bytemuck::{Pod, Zeroable};
4
5use crate::{
6    composition::Composition,
7    error::RenderError,
8    expr::ExpressionContext,
9    gpu::{
10        BoundFrame, CompiledComposition, CompiledOutput, FrameBinding, RasterHandle, RasterMetadata,
11    },
12    media::MediaStore,
13    node::{NodeId, NodeKind, NodeProperty, PortRef},
14};
15
16pub trait GpuCompileNode {
17    fn compile_gpu(
18        &self,
19        ctx: &mut CompileContext<'_>,
20        port: &PortRef,
21    ) -> crate::Result<CompiledOutput>;
22}
23
24pub trait GpuFrameBindNode {
25    fn bind_gpu_frame(
26        &self,
27        ctx: &FrameBindContext<'_>,
28        binding: &FrameBinding,
29        bound: &mut BoundFrame,
30    ) -> crate::Result<()>;
31}
32
33#[derive(Debug)]
34struct CompiledPortKey {
35    port: PortRef,
36    frame: u32,
37}
38
39impl PartialEq for CompiledPortKey {
40    fn eq(&self, other: &Self) -> bool {
41        self.frame == other.frame && self.port == other.port
42    }
43}
44
45impl Eq for CompiledPortKey {}
46
47impl std::hash::Hash for CompiledPortKey {
48    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
49        self.port.hash(state);
50        self.frame.hash(state);
51    }
52}
53
54#[derive(Debug)]
55pub struct CompileContext<'a> {
56    composition: &'a Composition,
57    frame: u32,
58    media: Option<&'a dyn MediaStore>,
59    builder: lumen_gpu::RenderPlanBuilder,
60    outputs: HashMap<CompiledPortKey, CompiledOutput>,
61    public_outputs: HashMap<PortRef, CompiledOutput>,
62    frame_bindings: Vec<FrameBinding>,
63    frame_binding_frames: Vec<Option<u32>>,
64    frame_binding_frame_override: Option<u32>,
65    output_format: lumen_gpu::wgpu::TextureFormat,
66}
67
68impl<'a> CompileContext<'a> {
69    pub fn new(composition: &'a Composition) -> Self {
70        Self::with_output_format(composition, lumen_gpu::wgpu::TextureFormat::Rgba8Unorm)
71    }
72
73    pub fn with_output_format(
74        composition: &'a Composition,
75        output_format: lumen_gpu::wgpu::TextureFormat,
76    ) -> Self {
77        Self::with_options(composition, 0, None, output_format)
78    }
79
80    pub fn with_frame(
81        composition: &'a Composition,
82        frame: u32,
83        output_format: lumen_gpu::wgpu::TextureFormat,
84    ) -> Self {
85        Self::with_options(composition, frame, None, output_format)
86    }
87
88    pub fn with_media<M: MediaStore>(
89        composition: &'a Composition,
90        media: &'a M,
91        output_format: lumen_gpu::wgpu::TextureFormat,
92    ) -> Self {
93        Self::with_media_for_frame(composition, 0, media, output_format)
94    }
95
96    pub fn with_media_for_frame<M: MediaStore>(
97        composition: &'a Composition,
98        frame: u32,
99        media: &'a M,
100        output_format: lumen_gpu::wgpu::TextureFormat,
101    ) -> Self {
102        Self::with_options(composition, frame, Some(media), output_format)
103    }
104
105    fn with_options(
106        composition: &'a Composition,
107        frame: u32,
108        media: Option<&'a dyn MediaStore>,
109        output_format: lumen_gpu::wgpu::TextureFormat,
110    ) -> Self {
111        Self {
112            composition,
113            frame,
114            media,
115            builder: lumen_gpu::RenderPlan::builder(),
116            outputs: HashMap::new(),
117            public_outputs: HashMap::new(),
118            frame_bindings: Vec::new(),
119            frame_binding_frames: Vec::new(),
120            frame_binding_frame_override: None,
121            output_format,
122        }
123    }
124
125    pub fn compile(mut self) -> crate::Result<CompiledComposition> {
126        let output_node = self.media_output_node()?;
127        let output_ref = PortRef::new(output_node, "output".to_string());
128        let output = self
129            .compile_port(&output_ref)?
130            .into_raster(output_node, "output")?;
131        Ok(CompiledComposition {
132            plan: self.builder.build(),
133            output,
134            node_outputs: self.public_outputs,
135            frame_bindings: self.frame_bindings,
136            frame_binding_frames: self.frame_binding_frames,
137        })
138    }
139
140    pub(crate) fn composition(&self) -> &Composition {
141        self.composition
142    }
143
144    pub(crate) fn media(&self) -> Option<&dyn MediaStore> {
145        self.media
146    }
147
148    pub(crate) fn output_format(&self) -> lumen_gpu::wgpu::TextureFormat {
149        self.output_format
150    }
151
152    pub(crate) fn builder_mut(&mut self) -> &mut lumen_gpu::RenderPlanBuilder {
153        &mut self.builder
154    }
155
156    pub(crate) fn push_frame_binding(&mut self, binding: FrameBinding) {
157        self.frame_bindings.push(binding);
158        self.frame_binding_frames
159            .push(self.frame_binding_frame_override);
160    }
161
162    pub(crate) fn compile_port(&mut self, port: &PortRef) -> crate::Result<CompiledOutput> {
163        let key = CompiledPortKey {
164            port: port.clone(),
165            frame: self.frame,
166        };
167        if let Some(output) = self.outputs.get(&key) {
168            return Ok(output.clone());
169        }
170
171        let node = self
172            .composition
173            .graph
174            .nodes
175            .get(&port.id)
176            .ok_or(RenderError::MissingNode {
177                frame: 0,
178                node_id: port.id,
179            })?;
180        let output = match node {
181            NodeKind::MediaIn(node) => node.compile_gpu(self, port)?,
182            NodeKind::SolidColor(node) => node.compile_gpu(self, port)?,
183            NodeKind::Text(node) => node.compile_gpu(self, port)?,
184            NodeKind::Path(node) => node.compile_gpu(self, port)?,
185            NodeKind::Shape(node) => node.compile_gpu(self, port)?,
186            NodeKind::Boolean(node) => node.compile_gpu(self, port)?,
187            NodeKind::Merge(node) => node.compile_gpu(self, port)?,
188            NodeKind::RasterMultiMerge(node) => node.compile_gpu(self, port)?,
189            NodeKind::AlphaPremultiply(node) => node.compile_gpu(self, port)?,
190            NodeKind::Blur(node) => node.compile_gpu(self, port)?,
191            NodeKind::ChannelShuffle(node) => node.compile_gpu(self, port)?,
192            NodeKind::ColorGrade(node) => node.compile_gpu(self, port)?,
193            NodeKind::Curves(node) => node.compile_gpu(self, port)?,
194            NodeKind::Exposure(node) => node.compile_gpu(self, port)?,
195            NodeKind::HueSaturation(node) => node.compile_gpu(self, port)?,
196            NodeKind::Levels(node) => node.compile_gpu(self, port)?,
197            NodeKind::Memo(node) => node.compile_gpu(self, port)?,
198            NodeKind::TimeRemap(node) => node.compile_gpu(self, port)?,
199            NodeKind::Transform(node) => node.compile_gpu(self, port)?,
200            NodeKind::Crop(node) => node.compile_gpu(self, port)?,
201            NodeKind::Resize(node) => node.compile_gpu(self, port)?,
202            NodeKind::Shadow(node) => node.compile_gpu(self, port)?,
203            NodeKind::WgslShader(node) => node.compile_gpu(self, port)?,
204            NodeKind::Switch(node) => node.compile_gpu(self, port)?,
205            NodeKind::MediaOutput(node) => node.compile_gpu(self, port)?,
206        };
207
208        self.outputs.insert(key, output.clone());
209        self.public_outputs
210            .entry(port.clone())
211            .or_insert_with(|| output.clone());
212        Ok(output)
213    }
214
215    pub(crate) fn with_frame_context<T>(
216        &mut self,
217        frame: u32,
218        f: impl FnOnce(&mut Self) -> crate::Result<T>,
219    ) -> crate::Result<T> {
220        let original_frame = self.frame;
221        let original_frame_override = self.frame_binding_frame_override;
222        self.frame = frame;
223        self.frame_binding_frame_override = Some(frame);
224        let result = f(self);
225        self.frame = original_frame;
226        self.frame_binding_frame_override = original_frame_override;
227        result
228    }
229
230    pub(crate) fn compile_unary_filter(
231        &mut self,
232        node_id: NodeId,
233        source_ref: &PortRef,
234        port: &PortRef,
235        label: &str,
236        shader: &str,
237        params_size: u64,
238    ) -> crate::Result<(RasterHandle, lumen_gpu::TextureId, lumen_gpu::BufferId)> {
239        if port.port != "output" {
240            return Err(self.missing_output(node_id, &port.port));
241        }
242
243        let source = self
244            .compile_port(source_ref)?
245            .into_raster(source_ref.id, &source_ref.port)?;
246        let size = source.domain.storage_size;
247        let texture = self.builder.texture_for(
248            lumen_gpu::NodeKey(node_id.0),
249            Some(format!("{label}:{}:output", node_id.0)),
250            lumen_gpu::TextureDesc::storage(size, lumen_gpu::wgpu::TextureFormat::Rgba8Unorm),
251        );
252        let params = self.builder.buffer_for(
253            lumen_gpu::NodeKey(node_id.0),
254            Some(format!("{label}:{}:params", node_id.0)),
255            lumen_gpu::BufferDesc::uniform(params_size),
256        );
257        let program = self.builder.program_for(
258            lumen_gpu::NodeKey(node_id.0),
259            lumen_gpu::ProgramDesc::Compute(lumen_gpu::ComputeProgramDesc {
260                label: Some(label.to_string()),
261                shader: shader.to_string(),
262                entry: "cs_main".to_string(),
263                bind_groups: lumen_gpu::BindGroupLayoutSpec::single(vec![
264                    lumen_gpu::BindingLayoutEntry::texture(
265                        0,
266                        lumen_gpu::wgpu::ShaderStages::COMPUTE,
267                    ),
268                    lumen_gpu::BindingLayoutEntry::uniform(
269                        1,
270                        lumen_gpu::wgpu::ShaderStages::COMPUTE,
271                    ),
272                    lumen_gpu::BindingLayoutEntry::storage_texture(
273                        2,
274                        lumen_gpu::wgpu::ShaderStages::COMPUTE,
275                        lumen_gpu::wgpu::TextureFormat::Rgba8Unorm,
276                        lumen_gpu::wgpu::StorageTextureAccess::WriteOnly,
277                    ),
278                ]),
279            }),
280        );
281        self.builder.compute_pass(lumen_gpu::ComputePassDesc {
282            label: Some(format!("{label}:{}:apply", node_id.0)),
283            owner: Some(lumen_gpu::NodeKey(node_id.0)),
284            program,
285            bindings: vec![
286                lumen_gpu::Binding::sampled_texture(0, 0, source.texture),
287                lumen_gpu::Binding::uniform(0, 1, params),
288                lumen_gpu::Binding::storage_texture(0, 2, texture),
289            ],
290            dispatch: dispatch_for(size).into(),
291        });
292        self.builder.param(
293            lumen_gpu::ParamKey {
294                owner: lumen_gpu::NodeKey(node_id.0),
295                slot: 0,
296            },
297            lumen_gpu::ParamTarget::Buffer(params),
298        );
299        Ok((source, texture, params))
300    }
301
302    pub(crate) fn compile_transparent(&mut self, node_id: NodeId) -> CompiledOutput {
303        let size = lumen_gpu::Size::new(
304            self.composition.render_settings.width.max(1),
305            self.composition.render_settings.height.max(1),
306        );
307        let texture = self.builder.texture_for(
308            lumen_gpu::NodeKey(node_id.0),
309            Some(format!("transparent:{}:output", node_id.0)),
310            lumen_gpu::TextureDesc::storage(size, lumen_gpu::wgpu::TextureFormat::Rgba8Unorm),
311        );
312        let params = self.builder.buffer_for(
313            lumen_gpu::NodeKey(node_id.0),
314            Some(format!("transparent:{}:params", node_id.0)),
315            lumen_gpu::BufferDesc::uniform(std::mem::size_of::<ColorParams>() as u64),
316        );
317        let program = self.builder.program_for(
318            lumen_gpu::NodeKey(node_id.0),
319            lumen_gpu::ProgramDesc::Compute(lumen_gpu::ComputeProgramDesc {
320                label: Some("transparent".to_string()),
321                shader: crate::node::source::solid_color::SHADER.to_string(),
322                entry: "cs_main".to_string(),
323                bind_groups: lumen_gpu::BindGroupLayoutSpec::single(vec![
324                    lumen_gpu::BindingLayoutEntry::uniform(
325                        0,
326                        lumen_gpu::wgpu::ShaderStages::COMPUTE,
327                    ),
328                    lumen_gpu::BindingLayoutEntry::storage_texture(
329                        1,
330                        lumen_gpu::wgpu::ShaderStages::COMPUTE,
331                        lumen_gpu::wgpu::TextureFormat::Rgba8Unorm,
332                        lumen_gpu::wgpu::StorageTextureAccess::WriteOnly,
333                    ),
334                ]),
335            }),
336        );
337        self.builder.compute_pass(lumen_gpu::ComputePassDesc {
338            label: Some(format!("transparent:{}:fill", node_id.0)),
339            owner: Some(lumen_gpu::NodeKey(node_id.0)),
340            program,
341            bindings: vec![
342                lumen_gpu::Binding::uniform(0, 0, params),
343                lumen_gpu::Binding::storage_texture(0, 1, texture),
344            ],
345            dispatch: dispatch_for(size).into(),
346        });
347        self.builder.param(
348            lumen_gpu::ParamKey {
349                owner: lumen_gpu::NodeKey(node_id.0),
350                slot: 0,
351            },
352            lumen_gpu::ParamTarget::Buffer(params),
353        );
354        self.push_frame_binding(FrameBinding::SolidColor {
355            node_id,
356            color: NodeProperty::Color([0, 0, 0, 0]),
357            buffer: params,
358        });
359
360        CompiledOutput::Raster(RasterHandle {
361            texture,
362            domain: lumen_gpu::TextureDomain::full_frame(size),
363            metadata: RasterMetadata::default(),
364        })
365    }
366
367    pub(crate) fn static_dimension(
368        &self,
369        property: &NodeProperty,
370        node_id: NodeId,
371        property_path: &str,
372    ) -> crate::Result<u32> {
373        let value = property.resolve_int(
374            node_id,
375            property_path,
376            &self.expr_context(node_id, property_path),
377        )?;
378        let value = if value <= 0 {
379            match property_path {
380                "width" => i64::from(self.composition.render_settings.width),
381                "height" => i64::from(self.composition.render_settings.height),
382                _ => value,
383            }
384        } else {
385            value
386        };
387        Ok(value.clamp(1, i64::from(u32::MAX)) as u32)
388    }
389
390    pub(crate) fn spatial_program(
391        &mut self,
392        node_id: NodeId,
393        label: &str,
394        shader: &str,
395    ) -> lumen_gpu::ProgramId {
396        self.builder.program_for(
397            lumen_gpu::NodeKey(node_id.0),
398            lumen_gpu::ProgramDesc::Compute(lumen_gpu::ComputeProgramDesc {
399                label: Some(label.to_string()),
400                shader: shader.to_string(),
401                entry: "cs_main".to_string(),
402                bind_groups: lumen_gpu::BindGroupLayoutSpec::single(vec![
403                    lumen_gpu::BindingLayoutEntry::texture(
404                        0,
405                        lumen_gpu::wgpu::ShaderStages::COMPUTE,
406                    ),
407                    lumen_gpu::BindingLayoutEntry::uniform(
408                        1,
409                        lumen_gpu::wgpu::ShaderStages::COMPUTE,
410                    ),
411                    lumen_gpu::BindingLayoutEntry::storage_texture(
412                        2,
413                        lumen_gpu::wgpu::ShaderStages::COMPUTE,
414                        lumen_gpu::wgpu::TextureFormat::Rgba8Unorm,
415                        lumen_gpu::wgpu::StorageTextureAccess::WriteOnly,
416                    ),
417                ]),
418            }),
419        )
420    }
421
422    #[allow(dead_code)]
423    fn compile_unary_compute(
424        &mut self,
425        node_id: NodeId,
426        port: &PortRef,
427        source_port: &PortRef,
428        label: &str,
429        shader: &str,
430        param_size: u64,
431    ) -> crate::Result<(RasterHandle, lumen_gpu::TextureId, lumen_gpu::BufferId)> {
432        if port.port != "output" {
433            return Err(self.missing_output(node_id, &port.port));
434        }
435
436        let source = self
437            .compile_port(source_port)?
438            .into_raster(source_port.id, &source_port.port)?;
439        let size = source.domain.storage_size;
440        let texture = self.builder.texture_for(
441            lumen_gpu::NodeKey(node_id.0),
442            Some(format!("{label}:{}:output", node_id.0)),
443            lumen_gpu::TextureDesc::storage(size, lumen_gpu::wgpu::TextureFormat::Rgba8Unorm),
444        );
445        let params = self.builder.buffer_for(
446            lumen_gpu::NodeKey(node_id.0),
447            Some(format!("{label}:{}:params", node_id.0)),
448            lumen_gpu::BufferDesc::uniform(param_size),
449        );
450        let program = self.spatial_program(node_id, label, shader);
451        self.builder.compute_pass(lumen_gpu::ComputePassDesc {
452            label: Some(format!("{label}:{}:apply", node_id.0)),
453            owner: Some(lumen_gpu::NodeKey(node_id.0)),
454            program,
455            bindings: spatial_bindings(source.texture, params, texture),
456            dispatch: dispatch_for(size).into(),
457        });
458        self.builder.param(
459            lumen_gpu::ParamKey {
460                owner: lumen_gpu::NodeKey(node_id.0),
461                slot: 0,
462            },
463            lumen_gpu::ParamTarget::Buffer(params),
464        );
465        Ok((source, texture, params))
466    }
467
468    fn media_output_node(&self) -> crate::Result<NodeId> {
469        let mut outputs = self
470            .composition
471            .graph
472            .nodes
473            .iter()
474            .filter_map(|(node_id, node)| {
475                matches!(node, NodeKind::MediaOutput(_)).then_some(*node_id)
476            });
477        let Some(output) = outputs.next() else {
478            return Err(crate::error::GraphValidationError::MissingMediaOutput.into());
479        };
480        if outputs.next().is_some() {
481            return Err(
482                crate::error::GraphValidationError::MultipleMediaOutputs { count: 2 }.into(),
483            );
484        }
485        Ok(output)
486    }
487
488    pub(crate) fn expr_context(
489        &self,
490        node_id: NodeId,
491        property_path: &str,
492    ) -> ExpressionContext<'_> {
493        ExpressionContext {
494            frame: self.frame,
495            fps: self.composition.timeline.fps,
496            width: self.composition.render_settings.width,
497            height: self.composition.render_settings.height,
498            duration_frames: self.composition.timeline.duration_frames,
499            path: Some(format!("{node_id}.{property_path}")),
500            graph: Some(&self.composition.graph),
501        }
502    }
503
504    pub(crate) fn missing_output(&self, node_id: NodeId, port: &str) -> crate::error::LumenError {
505        crate::error::PropertyError::MissingProperty {
506            node_id,
507            property_path: format!("output port `{port}`"),
508        }
509        .into()
510    }
511}
512
513#[derive(Debug)]
514pub struct FrameBindContext<'a> {
515    composition: &'a Composition,
516    frame: u32,
517    media: Option<&'a dyn MediaStore>,
518}
519
520impl<'a> FrameBindContext<'a> {
521    pub fn new(composition: &'a Composition, frame: u32) -> Self {
522        Self {
523            composition,
524            frame,
525            media: None,
526        }
527    }
528
529    pub fn with_media<M: MediaStore>(
530        composition: &'a Composition,
531        frame: u32,
532        media: &'a M,
533    ) -> Self {
534        Self {
535            composition,
536            frame,
537            media: Some(media),
538        }
539    }
540
541    pub fn bind(&self, compiled: &CompiledComposition) -> crate::Result<BoundFrame> {
542        tracing::trace!(
543            target: "lumen_bind",
544            frame = self.frame,
545            bindings = compiled.frame_bindings.len(),
546            "bind compiled frame"
547        );
548        let mut bound = BoundFrame::new();
549        for (index, binding) in compiled.frame_bindings.iter().enumerate() {
550            let binding_frame = compiled
551                .frame_binding_frames
552                .get(index)
553                .copied()
554                .flatten()
555                .unwrap_or(self.frame);
556            let binding_context = Self {
557                composition: self.composition,
558                frame: binding_frame,
559                media: self.media,
560            };
561            let node_id = binding.node_id();
562            tracing::trace!(
563                target: "lumen_bind",
564                frame = self.frame,
565                binding_frame,
566                node_id = node_id.0,
567                binding_index = index,
568                "bind frame resource"
569            );
570            let node =
571                self.composition
572                    .graph
573                    .nodes
574                    .get(&node_id)
575                    .ok_or(RenderError::MissingNode {
576                        frame: self.frame,
577                        node_id,
578                    })?;
579            match node {
580                NodeKind::MediaIn(node) => {
581                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
582                }
583                NodeKind::SolidColor(node) => {
584                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
585                }
586                NodeKind::Text(node) => {
587                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
588                }
589                NodeKind::Path(node) => {
590                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
591                }
592                NodeKind::Shape(node) => {
593                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
594                }
595                NodeKind::Boolean(node) => {
596                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
597                }
598                NodeKind::Merge(node) => {
599                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
600                }
601                NodeKind::RasterMultiMerge(node) => {
602                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
603                }
604                NodeKind::AlphaPremultiply(node) => {
605                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
606                }
607                NodeKind::Blur(node) => {
608                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
609                }
610                NodeKind::ChannelShuffle(node) => {
611                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
612                }
613                NodeKind::ColorGrade(node) => {
614                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
615                }
616                NodeKind::Curves(node) => {
617                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
618                }
619                NodeKind::Exposure(node) => {
620                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
621                }
622                NodeKind::HueSaturation(node) => {
623                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
624                }
625                NodeKind::Levels(node) => {
626                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
627                }
628                NodeKind::Memo(node) => {
629                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
630                }
631                NodeKind::TimeRemap(node) => {
632                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
633                }
634                NodeKind::Transform(node) => {
635                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
636                }
637                NodeKind::Crop(node) => {
638                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
639                }
640                NodeKind::Resize(node) => {
641                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
642                }
643                NodeKind::Shadow(node) => {
644                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
645                }
646                NodeKind::WgslShader(node) => {
647                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
648                }
649                NodeKind::Switch(node) => {
650                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
651                }
652                NodeKind::MediaOutput(node) => {
653                    node.bind_gpu_frame(&binding_context, binding, &mut bound)?
654                }
655            }
656        }
657        Ok(bound)
658    }
659
660    pub(crate) fn frame(&self) -> u32 {
661        self.frame
662    }
663
664    pub(crate) fn media(&self) -> Option<&dyn MediaStore> {
665        self.media
666    }
667
668    pub(crate) fn expr_context(
669        &self,
670        node_id: NodeId,
671        property_path: &str,
672    ) -> ExpressionContext<'_> {
673        ExpressionContext {
674            frame: self.frame,
675            fps: self.composition.timeline.fps,
676            width: self.composition.render_settings.width,
677            height: self.composition.render_settings.height,
678            duration_frames: self.composition.timeline.duration_frames,
679            path: Some(format!("{node_id}.{property_path}")),
680            graph: Some(&self.composition.graph),
681        }
682    }
683}
684
685#[repr(C)]
686#[derive(Debug, Clone, Copy, Pod, Zeroable)]
687pub(crate) struct ColorParams {
688    pub(crate) color: [f32; 4],
689}
690
691impl ColorParams {
692    pub(crate) fn from_rgba8(color: [u8; 4]) -> Self {
693        Self {
694            color: [
695                f32::from(color[0]) / 255.0,
696                f32::from(color[1]) / 255.0,
697                f32::from(color[2]) / 255.0,
698                f32::from(color[3]) / 255.0,
699            ],
700        }
701    }
702}
703
704#[repr(C)]
705#[derive(Debug, Clone, Copy, Pod, Zeroable)]
706pub(crate) struct AlphaPremultiplyParams {
707    pub(crate) values: [f32; 4],
708}
709
710#[repr(C)]
711#[derive(Debug, Clone, Copy, Pod, Zeroable)]
712pub(crate) struct ChannelShuffleParams {
713    pub(crate) selector_indices: [f32; 4],
714    pub(crate) selector_values: [f32; 4],
715}
716
717#[repr(C)]
718#[derive(Debug, Clone, Copy, Pod, Zeroable)]
719pub(crate) struct ColorGradeParams {
720    pub(crate) strength: f32,
721    pub(crate) interpolation: u32,
722    pub(crate) _pad: [u32; 2],
723}
724
725#[repr(C)]
726#[derive(Debug, Clone, Copy, Pod, Zeroable)]
727pub(crate) struct ColorGradeLut {
728    stops: [[f32; 4]; LUT_TABLE_SIZE],
729}
730
731impl ColorGradeLut {
732    pub(crate) fn parse(node_id: NodeId, frame: u32, source: &str) -> crate::Result<Self> {
733        let stops = parse_lut_stops(node_id, frame, source)?;
734        let mut table = [[0.0; 4]; LUT_TABLE_SIZE];
735        for (index, entry) in table.iter_mut().enumerate() {
736            let value = index as f32 / (LUT_TABLE_SIZE - 1) as f32;
737            let scaled = value * (stops.len() - 1) as f32;
738            let low = scaled.floor() as usize;
739            let high = (low + 1).min(stops.len() - 1);
740            let t = scaled - low as f32;
741            *entry = [
742                stops[low][0] + (stops[high][0] - stops[low][0]) * t,
743                stops[low][1] + (stops[high][1] - stops[low][1]) * t,
744                stops[low][2] + (stops[high][2] - stops[low][2]) * t,
745                1.0,
746            ];
747        }
748        Ok(Self { stops: table })
749    }
750}
751
752#[repr(C)]
753#[derive(Debug, Clone, Copy, Pod, Zeroable)]
754pub(crate) struct ExposureParams {
755    pub(crate) exposure: f32,
756    pub(crate) contrast: f32,
757    pub(crate) offset: f32,
758    pub(crate) _pad: f32,
759}
760
761#[repr(C)]
762#[derive(Debug, Clone, Copy, Pod, Zeroable)]
763pub(crate) struct HueSaturationParams {
764    pub(crate) hue_offset: f32,
765    pub(crate) saturation: f32,
766    pub(crate) lightness: f32,
767    pub(crate) _pad: f32,
768}
769
770#[repr(C)]
771#[derive(Debug, Clone, Copy, Pod, Zeroable)]
772pub(crate) struct LevelsParams {
773    pub(crate) black_point: f32,
774    pub(crate) white_point: f32,
775    pub(crate) gamma: f32,
776    pub(crate) output_black: f32,
777    pub(crate) output_white: f32,
778    pub(crate) _pad: [f32; 3],
779}
780
781#[repr(C)]
782#[derive(Debug, Clone, Copy, Pod, Zeroable)]
783pub(crate) struct BlurParams {
784    pub(crate) values: [u32; 4],
785}
786
787#[repr(C)]
788#[derive(Debug, Clone, Copy, Pod, Zeroable)]
789pub(crate) struct CurvesParams {
790    pub(crate) values: [f32; 4],
791}
792
793#[repr(C)]
794#[derive(Debug, Clone, Copy, Pod, Zeroable)]
795pub(crate) struct CurvesTable {
796    entries: [[f32; 4]; LUT_TABLE_SIZE],
797}
798
799impl CurvesTable {
800    pub(crate) fn parse(node_id: NodeId, frame: u32, source: &str) -> crate::Result<Self> {
801        let stops = parse_lut_stops(node_id, frame, source)?;
802        let mut entries = [[0.0; 4]; LUT_TABLE_SIZE];
803        for (index, entry) in entries.iter_mut().enumerate() {
804            let value = index as f32 / (LUT_TABLE_SIZE - 1) as f32;
805            let scaled = value * (stops.len() - 1) as f32;
806            let low = scaled.floor() as usize;
807            let high = (low + 1).min(stops.len() - 1);
808            let t = scaled - low as f32;
809            *entry = [
810                stops[low][0] + (stops[high][0] - stops[low][0]) * t,
811                stops[low][1] + (stops[high][1] - stops[low][1]) * t,
812                stops[low][2] + (stops[high][2] - stops[low][2]) * t,
813                1.0,
814            ];
815        }
816        Ok(Self { entries })
817    }
818}
819
820#[repr(C)]
821#[derive(Debug, Clone, Copy, Pod, Zeroable)]
822pub(crate) struct ShadowParams {
823    pub(crate) color: [f32; 4],
824    pub(crate) values: [f32; 4],
825}
826
827#[repr(C)]
828#[derive(Debug, Clone, Copy, Pod, Zeroable)]
829pub(crate) struct WgslShaderParams {
830    pub(crate) values: [f32; 4],
831}
832
833#[repr(C)]
834#[derive(Debug, Clone, Copy, Pod, Zeroable)]
835pub(crate) struct MergeParams {
836    pub(crate) opacity: f32,
837    pub(crate) blend_mode: u32,
838    pub(crate) has_mask: u32,
839    pub(crate) _pad: u32,
840}
841
842#[repr(C)]
843#[derive(Debug, Clone, Copy, Pod, Zeroable)]
844pub(crate) struct BooleanParams {
845    pub(crate) values: [f32; 4],
846}
847
848#[repr(C)]
849#[derive(Debug, Clone, Copy, Pod, Zeroable)]
850pub(crate) struct RasterMultiMergeParams {
851    pub(crate) values: [f32; 4],
852}
853
854#[derive(Debug, Clone, Copy)]
855pub(crate) struct ChannelSelector {
856    pub(crate) index: f32,
857    pub(crate) value: f32,
858}
859
860const LUT_TABLE_SIZE: usize = 256;
861
862#[repr(C)]
863#[derive(Debug, Clone, Copy, Pod, Zeroable)]
864pub(crate) struct TransformParams {
865    pub(crate) scale: [f32; 2],
866    pub(crate) translate: [f32; 2],
867    pub(crate) pivot: [f32; 2],
868    pub(crate) rotate_radians: f32,
869    pub(crate) sampling: u32,
870    pub(crate) _pad: [u32; 4],
871}
872
873#[repr(C)]
874#[derive(Debug, Clone, Copy, Pod, Zeroable)]
875pub(crate) struct CropParams {
876    pub(crate) origin: [i32; 2],
877    pub(crate) size: [u32; 2],
878}
879
880#[repr(C)]
881#[derive(Debug, Clone, Copy, Pod, Zeroable)]
882pub(crate) struct ResizeParams {
883    pub(crate) size: [u32; 2],
884    pub(crate) mode: u32,
885    pub(crate) sampling: u32,
886}
887
888pub(crate) fn dispatch_for(size: lumen_gpu::Size) -> lumen_gpu::Dispatch {
889    lumen_gpu::Dispatch {
890        x: size.width.div_ceil(8),
891        y: size.height.div_ceil(8),
892        z: 1,
893    }
894}
895
896pub(crate) fn spatial_bindings(
897    input: lumen_gpu::TextureId,
898    params: lumen_gpu::BufferId,
899    output: lumen_gpu::TextureId,
900) -> Vec<lumen_gpu::Binding> {
901    vec![
902        lumen_gpu::Binding::sampled_texture(0, 0, input),
903        lumen_gpu::Binding::uniform(0, 1, params),
904        lumen_gpu::Binding::storage_texture(0, 2, output),
905    ]
906}
907
908pub(crate) fn alpha_operation(node_id: NodeId, mode: &str) -> crate::Result<f32> {
909    match mode.trim().to_ascii_lowercase().as_str() {
910        "premultiply" | "premul" | "multiply" => Ok(0.0),
911        "unpremultiply" | "unpremul" | "straight" | "unmultiply" => Ok(1.0),
912        _ => Err(crate::error::PropertyError::InvalidType {
913            node_id,
914            property_path: "mode".to_string(),
915            expected: "`premultiply` or `unpremultiply`",
916            actual: "String",
917        }
918        .into()),
919    }
920}
921
922pub(crate) fn channel_selector(
923    node_id: NodeId,
924    property_path: &str,
925    spec: &str,
926) -> crate::Result<ChannelSelector> {
927    let normalized = spec.trim().to_ascii_lowercase();
928    match normalized.as_str() {
929        "r" | "red" => Ok(ChannelSelector {
930            index: 0.0,
931            value: 0.0,
932        }),
933        "g" | "green" => Ok(ChannelSelector {
934            index: 1.0,
935            value: 0.0,
936        }),
937        "b" | "blue" => Ok(ChannelSelector {
938            index: 2.0,
939            value: 0.0,
940        }),
941        "a" | "alpha" => Ok(ChannelSelector {
942            index: 3.0,
943            value: 0.0,
944        }),
945        "zero" => Ok(ChannelSelector {
946            index: 4.0,
947            value: 0.0,
948        }),
949        "one" => Ok(ChannelSelector {
950            index: 4.0,
951            value: 1.0,
952        }),
953        _ => {
954            let value = normalized.parse::<f32>().map_err(|_| {
955                crate::error::PropertyError::InvalidType {
956                    node_id,
957                    property_path: property_path.to_string(),
958                    expected: "channel name or numeric constant",
959                    actual: "String",
960                }
961            })?;
962            Ok(ChannelSelector {
963                index: 4.0,
964                value: if value <= 1.0 {
965                    value.clamp(0.0, 1.0)
966                } else {
967                    (value / 255.0).clamp(0.0, 1.0)
968                },
969            })
970        }
971    }
972}
973
974fn parse_lut_stops(node_id: NodeId, frame: u32, source: &str) -> crate::Result<Vec<[f32; 3]>> {
975    let source = source.trim();
976    if source.is_empty()
977        || source.eq_ignore_ascii_case(crate::node::processing::color_grade::IDENTITY_LUT)
978    {
979        return Ok(vec![[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]]);
980    }
981
982    let source = source
983        .strip_prefix("rgb1d")
984        .and_then(|rest| rest.strip_prefix(':'))
985        .unwrap_or(source);
986    let mut stops = Vec::new();
987    for triplet in source.split(';') {
988        let triplet = triplet.trim();
989        if triplet.is_empty() {
990            continue;
991        }
992        let components = triplet
993            .split([',', ' ', '\t'])
994            .filter(|part| !part.is_empty())
995            .map(str::parse::<f32>)
996            .collect::<Result<Vec<_>, _>>()
997            .map_err(|_| lut_error(node_id, frame, "LUT contains a non-numeric component"))?;
998        if components.len() != 3 {
999            return Err(lut_error(
1000                node_id,
1001                frame,
1002                format!("LUT triplet `{triplet}` must contain exactly three RGB components"),
1003            ));
1004        }
1005        stops.push([
1006            normalize_lut_component(components[0]),
1007            normalize_lut_component(components[1]),
1008            normalize_lut_component(components[2]),
1009        ]);
1010    }
1011    if stops.len() < 2 {
1012        return Err(lut_error(
1013            node_id,
1014            frame,
1015            "LUT must contain at least two RGB triplets",
1016        ));
1017    }
1018    Ok(stops)
1019}
1020
1021fn normalize_lut_component(value: f32) -> f32 {
1022    if value > 1.0 {
1023        (value / 255.0).clamp(0.0, 1.0)
1024    } else {
1025        value.clamp(0.0, 1.0)
1026    }
1027}
1028
1029fn lut_error(node_id: NodeId, frame: u32, details: impl Into<String>) -> crate::error::LumenError {
1030    RenderError::NodeEvaluation {
1031        frame,
1032        node_id,
1033        node_kind: "ColorGrade",
1034        details: details.into(),
1035    }
1036    .into()
1037}
1038
1039pub(crate) fn copyable_texture_desc(size: lumen_gpu::Size) -> lumen_gpu::TextureDesc {
1040    lumen_gpu::TextureDesc {
1041        domain: lumen_gpu::TextureDomain::full_frame(size),
1042        format: lumen_gpu::wgpu::TextureFormat::Rgba8Unorm,
1043        usage: lumen_gpu::wgpu::TextureUsages::COPY_DST
1044            | lumen_gpu::wgpu::TextureUsages::COPY_SRC
1045            | lumen_gpu::wgpu::TextureUsages::TEXTURE_BINDING
1046            | lumen_gpu::wgpu::TextureUsages::STORAGE_BINDING
1047            | lumen_gpu::wgpu::TextureUsages::RENDER_ATTACHMENT,
1048    }
1049}