Skip to main content

lumen_engine/node/
media_output.rs

1use crate::node::{NodeId, PortRef};
2
3use crate::gpu::{
4    BoundFrame, CompiledOutput, FrameBindContext, FrameBinding, GpuCompileNode, GpuFrameBindNode,
5    RasterHandle, compiler,
6};
7
8pub(crate) const SHADER: &str = include_str!("media_output.wgsl");
9pub(crate) const RENDER_SHADER: &str = include_str!("media_output_render.wgsl");
10
11/// Copies the compiled raster into the final composition output.
12#[derive(Debug, Clone, lumen_macros::Node)]
13#[node(kind = "media_output", name = "Media Output", category = "output")]
14pub struct MediaOutput {
15    pub id: NodeId,
16    #[input()]
17    pub source: PortRef,
18}
19
20impl Default for MediaOutput {
21    fn default() -> Self {
22        Self {
23            id: NodeId::new(0),
24            source: PortRef::empty(),
25        }
26    }
27}
28
29impl GpuCompileNode for MediaOutput {
30    fn compile_gpu(
31        &self,
32        ctx: &mut crate::gpu::CompileContext<'_>,
33        port: &PortRef,
34    ) -> crate::Result<CompiledOutput> {
35        if port.port != "output" {
36            return Err(ctx.missing_output(self.id, &port.port));
37        }
38
39        let source = ctx
40            .compile_port(&self.source)?
41            .into_raster(self.source.id, &self.source.port)?;
42        let size = lumen_gpu::Size::new(
43            ctx.composition().render_settings.width.max(1),
44            ctx.composition().render_settings.height.max(1),
45        );
46        let output_format = ctx.output_format();
47        let output = ctx.builder_mut().texture_for(
48            lumen_gpu::NodeKey(self.id.0),
49            Some("media-output:final".to_string()),
50            media_output_texture_desc(size, output_format),
51        );
52        if output_format == lumen_gpu::wgpu::TextureFormat::Rgba8Unorm {
53            let program = ctx.builder_mut().program_for(
54                lumen_gpu::NodeKey(self.id.0),
55                lumen_gpu::ProgramDesc::Compute(lumen_gpu::ComputeProgramDesc {
56                    label: Some("media-output".to_string()),
57                    shader: SHADER.to_string(),
58                    entry: "cs_main".to_string(),
59                    bind_groups: lumen_gpu::BindGroupLayoutSpec::single(vec![
60                        lumen_gpu::BindingLayoutEntry::texture(
61                            0,
62                            lumen_gpu::wgpu::ShaderStages::COMPUTE,
63                        ),
64                        lumen_gpu::BindingLayoutEntry::storage_texture(
65                            1,
66                            lumen_gpu::wgpu::ShaderStages::COMPUTE,
67                            lumen_gpu::wgpu::TextureFormat::Rgba8Unorm,
68                            lumen_gpu::wgpu::StorageTextureAccess::WriteOnly,
69                        ),
70                    ]),
71                }),
72            );
73            ctx.builder_mut().compute_pass(lumen_gpu::ComputePassDesc {
74                label: Some("media-output:copy".to_string()),
75                owner: Some(lumen_gpu::NodeKey(self.id.0)),
76                program,
77                bindings: vec![
78                    lumen_gpu::Binding::sampled_texture(0, 0, source.texture),
79                    lumen_gpu::Binding::storage_texture(0, 1, output),
80                ],
81                dispatch: compiler::dispatch_for(size).into(),
82            });
83        } else {
84            let sampler = ctx.builder_mut().sampler(
85                Some("media-output:sampler".to_string()),
86                lumen_gpu::wgpu::SamplerDescriptor {
87                    address_mode_u: lumen_gpu::wgpu::AddressMode::ClampToEdge,
88                    address_mode_v: lumen_gpu::wgpu::AddressMode::ClampToEdge,
89                    address_mode_w: lumen_gpu::wgpu::AddressMode::ClampToEdge,
90                    mag_filter: lumen_gpu::wgpu::FilterMode::Nearest,
91                    min_filter: lumen_gpu::wgpu::FilterMode::Nearest,
92                    mipmap_filter: lumen_gpu::wgpu::MipmapFilterMode::Nearest,
93                    ..Default::default()
94                },
95            );
96            let program = ctx.builder_mut().program_for(
97                lumen_gpu::NodeKey(self.id.0),
98                lumen_gpu::ProgramDesc::Render(lumen_gpu::RenderProgramDesc {
99                    label: Some("media-output".to_string()),
100                    shader: RENDER_SHADER.to_string(),
101                    vertex_entry: "vs_main".to_string(),
102                    fragment_entry: "fs_main".to_string(),
103                    bind_groups: lumen_gpu::BindGroupLayoutSpec::single(vec![
104                        lumen_gpu::BindingLayoutEntry::texture(
105                            0,
106                            lumen_gpu::wgpu::ShaderStages::FRAGMENT,
107                        ),
108                        lumen_gpu::BindingLayoutEntry::sampler(
109                            1,
110                            lumen_gpu::wgpu::ShaderStages::FRAGMENT,
111                        ),
112                    ]),
113                    targets: vec![Some(lumen_gpu::wgpu::ColorTargetState {
114                        format: output_format,
115                        blend: Some(lumen_gpu::wgpu::BlendState::REPLACE),
116                        write_mask: lumen_gpu::wgpu::ColorWrites::ALL,
117                    })],
118                    vertex_buffers: Vec::new(),
119                    primitive: lumen_gpu::wgpu::PrimitiveState::default(),
120                }),
121            );
122            ctx.builder_mut().render_pass(lumen_gpu::RenderPassDesc {
123                label: Some("media-output:render".to_string()),
124                owner: Some(lumen_gpu::NodeKey(self.id.0)),
125                program,
126                targets: vec![lumen_gpu::RenderTargetRef {
127                    texture: output,
128                    load: lumen_gpu::LoadOp::Clear(lumen_gpu::wgpu::Color::TRANSPARENT),
129                    store: lumen_gpu::wgpu::StoreOp::Store,
130                }],
131                bindings: vec![
132                    lumen_gpu::Binding::sampled_texture(0, 0, source.texture),
133                    lumen_gpu::Binding::sampler(0, 1, sampler),
134                ],
135                vertex_buffers: Vec::new(),
136                index_buffer: None,
137                draw: lumen_gpu::DrawCommand::Draw(lumen_gpu::Draw {
138                    vertices: 0..3,
139                    instances: 0..1,
140                }),
141                scissor: None,
142            });
143        }
144
145        Ok(CompiledOutput::Raster(RasterHandle {
146            texture: output,
147            domain: lumen_gpu::TextureDomain::full_frame(size),
148            metadata: source.metadata,
149        }))
150    }
151}
152
153fn media_output_texture_desc(
154    size: lumen_gpu::Size,
155    format: lumen_gpu::wgpu::TextureFormat,
156) -> lumen_gpu::TextureDesc {
157    if format == lumen_gpu::wgpu::TextureFormat::Rgba8Unorm {
158        compiler::copyable_texture_desc(size)
159    } else {
160        lumen_gpu::TextureDesc {
161            domain: lumen_gpu::TextureDomain::full_frame(size),
162            format,
163            usage: lumen_gpu::wgpu::TextureUsages::COPY_SRC
164                | lumen_gpu::wgpu::TextureUsages::TEXTURE_BINDING
165                | lumen_gpu::wgpu::TextureUsages::RENDER_ATTACHMENT,
166        }
167    }
168}
169
170impl GpuFrameBindNode for MediaOutput {
171    fn bind_gpu_frame(
172        &self,
173        _ctx: &FrameBindContext<'_>,
174        _binding: &FrameBinding,
175        _bound: &mut BoundFrame,
176    ) -> crate::Result<()> {
177        Ok(())
178    }
179}