1use std::collections::{HashMap, HashSet};
8
9use bytemuck::{Pod, Zeroable};
10use glam::{Mat4, Vec3};
11use wgpu::util::DeviceExt;
12
13use crate::application::Application;
14use crate::blend::BlendMode;
15use crate::render::blend_pipeline::BlendPipelineMap;
16use crate::render::scene_walk::walk_visible_subtree;
17use crate::scene::text::{Font, Text};
18use crate::scene::{Node, NodeId, Stage};
19
20#[repr(C)]
21#[derive(Clone, Copy, Pod, Zeroable)]
22pub(crate) struct TextInstance {
23 pub model: [[f32; 4]; 4],
24 pub color: [f32; 4],
25 pub uv_rect: [f32; 4],
26}
27
28const ATTR_LAYOUT: [wgpu::VertexAttribute; 6] = wgpu::vertex_attr_array![
29 0 => Float32x4,
30 1 => Float32x4,
31 2 => Float32x4,
32 3 => Float32x4,
33 4 => Float32x4,
34 5 => Float32x4,
35];
36
37pub(crate) struct TextPipeline {
38 pipelines: BlendPipelineMap,
39 texture_layout: wgpu::BindGroupLayout,
40}
41
42impl TextPipeline {
43 pub(crate) fn new(app: &Application, output_format: wgpu::TextureFormat) -> Self {
44 let device = app.device();
45 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
46 label: Some("wisp::text"),
47 source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/text.wgsl").into()),
48 });
49
50 let texture_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
51 label: Some("wisp::text atlas layout"),
52 entries: &[
53 wgpu::BindGroupLayoutEntry {
54 binding: 0,
55 visibility: wgpu::ShaderStages::FRAGMENT,
56 ty: wgpu::BindingType::Texture {
57 sample_type: wgpu::TextureSampleType::Float { filterable: true },
58 view_dimension: wgpu::TextureViewDimension::D2,
59 multisampled: false,
60 },
61 count: None,
62 },
63 wgpu::BindGroupLayoutEntry {
64 binding: 1,
65 visibility: wgpu::ShaderStages::FRAGMENT,
66 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
67 count: None,
68 },
69 ],
70 });
71
72 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
73 label: Some("wisp::text pipeline layout"),
74 bind_group_layouts: &[&texture_layout],
75 push_constant_ranges: &[],
76 });
77
78 let pipelines = BlendPipelineMap::new(|blend| {
79 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
80 label: Some("wisp::text pipeline"),
81 layout: Some(&pipeline_layout),
82 vertex: wgpu::VertexState {
83 module: &shader,
84 entry_point: Some("main_vs"),
85 buffers: &[wgpu::VertexBufferLayout {
86 array_stride: std::mem::size_of::<TextInstance>() as wgpu::BufferAddress,
87 step_mode: wgpu::VertexStepMode::Instance,
88 attributes: &ATTR_LAYOUT,
89 }],
90 compilation_options: wgpu::PipelineCompilationOptions::default(),
91 },
92 fragment: Some(wgpu::FragmentState {
93 module: &shader,
94 entry_point: Some("main_fs"),
95 targets: &[Some(wgpu::ColorTargetState {
96 format: output_format,
97 blend: Some(blend),
98 write_mask: wgpu::ColorWrites::ALL,
99 })],
100 compilation_options: wgpu::PipelineCompilationOptions::default(),
101 }),
102 primitive: wgpu::PrimitiveState::default(),
103 depth_stencil: None,
104 multisample: wgpu::MultisampleState::default(),
105 multiview: None,
106 cache: None,
107 })
108 });
109
110 Self {
111 pipelines,
112 texture_layout,
113 }
114 }
115
116 pub(crate) fn draw_stage(
120 &self,
121 app: &Application,
122 pass: &mut wgpu::RenderPass<'_>,
123 stage: &Stage,
124 ) -> (u32, u32) {
125 self.draw_subtree(app, pass, stage, stage.root(), &HashSet::new())
126 }
127
128 pub(crate) fn draw_subtree(
130 &self,
131 app: &Application,
132 pass: &mut wgpu::RenderPass<'_>,
133 stage: &Stage,
134 start: NodeId,
135 exclude: &HashSet<NodeId>,
136 ) -> (u32, u32) {
137 let batches = collect_batches(stage, start, exclude);
138 let mut draw_calls = 0u32;
139 let mut glyphs = 0u32;
140
141 for batch in &batches {
142 if batch.instances.is_empty() {
143 continue;
144 }
145 let count = u32::try_from(batch.instances.len()).expect("glyph count fits in u32");
146 glyphs = glyphs.saturating_add(count);
147 let buffer = app
148 .device()
149 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
150 label: Some("wisp::text instances"),
151 contents: bytemuck::cast_slice(&batch.instances),
152 usage: wgpu::BufferUsages::VERTEX,
153 });
154 let bg = app.device().create_bind_group(&wgpu::BindGroupDescriptor {
155 label: Some("wisp::text atlas bg"),
156 layout: &self.texture_layout,
157 entries: &[
158 wgpu::BindGroupEntry {
159 binding: 0,
160 resource: wgpu::BindingResource::TextureView(batch.atlas.view()),
161 },
162 wgpu::BindGroupEntry {
163 binding: 1,
164 resource: wgpu::BindingResource::Sampler(batch.atlas.sampler()),
165 },
166 ],
167 });
168
169 pass.set_pipeline(self.pipelines.get(batch.blend_mode));
170 pass.set_bind_group(0, &bg, &[]);
171 pass.set_vertex_buffer(0, buffer.slice(..));
172 pass.draw(0..6, 0..count);
173 draw_calls += 1;
174 }
175
176 (draw_calls, glyphs)
177 }
178}
179
180struct Batch {
181 atlas: crate::texture::Texture,
182 blend_mode: BlendMode,
183 instances: Vec<TextInstance>,
184}
185
186fn collect_batches(stage: &Stage, start: NodeId, exclude: &HashSet<NodeId>) -> Vec<Batch> {
187 type Key = (usize, BlendMode);
188 let mut grouped: HashMap<Key, (crate::texture::Texture, BlendMode, Vec<TextInstance>)> =
189 HashMap::new();
190 let mut order: Vec<Key> = Vec::new();
191
192 walk_visible_subtree(stage, start, exclude, |_id, node, world| {
193 let container = node.container();
194 if let Node::Text(text) = node {
195 push_text_glyphs(
196 text,
197 container.blend_mode,
198 world,
199 container.alpha,
200 &mut grouped,
201 &mut order,
202 );
203 }
204 });
205
206 order
207 .into_iter()
208 .filter_map(|key| {
209 grouped
210 .remove(&key)
211 .map(|(atlas, blend_mode, instances)| Batch {
212 atlas,
213 blend_mode,
214 instances,
215 })
216 })
217 .collect()
218}
219
220fn push_text_glyphs(
221 text: &Text,
222 blend_mode: BlendMode,
223 world: Mat4,
224 parent_alpha: f32,
225 grouped: &mut HashMap<
226 (usize, BlendMode),
227 (crate::texture::Texture, BlendMode, Vec<TextInstance>),
228 >,
229 order: &mut Vec<(usize, BlendMode)>,
230) {
231 let key = (text.font.atlas().id(), blend_mode);
232 let atlas = text.font.atlas().clone();
233 let entry = grouped.entry(key).or_insert_with(|| {
234 order.push(key);
235 (atlas, blend_mode, Vec::new())
236 });
237
238 let cell_pixels = f32_from_u32(text.font.cell_pixels());
239 let glyph_size = text.cell_size * cell_pixels;
240 let advance = glyph_size;
241 let line_height = glyph_size * 1.25; let mut cursor_x = 0.0f32;
244 let mut cursor_y = 0.0f32;
245
246 for c in text.content.chars() {
247 if c == '\n' {
248 cursor_x = 0.0;
249 cursor_y -= line_height;
250 continue;
251 }
252
253 let Some(glyph) = font_glyph(&text.font, c) else {
254 cursor_x += advance;
255 continue;
256 };
257
258 let center_x = cursor_x + glyph_size * 0.5;
260 let center_y = cursor_y - glyph_size * 0.5;
261 let model = world
262 * Mat4::from_translation(Vec3::new(center_x, center_y, 0.0))
263 * Mat4::from_scale(Vec3::new(glyph_size * 0.5, glyph_size * 0.5, 1.0));
264
265 entry.2.push(TextInstance {
266 model: model.to_cols_array_2d(),
267 color: [
268 text.color.r,
269 text.color.g,
270 text.color.b,
271 text.color.a * parent_alpha,
272 ],
273 uv_rect: [glyph.u_min, glyph.v_min, glyph.u_max, glyph.v_max],
274 });
275
276 cursor_x += advance;
277 }
278}
279
280fn font_glyph(font: &Font, c: char) -> Option<crate::scene::text::GlyphMetrics> {
281 font.glyph(c)
282}
283
284fn f32_from_u32(v: u32) -> f32 {
285 #[allow(
287 clippy::cast_precision_loss,
288 reason = "atlas cell pixels fit easily in f32 mantissa (8 today, max 1024)"
289 )]
290 {
291 v as f32
292 }
293}