Skip to main content

lumen_engine/node/vector/
renderer.rs

1use bytemuck::{Pod, Zeroable};
2
3use crate::{
4    gpu::{CompiledOutput, RasterHandle, RasterMetadata, compiler},
5    node::{NodeId, PortRef},
6};
7
8use crate::node::source::text::{CompiledText, Text};
9
10use super::{
11    path::{CompiledPath, Path},
12    shape::{CompiledShape, Shape},
13};
14
15pub(crate) const SHAPE_SHADER: &str = include_str!("shape_renderer.wgsl");
16pub(crate) const PATH_SHADER: &str = include_str!("path_renderer.wgsl");
17pub(crate) const TEXT_ATLAS_SIZE: lumen_gpu::Size = lumen_gpu::Size {
18    width: 2048,
19    height: 2048,
20};
21pub(crate) const MAX_TEXT_GLYPHS: usize = 2048;
22pub(crate) const MAX_PATH_POINTS: usize = 128;
23
24pub(crate) struct VectorRenderer<'a, 'b> {
25    ctx: &'a mut crate::gpu::CompileContext<'b>,
26}
27
28impl<'a, 'b> VectorRenderer<'a, 'b> {
29    pub(crate) fn new(ctx: &'a mut crate::gpu::CompileContext<'b>) -> Self {
30        Self { ctx }
31    }
32
33    pub(crate) fn compile_shape(
34        &mut self,
35        shape: &Shape,
36        port: &PortRef,
37    ) -> crate::Result<CompiledOutput> {
38        let (texture, params, size) = self.compile_vector_source(
39            shape.id,
40            port,
41            "shape",
42            SHAPE_SHADER,
43            std::mem::size_of::<ShapeParams>() as u64,
44        )?;
45        self.ctx.register_compiled_node(CompiledShape {
46            node_id: shape.id,
47            params: shape.params.clone(),
48            buffer: params,
49        });
50
51        Ok(CompiledOutput::Raster(RasterHandle {
52            texture,
53            domain: lumen_gpu::TextureDomain::full_frame(size),
54            metadata: RasterMetadata::default(),
55        }))
56    }
57
58    pub(crate) fn compile_path(
59        &mut self,
60        path: &Path,
61        port: &PortRef,
62    ) -> crate::Result<CompiledOutput> {
63        if port.port != "output" {
64            return Err(self.ctx.missing_output(path.id, &port.port));
65        }
66
67        let size = lumen_gpu::Size::new(
68            self.ctx.composition().render_settings.width.max(1),
69            self.ctx.composition().render_settings.height.max(1),
70        );
71        let texture = self.ctx.builder_mut().texture_for(
72            lumen_gpu::NodeKey(path.id.0),
73            Some(format!("path:{}:output", path.id.0)),
74            lumen_gpu::TextureDesc::storage(size, lumen_gpu::wgpu::TextureFormat::Rgba8Unorm),
75        );
76        let params = self.ctx.builder_mut().buffer_for(
77            lumen_gpu::NodeKey(path.id.0),
78            Some(format!("path:{}:params", path.id.0)),
79            lumen_gpu::BufferDesc::uniform(std::mem::size_of::<PathParams>() as u64),
80        );
81        let points = self.ctx.builder_mut().buffer_for(
82            lumen_gpu::NodeKey(path.id.0),
83            Some(format!("path:{}:points", path.id.0)),
84            lumen_gpu::BufferDesc::storage(
85                (MAX_PATH_POINTS * std::mem::size_of::<PathPoint>()) as u64,
86            ),
87        );
88        let program = self.ctx.builder_mut().program_for(
89            lumen_gpu::NodeKey(path.id.0),
90            lumen_gpu::ProgramDesc::Compute(lumen_gpu::ComputeProgramDesc {
91                label: Some("path".to_string()),
92                shader: PATH_SHADER.to_string(),
93                entry: "cs_main".to_string(),
94                bind_groups: lumen_gpu::BindGroupLayoutSpec::single(vec![
95                    lumen_gpu::BindingLayoutEntry::uniform(
96                        0,
97                        lumen_gpu::wgpu::ShaderStages::COMPUTE,
98                    ),
99                    lumen_gpu::BindingLayoutEntry::storage(
100                        1,
101                        lumen_gpu::wgpu::ShaderStages::COMPUTE,
102                        true,
103                    ),
104                    lumen_gpu::BindingLayoutEntry::storage_texture(
105                        2,
106                        lumen_gpu::wgpu::ShaderStages::COMPUTE,
107                        lumen_gpu::wgpu::TextureFormat::Rgba8Unorm,
108                        lumen_gpu::wgpu::StorageTextureAccess::WriteOnly,
109                    ),
110                ]),
111            }),
112        );
113        self.ctx
114            .builder_mut()
115            .compute_pass(lumen_gpu::ComputePassDesc {
116                label: Some(format!("path:{}:rasterize", path.id.0)),
117                owner: Some(lumen_gpu::NodeKey(path.id.0)),
118                program,
119                bindings: vec![
120                    lumen_gpu::Binding::uniform(0, 0, params),
121                    lumen_gpu::Binding::storage_buffer(0, 1, points),
122                    lumen_gpu::Binding::storage_texture(0, 2, texture),
123                ],
124                dispatch: compiler::dispatch_for(size).into(),
125            });
126        self.ctx.builder_mut().param(
127            lumen_gpu::ParamKey {
128                owner: lumen_gpu::NodeKey(path.id.0),
129                slot: 0,
130            },
131            lumen_gpu::ParamTarget::Buffer(params),
132        );
133        self.ctx.register_compiled_node(CompiledPath {
134            node_id: path.id,
135            params: path.params.clone(),
136            params_buffer: params,
137            points_buffer: points,
138            max_points: MAX_PATH_POINTS,
139        });
140
141        Ok(CompiledOutput::Raster(RasterHandle {
142            texture,
143            domain: lumen_gpu::TextureDomain::full_frame(size),
144            metadata: RasterMetadata::default(),
145        }))
146    }
147
148    pub(crate) fn compile_text(
149        &mut self,
150        text: &Text,
151        port: &PortRef,
152    ) -> crate::Result<CompiledOutput> {
153        if port.port != "output" {
154            return Err(self.ctx.missing_output(text.id, &port.port));
155        }
156        crate::node::source::text::clear_text_cache_for(text.id);
157
158        let size = lumen_gpu::Size::new(
159            self.ctx.composition().render_settings.width.max(1),
160            self.ctx.composition().render_settings.height.max(1),
161        );
162        let texture = self.ctx.builder_mut().texture_for(
163            lumen_gpu::NodeKey(text.id.0),
164            Some(format!("text:{}:output", text.id.0)),
165            lumen_gpu::TextureDesc::render_target(size, lumen_gpu::wgpu::TextureFormat::Rgba8Unorm),
166        );
167        let atlas_texture = self.ctx.builder_mut().texture_for(
168            lumen_gpu::NodeKey(text.id.0),
169            Some(format!("text:{}:atlas", text.id.0)),
170            lumen_gpu::TextureDesc::sampled(
171                TEXT_ATLAS_SIZE,
172                lumen_gpu::wgpu::TextureFormat::Rgba8Unorm,
173            ),
174        );
175        let globals_buffer =
176            self.ctx.builder_mut().buffer_for(
177                lumen_gpu::NodeKey(text.id.0),
178                Some(format!("text:{}:globals", text.id.0)),
179                lumen_gpu::BufferDesc::uniform(
180                    std::mem::size_of::<lumen_text::GpuTextGlobals>() as u64
181                ),
182            );
183        let instances_buffer = self.ctx.builder_mut().buffer_for(
184            lumen_gpu::NodeKey(text.id.0),
185            Some(format!("text:{}:instances", text.id.0)),
186            lumen_gpu::BufferDesc::storage(
187                (MAX_TEXT_GLYPHS * std::mem::size_of::<lumen_text::GpuGlyphInstance>()) as u64,
188            ),
189        );
190        let atlas_sampler = self.ctx.builder_mut().sampler(
191            Some(format!("text:{}:atlas-sampler", text.id.0)),
192            lumen_gpu::wgpu::SamplerDescriptor {
193                label: Some("lumen text atlas sampler"),
194                address_mode_u: lumen_gpu::wgpu::AddressMode::ClampToEdge,
195                address_mode_v: lumen_gpu::wgpu::AddressMode::ClampToEdge,
196                address_mode_w: lumen_gpu::wgpu::AddressMode::ClampToEdge,
197                mag_filter: lumen_gpu::wgpu::FilterMode::Linear,
198                min_filter: lumen_gpu::wgpu::FilterMode::Linear,
199                mipmap_filter: lumen_gpu::wgpu::MipmapFilterMode::Nearest,
200                ..Default::default()
201            },
202        );
203        let program = self.ctx.builder_mut().program_for(
204            lumen_gpu::NodeKey(text.id.0),
205            lumen_gpu::ProgramDesc::Render(lumen_gpu::RenderProgramDesc {
206                label: Some("text".to_string()),
207                shader: lumen_text::ALPHA_TEXT_SHADER.to_string(),
208                vertex_entry: "vs_main".to_string(),
209                fragment_entry: "fs_main".to_string(),
210                bind_groups: lumen_gpu::BindGroupLayoutSpec::single(vec![
211                    lumen_gpu::BindingLayoutEntry::uniform(
212                        0,
213                        lumen_gpu::wgpu::ShaderStages::VERTEX_FRAGMENT,
214                    ),
215                    lumen_gpu::BindingLayoutEntry::texture(
216                        1,
217                        lumen_gpu::wgpu::ShaderStages::FRAGMENT,
218                    ),
219                    lumen_gpu::BindingLayoutEntry::sampler(
220                        2,
221                        lumen_gpu::wgpu::ShaderStages::FRAGMENT,
222                    ),
223                    lumen_gpu::BindingLayoutEntry::storage(
224                        3,
225                        lumen_gpu::wgpu::ShaderStages::VERTEX,
226                        true,
227                    ),
228                ]),
229                targets: vec![Some(lumen_gpu::wgpu::ColorTargetState {
230                    format: lumen_gpu::wgpu::TextureFormat::Rgba8Unorm,
231                    blend: Some(lumen_gpu::wgpu::BlendState::ALPHA_BLENDING),
232                    write_mask: lumen_gpu::wgpu::ColorWrites::ALL,
233                })],
234                vertex_buffers: Vec::new(),
235                primitive: lumen_gpu::wgpu::PrimitiveState::default(),
236            }),
237        );
238        self.ctx
239            .builder_mut()
240            .render_pass(lumen_gpu::RenderPassDesc {
241                label: Some(format!("text:{}:render", text.id.0)),
242                owner: Some(lumen_gpu::NodeKey(text.id.0)),
243                program,
244                targets: vec![lumen_gpu::RenderTargetRef {
245                    texture,
246                    load: lumen_gpu::LoadOp::Clear(lumen_gpu::wgpu::Color::TRANSPARENT),
247                    store: lumen_gpu::wgpu::StoreOp::Store,
248                }],
249                bindings: vec![
250                    lumen_gpu::Binding::uniform(0, 0, globals_buffer),
251                    lumen_gpu::Binding::sampled_texture(0, 1, atlas_texture),
252                    lumen_gpu::Binding::sampler(0, 2, atlas_sampler),
253                    lumen_gpu::Binding::storage_buffer(0, 3, instances_buffer),
254                ],
255                vertex_buffers: Vec::new(),
256                index_buffer: None,
257                draw: lumen_gpu::DrawCommand::Draw(lumen_gpu::Draw {
258                    vertices: 0..6,
259                    instances: 0..MAX_TEXT_GLYPHS as u32,
260                }),
261                scissor: None,
262            });
263        self.ctx.register_compiled_node(CompiledText {
264            node_id: text.id,
265            params: text.params.clone(),
266            atlas_texture,
267            globals_buffer,
268            instances_buffer,
269            atlas_size: TEXT_ATLAS_SIZE,
270            max_glyphs: MAX_TEXT_GLYPHS,
271            size,
272        });
273
274        Ok(CompiledOutput::Raster(RasterHandle {
275            texture,
276            domain: lumen_gpu::TextureDomain::full_frame(size),
277            metadata: RasterMetadata::default(),
278        }))
279    }
280
281    fn compile_vector_source(
282        &mut self,
283        node_id: NodeId,
284        port: &PortRef,
285        label: &str,
286        shader: &str,
287        params_size: u64,
288    ) -> crate::Result<(lumen_gpu::TextureId, lumen_gpu::BufferId, lumen_gpu::Size)> {
289        if port.port != "output" {
290            return Err(self.ctx.missing_output(node_id, &port.port));
291        }
292
293        let size = lumen_gpu::Size::new(
294            self.ctx.composition().render_settings.width.max(1),
295            self.ctx.composition().render_settings.height.max(1),
296        );
297        let texture = self.ctx.builder_mut().texture_for(
298            lumen_gpu::NodeKey(node_id.0),
299            Some(format!("{label}:{}:output", node_id.0)),
300            lumen_gpu::TextureDesc::storage(size, lumen_gpu::wgpu::TextureFormat::Rgba8Unorm),
301        );
302        let params = self.ctx.builder_mut().buffer_for(
303            lumen_gpu::NodeKey(node_id.0),
304            Some(format!("{label}:{}:params", node_id.0)),
305            lumen_gpu::BufferDesc::uniform(params_size),
306        );
307        let program = self.ctx.builder_mut().program_for(
308            lumen_gpu::NodeKey(node_id.0),
309            lumen_gpu::ProgramDesc::Compute(lumen_gpu::ComputeProgramDesc {
310                label: Some(label.to_string()),
311                shader: shader.to_string(),
312                entry: "cs_main".to_string(),
313                bind_groups: lumen_gpu::BindGroupLayoutSpec::single(vec![
314                    lumen_gpu::BindingLayoutEntry::uniform(
315                        0,
316                        lumen_gpu::wgpu::ShaderStages::COMPUTE,
317                    ),
318                    lumen_gpu::BindingLayoutEntry::storage_texture(
319                        1,
320                        lumen_gpu::wgpu::ShaderStages::COMPUTE,
321                        lumen_gpu::wgpu::TextureFormat::Rgba8Unorm,
322                        lumen_gpu::wgpu::StorageTextureAccess::WriteOnly,
323                    ),
324                ]),
325            }),
326        );
327        self.ctx
328            .builder_mut()
329            .compute_pass(lumen_gpu::ComputePassDesc {
330                label: Some(format!("{label}:{}:rasterize", node_id.0)),
331                owner: Some(lumen_gpu::NodeKey(node_id.0)),
332                program,
333                bindings: vec![
334                    lumen_gpu::Binding::uniform(0, 0, params),
335                    lumen_gpu::Binding::storage_texture(0, 1, texture),
336                ],
337                dispatch: compiler::dispatch_for(size).into(),
338            });
339        self.ctx.builder_mut().param(
340            lumen_gpu::ParamKey {
341                owner: lumen_gpu::NodeKey(node_id.0),
342                slot: 0,
343            },
344            lumen_gpu::ParamTarget::Buffer(params),
345        );
346        Ok((texture, params, size))
347    }
348}
349
350#[repr(C)]
351#[derive(Debug, Clone, Copy, Pod, Zeroable)]
352pub(crate) struct ShapeParams {
353    pub(crate) fill_paint: super::paint::GpuPaint,
354    pub(crate) stroke_paint: super::paint::GpuPaint,
355    pub(crate) position: [f32; 2],
356    pub(crate) size: [f32; 2],
357    pub(crate) border_radius: f32,
358    pub(crate) stroke_width: f32,
359    pub(crate) geometry_kind: u32,
360    pub(crate) flags: u32,
361}
362
363#[repr(C)]
364#[derive(Debug, Clone, Copy, Pod, Zeroable)]
365pub(crate) struct PathParams {
366    pub(crate) fill_paint: super::paint::GpuPaint,
367    pub(crate) stroke_paint: super::paint::GpuPaint,
368    pub(crate) position: [f32; 2],
369    pub(crate) bounds_min: [f32; 2],
370    pub(crate) bounds_size: [f32; 2],
371    pub(crate) stroke_width: f32,
372    pub(crate) flags: u32,
373    pub(crate) point_count: u32,
374    pub(crate) _pad: [u32; 3],
375}
376
377#[repr(C)]
378#[derive(Debug, Clone, Copy, Pod, Zeroable)]
379pub(crate) struct PathPoint {
380    pub(crate) position: [f32; 2],
381}