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] 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);