From 114cfa3e0291aeacfd5c0bd517071552fd7c8c33 Mon Sep 17 00:00:00 2001 From: Jose Acevedo Date: Tue, 10 Dec 2024 13:33:03 -0500 Subject: [PATCH 01/10] fix: use text area bounds to determine visibility --- src/text_render.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text_render.rs b/src/text_render.rs index 1d4f802..7bd60bb 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -76,7 +76,7 @@ impl TextRenderer { 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 <= bounds_max_y && bounds_min_y <= end_y + start_y <= text_area.bounds.bottom && text_area.bounds.top <= end_y }; let layout_runs = text_area From 505f12f6cec6c00f893971c999c96957b06683a0 Mon Sep 17 00:00:00 2001 From: Alphyr <47725341+a1phyr@users.noreply.github.com> Date: Wed, 5 Feb 2025 03:04:11 +0100 Subject: [PATCH 02/10] Small improvements to cache (#130) * Remove `Arc` around `RenderPipeline` It is not necessary anymore in last `wgpu` version. * Replace `RwLock` with `Mutex` It was only used for writing. --- src/cache.rs | 19 +++++++++---------- src/text_atlas.rs | 4 ++-- src/text_render.rs | 4 ++-- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index c567341..61d2e4d 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,5 +1,4 @@ use crate::{GlyphToRender, Params}; - use wgpu::{ BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutEntry, BindingResource, BindingType, BlendState, Buffer, BufferBindingType, ColorTargetState, @@ -14,7 +13,7 @@ use std::borrow::Cow; use std::mem; use std::num::NonZeroU64; use std::ops::Deref; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, Mutex}; #[derive(Debug, Clone)] pub struct Cache(Arc); @@ -27,12 +26,12 @@ struct Inner { atlas_layout: BindGroupLayout, uniforms_layout: BindGroupLayout, pipeline_layout: PipelineLayout, - cache: RwLock< + cache: Mutex< Vec<( TextureFormat, MultisampleState, Option, - Arc, + RenderPipeline, )>, >, } @@ -150,7 +149,7 @@ impl Cache { uniforms_layout, atlas_layout, pipeline_layout, - cache: RwLock::new(Vec::new()), + cache: Mutex::new(Vec::new()), })) } @@ -197,7 +196,7 @@ impl Cache { format: TextureFormat, multisample: MultisampleState, depth_stencil: Option, - ) -> Arc { + ) -> RenderPipeline { let Inner { cache, pipeline_layout, @@ -206,14 +205,14 @@ impl Cache { .. } = self.0.deref(); - let mut cache = cache.write().expect("Write pipeline cache"); + let mut cache = cache.lock().expect("Write pipeline cache"); cache .iter() .find(|(fmt, ms, ds, _)| fmt == &format && ms == &multisample && ds == &depth_stencil) - .map(|(_, _, _, p)| Arc::clone(p)) + .map(|(_, _, _, p)| p.clone()) .unwrap_or_else(|| { - let pipeline = Arc::new(device.create_render_pipeline(&RenderPipelineDescriptor { + let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor { label: Some("glyphon pipeline"), layout: Some(pipeline_layout), vertex: VertexState { @@ -240,7 +239,7 @@ impl Cache { multisample, multiview: None, cache: None, - })); + }); cache.push((format, multisample, depth_stencil, pipeline.clone())); diff --git a/src/text_atlas.rs b/src/text_atlas.rs index 0b90a0f..8313672 100644 --- a/src/text_atlas.rs +++ b/src/text_atlas.rs @@ -4,7 +4,7 @@ use crate::{ use etagere::{size2, Allocation, BucketedAtlasAllocator}; use lru::LruCache; use rustc_hash::FxHasher; -use std::{collections::HashSet, hash::BuildHasherDefault, sync::Arc}; +use std::{collections::HashSet, hash::BuildHasherDefault}; use wgpu::{ BindGroup, DepthStencilState, Device, Extent3d, MultisampleState, Origin3d, Queue, RenderPipeline, TexelCopyBufferLayout, TexelCopyTextureInfo, Texture, TextureAspect, @@ -329,7 +329,7 @@ impl TextAtlas { device: &Device, multisample: MultisampleState, depth_stencil: Option, - ) -> Arc { + ) -> RenderPipeline { self.cache .get_or_create_pipeline(device, self.format, multisample, depth_stencil) } diff --git a/src/text_render.rs b/src/text_render.rs index 7bd60bb..d1b4150 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -2,7 +2,7 @@ use crate::{ ColorMode, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError, SwashCache, SwashContent, TextArea, TextAtlas, Viewport, }; -use std::{num::NonZeroU64, slice, sync::Arc}; +use std::{num::NonZeroU64, slice}; use wgpu::util::StagingBelt; use wgpu::{ Buffer, BufferDescriptor, BufferUsages, CommandEncoder, DepthStencilState, Device, Extent3d, @@ -15,7 +15,7 @@ pub struct TextRenderer { staging_belt: StagingBelt, vertex_buffer: Buffer, vertex_buffer_size: u64, - pipeline: Arc, + pipeline: RenderPipeline, glyph_vertices: Vec, glyphs_to_render: u32, } From 9ab32ae1c3b25a91aacbd9b25d0406ee551aae04 Mon Sep 17 00:00:00 2001 From: StratusFearMe21 <57533634+StratusFearMe21@users.noreply.github.com> Date: Tue, 4 Mar 2025 19:36:29 -0700 Subject: [PATCH 03/10] Fix the `is_run_visible` calculation (#134) * Fix the `is_run_visible` calculation * Clarify variable names in the `is_run_visible` calculation * Make the physical run_line scales `i32` instead of `f32` * Make entire `start_y_physical` expression in `is_run_visible` calculation an `i32` --- src/text_render.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/text_render.rs b/src/text_render.rs index d1b4150..6f0cb47 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -73,10 +73,10 @@ impl TextRenderer { 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 + let start_y_physical = (text_area.top + (run.line_top * text_area.scale)) as i32; + let end_y_physical = start_y_physical + (run.line_height * text_area.scale) as i32; + + start_y_physical <= text_area.bounds.bottom && text_area.bounds.top <= end_y_physical }; let layout_runs = text_area From 007fc4b0d58c887e49976d312a2d21d32837df05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 9 Mar 2025 01:00:26 +0100 Subject: [PATCH 04/10] Run `cargo fmt` --- src/text_render.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/text_render.rs b/src/text_render.rs index 6f0cb47..ff6dc60 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -75,8 +75,9 @@ impl TextRenderer { let is_run_visible = |run: &cosmic_text::LayoutRun| { let start_y_physical = (text_area.top + (run.line_top * text_area.scale)) as i32; let end_y_physical = start_y_physical + (run.line_height * text_area.scale) as i32; - - start_y_physical <= text_area.bounds.bottom && text_area.bounds.top <= end_y_physical + + start_y_physical <= text_area.bounds.bottom + && text_area.bounds.top <= end_y_physical }; let layout_runs = text_area From a886f2427e612b23788a4e36658ff8cd55ba6695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 9 Mar 2025 01:01:09 +0100 Subject: [PATCH 05/10] Update `edition` to `2024` --- Cargo.toml | 2 +- benches/prepare.rs | 2 +- src/lib.rs | 6 +++--- src/text_atlas.rs | 4 ++-- src/text_render.rs | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e210e48..216f294 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "cryoglyph" description = "Fast, simple 2D text rendering for wgpu" version = "0.1.0" -edition = "2021" +edition = "2024" repository = "https://github.com/iced-rs/cryoglyph" license = "MIT OR Apache-2.0 OR Zlib" diff --git a/benches/prepare.rs b/benches/prepare.rs index ce30b1e..0e5669d 100644 --- a/benches/prepare.rs +++ b/benches/prepare.rs @@ -1,5 +1,5 @@ use cosmic_text::{Attrs, Buffer, Color, Family, FontSystem, Metrics, Shaping, SwashCache}; -use criterion::{criterion_group, criterion_main, Criterion}; +use criterion::{Criterion, criterion_group, criterion_main}; use cryoglyph::{ Cache, ColorMode, Resolution, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Weight, }; diff --git a/src/lib.rs b/src/lib.rs index 3b31120..9360b30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,11 +21,11 @@ use text_render::ContentType; // Re-export all top-level types from `cosmic-text` for convenience. #[doc(no_inline)] pub use cosmic_text::{ - self, fontdb, Action, Affinity, Attrs, AttrsList, AttrsOwned, Buffer, BufferLine, CacheKey, - Color, Command, Cursor, Edit, Editor, Family, FamilyOwned, Font, FontSystem, LayoutCursor, + self, Action, Affinity, Attrs, AttrsList, AttrsOwned, Buffer, BufferLine, CacheKey, Color, + Command, Cursor, Edit, Editor, Family, FamilyOwned, Font, FontSystem, LayoutCursor, LayoutGlyph, LayoutLine, LayoutRun, LayoutRunIter, Metrics, ShapeGlyph, ShapeLine, ShapeSpan, ShapeWord, Shaping, Stretch, Style, SubpixelBin, SwashCache, SwashContent, SwashImage, Weight, - Wrap, + Wrap, fontdb, }; use etagere::AllocId; diff --git a/src/text_atlas.rs b/src/text_atlas.rs index 8313672..54a0536 100644 --- a/src/text_atlas.rs +++ b/src/text_atlas.rs @@ -1,7 +1,7 @@ use crate::{ - text_render::ContentType, Cache, CacheKey, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, + Cache, CacheKey, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, text_render::ContentType, }; -use etagere::{size2, Allocation, BucketedAtlasAllocator}; +use etagere::{Allocation, BucketedAtlasAllocator, size2}; use lru::LruCache; use rustc_hash::FxHasher; use std::{collections::HashSet, hash::BuildHasherDefault}; diff --git a/src/text_render.rs b/src/text_render.rs index ff6dc60..8b73fe7 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -5,9 +5,9 @@ use crate::{ use std::{num::NonZeroU64, slice}; use wgpu::util::StagingBelt; use wgpu::{ - Buffer, BufferDescriptor, BufferUsages, CommandEncoder, DepthStencilState, Device, Extent3d, - MultisampleState, Origin3d, Queue, RenderPass, RenderPipeline, TexelCopyBufferLayout, - TexelCopyTextureInfo, TextureAspect, COPY_BUFFER_ALIGNMENT, + Buffer, BufferDescriptor, BufferUsages, COPY_BUFFER_ALIGNMENT, CommandEncoder, + DepthStencilState, Device, Extent3d, MultisampleState, Origin3d, Queue, RenderPass, + RenderPipeline, TexelCopyBufferLayout, TexelCopyTextureInfo, TextureAspect, }; /// A text renderer that uses cached glyphs to render text into an existing render pass. From be2defe4a13fd7c97c6f4c81e8e085463eb578dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 10 Mar 2025 19:39:43 +0100 Subject: [PATCH 06/10] Update `cosmic-text` to `0.13` --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 216f294..b9f2951 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0 OR Zlib" [dependencies] wgpu = { version = "24", default-features = false, features = ["wgsl"] } etagere = "0.2.10" -cosmic-text = "0.12" +cosmic-text = "0.13" lru = { version = "0.12.1", default-features = false } rustc-hash = "2.0" From a456d1c17bbcf33afcca41d9e5e299f9f1193819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 9 Apr 2025 18:31:38 +0200 Subject: [PATCH 07/10] Update `cosmic-text` to `0.14` --- Cargo.toml | 2 +- examples/hello-world.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b9f2951..ba8558c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0 OR Zlib" [dependencies] wgpu = { version = "24", default-features = false, features = ["wgsl"] } etagere = "0.2.10" -cosmic-text = "0.13" +cosmic-text = "0.14" lru = { version = "0.12.1", default-features = false } rustc-hash = "2.0" diff --git a/examples/hello-world.rs b/examples/hello-world.rs index 1a13dd1..51671cc 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -87,7 +87,7 @@ impl WindowState { 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.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 { From 8fa8c752ede960a6a9316067959d1aca08575b80 Mon Sep 17 00:00:00 2001 From: Chris Cochrun Date: Thu, 1 May 2025 06:30:08 -0500 Subject: [PATCH 08/10] adding flake --- .envrc | 1 + flake.nix | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 .envrc create mode 100644 flake.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..a5dbbcb --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake . diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..4e418a0 --- /dev/null +++ b/flake.nix @@ -0,0 +1,85 @@ +{ + description = "A fast, simple 2D text renderer for wgpu"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + naersk.url = "github:nix-community/naersk"; + flake-utils.url = "github:numtide/flake-utils"; + fenix.url = "github:nix-community/fenix"; + }; + + outputs = inputs: with inputs; + flake-utils.lib.eachDefaultSystem + (system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [fenix.overlays.default]; + # overlays = [cargo2nix.overlays.default]; + }; + naersk' = pkgs.callPackage naersk {}; + nbi = with pkgs; [ + # Rust tools + alejandra + (pkgs.fenix.stable.withComponents [ + "cargo" + "clippy" + "rust-src" + "rustc" + "rustfmt" + ]) + rust-analyzer + vulkan-loader + wayland + wayland-protocols + libxkbcommon + pkg-config + ]; + + bi = with pkgs; [ + gcc + stdenv + gnumake + gdb + cmake + makeWrapper + vulkan-headers + vulkan-loader + vulkan-tools + harfbuzz + libGL + cargo-flamegraph + fontconfig + just + sqlx-cli + cargo-watch + ]; + in rec + { + devShell = pkgs.mkShell.override { + stdenv = pkgs.stdenvAdapters.useMoldLinker pkgs.clangStdenv; + } { + nativeBuildInputs = nbi; + buildInputs = bi; + LD_LIBRARY_PATH = "$LD_LIBRARY_PATH:${ + with pkgs; + pkgs.lib.makeLibraryPath [ + pkgs.vulkan-loader + pkgs.wayland + pkgs.wayland-protocols + pkgs.libxkbcommon + ] + }"; + DATABASE_URL = "sqlite:///home/chris/.local/share/lumina/library-db.sqlite3"; + }; + defaultPackage = naersk'.buildPackage { + src = ./.; + }; + packages = { + default = naersk'.buildPackage { + src = ./.; + }; + }; + } + ); +} From 84de74e7c2e49f27df06573216e11037796bdead Mon Sep 17 00:00:00 2001 From: Chris Cochrun Date: Wed, 9 Jul 2025 06:04:05 -0500 Subject: [PATCH 09/10] idk --- examples/hello-world.rs | 4 +- flake.lock | 149 ++++++++++++++++++++++++++++++++++++++++ flake.nix | 1 - 3 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 flake.lock diff --git a/examples/hello-world.rs b/examples/hello-world.rs index 51671cc..738f061 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -189,7 +189,7 @@ impl winit::application::ApplicationHandler for Application { right: 600, bottom: 160, }, - default_color: Color::rgb(255, 255, 255), + default_color: Color::rgb(0, 0, 0), }], swash_cache, ) @@ -205,7 +205,7 @@ impl winit::application::ApplicationHandler for Application { view: &view, resolve_target: None, ops: Operations { - load: LoadOp::Clear(wgpu::Color::BLACK), + load: LoadOp::Clear(wgpu::Color::WHITE), store: wgpu::StoreOp::Store, }, })], diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..51620a2 --- /dev/null +++ b/flake.lock @@ -0,0 +1,149 @@ +{ + "nodes": { + "fenix": { + "inputs": { + "nixpkgs": "nixpkgs", + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1746081462, + "narHash": "sha256-WmJBaktb33WwqNn5BwdJghAoiBnvnPhgHSBksTrF5K8=", + "owner": "nix-community", + "repo": "fenix", + "rev": "e3be528e4f03538852ba49b413ec4ac843edeb60", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "naersk": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1745925850, + "narHash": "sha256-cyAAMal0aPrlb1NgzMxZqeN1mAJ2pJseDhm2m6Um8T0=", + "owner": "nix-community", + "repo": "naersk", + "rev": "38bc60bbc157ae266d4a0c96671c6c742ee17a5f", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "naersk", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1745930157, + "narHash": "sha256-y3h3NLnzRSiUkYpnfvnS669zWZLoqqI6NprtLQ+5dck=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "46e634be05ce9dc6d4db8e664515ba10b78151ae", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1746061036, + "narHash": "sha256-OxYwCGJf9VJ2KnUO+w/hVJVTjOgscdDg/lPv8Eus07Y=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3afd19146cac33ed242fc0fc87481c67c758a59e", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1745930157, + "narHash": "sha256-y3h3NLnzRSiUkYpnfvnS669zWZLoqqI6NprtLQ+5dck=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "46e634be05ce9dc6d4db8e664515ba10b78151ae", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "fenix": "fenix", + "flake-utils": "flake-utils", + "naersk": "naersk", + "nixpkgs": "nixpkgs_3" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1746024678, + "narHash": "sha256-Q5J7+RoTPH4zPeu0Ka7iSXtXty228zKjS0Ed4R+ohpA=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "5d66d45005fef79751294419ab9a9fa304dfdf5c", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix index 4e418a0..2b2225b 100644 --- a/flake.nix +++ b/flake.nix @@ -51,7 +51,6 @@ cargo-flamegraph fontconfig just - sqlx-cli cargo-watch ]; in rec From da3b02cf8af74058f7a07e68cc21980afaec9e54 Mon Sep 17 00:00:00 2001 From: Chris Cochrun Date: Thu, 10 Jul 2025 10:03:03 -0500 Subject: [PATCH 10/10] adding together a jump-flood algorithm for rendering outlines --- examples/hello-world.rs | 36 ++-- src/cache.rs | 10 + src/lib.rs | 12 +- src/shader.wgsl | 60 +++++- src/text_atlas.rs | 4 +- src/text_render.rs | 419 ++++++++++++++++++++++------------------ 6 files changed, 332 insertions(+), 209 deletions(-) diff --git a/examples/hello-world.rs b/examples/hello-world.rs index 738f061..88afb29 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -1,6 +1,7 @@ use cryoglyph::{ - Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, - TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, + cosmic_text::LetterSpacing, Attrs, AttrsOwned, Buffer, Cache, Color, Family, FontSystem, + Metrics, Resolution, Shaping, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, + Viewport, }; use std::sync::Arc; use wgpu::{ @@ -169,6 +170,21 @@ impl winit::application::ApplicationHandler for Application { let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor { label: None }); + let text_area = TextArea { + buffer: text_buffer, + left: 10.0, + top: 10.0, + scale: 3.5, + bounds: TextBounds { + left: 0, + top: 0, + right: 600, + bottom: 160, + }, + default_color: Color::rgb(255, 255, 255), + stroke_size: 5.5, + stroke_color: Color::rgb(50, 50, 255), + }; text_renderer .prepare( @@ -178,19 +194,7 @@ impl winit::application::ApplicationHandler for Application { 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(0, 0, 0), - }], + [text_area], swash_cache, ) .unwrap(); @@ -205,7 +209,7 @@ impl winit::application::ApplicationHandler for Application { view: &view, resolve_target: None, ops: Operations { - load: LoadOp::Clear(wgpu::Color::WHITE), + load: LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store, }, })], diff --git a/src/cache.rs b/src/cache.rs index 61d2e4d..9a1e1dc 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -87,6 +87,16 @@ impl Cache { offset: mem::size_of::() as u64 * 6, shader_location: 5, }, + wgpu::VertexAttribute { + format: VertexFormat::Uint32, + offset: mem::size_of::() as u64 * 7, + shader_location: 6, + }, + wgpu::VertexAttribute { + format: VertexFormat::Float32, + offset: mem::size_of::() as u64 * 8, + shader_location: 7, + }, ], }; diff --git a/src/lib.rs b/src/lib.rs index 9360b30..31f5ef7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,11 +21,11 @@ use text_render::ContentType; // Re-export all top-level types from `cosmic-text` for convenience. #[doc(no_inline)] pub use cosmic_text::{ - self, Action, Affinity, Attrs, AttrsList, AttrsOwned, Buffer, BufferLine, CacheKey, Color, - Command, Cursor, Edit, Editor, Family, FamilyOwned, Font, FontSystem, LayoutCursor, + self, fontdb, Action, Affinity, Attrs, AttrsList, AttrsOwned, Buffer, BufferLine, CacheKey, + Color, Command, Cursor, Edit, Editor, Family, FamilyOwned, Font, FontSystem, LayoutCursor, LayoutGlyph, LayoutLine, LayoutRun, LayoutRunIter, Metrics, ShapeGlyph, ShapeLine, ShapeSpan, ShapeWord, Shaping, Stretch, Style, SubpixelBin, SwashCache, SwashContent, SwashImage, Weight, - Wrap, fontdb, + Wrap, }; use etagere::AllocId; @@ -57,6 +57,8 @@ pub(crate) struct GlyphToRender { color: u32, content_type_with_srgb: [u16; 2], depth: f32, + stroke_color: u32, + stroke_size: f32, } /// The screen resolution to use when rendering text. @@ -117,4 +119,8 @@ pub struct TextArea<'a> { pub bounds: TextBounds, // The default color of the text area. pub default_color: Color, + /// The stroke (outline) size + pub stroke_size: f32, + /// The stroke (outline) color + pub stroke_color: Color, } diff --git a/src/shader.wgsl b/src/shader.wgsl index 1813a66..a88e9f6 100644 --- a/src/shader.wgsl +++ b/src/shader.wgsl @@ -6,6 +6,8 @@ struct VertexInput { @location(3) color: u32, @location(4) content_type_with_srgb: u32, @location(5) depth: f32, + @location(6) stroke_color: u32, + @location(7) stroke_size: f32, } struct VertexOutput { @@ -13,6 +15,8 @@ struct VertexOutput { @location(0) color: vec4, @location(1) uv: vec2, @location(2) @interpolate(flat) content_type: u32, + @location(3) stroke_color: vec4, + @location(4) stroke_size: f32, }; struct Params { @@ -42,11 +46,13 @@ fn srgb_to_linear(c: f32) -> f32 { @vertex fn vs_main(in_vert: VertexInput) -> VertexOutput { - var pos = in_vert.pos; - let width = in_vert.dim & 0xffffu; - let height = (in_vert.dim & 0xffff0000u) >> 16u; + var stroke = in_vert.stroke_size; + var width = in_vert.dim & 0xffffu; + var height = (in_vert.dim & 0xffff0000u) >> 16u; + var pos = in_vert.pos; + var uv = vec2(in_vert.uv & 0xffffu, (in_vert.uv & 0xffff0000u) >> 16u); let color = in_vert.color; - var uv = vec2(in_vert.uv & 0xffffu, (in_vert.uv & 0xffff0000u) >> 16u); + let stroke_color = in_vert.stroke_color; let v = in_vert.vertex_idx; let corner_position = vec2( @@ -80,6 +86,12 @@ fn vs_main(in_vert: VertexInput) -> VertexOutput { f32(color & 0x000000ffu) / 255.0, f32((color & 0xff000000u) >> 24u) / 255.0, ); + vert_output.stroke_color = vec4( + f32((stroke_color & 0x00ff0000u) >> 16u) / 255.0, + f32((stroke_color & 0x0000ff00u) >> 8u) / 255.0, + f32(stroke_color & 0x000000ffu) / 255.0, + f32((stroke_color & 0xff000000u) >> 24u) / 255.0, + ); } case 1u: { vert_output.color = vec4( @@ -88,6 +100,12 @@ fn vs_main(in_vert: VertexInput) -> VertexOutput { srgb_to_linear(f32(color & 0x000000ffu) / 255.0), f32((color & 0xff000000u) >> 24u) / 255.0, ); + vert_output.stroke_color = vec4( + srgb_to_linear(f32((stroke_color & 0x00ff0000u) >> 16u) / 255.0), + srgb_to_linear(f32((stroke_color & 0x0000ff00u) >> 8u) / 255.0), + srgb_to_linear(f32(stroke_color & 0x000000ffu) / 255.0), + f32((stroke_color & 0xff000000u) >> 24u) / 255.0, + ); } default: {} } @@ -108,20 +126,54 @@ fn vs_main(in_vert: VertexInput) -> VertexOutput { vert_output.content_type = content_type; vert_output.uv = vec2(uv) / vec2(dim); + vert_output.stroke_size = in_vert.stroke_size; return vert_output; } @fragment fn fs_main(in_frag: VertexOutput) -> @location(0) vec4 { + let dims = vec2(textureDimensions(mask_atlas_texture)); + var bit: u32 = 1; + + let current = textureSample(mask_atlas_texture, atlas_sampler, dims); + var closest_dist = distance(vec2(in_frag.position.xy), vec2(current.xy)); + var result = current; + + for (var step: u32 = 0; step <= 32; step++) { + bit = 1u << step; + for (var dy = -1; dy <= 1; dy++) { + for (var dx = -1; dx <= 1; dx++) { + if (dx == 0 && dy == 0) { + continue; + } + + let neighbour_coord = in_frag.position.xy + vec2(f32(dx * bit), f32(dy * bit)); + let neighbour = textureSample(mask_atlas_texture, atlas_sampler, neighbour_coord / dims); + let dist = distance(vec2(in_frag.position.xy), vec2(neighbour.xy)); + + if (dist < closest_dist) { + closest_dist = dist; + result = vec4(in_frag.stroke_color.rgb, in_frag.stroke_color.a * textureSampleLevel(mask_atlas_texture, atlas_sampler, neighbour.xy, 0.0).x); + } + } + } + } + + switch in_frag.content_type { case 0u: { return textureSampleLevel(color_atlas_texture, atlas_sampler, in_frag.uv, 0.0); } case 1u: { + if in_frag.stroke_size > 0.0 { + return result; + } else { return vec4(in_frag.color.rgb, in_frag.color.a * textureSampleLevel(mask_atlas_texture, atlas_sampler, in_frag.uv, 0.0).x); + } } default: { + // return result; return vec4(0.0); } } diff --git a/src/text_atlas.rs b/src/text_atlas.rs index 54a0536..8313672 100644 --- a/src/text_atlas.rs +++ b/src/text_atlas.rs @@ -1,7 +1,7 @@ use crate::{ - Cache, CacheKey, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, text_render::ContentType, + text_render::ContentType, Cache, CacheKey, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, }; -use etagere::{Allocation, BucketedAtlasAllocator, size2}; +use etagere::{size2, Allocation, BucketedAtlasAllocator}; use lru::LruCache; use rustc_hash::FxHasher; use std::{collections::HashSet, hash::BuildHasherDefault}; diff --git a/src/text_render.rs b/src/text_render.rs index 8b73fe7..39cadc6 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -2,12 +2,13 @@ use crate::{ ColorMode, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError, SwashCache, SwashContent, TextArea, TextAtlas, Viewport, }; +use cosmic_text::LayoutRun; use std::{num::NonZeroU64, slice}; use wgpu::util::StagingBelt; use wgpu::{ - Buffer, BufferDescriptor, BufferUsages, COPY_BUFFER_ALIGNMENT, CommandEncoder, - DepthStencilState, Device, Extent3d, MultisampleState, Origin3d, Queue, RenderPass, - RenderPipeline, TexelCopyBufferLayout, TexelCopyTextureInfo, TextureAspect, + Buffer, BufferDescriptor, BufferUsages, CommandEncoder, DepthStencilState, Device, Extent3d, + MultisampleState, Origin3d, Queue, RenderPass, RenderPipeline, TexelCopyBufferLayout, + TexelCopyTextureInfo, TextureAspect, COPY_BUFFER_ALIGNMENT, }; /// A text renderer that uses cached glyphs to render text into an existing render pass. @@ -87,188 +88,40 @@ impl TextRenderer { .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); - - let cache_key = physical_glyph.cache_key; - - let details = if let Some(details) = - atlas.mask_atlas.glyph_cache.get(&cache_key) - { - atlas.mask_atlas.glyphs_in_use.insert(cache_key); - details - } else if let Some(details) = atlas.color_atlas.glyph_cache.get(&cache_key) { - atlas.color_atlas.glyphs_in_use.insert(cache_key); - details - } 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( - TexelCopyTextureInfo { - 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, - TexelCopyBufferLayout { - 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.glyphs_in_use.insert(cache_key); - // Insert the glyph into the cache and return the details reference - inner.glyph_cache.get_or_insert(cache_key, || GlyphDetails { - width: image.placement.width as u16, - height: image.placement.height as u16, - gpu_cache, - atlas_id, - top: image.placement.top as i16, - left: image.placement.left as i16, - }) - }; - - 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; + if text_area.stroke_size > 0.0 { + for i in 0..2 { + self.prepare_glyphs( + &run, + &text_area, + atlas, + cache, + font_system, + device, + queue, + bounds_max_x, + bounds_max_y, + bounds_min_y, + bounds_min_x, + i == 0, + &mut metadata_to_depth, + )?; } - - // 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); - - 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, - }); + } else { + self.prepare_glyphs( + &run, + &text_area, + atlas, + cache, + font_system, + device, + queue, + bounds_max_x, + bounds_max_y, + bounds_min_y, + bounds_min_x, + false, + &mut metadata_to_depth, + )?; } } } @@ -363,6 +216,204 @@ impl TextRenderer { Ok(()) } + + fn prepare_glyphs( + &mut self, + run: &LayoutRun, + text_area: &TextArea, + atlas: &mut TextAtlas, + cache: &mut SwashCache, + font_system: &mut FontSystem, + device: &Device, + queue: &Queue, + bounds_max_x: i32, + bounds_max_y: i32, + bounds_min_y: i32, + bounds_min_x: i32, + stroke: bool, + mut metadata_to_depth: impl FnMut(usize) -> f32, + ) -> Result<(), PrepareError> { + for glyph in run.glyphs.iter() { + let physical_glyph = glyph.physical((text_area.left, text_area.top), text_area.scale); + + let cache_key = physical_glyph.cache_key; + + let details = if let Some(details) = atlas.mask_atlas.glyph_cache.get(&cache_key) { + atlas.mask_atlas.glyphs_in_use.insert(cache_key); + details + } else if let Some(details) = atlas.color_atlas.glyph_cache.get(&cache_key) { + atlas.color_atlas.glyphs_in_use.insert(cache_key); + details + } 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( + TexelCopyTextureInfo { + 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, + TexelCopyBufferLayout { + 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.glyphs_in_use.insert(cache_key); + // Insert the glyph into the cache and return the details reference + inner.glyph_cache.get_or_insert(cache_key, || GlyphDetails { + width: image.placement.width as u16, + height: image.placement.height as u16, + gpu_cache, + atlas_id, + top: image.placement.top as i16, + left: image.placement.left as i16, + }) + }; + + 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 = if stroke { + text_area.stroke_color + } else { + match glyph.color_opt { + Some(some) => some, + None => text_area.default_color, + } + }; + + let depth = metadata_to_depth(glyph.metadata); + + 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, + stroke_color: text_area.stroke_color.0, + stroke_size: if stroke { text_area.stroke_size } else { 0.0 }, + }); + } + Ok(()) + } } #[repr(u16)]