Skip to main content

wisp/render/
flex_text_pipeline.rs

1//! Late-pass textured-quad pipeline for [`FlexText`] scene nodes.
2//!
3//! Structurally identical to [`SpritePipeline`](super::sprite_pipeline)
4//! — same shader, same instancing layout, same blend-mode cache —
5//! but walks the scene for [`Node::FlexText`] instead of
6//! [`Node::Sprite`] and is invoked by the renderer in the **fourth
7//! render pass**, after [`TextPipeline`](super::text_pipeline). The
8//! intent is for cosmic-text-rendered axis labels / legends / KPI
9//! big numbers to paint on top of the chart's
10//! [`Graphics`](crate::scene::Graphics) primitives instead of under
11//! them.
12//!
13//! Kept as a separate file from `sprite_pipeline.rs` so the pass
14//! order is self-documenting at the call site in
15//! [`crate::render::Renderer::render_stage_fast`].
16
17use std::collections::{HashMap, HashSet};
18
19use bytemuck::{Pod, Zeroable};
20use glam::Mat4;
21use wgpu::util::DeviceExt;
22
23use crate::application::Application;
24use crate::blend::BlendMode;
25use crate::color::Color;
26use crate::render::blend_pipeline::BlendPipelineMap;
27use crate::render::scene_walk::walk_visible_subtree;
28use crate::scene::{FlexText, Node, NodeId, Stage};
29use crate::texture::Texture;
30
31#[repr(C)]
32#[derive(Clone, Copy, Pod, Zeroable)]
33pub(crate) struct FlexTextInstance {
34    pub model: [[f32; 4]; 4],
35    pub tint: [f32; 4],
36    pub anchor: [f32; 2],
37    pub _padding: [f32; 2],
38}
39
40const ATTR_LAYOUT: [wgpu::VertexAttribute; 6] = wgpu::vertex_attr_array![
41    0 => Float32x4,
42    1 => Float32x4,
43    2 => Float32x4,
44    3 => Float32x4,
45    4 => Float32x4,
46    5 => Float32x2,
47];
48
49pub(crate) struct FlexTextPipeline {
50    pipelines: BlendPipelineMap,
51    texture_layout: wgpu::BindGroupLayout,
52}
53
54impl FlexTextPipeline {
55    pub(crate) fn new(app: &Application, output_format: wgpu::TextureFormat) -> Self {
56        let device = app.device();
57        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
58            label: Some("wisp::flex_text"),
59            source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/sprite.wgsl").into()),
60        });
61
62        let texture_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
63            label: Some("wisp::flex_text texture layout"),
64            entries: &[
65                wgpu::BindGroupLayoutEntry {
66                    binding: 0,
67                    visibility: wgpu::ShaderStages::FRAGMENT,
68                    ty: wgpu::BindingType::Texture {
69                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
70                        view_dimension: wgpu::TextureViewDimension::D2,
71                        multisampled: false,
72                    },
73                    count: None,
74                },
75                wgpu::BindGroupLayoutEntry {
76                    binding: 1,
77                    visibility: wgpu::ShaderStages::FRAGMENT,
78                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
79                    count: None,
80                },
81            ],
82        });
83
84        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
85            label: Some("wisp::flex_text pipeline layout"),
86            bind_group_layouts: &[&texture_layout],
87            push_constant_ranges: &[],
88        });
89
90        let pipelines = BlendPipelineMap::new(|blend| {
91            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
92                label: Some("wisp::flex_text pipeline"),
93                layout: Some(&pipeline_layout),
94                vertex: wgpu::VertexState {
95                    module: &shader,
96                    entry_point: Some("main_vs"),
97                    buffers: &[wgpu::VertexBufferLayout {
98                        array_stride: std::mem::size_of::<FlexTextInstance>()
99                            as wgpu::BufferAddress,
100                        step_mode: wgpu::VertexStepMode::Instance,
101                        attributes: &ATTR_LAYOUT,
102                    }],
103                    compilation_options: wgpu::PipelineCompilationOptions::default(),
104                },
105                fragment: Some(wgpu::FragmentState {
106                    module: &shader,
107                    entry_point: Some("main_fs"),
108                    targets: &[Some(wgpu::ColorTargetState {
109                        format: output_format,
110                        blend: Some(blend),
111                        write_mask: wgpu::ColorWrites::ALL,
112                    })],
113                    compilation_options: wgpu::PipelineCompilationOptions::default(),
114                }),
115                primitive: wgpu::PrimitiveState::default(),
116                depth_stencil: None,
117                multisample: wgpu::MultisampleState::default(),
118                multiview: None,
119                cache: None,
120            })
121        });
122
123        Self {
124            pipelines,
125            texture_layout,
126        }
127    }
128
129    /// Traverse `stage` in pre-order, accumulate flex-text instances
130    /// grouped by `(texture id, blend mode)`, then draw one batch per
131    /// group.
132    ///
133    /// Returns `(draw_calls, instances_drawn)`.
134    pub(crate) fn draw_stage(
135        &self,
136        app: &Application,
137        pass: &mut wgpu::RenderPass<'_>,
138        stage: &Stage,
139    ) -> (u32, u32) {
140        self.draw_subtree(app, pass, stage, stage.root(), &HashSet::new())
141    }
142
143    /// Render only the subtree rooted at `start`. Nodes whose IDs
144    /// are in `exclude` (and their descendants) are skipped.
145    pub(crate) fn draw_subtree(
146        &self,
147        app: &Application,
148        pass: &mut wgpu::RenderPass<'_>,
149        stage: &Stage,
150        start: NodeId,
151        exclude: &HashSet<NodeId>,
152    ) -> (u32, u32) {
153        let batches = collect_batches(stage, start, exclude);
154        let mut draw_calls = 0u32;
155        let mut nodes_drawn = 0u32;
156
157        for batch in &batches {
158            let count = u32::try_from(batch.instances.len()).expect("instance count fits in u32");
159            nodes_drawn = nodes_drawn.saturating_add(count);
160            let buffer = app
161                .device()
162                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
163                    label: Some("wisp::flex_text instances"),
164                    contents: bytemuck::cast_slice(&batch.instances),
165                    usage: wgpu::BufferUsages::VERTEX,
166                });
167
168            let bg = app.device().create_bind_group(&wgpu::BindGroupDescriptor {
169                label: Some("wisp::flex_text texture bg"),
170                layout: &self.texture_layout,
171                entries: &[
172                    wgpu::BindGroupEntry {
173                        binding: 0,
174                        resource: wgpu::BindingResource::TextureView(batch.texture.view()),
175                    },
176                    wgpu::BindGroupEntry {
177                        binding: 1,
178                        resource: wgpu::BindingResource::Sampler(batch.texture.sampler()),
179                    },
180                ],
181            });
182
183            pass.set_pipeline(self.pipelines.get(batch.blend_mode));
184            pass.set_bind_group(0, &bg, &[]);
185            pass.set_vertex_buffer(0, buffer.slice(..));
186            pass.draw(0..6, 0..count);
187            draw_calls += 1;
188        }
189
190        (draw_calls, nodes_drawn)
191    }
192}
193
194struct Batch {
195    texture: Texture,
196    blend_mode: BlendMode,
197    instances: Vec<FlexTextInstance>,
198}
199
200fn collect_batches(stage: &Stage, start: NodeId, exclude: &HashSet<NodeId>) -> Vec<Batch> {
201    type Key = (usize, BlendMode);
202    let mut grouped: HashMap<Key, (Texture, BlendMode, Vec<FlexTextInstance>)> = HashMap::new();
203    let mut order: Vec<Key> = Vec::new();
204
205    walk_visible_subtree(stage, start, exclude, |_id, node, world| {
206        if let Node::FlexText(flex) = node {
207            push_flex_text_instance(flex, world, &mut grouped, &mut order);
208        }
209    });
210
211    order
212        .into_iter()
213        .filter_map(|key| {
214            grouped
215                .remove(&key)
216                .map(|(texture, blend_mode, instances)| Batch {
217                    texture,
218                    blend_mode,
219                    instances,
220                })
221        })
222        .collect()
223}
224
225fn push_flex_text_instance(
226    flex: &FlexText,
227    world: Mat4,
228    grouped: &mut HashMap<(usize, BlendMode), (Texture, BlendMode, Vec<FlexTextInstance>)>,
229    order: &mut Vec<(usize, BlendMode)>,
230) {
231    let key = (flex.texture.id(), flex.container.blend_mode);
232    let alpha_tint = with_premultiplied_alpha(flex.tint, flex.container.alpha);
233    let entry = grouped.entry(key).or_insert_with(|| {
234        order.push(key);
235        (flex.texture.clone(), flex.container.blend_mode, Vec::new())
236    });
237    entry.2.push(FlexTextInstance {
238        model: world.to_cols_array_2d(),
239        tint: [alpha_tint.r, alpha_tint.g, alpha_tint.b, alpha_tint.a],
240        anchor: [flex.anchor.x, flex.anchor.y],
241        _padding: [0.0, 0.0],
242    });
243}
244
245fn with_premultiplied_alpha(tint: Color, alpha: f32) -> Color {
246    Color {
247        r: tint.r,
248        g: tint.g,
249        b: tint.b,
250        a: tint.a * alpha,
251    }
252}