Skip to main content

lumen_engine/node/vector/
renderer.rs

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