From 4700e54f1693bed265eac4f94fce276c7f5e77fe Mon Sep 17 00:00:00 2001 From: Jengamon Date: Wed, 21 Feb 2024 16:49:54 -0800 Subject: [PATCH 01/24] Do a little feature magic to allow to build for web (#83) * Remove default features * Enable default features for dev (testing) builds * Enable WGSL unconditionally --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 222c2be..4bea6a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,11 +8,12 @@ repository = "https://github.com/grovesNL/glyphon" license = "MIT OR Apache-2.0 OR Zlib" [dependencies] -wgpu = "0.19" +wgpu = { version = "0.19", default-features = false, features = ["wgsl"] } etagere = "0.2.10" cosmic-text = "0.10" lru = "0.12.1" [dev-dependencies] winit = { version = "0.29.10", features = ["rwh_05"] } +wgpu = { version = "0.19", default-features = true } pollster = "0.3.0" From 01ab64704b123d1ae61a5022b2b8ea2a7a4573e8 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Mon, 18 Mar 2024 18:30:28 -0400 Subject: [PATCH 02/24] Add opacity --- Cargo.toml | 2 +- src/lib.rs | 4 +++- src/text_render.rs | 7 +++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 222c2be..8b158ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,8 @@ license = "MIT OR Apache-2.0 OR Zlib" [dependencies] wgpu = "0.19" etagere = "0.2.10" -cosmic-text = "0.10" lru = "0.12.1" +cosmic-text = "0.11.2" [dev-dependencies] winit = { version = "0.29.10", features = ["rwh_05"] } diff --git a/src/lib.rs b/src/lib.rs index b9194c5..5b4dc51 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -111,6 +111,8 @@ pub struct TextArea<'a> { /// The visible bounds of the text area. This is used to clip the text and doesn't have to /// match the `left` and `top` values. pub bounds: TextBounds, - // The default color of the text area. + /// The default color of the text area. pub default_color: Color, + /// The opacity to set the text area to (in gamma space) + pub opacity: f32, } diff --git a/src/text_render.rs b/src/text_render.rs index 9818bd1..8a1a595 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -268,6 +268,13 @@ impl TextRenderer { None => text_area.default_color, }; + let color = cosmic_text::Color::rgba( + (color.r() as f32 * text_area.opacity + 0.5) as u8, + (color.g() as f32 * text_area.opacity + 0.5) as u8, + (color.b() as f32 * text_area.opacity + 0.5) as u8, + (color.a() as f32 * text_area.opacity + 0.5) as u8, + ); + let depth = metadata_to_depth(glyph.metadata); glyph_vertices.extend( From f95e66f6126eadcc925b911434b940f83e047fef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Pakalns?= Date: Tue, 26 Mar 2024 02:25:57 +0200 Subject: [PATCH 03/24] Support multi viewport rendering with reusable text atlas (#88) --- src/shader.wgsl | 6 +-- src/text_atlas.rs | 92 ++++++++++++++++++---------------------------- src/text_render.rs | 63 +++++++++++++++++++------------ 3 files changed, 78 insertions(+), 83 deletions(-) diff --git a/src/shader.wgsl b/src/shader.wgsl index d7f7d86..3d1ba4f 100644 --- a/src/shader.wgsl +++ b/src/shader.wgsl @@ -23,13 +23,13 @@ struct Params { @group(0) @binding(0) var params: Params; -@group(0) @binding(1) +@group(1) @binding(0) var color_atlas_texture: texture_2d; -@group(0) @binding(2) +@group(1) @binding(1) var mask_atlas_texture: texture_2d; -@group(0) @binding(3) +@group(1) @binding(2) var atlas_sampler: sampler; fn srgb_to_linear(c: f32) -> f32 { diff --git a/src/text_atlas.rs b/src/text_atlas.rs index 4110554..44e017c 100644 --- a/src/text_atlas.rs +++ b/src/text_atlas.rs @@ -1,20 +1,20 @@ use crate::{ text_render::ContentType, CacheKey, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, - Params, Resolution, SwashCache, + Params, SwashCache, }; use etagere::{size2, Allocation, BucketedAtlasAllocator}; use lru::LruCache; use std::{borrow::Cow, collections::HashSet, mem::size_of, num::NonZeroU64, sync::Arc}; use wgpu::{ BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutEntry, - BindingResource, BindingType, BlendState, Buffer, BufferBindingType, BufferDescriptor, - BufferUsages, ColorTargetState, ColorWrites, DepthStencilState, Device, Extent3d, FilterMode, - FragmentState, ImageCopyTexture, ImageDataLayout, MultisampleState, Origin3d, PipelineLayout, - PipelineLayoutDescriptor, PrimitiveState, Queue, RenderPipeline, RenderPipelineDescriptor, - Sampler, SamplerBindingType, SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, - ShaderSource, ShaderStages, Texture, TextureAspect, TextureDescriptor, TextureDimension, - TextureFormat, TextureSampleType, TextureUsages, TextureView, TextureViewDescriptor, - TextureViewDimension, VertexFormat, VertexState, + BindingResource, BindingType, BlendState, BufferBindingType, ColorTargetState, ColorWrites, + DepthStencilState, Device, Extent3d, FilterMode, FragmentState, ImageCopyTexture, + ImageDataLayout, MultisampleState, Origin3d, PipelineLayout, PipelineLayoutDescriptor, + PrimitiveState, Queue, RenderPipeline, RenderPipelineDescriptor, Sampler, SamplerBindingType, + SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, Texture, + TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, + TextureUsages, TextureView, TextureViewDescriptor, TextureViewDimension, VertexFormat, + VertexState, }; #[allow(dead_code)] @@ -255,8 +255,6 @@ pub enum ColorMode { /// An atlas containing a cache of rasterized glyphs that can be rendered. pub struct TextAtlas { - pub(crate) params: Params, - pub(crate) params_buffer: Buffer, pub(crate) cached_pipelines: Vec<( MultisampleState, Option, @@ -264,6 +262,7 @@ pub struct TextAtlas { )>, pub(crate) bind_group: Arc, pub(crate) bind_group_layout: BindGroupLayout, + pub(crate) text_render_bind_group_layout: BindGroupLayout, pub(crate) sampler: Sampler, pub(crate) color_atlas: InnerAtlas, pub(crate) mask_atlas: InnerAtlas, @@ -340,9 +339,9 @@ impl TextAtlas { ], }]; - let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - entries: &[ - BindGroupLayoutEntry { + let text_render_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[BindGroupLayoutEntry { binding: 0, visibility: ShaderStages::VERTEX, ty: BindingType::Buffer { @@ -351,6 +350,21 @@ impl TextAtlas { min_binding_size: NonZeroU64::new(size_of::() as u64), }, count: None, + }], + label: Some("glyphon text render bind group layout"), + }); + + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[ + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, + ty: BindingType::Texture { + multisampled: false, + view_dimension: TextureViewDimension::D2, + sample_type: TextureSampleType::Float { filterable: true }, + }, + count: None, }, BindGroupLayoutEntry { binding: 1, @@ -364,37 +378,12 @@ impl TextAtlas { }, BindGroupLayoutEntry { binding: 2, - visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, - ty: BindingType::Texture { - multisampled: false, - view_dimension: TextureViewDimension::D2, - sample_type: TextureSampleType::Float { filterable: true }, - }, - count: None, - }, - BindGroupLayoutEntry { - binding: 3, visibility: ShaderStages::FRAGMENT, ty: BindingType::Sampler(SamplerBindingType::Filtering), count: None, }, ], - label: Some("glyphon bind group layout"), - }); - - let params = Params { - screen_resolution: Resolution { - width: 0, - height: 0, - }, - _pad: [0, 0], - }; - - let params_buffer = device.create_buffer(&BufferDescriptor { - label: Some("glyphon params"), - size: size_of::() as u64, - usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, - mapped_at_creation: false, + label: Some("glyphon text atlas bind group layout"), }); let color_atlas = InnerAtlas::new( @@ -414,34 +403,29 @@ impl TextAtlas { entries: &[ BindGroupEntry { binding: 0, - resource: params_buffer.as_entire_binding(), - }, - BindGroupEntry { - binding: 1, resource: BindingResource::TextureView(&color_atlas.texture_view), }, BindGroupEntry { - binding: 2, + binding: 1, resource: BindingResource::TextureView(&mask_atlas.texture_view), }, BindGroupEntry { - binding: 3, + binding: 2, resource: BindingResource::Sampler(&sampler), }, ], - label: Some("glyphon bind group"), + label: Some("glyphon text atlas bind group"), })); let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { label: None, - bind_group_layouts: &[&bind_group_layout], + bind_group_layouts: &[&text_render_bind_group_layout, &bind_group_layout], push_constant_ranges: &[], }); Self { - params, - params_buffer, cached_pipelines: Vec::new(), + text_render_bind_group_layout, bind_group, bind_group_layout, sampler, @@ -540,18 +524,14 @@ impl TextAtlas { entries: &[ BindGroupEntry { binding: 0, - resource: self.params_buffer.as_entire_binding(), - }, - BindGroupEntry { - binding: 1, resource: BindingResource::TextureView(&self.color_atlas.texture_view), }, BindGroupEntry { - binding: 2, + binding: 1, resource: BindingResource::TextureView(&self.mask_atlas.texture_view), }, BindGroupEntry { - binding: 3, + binding: 2, resource: BindingResource::Sampler(&self.sampler), }, ], diff --git a/src/text_render.rs b/src/text_render.rs index 9818bd1..81dd5f5 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -4,20 +4,22 @@ use crate::{ }; use std::{iter, mem::size_of, slice, sync::Arc}; use wgpu::{ - Buffer, BufferDescriptor, BufferUsages, DepthStencilState, Device, Extent3d, ImageCopyTexture, - ImageDataLayout, IndexFormat, MultisampleState, Origin3d, Queue, RenderPass, RenderPipeline, - TextureAspect, COPY_BUFFER_ALIGNMENT, + BindGroupDescriptor, BindGroupEntry, Buffer, BufferDescriptor, BufferUsages, DepthStencilState, + Device, Extent3d, ImageCopyTexture, ImageDataLayout, IndexFormat, MultisampleState, Origin3d, + Queue, RenderPass, RenderPipeline, TextureAspect, COPY_BUFFER_ALIGNMENT, }; /// A text renderer that uses cached glyphs to render text into an existing render pass. pub struct TextRenderer { + params: Params, + params_buffer: Buffer, vertex_buffer: Buffer, vertex_buffer_size: u64, index_buffer: Buffer, index_buffer_size: u64, vertices_to_render: u32, - screen_resolution: Resolution, pipeline: Arc, + bind_group: wgpu::BindGroup, } impl TextRenderer { @@ -46,17 +48,40 @@ impl TextRenderer { let pipeline = atlas.get_or_create_pipeline(device, multisample, depth_stencil); + let params = Params { + screen_resolution: Resolution { + width: 0, + height: 0, + }, + _pad: [0, 0], + }; + + let params_buffer = device.create_buffer(&BufferDescriptor { + label: Some("glyphon params"), + size: size_of::() as u64, + usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let bind_group = device.create_bind_group(&BindGroupDescriptor { + layout: &atlas.text_render_bind_group_layout, + entries: &[BindGroupEntry { + binding: 0, + resource: params_buffer.as_entire_binding(), + }], + label: Some("glyphon text render bind group"), + }); + Self { + params, + params_buffer, vertex_buffer, vertex_buffer_size, index_buffer, index_buffer_size, vertices_to_render: 0, - screen_resolution: Resolution { - width: 0, - height: 0, - }, pipeline, + bind_group, } } @@ -72,15 +97,11 @@ impl TextRenderer { cache: &mut SwashCache, mut metadata_to_depth: impl FnMut(usize) -> f32, ) -> Result<(), PrepareError> { - self.screen_resolution = screen_resolution; - - let atlas_current_resolution = { atlas.params.screen_resolution }; - - if screen_resolution != atlas_current_resolution { - atlas.params.screen_resolution = screen_resolution; - queue.write_buffer(&atlas.params_buffer, 0, unsafe { + if self.params.screen_resolution != screen_resolution { + self.params.screen_resolution = screen_resolution; + queue.write_buffer(&self.params_buffer, 0, unsafe { slice::from_raw_parts( - &atlas.params as *const Params as *const u8, + &self.params as *const Params as *const u8, size_of::(), ) }); @@ -394,15 +415,9 @@ impl TextRenderer { return Ok(()); } - { - // Validate that screen resolution hasn't changed since `prepare` - if self.screen_resolution != atlas.params.screen_resolution { - return Err(RenderError::ScreenResolutionChanged); - } - } - pass.set_pipeline(&self.pipeline); - pass.set_bind_group(0, &atlas.bind_group, &[]); + pass.set_bind_group(0, &self.bind_group, &[]); + pass.set_bind_group(1, &atlas.bind_group, &[]); pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); pass.set_index_buffer(self.index_buffer.slice(..), IndexFormat::Uint32); pass.draw_indexed(0..self.vertices_to_render, 0, 0..1); From c16b6eb9575684c78bda0a0abd1fd9bb1fd0fe30 Mon Sep 17 00:00:00 2001 From: Xavier Lambein Date: Tue, 26 Mar 2024 01:26:37 +0100 Subject: [PATCH 04/24] Bump cosmic text to 0.11 (#89) Co-authored-by: Xavier Lambein --- Cargo.toml | 2 +- examples/hello-world.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4bea6a8..e79ac24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ license = "MIT OR Apache-2.0 OR Zlib" [dependencies] wgpu = { version = "0.19", default-features = false, features = ["wgsl"] } etagere = "0.2.10" -cosmic-text = "0.10" +cosmic-text = "0.11" lru = "0.12.1" [dev-dependencies] diff --git a/examples/hello-world.rs b/examples/hello-world.rs index daafbb3..8924702 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -78,7 +78,7 @@ async fn run() { buffer.set_size(&mut font_system, physical_width, physical_height); buffer.set_text(&mut font_system, "Hello world! 👋\nThis is rendered with 🦅 glyphon 🦁\nThe text below should be partially clipped.\na b c d e f g h i j k l m n o p q r s t u v w x y z", Attrs::new().family(Family::SansSerif), Shaping::Advanced); - buffer.shape_until_scroll(&mut font_system); + buffer.shape_until_scroll(&mut font_system, false); event_loop .run(move |event, target| { From 4f24305ac5d84f09122bebedb80ae711b59acf65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 29 Mar 2024 23:46:42 +0100 Subject: [PATCH 05/24] Reuse `Vec` allocations in `TextRenderer` --- src/text_render.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/text_render.rs b/src/text_render.rs index 81dd5f5..c8a3bef 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -20,6 +20,8 @@ pub struct TextRenderer { vertices_to_render: u32, pipeline: Arc, bind_group: wgpu::BindGroup, + glyph_vertices: Vec, + glyph_indices: Vec, } impl TextRenderer { @@ -82,6 +84,8 @@ impl TextRenderer { vertices_to_render: 0, pipeline, bind_group, + glyph_vertices: Vec::new(), + glyph_indices: Vec::new(), } } @@ -107,8 +111,8 @@ impl TextRenderer { }); } - let mut glyph_vertices: Vec = Vec::new(); - let mut glyph_indices: Vec = Vec::new(); + self.glyph_vertices.clear(); + self.glyph_indices.clear(); let mut glyphs_added = 0; for text_area in text_areas { @@ -291,7 +295,7 @@ impl TextRenderer { let depth = metadata_to_depth(glyph.metadata); - glyph_vertices.extend( + self.glyph_vertices.extend( iter::repeat(GlyphToRender { pos: [x, y], dim: [width as u16, height as u16], @@ -310,7 +314,7 @@ impl TextRenderer { ); let start = 4 * glyphs_added as u32; - glyph_indices.extend([ + self.glyph_indices.extend([ start, start + 1, start + 2, @@ -332,7 +336,7 @@ impl TextRenderer { return Ok(()); } - let vertices = glyph_vertices.as_slice(); + let vertices = self.glyph_vertices.as_slice(); let vertices_raw = unsafe { slice::from_raw_parts( vertices as *const _ as *const u8, @@ -356,7 +360,7 @@ impl TextRenderer { self.vertex_buffer_size = buffer_size; } - let indices = glyph_indices.as_slice(); + let indices = self.glyph_indices.as_slice(); let indices_raw = unsafe { slice::from_raw_parts( indices as *const _ as *const u8, From 3e281d1828701a4a85036a60f7fb28c7755335da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n?= Date: Sat, 30 Mar 2024 02:17:39 +0100 Subject: [PATCH 06/24] Use `rustc-hash` for `HashSet` of `glyphs_in_use` (#90) --- Cargo.toml | 3 ++- src/text_atlas.rs | 16 +++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e79ac24..d421ca9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,8 @@ license = "MIT OR Apache-2.0 OR Zlib" wgpu = { version = "0.19", default-features = false, features = ["wgsl"] } etagere = "0.2.10" cosmic-text = "0.11" -lru = "0.12.1" +lru = { version = "0.12.1", default-features = false } +rustc-hash = "1.1" [dev-dependencies] winit = { version = "0.29.10", features = ["rwh_05"] } diff --git a/src/text_atlas.rs b/src/text_atlas.rs index 44e017c..c1bc0a8 100644 --- a/src/text_atlas.rs +++ b/src/text_atlas.rs @@ -4,7 +4,11 @@ use crate::{ }; use etagere::{size2, Allocation, BucketedAtlasAllocator}; use lru::LruCache; -use std::{borrow::Cow, collections::HashSet, mem::size_of, num::NonZeroU64, sync::Arc}; +use rustc_hash::FxHasher; +use std::{ + borrow::Cow, collections::HashSet, hash::BuildHasherDefault, mem::size_of, num::NonZeroU64, + sync::Arc, +}; use wgpu::{ BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutEntry, BindingResource, BindingType, BlendState, BufferBindingType, ColorTargetState, ColorWrites, @@ -17,6 +21,8 @@ use wgpu::{ VertexState, }; +type Hasher = BuildHasherDefault; + #[allow(dead_code)] pub(crate) struct InnerAtlas { pub kind: Kind, @@ -24,8 +30,8 @@ pub(crate) struct InnerAtlas { pub texture_view: TextureView, pub packer: BucketedAtlasAllocator, pub size: u32, - pub glyph_cache: LruCache, - pub glyphs_in_use: HashSet, + pub glyph_cache: LruCache, + pub glyphs_in_use: HashSet, pub max_texture_dimension_2d: u32, } @@ -56,8 +62,8 @@ impl InnerAtlas { let texture_view = texture.create_view(&TextureViewDescriptor::default()); - let glyph_cache = LruCache::unbounded(); - let glyphs_in_use = HashSet::new(); + let glyph_cache = LruCache::unbounded_with_hasher(Hasher::default()); + let glyphs_in_use = HashSet::with_hasher(Hasher::default()); Self { kind, From 670140e2a1482a1ad3607dead44c40d8261ba582 Mon Sep 17 00:00:00 2001 From: EggShark <46581412+EggShark@users.noreply.github.com> Date: Mon, 29 Apr 2024 10:31:15 -0500 Subject: [PATCH 07/24] update to wgpu 0.20.0 (#93) --- Cargo.toml | 4 ++-- examples/hello-world.rs | 16 ++++++++++------ src/text_atlas.rs | 14 ++++++++------ 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d421ca9..9bfb0d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/grovesNL/glyphon" license = "MIT OR Apache-2.0 OR Zlib" [dependencies] -wgpu = { version = "0.19", default-features = false, features = ["wgsl"] } +wgpu = { version = "0.20", default-features = false, features = ["wgsl"] } etagere = "0.2.10" cosmic-text = "0.11" lru = { version = "0.12.1", default-features = false } @@ -16,5 +16,5 @@ rustc-hash = "1.1" [dev-dependencies] winit = { version = "0.29.10", features = ["rwh_05"] } -wgpu = { version = "0.19", default-features = true } +wgpu = { version = "0.20", default-features = true } pollster = "0.3.0" diff --git a/examples/hello-world.rs b/examples/hello-world.rs index 8924702..08e4b36 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -25,11 +25,13 @@ async fn run() { // Set up window let (width, height) = (800, 600); let event_loop = EventLoop::new().unwrap(); - let window = Arc::new(WindowBuilder::new() - .with_inner_size(LogicalSize::new(width as f64, height as f64)) - .with_title("glyphon hello world") - .build(&event_loop) - .unwrap()); + let window = Arc::new( + WindowBuilder::new() + .with_inner_size(LogicalSize::new(width as f64, height as f64)) + .with_title("glyphon hello world") + .build(&event_loop) + .unwrap(), + ); let size = window.inner_size(); let scale_factor = window.scale_factor(); @@ -51,7 +53,9 @@ async fn run() { .await .unwrap(); - let surface = instance.create_surface(window.clone()).expect("Create surface"); + let surface = instance + .create_surface(window.clone()) + .expect("Create surface"); let swapchain_format = TextureFormat::Bgra8UnormSrgb; let mut config = SurfaceConfiguration { usage: TextureUsages::RENDER_ATTACHMENT, diff --git a/src/text_atlas.rs b/src/text_atlas.rs index c1bc0a8..b855f81 100644 --- a/src/text_atlas.rs +++ b/src/text_atlas.rs @@ -13,12 +13,12 @@ use wgpu::{ BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutEntry, BindingResource, BindingType, BlendState, BufferBindingType, ColorTargetState, ColorWrites, DepthStencilState, Device, Extent3d, FilterMode, FragmentState, ImageCopyTexture, - ImageDataLayout, MultisampleState, Origin3d, PipelineLayout, PipelineLayoutDescriptor, - PrimitiveState, Queue, RenderPipeline, RenderPipelineDescriptor, Sampler, SamplerBindingType, - SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, Texture, - TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, - TextureUsages, TextureView, TextureViewDescriptor, TextureViewDimension, VertexFormat, - VertexState, + ImageDataLayout, MultisampleState, Origin3d, PipelineCompilationOptions, PipelineLayout, + PipelineLayoutDescriptor, PrimitiveState, Queue, RenderPipeline, RenderPipelineDescriptor, + Sampler, SamplerBindingType, SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, + ShaderSource, ShaderStages, Texture, TextureAspect, TextureDescriptor, TextureDimension, + TextureFormat, TextureSampleType, TextureUsages, TextureView, TextureViewDescriptor, + TextureViewDimension, VertexFormat, VertexState, }; type Hasher = BuildHasherDefault; @@ -502,6 +502,7 @@ impl TextAtlas { module: &self.shader, entry_point: "vs_main", buffers: &self.vertex_buffers, + compilation_options: PipelineCompilationOptions::default(), }, fragment: Some(FragmentState { module: &self.shader, @@ -511,6 +512,7 @@ impl TextAtlas { blend: Some(BlendState::ALPHA_BLENDING), write_mask: ColorWrites::default(), })], + compilation_options: PipelineCompilationOptions::default(), }), primitive: PrimitiveState::default(), depth_stencil: depth_stencil.clone(), From 5aed9e1477beb06029c82f4394338b8bba303500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n?= Date: Wed, 8 May 2024 15:39:19 +0200 Subject: [PATCH 08/24] Support sharing `Pipeline` state between `TextAtlas` (#95) * Support sharing `Pipeline` state between `TextAtlas` * Keep using `Vec` for pipeline cache * Use `OnceCell` to keep `Pipeline` private * Revert "Use `OnceCell` to keep `Pipeline` private" This reverts commit 4112732b1734a3bb6b915d2103e699ef549b77c1. * Rename `Pipeline` type to `Cache` --- examples/hello-world.rs | 11 +- src/cache.rs | 247 ++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + src/shader.wgsl | 10 +- src/text_atlas.rs | 239 +++++--------------------------------- src/text_render.rs | 23 ++-- 6 files changed, 295 insertions(+), 237 deletions(-) create mode 100644 src/cache.rs diff --git a/examples/hello-world.rs b/examples/hello-world.rs index 08e4b36..29d1768 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -1,6 +1,6 @@ use glyphon::{ - Attrs, Buffer, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, TextArea, - TextAtlas, TextBounds, TextRenderer, + Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, + TextArea, TextAtlas, TextBounds, TextRenderer, }; use wgpu::{ CommandEncoderDescriptor, CompositeAlphaMode, DeviceDescriptor, Features, Instance, @@ -71,8 +71,9 @@ async fn run() { // Set up text renderer let mut font_system = FontSystem::new(); - let mut cache = SwashCache::new(); - let mut atlas = TextAtlas::new(&device, &queue, swapchain_format); + let mut swash_cache = SwashCache::new(); + let cache = Cache::new(&device); + let mut atlas = TextAtlas::new(&device, &queue, &cache, swapchain_format); let mut text_renderer = TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None); let mut buffer = Buffer::new(&mut font_system, Metrics::new(30.0, 42.0)); @@ -122,7 +123,7 @@ async fn run() { }, default_color: Color::rgb(255, 255, 255), }], - &mut cache, + &mut swash_cache, ) .unwrap(); diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..f91ace8 --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,247 @@ +use crate::{GlyphToRender, Params}; + +use wgpu::{ + BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutEntry, + BindingResource, BindingType, BlendState, Buffer, BufferBindingType, ColorTargetState, + ColorWrites, DepthStencilState, Device, FilterMode, FragmentState, MultisampleState, + PipelineCompilationOptions, PipelineLayout, PipelineLayoutDescriptor, PrimitiveState, + RenderPipeline, RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, + ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, TextureFormat, + TextureSampleType, TextureView, TextureViewDimension, VertexFormat, VertexState, +}; + +use std::borrow::Cow; +use std::mem; +use std::num::NonZeroU64; +use std::ops::Deref; +use std::sync::{Arc, RwLock}; + +#[derive(Debug, Clone)] +pub struct Cache(Arc); + +#[derive(Debug)] +struct Inner { + sampler: Sampler, + shader: ShaderModule, + vertex_buffers: [wgpu::VertexBufferLayout<'static>; 1], + atlas_layout: BindGroupLayout, + uniforms_layout: BindGroupLayout, + pipeline_layout: PipelineLayout, + cache: RwLock< + Vec<( + TextureFormat, + MultisampleState, + Option, + Arc, + )>, + >, +} + +impl Cache { + pub fn new(device: &Device) -> Self { + let sampler = device.create_sampler(&SamplerDescriptor { + label: Some("glyphon sampler"), + min_filter: FilterMode::Nearest, + mag_filter: FilterMode::Nearest, + mipmap_filter: FilterMode::Nearest, + lod_min_clamp: 0f32, + lod_max_clamp: 0f32, + ..Default::default() + }); + + let shader = device.create_shader_module(ShaderModuleDescriptor { + label: Some("glyphon shader"), + source: ShaderSource::Wgsl(Cow::Borrowed(include_str!("shader.wgsl"))), + }); + + let vertex_buffer_layout = wgpu::VertexBufferLayout { + array_stride: mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[ + wgpu::VertexAttribute { + format: VertexFormat::Sint32x2, + offset: 0, + shader_location: 0, + }, + wgpu::VertexAttribute { + format: VertexFormat::Uint32, + offset: mem::size_of::() as u64 * 2, + shader_location: 1, + }, + wgpu::VertexAttribute { + format: VertexFormat::Uint32, + offset: mem::size_of::() as u64 * 3, + shader_location: 2, + }, + wgpu::VertexAttribute { + format: VertexFormat::Uint32, + offset: mem::size_of::() as u64 * 4, + shader_location: 3, + }, + wgpu::VertexAttribute { + format: VertexFormat::Uint32, + offset: mem::size_of::() as u64 * 5, + shader_location: 4, + }, + wgpu::VertexAttribute { + format: VertexFormat::Float32, + offset: mem::size_of::() as u64 * 6, + shader_location: 5, + }, + ], + }; + + let atlas_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[ + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, + ty: BindingType::Texture { + multisampled: false, + view_dimension: TextureViewDimension::D2, + sample_type: TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, + ty: BindingType::Texture { + multisampled: false, + view_dimension: TextureViewDimension::D2, + sample_type: TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + BindGroupLayoutEntry { + binding: 2, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Sampler(SamplerBindingType::Filtering), + count: None, + }, + ], + label: Some("glyphon atlas bind group layout"), + }); + + let uniforms_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: NonZeroU64::new(mem::size_of::() as u64), + }, + count: None, + }], + label: Some("glyphon uniforms bind group layout"), + }); + + let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[&atlas_layout, &uniforms_layout], + push_constant_ranges: &[], + }); + + Self(Arc::new(Inner { + sampler, + shader, + vertex_buffers: [vertex_buffer_layout], + uniforms_layout, + atlas_layout, + pipeline_layout, + cache: RwLock::new(Vec::new()), + })) + } + + pub(crate) fn create_atlas_bind_group( + &self, + device: &Device, + color_atlas: &TextureView, + mask_atlas: &TextureView, + ) -> BindGroup { + device.create_bind_group(&BindGroupDescriptor { + layout: &self.0.atlas_layout, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(color_atlas), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::TextureView(mask_atlas), + }, + BindGroupEntry { + binding: 2, + resource: BindingResource::Sampler(&self.0.sampler), + }, + ], + label: Some("glyphon atlas bind group"), + }) + } + + pub(crate) fn create_uniforms_bind_group(&self, device: &Device, buffer: &Buffer) -> BindGroup { + device.create_bind_group(&BindGroupDescriptor { + layout: &self.0.uniforms_layout, + entries: &[BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + label: Some("glyphon uniforms bind group"), + }) + } + + pub(crate) fn get_or_create_pipeline( + &self, + device: &Device, + format: TextureFormat, + multisample: MultisampleState, + depth_stencil: Option, + ) -> Arc { + let Inner { + cache, + pipeline_layout, + shader, + vertex_buffers, + .. + } = self.0.deref(); + + let mut cache = cache.write().expect("Write pipeline cache"); + + cache + .iter() + .find(|(fmt, ms, ds, _)| fmt == &format && ms == &multisample && ds == &depth_stencil) + .map(|(_, _, _, p)| Arc::clone(p)) + .unwrap_or_else(|| { + let pipeline = Arc::new(device.create_render_pipeline(&RenderPipelineDescriptor { + label: Some("glyphon pipeline"), + layout: Some(pipeline_layout), + vertex: VertexState { + module: shader, + entry_point: "vs_main", + buffers: vertex_buffers, + compilation_options: PipelineCompilationOptions::default(), + }, + fragment: Some(FragmentState { + module: shader, + entry_point: "fs_main", + targets: &[Some(ColorTargetState { + format, + blend: Some(BlendState::ALPHA_BLENDING), + write_mask: ColorWrites::default(), + })], + compilation_options: PipelineCompilationOptions::default(), + }), + primitive: PrimitiveState::default(), + depth_stencil: depth_stencil.clone(), + multisample, + multiview: None, + })); + + cache.push((format, multisample, depth_stencil, pipeline.clone())); + + pipeline + }) + .clone() + } +} diff --git a/src/lib.rs b/src/lib.rs index b9194c5..07c2993 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,10 +4,12 @@ //! [cosmic-text]: https://github.com/pop-os/cosmic-text //! [etagere]: https://github.com/nical/etagere +mod cache; mod error; mod text_atlas; mod text_render; +pub use cache::Cache; pub use error::{PrepareError, RenderError}; pub use text_atlas::{ColorMode, TextAtlas}; pub use text_render::TextRenderer; diff --git a/src/shader.wgsl b/src/shader.wgsl index 3d1ba4f..eae770a 100644 --- a/src/shader.wgsl +++ b/src/shader.wgsl @@ -21,17 +21,17 @@ struct Params { }; @group(0) @binding(0) -var params: Params; - -@group(1) @binding(0) var color_atlas_texture: texture_2d; -@group(1) @binding(1) +@group(0) @binding(1) var mask_atlas_texture: texture_2d; -@group(1) @binding(2) +@group(0) @binding(2) var atlas_sampler: sampler; +@group(1) @binding(0) +var params: Params; + fn srgb_to_linear(c: f32) -> f32 { if c <= 0.04045 { return c / 12.92; diff --git a/src/text_atlas.rs b/src/text_atlas.rs index b855f81..e32c1b0 100644 --- a/src/text_atlas.rs +++ b/src/text_atlas.rs @@ -1,24 +1,14 @@ use crate::{ - text_render::ContentType, CacheKey, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, - Params, SwashCache, + text_render::ContentType, Cache, CacheKey, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, }; use etagere::{size2, Allocation, BucketedAtlasAllocator}; use lru::LruCache; use rustc_hash::FxHasher; -use std::{ - borrow::Cow, collections::HashSet, hash::BuildHasherDefault, mem::size_of, num::NonZeroU64, - sync::Arc, -}; +use std::{collections::HashSet, hash::BuildHasherDefault, sync::Arc}; use wgpu::{ - BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutEntry, - BindingResource, BindingType, BlendState, BufferBindingType, ColorTargetState, ColorWrites, - DepthStencilState, Device, Extent3d, FilterMode, FragmentState, ImageCopyTexture, - ImageDataLayout, MultisampleState, Origin3d, PipelineCompilationOptions, PipelineLayout, - PipelineLayoutDescriptor, PrimitiveState, Queue, RenderPipeline, RenderPipelineDescriptor, - Sampler, SamplerBindingType, SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, - ShaderSource, ShaderStages, Texture, TextureAspect, TextureDescriptor, TextureDimension, - TextureFormat, TextureSampleType, TextureUsages, TextureView, TextureViewDescriptor, - TextureViewDimension, VertexFormat, VertexState, + BindGroup, Buffer, DepthStencilState, Device, Extent3d, ImageCopyTexture, ImageDataLayout, + MultisampleState, Origin3d, Queue, RenderPipeline, Texture, TextureAspect, TextureDescriptor, + TextureDimension, TextureFormat, TextureUsages, TextureView, TextureViewDescriptor, }; type Hasher = BuildHasherDefault; @@ -261,137 +251,28 @@ pub enum ColorMode { /// An atlas containing a cache of rasterized glyphs that can be rendered. pub struct TextAtlas { - pub(crate) cached_pipelines: Vec<( - MultisampleState, - Option, - Arc, - )>, - pub(crate) bind_group: Arc, - pub(crate) bind_group_layout: BindGroupLayout, - pub(crate) text_render_bind_group_layout: BindGroupLayout, - pub(crate) sampler: Sampler, + cache: Cache, + pub(crate) bind_group: BindGroup, pub(crate) color_atlas: InnerAtlas, pub(crate) mask_atlas: InnerAtlas, - pub(crate) pipeline_layout: PipelineLayout, - pub(crate) shader: ShaderModule, - pub(crate) vertex_buffers: [wgpu::VertexBufferLayout<'static>; 1], pub(crate) format: TextureFormat, pub(crate) color_mode: ColorMode, } impl TextAtlas { /// Creates a new [`TextAtlas`]. - pub fn new(device: &Device, queue: &Queue, format: TextureFormat) -> Self { - Self::with_color_mode(device, queue, format, ColorMode::Accurate) + pub fn new(device: &Device, queue: &Queue, cache: &Cache, format: TextureFormat) -> Self { + Self::with_color_mode(device, queue, cache, format, ColorMode::Accurate) } /// Creates a new [`TextAtlas`] with the given [`ColorMode`]. pub fn with_color_mode( device: &Device, queue: &Queue, + cache: &Cache, format: TextureFormat, color_mode: ColorMode, ) -> Self { - let sampler = device.create_sampler(&SamplerDescriptor { - label: Some("glyphon sampler"), - min_filter: FilterMode::Nearest, - mag_filter: FilterMode::Nearest, - mipmap_filter: FilterMode::Nearest, - lod_min_clamp: 0f32, - lod_max_clamp: 0f32, - ..Default::default() - }); - - // Create a render pipeline to use for rendering later - let shader = device.create_shader_module(ShaderModuleDescriptor { - label: Some("glyphon shader"), - source: ShaderSource::Wgsl(Cow::Borrowed(include_str!("shader.wgsl"))), - }); - - let vertex_buffers = [wgpu::VertexBufferLayout { - array_stride: size_of::() as wgpu::BufferAddress, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &[ - wgpu::VertexAttribute { - format: VertexFormat::Sint32x2, - offset: 0, - shader_location: 0, - }, - wgpu::VertexAttribute { - format: VertexFormat::Uint32, - offset: size_of::() as u64 * 2, - shader_location: 1, - }, - wgpu::VertexAttribute { - format: VertexFormat::Uint32, - offset: size_of::() as u64 * 3, - shader_location: 2, - }, - wgpu::VertexAttribute { - format: VertexFormat::Uint32, - offset: size_of::() as u64 * 4, - shader_location: 3, - }, - wgpu::VertexAttribute { - format: VertexFormat::Uint32, - offset: size_of::() as u64 * 5, - shader_location: 4, - }, - wgpu::VertexAttribute { - format: VertexFormat::Float32, - offset: size_of::() as u64 * 6, - shader_location: 5, - }, - ], - }]; - - let text_render_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - entries: &[BindGroupLayoutEntry { - binding: 0, - visibility: ShaderStages::VERTEX, - ty: BindingType::Buffer { - ty: BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: NonZeroU64::new(size_of::() as u64), - }, - count: None, - }], - label: Some("glyphon text render bind group layout"), - }); - - let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - entries: &[ - BindGroupLayoutEntry { - binding: 0, - visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, - ty: BindingType::Texture { - multisampled: false, - view_dimension: TextureViewDimension::D2, - sample_type: TextureSampleType::Float { filterable: true }, - }, - count: None, - }, - BindGroupLayoutEntry { - binding: 1, - visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, - ty: BindingType::Texture { - multisampled: false, - view_dimension: TextureViewDimension::D2, - sample_type: TextureSampleType::Float { filterable: true }, - }, - count: None, - }, - BindGroupLayoutEntry { - binding: 2, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Sampler(SamplerBindingType::Filtering), - count: None, - }, - ], - label: Some("glyphon text atlas bind group layout"), - }); - let color_atlas = InnerAtlas::new( device, queue, @@ -404,42 +285,17 @@ impl TextAtlas { ); let mask_atlas = InnerAtlas::new(device, queue, Kind::Mask); - let bind_group = Arc::new(device.create_bind_group(&BindGroupDescriptor { - layout: &bind_group_layout, - entries: &[ - BindGroupEntry { - binding: 0, - resource: BindingResource::TextureView(&color_atlas.texture_view), - }, - BindGroupEntry { - binding: 1, - resource: BindingResource::TextureView(&mask_atlas.texture_view), - }, - BindGroupEntry { - binding: 2, - resource: BindingResource::Sampler(&sampler), - }, - ], - label: Some("glyphon text atlas bind group"), - })); - - let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { - label: None, - bind_group_layouts: &[&text_render_bind_group_layout, &bind_group_layout], - push_constant_ranges: &[], - }); + let bind_group = cache.create_atlas_bind_group( + device, + &color_atlas.texture_view, + &mask_atlas.texture_view, + ); Self { - cached_pipelines: Vec::new(), - text_render_bind_group_layout, + cache: cache.clone(), bind_group, - bind_group_layout, - sampler, color_atlas, mask_atlas, - pipeline_layout, - shader, - vertex_buffers, format, color_mode, } @@ -485,65 +341,24 @@ impl TextAtlas { } pub(crate) fn get_or_create_pipeline( - &mut self, + &self, device: &Device, multisample: MultisampleState, depth_stencil: Option, ) -> Arc { - self.cached_pipelines - .iter() - .find(|(ms, ds, _)| ms == &multisample && ds == &depth_stencil) - .map(|(_, _, p)| Arc::clone(p)) - .unwrap_or_else(|| { - let pipeline = Arc::new(device.create_render_pipeline(&RenderPipelineDescriptor { - label: Some("glyphon pipeline"), - layout: Some(&self.pipeline_layout), - vertex: VertexState { - module: &self.shader, - entry_point: "vs_main", - buffers: &self.vertex_buffers, - compilation_options: PipelineCompilationOptions::default(), - }, - fragment: Some(FragmentState { - module: &self.shader, - entry_point: "fs_main", - targets: &[Some(ColorTargetState { - format: self.format, - blend: Some(BlendState::ALPHA_BLENDING), - write_mask: ColorWrites::default(), - })], - compilation_options: PipelineCompilationOptions::default(), - }), - primitive: PrimitiveState::default(), - depth_stencil: depth_stencil.clone(), - multisample, - multiview: None, - })); + self.cache + .get_or_create_pipeline(device, self.format, multisample, depth_stencil) + } - self.cached_pipelines - .push((multisample, depth_stencil, pipeline.clone())); - pipeline - }) + pub(crate) fn create_uniforms_bind_group(&self, device: &Device, buffer: &Buffer) -> BindGroup { + self.cache.create_uniforms_bind_group(device, buffer) } fn rebind(&mut self, device: &wgpu::Device) { - self.bind_group = Arc::new(device.create_bind_group(&BindGroupDescriptor { - layout: &self.bind_group_layout, - entries: &[ - BindGroupEntry { - binding: 0, - resource: BindingResource::TextureView(&self.color_atlas.texture_view), - }, - BindGroupEntry { - binding: 1, - resource: BindingResource::TextureView(&self.mask_atlas.texture_view), - }, - BindGroupEntry { - binding: 2, - resource: BindingResource::Sampler(&self.sampler), - }, - ], - label: Some("glyphon bind group"), - })); + self.bind_group = self.cache.create_atlas_bind_group( + device, + &self.color_atlas.texture_view, + &self.mask_atlas.texture_view, + ); } } diff --git a/src/text_render.rs b/src/text_render.rs index c8a3bef..0102e84 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -4,9 +4,9 @@ use crate::{ }; use std::{iter, mem::size_of, slice, sync::Arc}; use wgpu::{ - BindGroupDescriptor, BindGroupEntry, Buffer, BufferDescriptor, BufferUsages, DepthStencilState, - Device, Extent3d, ImageCopyTexture, ImageDataLayout, IndexFormat, MultisampleState, Origin3d, - Queue, RenderPass, RenderPipeline, TextureAspect, COPY_BUFFER_ALIGNMENT, + Buffer, BufferDescriptor, BufferUsages, DepthStencilState, Device, Extent3d, ImageCopyTexture, + ImageDataLayout, IndexFormat, MultisampleState, Origin3d, Queue, RenderPass, RenderPipeline, + TextureAspect, COPY_BUFFER_ALIGNMENT, }; /// A text renderer that uses cached glyphs to render text into an existing render pass. @@ -48,8 +48,6 @@ impl TextRenderer { mapped_at_creation: false, }); - let pipeline = atlas.get_or_create_pipeline(device, multisample, depth_stencil); - let params = Params { screen_resolution: Resolution { width: 0, @@ -65,14 +63,8 @@ impl TextRenderer { mapped_at_creation: false, }); - let bind_group = device.create_bind_group(&BindGroupDescriptor { - layout: &atlas.text_render_bind_group_layout, - entries: &[BindGroupEntry { - binding: 0, - resource: params_buffer.as_entire_binding(), - }], - label: Some("glyphon text render bind group"), - }); + let pipeline = atlas.get_or_create_pipeline(device, multisample, depth_stencil); + let bind_group = atlas.create_uniforms_bind_group(device, ¶ms_buffer); Self { params, @@ -103,6 +95,7 @@ impl TextRenderer { ) -> Result<(), PrepareError> { if self.params.screen_resolution != screen_resolution { self.params.screen_resolution = screen_resolution; + queue.write_buffer(&self.params_buffer, 0, unsafe { slice::from_raw_parts( &self.params as *const Params as *const u8, @@ -420,8 +413,8 @@ impl TextRenderer { } pass.set_pipeline(&self.pipeline); - pass.set_bind_group(0, &self.bind_group, &[]); - pass.set_bind_group(1, &atlas.bind_group, &[]); + pass.set_bind_group(0, &atlas.bind_group, &[]); + pass.set_bind_group(1, &self.bind_group, &[]); pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); pass.set_index_buffer(self.index_buffer.slice(..), IndexFormat::Uint32); pass.draw_indexed(0..self.vertices_to_render, 0, 0..1); From b411ea71e70f33979e4318f8570a0c5af6e78039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n?= Date: Wed, 8 May 2024 16:17:58 +0200 Subject: [PATCH 09/24] Introduce explicit `Viewport` sharing (#96) --- examples/hello-world.rs | 18 ++++++++----- src/lib.rs | 2 ++ src/text_atlas.rs | 6 +---- src/text_render.rs | 54 +++++++++----------------------------- src/viewport.rs | 57 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 84 insertions(+), 53 deletions(-) create mode 100644 src/viewport.rs diff --git a/examples/hello-world.rs b/examples/hello-world.rs index 29d1768..1ec5872 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -1,6 +1,6 @@ use glyphon::{ Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, - TextArea, TextAtlas, TextBounds, TextRenderer, + TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, }; use wgpu::{ CommandEncoderDescriptor, CompositeAlphaMode, DeviceDescriptor, Features, Instance, @@ -73,6 +73,7 @@ async fn run() { let mut font_system = FontSystem::new(); let mut swash_cache = SwashCache::new(); let cache = Cache::new(&device); + let mut viewport = Viewport::new(&device, &cache); let mut atlas = TextAtlas::new(&device, &queue, &cache, swapchain_format); let mut text_renderer = TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None); @@ -100,16 +101,21 @@ async fn run() { window.request_redraw(); } WindowEvent::RedrawRequested => { + viewport.update( + &queue, + Resolution { + width: config.width, + height: config.height, + }, + ); + text_renderer .prepare( &device, &queue, &mut font_system, &mut atlas, - Resolution { - width: config.width, - height: config.height, - }, + &viewport, [TextArea { buffer: &buffer, left: 10.0, @@ -147,7 +153,7 @@ async fn run() { occlusion_query_set: None, }); - text_renderer.render(&atlas, &mut pass).unwrap(); + text_renderer.render(&atlas, &viewport, &mut pass).unwrap(); } queue.submit(Some(encoder.finish())); diff --git a/src/lib.rs b/src/lib.rs index 07c2993..3b31120 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,11 +8,13 @@ mod cache; mod error; mod text_atlas; mod text_render; +mod viewport; pub use cache::Cache; pub use error::{PrepareError, RenderError}; pub use text_atlas::{ColorMode, TextAtlas}; pub use text_render::TextRenderer; +pub use viewport::Viewport; use text_render::ContentType; diff --git a/src/text_atlas.rs b/src/text_atlas.rs index e32c1b0..1cabbc4 100644 --- a/src/text_atlas.rs +++ b/src/text_atlas.rs @@ -6,7 +6,7 @@ use lru::LruCache; use rustc_hash::FxHasher; use std::{collections::HashSet, hash::BuildHasherDefault, sync::Arc}; use wgpu::{ - BindGroup, Buffer, DepthStencilState, Device, Extent3d, ImageCopyTexture, ImageDataLayout, + BindGroup, DepthStencilState, Device, Extent3d, ImageCopyTexture, ImageDataLayout, MultisampleState, Origin3d, Queue, RenderPipeline, Texture, TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView, TextureViewDescriptor, }; @@ -350,10 +350,6 @@ impl TextAtlas { .get_or_create_pipeline(device, self.format, multisample, depth_stencil) } - pub(crate) fn create_uniforms_bind_group(&self, device: &Device, buffer: &Buffer) -> BindGroup { - self.cache.create_uniforms_bind_group(device, buffer) - } - fn rebind(&mut self, device: &wgpu::Device) { self.bind_group = self.cache.create_atlas_bind_group( device, diff --git a/src/text_render.rs b/src/text_render.rs index 0102e84..23ca20f 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -1,8 +1,8 @@ use crate::{ - ColorMode, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, Params, PrepareError, - RenderError, Resolution, SwashCache, SwashContent, TextArea, TextAtlas, + ColorMode, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError, + SwashCache, SwashContent, TextArea, TextAtlas, Viewport, }; -use std::{iter, mem::size_of, slice, sync::Arc}; +use std::{iter, slice, sync::Arc}; use wgpu::{ Buffer, BufferDescriptor, BufferUsages, DepthStencilState, Device, Extent3d, ImageCopyTexture, ImageDataLayout, IndexFormat, MultisampleState, Origin3d, Queue, RenderPass, RenderPipeline, @@ -11,15 +11,12 @@ use wgpu::{ /// A text renderer that uses cached glyphs to render text into an existing render pass. pub struct TextRenderer { - params: Params, - params_buffer: Buffer, vertex_buffer: Buffer, vertex_buffer_size: u64, index_buffer: Buffer, index_buffer_size: u64, vertices_to_render: u32, pipeline: Arc, - bind_group: wgpu::BindGroup, glyph_vertices: Vec, glyph_indices: Vec, } @@ -48,34 +45,15 @@ impl TextRenderer { mapped_at_creation: false, }); - let params = Params { - screen_resolution: Resolution { - width: 0, - height: 0, - }, - _pad: [0, 0], - }; - - let params_buffer = device.create_buffer(&BufferDescriptor { - label: Some("glyphon params"), - size: size_of::() as u64, - usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - let pipeline = atlas.get_or_create_pipeline(device, multisample, depth_stencil); - let bind_group = atlas.create_uniforms_bind_group(device, ¶ms_buffer); Self { - params, - params_buffer, vertex_buffer, vertex_buffer_size, index_buffer, index_buffer_size, vertices_to_render: 0, pipeline, - bind_group, glyph_vertices: Vec::new(), glyph_indices: Vec::new(), } @@ -88,26 +66,17 @@ impl TextRenderer { queue: &Queue, font_system: &mut FontSystem, atlas: &mut TextAtlas, - screen_resolution: Resolution, + viewport: &Viewport, text_areas: impl IntoIterator>, cache: &mut SwashCache, mut metadata_to_depth: impl FnMut(usize) -> f32, ) -> Result<(), PrepareError> { - if self.params.screen_resolution != screen_resolution { - self.params.screen_resolution = screen_resolution; - - queue.write_buffer(&self.params_buffer, 0, unsafe { - slice::from_raw_parts( - &self.params as *const Params as *const u8, - size_of::(), - ) - }); - } - self.glyph_vertices.clear(); self.glyph_indices.clear(); let mut glyphs_added = 0; + let resolution = viewport.resolution(); + for text_area in text_areas { for run in text_area.buffer.layout_runs() { for glyph in run.glyphs.iter() { @@ -238,8 +207,8 @@ impl TextRenderer { let bounds_min_x = text_area.bounds.left.max(0); let bounds_min_y = text_area.bounds.top.max(0); - let bounds_max_x = text_area.bounds.right.min(screen_resolution.width as i32); - let bounds_max_y = text_area.bounds.bottom.min(screen_resolution.height as i32); + let bounds_max_x = text_area.bounds.right.min(resolution.width as i32); + let bounds_max_y = text_area.bounds.bottom.min(resolution.height as i32); // Starts beyond right edge or ends beyond left edge let max_x = x + width; @@ -386,7 +355,7 @@ impl TextRenderer { queue: &Queue, font_system: &mut FontSystem, atlas: &mut TextAtlas, - screen_resolution: Resolution, + viewport: &Viewport, text_areas: impl IntoIterator>, cache: &mut SwashCache, ) -> Result<(), PrepareError> { @@ -395,7 +364,7 @@ impl TextRenderer { queue, font_system, atlas, - screen_resolution, + viewport, text_areas, cache, zero_depth, @@ -406,6 +375,7 @@ impl TextRenderer { pub fn render<'pass>( &'pass self, atlas: &'pass TextAtlas, + viewport: &'pass Viewport, pass: &mut RenderPass<'pass>, ) -> Result<(), RenderError> { if self.vertices_to_render == 0 { @@ -414,7 +384,7 @@ impl TextRenderer { pass.set_pipeline(&self.pipeline); pass.set_bind_group(0, &atlas.bind_group, &[]); - pass.set_bind_group(1, &self.bind_group, &[]); + pass.set_bind_group(1, &viewport.bind_group, &[]); pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); pass.set_index_buffer(self.index_buffer.slice(..), IndexFormat::Uint32); pass.draw_indexed(0..self.vertices_to_render, 0, 0..1); diff --git a/src/viewport.rs b/src/viewport.rs new file mode 100644 index 0000000..14d3412 --- /dev/null +++ b/src/viewport.rs @@ -0,0 +1,57 @@ +use crate::{Cache, Params, Resolution}; + +use wgpu::{BindGroup, Buffer, BufferDescriptor, BufferUsages, Device, Queue}; + +use std::mem; +use std::slice; + +#[derive(Debug)] +pub struct Viewport { + params: Params, + params_buffer: Buffer, + pub(crate) bind_group: BindGroup, +} + +impl Viewport { + pub fn new(device: &Device, cache: &Cache) -> Self { + let params = Params { + screen_resolution: Resolution { + width: 0, + height: 0, + }, + _pad: [0, 0], + }; + + let params_buffer = device.create_buffer(&BufferDescriptor { + label: Some("glyphon params"), + size: mem::size_of::() as u64, + usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let bind_group = cache.create_uniforms_bind_group(device, ¶ms_buffer); + + Self { + params, + params_buffer, + bind_group, + } + } + + pub fn update(&mut self, queue: &Queue, resolution: Resolution) { + if self.params.screen_resolution != resolution { + self.params.screen_resolution = resolution; + + queue.write_buffer(&self.params_buffer, 0, unsafe { + slice::from_raw_parts( + &self.params as *const Params as *const u8, + mem::size_of::(), + ) + }); + } + } + + pub fn resolution(&self) -> Resolution { + self.params.screen_resolution + } +} From 0fdbd90c55781da460456a05ec8fb6864d9295e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Pakalns?= Date: Mon, 27 May 2024 04:22:30 +0300 Subject: [PATCH 10/24] Simplify rendering pipeline, reduce gpu memory usage, remove duplicated data (#86) --- src/cache.rs | 13 +++--- src/shader.wgsl | 28 +++++-------- src/text_render.rs | 101 +++++++++------------------------------------ 3 files changed, 38 insertions(+), 104 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index f91ace8..1acd514 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -5,9 +5,9 @@ use wgpu::{ BindingResource, BindingType, BlendState, Buffer, BufferBindingType, ColorTargetState, ColorWrites, DepthStencilState, Device, FilterMode, FragmentState, MultisampleState, PipelineCompilationOptions, PipelineLayout, PipelineLayoutDescriptor, PrimitiveState, - RenderPipeline, RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, - ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, TextureFormat, - TextureSampleType, TextureView, TextureViewDimension, VertexFormat, VertexState, + PrimitiveTopology, RenderPipeline, RenderPipelineDescriptor, Sampler, SamplerBindingType, + SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, + TextureFormat, TextureSampleType, TextureView, TextureViewDimension, VertexFormat, VertexState, }; use std::borrow::Cow; @@ -56,7 +56,7 @@ impl Cache { let vertex_buffer_layout = wgpu::VertexBufferLayout { array_stride: mem::size_of::() as wgpu::BufferAddress, - step_mode: wgpu::VertexStepMode::Vertex, + step_mode: wgpu::VertexStepMode::Instance, attributes: &[ wgpu::VertexAttribute { format: VertexFormat::Sint32x2, @@ -232,7 +232,10 @@ impl Cache { })], compilation_options: PipelineCompilationOptions::default(), }), - primitive: PrimitiveState::default(), + primitive: PrimitiveState { + topology: PrimitiveTopology::TriangleStrip, + ..Default::default() + }, depth_stencil: depth_stencil.clone(), multisample, multiview: None, diff --git a/src/shader.wgsl b/src/shader.wgsl index eae770a..1813a66 100644 --- a/src/shader.wgsl +++ b/src/shader.wgsl @@ -47,25 +47,17 @@ fn vs_main(in_vert: VertexInput) -> VertexOutput { let height = (in_vert.dim & 0xffff0000u) >> 16u; let color = in_vert.color; var uv = vec2(in_vert.uv & 0xffffu, (in_vert.uv & 0xffff0000u) >> 16u); - let v = in_vert.vertex_idx % 4u; + let v = in_vert.vertex_idx; - switch v { - case 1u: { - pos.x += i32(width); - uv.x += width; - } - case 2u: { - pos.x += i32(width); - pos.y += i32(height); - uv.x += width; - uv.y += height; - } - case 3u: { - pos.y += i32(height); - uv.y += height; - } - default: {} - } + let corner_position = vec2( + in_vert.vertex_idx & 1u, + (in_vert.vertex_idx >> 1u) & 1u, + ); + + let corner_offset = vec2(width, height) * corner_position; + + uv = uv + corner_offset; + pos = pos + vec2(corner_offset); var vert_output: VertexOutput; diff --git a/src/text_render.rs b/src/text_render.rs index 23ca20f..ce5549b 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -2,23 +2,19 @@ use crate::{ ColorMode, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError, SwashCache, SwashContent, TextArea, TextAtlas, Viewport, }; -use std::{iter, slice, sync::Arc}; +use std::{slice, sync::Arc}; use wgpu::{ Buffer, BufferDescriptor, BufferUsages, DepthStencilState, Device, Extent3d, ImageCopyTexture, - ImageDataLayout, IndexFormat, MultisampleState, Origin3d, Queue, RenderPass, RenderPipeline, - TextureAspect, COPY_BUFFER_ALIGNMENT, + ImageDataLayout, MultisampleState, Origin3d, Queue, RenderPass, RenderPipeline, TextureAspect, + COPY_BUFFER_ALIGNMENT, }; /// A text renderer that uses cached glyphs to render text into an existing render pass. pub struct TextRenderer { vertex_buffer: Buffer, vertex_buffer_size: u64, - index_buffer: Buffer, - index_buffer_size: u64, - vertices_to_render: u32, pipeline: Arc, glyph_vertices: Vec, - glyph_indices: Vec, } impl TextRenderer { @@ -37,25 +33,13 @@ impl TextRenderer { mapped_at_creation: false, }); - let index_buffer_size = next_copy_buffer_size(4096); - let index_buffer = device.create_buffer(&BufferDescriptor { - label: Some("glyphon indices"), - size: index_buffer_size, - usage: BufferUsages::INDEX | BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - let pipeline = atlas.get_or_create_pipeline(device, multisample, depth_stencil); Self { vertex_buffer, vertex_buffer_size, - index_buffer, - index_buffer_size, - vertices_to_render: 0, pipeline, glyph_vertices: Vec::new(), - glyph_indices: Vec::new(), } } @@ -72,8 +56,6 @@ impl TextRenderer { mut metadata_to_depth: impl FnMut(usize) -> f32, ) -> Result<(), PrepareError> { self.glyph_vertices.clear(); - self.glyph_indices.clear(); - let mut glyphs_added = 0; let resolution = viewport.resolution(); @@ -257,43 +239,25 @@ impl TextRenderer { let depth = metadata_to_depth(glyph.metadata); - self.glyph_vertices.extend( - iter::repeat(GlyphToRender { - pos: [x, y], - dim: [width as u16, height as u16], - uv: [atlas_x, atlas_y], - color: color.0, - content_type_with_srgb: [ - content_type as u16, - match atlas.color_mode { - ColorMode::Accurate => TextColorConversion::ConvertToLinear, - ColorMode::Web => TextColorConversion::None, - } as u16, - ], - depth, - }) - .take(4), - ); - - let start = 4 * glyphs_added as u32; - self.glyph_indices.extend([ - start, - start + 1, - start + 2, - start, - start + 2, - start + 3, - ]); - - glyphs_added += 1; + self.glyph_vertices.push(GlyphToRender { + pos: [x, y], + dim: [width as u16, height as u16], + uv: [atlas_x, atlas_y], + color: color.0, + content_type_with_srgb: [ + content_type as u16, + match atlas.color_mode { + ColorMode::Accurate => TextColorConversion::ConvertToLinear, + ColorMode::Web => TextColorConversion::None, + } as u16, + ], + depth, + }); } } } - const VERTICES_PER_GLYPH: u32 = 6; - self.vertices_to_render = glyphs_added as u32 * VERTICES_PER_GLYPH; - - let will_render = glyphs_added > 0; + let will_render = !self.glyph_vertices.is_empty(); if !will_render { return Ok(()); } @@ -322,30 +286,6 @@ impl TextRenderer { self.vertex_buffer_size = buffer_size; } - let indices = self.glyph_indices.as_slice(); - let indices_raw = unsafe { - slice::from_raw_parts( - indices as *const _ as *const u8, - std::mem::size_of_val(indices), - ) - }; - - if self.index_buffer_size >= indices_raw.len() as u64 { - queue.write_buffer(&self.index_buffer, 0, indices_raw); - } else { - self.index_buffer.destroy(); - - let (buffer, buffer_size) = create_oversized_buffer( - device, - Some("glyphon indices"), - indices_raw, - BufferUsages::INDEX | BufferUsages::COPY_DST, - ); - - self.index_buffer = buffer; - self.index_buffer_size = buffer_size; - } - Ok(()) } @@ -378,7 +318,7 @@ impl TextRenderer { viewport: &'pass Viewport, pass: &mut RenderPass<'pass>, ) -> Result<(), RenderError> { - if self.vertices_to_render == 0 { + if self.glyph_vertices.is_empty() { return Ok(()); } @@ -386,8 +326,7 @@ impl TextRenderer { pass.set_bind_group(0, &atlas.bind_group, &[]); pass.set_bind_group(1, &viewport.bind_group, &[]); pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); - pass.set_index_buffer(self.index_buffer.slice(..), IndexFormat::Uint32); - pass.draw_indexed(0..self.vertices_to_render, 0, 0..1); + pass.draw(0..4, 0..self.glyph_vertices.len() as u32); Ok(()) } From 2a457087674b0c124d37c85bc6769ba27dcde173 Mon Sep 17 00:00:00 2001 From: Josh Groves Date: Mon, 27 May 2024 00:04:17 -0230 Subject: [PATCH 11/24] Use default limits for example (#98) --- examples/hello-world.rs | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/examples/hello-world.rs b/examples/hello-world.rs index 1ec5872..c2464c3 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -2,11 +2,12 @@ use glyphon::{ Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, }; +use std::sync::Arc; use wgpu::{ - CommandEncoderDescriptor, CompositeAlphaMode, DeviceDescriptor, Features, Instance, - InstanceDescriptor, Limits, LoadOp, MultisampleState, Operations, PresentMode, - RenderPassColorAttachment, RenderPassDescriptor, RequestAdapterOptions, SurfaceConfiguration, - TextureFormat, TextureUsages, TextureViewDescriptor, + CommandEncoderDescriptor, CompositeAlphaMode, DeviceDescriptor, Instance, InstanceDescriptor, + LoadOp, MultisampleState, Operations, PresentMode, RenderPassColorAttachment, + RenderPassDescriptor, RequestAdapterOptions, SurfaceConfiguration, TextureFormat, + TextureUsages, TextureViewDescriptor, }; use winit::{ dpi::LogicalSize, @@ -15,8 +16,6 @@ use winit::{ window::WindowBuilder, }; -use std::sync::Arc; - fn main() { pollster::block_on(run()); } @@ -42,14 +41,7 @@ async fn run() { .await .unwrap(); let (device, queue) = adapter - .request_device( - &DeviceDescriptor { - label: None, - required_features: Features::empty(), - required_limits: Limits::downlevel_defaults(), - }, - None, - ) + .request_device(&DeviceDescriptor::default(), None) .await .unwrap(); From 47f4126f999753e14728c0862b822677f1b039e2 Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Mon, 8 Jul 2024 07:52:33 -0500 Subject: [PATCH 12/24] update to cosmic-text 0.12 and rustc-hash 2.0 (#104) --- Cargo.toml | 4 ++-- examples/hello-world.rs | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9bfb0d2..9e70036 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,9 @@ license = "MIT OR Apache-2.0 OR Zlib" [dependencies] wgpu = { version = "0.20", default-features = false, features = ["wgsl"] } etagere = "0.2.10" -cosmic-text = "0.11" +cosmic-text = "0.12" lru = { version = "0.12.1", default-features = false } -rustc-hash = "1.1" +rustc-hash = "2.0" [dev-dependencies] winit = { version = "0.29.10", features = ["rwh_05"] } diff --git a/examples/hello-world.rs b/examples/hello-world.rs index c2464c3..b1cfabe 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -74,7 +74,11 @@ async fn run() { let physical_width = (width as f64 * scale_factor) as f32; let physical_height = (height as f64 * scale_factor) as f32; - buffer.set_size(&mut font_system, physical_width, physical_height); + buffer.set_size( + &mut font_system, + Some(physical_width), + Some(physical_height), + ); buffer.set_text(&mut font_system, "Hello world! 👋\nThis is rendered with 🦅 glyphon 🦁\nThe text below should be partially clipped.\na b c d e f g h i j k l m n o p q r s t u v w x y z", Attrs::new().family(Family::SansSerif), Shaping::Advanced); buffer.shape_until_scroll(&mut font_system, false); From 9def85304266605d2bd93fc4bf33a294db67f3d3 Mon Sep 17 00:00:00 2001 From: Erich Gubler Date: Tue, 23 Jul 2024 23:16:06 -0400 Subject: [PATCH 13/24] =?UTF-8?q?build:=20upgrade=20`wgpu`=200.20=20?= =?UTF-8?q?=E2=86=92=2022=20(#106)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 4 ++-- src/cache.rs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9e70036..47048df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/grovesNL/glyphon" license = "MIT OR Apache-2.0 OR Zlib" [dependencies] -wgpu = { version = "0.20", default-features = false, features = ["wgsl"] } +wgpu = { version = "22", default-features = false, features = ["wgsl"] } etagere = "0.2.10" cosmic-text = "0.12" lru = { version = "0.12.1", default-features = false } @@ -16,5 +16,5 @@ rustc-hash = "2.0" [dev-dependencies] winit = { version = "0.29.10", features = ["rwh_05"] } -wgpu = { version = "0.20", default-features = true } +wgpu = { version = "22", default-features = true } pollster = "0.3.0" diff --git a/src/cache.rs b/src/cache.rs index 1acd514..46dbd25 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -239,6 +239,7 @@ impl Cache { depth_stencil: depth_stencil.clone(), multisample, multiview: None, + cache: None, })); cache.push((format, multisample, depth_stencil, pipeline.clone())); From 719d69a1ee359637ea74a200f47257ca2284dee8 Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:05:55 -0500 Subject: [PATCH 14/24] update example to use winit 0.30 (#105) * update example to use winit 0.30 * omit unneeded field in example --- Cargo.toml | 4 +- examples/hello-world.rs | 358 +++++++++++++++++++++++----------------- 2 files changed, 211 insertions(+), 151 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 47048df..b87a8e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,6 @@ lru = { version = "0.12.1", default-features = false } rustc-hash = "2.0" [dev-dependencies] -winit = { version = "0.29.10", features = ["rwh_05"] } -wgpu = { version = "22", default-features = true } +winit = "0.30.3" +wgpu = "22" pollster = "0.3.0" diff --git a/examples/hello-world.rs b/examples/hello-world.rs index b1cfabe..06ce896 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -9,158 +9,218 @@ use wgpu::{ RenderPassDescriptor, RequestAdapterOptions, SurfaceConfiguration, TextureFormat, TextureUsages, TextureViewDescriptor, }; -use winit::{ - dpi::LogicalSize, - event::{Event, WindowEvent}, - event_loop::EventLoop, - window::WindowBuilder, -}; +use winit::{dpi::LogicalSize, event::WindowEvent, event_loop::EventLoop, window::Window}; fn main() { - pollster::block_on(run()); -} - -async fn run() { - // Set up window - let (width, height) = (800, 600); let event_loop = EventLoop::new().unwrap(); - let window = Arc::new( - WindowBuilder::new() - .with_inner_size(LogicalSize::new(width as f64, height as f64)) - .with_title("glyphon hello world") - .build(&event_loop) - .unwrap(), - ); - let size = window.inner_size(); - let scale_factor = window.scale_factor(); - - // Set up surface - let instance = Instance::new(InstanceDescriptor::default()); - let adapter = instance - .request_adapter(&RequestAdapterOptions::default()) - .await - .unwrap(); - let (device, queue) = adapter - .request_device(&DeviceDescriptor::default(), None) - .await - .unwrap(); - - let surface = instance - .create_surface(window.clone()) - .expect("Create surface"); - let swapchain_format = TextureFormat::Bgra8UnormSrgb; - let mut config = SurfaceConfiguration { - usage: TextureUsages::RENDER_ATTACHMENT, - format: swapchain_format, - width: size.width, - height: size.height, - present_mode: PresentMode::Fifo, - alpha_mode: CompositeAlphaMode::Opaque, - view_formats: vec![], - desired_maximum_frame_latency: 2, - }; - surface.configure(&device, &config); - - // Set up text renderer - let mut font_system = FontSystem::new(); - let mut swash_cache = SwashCache::new(); - let cache = Cache::new(&device); - let mut viewport = Viewport::new(&device, &cache); - let mut atlas = TextAtlas::new(&device, &queue, &cache, swapchain_format); - let mut text_renderer = - TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None); - let mut buffer = Buffer::new(&mut font_system, Metrics::new(30.0, 42.0)); - - let physical_width = (width as f64 * scale_factor) as f32; - let physical_height = (height as f64 * scale_factor) as f32; - - buffer.set_size( - &mut font_system, - Some(physical_width), - Some(physical_height), - ); - buffer.set_text(&mut font_system, "Hello world! 👋\nThis is rendered with 🦅 glyphon 🦁\nThe text below should be partially clipped.\na b c d e f g h i j k l m n o p q r s t u v w x y z", Attrs::new().family(Family::SansSerif), Shaping::Advanced); - buffer.shape_until_scroll(&mut font_system, false); - event_loop - .run(move |event, target| { - if let Event::WindowEvent { - window_id: _, - event, - } = event - { - match event { - WindowEvent::Resized(size) => { - config.width = size.width; - config.height = size.height; - surface.configure(&device, &config); - window.request_redraw(); - } - WindowEvent::RedrawRequested => { - viewport.update( - &queue, - Resolution { - width: config.width, - height: config.height, - }, - ); - - text_renderer - .prepare( - &device, - &queue, - &mut font_system, - &mut atlas, - &viewport, - [TextArea { - buffer: &buffer, - left: 10.0, - top: 10.0, - scale: 1.0, - bounds: TextBounds { - left: 0, - top: 0, - right: 600, - bottom: 160, - }, - default_color: Color::rgb(255, 255, 255), - }], - &mut swash_cache, - ) - .unwrap(); - - let frame = surface.get_current_texture().unwrap(); - let view = frame.texture.create_view(&TextureViewDescriptor::default()); - let mut encoder = device - .create_command_encoder(&CommandEncoderDescriptor { label: None }); - { - let mut pass = encoder.begin_render_pass(&RenderPassDescriptor { - label: None, - color_attachments: &[Some(RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: Operations { - load: LoadOp::Clear(wgpu::Color::BLACK), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - text_renderer.render(&atlas, &viewport, &mut pass).unwrap(); - } - - queue.submit(Some(encoder.finish())); - frame.present(); - - atlas.trim(); - } - WindowEvent::CloseRequested => target.exit(), - _ => {} - } - } - }) + .run_app(&mut Application { window_state: None }) .unwrap(); } + +struct WindowState { + device: wgpu::Device, + queue: wgpu::Queue, + surface: wgpu::Surface<'static>, + surface_config: SurfaceConfiguration, + + font_system: FontSystem, + swash_cache: SwashCache, + viewport: glyphon::Viewport, + atlas: glyphon::TextAtlas, + text_renderer: glyphon::TextRenderer, + text_buffer: glyphon::Buffer, + + // Make sure that the winit window is last in the struct so that + // it is dropped after the wgpu surface is dropped, otherwise the + // program may crash when closed. This is probably a bug in wgpu. + window: Arc, +} + +impl WindowState { + async fn new(window: Arc) -> Self { + let physical_size = window.inner_size(); + let scale_factor = window.scale_factor(); + + // Set up surface + let instance = Instance::new(InstanceDescriptor::default()); + let adapter = instance + .request_adapter(&RequestAdapterOptions::default()) + .await + .unwrap(); + let (device, queue) = adapter + .request_device(&DeviceDescriptor::default(), None) + .await + .unwrap(); + + let surface = instance + .create_surface(window.clone()) + .expect("Create surface"); + let swapchain_format = TextureFormat::Bgra8UnormSrgb; + let surface_config = SurfaceConfiguration { + usage: TextureUsages::RENDER_ATTACHMENT, + format: swapchain_format, + width: physical_size.width, + height: physical_size.height, + present_mode: PresentMode::Fifo, + alpha_mode: CompositeAlphaMode::Opaque, + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + surface.configure(&device, &surface_config); + + // Set up text renderer + let mut font_system = FontSystem::new(); + let swash_cache = SwashCache::new(); + let cache = Cache::new(&device); + let viewport = Viewport::new(&device, &cache); + let mut atlas = TextAtlas::new(&device, &queue, &cache, swapchain_format); + let text_renderer = + TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None); + let mut text_buffer = Buffer::new(&mut font_system, Metrics::new(30.0, 42.0)); + + let physical_width = (physical_size.width as f64 * scale_factor) as f32; + let physical_height = (physical_size.height as f64 * scale_factor) as f32; + + text_buffer.set_size( + &mut font_system, + Some(physical_width), + Some(physical_height), + ); + text_buffer.set_text(&mut font_system, "Hello world! 👋\nThis is rendered with 🦅 glyphon 🦁\nThe text below should be partially clipped.\na b c d e f g h i j k l m n o p q r s t u v w x y z", Attrs::new().family(Family::SansSerif), Shaping::Advanced); + text_buffer.shape_until_scroll(&mut font_system, false); + + Self { + device, + queue, + surface, + surface_config, + font_system, + swash_cache, + viewport, + atlas, + text_renderer, + text_buffer, + window, + } + } +} + +struct Application { + window_state: Option, +} + +impl winit::application::ApplicationHandler for Application { + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + if self.window_state.is_some() { + return; + } + + // Set up window + let (width, height) = (800, 600); + let window_attributes = Window::default_attributes() + .with_inner_size(LogicalSize::new(width as f64, height as f64)) + .with_title("glyphon hello world"); + let window = Arc::new(event_loop.create_window(window_attributes).unwrap()); + + self.window_state = Some(pollster::block_on(WindowState::new(window))); + } + + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + _window_id: winit::window::WindowId, + event: WindowEvent, + ) { + let Some(state) = &mut self.window_state else { + return; + }; + + let WindowState { + window, + device, + queue, + surface, + surface_config, + font_system, + swash_cache, + viewport, + atlas, + text_renderer, + text_buffer, + .. + } = state; + + match event { + WindowEvent::Resized(size) => { + surface_config.width = size.width; + surface_config.height = size.height; + surface.configure(&device, &surface_config); + window.request_redraw(); + } + WindowEvent::RedrawRequested => { + viewport.update( + &queue, + Resolution { + width: surface_config.width, + height: surface_config.height, + }, + ); + + text_renderer + .prepare( + device, + queue, + font_system, + atlas, + viewport, + [TextArea { + buffer: text_buffer, + left: 10.0, + top: 10.0, + scale: 1.0, + bounds: TextBounds { + left: 0, + top: 0, + right: 600, + bottom: 160, + }, + default_color: Color::rgb(255, 255, 255), + }], + swash_cache, + ) + .unwrap(); + + let frame = surface.get_current_texture().unwrap(); + let view = frame.texture.create_view(&TextureViewDescriptor::default()); + let mut encoder = + device.create_command_encoder(&CommandEncoderDescriptor { label: None }); + { + let mut pass = encoder.begin_render_pass(&RenderPassDescriptor { + label: None, + color_attachments: &[Some(RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + text_renderer.render(&atlas, &viewport, &mut pass).unwrap(); + } + + queue.submit(Some(encoder.finish())); + frame.present(); + + atlas.trim(); + } + WindowEvent::CloseRequested => event_loop.exit(), + _ => {} + } + } +} From f82094703e93110689a02ffd07b5f11a3db6d563 Mon Sep 17 00:00:00 2001 From: tamewild <138316124+tamewild@users.noreply.github.com> Date: Sun, 28 Jul 2024 18:34:16 +0700 Subject: [PATCH 15/24] iterate on visible runs within textbounds (#108) --- src/text_render.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/text_render.rs b/src/text_render.rs index ce5549b..f73da12 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -60,7 +60,18 @@ impl TextRenderer { let resolution = viewport.resolution(); for text_area in text_areas { - for run in text_area.buffer.layout_runs() { + let is_run_visible = |run: &cosmic_text::LayoutRun| { + let start_y = (text_area.top + run.line_top) as i32; + let end_y = (text_area.top + run.line_top + run.line_height) as i32; + + start_y <= text_area.bounds.bottom && text_area.bounds.top <= end_y + }; + + let layout_runs = text_area.buffer.layout_runs() + .skip_while(|run| !is_run_visible(run)) + .take_while(is_run_visible); + + for run in layout_runs { for glyph in run.glyphs.iter() { let physical_glyph = glyph.physical((text_area.left, text_area.top), text_area.scale); From 3dab1f2dc47e50b7d3d125683a83d704d788f341 Mon Sep 17 00:00:00 2001 From: Josh Groves Date: Mon, 29 Jul 2024 11:14:41 -0230 Subject: [PATCH 16/24] Update CI (#109) * Update to checkout 4 * Make sure CI includes all targets --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e39b36..580c434 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,8 +14,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Build - run: cargo build --verbose + run: cargo build --all-targets --verbose - name: Run tests run: cargo test --verbose From ce6ede951caf84569563ffdf1ef5a98aeae484e1 Mon Sep 17 00:00:00 2001 From: Josh Groves Date: Mon, 29 Jul 2024 11:59:26 -0230 Subject: [PATCH 17/24] Move text area bounds outside run/glyph loops (#110) --- src/text_render.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/text_render.rs b/src/text_render.rs index f73da12..d89fd02 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -60,14 +60,21 @@ impl TextRenderer { let resolution = viewport.resolution(); for text_area in text_areas { + let bounds_min_x = text_area.bounds.left.max(0); + let bounds_min_y = text_area.bounds.top.max(0); + let bounds_max_x = text_area.bounds.right.min(resolution.width as i32); + let bounds_max_y = text_area.bounds.bottom.min(resolution.height as i32); + let is_run_visible = |run: &cosmic_text::LayoutRun| { let start_y = (text_area.top + run.line_top) as i32; let end_y = (text_area.top + run.line_top + run.line_height) as i32; - start_y <= text_area.bounds.bottom && text_area.bounds.top <= end_y + start_y <= bounds_max_y && bounds_min_y <= end_y }; - let layout_runs = text_area.buffer.layout_runs() + let layout_runs = text_area + .buffer + .layout_runs() .skip_while(|run| !is_run_visible(run)) .take_while(is_run_visible); @@ -198,11 +205,6 @@ impl TextRenderer { let mut width = details.width as i32; let mut height = details.height as i32; - let bounds_min_x = text_area.bounds.left.max(0); - let bounds_min_y = text_area.bounds.top.max(0); - let bounds_max_x = text_area.bounds.right.min(resolution.width as i32); - let bounds_max_y = text_area.bounds.bottom.min(resolution.height as i32); - // Starts beyond right edge or ends beyond left edge let max_x = x + width; if x > bounds_max_x || max_x < bounds_min_x { From b2129f1765333156a57e19ed5ddc07d4584188b3 Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Mon, 16 Sep 2024 23:57:40 -0500 Subject: [PATCH 18/24] Add support for custom icons/glyphs (#102) * add support for svg icons * remove SVG helper struct * forgot to remove default features * rework api for custom glyphs * remove unused file * expose custom glyph structs * remove `InlineBox` * use slice for TextArea::custom_glyphs * offset custom glyphs by text area position * remove svg feature * remove unused file * add scale field to CustomGlyphInput * update custom-glyphs example to winit 0.30 * fix the mess merge conflicts made * add final newline * make custom-glyphs a default feature * remove custom-glyphs feature * remove unnecessary pub(crate) * rename CustomGlyphDesc to CustomGlyph * rename CustomGlyphID to CustomGlyphId * improve custom glyph API and refactor text renderer * rename CustomGlyphInput and CustomGlyphOutput, add some docs --- Cargo.toml | 1 + examples/custom-glyphs.rs | 332 ++++++++++++++++++++ examples/eagle.svg | 72 +++++ examples/hello-world.rs | 1 + examples/lion.svg | 169 +++++++++++ src/custom_glyph.rs | 113 +++++++ src/lib.rs | 9 +- src/text_atlas.rs | 78 ++++- src/text_render.rs | 620 +++++++++++++++++++++++++------------- 9 files changed, 1173 insertions(+), 222 deletions(-) create mode 100644 examples/custom-glyphs.rs create mode 100644 examples/eagle.svg create mode 100644 examples/lion.svg create mode 100644 src/custom_glyph.rs diff --git a/Cargo.toml b/Cargo.toml index b87a8e4..5bbd692 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,4 +17,5 @@ rustc-hash = "2.0" [dev-dependencies] winit = "0.30.3" wgpu = "22" +resvg = { version = "0.42", default-features = false } pollster = "0.3.0" diff --git a/examples/custom-glyphs.rs b/examples/custom-glyphs.rs new file mode 100644 index 0000000..6b1ecc5 --- /dev/null +++ b/examples/custom-glyphs.rs @@ -0,0 +1,332 @@ +use glyphon::{ + Attrs, Buffer, Cache, Color, ContentType, CustomGlyph, RasterizationRequest, RasterizedCustomGlyph, + Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, TextArea, TextAtlas, TextBounds, + TextRenderer, Viewport, +}; +use std::sync::Arc; +use wgpu::{ + CommandEncoderDescriptor, CompositeAlphaMode, DeviceDescriptor, Instance, InstanceDescriptor, + LoadOp, MultisampleState, Operations, PresentMode, RenderPassColorAttachment, + RenderPassDescriptor, RequestAdapterOptions, SurfaceConfiguration, TextureFormat, + TextureUsages, TextureViewDescriptor, +}; +use winit::{dpi::LogicalSize, event::WindowEvent, event_loop::EventLoop, window::Window}; + +// Example SVG icons are from https://publicdomainvectors.org/ +static LION_SVG: &[u8] = include_bytes!("./lion.svg"); +static EAGLE_SVG: &[u8] = include_bytes!("./eagle.svg"); + +fn main() { + let event_loop = EventLoop::new().unwrap(); + event_loop + .run_app(&mut Application { window_state: None }) + .unwrap(); +} + +struct WindowState { + device: wgpu::Device, + queue: wgpu::Queue, + surface: wgpu::Surface<'static>, + surface_config: SurfaceConfiguration, + + font_system: FontSystem, + swash_cache: SwashCache, + viewport: glyphon::Viewport, + atlas: glyphon::TextAtlas, + text_renderer: glyphon::TextRenderer, + text_buffer: glyphon::Buffer, + + rasterize_svg: Box Option>, + + // Make sure that the winit window is last in the struct so that + // it is dropped after the wgpu surface is dropped, otherwise the + // program may crash when closed. This is probably a bug in wgpu. + window: Arc, +} + +impl WindowState { + async fn new(window: Arc) -> Self { + let physical_size = window.inner_size(); + let scale_factor = window.scale_factor(); + + // Set up surface + let instance = Instance::new(InstanceDescriptor::default()); + let adapter = instance + .request_adapter(&RequestAdapterOptions::default()) + .await + .unwrap(); + let (device, queue) = adapter + .request_device(&DeviceDescriptor::default(), None) + .await + .unwrap(); + + let surface = instance + .create_surface(window.clone()) + .expect("Create surface"); + let swapchain_format = TextureFormat::Bgra8UnormSrgb; + let surface_config = SurfaceConfiguration { + usage: TextureUsages::RENDER_ATTACHMENT, + format: swapchain_format, + width: physical_size.width, + height: physical_size.height, + present_mode: PresentMode::Fifo, + alpha_mode: CompositeAlphaMode::Opaque, + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + surface.configure(&device, &surface_config); + + // Set up text renderer + let mut font_system = FontSystem::new(); + let swash_cache = SwashCache::new(); + let cache = Cache::new(&device); + let viewport = Viewport::new(&device, &cache); + let mut atlas = TextAtlas::new(&device, &queue, &cache, swapchain_format); + let text_renderer = + TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None); + let mut text_buffer = Buffer::new(&mut font_system, Metrics::new(30.0, 42.0)); + + let physical_width = (physical_size.width as f64 * scale_factor) as f32; + let physical_height = (physical_size.height as f64 * scale_factor) as f32; + + text_buffer.set_size( + &mut font_system, + Some(physical_width), + Some(physical_height), + ); + text_buffer.set_text( + &mut font_system, + "SVG icons! --->\n\nThe icons below should be partially clipped.", + Attrs::new().family(Family::SansSerif), + Shaping::Advanced, + ); + text_buffer.shape_until_scroll(&mut font_system, false); + + // Set up custom svg renderer + let svg_0 = resvg::usvg::Tree::from_data(LION_SVG, &Default::default()).unwrap(); + let svg_1 = resvg::usvg::Tree::from_data(EAGLE_SVG, &Default::default()).unwrap(); + + let rasterize_svg = move |input: RasterizationRequest| -> Option { + // Select the svg data based on the custom glyph ID. + let (svg, content_type) = match input.id { + 0 => (&svg_0, ContentType::Mask), + 1 => (&svg_1, ContentType::Color), + _ => return None, + }; + + // Calculate the scale based on the "glyph size". + let svg_size = svg.size(); + let scale_x = input.width as f32 / svg_size.width(); + let scale_y = input.height as f32 / svg_size.height(); + + let Some(mut pixmap) = + resvg::tiny_skia::Pixmap::new(input.width as u32, input.height as u32) + else { + return None; + }; + + let mut transform = resvg::usvg::Transform::from_scale(scale_x, scale_y); + + // Offset the glyph by the subpixel amount. + let offset_x = input.x_bin.as_float(); + let offset_y = input.y_bin.as_float(); + if offset_x != 0.0 || offset_y != 0.0 { + transform = transform.post_translate(offset_x, offset_y); + } + + resvg::render(svg, transform, &mut pixmap.as_mut()); + + let data: Vec = if let ContentType::Mask = content_type { + // Only use the alpha channel for symbolic icons. + pixmap.data().iter().skip(3).step_by(4).copied().collect() + } else { + pixmap.data().to_vec() + }; + + Some(RasterizedCustomGlyph { data, content_type }) + }; + + Self { + device, + queue, + surface, + surface_config, + font_system, + swash_cache, + viewport, + atlas, + text_renderer, + text_buffer, + rasterize_svg: Box::new(rasterize_svg), + window, + } + } +} + +struct Application { + window_state: Option, +} + +impl winit::application::ApplicationHandler for Application { + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + if self.window_state.is_some() { + return; + } + + // Set up window + let (width, height) = (800, 600); + let window_attributes = Window::default_attributes() + .with_inner_size(LogicalSize::new(width as f64, height as f64)) + .with_title("glyphon hello world"); + let window = Arc::new(event_loop.create_window(window_attributes).unwrap()); + + self.window_state = Some(pollster::block_on(WindowState::new(window))); + } + + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + _window_id: winit::window::WindowId, + event: WindowEvent, + ) { + let Some(state) = &mut self.window_state else { + return; + }; + + let WindowState { + window, + device, + queue, + surface, + surface_config, + font_system, + swash_cache, + viewport, + atlas, + text_renderer, + text_buffer, + rasterize_svg, + .. + } = state; + + match event { + WindowEvent::Resized(size) => { + surface_config.width = size.width; + surface_config.height = size.height; + surface.configure(&device, &surface_config); + window.request_redraw(); + } + WindowEvent::RedrawRequested => { + viewport.update( + &queue, + Resolution { + width: surface_config.width, + height: surface_config.height, + }, + ); + + text_renderer + .prepare_with_custom( + device, + queue, + font_system, + atlas, + viewport, + [TextArea { + buffer: &text_buffer, + left: 10.0, + top: 10.0, + scale: 1.0, + bounds: TextBounds { + left: 0, + top: 0, + right: 650, + bottom: 180, + }, + default_color: Color::rgb(255, 255, 255), + custom_glyphs: &[ + CustomGlyph { + id: 0, + left: 300.0, + top: 5.0, + width: 64.0, + height: 64.0, + color: Some(Color::rgb(200, 200, 255)), + snap_to_physical_pixel: true, + metadata: 0, + }, + CustomGlyph { + id: 1, + left: 400.0, + top: 5.0, + width: 64.0, + height: 64.0, + color: None, + snap_to_physical_pixel: true, + metadata: 0, + }, + CustomGlyph { + id: 0, + left: 300.0, + top: 130.0, + width: 64.0, + height: 64.0, + color: Some(Color::rgb(200, 255, 200)), + snap_to_physical_pixel: true, + metadata: 0, + }, + CustomGlyph { + id: 1, + left: 400.0, + top: 130.0, + width: 64.0, + height: 64.0, + color: None, + snap_to_physical_pixel: true, + metadata: 0, + }, + ], + }], + swash_cache, + rasterize_svg, + ) + .unwrap(); + + let frame = surface.get_current_texture().unwrap(); + let view = frame.texture.create_view(&TextureViewDescriptor::default()); + let mut encoder = + device.create_command_encoder(&CommandEncoderDescriptor { label: None }); + { + let mut pass = encoder.begin_render_pass(&RenderPassDescriptor { + label: None, + color_attachments: &[Some(RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(wgpu::Color { + r: 0.02, + g: 0.02, + b: 0.02, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + text_renderer.render(&atlas, &viewport, &mut pass).unwrap(); + } + + queue.submit(Some(encoder.finish())); + frame.present(); + + atlas.trim(); + } + WindowEvent::CloseRequested => event_loop.exit(), + _ => {} + } + } +} diff --git a/examples/eagle.svg b/examples/eagle.svg new file mode 100644 index 0000000..53ad249 --- /dev/null +++ b/examples/eagle.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Openclipart + + + Eagle + 2007-01-24T06:25:54 + animal, animal, bird, bird, clip art, clipart, eagle, eagle, head, head, image, media, nature, nature, public domain, svg, + http://openclipart.org/detail/2962/eagle-by-nfroidure + + + nfroidure + + + + + animal + bird + clip art + clipart + eagle + head + image + media + nature + public domain + svg + + + + + + + + + + + diff --git a/examples/hello-world.rs b/examples/hello-world.rs index 06ce896..51c5c49 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -186,6 +186,7 @@ impl winit::application::ApplicationHandler for Application { bottom: 160, }, default_color: Color::rgb(255, 255, 255), + custom_glyphs: &[], }], swash_cache, ) diff --git a/examples/lion.svg b/examples/lion.svg new file mode 100644 index 0000000..6fbde49 --- /dev/null +++ b/examples/lion.svg @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Openclipart + + + + + + + + + + + diff --git a/src/custom_glyph.rs b/src/custom_glyph.rs new file mode 100644 index 0000000..a0df98c --- /dev/null +++ b/src/custom_glyph.rs @@ -0,0 +1,113 @@ +use crate::Color; +use cosmic_text::SubpixelBin; + +pub type CustomGlyphId = u16; + +/// A custom glyph to render +#[derive(Default, Debug, Clone, Copy, PartialEq)] +pub struct CustomGlyph { + /// The unique identifier for this glyph + pub id: CustomGlyphId, + /// The position of the left edge of the glyph + pub left: f32, + /// The position of the top edge of the glyph + pub top: f32, + /// The width of the glyph + pub width: f32, + /// The height of the glyph + pub height: f32, + /// The color of this glyph (only relevant if the glyph is rendered with the + /// type [`ContentType::Mask`]) + /// + /// Set to `None` to use [`TextArea::default_color`]. + pub color: Option, + /// If `true`, then this glyph will be snapped to the nearest whole physical + /// pixel and the resulting `SubpixelBin`'s in `RasterizationRequest` will always + /// be `Zero` (useful for images and other large glyphs). + pub snap_to_physical_pixel: bool, + /// Additional metadata about the glyph + pub metadata: usize, +} + +/// A request to rasterize a custom glyph +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct RasterizationRequest { + /// The unique identifier of the glyph + pub id: CustomGlyphId, + /// The width of the glyph in physical pixels + pub width: u16, + /// The height of the glyph in physical pixels + pub height: u16, + /// Binning of fractional X offset + /// + /// If `CustomGlyph::snap_to_physical_pixel` was set to `true`, then this + /// will always be `Zero`. + pub x_bin: SubpixelBin, + /// Binning of fractional Y offset + /// + /// If `CustomGlyph::snap_to_physical_pixel` was set to `true`, then this + /// will always be `Zero`. + pub y_bin: SubpixelBin, + /// The scaling factor applied to the text area (Note that `width` and + /// `height` are already scaled by this factor.) + pub scale: f32, +} + +/// A rasterized custom glyph +#[derive(Debug, Clone)] +pub struct RasterizedCustomGlyph { + /// The raw image data + pub data: Vec, + /// The type of image data contained in `data` + pub content_type: ContentType, +} + +impl RasterizedCustomGlyph { + pub(crate) fn validate(&self, input: &RasterizationRequest, expected_type: Option) { + if let Some(expected_type) = expected_type { + assert_eq!(self.content_type, expected_type, "Custom glyph rasterizer must always produce the same content type for a given input. Expected {:?}, got {:?}. Input: {:?}", expected_type, self.content_type, input); + } + + assert_eq!( + self.data.len(), + input.width as usize * input.height as usize * self.content_type.bytes_per_pixel(), + "Invalid custom glyph rasterizer output. Expected data of length {}, got length {}. Input: {:?}", + input.width as usize * input.height as usize * self.content_type.bytes_per_pixel(), + self.data.len(), + input, + ); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct CustomGlyphCacheKey { + /// Font ID + pub glyph_id: CustomGlyphId, + /// Glyph width + pub width: u16, + /// Glyph height + pub height: u16, + /// Binning of fractional X offset + pub x_bin: SubpixelBin, + /// Binning of fractional Y offset + pub y_bin: SubpixelBin, +} + +/// The type of image data contained in a rasterized glyph +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum ContentType { + /// Each pixel contains 32 bits of rgba data + Color, + /// Each pixel contains a single 8 bit channel + Mask, +} + +impl ContentType { + /// The number of bytes per pixel for this content type + pub fn bytes_per_pixel(&self) -> usize { + match self { + Self::Color => 4, + Self::Mask => 1, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 3b31120..9be0cdc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,19 +5,21 @@ //! [etagere]: https://github.com/nical/etagere mod cache; +mod custom_glyph; mod error; mod text_atlas; mod text_render; mod viewport; pub use cache::Cache; +pub use custom_glyph::{ + ContentType, CustomGlyph, CustomGlyphId, RasterizationRequest, RasterizedCustomGlyph, +}; pub use error::{PrepareError, RenderError}; pub use text_atlas::{ColorMode, TextAtlas}; pub use text_render::TextRenderer; pub use viewport::Viewport; -use text_render::ContentType; - // Re-export all top-level types from `cosmic-text` for convenience. #[doc(no_inline)] pub use cosmic_text::{ @@ -117,4 +119,7 @@ pub struct TextArea<'a> { pub bounds: TextBounds, // The default color of the text area. pub default_color: Color, + + /// Additional custom glyphs to render + pub custom_glyphs: &'a [CustomGlyph], } diff --git a/src/text_atlas.rs b/src/text_atlas.rs index 1cabbc4..cf86464 100644 --- a/src/text_atlas.rs +++ b/src/text_atlas.rs @@ -1,5 +1,6 @@ use crate::{ - text_render::ContentType, Cache, CacheKey, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, + text_render::GlyphonCacheKey, Cache, ContentType, RasterizationRequest, RasterizedCustomGlyph, + FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, }; use etagere::{size2, Allocation, BucketedAtlasAllocator}; use lru::LruCache; @@ -20,8 +21,8 @@ pub(crate) struct InnerAtlas { pub texture_view: TextureView, pub packer: BucketedAtlasAllocator, pub size: u32, - pub glyph_cache: LruCache, - pub glyphs_in_use: HashSet, + pub glyph_cache: LruCache, + pub glyphs_in_use: HashSet, pub max_texture_dimension_2d: u32, } @@ -106,12 +107,12 @@ impl InnerAtlas { self.kind.num_channels() } - pub(crate) fn promote(&mut self, glyph: CacheKey) { + pub(crate) fn promote(&mut self, glyph: GlyphonCacheKey) { self.glyph_cache.promote(&glyph); self.glyphs_in_use.insert(glyph); } - pub(crate) fn put(&mut self, glyph: CacheKey, details: GlyphDetails) { + pub(crate) fn put(&mut self, glyph: GlyphonCacheKey, details: GlyphDetails) { self.glyph_cache.put(glyph, details); self.glyphs_in_use.insert(glyph); } @@ -122,6 +123,8 @@ impl InnerAtlas { queue: &wgpu::Queue, font_system: &mut FontSystem, cache: &mut SwashCache, + scale_factor: f32, + mut rasterize_custom_glyph: impl FnMut(RasterizationRequest) -> Option, ) -> bool { if self.size >= self.max_texture_dimension_2d { return false; @@ -157,10 +160,38 @@ impl InnerAtlas { GpuCacheStatus::SkipRasterization => continue, }; - let image = cache.get_image_uncached(font_system, cache_key).unwrap(); + let (image_data, width, height) = match cache_key { + GlyphonCacheKey::Text(cache_key) => { + let image = cache.get_image_uncached(font_system, cache_key).unwrap(); + let width = image.placement.width as usize; + let height = image.placement.height as usize; - let width = image.placement.width as usize; - let height = image.placement.height as usize; + (image.data, width, height) + } + GlyphonCacheKey::Custom(cache_key) => { + let input = RasterizationRequest { + id: cache_key.glyph_id, + width: cache_key.width, + height: cache_key.height, + x_bin: cache_key.x_bin, + y_bin: cache_key.y_bin, + scale: scale_factor, + }; + + let Some(rasterized_glyph) = (rasterize_custom_glyph)(input) else { + panic!("Custom glyph rasterizer returned `None` when it previously returned `Some` for the same input {:?}", &input); + }; + + // Sanity checks on the rasterizer output + rasterized_glyph.validate(&input, Some(self.kind.as_content_type())); + + ( + rasterized_glyph.data, + cache_key.width as usize, + cache_key.height as usize, + ) + } + }; queue.write_texture( ImageCopyTexture { @@ -173,7 +204,7 @@ impl InnerAtlas { }, aspect: TextureAspect::All, }, - &image.data, + &image_data, ImageDataLayout { offset: 0, bytes_per_row: Some(width as u32 * self.kind.num_channels() as u32), @@ -224,6 +255,13 @@ impl Kind { } } } + + fn as_content_type(&self) -> ContentType { + match self { + Self::Mask => ContentType::Mask, + Self::Color { .. } => ContentType::Color, + } + } } /// The color mode of an [`Atlas`]. @@ -313,10 +351,26 @@ impl TextAtlas { font_system: &mut FontSystem, cache: &mut SwashCache, content_type: ContentType, + scale_factor: f32, + rasterize_custom_glyph: impl FnMut(RasterizationRequest) -> Option, ) -> bool { let did_grow = match content_type { - ContentType::Mask => self.mask_atlas.grow(device, queue, font_system, cache), - ContentType::Color => self.color_atlas.grow(device, queue, font_system, cache), + ContentType::Mask => self.mask_atlas.grow( + device, + queue, + font_system, + cache, + scale_factor, + rasterize_custom_glyph, + ), + ContentType::Color => self.color_atlas.grow( + device, + queue, + font_system, + cache, + scale_factor, + rasterize_custom_glyph, + ), }; if did_grow { @@ -326,7 +380,7 @@ impl TextAtlas { did_grow } - pub(crate) fn glyph(&self, glyph: &CacheKey) -> Option<&GlyphDetails> { + pub(crate) fn glyph(&self, glyph: &GlyphonCacheKey) -> Option<&GlyphDetails> { self.mask_atlas .glyph_cache .peek(glyph) diff --git a/src/text_render.rs b/src/text_render.rs index d89fd02..21592b8 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -1,7 +1,9 @@ use crate::{ - ColorMode, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError, - SwashCache, SwashContent, TextArea, TextAtlas, Viewport, + custom_glyph::CustomGlyphCacheKey, ColorMode, ContentType, RasterizationRequest, RasterizedCustomGlyph, + FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError, SwashCache, + SwashContent, TextArea, TextAtlas, Viewport, }; +use cosmic_text::{Color, SubpixelBin}; use std::{slice, sync::Arc}; use wgpu::{ Buffer, BufferDescriptor, BufferUsages, DepthStencilState, Device, Extent3d, ImageCopyTexture, @@ -43,8 +45,82 @@ impl TextRenderer { } } + /// Prepares all of the provided text areas for rendering. + pub fn prepare<'a>( + &mut self, + device: &Device, + queue: &Queue, + font_system: &mut FontSystem, + atlas: &mut TextAtlas, + viewport: &Viewport, + text_areas: impl IntoIterator>, + cache: &mut SwashCache, + ) -> Result<(), PrepareError> { + self.prepare_with_depth_and_custom( + device, + queue, + font_system, + atlas, + viewport, + text_areas, + cache, + zero_depth, + |_| None, + ) + } + /// Prepares all of the provided text areas for rendering. pub fn prepare_with_depth<'a>( + &mut self, + device: &Device, + queue: &Queue, + font_system: &mut FontSystem, + atlas: &mut TextAtlas, + viewport: &Viewport, + text_areas: impl IntoIterator>, + cache: &mut SwashCache, + metadata_to_depth: impl FnMut(usize) -> f32, + ) -> Result<(), PrepareError> { + self.prepare_with_depth_and_custom( + device, + queue, + font_system, + atlas, + viewport, + text_areas, + cache, + metadata_to_depth, + |_| None, + ) + } + + /// Prepares all of the provided text areas for rendering. + pub fn prepare_with_custom<'a>( + &mut self, + device: &Device, + queue: &Queue, + font_system: &mut FontSystem, + atlas: &mut TextAtlas, + viewport: &Viewport, + text_areas: impl IntoIterator>, + cache: &mut SwashCache, + rasterize_custom_glyph: impl FnMut(RasterizationRequest) -> Option, + ) -> Result<(), PrepareError> { + self.prepare_with_depth_and_custom( + device, + queue, + font_system, + atlas, + viewport, + text_areas, + cache, + zero_depth, + rasterize_custom_glyph, + ) + } + + /// Prepares all of the provided text areas for rendering. + pub fn prepare_with_depth_and_custom<'a>( &mut self, device: &Device, queue: &Queue, @@ -54,6 +130,7 @@ impl TextRenderer { text_areas: impl IntoIterator>, cache: &mut SwashCache, mut metadata_to_depth: impl FnMut(usize) -> f32, + mut rasterize_custom_glyph: impl FnMut(RasterizationRequest) -> Option, ) -> Result<(), PrepareError> { self.glyph_vertices.clear(); @@ -65,6 +142,88 @@ impl TextRenderer { let bounds_max_x = text_area.bounds.right.min(resolution.width as i32); let bounds_max_y = text_area.bounds.bottom.min(resolution.height as i32); + for glyph in text_area.custom_glyphs.iter() { + let x = text_area.left + (glyph.left * text_area.scale); + let y = text_area.top + (glyph.top * text_area.scale); + let width = (glyph.width * text_area.scale).round() as u16; + let height = (glyph.height * text_area.scale).round() as u16; + + let (x, y, x_bin, y_bin) = if glyph.snap_to_physical_pixel { + ( + x.round() as i32, + y.round() as i32, + SubpixelBin::Zero, + SubpixelBin::Zero, + ) + } else { + let (x, x_bin) = SubpixelBin::new(x); + let (y, y_bin) = SubpixelBin::new(y); + (x, y, x_bin, y_bin) + }; + + let cache_key = GlyphonCacheKey::Custom(CustomGlyphCacheKey { + glyph_id: glyph.id, + width, + height, + x_bin, + y_bin, + }); + + let color = glyph.color.unwrap_or(text_area.default_color); + + if let Some(glyph_to_render) = prepare_glyph( + x, + y, + 0.0, + color, + glyph.metadata, + cache_key, + atlas, + device, + queue, + cache, + font_system, + text_area.scale, + bounds_min_x, + bounds_min_y, + bounds_max_x, + bounds_max_y, + |_cache, _font_system, rasterize_custom_glyph| -> Option { + if width == 0 || height == 0 { + return None; + } + + let input = RasterizationRequest { + id: glyph.id, + width, + height, + x_bin, + y_bin, + scale: text_area.scale, + }; + + let Some(output) = (rasterize_custom_glyph)(input) else { + return None; + }; + + output.validate(&input, None); + + Some(GetGlyphImageResult { + content_type: output.content_type, + top: 0, + left: 0, + width, + height, + data: output.data, + }) + }, + &mut metadata_to_depth, + &mut rasterize_custom_glyph, + )? { + self.glyph_vertices.push(glyph_to_render); + } + } + let is_run_visible = |run: &cosmic_text::LayoutRun| { let start_y = (text_area.top + run.line_top) as i32; let end_y = (text_area.top + run.line_top + run.line_height) as i32; @@ -83,189 +242,61 @@ impl TextRenderer { let physical_glyph = glyph.physical((text_area.left, text_area.top), text_area.scale); - if atlas - .mask_atlas - .glyph_cache - .contains(&physical_glyph.cache_key) - { - atlas.mask_atlas.promote(physical_glyph.cache_key); - } else if atlas - .color_atlas - .glyph_cache - .contains(&physical_glyph.cache_key) - { - atlas.color_atlas.promote(physical_glyph.cache_key); - } else { - let Some(image) = - cache.get_image_uncached(font_system, physical_glyph.cache_key) - else { - continue; - }; - - let content_type = match image.content { - SwashContent::Color => ContentType::Color, - SwashContent::Mask => ContentType::Mask, - SwashContent::SubpixelMask => { - // Not implemented yet, but don't panic if this happens. - ContentType::Mask - } - }; - - let width = image.placement.width as usize; - let height = image.placement.height as usize; - - let should_rasterize = width > 0 && height > 0; - - let (gpu_cache, atlas_id, inner) = if should_rasterize { - let mut inner = atlas.inner_for_content_mut(content_type); - - // Find a position in the packer - let allocation = loop { - match inner.try_allocate(width, height) { - Some(a) => break a, - None => { - if !atlas.grow( - device, - queue, - font_system, - cache, - content_type, - ) { - return Err(PrepareError::AtlasFull); - } - - inner = atlas.inner_for_content_mut(content_type); - } - } - }; - let atlas_min = allocation.rectangle.min; - - queue.write_texture( - ImageCopyTexture { - texture: &inner.texture, - mip_level: 0, - origin: Origin3d { - x: atlas_min.x as u32, - y: atlas_min.y as u32, - z: 0, - }, - aspect: TextureAspect::All, - }, - &image.data, - ImageDataLayout { - offset: 0, - bytes_per_row: Some(width as u32 * inner.num_channels() as u32), - rows_per_image: None, - }, - Extent3d { - width: width as u32, - height: height as u32, - depth_or_array_layers: 1, - }, - ); - - ( - GpuCacheStatus::InAtlas { - x: atlas_min.x as u16, - y: atlas_min.y as u16, - content_type, - }, - Some(allocation.id), - inner, - ) - } else { - let inner = &mut atlas.color_atlas; - (GpuCacheStatus::SkipRasterization, None, inner) - }; - - inner.put( - physical_glyph.cache_key, - GlyphDetails { - width: width as u16, - height: height as u16, - gpu_cache, - atlas_id, - top: image.placement.top as i16, - left: image.placement.left as i16, - }, - ); - } - - let details = atlas.glyph(&physical_glyph.cache_key).unwrap(); - - let mut x = physical_glyph.x + details.left as i32; - let mut y = (run.line_y * text_area.scale).round() as i32 + physical_glyph.y - - details.top as i32; - - let (mut atlas_x, mut atlas_y, content_type) = match details.gpu_cache { - GpuCacheStatus::InAtlas { x, y, content_type } => (x, y, content_type), - GpuCacheStatus::SkipRasterization => continue, - }; - - let mut width = details.width as i32; - let mut height = details.height as i32; - - // Starts beyond right edge or ends beyond left edge - let max_x = x + width; - if x > bounds_max_x || max_x < bounds_min_x { - continue; - } - - // Starts beyond bottom edge or ends beyond top edge - let max_y = y + height; - if y > bounds_max_y || max_y < bounds_min_y { - continue; - } - - // Clip left ege - if x < bounds_min_x { - let right_shift = bounds_min_x - x; - - x = bounds_min_x; - width = max_x - bounds_min_x; - atlas_x += right_shift as u16; - } - - // Clip right edge - if x + width > bounds_max_x { - width = bounds_max_x - x; - } - - // Clip top edge - if y < bounds_min_y { - let bottom_shift = bounds_min_y - y; - - y = bounds_min_y; - height = max_y - bounds_min_y; - atlas_y += bottom_shift as u16; - } - - // Clip bottom edge - if y + height > bounds_max_y { - height = bounds_max_y - y; - } - let color = match glyph.color_opt { Some(some) => some, None => text_area.default_color, }; - let depth = metadata_to_depth(glyph.metadata); + if let Some(glyph_to_render) = prepare_glyph( + physical_glyph.x, + physical_glyph.y, + run.line_y, + color, + glyph.metadata, + GlyphonCacheKey::Text(physical_glyph.cache_key), + atlas, + device, + queue, + cache, + font_system, + text_area.scale, + bounds_min_x, + bounds_min_y, + bounds_max_x, + bounds_max_y, + |cache, + font_system, + _rasterize_custom_glyph| + -> Option { + let Some(image) = + cache.get_image_uncached(font_system, physical_glyph.cache_key) + else { + return None; + }; - self.glyph_vertices.push(GlyphToRender { - pos: [x, y], - dim: [width as u16, height as u16], - uv: [atlas_x, atlas_y], - color: color.0, - content_type_with_srgb: [ - content_type as u16, - match atlas.color_mode { - ColorMode::Accurate => TextColorConversion::ConvertToLinear, - ColorMode::Web => TextColorConversion::None, - } as u16, - ], - depth, - }); + let content_type = match image.content { + SwashContent::Color => ContentType::Color, + SwashContent::Mask => ContentType::Mask, + SwashContent::SubpixelMask => { + // Not implemented yet, but don't panic if this happens. + ContentType::Mask + } + }; + + Some(GetGlyphImageResult { + content_type, + top: image.placement.top as i16, + left: image.placement.left as i16, + width: image.placement.width as u16, + height: image.placement.height as u16, + data: image.data, + }) + }, + &mut metadata_to_depth, + &mut rasterize_custom_glyph, + )? { + self.glyph_vertices.push(glyph_to_render); + } } } } @@ -302,28 +333,6 @@ impl TextRenderer { Ok(()) } - pub fn prepare<'a>( - &mut self, - device: &Device, - queue: &Queue, - font_system: &mut FontSystem, - atlas: &mut TextAtlas, - viewport: &Viewport, - text_areas: impl IntoIterator>, - cache: &mut SwashCache, - ) -> Result<(), PrepareError> { - self.prepare_with_depth( - device, - queue, - font_system, - atlas, - viewport, - text_areas, - cache, - zero_depth, - ) - } - /// Renders all layouts that were previously provided to `prepare`. pub fn render<'pass>( &'pass self, @@ -345,13 +354,6 @@ impl TextRenderer { } } -#[repr(u16)] -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub enum ContentType { - Color = 0, - Mask = 1, -} - #[repr(u16)] #[derive(Debug, Clone, Copy, Eq, PartialEq)] enum TextColorConversion { @@ -359,6 +361,12 @@ enum TextColorConversion { ConvertToLinear = 1, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum GlyphonCacheKey { + Text(cosmic_text::CacheKey), + Custom(CustomGlyphCacheKey), +} + fn next_copy_buffer_size(size: u64) -> u64 { let align_mask = COPY_BUFFER_ALIGNMENT - 1; ((size.next_power_of_two() + align_mask) & !align_mask).max(COPY_BUFFER_ALIGNMENT) @@ -385,3 +393,199 @@ fn create_oversized_buffer( fn zero_depth(_: usize) -> f32 { 0f32 } + +struct GetGlyphImageResult { + content_type: ContentType, + top: i16, + left: i16, + width: u16, + height: u16, + data: Vec, +} + +fn prepare_glyph( + x: i32, + y: i32, + line_y: f32, + color: Color, + metadata: usize, + cache_key: GlyphonCacheKey, + atlas: &mut TextAtlas, + device: &Device, + queue: &Queue, + cache: &mut SwashCache, + font_system: &mut FontSystem, + scale_factor: f32, + bounds_min_x: i32, + bounds_min_y: i32, + bounds_max_x: i32, + bounds_max_y: i32, + get_glyph_image: impl FnOnce( + &mut SwashCache, + &mut FontSystem, + &mut R, + ) -> Option, + mut metadata_to_depth: impl FnMut(usize) -> f32, + mut rasterize_custom_glyph: R, +) -> Result, PrepareError> +where + R: FnMut(RasterizationRequest) -> Option, +{ + if atlas.mask_atlas.glyph_cache.contains(&cache_key) { + atlas.mask_atlas.promote(cache_key); + } else if atlas.color_atlas.glyph_cache.contains(&cache_key) { + atlas.color_atlas.promote(cache_key); + } else { + let Some(image) = (get_glyph_image)(cache, font_system, &mut rasterize_custom_glyph) else { + return Ok(None); + }; + + let should_rasterize = image.width > 0 && image.height > 0; + + let (gpu_cache, atlas_id, inner) = if should_rasterize { + let mut inner = atlas.inner_for_content_mut(image.content_type); + + // Find a position in the packer + let allocation = loop { + match inner.try_allocate(image.width as usize, image.height as usize) { + Some(a) => break a, + None => { + if !atlas.grow( + device, + queue, + font_system, + cache, + image.content_type, + scale_factor, + &mut rasterize_custom_glyph, + ) { + return Err(PrepareError::AtlasFull); + } + + inner = atlas.inner_for_content_mut(image.content_type); + } + } + }; + let atlas_min = allocation.rectangle.min; + + queue.write_texture( + ImageCopyTexture { + texture: &inner.texture, + mip_level: 0, + origin: Origin3d { + x: atlas_min.x as u32, + y: atlas_min.y as u32, + z: 0, + }, + aspect: TextureAspect::All, + }, + &image.data, + ImageDataLayout { + offset: 0, + bytes_per_row: Some(image.width as u32 * inner.num_channels() as u32), + rows_per_image: None, + }, + Extent3d { + width: image.width as u32, + height: image.height as u32, + depth_or_array_layers: 1, + }, + ); + + ( + GpuCacheStatus::InAtlas { + x: atlas_min.x as u16, + y: atlas_min.y as u16, + content_type: image.content_type, + }, + Some(allocation.id), + inner, + ) + } else { + let inner = &mut atlas.color_atlas; + (GpuCacheStatus::SkipRasterization, None, inner) + }; + + inner.put( + cache_key, + GlyphDetails { + width: image.width, + height: image.height, + gpu_cache, + atlas_id, + top: image.top, + left: image.left, + }, + ); + } + + let details = atlas.glyph(&cache_key).unwrap(); + + let mut x = x + details.left as i32; + let mut y = (line_y * scale_factor).round() as i32 + y - details.top as i32; + + let (mut atlas_x, mut atlas_y, content_type) = match details.gpu_cache { + GpuCacheStatus::InAtlas { x, y, content_type } => (x, y, content_type), + GpuCacheStatus::SkipRasterization => return Ok(None), + }; + + let mut width = details.width as i32; + let mut height = details.height as i32; + + // Starts beyond right edge or ends beyond left edge + let max_x = x + width; + if x > bounds_max_x || max_x < bounds_min_x { + return Ok(None); + } + + // Starts beyond bottom edge or ends beyond top edge + let max_y = y + height; + if y > bounds_max_y || max_y < bounds_min_y { + return Ok(None); + } + + // Clip left ege + if x < bounds_min_x { + let right_shift = bounds_min_x - x; + + x = bounds_min_x; + width = max_x - bounds_min_x; + atlas_x += right_shift as u16; + } + + // Clip right edge + if x + width > bounds_max_x { + width = bounds_max_x - x; + } + + // Clip top edge + if y < bounds_min_y { + let bottom_shift = bounds_min_y - y; + + y = bounds_min_y; + height = max_y - bounds_min_y; + atlas_y += bottom_shift as u16; + } + + // Clip bottom edge + if y + height > bounds_max_y { + height = bounds_max_y - y; + } + + let depth = metadata_to_depth(metadata); + + Ok(Some(GlyphToRender { + pos: [x, y], + dim: [width as u16, height as u16], + uv: [atlas_x, atlas_y], + color: color.0, + content_type_with_srgb: [ + content_type as u16, + match atlas.color_mode { + ColorMode::Accurate => TextColorConversion::ConvertToLinear, + ColorMode::Web => TextColorConversion::None, + } as u16, + ], + depth, + })) +} From 2daf883cad96648d15830a66ed9f3759db0f48c0 Mon Sep 17 00:00:00 2001 From: Billy Messenger <60663878+BillyDM@users.noreply.github.com> Date: Tue, 17 Sep 2024 00:00:18 -0500 Subject: [PATCH 19/24] add text-sizes example (#114) * add text-sizes example * rename window title in text-sizes example --- examples/text-sizes.rs | 303 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 303 insertions(+) create mode 100644 examples/text-sizes.rs diff --git a/examples/text-sizes.rs b/examples/text-sizes.rs new file mode 100644 index 0000000..43b9f69 --- /dev/null +++ b/examples/text-sizes.rs @@ -0,0 +1,303 @@ +use glyphon::{ + Attrs, Buffer, Cache, Color, ColorMode, Family, FontSystem, Metrics, Resolution, Shaping, + SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Weight, +}; +use std::sync::Arc; +use wgpu::{ + CommandEncoderDescriptor, CompositeAlphaMode, DeviceDescriptor, Instance, InstanceDescriptor, + LoadOp, MultisampleState, Operations, PresentMode, RenderPassColorAttachment, + RenderPassDescriptor, RequestAdapterOptions, SurfaceConfiguration, TextureFormat, + TextureUsages, TextureViewDescriptor, +}; +use winit::{ + dpi::{LogicalSize, PhysicalSize}, + event::WindowEvent, + event_loop::EventLoop, + window::Window, +}; + +const TEXT: &str = "The quick brown fox jumped over the lazy doggo. 🐕"; +const WEIGHT: Weight = Weight::NORMAL; +const SIZES: [f32; 16] = [ + 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 18.0, 20.0, 22.0, 24.0, 28.0, 32.0, 48.0, +]; +const LINE_HEIGHT: f32 = 1.15; +const BG_COLOR: wgpu::Color = wgpu::Color::WHITE; +const FONT_COLOR: Color = Color::rgb(0, 0, 0); +//const BG_COLOR: wgpu::Color = wgpu::Color::BLACK; +//const FONT_COLOR: Color = Color::rgb(255, 255, 255); +const USE_WEB_COLORS: bool = true; + +fn main() { + let event_loop = EventLoop::new().unwrap(); + event_loop + .run_app(&mut Application { window_state: None }) + .unwrap(); +} + +struct WindowState { + device: wgpu::Device, + queue: wgpu::Queue, + surface: wgpu::Surface<'static>, + surface_config: SurfaceConfiguration, + physical_size: PhysicalSize, + scale_factor: f32, + + font_system: FontSystem, + swash_cache: SwashCache, + viewport: glyphon::Viewport, + atlas: glyphon::TextAtlas, + text_renderer: glyphon::TextRenderer, + buffers: Vec, + + // Make sure that the winit window is last in the struct so that + // it is dropped after the wgpu surface is dropped, otherwise the + // program may crash when closed. This is probably a bug in wgpu. + window: Arc, +} + +impl WindowState { + async fn new(window: Arc) -> Self { + let physical_size = window.inner_size(); + let scale_factor = window.scale_factor() as f32; + + // Set up surface + let instance = Instance::new(InstanceDescriptor::default()); + let adapter = instance + .request_adapter(&RequestAdapterOptions::default()) + .await + .unwrap(); + let (device, queue) = adapter + .request_device(&DeviceDescriptor::default(), None) + .await + .unwrap(); + + let (color_mode, swapchain_format) = if USE_WEB_COLORS { + (ColorMode::Web, TextureFormat::Bgra8Unorm) + } else { + (ColorMode::Accurate, TextureFormat::Bgra8UnormSrgb) + }; + + let surface = instance + .create_surface(window.clone()) + .expect("Create surface"); + let surface_config = SurfaceConfiguration { + usage: TextureUsages::RENDER_ATTACHMENT, + format: swapchain_format, + width: physical_size.width, + height: physical_size.height, + present_mode: PresentMode::Fifo, + alpha_mode: CompositeAlphaMode::Opaque, + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + surface.configure(&device, &surface_config); + + let logical_width = physical_size.width as f32 / scale_factor; + + // Set up text renderer + let mut font_system = FontSystem::new(); + let swash_cache = SwashCache::new(); + let cache = Cache::new(&device); + let viewport = Viewport::new(&device, &cache); + let mut atlas = + TextAtlas::with_color_mode(&device, &queue, &cache, swapchain_format, color_mode); + let text_renderer = + TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None); + + let attrs = Attrs::new().family(Family::SansSerif).weight(WEIGHT); + let shaping = Shaping::Advanced; + + let buffers: Vec = SIZES + .iter() + .copied() + .map(|s| { + let mut text_buffer = + Buffer::new(&mut font_system, Metrics::relative(s, LINE_HEIGHT)); + + text_buffer.set_size(&mut font_system, Some(logical_width - 20.0), None); + + text_buffer.set_text( + &mut font_system, + &format!("size {s}: {TEXT}"), + attrs, + shaping, + ); + + text_buffer.shape_until_scroll(&mut font_system, false); + + text_buffer + }) + .collect(); + + Self { + device, + queue, + surface, + surface_config, + physical_size: physical_size.cast(), + scale_factor: scale_factor as f32, + font_system, + swash_cache, + viewport, + atlas, + text_renderer, + buffers, + window, + } + } +} + +struct Application { + window_state: Option, +} + +impl winit::application::ApplicationHandler for Application { + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + if self.window_state.is_some() { + return; + } + + // Set up window + let (width, height) = (800, 600); + let window_attributes = Window::default_attributes() + .with_inner_size(LogicalSize::new(width as f64, height as f64)) + .with_title("glyphon text sizes test"); + let window = Arc::new(event_loop.create_window(window_attributes).unwrap()); + + self.window_state = Some(pollster::block_on(WindowState::new(window))); + } + + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + _window_id: winit::window::WindowId, + event: WindowEvent, + ) { + let Some(state) = &mut self.window_state else { + return; + }; + + let WindowState { + window, + device, + queue, + surface, + surface_config, + font_system, + swash_cache, + viewport, + atlas, + text_renderer, + buffers, + scale_factor, + physical_size, + .. + } = state; + + match event { + WindowEvent::Resized(size) => { + surface_config.width = size.width; + surface_config.height = size.height; + surface.configure(&device, &surface_config); + window.request_redraw(); + + *scale_factor = window.scale_factor() as f32; + *physical_size = size.cast(); + + let logical_width = size.width as f32 / *scale_factor; + + for b in buffers.iter_mut() { + b.set_size(font_system, Some(logical_width - 20.0), None); + b.shape_until_scroll(font_system, false); + } + } + WindowEvent::RedrawRequested => { + viewport.update( + &queue, + Resolution { + width: surface_config.width, + height: surface_config.height, + }, + ); + + let scale_factor = *scale_factor; + + let left = 10.0 * scale_factor; + let mut top = 10.0 * scale_factor; + + let bounds_left = left.floor() as i32; + let bounds_right = physical_size.width - 10; + + let text_areas: Vec