Compare commits

..

10 commits

Author SHA1 Message Date
Chris Cochrun
da3b02cf8a adding together a jump-flood algorithm for rendering outlines
Some checks are pending
Format / all (push) Waiting to run
Lint / all (push) Waiting to run
Test / all (macOS-latest, beta) (push) Waiting to run
Test / all (macOS-latest, stable) (push) Waiting to run
Test / all (ubuntu-latest, beta) (push) Waiting to run
Test / all (ubuntu-latest, stable) (push) Waiting to run
Test / all (windows-latest, beta) (push) Waiting to run
Test / all (windows-latest, stable) (push) Waiting to run
2025-07-10 10:03:03 -05:00
Chris Cochrun
84de74e7c2 idk 2025-07-09 06:04:05 -05:00
Chris Cochrun
8fa8c752ed adding flake 2025-05-01 06:30:08 -05:00
Héctor Ramón Jiménez
a456d1c17b
Update cosmic-text to 0.14 2025-04-09 18:31:38 +02:00
Héctor Ramón Jiménez
be2defe4a1
Update cosmic-text to 0.13 2025-03-10 19:39:43 +01:00
Héctor Ramón Jiménez
a886f2427e
Update edition to 2024 2025-03-09 01:01:09 +01:00
Héctor Ramón Jiménez
007fc4b0d5
Run cargo fmt 2025-03-09 01:00:26 +01:00
StratusFearMe21
9ab32ae1c3
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`
2025-03-09 00:59:36 +01:00
Alphyr
505f12f6ce
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.
2025-03-09 00:59:20 +01:00
Jose Acevedo
114cfa3e02
fix: use text area bounds to determine visibility 2025-03-09 00:56:37 +01:00
11 changed files with 578 additions and 221 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake .

View file

@ -2,14 +2,14 @@
name = "cryoglyph" name = "cryoglyph"
description = "Fast, simple 2D text rendering for wgpu" description = "Fast, simple 2D text rendering for wgpu"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
repository = "https://github.com/iced-rs/cryoglyph" repository = "https://github.com/iced-rs/cryoglyph"
license = "MIT OR Apache-2.0 OR Zlib" license = "MIT OR Apache-2.0 OR Zlib"
[dependencies] [dependencies]
wgpu = { version = "24", default-features = false, features = ["wgsl"] } wgpu = { version = "24", default-features = false, features = ["wgsl"] }
etagere = "0.2.10" etagere = "0.2.10"
cosmic-text = "0.12" cosmic-text = "0.14"
lru = { version = "0.12.1", default-features = false } lru = { version = "0.12.1", default-features = false }
rustc-hash = "2.0" rustc-hash = "2.0"

View file

@ -1,5 +1,5 @@
use cosmic_text::{Attrs, Buffer, Color, Family, FontSystem, Metrics, Shaping, SwashCache}; 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::{ use cryoglyph::{
Cache, ColorMode, Resolution, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Weight, Cache, ColorMode, Resolution, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Weight,
}; };

View file

@ -1,6 +1,7 @@
use cryoglyph::{ use cryoglyph::{
Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, cosmic_text::LetterSpacing, Attrs, AttrsOwned, Buffer, Cache, Color, Family, FontSystem,
TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Metrics, Resolution, Shaping, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer,
Viewport,
}; };
use std::sync::Arc; use std::sync::Arc;
use wgpu::{ use wgpu::{
@ -87,7 +88,7 @@ impl WindowState {
Some(physical_width), Some(physical_width),
Some(physical_height), 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); text_buffer.shape_until_scroll(&mut font_system, false);
Self { Self {
@ -169,6 +170,21 @@ impl winit::application::ApplicationHandler for Application {
let mut encoder = let mut encoder =
device.create_command_encoder(&CommandEncoderDescriptor { label: None }); 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 text_renderer
.prepare( .prepare(
@ -178,19 +194,7 @@ impl winit::application::ApplicationHandler for Application {
font_system, font_system,
atlas, atlas,
viewport, viewport,
[TextArea { [text_area],
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, swash_cache,
) )
.unwrap(); .unwrap();

149
flake.lock generated Normal file
View file

@ -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
}

84
flake.nix Normal file
View file

@ -0,0 +1,84 @@
{
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
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 = ./.;
};
};
}
);
}

View file

@ -1,5 +1,4 @@
use crate::{GlyphToRender, Params}; use crate::{GlyphToRender, Params};
use wgpu::{ use wgpu::{
BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutEntry, BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutEntry,
BindingResource, BindingType, BlendState, Buffer, BufferBindingType, ColorTargetState, BindingResource, BindingType, BlendState, Buffer, BufferBindingType, ColorTargetState,
@ -14,7 +13,7 @@ use std::borrow::Cow;
use std::mem; use std::mem;
use std::num::NonZeroU64; use std::num::NonZeroU64;
use std::ops::Deref; use std::ops::Deref;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, Mutex};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Cache(Arc<Inner>); pub struct Cache(Arc<Inner>);
@ -27,12 +26,12 @@ struct Inner {
atlas_layout: BindGroupLayout, atlas_layout: BindGroupLayout,
uniforms_layout: BindGroupLayout, uniforms_layout: BindGroupLayout,
pipeline_layout: PipelineLayout, pipeline_layout: PipelineLayout,
cache: RwLock< cache: Mutex<
Vec<( Vec<(
TextureFormat, TextureFormat,
MultisampleState, MultisampleState,
Option<DepthStencilState>, Option<DepthStencilState>,
Arc<RenderPipeline>, RenderPipeline,
)>, )>,
>, >,
} }
@ -88,6 +87,16 @@ impl Cache {
offset: mem::size_of::<u32>() as u64 * 6, offset: mem::size_of::<u32>() as u64 * 6,
shader_location: 5, shader_location: 5,
}, },
wgpu::VertexAttribute {
format: VertexFormat::Uint32,
offset: mem::size_of::<u32>() as u64 * 7,
shader_location: 6,
},
wgpu::VertexAttribute {
format: VertexFormat::Float32,
offset: mem::size_of::<u32>() as u64 * 8,
shader_location: 7,
},
], ],
}; };
@ -150,7 +159,7 @@ impl Cache {
uniforms_layout, uniforms_layout,
atlas_layout, atlas_layout,
pipeline_layout, pipeline_layout,
cache: RwLock::new(Vec::new()), cache: Mutex::new(Vec::new()),
})) }))
} }
@ -197,7 +206,7 @@ impl Cache {
format: TextureFormat, format: TextureFormat,
multisample: MultisampleState, multisample: MultisampleState,
depth_stencil: Option<DepthStencilState>, depth_stencil: Option<DepthStencilState>,
) -> Arc<RenderPipeline> { ) -> RenderPipeline {
let Inner { let Inner {
cache, cache,
pipeline_layout, pipeline_layout,
@ -206,14 +215,14 @@ impl Cache {
.. ..
} = self.0.deref(); } = self.0.deref();
let mut cache = cache.write().expect("Write pipeline cache"); let mut cache = cache.lock().expect("Write pipeline cache");
cache cache
.iter() .iter()
.find(|(fmt, ms, ds, _)| fmt == &format && ms == &multisample && ds == &depth_stencil) .find(|(fmt, ms, ds, _)| fmt == &format && ms == &multisample && ds == &depth_stencil)
.map(|(_, _, _, p)| Arc::clone(p)) .map(|(_, _, _, p)| p.clone())
.unwrap_or_else(|| { .unwrap_or_else(|| {
let pipeline = Arc::new(device.create_render_pipeline(&RenderPipelineDescriptor { let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some("glyphon pipeline"), label: Some("glyphon pipeline"),
layout: Some(pipeline_layout), layout: Some(pipeline_layout),
vertex: VertexState { vertex: VertexState {
@ -240,7 +249,7 @@ impl Cache {
multisample, multisample,
multiview: None, multiview: None,
cache: None, cache: None,
})); });
cache.push((format, multisample, depth_stencil, pipeline.clone())); cache.push((format, multisample, depth_stencil, pipeline.clone()));

View file

@ -57,6 +57,8 @@ pub(crate) struct GlyphToRender {
color: u32, color: u32,
content_type_with_srgb: [u16; 2], content_type_with_srgb: [u16; 2],
depth: f32, depth: f32,
stroke_color: u32,
stroke_size: f32,
} }
/// The screen resolution to use when rendering text. /// The screen resolution to use when rendering text.
@ -117,4 +119,8 @@ pub struct TextArea<'a> {
pub bounds: TextBounds, pub bounds: TextBounds,
// The default color of the text area. // The default color of the text area.
pub default_color: Color, pub default_color: Color,
/// The stroke (outline) size
pub stroke_size: f32,
/// The stroke (outline) color
pub stroke_color: Color,
} }

View file

@ -6,6 +6,8 @@ struct VertexInput {
@location(3) color: u32, @location(3) color: u32,
@location(4) content_type_with_srgb: u32, @location(4) content_type_with_srgb: u32,
@location(5) depth: f32, @location(5) depth: f32,
@location(6) stroke_color: u32,
@location(7) stroke_size: f32,
} }
struct VertexOutput { struct VertexOutput {
@ -13,6 +15,8 @@ struct VertexOutput {
@location(0) color: vec4<f32>, @location(0) color: vec4<f32>,
@location(1) uv: vec2<f32>, @location(1) uv: vec2<f32>,
@location(2) @interpolate(flat) content_type: u32, @location(2) @interpolate(flat) content_type: u32,
@location(3) stroke_color: vec4<f32>,
@location(4) stroke_size: f32,
}; };
struct Params { struct Params {
@ -42,11 +46,13 @@ fn srgb_to_linear(c: f32) -> f32 {
@vertex @vertex
fn vs_main(in_vert: VertexInput) -> VertexOutput { fn vs_main(in_vert: VertexInput) -> VertexOutput {
var pos = in_vert.pos; var stroke = in_vert.stroke_size;
let width = in_vert.dim & 0xffffu; var width = in_vert.dim & 0xffffu;
let height = (in_vert.dim & 0xffff0000u) >> 16u; var height = (in_vert.dim & 0xffff0000u) >> 16u;
var pos = in_vert.pos;
var uv = vec2<u32>(in_vert.uv & 0xffffu, (in_vert.uv & 0xffff0000u) >> 16u);
let color = in_vert.color; let color = in_vert.color;
var uv = vec2<u32>(in_vert.uv & 0xffffu, (in_vert.uv & 0xffff0000u) >> 16u); let stroke_color = in_vert.stroke_color;
let v = in_vert.vertex_idx; let v = in_vert.vertex_idx;
let corner_position = vec2<u32>( let corner_position = vec2<u32>(
@ -80,6 +86,12 @@ fn vs_main(in_vert: VertexInput) -> VertexOutput {
f32(color & 0x000000ffu) / 255.0, f32(color & 0x000000ffu) / 255.0,
f32((color & 0xff000000u) >> 24u) / 255.0, f32((color & 0xff000000u) >> 24u) / 255.0,
); );
vert_output.stroke_color = vec4<f32>(
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: { case 1u: {
vert_output.color = vec4<f32>( vert_output.color = vec4<f32>(
@ -88,6 +100,12 @@ fn vs_main(in_vert: VertexInput) -> VertexOutput {
srgb_to_linear(f32(color & 0x000000ffu) / 255.0), srgb_to_linear(f32(color & 0x000000ffu) / 255.0),
f32((color & 0xff000000u) >> 24u) / 255.0, f32((color & 0xff000000u) >> 24u) / 255.0,
); );
vert_output.stroke_color = vec4<f32>(
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: {} default: {}
} }
@ -108,20 +126,54 @@ fn vs_main(in_vert: VertexInput) -> VertexOutput {
vert_output.content_type = content_type; vert_output.content_type = content_type;
vert_output.uv = vec2<f32>(uv) / vec2<f32>(dim); vert_output.uv = vec2<f32>(uv) / vec2<f32>(dim);
vert_output.stroke_size = in_vert.stroke_size;
return vert_output; return vert_output;
} }
@fragment @fragment
fn fs_main(in_frag: VertexOutput) -> @location(0) vec4<f32> { fn fs_main(in_frag: VertexOutput) -> @location(0) vec4<f32> {
let dims = vec2<f32>(textureDimensions(mask_atlas_texture));
var bit: u32 = 1;
let current = textureSample(mask_atlas_texture, atlas_sampler, dims);
var closest_dist = distance(vec2<f32>(in_frag.position.xy), vec2<f32>(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>(f32(dx * bit), f32(dy * bit));
let neighbour = textureSample(mask_atlas_texture, atlas_sampler, neighbour_coord / dims);
let dist = distance(vec2<f32>(in_frag.position.xy), vec2<f32>(neighbour.xy));
if (dist < closest_dist) {
closest_dist = dist;
result = vec4<f32>(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 { switch in_frag.content_type {
case 0u: { case 0u: {
return textureSampleLevel(color_atlas_texture, atlas_sampler, in_frag.uv, 0.0); return textureSampleLevel(color_atlas_texture, atlas_sampler, in_frag.uv, 0.0);
} }
case 1u: { case 1u: {
if in_frag.stroke_size > 0.0 {
return result;
} else {
return vec4<f32>(in_frag.color.rgb, in_frag.color.a * textureSampleLevel(mask_atlas_texture, atlas_sampler, in_frag.uv, 0.0).x); return vec4<f32>(in_frag.color.rgb, in_frag.color.a * textureSampleLevel(mask_atlas_texture, atlas_sampler, in_frag.uv, 0.0).x);
}
} }
default: { default: {
// return result;
return vec4<f32>(0.0); return vec4<f32>(0.0);
} }
} }

View file

@ -4,7 +4,7 @@ use crate::{
use etagere::{size2, Allocation, BucketedAtlasAllocator}; use etagere::{size2, Allocation, BucketedAtlasAllocator};
use lru::LruCache; use lru::LruCache;
use rustc_hash::FxHasher; use rustc_hash::FxHasher;
use std::{collections::HashSet, hash::BuildHasherDefault, sync::Arc}; use std::{collections::HashSet, hash::BuildHasherDefault};
use wgpu::{ use wgpu::{
BindGroup, DepthStencilState, Device, Extent3d, MultisampleState, Origin3d, Queue, BindGroup, DepthStencilState, Device, Extent3d, MultisampleState, Origin3d, Queue,
RenderPipeline, TexelCopyBufferLayout, TexelCopyTextureInfo, Texture, TextureAspect, RenderPipeline, TexelCopyBufferLayout, TexelCopyTextureInfo, Texture, TextureAspect,
@ -329,7 +329,7 @@ impl TextAtlas {
device: &Device, device: &Device,
multisample: MultisampleState, multisample: MultisampleState,
depth_stencil: Option<DepthStencilState>, depth_stencil: Option<DepthStencilState>,
) -> Arc<RenderPipeline> { ) -> RenderPipeline {
self.cache self.cache
.get_or_create_pipeline(device, self.format, multisample, depth_stencil) .get_or_create_pipeline(device, self.format, multisample, depth_stencil)
} }

View file

@ -2,7 +2,8 @@ use crate::{
ColorMode, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError, ColorMode, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError,
SwashCache, SwashContent, TextArea, TextAtlas, Viewport, SwashCache, SwashContent, TextArea, TextAtlas, Viewport,
}; };
use std::{num::NonZeroU64, slice, sync::Arc}; use cosmic_text::LayoutRun;
use std::{num::NonZeroU64, slice};
use wgpu::util::StagingBelt; use wgpu::util::StagingBelt;
use wgpu::{ use wgpu::{
Buffer, BufferDescriptor, BufferUsages, CommandEncoder, DepthStencilState, Device, Extent3d, Buffer, BufferDescriptor, BufferUsages, CommandEncoder, DepthStencilState, Device, Extent3d,
@ -15,7 +16,7 @@ pub struct TextRenderer {
staging_belt: StagingBelt, staging_belt: StagingBelt,
vertex_buffer: Buffer, vertex_buffer: Buffer,
vertex_buffer_size: u64, vertex_buffer_size: u64,
pipeline: Arc<RenderPipeline>, pipeline: RenderPipeline,
glyph_vertices: Vec<GlyphToRender>, glyph_vertices: Vec<GlyphToRender>,
glyphs_to_render: u32, glyphs_to_render: u32,
} }
@ -73,10 +74,11 @@ impl TextRenderer {
let bounds_max_y = text_area.bounds.bottom.min(resolution.height as i32); let bounds_max_y = text_area.bounds.bottom.min(resolution.height as i32);
let is_run_visible = |run: &cosmic_text::LayoutRun| { let is_run_visible = |run: &cosmic_text::LayoutRun| {
let start_y = (text_area.top + run.line_top) as i32; let start_y_physical = (text_area.top + (run.line_top * text_area.scale)) as i32;
let end_y = (text_area.top + run.line_top + run.line_height) as i32; let end_y_physical = start_y_physical + (run.line_height * text_area.scale) as i32;
start_y <= bounds_max_y && bounds_min_y <= end_y start_y_physical <= text_area.bounds.bottom
&& text_area.bounds.top <= end_y_physical
}; };
let layout_runs = text_area let layout_runs = text_area
@ -86,188 +88,40 @@ impl TextRenderer {
.take_while(is_run_visible); .take_while(is_run_visible);
for run in layout_runs { for run in layout_runs {
for glyph in run.glyphs.iter() { if text_area.stroke_size > 0.0 {
let physical_glyph = for i in 0..2 {
glyph.physical((text_area.left, text_area.top), text_area.scale); self.prepare_glyphs(
&run,
let cache_key = physical_glyph.cache_key; &text_area,
atlas,
let details = if let Some(details) = cache,
atlas.mask_atlas.glyph_cache.get(&cache_key) font_system,
{ device,
atlas.mask_atlas.glyphs_in_use.insert(cache_key); queue,
details bounds_max_x,
} else if let Some(details) = atlas.color_atlas.glyph_cache.get(&cache_key) { bounds_max_y,
atlas.color_atlas.glyphs_in_use.insert(cache_key); bounds_min_y,
details bounds_min_x,
} else { i == 0,
let Some(image) = &mut metadata_to_depth,
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;
} }
} else {
// Starts beyond bottom edge or ends beyond top edge self.prepare_glyphs(
let max_y = y + height; &run,
if y > bounds_max_y || max_y < bounds_min_y { &text_area,
continue; atlas,
} cache,
font_system,
// Clip left ege device,
if x < bounds_min_x { queue,
let right_shift = bounds_min_x - x; bounds_max_x,
bounds_max_y,
x = bounds_min_x; bounds_min_y,
width = max_x - bounds_min_x; bounds_min_x,
atlas_x += right_shift as u16; false,
} &mut metadata_to_depth,
)?;
// 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,
});
} }
} }
} }
@ -362,6 +216,204 @@ impl TextRenderer {
Ok(()) 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)] #[repr(u16)]