1
1
mirror of https://github.com/wez/wezterm.git synced 2024-11-27 12:23:46 +03:00

font: make system fallback async wrt. rendering

If shaping can't resolve some glyphs, queue the font locator
fallback resolution to another thread; meanwhile, a last resort
glyph is used.

That thread can trigger an invalidation once the fallback resolve
is complete, the window is invalidated and the last resort glyph
is replaced by the resolve glyph.

refs: https://github.com/wez/wezterm/issues/559
refs: https://github.com/wez/wezterm/issues/508
This commit is contained in:
Wez Furlong 2021-03-22 20:36:32 -07:00
parent e69485d3ca
commit 8f856d0b81
4 changed files with 148 additions and 115 deletions

View File

@ -6,16 +6,15 @@ use anyhow::{anyhow, Context};
use config::{Config, FontAttributes};
use rangeset::RangeSet;
use std::borrow::Cow;
use std::cell::RefCell;
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
struct Entry {
names: Names,
handle: FontDataHandle,
coverage: RefCell<Option<RangeSet<u32>>>,
coverage: Mutex<Option<RangeSet<u32>>>,
}
impl Entry {
@ -50,7 +49,7 @@ impl Entry {
/// Computes the codepoint coverage for this font entry if we haven't
/// already done so.
fn coverage_intersection(&self, wanted: &RangeSet<u32>) -> anyhow::Result<RangeSet<u32>> {
let mut coverage = self.coverage.borrow_mut();
let mut coverage = self.coverage.lock().unwrap();
if coverage.is_none() {
let t = std::time::Instant::now();
coverage.replace(self.compute_coverage()?);
@ -68,8 +67,8 @@ impl Entry {
}
pub struct FontDatabase {
by_family: HashMap<String, Vec<Rc<Entry>>>,
by_full_name: HashMap<String, Rc<Entry>>,
by_family: HashMap<String, Vec<Arc<Entry>>>,
by_full_name: HashMap<String, Arc<Entry>>,
}
impl FontDatabase {
@ -82,17 +81,17 @@ impl FontDatabase {
fn load_font_info(&mut self, font_info: Vec<(Names, PathBuf, FontDataHandle)>) {
for (names, _path, handle) in font_info {
let entry = Rc::new(Entry {
let entry = Arc::new(Entry {
names,
handle,
coverage: RefCell::new(None),
coverage: Mutex::new(None),
});
if let Some(family) = entry.names.family.as_ref() {
self.by_family
.entry(family.to_string())
.or_insert_with(Vec::new)
.push(Rc::clone(&entry));
.push(Arc::clone(&entry));
}
self.by_full_name

View File

@ -7,6 +7,8 @@ use config::{configuration, ConfigHandle, FontRasterizerSelection, TextStyle};
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::rc::{Rc, Weak};
use std::sync::{Arc, Mutex};
use thiserror::Error;
use wezterm_term::CellAttributes;
use window::default_dpi;
@ -26,6 +28,10 @@ pub mod fcwrap;
pub use crate::rasterizer::RasterizedGlyph;
pub use crate::shaper::{FallbackIdx, FontMetrics, GlyphInfo};
#[derive(Debug, Error)]
#[error("Font fallback recalculated")]
pub struct ClearShapeCache {}
pub struct LoadedFont {
rasterizers: RefCell<HashMap<FallbackIdx, Box<dyn FontRasterizer>>>,
handles: RefCell<Vec<FontDataHandle>>,
@ -34,6 +40,7 @@ pub struct LoadedFont {
font_size: f64,
dpi: u32,
font_config: Weak<FontConfigInner>,
pending_fallback: Arc<Mutex<Vec<FontDataHandle>>>,
}
impl LoadedFont {
@ -47,18 +54,15 @@ impl LoadedFont {
let mut handles = self.handles.borrow_mut();
for h in extra_handles {
if !handles.iter().any(|existing| *existing == h) {
match crate::parser::ParsedFont::from_locator(&h) {
Ok(_parsed) => {
let idx = handles.len() - 1;
handles.insert(idx, h);
loaded = true;
}
Err(err) => {
log::error!("Failed to parse font from {:?}: {:?}", h, err);
}
}
let idx = handles.len() - 1;
handles.insert(idx, h);
self.rasterizers.borrow_mut().remove(&idx);
loaded = true;
}
}
if loaded {
log::trace!("revised fallback: {:?}", handles);
}
}
if loaded {
if let Some(font_config) = self.font_config.upgrade() {
@ -69,79 +73,38 @@ impl LoadedFont {
Ok(loaded)
}
pub fn shape(&self, text: &str) -> anyhow::Result<Vec<GlyphInfo>> {
pub fn shape<F: FnOnce() + Send + Sync + 'static>(
&self,
text: &str,
completion: F,
) -> anyhow::Result<Vec<GlyphInfo>> {
let mut no_glyphs = vec![];
{
let mut pending = self.pending_fallback.lock().unwrap();
if !pending.is_empty() {
match self.insert_fallback_handles(pending.split_off(0)) {
Ok(true) => return Err(ClearShapeCache {})?,
Ok(false) => {}
Err(err) => {
log::error!("Error adding fallback: {:#}", err);
}
}
}
}
let result = self
.shaper
.borrow()
.shape(text, self.font_size, self.dpi, &mut no_glyphs);
if !no_glyphs.is_empty() {
no_glyphs.sort();
no_glyphs.dedup();
if let Some(font_config) = self.font_config.upgrade() {
let mut extra_handles = vec![];
let fallback_str = no_glyphs.iter().collect::<String>();
match font_config
.font_dirs
.borrow()
.locate_fallback_for_codepoints(&no_glyphs)
{
Ok(ref mut handles) => extra_handles.append(handles),
Err(err) => log::error!(
"Error: {} while resolving fallback for {} from font_dirs",
err,
fallback_str.escape_unicode()
),
}
if extra_handles.is_empty() {
match font_config
.built_in
.borrow()
.locate_fallback_for_codepoints(&no_glyphs)
{
Ok(ref mut handles) => extra_handles.append(handles),
Err(err) => log::error!(
"Error: {} while resolving fallback for {} for built-in fonts",
err,
fallback_str.escape_unicode()
),
}
}
if extra_handles.is_empty() {
match font_config
.locator
.locate_fallback_for_codepoints(&no_glyphs)
{
Ok(ref mut handles) => extra_handles.append(handles),
Err(err) => log::error!(
"Error: {} while resolving fallback for {} from font-locator",
err,
fallback_str.escape_unicode()
),
}
}
if extra_handles.is_empty() {
font_config.advise_no_glyphs(&fallback_str);
} else {
match self.insert_fallback_handles(extra_handles) {
Ok(true) => {
log::trace!("handles is now: {:#?}", self.handles);
return self.shape(text);
}
Err(err) => {
log::error!("Failed to insert fallback handles: {:#}", err);
font_config.advise_no_glyphs(&fallback_str);
}
Ok(false) => {
font_config.advise_no_glyphs(&fallback_str);
}
}
}
font_config.schedule_fallback_resolve(
no_glyphs,
&self.pending_fallback,
completion,
);
}
}
@ -183,9 +146,9 @@ struct FontConfigInner {
dpi_scale: RefCell<f64>,
font_scale: RefCell<f64>,
config: RefCell<ConfigHandle>,
locator: Box<dyn FontLocator>,
font_dirs: RefCell<FontDatabase>,
built_in: RefCell<FontDatabase>,
locator: Arc<dyn FontLocator + Send + Sync>,
font_dirs: RefCell<Arc<FontDatabase>>,
built_in: RefCell<Arc<FontDatabase>>,
no_glyphs: RefCell<HashSet<char>>,
}
@ -206,8 +169,8 @@ impl FontConfigInner {
font_scale: RefCell::new(1.0),
dpi_scale: RefCell::new(1.0),
config: RefCell::new(config.clone()),
font_dirs: RefCell::new(FontDatabase::with_font_dirs(&config)?),
built_in: RefCell::new(FontDatabase::with_built_in()?),
font_dirs: RefCell::new(Arc::new(FontDatabase::with_font_dirs(&config)?)),
built_in: RefCell::new(Arc::new(FontDatabase::with_built_in()?)),
no_glyphs: RefCell::new(HashSet::new()),
})
}
@ -219,27 +182,66 @@ impl FontConfigInner {
fonts.clear();
self.metrics.borrow_mut().take();
self.no_glyphs.borrow_mut().clear();
*self.font_dirs.borrow_mut() = FontDatabase::with_font_dirs(config)?;
*self.font_dirs.borrow_mut() = Arc::new(FontDatabase::with_font_dirs(config)?);
Ok(())
}
fn advise_no_glyphs(&self, fallback_str: &str) {
let mut no_glyphs = self.no_glyphs.borrow_mut();
let mut notif = String::new();
for c in fallback_str.chars() {
if !no_glyphs.contains(&c) {
notif.push(c);
no_glyphs.insert(c);
fn schedule_fallback_resolve<F: FnOnce() + Send + Sync + 'static>(
&self,
mut no_glyphs: Vec<char>,
pending: &Arc<Mutex<Vec<FontDataHandle>>>,
completion: F,
) {
let mut ng = self.no_glyphs.borrow_mut();
no_glyphs.retain(|c| !ng.contains(c));
for c in &no_glyphs {
ng.insert(*c);
}
if no_glyphs.is_empty() {
return;
}
let font_dirs = Arc::clone(&*self.font_dirs.borrow());
let built_in = Arc::clone(&*self.built_in.borrow());
let locator = Arc::clone(&self.locator);
let pending = Arc::clone(pending);
std::thread::spawn(move || {
let fallback_str = no_glyphs.iter().collect::<String>();
let mut extra_handles = vec![];
match font_dirs.locate_fallback_for_codepoints(&no_glyphs) {
Ok(ref mut handles) => extra_handles.append(handles),
Err(err) => log::error!(
"Error: {} while resolving fallback for {} from font_dirs",
err,
fallback_str.escape_unicode()
),
}
}
if !notif.is_empty() {
mux::connui::show_configuration_error_message(&format!(
"Unable to resolve a font containing glyphs for {}. \
A glyph from the built-in \"last resort\" font is being \
used instead.",
notif.escape_unicode()
));
}
match built_in.locate_fallback_for_codepoints(&no_glyphs) {
Ok(ref mut handles) => extra_handles.append(handles),
Err(err) => log::error!(
"Error: {} while resolving fallback for {} for built-in fonts",
err,
fallback_str.escape_unicode()
),
}
match locator.locate_fallback_for_codepoints(&no_glyphs) {
Ok(ref mut handles) => extra_handles.append(handles),
Err(err) => log::error!(
"Error: {} while resolving fallback for {} from font-locator",
err,
fallback_str.escape_unicode()
),
}
if !extra_handles.is_empty() {
let mut pending = pending.lock().unwrap();
pending.append(&mut extra_handles);
completion();
}
});
}
/// Given a text style, load (with caching) the font that best
@ -342,6 +344,7 @@ impl FontConfigInner {
font_size,
dpi,
font_config: Rc::downgrade(myself),
pending_fallback: Arc::new(Mutex::new(vec![])),
});
fonts.insert(style.clone(), Rc::clone(&loaded));

View File

@ -3,6 +3,7 @@ use std::borrow::Cow;
use std::cmp::Ordering;
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::Arc;
pub mod core_text;
#[cfg(all(unix, not(target_os = "macos")))]
@ -122,27 +123,27 @@ pub trait FontLocator {
) -> anyhow::Result<Vec<FontDataHandle>>;
}
pub fn new_locator(locator: FontLocatorSelection) -> Box<dyn FontLocator> {
pub fn new_locator(locator: FontLocatorSelection) -> Arc<dyn FontLocator + Send + Sync> {
match locator {
FontLocatorSelection::FontConfig => {
#[cfg(all(unix, not(target_os = "macos")))]
return Box::new(font_config::FontConfigFontLocator {});
return Arc::new(font_config::FontConfigFontLocator {});
#[cfg(not(all(unix, not(target_os = "macos"))))]
panic!("fontconfig not compiled in");
}
FontLocatorSelection::CoreText => {
#[cfg(target_os = "macos")]
return Box::new(core_text::CoreTextFontLocator {});
return Arc::new(core_text::CoreTextFontLocator {});
#[cfg(not(target_os = "macos"))]
panic!("CoreText not compiled in");
}
FontLocatorSelection::Gdi => {
#[cfg(windows)]
return Box::new(gdi::GdiFontLocator {});
return Arc::new(gdi::GdiFontLocator {});
#[cfg(not(windows))]
panic!("Gdi not compiled in");
}
FontLocatorSelection::ConfigDirsOnly => Box::new(NopSystemSource {}),
FontLocatorSelection::ConfigDirsOnly => Arc::new(NopSystemSource {}),
}
}

View File

@ -9,6 +9,7 @@ use ::window::glium::uniforms::{
MagnifySamplerFilter, MinifySamplerFilter, Sampler, SamplerWrapFunction,
};
use ::window::glium::{uniform, BlendingFunction, LinearBlendingFactor, Surface};
use ::window::WindowOps;
use anyhow::anyhow;
use config::ConfigHandle;
use config::TextStyle;
@ -22,7 +23,7 @@ use std::time::Instant;
use termwiz::cellcluster::CellCluster;
use termwiz::surface::{CursorShape, CursorVisibility};
use wezterm_font::units::PixelLength;
use wezterm_font::GlyphInfo;
use wezterm_font::{ClearShapeCache, GlyphInfo};
use wezterm_term::color::{ColorAttribute, ColorPalette, RgbColor};
use wezterm_term::{CellAttributes, Line, StableRowIndex};
use window::bitmaps::atlas::SpriteSlice;
@ -97,7 +98,7 @@ impl super::TermWindow {
if let Some(&OutOfTextureSpace {
size: Some(size),
current_size,
}) = err.downcast_ref::<OutOfTextureSpace>()
}) = err.root_cause().downcast_ref::<OutOfTextureSpace>()
{
let result = if pass == 0 {
// Let's try clearing out the atlas and trying again
@ -117,6 +118,8 @@ impl super::TermWindow {
);
break;
}
} else if err.root_cause().downcast_ref::<ClearShapeCache>().is_some() {
self.shape_cache.borrow_mut().clear();
} else {
log::error!("paint_opengl_pass failed: {:#}", err);
break;
@ -504,7 +507,8 @@ impl super::TermWindow {
Some(Err(err)) => return Err(err),
None => {
let font = self.fonts.resolve_font(style)?;
match font.shape(text) {
let window = self.window.as_ref().unwrap().clone();
match font.shape(text, || Self::invalidate_post_font_resolve(window)) {
Ok(info) => {
let line = Line::from_text(&text, &CellAttributes::default());
let clusters = line.cluster();
@ -527,6 +531,10 @@ impl super::TermWindow {
self.lookup_cached_shape(&key).unwrap().unwrap()
}
Err(err) => {
if err.root_cause().downcast_ref::<ClearShapeCache>().is_some() {
return Err(err);
}
let res = anyhow!("shaper error: {}", err);
self.shape_cache.borrow_mut().put(key.to_owned(), Err(err));
return Err(res);
@ -615,6 +623,21 @@ impl super::TermWindow {
Ok(())
}
fn invalidate_post_font_resolve(window: ::window::Window) {
promise::spawn::spawn_into_main_thread(async move {
window
.apply(move |tw, _| {
if let Some(tw) = tw.downcast_mut::<Self>() {
tw.shape_cache.borrow_mut().clear();
tw.window.as_ref().unwrap().invalidate();
}
Ok(())
})
.await
})
.detach();
}
/// "Render" a line of the terminal screen into the vertex buffer.
/// This is nominally a matter of setting the fg/bg color and the
/// texture coordinates for a given glyph. There's a little bit
@ -760,7 +783,10 @@ impl super::TermWindow {
Some(Err(err)) => return Err(err),
None => {
let font = self.fonts.resolve_font(style)?;
match font.shape(&cluster.text) {
let window = self.window.as_ref().unwrap().clone();
match font
.shape(&cluster.text, || Self::invalidate_post_font_resolve(window))
{
Ok(info) => {
let glyphs = self.glyph_infos_to_glyphs(
cluster,
@ -782,6 +808,10 @@ impl super::TermWindow {
self.lookup_cached_shape(&key).unwrap().unwrap()
}
Err(err) => {
if err.root_cause().downcast_ref::<ClearShapeCache>().is_some() {
return Err(err);
}
let res = anyhow!("shaper error: {}", err);
self.shape_cache.borrow_mut().put(key.to_owned(), Err(err));
return Err(res);