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}