Add a galley cache to Fonts to avoid doing the same layout each frame
This commit is contained in:
parent
f9c4be33a7
commit
94baf98eab
5 changed files with 135 additions and 12 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -840,6 +840,7 @@ dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"atomic_refcell",
|
"atomic_refcell",
|
||||||
"emath",
|
"emath",
|
||||||
|
"ordered-float",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"rusttype",
|
"rusttype",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -1618,6 +1619,15 @@ version = "11.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
|
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ordered-float"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "766f840da25490628d8e63e529cd21c014f6600c6b8517add12a6fa6167a6218"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "osmesa-sys"
|
name = "osmesa-sys"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
|
|
@ -6,10 +6,9 @@
|
||||||
[](https://github.com/emilk/egui/actions?workflow=CI)
|
[](https://github.com/emilk/egui/actions?workflow=CI)
|
||||||

|

|
||||||

|

|
||||||
**dependencies**: [`rusttype`](https://crates.io/crates/rusttype) [`atomic_refcell`](https://crates.io/crates/atomic_refcell) [`ahash`](https://crates.io/crates/ahash)
|
|
||||||
|
|
||||||
|
|
||||||
egui is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and in your favorite game engine (or will soon).
|
egui is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and [in your favorite game engine](#integrations) (or will soon).
|
||||||
|
|
||||||
egui aims to be the easiest-to-use Rust GUI libary, and the simplest way to make a web app in Rust.
|
egui aims to be the easiest-to-use Rust GUI libary, and the simplest way to make a web app in Rust.
|
||||||
|
|
||||||
|
@ -80,7 +79,7 @@ ui.label(format!("Hello '{}', age {}", name, age));
|
||||||
* Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/egui_demo_lib/src/apps/demo/toggle_switch.rs)
|
* Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/egui_demo_lib/src/apps/demo/toggle_switch.rs)
|
||||||
* Modular: You should be able to use small parts of egui and combine them in new ways
|
* Modular: You should be able to use small parts of egui and combine them in new ways
|
||||||
* Safe: there is no `unsafe` code in egui
|
* Safe: there is no `unsafe` code in egui
|
||||||
* Minimal dependencies: [`rusttype`](https://crates.io/crates/rusttype), [`atomic_refcell`](https://crates.io/crates/atomic_refcell) and [`ahash`](https://crates.io/crates/ahash).
|
* Minimal dependencies: [`ahash`](https://crates.io/crates/ahash) [`atomic_refcell`](https://crates.io/crates/atomic_refcell) [`ordered-float`](https://crates.io/crates/) [`rusttype`](https://crates.io/crates/rusttype).
|
||||||
|
|
||||||
egui is *not* a framework. egui is a library you call into, not an environment you program for.
|
egui is *not* a framework. egui is a library you call into, not an environment you program for.
|
||||||
|
|
||||||
|
|
|
@ -582,6 +582,8 @@ impl Context {
|
||||||
self.memory()
|
self.memory()
|
||||||
.end_frame(&self.input, &self.frame_state().used_ids);
|
.end_frame(&self.input, &self.frame_state().used_ids);
|
||||||
|
|
||||||
|
self.fonts().end_frame();
|
||||||
|
|
||||||
let mut output: Output = std::mem::take(&mut self.output());
|
let mut output: Output = std::mem::take(&mut self.output());
|
||||||
if self.repaint_requests.load(SeqCst) > 0 {
|
if self.repaint_requests.load(SeqCst) > 0 {
|
||||||
self.repaint_requests.fetch_sub(1, SeqCst);
|
self.repaint_requests.fetch_sub(1, SeqCst);
|
||||||
|
@ -765,7 +767,7 @@ impl Context {
|
||||||
"Wants keyboard input: {}",
|
"Wants keyboard input: {}",
|
||||||
self.wants_keyboard_input()
|
self.wants_keyboard_input()
|
||||||
))
|
))
|
||||||
.on_hover_text("Is egui currently listening for text input");
|
.on_hover_text("Is egui currently listening for text input?");
|
||||||
ui.label(format!(
|
ui.label(format!(
|
||||||
"keyboard focus widget: {}",
|
"keyboard focus widget: {}",
|
||||||
self.memory()
|
self.memory()
|
||||||
|
@ -776,7 +778,14 @@ impl Context {
|
||||||
.map(Id::short_debug_format)
|
.map(Id::short_debug_format)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
))
|
))
|
||||||
.on_hover_text("Is egui currently listening for text input");
|
.on_hover_text("Is egui currently listening for text input?");
|
||||||
|
ui.advance_cursor(16.0);
|
||||||
|
|
||||||
|
ui.label(format!(
|
||||||
|
"There are {} text galleys in the layout cache",
|
||||||
|
self.fonts().num_galleys_in_cache()
|
||||||
|
))
|
||||||
|
.on_hover_text("This is approximately the number of text strings on screen");
|
||||||
ui.advance_cursor(16.0);
|
ui.advance_cursor(16.0);
|
||||||
|
|
||||||
CollapsingHeader::new("📥 Input")
|
CollapsingHeader::new("📥 Input")
|
||||||
|
|
|
@ -26,6 +26,7 @@ emath = { version = "0.10.0", path = "../emath" }
|
||||||
|
|
||||||
ahash = { version = "0.7", features = ["std"], default-features = false }
|
ahash = { version = "0.7", features = ["std"], default-features = false }
|
||||||
atomic_refcell = { version = "0.1", optional = true } # Used instead of parking_lot when you are always using epaint in a single thread. About as fast as parking_lot. Panics on multi-threaded use.
|
atomic_refcell = { version = "0.1", optional = true } # Used instead of parking_lot when you are always using epaint in a single thread. About as fast as parking_lot. Panics on multi-threaded use.
|
||||||
|
ordered-float = "2"
|
||||||
parking_lot = { version = "0.11", optional = true } # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios.
|
parking_lot = { version = "0.11", optional = true } # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios.
|
||||||
rusttype = "0.9"
|
rusttype = "0.9"
|
||||||
serde = { version = "1", features = ["derive"], optional = true }
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::BTreeMap,
|
collections::{BTreeMap, HashMap},
|
||||||
hash::{Hash, Hasher},
|
hash::{Hash, Hasher},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
@ -183,6 +183,8 @@ pub struct Fonts {
|
||||||
/// Copy of the texture in the texture atlas.
|
/// Copy of the texture in the texture atlas.
|
||||||
/// This is so we can return a reference to it (the texture atlas is behind a lock).
|
/// This is so we can return a reference to it (the texture atlas is behind a lock).
|
||||||
buffered_texture: Mutex<Arc<Texture>>,
|
buffered_texture: Mutex<Arc<Texture>>,
|
||||||
|
|
||||||
|
galley_cache: Mutex<GalleyCache>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Fonts {
|
impl Fonts {
|
||||||
|
@ -241,6 +243,7 @@ impl Fonts {
|
||||||
fonts,
|
fonts,
|
||||||
atlas,
|
atlas,
|
||||||
buffered_texture: Default::default(), //atlas.lock().texture().clone();
|
buffered_texture: Default::default(), //atlas.lock().texture().clone();
|
||||||
|
galley_cache: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,7 +289,14 @@ impl Fonts {
|
||||||
/// Most often you probably want `\n` to produce a new row,
|
/// Most often you probably want `\n` to produce a new row,
|
||||||
/// and so [`Self::layout_no_wrap`] may be a better choice.
|
/// and so [`Self::layout_no_wrap`] may be a better choice.
|
||||||
pub fn layout_single_line(&self, text_style: TextStyle, text: String) -> Galley {
|
pub fn layout_single_line(&self, text_style: TextStyle, text: String) -> Galley {
|
||||||
self.fonts[&text_style].layout_single_line(text)
|
self.galley_cache.lock().layout(
|
||||||
|
&self.fonts,
|
||||||
|
LayoutJob {
|
||||||
|
text_style,
|
||||||
|
text,
|
||||||
|
layout_params: LayoutParams::SingleLine,
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Always returns at least one row.
|
/// Always returns at least one row.
|
||||||
|
@ -315,12 +325,27 @@ impl Fonts {
|
||||||
first_row_indentation: f32,
|
first_row_indentation: f32,
|
||||||
max_width_in_points: f32,
|
max_width_in_points: f32,
|
||||||
) -> Galley {
|
) -> Galley {
|
||||||
self.fonts[&text_style].layout_multiline_with_indentation_and_max_width(
|
self.galley_cache.lock().layout(
|
||||||
text,
|
&self.fonts,
|
||||||
first_row_indentation,
|
LayoutJob {
|
||||||
max_width_in_points,
|
text_style,
|
||||||
|
text,
|
||||||
|
layout_params: LayoutParams::Multiline {
|
||||||
|
first_row_indentation: first_row_indentation.into(),
|
||||||
|
max_width_in_points: max_width_in_points.into(),
|
||||||
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn num_galleys_in_cache(&self) -> usize {
|
||||||
|
self.galley_cache.lock().num_galleys_in_cache()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Must be called once per frame to clear the [`Galley`] cache.
|
||||||
|
pub fn end_frame(&self) {
|
||||||
|
self.galley_cache.lock().end_frame()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::Index<TextStyle> for Fonts {
|
impl std::ops::Index<TextStyle> for Fonts {
|
||||||
|
@ -333,10 +358,89 @@ impl std::ops::Index<TextStyle> for Fonts {
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
|
||||||
|
enum LayoutParams {
|
||||||
|
SingleLine,
|
||||||
|
Multiline {
|
||||||
|
first_row_indentation: ordered_float::OrderedFloat<f32>,
|
||||||
|
max_width_in_points: ordered_float::OrderedFloat<f32>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||||
|
struct LayoutJob {
|
||||||
|
text_style: TextStyle,
|
||||||
|
layout_params: LayoutParams,
|
||||||
|
text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CachedGalley {
|
||||||
|
/// When it was last used
|
||||||
|
last_used: u32,
|
||||||
|
galley: Galley, // TODO: use an Arc instead!
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct GalleyCache {
|
||||||
|
/// Frame counter used to do garbage collection on the cache
|
||||||
|
generation: u32,
|
||||||
|
cache: HashMap<LayoutJob, CachedGalley>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GalleyCache {
|
||||||
|
fn layout(&mut self, fonts: &BTreeMap<TextStyle, Font>, job: LayoutJob) -> Galley {
|
||||||
|
if let Some(cached) = self.cache.get_mut(&job) {
|
||||||
|
cached.last_used = self.generation;
|
||||||
|
cached.galley.clone()
|
||||||
|
} else {
|
||||||
|
let LayoutJob {
|
||||||
|
text_style,
|
||||||
|
layout_params,
|
||||||
|
text,
|
||||||
|
} = job.clone();
|
||||||
|
let font = &fonts[&text_style];
|
||||||
|
let galley = match layout_params {
|
||||||
|
LayoutParams::SingleLine => font.layout_single_line(text),
|
||||||
|
LayoutParams::Multiline {
|
||||||
|
first_row_indentation,
|
||||||
|
max_width_in_points,
|
||||||
|
} => font.layout_multiline_with_indentation_and_max_width(
|
||||||
|
text,
|
||||||
|
first_row_indentation.into_inner(),
|
||||||
|
max_width_in_points.into_inner(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
self.cache.insert(
|
||||||
|
job,
|
||||||
|
CachedGalley {
|
||||||
|
last_used: self.generation,
|
||||||
|
galley: galley.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
galley
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn num_galleys_in_cache(&self) -> usize {
|
||||||
|
self.cache.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Must be called once per frame to clear the [`Galley`] cache.
|
||||||
|
pub fn end_frame(&mut self) {
|
||||||
|
let current_generation = self.generation;
|
||||||
|
self.cache.retain(|_key, cached| {
|
||||||
|
cached.last_used == current_generation // only keep those that were used this frame
|
||||||
|
});
|
||||||
|
self.generation = self.generation.wrapping_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
struct FontImplCache {
|
struct FontImplCache {
|
||||||
atlas: Arc<Mutex<TextureAtlas>>,
|
atlas: Arc<Mutex<TextureAtlas>>,
|
||||||
pixels_per_point: f32,
|
pixels_per_point: f32,
|
||||||
rusttype_fonts: std::collections::BTreeMap<String, Arc<rusttype::Font<'static>>>,
|
rusttype_fonts: BTreeMap<String, Arc<rusttype::Font<'static>>>,
|
||||||
|
|
||||||
/// Map font names and size to the cached `FontImpl`.
|
/// Map font names and size to the cached `FontImpl`.
|
||||||
/// Can't have f32 in a HashMap or BTreeMap, so let's do a linear search
|
/// Can't have f32 in a HashMap or BTreeMap, so let's do a linear search
|
||||||
|
|
Loading…
Reference in a new issue