Cache loaded fonts and glyph outlines.

Approximately a 70% CPU time improvement on the NanoVG demo.
This commit is contained in:
Patrick Walton 2020-04-16 16:27:28 -07:00
parent 582f025c91
commit 0b43f629cd
6 changed files with 216 additions and 139 deletions

1
Cargo.lock generated
View File

@ -285,6 +285,7 @@ dependencies = [
"gl 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)",
"image 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)",
"jemallocator 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"pathfinder_canvas 0.1.0",
"pathfinder_color 0.1.0",
"pathfinder_content 0.1.0",

View File

@ -94,16 +94,18 @@ impl Canvas {
self.scene
}
pub fn get_context_2d(self, font_context: CanvasFontContext) -> CanvasRenderingContext2D {
pub fn get_context_2d(self, canvas_font_context: CanvasFontContext)
-> CanvasRenderingContext2D {
#[cfg(feature = "pf-text")]
let default_font_collection = font_context.default_font_collection.clone();
let default_font_collection =
canvas_font_context.0.borrow().default_font_collection.clone();
#[cfg(not(feature = "pf-text"))]
let default_font_collection = Arc::new(FontCollection);
CanvasRenderingContext2D {
canvas: self,
current_state: State::default(default_font_collection),
saved_states: vec![],
font_context,
canvas_font_context,
}
}
@ -118,7 +120,7 @@ pub struct CanvasRenderingContext2D {
current_state: State,
saved_states: Vec<State>,
#[allow(dead_code)]
font_context: CanvasFontContext,
canvas_font_context: CanvasFontContext,
}
impl CanvasRenderingContext2D {
@ -134,11 +136,6 @@ impl CanvasRenderingContext2D {
self.canvas
}
#[inline]
pub fn font_context(&self) -> CanvasFontContext {
self.font_context.clone()
}
// Drawing rectangles
#[inline]

View File

@ -21,8 +21,10 @@ use pathfinder_geometry::transform2d::Transform2F;
use pathfinder_geometry::util;
use pathfinder_geometry::vector::{Vector2F, vec2f};
use pathfinder_renderer::paint::PaintId;
use pathfinder_text::{SceneExt, TextRenderMode};
use pathfinder_text::{FontContext, FontRenderOptions, TextRenderMode};
use skribo::{FontCollection, FontFamily, FontRef, Layout, TextStyle};
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
impl CanvasRenderingContext2D {
@ -51,14 +53,22 @@ impl CanvasRenderingContext2D {
let clip_path = self.current_state.clip_path;
let blend_mode = self.current_state.global_composite_operation.to_blend_mode();
drop(self.canvas.scene.push_layout(&layout,
&TextStyle { size: self.current_state.font_size },
&(transform * self.current_state.transform),
TextRenderMode::Fill,
HintingOptions::None,
clip_path,
blend_mode,
paint_id));
// TODO(pcwalton): Report errors.
drop(self.canvas_font_context
.0
.borrow_mut()
.font_context
.push_layout(&mut self.canvas.scene,
&layout,
&TextStyle { size: self.current_state.font_size },
&FontRenderOptions {
transform: transform * self.current_state.transform,
render_mode: TextRenderMode::Fill,
hinting_options: HintingOptions::None,
clip_path,
blend_mode,
paint_id,
}));
}
fn fill_or_stroke_text(&mut self,
@ -75,14 +85,21 @@ impl CanvasRenderingContext2D {
let transform = self.current_state.transform * Transform2F::from_translation(position);
// TODO(pcwalton): Report errors.
drop(self.canvas.scene.push_layout(&layout,
&TextStyle { size: self.current_state.font_size },
&transform,
render_mode,
HintingOptions::None,
clip_path,
blend_mode,
paint_id));
drop(self.canvas_font_context
.0
.borrow_mut()
.font_context
.push_layout(&mut self.canvas.scene,
&layout,
&TextStyle { size: self.current_state.font_size },
&FontRenderOptions {
transform,
render_mode,
hinting_options: HintingOptions::None,
clip_path,
blend_mode,
paint_id,
}));
}
fn layout_text(&self, string: &str) -> Layout {
@ -100,7 +117,7 @@ impl CanvasRenderingContext2D {
#[inline]
pub fn set_font<FC>(&mut self, font_collection: FC) where FC: IntoFontCollection {
let font_collection = font_collection.into_font_collection(&self.font_context);
let font_collection = font_collection.into_font_collection(&self.canvas_font_context);
self.current_state.font_collection = font_collection;
}
@ -179,7 +196,10 @@ pub struct TextMetrics {
#[cfg(feature = "pf-text")]
#[derive(Clone)]
pub struct CanvasFontContext {
pub struct CanvasFontContext(pub(crate) Rc<RefCell<CanvasFontContextData>>);
pub(super) struct CanvasFontContextData {
pub(super) font_context: FontContext<Font>,
#[allow(dead_code)]
pub(super) font_source: Arc<dyn Source>,
#[allow(dead_code)]
@ -196,10 +216,11 @@ impl CanvasFontContext {
}
}
CanvasFontContext {
CanvasFontContext(Rc::new(RefCell::new(CanvasFontContextData {
font_source,
default_font_collection: Arc::new(default_font_collection),
}
font_context: FontContext::new(),
})))
}
/// A convenience method to create a font context with the system source.
@ -212,6 +233,18 @@ impl CanvasFontContext {
pub fn from_fonts<I>(fonts: I) -> CanvasFontContext where I: Iterator<Item = Handle> {
CanvasFontContext::new(Arc::new(MemSource::from_fonts(fonts).unwrap()))
}
fn get_font_by_postscript_name(&self, postscript_name: &str) -> Font {
let this = self.0.borrow();
if let Some(cached_font) = this.font_context.get_cached_font(postscript_name) {
return (*cached_font).clone();
}
this.font_source
.select_by_postscript_name(postscript_name)
.expect("Couldn't find a font with that PostScript name!")
.load()
.expect("Failed to load the font!")
}
}
// Text layout utilities
@ -413,6 +446,7 @@ impl IntoFontCollection for Vec<FontFamily> {
}
}
/*
impl IntoFontCollection for Handle {
#[inline]
fn into_font_collection(self, context: &CanvasFontContext) -> Arc<FontCollection> {
@ -422,15 +456,18 @@ impl IntoFontCollection for Handle {
impl<'a> IntoFontCollection for &'a [Handle] {
#[inline]
fn into_font_collection(self, _: &CanvasFontContext) -> Arc<FontCollection> {
fn into_font_collection(self, context: &CanvasFontContext) -> Arc<FontCollection> {
let mut font_collection = FontCollection::new();
for handle in self {
let postscript_name = handle.postscript_name();
let font = handle.load().expect("Failed to load the font!");
font_collection.add_family(FontFamily::new_from_font(font));
}
Arc::new(font_collection)
}
}
*/
impl IntoFontCollection for Font {
#[inline]
@ -453,10 +490,7 @@ impl<'a> IntoFontCollection for &'a [Font] {
impl<'a> IntoFontCollection for &'a str {
#[inline]
fn into_font_collection(self, context: &CanvasFontContext) -> Arc<FontCollection> {
context.font_source
.select_by_postscript_name(self)
.expect("Couldn't find a font with that PostScript name!")
.into_font_collection(context)
context.get_font_by_postscript_name(self).into_font_collection(context)
}
}
@ -465,11 +499,7 @@ impl<'a, 'b> IntoFontCollection for &'a [&'b str] {
fn into_font_collection(self, context: &CanvasFontContext) -> Arc<FontCollection> {
let mut font_collection = FontCollection::new();
for postscript_name in self {
let font = context.font_source
.select_by_postscript_name(postscript_name)
.expect("Failed to find a font with that PostScript name!")
.load()
.expect("Failed to load the font!");
let font = context.get_font_by_postscript_name(postscript_name);
font_collection.add_family(FontFamily::new_from_font(font));
}
Arc::new(font_collection)

View File

@ -16,6 +16,10 @@ version = "0.23"
default-features = false
features = ["png"]
[dependencies.log]
version = "0.4"
features = ["release_max_level_info"]
[dependencies.pathfinder_canvas]
path = "../../canvas"
features = ["pf-text"]

View File

@ -1544,7 +1544,8 @@ fn main() {
gpu_graph.render(&mut context, vec2f(415.0, 5.0));
// Render the canvas to screen.
let scene = SceneProxy::from_scene(context.into_canvas().into_scene(), RayonExecutor);
let canvas = context.into_canvas();
let scene = SceneProxy::from_scene(canvas.into_scene(), RayonExecutor);
scene.build_and_render(&mut renderer, BuildOptions::default());
window.gl_swap_window();

View File

@ -11,138 +11,182 @@
use font_kit::error::GlyphLoadingError;
use font_kit::hinting::HintingOptions;
use font_kit::loader::Loader;
use font_kit::loaders::default::Font as DefaultLoader;
use font_kit::outline::OutlineSink;
use pathfinder_content::effects::BlendMode;
use pathfinder_content::outline::{Contour, Outline};
use pathfinder_content::stroke::{OutlineStrokeToFill, StrokeStyle};
use pathfinder_geometry::line_segment::LineSegment2F;
use pathfinder_geometry::transform2d::Transform2F;
use pathfinder_geometry::vector::Vector2F;
use pathfinder_geometry::vector::{Vector2F, vec2f};
use pathfinder_renderer::paint::PaintId;
use pathfinder_renderer::scene::{ClipPathId, DrawPath, Scene};
use skribo::{FontCollection, Layout, TextStyle};
use std::collections::HashMap;
use std::mem;
// FIXME(pcwalton): Too many parameters!
pub trait SceneExt {
// TODO(pcwalton): Support stroked glyphs.
fn push_glyph<F>(&mut self,
font: &F,
glyph_id: u32,
transform: &Transform2F,
render_mode: TextRenderMode,
hinting_options: HintingOptions,
clip_path: Option<ClipPathId>,
blend_mode: BlendMode,
paint_id: PaintId)
-> Result<(), GlyphLoadingError>
where F: Loader;
fn push_layout(&mut self,
layout: &Layout,
style: &TextStyle,
transform: &Transform2F,
render_mode: TextRenderMode,
hinting_options: HintingOptions,
clip_path: Option<ClipPathId>,
blend_mode: BlendMode,
paint_id: PaintId)
-> Result<(), GlyphLoadingError>;
fn push_text(&mut self,
text: &str,
style: &TextStyle,
collection: &FontCollection,
transform: &Transform2F,
render_mode: TextRenderMode,
hinting_options: HintingOptions,
clip_path: Option<ClipPathId>,
blend_mode: BlendMode,
paint_id: PaintId)
-> Result<(), GlyphLoadingError>;
#[derive(Clone)]
pub struct FontContext<F> where F: Loader {
font_info: HashMap<String, FontInfo<F>>,
}
impl SceneExt for Scene {
#[inline]
fn push_glyph<F>(&mut self,
font: &F,
glyph_id: u32,
transform: &Transform2F,
render_mode: TextRenderMode,
hinting_options: HintingOptions,
clip_path: Option<ClipPathId>,
blend_mode: BlendMode,
paint_id: PaintId)
-> Result<(), GlyphLoadingError>
where F: Loader {
let mut outline_builder = OutlinePathBuilder::new(transform);
font.outline(glyph_id, hinting_options, &mut outline_builder)?;
let mut outline = outline_builder.build();
#[derive(Clone)]
struct FontInfo<F> where F: Loader {
font: F,
outline_cache: HashMap<GlyphId, Outline>,
}
if let TextRenderMode::Stroke(stroke_style) = render_mode {
#[derive(Clone, Copy)]
pub struct FontRenderOptions {
pub transform: Transform2F,
pub render_mode: TextRenderMode,
pub hinting_options: HintingOptions,
pub clip_path: Option<ClipPathId>,
pub blend_mode: BlendMode,
pub paint_id: PaintId,
}
impl Default for FontRenderOptions {
#[inline]
fn default() -> FontRenderOptions {
FontRenderOptions {
transform: Transform2F::default(),
render_mode: TextRenderMode::Fill,
hinting_options: HintingOptions::None,
clip_path: None,
blend_mode: BlendMode::SrcOver,
paint_id: PaintId(0),
}
}
}
#[derive(Clone, Copy, PartialEq, Debug, Eq, Hash)]
pub struct GlyphId(pub u32);
impl<F> FontContext<F> where F: Loader {
#[inline]
pub fn new() -> FontContext<F> {
FontContext { font_info: HashMap::new() }
}
pub fn push_glyph(&mut self,
scene: &mut Scene,
font: &F,
glyph_id: GlyphId,
render_options: &FontRenderOptions)
-> Result<(), GlyphLoadingError> {
let font_key = font.postscript_name();
let metrics = font.metrics();
// Insert the font into the cache if needed.
if let Some(ref font_key) = font_key {
if !self.font_info.contains_key(&*font_key) {
self.font_info.insert((*font_key).clone(), FontInfo::new((*font).clone()));
}
}
// See if we have a cached outline.
//
// TODO(pcwalton): Cache hinted outlines too.
let mut cached_outline = None;
let can_cache_outline = font_key.is_some() &&
render_options.hinting_options == HintingOptions::None;
if can_cache_outline {
if let Some(ref font_info) = self.font_info.get(&*font_key.as_ref().unwrap()) {
if let Some(ref outline) = font_info.outline_cache.get(&glyph_id) {
cached_outline = Some((*outline).clone());
}
}
}
let mut outline = match cached_outline {
Some(mut cached_outline) => {
let scale = 1.0 / metrics.units_per_em as f32;
cached_outline.transform(&(render_options.transform *
Transform2F::from_scale(scale)));
cached_outline
}
None => {
let transform = if can_cache_outline {
Transform2F::from_scale(metrics.units_per_em as f32)
} else {
render_options.transform
};
let mut outline_builder = OutlinePathBuilder::new(&transform);
font.outline(glyph_id.0, render_options.hinting_options, &mut outline_builder)?;
let mut outline = outline_builder.build();
if can_cache_outline {
let font_key = font_key.as_ref().unwrap();
let font_info = self.font_info.get_mut(&*font_key).unwrap();
font_info.outline_cache.insert(glyph_id, outline.clone());
let scale = 1.0 / metrics.units_per_em as f32;
outline.transform(&(render_options.transform *
Transform2F::from_scale(scale)));
}
outline
}
};
if let TextRenderMode::Stroke(stroke_style) = render_options.render_mode {
let mut stroke_to_fill = OutlineStrokeToFill::new(&outline, stroke_style);
stroke_to_fill.offset();
outline = stroke_to_fill.into_outline();
}
let mut path = DrawPath::new(outline, paint_id);
path.set_clip_path(clip_path);
path.set_blend_mode(blend_mode);
let mut path = DrawPath::new(outline, render_options.paint_id);
path.set_clip_path(render_options.clip_path);
path.set_blend_mode(render_options.blend_mode);
self.push_path(path);
scene.push_path(path);
Ok(())
}
fn push_layout(&mut self,
layout: &Layout,
style: &TextStyle,
transform: &Transform2F,
render_mode: TextRenderMode,
hinting_options: HintingOptions,
clip_path: Option<ClipPathId>,
blend_mode: BlendMode,
paint_id: PaintId)
-> Result<(), GlyphLoadingError> {
/// Attempts to look up a font in the font cache.
#[inline]
pub fn get_cached_font(&self, postscript_name: &str) -> Option<&F> {
self.font_info.get(postscript_name).map(|font_info| &font_info.font)
}
}
impl FontContext<DefaultLoader> {
pub fn push_layout(&mut self,
scene: &mut Scene,
layout: &Layout,
style: &TextStyle,
render_options: &FontRenderOptions)
-> Result<(), GlyphLoadingError> {
for glyph in &layout.glyphs {
let offset = glyph.offset;
let font = &*glyph.font.font;
// FIXME(pcwalton): Cache this!
let scale = style.size / (font.metrics().units_per_em as f32);
let scale = Vector2F::new(scale, -scale);
let transform = *transform * Transform2F::from_scale(scale).translate(offset);
self.push_glyph(font,
glyph.glyph_id,
&transform,
render_mode,
hinting_options,
clip_path,
blend_mode,
paint_id)?;
let scale = vec2f(scale, -scale);
let render_options = FontRenderOptions {
transform: render_options.transform *
Transform2F::from_scale(scale).translate(offset),
..*render_options
};
self.push_glyph(scene, font, GlyphId(glyph.glyph_id), &render_options)?;
}
Ok(())
}
#[inline]
fn push_text(&mut self,
text: &str,
style: &TextStyle,
collection: &FontCollection,
transform: &Transform2F,
render_mode: TextRenderMode,
hinting_options: HintingOptions,
clip_path: Option<ClipPathId>,
blend_mode: BlendMode,
paint_id: PaintId)
-> Result<(), GlyphLoadingError> {
pub fn push_text(&mut self,
scene: &mut Scene,
text: &str,
style: &TextStyle,
collection: &FontCollection,
render_options: &FontRenderOptions)
-> Result<(), GlyphLoadingError> {
let layout = skribo::layout(style, collection, text);
self.push_layout(&layout,
style,
&transform,
render_mode,
hinting_options,
clip_path,
blend_mode,
paint_id)
self.push_layout(scene, &layout, style, render_options)
}
}
impl<F> FontInfo<F> where F: Loader {
fn new(font: F) -> FontInfo<F> {
FontInfo { font, outline_cache: HashMap::new() }
}
}