Skip to main content

max / shop

31.1 KB · 840 lines History Blame Raw
1 //! wgpu text pipeline: one instanced draw call per frame.
2
3 use std::collections::HashMap;
4
5 use bytemuck::{Pod, Zeroable};
6 use swash::GlyphId;
7 use wgpu::util::DeviceExt;
8
9 use crate::{
10 atlas::{AtlasSlot, GlyphAtlas},
11 shaper::{FontId, PRIMARY, Shaper},
12 };
13
14 /// One character with its cell-anchored position and color.
15 #[derive(Debug, Clone, Copy)]
16 pub struct CellDraw {
17 pub c: char,
18 /// Top-left of the cell, in pixels.
19 pub x: f32,
20 pub y: f32,
21 pub color: [f32; 4],
22 }
23
24 /// Solid-color rectangle. Used for cell backgrounds and the cursor.
25 #[derive(Debug, Clone, Copy)]
26 pub struct BgFill {
27 pub x: f32,
28 pub y: f32,
29 pub w: f32,
30 pub h: f32,
31 pub color: [f32; 4],
32 }
33
34 const ATLAS_SIZE: u32 = 1024;
35
36 #[repr(C)]
37 #[derive(Clone, Copy, Pod, Zeroable)]
38 struct Instance {
39 pos: [f32; 2],
40 size: [f32; 2],
41 uv_min: [f32; 2],
42 uv_max: [f32; 2],
43 color: [f32; 4],
44 }
45
46 #[repr(C)]
47 #[derive(Clone, Copy, Pod, Zeroable)]
48 struct Uniforms {
49 screen: [f32; 2],
50 _pad: [f32; 2],
51 }
52
53 const QUAD: [[f32; 2]; 4] = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
54 const INDICES: [u16; 6] = [0, 1, 2, 2, 1, 3];
55
56 const SHADER: &str = r"
57 struct Uniforms {
58 screen: vec2<f32>,
59 _pad: vec2<f32>,
60 };
61
62 @group(0) @binding(0) var<uniform> u: Uniforms;
63 @group(0) @binding(1) var atlas: texture_2d<f32>;
64 @group(0) @binding(2) var samp: sampler;
65
66 struct VOut {
67 @builtin(position) pos: vec4<f32>,
68 @location(0) uv: vec2<f32>,
69 @location(1) color: vec4<f32>,
70 };
71
72 @vertex
73 fn vs(
74 @location(0) corner: vec2<f32>,
75 @location(1) inst_pos: vec2<f32>,
76 @location(2) inst_size: vec2<f32>,
77 @location(3) uv_min: vec2<f32>,
78 @location(4) uv_max: vec2<f32>,
79 @location(5) color: vec4<f32>,
80 ) -> VOut {
81 let px = inst_pos + corner * inst_size;
82 let ndc = vec2<f32>(
83 (px.x / u.screen.x) * 2.0 - 1.0,
84 1.0 - (px.y / u.screen.y) * 2.0,
85 );
86 var out: VOut;
87 out.pos = vec4<f32>(ndc, 0.0, 1.0);
88 out.uv = mix(uv_min, uv_max, corner);
89 out.color = color;
90 return out;
91 }
92
93 @fragment
94 fn fs(in: VOut) -> @location(0) vec4<f32> {
95 let a = textureSample(atlas, samp, in.uv).r;
96 return vec4<f32>(in.color.rgb, in.color.a * a);
97 }
98 ";
99
100 pub struct TextRenderer {
101 pipeline: wgpu::RenderPipeline,
102 bind_group: wgpu::BindGroup,
103 uniforms_buf: wgpu::Buffer,
104 quad_buf: wgpu::Buffer,
105 index_buf: wgpu::Buffer,
106 instance_buf: wgpu::Buffer,
107 instance_cap: u64,
108 atlas: GlyphAtlas,
109 shaper: Shaper,
110 /// Keyed by (font, glyph). A glyph id is only unique inside the font that
111 /// issued it, so the moment a fallback face joins the set, keying on the id
112 /// alone would serve one font's glyph from another's atlas slot.
113 cache: HashMap<(FontId, GlyphId), Option<CachedGlyph>>,
114 /// Per-row cache of "cheap-to-recompute" instance data. Positions get
115 /// computed at draw time from row_idx + cell_advance, so scroll can
116 /// just rotate this vec.
117 row_cache: Vec<Vec<CachedCell>>,
118 }
119
120 #[derive(Clone, Copy)]
121 struct CachedGlyph {
122 slot: AtlasSlot,
123 left: i32,
124 top: i32,
125 }
126
127 /// Per-glyph metadata cached for a live cell. Position is NOT stored — it's
128 /// derived from (row_idx, col, scale, pad) at draw time so the same cache
129 /// entry stays valid across scroll rotations and doesn't need re-computing
130 /// on scale changes.
131 #[derive(Clone, Copy)]
132 struct CachedCell {
133 col: u16,
134 slot: AtlasSlot,
135 left: i32,
136 top: i32,
137 /// Offset within the cell, in pixels. Zero for the one glyph an ordinary
138 /// cell draws; non-zero only for the marks of a cluster, which the shaper
139 /// positions against their base rather than against the cell.
140 dx: f32,
141 dy: f32,
142 color: [f32; 4],
143 }
144
145 /// What one cell draws.
146 ///
147 /// Almost always a single character looked up in the font's charmap, which is
148 /// the path worth keeping cheap. A cell carrying combining marks is shaped
149 /// instead, because where a mark sits over its base is the font's business and
150 /// GPOS is how it says so.
151 pub enum CellText {
152 Char(char),
153 Cluster(String),
154 }
155
156 impl TextRenderer {
157 pub fn new(
158 device: &wgpu::Device,
159 queue: &wgpu::Queue,
160 format: wgpu::TextureFormat,
161 font_data: Vec<u8>,
162 font_px: f32,
163 // The grid's step, in physical pixels. Furniture is snapped to it, so
164 // it has to be the cell the caller actually lays out with rather than
165 // one this constructor measures for itself.
166 cell: crate::CellMetrics,
167 // Where on the face's weight axis to draw. The bundled face is variable
168 // and its own default is the light end of the axis, so this is named
169 // rather than taken from the file.
170 weight: f32,
171 ) -> anyhow::Result<Self> {
172 let atlas = GlyphAtlas::new(device, queue, ATLAS_SIZE);
173 let shaper = Shaper::new(font_data, font_px, cell, weight)?;
174
175 let uniforms_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
176 label: Some("shop.text.uniforms"),
177 contents: bytemuck::bytes_of(&Uniforms {
178 screen: [1.0, 1.0],
179 _pad: [0.0, 0.0],
180 }),
181 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
182 });
183 let quad_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
184 label: Some("shop.text.quad"),
185 contents: bytemuck::cast_slice(&QUAD),
186 usage: wgpu::BufferUsages::VERTEX,
187 });
188 let index_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
189 label: Some("shop.text.index"),
190 contents: bytemuck::cast_slice(&INDICES),
191 usage: wgpu::BufferUsages::INDEX,
192 });
193 let instance_cap: u64 = 256;
194 let instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
195 label: Some("shop.text.instances"),
196 size: instance_cap * std::mem::size_of::<Instance>() as u64,
197 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
198 mapped_at_creation: false,
199 });
200
201 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
202 label: Some("shop.text.sampler"),
203 mag_filter: wgpu::FilterMode::Linear,
204 min_filter: wgpu::FilterMode::Linear,
205 ..Default::default()
206 });
207
208 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
209 label: Some("shop.text.bgl"),
210 entries: &[
211 wgpu::BindGroupLayoutEntry {
212 binding: 0,
213 visibility: wgpu::ShaderStages::VERTEX,
214 ty: wgpu::BindingType::Buffer {
215 ty: wgpu::BufferBindingType::Uniform,
216 has_dynamic_offset: false,
217 min_binding_size: None,
218 },
219 count: None,
220 },
221 wgpu::BindGroupLayoutEntry {
222 binding: 1,
223 visibility: wgpu::ShaderStages::FRAGMENT,
224 ty: wgpu::BindingType::Texture {
225 sample_type: wgpu::TextureSampleType::Float { filterable: true },
226 view_dimension: wgpu::TextureViewDimension::D2,
227 multisampled: false,
228 },
229 count: None,
230 },
231 wgpu::BindGroupLayoutEntry {
232 binding: 2,
233 visibility: wgpu::ShaderStages::FRAGMENT,
234 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
235 count: None,
236 },
237 ],
238 });
239
240 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
241 label: Some("shop.text.bg"),
242 layout: &bind_group_layout,
243 entries: &[
244 wgpu::BindGroupEntry {
245 binding: 0,
246 resource: uniforms_buf.as_entire_binding(),
247 },
248 wgpu::BindGroupEntry {
249 binding: 1,
250 resource: wgpu::BindingResource::TextureView(&atlas.view),
251 },
252 wgpu::BindGroupEntry {
253 binding: 2,
254 resource: wgpu::BindingResource::Sampler(&sampler),
255 },
256 ],
257 });
258
259 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
260 label: Some("shop.text.shader"),
261 source: wgpu::ShaderSource::Wgsl(SHADER.into()),
262 });
263 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
264 label: Some("shop.text.pl"),
265 bind_group_layouts: &[Some(&bind_group_layout)],
266 immediate_size: 0,
267 });
268 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
269 label: Some("shop.text.pipeline"),
270 layout: Some(&pipeline_layout),
271 vertex: wgpu::VertexState {
272 module: &module,
273 entry_point: Some("vs"),
274 compilation_options: wgpu::PipelineCompilationOptions::default(),
275 buffers: &[
276 Some(wgpu::VertexBufferLayout {
277 array_stride: 8,
278 step_mode: wgpu::VertexStepMode::Vertex,
279 attributes: &wgpu::vertex_attr_array![0 => Float32x2],
280 }),
281 Some(wgpu::VertexBufferLayout {
282 array_stride: std::mem::size_of::<Instance>() as u64,
283 step_mode: wgpu::VertexStepMode::Instance,
284 attributes: &wgpu::vertex_attr_array![
285 1 => Float32x2,
286 2 => Float32x2,
287 3 => Float32x2,
288 4 => Float32x2,
289 5 => Float32x4,
290 ],
291 }),
292 ],
293 },
294 fragment: Some(wgpu::FragmentState {
295 module: &module,
296 entry_point: Some("fs"),
297 compilation_options: wgpu::PipelineCompilationOptions::default(),
298 targets: &[Some(wgpu::ColorTargetState {
299 format,
300 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
301 write_mask: wgpu::ColorWrites::ALL,
302 })],
303 }),
304 primitive: wgpu::PrimitiveState::default(),
305 depth_stencil: None,
306 multisample: wgpu::MultisampleState::default(),
307 multiview_mask: None,
308 cache: None,
309 });
310
311 Ok(Self {
312 pipeline,
313 bind_group,
314 uniforms_buf,
315 quad_buf,
316 index_buf,
317 instance_buf,
318 instance_cap,
319 atlas,
320 shaper,
321 cache: HashMap::new(),
322 row_cache: Vec::new(),
323 })
324 }
325
326 /// Ensure the per-row cache has `rows` slots.
327 pub fn ensure_rows(&mut self, rows: u16) {
328 self.row_cache.resize(rows as usize, Vec::new());
329 }
330
331 /// Wipe the per-row cache — alt-screen swap or full grid resize.
332 pub fn clear_rows(&mut self) {
333 for row in &mut self.row_cache {
334 row.clear();
335 }
336 }
337
338 /// Rotate the per-row cache to reflect a fullscreen scroll. Positive =
339 /// scrolled up (contents move up N rows, blank appears at bottom).
340 pub fn scroll(&mut self, delta: i16) {
341 if delta == 0 || self.row_cache.is_empty() {
342 return;
343 }
344 let n = delta.unsigned_abs() as usize;
345 let n = n.min(self.row_cache.len());
346 if delta > 0 {
347 self.row_cache.rotate_left(n);
348 } else {
349 self.row_cache.rotate_right(n);
350 }
351 }
352
353 /// Replace one row's cached glyph batch. Blanks pre-filtered by caller.
354 pub fn update_row(
355 &mut self,
356 queue: &wgpu::Queue,
357 row: u16,
358 cells: impl IntoIterator<Item = (u16, CellText, [f32; 4])>,
359 ) {
360 let Some(slot) = self.row_cache.get_mut(row as usize) else {
361 return;
362 };
363 slot.clear();
364 let atlas = &mut self.atlas;
365 let cache = &mut self.cache;
366 let shaper = &mut self.shaper;
367 // One glyph id to an atlas slot, memoized. The id is what the cache is
368 // keyed on either way, so a mark rasterized for one base is reused for
369 // every other base it ever sits on.
370 let mut push = |slot: &mut Vec<CachedCell>,
371 shaper: &mut Shaper,
372 font: FontId,
373 id,
374 col,
375 dx: f32,
376 dy: f32,
377 color| {
378 let cached = match cache.get(&(font, id)).copied() {
379 Some(v) => v,
380 None => {
381 let entry = shaper.rasterize(font, id).and_then(|r| {
382 atlas
383 .upload(queue, r.width, r.height, &r.bitmap)
384 .map(|s| CachedGlyph {
385 slot: s,
386 left: r.placement_left,
387 top: r.placement_top,
388 })
389 });
390 cache.insert((font, id), entry);
391 entry
392 }
393 };
394 if let Some(g) = cached
395 && g.slot.px[0] > 0
396 && g.slot.px[1] > 0
397 {
398 slot.push(CachedCell {
399 col,
400 slot: g.slot,
401 left: g.left,
402 top: g.top,
403 dx,
404 dy,
405 color,
406 });
407 }
408 };
409 for (col, text, color) in cells {
410 match text {
411 CellText::Char(c) => {
412 let (font, id) = shaper.glyph_id_for(c);
413 push(slot, shaper, font, id, col, 0.0, 0.0, color);
414 }
415 CellText::Cluster(s) => {
416 // The pen DOES advance across the cluster, and the marks
417 // still land on the base. Measured on the face bundled at
418 // the time, IosevkaTerm, and it is a property of how fonts
419 // are built rather than of that face:
420 // a combining glyph carries a full cell of advance and its
421 // ink hangs off the LEFT of its origin (acute -5px for a
422 // 3px mark, diaeresis -7px for a 6px one). The font is
423 // built for a pen that has already stepped past the base,
424 // and the negative bearing is what pulls the mark back
425 // over it, centred.
426 //
427 // Anchoring every glyph at the cell origin instead — which
428 // looks right, and which this did briefly — draws each mark
429 // a whole cell to the LEFT, over the previous character.
430 // Do not "fix" it back without looking at a window first.
431 //
432 // The two other shapes this can take are handled by the
433 // same line: where the shaper composes the pair into one
434 // precomposed glyph (Quasi Mono maps U+00E9 to a glyph of
435 // its own) there is
436 // nothing after the base to place, and where a font
437 // positions marks by GPOS the mark's advance is zero and
438 // its offsets do the work.
439 //
440 // Shaped against the font that has the BASE. A cluster is
441 // one character plus marks on it, so it belongs to one
442 // font; shaping it against the bundled font when the base
443 // came from a fallback would look every glyph up in the
444 // wrong charmap and return notdef for the lot.
445 let base = s.chars().next().unwrap_or(' ');
446 let (font, _) = shaper.glyph_id_for(base);
447 let mut pen_x = 0.0;
448 for g in shaper.shape(font, &s) {
449 push(
450 slot,
451 shaper,
452 font,
453 g.id,
454 col,
455 pen_x + g.x_offset,
456 g.y_offset,
457 color,
458 );
459 pen_x += g.advance;
460 }
461 }
462 }
463 }
464 }
465
466 /// Draw one frame from cached per-row glyphs + caller-supplied fills
467 /// (bg, underline, cursor). Fills are cheap to fully rebuild each frame.
468 // wgpu handles plus the frame geometry. Grouping them into a struct would
469 // add a type that exists only to satisfy the lint.
470 #[allow(clippy::too_many_arguments)]
471 pub fn draw_cached(
472 &mut self,
473 device: &wgpu::Device,
474 queue: &wgpu::Queue,
475 view: &wgpu::TextureView,
476 encoder: &mut wgpu::CommandEncoder,
477 fills: &[BgFill],
478 pad_x: f32,
479 pad_y: f32,
480 cell_w: f32,
481 cell_h: f32,
482 ) {
483 let baseline = self.shaper.baseline();
484 let glyph_count: usize = self.row_cache.iter().map(Vec::len).sum();
485 let mut instances: Vec<Instance> = Vec::with_capacity(fills.len() + glyph_count);
486 let solid_uv = self.atlas.solid_uv;
487
488 for fill in fills {
489 instances.push(Instance {
490 pos: [fill.x, fill.y],
491 size: [fill.w, fill.h],
492 uv_min: solid_uv,
493 uv_max: solid_uv,
494 color: fill.color,
495 });
496 }
497
498 for (row_idx, row) in self.row_cache.iter().enumerate() {
499 let row_y = pad_y + row_idx as f32 * cell_h;
500 for cell in row {
501 let cell_x = pad_x + cell.col as f32 * cell_w;
502 instances.push(Instance {
503 pos: [
504 cell_x + cell.left as f32 + cell.dx,
505 row_y + baseline - cell.top as f32 - cell.dy,
506 ],
507 size: [cell.slot.px[0] as f32, cell.slot.px[1] as f32],
508 uv_min: cell.slot.uv_min,
509 uv_max: cell.slot.uv_max,
510 color: cell.color,
511 });
512 }
513 }
514
515 if instances.is_empty() {
516 return;
517 }
518 let needed = instances.len() as u64;
519 if needed > self.instance_cap {
520 let new_cap = needed.next_power_of_two();
521 self.instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
522 label: Some("shop.text.instances"),
523 size: new_cap * std::mem::size_of::<Instance>() as u64,
524 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
525 mapped_at_creation: false,
526 });
527 self.instance_cap = new_cap;
528 }
529 queue.write_buffer(&self.instance_buf, 0, bytemuck::cast_slice(&instances));
530
531 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
532 label: Some("shop.text.cached"),
533 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
534 view,
535 resolve_target: None,
536 ops: wgpu::Operations {
537 load: wgpu::LoadOp::Load,
538 store: wgpu::StoreOp::Store,
539 },
540 depth_slice: None,
541 })],
542 ..Default::default()
543 });
544 pass.set_pipeline(&self.pipeline);
545 pass.set_bind_group(0, &self.bind_group, &[]);
546 pass.set_vertex_buffer(0, self.quad_buf.slice(..));
547 pass.set_vertex_buffer(1, self.instance_buf.slice(..));
548 pass.set_index_buffer(self.index_buf.slice(..), wgpu::IndexFormat::Uint16);
549 pass.draw_indexed(0..6, 0, 0..instances.len() as u32);
550 }
551
552 /// Draw a frame: solid-color fills first (bg + cursor), glyph cells second.
553 /// All emitted as a single instanced draw call using the atlas's reserved
554 /// solid-white block for the fill quads.
555 pub fn draw_frame(
556 &mut self,
557 device: &wgpu::Device,
558 queue: &wgpu::Queue,
559 view: &wgpu::TextureView,
560 encoder: &mut wgpu::CommandEncoder,
561 fills: &[BgFill],
562 cells: &[CellDraw],
563 ) {
564 let baseline = self.shaper.baseline();
565 let solid_uv = self.atlas.solid_uv;
566 let mut instances: Vec<Instance> = Vec::with_capacity(fills.len() + cells.len());
567
568 // Fills go first so alpha blending stacks glyphs on top.
569 for fill in fills {
570 instances.push(Instance {
571 pos: [fill.x, fill.y],
572 size: [fill.w, fill.h],
573 uv_min: solid_uv,
574 uv_max: solid_uv,
575 color: fill.color,
576 });
577 }
578
579 for cell in cells {
580 let (font, glyph_id) = self.shaper.glyph_id_for(cell.c);
581 let cached = match self.cache.get(&(font, glyph_id)).copied() {
582 Some(v) => v,
583 None => {
584 let entry = self.shaper.rasterize(font, glyph_id).and_then(|r| {
585 self.atlas
586 .upload(queue, r.width, r.height, &r.bitmap)
587 .map(|slot| CachedGlyph {
588 slot,
589 left: r.placement_left,
590 top: r.placement_top,
591 })
592 });
593 self.cache.insert((font, glyph_id), entry);
594 entry
595 }
596 };
597 if let Some(c) = cached
598 && c.slot.px[0] > 0
599 && c.slot.px[1] > 0
600 {
601 instances.push(Instance {
602 pos: [cell.x + c.left as f32, cell.y + baseline - c.top as f32],
603 size: [c.slot.px[0] as f32, c.slot.px[1] as f32],
604 uv_min: c.slot.uv_min,
605 uv_max: c.slot.uv_max,
606 color: cell.color,
607 });
608 }
609 }
610
611 if instances.is_empty() {
612 return;
613 }
614
615 let needed = instances.len() as u64;
616 if needed > self.instance_cap {
617 let new_cap = needed.next_power_of_two();
618 self.instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
619 label: Some("shop.text.instances"),
620 size: new_cap * std::mem::size_of::<Instance>() as u64,
621 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
622 mapped_at_creation: false,
623 });
624 self.instance_cap = new_cap;
625 }
626 queue.write_buffer(&self.instance_buf, 0, bytemuck::cast_slice(&instances));
627
628 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
629 label: Some("shop.text.frame"),
630 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
631 view,
632 resolve_target: None,
633 ops: wgpu::Operations {
634 load: wgpu::LoadOp::Load,
635 store: wgpu::StoreOp::Store,
636 },
637 depth_slice: None,
638 })],
639 ..Default::default()
640 });
641 pass.set_pipeline(&self.pipeline);
642 pass.set_bind_group(0, &self.bind_group, &[]);
643 pass.set_vertex_buffer(0, self.quad_buf.slice(..));
644 pass.set_vertex_buffer(1, self.instance_buf.slice(..));
645 pass.set_index_buffer(self.index_buf.slice(..), wgpu::IndexFormat::Uint16);
646 pass.draw_indexed(0..6, 0, 0..instances.len() as u32);
647 }
648
649 /// Draw a batch of cell-anchored characters in one instanced draw call.
650 /// Uses the font's charmap for glyph lookup (no shaping), so ligatures do
651 /// not activate and per-glyph advance is ignored — each character sits at
652 /// its passed-in cell coordinate. Missing glyphs are silently skipped.
653 pub fn draw_cells(
654 &mut self,
655 device: &wgpu::Device,
656 queue: &wgpu::Queue,
657 view: &wgpu::TextureView,
658 encoder: &mut wgpu::CommandEncoder,
659 cells: &[CellDraw],
660 ) {
661 let baseline = self.shaper.baseline();
662 let mut instances: Vec<Instance> = Vec::with_capacity(cells.len());
663
664 for cell in cells {
665 let (font, glyph_id) = self.shaper.glyph_id_for(cell.c);
666 let cached = match self.cache.get(&(font, glyph_id)).copied() {
667 Some(v) => v,
668 None => {
669 let entry = self.shaper.rasterize(font, glyph_id).and_then(|r| {
670 self.atlas
671 .upload(queue, r.width, r.height, &r.bitmap)
672 .map(|slot| CachedGlyph {
673 slot,
674 left: r.placement_left,
675 top: r.placement_top,
676 })
677 });
678 self.cache.insert((font, glyph_id), entry);
679 entry
680 }
681 };
682 if let Some(c) = cached
683 && c.slot.px[0] > 0
684 && c.slot.px[1] > 0
685 {
686 let gx = cell.x + c.left as f32;
687 let gy = cell.y + baseline - c.top as f32;
688 instances.push(Instance {
689 pos: [gx, gy],
690 size: [c.slot.px[0] as f32, c.slot.px[1] as f32],
691 uv_min: c.slot.uv_min,
692 uv_max: c.slot.uv_max,
693 color: cell.color,
694 });
695 }
696 }
697
698 if instances.is_empty() {
699 return;
700 }
701
702 let needed = instances.len() as u64;
703 if needed > self.instance_cap {
704 let new_cap = needed.next_power_of_two();
705 self.instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
706 label: Some("shop.text.instances"),
707 size: new_cap * std::mem::size_of::<Instance>() as u64,
708 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
709 mapped_at_creation: false,
710 });
711 self.instance_cap = new_cap;
712 }
713 queue.write_buffer(&self.instance_buf, 0, bytemuck::cast_slice(&instances));
714
715 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
716 label: Some("shop.text.cells"),
717 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
718 view,
719 resolve_target: None,
720 ops: wgpu::Operations {
721 load: wgpu::LoadOp::Load,
722 store: wgpu::StoreOp::Store,
723 },
724 depth_slice: None,
725 })],
726 ..Default::default()
727 });
728 pass.set_pipeline(&self.pipeline);
729 pass.set_bind_group(0, &self.bind_group, &[]);
730 pass.set_vertex_buffer(0, self.quad_buf.slice(..));
731 pass.set_vertex_buffer(1, self.instance_buf.slice(..));
732 pass.set_index_buffer(self.index_buf.slice(..), wgpu::IndexFormat::Uint16);
733 pass.draw_indexed(0..6, 0, 0..instances.len() as u32);
734 }
735
736 pub fn resize(&self, queue: &wgpu::Queue, width: u32, height: u32) {
737 let u = Uniforms {
738 screen: [width as f32, height as f32],
739 _pad: [0.0, 0.0],
740 };
741 queue.write_buffer(&self.uniforms_buf, 0, bytemuck::bytes_of(&u));
742 }
743
744 /// Shape `text`, rasterize any missing glyphs, and draw the resulting
745 /// quads. `x`/`y` is the pen origin in pixels (top-left convention, with
746 /// the baseline placed at `y + baseline`).
747 // wgpu handles plus pen origin and colour. See `draw_cached`.
748 #[allow(clippy::too_many_arguments)]
749 pub fn draw(
750 &mut self,
751 device: &wgpu::Device,
752 queue: &wgpu::Queue,
753 view: &wgpu::TextureView,
754 encoder: &mut wgpu::CommandEncoder,
755 text: &str,
756 x: f32,
757 y: f32,
758 color: [f32; 4],
759 ) {
760 let baseline = self.shaper.baseline();
761 let baseline_y = y + baseline;
762 // Chrome text, not grid text: drawn from the bundled font, which is
763 // the one whose metrics the surrounding layout is built on.
764 let glyphs = self.shaper.shape(PRIMARY, text);
765 let mut pen_x = x;
766 let mut instances: Vec<Instance> = Vec::with_capacity(glyphs.len());
767
768 for g in glyphs {
769 let cached = match self.cache.get(&(PRIMARY, g.id)).copied() {
770 Some(v) => v,
771 None => {
772 let entry = self.shaper.rasterize(PRIMARY, g.id).and_then(|r| {
773 self.atlas
774 .upload(queue, r.width, r.height, &r.bitmap)
775 .map(|slot| CachedGlyph {
776 slot,
777 left: r.placement_left,
778 top: r.placement_top,
779 })
780 });
781 self.cache.insert((PRIMARY, g.id), entry);
782 entry
783 }
784 };
785 if let Some(c) = cached
786 && c.slot.px[0] > 0
787 && c.slot.px[1] > 0
788 {
789 let gx = pen_x + g.x_offset + c.left as f32;
790 let gy = baseline_y - g.y_offset - c.top as f32;
791 instances.push(Instance {
792 pos: [gx, gy],
793 size: [c.slot.px[0] as f32, c.slot.px[1] as f32],
794 uv_min: c.slot.uv_min,
795 uv_max: c.slot.uv_max,
796 color,
797 });
798 }
799 pen_x += g.advance;
800 }
801
802 if instances.is_empty() {
803 return;
804 }
805
806 let needed = instances.len() as u64;
807 if needed > self.instance_cap {
808 let new_cap = needed.next_power_of_two();
809 self.instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
810 label: Some("shop.text.instances"),
811 size: new_cap * std::mem::size_of::<Instance>() as u64,
812 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
813 mapped_at_creation: false,
814 });
815 self.instance_cap = new_cap;
816 }
817 queue.write_buffer(&self.instance_buf, 0, bytemuck::cast_slice(&instances));
818
819 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
820 label: Some("shop.text.pass"),
821 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
822 view,
823 resolve_target: None,
824 ops: wgpu::Operations {
825 load: wgpu::LoadOp::Load,
826 store: wgpu::StoreOp::Store,
827 },
828 depth_slice: None,
829 })],
830 ..Default::default()
831 });
832 pass.set_pipeline(&self.pipeline);
833 pass.set_bind_group(0, &self.bind_group, &[]);
834 pass.set_vertex_buffer(0, self.quad_buf.slice(..));
835 pass.set_vertex_buffer(1, self.instance_buf.slice(..));
836 pass.set_index_buffer(self.index_buf.slice(..), wgpu::IndexFormat::Uint16);
837 pass.draw_indexed(0..6, 0, 0..instances.len() as u32);
838 }
839 }
840