authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-25 11:46:41-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-25 20:26:21-07:00
log939081be8a62e17d4e2ec4abb68c052f8e3c506d
tree41bced825034984b492780c823359f7039912725
parent1b0d6e5c862e65471df75c1239a1a111b9c11302
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

refactor: move the renderer out of canvas into a draw crate

Page and application chrome will paint through one renderer, atlas and image cache, while an iOS host embeds the canvas without the desktop UI kit. draw paints layers (transform, device clip, primitives); text arrives as glyph runs through the Glyphs trait, so canvas keeps its Windows-metric line boxes and the renderer never measures text. Tag artwork becomes a generic SVG icon primitive. canvas without the gpu feature stays free of wgpu for the fuzz targets. All 23 corpus pages render pixel-identical to the previous renderer. Assisted-by: claude-opus-5.5

19 files changed, 2471 insertions(+), 1883 deletions(-)

Cargo.lock+16-4
......@@ -395,17 +395,14 @@ version = "0.1.0"
395395dependencies = [
396396 "accesskit",
397397 "accesskit_consumer",
398 "bytemuck",
399 "image",
398 "draw",
400399 "onestore",
401400 "parley",
402401 "png",
403402 "pollster",
404 "roxmltree",
405403 "serde",
406404 "serde_json",
407405 "skrifa",
408 "swash",
409406 "wgpu",
410407]
411408
......@@ -724,6 +721,20 @@ version = "0.1.2"
724721source = "registry+https://github.com/rust-lang/crates.io-index"
725722checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
726723
724[[package]]
725name = "draw"
726version = "0.1.0"
727dependencies = [
728 "bytemuck",
729 "image",
730 "parley",
731 "png",
732 "pollster",
733 "roxmltree",
734 "swash",
735 "wgpu",
736]
737
727738[[package]]
728739name = "equivalent"
729740version = "1.0.2"
......@@ -2632,6 +2643,7 @@ dependencies = [
26322643 "accesskit_winit",
26332644 "arboard",
26342645 "canvas",
2646 "draw",
26352647 "notebook",
26362648 "objc2 0.5.2",
26372649 "objc2-app-kit 0.2.2",
crates/canvas/Cargo.toml+3-6
......@@ -5,7 +5,7 @@ edition = "2024"
55publish = false
66
77[features]
8gpu = ["dep:bytemuck", "dep:swash", "dep:wgpu", "dep:image", "dep:roxmltree"]
8gpu = ["dep:draw"]
99# How a page responds to input and presents itself to assistive technology.
1010interaction = ["gpu", "dep:accesskit"]
1111
......@@ -16,11 +16,7 @@ serde = { version = "1.0.229", features = ["derive"] }
1616serde_json = "1.0.151"
1717skrifa = "0.44.0"
1818
19bytemuck = { version = "1.25", features = ["derive"], optional = true }
20swash = { version = "0.2.10", optional = true }
21wgpu = { workspace = true, optional = true }
22image = { version = "0.25", default-features = false, features = ["gif", "jpeg", "png"], optional = true }
23roxmltree = { version = "0.21", optional = true }
19draw = { path = "../draw", optional = true }
2420accesskit = { version = "0.25.0", optional = true }
2521
2622[[bin]]
......@@ -35,6 +31,7 @@ path = "src/bin/page_probe.rs"
3531accesskit_consumer = "0.39.0"
3632pollster = "0.4"
3733png = "0.18"
34wgpu.workspace = true
3835
3936[[example]]
4037name = "render_page"
crates/canvas/examples/render_page.rs+13-10
......@@ -1,5 +1,6 @@
1use canvas::gpu::{Renderer, Viewport, page::PageScene};
1use canvas::gpu::{Viewport, page::PageScene};
22use canvas::layout::TextEngine;
3use draw::Renderer;
34use onestore::page::Page;
45use onestore::{RevisionIndex, Store, document::Document};
56use std::{env, fs, sync::Arc, time::Duration};
......@@ -67,18 +68,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
6768 mapped_at_creation: false,
6869 });
6970 let mut renderer = Renderer::new(device, queue, format);
71 let viewport = Viewport {
72 size,
73 scale: 96.0 / 72.0,
74 origin: [
75 (36.0 - margin_origin[0]) * (96.0 / 72.0),
76 (14.4 - margin_origin[1]) * (96.0 / 72.0) + origin_y,
77 ],
78 };
7079 renderer
7180 .draw(
7281 &target.create_view(&Default::default()),
73 Viewport {
74 size,
75 scale: 96.0 / 72.0,
76 origin: [
77 (36.0 - margin_origin[0]) * (96.0 / 72.0),
78 (14.4 - margin_origin[1]) * (96.0 / 72.0) + origin_y,
79 ],
80 },
81 &primitives,
82 size,
83 [1.0; 4],
84 &[viewport.layer(&primitives)],
8285 )
8386 .map_err(|e| format!("Page comparison rendering failed: {e:?}"))?;
8487 let mut encoder = renderer.device.create_command_encoder(&Default::default());
crates/canvas/src/gpu/canvas.wgsl deleted-87
......@@ -1,87 +0,0 @@
1struct Vertex {
2 @builtin(position) position: vec4<f32>,
3 @location(0) uv: vec2<f32>,
4 @location(1) color: vec4<f32>,
5 @location(2) local: vec2<f32>,
6 @location(3) @interpolate(flat) shape: vec4<f32>,
7 @location(4) @interpolate(flat) stroke: f32,
8}
9
10@group(0) @binding(0) var atlas: texture_2d<f32>;
11@group(0) @binding(1) var atlas_sampler: sampler;
12
13@vertex
14fn vertex(@location(0) position: vec2<f32>, @location(1) uv: vec2<f32>,
15 @location(2) color: vec4<f32>, @location(3) local: vec2<f32>,
16 @location(4) shape: vec4<f32>, @location(5) stroke: f32) -> Vertex {
17 return Vertex(vec4<f32>(position, 0.0, 1.0), uv, color, local, shape, stroke);
18}
19
20fn ellipse_arc(angle: f32, radius: vec2<f32>) -> f32 {
21 if radius.x == radius.y {
22 return angle * radius.x;
23 }
24 // Four-point Gauss-Legendre quadrature.
25 let nodes = vec4<f32>(-0.86113631, -0.33998104, 0.33998104, 0.86113631);
26 let weights = vec4<f32>(0.34785485, 0.65214515, 0.65214515, 0.34785485);
27 let half = angle * 0.5;
28 let scale = max(radius.x, radius.y);
29 var sum = 0.0;
30 for (var i = 0u; i < 4u; i += 1u) {
31 let t = half * (nodes[i] + 1.0);
32 sum += weights[i] * length((radius / scale) * vec2<f32>(sin(t), cos(t)));
33 }
34 return half * sum * scale;
35}
36
37@fragment
38fn fragment(input: Vertex) -> @location(0) vec4<f32> {
39 var color = textureSample(atlas, atlas_sampler, input.uv) * input.color;
40 if input.shape.x > 0.0 && input.shape.y > 0.0 {
41 let q = abs(input.local) - input.shape.xy + input.shape.zw;
42 var distance: f32;
43 if input.shape.z == input.shape.w {
44 distance = length(max(q, vec2<f32>(0.0))) + min(max(q.x, q.y), 0.0) - input.shape.z;
45 } else if all(q > vec2<f32>(0.0)) {
46 let radius = max(input.shape.zw, vec2<f32>(0.0001));
47 let normalized = q / radius;
48 let k0 = length(normalized);
49 distance = k0 * (k0 - 1.0) / max(length(normalized / radius), 1e-20);
50 } else {
51 let edge = abs(input.local) - input.shape.xy;
52 distance = max(edge.x, edge.y);
53 }
54 var coverage = clamp(0.5 - distance, 0.0, 1.0);
55 let width = abs(input.stroke);
56 if width > 0.0 {
57 coverage *= clamp(distance + width + 0.5, 0.0, 1.0);
58 }
59 if input.stroke < 0.0 {
60 var radius = max(input.shape.zw - width * 0.5, vec2<f32>(0.0));
61 if any(radius == vec2<f32>(0.0)) {
62 radius = vec2<f32>(0.0);
63 }
64 let straight = input.shape.xy - width * 0.5 - radius;
65 let p = abs(input.local);
66 let quarter = straight.x + straight.y + ellipse_arc(1.57079632679, radius);
67 var along: f32;
68 if p.y <= straight.y {
69 along = p.y;
70 } else if p.x <= straight.x || radius.x == 0.0 {
71 along = quarter - p.x;
72 } else {
73 let angle = atan2((p.y - straight.y) / radius.y, (p.x - straight.x) / radius.x);
74 along = straight.y + ellipse_arc(angle, radius);
75 }
76 if input.local.x < 0.0 {
77 along = select(2.0 * quarter - along, 2.0 * quarter + along, input.local.y < 0.0);
78 } else if input.local.y < 0.0 {
79 along = 4.0 * quarter - along;
80 }
81 let phase = fract(along / (4.0 * width)) * (4.0 * width);
82 coverage *= clamp(width + 0.5 - abs(phase - 2.0 * width), 0.0, 1.0);
83 }
84 color *= coverage;
85 }
86 return color;
87}
crates/canvas/src/gpu/mod.rs+144-1562
......@@ -1,150 +1,13 @@
11pub mod page;
22#[cfg(test)]
33mod profile;
4mod tags;
54
65use crate::{
7 layout::TextLayout,
8 outline::{ParagraphTag, TagIcon},
6 layout::{TextBrush, TextLayout},
7 outline::TagIcon,
98};
10use bytemuck::{Pod, Zeroable};
11use image::ImageDecoder;
12use parley::{PositionedLayoutItem, fontique::Blob};
13use std::{
14 collections::{HashMap, HashSet},
15 ops::Range,
16};
17use swash::{
18 FontRef,
19 scale::{Render, ScaleContext, Source, StrikeWith, image::Content},
20 zeno::{Angle, Format, Transform, Vector},
21};
22
23const ATLAS_SIZE: u32 = 2048;
24const MAX_GLYPHS: usize = 8192;
25const MAX_VERTICES: usize = 65_536;
26const MAX_IMAGE_BYTES: u64 = 64 * 1024 * 1024;
27const MAX_IMAGES: usize = 256;
28
29#[derive(Clone)]
30pub struct RasterImage {
31 size: [u32; 2],
32 pixels: Blob<u8>,
33}
34
35impl RasterImage {
36 pub fn decode(encoded: &[u8]) -> Result<Self, RenderError> {
37 if encoded.len() as u64 > MAX_IMAGE_BYTES {
38 return Err(RenderError::ImageBudget);
39 }
40 let format = image::guess_format(encoded).map_err(RenderError::ImageDecode)?;
41 let mut reader = image::ImageReader::with_format(std::io::Cursor::new(encoded), format);
42 let mut limits = image::Limits::default();
43 limits.max_image_width = Some(16_384);
44 limits.max_image_height = Some(16_384);
45 limits.max_alloc = Some(MAX_IMAGE_BYTES);
46 reader.limits(limits.clone());
47 let mut decoder = reader.into_decoder().map_err(RenderError::ImageDecode)?;
48 let (width, height) = decoder.dimensions();
49 if u64::from(width) * u64::from(height) * 4 > MAX_IMAGE_BYTES
50 || decoder.total_bytes() > MAX_IMAGE_BYTES
51 {
52 return Err(RenderError::ImageBudget);
53 }
54 limits
55 .reserve(decoder.total_bytes())
56 .map_err(RenderError::ImageDecode)?;
57 decoder
58 .set_limits(limits)
59 .map_err(RenderError::ImageDecode)?;
60 let pixels = image::DynamicImage::from_decoder(decoder)
61 .map_err(RenderError::ImageDecode)?
62 .into_rgba8();
63 Self::new([width, height], pixels.into_raw())
64 }
65
66 /// Immutable, straight-alpha sRGB RGBA pixels retain one cache identity across clones.
67 pub fn new(size: [u32; 2], mut pixels: Vec<u8>) -> Result<Self, RenderError> {
68 let bytes = u64::from(size[0])
69 .checked_mul(u64::from(size[1]))
70 .and_then(|v| v.checked_mul(4))
71 .ok_or(RenderError::InvalidImage)?;
72 if size.contains(&0) || bytes != pixels.len() as u64 {
73 return Err(RenderError::InvalidImage);
74 }
75 if bytes > MAX_IMAGE_BYTES {
76 return Err(RenderError::ImageBudget);
77 }
78 // Premultiply in linear light before filtering; transparent RGB must not bleed at edges.
79 for pixel in pixels.chunks_exact_mut(4) {
80 let alpha = pixel[3];
81 if alpha == 255 {
82 continue;
83 }
84 let linear = colorref(u32::from_le_bytes(pixel.try_into().unwrap()));
85 for (byte, value) in pixel[..3].iter_mut().zip(linear) {
86 let value = value * f32::from(alpha) / 255.0;
87 *byte = srgb_byte(value);
88 }
89 }
90 Ok(Self {
91 size,
92 pixels: pixels.into(),
93 })
94 }
95}
96
97struct CachedImage {
98 binding: wgpu::BindGroup,
99 bytes: u64,
100}
101
102struct Batch {
103 vertices: Range<u32>,
104 image: Option<u64>,
105 scissor: [u32; 4],
106}
107
108#[repr(C)]
109#[derive(Clone, Copy, Pod, Zeroable)]
110struct Vertex {
111 position: [f32; 2],
112 uv: [f32; 2],
113 color: [f32; 4],
114 local: [f32; 2],
115 shape: [f32; 4],
116 stroke: f32,
117}
118
119#[derive(Hash, PartialEq, Eq)]
120enum AtlasKey {
121 Text {
122 font: u64,
123 index: u32,
124 glyph: u16,
125 size: u32,
126 coords: Vec<i16>,
127 phase: [u8; 2],
128 embolden: bool,
129 skew: u32,
130 },
131 Tag {
132 icon: TagIcon,
133 size: u32,
134 phase: [u8; 2],
135 },
136}
137
138#[derive(Clone, Copy)]
139struct AtlasGlyph {
140 x: u32,
141 y: u32,
142 width: u32,
143 height: u32,
144 left: i32,
145 top: i32,
146 color: bool,
147}
9use draw::{GlyphRun, Glyphs, Layer, Primitive, RenderError};
10use parley::PositionedLayoutItem;
14811
14912#[derive(Clone, Copy)]
15013pub struct Viewport {
......@@ -163,1492 +26,211 @@ impl Viewport {
16326 ]
16427 }
16528
166 fn visible_image_rect(&self, rect: [f32; 4]) -> Result<Option<[f32; 4]>, RenderError> {
167 let rect = [
168 rect[0] * self.scale + self.origin[0],
169 rect[1] * self.scale + self.origin[1],
170 rect[2] * self.scale + self.origin[0],
171 rect[3] * self.scale + self.origin[1],
172 ];
173 if rect.iter().any(|v| !v.is_finite()) || rect[2] < rect[0] || rect[3] < rect[1] {
174 return Err(RenderError::InvalidPrimitive);
29 /// Document-point primitives drawn through this viewport.
30 pub fn layer<'a>(&self, primitives: &'a [Primitive<'a>]) -> Layer<'a> {
31 Layer {
32 scale: self.scale,
33 origin: self.origin,
34 clip: None,
35 primitives,
17536 }
176 Ok((rect[0] < self.size[0] as f32
177 && rect[1] < self.size[1] as f32
178 && rect[2] > 0.0
179 && rect[3] > 0.0
180 && rect[0] < rect[2]
181 && rect[1] < rect[3])
182 .then_some(rect))
18337 }
18438}
18539
186#[derive(Clone, Copy, Debug, PartialEq)]
187pub enum Stroke {
188 Solid(f32),
189 /// Dash and gap lengths are twice the stroke width.
190 Dashed(f32),
191}
192
193/// Coordinates are document points; colors are linear RGBA.
194pub enum Primitive<'a> {
195 Tag {
196 tag: &'a ParagraphTag,
197 origin: [f32; 2],
198 },
199 Text {
200 layout: &'a TextLayout,
201 origin: [f32; 2],
202 /// Document-space paint bounds preserve the full shaping and caret advances.
203 clip: Option<[f32; 4]>,
204 },
205 Rect {
206 rect: [f32; 4],
207 color: [f32; 4],
208 },
209 RoundedRect {
210 rect: [f32; 4],
211 radius: [f32; 2],
212 /// None fills the shape; stroke widths are positive document points.
213 stroke: Option<Stroke>,
214 color: [f32; 4],
215 },
216 Image {
217 image: &'a RasterImage,
218 rect: [f32; 4],
219 },
220 /// A pen stroke between two points, `width` points across.
221 Segment {
222 from: [f32; 2],
223 to: [f32; 2],
224 width: f32,
225 /// A round pen tip caps the ends; otherwise they are square.
226 round: bool,
227 color: [f32; 4],
228 },
229}
230
231#[derive(Debug)]
232pub enum RenderError {
233 AtlasFull,
234 FrameTooLarge,
235 UnreadableFont,
236 UnsupportedGlyph,
237 InvalidViewport,
238 InvalidPrimitive,
239 InvalidImage,
240 ImageBudget,
241 ImageTooLarge,
242 ImageDecode(image::ImageError),
243}
244
245pub struct Renderer {
246 pub device: wgpu::Device,
247 pub queue: wgpu::Queue,
248 pipeline: wgpu::RenderPipeline,
249 image_pipeline: wgpu::RenderPipeline,
250 atlas: wgpu::Texture,
251 bind_group: wgpu::BindGroup,
252 image_sampler: wgpu::Sampler,
253 vertex_buffer: wgpu::Buffer,
254 vertices: Vec<Vertex>,
255 glyphs: HashMap<AtlasKey, Option<AtlasGlyph>>,
256 images: HashMap<u64, CachedImage>,
257 batches: Vec<Batch>,
258 scaler: ScaleContext,
259 pen: [u32; 2],
260 row_height: u32,
261}
262
263impl Renderer {
264 pub fn new(device: wgpu::Device, queue: wgpu::Queue, format: wgpu::TextureFormat) -> Self {
265 let shader = device.create_shader_module(wgpu::include_wgsl!("canvas.wgsl"));
266 let binding_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
267 label: Some("Canvas texture"),
268 entries: &[
269 wgpu::BindGroupLayoutEntry {
270 binding: 0,
271 visibility: wgpu::ShaderStages::FRAGMENT,
272 ty: wgpu::BindingType::Texture {
273 sample_type: wgpu::TextureSampleType::Float { filterable: true },
274 view_dimension: wgpu::TextureViewDimension::D2,
275 multisampled: false,
276 },
277 count: None,
278 },
279 wgpu::BindGroupLayoutEntry {
280 binding: 1,
281 visibility: wgpu::ShaderStages::FRAGMENT,
282 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
283 count: None,
284 },
285 ],
286 });
287 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
288 label: Some("Canvas"),
289 bind_group_layouts: &[Some(&binding_layout)],
290 ..Default::default()
291 });
292 let make_pipeline = |blend| {
293 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
294 label: Some("Canvas"),
295 layout: Some(&pipeline_layout),
296 vertex: wgpu::VertexState {
297 module: &shader,
298 entry_point: Some("vertex"),
299 compilation_options: Default::default(),
300 buffers: &[Some(wgpu::VertexBufferLayout {
301 array_stride: size_of::<Vertex>() as u64,
302 step_mode: wgpu::VertexStepMode::Vertex,
303 attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Float32x4, 3 => Float32x2, 4 => Float32x4, 5 => Float32],
304 })],
305 },
306 fragment: Some(wgpu::FragmentState {
307 module: &shader,
308 entry_point: Some("fragment"),
309 compilation_options: Default::default(),
310 targets: &[Some(wgpu::ColorTargetState {
311 format,
312 blend: Some(blend),
313 write_mask: wgpu::ColorWrites::ALL,
314 })],
315 }),
316 primitive: Default::default(),
317 depth_stencil: None,
318 multisample: Default::default(),
319 multiview_mask: None,
320 cache: None,
321 })
322 };
323 let pipeline = make_pipeline(wgpu::BlendState::ALPHA_BLENDING);
324 let image_pipeline = make_pipeline(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING);
325 let atlas = device.create_texture(&wgpu::TextureDescriptor {
326 label: Some("Glyph atlas"),
327 size: wgpu::Extent3d {
328 width: ATLAS_SIZE,
329 height: ATLAS_SIZE,
330 depth_or_array_layers: 1,
331 },
332 mip_level_count: 1,
333 sample_count: 1,
334 dimension: wgpu::TextureDimension::D2,
335 format: wgpu::TextureFormat::Rgba8UnormSrgb,
336 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
337 view_formats: &[],
338 });
339 let view = atlas.create_view(&Default::default());
340 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
341 mag_filter: wgpu::FilterMode::Nearest,
342 min_filter: wgpu::FilterMode::Nearest,
343 ..Default::default()
344 });
345 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
346 label: Some("Glyph atlas"),
347 layout: &pipeline.get_bind_group_layout(0),
348 entries: &[
349 wgpu::BindGroupEntry {
350 binding: 0,
351 resource: wgpu::BindingResource::TextureView(&view),
352 },
353 wgpu::BindGroupEntry {
354 binding: 1,
355 resource: wgpu::BindingResource::Sampler(&sampler),
356 },
357 ],
358 });
359 let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
360 label: Some("Canvas vertices"),
361 size: (MAX_VERTICES * size_of::<Vertex>()) as u64,
362 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
363 mapped_at_creation: false,
364 });
365 let image_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
366 label: Some("Canvas image filtering"),
367 mag_filter: wgpu::FilterMode::Linear,
368 min_filter: wgpu::FilterMode::Linear,
369 ..Default::default()
370 });
371 queue.write_texture(
372 atlas.as_image_copy(),
373 &[255; 4],
374 wgpu::TexelCopyBufferLayout {
375 offset: 0,
376 bytes_per_row: Some(4),
377 rows_per_image: Some(1),
378 },
379 wgpu::Extent3d {
380 width: 1,
381 height: 1,
382 depth_or_array_layers: 1,
383 },
384 );
385 Self {
386 device,
387 queue,
388 pipeline,
389 image_pipeline,
390 atlas,
391 bind_group,
392 image_sampler,
393 vertex_buffer,
394 vertices: Vec::new(),
395 glyphs: HashMap::new(),
396 images: HashMap::new(),
397 batches: Vec::new(),
398 scaler: ScaleContext::with_max_entries(32),
399 pen: [1, 0],
400 row_height: 1,
401 }
402 }
403
404 pub fn clear_glyph_cache(&mut self) {
405 self.glyphs.clear();
406 self.pen = [1, 0];
407 self.row_height = 1;
408 }
409
410 pub fn draw(
411 &mut self,
412 target: &wgpu::TextureView,
413 viewport: Viewport,
414 primitives: &[Primitive<'_>],
40impl Glyphs for TextLayout {
41 fn runs(
42 &self,
43 paint: &mut dyn FnMut(GlyphRun<'_>) -> Result<(), RenderError>,
41544 ) -> Result<(), RenderError> {
416 if viewport.size.contains(&0)
417 || !viewport.scale.is_finite()
418 || viewport.scale <= 0.0
419 || viewport.origin.iter().any(|v| !v.is_finite())
420 {
421 return Err(RenderError::InvalidViewport);
422 }
423 let mut active_images = HashSet::new();
424 let mut image_bytes = 0;
425 for primitive in primitives {
426 if let Primitive::Image { image, rect } = primitive
427 && viewport.visible_image_rect(*rect)?.is_some()
428 && active_images.insert(image.pixels.id())
429 {
430 image_bytes += image.pixels.as_ref().len() as u64;
431 if image_bytes > MAX_IMAGE_BYTES || active_images.len() > MAX_IMAGES {
432 return Err(RenderError::ImageBudget);
433 }
434 }
435 }
436 for attempt in 0..2 {
437 self.vertices.clear();
438 self.batches.clear();
439 match self.prepare(viewport, primitives, &active_images) {
440 Err(RenderError::AtlasFull) if attempt == 0 => self.clear_glyph_cache(),
441 result => {
442 result?;
443 break;
444 }
445 }
446 }
447 self.queue
448 .write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&self.vertices));
449 let mut encoder = self.device.create_command_encoder(&Default::default());
450 {
451 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
452 label: Some("Canvas"),
453 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
454 view: target,
455 depth_slice: None,
456 resolve_target: None,
457 ops: wgpu::Operations {
458 load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),
459 store: wgpu::StoreOp::Store,
460 },
461 })],
462 ..Default::default()
463 });
464 pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
465 for batch in &self.batches {
466 let [x, y, width, height] = batch.scissor;
467 pass.set_scissor_rect(x, y, width, height);
468 pass.set_pipeline(if batch.image.is_some() {
469 &self.image_pipeline
470 } else {
471 &self.pipeline
472 });
473 let binding = batch
474 .image
475 .map(|id| &self.images[&id].binding)
476 .unwrap_or(&self.bind_group);
477 pass.set_bind_group(0, binding, &[]);
478 pass.draw(batch.vertices.clone(), 0..1);
479 }
480 }
481 self.queue.submit([encoder.finish()]);
482 Ok(())
483 }
484
485 fn prepare(
486 &mut self,
487 viewport: Viewport,
488 primitives: &[Primitive<'_>],
489 active_images: &HashSet<u64>,
490 ) -> Result<(), RenderError> {
491 for primitive in primitives {
492 let start = self.vertices.len() as u32;
493 let mut image_id = None;
494 let mut scissor = [0, 0, viewport.size[0], viewport.size[1]];
495 match primitive {
496 Primitive::Tag { tag, origin } => {
497 let origin = [
498 viewport.origin[0] + (origin[0] + tag.origin[0]) * viewport.scale,
499 viewport.origin[1] + (origin[1] + tag.origin[1]) * viewport.scale,
500 ];
501 self.tag(Viewport { origin, ..viewport }, tag)?;
502 }
503 Primitive::Text {
504 layout,
505 origin,
506 clip,
507 } => {
508 let origin = [
509 viewport.origin[0] + origin[0] * viewport.scale,
510 viewport.origin[1] + origin[1] * viewport.scale,
511 ];
512 if origin.iter().any(|v| !v.is_finite()) {
513 return Err(RenderError::InvalidPrimitive);
514 }
515 if let Some(clip) = clip {
516 let Some(rect) = viewport.visible_image_rect(*clip)? else {
517 continue;
518 };
519 let left = rect[0].max(0.0).floor() as u32;
520 let top = rect[1].max(0.0).floor() as u32;
521 let right = rect[2].min(viewport.size[0] as f32).ceil() as u32;
522 let bottom = rect[3].min(viewport.size[1] as f32).ceil() as u32;
523 scissor = [left, top, right - left, bottom - top];
524 }
525 self.text(Viewport { origin, ..viewport }, layout)?;
526 }
527 Primitive::Rect { rect, color } => {
528 self.quad(
529 viewport,
530 [
531 rect[0] * viewport.scale + viewport.origin[0],
532 rect[1] * viewport.scale + viewport.origin[1],
533 rect[2] * viewport.scale + viewport.origin[0],
534 rect[3] * viewport.scale + viewport.origin[1],
535 ],
536 [0.5 / ATLAS_SIZE as f32; 4],
537 *color,
538 )?;
539 }
540 Primitive::RoundedRect {
541 rect,
542 radius,
543 stroke,
544 color,
545 } => {
546 if radius
547 .iter()
548 .any(|value| !value.is_finite() || *value < 0.0)
549 {
550 return Err(RenderError::InvalidPrimitive);
551 }
552 let width = match stroke {
553 None => 0.0,
554 Some(Stroke::Solid(width) | Stroke::Dashed(width)) => {
555 if !width.is_finite() || *width <= 0.0 || width * viewport.scale == 0.0
556 {
557 return Err(RenderError::InvalidPrimitive);
558 }
559 *width
560 }
561 };
562 let rect = [
563 rect[0] * viewport.scale + viewport.origin[0],
564 rect[1] * viewport.scale + viewport.origin[1],
565 rect[2] * viewport.scale + viewport.origin[0],
566 rect[3] * viewport.scale + viewport.origin[1],
567 ];
568 self.quad(viewport, rect, [0.5 / ATLAS_SIZE as f32; 4], *color)?;
569 let half = [(rect[2] - rect[0]) * 0.5, (rect[3] - rect[1]) * 0.5];
570 if !(4.0 * (half[0] + half[1])).is_finite() {
571 return Err(RenderError::InvalidPrimitive);
572 }
573 let radius = if radius.contains(&0.0) {
574 [0.0; 2]
575 } else {
576 std::array::from_fn(|axis| (radius[axis] * viewport.scale).min(half[axis]))
577 };
578 let width = (width * viewport.scale).min(half[0].min(half[1]));
579 // The stroke sign selects its dash pattern at the shader boundary.
580 let width = if matches!(stroke, Some(Stroke::Dashed(_))) {
581 -width
582 } else {
583 width
584 };
585 for vertex in &mut self.vertices[start as usize..] {
586 vertex.shape = [half[0], half[1], radius[0], radius[1]];
587 vertex.stroke = width;
588 }
589 }
590 Primitive::Segment {
591 from,
592 to,
593 width,
594 round,
595 color,
596 } => self.segment(viewport, *from, *to, *width, *round, *color)?,
597 Primitive::Image { image, rect } => {
598 if let Some(rect) = viewport.visible_image_rect(*rect)? {
599 self.image(image, active_images)?;
600 image_id = Some(image.pixels.id());
601 self.quad(viewport, rect, [0.0, 0.0, 1.0, 1.0], [1.0; 4])?;
602 }
603 }
604 }
605 let end = self.vertices.len() as u32;
606 if start != end {
607 if let Some(last) = self.batches.last_mut()
608 && last.image == image_id
609 && last.scissor == scissor
610 {
611 last.vertices.end = end;
612 } else {
613 self.batches.push(Batch {
614 vertices: start..end,
615 image: image_id,
616 scissor,
617 });
618 }
619 }
620 }
621 Ok(())
622 }
623
624 fn image(&mut self, image: &RasterImage, active: &HashSet<u64>) -> Result<(), RenderError> {
625 if self.images.contains_key(&image.pixels.id()) {
626 return Ok(());
627 }
628 if image
629 .size
630 .iter()
631 .any(|v| *v > self.device.limits().max_texture_dimension_2d)
632 {
633 return Err(RenderError::ImageTooLarge);
634 }
635 let bytes = image.pixels.as_ref().len() as u64;
636 while self.images.values().map(|i| i.bytes).sum::<u64>() + bytes > MAX_IMAGE_BYTES
637 || self.images.len() >= MAX_IMAGES
638 {
639 let id = self
640 .images
641 .keys()
642 .find(|id| !active.contains(id))
643 .copied()
644 .ok_or(RenderError::ImageBudget)?;
645 self.images.remove(&id);
646 }
647 let size = wgpu::Extent3d {
648 width: image.size[0],
649 height: image.size[1],
650 depth_or_array_layers: 1,
651 };
652 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
653 label: Some("Canvas image"),
654 size,
655 mip_level_count: 1,
656 sample_count: 1,
657 dimension: wgpu::TextureDimension::D2,
658 format: wgpu::TextureFormat::Rgba8UnormSrgb,
659 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
660 view_formats: &[],
661 });
662 self.queue.write_texture(
663 texture.as_image_copy(),
664 image.pixels.as_ref(),
665 wgpu::TexelCopyBufferLayout {
666 offset: 0,
667 bytes_per_row: Some(image.size[0] * 4),
668 rows_per_image: Some(image.size[1]),
669 },
670 size,
671 );
672 let binding = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
673 label: Some("Canvas image"),
674 layout: &self.pipeline.get_bind_group_layout(0),
675 entries: &[
676 wgpu::BindGroupEntry {
677 binding: 0,
678 resource: wgpu::BindingResource::TextureView(
679 &texture.create_view(&Default::default()),
680 ),
681 },
682 wgpu::BindGroupEntry {
683 binding: 1,
684 resource: wgpu::BindingResource::Sampler(&self.image_sampler),
685 },
686 ],
687 });
688 self.images
689 .insert(image.pixels.id(), CachedImage { binding, bytes });
690 Ok(())
691 }
692
693 fn text(&mut self, viewport: Viewport, layout: &TextLayout) -> Result<(), RenderError> {
694 let scale = viewport.scale;
695 let origin = viewport.origin;
696 for (line, line_box) in layout.lines() {
697 let top = line_box.top * scale + origin[1];
698 if top + line_box.height * scale < 0.0 || top > viewport.size[1] as f32 {
699 continue;
700 }
45 for (line, bounds) in self.lines() {
70146 for item in line.items() {
702 let PositionedLayoutItem::GlyphRun(glyph_run) = item else {
47 let PositionedLayoutItem::GlyphRun(run) = item else {
70348 continue;
70449 };
705 let run = glyph_run.run();
706 let data = &run.font().font;
707 let size = run.font_size() * scale;
708 if !size.is_finite() || size > ATLAS_SIZE as f32 {
709 return Err(RenderError::AtlasFull);
710 }
711 let synthesis = run.synthesis();
712 let coords: Vec<i16> = run
713 .normalized_coords()
714 .iter()
715 .map(|c| c.to_bits())
716 .collect();
717 let color = glyph_run.style().brush.color;
718 let rise = glyph_run.style().brush.rise;
719 for glyph in glyph_run.positioned_glyphs() {
720 let x = glyph.x * scale + origin[0];
721 let y = (glyph.y + line_box.baseline - line.metrics().baseline - rise) * scale
722 + origin[1];
723 // Quantization affects raster coverage only, never advances or line breaks.
724 let x = (x * 4.0).round() * 0.25;
725 let y = (y * 4.0).round() * 0.25;
726 let glyph_id = glyph
727 .id
728 .try_into()
729 .map_err(|_| RenderError::UnsupportedGlyph)?;
730 let phase = [((x - x.floor()) * 4.0) as u8, ((y - y.floor()) * 4.0) as u8];
731 let key = AtlasKey::Text {
732 font: data.data.id(),
733 index: data.index,
734 glyph: glyph_id,
735 size: size.to_bits(),
736 coords: coords.clone(),
737 phase,
738 embolden: synthesis.embolden(),
739 skew: synthesis.skew().unwrap_or(0.0).to_bits(),
740 };
741 let cached = if let Some(cached) = self.glyphs.get(&key) {
742 *cached
743 } else {
744 if self.glyphs.len() >= MAX_GLYPHS {
745 return Err(RenderError::AtlasFull);
746 }
747 let font = FontRef::from_index(data.data.as_ref(), data.index as usize)
748 .ok_or(RenderError::UnreadableFont)?;
749 let mut scaler = self
750 .scaler
751 .builder_with_id(font, [data.data.id(), u64::from(data.index)])
752 .size(size)
753 .hint(false)
754 .normalized_coords(coords.iter().copied())
755 .build();
756 let mut render = Render::new(&[
757 Source::ColorOutline(0),
758 Source::ColorBitmap(StrikeWith::BestFit),
759 Source::Outline,
760 ]);
761 // Swash rasterizes outlines in Y-up coordinates.
762 render.format(Format::Alpha).offset(Vector::new(
763 f32::from(phase[0]) * 0.25,
764 -f32::from(phase[1]) * 0.25,
765 ));
766 if synthesis.embolden() {
767 render.embolden(size / 24.0);
768 }
769 if let Some(skew) = synthesis.skew() {
770 render.transform(Some(Transform::skew(
771 Angle::from_degrees(skew),
772 Angle::from_degrees(0.0),
773 )));
774 }
775 let image = render.render(&mut scaler, glyph_id);
776 let cached = if let Some(image) =
777 image.filter(|i| i.placement.width > 0 && i.placement.height > 0)
778 {
779 Some(self.upload(image)?)
780 } else {
781 None
782 };
783 self.glyphs.insert(key, cached);
784 cached
785 };
786 if let Some(glyph) = cached {
787 let mut left = x.floor() + glyph.left as f32;
788 let mut top = y.floor() - glyph.top as f32;
789 let mut width = glyph.width as f32;
790 let mut height = glyph.height as f32;
791 if glyph.color {
792 let line_top = line_box.top * scale + origin[1];
793 let line_height = line_box.height * scale;
794 let ratio = (line_height / height).min(1.0);
795 left += width * (1.0 - ratio) * 0.5;
796 width *= ratio;
797 height *= ratio;
798 top = top
799 .clamp(line_top, (line_top + line_height - height).max(line_top));
800 }
801 let atlas_size = ATLAS_SIZE as f32;
802 self.quad(
803 viewport,
804 [left, top, left + width, top + height],
805 [
806 glyph.x as f32 / atlas_size,
807 glyph.y as f32 / atlas_size,
808 (glyph.x + glyph.width) as f32 / atlas_size,
809 (glyph.y + glyph.height) as f32 / atlas_size,
810 ],
811 if glyph.color {
812 [1.0; 4]
813 } else {
814 colorref(color)
815 },
816 )?;
817 }
818 }
819 let metrics = run.font_metrics();
820 for (decoration, offset, thickness) in [
821 (
822 &glyph_run.style().underline,
823 metrics.underline_offset,
824 metrics.underline_size,
825 ),
826 (
827 &glyph_run.style().strikethrough,
828 metrics.strikethrough_offset,
829 metrics.strikethrough_size,
830 ),
831 ] {
832 if let Some(decoration) = decoration {
833 let x = glyph_run.offset() * scale + origin[0];
834 let y = (line_box.baseline - rise - decoration.offset.unwrap_or(offset))
835 * scale
836 + origin[1];
837 self.quad(
838 viewport,
839 [
840 x,
841 y,
842 x + glyph_run.advance() * scale,
843 y + (decoration.size.unwrap_or(thickness) * scale).max(1.0),
844 ],
845 [0.5 / ATLAS_SIZE as f32; 4],
846 colorref(decoration.brush.color),
847 )?;
848 }
849 }
50 draw::paint_parley_run(
51 &run,
52 bounds.baseline,
53 run.style().brush.rise,
54 [bounds.top, bounds.height],
55 |brush: &TextBrush| colorref(brush.color),
56 paint,
57 )?;
85058 }
85159 }
85260 Ok(())
85361 }
854
855 fn upload(&mut self, image: swash::scale::image::Image) -> Result<AtlasGlyph, RenderError> {
856 let p = image.placement;
857 if p.width + 2 > ATLAS_SIZE || p.height + 2 > ATLAS_SIZE {
858 return Err(RenderError::AtlasFull);
859 }
860 if self.pen[0] + p.width + 1 > ATLAS_SIZE {
861 self.pen[0] = 0;
862 self.pen[1] += self.row_height + 1;
863 self.row_height = 0;
864 }
865 if self.pen[1] + p.height + 1 > ATLAS_SIZE {
866 return Err(RenderError::AtlasFull);
867 }
868 let cached = AtlasGlyph {
869 x: self.pen[0],
870 y: self.pen[1],
871 width: p.width,
872 height: p.height,
873 left: p.left,
874 top: p.top,
875 color: image.content == Content::Color,
876 };
877 let rgba = match image.content {
878 Content::Mask => image
879 .data
880 .iter()
881 .flat_map(|a| [255, 255, 255, *a])
882 .collect(),
883 _ => image.data,
884 };
885 self.queue.write_texture(
886 wgpu::TexelCopyTextureInfo {
887 origin: wgpu::Origin3d {
888 x: cached.x,
889 y: cached.y,
890 z: 0,
891 },
892 ..self.atlas.as_image_copy()
893 },
894 &rgba,
895 wgpu::TexelCopyBufferLayout {
896 offset: 0,
897 bytes_per_row: Some(p.width * 4),
898 rows_per_image: Some(p.height),
899 },
900 wgpu::Extent3d {
901 width: p.width,
902 height: p.height,
903 depth_or_array_layers: 1,
904 },
905 );
906 self.pen[0] += p.width + 1;
907 self.row_height = self.row_height.max(p.height);
908 Ok(cached)
909 }
910
911 fn tag(&mut self, viewport: Viewport, tag: &ParagraphTag) -> Result<(), RenderError> {
912 let size = ParagraphTag::SIZE * viewport.scale;
913 let [x, y] = viewport.origin.map(|v| (v * 4.0).round() * 0.25);
914 if !size.is_finite() || [x, y].iter().any(|v| !v.is_finite()) {
915 return Err(RenderError::InvalidPrimitive);
916 }
917 if x + size + 1.0 < 0.0
918 || y + size + 1.0 < 0.0
919 || x - 1.0 > viewport.size[0] as f32
920 || y - 1.0 > viewport.size[1] as f32
921 {
922 return Ok(());
923 }
924 if size + 3.0 > ATLAS_SIZE as f32 {
925 return Err(RenderError::AtlasFull);
926 }
927 let phase = [((x - x.floor()) * 4.0) as u8, ((y - y.floor()) * 4.0) as u8];
928 let key = AtlasKey::Tag {
929 icon: tag.icon,
930 size: size.to_bits(),
931 phase,
932 };
933 let glyph = if let Some(Some(glyph)) = self.glyphs.get(&key) {
934 *glyph
935 } else {
936 if self.glyphs.len() >= MAX_GLYPHS {
937 return Err(RenderError::AtlasFull);
938 }
939 let glyph = self.upload(tags::rasterize(tag.icon, size, phase))?;
940 self.glyphs.insert(key, Some(glyph));
941 glyph
942 };
943 let left = x.floor();
944 let top = y.floor();
945 let atlas = ATLAS_SIZE as f32;
946 self.quad(
947 viewport,
948 [
949 left,
950 top,
951 left + glyph.width as f32,
952 top + glyph.height as f32,
953 ],
954 [
955 glyph.x as f32 / atlas,
956 glyph.y as f32 / atlas,
957 (glyph.x + glyph.width) as f32 / atlas,
958 (glyph.y + glyph.height) as f32 / atlas,
959 ],
960 [1.0, 1.0, 1.0, if tag.disabled { 0.45 } else { 1.0 }],
961 )
962 }
963
964 /// Draws the segment as a capsule in its own frame, reusing the rounded-rectangle distance.
965 fn segment(
966 &mut self,
967 viewport: Viewport,
968 from: [f32; 2],
969 to: [f32; 2],
970 width: f32,
971 round: bool,
972 color: [f32; 4],
973 ) -> Result<(), RenderError> {
974 let pixel = |p: [f32; 2]| {
975 [
976 p[0] * viewport.scale + viewport.origin[0],
977 p[1] * viewport.scale + viewport.origin[1],
978 ]
979 };
980 let [from, to] = [pixel(from), pixel(to)];
981 // Hairlines stay one device pixel wide, as OneNote draws its thinnest pen.
982 let radius = (width * viewport.scale).max(1.0) * 0.5;
983 if from.iter().chain(&to).chain(&color).any(|v| !v.is_finite()) || !radius.is_finite() {
984 return Err(RenderError::InvalidPrimitive);
985 }
986 let pad = radius + 1.0;
987 if from[0].max(to[0]) + pad < 0.0
988 || from[1].max(to[1]) + pad < 0.0
989 || from[0].min(to[0]) - pad > viewport.size[0] as f32
990 || from[1].min(to[1]) - pad > viewport.size[1] as f32
991 {
992 return Ok(());
993 }
994 if self.vertices.len() + 6 > MAX_VERTICES {
995 return Err(RenderError::FrameTooLarge);
996 }
997 let delta = [to[0] - from[0], to[1] - from[1]];
998 let length = delta[0].hypot(delta[1]);
999 let along = if length > 0.0 {
1000 [delta[0] / length, delta[1] / length]
1001 } else {
1002 [1.0, 0.0]
1003 };
1004 let across = [-along[1], along[0]];
1005 let center = [(from[0] + to[0]) * 0.5, (from[1] + to[1]) * 0.5];
1006 let half = [length * 0.5 + radius, radius];
1007 let corner = if round { radius } else { 0.0 };
1008 let [hx, hy] = [half[0] + 1.0, half[1] + 1.0];
1009 for local in [
1010 [-hx, -hy],
1011 [-hx, hy],
1012 [hx, hy],
1013 [-hx, -hy],
1014 [hx, hy],
1015 [hx, -hy],
1016 ] {
1017 let x = center[0] + along[0] * local[0] + across[0] * local[1];
1018 let y = center[1] + along[1] * local[0] + across[1] * local[1];
1019 self.vertices.push(Vertex {
1020 position: [
1021 x * 2.0 / viewport.size[0] as f32 - 1.0,
1022 1.0 - y * 2.0 / viewport.size[1] as f32,
1023 ],
1024 uv: [0.5 / ATLAS_SIZE as f32; 2],
1025 color,
1026 local,
1027 shape: [half[0], half[1], corner, corner],
1028 stroke: 0.0,
1029 });
1030 }
1031 Ok(())
1032 }
1033
1034 fn quad(
1035 &mut self,
1036 viewport: Viewport,
1037 rect: [f32; 4],
1038 uv: [f32; 4],
1039 color: [f32; 4],
1040 ) -> Result<(), RenderError> {
1041 if rect.iter().chain(&color).any(|v| !v.is_finite())
1042 || rect[2] < rect[0]
1043 || rect[3] < rect[1]
1044 {
1045 return Err(RenderError::InvalidPrimitive);
1046 }
1047 if rect[2] < 0.0
1048 || rect[3] < 0.0
1049 || rect[0] > viewport.size[0] as f32
1050 || rect[1] > viewport.size[1] as f32
1051 {
1052 return Ok(());
1053 }
1054 if self.vertices.len() + 6 > MAX_VERTICES {
1055 return Err(RenderError::FrameTooLarge);
1056 }
1057 let x0 = rect[0] * 2.0 / viewport.size[0] as f32 - 1.0;
1058 let x1 = rect[2] * 2.0 / viewport.size[0] as f32 - 1.0;
1059 let y0 = 1.0 - rect[1] * 2.0 / viewport.size[1] as f32;
1060 let y1 = 1.0 - rect[3] * 2.0 / viewport.size[1] as f32;
1061 if [x0, x1, y0, y1].iter().any(|v| !v.is_finite()) {
1062 return Err(RenderError::InvalidPrimitive);
1063 }
1064 let [hx, hy] = [(rect[2] - rect[0]) * 0.5, (rect[3] - rect[1]) * 0.5];
1065 for (position, uv, local) in [
1066 ([x0, y0], [uv[0], uv[1]], [-hx, -hy]),
1067 ([x0, y1], [uv[0], uv[3]], [-hx, hy]),
1068 ([x1, y1], [uv[2], uv[3]], [hx, hy]),
1069 ([x0, y0], [uv[0], uv[1]], [-hx, -hy]),
1070 ([x1, y1], [uv[2], uv[3]], [hx, hy]),
1071 ([x1, y0], [uv[2], uv[1]], [hx, -hy]),
1072 ] {
1073 self.vertices.push(Vertex {
1074 position,
1075 uv,
1076 color,
1077 local,
1078 shape: [0.0; 4],
1079 stroke: 0.0,
1080 });
1081 }
1082 Ok(())
1083 }
1084}
1085
1086fn srgb_byte(value: f32) -> u8 {
1087 let srgb = if value <= 0.0031308 {
1088 value * 12.92
1089 } else {
1090 1.055 * value.powf(1.0 / 2.4) - 0.055
1091 };
1092 (srgb * 255.0).round() as u8
109362}
109463
64/// The layout a text primitive paints.
65#[cfg(test)]
66pub(crate) fn painted_layout(text: &dyn Glyphs) -> &TextLayout {
67 (text as &dyn std::any::Any)
68 .downcast_ref()
69 .expect("Page text is a TextLayout")
70}
71
72const CHECKBOX: &str = include_str!("../../assets/tags/checkbox.svg");
73const CHECKMARK: &str = include_str!("../../assets/tags/checkmark.svg");
74const QUESTION: &str = include_str!("../../assets/tags/question.svg");
75const MUSIC: &str = include_str!("../../assets/tags/music.svg");
76const EXCLAMATION: &str = include_str!("../../assets/tags/exclamation.svg");
77const RED_SQUARE: &str = include_str!("../../assets/tags/red-square.svg");
78const YELLOW_SQUARE: &str = include_str!("../../assets/tags/yellow-square.svg");
79const BLUE_SQUARE: &str = include_str!("../../assets/tags/blue-square.svg");
80
81/// The tag's artwork, drawn in order.
82pub fn tag_sources(icon: TagIcon) -> &'static [&'static str] {
83 match icon {
84 TagIcon::CheckBox { checked: false } => &[CHECKBOX],
85 TagIcon::CheckBox { checked: true } => &[CHECKBOX, CHECKMARK],
86 TagIcon::Question => &[QUESTION],
87 TagIcon::Music => &[MUSIC],
88 TagIcon::Exclamation => &[EXCLAMATION],
89 TagIcon::RedSquare => &[RED_SQUARE],
90 TagIcon::YellowSquare => &[YELLOW_SQUARE],
91 TagIcon::BlueSquare => &[BLUE_SQUARE],
92 }
93}
94
95/// A COLORREF (0x00BBGGRR) as linear RGBA.
109596pub fn colorref(color: u32) -> [f32; 4] {
1096 let linear = |byte: u32| {
1097 let value = (byte & 255) as f32 / 255.0;
1098 if value <= 0.04045 {
1099 value / 12.92
1100 } else {
1101 ((value + 0.055) / 1.055).powf(2.4)
1102 }
1103 };
1104 [linear(color), linear(color >> 8), linear(color >> 16), 1.0]
97 let [red, green, blue, _] = color.to_le_bytes();
98 draw::srgb(red, green, blue)
110599}
1106100
1107101#[cfg(test)]
1108102mod tests {
1109103 use super::*;
1110104 use crate::layout::TextEngine;
1111 use onestore::document::Format as TextFormat;
105 use onestore::document::Format;
1112106 use onestore::page::text::Paragraph;
1113107 use std::time::Duration;
1114108
1115 #[test]
1116 fn decoding_preserves_pixels_and_rejects_large_or_truncated_images() {
1117 let mut encoded = Vec::new();
1118 let mut encoder = png::Encoder::new(&mut encoded, 2, 1);
1119 encoder.set_color(png::ColorType::Rgba);
1120 encoder.set_depth(png::BitDepth::Eight);
1121 let rgba = [255, 64, 0, 255, 0, 128, 255, 128];
1122 encoder
1123 .write_header()
1124 .unwrap()
1125 .write_image_data(&rgba)
1126 .unwrap();
1127 let decoded = RasterImage::decode(&encoded).unwrap();
1128 assert_eq!(decoded.size, [2, 1]);
1129 assert_eq!(decoded.pixels.as_ref(), [255, 64, 0, 255, 0, 93, 188, 128]);
1130 for end in 0..encoded.len() - 12 {
1131 assert!(RasterImage::decode(&encoded[..end]).is_err(), "{end}");
1132 }
1133
1134 let mut jpeg = Vec::new();
1135 image::codecs::jpeg::JpegEncoder::new(&mut jpeg)
1136 .encode(&[255; 4 * 4 * 3], 4, 4, image::ExtendedColorType::Rgb8)
1137 .unwrap();
1138 let decoded = RasterImage::decode(&jpeg).unwrap();
1139 assert_eq!(decoded.size, [4, 4]);
1140 assert_eq!(decoded.pixels.as_ref(), [255; 4 * 4 * 4]);
1141
1142 let mut gif = Vec::new();
1143 image::codecs::gif::GifEncoder::new(&mut gif)
1144 .encode(&[255; 2 * 2 * 4], 2, 2, image::ExtendedColorType::Rgba8)
1145 .unwrap();
1146 let decoded = RasterImage::decode(&gif).unwrap();
1147 assert_eq!(decoded.size, [2, 2]);
1148 assert_eq!(decoded.pixels.as_ref(), [255; 2 * 2 * 4]);
1149
1150 for size in [[8192, 8192], [16_385, 1]] {
1151 let mut header = Vec::new();
1152 let mut encoder = png::Encoder::new(&mut header, size[0], size[1]);
1153 encoder.set_color(png::ColorType::Rgba);
1154 encoder.set_depth(png::BitDepth::Eight);
1155 let mut writer = encoder.write_header().unwrap();
1156 writer.write_chunk(png::chunk::IDAT, &[]).unwrap();
1157 drop(writer);
1158 let error = RasterImage::decode(&header).err().unwrap();
1159 if size[0] == 8192 {
1160 assert!(matches!(error, RenderError::ImageBudget), "{error:?}");
1161 } else {
1162 assert!(
1163 matches!(
1164 error,
1165 RenderError::ImageDecode(image::ImageError::Limits(_))
1166 ),
1167 "{error:?}"
1168 );
1169 }
1170 }
1171 }
1172
1173 #[test]
1174 fn image_pixels_have_valid_dimensions_and_immutable_clone_identity() {
1175 assert!(matches!(
1176 RasterImage::new([0, 1], Vec::new()),
1177 Err(RenderError::InvalidImage)
1178 ));
1179 assert!(matches!(
1180 RasterImage::new([2, 2], vec![255; 15]),
1181 Err(RenderError::InvalidImage)
1182 ));
1183 assert!(matches!(
1184 RasterImage::new([u32::MAX; 2], Vec::new()),
1185 Err(RenderError::InvalidImage)
1186 ));
1187 let image = RasterImage::new([1, 1], vec![1, 2, 3, 255]).unwrap();
1188 assert_eq!(image.pixels.id(), image.clone().pixels.id());
1189 }
1190
1191109 #[test]
1192110 #[ignore = "requires a native GPU adapter"]
1193 fn eviction_and_repaint_reproduce_pixels() {
111 fn tags_and_page_text_paint_through_the_renderer() {
1194112 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
1195113 let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
1196114 let (device, queue) =
1197115 pollster::block_on(adapter.request_device(&Default::default())).unwrap();
1198 let format = wgpu::TextureFormat::Rgba8UnormSrgb;
116 let size = [256, 64];
1199117 let target = device.create_texture(&wgpu::TextureDescriptor {
1200 label: Some("Canvas readback test"),
118 label: Some("Tag readback test"),
1201119 size: wgpu::Extent3d {
1202 width: 512,
1203 height: 256,
120 width: size[0],
121 height: size[1],
1204122 depth_or_array_layers: 1,
1205123 },
1206124 mip_level_count: 1,
1207125 sample_count: 1,
1208126 dimension: wgpu::TextureDimension::D2,
1209 format,
127 format: wgpu::TextureFormat::Rgba8UnormSrgb,
1210128 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
1211129 view_formats: &[],
1212130 });
1213131 let readback = device.create_buffer(&wgpu::BufferDescriptor {
1214 label: Some("Canvas readback"),
1215 size: 512 * 256 * 4,
132 label: Some("Tag readback"),
133 size: u64::from(size[0] * size[1] * 4),
1216134 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
1217135 mapped_at_creation: false,
1218136 });
1219 let mut renderer = Renderer::new(device, queue, format);
1220 let mut engine = TextEngine::default();
1221 let layout = engine
1222 .layout(
1223 &Paragraph::new(
1224 "Hello 🌳\nCafé e\u{301} שלום".into(),
1225 TextFormat {
1226 underline: Some(true),
1227 ..TextFormat::default()
1228 },
1229 ),
1230 220.0,
1231 )
1232 .unwrap();
1233 let viewport = Viewport {
1234 size: [512, 256],
1235 scale: 2.0,
1236 origin: [24.0; 2],
1237 };
1238 let mut captures = Vec::new();
1239 let image = RasterImage::new(
1240 [2, 2],
1241 vec![255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 0, 0, 0, 0],
1242 )
1243 .unwrap();
1244 let transparent_edge =
1245 RasterImage::new([2, 1], vec![255, 0, 0, 255, 0, 0, 255, 0]).unwrap();
1246 let tags = [
137 let mut renderer = draw::Renderer::new(device, queue, wgpu::TextureFormat::Rgba8UnormSrgb);
138 let icons = [
1247139 TagIcon::CheckBox { checked: false },
1248140 TagIcon::CheckBox { checked: true },
1249141 TagIcon::Question,
1250142 TagIcon::Music,
1251 ]
1252 .map(|icon| ParagraphTag {
1253 icon,
1254 origin: [0.0; 2],
1255 label: String::new(),
1256 disabled: false,
1257 });
1258 let phase_layout = engine
143 TagIcon::Exclamation,
144 TagIcon::RedSquare,
145 TagIcon::YellowSquare,
146 TagIcon::BlueSquare,
147 ];
148 let text = TextEngine::default()
1259149 .layout(
1260150 &Paragraph::new(
1261 "H".into(),
1262 TextFormat {
1263 font: Some("Arial".into()),
1264 font_size: Some(11.0),
1265 ..TextFormat::default()
151 "Tagged".into(),
152 Format {
153 color: Some(0x000000ff),
154 underline: Some(true),
155 ..Format::default()
1266156 },
1267157 ),
1268 20.0,
158 100.0,
1269159 )
1270160 .unwrap();
1271 let mut primitives = vec![
1272 Primitive::Rect {
1273 rect: [0.0, 0.0, 70.0, 14.0],
1274 color: [0.55, 0.73, 1.0, 0.5],
1275 },
1276 Primitive::Text {
1277 clip: None,
1278 layout: &layout,
1279 origin: [0.0; 2],
1280 },
1281 Primitive::Text {
1282 clip: None,
1283 layout: &layout,
1284 origin: [8.0, 64.0],
1285 },
1286 Primitive::Image {
1287 image: &image,
1288 rect: [128.0, 8.0, 160.0, 40.0],
1289 },
1290 Primitive::Rect {
1291 rect: [128.0, 8.0, 132.0, 12.0],
1292 color: [0.0, 0.0, 1.0, 1.0],
1293 },
1294 Primitive::Image {
1295 image: &transparent_edge,
1296 rect: [176.0, 8.0, 208.0, 40.0],
1297 },
1298 Primitive::RoundedRect {
1299 rect: [128.0, 48.0, 148.0, 68.0],
1300 radius: [5.0, 10.0],
1301 stroke: None,
1302 color: [1.0, 0.0, 0.0, 1.0],
1303 },
1304 Primitive::RoundedRect {
1305 rect: [164.0, 48.0, 184.0, 68.0],
1306 radius: [5.0; 2],
1307 stroke: Some(Stroke::Solid(1.0)),
1308 color: [0.0, 0.0, 1.0, 1.0],
1309 },
1310 Primitive::RoundedRect {
1311 rect: [200.0, 48.0, 240.0, 68.0],
1312 radius: [3.0, 10.0],
1313 stroke: Some(Stroke::Dashed(1.0)),
1314 color: [0.0, 0.0, 1.0, 1.0],
1315 },
1316 Primitive::Tag {
1317 tag: &tags[0],
1318 origin: [128.125, 80.25],
1319 },
1320 Primitive::Tag {
1321 tag: &tags[1],
1322 origin: [148.125, 80.25],
1323 },
1324 Primitive::Tag {
1325 tag: &tags[2],
1326 origin: [168.125, 80.25],
1327 },
1328 Primitive::Tag {
1329 tag: &tags[3],
1330 origin: [188.125, 80.25],
1331 },
1332 Primitive::Segment {
1333 from: [226.0, 104.0],
1334 to: [240.0, 104.0],
1335 width: 4.0,
1336 round: true,
1337 color: [1.0, 0.0, 0.0, 1.0],
1338 },
1339 ];
1340 primitives.extend((0..9).map(|step| Primitive::Text {
1341 clip: None,
1342 layout: &phase_layout,
1343 origin: [8.0 + 24.0 * step as f32, 96.0 + 0.125 * step as f32],
1344 }));
1345 for pass in 0..6 {
1346 if pass == 2 {
1347 renderer.clear_glyph_cache();
1348 renderer.images.clear();
1349 } else if pass == 3 {
1350 renderer = Renderer::new(renderer.device.clone(), renderer.queue.clone(), format);
1351 }
1352 let clipping = [
1353 Primitive::Text {
1354 layout: &layout,
1355 origin: [0.0; 2],
1356 clip: (pass == 5).then_some([2.5, 1.25, 30.75, 20.5]),
1357 },
1358 Primitive::Rect {
1359 rect: [40.0, 40.0, 70.0, 50.0],
1360 color: [0.0, 0.0, 1.0, 1.0],
1361 },
1362 ];
1363 renderer
1364 .draw(
1365 &target.create_view(&Default::default()),
1366 viewport,
1367 if pass < 4 { &primitives } else { &clipping },
1368 )
1369 .unwrap();
1370 let mut encoder = renderer.device.create_command_encoder(&Default::default());
1371 encoder.copy_texture_to_buffer(
1372 target.as_image_copy(),
1373 wgpu::TexelCopyBufferInfo {
1374 buffer: &readback,
1375 layout: wgpu::TexelCopyBufferLayout {
1376 offset: 0,
1377 bytes_per_row: Some(512 * 4),
1378 rows_per_image: Some(256),
1379 },
1380 },
1381 wgpu::Extent3d {
1382 width: 512,
1383 height: 256,
1384 depth_or_array_layers: 1,
1385 },
1386 );
1387 renderer.queue.submit([encoder.finish()]);
1388 let (sender, receiver) = std::sync::mpsc::channel();
1389 readback.map_async(wgpu::MapMode::Read, .., move |result| {
1390 sender.send(result).unwrap();
1391 });
1392 renderer
1393 .device
1394 .poll(wgpu::PollType::Wait {
1395 submission_index: None,
1396 timeout: Some(Duration::from_secs(5)),
1397 })
1398 .unwrap();
1399 receiver
1400 .recv_timeout(Duration::from_secs(5))
1401 .unwrap()
1402 .unwrap();
1403 captures.push(readback.get_mapped_range(..).unwrap().to_vec());
1404 readback.unmap();
1405 }
1406 let centroids: Vec<_> = (0..9)
1407 .map(|step| {
1408 let mut weight = 0.0_f64;
1409 let mut moment = 0.0_f64;
1410 for y in 212..252 {
1411 for x in (40 + 48 * step)..(72 + 48 * step) {
1412 let value = captures[0][(y * 512 + x) * 4];
1413 let coverage = 1.0 - f64::from(colorref(u32::from(value))[0]);
1414 weight += coverage;
1415 moment += coverage * y as f64;
1416 }
1417 }
1418 assert!(weight > 10.0, "phase sample {step} was not painted");
1419 moment / weight
161 let mut primitives: Vec<_> = icons
162 .iter()
163 .enumerate()
164 .map(|(index, icon)| Primitive::Icon {
165 sources: tag_sources(*icon),
166 origin: [2.0 + 16.0 * index as f32, 2.0],
167 size: crate::outline::ParagraphTag::SIZE,
168 opacity: 1.0,
1420169 })
1421170 .collect();
1422 let pixel = |x: usize, y: usize| &captures[0][(y * 512 + x) * 4..(y * 512 + x) * 4 + 4];
1423 assert_eq!(pixel(490, 232), [255, 0, 0, 255]);
1424 assert_eq!(pixel(473, 232), [255, 0, 0, 255]);
1425 assert_eq!(pixel(490, 238), [255; 4]);
1426 assert_eq!(pixel(472, 228), [255; 4]);
1427 assert_eq!(pixel(300, 140), [255, 0, 0, 255]);
1428 assert_eq!(pixel(280, 120), [255; 4]);
1429 assert_eq!(pixel(282, 124), [255; 4]);
1430 assert_eq!(pixel(281, 140), [255, 0, 0, 255]);
1431 assert_eq!(pixel(372, 140), [255; 4]);
1432 assert_eq!(pixel(372, 120), [0, 0, 255, 255]);
1433 assert_eq!(pixel(352, 120), [255; 4]);
1434 assert_eq!(pixel(424, 120), [255; 4]);
1435 assert_eq!(pixel(464, 140), [255; 4]);
1436 assert!((440..480).any(|x| pixel(x, 120)[0] < 40));
1437 assert!((440..480).any(|x| pixel(x, 120)[0] > 250));
1438 for x in 440..472 {
1439 assert!(pixel(x, 120)[0].abs_diff(pixel(x + 8, 120)[0]) <= 1);
1440 }
1441 for (step, centroid) in centroids.iter().enumerate() {
1442 let movement = centroid - centroids[0];
1443 assert!(
1444 (movement - step as f64 * 0.25).abs() < 0.06,
1445 "quarter-pixel step {step} moved the glyph by {movement} px"
1446 );
1447 }
1448 assert!(!renderer.glyphs.is_empty());
1449 assert!(renderer.glyphs.len() <= MAX_GLYPHS);
1450 assert_eq!(
1451 renderer
1452 .glyphs
1453 .keys()
1454 .filter(|key| matches!(key, AtlasKey::Tag { .. }))
1455 .count(),
1456 4
1457 );
1458 for x in [280, 320, 360, 400] {
1459 let colored = (184..210)
1460 .flat_map(|y| (x..x + 26).map(move |x| (y * 512 + x) * 4))
1461 .filter(|offset| captures[0][*offset..*offset + 3].iter().any(|v| *v < 180))
1462 .count();
1463 assert!(colored > 10, "tag at {x} was not painted");
1464 }
1465 let count = renderer.glyphs.len();
171 primitives.push(Primitive::Text {
172 text: &text,
173 origin: [2.0, 20.0],
174 clip: None,
175 });
176 let viewport = Viewport {
177 size,
178 scale: 2.0,
179 origin: [0.0; 2],
180 };
1466181 renderer
1467182 .draw(
1468183 &target.create_view(&Default::default()),
1469 viewport,
1470 &[Primitive::Tag {
1471 tag: &tags[0],
1472 origin: [10000.0; 2],
1473 }],
184 size,
185 [1.0; 4],
186 &[viewport.layer(&primitives)],
1474187 )
1475188 .unwrap();
1476 assert_eq!(renderer.glyphs.len(), count);
1477 assert!(
1478 captures[0]
1479 .chunks_exact(4)
1480 .filter(|pixel| pixel[0] < 200)
1481 .count()
1482 > 100
1483 );
1484 assert_eq!(captures[0], captures[1]);
1485 assert_eq!(captures[0], captures[2]);
1486 assert_eq!(captures[0], captures[3]);
1487 assert_ne!(captures[4], captures[5]);
1488 for y in 0..256 {
1489 for x in 0..512 {
1490 let offset = (y * 512 + x) * 4;
1491 let expected = if ((29..86).contains(&x) && (26..65).contains(&y))
1492 || ((104..164).contains(&x) && (104..124).contains(&y))
1493 {
1494 &captures[4][offset..offset + 4]
1495 } else {
1496 &[255; 4]
1497 };
1498 assert_eq!(
1499 &captures[5][offset..offset + 4],
1500 expected,
1501 "clipping at {x},{y}"
1502 );
1503 }
1504 }
1505 for (x, y, color) in [
1506 (288, 48, [255, 0, 0, 255]),
1507 (336, 96, [255; 4]),
1508 (284, 44, [0, 0, 255, 255]),
1509 ] {
1510 let offset = (y * 512 + x) * 4;
1511 assert_eq!(captures[0][offset..offset + 4], color);
1512 }
1513 let edge = &captures[0][(48 * 512 + 408) * 4..(48 * 512 + 408) * 4 + 4];
1514 assert_eq!(edge[0], 255, "{edge:?}");
1515 assert_eq!(edge[1], edge[2], "{edge:?}");
1516 assert!((180..=200).contains(&edge[1]), "{edge:?}");
1517 assert_eq!(renderer.images.len(), 2);
1518 for primitive in [
1519 Primitive::RoundedRect {
1520 rect: [-1e38, 0.0, 1e38, 1e38],
1521 radius: [3.0; 2],
1522 stroke: Some(Stroke::Dashed(1.0)),
1523 color: [1.0; 4],
1524 },
1525 Primitive::RoundedRect {
1526 rect: [0.0, 0.0, 10.0, 10.0],
1527 radius: [f32::NAN, 2.0],
1528 stroke: None,
1529 color: [1.0; 4],
1530 },
1531 Primitive::RoundedRect {
1532 rect: [0.0, 0.0, 10.0, 10.0],
1533 radius: [2.0; 2],
1534 stroke: Some(Stroke::Solid(0.0)),
1535 color: [1.0; 4],
1536 },
1537 Primitive::RoundedRect {
1538 rect: [0.0, 0.0, 10.0, 10.0],
1539 radius: [2.0; 2],
1540 stroke: Some(Stroke::Dashed(f32::NAN)),
1541 color: [1.0; 4],
1542 },
1543 Primitive::Rect {
1544 rect: [0.0, 0.0, f32::NAN, 1.0],
1545 color: [1.0; 4],
1546 },
1547 Primitive::Rect {
1548 rect: [2.0, 0.0, 1.0, 1.0],
1549 color: [1.0; 4],
1550 },
1551 Primitive::Text {
1552 clip: None,
1553 layout: &layout,
1554 origin: [f32::MAX; 2],
1555 },
1556 Primitive::Text {
1557 clip: Some([0.0, 0.0, f32::NAN, 1.0]),
1558 layout: &layout,
1559 origin: [0.0; 2],
189 let mut encoder = renderer.device.create_command_encoder(&Default::default());
190 encoder.copy_texture_to_buffer(
191 target.as_image_copy(),
192 wgpu::TexelCopyBufferInfo {
193 buffer: &readback,
194 layout: wgpu::TexelCopyBufferLayout {
195 offset: 0,
196 bytes_per_row: Some(size[0] * 4),
197 rows_per_image: Some(size[1]),
198 },
1560199 },
1561 Primitive::Text {
1562 clip: Some([2.0, 0.0, 1.0, 1.0]),
1563 layout: &layout,
1564 origin: [0.0; 2],
200 wgpu::Extent3d {
201 width: size[0],
202 height: size[1],
203 depth_or_array_layers: 1,
1565204 },
1566 ] {
1567 assert!(matches!(
1568 renderer.draw(
1569 &target.create_view(&Default::default()),
1570 viewport,
1571 &[primitive]
1572 ),
1573 Err(RenderError::InvalidPrimitive)
1574 ));
1575 }
1576 let too_many: Vec<_> = (0..=MAX_IMAGES)
1577 .map(|_| RasterImage::new([1, 1], vec![255; 4]).unwrap())
1578 .collect();
1579 let primitives: Vec<_> = too_many
1580 .iter()
1581 .map(|image| Primitive::Image {
1582 image,
1583 rect: [0.0, 0.0, 1.0, 1.0],
1584 })
1585 .collect();
1586 assert!(matches!(
1587 renderer.draw(
1588 &target.create_view(&Default::default()),
1589 viewport,
1590 &primitives
1591 ),
1592 Err(RenderError::ImageBudget)
1593 ));
1594 let primitives: Vec<_> = too_many
1595 .iter()
1596 .map(|image| Primitive::Image {
1597 image,
1598 rect: [-20.0, -20.0, -19.0, -19.0],
1599 })
1600 .collect();
205 );
206 renderer.queue.submit([encoder.finish()]);
207 readback.map_async(wgpu::MapMode::Read, .., |result| result.unwrap());
1601208 renderer
1602 .draw(
1603 &target.create_view(&Default::default()),
1604 viewport,
1605 &primitives,
1606 )
209 .device
210 .poll(wgpu::PollType::Wait {
211 submission_index: None,
212 timeout: Some(Duration::from_secs(5)),
213 })
1607214 .unwrap();
1608 assert_eq!(renderer.images.len(), 2);
1609 let mut seen = HashSet::from([image.pixels.id(), transparent_edge.pixels.id()]);
1610 let mut previous = None;
1611 for _ in 0..5 {
1612 let large = RasterImage::new([2048; 2], vec![255; 2048 * 2048 * 4]).unwrap();
1613 seen.insert(large.pixels.id());
1614 let mut primitives = vec![Primitive::Image {
1615 image: &large,
1616 rect: [0.0, 0.0, 16.0, 16.0],
1617 }];
1618 if let Some(image) = &previous {
1619 primitives.push(Primitive::Image {
1620 image,
1621 rect: [16.0, 0.0, 32.0, 16.0],
1622 });
1623 }
1624 renderer
1625 .draw(
1626 &target.create_view(&Default::default()),
1627 viewport,
1628 &primitives,
1629 )
1630 .unwrap();
1631 assert!(renderer.images.contains_key(&large.pixels.id()));
1632 if let Some(image) = &previous {
1633 assert!(renderer.images.contains_key(&image.pixels.id()));
1634 }
1635 assert!(renderer.images.values().map(|i| i.bytes).sum::<u64>() <= MAX_IMAGE_BYTES);
1636 previous = Some(large);
215 let pixels = readback.get_mapped_range(..).unwrap().to_vec();
216 let pixel = |x: u32, y: u32| {
217 let offset = ((y * size[0] + x) * 4) as usize;
218 [pixels[offset], pixels[offset + 1], pixels[offset + 2]]
219 };
220 for index in 0..icons.len() as u32 {
221 let painted = (4..28)
222 .flat_map(|y| (4 + 32 * index..28 + 32 * index).map(move |x| (x, y)))
223 .filter(|(x, y)| pixel(*x, *y).iter().any(|v| *v < 200))
224 .count();
225 assert!(painted > 20, "tag {index} was not painted");
1637226 }
1638 assert!(renderer.images.len() <= MAX_IMAGES);
1639 assert!(seen.iter().any(|id| !renderer.images.contains_key(id)));
1640 renderer.images.clear();
1641 renderer
1642 .draw(
1643 &target.create_view(&Default::default()),
1644 viewport,
1645 &[Primitive::Image {
1646 image: &image,
1647 rect: [128.0, 8.0, 160.0, 40.0],
1648 }],
1649 )
1650 .unwrap();
1651 assert_eq!(renderer.images.len(), 1);
1652 assert!(renderer.images.contains_key(&image.pixels.id()));
227 let red = (40..64)
228 .flat_map(|y| (4..120).map(move |x| (x, y)))
229 .filter(|(x, y)| {
230 let [r, g, b] = pixel(*x, *y);
231 r > 200 && g < 80 && b < 80
232 })
233 .count();
234 assert!(red > 50, "text colour or underline was not painted");
1653235 }
1654236}
crates/canvas/src/gpu/page.rs+74-31
......@@ -1,4 +1,4 @@
1use super::{Primitive, RasterImage, RenderError, colorref};
1use super::colorref;
22use crate::editor::ReadOnlyObject;
33use crate::editor::page::Content;
44use crate::{
......@@ -6,6 +6,7 @@ use crate::{
66 editor::{CanvasEditor, EditorError},
77 layout::{LayoutError, TextEngine},
88};
9use draw::{Primitive, RasterImage, RenderError};
910use onestore::page::Page;
1011use std::fmt;
1112
......@@ -197,8 +198,8 @@ impl PageScene {
197198 for (id, encoded) in payloads {
198199 let image = RasterImage::decode(encoded.ok_or(SceneError::MissingImage)?)
199200 .map_err(SceneError::Image)?;
200 bytes += image.pixels.as_ref().len() as u64;
201 if bytes > super::MAX_IMAGE_BYTES {
201 bytes += image.pixels().len() as u64;
202 if bytes > draw::MAX_IMAGE_BYTES {
202203 return Err(SceneError::Image(RenderError::ImageBudget));
203204 }
204205 if images.insert(id, image).is_some() {
......@@ -253,7 +254,7 @@ impl PageScene {
253254 }
254255 if let Some(label) = object.label() {
255256 primitives.push(Primitive::Text {
256 layout: &label.text,
257 text: &label.text,
257258 origin: [origin[0] + label.origin[0], origin[1] + label.origin[1]],
258259 clip: None,
259260 });
......@@ -508,7 +509,7 @@ impl PageScene {
508509 });
509510 primitives.push(Primitive::Text {
510511 clip: None,
511 layout: &object.label,
512 text: &object.label,
512513 origin: [rect[0] + 8.0, rect[1] + 8.0],
513514 });
514515 continue;
......@@ -632,7 +633,7 @@ impl crate::outline::OutlineLayout {
632633 primitives.push(match item {
633634 crate::math::MathItem::Text { layout, origin } => Primitive::Text {
634635 clip,
635 layout,
636 text: layout,
636637 origin: [x + origin[0], y + origin[1]],
637638 },
638639 // As pen strokes, so hairline rules keep a device pixel.
......@@ -665,21 +666,26 @@ impl crate::outline::OutlineLayout {
665666 }
666667 None => primitives.push(Primitive::Text {
667668 clip,
668 layout: &paragraph.text,
669 text: &paragraph.text,
669670 origin: [x, y],
670671 }),
671672 }
672673 for (layout, marker) in &paragraph.markers {
673674 primitives.push(Primitive::Text {
674675 clip,
675 layout,
676 text: layout,
676677 origin: [origin[0] + marker[0], y + marker[1]],
677678 });
678679 }
679680 for tag in &paragraph.tags {
680 primitives.push(Primitive::Tag {
681 tag,
682 origin: [origin[0] + self.tag_column_offset(), y],
681 primitives.push(Primitive::Icon {
682 sources: super::tag_sources(tag.icon),
683 origin: [
684 origin[0] + self.tag_column_offset() + tag.origin[0],
685 y + tag.origin[1],
686 ],
687 size: crate::outline::ParagraphTag::SIZE,
688 opacity: if tag.disabled { 0.45 } else { 1.0 },
683689 });
684690 }
685691 }
......@@ -702,7 +708,7 @@ impl crate::outline::OutlineLayout {
702708 bounds[3] + origin[1] + 0.375,
703709 ],
704710 radius: [3.6; 2],
705 stroke: Some(super::Stroke::Solid(0.75)),
711 stroke: Some(draw::Stroke::Solid(0.75)),
706712 color,
707713 });
708714 for cell in &table.cells {
......@@ -738,6 +744,7 @@ impl crate::outline::OutlineLayout {
738744mod tests {
739745 use super::*;
740746 use crate::editor::TextOutline;
747 use crate::gpu::painted_layout;
741748 use onestore::page::text::Paragraph;
742749 use onestore::page::{Image, PageObject};
743750 use onestore::{ExGuid, document::Layout};
......@@ -1154,7 +1161,7 @@ mod tests {
11541161 primitives
11551162 .into_iter()
11561163 .filter_map(|primitive| match primitive {
1157 Primitive::Image { image, rect } => Some((image.pixels.id(), rect)),
1164 Primitive::Image { image, rect } => Some((image.id(), rect)),
11581165 _ => None,
11591166 })
11601167 .collect::<Vec<_>>()
......@@ -1555,7 +1562,7 @@ mod tests {
15551562 for (_, paragraph) in outline.layouts() {
15561563 primitives.push(Primitive::Text {
15571564 clip: None,
1558 layout: &paragraph.text,
1565 text: &paragraph.text,
15591566 origin: [
15601567 outline.origin()[0] + paragraph.origin[0] + offset[0],
15611568 outline.origin()[1] + paragraph.origin[1] + offset[1],
......@@ -1647,19 +1654,31 @@ mod tests {
16471654 (
16481655 Primitive::Text {
16491656 clip: None,
1650 layout: a,
1657 text: a,
16511658 origin: x,
16521659 },
16531660 Primitive::Text {
16541661 clip: None,
1655 layout: b,
1662 text: b,
16561663 origin: y,
16571664 },
16581665 ) => {
16591666 assert_eq!(x, y);
16601667 assert_eq!(
1661 a.lines().next().unwrap().0.metrics().advance,
1662 b.lines().next().unwrap().0.metrics().advance
1668 painted_layout(*a)
1669 .lines()
1670 .next()
1671 .unwrap()
1672 .0
1673 .metrics()
1674 .advance,
1675 painted_layout(*b)
1676 .lines()
1677 .next()
1678 .unwrap()
1679 .0
1680 .metrics()
1681 .advance
16631682 );
16641683 }
16651684 (
......@@ -1667,7 +1686,7 @@ mod tests {
16671686 Primitive::Image { image: b, rect: y },
16681687 ) => {
16691688 assert_eq!(x, y);
1670 assert_eq!(a.pixels.as_ref(), b.pixels.as_ref());
1689 assert_eq!(a.pixels(), b.pixels());
16711690 }
16721691 _ => panic!("paint order changed"),
16731692 }
......@@ -1675,39 +1694,63 @@ mod tests {
16751694 let Primitive::Image { image, .. } = &actual[1] else {
16761695 panic!()
16771696 };
1678 image_id = image.pixels.id();
1697 image_id = image.id();
16791698 }
16801699 let original = editor.active_outline().document().clone();
16811700 editor.insert(&mut engine, "new words ").unwrap();
16821701 {
16831702 let actual = paint_editor(&scene, &editor);
1684 let Primitive::Text { layout, .. } = &actual[0] else {
1703 let Primitive::Text { text, .. } = &actual[0] else {
16851704 panic!()
16861705 };
1687 let Primitive::Text { layout: before, .. } = &reference_primitives[0] else {
1706 let Primitive::Text { text: before, .. } = &reference_primitives[0] else {
16881707 panic!()
16891708 };
16901709 assert!(
1691 layout.lines().next().unwrap().0.metrics().advance
1692 > before.lines().next().unwrap().0.metrics().advance
1710 painted_layout(*text)
1711 .lines()
1712 .next()
1713 .unwrap()
1714 .0
1715 .metrics()
1716 .advance
1717 > painted_layout(*before)
1718 .lines()
1719 .next()
1720 .unwrap()
1721 .0
1722 .metrics()
1723 .advance
16931724 );
16941725 let Primitive::Image { image, .. } = &actual[1] else {
16951726 panic!()
16961727 };
1697 assert_eq!(image.pixels.id(), image_id);
1728 assert_eq!(image.id(), image_id);
16981729 }
16991730 editor.undo(&mut engine).unwrap();
17001731 assert_eq!(editor.active_outline().document(), &original);
17011732 let actual = paint_editor(&scene, &editor);
1702 let Primitive::Text { layout, .. } = &actual[0] else {
1733 let Primitive::Text { text, .. } = &actual[0] else {
17031734 panic!()
17041735 };
1705 let Primitive::Text { layout: before, .. } = &reference_primitives[0] else {
1736 let Primitive::Text { text: before, .. } = &reference_primitives[0] else {
17061737 panic!()
17071738 };
17081739 assert_eq!(
1709 layout.lines().next().unwrap().0.metrics().advance,
1710 before.lines().next().unwrap().0.metrics().advance
1740 painted_layout(*text)
1741 .lines()
1742 .next()
1743 .unwrap()
1744 .0
1745 .metrics()
1746 .advance,
1747 painted_layout(*before)
1748 .lines()
1749 .next()
1750 .unwrap()
1751 .0
1752 .metrics()
1753 .advance
17111754 );
17121755 assert!(matches!(
17131756 scene.append_primitives(&mut Vec::new(), [0.0; 2]),
......@@ -1846,8 +1889,8 @@ mod tests {
18461889 };
18471890 assert_eq!(*a, [2.0, 5.0, 5.0, 9.0]);
18481891 assert_eq!(*b, [12.0, -15.0, 15.0, -11.0]);
1849 assert_eq!(first.pixels.id(), second.pixels.id());
1850 assert_eq!(first.pixels.as_ref(), [255, 0, 0, 255]);
1892 assert_eq!(first.id(), second.id());
1893 assert_eq!(first.pixels(), [255, 0, 0, 255]);
18511894 }
18521895
18531896 #[test]
crates/canvas/src/gpu/profile.rs+25-26
......@@ -1,7 +1,9 @@
11use super::*;
22use crate::layout::TextEngine;
3use draw::Renderer;
34use onestore::page::Page;
45use onestore::page::text::Paragraph;
6use parley::fontique::Blob;
57use std::{
68 sync::Arc,
79 time::{Duration, Instant},
......@@ -54,10 +56,11 @@ fn renderer_cost() {
5456 });
5557 let view = target.create_view(&Default::default());
5658 let mut renderer = Renderer::new(device, queue, format);
59 let occupancy = renderer.occupancy();
5760 eprintln!(
5861 "canvas_gpu_allocated_bytes\tatlas\t{}\tvertex_buffer\t{}\ttarget\t{}",
59 u64::from(renderer.atlas.width()) * u64::from(renderer.atlas.height()) * 4,
60 renderer.vertex_buffer.size(),
62 occupancy.atlas_bytes,
63 occupancy.vertex_buffer_bytes,
6164 u64::from(size[0]) * u64::from(size[1]) * 4
6265 );
6366 for phase in [
......@@ -74,12 +77,12 @@ fn renderer_cost() {
7477 }
7578 if phase.starts_with("offscreen") {
7679 renderer.clear_glyph_cache();
77 renderer.images.clear();
80 renderer.clear_images();
7881 }
7982 for sample in 0..32 {
8083 if phase == "evict" {
8184 renderer.clear_glyph_cache();
82 renderer.images.clear();
85 renderer.clear_images();
8386 }
8487 let mut viewport = Viewport {
8588 size,
......@@ -100,14 +103,16 @@ fn renderer_cost() {
100103 if let Some(scene) = &scene {
101104 scene.append_primitives(&mut primitives, [0.0; 2]).unwrap();
102105 } else {
103 primitives.push(Primitive::Text {
106 primitives.push(draw::Primitive::Text {
104107 clip: None,
105 layout: layout.as_ref().unwrap(),
108 text: layout.as_ref().unwrap(),
106109 origin: [36.0, 90.0],
107110 });
108111 }
109112 let start = Instant::now();
110 renderer.draw(&view, viewport, &primitives).unwrap();
113 renderer
114 .draw(&view, size, [1.0; 4], &[viewport.layer(&primitives)])
115 .unwrap();
111116 let submitted = start.elapsed().as_nanos();
112117 renderer
113118 .device
......@@ -117,26 +122,20 @@ fn renderer_cost() {
117122 })
118123 .unwrap();
119124 let completed = start.elapsed().as_nanos();
120 let image_bytes: u64 = renderer.images.values().map(|image| image.bytes).sum();
121 let atlas_texels: u64 = renderer
122 .glyphs
123 .values()
124 .flatten()
125 .map(|glyph| u64::from(glyph.width) * u64::from(glyph.height))
126 .sum();
127 assert!(renderer.glyphs.len() <= MAX_GLYPHS);
128 assert!(renderer.images.len() <= MAX_IMAGES && image_bytes <= MAX_IMAGE_BYTES);
129 assert!(renderer.vertices.len() <= MAX_VERTICES);
125 let occupancy = renderer.occupancy();
126 assert!(occupancy.within_budget);
130127 eprintln!(
131 "canvas_gpu_sample\t{phase}\t{sample}\t{submitted}\t{completed}\t{}\t{atlas_texels}\t{}\t{image_bytes}\t{}\t{}\t{}\t{}\t{}\t{}",
132 renderer.glyphs.len(),
133 renderer.images.len(),
134 renderer.vertices.len(),
135 renderer.batches.len(),
136 renderer.vertices.capacity() * size_of::<Vertex>(),
137 renderer.batches.capacity() * size_of::<Batch>(),
138 renderer.glyphs.capacity(),
139 renderer.images.capacity()
128 "canvas_gpu_sample\t{phase}\t{sample}\t{submitted}\t{completed}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}",
129 occupancy.glyphs,
130 occupancy.atlas_texels,
131 occupancy.images,
132 occupancy.image_bytes,
133 occupancy.vertices,
134 occupancy.batches,
135 occupancy.vertex_capacity_bytes,
136 occupancy.batch_capacity_bytes,
137 occupancy.glyph_capacity,
138 occupancy.image_capacity
140139 );
141140 }
142141 }
crates/canvas/src/gpu/tags.rs deleted-140
......@@ -1,140 +0,0 @@
1use crate::outline::TagIcon;
2use swash::{
3 scale::image::{Content, Image},
4 zeno::{Mask, Placement, Transform, Vector},
5};
6
7pub(crate) fn rasterize(icon: TagIcon, size: f32, phase: [u8; 2]) -> Image {
8 let (source, overlay) = match icon {
9 TagIcon::CheckBox { checked } => (
10 include_str!("../../assets/tags/checkbox.svg"),
11 checked.then_some(include_str!("../../assets/tags/checkmark.svg")),
12 ),
13 TagIcon::Question => (include_str!("../../assets/tags/question.svg"), None),
14 TagIcon::Music => (include_str!("../../assets/tags/music.svg"), None),
15 TagIcon::Exclamation => (include_str!("../../assets/tags/exclamation.svg"), None),
16 TagIcon::RedSquare => (include_str!("../../assets/tags/red-square.svg"), None),
17 TagIcon::YellowSquare => (include_str!("../../assets/tags/yellow-square.svg"), None),
18 TagIcon::BlueSquare => (include_str!("../../assets/tags/blue-square.svg"), None),
19 };
20 let width = (size + 1.0).ceil() as u32;
21 let mut pixels = vec![[0.0_f32; 4]; (width * width) as usize];
22 for source in std::iter::once(source).chain(overlay) {
23 let svg = roxmltree::Document::parse(source).expect("Bundled tag SVG must be valid");
24 assert_eq!(svg.root_element().attribute("viewBox"), Some("0 0 16 16"));
25 for path in svg.descendants().filter(|node| node.has_tag_name("path")) {
26 let gradient = path
27 .attribute("fill")
28 .unwrap()
29 .strip_prefix("url(#")
30 .unwrap()
31 .strip_suffix(')')
32 .unwrap();
33 let gradient = svg
34 .descendants()
35 .find(|node| node.attribute("id") == Some(gradient))
36 .unwrap();
37 let mut stops = gradient
38 .children()
39 .filter(|node| node.has_tag_name("stop"))
40 .map(|node| {
41 let color = u32::from_str_radix(
42 node.attribute("stop-color")
43 .unwrap()
44 .strip_prefix('#')
45 .unwrap(),
46 16,
47 )
48 .unwrap();
49 let [_, r, g, b] = color.to_be_bytes();
50 super::colorref(u32::from_le_bytes([r, g, b, 0]))
51 });
52 let top = stops.next().unwrap();
53 let bottom = stops.next().unwrap();
54 assert!(stops.next().is_none());
55 let (mask, _) = Mask::new(path.attribute("d").unwrap())
56 .transform(Some(Transform::scale(size / 16.0, size / 16.0)))
57 .render_offset(Vector::new(phase[0] as f32 * 0.25, phase[1] as f32 * 0.25))
58 .size(width, width)
59 .render();
60 for (index, (pixel, coverage)) in pixels.iter_mut().zip(mask).enumerate() {
61 let alpha = f32::from(coverage) / 255.0;
62 let y = ((index / width as usize) as f32 / size).min(1.0);
63 for channel in 0..3 {
64 let color = top[channel] + (bottom[channel] - top[channel]) * y;
65 pixel[channel] = color * alpha + pixel[channel] * (1.0 - alpha);
66 }
67 pixel[3] = alpha + pixel[3] * (1.0 - alpha);
68 }
69 }
70 }
71 let data = pixels
72 .into_iter()
73 .flat_map(|pixel| {
74 let alpha = pixel[3];
75 let channel = |value: f32| {
76 let value = if alpha > 0.0 { value / alpha } else { 0.0 };
77 super::srgb_byte(value)
78 };
79 [
80 channel(pixel[0]),
81 channel(pixel[1]),
82 channel(pixel[2]),
83 (alpha * 255.0).round() as u8,
84 ]
85 })
86 .collect();
87 Image {
88 content: Content::Color,
89 placement: Placement {
90 left: 0,
91 top: 0,
92 width,
93 height: width,
94 },
95 data,
96 ..Image::default()
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn tag_masks_keep_transparency_and_checked_state_across_scales() {
106 for size in [8.0, 16.0, 32.0, 57.5] {
107 for phase in [[0, 0], [1, 2], [3, 3]] {
108 let mut masks = Vec::new();
109 for icon in [
110 TagIcon::CheckBox { checked: false },
111 TagIcon::CheckBox { checked: true },
112 TagIcon::Question,
113 TagIcon::Music,
114 TagIcon::Exclamation,
115 TagIcon::RedSquare,
116 TagIcon::YellowSquare,
117 TagIcon::BlueSquare,
118 ] {
119 let image = rasterize(icon, size, phase);
120 assert_eq!(
121 image.data.len(),
122 (image.placement.width * image.placement.height * 4) as usize
123 );
124 assert!(image.data.chunks_exact(4).any(|p| p[3] > 0));
125 assert!(image.data.chunks_exact(4).any(|p| p[3] < 255));
126 assert!(
127 image
128 .data
129 .chunks_exact(4)
130 .filter(|p| p[3] == 0)
131 .all(|p| p[..3] == [0; 3])
132 );
133 masks.push(image.data);
134 }
135 assert_ne!(masks[0], masks[1]);
136 assert!(masks[1].chunks_exact(4).any(|p| p[0] > p[2] && p[3] > 0));
137 }
138 }
139 }
140}
crates/canvas/src/interaction/mod.rs+2-1
......@@ -9,7 +9,7 @@ mod scroll;
99#[cfg(test)]
1010mod tests;
1111
12use crate::gpu::{Primitive, Stroke, Viewport, page::PageScene};
12use crate::gpu::{Viewport, page::PageScene};
1313use crate::{
1414 date::DateField,
1515 editor::{
......@@ -17,6 +17,7 @@ use crate::{
1717 },
1818 layout::TextEngine,
1919};
20use draw::{Primitive, Stroke};
2021use std::{
2122 error::Error,
2223 time::{Duration, Instant},
crates/canvas/src/interaction/scroll.rs+2-1
......@@ -1,4 +1,5 @@
1use crate::gpu::{Primitive, Viewport};
1use crate::gpu::Viewport;
2use draw::Primitive;
23
34pub struct Scroll {
45 pub min: [f32; 2],
crates/canvas/src/interaction/tests.rs+13-9
......@@ -1,5 +1,6 @@
11use super::*;
22use crate::document::TextDocument;
3use crate::gpu::painted_layout;
34use onestore::document::Format;
45use onestore::page::Page;
56use onestore::page::text::Paragraph;
......@@ -385,7 +386,12 @@ fn imported_page_widgets_and_accessibility_follow_edits() {
385386 .unwrap()
386387 .into_iter()
387388 .filter_map(|primitive| match primitive {
388 Primitive::Tag { tag, origin } => Some((tag.clone(), origin)),
389 Primitive::Icon {
390 sources,
391 origin,
392 opacity,
393 ..
394 } => Some(((sources, opacity), origin)),
389395 _ => None,
390396 })
391397 .collect::<Vec<_>>()
......@@ -427,9 +433,7 @@ fn imported_page_widgets_and_accessibility_follow_edits() {
427433 let edited_tags = tags(&editor);
428434 assert_eq!(edited_tags.len(), original_tags.len());
429435 for ((edited, _), (original, _)) in edited_tags.iter().zip(&original_tags) {
430 assert_eq!(edited.icon, original.icon);
431 assert_eq!(edited.label, original.label);
432 assert_eq!(edited.disabled, original.disabled);
436 assert_eq!(edited, original);
433437 }
434438 }
435439 editor.undo(&mut engine).unwrap();
......@@ -891,14 +895,14 @@ fn editable_tables_paint_borders_before_selection_and_cell_text() {
891895 .unwrap();
892896 let color = crate::gpu::colorref(0x00a3a3a3);
893897 assert!(matches!(primitives[0], Primitive::RoundedRect {
894 stroke: Some(crate::gpu::Stroke::Solid(0.75)), color: actual, ..
898 stroke: Some(draw::Stroke::Solid(0.75)), color: actual, ..
895899 } if actual == color));
896900 assert!(matches!(primitives[1], Primitive::Rect { color: actual, .. } if actual == color));
897901 assert!(matches!(primitives[2], Primitive::Rect { color, .. } if color == COLORS.selection));
898902 let painted = primitives
899903 .iter()
900904 .filter_map(|primitive| match primitive {
901 Primitive::Text { layout, origin, .. } => Some((layout.id(), *origin)),
905 Primitive::Text { text, origin, .. } => Some((painted_layout(*text).id(), *origin)),
902906 _ => None,
903907 })
904908 .collect::<Vec<_>>();
......@@ -948,7 +952,7 @@ fn resize_preview_paints_reflowed_text_without_changing_the_editor() {
948952 let painted = primitives
949953 .iter()
950954 .find_map(|primitive| match primitive {
951 Primitive::Text { layout, .. } => Some(*layout),
955 Primitive::Text { text, .. } => Some(painted_layout(*text)),
952956 _ => None,
953957 })
954958 .unwrap();
......@@ -1161,7 +1165,7 @@ fn overlapping_objects_follow_paint_order_through_creation_movement_and_undo() {
11611165 .unwrap()
11621166 .into_iter()
11631167 .filter_map(|p| match p {
1164 Primitive::Text { layout, .. } => Some(layout.id()),
1168 Primitive::Text { text, .. } => Some(painted_layout(text).id()),
11651169 _ => None,
11661170 })
11671171 .collect();
......@@ -1213,7 +1217,7 @@ fn overlapping_objects_follow_paint_order_through_creation_movement_and_undo() {
12131217 .unwrap()
12141218 .into_iter()
12151219 .filter_map(|p| match p {
1216 Primitive::Text { layout, .. } => Some(layout.id()),
1220 Primitive::Text { text, .. } => Some(painted_layout(text).id()),
12171221 _ => None,
12181222 })
12191223 .collect();
crates/draw/Cargo.toml created+17
......@@ -0,0 +1,17 @@
1[package]
2name = "draw"
3version = "0.1.0"
4edition = "2024"
5publish = false
6
7[dependencies]
8bytemuck = { version = "1.25", features = ["derive"] }
9image = { version = "0.25", default-features = false, features = ["gif", "jpeg", "png"] }
10parley.workspace = true
11roxmltree = "0.21"
12swash = "0.2.10"
13wgpu.workspace = true
14
15[dev-dependencies]
16pollster = "0.4"
17png = "0.18"
crates/draw/src/draw.wgsl created+87
......@@ -0,0 +1,87 @@
1struct Vertex {
2 @builtin(position) position: vec4<f32>,
3 @location(0) uv: vec2<f32>,
4 @location(1) color: vec4<f32>,
5 @location(2) local: vec2<f32>,
6 @location(3) @interpolate(flat) shape: vec4<f32>,
7 @location(4) @interpolate(flat) stroke: f32,
8}
9
10@group(0) @binding(0) var atlas: texture_2d<f32>;
11@group(0) @binding(1) var atlas_sampler: sampler;
12
13@vertex
14fn vertex(@location(0) position: vec2<f32>, @location(1) uv: vec2<f32>,
15 @location(2) color: vec4<f32>, @location(3) local: vec2<f32>,
16 @location(4) shape: vec4<f32>, @location(5) stroke: f32) -> Vertex {
17 return Vertex(vec4<f32>(position, 0.0, 1.0), uv, color, local, shape, stroke);
18}
19
20fn ellipse_arc(angle: f32, radius: vec2<f32>) -> f32 {
21 if radius.x == radius.y {
22 return angle * radius.x;
23 }
24 // Four-point Gauss-Legendre quadrature.
25 let nodes = vec4<f32>(-0.86113631, -0.33998104, 0.33998104, 0.86113631);
26 let weights = vec4<f32>(0.34785485, 0.65214515, 0.65214515, 0.34785485);
27 let half = angle * 0.5;
28 let scale = max(radius.x, radius.y);
29 var sum = 0.0;
30 for (var i = 0u; i < 4u; i += 1u) {
31 let t = half * (nodes[i] + 1.0);
32 sum += weights[i] * length((radius / scale) * vec2<f32>(sin(t), cos(t)));
33 }
34 return half * sum * scale;
35}
36
37@fragment
38fn fragment(input: Vertex) -> @location(0) vec4<f32> {
39 var color = textureSample(atlas, atlas_sampler, input.uv) * input.color;
40 if input.shape.x > 0.0 && input.shape.y > 0.0 {
41 let q = abs(input.local) - input.shape.xy + input.shape.zw;
42 var distance: f32;
43 if input.shape.z == input.shape.w {
44 distance = length(max(q, vec2<f32>(0.0))) + min(max(q.x, q.y), 0.0) - input.shape.z;
45 } else if all(q > vec2<f32>(0.0)) {
46 let radius = max(input.shape.zw, vec2<f32>(0.0001));
47 let normalized = q / radius;
48 let k0 = length(normalized);
49 distance = k0 * (k0 - 1.0) / max(length(normalized / radius), 1e-20);
50 } else {
51 let edge = abs(input.local) - input.shape.xy;
52 distance = max(edge.x, edge.y);
53 }
54 var coverage = clamp(0.5 - distance, 0.0, 1.0);
55 let width = abs(input.stroke);
56 if width > 0.0 {
57 coverage *= clamp(distance + width + 0.5, 0.0, 1.0);
58 }
59 if input.stroke < 0.0 {
60 var radius = max(input.shape.zw - width * 0.5, vec2<f32>(0.0));
61 if any(radius == vec2<f32>(0.0)) {
62 radius = vec2<f32>(0.0);
63 }
64 let straight = input.shape.xy - width * 0.5 - radius;
65 let p = abs(input.local);
66 let quarter = straight.x + straight.y + ellipse_arc(1.57079632679, radius);
67 var along: f32;
68 if p.y <= straight.y {
69 along = p.y;
70 } else if p.x <= straight.x || radius.x == 0.0 {
71 along = quarter - p.x;
72 } else {
73 let angle = atan2((p.y - straight.y) / radius.y, (p.x - straight.x) / radius.x);
74 along = straight.y + ellipse_arc(angle, radius);
75 }
76 if input.local.x < 0.0 {
77 along = select(2.0 * quarter - along, 2.0 * quarter + along, input.local.y < 0.0);
78 } else if input.local.y < 0.0 {
79 along = 4.0 * quarter - along;
80 }
81 let phase = fract(along / (4.0 * width)) * (4.0 * width);
82 coverage *= clamp(width + 0.5 - abs(phase - 2.0 * width), 0.0, 1.0);
83 }
84 color *= coverage;
85 }
86 return color;
87}
crates/draw/src/icon.rs created+122
......@@ -0,0 +1,122 @@
1use swash::{
2 scale::image::{Content, Image},
3 zeno::{Mask, Placement, Transform, Vector},
4};
5
6/// Rasterizes 16×16 SVG sources over each other at `size` device pixels, offset by
7/// `phase` quarter pixels. Paths are filled with two-stop vertical gradients.
8pub(crate) fn rasterize(sources: &[&str], size: f32, phase: [u8; 2]) -> Image {
9 let width = (size + 1.0).ceil() as u32;
10 let mut pixels = vec![[0.0_f32; 4]; (width * width) as usize];
11 for source in sources {
12 let svg = roxmltree::Document::parse(source).expect("Bundled icon SVG must be valid");
13 assert_eq!(svg.root_element().attribute("viewBox"), Some("0 0 16 16"));
14 for path in svg.descendants().filter(|node| node.has_tag_name("path")) {
15 let gradient = path
16 .attribute("fill")
17 .unwrap()
18 .strip_prefix("url(#")
19 .unwrap()
20 .strip_suffix(')')
21 .unwrap();
22 let gradient = svg
23 .descendants()
24 .find(|node| node.attribute("id") == Some(gradient))
25 .unwrap();
26 let mut stops = gradient
27 .children()
28 .filter(|node| node.has_tag_name("stop"))
29 .map(|node| {
30 let color = u32::from_str_radix(
31 node.attribute("stop-color")
32 .unwrap()
33 .strip_prefix('#')
34 .unwrap(),
35 16,
36 )
37 .unwrap();
38 let [_, r, g, b] = color.to_be_bytes();
39 super::srgb(r, g, b)
40 });
41 let top = stops.next().unwrap();
42 let bottom = stops.next().unwrap();
43 assert!(stops.next().is_none());
44 let (mask, _) = Mask::new(path.attribute("d").unwrap())
45 .transform(Some(Transform::scale(size / 16.0, size / 16.0)))
46 .render_offset(Vector::new(phase[0] as f32 * 0.25, phase[1] as f32 * 0.25))
47 .size(width, width)
48 .render();
49 for (index, (pixel, coverage)) in pixels.iter_mut().zip(mask).enumerate() {
50 let alpha = f32::from(coverage) / 255.0;
51 let y = ((index / width as usize) as f32 / size).min(1.0);
52 for channel in 0..3 {
53 let color = top[channel] + (bottom[channel] - top[channel]) * y;
54 pixel[channel] = color * alpha + pixel[channel] * (1.0 - alpha);
55 }
56 pixel[3] = alpha + pixel[3] * (1.0 - alpha);
57 }
58 }
59 }
60 let data = pixels
61 .into_iter()
62 .flat_map(|pixel| {
63 let alpha = pixel[3];
64 let channel = |value: f32| {
65 let value = if alpha > 0.0 { value / alpha } else { 0.0 };
66 super::srgb_byte(value)
67 };
68 [
69 channel(pixel[0]),
70 channel(pixel[1]),
71 channel(pixel[2]),
72 (alpha * 255.0).round() as u8,
73 ]
74 })
75 .collect();
76 Image {
77 content: Content::Color,
78 placement: Placement {
79 left: 0,
80 top: 0,
81 width,
82 height: width,
83 },
84 data,
85 ..Image::default()
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 const BOX: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><linearGradient id="a" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#8090a0"/></linearGradient></defs><path fill="url(#a)" d="M2 2h12v12H2z"/></svg>"##;
94 const MARK: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><linearGradient id="b" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ff2000"/><stop offset="1" stop-color="#a00000"/></linearGradient></defs><path fill="url(#b)" d="M4 8l3 3 5-7-1-1-4 5-2-2z"/></svg>"##;
95
96 #[test]
97 fn icons_keep_transparency_and_overlays_across_scales() {
98 for size in [8.0, 16.0, 32.0, 57.5] {
99 for phase in [[0, 0], [1, 2], [3, 3]] {
100 let plain = rasterize(&[BOX], size, phase);
101 let marked = rasterize(&[BOX, MARK], size, phase);
102 for image in [&plain, &marked] {
103 assert_eq!(
104 image.data.len(),
105 (image.placement.width * image.placement.height * 4) as usize
106 );
107 assert!(image.data.chunks_exact(4).any(|p| p[3] > 0));
108 assert!(image.data.chunks_exact(4).any(|p| p[3] < 255));
109 assert!(
110 image
111 .data
112 .chunks_exact(4)
113 .filter(|p| p[3] == 0)
114 .all(|p| p[..3] == [0; 3])
115 );
116 }
117 assert_ne!(plain.data, marked.data);
118 assert!(marked.data.chunks_exact(4).any(|p| p[0] > p[2] && p[3] > 0));
119 }
120 }
121 }
122}
crates/draw/src/lib.rs created+1828
......@@ -0,0 +1,1828 @@
1//! Paints layers of primitives with wgpu: glyphs and icons through one atlas, images,
2//! rounded rectangles and pen strokes. Each layer has its own transform and clip.
3
4mod icon;
5mod text;
6
7pub use text::{Decoration, Glyph, GlyphRun, Glyphs, paint_parley_run};
8
9use bytemuck::{Pod, Zeroable};
10use image::ImageDecoder;
11use parley::fontique::Blob;
12use std::{
13 collections::{HashMap, HashSet},
14 ops::Range,
15};
16use swash::{
17 FontRef,
18 scale::{Render, ScaleContext, Source, StrikeWith, image::Content},
19 zeno::{Angle, Format, Transform, Vector},
20};
21
22const ATLAS_SIZE: u32 = 2048;
23const MAX_GLYPHS: usize = 8192;
24const MAX_VERTICES: usize = 65_536;
25/// Decoded bytes of all images one frame may paint.
26pub const MAX_IMAGE_BYTES: u64 = 64 * 1024 * 1024;
27const MAX_IMAGES: usize = 256;
28
29#[derive(Clone)]
30pub struct RasterImage {
31 size: [u32; 2],
32 pixels: Blob<u8>,
33}
34
35impl RasterImage {
36 pub fn decode(encoded: &[u8]) -> Result<Self, RenderError> {
37 if encoded.len() as u64 > MAX_IMAGE_BYTES {
38 return Err(RenderError::ImageBudget);
39 }
40 let format = image::guess_format(encoded).map_err(RenderError::ImageDecode)?;
41 let mut reader = image::ImageReader::with_format(std::io::Cursor::new(encoded), format);
42 let mut limits = image::Limits::default();
43 limits.max_image_width = Some(16_384);
44 limits.max_image_height = Some(16_384);
45 limits.max_alloc = Some(MAX_IMAGE_BYTES);
46 reader.limits(limits.clone());
47 let mut decoder = reader.into_decoder().map_err(RenderError::ImageDecode)?;
48 let (width, height) = decoder.dimensions();
49 if u64::from(width) * u64::from(height) * 4 > MAX_IMAGE_BYTES
50 || decoder.total_bytes() > MAX_IMAGE_BYTES
51 {
52 return Err(RenderError::ImageBudget);
53 }
54 limits
55 .reserve(decoder.total_bytes())
56 .map_err(RenderError::ImageDecode)?;
57 decoder
58 .set_limits(limits)
59 .map_err(RenderError::ImageDecode)?;
60 let pixels = image::DynamicImage::from_decoder(decoder)
61 .map_err(RenderError::ImageDecode)?
62 .into_rgba8();
63 Self::new([width, height], pixels.into_raw())
64 }
65
66 /// Immutable, straight-alpha sRGB RGBA pixels retain one cache identity across clones.
67 pub fn new(size: [u32; 2], mut pixels: Vec<u8>) -> Result<Self, RenderError> {
68 let bytes = u64::from(size[0])
69 .checked_mul(u64::from(size[1]))
70 .and_then(|v| v.checked_mul(4))
71 .ok_or(RenderError::InvalidImage)?;
72 if size.contains(&0) || bytes != pixels.len() as u64 {
73 return Err(RenderError::InvalidImage);
74 }
75 if bytes > MAX_IMAGE_BYTES {
76 return Err(RenderError::ImageBudget);
77 }
78 // Premultiply in linear light before filtering; transparent RGB must not bleed at edges.
79 for pixel in pixels.chunks_exact_mut(4) {
80 let alpha = pixel[3];
81 if alpha == 255 {
82 continue;
83 }
84 let linear = srgb(pixel[0], pixel[1], pixel[2]);
85 for (byte, value) in pixel[..3].iter_mut().zip(linear) {
86 let value = value * f32::from(alpha) / 255.0;
87 *byte = srgb_byte(value);
88 }
89 }
90 Ok(Self {
91 size,
92 pixels: pixels.into(),
93 })
94 }
95
96 /// The identity caches and uploads share across clones.
97 pub fn id(&self) -> u64 {
98 self.pixels.id()
99 }
100
101 /// Premultiplied sRGB RGBA rows.
102 pub fn pixels(&self) -> &[u8] {
103 self.pixels.as_ref()
104 }
105}
106
107struct CachedImage {
108 binding: wgpu::BindGroup,
109 bytes: u64,
110}
111
112struct Batch {
113 vertices: Range<u32>,
114 image: Option<u64>,
115 scissor: [u32; 4],
116}
117
118#[repr(C)]
119#[derive(Clone, Copy, Pod, Zeroable)]
120struct Vertex {
121 position: [f32; 2],
122 uv: [f32; 2],
123 color: [f32; 4],
124 local: [f32; 2],
125 shape: [f32; 4],
126 stroke: f32,
127}
128
129#[derive(Hash, PartialEq, Eq)]
130enum AtlasKey {
131 Text {
132 font: u64,
133 index: u32,
134 glyph: u16,
135 size: u32,
136 coords: Vec<i16>,
137 phase: [u8; 2],
138 embolden: bool,
139 skew: u32,
140 },
141 Icon {
142 sources: &'static [&'static str],
143 size: u32,
144 phase: [u8; 2],
145 },
146}
147
148#[derive(Clone, Copy)]
149struct AtlasGlyph {
150 x: u32,
151 y: u32,
152 width: u32,
153 height: u32,
154 left: i32,
155 top: i32,
156 color: bool,
157}
158
159/// Primitives sharing one transform and clip.
160pub struct Layer<'a> {
161 /// Device pixels per layer unit.
162 pub scale: f32,
163 /// Device position of the layer origin.
164 pub origin: [f32; 2],
165 /// Device bounds `[left, top, right, bottom]` the layer paints within.
166 pub clip: Option<[f32; 4]>,
167 pub primitives: &'a [Primitive<'a>],
168}
169
170/// A layer's transform onto the target.
171#[derive(Clone, Copy)]
172struct Space {
173 size: [u32; 2],
174 scale: f32,
175 origin: [f32; 2],
176}
177
178impl Space {
179 fn visible_rect(&self, rect: [f32; 4]) -> Result<Option<[f32; 4]>, RenderError> {
180 let rect = [
181 rect[0] * self.scale + self.origin[0],
182 rect[1] * self.scale + self.origin[1],
183 rect[2] * self.scale + self.origin[0],
184 rect[3] * self.scale + self.origin[1],
185 ];
186 if rect.iter().any(|v| !v.is_finite()) || rect[2] < rect[0] || rect[3] < rect[1] {
187 return Err(RenderError::InvalidPrimitive);
188 }
189 Ok((rect[0] < self.size[0] as f32
190 && rect[1] < self.size[1] as f32
191 && rect[2] > 0.0
192 && rect[3] > 0.0
193 && rect[0] < rect[2]
194 && rect[1] < rect[3])
195 .then_some(rect))
196 }
197
198 /// Whole device pixels covering `rect` within the target, as a scissor rectangle.
199 fn scissor(&self, rect: [f32; 4]) -> [u32; 4] {
200 let left = rect[0].max(0.0).floor() as u32;
201 let top = rect[1].max(0.0).floor() as u32;
202 let right = (rect[2].min(self.size[0] as f32).ceil() as u32).max(left);
203 let bottom = (rect[3].min(self.size[1] as f32).ceil() as u32).max(top);
204 [left, top, right - left, bottom - top]
205 }
206}
207
208fn intersect(a: [u32; 4], b: [u32; 4]) -> [u32; 4] {
209 let left = a[0].max(b[0]);
210 let top = a[1].max(b[1]);
211 let right = (a[0] + a[2]).min(b[0] + b[2]).max(left);
212 let bottom = (a[1] + a[3]).min(b[1] + b[3]).max(top);
213 [left, top, right - left, bottom - top]
214}
215
216#[derive(Clone, Copy, Debug, PartialEq)]
217pub enum Stroke {
218 Solid(f32),
219 /// Dash and gap lengths are twice the stroke width.
220 Dashed(f32),
221}
222
223/// Coordinates are layer units; colors are linear RGBA.
224pub enum Primitive<'a> {
225 Text {
226 text: &'a dyn Glyphs,
227 origin: [f32; 2],
228 /// Layer bounds of the painted glyphs, which keep their full shaping and advances.
229 clip: Option<[f32; 4]>,
230 },
231 /// 16×16 SVG artwork painted `size` units square, later sources over earlier ones.
232 Icon {
233 sources: &'static [&'static str],
234 origin: [f32; 2],
235 size: f32,
236 opacity: f32,
237 },
238 Rect {
239 rect: [f32; 4],
240 color: [f32; 4],
241 },
242 RoundedRect {
243 rect: [f32; 4],
244 radius: [f32; 2],
245 /// None fills the shape; stroke widths are positive layer units.
246 stroke: Option<Stroke>,
247 color: [f32; 4],
248 },
249 Image {
250 image: &'a RasterImage,
251 rect: [f32; 4],
252 },
253 /// A pen stroke between two points, `width` units across.
254 Segment {
255 from: [f32; 2],
256 to: [f32; 2],
257 width: f32,
258 /// A round pen tip caps the ends; otherwise they are square.
259 round: bool,
260 color: [f32; 4],
261 },
262}
263
264#[derive(Debug)]
265pub enum RenderError {
266 AtlasFull,
267 FrameTooLarge,
268 UnreadableFont,
269 UnsupportedGlyph,
270 InvalidLayer,
271 InvalidPrimitive,
272 InvalidImage,
273 ImageBudget,
274 ImageTooLarge,
275 ImageDecode(image::ImageError),
276}
277
278/// Cache and buffer use after the last frame, for resource probes. Texture and buffer
279/// sizes are as requested, without driver overhead.
280#[derive(Debug)]
281pub struct Occupancy {
282 /// Glyph and icon atlas entries, including glyphs with no pixels.
283 pub glyphs: usize,
284 pub atlas_texels: u64,
285 pub images: usize,
286 pub image_bytes: u64,
287 pub vertices: usize,
288 pub batches: usize,
289 pub vertex_capacity_bytes: usize,
290 pub batch_capacity_bytes: usize,
291 pub glyph_capacity: usize,
292 pub image_capacity: usize,
293 pub atlas_bytes: u64,
294 pub vertex_buffer_bytes: u64,
295 pub within_budget: bool,
296}
297
298pub struct Renderer {
299 pub device: wgpu::Device,
300 pub queue: wgpu::Queue,
301 pipeline: wgpu::RenderPipeline,
302 image_pipeline: wgpu::RenderPipeline,
303 atlas: wgpu::Texture,
304 bind_group: wgpu::BindGroup,
305 image_sampler: wgpu::Sampler,
306 vertex_buffer: wgpu::Buffer,
307 vertices: Vec<Vertex>,
308 glyphs: HashMap<AtlasKey, Option<AtlasGlyph>>,
309 images: HashMap<u64, CachedImage>,
310 batches: Vec<Batch>,
311 scaler: ScaleContext,
312 pen: [u32; 2],
313 row_height: u32,
314}
315
316impl Renderer {
317 pub fn new(device: wgpu::Device, queue: wgpu::Queue, format: wgpu::TextureFormat) -> Self {
318 let shader = device.create_shader_module(wgpu::include_wgsl!("draw.wgsl"));
319 let binding_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
320 label: Some("Draw texture"),
321 entries: &[
322 wgpu::BindGroupLayoutEntry {
323 binding: 0,
324 visibility: wgpu::ShaderStages::FRAGMENT,
325 ty: wgpu::BindingType::Texture {
326 sample_type: wgpu::TextureSampleType::Float { filterable: true },
327 view_dimension: wgpu::TextureViewDimension::D2,
328 multisampled: false,
329 },
330 count: None,
331 },
332 wgpu::BindGroupLayoutEntry {
333 binding: 1,
334 visibility: wgpu::ShaderStages::FRAGMENT,
335 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
336 count: None,
337 },
338 ],
339 });
340 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
341 label: Some("Draw"),
342 bind_group_layouts: &[Some(&binding_layout)],
343 ..Default::default()
344 });
345 let make_pipeline = |blend| {
346 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
347 label: Some("Draw"),
348 layout: Some(&pipeline_layout),
349 vertex: wgpu::VertexState {
350 module: &shader,
351 entry_point: Some("vertex"),
352 compilation_options: Default::default(),
353 buffers: &[Some(wgpu::VertexBufferLayout {
354 array_stride: size_of::<Vertex>() as u64,
355 step_mode: wgpu::VertexStepMode::Vertex,
356 attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Float32x4, 3 => Float32x2, 4 => Float32x4, 5 => Float32],
357 })],
358 },
359 fragment: Some(wgpu::FragmentState {
360 module: &shader,
361 entry_point: Some("fragment"),
362 compilation_options: Default::default(),
363 targets: &[Some(wgpu::ColorTargetState {
364 format,
365 blend: Some(blend),
366 write_mask: wgpu::ColorWrites::ALL,
367 })],
368 }),
369 primitive: Default::default(),
370 depth_stencil: None,
371 multisample: Default::default(),
372 multiview_mask: None,
373 cache: None,
374 })
375 };
376 let pipeline = make_pipeline(wgpu::BlendState::ALPHA_BLENDING);
377 let image_pipeline = make_pipeline(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING);
378 let atlas = device.create_texture(&wgpu::TextureDescriptor {
379 label: Some("Glyph atlas"),
380 size: wgpu::Extent3d {
381 width: ATLAS_SIZE,
382 height: ATLAS_SIZE,
383 depth_or_array_layers: 1,
384 },
385 mip_level_count: 1,
386 sample_count: 1,
387 dimension: wgpu::TextureDimension::D2,
388 format: wgpu::TextureFormat::Rgba8UnormSrgb,
389 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
390 view_formats: &[],
391 });
392 let view = atlas.create_view(&Default::default());
393 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
394 mag_filter: wgpu::FilterMode::Nearest,
395 min_filter: wgpu::FilterMode::Nearest,
396 ..Default::default()
397 });
398 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
399 label: Some("Glyph atlas"),
400 layout: &pipeline.get_bind_group_layout(0),
401 entries: &[
402 wgpu::BindGroupEntry {
403 binding: 0,
404 resource: wgpu::BindingResource::TextureView(&view),
405 },
406 wgpu::BindGroupEntry {
407 binding: 1,
408 resource: wgpu::BindingResource::Sampler(&sampler),
409 },
410 ],
411 });
412 let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
413 label: Some("Draw vertices"),
414 size: (MAX_VERTICES * size_of::<Vertex>()) as u64,
415 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
416 mapped_at_creation: false,
417 });
418 let image_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
419 label: Some("Draw image filtering"),
420 mag_filter: wgpu::FilterMode::Linear,
421 min_filter: wgpu::FilterMode::Linear,
422 ..Default::default()
423 });
424 queue.write_texture(
425 atlas.as_image_copy(),
426 &[255; 4],
427 wgpu::TexelCopyBufferLayout {
428 offset: 0,
429 bytes_per_row: Some(4),
430 rows_per_image: Some(1),
431 },
432 wgpu::Extent3d {
433 width: 1,
434 height: 1,
435 depth_or_array_layers: 1,
436 },
437 );
438 Self {
439 device,
440 queue,
441 pipeline,
442 image_pipeline,
443 atlas,
444 bind_group,
445 image_sampler,
446 vertex_buffer,
447 vertices: Vec::new(),
448 glyphs: HashMap::new(),
449 images: HashMap::new(),
450 batches: Vec::new(),
451 scaler: ScaleContext::with_max_entries(32),
452 pen: [1, 0],
453 row_height: 1,
454 }
455 }
456
457 pub fn clear_glyph_cache(&mut self) {
458 self.glyphs.clear();
459 self.pen = [1, 0];
460 self.row_height = 1;
461 }
462
463 /// Forgets uploaded images; they upload again when next painted.
464 pub fn clear_images(&mut self) {
465 self.images.clear();
466 }
467
468 pub fn occupancy(&self) -> Occupancy {
469 let image_bytes = self.images.values().map(|image| image.bytes).sum();
470 Occupancy {
471 glyphs: self.glyphs.len(),
472 atlas_texels: self
473 .glyphs
474 .values()
475 .flatten()
476 .map(|glyph| u64::from(glyph.width) * u64::from(glyph.height))
477 .sum(),
478 images: self.images.len(),
479 image_bytes,
480 vertices: self.vertices.len(),
481 batches: self.batches.len(),
482 vertex_capacity_bytes: self.vertices.capacity() * size_of::<Vertex>(),
483 batch_capacity_bytes: self.batches.capacity() * size_of::<Batch>(),
484 glyph_capacity: self.glyphs.capacity(),
485 image_capacity: self.images.capacity(),
486 atlas_bytes: u64::from(self.atlas.width()) * u64::from(self.atlas.height()) * 4,
487 vertex_buffer_bytes: self.vertex_buffer.size(),
488 within_budget: self.glyphs.len() <= MAX_GLYPHS
489 && self.images.len() <= MAX_IMAGES
490 && image_bytes <= MAX_IMAGE_BYTES
491 && self.vertices.len() <= MAX_VERTICES,
492 }
493 }
494
495 /// Clears `target`, `size` device pixels, to linear `clear` and paints the layers in order.
496 pub fn draw(
497 &mut self,
498 target: &wgpu::TextureView,
499 size: [u32; 2],
500 clear: [f32; 4],
501 layers: &[Layer<'_>],
502 ) -> Result<(), RenderError> {
503 if size.contains(&0)
504 || clear.iter().any(|v| !v.is_finite())
505 || layers.iter().any(|layer| {
506 !layer.scale.is_finite()
507 || layer.scale <= 0.0
508 || layer.origin.iter().any(|v| !v.is_finite())
509 || layer
510 .clip
511 .is_some_and(|clip| clip.iter().any(|v| !v.is_finite()))
512 })
513 {
514 return Err(RenderError::InvalidLayer);
515 }
516 let mut active_images = HashSet::new();
517 let mut image_bytes = 0;
518 for layer in layers {
519 let space = Space {
520 size,
521 scale: layer.scale,
522 origin: layer.origin,
523 };
524 for primitive in layer.primitives {
525 if let Primitive::Image { image, rect } = primitive
526 && space.visible_rect(*rect)?.is_some()
527 && active_images.insert(image.id())
528 {
529 image_bytes += image.pixels().len() as u64;
530 if image_bytes > MAX_IMAGE_BYTES || active_images.len() > MAX_IMAGES {
531 return Err(RenderError::ImageBudget);
532 }
533 }
534 }
535 }
536 for attempt in 0..2 {
537 self.vertices.clear();
538 self.batches.clear();
539 match self.prepare(size, layers, &active_images) {
540 Err(RenderError::AtlasFull) if attempt == 0 => self.clear_glyph_cache(),
541 result => {
542 result?;
543 break;
544 }
545 }
546 }
547 self.queue
548 .write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&self.vertices));
549 let mut encoder = self.device.create_command_encoder(&Default::default());
550 {
551 let [r, g, b, a] = clear.map(f64::from);
552 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
553 label: Some("Draw"),
554 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
555 view: target,
556 depth_slice: None,
557 resolve_target: None,
558 ops: wgpu::Operations {
559 load: wgpu::LoadOp::Clear(wgpu::Color { r, g, b, a }),
560 store: wgpu::StoreOp::Store,
561 },
562 })],
563 ..Default::default()
564 });
565 pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
566 for batch in &self.batches {
567 let [x, y, width, height] = batch.scissor;
568 pass.set_scissor_rect(x, y, width, height);
569 pass.set_pipeline(if batch.image.is_some() {
570 &self.image_pipeline
571 } else {
572 &self.pipeline
573 });
574 let binding = batch
575 .image
576 .map(|id| &self.images[&id].binding)
577 .unwrap_or(&self.bind_group);
578 pass.set_bind_group(0, binding, &[]);
579 pass.draw(batch.vertices.clone(), 0..1);
580 }
581 }
582 self.queue.submit([encoder.finish()]);
583 Ok(())
584 }
585
586 fn prepare(
587 &mut self,
588 size: [u32; 2],
589 layers: &[Layer<'_>],
590 active_images: &HashSet<u64>,
591 ) -> Result<(), RenderError> {
592 for layer in layers {
593 let space = Space {
594 size,
595 scale: layer.scale,
596 origin: layer.origin,
597 };
598 let bounds = match layer.clip {
599 Some(clip) => space.scissor(clip),
600 None => [0, 0, size[0], size[1]],
601 };
602 if bounds[2] == 0 || bounds[3] == 0 {
603 continue;
604 }
605 for primitive in layer.primitives {
606 self.primitive(space, bounds, primitive, active_images)?;
607 }
608 }
609 Ok(())
610 }
611
612 fn primitive(
613 &mut self,
614 space: Space,
615 bounds: [u32; 4],
616 primitive: &Primitive<'_>,
617 active_images: &HashSet<u64>,
618 ) -> Result<(), RenderError> {
619 let start = self.vertices.len() as u32;
620 let mut image_id = None;
621 let mut scissor = bounds;
622 match primitive {
623 Primitive::Icon {
624 sources,
625 origin,
626 size,
627 opacity,
628 } => {
629 let origin = [
630 space.origin[0] + origin[0] * space.scale,
631 space.origin[1] + origin[1] * space.scale,
632 ];
633 self.icon(Space { origin, ..space }, sources, *size, *opacity)?;
634 }
635 Primitive::Text { text, origin, clip } => {
636 let origin = [
637 space.origin[0] + origin[0] * space.scale,
638 space.origin[1] + origin[1] * space.scale,
639 ];
640 if origin.iter().any(|v| !v.is_finite()) {
641 return Err(RenderError::InvalidPrimitive);
642 }
643 if let Some(clip) = clip {
644 let Some(rect) = space.visible_rect(*clip)? else {
645 return Ok(());
646 };
647 scissor = intersect(bounds, space.scissor(rect));
648 if scissor[2] == 0 || scissor[3] == 0 {
649 return Ok(());
650 }
651 }
652 let space = Space { origin, ..space };
653 text.runs(&mut |run| self.glyph_run(space, run))?;
654 }
655 Primitive::Rect { rect, color } => {
656 self.quad(
657 space,
658 [
659 rect[0] * space.scale + space.origin[0],
660 rect[1] * space.scale + space.origin[1],
661 rect[2] * space.scale + space.origin[0],
662 rect[3] * space.scale + space.origin[1],
663 ],
664 [0.5 / ATLAS_SIZE as f32; 4],
665 *color,
666 )?;
667 }
668 Primitive::RoundedRect {
669 rect,
670 radius,
671 stroke,
672 color,
673 } => {
674 if radius
675 .iter()
676 .any(|value| !value.is_finite() || *value < 0.0)
677 {
678 return Err(RenderError::InvalidPrimitive);
679 }
680 let width = match stroke {
681 None => 0.0,
682 Some(Stroke::Solid(width) | Stroke::Dashed(width)) => {
683 if !width.is_finite() || *width <= 0.0 || width * space.scale == 0.0 {
684 return Err(RenderError::InvalidPrimitive);
685 }
686 *width
687 }
688 };
689 let rect = [
690 rect[0] * space.scale + space.origin[0],
691 rect[1] * space.scale + space.origin[1],
692 rect[2] * space.scale + space.origin[0],
693 rect[3] * space.scale + space.origin[1],
694 ];
695 self.quad(space, rect, [0.5 / ATLAS_SIZE as f32; 4], *color)?;
696 let half = [(rect[2] - rect[0]) * 0.5, (rect[3] - rect[1]) * 0.5];
697 if !(4.0 * (half[0] + half[1])).is_finite() {
698 return Err(RenderError::InvalidPrimitive);
699 }
700 let radius = if radius.contains(&0.0) {
701 [0.0; 2]
702 } else {
703 std::array::from_fn(|axis| (radius[axis] * space.scale).min(half[axis]))
704 };
705 let width = (width * space.scale).min(half[0].min(half[1]));
706 // The stroke sign selects its dash pattern at the shader boundary.
707 let width = if matches!(stroke, Some(Stroke::Dashed(_))) {
708 -width
709 } else {
710 width
711 };
712 for vertex in &mut self.vertices[start as usize..] {
713 vertex.shape = [half[0], half[1], radius[0], radius[1]];
714 vertex.stroke = width;
715 }
716 }
717 Primitive::Segment {
718 from,
719 to,
720 width,
721 round,
722 color,
723 } => self.segment(space, *from, *to, *width, *round, *color)?,
724 Primitive::Image { image, rect } => {
725 if let Some(rect) = space.visible_rect(*rect)? {
726 self.image(image, active_images)?;
727 image_id = Some(image.id());
728 self.quad(space, rect, [0.0, 0.0, 1.0, 1.0], [1.0; 4])?;
729 }
730 }
731 }
732 let end = self.vertices.len() as u32;
733 if start != end {
734 if let Some(last) = self.batches.last_mut()
735 && last.image == image_id
736 && last.scissor == scissor
737 {
738 last.vertices.end = end;
739 } else {
740 self.batches.push(Batch {
741 vertices: start..end,
742 image: image_id,
743 scissor,
744 });
745 }
746 }
747 Ok(())
748 }
749
750 fn image(&mut self, image: &RasterImage, active: &HashSet<u64>) -> Result<(), RenderError> {
751 if self.images.contains_key(&image.id()) {
752 return Ok(());
753 }
754 if image
755 .size
756 .iter()
757 .any(|v| *v > self.device.limits().max_texture_dimension_2d)
758 {
759 return Err(RenderError::ImageTooLarge);
760 }
761 let bytes = image.pixels().len() as u64;
762 while self.images.values().map(|i| i.bytes).sum::<u64>() + bytes > MAX_IMAGE_BYTES
763 || self.images.len() >= MAX_IMAGES
764 {
765 let id = self
766 .images
767 .keys()
768 .find(|id| !active.contains(id))
769 .copied()
770 .ok_or(RenderError::ImageBudget)?;
771 self.images.remove(&id);
772 }
773 let size = wgpu::Extent3d {
774 width: image.size[0],
775 height: image.size[1],
776 depth_or_array_layers: 1,
777 };
778 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
779 label: Some("Draw image"),
780 size,
781 mip_level_count: 1,
782 sample_count: 1,
783 dimension: wgpu::TextureDimension::D2,
784 format: wgpu::TextureFormat::Rgba8UnormSrgb,
785 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
786 view_formats: &[],
787 });
788 self.queue.write_texture(
789 texture.as_image_copy(),
790 image.pixels(),
791 wgpu::TexelCopyBufferLayout {
792 offset: 0,
793 bytes_per_row: Some(image.size[0] * 4),
794 rows_per_image: Some(image.size[1]),
795 },
796 size,
797 );
798 let binding = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
799 label: Some("Draw image"),
800 layout: &self.pipeline.get_bind_group_layout(0),
801 entries: &[
802 wgpu::BindGroupEntry {
803 binding: 0,
804 resource: wgpu::BindingResource::TextureView(
805 &texture.create_view(&Default::default()),
806 ),
807 },
808 wgpu::BindGroupEntry {
809 binding: 1,
810 resource: wgpu::BindingResource::Sampler(&self.image_sampler),
811 },
812 ],
813 });
814 self.images
815 .insert(image.id(), CachedImage { binding, bytes });
816 Ok(())
817 }
818
819 fn glyph_run(&mut self, space: Space, run: GlyphRun<'_>) -> Result<(), RenderError> {
820 let scale = space.scale;
821 let origin = space.origin;
822 let line_top = run.line[0] * scale + origin[1];
823 let line_height = run.line[1] * scale;
824 if line_top + line_height < 0.0 || line_top > space.size[1] as f32 {
825 return Ok(());
826 }
827 let size = run.size * scale;
828 if !size.is_finite() || size > ATLAS_SIZE as f32 {
829 return Err(RenderError::AtlasFull);
830 }
831 for glyph in run.glyphs {
832 let x = glyph.x * scale + origin[0];
833 let y = glyph.y * scale + origin[1];
834 // Quantization affects raster coverage only, never advances or line breaks.
835 let x = (x * 4.0).round() * 0.25;
836 let y = (y * 4.0).round() * 0.25;
837 let glyph_id = glyph
838 .id
839 .try_into()
840 .map_err(|_| RenderError::UnsupportedGlyph)?;
841 let phase = [((x - x.floor()) * 4.0) as u8, ((y - y.floor()) * 4.0) as u8];
842 let key = AtlasKey::Text {
843 font: run.font.id(),
844 index: run.index,
845 glyph: glyph_id,
846 size: size.to_bits(),
847 coords: run.coords.to_vec(),
848 phase,
849 embolden: run.embolden,
850 skew: run.skew.unwrap_or(0.0).to_bits(),
851 };
852 let cached = if let Some(cached) = self.glyphs.get(&key) {
853 *cached
854 } else {
855 if self.glyphs.len() >= MAX_GLYPHS {
856 return Err(RenderError::AtlasFull);
857 }
858 let font = FontRef::from_index(run.font.as_ref(), run.index as usize)
859 .ok_or(RenderError::UnreadableFont)?;
860 let mut scaler = self
861 .scaler
862 .builder_with_id(font, [run.font.id(), u64::from(run.index)])
863 .size(size)
864 .hint(false)
865 .normalized_coords(run.coords.iter().copied())
866 .build();
867 let mut render = Render::new(&[
868 Source::ColorOutline(0),
869 Source::ColorBitmap(StrikeWith::BestFit),
870 Source::Outline,
871 ]);
872 // Swash rasterizes outlines in Y-up coordinates.
873 render.format(Format::Alpha).offset(Vector::new(
874 f32::from(phase[0]) * 0.25,
875 -f32::from(phase[1]) * 0.25,
876 ));
877 if run.embolden {
878 render.embolden(size / 24.0);
879 }
880 if let Some(skew) = run.skew {
881 render.transform(Some(Transform::skew(
882 Angle::from_degrees(skew),
883 Angle::from_degrees(0.0),
884 )));
885 }
886 let image = render.render(&mut scaler, glyph_id);
887 let cached = if let Some(image) =
888 image.filter(|i| i.placement.width > 0 && i.placement.height > 0)
889 {
890 Some(self.upload(image)?)
891 } else {
892 None
893 };
894 self.glyphs.insert(key, cached);
895 cached
896 };
897 if let Some(glyph) = cached {
898 let mut left = x.floor() + glyph.left as f32;
899 let mut top = y.floor() - glyph.top as f32;
900 let mut width = glyph.width as f32;
901 let mut height = glyph.height as f32;
902 if glyph.color {
903 let ratio = (line_height / height).min(1.0);
904 left += width * (1.0 - ratio) * 0.5;
905 width *= ratio;
906 height *= ratio;
907 top = top.clamp(line_top, (line_top + line_height - height).max(line_top));
908 }
909 let atlas_size = ATLAS_SIZE as f32;
910 self.quad(
911 space,
912 [left, top, left + width, top + height],
913 [
914 glyph.x as f32 / atlas_size,
915 glyph.y as f32 / atlas_size,
916 (glyph.x + glyph.width) as f32 / atlas_size,
917 (glyph.y + glyph.height) as f32 / atlas_size,
918 ],
919 if glyph.color { [1.0; 4] } else { run.color },
920 )?;
921 }
922 }
923 for decoration in run.decorations {
924 let x = decoration.x * scale + origin[0];
925 let y = decoration.y * scale + origin[1];
926 self.quad(
927 space,
928 [
929 x,
930 y,
931 x + decoration.width * scale,
932 y + (decoration.thickness * scale).max(1.0),
933 ],
934 [0.5 / ATLAS_SIZE as f32; 4],
935 decoration.color,
936 )?;
937 }
938 Ok(())
939 }
940
941 fn upload(&mut self, image: swash::scale::image::Image) -> Result<AtlasGlyph, RenderError> {
942 let p = image.placement;
943 if p.width + 2 > ATLAS_SIZE || p.height + 2 > ATLAS_SIZE {
944 return Err(RenderError::AtlasFull);
945 }
946 if self.pen[0] + p.width + 1 > ATLAS_SIZE {
947 self.pen[0] = 0;
948 self.pen[1] += self.row_height + 1;
949 self.row_height = 0;
950 }
951 if self.pen[1] + p.height + 1 > ATLAS_SIZE {
952 return Err(RenderError::AtlasFull);
953 }
954 let cached = AtlasGlyph {
955 x: self.pen[0],
956 y: self.pen[1],
957 width: p.width,
958 height: p.height,
959 left: p.left,
960 top: p.top,
961 color: image.content == Content::Color,
962 };
963 let rgba = match image.content {
964 Content::Mask => image
965 .data
966 .iter()
967 .flat_map(|a| [255, 255, 255, *a])
968 .collect(),
969 _ => image.data,
970 };
971 self.queue.write_texture(
972 wgpu::TexelCopyTextureInfo {
973 origin: wgpu::Origin3d {
974 x: cached.x,
975 y: cached.y,
976 z: 0,
977 },
978 ..self.atlas.as_image_copy()
979 },
980 &rgba,
981 wgpu::TexelCopyBufferLayout {
982 offset: 0,
983 bytes_per_row: Some(p.width * 4),
984 rows_per_image: Some(p.height),
985 },
986 wgpu::Extent3d {
987 width: p.width,
988 height: p.height,
989 depth_or_array_layers: 1,
990 },
991 );
992 self.pen[0] += p.width + 1;
993 self.row_height = self.row_height.max(p.height);
994 Ok(cached)
995 }
996
997 fn icon(
998 &mut self,
999 space: Space,
1000 sources: &'static [&'static str],
1001 size: f32,
1002 opacity: f32,
1003 ) -> Result<(), RenderError> {
1004 let size = size * space.scale;
1005 let [x, y] = space.origin.map(|v| (v * 4.0).round() * 0.25);
1006 if !size.is_finite() || [x, y].iter().any(|v| !v.is_finite()) {
1007 return Err(RenderError::InvalidPrimitive);
1008 }
1009 if x + size + 1.0 < 0.0
1010 || y + size + 1.0 < 0.0
1011 || x - 1.0 > space.size[0] as f32
1012 || y - 1.0 > space.size[1] as f32
1013 {
1014 return Ok(());
1015 }
1016 if size + 3.0 > ATLAS_SIZE as f32 {
1017 return Err(RenderError::AtlasFull);
1018 }
1019 let phase = [((x - x.floor()) * 4.0) as u8, ((y - y.floor()) * 4.0) as u8];
1020 let key = AtlasKey::Icon {
1021 sources,
1022 size: size.to_bits(),
1023 phase,
1024 };
1025 let glyph = if let Some(Some(glyph)) = self.glyphs.get(&key) {
1026 *glyph
1027 } else {
1028 if self.glyphs.len() >= MAX_GLYPHS {
1029 return Err(RenderError::AtlasFull);
1030 }
1031 let glyph = self.upload(icon::rasterize(sources, size, phase))?;
1032 self.glyphs.insert(key, Some(glyph));
1033 glyph
1034 };
1035 let left = x.floor();
1036 let top = y.floor();
1037 let atlas = ATLAS_SIZE as f32;
1038 self.quad(
1039 space,
1040 [
1041 left,
1042 top,
1043 left + glyph.width as f32,
1044 top + glyph.height as f32,
1045 ],
1046 [
1047 glyph.x as f32 / atlas,
1048 glyph.y as f32 / atlas,
1049 (glyph.x + glyph.width) as f32 / atlas,
1050 (glyph.y + glyph.height) as f32 / atlas,
1051 ],
1052 [1.0, 1.0, 1.0, opacity],
1053 )
1054 }
1055
1056 /// Draws the segment as a capsule in its own frame, reusing the rounded-rectangle distance.
1057 fn segment(
1058 &mut self,
1059 space: Space,
1060 from: [f32; 2],
1061 to: [f32; 2],
1062 width: f32,
1063 round: bool,
1064 color: [f32; 4],
1065 ) -> Result<(), RenderError> {
1066 let pixel = |p: [f32; 2]| {
1067 [
1068 p[0] * space.scale + space.origin[0],
1069 p[1] * space.scale + space.origin[1],
1070 ]
1071 };
1072 let [from, to] = [pixel(from), pixel(to)];
1073 // Hairlines stay one device pixel wide.
1074 let radius = (width * space.scale).max(1.0) * 0.5;
1075 if from.iter().chain(&to).chain(&color).any(|v| !v.is_finite()) || !radius.is_finite() {
1076 return Err(RenderError::InvalidPrimitive);
1077 }
1078 let pad = radius + 1.0;
1079 if from[0].max(to[0]) + pad < 0.0
1080 || from[1].max(to[1]) + pad < 0.0
1081 || from[0].min(to[0]) - pad > space.size[0] as f32
1082 || from[1].min(to[1]) - pad > space.size[1] as f32
1083 {
1084 return Ok(());
1085 }
1086 if self.vertices.len() + 6 > MAX_VERTICES {
1087 return Err(RenderError::FrameTooLarge);
1088 }
1089 let delta = [to[0] - from[0], to[1] - from[1]];
1090 let length = delta[0].hypot(delta[1]);
1091 let along = if length > 0.0 {
1092 [delta[0] / length, delta[1] / length]
1093 } else {
1094 [1.0, 0.0]
1095 };
1096 let across = [-along[1], along[0]];
1097 let center = [(from[0] + to[0]) * 0.5, (from[1] + to[1]) * 0.5];
1098 let half = [length * 0.5 + radius, radius];
1099 let corner = if round { radius } else { 0.0 };
1100 let [hx, hy] = [half[0] + 1.0, half[1] + 1.0];
1101 for local in [
1102 [-hx, -hy],
1103 [-hx, hy],
1104 [hx, hy],
1105 [-hx, -hy],
1106 [hx, hy],
1107 [hx, -hy],
1108 ] {
1109 let x = center[0] + along[0] * local[0] + across[0] * local[1];
1110 let y = center[1] + along[1] * local[0] + across[1] * local[1];
1111 self.vertices.push(Vertex {
1112 position: [
1113 x * 2.0 / space.size[0] as f32 - 1.0,
1114 1.0 - y * 2.0 / space.size[1] as f32,
1115 ],
1116 uv: [0.5 / ATLAS_SIZE as f32; 2],
1117 color,
1118 local,
1119 shape: [half[0], half[1], corner, corner],
1120 stroke: 0.0,
1121 });
1122 }
1123 Ok(())
1124 }
1125
1126 fn quad(
1127 &mut self,
1128 space: Space,
1129 rect: [f32; 4],
1130 uv: [f32; 4],
1131 color: [f32; 4],
1132 ) -> Result<(), RenderError> {
1133 if rect.iter().chain(&color).any(|v| !v.is_finite())
1134 || rect[2] < rect[0]
1135 || rect[3] < rect[1]
1136 {
1137 return Err(RenderError::InvalidPrimitive);
1138 }
1139 if rect[2] < 0.0
1140 || rect[3] < 0.0
1141 || rect[0] > space.size[0] as f32
1142 || rect[1] > space.size[1] as f32
1143 {
1144 return Ok(());
1145 }
1146 if self.vertices.len() + 6 > MAX_VERTICES {
1147 return Err(RenderError::FrameTooLarge);
1148 }
1149 let x0 = rect[0] * 2.0 / space.size[0] as f32 - 1.0;
1150 let x1 = rect[2] * 2.0 / space.size[0] as f32 - 1.0;
1151 let y0 = 1.0 - rect[1] * 2.0 / space.size[1] as f32;
1152 let y1 = 1.0 - rect[3] * 2.0 / space.size[1] as f32;
1153 if [x0, x1, y0, y1].iter().any(|v| !v.is_finite()) {
1154 return Err(RenderError::InvalidPrimitive);
1155 }
1156 let [hx, hy] = [(rect[2] - rect[0]) * 0.5, (rect[3] - rect[1]) * 0.5];
1157 for (position, uv, local) in [
1158 ([x0, y0], [uv[0], uv[1]], [-hx, -hy]),
1159 ([x0, y1], [uv[0], uv[3]], [-hx, hy]),
1160 ([x1, y1], [uv[2], uv[3]], [hx, hy]),
1161 ([x0, y0], [uv[0], uv[1]], [-hx, -hy]),
1162 ([x1, y1], [uv[2], uv[3]], [hx, hy]),
1163 ([x1, y0], [uv[2], uv[1]], [hx, -hy]),
1164 ] {
1165 self.vertices.push(Vertex {
1166 position,
1167 uv,
1168 color,
1169 local,
1170 shape: [0.0; 4],
1171 stroke: 0.0,
1172 });
1173 }
1174 Ok(())
1175 }
1176}
1177
1178fn srgb_byte(value: f32) -> u8 {
1179 let srgb = if value <= 0.0031308 {
1180 value * 12.92
1181 } else {
1182 1.055 * value.powf(1.0 / 2.4) - 0.055
1183 };
1184 (srgb * 255.0).round() as u8
1185}
1186
1187/// Linear RGBA of an opaque sRGB colour.
1188pub fn srgb(red: u8, green: u8, blue: u8) -> [f32; 4] {
1189 let linear = |byte: u8| {
1190 let value = f32::from(byte) / 255.0;
1191 if value <= 0.04045 {
1192 value / 12.92
1193 } else {
1194 ((value + 0.055) / 1.055).powf(2.4)
1195 }
1196 };
1197 [linear(red), linear(green), linear(blue), 1.0]
1198}
1199
1200#[cfg(test)]
1201mod tests {
1202 use super::*;
1203 use parley::{
1204 FontContext, FontFamily, FontFamilyName, GenericFamily, Layout, LayoutContext,
1205 PositionedLayoutItem, StyleProperty,
1206 };
1207 use std::time::Duration;
1208
1209 const CHECKBOX: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><linearGradient id="a" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#7090b0"/></linearGradient></defs><path fill="url(#a)" d="M2 2h12v12H2zM4 4v8h8V4z"/></svg>"##;
1210 const MARK: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><linearGradient id="b" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#20c040"/><stop offset="1" stop-color="#107020"/></linearGradient></defs><path fill="url(#b)" d="M4 8l3 3 5-7-1-1-4 5-2-2z"/></svg>"##;
1211 const DOT: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><linearGradient id="c" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffd000"/><stop offset="1" stop-color="#c08000"/></linearGradient></defs><path fill="url(#c)" d="M8 2a6 6 0 1 0 0.01 0z"/></svg>"##;
1212
1213 struct Text(Layout<[u8; 4]>);
1214
1215 impl Glyphs for Text {
1216 fn runs(
1217 &self,
1218 paint: &mut dyn FnMut(GlyphRun<'_>) -> Result<(), RenderError>,
1219 ) -> Result<(), RenderError> {
1220 for line in self.0.lines() {
1221 let metrics = line.metrics();
1222 for item in line.items() {
1223 if let PositionedLayoutItem::GlyphRun(run) = item {
1224 paint_parley_run(
1225 &run,
1226 metrics.baseline,
1227 0.0,
1228 [metrics.block_min_coord, metrics.line_height],
1229 |_| [0.0, 0.0, 0.0, 1.0],
1230 paint,
1231 )?;
1232 }
1233 }
1234 }
1235 Ok(())
1236 }
1237 }
1238
1239 fn shape(text: &str, family: &str, size: f32, underline: bool, width: f32) -> Text {
1240 let mut fonts = FontContext::default();
1241 let mut context = LayoutContext::default();
1242 let mut builder = context.ranged_builder(&mut fonts, text, 1.0, false);
1243 builder.push_default(StyleProperty::FontFamily(FontFamily::List(
1244 vec![
1245 FontFamilyName::named(family),
1246 GenericFamily::SansSerif.into(),
1247 ]
1248 .into(),
1249 )));
1250 builder.push_default(StyleProperty::FontSize(size));
1251 builder.push_default(StyleProperty::Underline(underline));
1252 let mut layout = builder.build(text);
1253 layout.break_all_lines(Some(width));
1254 Text(layout)
1255 }
1256
1257 #[test]
1258 fn decoding_preserves_pixels_and_rejects_large_or_truncated_images() {
1259 let mut encoded = Vec::new();
1260 let mut encoder = png::Encoder::new(&mut encoded, 2, 1);
1261 encoder.set_color(png::ColorType::Rgba);
1262 encoder.set_depth(png::BitDepth::Eight);
1263 let rgba = [255, 64, 0, 255, 0, 128, 255, 128];
1264 encoder
1265 .write_header()
1266 .unwrap()
1267 .write_image_data(&rgba)
1268 .unwrap();
1269 let decoded = RasterImage::decode(&encoded).unwrap();
1270 assert_eq!(decoded.size, [2, 1]);
1271 assert_eq!(decoded.pixels(), [255, 64, 0, 255, 0, 93, 188, 128]);
1272 for end in 0..encoded.len() - 12 {
1273 assert!(RasterImage::decode(&encoded[..end]).is_err(), "{end}");
1274 }
1275
1276 let mut jpeg = Vec::new();
1277 image::codecs::jpeg::JpegEncoder::new(&mut jpeg)
1278 .encode(&[255; 4 * 4 * 3], 4, 4, image::ExtendedColorType::Rgb8)
1279 .unwrap();
1280 let decoded = RasterImage::decode(&jpeg).unwrap();
1281 assert_eq!(decoded.size, [4, 4]);
1282 assert_eq!(decoded.pixels(), [255; 4 * 4 * 4]);
1283
1284 let mut gif = Vec::new();
1285 image::codecs::gif::GifEncoder::new(&mut gif)
1286 .encode(&[255; 2 * 2 * 4], 2, 2, image::ExtendedColorType::Rgba8)
1287 .unwrap();
1288 let decoded = RasterImage::decode(&gif).unwrap();
1289 assert_eq!(decoded.size, [2, 2]);
1290 assert_eq!(decoded.pixels(), [255; 2 * 2 * 4]);
1291
1292 for size in [[8192, 8192], [16_385, 1]] {
1293 let mut header = Vec::new();
1294 let mut encoder = png::Encoder::new(&mut header, size[0], size[1]);
1295 encoder.set_color(png::ColorType::Rgba);
1296 encoder.set_depth(png::BitDepth::Eight);
1297 let mut writer = encoder.write_header().unwrap();
1298 writer.write_chunk(png::chunk::IDAT, &[]).unwrap();
1299 drop(writer);
1300 let error = RasterImage::decode(&header).err().unwrap();
1301 if size[0] == 8192 {
1302 assert!(matches!(error, RenderError::ImageBudget), "{error:?}");
1303 } else {
1304 assert!(
1305 matches!(
1306 error,
1307 RenderError::ImageDecode(image::ImageError::Limits(_))
1308 ),
1309 "{error:?}"
1310 );
1311 }
1312 }
1313 }
1314
1315 #[test]
1316 fn image_pixels_have_valid_dimensions_and_immutable_clone_identity() {
1317 assert!(matches!(
1318 RasterImage::new([0, 1], Vec::new()),
1319 Err(RenderError::InvalidImage)
1320 ));
1321 assert!(matches!(
1322 RasterImage::new([2, 2], vec![255; 15]),
1323 Err(RenderError::InvalidImage)
1324 ));
1325 assert!(matches!(
1326 RasterImage::new([u32::MAX; 2], Vec::new()),
1327 Err(RenderError::InvalidImage)
1328 ));
1329 let image = RasterImage::new([1, 1], vec![1, 2, 3, 255]).unwrap();
1330 assert_eq!(image.id(), image.clone().id());
1331 }
1332
1333 struct Target {
1334 texture: wgpu::Texture,
1335 readback: wgpu::Buffer,
1336 }
1337
1338 impl Target {
1339 fn new(device: &wgpu::Device) -> Self {
1340 Self {
1341 texture: device.create_texture(&wgpu::TextureDescriptor {
1342 label: Some("Draw readback test"),
1343 size: wgpu::Extent3d {
1344 width: 512,
1345 height: 256,
1346 depth_or_array_layers: 1,
1347 },
1348 mip_level_count: 1,
1349 sample_count: 1,
1350 dimension: wgpu::TextureDimension::D2,
1351 format: wgpu::TextureFormat::Rgba8UnormSrgb,
1352 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
1353 view_formats: &[],
1354 }),
1355 readback: device.create_buffer(&wgpu::BufferDescriptor {
1356 label: Some("Draw readback"),
1357 size: 512 * 256 * 4,
1358 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
1359 mapped_at_creation: false,
1360 }),
1361 }
1362 }
1363
1364 fn draw(&self, renderer: &mut Renderer, layers: &[Layer<'_>]) -> Result<(), RenderError> {
1365 renderer.draw(
1366 &self.texture.create_view(&Default::default()),
1367 [512, 256],
1368 [1.0; 4],
1369 layers,
1370 )
1371 }
1372
1373 fn capture(&self, renderer: &Renderer) -> Vec<u8> {
1374 let mut encoder = renderer.device.create_command_encoder(&Default::default());
1375 encoder.copy_texture_to_buffer(
1376 self.texture.as_image_copy(),
1377 wgpu::TexelCopyBufferInfo {
1378 buffer: &self.readback,
1379 layout: wgpu::TexelCopyBufferLayout {
1380 offset: 0,
1381 bytes_per_row: Some(512 * 4),
1382 rows_per_image: Some(256),
1383 },
1384 },
1385 wgpu::Extent3d {
1386 width: 512,
1387 height: 256,
1388 depth_or_array_layers: 1,
1389 },
1390 );
1391 renderer.queue.submit([encoder.finish()]);
1392 let (sender, receiver) = std::sync::mpsc::channel();
1393 self.readback
1394 .map_async(wgpu::MapMode::Read, .., move |result| {
1395 sender.send(result).unwrap();
1396 });
1397 renderer
1398 .device
1399 .poll(wgpu::PollType::Wait {
1400 submission_index: None,
1401 timeout: Some(Duration::from_secs(5)),
1402 })
1403 .unwrap();
1404 receiver
1405 .recv_timeout(Duration::from_secs(5))
1406 .unwrap()
1407 .unwrap();
1408 let capture = self.readback.get_mapped_range(..).unwrap().to_vec();
1409 self.readback.unmap();
1410 capture
1411 }
1412 }
1413
1414 fn page<'a>(primitives: &'a [Primitive<'a>]) -> [Layer<'a>; 1] {
1415 [Layer {
1416 scale: 2.0,
1417 origin: [24.0; 2],
1418 clip: None,
1419 primitives,
1420 }]
1421 }
1422
1423 #[test]
1424 #[ignore = "requires a native GPU adapter"]
1425 fn eviction_and_repaint_reproduce_pixels() {
1426 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
1427 let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
1428 let (device, queue) =
1429 pollster::block_on(adapter.request_device(&Default::default())).unwrap();
1430 let format = wgpu::TextureFormat::Rgba8UnormSrgb;
1431 let target = Target::new(&device);
1432 let mut renderer = Renderer::new(device, queue, format);
1433 let layout = shape("Hello 🌳\nCafé e\u{301} שלום", "Arial", 11.0, true, 220.0);
1434 let mut captures = Vec::new();
1435 let image = RasterImage::new(
1436 [2, 2],
1437 vec![255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 0, 0, 0, 0],
1438 )
1439 .unwrap();
1440 let transparent_edge =
1441 RasterImage::new([2, 1], vec![255, 0, 0, 255, 0, 0, 255, 0]).unwrap();
1442 let icons: [&'static [&'static str]; 4] = [&[CHECKBOX], &[CHECKBOX, MARK], &[DOT], &[MARK]];
1443 let phase_layout = shape("H", "Arial", 11.0, false, 20.0);
1444 let mut primitives = vec![
1445 Primitive::Rect {
1446 rect: [0.0, 0.0, 70.0, 14.0],
1447 color: [0.55, 0.73, 1.0, 0.5],
1448 },
1449 Primitive::Text {
1450 clip: None,
1451 text: &layout,
1452 origin: [0.0; 2],
1453 },
1454 Primitive::Text {
1455 clip: None,
1456 text: &layout,
1457 origin: [8.0, 64.0],
1458 },
1459 Primitive::Image {
1460 image: &image,
1461 rect: [128.0, 8.0, 160.0, 40.0],
1462 },
1463 Primitive::Rect {
1464 rect: [128.0, 8.0, 132.0, 12.0],
1465 color: [0.0, 0.0, 1.0, 1.0],
1466 },
1467 Primitive::Image {
1468 image: &transparent_edge,
1469 rect: [176.0, 8.0, 208.0, 40.0],
1470 },
1471 Primitive::RoundedRect {
1472 rect: [128.0, 48.0, 148.0, 68.0],
1473 radius: [5.0, 10.0],
1474 stroke: None,
1475 color: [1.0, 0.0, 0.0, 1.0],
1476 },
1477 Primitive::RoundedRect {
1478 rect: [164.0, 48.0, 184.0, 68.0],
1479 radius: [5.0; 2],
1480 stroke: Some(Stroke::Solid(1.0)),
1481 color: [0.0, 0.0, 1.0, 1.0],
1482 },
1483 Primitive::RoundedRect {
1484 rect: [200.0, 48.0, 240.0, 68.0],
1485 radius: [3.0, 10.0],
1486 stroke: Some(Stroke::Dashed(1.0)),
1487 color: [0.0, 0.0, 1.0, 1.0],
1488 },
1489 Primitive::Segment {
1490 from: [226.0, 104.0],
1491 to: [240.0, 104.0],
1492 width: 4.0,
1493 round: true,
1494 color: [1.0, 0.0, 0.0, 1.0],
1495 },
1496 ];
1497 for (index, sources) in icons.iter().enumerate() {
1498 primitives.push(Primitive::Icon {
1499 sources,
1500 origin: [128.125 + 20.0 * index as f32, 80.25],
1501 size: 12.0,
1502 opacity: 1.0,
1503 });
1504 }
1505 primitives.extend((0..9).map(|step| Primitive::Text {
1506 clip: None,
1507 text: &phase_layout,
1508 origin: [8.0 + 24.0 * step as f32, 96.0 + 0.125 * step as f32],
1509 }));
1510 for pass in 0..6 {
1511 if pass == 2 {
1512 renderer.clear_glyph_cache();
1513 renderer.images.clear();
1514 } else if pass == 3 {
1515 renderer = Renderer::new(renderer.device.clone(), renderer.queue.clone(), format);
1516 }
1517 let clipping = [
1518 Primitive::Text {
1519 text: &layout,
1520 origin: [0.0; 2],
1521 clip: (pass == 5).then_some([2.5, 1.25, 30.75, 20.5]),
1522 },
1523 Primitive::Rect {
1524 rect: [40.0, 40.0, 70.0, 50.0],
1525 color: [0.0, 0.0, 1.0, 1.0],
1526 },
1527 ];
1528 target
1529 .draw(
1530 &mut renderer,
1531 &page(if pass < 4 { &primitives } else { &clipping }),
1532 )
1533 .unwrap();
1534 captures.push(target.capture(&renderer));
1535 }
1536 let centroids: Vec<_> = (0..9)
1537 .map(|step| {
1538 let mut weight = 0.0_f64;
1539 let mut moment = 0.0_f64;
1540 for y in 212..252 {
1541 for x in (40 + 48 * step)..(72 + 48 * step) {
1542 let value = captures[0][(y * 512 + x) * 4];
1543 let coverage = 1.0 - f64::from(srgb(value, 0, 0)[0]);
1544 weight += coverage;
1545 moment += coverage * y as f64;
1546 }
1547 }
1548 assert!(weight > 10.0, "phase sample {step} was not painted");
1549 moment / weight
1550 })
1551 .collect();
1552 let pixel = |x: usize, y: usize| &captures[0][(y * 512 + x) * 4..(y * 512 + x) * 4 + 4];
1553 assert_eq!(pixel(490, 232), [255, 0, 0, 255]);
1554 assert_eq!(pixel(473, 232), [255, 0, 0, 255]);
1555 assert_eq!(pixel(490, 238), [255; 4]);
1556 assert_eq!(pixel(472, 228), [255; 4]);
1557 assert_eq!(pixel(300, 140), [255, 0, 0, 255]);
1558 assert_eq!(pixel(280, 120), [255; 4]);
1559 assert_eq!(pixel(282, 124), [255; 4]);
1560 assert_eq!(pixel(281, 140), [255, 0, 0, 255]);
1561 assert_eq!(pixel(372, 140), [255; 4]);
1562 assert_eq!(pixel(372, 120), [0, 0, 255, 255]);
1563 assert_eq!(pixel(352, 120), [255; 4]);
1564 assert_eq!(pixel(424, 120), [255; 4]);
1565 assert_eq!(pixel(464, 140), [255; 4]);
1566 assert!((440..480).any(|x| pixel(x, 120)[0] < 40));
1567 assert!((440..480).any(|x| pixel(x, 120)[0] > 250));
1568 for x in 440..472 {
1569 assert!(pixel(x, 120)[0].abs_diff(pixel(x + 8, 120)[0]) <= 1);
1570 }
1571 for (step, centroid) in centroids.iter().enumerate() {
1572 let movement = centroid - centroids[0];
1573 assert!(
1574 (movement - step as f64 * 0.25).abs() < 0.06,
1575 "quarter-pixel step {step} moved the glyph by {movement} px"
1576 );
1577 }
1578 assert!(!renderer.glyphs.is_empty());
1579 assert!(renderer.glyphs.len() <= MAX_GLYPHS);
1580 assert_eq!(
1581 renderer
1582 .glyphs
1583 .keys()
1584 .filter(|key| matches!(key, AtlasKey::Icon { .. }))
1585 .count(),
1586 4
1587 );
1588 for x in [280, 320, 360, 400] {
1589 let colored = (184..210)
1590 .flat_map(|y| (x..x + 26).map(move |x| (y * 512 + x) * 4))
1591 .filter(|offset| captures[0][*offset..*offset + 3].iter().any(|v| *v < 180))
1592 .count();
1593 assert!(colored > 10, "icon at {x} was not painted");
1594 }
1595 let count = renderer.glyphs.len();
1596 target
1597 .draw(
1598 &mut renderer,
1599 &page(&[Primitive::Icon {
1600 sources: icons[0],
1601 origin: [10000.0; 2],
1602 size: 12.0,
1603 opacity: 1.0,
1604 }]),
1605 )
1606 .unwrap();
1607 assert_eq!(renderer.glyphs.len(), count);
1608 assert!(
1609 captures[0]
1610 .chunks_exact(4)
1611 .filter(|pixel| pixel[0] < 200)
1612 .count()
1613 > 100
1614 );
1615 assert_eq!(captures[0], captures[1]);
1616 assert_eq!(captures[0], captures[2]);
1617 assert_eq!(captures[0], captures[3]);
1618 assert_ne!(captures[4], captures[5]);
1619 for y in 0..256 {
1620 for x in 0..512 {
1621 let offset = (y * 512 + x) * 4;
1622 let expected = if ((29..86).contains(&x) && (26..65).contains(&y))
1623 || ((104..164).contains(&x) && (104..124).contains(&y))
1624 {
1625 &captures[4][offset..offset + 4]
1626 } else {
1627 &[255; 4]
1628 };
1629 assert_eq!(
1630 &captures[5][offset..offset + 4],
1631 expected,
1632 "clipping at {x},{y}"
1633 );
1634 }
1635 }
1636 for (x, y, color) in [
1637 (288, 48, [255, 0, 0, 255]),
1638 (336, 96, [255; 4]),
1639 (284, 44, [0, 0, 255, 255]),
1640 ] {
1641 let offset = (y * 512 + x) * 4;
1642 assert_eq!(captures[0][offset..offset + 4], color);
1643 }
1644 let edge = &captures[0][(48 * 512 + 408) * 4..(48 * 512 + 408) * 4 + 4];
1645 assert_eq!(edge[0], 255, "{edge:?}");
1646 assert_eq!(edge[1], edge[2], "{edge:?}");
1647 assert!((180..=200).contains(&edge[1]), "{edge:?}");
1648 assert_eq!(renderer.images.len(), 2);
1649 for primitive in [
1650 Primitive::RoundedRect {
1651 rect: [-1e38, 0.0, 1e38, 1e38],
1652 radius: [3.0; 2],
1653 stroke: Some(Stroke::Dashed(1.0)),
1654 color: [1.0; 4],
1655 },
1656 Primitive::RoundedRect {
1657 rect: [0.0, 0.0, 10.0, 10.0],
1658 radius: [f32::NAN, 2.0],
1659 stroke: None,
1660 color: [1.0; 4],
1661 },
1662 Primitive::RoundedRect {
1663 rect: [0.0, 0.0, 10.0, 10.0],
1664 radius: [2.0; 2],
1665 stroke: Some(Stroke::Solid(0.0)),
1666 color: [1.0; 4],
1667 },
1668 Primitive::RoundedRect {
1669 rect: [0.0, 0.0, 10.0, 10.0],
1670 radius: [2.0; 2],
1671 stroke: Some(Stroke::Dashed(f32::NAN)),
1672 color: [1.0; 4],
1673 },
1674 Primitive::Rect {
1675 rect: [0.0, 0.0, f32::NAN, 1.0],
1676 color: [1.0; 4],
1677 },
1678 Primitive::Rect {
1679 rect: [2.0, 0.0, 1.0, 1.0],
1680 color: [1.0; 4],
1681 },
1682 Primitive::Text {
1683 clip: None,
1684 text: &layout,
1685 origin: [f32::MAX; 2],
1686 },
1687 Primitive::Text {
1688 clip: Some([0.0, 0.0, f32::NAN, 1.0]),
1689 text: &layout,
1690 origin: [0.0; 2],
1691 },
1692 Primitive::Text {
1693 clip: Some([2.0, 0.0, 1.0, 1.0]),
1694 text: &layout,
1695 origin: [0.0; 2],
1696 },
1697 ] {
1698 assert!(matches!(
1699 target.draw(&mut renderer, &page(&[primitive])),
1700 Err(RenderError::InvalidPrimitive)
1701 ));
1702 }
1703 let too_many: Vec<_> = (0..=MAX_IMAGES)
1704 .map(|_| RasterImage::new([1, 1], vec![255; 4]).unwrap())
1705 .collect();
1706 let primitives: Vec<_> = too_many
1707 .iter()
1708 .map(|image| Primitive::Image {
1709 image,
1710 rect: [0.0, 0.0, 1.0, 1.0],
1711 })
1712 .collect();
1713 assert!(matches!(
1714 target.draw(&mut renderer, &page(&primitives)),
1715 Err(RenderError::ImageBudget)
1716 ));
1717 let primitives: Vec<_> = too_many
1718 .iter()
1719 .map(|image| Primitive::Image {
1720 image,
1721 rect: [-20.0, -20.0, -19.0, -19.0],
1722 })
1723 .collect();
1724 target.draw(&mut renderer, &page(&primitives)).unwrap();
1725 assert_eq!(renderer.images.len(), 2);
1726 let mut seen = HashSet::from([image.id(), transparent_edge.id()]);
1727 let mut previous = None;
1728 for _ in 0..5 {
1729 let large = RasterImage::new([2048; 2], vec![255; 2048 * 2048 * 4]).unwrap();
1730 seen.insert(large.id());
1731 let mut primitives = vec![Primitive::Image {
1732 image: &large,
1733 rect: [0.0, 0.0, 16.0, 16.0],
1734 }];
1735 if let Some(image) = &previous {
1736 primitives.push(Primitive::Image {
1737 image,
1738 rect: [16.0, 0.0, 32.0, 16.0],
1739 });
1740 }
1741 target.draw(&mut renderer, &page(&primitives)).unwrap();
1742 assert!(renderer.images.contains_key(&large.id()));
1743 if let Some(image) = &previous {
1744 assert!(renderer.images.contains_key(&image.id()));
1745 }
1746 assert!(renderer.occupancy().within_budget);
1747 previous = Some(large);
1748 }
1749 assert!(renderer.images.len() <= MAX_IMAGES);
1750 assert!(seen.iter().any(|id| !renderer.images.contains_key(id)));
1751 renderer.images.clear();
1752 target
1753 .draw(
1754 &mut renderer,
1755 &page(&[Primitive::Image {
1756 image: &image,
1757 rect: [128.0, 8.0, 160.0, 40.0],
1758 }]),
1759 )
1760 .unwrap();
1761 assert_eq!(renderer.images.len(), 1);
1762 assert!(renderer.images.contains_key(&image.id()));
1763 }
1764
1765 #[test]
1766 #[ignore = "requires a native GPU adapter"]
1767 fn layers_transform_and_clip_independently() {
1768 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
1769 let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
1770 let (device, queue) =
1771 pollster::block_on(adapter.request_device(&Default::default())).unwrap();
1772 let target = Target::new(&device);
1773 let mut renderer = Renderer::new(device, queue, wgpu::TextureFormat::Rgba8UnormSrgb);
1774 let fill = [Primitive::Rect {
1775 rect: [0.0, 0.0, 300.0, 100.0],
1776 color: [1.0, 0.0, 0.0, 1.0],
1777 }];
1778 let chrome = [Primitive::Rect {
1779 rect: [0.0, 0.0, 10.0, 10.0],
1780 color: [0.0, 0.0, 1.0, 1.0],
1781 }];
1782 target
1783 .draw(
1784 &mut renderer,
1785 &[
1786 Layer {
1787 scale: 1.0,
1788 origin: [0.0; 2],
1789 clip: Some([100.0, 50.0, 200.5, 80.0]),
1790 primitives: &fill,
1791 },
1792 Layer {
1793 scale: 2.0,
1794 origin: [300.0, 10.0],
1795 clip: None,
1796 primitives: &chrome,
1797 },
1798 ],
1799 )
1800 .unwrap();
1801 let capture = target.capture(&renderer);
1802 let pixel = |x: usize, y: usize| &capture[(y * 512 + x) * 4..(y * 512 + x) * 4 + 4];
1803 assert_eq!(pixel(10, 10), [255; 4], "clipped away");
1804 assert_eq!(pixel(100, 50), [255, 0, 0, 255]);
1805 assert_eq!(
1806 pixel(200, 79),
1807 [255, 0, 0, 255],
1808 "partial pixels round outward"
1809 );
1810 assert_eq!(pixel(99, 60), [255; 4]);
1811 assert_eq!(pixel(201, 60), [255; 4]);
1812 assert_eq!(pixel(150, 80), [255; 4]);
1813 assert_eq!(pixel(319, 29), [0, 0, 255, 255]);
1814 assert_eq!(pixel(320, 30), [255; 4]);
1815 assert!(matches!(
1816 target.draw(
1817 &mut renderer,
1818 &[Layer {
1819 scale: 0.0,
1820 origin: [0.0; 2],
1821 clip: None,
1822 primitives: &fill,
1823 }]
1824 ),
1825 Err(RenderError::InvalidLayer)
1826 ));
1827 }
1828}
crates/draw/src/text.rs created+113
......@@ -0,0 +1,113 @@
1use crate::RenderError;
2use parley::fontique::Blob;
3
4/// Text the renderer can paint, as glyph runs in layer units. Owners recover their
5/// concrete text from a primitive through `Any`.
6pub trait Glyphs: std::any::Any {
7 fn runs(
8 &self,
9 paint: &mut dyn FnMut(GlyphRun<'_>) -> Result<(), RenderError>,
10 ) -> Result<(), RenderError>;
11}
12
13/// Glyphs sharing one font, size and colour.
14pub struct GlyphRun<'a> {
15 pub font: &'a Blob<u8>,
16 /// The face within a font collection.
17 pub index: u32,
18 pub size: f32,
19 /// Variation coordinates in F2Dot14 bits.
20 pub coords: &'a [i16],
21 pub embolden: bool,
22 /// Synthesized oblique angle in degrees.
23 pub skew: Option<f32>,
24 pub color: [f32; 4],
25 /// Top and height of the run's line: lines outside the target are skipped, and
26 /// colour glyphs shrink to fit.
27 pub line: [f32; 2],
28 pub glyphs: &'a mut dyn Iterator<Item = Glyph>,
29 /// Underline and strikethrough, painted after the glyphs.
30 pub decorations: &'a [Decoration],
31}
32
33/// A glyph's baseline position.
34#[derive(Clone, Copy)]
35pub struct Glyph {
36 pub id: u32,
37 pub x: f32,
38 pub y: f32,
39}
40
41/// A decoration line whose top edge is `y`; it stays at least one device pixel thick.
42#[derive(Clone, Copy)]
43pub struct Decoration {
44 pub x: f32,
45 pub y: f32,
46 pub width: f32,
47 pub thickness: f32,
48 pub color: [f32; 4],
49}
50
51/// Paints one parley glyph run with its line's baseline moved to `baseline` and its
52/// glyphs raised by `rise`.
53pub fn paint_parley_run<B: parley::Brush>(
54 run: &parley::GlyphRun<'_, B>,
55 baseline: f32,
56 rise: f32,
57 line: [f32; 2],
58 color: impl Fn(&B) -> [f32; 4],
59 paint: &mut dyn FnMut(GlyphRun<'_>) -> Result<(), RenderError>,
60) -> Result<(), RenderError> {
61 let shaped = run.run();
62 let font = &shaped.font().font;
63 let coords: Vec<i16> = shaped
64 .normalized_coords()
65 .iter()
66 .map(|c| c.to_bits())
67 .collect();
68 let synthesis = shaped.synthesis();
69 let style = run.style();
70 let metrics = shaped.font_metrics();
71 let decorations: Vec<_> = [
72 (
73 &style.underline,
74 metrics.underline_offset,
75 metrics.underline_size,
76 ),
77 (
78 &style.strikethrough,
79 metrics.strikethrough_offset,
80 metrics.strikethrough_size,
81 ),
82 ]
83 .into_iter()
84 .filter_map(|(decoration, offset, thickness)| {
85 let decoration = decoration.as_ref()?;
86 Some(Decoration {
87 x: run.offset(),
88 y: baseline - rise - decoration.offset.unwrap_or(offset),
89 width: run.advance(),
90 thickness: decoration.size.unwrap_or(thickness),
91 color: color(&decoration.brush),
92 })
93 })
94 .collect();
95 let line_baseline = run.baseline();
96 let mut glyphs = run.positioned_glyphs().map(|glyph| Glyph {
97 id: glyph.id,
98 x: glyph.x,
99 y: glyph.y + baseline - line_baseline - rise,
100 });
101 paint(GlyphRun {
102 font: &font.data,
103 index: font.index,
104 size: shaped.font_size(),
105 coords: &coords,
106 embolden: synthesis.embolden(),
107 skew: synthesis.skew(),
108 color: color(&style.brush),
109 line,
110 glyphs: &mut glyphs,
111 decorations: &decorations,
112 })
113}
crates/snowbound/Cargo.toml+1
......@@ -6,6 +6,7 @@ publish = false
66
77[dependencies]
88canvas = { path = "../canvas", features = ["interaction"] }
9draw = { path = "../draw" }
910onestore = { path = "../onestore" }
1011notebook = { path = "../notebook" }
1112pollster = "0.4"
crates/snowbound/src/main.rs+5-3
......@@ -1,6 +1,6 @@
11mod macos;
22
3use canvas::gpu::{Renderer, page::PageScene};
3use canvas::gpu::page::PageScene;
44use canvas::interaction::{
55 Cursor, Key as PageKey, Modifiers, NamedKey as PageNamedKey, PageView, Request, Response,
66 TextColors, accessibility,
......@@ -11,6 +11,7 @@ use canvas::{
1111 editor::{CanvasEditor, DEFAULT_OUTLINE_WIDTH, TextOutline},
1212 layout::TextEngine,
1313};
14use draw::Renderer;
1415use onestore::document::Format;
1516use onestore::page::Page;
1617use onestore::page::text::Paragraph;
......@@ -604,8 +605,9 @@ impl State {
604605 self.renderer
605606 .draw(
606607 &frame.texture.create_view(&Default::default()),
607 viewport,
608 &primitives,
608 viewport.size,
609 [1.0; 4],
610 &[viewport.layer(&primitives)],
609611 )
610612 .map_err(|error| format!("Canvas drawing failed: {error:?}"))?;
611613 self.window.pre_present_notify();
tools/canvas/README.md+6-3
......@@ -10,6 +10,7 @@ The host opens nonempty UTF-8 text as paragraphs in one temporary text outline.
1010python3 tools/canvas/build_macos.py
1111"target/Snowbound.app/Contents/MacOS/Snowbound"
1212cargo test -p canvas --features interaction
13cargo test -p draw -- --ignored
1314cargo test -p canvas --features gpu gpu:: -- --ignored
1415```
1516
......@@ -30,7 +31,9 @@ onestore::Format
3031 ↓
3132canvas text edits · inverse history · composition · paragraph layout · structural edits
3233 ↓
33canvas::gpu Swash glyph rasterization · custom wgpu quads · bounded glyph/image caches
34canvas::gpu page scene · text runs, tag icons, pictures and ink as draw primitives
35 ↓
36draw Swash glyph rasterization · custom wgpu quads · bounded glyph/image caches · layers
3437 ↓
3538canvas::interaction hit layers · drags · placement grid · picture handles · chrome · key routing
3639 · scrolling and zoom · caret blink · AccessKit tree
......@@ -40,9 +43,9 @@ snowbound Winit window and events · AppKit pickers and quit guard · clipboar
4043
4144The renderer allocates a 2048² RGBA glyph atlas and a 3.5 MiB vertex buffer, limits combined text-glyph and tag-icon entries to 8192, and rebuilds a frame after atlas eviction. Font scaling uses 32 cache entries. These limits bound those caches; they are not a process-memory or worst-case rasterization budget. Drawing and hit-testing share Windows-metric line boxes. First-baseline fidelity and substitute-font metric selection remain experimental; the `windows_*` probe values expose that policy independently from Parley's original metrics.
4245
43Drawing accepts a flat sequence of positioned text layouts, tag icons, rectangles and immutable raster images. Consecutive primitives with the same texture share a batch without changing paint order. The host emits selection, composition and caret rectangles through that same interface; repainting multiple placements reuses the retained layout and glyph cache.
46The `draw` crate owns the renderer so the page and application chrome share one pass, atlas and image cache. It paints layers, each a transform and device clip over a flat sequence of glyph runs, SVG icons, rectangles, pen strokes and immutable raster images. Text reaches it through the `Glyphs` trait, which visits positioned glyph runs, so the page's Windows-metric line boxes stay in `canvas` and the renderer never measures text. Consecutive primitives with the same texture and clip share a batch without changing paint order. The host emits selection, composition and caret rectangles through that same interface; repainting multiple placements reuses the retained layout and glyph cache.
4447
45Glyph coverage uses quarter-pixel phases with the vertical sign converted to Swash's Y-up coordinates. The GPU readback test checks nine successive quarter-pixel translations by their linear-light ink centroids, then requires identical pixels on repaint, cache eviction and renderer reconstruction on the same device. The native caret experiment exposed the previous inverted vertical phase; correcting raster placement does not change line metrics or wrapping.
48Glyph coverage uses quarter-pixel phases with the vertical sign converted to Swash's Y-up coordinates. The `draw` GPU readback test checks nine successive quarter-pixel translations by their linear-light ink centroids, then requires identical pixels on repaint, cache eviction and renderer reconstruction on the same device. The native caret experiment exposed the previous inverted vertical phase; correcting raster placement does not change line metrics or wrapping.
4649
4750Image textures have a separate 64 MiB / 256-entry cache. Offscreen images do not upload or consume the active-frame budget; a frame whose visible images exceed that budget returns an error. Under pressure, eviction preserves every image needed by the current frame. PNG/JPEG decoding checks encoded size, dimensions and decoded output size, with a best-effort codec allocation limit. These limits do not bound all CPU buffers or driver allocations. Images use linear filtering of premultiplied, linear-light colors; glyphs retain their own sampling/blending policy. Decoded color values are currently interpreted as sRGB; ICC and orientation metadata are not transformed by this experiment.
4851