GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
perf: implement TextBufferCache and scroll event coalescing to avoid redundant font shaping and paint overhead
src/backend/window_runner.rs | 85 ++++++++++++++++++++++++++++++++++++-----
src/main.rs | 62 ++++++++++++++++++++----------
src/widget/display/list_item.rs | 31 +--------------
3 files changed, 117 insertions(+), 61 deletions(-)
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index b29634e..35fba63 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -34,7 +34,7 @@ use glyphon::{
Cache, FontSystem, Resolution, TextArea,
TextBounds, Viewport, Buffer, Attrs, Metrics,
};
-use crate::widget::{Element, TextItem, MouseButton, ElementState, MouseScrollDelta, KeyEvent, Key, NamedKey};
+use crate::widget::{Element, TextItem, MouseButton, ElementState, MouseScrollDelta, KeyEvent, Key, NamedKey, Position};
use crate::wayland::{WaylandSurfaceHandle, detect_scale_factor};
use crate::backend::WgpuAdapter;
@@ -51,7 +51,18 @@ pub struct ActivePopup {
pub vertex_buffer: Option<wgpu::Buffer>,
}
-fn make_text_buffer_with_font(fs: &mut FontSystem, text: &str, size: f32, font: Option<&str>) -> Buffer {
+#[derive(Hash, PartialEq, Eq, Clone)]
+struct BufferCacheKey {
+ text: String,
+ size_milli: u32,
+ font: Option<String>,
+}
+
+std::thread_local! {
+ static BUFFER_CACHE: std::cell::RefCell<std::collections::HashMap<BufferCacheKey, Buffer>> = std::cell::RefCell::new(std::collections::HashMap::new());
+}
+
+pub fn get_text_buffer(fs: &mut FontSystem, text: &str, size: f32, font: Option<&str>) -> Buffer {
let scale = crate::scale::scale_factor();
let mut font_size = size;
let mut family_name = None;
@@ -65,6 +76,21 @@ fn make_text_buffer_with_font(fs: &mut FontSystem, text: &str, size: f32, font:
}
let physical_size = font_size * scale;
+ let size_key = (physical_size * 1000.0).round() as u32;
+ let key = BufferCacheKey {
+ text: text.to_string(),
+ size_milli: size_key,
+ font: family_name.clone(),
+ };
+
+ let cached = BUFFER_CACHE.with(|cache| {
+ cache.borrow().get(&key).cloned()
+ });
+
+ if let Some(buf) = cached {
+ return buf;
+ }
+
let metrics = Metrics::new(physical_size, physical_size * 1.4);
let mut buf = Buffer::new(fs, metrics);
let mut attrs = Attrs::new();
@@ -79,9 +105,22 @@ fn make_text_buffer_with_font(fs: &mut FontSystem, text: &str, size: f32, font:
}
buf.set_text(fs, text, attrs, glyphon::Shaping::Advanced);
buf.shape_until_scroll(fs, true);
+
+ BUFFER_CACHE.with(|cache| {
+ let mut cache = cache.borrow_mut();
+ if cache.len() > 2000 {
+ cache.clear();
+ }
+ cache.insert(key, buf.clone());
+ });
+
buf
}
+fn make_text_buffer_with_font(fs: &mut FontSystem, text: &str, size: f32, font: Option<&str>) -> Buffer {
+ get_text_buffer(fs, text, size, font)
+}
+
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Vertex {
@@ -1615,6 +1654,13 @@ impl<A: Application> PointerHandler for EngineState<A> {
events: &[smithay_client_toolkit::seat::pointer::PointerEvent],
) {
use smithay_client_toolkit::seat::pointer::PointerEventKind;
+ let mut coalesced_h = 0.0f64;
+ let mut coalesced_v = 0.0f64;
+ let mut discrete_h = 0;
+ let mut discrete_v = 0;
+ let mut has_scroll = false;
+ let (mut last_lx, mut last_ly) = (0.0f32, 0.0f32);
+
for event in events {
let (x, y) = event.position;
let mut lx = x as f32;
@@ -1744,17 +1790,36 @@ impl<A: Application> PointerHandler for EngineState<A> {
}
}
PointerEventKind::Axis { horizontal, vertical, .. } => {
- let h_scroll = horizontal.absolute as f32;
- let v_scroll = vertical.absolute as f32;
- let delta = MouseScrollDelta::LineDelta(-h_scroll / 10.0, -v_scroll / 10.0);
- let mut rebuild = false;
- self.inner.handle_mouse_wheel(&delta, LogicalPosition::new(lx, ly), &mut rebuild);
- if rebuild {
- self.redraw = true;
- }
+ coalesced_h += horizontal.absolute;
+ coalesced_v += vertical.absolute;
+ discrete_h += horizontal.discrete;
+ discrete_v += vertical.discrete;
+ last_lx = lx;
+ last_ly = ly;
+ has_scroll = true;
}
}
}
+
+ if has_scroll {
+ let delta = if discrete_h == 0 && discrete_v == 0 {
+ // Pixel scroll event from touchpad / smooth mouse
+ MouseScrollDelta::PixelDelta(Position {
+ x: -coalesced_h,
+ y: -coalesced_v,
+ })
+ } else {
+ // Discrete scroll event (e.g. wheel clicks)
+ let h_lines = if discrete_h != 0 { discrete_h as f32 } else { coalesced_h as f32 / 10.0 };
+ let v_lines = if discrete_v != 0 { discrete_v as f32 } else { coalesced_v as f32 / 10.0 };
+ MouseScrollDelta::LineDelta(-h_lines, -v_lines)
+ };
+ let mut rebuild = false;
+ self.inner.handle_mouse_wheel(&delta, LogicalPosition::new(last_lx, last_ly), &mut rebuild);
+ if rebuild {
+ self.redraw = true;
+ }
+ }
}
}
diff --git a/src/main.rs b/src/main.rs
index c594014..d0905b3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1110,6 +1110,11 @@ impl PointerHandler for AppState {
events: &[smithay_client_toolkit::seat::pointer::PointerEvent],
) {
use smithay_client_toolkit::seat::pointer::PointerEventKind;
+ let mut coalesced_h = 0.0f64;
+ let mut coalesced_v = 0.0f64;
+ let mut discrete_h = 0;
+ let mut discrete_v = 0;
+ let mut has_scroll = false;
for event in events {
let (x, y) = event.position;
if let Some(state) = &mut self.state {
@@ -1317,31 +1322,46 @@ impl PointerHandler for AppState {
}
}
PointerEventKind::Axis { horizontal, vertical, .. } => {
- if let Some(state) = &mut self.state {
- let h_scroll = horizontal.absolute as f32;
- let v_scroll = vertical.absolute as f32;
-
- let delta = cce_ui::widget::MouseScrollDelta::LineDelta(-h_scroll / 10.0, -v_scroll / 10.0);
- let mut changed = false;
- if !state.layout_mode {
- for w in &mut state.widgets {
- if w.mouse_wheel(&delta, state.cursor_x, state.cursor_y, &mut state.ui_context) {
- changed = true;
- }
- }
- } else {
- if let Some(jl) = &mut state.json_layout {
- if jl.mouse_wheel(&delta, state.cursor_x, state.cursor_y, &mut state.ui_context) {
- changed = true;
- }
- }
+ coalesced_h += horizontal.absolute;
+ coalesced_v += vertical.absolute;
+ discrete_h += horizontal.discrete;
+ discrete_v += vertical.discrete;
+ has_scroll = true;
+ }
+ }
+ }
+
+ if has_scroll {
+ if let Some(state) = &mut self.state {
+ let delta = if discrete_h == 0 && discrete_v == 0 {
+ cce_ui::widget::MouseScrollDelta::PixelDelta(cce_ui::widget::Position {
+ x: -coalesced_h,
+ y: -coalesced_v,
+ })
+ } else {
+ let h_lines = if discrete_h != 0 { discrete_h as f32 } else { coalesced_h as f32 / 10.0 };
+ let v_lines = if discrete_v != 0 { discrete_v as f32 } else { coalesced_v as f32 / 10.0 };
+ cce_ui::widget::MouseScrollDelta::LineDelta(-h_lines, -v_lines)
+ };
+
+ let mut changed = false;
+ if !state.layout_mode {
+ for w in &mut state.widgets {
+ if w.mouse_wheel(&delta, state.cursor_x, state.cursor_y, &mut state.ui_context) {
+ changed = true;
}
- if changed {
- state.upload_vertices();
- self.redraw = true;
+ }
+ } else {
+ if let Some(jl) = &mut state.json_layout {
+ if jl.mouse_wheel(&delta, state.cursor_x, state.cursor_y, &mut state.ui_context) {
+ changed = true;
}
}
}
+ if changed {
+ state.upload_vertices();
+ self.redraw = true;
+ }
}
}
}
diff --git a/src/widget/display/list_item.rs b/src/widget/display/list_item.rs
index b667bc0..290620d 100644
--- a/src/widget/display/list_item.rs
+++ b/src/widget/display/list_item.rs
@@ -22,36 +22,7 @@ impl TextItem {
font: Option<&str>,
bounds: Option<[f32; 4]>,
) -> Self {
- let scale = crate::scale::scale_factor();
- let mut font_size = size;
- let mut family_name = None;
-
- if let Some(font_str) = font {
- let (parsed_family, parsed_size) = crate::layout::parse_font_string(font_str);
- if let Some(ps) = parsed_size {
- font_size = ps;
- }
- family_name = Some(parsed_family);
- }
-
- let physical_size = font_size * scale;
- let metrics = glyphon::Metrics::new(physical_size, physical_size * 1.4);
- let mut buffer = glyphon::Buffer::new(fs, metrics);
- let mut attrs = glyphon::Attrs::new();
-
- if let Some(ref fam) = family_name {
- let family = match fam.as_str() {
- "monospace" => glyphon::Family::Name(crate::layout::get_system_monospace_font()),
- "sans-serif" => glyphon::Family::SansSerif,
- "serif" => glyphon::Family::Serif,
- _ => glyphon::Family::Name(fam),
- };
- attrs = attrs.family(family);
- }
-
- buffer.set_text(fs, text, attrs, glyphon::Shaping::Advanced);
- buffer.shape_until_scroll(fs, true);
-
+ let buffer = crate::backend::window_runner::get_text_buffer(fs, text, size, font);
Self {
buffer,
x,