Compare commits

...

10 commits

Author SHA1 Message Date
01ab64704b
Add opacity 2024-03-18 18:30:28 -04:00
grovesNL
4590abae08 Release 0.5.0 2024-01-17 20:33:06 -03:30
TheEggShark
65825aa0d4 update to wgpu 0.19.0 2024-01-17 20:25:33 -03:30
grovesNL
3425efd522 Release 0.4.1 2024-01-15 21:36:08 -03:30
grovesNL
bfb07147cd Don't convert web text colors to linear 2024-01-15 21:32:22 -03:30
grovesNL
7f9afac93e Release 0.4.0 2024-01-15 16:24:45 -03:30
grovesNL
1bb46e41ff Update lru 2024-01-15 16:04:27 -03:30
grovesNL
c2469de817 Update winit 2024-01-15 16:04:27 -03:30
grovesNL
ce394f9d53 Internally convert text colors to linear 2024-01-15 13:05:48 -03:30
grovesNL
ca5f82e5e9 Grow by a factor of 2
This reduces the amount of growing necessary for large glyphs
2024-01-15 11:49:35 -03:30
6 changed files with 154 additions and 104 deletions

View file

@ -1,18 +1,18 @@
[package] [package]
name = "glyphon" name = "glyphon"
description = "Fast, simple 2D text rendering for wgpu" description = "Fast, simple 2D text rendering for wgpu"
version = "0.3.0" version = "0.5.0"
edition = "2021" edition = "2021"
homepage = "https://github.com/grovesNL/glyphon.git" homepage = "https://github.com/grovesNL/glyphon.git"
repository = "https://github.com/grovesNL/glyphon" repository = "https://github.com/grovesNL/glyphon"
license = "MIT OR Apache-2.0 OR Zlib" license = "MIT OR Apache-2.0 OR Zlib"
[dependencies] [dependencies]
wgpu = "0.18" wgpu = "0.19"
etagere = "0.2.10" etagere = "0.2.10"
cosmic-text = "0.10" lru = "0.12.1"
lru = "0.11" cosmic-text = "0.11.2"
[dev-dependencies] [dev-dependencies]
winit = "0.28.7" winit = { version = "0.29.10", features = ["rwh_05"] }
pollster = "0.3.0" pollster = "0.3.0"

View file

@ -11,10 +11,12 @@ use wgpu::{
use winit::{ use winit::{
dpi::LogicalSize, dpi::LogicalSize,
event::{Event, WindowEvent}, event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop}, event_loop::EventLoop,
window::WindowBuilder, window::WindowBuilder,
}; };
use std::sync::Arc;
fn main() { fn main() {
pollster::block_on(run()); pollster::block_on(run());
} }
@ -22,12 +24,12 @@ fn main() {
async fn run() { async fn run() {
// Set up window // Set up window
let (width, height) = (800, 600); let (width, height) = (800, 600);
let event_loop = EventLoop::new(); let event_loop = EventLoop::new().unwrap();
let window = WindowBuilder::new() let window = Arc::new(WindowBuilder::new()
.with_inner_size(LogicalSize::new(width as f64, height as f64)) .with_inner_size(LogicalSize::new(width as f64, height as f64))
.with_title("glyphon hello world") .with_title("glyphon hello world")
.build(&event_loop) .build(&event_loop)
.unwrap(); .unwrap());
let size = window.inner_size(); let size = window.inner_size();
let scale_factor = window.scale_factor(); let scale_factor = window.scale_factor();
@ -41,14 +43,15 @@ async fn run() {
.request_device( .request_device(
&DeviceDescriptor { &DeviceDescriptor {
label: None, label: None,
features: Features::empty(), required_features: Features::empty(),
limits: Limits::downlevel_defaults(), required_limits: Limits::downlevel_defaults(),
}, },
None, None,
) )
.await .await
.unwrap(); .unwrap();
let surface = unsafe { instance.create_surface(&window) }.expect("Create surface");
let surface = instance.create_surface(window.clone()).expect("Create surface");
let swapchain_format = TextureFormat::Bgra8UnormSrgb; let swapchain_format = TextureFormat::Bgra8UnormSrgb;
let mut config = SurfaceConfiguration { let mut config = SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT, usage: TextureUsages::RENDER_ATTACHMENT,
@ -58,6 +61,7 @@ async fn run() {
present_mode: PresentMode::Fifo, present_mode: PresentMode::Fifo,
alpha_mode: CompositeAlphaMode::Opaque, alpha_mode: CompositeAlphaMode::Opaque,
view_formats: vec![], view_formats: vec![],
desired_maximum_frame_latency: 2,
}; };
surface.configure(&device, &config); surface.configure(&device, &config);
@ -76,84 +80,80 @@ async fn run() {
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.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);
event_loop.run(move |event, _, control_flow| { event_loop
let _ = (&instance, &adapter); .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 => {
text_renderer
.prepare(
&device,
&queue,
&mut font_system,
&mut atlas,
Resolution {
width: config.width,
height: config.height,
},
[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 cache,
)
.unwrap();
*control_flow = ControlFlow::Poll; let frame = surface.get_current_texture().unwrap();
match event { let view = frame.texture.create_view(&TextureViewDescriptor::default());
Event::WindowEvent { let mut encoder = device
event: WindowEvent::Resized(size), .create_command_encoder(&CommandEncoderDescriptor { label: None });
.. {
} => { let mut pass = encoder.begin_render_pass(&RenderPassDescriptor {
config.width = size.width; label: None,
config.height = size.height; color_attachments: &[Some(RenderPassColorAttachment {
surface.configure(&device, &config); view: &view,
window.request_redraw(); resolve_target: None,
} ops: Operations {
Event::RedrawRequested(_) => { load: LoadOp::Clear(wgpu::Color::BLACK),
text_renderer store: wgpu::StoreOp::Store,
.prepare( },
&device, })],
&queue, depth_stencil_attachment: None,
&mut font_system, timestamp_writes: None,
&mut atlas, occlusion_query_set: None,
Resolution { });
width: config.width,
height: config.height,
},
[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 cache,
)
.unwrap();
let frame = surface.get_current_texture().unwrap(); text_renderer.render(&atlas, &mut pass).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, &mut pass).unwrap(); queue.submit(Some(encoder.finish()));
frame.present();
atlas.trim();
}
WindowEvent::CloseRequested => target.exit(),
_ => {}
} }
queue.submit(Some(encoder.finish()));
frame.present();
atlas.trim();
} }
Event::WindowEvent { })
event: WindowEvent::CloseRequested, .unwrap();
..
} => *control_flow = ControlFlow::Exit,
Event::MainEventsCleared => {
window.request_redraw();
}
_ => {}
}
});
} }

View file

@ -51,7 +51,7 @@ pub(crate) struct GlyphToRender {
dim: [u16; 2], dim: [u16; 2],
uv: [u16; 2], uv: [u16; 2],
color: u32, color: u32,
content_type: u32, content_type_with_srgb: [u16; 2],
depth: f32, depth: f32,
} }
@ -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 /// 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. /// match the `left` and `top` values.
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 opacity to set the text area to (in gamma space)
pub opacity: f32,
} }

View file

@ -4,7 +4,7 @@ struct VertexInput {
@location(1) dim: u32, @location(1) dim: u32,
@location(2) uv: u32, @location(2) uv: u32,
@location(3) color: u32, @location(3) color: u32,
@location(4) content_type: u32, @location(4) content_type_with_srgb: u32,
@location(5) depth: f32, @location(5) depth: f32,
} }
@ -32,6 +32,14 @@ var mask_atlas_texture: texture_2d<f32>;
@group(0) @binding(3) @group(0) @binding(3)
var atlas_sampler: sampler; var atlas_sampler: sampler;
fn srgb_to_linear(c: f32) -> f32 {
if c <= 0.04045 {
return c / 12.92;
} else {
return pow((c + 0.055) / 1.055, 2.4);
}
}
@vertex @vertex
fn vs_main(in_vert: VertexInput) -> VertexOutput { fn vs_main(in_vert: VertexInput) -> VertexOutput {
var pos = in_vert.pos; var pos = in_vert.pos;
@ -69,15 +77,31 @@ fn vs_main(in_vert: VertexInput) -> VertexOutput {
vert_output.position.y *= -1.0; vert_output.position.y *= -1.0;
vert_output.color = vec4<f32>( let content_type = in_vert.content_type_with_srgb & 0xffffu;
f32((color & 0x00ff0000u) >> 16u), let srgb = (in_vert.content_type_with_srgb & 0xffff0000u) >> 16u;
f32((color & 0x0000ff00u) >> 8u),
f32(color & 0x000000ffu), switch srgb {
f32((color & 0xff000000u) >> 24u), case 0u: {
) / 255.0; vert_output.color = vec4<f32>(
f32((color & 0x00ff0000u) >> 16u) / 255.0,
f32((color & 0x0000ff00u) >> 8u) / 255.0,
f32(color & 0x000000ffu) / 255.0,
f32((color & 0xff000000u) >> 24u) / 255.0,
);
}
case 1u: {
vert_output.color = vec4<f32>(
srgb_to_linear(f32((color & 0x00ff0000u) >> 16u) / 255.0),
srgb_to_linear(f32((color & 0x0000ff00u) >> 8u) / 255.0),
srgb_to_linear(f32(color & 0x000000ffu) / 255.0),
f32((color & 0xff000000u) >> 24u) / 255.0,
);
}
default: {}
}
var dim: vec2<u32> = vec2(0u); var dim: vec2<u32> = vec2(0u);
switch in_vert.content_type { switch content_type {
case 0u: { case 0u: {
dim = textureDimensions(color_atlas_texture); dim = textureDimensions(color_atlas_texture);
break; break;
@ -89,7 +113,7 @@ fn vs_main(in_vert: VertexInput) -> VertexOutput {
default: {} default: {}
} }
vert_output.content_type = in_vert.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);

View file

@ -131,8 +131,10 @@ impl InnerAtlas {
return false; return false;
} }
// TODO: Better resizing logic (?) // Grow each dimension by a factor of 2. The growth factor was chosen to match the growth
let new_size = (self.size + Self::INITIAL_SIZE).min(self.max_texture_dimension_2d); // factor of `Vec`.`
const GROWTH_FACTOR: u32 = 2;
let new_size = (self.size * GROWTH_FACTOR).min(self.max_texture_dimension_2d);
self.packer.grow(size2(new_size as i32, new_size as i32)); self.packer.grow(size2(new_size as i32, new_size as i32));
@ -269,6 +271,7 @@ pub struct TextAtlas {
pub(crate) shader: ShaderModule, pub(crate) shader: ShaderModule,
pub(crate) vertex_buffers: [wgpu::VertexBufferLayout<'static>; 1], pub(crate) vertex_buffers: [wgpu::VertexBufferLayout<'static>; 1],
pub(crate) format: TextureFormat, pub(crate) format: TextureFormat,
pub(crate) color_mode: ColorMode,
} }
impl TextAtlas { impl TextAtlas {
@ -448,6 +451,7 @@ impl TextAtlas {
shader, shader,
vertex_buffers, vertex_buffers,
format, format,
color_mode,
} }
} }

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, Params, PrepareError, RenderError, ColorMode, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, Params, PrepareError,
Resolution, SwashCache, SwashContent, TextArea, TextAtlas, RenderError, Resolution, SwashCache, SwashContent, TextArea, TextAtlas,
}; };
use std::{iter, mem::size_of, slice, sync::Arc}; use std::{iter, mem::size_of, slice, sync::Arc};
use wgpu::{ use wgpu::{
@ -268,6 +268,13 @@ impl TextRenderer {
None => text_area.default_color, 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); let depth = metadata_to_depth(glyph.metadata);
glyph_vertices.extend( glyph_vertices.extend(
@ -276,7 +283,13 @@ impl TextRenderer {
dim: [width as u16, height as u16], dim: [width as u16, height as u16],
uv: [atlas_x, atlas_y], uv: [atlas_x, atlas_y],
color: color.0, color: color.0,
content_type: content_type as u32, content_type_with_srgb: [
content_type as u16,
match atlas.color_mode {
ColorMode::Accurate => TextColorConversion::ConvertToLinear,
ColorMode::Web => TextColorConversion::None,
} as u16,
],
depth, depth,
}) })
.take(4), .take(4),
@ -405,13 +418,20 @@ impl TextRenderer {
} }
} }
#[repr(u32)] #[repr(u16)]
#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum ContentType { pub enum ContentType {
Color = 0, Color = 0,
Mask = 1, Mask = 1,
} }
#[repr(u16)]
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum TextColorConversion {
None = 0,
ConvertToLinear = 1,
}
fn next_copy_buffer_size(size: u64) -> u64 { fn next_copy_buffer_size(size: u64) -> u64 {
let align_mask = COPY_BUFFER_ALIGNMENT - 1; let align_mask = COPY_BUFFER_ALIGNMENT - 1;
((size.next_power_of_two() + align_mask) & !align_mask).max(COPY_BUFFER_ALIGNMENT) ((size.next_power_of_two() + align_mask) & !align_mask).max(COPY_BUFFER_ALIGNMENT)