diff options
Diffstat (limited to 'gfx/wr/wr_glyph_rasterizer')
-rw-r--r-- | gfx/wr/wr_glyph_rasterizer/Cargo.toml | 51 | ||||
-rw-r--r-- | gfx/wr/wr_glyph_rasterizer/README.md | 3 | ||||
-rw-r--r-- | gfx/wr/wr_glyph_rasterizer/examples/basic.rs | 177 | ||||
-rw-r--r-- | gfx/wr/wr_glyph_rasterizer/examples/common/boilerplate.rs | 298 | ||||
-rw-r--r-- | gfx/wr/wr_glyph_rasterizer/src/gamma_lut.rs | 413 | ||||
-rw-r--r-- | gfx/wr/wr_glyph_rasterizer/src/lib.rs | 60 | ||||
-rw-r--r-- | gfx/wr/wr_glyph_rasterizer/src/platform/macos/font.rs | 899 | ||||
-rw-r--r-- | gfx/wr/wr_glyph_rasterizer/src/platform/unix/font.rs | 1099 | ||||
-rw-r--r-- | gfx/wr/wr_glyph_rasterizer/src/platform/windows/font.rs | 639 | ||||
-rw-r--r-- | gfx/wr/wr_glyph_rasterizer/src/profiler.rs | 12 | ||||
-rw-r--r-- | gfx/wr/wr_glyph_rasterizer/src/rasterizer.rs | 1879 | ||||
-rw-r--r-- | gfx/wr/wr_glyph_rasterizer/src/telemetry.rs | 28 | ||||
-rw-r--r-- | gfx/wr/wr_glyph_rasterizer/src/types.rs | 10 |
13 files changed, 5568 insertions, 0 deletions
diff --git a/gfx/wr/wr_glyph_rasterizer/Cargo.toml b/gfx/wr/wr_glyph_rasterizer/Cargo.toml new file mode 100644 index 0000000000..de53902ec5 --- /dev/null +++ b/gfx/wr/wr_glyph_rasterizer/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "wr_glyph_rasterizer" +version = "0.1.0" +authors = ["The Mozilla Project Developers"] +description = "A glyph rasterizer for WebRender" +license = "MPL-2.0" +edition = "2018" + +[features] +default = ["static_freetype"] +dynamic_freetype = [] +static_freetype = ["freetype/freetype-sys"] +capture = ["api/serialize", "serde", "smallvec/serde"] +replay = ["api/deserialize", "serde", "smallvec/serde"] +gecko = ["firefox-on-glean", "glean"] + +[dependencies] +api = { version = "0.62.0", path = "../webrender_api", package = "webrender_api" } +euclid = { version = "0.22.0", features = ["serde"] } +malloc_size_of = { version = "0.0.2", path = "../wr_malloc_size_of", package = "wr_malloc_size_of" } +malloc_size_of_derive = "0.1" +rayon = "1" +smallvec = "1" +tracy-rs = "0.1.2" +log = "0.4" +lazy_static = "1" +fxhash = "0.2.1" +glean = { version = "52.7.0", optional = true } +firefox-on-glean = { version = "0.1.0", optional = true } +serde = { optional = true, version = "1.0", features = ["serde_derive"] } + +[dev-dependencies] +env_logger = { version = "0.10", default_features = false } +euclid = "0.22" +gleam = "0.15" +glutin = "0.28" +rayon = "1" +winit = "0.26" + +[target.'cfg(any(target_os = "android", all(unix, not(target_os = "macos"))))'.dependencies] +freetype = { version = "0.7", default-features = false } +libc = "0.2" + +[target.'cfg(target_os = "windows")'.dependencies] +dwrote = "0.11" + +[target.'cfg(target_os = "macos")'.dependencies] +core-foundation = "0.9.2" +core-graphics = "0.22.3" +core-text = { version = "19.2", default-features = false } +objc = "0.2" diff --git a/gfx/wr/wr_glyph_rasterizer/README.md b/gfx/wr/wr_glyph_rasterizer/README.md new file mode 100644 index 0000000000..0c23554e44 --- /dev/null +++ b/gfx/wr/wr_glyph_rasterizer/README.md @@ -0,0 +1,3 @@ +# A glyph rasterizer for webrender + +## Benchmark diff --git a/gfx/wr/wr_glyph_rasterizer/examples/basic.rs b/gfx/wr/wr_glyph_rasterizer/examples/basic.rs new file mode 100644 index 0000000000..4c46004cdf --- /dev/null +++ b/gfx/wr/wr_glyph_rasterizer/examples/basic.rs @@ -0,0 +1,177 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::sync::Arc; +use std::mem; + +use api::{ + IdNamespace, FontTemplate, FontKey, FontInstanceKey, FontInstanceOptions, + FontInstancePlatformOptions, ColorF, FontInstanceFlags, units::DevicePoint, +}; +use glutin::ContextBuilder; +use glutin::dpi::PhysicalSize; +use glutin::event::{Event, WindowEvent}; +use glutin::event_loop::{ControlFlow, EventLoop}; +use glutin::window::WindowBuilder; +use rayon::ThreadPoolBuilder; +use wr_glyph_rasterizer::RasterizedGlyph; +use wr_glyph_rasterizer::{ + SharedFontResources, BaseFontInstance, GlyphRasterizer, FontInstance, GlyphKey, + SubpixelDirection, profiler::GlyphRasterizeProfiler, +}; + +#[path = "common/boilerplate.rs"] +mod boilerplate; + +struct Profiler; + +impl GlyphRasterizeProfiler for Profiler { + fn start_time(&mut self) {} + fn end_time(&mut self) -> f64 { + 0. + } + fn set(&mut self, _value: f64) {} +} + +fn load_glyphs() -> Vec<RasterizedGlyph> { + let namespace = IdNamespace(0); + let mut fonts = SharedFontResources::new(namespace); + + let font_key = FontKey::new(namespace, 0); + let raw_font_data = include_bytes!("../../wrench/reftests/text/FreeSans.ttf"); + let font_template = FontTemplate::Raw(Arc::new(raw_font_data.to_vec()), 0); + let shared_font_key = fonts + .font_keys + .add_key(&font_key, &font_template) + .expect("Failed to add font key"); + + let font_instance_key = FontInstanceKey::new(namespace, 1); + fonts.templates.add_font(shared_font_key, font_template); + assert!(fonts.templates.has_font(&shared_font_key)); + + // AddFontInstance will only be processed here, not in the resource cache, so it + // is safe to take the options rather than clone them. + let base = BaseFontInstance::new( + font_instance_key, + shared_font_key, + 32., + mem::take(&mut Some(FontInstanceOptions::default())), + mem::take(&mut Some(FontInstancePlatformOptions::default())), + mem::take(&mut Vec::new()), + ); + let shared_instance = fonts + .instance_keys + .add_key(base) + .expect("Failed to add font instance key"); + fonts.instances.add_font_instance(shared_instance); + + let workers = { + let worker = ThreadPoolBuilder::new() + .thread_name(|idx| format!("WRWorker#{}", idx)) + .build(); + Arc::new(worker.unwrap()) + }; + let mut glyph_rasterizer = GlyphRasterizer::new(workers, false); + + glyph_rasterizer.add_font( + shared_font_key, + fonts + .templates + .get_font(&shared_font_key) + .expect("Could not find FontTemplate"), + ); + + let mut font = FontInstance::new( + fonts + .instances + .get_font_instance(font_instance_key) + .expect("Could not found BaseFontInstance"), + ColorF::BLACK.into(), + api::FontRenderMode::Alpha, + FontInstanceFlags::SUBPIXEL_POSITION | FontInstanceFlags::FONT_SMOOTHING, + ); + + glyph_rasterizer.prepare_font(&mut font); + + let glyph_keys = [ + glyph_rasterizer + .get_glyph_index(shared_font_key, 'A') + .expect("Failed to get glyph index"), + glyph_rasterizer + .get_glyph_index(shared_font_key, 'B') + .expect("Failed to get glyph index"), + glyph_rasterizer + .get_glyph_index(shared_font_key, 'C') + .expect("Failed to get glyph index"), + ]; + + let glyph_keys = glyph_keys.map(|g| { + GlyphKey::new( + g, + DevicePoint::new(100., 100.), + SubpixelDirection::Horizontal, + ) + }); + + glyph_rasterizer.request_glyphs(font, &glyph_keys, |_| true); + + let mut glyphs = vec![]; + glyph_rasterizer.resolve_glyphs( + |job, _| { + if let Ok(glyph) = job.result { + glyphs.push(glyph); + } + }, + &mut Profiler, + ); + + glyphs +} + +fn main() { + let glyphs = load_glyphs(); + + let el = EventLoop::new(); + let wb = WindowBuilder::new() + .with_title("A fantastic window!") + .with_inner_size(PhysicalSize::new(1900. as f64, 1300. as f64)); + + let windowed_context = ContextBuilder::new() + .with_gl(glutin::GlRequest::GlThenGles { + opengl_version: (3, 2), + opengles_version: (3, 0), + }) + .build_windowed(wb, &el) + .unwrap(); + + let windowed_context = unsafe { windowed_context.make_current().unwrap() }; + + let gl = boilerplate::load(windowed_context.context(), glyphs); + + el.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::LoopDestroyed => (), + Event::WindowEvent { event, .. } => match event { + WindowEvent::Resized(physical_size) => windowed_context.resize(physical_size), + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + _ => (), + }, + Event::RedrawRequested(_) => { + let size = windowed_context.window().inner_size(); + let scale_factor = windowed_context.window().scale_factor(); + gl.draw_frame( + size.width as f32, + size.height as f32, + [0., 0., 0., 1.0], + [1., 1., 1., 1.0], + scale_factor as f32, + ); + windowed_context.swap_buffers().unwrap(); + } + _ => (), + } + }); +} diff --git a/gfx/wr/wr_glyph_rasterizer/examples/common/boilerplate.rs b/gfx/wr/wr_glyph_rasterizer/examples/common/boilerplate.rs new file mode 100644 index 0000000000..9590f92b4c --- /dev/null +++ b/gfx/wr/wr_glyph_rasterizer/examples/common/boilerplate.rs @@ -0,0 +1,298 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use api::ImageFormat; +use euclid::{Transform3D, UnknownUnit}; +use glutin::{self, PossiblyCurrent}; +use gleam::gl; +use wr_glyph_rasterizer::{RasterizedGlyph, GlyphFormat}; + +use std::{ffi::CStr, rc::Rc}; + +#[allow(unused)] +pub struct Gl { + pub gl: Rc<dyn gl::Gl>, + program: gl::GLuint, + vb: gl::GLuint, + vao: gl::GLuint, + vs: gl::GLuint, + fs: gl::GLuint, + textures: Vec<gl::GLuint>, + u_transform: i32, + u_text_color: i32, + u_sampler_color: i32, + glyphs: Vec<RasterizedGlyph>, +} + +pub fn load(gl_context: &glutin::Context<PossiblyCurrent>, glyphs: Vec<RasterizedGlyph>) -> Gl { + env_logger::init(); + + #[cfg(target_os = "macos")] + { + use core_foundation::{self as cf, base::TCFType}; + let i = cf::bundle::CFBundle::main_bundle().info_dictionary(); + let mut i = unsafe { i.to_mutable() }; + i.set( + cf::string::CFString::new("NSSupportsAutomaticGraphicsSwitching"), + cf::boolean::CFBoolean::true_value().into_CFType(), + ); + } + + let gl = match gl_context.get_api() { + glutin::Api::OpenGl => unsafe { + gl::GlFns::load_with(|symbol| gl_context.get_proc_address(symbol) as *const _) + }, + glutin::Api::OpenGlEs => unsafe { + gl::GlesFns::load_with(|symbol| gl_context.get_proc_address(symbol) as *const _) + }, + glutin::Api::WebGl => unimplemented!(), + }; + + let version = unsafe { + let data = CStr::from_ptr(gl.get_string(gl::VERSION).as_ptr() as *const _) + .to_bytes() + .to_vec(); + String::from_utf8(data).unwrap() + }; + + println!("OpenGL version {}", version); + + let vs = gl.create_shader(gl::VERTEX_SHADER); + gl.shader_source(vs, &[VS_SRC]); + gl.compile_shader(vs); + + let fs = gl.create_shader(gl::FRAGMENT_SHADER); + gl.shader_source(fs, &[FS_SRC]); + gl.compile_shader(fs); + + let program = gl.create_program(); + gl.attach_shader(program, vs); + gl.attach_shader(program, fs); + gl.link_program(program); + gl.use_program(program); + + let vb = gl.gen_buffers(1)[0]; + gl.bind_buffer(gl::ARRAY_BUFFER, vb); + gl.buffer_data_untyped( + gl::ARRAY_BUFFER, + (6 * 4 * std::mem::size_of::<f32>()) as gl::types::GLsizeiptr, + std::ptr::null(), + gl::DYNAMIC_DRAW, + ); + + let vao = gl.gen_vertex_arrays(1)[0]; + gl.bind_vertex_array(vao); + + let u_transform = gl.get_uniform_location(program, "uTransform"); + let u_text_color = gl.get_uniform_location(program, "uTextColor"); + let u_sampler_color = gl.get_uniform_location(program, "uSamplerColor"); + let i_position = gl.get_attrib_location(program, "iPosition"); + let i_tex_coords = gl.get_attrib_location(program, "iTexCoords"); + gl.vertex_attrib_pointer( + i_position as gl::types::GLuint, + 2, + gl::FLOAT, + false, + 4 * std::mem::size_of::<f32>() as gl::types::GLsizei, + 0, + ); + gl.vertex_attrib_pointer( + i_tex_coords as gl::types::GLuint, + 2, + gl::FLOAT, + false, + 4 * std::mem::size_of::<f32>() as gl::types::GLsizei, + (2 * std::mem::size_of::<f32>()) as gl::types::GLuint, + ); + gl.enable_vertex_attrib_array(i_position as gl::types::GLuint); + gl.enable_vertex_attrib_array(i_tex_coords as gl::types::GLuint); + + let textures = create_texture(&gl, &glyphs); + + gl.bind_buffer(gl::ARRAY_BUFFER, 0); + gl.bind_vertex_array(0); + + unsafe { log_shader(&gl, vs) }; + unsafe { log_shader(&gl, fs) }; + + Gl { + gl, + program, + vb, + vao, + u_transform, + u_text_color, + u_sampler_color, + glyphs, + textures, + vs, + fs, + } +} + +fn create_texture(gl: &Rc<dyn gl::Gl>, glyphs: &[RasterizedGlyph]) -> Vec<gl::GLuint> { + let textures = gl.gen_textures(glyphs.len() as gl::types::GLsizei); + for (i, glyph) in glyphs.iter().enumerate() { + let (internal_format, external_format) = get_texture_format(&glyph.format); + let texture = textures[i]; + gl.bind_texture(gl::TEXTURE_2D, texture); + gl.tex_parameter_i( + gl::TEXTURE_2D, + gl::TEXTURE_MAG_FILTER, + gl::LINEAR as gl::GLint, + ); + gl.tex_parameter_i( + gl::TEXTURE_2D, + gl::TEXTURE_MIN_FILTER, + gl::LINEAR as gl::GLint, + ); + gl.tex_parameter_i( + gl::TEXTURE_2D, + gl::TEXTURE_WRAP_S, + gl::CLAMP_TO_EDGE as gl::GLint, + ); + gl.tex_parameter_i( + gl::TEXTURE_2D, + gl::TEXTURE_WRAP_T, + gl::CLAMP_TO_EDGE as gl::GLint, + ); + // TODO: use tex_storage_2d + gl.tex_image_2d( + gl::TEXTURE_2D, + 0, + internal_format as gl::GLint, + glyph.width, + glyph.height, + 0, + external_format, + gl::UNSIGNED_BYTE, + Some(&glyph.bytes), + ); + + gl.pixel_store_i(gl::UNPACK_ALIGNMENT, 1); + gl.enable(gl::BLEND); + gl.blend_func(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA); + } + + textures +} + +fn get_texture_format(format: &GlyphFormat) -> (gl::GLuint, gl::GLuint) { + match format.image_format(false) { + ImageFormat::BGRA8 => (gl::RGBA, gl::BGRA), + _ => unimplemented!(), + } +} + +unsafe fn log_shader(gl: &Rc<dyn gl::Gl>, shader: gl::GLuint) { + let log = gl.get_shader_info_log(shader); + if log.len() != 0 { + println!("[ERROR] {}", log); + } +} + +impl Gl { + pub fn draw_frame( + &self, + width: f32, + height: f32, + text_color: [f32; 4], + background_color: [f32; 4], + scale_factor: f32, + ) { + let projection: Transform3D<f32, UnknownUnit, UnknownUnit> = + Transform3D::ortho(0., width, height, 0., -1., 1.); + self.gl + .uniform_matrix_4fv(self.u_transform, false, &projection.to_array()); + self.gl.uniform_4fv(self.u_text_color, &text_color); + + self.gl.active_texture(gl::TEXTURE0); + + self.gl.bind_vertex_array(self.vao); + + self.gl.clear_color( + background_color[0], + background_color[1], + background_color[2], + background_color[3], + ); + self.gl.clear(gl::COLOR_BUFFER_BIT); + + let mut ax = 0.; + for (i, glyph) in self.glyphs.iter().enumerate() { + let texture = self.textures[i]; + + let x = ax + glyph.left; + let y = glyph.top; + + let w = (glyph.width as f32) * scale_factor; + let h = (glyph.height as f32) * scale_factor; + + #[rustfmt::skip] + let vertices = [ + x, y, 0.0, 0.0, + x, y + h, 0.0, 1.0, + x + w, y + h, 1.0, 1.0, + + x, y, 0.0, 0.0, + x + w, y + h, 1.0, 1.0, + x + w, y, 1.0, 0.0 + ]; + + self.gl.uniform_1i(self.u_sampler_color, 0); + self.gl.bind_texture(gl::TEXTURE_2D, texture); + self.gl.bind_buffer(gl::ARRAY_BUFFER, self.vb); + self.gl.buffer_data_untyped( + gl::ARRAY_BUFFER, + (vertices.len() * std::mem::size_of::<f32>()) as gl::GLsizeiptr, + vertices.as_ptr() as *const _, + gl::DYNAMIC_DRAW, + ); + self.gl.bind_buffer(gl::ARRAY_BUFFER, 0); + + self.gl.draw_arrays(gl::TRIANGLES, 0, 6); + + ax += (glyph.left * scale_factor) + (glyph.width as f32 * scale_factor); + } + self.gl.bind_vertex_array(0); + self.gl.bind_texture(gl::TEXTURE_2D, 0); + + unsafe { + log_shader(&self.gl, self.vs); + log_shader(&self.gl, self.fs); + }; + } +} + +const VS_SRC: &[u8] = b" +#version 150 + +in vec2 iPosition; +in vec2 iTexCoords; + +uniform mat4 uTransform; + +out vec2 vColorTexCoord; + +void main() { + gl_Position = uTransform * vec4(iPosition, 0.0, 1.0); + vColorTexCoord = iTexCoords; +} +\0"; + +const FS_SRC: &[u8] = b" +#version 150 + +in vec2 vColorTexCoord; + +uniform sampler2D uSamplerColor; +uniform vec4 uTextColor; + +out vec4 oFragmentColor; + +void main() { + vec4 alpha = vec4(1.0, 1.0, 1.0, texture(uSamplerColor, vColorTexCoord).r); + oFragmentColor = uTextColor * alpha; +} +\0"; diff --git a/gfx/wr/wr_glyph_rasterizer/src/gamma_lut.rs b/gfx/wr/wr_glyph_rasterizer/src/gamma_lut.rs new file mode 100644 index 0000000000..15075bacbf --- /dev/null +++ b/gfx/wr/wr_glyph_rasterizer/src/gamma_lut.rs @@ -0,0 +1,413 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/*! +Gamma correction lookup tables. + +This is a port of Skia gamma LUT logic into Rust, used by WebRender. +*/ +//#![warn(missing_docs)] //TODO +#![allow(dead_code)] + +use api::ColorU; +use std::cmp::max; + +/// Color space responsible for converting between lumas and luminances. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum LuminanceColorSpace { + /// Linear space - no conversion involved. + Linear, + /// Simple gamma space - uses the `luminance ^ gamma` function. + Gamma(f32), + /// Srgb space. + Srgb, +} + +impl LuminanceColorSpace { + pub fn new(gamma: f32) -> LuminanceColorSpace { + if gamma == 1.0 { + LuminanceColorSpace::Linear + } else if gamma == 0.0 { + LuminanceColorSpace::Srgb + } else { + LuminanceColorSpace::Gamma(gamma) + } + } + + pub fn to_luma(&self, luminance: f32) -> f32 { + match *self { + LuminanceColorSpace::Linear => luminance, + LuminanceColorSpace::Gamma(gamma) => luminance.powf(gamma), + LuminanceColorSpace::Srgb => { + //The magic numbers are derived from the sRGB specification. + //See http://www.color.org/chardata/rgb/srgb.xalter . + if luminance <= 0.04045 { + luminance / 12.92 + } else { + ((luminance + 0.055) / 1.055).powf(2.4) + } + } + } + } + + pub fn from_luma(&self, luma: f32) -> f32 { + match *self { + LuminanceColorSpace::Linear => luma, + LuminanceColorSpace::Gamma(gamma) => luma.powf(1. / gamma), + LuminanceColorSpace::Srgb => { + //The magic numbers are derived from the sRGB specification. + //See http://www.color.org/chardata/rgb/srgb.xalter . + if luma <= 0.0031308 { + luma * 12.92 + } else { + 1.055 * luma.powf(1./2.4) - 0.055 + } + } + } + } +} + +//TODO: tests +fn round_to_u8(x : f32) -> u8 { + let v = (x + 0.5).floor() as i32; + assert!(0 <= v && v < 0x100); + v as u8 +} + +//TODO: tests +/* + * Scales base <= 2^N-1 to 2^8-1 + * @param N [1, 8] the number of bits used by base. + * @param base the number to be scaled to [0, 255]. + */ +fn scale255(n: u8, mut base: u8) -> u8 { + base <<= 8 - n; + let mut lum = base; + let mut i = n; + + while i < 8 { + lum |= base >> i; + i += n; + } + + lum +} + +// Computes the luminance from the given r, g, and b in accordance with +// SK_LUM_COEFF_X. For correct results, r, g, and b should be in linear space. +fn compute_luminance(r: u8, g: u8, b: u8) -> u8 { + // The following is + // r * SK_LUM_COEFF_R + g * SK_LUM_COEFF_G + b * SK_LUM_COEFF_B + // with SK_LUM_COEFF_X in 1.8 fixed point (rounding adjusted to sum to 256). + let val: u32 = r as u32 * 54 + g as u32 * 183 + b as u32 * 19; + assert!(val < 0x10000); + (val >> 8) as u8 +} + +// Skia uses 3 bits per channel for luminance. +const LUM_BITS: u8 = 3; +// Mask of the highest used bits. +const LUM_MASK: u8 = ((1 << LUM_BITS) - 1) << (8 - LUM_BITS); + +pub trait ColorLut { + fn quantize(&self) -> ColorU; + fn quantized_floor(&self) -> ColorU; + fn quantized_ceil(&self) -> ColorU; + fn luminance(&self) -> u8; + fn luminance_color(&self) -> ColorU; +} + +impl ColorLut for ColorU { + // Compute a canonical color that is equivalent to the input color + // for preblend table lookups. The alpha channel is never used for + // preblending, so overwrite it with opaque. + fn quantize(&self) -> ColorU { + ColorU::new( + scale255(LUM_BITS, self.r >> (8 - LUM_BITS)), + scale255(LUM_BITS, self.g >> (8 - LUM_BITS)), + scale255(LUM_BITS, self.b >> (8 - LUM_BITS)), + 255, + ) + } + + // Quantize to the smallest value that yields the same table index. + fn quantized_floor(&self) -> ColorU { + ColorU::new( + self.r & LUM_MASK, + self.g & LUM_MASK, + self.b & LUM_MASK, + 255, + ) + } + + // Quantize to the largest value that yields the same table index. + fn quantized_ceil(&self) -> ColorU { + ColorU::new( + self.r | !LUM_MASK, + self.g | !LUM_MASK, + self.b | !LUM_MASK, + 255, + ) + } + + // Compute a luminance value suitable for grayscale preblend table + // lookups. + fn luminance(&self) -> u8 { + compute_luminance(self.r, self.g, self.b) + } + + // Make a grayscale color from the computed luminance. + fn luminance_color(&self) -> ColorU { + let lum = self.luminance(); + ColorU::new(lum, lum, lum, self.a) + } +} + +// This will invert the gamma applied by CoreGraphics, +// so we can get linear values. +// CoreGraphics obscurely defaults to 2.0 as the smoothing gamma value. +// The color space used does not appear to affect this choice. +#[cfg(target_os="macos")] +fn get_inverse_gamma_table_coregraphics_smoothing() -> [u8; 256] { + let mut table = [0u8; 256]; + + for (i, v) in table.iter_mut().enumerate() { + let x = i as f32 / 255.0; + *v = round_to_u8(x * x * 255.0); + } + + table +} + +// A value of 0.5 for SK_GAMMA_CONTRAST appears to be a good compromise. +// With lower values small text appears washed out (though correctly so). +// With higher values lcd fringing is worse and the smoothing effect of +// partial coverage is diminished. +fn apply_contrast(srca: f32, contrast: f32) -> f32 { + srca + ((1.0 - srca) * contrast * srca) +} + +// The approach here is not necessarily the one with the lowest error +// See https://bel.fi/alankila/lcd/alpcor.html for a similar kind of thing +// that just search for the adjusted alpha value +pub fn build_gamma_correcting_lut(table: &mut [u8; 256], src: u8, contrast: f32, + src_space: LuminanceColorSpace, + dst_convert: LuminanceColorSpace) { + let src = src as f32 / 255.0; + let lin_src = src_space.to_luma(src); + // Guess at the dst. The perceptual inverse provides smaller visual + // discontinuities when slight changes to desaturated colors cause a channel + // to map to a different correcting lut with neighboring srcI. + // See https://code.google.com/p/chromium/issues/detail?id=141425#c59 . + let dst = 1.0 - src; + let lin_dst = dst_convert.to_luma(dst); + + // Contrast value tapers off to 0 as the src luminance becomes white + let adjusted_contrast = contrast * lin_dst; + + // Remove discontinuity and instability when src is close to dst. + // The value 1/256 is arbitrary and appears to contain the instability. + if (src - dst).abs() < (1.0 / 256.0) { + let mut ii : f32 = 0.0; + for v in table.iter_mut() { + let raw_srca = ii / 255.0; + let srca = apply_contrast(raw_srca, adjusted_contrast); + + *v = round_to_u8(255.0 * srca); + ii += 1.0; + } + } else { + // Avoid slow int to float conversion. + let mut ii : f32 = 0.0; + for v in table.iter_mut() { + // 'raw_srca += 1.0f / 255.0f' and even + // 'raw_srca = i * (1.0f / 255.0f)' can add up to more than 1.0f. + // When this happens the table[255] == 0x0 instead of 0xff. + // See http://code.google.com/p/chromium/issues/detail?id=146466 + let raw_srca = ii / 255.0; + let srca = apply_contrast(raw_srca, adjusted_contrast); + assert!(srca <= 1.0); + let dsta = 1.0 - srca; + + // Calculate the output we want. + let lin_out = lin_src * srca + dsta * lin_dst; + assert!(lin_out <= 1.0); + let out = dst_convert.from_luma(lin_out); + + // Undo what the blit blend will do. + // i.e. given the formula for OVER: out = src * result + (1 - result) * dst + // solving for result gives: + let result = (out - dst) / (src - dst); + + *v = round_to_u8(255.0 * result); + debug!("Setting {:?} to {:?}", ii as u8, *v); + + ii += 1.0; + } + } +} + +pub struct GammaLut { + tables: [[u8; 256]; 1 << LUM_BITS], + #[cfg(target_os="macos")] + cg_inverse_gamma: [u8; 256], +} + +impl GammaLut { + // Skia actually makes 9 gamma tables, then based on the luminance color, + // fetches the RGB gamma table for that color. + fn generate_tables(&mut self, contrast: f32, paint_gamma: f32, device_gamma: f32) { + let paint_color_space = LuminanceColorSpace::new(paint_gamma); + let device_color_space = LuminanceColorSpace::new(device_gamma); + + for (i, entry) in self.tables.iter_mut().enumerate() { + let luminance = scale255(LUM_BITS, i as u8); + build_gamma_correcting_lut(entry, + luminance, + contrast, + paint_color_space, + device_color_space); + } + } + + pub fn table_count(&self) -> usize { + self.tables.len() + } + + pub fn get_table(&self, color: u8) -> &[u8; 256] { + &self.tables[(color >> (8 - LUM_BITS)) as usize] + } + + pub fn new(contrast: f32, paint_gamma: f32, device_gamma: f32) -> GammaLut { + #[cfg(target_os="macos")] + let mut table = GammaLut { + tables: [[0; 256]; 1 << LUM_BITS], + cg_inverse_gamma: get_inverse_gamma_table_coregraphics_smoothing(), + }; + #[cfg(not(target_os="macos"))] + let mut table = GammaLut { + tables: [[0; 256]; 1 << LUM_BITS], + }; + + table.generate_tables(contrast, paint_gamma, device_gamma); + + table + } + + // Assumes pixels are in BGRA format. Assumes pixel values are in linear space already. + pub fn preblend(&self, pixels: &mut [u8], color: ColorU) { + let table_r = self.get_table(color.r); + let table_g = self.get_table(color.g); + let table_b = self.get_table(color.b); + + for pixel in pixels.chunks_mut(4) { + let (b, g, r) = (table_b[pixel[0] as usize], table_g[pixel[1] as usize], table_r[pixel[2] as usize]); + pixel[0] = b; + pixel[1] = g; + pixel[2] = r; + pixel[3] = max(max(b, g), r); + } + } + + // Assumes pixels are in BGRA format. Assumes pixel values are in linear space already. + pub fn preblend_scaled(&self, pixels: &mut [u8], color: ColorU, percent: u8) { + if percent >= 100 { + self.preblend(pixels, color); + return; + } + + let table_r = self.get_table(color.r); + let table_g = self.get_table(color.g); + let table_b = self.get_table(color.b); + let scale = (percent as i32 * 256) / 100; + + for pixel in pixels.chunks_mut(4) { + let (mut b, g, mut r) = ( + table_b[pixel[0] as usize] as i32, + table_g[pixel[1] as usize] as i32, + table_r[pixel[2] as usize] as i32, + ); + b = g + (((b - g) * scale) >> 8); + r = g + (((r - g) * scale) >> 8); + pixel[0] = b as u8; + pixel[1] = g as u8; + pixel[2] = r as u8; + pixel[3] = max(max(b, g), r) as u8; + } + } + + #[cfg(target_os="macos")] + pub fn coregraphics_convert_to_linear(&self, pixels: &mut [u8]) { + for pixel in pixels.chunks_mut(4) { + pixel[0] = self.cg_inverse_gamma[pixel[0] as usize]; + pixel[1] = self.cg_inverse_gamma[pixel[1] as usize]; + pixel[2] = self.cg_inverse_gamma[pixel[2] as usize]; + } + } + + // Assumes pixels are in BGRA format. Assumes pixel values are in linear space already. + pub fn preblend_grayscale(&self, pixels: &mut [u8], color: ColorU) { + let table_g = self.get_table(color.g); + + for pixel in pixels.chunks_mut(4) { + let luminance = compute_luminance(pixel[2], pixel[1], pixel[0]); + let alpha = table_g[luminance as usize]; + pixel[0] = alpha; + pixel[1] = alpha; + pixel[2] = alpha; + pixel[3] = alpha; + } + } + +} // end impl GammaLut + +#[cfg(test)] +mod tests { + use super::*; + + fn over(dst: u32, src: u32, alpha: u32) -> u32 { + (src * alpha + dst * (255 - alpha))/255 + } + + fn overf(dst: f32, src: f32, alpha: f32) -> f32 { + ((src * alpha + dst * (255. - alpha))/255.) as f32 + } + + + fn absdiff(a: u32, b: u32) -> u32 { + if a < b { b - a } else { a - b } + } + + #[test] + fn gamma() { + let mut table = [0u8; 256]; + let g = 2.0; + let space = LuminanceColorSpace::Gamma(g); + let mut src : u32 = 131; + while src < 256 { + build_gamma_correcting_lut(&mut table, src as u8, 0., space, space); + let mut max_diff = 0; + let mut dst = 0; + while dst < 256 { + for alpha in 0u32..256 { + let preblend = table[alpha as usize]; + let lin_dst = (dst as f32 / 255.).powf(g) * 255.; + let lin_src = (src as f32 / 255.).powf(g) * 255.; + + let preblend_result = over(dst, src, preblend as u32); + let true_result = ((overf(lin_dst, lin_src, alpha as f32) / 255.).powf(1. / g) * 255.) as u32; + let diff = absdiff(preblend_result, true_result); + //debug!("{} -- {} {} = {}", alpha, preblend_result, true_result, diff); + max_diff = max(max_diff, diff); + } + + //debug!("{} {} max {}", src, dst, max_diff); + assert!(max_diff <= 33); + dst += 1; + + } + src += 1; + } + } +} // end mod diff --git a/gfx/wr/wr_glyph_rasterizer/src/lib.rs b/gfx/wr/wr_glyph_rasterizer/src/lib.rs new file mode 100644 index 0000000000..27ceb71992 --- /dev/null +++ b/gfx/wr/wr_glyph_rasterizer/src/lib.rs @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! A glyph rasterizer for webrender +//! +//! ## Overview +//! +//! ## Usage +//! + +#[cfg(any(target_os = "macos", target_os = "windows"))] +mod gamma_lut; +mod rasterizer; +mod telemetry; +mod types; + +pub mod profiler; + +pub use rasterizer::*; +pub use types::*; + +#[macro_use] +extern crate malloc_size_of_derive; +#[macro_use] +extern crate tracy_rs; +#[macro_use] +extern crate log; +#[macro_use] +extern crate lazy_static; +#[macro_use] +extern crate smallvec; + +#[cfg(any(feature = "serde"))] +#[macro_use] +extern crate serde; + +extern crate malloc_size_of; + +pub mod platform { + #[cfg(target_os = "macos")] + pub use crate::platform::macos::font; + #[cfg(any(target_os = "android", all(unix, not(target_os = "macos"))))] + pub use crate::platform::unix::font; + #[cfg(target_os = "windows")] + pub use crate::platform::windows::font; + + #[cfg(target_os = "macos")] + pub mod macos { + pub mod font; + } + #[cfg(any(target_os = "android", all(unix, not(target_os = "macos"))))] + pub mod unix { + pub mod font; + } + #[cfg(target_os = "windows")] + pub mod windows { + pub mod font; + } +} diff --git a/gfx/wr/wr_glyph_rasterizer/src/platform/macos/font.rs b/gfx/wr/wr_glyph_rasterizer/src/platform/macos/font.rs new file mode 100644 index 0000000000..804dbc93e8 --- /dev/null +++ b/gfx/wr/wr_glyph_rasterizer/src/platform/macos/font.rs @@ -0,0 +1,899 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use api::{ColorF, ColorU, FontKey, FontRenderMode, FontSize, GlyphDimensions}; +use api::{FontInstanceFlags, FontVariation, NativeFontHandle}; +use core_foundation::data::CFData; +use core_foundation::base::TCFType; +use core_foundation::dictionary::CFDictionary; +use core_foundation::number::{CFNumber}; +use core_foundation::string::CFString; +use core_graphics::base::{kCGImageAlphaNoneSkipFirst, kCGImageAlphaPremultipliedFirst}; +use core_graphics::base::{kCGBitmapByteOrder32Little}; +use core_graphics::color_space::CGColorSpace; +use core_graphics::context::CGContext; +use core_graphics::context::{CGBlendMode, CGTextDrawingMode}; +use core_graphics::font::{CGFont, CGGlyph}; +use core_graphics::geometry::{CGAffineTransform, CGPoint, CGSize}; +use core_graphics::geometry::{CG_AFFINE_TRANSFORM_IDENTITY, CGRect}; +use core_text; +use core_text::font::CTFont; +use core_text::font_descriptor::{CTFontDescriptor, kCTFontDefaultOrientation, kCTFontVariationAttribute}; +use core_text::font_manager; +use euclid::default::Size2D; +use crate::gamma_lut::{ColorLut, GammaLut}; +use crate::rasterizer::{FontInstance, FontTransform, GlyphKey}; +use crate::rasterizer::{GlyphFormat, GlyphRasterError, GlyphRasterResult, RasterizedGlyph}; +use crate::types::FastHashMap; +use std::collections::hash_map::Entry; +use std::sync::Arc; + +const INITIAL_CG_CONTEXT_SIDE_LENGTH: u32 = 32; + +pub struct FontContext { + ct_font_descs: FastHashMap<FontKey, CTFontDescriptor>, + // Table mapping a sized font key with variations to its instantiated CoreText font. + ct_fonts: FastHashMap<(FontKey, FontSize, Vec<FontVariation>), CTFont>, + #[allow(dead_code)] + graphics_context: GraphicsContext, + #[allow(dead_code)] + gamma_lut: GammaLut, +} + +// core text is safe to use on multiple threads and non-shareable resources are +// all hidden inside their font context. +unsafe impl Send for FontContext {} + +struct GlyphMetrics { + rasterized_left: i32, + #[allow(dead_code)] + rasterized_descent: i32, + rasterized_ascent: i32, + rasterized_width: i32, + rasterized_height: i32, + advance: f32, +} + +// There are a number of different OS prefs that control whether or not +// requesting font smoothing actually results in subpixel AA. This gets even +// murkier in newer macOS versions that deprecate subpixel AA, with the prefs +// potentially interacting and overriding each other. In an attempt to future- +// proof things against any new prefs or interpretation of those prefs in +// future macOS versions, we do a check here to request font smoothing and see +// what result it actually gives us much like Skia does. We need to check for +// each of three potential results and process them in the font backend in +// distinct ways: +// 1) subpixel AA (differing RGB channels) with dilation +// 2) grayscale AA (matching RGB channels) with dilation, a compatibility mode +// 3) grayscale AA without dilation as if font smoothing was not requested +// We can discern between case 1 and the rest by checking if the subpixels differ. +// We can discern between cases 2 and 3 by rendering with and without smoothing +// and comparing the two to determine if there was some dilation. +// This returns the actual FontRenderMode needed to support each case, if any. +fn determine_font_smoothing_mode() -> Option<FontRenderMode> { + let mut smooth_context = CGContext::create_bitmap_context( + None, + 12, + 12, + 8, + 12 * 4, + &CGColorSpace::create_device_rgb(), + kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Little, + ); + smooth_context.set_should_smooth_fonts(true); + smooth_context.set_should_antialias(true); + smooth_context.set_rgb_fill_color(1.0, 1.0, 1.0, 1.0); + let mut gray_context = CGContext::create_bitmap_context( + None, + 12, + 12, + 8, + 12 * 4, + &CGColorSpace::create_device_rgb(), + kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Little, + ); + gray_context.set_should_smooth_fonts(false); + gray_context.set_should_antialias(true); + gray_context.set_rgb_fill_color(1.0, 1.0, 1.0, 1.0); + + // Autorelease pool for CTFont + objc::rc::autoreleasepool(|| { + // Lucida Grande 12 is the default fallback font in Firefox + let ct_font = core_text::font::new_from_name("Lucida Grande", 12.).unwrap(); + let point = CGPoint { x: 0., y: 0. }; + let glyph = 'X' as CGGlyph; + ct_font.draw_glyphs(&[glyph], &[point], smooth_context.clone()); + ct_font.draw_glyphs(&[glyph], &[point], gray_context.clone()); + }); + + let mut mode = None; + for (smooth, gray) in smooth_context.data().chunks(4).zip(gray_context.data().chunks(4)) { + if smooth[0] != smooth[1] || smooth[1] != smooth[2] { + return Some(FontRenderMode::Subpixel); + } + if smooth[0] != gray[0] || smooth[1] != gray[1] || smooth[2] != gray[2] { + mode = Some(FontRenderMode::Alpha); + } + } + return mode; +} + +// We cache the font smoothing mode globally, rather than storing it in each FontContext, +// to avoid having to determine this redundantly in each context and to avoid needing to +// lock them to access this setting in prepare_font. +lazy_static! { + static ref FONT_SMOOTHING_MODE: Option<FontRenderMode> = determine_font_smoothing_mode(); +} + +fn should_use_white_on_black(color: ColorU) -> bool { + let (r, g, b) = (color.r as u32, color.g as u32, color.b as u32); + // These thresholds were determined on 10.12 by observing what CG does. + r >= 85 && g >= 85 && b >= 85 && r + g + b >= 2 * 255 +} + +fn get_glyph_metrics( + ct_font: &CTFont, + transform: Option<&CGAffineTransform>, + glyph: CGGlyph, + x_offset: f64, + y_offset: f64, + extra_width: f64, +) -> GlyphMetrics { + let mut bounds = ct_font.get_bounding_rects_for_glyphs(kCTFontDefaultOrientation, &[glyph]); + + if bounds.origin.x.is_nan() || bounds.origin.y.is_nan() || bounds.size.width.is_nan() || + bounds.size.height.is_nan() + { + // If an unexpected glyph index is requested, core text will return NaN values + // which causes us to do bad thing as the value is cast into an integer and + // overflow when expanding the bounds a few lines below. + // Instead we are better off returning zero-sized metrics because this special + // case is handled by the callers of this method. + return GlyphMetrics { + rasterized_left: 0, + rasterized_width: 0, + rasterized_height: 0, + rasterized_ascent: 0, + rasterized_descent: 0, + advance: 0.0, + }; + } + + let mut advance = CGSize { width: 0.0, height: 0.0 }; + unsafe { + ct_font.get_advances_for_glyphs(kCTFontDefaultOrientation, &glyph, &mut advance, 1); + } + + if bounds.size.width > 0.0 { + bounds.size.width += extra_width; + } + if advance.width > 0.0 { + advance.width += extra_width; + } + + if let Some(transform) = transform { + bounds = bounds.apply_transform(transform); + } + + // First round out to pixel boundaries + // CG Origin is bottom left + let mut left = bounds.origin.x.floor() as i32; + let mut bottom = bounds.origin.y.floor() as i32; + let mut right = (bounds.origin.x + bounds.size.width + x_offset).ceil() as i32; + let mut top = (bounds.origin.y + bounds.size.height + y_offset).ceil() as i32; + + // Expand the bounds by 1 pixel, to give CG room for anti-aliasing. + // Note that this outset is to allow room for LCD smoothed glyphs. However, the correct outset + // is not currently known, as CG dilates the outlines by some percentage. + // This is taken from Skia. + left -= 1; + bottom -= 1; + right += 1; + top += 1; + + let width = right - left; + let height = top - bottom; + + GlyphMetrics { + rasterized_left: left, + rasterized_width: width, + rasterized_height: height, + rasterized_ascent: top, + rasterized_descent: -bottom, + advance: advance.width as f32, + } +} + +fn new_ct_font_with_variations(ct_font_desc: &CTFontDescriptor, size: f64, variations: &[FontVariation]) -> CTFont { + let ct_font = core_text::font::new_from_descriptor(ct_font_desc, size); + if variations.is_empty() { + return ct_font; + } + let mut vals: Vec<(CFNumber, CFNumber)> = Vec::with_capacity(variations.len() as usize); + for variation in variations { + vals.push((CFNumber::from(variation.tag as i64), CFNumber::from(variation.value as f64))); + } + if vals.is_empty() { + return ct_font; + } + let vals_dict = CFDictionary::from_CFType_pairs(&vals); + let variation_attribute = unsafe { CFString::wrap_under_get_rule(kCTFontVariationAttribute) }; + let attrs_dict = CFDictionary::from_CFType_pairs(&[(variation_attribute, vals_dict)]); + let ct_var_font_desc = ct_font.copy_descriptor().create_copy_with_attributes(attrs_dict.to_untyped()).unwrap(); + core_text::font::new_from_descriptor(&ct_var_font_desc, size) + +} + +// We rely on Gecko to determine whether the font may have color glyphs to avoid +// needing to load the font ahead of time to query its symbolic traits. +fn is_bitmap_font(font: &FontInstance) -> bool { + font.flags.contains(FontInstanceFlags::EMBEDDED_BITMAPS) +} + +impl FontContext { + pub fn distribute_across_threads() -> bool { + true + } + + pub fn new() -> FontContext { + debug!("Test for subpixel AA support: {:?}", *FONT_SMOOTHING_MODE); + + // Force CG to use sRGB color space to gamma correct. + let contrast = 0.0; + let gamma = 0.0; + + FontContext { + ct_font_descs: FastHashMap::default(), + ct_fonts: FastHashMap::default(), + graphics_context: GraphicsContext::new(), + gamma_lut: GammaLut::new(contrast, gamma, gamma), + } + } + + pub fn add_raw_font(&mut self, font_key: &FontKey, bytes: Arc<Vec<u8>>, index: u32) { + if self.ct_font_descs.contains_key(font_key) { + return; + } + + assert_eq!(index, 0); + let data = CFData::from_arc(bytes); + let ct_font_desc = match font_manager::create_font_descriptor_with_data(data) { + Err(_) => return, + Ok(cg_font) => cg_font, + }; + self.ct_font_descs.insert(*font_key, ct_font_desc); + } + + pub fn add_native_font(&mut self, font_key: &FontKey, native_font_handle: NativeFontHandle) { + if self.ct_font_descs.contains_key(font_key) { + return; + } + + // There's no way great way to go from a CGFont to a CTFontDescriptor + // We could use the postscript name but that doesn't work for the + // system UI fonts on newer macOS versions. Instead we create a CTFont + // and use the descriptor for that. Normally we'd try to avoid new_from_CGFont + // because that adds the CGFont to the descriptor cache which can keep the CGFont + // around for a long time, but that should be ok for non-web (native) fonts. + let cf_name = CFString::new(&native_font_handle.name); + + // For "hidden" system fonts, whose names start with a period, + // we can't instantiate CTFonts via a descriptor. We're really + // supposed to use CTFontCreateUIFontForLanguage, but for now + // we just use the CGFont. + let desc = if native_font_handle.name.starts_with('.') { + let cg_font = match CGFont::from_name(&cf_name) { + Ok(cg_font) => cg_font, + Err(_) => { + // If for some reason we failed to load a font descriptor, then our + // only options are to either abort or substitute a fallback font. + // It is preferable to use a fallback font instead so that rendering + // can at least still proceed in some fashion without erroring. + // Lucida Grande is the fallback font in Gecko, so use that here. + CGFont::from_name(&CFString::from_static_string("Lucida Grande")) + .expect("couldn't find font with postscript name and couldn't load fallback font") + } + }; + core_text::font::new_from_CGFont(&cg_font, 0.).copy_descriptor() + } else { + core_text::font_descriptor::new_from_postscript_name(&cf_name) + }; + + self.ct_font_descs + .insert(*font_key, desc); + } + + pub fn delete_font(&mut self, font_key: &FontKey) { + if let Some(_) = self.ct_font_descs.remove(font_key) { + self.ct_fonts.retain(|k, _| k.0 != *font_key); + } + } + + pub fn delete_font_instance(&mut self, instance: &FontInstance) { + // Remove the CoreText font corresponding to this instance. + let size = FontSize::from_f64_px(instance.get_transformed_size()); + self.ct_fonts.remove(&(instance.font_key, size, instance.variations.clone())); + } + + fn get_ct_font( + &mut self, + font_key: FontKey, + size: f64, + variations: &[FontVariation], + ) -> Option<CTFont> { + // Interacting with CoreText can create autorelease garbage. + objc::rc::autoreleasepool(|| { + match self.ct_fonts.entry((font_key, FontSize::from_f64_px(size), variations.to_vec())) { + Entry::Occupied(entry) => Some((*entry.get()).clone()), + Entry::Vacant(entry) => { + let ct_font_desc = self.ct_font_descs.get(&font_key)?; + let ct_font = new_ct_font_with_variations(ct_font_desc, size, variations); + entry.insert(ct_font.clone()); + Some(ct_font) + } + } + }) + } + + pub fn get_glyph_index(&mut self, font_key: FontKey, ch: char) -> Option<u32> { + let character = ch as u16; + let mut glyph = 0; + + self.get_ct_font(font_key, 16.0, &[]) + .and_then(|ct_font| { + unsafe { + let result = ct_font.get_glyphs_for_characters(&character, &mut glyph, 1); + + if result { + Some(glyph as u32) + } else { + None + } + } + }) + } + + pub fn get_glyph_dimensions( + &mut self, + font: &FontInstance, + key: &GlyphKey, + ) -> Option<GlyphDimensions> { + let (x_scale, y_scale) = font.transform.compute_scale().unwrap_or((1.0, 1.0)); + let size = font.size.to_f64_px() * y_scale; + self.get_ct_font(font.font_key, size, &font.variations) + .and_then(|ct_font| { + let glyph = key.index() as CGGlyph; + let bitmap = is_bitmap_font(font); + let (mut shape, (x_offset, y_offset)) = if bitmap { + (FontTransform::identity(), (0.0, 0.0)) + } else { + (font.transform.invert_scale(y_scale, y_scale), font.get_subpx_offset(key)) + }; + if font.flags.contains(FontInstanceFlags::FLIP_X) { + shape = shape.flip_x(); + } + if font.flags.contains(FontInstanceFlags::FLIP_Y) { + shape = shape.flip_y(); + } + if font.flags.contains(FontInstanceFlags::TRANSPOSE) { + shape = shape.swap_xy(); + } + let (mut tx, mut ty) = (0.0, 0.0); + if font.synthetic_italics.is_enabled() { + let (shape_, (tx_, ty_)) = font.synthesize_italics(shape, size); + shape = shape_; + tx = tx_; + ty = ty_; + } + let transform = if !shape.is_identity() || (tx, ty) != (0.0, 0.0) { + Some(CGAffineTransform { + a: shape.scale_x as f64, + b: -shape.skew_y as f64, + c: -shape.skew_x as f64, + d: shape.scale_y as f64, + tx: tx, + ty: -ty, + }) + } else { + None + }; + let (strike_scale, pixel_step) = if bitmap { + (y_scale, 1.0) + } else { + (x_scale, y_scale / x_scale) + }; + let extra_strikes = font.get_extra_strikes( + FontInstanceFlags::SYNTHETIC_BOLD | FontInstanceFlags::MULTISTRIKE_BOLD, + strike_scale, + ); + let metrics = get_glyph_metrics( + &ct_font, + transform.as_ref(), + glyph, + x_offset, + y_offset, + extra_strikes as f64 * pixel_step, + ); + if metrics.rasterized_width == 0 || metrics.rasterized_height == 0 { + None + } else { + Some(GlyphDimensions { + left: metrics.rasterized_left, + top: metrics.rasterized_ascent, + width: metrics.rasterized_width, + height: metrics.rasterized_height, + advance: metrics.advance, + }) + } + }) + } + + // Assumes the pixels here are linear values from CG + fn gamma_correct_pixels( + &self, + pixels: &mut Vec<u8>, + render_mode: FontRenderMode, + color: ColorU, + ) { + // Then convert back to gamma corrected values. + match render_mode { + FontRenderMode::Alpha => { + self.gamma_lut.preblend_grayscale(pixels, color); + } + FontRenderMode::Subpixel => { + self.gamma_lut.preblend(pixels, color); + } + _ => {} // Again, give mono untouched since only the alpha matters. + } + } + + #[allow(dead_code)] + fn print_glyph_data(&mut self, data: &[u8], width: usize, height: usize) { + // Rust doesn't have step_by support on stable :( + debug!("Width is: {:?} height: {:?}", width, height); + for i in 0 .. height { + let current_height = i * width * 4; + + for pixel in data[current_height .. current_height + (width * 4)].chunks(4) { + let b = pixel[0]; + let g = pixel[1]; + let r = pixel[2]; + let a = pixel[3]; + debug!("({}, {}, {}, {}) ", r, g, b, a); + } + } + } + + pub fn prepare_font(font: &mut FontInstance) { + if is_bitmap_font(font) { + // Render mode is ignored for bitmap fonts. Also, avoid normalizing the color + // in case CoreText needs the current color for rendering glyph color layers. + font.render_mode = FontRenderMode::Mono; + font.disable_subpixel_position(); + return; + } + // Sanitize the render mode for font smoothing. If font smoothing is supported, + // then we just need to ensure the render mode is limited to what is supported. + // If font smoothing is actually disabled, then we need to fall back to grayscale. + if font.flags.contains(FontInstanceFlags::FONT_SMOOTHING) || + font.render_mode == FontRenderMode::Subpixel { + match *FONT_SMOOTHING_MODE { + Some(mode) => { + font.render_mode = font.render_mode.limit_by(mode); + font.flags.insert(FontInstanceFlags::FONT_SMOOTHING); + } + None => { + font.render_mode = font.render_mode.limit_by(FontRenderMode::Alpha); + font.flags.remove(FontInstanceFlags::FONT_SMOOTHING); + } + } + } + match font.render_mode { + FontRenderMode::Mono => { + // In mono mode the color of the font is irrelevant. + font.color = ColorU::new(255, 255, 255, 255); + // Subpixel positioning is disabled in mono mode. + font.disable_subpixel_position(); + } + FontRenderMode::Alpha => { + font.color = if font.flags.contains(FontInstanceFlags::FONT_SMOOTHING) { + // Only the G channel is used to index grayscale tables, + // so use R and B to preserve light/dark determination. + let ColorU { g, a, .. } = font.color.luminance_color().quantized_ceil(); + let rb = if should_use_white_on_black(font.color) { 255 } else { 0 }; + ColorU::new(rb, g, rb, a) + } else { + ColorU::new(255, 255, 255, 255) + }; + } + FontRenderMode::Subpixel => { + // Quantization may change the light/dark determination, so quantize in the + // direction necessary to respect the threshold. + font.color = if should_use_white_on_black(font.color) { + font.color.quantized_ceil() + } else { + font.color.quantized_floor() + }; + } + } + } + + pub fn begin_rasterize(_font: &FontInstance) { + } + + pub fn end_rasterize(_font: &FontInstance) { + } + + pub fn rasterize_glyph(&mut self, font: &FontInstance, key: &GlyphKey) -> GlyphRasterResult { + objc::rc::autoreleasepool(|| { + let (x_scale, y_scale) = font.transform.compute_scale().unwrap_or((1.0, 1.0)); + let size = font.size.to_f64_px() * y_scale; + let ct_font = + self.get_ct_font(font.font_key, size, &font.variations).ok_or(GlyphRasterError::LoadFailed)?; + let glyph_type = if is_bitmap_font(font) { + GlyphType::Bitmap + } else { + GlyphType::Vector + }; + + let (mut shape, (x_offset, y_offset)) = match glyph_type { + GlyphType::Bitmap => (FontTransform::identity(), (0.0, 0.0)), + GlyphType::Vector => { + (font.transform.invert_scale(y_scale, y_scale), font.get_subpx_offset(key)) + } + }; + if font.flags.contains(FontInstanceFlags::FLIP_X) { + shape = shape.flip_x(); + } + if font.flags.contains(FontInstanceFlags::FLIP_Y) { + shape = shape.flip_y(); + } + if font.flags.contains(FontInstanceFlags::TRANSPOSE) { + shape = shape.swap_xy(); + } + let (mut tx, mut ty) = (0.0, 0.0); + if font.synthetic_italics.is_enabled() { + let (shape_, (tx_, ty_)) = font.synthesize_italics(shape, size); + shape = shape_; + tx = tx_; + ty = ty_; + } + let transform = if !shape.is_identity() || (tx, ty) != (0.0, 0.0) { + Some(CGAffineTransform { + a: shape.scale_x as f64, + b: -shape.skew_y as f64, + c: -shape.skew_x as f64, + d: shape.scale_y as f64, + tx: tx, + ty: -ty, + }) + } else { + None + }; + + let glyph = key.index() as CGGlyph; + let (strike_scale, pixel_step) = if glyph_type == GlyphType::Bitmap { + (y_scale, 1.0) + } else { + (x_scale, y_scale / x_scale) + }; + let extra_strikes = font.get_extra_strikes( + FontInstanceFlags::SYNTHETIC_BOLD | FontInstanceFlags::MULTISTRIKE_BOLD, + strike_scale, + ); + let metrics = get_glyph_metrics( + &ct_font, + transform.as_ref(), + glyph, + x_offset, + y_offset, + extra_strikes as f64 * pixel_step, + ); + if metrics.rasterized_width == 0 || metrics.rasterized_height == 0 { + return Err(GlyphRasterError::LoadFailed); + } + + let raster_size = Size2D::new( + metrics.rasterized_width as u32, + metrics.rasterized_height as u32 + ); + + // If the font render mode is Alpha, we support two different ways to + // compute the grayscale mask, depending on the value of the platform + // options' font_smoothing flag: + // - Alpha + smoothing: + // We will recover a grayscale mask from a subpixel rasterization, in + // such a way that the result looks as close to subpixel text + // blending as we can make it. This involves gamma correction, + // luminance computations and preblending based on the text color, + // just like with the Subpixel render mode. + // - Alpha without smoothing: + // We will ask CoreGraphics to rasterize the text with font_smoothing + // off. This will cause it to use grayscale anti-aliasing with + // comparatively thin text. This method of text rendering is not + // gamma-aware. + // + // For subpixel rasterization, starting with macOS 10.11, CoreGraphics + // uses different glyph dilation based on the text color. Bright text + // uses less font dilation (looks thinner) than dark text. + // As a consequence, when we ask CG to rasterize with subpixel AA, we + // will render white-on-black text as opposed to black-on-white text if + // the text color brightness exceeds a certain threshold. This applies + // to both the Subpixel and the "Alpha + smoothing" modes, but not to + // the "Alpha without smoothing" and Mono modes. + // + // Fonts with color glyphs may, depending on the state within per-glyph + // table data, require the current font color to determine the output + // color. For such fonts we must thus supply the current font color just + // in case it is necessary. + let use_white_on_black = should_use_white_on_black(font.color); + let use_font_smoothing = font.flags.contains(FontInstanceFlags::FONT_SMOOTHING); + let (antialias, smooth, text_color, bg_color, invert) = match glyph_type { + GlyphType::Bitmap => (true, false, ColorF::from(font.color), ColorF::TRANSPARENT, false), + GlyphType::Vector => { + match (font.render_mode, use_font_smoothing) { + (FontRenderMode::Subpixel, _) | + (FontRenderMode::Alpha, true) => if use_white_on_black { + (true, true, ColorF::WHITE, ColorF::BLACK, false) + } else { + (true, true, ColorF::BLACK, ColorF::WHITE, true) + }, + (FontRenderMode::Alpha, false) => (true, false, ColorF::BLACK, ColorF::WHITE, true), + (FontRenderMode::Mono, _) => (false, false, ColorF::BLACK, ColorF::WHITE, true), + } + } + }; + + { + let cg_context = self.graphics_context.get_context(&raster_size, glyph_type); + + // These are always true in Gecko, even for non-AA fonts + cg_context.set_allows_font_subpixel_positioning(true); + cg_context.set_should_subpixel_position_fonts(true); + + // Don't quantize because we're doing it already. + cg_context.set_allows_font_subpixel_quantization(false); + cg_context.set_should_subpixel_quantize_fonts(false); + + cg_context.set_should_smooth_fonts(smooth); + cg_context.set_should_antialias(antialias); + + // Fill the background. This could be opaque white, opaque black, or + // transparency. + cg_context.set_rgb_fill_color( + bg_color.r.into(), + bg_color.g.into(), + bg_color.b.into(), + bg_color.a.into(), + ); + let rect = CGRect { + origin: CGPoint { x: 0.0, y: 0.0 }, + size: CGSize { + width: metrics.rasterized_width as f64, + height: metrics.rasterized_height as f64, + }, + }; + + // Make sure we use the Copy blend mode, or else we'll get the Porter-Duff OVER + // operator, which can't clear to the transparent color! + cg_context.set_blend_mode(CGBlendMode::Copy); + cg_context.fill_rect(rect); + cg_context.set_blend_mode(CGBlendMode::Normal); + + // Set the text color and draw the glyphs. + cg_context.set_rgb_fill_color( + text_color.r.into(), + text_color.g.into(), + text_color.b.into(), + 1.0, + ); + cg_context.set_text_drawing_mode(CGTextDrawingMode::CGTextFill); + + // CG Origin is bottom left, WR is top left. Need -y offset + let mut draw_origin = CGPoint { + x: -metrics.rasterized_left as f64 + x_offset + tx, + y: metrics.rasterized_descent as f64 - y_offset - ty, + }; + + if let Some(transform) = transform { + cg_context.set_text_matrix(&transform); + + draw_origin = draw_origin.apply_transform(&transform.invert()); + } else { + // Make sure to reset this because some previous glyph rasterization might have + // changed it. + cg_context.set_text_matrix(&CG_AFFINE_TRANSFORM_IDENTITY); + } + + ct_font.draw_glyphs(&[glyph], &[draw_origin], cg_context.clone()); + + // We'd like to render all the strikes in a single ct_font.draw_glyphs call, + // passing an array of glyph IDs and an array of origins, but unfortunately + // with some fonts, Core Text may inappropriately pixel-snap the rasterization, + // such that the strikes overprint instead of being offset. Rendering the + // strikes with individual draw_glyphs calls avoids this. + // (See https://bugzilla.mozilla.org/show_bug.cgi?id=1633397 for details.) + for i in 1 ..= extra_strikes { + let origin = CGPoint { + x: draw_origin.x + i as f64 * pixel_step, + y: draw_origin.y, + }; + ct_font.draw_glyphs(&[glyph], &[origin], cg_context.clone()); + } + } + + let mut rasterized_pixels = self.graphics_context + .get_rasterized_pixels(&raster_size, glyph_type); + + if glyph_type == GlyphType::Vector { + // We rendered text into an opaque surface. The code below needs to + // ignore the current value of each pixel's alpha channel. But it's + // allowed to write to the alpha channel, because we're done calling + // CG functions now. + + if smooth { + // Convert to linear space for subpixel AA. + // We explicitly do not do this for grayscale AA ("Alpha without + // smoothing" or Mono) because those rendering modes are not + // gamma-aware in CoreGraphics. + self.gamma_lut.coregraphics_convert_to_linear( + &mut rasterized_pixels, + ); + } + + for pixel in rasterized_pixels.chunks_mut(4) { + if invert { + pixel[0] = 255 - pixel[0]; + pixel[1] = 255 - pixel[1]; + pixel[2] = 255 - pixel[2]; + } + + // Set alpha to the value of the green channel. For grayscale + // text, all three channels have the same value anyway. + // For subpixel text, the mask's alpha only makes a difference + // when computing the destination alpha on destination pixels + // that are not completely opaque. Picking an alpha value + // that's somehow based on the mask at least ensures that text + // blending doesn't modify the destination alpha on pixels where + // the mask is entirely zero. + pixel[3] = pixel[1]; + } + + if smooth { + // Convert back from linear space into device space, and perform + // some "preblending" based on the text color. + // In Alpha + smoothing mode, this will also convert subpixel AA + // into grayscale AA. + self.gamma_correct_pixels( + &mut rasterized_pixels, + font.render_mode, + font.color, + ); + } + } + + Ok(RasterizedGlyph { + left: metrics.rasterized_left as f32, + top: metrics.rasterized_ascent as f32, + width: metrics.rasterized_width, + height: metrics.rasterized_height, + scale: match glyph_type { + GlyphType::Bitmap => y_scale.recip() as f32, + GlyphType::Vector => 1.0, + }, + format: match glyph_type { + GlyphType::Bitmap => GlyphFormat::ColorBitmap, + GlyphType::Vector => font.get_glyph_format(), + }, + bytes: rasterized_pixels, + })}) + } +} + +// Avoids taking locks by recycling Core Graphics contexts. +#[allow(dead_code)] +struct GraphicsContext { + vector_context: CGContext, + vector_context_size: Size2D<u32>, + bitmap_context: CGContext, + bitmap_context_size: Size2D<u32>, +} + +impl GraphicsContext { + fn new() -> GraphicsContext { + let size = Size2D::new(INITIAL_CG_CONTEXT_SIDE_LENGTH, INITIAL_CG_CONTEXT_SIDE_LENGTH); + GraphicsContext { + vector_context: GraphicsContext::create_cg_context(&size, GlyphType::Vector), + vector_context_size: size, + bitmap_context: GraphicsContext::create_cg_context(&size, GlyphType::Bitmap), + bitmap_context_size: size, + } + } + + #[allow(dead_code)] + fn get_context(&mut self, size: &Size2D<u32>, glyph_type: GlyphType) + -> &mut CGContext { + let (cached_context, cached_size) = match glyph_type { + GlyphType::Vector => { + (&mut self.vector_context, &mut self.vector_context_size) + } + GlyphType::Bitmap => { + (&mut self.bitmap_context, &mut self.bitmap_context_size) + } + }; + let rounded_size = Size2D::new(size.width.next_power_of_two(), + size.height.next_power_of_two()); + if rounded_size.width > cached_size.width || rounded_size.height > cached_size.height { + *cached_size = Size2D::new(u32::max(cached_size.width, rounded_size.width), + u32::max(cached_size.height, rounded_size.height)); + *cached_context = GraphicsContext::create_cg_context(cached_size, glyph_type); + } + cached_context + } + + #[allow(dead_code)] + fn get_rasterized_pixels(&mut self, size: &Size2D<u32>, glyph_type: GlyphType) + -> Vec<u8> { + let (cached_context, cached_size) = match glyph_type { + GlyphType::Vector => (&mut self.vector_context, &self.vector_context_size), + GlyphType::Bitmap => (&mut self.bitmap_context, &self.bitmap_context_size), + }; + let cached_data = cached_context.data(); + let cached_stride = cached_size.width as usize * 4; + + let result_len = size.width as usize * size.height as usize * 4; + let mut result = Vec::with_capacity(result_len); + for y in (cached_size.height - size.height)..cached_size.height { + let cached_start = y as usize * cached_stride; + let cached_end = cached_start + size.width as usize * 4; + result.extend_from_slice(&cached_data[cached_start..cached_end]); + } + debug_assert_eq!(result.len(), result_len); + result + } + + fn create_cg_context(size: &Size2D<u32>, glyph_type: GlyphType) -> CGContext { + // The result of rasterization, in all render modes, is going to be a + // BGRA surface with white text on transparency using premultiplied + // alpha. For subpixel text, the RGB values will be the mask value for + // the individual components. For bitmap glyphs, the RGB values will be + // the (premultiplied) color of the pixel. For Alpha and Mono, each + // pixel will have R==G==B==A at the end of this function. + // We access the color channels in little-endian order. + // The CGContext will create and own our pixel buffer. + // In the non-Bitmap cases, we will ask CoreGraphics to draw text onto + // an opaque background. In order to hit the most efficient path in CG + // for this, we will tell CG that the CGContext is opaque, by passing + // an "[...]AlphaNone[...]" context flag. This creates a slight + // contradiction to the way we use the buffer after CG is done with it, + // because we will convert it into text-on-transparency. But that's ok; + // we still get four bytes per pixel and CG won't mess with the alpha + // channel after we've stopped calling CG functions. We just need to + // make sure that we don't look at the alpha values of the pixels that + // we get from CG, and compute our own alpha value only from RGB. + // Note that CG requires kCGBitmapByteOrder32Little in order to do + // subpixel AA at all (which we need it to do in both Subpixel and + // Alpha+smoothing mode). But little-endian is what we want anyway, so + // this works out nicely. + let color_type = match glyph_type { + GlyphType::Vector => kCGImageAlphaNoneSkipFirst, + GlyphType::Bitmap => kCGImageAlphaPremultipliedFirst, + }; + + CGContext::create_bitmap_context(None, + size.width as usize, + size.height as usize, + 8, + size.width as usize * 4, + &CGColorSpace::create_device_rgb(), + kCGBitmapByteOrder32Little | color_type) + } +} + +#[derive(Clone, Copy, PartialEq, Debug)] +enum GlyphType { + Vector, + Bitmap, +} + diff --git a/gfx/wr/wr_glyph_rasterizer/src/platform/unix/font.rs b/gfx/wr/wr_glyph_rasterizer/src/platform/unix/font.rs new file mode 100644 index 0000000000..5885d8f9a2 --- /dev/null +++ b/gfx/wr/wr_glyph_rasterizer/src/platform/unix/font.rs @@ -0,0 +1,1099 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use api::{ColorU, GlyphDimensions, FontKey, FontRenderMode}; +use api::{FontInstancePlatformOptions, FontLCDFilter, FontHinting}; +use api::{FontInstanceFlags, FontTemplate, FontVariation, NativeFontHandle}; +use freetype::freetype::{FT_BBox, FT_Outline_Translate, FT_Pixel_Mode, FT_Render_Mode}; +use freetype::freetype::{FT_Done_Face, FT_Error, FT_Get_Char_Index, FT_Int32}; +use freetype::freetype::{FT_Done_FreeType, FT_Library_SetLcdFilter, FT_Pos}; +use freetype::freetype::{FT_F26Dot6, FT_Face, FT_Glyph_Format, FT_Long, FT_UInt}; +use freetype::freetype::{FT_GlyphSlot, FT_LcdFilter, FT_New_Face, FT_New_Memory_Face}; +use freetype::freetype::{FT_Init_FreeType, FT_Load_Glyph, FT_Render_Glyph}; +use freetype::freetype::{FT_Library, FT_Outline_Get_CBox, FT_Set_Char_Size, FT_Select_Size}; +use freetype::freetype::{FT_Fixed, FT_Matrix, FT_Set_Transform, FT_String, FT_ULong, FT_Vector}; +use freetype::freetype::{FT_Err_Unimplemented_Feature, FT_MulFix, FT_Outline_Embolden}; +use freetype::freetype::{FT_LOAD_COLOR, FT_LOAD_DEFAULT, FT_LOAD_FORCE_AUTOHINT}; +use freetype::freetype::{FT_LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH, FT_LOAD_NO_AUTOHINT}; +use freetype::freetype::{FT_LOAD_NO_BITMAP, FT_LOAD_NO_HINTING}; +use freetype::freetype::{FT_FACE_FLAG_SCALABLE, FT_FACE_FLAG_FIXED_SIZES}; +use freetype::freetype::{FT_FACE_FLAG_MULTIPLE_MASTERS}; +use freetype::succeeded; +use crate::rasterizer::{FontInstance, GlyphFormat, GlyphKey}; +use crate::rasterizer::{GlyphRasterError, GlyphRasterResult, RasterizedGlyph}; +use crate::types::FastHashMap; +#[cfg(any(not(target_os = "android"), feature = "dynamic_freetype"))] +use libc::{dlsym, RTLD_DEFAULT}; +use libc::free; +use std::{cmp, mem, ptr, slice}; +use std::cmp::max; +use std::ffi::CString; +use std::sync::{Arc, Condvar, Mutex, MutexGuard}; + +// These constants are not present in the freetype +// bindings due to bindgen not handling the way +// the macros are defined. +//const FT_LOAD_TARGET_NORMAL: FT_UInt = 0 << 16; +const FT_LOAD_TARGET_LIGHT: FT_UInt = 1 << 16; +const FT_LOAD_TARGET_MONO: FT_UInt = 2 << 16; +const FT_LOAD_TARGET_LCD: FT_UInt = 3 << 16; +const FT_LOAD_TARGET_LCD_V: FT_UInt = 4 << 16; + +#[repr(C)] +struct FT_Var_Axis { + pub name: *mut FT_String, + pub minimum: FT_Fixed, + pub def: FT_Fixed, + pub maximum: FT_Fixed, + pub tag: FT_ULong, + pub strid: FT_UInt, +} + +#[repr(C)] +struct FT_Var_Named_Style { + pub coords: *mut FT_Fixed, + pub strid: FT_UInt, + pub psid: FT_UInt, +} + +#[repr(C)] +struct FT_MM_Var { + pub num_axis: FT_UInt, + pub num_designs: FT_UInt, + pub num_namedstyles: FT_UInt, + pub axis: *mut FT_Var_Axis, + pub namedstyle: *mut FT_Var_Named_Style, +} + +#[inline] +pub fn unimplemented(error: FT_Error) -> bool { + error == FT_Err_Unimplemented_Feature as FT_Error +} + +// Use dlsym to check for symbols. If not available. just return an unimplemented error. +#[cfg(any(not(target_os = "android"), feature = "dynamic_freetype"))] +macro_rules! ft_dyn_fn { + ($func_name:ident($($arg_name:ident:$arg_type:ty),*) -> FT_Error) => { + #[allow(non_snake_case)] + unsafe fn $func_name($($arg_name:$arg_type),*) -> FT_Error { + extern "C" fn unimpl_func($(_:$arg_type),*) -> FT_Error { + FT_Err_Unimplemented_Feature as FT_Error + } + lazy_static! { + static ref FUNC: unsafe extern "C" fn($($arg_type),*) -> FT_Error = { + unsafe { + let cname = CString::new(stringify!($func_name)).unwrap(); + let ptr = dlsym(RTLD_DEFAULT, cname.as_ptr()); + if !ptr.is_null() { mem::transmute(ptr) } else { unimpl_func } + } + }; + } + (*FUNC)($($arg_name),*) + } + } +} + +// On Android, just statically link in the symbols... +#[cfg(all(target_os = "android", not(feature = "dynamic_freetype")))] +macro_rules! ft_dyn_fn { + ($($proto:tt)+) => { extern "C" { fn $($proto)+; } } +} + +ft_dyn_fn!(FT_Get_MM_Var(face: FT_Face, desc: *mut *mut FT_MM_Var) -> FT_Error); +ft_dyn_fn!(FT_Done_MM_Var(library: FT_Library, desc: *mut FT_MM_Var) -> FT_Error); +ft_dyn_fn!(FT_Set_Var_Design_Coordinates(face: FT_Face, num_vals: FT_UInt, vals: *mut FT_Fixed) -> FT_Error); +ft_dyn_fn!(FT_Get_Var_Design_Coordinates(face: FT_Face, num_vals: FT_UInt, vals: *mut FT_Fixed) -> FT_Error); + +extern "C" { + fn FT_GlyphSlot_Embolden(slot: FT_GlyphSlot); +} + +// Custom version of FT_GlyphSlot_Embolden to be less aggressive with outline +// fonts than the default implementation in FreeType. +#[no_mangle] +pub extern "C" fn mozilla_glyphslot_embolden_less(slot: FT_GlyphSlot) { + if slot.is_null() { + return; + } + + let slot_ = unsafe { &mut *slot }; + let format = slot_.format; + if format != FT_Glyph_Format::FT_GLYPH_FORMAT_OUTLINE { + // For non-outline glyphs, just fall back to FreeType's function. + unsafe { FT_GlyphSlot_Embolden(slot) }; + return; + } + + let face_ = unsafe { *slot_.face }; + + // FT_GlyphSlot_Embolden uses a divisor of 24 here; we'll be only half as + // bold. + let size_ = unsafe { *face_.size }; + let strength = + unsafe { FT_MulFix(face_.units_per_EM as FT_Long, + size_.metrics.y_scale) / 48 }; + unsafe { FT_Outline_Embolden(&mut slot_.outline, strength) }; + + // Adjust metrics to suit the fattened glyph. + if slot_.advance.x != 0 { + slot_.advance.x += strength; + } + if slot_.advance.y != 0 { + slot_.advance.y += strength; + } + slot_.metrics.width += strength; + slot_.metrics.height += strength; + slot_.metrics.horiAdvance += strength; + slot_.metrics.vertAdvance += strength; + slot_.metrics.horiBearingY += strength; +} + +struct CachedFont { + template: FontTemplate, + face: FT_Face, + mm_var: *mut FT_MM_Var, + variations: Vec<FontVariation>, +} + +impl Drop for CachedFont { + fn drop(&mut self) { + unsafe { + if !self.mm_var.is_null() && + unimplemented(FT_Done_MM_Var((*(*self.face).glyph).library, self.mm_var)) { + free(self.mm_var as _); + } + + FT_Done_Face(self.face); + } + } +} + +struct FontCache { + lib: FT_Library, + // Maps a template to a cached font that may be used across all threads. + fonts: FastHashMap<FontTemplate, Arc<Mutex<CachedFont>>>, + // The current LCD filter installed in the library. + lcd_filter: FontLCDFilter, + // The number of threads currently relying on the LCD filter state. + lcd_filter_uses: usize, +} + +// FreeType resources are safe to move between threads as long as they +// are not concurrently accessed. In our case, everything is behind a +// Mutex so it is safe to move them between threads. +unsafe impl Send for CachedFont {} +unsafe impl Send for FontCache {} + +impl FontCache { + fn new() -> Self { + let mut lib: FT_Library = ptr::null_mut(); + let result = unsafe { FT_Init_FreeType(&mut lib) }; + if succeeded(result) { + // Ensure the library uses the default LCD filter initially. + unsafe { FT_Library_SetLcdFilter(lib, FT_LcdFilter::FT_LCD_FILTER_DEFAULT) }; + } else { + panic!("Failed to initialize FreeType - {}", result) + } + + FontCache { + lib, + fonts: FastHashMap::default(), + lcd_filter: FontLCDFilter::Default, + lcd_filter_uses: 0, + } + } + + fn add_font(&mut self, template: FontTemplate) -> Result<Arc<Mutex<CachedFont>>, FT_Error> { + if let Some(cached) = self.fonts.get(&template) { + return Ok(cached.clone()); + } + unsafe { + let mut face: FT_Face = ptr::null_mut(); + let result = match template { + FontTemplate::Raw(ref bytes, index) => { + FT_New_Memory_Face( + self.lib, + bytes.as_ptr(), + bytes.len() as FT_Long, + index as FT_Long, + &mut face, + ) + } + FontTemplate::Native(NativeFontHandle { ref path, index }) => { + let str = path.as_os_str().to_str().unwrap(); + let cstr = CString::new(str).unwrap(); + FT_New_Face( + self.lib, + cstr.as_ptr(), + index as FT_Long, + &mut face, + ) + } + }; + if !succeeded(result) || face.is_null() { + return Err(result); + } + let mut mm_var = ptr::null_mut(); + if ((*face).face_flags & (FT_FACE_FLAG_MULTIPLE_MASTERS as FT_Long)) != 0 && + succeeded(FT_Get_MM_Var(face, &mut mm_var)) { + // Calling this before FT_Set_Var_Design_Coordinates avoids a bug with font variations + // not initialized properly in the font face, even if we ignore the result. + // See bug 1647035. + let mut tmp = [0; 16]; + let res = FT_Get_Var_Design_Coordinates( + face, + (*mm_var).num_axis.min(16), + tmp.as_mut_ptr() + ); + debug_assert!(succeeded(res)); + } + let cached = Arc::new(Mutex::new(CachedFont { + template: template.clone(), + face, + mm_var, + variations: Vec::new(), + })); + self.fonts.insert(template, cached.clone()); + Ok(cached) + } + } + + fn delete_font(&mut self, cached: Arc<Mutex<CachedFont>>) { + self.fonts.remove(&cached.lock().unwrap().template); + } +} + +impl Drop for FontCache { + fn drop(&mut self) { + self.fonts.clear(); + unsafe { + FT_Done_FreeType(self.lib); + } + } +} + +lazy_static! { + static ref FONT_CACHE: Mutex<FontCache> = Mutex::new(FontCache::new()); + static ref LCD_FILTER_UNUSED: Condvar = Condvar::new(); +} + +pub struct FontContext { + fonts: FastHashMap<FontKey, Arc<Mutex<CachedFont>>>, +} + +fn get_skew_bounds(bottom: i32, top: i32, skew_factor: f32, _vertical: bool) -> (f32, f32) { + let skew_min = (bottom as f32 + 0.5) * skew_factor; + let skew_max = (top as f32 - 0.5) * skew_factor; + // Negative skew factor may switch the sense of skew_min and skew_max. + (skew_min.min(skew_max).floor(), skew_min.max(skew_max).ceil()) +} + +fn skew_bitmap( + bitmap: &[u8], + width: usize, + height: usize, + left: i32, + top: i32, + skew_factor: f32, + vertical: bool, // TODO: vertical skew not yet implemented! +) -> (Vec<u8>, usize, i32) { + let stride = width * 4; + // Calculate the skewed horizontal offsets of the bottom and top of the glyph. + let (skew_min, skew_max) = get_skew_bounds(top - height as i32, top, skew_factor, vertical); + // Allocate enough extra width for the min/max skew offsets. + let skew_width = width + (skew_max - skew_min) as usize; + let mut skew_buffer = vec![0u8; skew_width * height * 4]; + for y in 0 .. height { + // Calculate a skew offset at the vertical center of the current row. + let offset = (top as f32 - y as f32 - 0.5) * skew_factor - skew_min; + // Get a blend factor in 0..256 constant across all pixels in the row. + let blend = (offset.fract() * 256.0) as u32; + let src_row = y * stride; + let dest_row = (y * skew_width + offset.floor() as usize) * 4; + let mut prev_px = [0u32; 4]; + for (src, dest) in + bitmap[src_row .. src_row + stride].chunks(4).zip( + skew_buffer[dest_row .. dest_row + stride].chunks_mut(4) + ) { + let px = [src[0] as u32, src[1] as u32, src[2] as u32, src[3] as u32]; + // Blend current pixel with previous pixel based on blend factor. + let next_px = [px[0] * blend, px[1] * blend, px[2] * blend, px[3] * blend]; + dest[0] = ((((px[0] << 8) - next_px[0]) + prev_px[0] + 128) >> 8) as u8; + dest[1] = ((((px[1] << 8) - next_px[1]) + prev_px[1] + 128) >> 8) as u8; + dest[2] = ((((px[2] << 8) - next_px[2]) + prev_px[2] + 128) >> 8) as u8; + dest[3] = ((((px[3] << 8) - next_px[3]) + prev_px[3] + 128) >> 8) as u8; + // Save the remainder for blending onto the next pixel. + prev_px = next_px; + } + // If the skew misaligns the final pixel, write out the remainder. + if blend > 0 { + let dest = &mut skew_buffer[dest_row + stride .. dest_row + stride + 4]; + dest[0] = ((prev_px[0] + 128) >> 8) as u8; + dest[1] = ((prev_px[1] + 128) >> 8) as u8; + dest[2] = ((prev_px[2] + 128) >> 8) as u8; + dest[3] = ((prev_px[3] + 128) >> 8) as u8; + } + } + (skew_buffer, skew_width, left + skew_min as i32) +} + +fn transpose_bitmap(bitmap: &[u8], width: usize, height: usize) -> Vec<u8> { + let mut transposed = vec![0u8; width * height * 4]; + for (y, row) in bitmap.chunks(width * 4).enumerate() { + let mut offset = y * 4; + for src in row.chunks(4) { + transposed[offset .. offset + 4].copy_from_slice(src); + offset += height * 4; + } + } + transposed +} + +fn flip_bitmap_x(bitmap: &mut [u8], width: usize, height: usize) { + assert!(bitmap.len() == width * height * 4); + let pixels = unsafe { slice::from_raw_parts_mut(bitmap.as_mut_ptr() as *mut u32, width * height) }; + for row in pixels.chunks_mut(width) { + row.reverse(); + } +} + +fn flip_bitmap_y(bitmap: &mut [u8], width: usize, height: usize) { + assert!(bitmap.len() == width * height * 4); + let pixels = unsafe { slice::from_raw_parts_mut(bitmap.as_mut_ptr() as *mut u32, width * height) }; + for y in 0 .. height / 2 { + let low_row = y * width; + let high_row = (height - 1 - y) * width; + for x in 0 .. width { + pixels.swap(low_row + x, high_row + x); + } + } +} + +impl FontContext { + pub fn distribute_across_threads() -> bool { + false + } + + pub fn new() -> FontContext { + FontContext { + fonts: FastHashMap::default(), + } + } + + pub fn add_raw_font(&mut self, font_key: &FontKey, bytes: Arc<Vec<u8>>, index: u32) { + if !self.fonts.contains_key(font_key) { + let len = bytes.len(); + match FONT_CACHE.lock().unwrap().add_font(FontTemplate::Raw(bytes, index)) { + Ok(font) => self.fonts.insert(*font_key, font), + Err(result) => panic!("adding raw font failed: {} bytes, err={:?}", len, result), + }; + } + } + + pub fn add_native_font(&mut self, font_key: &FontKey, native_font_handle: NativeFontHandle) { + if !self.fonts.contains_key(font_key) { + let path = native_font_handle.path.to_string_lossy().into_owned(); + match FONT_CACHE.lock().unwrap().add_font(FontTemplate::Native(native_font_handle)) { + Ok(font) => self.fonts.insert(*font_key, font), + Err(result) => panic!("adding native font failed: file={} err={:?}", path, result), + }; + } + } + + pub fn delete_font(&mut self, font_key: &FontKey) { + if let Some(cached) = self.fonts.remove(font_key) { + // If the only references to this font are the FontCache and this FontContext, + // then delete the font as there are no other existing users. + if Arc::strong_count(&cached) <= 2 { + FONT_CACHE.lock().unwrap().delete_font(cached); + } + } + } + + pub fn delete_font_instance(&mut self, _instance: &FontInstance) { + } + + fn load_glyph(&mut self, font: &FontInstance, glyph: &GlyphKey) + -> Option<(MutexGuard<CachedFont>, FT_GlyphSlot, f32)> { + let mut cached = self.fonts.get(&font.font_key)?.lock().ok()?; + let face = cached.face; + + let mm_var = cached.mm_var; + if !mm_var.is_null() && font.variations != cached.variations { + cached.variations.clear(); + cached.variations.extend_from_slice(&font.variations); + + unsafe { + let num_axis = (*mm_var).num_axis; + let mut coords: Vec<FT_Fixed> = Vec::with_capacity(num_axis as usize); + for i in 0 .. num_axis { + let axis = (*mm_var).axis.offset(i as isize); + let mut value = (*axis).def; + for var in &font.variations { + if var.tag as FT_ULong == (*axis).tag { + value = (var.value * 65536.0 + 0.5) as FT_Fixed; + value = cmp::min(value, (*axis).maximum); + value = cmp::max(value, (*axis).minimum); + break; + } + } + coords.push(value); + } + let res = FT_Set_Var_Design_Coordinates(face, num_axis, coords.as_mut_ptr()); + debug_assert!(succeeded(res)); + } + } + + let mut load_flags = FT_LOAD_DEFAULT; + let FontInstancePlatformOptions { mut hinting, .. } = font.platform_options.unwrap_or_default(); + // Disable hinting if there is a non-axis-aligned transform. + if font.synthetic_italics.is_enabled() || + ((font.transform.scale_x != 0.0 || font.transform.scale_y != 0.0) && + (font.transform.skew_x != 0.0 || font.transform.skew_y != 0.0)) { + hinting = FontHinting::None; + } + match (hinting, font.render_mode) { + (FontHinting::None, _) => load_flags |= FT_LOAD_NO_HINTING, + (FontHinting::Mono, _) => load_flags = FT_LOAD_TARGET_MONO, + (FontHinting::Light, _) => load_flags = FT_LOAD_TARGET_LIGHT, + (FontHinting::LCD, FontRenderMode::Subpixel) => { + load_flags = if font.flags.contains(FontInstanceFlags::LCD_VERTICAL) { + FT_LOAD_TARGET_LCD_V + } else { + FT_LOAD_TARGET_LCD + }; + if font.flags.contains(FontInstanceFlags::FORCE_AUTOHINT) { + load_flags |= FT_LOAD_FORCE_AUTOHINT; + } + } + _ => { + if font.flags.contains(FontInstanceFlags::FORCE_AUTOHINT) { + load_flags |= FT_LOAD_FORCE_AUTOHINT; + } + } + } + + if font.flags.contains(FontInstanceFlags::NO_AUTOHINT) { + load_flags |= FT_LOAD_NO_AUTOHINT; + } + if !font.flags.contains(FontInstanceFlags::EMBEDDED_BITMAPS) { + load_flags |= FT_LOAD_NO_BITMAP; + } + + let face_flags = unsafe { (*face).face_flags }; + if (face_flags & (FT_FACE_FLAG_FIXED_SIZES as FT_Long)) != 0 { + // We only set FT_LOAD_COLOR if there are bitmap strikes; + // COLR (color-layer) fonts are handled internally by Gecko, and + // WebRender is just asked to paint individual layers. + load_flags |= FT_LOAD_COLOR; + } + + load_flags |= FT_LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH; + + let (x_scale, y_scale) = font.transform.compute_scale().unwrap_or((1.0, 1.0)); + let req_size = font.size.to_f64_px(); + + let mut result = if (face_flags & (FT_FACE_FLAG_FIXED_SIZES as FT_Long)) != 0 && + (face_flags & (FT_FACE_FLAG_SCALABLE as FT_Long)) == 0 && + (load_flags & FT_LOAD_NO_BITMAP) == 0 { + unsafe { FT_Set_Transform(face, ptr::null_mut(), ptr::null_mut()) }; + self.choose_bitmap_size(face, req_size * y_scale) + } else { + let mut shape = font.transform.invert_scale(x_scale, y_scale); + if font.flags.contains(FontInstanceFlags::FLIP_X) { + shape = shape.flip_x(); + } + if font.flags.contains(FontInstanceFlags::FLIP_Y) { + shape = shape.flip_y(); + } + if font.flags.contains(FontInstanceFlags::TRANSPOSE) { + shape = shape.swap_xy(); + } + let (mut tx, mut ty) = (0.0, 0.0); + if font.synthetic_italics.is_enabled() { + let (shape_, (tx_, ty_)) = font.synthesize_italics(shape, y_scale * req_size); + shape = shape_; + tx = tx_; + ty = ty_; + }; + let mut ft_shape = FT_Matrix { + xx: (shape.scale_x * 65536.0) as FT_Fixed, + xy: (shape.skew_x * -65536.0) as FT_Fixed, + yx: (shape.skew_y * -65536.0) as FT_Fixed, + yy: (shape.scale_y * 65536.0) as FT_Fixed, + }; + // The delta vector for FT_Set_Transform is in units of 1/64 pixel. + let mut ft_delta = FT_Vector { + x: (tx * 64.0) as FT_F26Dot6, + y: (ty * -64.0) as FT_F26Dot6, + }; + unsafe { + FT_Set_Transform(face, &mut ft_shape, &mut ft_delta); + FT_Set_Char_Size( + face, + (req_size * x_scale * 64.0 + 0.5) as FT_F26Dot6, + (req_size * y_scale * 64.0 + 0.5) as FT_F26Dot6, + 0, + 0, + ) + } + }; + + if !succeeded(result) { + error!("Unable to set glyph size and transform: {}", result); + //let raw_error = unsafe { FT_Error_String(result) }; + //if !raw_error.is_ptr() { + // error!("\tcode {:?}", CStr::from_ptr(raw_error)); + //} + debug!( + "\t[{}] for size {:?} and scale {:?} from font {:?}", + glyph.index(), + req_size, + (x_scale, y_scale), + font.font_key, + ); + return None; + } + + result = unsafe { FT_Load_Glyph(face, glyph.index() as FT_UInt, load_flags as FT_Int32) }; + if !succeeded(result) { + error!("Unable to load glyph: {}", result); + //let raw_error = unsafe { FT_Error_String(result) }; + //if !raw_error.is_ptr() { + // error!("\tcode {:?}", CStr::from_ptr(raw_error)); + //} + debug!( + "\t[{}] with flags {:?} from font {:?}", + glyph.index(), + load_flags, + font.font_key, + ); + return None; + } + + let slot = unsafe { (*face).glyph }; + assert!(slot != ptr::null_mut()); + + if font.flags.contains(FontInstanceFlags::SYNTHETIC_BOLD) { + mozilla_glyphslot_embolden_less(slot); + } + + let format = unsafe { (*slot).format }; + match format { + FT_Glyph_Format::FT_GLYPH_FORMAT_BITMAP => { + let bitmap_size = unsafe { (*(*(*slot).face).size).metrics.y_ppem }; + Some((cached, slot, req_size as f32 / bitmap_size as f32)) + } + FT_Glyph_Format::FT_GLYPH_FORMAT_OUTLINE => Some((cached, slot, 1.0)), + _ => { + error!("Unsupported format"); + debug!("format={:?}", format); + None + } + } + } + + fn pad_bounding_box(font: &FontInstance, cbox: &mut FT_BBox) { + // Apply extra pixel of padding for subpixel AA, due to the filter. + if font.render_mode == FontRenderMode::Subpixel { + // Using an LCD filter may add one full pixel to each side if support is built in. + // As of FreeType 2.8.1, an LCD filter is always used regardless of settings + // if support for the patent-encumbered LCD filter algorithms is not built in. + // Thus, the only reasonable way to guess padding is to unconditonally add it if + // subpixel AA is used. + let lcd_extra_pixels = 1; + let padding = (lcd_extra_pixels * 64) as FT_Pos; + if font.flags.contains(FontInstanceFlags::LCD_VERTICAL) { + cbox.yMin -= padding; + cbox.yMax += padding; + } else { + cbox.xMin -= padding; + cbox.xMax += padding; + } + } + } + + // Get the bounding box for a glyph, accounting for sub-pixel positioning. + fn get_bounding_box( + slot: FT_GlyphSlot, + font: &FontInstance, + glyph: &GlyphKey, + scale: f32, + ) -> FT_BBox { + // Get the estimated bounding box from FT (control points). + let mut cbox = FT_BBox { xMin: 0, yMin: 0, xMax: 0, yMax: 0 }; + + unsafe { + FT_Outline_Get_CBox(&(*slot).outline, &mut cbox); + } + + // For spaces and other non-printable characters, early out. + if unsafe { (*slot).outline.n_contours } == 0 { + return cbox; + } + + Self::pad_bounding_box(font, &mut cbox); + + // Offset the bounding box by subpixel positioning. + // Convert to 26.6 fixed point format for FT. + let (dx, dy) = font.get_subpx_offset(glyph); + let (dx, dy) = ( + (dx / scale as f64 * 64.0 + 0.5) as FT_Pos, + -(dy / scale as f64 * 64.0 + 0.5) as FT_Pos, + ); + cbox.xMin += dx; + cbox.xMax += dx; + cbox.yMin += dy; + cbox.yMax += dy; + + // Outset the box to device pixel boundaries + cbox.xMin &= !63; + cbox.yMin &= !63; + cbox.xMax = (cbox.xMax + 63) & !63; + cbox.yMax = (cbox.yMax + 63) & !63; + + cbox + } + + fn get_glyph_dimensions_impl( + slot: FT_GlyphSlot, + font: &FontInstance, + glyph: &GlyphKey, + scale: f32, + use_transform: bool, + ) -> Option<GlyphDimensions> { + let format = unsafe { (*slot).format }; + let (mut left, mut top, mut width, mut height) = match format { + FT_Glyph_Format::FT_GLYPH_FORMAT_BITMAP => { + unsafe { ( + (*slot).bitmap_left as i32, + (*slot).bitmap_top as i32, + (*slot).bitmap.width as i32, + (*slot).bitmap.rows as i32, + ) } + } + FT_Glyph_Format::FT_GLYPH_FORMAT_OUTLINE => { + let cbox = Self::get_bounding_box(slot, font, glyph, scale); + ( + (cbox.xMin >> 6) as i32, + (cbox.yMax >> 6) as i32, + ((cbox.xMax - cbox.xMin) >> 6) as i32, + ((cbox.yMax - cbox.yMin) >> 6) as i32, + ) + } + _ => return None, + }; + let mut advance = unsafe { (*slot).metrics.horiAdvance as f32 / 64.0 }; + if use_transform { + if scale != 1.0 { + let x0 = left as f32 * scale; + let x1 = width as f32 * scale + x0; + let y1 = top as f32 * scale; + let y0 = y1 - height as f32 * scale; + left = x0.round() as i32; + top = y1.round() as i32; + width = (x1.ceil() - x0.floor()) as i32; + height = (y1.ceil() - y0.floor()) as i32; + advance *= scale; + } + // An outline glyph's cbox would have already been transformed inside FT_Load_Glyph, + // so only handle bitmap glyphs which are not handled by FT_Load_Glyph. + if format == FT_Glyph_Format::FT_GLYPH_FORMAT_BITMAP { + if font.synthetic_italics.is_enabled() { + let (skew_min, skew_max) = get_skew_bounds( + top - height as i32, + top, + font.synthetic_italics.to_skew(), + font.flags.contains(FontInstanceFlags::VERTICAL), + ); + left += skew_min as i32; + width += (skew_max - skew_min) as i32; + } + if font.flags.contains(FontInstanceFlags::TRANSPOSE) { + mem::swap(&mut width, &mut height); + mem::swap(&mut left, &mut top); + left -= width as i32; + top += height as i32; + } + if font.flags.contains(FontInstanceFlags::FLIP_X) { + left = -(left + width as i32); + } + if font.flags.contains(FontInstanceFlags::FLIP_Y) { + top = -(top - height as i32); + } + } + } + Some(GlyphDimensions { + left, + top, + width, + height, + advance, + }) + } + + pub fn get_glyph_index(&mut self, font_key: FontKey, ch: char) -> Option<u32> { + let cached = self.fonts.get(&font_key)?.lock().ok()?; + let face = cached.face; + unsafe { + let idx = FT_Get_Char_Index(face, ch as _); + if idx != 0 { + Some(idx) + } else { + None + } + } + } + + pub fn get_glyph_dimensions( + &mut self, + font: &FontInstance, + key: &GlyphKey, + ) -> Option<GlyphDimensions> { + let (_cached, slot, scale) = self.load_glyph(font, key)?; + Self::get_glyph_dimensions_impl(slot, &font, key, scale, true) + } + + fn choose_bitmap_size(&self, face: FT_Face, requested_size: f64) -> FT_Error { + let mut best_dist = unsafe { *(*face).available_sizes.offset(0) }.y_ppem as f64 / 64.0 - requested_size; + let mut best_size = 0; + let num_fixed_sizes = unsafe { (*face).num_fixed_sizes }; + for i in 1 .. num_fixed_sizes { + // Distance is positive if strike is larger than desired size, + // or negative if smaller. If previously a found smaller strike, + // then prefer a larger strike. Otherwise, minimize distance. + let dist = unsafe { *(*face).available_sizes.offset(i as isize) }.y_ppem as f64 / 64.0 - requested_size; + if (best_dist < 0.0 && dist >= best_dist) || dist.abs() <= best_dist { + best_dist = dist; + best_size = i; + } + } + unsafe { FT_Select_Size(face, best_size) } + } + + pub fn prepare_font(font: &mut FontInstance) { + match font.render_mode { + FontRenderMode::Mono => { + // In mono mode the color of the font is irrelevant. + font.color = ColorU::new(0xFF, 0xFF, 0xFF, 0xFF); + // Subpixel positioning is disabled in mono mode. + font.disable_subpixel_position(); + } + FontRenderMode::Alpha | FontRenderMode::Subpixel => { + // We don't do any preblending with FreeType currently, so the color is not used. + font.color = ColorU::new(0xFF, 0xFF, 0xFF, 0xFF); + } + } + } + + fn rasterize_glyph_outline( + slot: FT_GlyphSlot, + font: &FontInstance, + key: &GlyphKey, + scale: f32, + ) -> bool { + // Get the subpixel offsets in FT 26.6 format. + let (dx, dy) = font.get_subpx_offset(key); + let (dx, dy) = ( + (dx / scale as f64 * 64.0 + 0.5) as FT_Pos, + -(dy / scale as f64 * 64.0 + 0.5) as FT_Pos, + ); + + // Move the outline curves to be at the origin, taking + // into account the subpixel positioning. + unsafe { + let outline = &(*slot).outline; + let mut cbox = FT_BBox { xMin: 0, yMin: 0, xMax: 0, yMax: 0 }; + FT_Outline_Get_CBox(outline, &mut cbox); + Self::pad_bounding_box(font, &mut cbox); + FT_Outline_Translate( + outline, + dx - ((cbox.xMin + dx) & !63), + dy - ((cbox.yMin + dy) & !63), + ); + } + + let render_mode = match font.render_mode { + FontRenderMode::Mono => FT_Render_Mode::FT_RENDER_MODE_MONO, + FontRenderMode::Alpha => FT_Render_Mode::FT_RENDER_MODE_NORMAL, + FontRenderMode::Subpixel => if font.flags.contains(FontInstanceFlags::LCD_VERTICAL) { + FT_Render_Mode::FT_RENDER_MODE_LCD_V + } else { + FT_Render_Mode::FT_RENDER_MODE_LCD + }, + }; + let result = unsafe { FT_Render_Glyph(slot, render_mode) }; + if !succeeded(result) { + error!("Unable to rasterize"); + debug!( + "{:?} with {:?}, {:?}", + key, + render_mode, + result + ); + false + } else { + true + } + } + + pub fn begin_rasterize(font: &FontInstance) { + // The global LCD filter state is only used in subpixel rendering modes. + if font.render_mode == FontRenderMode::Subpixel { + let mut cache = FONT_CACHE.lock().unwrap(); + let FontInstancePlatformOptions { lcd_filter, .. } = font.platform_options.unwrap_or_default(); + // Check if the current LCD filter matches the requested one. + if cache.lcd_filter != lcd_filter { + // If the filter doesn't match, we have to wait for all other currently rasterizing threads + // that may use the LCD filter state to finish before we can override it. + while cache.lcd_filter_uses != 0 { + cache = LCD_FILTER_UNUSED.wait(cache).unwrap(); + } + // Finally set the LCD filter to the requested one now that the library is unused. + cache.lcd_filter = lcd_filter; + let filter = match lcd_filter { + FontLCDFilter::None => FT_LcdFilter::FT_LCD_FILTER_NONE, + FontLCDFilter::Default => FT_LcdFilter::FT_LCD_FILTER_DEFAULT, + FontLCDFilter::Light => FT_LcdFilter::FT_LCD_FILTER_LIGHT, + FontLCDFilter::Legacy => FT_LcdFilter::FT_LCD_FILTER_LEGACY, + }; + unsafe { + let result = FT_Library_SetLcdFilter(cache.lib, filter); + // Setting the legacy filter may fail, so just use the default filter instead. + if !succeeded(result) { + FT_Library_SetLcdFilter(cache.lib, FT_LcdFilter::FT_LCD_FILTER_DEFAULT); + } + } + } + cache.lcd_filter_uses += 1; + } + } + + pub fn end_rasterize(font: &FontInstance) { + if font.render_mode == FontRenderMode::Subpixel { + let mut cache = FONT_CACHE.lock().unwrap(); + // If this is the last use of the LCD filter, then signal that the LCD filter isn't used. + cache.lcd_filter_uses -= 1; + if cache.lcd_filter_uses == 0 { + LCD_FILTER_UNUSED.notify_all(); + } + } + } + + pub fn rasterize_glyph(&mut self, font: &FontInstance, key: &GlyphKey) -> GlyphRasterResult { + let (_cached, slot, scale) = self.load_glyph(font, key) + .ok_or(GlyphRasterError::LoadFailed)?; + + // Get dimensions of the glyph, to see if we need to rasterize it. + // Don't apply scaling to the dimensions, as the glyph cache needs to know the actual + // footprint of the glyph. + let dimensions = Self::get_glyph_dimensions_impl(slot, font, key, scale, false) + .ok_or(GlyphRasterError::LoadFailed)?; + let GlyphDimensions { mut left, mut top, width, height, .. } = dimensions; + + // For spaces and other non-printable characters, early out. + if width == 0 || height == 0 { + return Err(GlyphRasterError::LoadFailed); + } + + let format = unsafe { (*slot).format }; + match format { + FT_Glyph_Format::FT_GLYPH_FORMAT_BITMAP => {} + FT_Glyph_Format::FT_GLYPH_FORMAT_OUTLINE => { + if !Self::rasterize_glyph_outline(slot, font, key, scale) { + return Err(GlyphRasterError::LoadFailed); + } + } + _ => { + error!("Unsupported format"); + debug!("format={:?}", format); + return Err(GlyphRasterError::LoadFailed); + } + }; + + debug!( + "Rasterizing {:?} as {:?} with dimensions {:?}", + key, + font.render_mode, + dimensions + ); + + let bitmap = unsafe { &(*slot).bitmap }; + let pixel_mode = unsafe { mem::transmute(bitmap.pixel_mode as u32) }; + let (mut actual_width, mut actual_height) = match pixel_mode { + FT_Pixel_Mode::FT_PIXEL_MODE_LCD => { + assert!(bitmap.width % 3 == 0); + ((bitmap.width / 3) as usize, bitmap.rows as usize) + } + FT_Pixel_Mode::FT_PIXEL_MODE_LCD_V => { + assert!(bitmap.rows % 3 == 0); + (bitmap.width as usize, (bitmap.rows / 3) as usize) + } + FT_Pixel_Mode::FT_PIXEL_MODE_MONO | + FT_Pixel_Mode::FT_PIXEL_MODE_GRAY | + FT_Pixel_Mode::FT_PIXEL_MODE_BGRA => { + (bitmap.width as usize, bitmap.rows as usize) + } + _ => panic!("Unsupported mode"), + }; + + // If we need padding, we will need to expand the buffer size. + let (buffer_width, buffer_height, padding) = if font.use_texture_padding() { + (actual_width + 2, actual_height + 2, 1) + } else { + (actual_width, actual_height, 0) + }; + + let mut final_buffer = vec![0u8; buffer_width * buffer_height * 4]; + + // Extract the final glyph from FT format into BGRA8 format, which is + // what WR expects. + let subpixel_bgr = font.flags.contains(FontInstanceFlags::SUBPIXEL_BGR); + let mut src_row = bitmap.buffer; + let mut dest = 4 * padding * (padding + buffer_width); + let actual_end = final_buffer.len() - 4 * padding * (buffer_width + 1); + while dest < actual_end { + let mut src = src_row; + let row_end = dest + actual_width * 4; + match pixel_mode { + FT_Pixel_Mode::FT_PIXEL_MODE_MONO => { + while dest < row_end { + // Cast the byte to signed so that we can left shift each bit into + // the top bit, then right shift to fill out the bits with 0s or 1s. + let mut byte: i8 = unsafe { *src as i8 }; + src = unsafe { src.offset(1) }; + let byte_end = cmp::min(row_end, dest + 8 * 4); + while dest < byte_end { + let alpha = (byte >> 7) as u8; + final_buffer[dest + 0] = alpha; + final_buffer[dest + 1] = alpha; + final_buffer[dest + 2] = alpha; + final_buffer[dest + 3] = alpha; + dest += 4; + byte <<= 1; + } + } + } + FT_Pixel_Mode::FT_PIXEL_MODE_GRAY => { + while dest < row_end { + let alpha = unsafe { *src }; + final_buffer[dest + 0] = alpha; + final_buffer[dest + 1] = alpha; + final_buffer[dest + 2] = alpha; + final_buffer[dest + 3] = alpha; + src = unsafe { src.offset(1) }; + dest += 4; + } + } + FT_Pixel_Mode::FT_PIXEL_MODE_LCD => { + while dest < row_end { + let (mut r, g, mut b) = unsafe { (*src, *src.offset(1), *src.offset(2)) }; + if subpixel_bgr { + mem::swap(&mut r, &mut b); + } + final_buffer[dest + 0] = b; + final_buffer[dest + 1] = g; + final_buffer[dest + 2] = r; + final_buffer[dest + 3] = max(max(b, g), r); + src = unsafe { src.offset(3) }; + dest += 4; + } + } + FT_Pixel_Mode::FT_PIXEL_MODE_LCD_V => { + while dest < row_end { + let (mut r, g, mut b) = + unsafe { (*src, *src.offset(bitmap.pitch as isize), + *src.offset((2 * bitmap.pitch) as isize)) }; + if subpixel_bgr { + mem::swap(&mut r, &mut b); + } + final_buffer[dest + 0] = b; + final_buffer[dest + 1] = g; + final_buffer[dest + 2] = r; + final_buffer[dest + 3] = max(max(b, g), r); + src = unsafe { src.offset(1) }; + dest += 4; + } + src_row = unsafe { src_row.offset((2 * bitmap.pitch) as isize) }; + } + FT_Pixel_Mode::FT_PIXEL_MODE_BGRA => { + // The source is premultiplied BGRA data. + let dest_slice = &mut final_buffer[dest .. row_end]; + let src_slice = unsafe { slice::from_raw_parts(src, dest_slice.len()) }; + dest_slice.copy_from_slice(src_slice); + } + _ => panic!("Unsupported mode"), + } + src_row = unsafe { src_row.offset(bitmap.pitch as isize) }; + dest = row_end + 8 * padding; + } + + if font.use_texture_padding() { + left -= padding as i32; + top += padding as i32; + actual_width = buffer_width; + actual_height = buffer_height; + } + + match format { + FT_Glyph_Format::FT_GLYPH_FORMAT_BITMAP => { + if font.synthetic_italics.is_enabled() { + let (skew_buffer, skew_width, skew_left) = skew_bitmap( + &final_buffer, + actual_width, + actual_height, + left, + top, + font.synthetic_italics.to_skew(), + font.flags.contains(FontInstanceFlags::VERTICAL), + ); + final_buffer = skew_buffer; + actual_width = skew_width; + left = skew_left; + } + if font.flags.contains(FontInstanceFlags::TRANSPOSE) { + final_buffer = transpose_bitmap(&final_buffer, actual_width, actual_height); + mem::swap(&mut actual_width, &mut actual_height); + mem::swap(&mut left, &mut top); + left -= actual_width as i32; + top += actual_height as i32; + } + if font.flags.contains(FontInstanceFlags::FLIP_X) { + flip_bitmap_x(&mut final_buffer, actual_width, actual_height); + left = -(left + actual_width as i32); + } + if font.flags.contains(FontInstanceFlags::FLIP_Y) { + flip_bitmap_y(&mut final_buffer, actual_width, actual_height); + top = -(top - actual_height as i32); + } + } + FT_Glyph_Format::FT_GLYPH_FORMAT_OUTLINE => { + unsafe { + left += (*slot).bitmap_left; + top += (*slot).bitmap_top - height as i32; + } + } + _ => {} + } + + let glyph_format = match (pixel_mode, format) { + (FT_Pixel_Mode::FT_PIXEL_MODE_LCD, _) | + (FT_Pixel_Mode::FT_PIXEL_MODE_LCD_V, _) => font.get_subpixel_glyph_format(), + (FT_Pixel_Mode::FT_PIXEL_MODE_BGRA, _) => GlyphFormat::ColorBitmap, + (_, FT_Glyph_Format::FT_GLYPH_FORMAT_BITMAP) => GlyphFormat::Bitmap, + _ => font.get_alpha_glyph_format(), + }; + + Ok(RasterizedGlyph { + left: left as f32, + top: top as f32, + width: actual_width as i32, + height: actual_height as i32, + scale, + format: glyph_format, + bytes: final_buffer, + }) + } +} + diff --git a/gfx/wr/wr_glyph_rasterizer/src/platform/windows/font.rs b/gfx/wr/wr_glyph_rasterizer/src/platform/windows/font.rs new file mode 100644 index 0000000000..81232d70c4 --- /dev/null +++ b/gfx/wr/wr_glyph_rasterizer/src/platform/windows/font.rs @@ -0,0 +1,639 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use api::{FontInstanceFlags, FontKey, FontRenderMode, FontVariation}; +use api::{ColorU, GlyphDimensions, NativeFontHandle}; +use dwrote; +use crate::gamma_lut::{ColorLut, GammaLut}; +use crate::rasterizer::{FontInstance, FontTransform, GlyphKey}; +use crate::rasterizer::{GlyphFormat, GlyphRasterError, GlyphRasterResult, RasterizedGlyph}; +use crate::rasterizer::apply_multistrike_bold; +use crate::types::{FastHashMap, FastHashSet}; +use std::borrow::Borrow; +use std::collections::hash_map::Entry; +use std::hash::{Hash, Hasher}; +use std::path::Path; +use std::sync::{Arc, Mutex}; +use api::FontInstancePlatformOptions; +use std::mem; + +lazy_static! { + static ref DEFAULT_FONT_DESCRIPTOR: dwrote::FontDescriptor = dwrote::FontDescriptor { + family_name: "Arial".to_owned(), + weight: dwrote::FontWeight::Regular, + stretch: dwrote::FontStretch::Normal, + style: dwrote::FontStyle::Normal, + }; +} + +type CachedFontKey = Arc<Path>; + +// A cached dwrote font file that is shared among all faces. +// Each face holds a CachedFontKey to keep track of how many users of the font there are. +struct CachedFont { + key: CachedFontKey, + file: dwrote::FontFile, +} + +// FontFile contains a ComPtr<IDWriteFontFile>, but DWrite font files are threadsafe. +unsafe impl Send for CachedFont {} + +impl PartialEq for CachedFont { + fn eq(&self, other: &CachedFont) -> bool { + self.key == other.key + } +} +impl Eq for CachedFont {} + +impl Hash for CachedFont { + fn hash<H: Hasher>(&self, state: &mut H) { + self.key.hash(state); + } +} + +impl Borrow<Path> for CachedFont { + fn borrow(&self) -> &Path { + &*self.key + } +} + +lazy_static! { + // This is effectively a weak map of dwrote FontFiles. CachedFonts are entered into the + // cache when there are any FontFaces using them. CachedFonts are removed from the cache + // when there are no more FontFaces using them at all. + static ref FONT_CACHE: Mutex<FastHashSet<CachedFont>> = Mutex::new(FastHashSet::default()); +} + +struct FontFace { + cached: Option<CachedFontKey>, + file: dwrote::FontFile, + index: u32, + face: dwrote::FontFace, +} + +pub struct FontContext { + fonts: FastHashMap<FontKey, FontFace>, + variations: FastHashMap<(FontKey, dwrote::DWRITE_FONT_SIMULATIONS, Vec<FontVariation>), dwrote::FontFace>, + gamma_luts: FastHashMap<(u16, u8), GammaLut>, +} + +// DirectWrite is safe to use on multiple threads and non-shareable resources are +// all hidden inside their font context. +unsafe impl Send for FontContext {} + +fn dwrite_texture_type(render_mode: FontRenderMode) -> dwrote::DWRITE_TEXTURE_TYPE { + match render_mode { + FontRenderMode::Mono => dwrote::DWRITE_TEXTURE_ALIASED_1x1, + FontRenderMode::Alpha | + FontRenderMode::Subpixel => dwrote::DWRITE_TEXTURE_CLEARTYPE_3x1, + } +} + +fn dwrite_measure_mode( + font: &FontInstance, + bitmaps: bool, +) -> dwrote::DWRITE_MEASURING_MODE { + if bitmaps || font.flags.contains(FontInstanceFlags::FORCE_GDI) { + dwrote::DWRITE_MEASURING_MODE_GDI_CLASSIC + } else { + match font.render_mode { + FontRenderMode::Mono => dwrote::DWRITE_MEASURING_MODE_GDI_CLASSIC, + FontRenderMode::Alpha | FontRenderMode::Subpixel => dwrote::DWRITE_MEASURING_MODE_NATURAL, + } + } +} + +fn dwrite_render_mode( + font_face: &dwrote::FontFace, + font: &FontInstance, + em_size: f32, + measure_mode: dwrote::DWRITE_MEASURING_MODE, + bitmaps: bool, +) -> dwrote::DWRITE_RENDERING_MODE { + let dwrite_render_mode = match font.render_mode { + FontRenderMode::Mono => dwrote::DWRITE_RENDERING_MODE_ALIASED, + FontRenderMode::Alpha | FontRenderMode::Subpixel => { + if bitmaps || font.flags.contains(FontInstanceFlags::FORCE_GDI) { + dwrote::DWRITE_RENDERING_MODE_GDI_CLASSIC + } else if font.flags.contains(FontInstanceFlags::FORCE_SYMMETRIC) { + dwrote::DWRITE_RENDERING_MODE_CLEARTYPE_NATURAL_SYMMETRIC + } else if font.flags.contains(FontInstanceFlags::NO_SYMMETRIC) { + dwrote::DWRITE_RENDERING_MODE_CLEARTYPE_NATURAL + } else { + font_face.get_recommended_rendering_mode_default_params(em_size, 1.0, measure_mode) + } + } + }; + + if dwrite_render_mode == dwrote::DWRITE_RENDERING_MODE_OUTLINE { + // Outline mode is not supported + return dwrote::DWRITE_RENDERING_MODE_CLEARTYPE_NATURAL_SYMMETRIC; + } + + dwrite_render_mode +} + +fn is_bitmap_font(font: &FontInstance) -> bool { + // If bitmaps are requested, then treat as a bitmap font to disable transforms. + // If mono AA is requested, let that take priority over using bitmaps. + font.render_mode != FontRenderMode::Mono && + font.flags.contains(FontInstanceFlags::EMBEDDED_BITMAPS) +} + +impl FontContext { + pub fn distribute_across_threads() -> bool { + true + } + + pub fn new() -> FontContext { + FontContext { + fonts: FastHashMap::default(), + variations: FastHashMap::default(), + gamma_luts: FastHashMap::default(), + } + } + + fn add_font_descriptor(&mut self, font_key: &FontKey, desc: &dwrote::FontDescriptor) { + let system_fc = dwrote::FontCollection::get_system(false); + if let Some(font) = system_fc.get_font_from_descriptor(desc) { + let face = font.create_font_face(); + let file = face.get_files().pop().unwrap(); + let index = face.get_index(); + self.fonts.insert(*font_key, FontFace { cached: None, file, index, face }); + } + } + + pub fn add_raw_font(&mut self, font_key: &FontKey, data: Arc<Vec<u8>>, index: u32) { + if self.fonts.contains_key(font_key) { + return; + } + + if let Some(file) = dwrote::FontFile::new_from_data(data) { + if let Ok(face) = file.create_face(index, dwrote::DWRITE_FONT_SIMULATIONS_NONE) { + self.fonts.insert(*font_key, FontFace { cached: None, file, index, face }); + return; + } + } + // XXX add_raw_font needs to have a way to return an error + debug!("DWrite WR failed to load font from data, using Arial instead"); + self.add_font_descriptor(font_key, &DEFAULT_FONT_DESCRIPTOR); + } + + pub fn add_native_font(&mut self, font_key: &FontKey, font_handle: NativeFontHandle) { + if self.fonts.contains_key(font_key) { + return; + } + + let index = font_handle.index; + let mut cache = FONT_CACHE.lock().unwrap(); + // Check to see if the font is already in the cache. If so, reuse it. + if let Some(font) = cache.get(font_handle.path.as_path()) { + if let Ok(face) = font.file.create_face(index, dwrote::DWRITE_FONT_SIMULATIONS_NONE) { + self.fonts.insert( + *font_key, + FontFace { cached: Some(font.key.clone()), file: font.file.clone(), index, face }, + ); + return; + } + } + if let Some(file) = dwrote::FontFile::new_from_path(&font_handle.path) { + // The font is not in the cache yet, so try to create the font and insert it in the cache. + if let Ok(face) = file.create_face(index, dwrote::DWRITE_FONT_SIMULATIONS_NONE) { + let key: CachedFontKey = font_handle.path.into(); + self.fonts.insert( + *font_key, + FontFace { cached: Some(key.clone()), file: file.clone(), index, face }, + ); + cache.insert(CachedFont { key, file }); + return; + } + } + + // XXX add_native_font needs to have a way to return an error + debug!("DWrite WR failed to load font from path, using Arial instead"); + self.add_font_descriptor(font_key, &DEFAULT_FONT_DESCRIPTOR); + } + + pub fn delete_font(&mut self, font_key: &FontKey) { + if let Some(face) = self.fonts.remove(font_key) { + self.variations.retain(|k, _| k.0 != *font_key); + // Check if this was a cached font. + if let Some(key) = face.cached { + let mut cache = FONT_CACHE.lock().unwrap(); + // If there are only two references left, that means only this face and + // the cache are using the font. So remove it from the cache. + if Arc::strong_count(&key) == 2 { + cache.remove(&*key); + } + } + } + } + + pub fn delete_font_instance(&mut self, instance: &FontInstance) { + // Ensure we don't keep around excessive amounts of stale variations. + if !instance.variations.is_empty() { + let sims = if instance.flags.contains(FontInstanceFlags::SYNTHETIC_BOLD) { + dwrote::DWRITE_FONT_SIMULATIONS_BOLD + } else { + dwrote::DWRITE_FONT_SIMULATIONS_NONE + }; + self.variations.remove(&(instance.font_key, sims, instance.variations.clone())); + } + } + + // Assumes RGB format from dwrite, which is 3 bytes per pixel as dwrite + // doesn't output an alpha value via GlyphRunAnalysis::CreateAlphaTexture + #[allow(dead_code)] + fn print_glyph_data(&self, data: &[u8], width: usize, height: usize) { + // Rust doesn't have step_by support on stable :( + for i in 0 .. height { + let current_height = i * width * 3; + + for pixel in data[current_height .. current_height + (width * 3)].chunks(3) { + let r = pixel[0]; + let g = pixel[1]; + let b = pixel[2]; + debug!("({}, {}, {}) ", r, g, b,); + } + } + } + + fn get_font_face( + &mut self, + font: &FontInstance, + ) -> &dwrote::FontFace { + if !font.flags.contains(FontInstanceFlags::SYNTHETIC_BOLD) && + font.variations.is_empty() { + return &self.fonts.get(&font.font_key).unwrap().face; + } + let sims = if font.flags.contains(FontInstanceFlags::SYNTHETIC_BOLD) { + dwrote::DWRITE_FONT_SIMULATIONS_BOLD + } else { + dwrote::DWRITE_FONT_SIMULATIONS_NONE + }; + match self.variations.entry((font.font_key, sims, font.variations.clone())) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => { + let normal_face = self.fonts.get(&font.font_key).unwrap(); + if !font.variations.is_empty() { + if let Some(var_face) = normal_face.face.create_font_face_with_variations( + sims, + &font.variations.iter().map(|var| { + dwrote::DWRITE_FONT_AXIS_VALUE { + // OpenType tags are big-endian, but DWrite wants little-endian. + axisTag: var.tag.swap_bytes(), + value: var.value, + } + }).collect::<Vec<_>>(), + ) { + return entry.insert(var_face); + } + } + let var_face = normal_face.file + .create_face(normal_face.index, sims) + .unwrap_or_else(|_| normal_face.face.clone()); + entry.insert(var_face) + } + } + } + + fn create_glyph_analysis( + &mut self, + font: &FontInstance, + key: &GlyphKey, + size: f32, + transform: Option<dwrote::DWRITE_MATRIX>, + bitmaps: bool, + ) -> Result<(dwrote::GlyphRunAnalysis, dwrote::DWRITE_TEXTURE_TYPE, dwrote::RECT), dwrote::HRESULT> { + let face = self.get_font_face(font); + let glyph = key.index() as u16; + let advance = 0.0f32; + let offset = dwrote::GlyphOffset { + advanceOffset: 0.0, + ascenderOffset: 0.0, + }; + + let glyph_run = dwrote::DWRITE_GLYPH_RUN { + fontFace: unsafe { face.as_ptr() }, + fontEmSize: size, // size in DIPs (1/96", same as CSS pixels) + glyphCount: 1, + glyphIndices: &glyph, + glyphAdvances: &advance, + glyphOffsets: &offset, + isSideways: 0, + bidiLevel: 0, + }; + + let dwrite_measure_mode = dwrite_measure_mode(font, bitmaps); + let dwrite_render_mode = dwrite_render_mode( + face, + font, + size, + dwrite_measure_mode, + bitmaps, + ); + + let analysis = dwrote::GlyphRunAnalysis::create( + &glyph_run, + 1.0, + transform, + dwrite_render_mode, + dwrite_measure_mode, + 0.0, + 0.0, + )?; + let texture_type = dwrite_texture_type(font.render_mode); + let bounds = analysis.get_alpha_texture_bounds(texture_type)?; + // If the bounds are empty, then we might not be able to render the glyph with cleartype. + // Try again with aliased rendering to check if that works instead. + if font.render_mode != FontRenderMode::Mono && + (bounds.left == bounds.right || bounds.top == bounds.bottom) { + let analysis2 = dwrote::GlyphRunAnalysis::create( + &glyph_run, + 1.0, + transform, + dwrote::DWRITE_RENDERING_MODE_ALIASED, + dwrite_measure_mode, + 0.0, + 0.0, + )?; + let bounds2 = analysis2.get_alpha_texture_bounds(dwrote::DWRITE_TEXTURE_ALIASED_1x1)?; + if bounds2.left != bounds2.right && bounds2.top != bounds2.bottom { + return Ok((analysis2, dwrote::DWRITE_TEXTURE_ALIASED_1x1, bounds2)); + } + } + Ok((analysis, texture_type, bounds)) + } + + pub fn get_glyph_index(&mut self, font_key: FontKey, ch: char) -> Option<u32> { + let face = &self.fonts.get(&font_key).unwrap().face; + let indices = face.get_glyph_indices(&[ch as u32]); + indices.first().map(|idx| *idx as u32) + } + + pub fn get_glyph_dimensions( + &mut self, + font: &FontInstance, + key: &GlyphKey, + ) -> Option<GlyphDimensions> { + let (size, x_scale, y_scale, bitmaps, transform) = Self::get_glyph_parameters(font, key); + let (_, _, bounds) = self.create_glyph_analysis(font, key, size, transform, bitmaps).ok()?; + + let width = (bounds.right - bounds.left) as i32; + let height = (bounds.bottom - bounds.top) as i32; + + // Alpha texture bounds can sometimes return an empty rect + // Such as for spaces + if width == 0 || height == 0 { + return None; + } + + let (strike_scale, pixel_step) = if bitmaps { + (y_scale, 1.0) + } else { + (x_scale, y_scale / x_scale) + }; + let extra_strikes = font.get_extra_strikes(FontInstanceFlags::MULTISTRIKE_BOLD, strike_scale); + let extra_width = extra_strikes as f64 * pixel_step; + + let face = self.get_font_face(font); + face.get_design_glyph_metrics(&[key.index() as u16], false) + .first() + .map(|metrics| { + let em_size = size / 16.; + let design_units_per_pixel = face.metrics().metrics0().designUnitsPerEm as f32 / 16. as f32; + let scaled_design_units_to_pixels = em_size / design_units_per_pixel; + let advance = metrics.advanceWidth as f32 * scaled_design_units_to_pixels; + + GlyphDimensions { + left: bounds.left, + top: -bounds.top, + width: width + extra_width.ceil() as i32, + height, + advance: advance + extra_width as f32, + } + }) + } + + // DWrite ClearType gives us values in RGB, but WR expects BGRA. + fn convert_to_bgra( + &self, + pixels: &[u8], + width: usize, + height: usize, + texture_type: dwrote::DWRITE_TEXTURE_TYPE, + render_mode: FontRenderMode, + bitmaps: bool, + subpixel_bgr: bool, + padding: usize, + ) -> (Vec<u8>, bool) { + let (buffer_width, buffer_height) = (width + padding * 2, height + padding * 2); + let buffer_length = buffer_width * buffer_height * 4; + let mut bgra_pixels: Vec<u8> = vec![0; buffer_length]; + + match (texture_type, render_mode, bitmaps) { + (dwrote::DWRITE_TEXTURE_ALIASED_1x1, _, _) => { + assert!(width * height == pixels.len()); + let mut i = 0; + for row in padding .. height + padding { + let row_offset = row * buffer_width; + for col in padding .. width + padding { + let offset = (row_offset + col) * 4; + let alpha = pixels[i]; + i += 1; + bgra_pixels[offset + 0] = alpha; + bgra_pixels[offset + 1] = alpha; + bgra_pixels[offset + 2] = alpha; + bgra_pixels[offset + 3] = alpha; + } + } + (bgra_pixels, false) + } + (_, FontRenderMode::Subpixel, false) => { + assert!(width * height * 3 == pixels.len()); + let mut i = 0; + for row in padding .. height + padding { + let row_offset = row * buffer_width; + for col in padding .. width + padding { + let offset = (row_offset + col) * 4; + let (mut r, g, mut b) = (pixels[i + 0], pixels[i + 1], pixels[i + 2]); + if subpixel_bgr { + mem::swap(&mut r, &mut b); + } + i += 3; + bgra_pixels[offset + 0] = b; + bgra_pixels[offset + 1] = g; + bgra_pixels[offset + 2] = r; + bgra_pixels[offset + 3] = 0xff; + } + } + (bgra_pixels, true) + } + _ => { + assert!(width * height * 3 == pixels.len()); + let mut i = 0; + for row in padding .. height + padding { + let row_offset = row * buffer_width; + for col in padding .. width + padding { + let offset = (row_offset + col) * 4; + // Only take the G channel, as its closest to D2D + let alpha = pixels[i + 1] as u8; + i += 3; + bgra_pixels[offset + 0] = alpha; + bgra_pixels[offset + 1] = alpha; + bgra_pixels[offset + 2] = alpha; + bgra_pixels[offset + 3] = alpha; + } + } + (bgra_pixels, false) + } + } + } + + pub fn prepare_font(font: &mut FontInstance) { + match font.render_mode { + FontRenderMode::Mono => { + // In mono mode the color of the font is irrelevant. + font.color = ColorU::new(255, 255, 255, 255); + // Subpixel positioning is disabled in mono mode. + font.disable_subpixel_position(); + } + FontRenderMode::Alpha => { + font.color = font.color.luminance_color().quantize(); + } + FontRenderMode::Subpixel => { + font.color = font.color.quantize(); + } + } + } + + fn get_glyph_parameters(font: &FontInstance, key: &GlyphKey) + -> (f32, f64, f64, bool, Option<dwrote::DWRITE_MATRIX>) { + let (x_scale, y_scale) = font.transform.compute_scale().unwrap_or((1.0, 1.0)); + let scaled_size = font.size.to_f64_px() * y_scale; + let bitmaps = is_bitmap_font(font); + let (mut shape, (mut x_offset, mut y_offset)) = if bitmaps { + (FontTransform::identity(), (0.0, 0.0)) + } else { + (font.transform.invert_scale(y_scale, y_scale), font.get_subpx_offset(key)) + }; + if font.flags.contains(FontInstanceFlags::FLIP_X) { + shape = shape.flip_x(); + } + if font.flags.contains(FontInstanceFlags::FLIP_Y) { + shape = shape.flip_y(); + } + if font.flags.contains(FontInstanceFlags::TRANSPOSE) { + shape = shape.swap_xy(); + } + let (mut tx, mut ty) = (0.0, 0.0); + if font.synthetic_italics.is_enabled() { + let (shape_, (tx_, ty_)) = font.synthesize_italics(shape, scaled_size); + shape = shape_; + tx = tx_; + ty = ty_; + }; + x_offset += tx; + y_offset += ty; + let transform = if !shape.is_identity() || (x_offset, y_offset) != (0.0, 0.0) { + Some(dwrote::DWRITE_MATRIX { + m11: shape.scale_x, + m12: shape.skew_y, + m21: shape.skew_x, + m22: shape.scale_y, + dx: x_offset as f32, + dy: y_offset as f32, + }) + } else { + None + }; + (scaled_size as f32, x_scale, y_scale, bitmaps, transform) + } + + pub fn begin_rasterize(_font: &FontInstance) { + } + + pub fn end_rasterize(_font: &FontInstance) { + } + + pub fn rasterize_glyph(&mut self, font: &FontInstance, key: &GlyphKey) -> GlyphRasterResult { + let (size, x_scale, y_scale, bitmaps, transform) = Self::get_glyph_parameters(font, key); + let (analysis, texture_type, bounds) = self.create_glyph_analysis(font, key, size, transform, bitmaps) + .or(Err(GlyphRasterError::LoadFailed))?; + let mut width = (bounds.right - bounds.left) as i32; + let height = (bounds.bottom - bounds.top) as i32; + // Alpha texture bounds can sometimes return an empty rect + // Such as for spaces + if width == 0 || height == 0 { + return Err(GlyphRasterError::LoadFailed); + } + + let pixels = analysis.create_alpha_texture(texture_type, bounds).or(Err(GlyphRasterError::LoadFailed))?; + let padding = if font.use_texture_padding() { 1 } else { 0 }; + let (mut bgra_pixels, is_subpixel) = self.convert_to_bgra( + &pixels, + width as usize, + height as usize, + texture_type, + font.render_mode, + bitmaps, + font.flags.contains(FontInstanceFlags::SUBPIXEL_BGR), + padding as usize, + ); + + // Apply multistrike bold, if necessary, and replace the current pixels with it. + let (strike_scale, pixel_step) = if bitmaps { + (y_scale, 1.0) + } else { + (x_scale, y_scale / x_scale) + }; + let extra_strikes = font.get_extra_strikes(FontInstanceFlags::MULTISTRIKE_BOLD, strike_scale); + if extra_strikes > 0 { + let (bold_pixels, bold_width) = apply_multistrike_bold( + &bgra_pixels, + (width + padding * 2) as usize, + (height + padding * 2) as usize, + is_subpixel, + extra_strikes, + pixel_step, + ); + width = bold_width as i32 - padding * 2; + bgra_pixels = bold_pixels; + } + + let FontInstancePlatformOptions { gamma, contrast, cleartype_level, .. } = + font.platform_options.unwrap_or_default(); + let gamma_lut = self.gamma_luts + .entry((gamma, contrast)) + .or_insert_with(|| + GammaLut::new( + contrast as f32 / 100.0, + gamma as f32 / 100.0, + gamma as f32 / 100.0, + )); + if is_subpixel { + gamma_lut.preblend_scaled(&mut bgra_pixels, font.color, cleartype_level); + } else { + gamma_lut.preblend(&mut bgra_pixels, font.color); + } + + let format = if bitmaps { + GlyphFormat::Bitmap + } else if texture_type == dwrote::DWRITE_TEXTURE_ALIASED_1x1 { + font.get_alpha_glyph_format() + } else { + font.get_glyph_format() + }; + + Ok(RasterizedGlyph { + left: (bounds.left - padding) as f32, + top: (-bounds.top + padding) as f32, + width: width + padding * 2, + height: height + padding * 2, + scale: (if bitmaps { y_scale.recip() } else { 1.0 }) as f32, + format, + bytes: bgra_pixels, + }) + } +} diff --git a/gfx/wr/wr_glyph_rasterizer/src/profiler.rs b/gfx/wr/wr_glyph_rasterizer/src/profiler.rs new file mode 100644 index 0000000000..89d126e3fc --- /dev/null +++ b/gfx/wr/wr_glyph_rasterizer/src/profiler.rs @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/// A profiler for profiling rasterize time. +pub trait GlyphRasterizeProfiler { + fn start_time(&mut self); + + fn end_time(&mut self) -> f64; + + fn set(&mut self, value: f64); +} diff --git a/gfx/wr/wr_glyph_rasterizer/src/rasterizer.rs b/gfx/wr/wr_glyph_rasterizer/src/rasterizer.rs new file mode 100644 index 0000000000..5153e60c54 --- /dev/null +++ b/gfx/wr/wr_glyph_rasterizer/src/rasterizer.rs @@ -0,0 +1,1879 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use api::{FontInstanceData, FontInstanceFlags, FontInstanceKey}; +use api::{FontInstanceOptions, FontInstancePlatformOptions}; +use api::{FontKey, FontRenderMode, FontSize, FontTemplate, FontVariation}; +use api::{ColorU, GlyphIndex, GlyphDimensions, SyntheticItalics}; +use api::{IdNamespace, BlobImageResources}; +use api::channel::crossbeam::{unbounded, Receiver, Sender}; +use api::units::*; +use api::ImageFormat; +use crate::platform::font::FontContext; +use crate::profiler::GlyphRasterizeProfiler; +use crate::types::{FastHashMap, FastHashSet}; +use crate::telemetry::Telemetry; +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; +use rayon::ThreadPool; +use rayon::prelude::*; +use euclid::approxeq::ApproxEq; +use smallvec::SmallVec; +use std::cmp; +use std::cell::Cell; +use std::hash::{Hash, Hasher}; +use std::mem; +use std::ops::Deref; +use std::sync::{Arc, Condvar, Mutex, MutexGuard, Weak}; +use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use std::sync::atomic::{AtomicBool, Ordering}; + +pub static GLYPH_FLASHING: AtomicBool = AtomicBool::new(false); + +impl FontContexts { + /// Get access to the font context associated to the current thread. + pub fn lock_current_context(&self) -> MutexGuard<FontContext> { + match self.current_worker_id() { + Some(id) => self.lock_context(id), + None => self.lock_any_context(), + } + } + + pub(in super) fn current_worker_id(&self) -> Option<usize> { + self.workers.current_thread_index() + } +} + +thread_local! { + pub static SEED: Cell<u32> = Cell::new(0); +} + +// super simple random to avoid dependency on rand +fn random() -> u32 { + SEED.with(|seed| { + seed.set(seed.get().wrapping_mul(22695477).wrapping_add(1)); + seed.get() + }) +} + +impl GlyphRasterizer { + pub fn request_glyphs<F>( + &mut self, + font: FontInstance, + glyph_keys: &[GlyphKey], + mut handle: F, + ) + where F: FnMut(&GlyphKey) -> bool + { + assert!(self.has_font(font.font_key)); + + let mut batch_size = 0; + + // select glyphs that have not been requested yet. + for key in glyph_keys { + if !handle(key) { + continue; + } + + // Increment the total number of glyphs that are pending. This is used to determine + // later whether to use worker threads for the remaining glyphs during resolve time. + self.pending_glyph_count += 1; + self.glyph_request_count += 1; + + // Find a batch container for the font instance for this glyph. Use get_mut to avoid + // cloning the font instance, since this is the common path. + match self.pending_glyph_requests.get_mut(&font) { + Some(container) => { + container.push(*key); + batch_size = container.len(); + } + None => { + // If no batch exists for this font instance, add the glyph to a new one. + self.pending_glyph_requests.insert( + font.clone(), + smallvec![*key], + ); + } + } + } + + // If the batch for this font instance is big enough, kick off an async + // job to start rasterizing these glyphs on other threads now. + if batch_size >= 8 { + let container = self.pending_glyph_requests.get_mut(&font).unwrap(); + let glyphs = mem::replace(container, SmallVec::new()); + self.flush_glyph_requests(font, glyphs, true); + } + } + + pub fn enable_multithreading(&mut self, enable: bool) { + self.enable_multithreading = enable; + } + + /// Internal method to flush a list of glyph requests to a set of worker threads, + /// or process on this thread if there isn't much work to do (in which case the + /// overhead of processing these on a thread is unlikely to be a performance win). + fn flush_glyph_requests( + &mut self, + font: FontInstance, + glyphs: SmallVec<[GlyphKey; 16]>, + use_workers: bool, + ) { + let font = Arc::new(font); + let font_contexts = Arc::clone(&self.font_contexts); + self.pending_glyph_jobs += glyphs.len(); + self.pending_glyph_count -= glyphs.len(); + + let can_use_r8_format = self.can_use_r8_format; + + let job_font = font.clone(); + let process_glyph = move |key: &GlyphKey| -> GlyphRasterJob { + profile_scope!("glyph-raster"); + let mut context = font_contexts.lock_current_context(); + let mut job = GlyphRasterJob { + font: Arc::clone(&job_font), + key: key.clone(), + result: context.rasterize_glyph(&job_font, key), + }; + + if let Ok(ref mut glyph) = job.result { + // Sanity check. + let bpp = 4; // We always render glyphs in 32 bits RGBA format. + assert_eq!( + glyph.bytes.len(), + bpp * (glyph.width * glyph.height) as usize + ); + + // a quick-and-dirty monochrome over + fn over(dst: u8, src: u8) -> u8 { + let a = src as u32; + let a = 256 - a; + let dst = ((dst as u32 * a) >> 8) as u8; + src + dst + } + + if GLYPH_FLASHING.load(Ordering::Relaxed) { + let color = (random() & 0xff) as u8; + for i in &mut glyph.bytes { + *i = over(*i, color); + } + } + + assert_eq!((glyph.left.fract(), glyph.top.fract()), (0.0, 0.0)); + + // Check if the glyph has a bitmap that needs to be downscaled. + glyph.downscale_bitmap_if_required(&job_font); + + // Convert from BGRA8 to R8 if required. In the future we can make it the + // backends' responsibility to output glyphs in the desired format, + // potentially reducing the number of copies. + if glyph.format.image_format(can_use_r8_format).bytes_per_pixel() == 1 { + glyph.bytes = glyph.bytes + .chunks_mut(4) + .map(|pixel| pixel[3]) + .collect::<Vec<_>>(); + } + } + + job + }; + + // if the number of glyphs is small, do it inline to avoid the threading overhead; + // send the result into glyph_tx so downstream code can't tell the difference. + if self.enable_multithreading && use_workers { + // spawn an async task to get off of the render backend thread as early as + // possible and in that task use rayon's fork join dispatch to rasterize the + // glyphs in the thread pool. + profile_scope!("spawning process_glyph jobs"); + self.workers.install(|| { + FontContext::begin_rasterize(&font); + // If the FontContext supports distributing a font across multiple threads, + // then use par_iter so different glyphs of the same font are processed on + // multiple threads. + if FontContext::distribute_across_threads() { + glyphs.par_iter().for_each(|key| { + let job = process_glyph(key); + self.glyph_tx.send(job).unwrap(); + }); + } else { + // For FontContexts that prefer to localize a font to a single thread, + // just process all the glyphs on the same worker to avoid contention. + for key in glyphs { + let job = process_glyph(&key); + self.glyph_tx.send(job).unwrap(); + } + } + FontContext::end_rasterize(&font); + }); + } else { + FontContext::begin_rasterize(&font); + for key in glyphs { + let job = process_glyph(&key); + self.glyph_tx.send(job).unwrap(); + } + FontContext::end_rasterize(&font); + } + } + + pub fn resolve_glyphs<F, G>( + &mut self, + mut handle: F, + profile: &mut G, + ) + where + F: FnMut(GlyphRasterJob, bool), + G: GlyphRasterizeProfiler, + { + profile.start_time(); + let timer_id = Telemetry::start_rasterize_glyphs_time(); + + // Work around the borrow checker, since we call flush_glyph_requests below + let mut pending_glyph_requests = mem::replace( + &mut self.pending_glyph_requests, + FastHashMap::default(), + ); + // If we have a large amount of remaining work to do, spawn to worker threads, + // even if that work is shared among a number of different font instances. + let use_workers = self.pending_glyph_count >= 8; + for (font, pending_glyphs) in pending_glyph_requests.drain() { + self.flush_glyph_requests( + font, + pending_glyphs, + use_workers, + ); + } + // Restore this so that we don't heap allocate next frame + self.pending_glyph_requests = pending_glyph_requests; + debug_assert_eq!(self.pending_glyph_count, 0); + debug_assert!(self.pending_glyph_requests.is_empty()); + + if self.glyph_request_count > 0 { + profile.set(self.glyph_request_count as f64); + self.glyph_request_count = 0; + } + + profile_scope!("resolve_glyphs"); + // TODO: rather than blocking until all pending glyphs are available + // we could try_recv and steal work from the thread pool to take advantage + // of the fact that this thread is alive and we avoid the added latency + // of blocking it. + let mut jobs = { + profile_scope!("blocking wait on glyph_rx"); + self.glyph_rx.iter().take(self.pending_glyph_jobs).collect::<Vec<_>>() + }; + assert_eq!(jobs.len(), self.pending_glyph_jobs, "BUG: Didn't receive all pending glyphs!"); + self.pending_glyph_jobs = 0; + + // Ensure that the glyphs are always processed in the same + // order for a given text run (since iterating a hash set doesn't + // guarantee order). This can show up as very small float inaccuracy + // differences in rasterizers due to the different coordinates + // that text runs get associated with by the texture cache allocator. + jobs.sort_by(|a, b| (*a.font).cmp(&*b.font).then(a.key.cmp(&b.key))); + + for job in jobs { + handle(job, self.can_use_r8_format); + } + + // Now that we are done with the critical path (rendering the glyphs), + // we can schedule removing the fonts if needed. + self.remove_dead_fonts(); + + Telemetry::stop_and_accumulate_rasterize_glyphs_time(timer_id); + profile.end_time(); + } +} + +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct FontTransform { + pub scale_x: f32, + pub skew_x: f32, + pub skew_y: f32, + pub scale_y: f32, +} + +// Floats don't impl Hash/Eq/Ord... +impl Eq for FontTransform {} +impl Ord for FontTransform { + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.partial_cmp(other).unwrap_or(cmp::Ordering::Equal) + } +} +impl Hash for FontTransform { + fn hash<H: Hasher>(&self, state: &mut H) { + // Note: this is inconsistent with the Eq impl for -0.0 (don't care). + self.scale_x.to_bits().hash(state); + self.skew_x.to_bits().hash(state); + self.skew_y.to_bits().hash(state); + self.scale_y.to_bits().hash(state); + } +} + +impl FontTransform { + const QUANTIZE_SCALE: f32 = 1024.0; + + pub fn new(scale_x: f32, skew_x: f32, skew_y: f32, scale_y: f32) -> Self { + FontTransform { scale_x, skew_x, skew_y, scale_y } + } + + pub fn identity() -> Self { + FontTransform::new(1.0, 0.0, 0.0, 1.0) + } + + #[allow(dead_code)] + pub fn is_identity(&self) -> bool { + *self == FontTransform::identity() + } + + pub fn quantize(&self) -> Self { + FontTransform::new( + (self.scale_x * Self::QUANTIZE_SCALE).round() / Self::QUANTIZE_SCALE, + (self.skew_x * Self::QUANTIZE_SCALE).round() / Self::QUANTIZE_SCALE, + (self.skew_y * Self::QUANTIZE_SCALE).round() / Self::QUANTIZE_SCALE, + (self.scale_y * Self::QUANTIZE_SCALE).round() / Self::QUANTIZE_SCALE, + ) + } + + #[allow(dead_code)] + pub fn determinant(&self) -> f64 { + self.scale_x as f64 * self.scale_y as f64 - self.skew_y as f64 * self.skew_x as f64 + } + + #[allow(dead_code)] + pub fn compute_scale(&self) -> Option<(f64, f64)> { + let det = self.determinant(); + if det != 0.0 { + let x_scale = (self.scale_x as f64).hypot(self.skew_y as f64); + let y_scale = det.abs() / x_scale; + Some((x_scale, y_scale)) + } else { + None + } + } + + #[allow(dead_code)] + pub fn pre_scale(&self, scale_x: f32, scale_y: f32) -> Self { + FontTransform::new( + self.scale_x * scale_x, + self.skew_x * scale_y, + self.skew_y * scale_x, + self.scale_y * scale_y, + ) + } + + #[allow(dead_code)] + pub fn scale(&self, scale: f32) -> Self { self.pre_scale(scale, scale) } + + #[allow(dead_code)] + pub fn invert_scale(&self, x_scale: f64, y_scale: f64) -> Self { + self.pre_scale(x_scale.recip() as f32, y_scale.recip() as f32) + } + + pub fn synthesize_italics(&self, angle: SyntheticItalics, size: f64, vertical: bool) -> (Self, (f64, f64)) { + let skew_factor = angle.to_skew(); + if vertical { + // origin delta to be applied so that we effectively skew around + // the middle rather than edge of the glyph + let (tx, ty) = (0.0, -size * 0.5 * skew_factor as f64); + (FontTransform::new( + self.scale_x + self.skew_x * skew_factor, + self.skew_x, + self.skew_y + self.scale_y * skew_factor, + self.scale_y, + ), (self.scale_x as f64 * tx + self.skew_x as f64 * ty, + self.skew_y as f64 * tx + self.scale_y as f64 * ty)) + } else { + (FontTransform::new( + self.scale_x, + self.skew_x - self.scale_x * skew_factor, + self.skew_y, + self.scale_y - self.skew_y * skew_factor, + ), (0.0, 0.0)) + } + } + + pub fn swap_xy(&self) -> Self { + FontTransform::new(self.skew_x, self.scale_x, self.scale_y, self.skew_y) + } + + pub fn flip_x(&self) -> Self { + FontTransform::new(-self.scale_x, self.skew_x, -self.skew_y, self.scale_y) + } + + pub fn flip_y(&self) -> Self { + FontTransform::new(self.scale_x, -self.skew_x, self.skew_y, -self.scale_y) + } + + pub fn transform(&self, point: &LayoutPoint) -> DevicePoint { + DevicePoint::new( + self.scale_x * point.x + self.skew_x * point.y, + self.skew_y * point.x + self.scale_y * point.y, + ) + } + + pub fn get_subpx_dir(&self) -> SubpixelDirection { + if self.skew_y.approx_eq(&0.0) { + // The X axis is not projected onto the Y axis + SubpixelDirection::Horizontal + } else if self.scale_x.approx_eq(&0.0) { + // The X axis has been swapped with the Y axis + SubpixelDirection::Vertical + } else { + // Use subpixel precision on all axes + SubpixelDirection::Mixed + } + } +} + +impl<'a> From<&'a LayoutToWorldTransform> for FontTransform { + fn from(xform: &'a LayoutToWorldTransform) -> Self { + FontTransform::new(xform.m11, xform.m21, xform.m12, xform.m22) + } +} + +// Some platforms (i.e. Windows) may have trouble rasterizing glyphs above this size. +// Ensure glyph sizes are reasonably limited to avoid that scenario. +pub const FONT_SIZE_LIMIT: f32 = 320.0; + +/// Immutable description of a font instance's shared state. +/// +/// `BaseFontInstance` can be identified by a `FontInstanceKey` to avoid hashing it. +#[derive(Clone, Debug, Ord, PartialOrd, MallocSizeOf)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct BaseFontInstance { + /// + pub instance_key: FontInstanceKey, + /// + pub font_key: FontKey, + /// + pub size: FontSize, + /// + pub options: FontInstanceOptions, + /// + #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))] + pub platform_options: Option<FontInstancePlatformOptions>, + /// + pub variations: Vec<FontVariation>, +} + +impl BaseFontInstance { + pub fn new( + instance_key: FontInstanceKey, + font_key: FontKey, + size: f32, + options: Option<FontInstanceOptions>, + platform_options: Option<FontInstancePlatformOptions>, + variations: Vec<FontVariation>, + ) -> Self { + BaseFontInstance { + instance_key, + font_key, + size: size.into(), + options: options.unwrap_or_default(), + platform_options, + variations, + } + } +} + +impl Deref for BaseFontInstance { + type Target = FontInstanceOptions; + fn deref(&self) -> &FontInstanceOptions { + &self.options + } +} + +impl Hash for BaseFontInstance { + fn hash<H: Hasher>(&self, state: &mut H) { + // Skip the instance key. + self.font_key.hash(state); + self.size.hash(state); + self.options.hash(state); + self.platform_options.hash(state); + self.variations.hash(state); + } +} + +impl PartialEq for BaseFontInstance { + fn eq(&self, other: &BaseFontInstance) -> bool { + // Skip the instance key. + self.font_key == other.font_key && + self.size == other.size && + self.options == other.options && + self.platform_options == other.platform_options && + self.variations == other.variations + } +} +impl Eq for BaseFontInstance {} + +struct MappedFontKey { + font_key: FontKey, + template: FontTemplate, +} + +struct FontKeyMapLocked { + namespace: IdNamespace, + next_id: u32, + template_map: FastHashMap<FontTemplate, Arc<MappedFontKey>>, + key_map: FastHashMap<FontKey, Arc<MappedFontKey>>, +} + +/// A shared map from fonts key local to a namespace to shared font keys that +/// can be shared across many namespaces. Local keys are tracked in a hashmap +/// that stores a strong reference per mapping so that their count can be +/// tracked. A map of font templates is used to hash font templates to their +/// final shared key. The shared key will stay alive so long as there are +/// any strong references to the mapping entry. Care must be taken when +/// clearing namespaces of shared keys as this may trigger shared font keys +/// to expire which require individual processing. Shared font keys will be +/// created within the provided unique namespace. +#[derive(Clone)] +pub struct FontKeyMap(Arc<RwLock<FontKeyMapLocked>>); + +impl FontKeyMap { + pub fn new(namespace: IdNamespace) -> Self { + FontKeyMap(Arc::new(RwLock::new(FontKeyMapLocked { + namespace, + next_id: 1, + template_map: FastHashMap::default(), + key_map: FastHashMap::default(), + }))) + } + + fn lock(&self) -> RwLockReadGuard<FontKeyMapLocked> { + self.0.read().unwrap() + } + + fn lock_mut(&mut self) -> RwLockWriteGuard<FontKeyMapLocked> { + self.0.write().unwrap() + } + + pub fn keys(&self) -> Vec<FontKey> { + self.lock().key_map.keys().cloned().collect() + } + + pub fn map_key(&self, font_key: &FontKey) -> FontKey { + match self.lock().key_map.get(font_key) { + Some(mapped) => mapped.font_key, + None => *font_key, + } + } + + pub fn add_key(&mut self, font_key: &FontKey, template: &FontTemplate) -> Option<FontKey> { + let mut locked = self.lock_mut(); + if locked.key_map.contains_key(font_key) { + return None; + } + if let Some(mapped) = locked.template_map.get(template).cloned() { + locked.key_map.insert(*font_key, mapped); + return None; + } + let shared_key = FontKey::new(locked.namespace, locked.next_id); + locked.next_id += 1; + let mapped = Arc::new(MappedFontKey { + font_key: shared_key, + template: template.clone(), + }); + locked.template_map.insert(template.clone(), mapped.clone()); + locked.key_map.insert(*font_key, mapped); + Some(shared_key) + } + + pub fn delete_key(&mut self, font_key: &FontKey) -> Option<FontKey> { + let mut locked = self.lock_mut(); + let mapped = match locked.key_map.remove(font_key) { + Some(mapped) => mapped, + None => return Some(*font_key), + }; + if Arc::strong_count(&mapped) <= 2 { + // Only the last mapped key and template map point to it. + locked.template_map.remove(&mapped.template); + Some(mapped.font_key) + } else { + None + } + } + + pub fn clear_namespace(&mut self, namespace: IdNamespace) -> Vec<FontKey> { + let mut locked = self.lock_mut(); + locked.key_map.retain(|key, _| { + if key.0 == namespace { + false + } else { + true + } + }); + let mut deleted_keys = Vec::new(); + locked.template_map.retain(|_, mapped| { + if Arc::strong_count(mapped) <= 1 { + // Only the template map points to it. + deleted_keys.push(mapped.font_key); + false + } else { + true + } + }); + deleted_keys + } +} + +type FontTemplateMapLocked = FastHashMap<FontKey, FontTemplate>; + +/// A map of font keys to font templates that might hold both namespace-local +/// font templates as well as shared templates. +#[derive(Clone)] +pub struct FontTemplateMap(Arc<RwLock<FontTemplateMapLocked>>); + +impl FontTemplateMap { + pub fn new() -> Self { + FontTemplateMap(Arc::new(RwLock::new(FastHashMap::default()))) + } + + pub fn lock(&self) -> RwLockReadGuard<FontTemplateMapLocked> { + self.0.read().unwrap() + } + + fn lock_mut(&mut self) -> RwLockWriteGuard<FontTemplateMapLocked> { + self.0.write().unwrap() + } + + pub fn clear(&mut self) { + self.lock_mut().clear(); + } + + pub fn len(&self) -> usize { + self.lock().len() + } + + pub fn has_font(&self, key: &FontKey) -> bool { + self.lock().contains_key(key) + } + + pub fn get_font(&self, key: &FontKey) -> Option<FontTemplate> { + self.lock().get(key).cloned() + } + + pub fn add_font(&mut self, key: FontKey, template: FontTemplate) -> bool { + self.lock_mut().insert(key, template).is_none() + } + + pub fn delete_font(&mut self, key: &FontKey) -> Option<FontTemplate> { + self.lock_mut().remove(key) + } + + pub fn delete_fonts(&mut self, keys: &[FontKey]) { + if !keys.is_empty() { + let mut map = self.lock_mut(); + for key in keys { + map.remove(key); + } + } + } + + pub fn clear_namespace(&mut self, namespace: IdNamespace) -> Vec<FontKey> { + let mut deleted_keys = Vec::new(); + self.lock_mut().retain(|key, _| { + if key.0 == namespace { + deleted_keys.push(*key); + false + } else { + true + } + }); + deleted_keys + } +} + +struct FontInstanceKeyMapLocked { + namespace: IdNamespace, + next_id: u32, + instances: FastHashSet<Arc<BaseFontInstance>>, + key_map: FastHashMap<FontInstanceKey, Weak<BaseFontInstance>>, +} + +/// A map of namespace-local font instance keys to shared keys. Weak references +/// are used to track the liveness of each key mapping as other consumers of +/// BaseFontInstance might hold strong references to the entry. A mapping from +/// BaseFontInstance to the shared key is then used to determine which shared +/// key to assign to that instance. When the weak count of the mapping is zero, +/// the entry is allowed to expire. Again, care must be taken when clearing +/// a namespace within the key map as it may cause shared key expirations that +/// require individual processing. Shared instance keys will be created within +/// the provided unique namespace. +#[derive(Clone)] +pub struct FontInstanceKeyMap(Arc<RwLock<FontInstanceKeyMapLocked>>); + +impl FontInstanceKeyMap { + pub fn new(namespace: IdNamespace) -> Self { + FontInstanceKeyMap(Arc::new(RwLock::new(FontInstanceKeyMapLocked { + namespace, + next_id: 1, + instances: FastHashSet::default(), + key_map: FastHashMap::default(), + }))) + } + + fn lock(&self) -> RwLockReadGuard<FontInstanceKeyMapLocked> { + self.0.read().unwrap() + } + + fn lock_mut(&mut self) -> RwLockWriteGuard<FontInstanceKeyMapLocked> { + self.0.write().unwrap() + } + + pub fn keys(&self) -> Vec<FontInstanceKey> { + self.lock().key_map.keys().cloned().collect() + } + + pub fn map_key(&self, key: &FontInstanceKey) -> FontInstanceKey { + match self.lock().key_map.get(key).and_then(|weak| weak.upgrade()) { + Some(mapped) => mapped.instance_key, + None => *key, + } + } + + pub fn add_key(&mut self, mut instance: BaseFontInstance) -> Option<Arc<BaseFontInstance>> { + let mut locked = self.lock_mut(); + if locked.key_map.contains_key(&instance.instance_key) { + return None; + } + if let Some(weak) = locked.instances.get(&instance).map(|mapped| Arc::downgrade(mapped)) { + locked.key_map.insert(instance.instance_key, weak); + return None; + } + let unmapped_key = instance.instance_key; + instance.instance_key = FontInstanceKey::new(locked.namespace, locked.next_id); + locked.next_id += 1; + let shared_instance = Arc::new(instance); + locked.instances.insert(shared_instance.clone()); + locked.key_map.insert(unmapped_key, Arc::downgrade(&shared_instance)); + Some(shared_instance) + } + + pub fn delete_key(&mut self, key: &FontInstanceKey) -> Option<FontInstanceKey> { + let mut locked = self.lock_mut(); + let mapped = match locked.key_map.remove(key).and_then(|weak| weak.upgrade()) { + Some(mapped) => mapped, + None => return Some(*key), + }; + if Arc::weak_count(&mapped) == 0 { + // Only the instance set points to it. + locked.instances.remove(&mapped); + Some(mapped.instance_key) + } else { + None + } + } + + pub fn clear_namespace(&mut self, namespace: IdNamespace) -> Vec<FontInstanceKey> { + let mut locked = self.lock_mut(); + locked.key_map.retain(|key, _| { + if key.0 == namespace { + false + } else { + true + } + }); + let mut deleted_keys = Vec::new(); + locked.instances.retain(|mapped| { + if Arc::weak_count(mapped) == 0 { + // Only the instance set points to it. + deleted_keys.push(mapped.instance_key); + false + } else { + true + } + }); + deleted_keys + } +} + +type FontInstanceMapLocked = FastHashMap<FontInstanceKey, Arc<BaseFontInstance>>; + +/// A map of font instance data accessed concurrently from multiple threads. +#[derive(Clone)] +pub struct FontInstanceMap(Arc<RwLock<FontInstanceMapLocked>>); + +impl FontInstanceMap { + /// Creates an empty shared map. + pub fn new() -> Self { + FontInstanceMap(Arc::new(RwLock::new(FastHashMap::default()))) + } + + /// Acquires a read lock on the shared map. + pub fn lock(&self) -> RwLockReadGuard<FontInstanceMapLocked> { + self.0.read().unwrap() + } + + /// Acquires a read lock on the shared map. + fn lock_mut(&mut self) -> RwLockWriteGuard<FontInstanceMapLocked> { + self.0.write().unwrap() + } + + /// + pub fn clear(&mut self) { + self.lock_mut().clear(); + } + + /// + pub fn get_font_instance_data(&self, key: FontInstanceKey) -> Option<FontInstanceData> { + match self.lock().get(&key) { + Some(instance) => Some(FontInstanceData { + font_key: instance.font_key, + size: instance.size.into(), + options: Some(FontInstanceOptions { + render_mode: instance.render_mode, + flags: instance.flags, + bg_color: instance.bg_color, + synthetic_italics: instance.synthetic_italics, + }), + platform_options: instance.platform_options, + variations: instance.variations.clone(), + }), + None => None, + } + } + + /// + pub fn get_font_instance(&self, instance_key: FontInstanceKey) -> Option<Arc<BaseFontInstance>> { + let instance_map = self.lock(); + instance_map.get(&instance_key).cloned() + } + + /// + pub fn add_font_instance(&mut self, instance: Arc<BaseFontInstance>) { + self.lock_mut().insert(instance.instance_key, instance); + } + + /// + pub fn delete_font_instance(&mut self, instance_key: FontInstanceKey) { + self.lock_mut().remove(&instance_key); + } + + /// + pub fn delete_font_instances(&mut self, keys: &[FontInstanceKey]) { + if !keys.is_empty() { + let mut map = self.lock_mut(); + for key in keys { + map.remove(key); + } + } + } + + /// + pub fn clear_namespace(&mut self, namespace: IdNamespace) { + self.lock_mut().retain(|key, _| key.0 != namespace); + } +} + +/// Shared font resources that may need to be passed between multiple threads +/// such as font templates and font instances. They are individually protected +/// by locks to ensure safety. +#[derive(Clone)] +pub struct SharedFontResources { + pub templates: FontTemplateMap, + pub instances: FontInstanceMap, + pub font_keys: FontKeyMap, + pub instance_keys: FontInstanceKeyMap, +} + +impl SharedFontResources { + pub fn new(namespace: IdNamespace) -> Self { + SharedFontResources { + templates: FontTemplateMap::new(), + instances: FontInstanceMap::new(), + font_keys: FontKeyMap::new(namespace), + instance_keys: FontInstanceKeyMap::new(namespace), + } + } +} + +impl BlobImageResources for SharedFontResources { + fn get_font_data(&self, key: FontKey) -> Option<FontTemplate> { + let shared_key = self.font_keys.map_key(&key); + self.templates.get_font(&shared_key) + } + + fn get_font_instance_data(&self, key: FontInstanceKey) -> Option<FontInstanceData> { + let shared_key = self.instance_keys.map_key(&key); + self.instances.get_font_instance_data(shared_key) + } +} + +/// A mutable font instance description. +/// +/// Performance is sensitive to the size of this structure, so it should only contain +/// the fields that we need to modify from the original base font instance. +#[derive(Clone, Debug, Ord, PartialOrd)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct FontInstance { + pub base: Arc<BaseFontInstance>, + pub transform: FontTransform, + pub render_mode: FontRenderMode, + pub flags: FontInstanceFlags, + pub color: ColorU, + // The font size is in *device/raster* pixels, not logical pixels. + // It is stored as an f32 since we need sub-pixel sizes. + pub size: FontSize, +} + +impl Hash for FontInstance { + fn hash<H: Hasher>(&self, state: &mut H) { + // Hash only the base instance's key to avoid the cost of hashing + // the rest. + self.base.instance_key.hash(state); + self.transform.hash(state); + self.render_mode.hash(state); + self.flags.hash(state); + self.color.hash(state); + self.size.hash(state); + } +} + +impl PartialEq for FontInstance { + fn eq(&self, other: &FontInstance) -> bool { + // Compare only the base instance's key. + self.base.instance_key == other.base.instance_key && + self.transform == other.transform && + self.render_mode == other.render_mode && + self.flags == other.flags && + self.color == other.color && + self.size == other.size + } +} +impl Eq for FontInstance {} + +impl Deref for FontInstance { + type Target = BaseFontInstance; + fn deref(&self) -> &BaseFontInstance { + self.base.as_ref() + } +} + +impl MallocSizeOf for FontInstance { + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { 0 } +} + +impl FontInstance { + pub fn new( + base: Arc<BaseFontInstance>, + color: ColorU, + render_mode: FontRenderMode, + flags: FontInstanceFlags, + ) -> Self { + FontInstance { + transform: FontTransform::identity(), + color, + size: base.size, + base, + render_mode, + flags, + } + } + + pub fn from_base( + base: Arc<BaseFontInstance>, + ) -> Self { + let color = ColorU::new(0, 0, 0, 255); + let render_mode = base.render_mode; + let flags = base.flags; + Self::new(base, color, render_mode, flags) + } + + pub fn use_texture_padding(&self) -> bool { + self.flags.contains(FontInstanceFlags::TEXTURE_PADDING) + } + + pub fn use_transform_glyphs(&self) -> bool { + self.flags.contains(FontInstanceFlags::TRANSFORM_GLYPHS) + } + + pub fn get_alpha_glyph_format(&self) -> GlyphFormat { + if self.use_transform_glyphs() { GlyphFormat::TransformedAlpha } else { GlyphFormat::Alpha } + } + + pub fn get_subpixel_glyph_format(&self) -> GlyphFormat { + if self.use_transform_glyphs() { GlyphFormat::TransformedSubpixel } else { GlyphFormat::Subpixel } + } + + pub fn disable_subpixel_aa(&mut self) { + self.render_mode = self.render_mode.limit_by(FontRenderMode::Alpha); + } + + pub fn disable_subpixel_position(&mut self) { + self.flags.remove(FontInstanceFlags::SUBPIXEL_POSITION); + } + + pub fn use_subpixel_position(&self) -> bool { + self.flags.contains(FontInstanceFlags::SUBPIXEL_POSITION) && + self.render_mode != FontRenderMode::Mono + } + + pub fn get_subpx_dir(&self) -> SubpixelDirection { + if self.use_subpixel_position() { + let mut subpx_dir = self.transform.get_subpx_dir(); + if self.flags.contains(FontInstanceFlags::TRANSPOSE) { + subpx_dir = subpx_dir.swap_xy(); + } + subpx_dir + } else { + SubpixelDirection::None + } + } + + #[allow(dead_code)] + pub fn get_subpx_offset(&self, glyph: &GlyphKey) -> (f64, f64) { + if self.use_subpixel_position() { + let (dx, dy) = glyph.subpixel_offset(); + (dx.into(), dy.into()) + } else { + (0.0, 0.0) + } + } + + #[allow(dead_code)] + pub fn get_glyph_format(&self) -> GlyphFormat { + match self.render_mode { + FontRenderMode::Mono | FontRenderMode::Alpha => self.get_alpha_glyph_format(), + FontRenderMode::Subpixel => self.get_subpixel_glyph_format(), + } + } + + #[allow(dead_code)] + pub fn get_extra_strikes(&self, flags: FontInstanceFlags, x_scale: f64) -> usize { + if self.flags.intersects(flags) { + let mut bold_offset = self.size.to_f64_px() / 48.0; + if bold_offset < 1.0 { + bold_offset = 0.25 + 0.75 * bold_offset; + } + (bold_offset * x_scale).max(1.0).round() as usize + } else { + 0 + } + } + + pub fn synthesize_italics(&self, transform: FontTransform, size: f64) -> (FontTransform, (f64, f64)) { + transform.synthesize_italics(self.synthetic_italics, size, self.flags.contains(FontInstanceFlags::VERTICAL)) + } + + #[allow(dead_code)] + pub fn get_transformed_size(&self) -> f64 { + let (_, y_scale) = self.transform.compute_scale().unwrap_or((1.0, 1.0)); + self.size.to_f64_px() * y_scale + } +} + +#[repr(u32)] +#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug, Ord, PartialOrd)] +pub enum SubpixelDirection { + None = 0, + Horizontal, + Vertical, + Mixed, +} + +impl SubpixelDirection { + // Limit the subpixel direction to what is supported by the glyph format. + pub fn limit_by(self, glyph_format: GlyphFormat) -> Self { + match glyph_format { + GlyphFormat::Bitmap | + GlyphFormat::ColorBitmap => SubpixelDirection::None, + _ => self, + } + } + + pub fn swap_xy(self) -> Self { + match self { + SubpixelDirection::None | SubpixelDirection::Mixed => self, + SubpixelDirection::Horizontal => SubpixelDirection::Vertical, + SubpixelDirection::Vertical => SubpixelDirection::Horizontal, + } + } +} + +#[repr(u8)] +#[derive(Hash, Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum SubpixelOffset { + Zero = 0, + Quarter = 1, + Half = 2, + ThreeQuarters = 3, +} + +impl SubpixelOffset { + // Skia quantizes subpixel offsets into 1/4 increments. + // Given the absolute position, return the quantized increment + fn quantize(pos: f32) -> Self { + // Following the conventions of Gecko and Skia, we want + // to quantize the subpixel position, such that abs(pos) gives: + // [0.0, 0.125) -> Zero + // [0.125, 0.375) -> Quarter + // [0.375, 0.625) -> Half + // [0.625, 0.875) -> ThreeQuarters, + // [0.875, 1.0) -> Zero + // The unit tests below check for this. + let apos = ((pos - pos.floor()) * 8.0) as i32; + + match apos { + 1..=2 => SubpixelOffset::Quarter, + 3..=4 => SubpixelOffset::Half, + 5..=6 => SubpixelOffset::ThreeQuarters, + _ => SubpixelOffset::Zero, + } + } +} + +impl Into<f64> for SubpixelOffset { + fn into(self) -> f64 { + match self { + SubpixelOffset::Zero => 0.0, + SubpixelOffset::Quarter => 0.25, + SubpixelOffset::Half => 0.5, + SubpixelOffset::ThreeQuarters => 0.75, + } + } +} + +#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug, Ord, PartialOrd)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct GlyphKey(u32); + +impl GlyphKey { + pub fn new( + index: u32, + point: DevicePoint, + subpx_dir: SubpixelDirection, + ) -> Self { + let (dx, dy) = match subpx_dir { + SubpixelDirection::None => (0.0, 0.0), + SubpixelDirection::Horizontal => (point.x, 0.0), + SubpixelDirection::Vertical => (0.0, point.y), + SubpixelDirection::Mixed => (point.x, point.y), + }; + let sox = SubpixelOffset::quantize(dx); + let soy = SubpixelOffset::quantize(dy); + assert_eq!(0, index & 0xF0000000); + + GlyphKey(index | (sox as u32) << 28 | (soy as u32) << 30) + } + + pub fn index(&self) -> GlyphIndex { + self.0 & 0x0FFFFFFF + } + + fn subpixel_offset(&self) -> (SubpixelOffset, SubpixelOffset) { + let x = (self.0 >> 28) as u8 & 3; + let y = (self.0 >> 30) as u8 & 3; + unsafe { + (mem::transmute(x), mem::transmute(y)) + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[allow(dead_code)] +pub enum GlyphFormat { + Alpha, + TransformedAlpha, + Subpixel, + TransformedSubpixel, + Bitmap, + ColorBitmap, +} + +impl GlyphFormat { + /// Returns the ImageFormat that a glyph should be stored as in the texture cache. + /// can_use_r8_format should be set false on platforms where we have encountered + /// issues with R8 textures, so that we do not use them for glyphs. + pub fn image_format(&self, can_use_r8_format: bool) -> ImageFormat { + match *self { + GlyphFormat::Alpha | + GlyphFormat::TransformedAlpha | + GlyphFormat::Bitmap => { + if can_use_r8_format { + ImageFormat::R8 + } else { + ImageFormat::BGRA8 + } + } + GlyphFormat::Subpixel | + GlyphFormat::TransformedSubpixel | + GlyphFormat::ColorBitmap => ImageFormat::BGRA8, + } + } +} + +#[allow(dead_code)] +#[inline] +fn blend_strike_pixel(dest: u8, src: u32, src_alpha: u32) -> u8 { + // Assume premultiplied alpha such that src and dest are already multiplied + // by their respective alpha values and in range 0..=255. The rounded over + // blend is then (src * 255 + dest * (255 - src_alpha) + 128) / 255. + // We approximate (x + 128) / 255 as (x + 128 + ((x + 128) >> 8)) >> 8. + let x = src * 255 + dest as u32 * (255 - src_alpha) + 128; + ((x + (x >> 8)) >> 8) as u8 +} + +// Blends a single strike at a given offset into a destination buffer, assuming +// the destination has been allocated with enough extra space to accommodate the +// offset. +#[allow(dead_code)] +fn blend_strike( + dest_bitmap: &mut [u8], + src_bitmap: &[u8], + width: usize, + height: usize, + subpixel_mask: bool, + offset: f64, +) { + let dest_stride = dest_bitmap.len() / height; + let src_stride = width * 4; + let offset_integer = offset.floor() as usize * 4; + let offset_fract = (offset.fract() * 256.0) as u32; + for (src_row, dest_row) in src_bitmap.chunks(src_stride).zip(dest_bitmap.chunks_mut(dest_stride)) { + let mut prev_px = [0u32; 4]; + let dest_row_offset = &mut dest_row[offset_integer .. offset_integer + src_stride]; + for (src, dest) in src_row.chunks(4).zip(dest_row_offset.chunks_mut(4)) { + let px = [src[0] as u32, src[1] as u32, src[2] as u32, src[3] as u32]; + // Blend current pixel with previous pixel based on fractional offset. + let next_px = [px[0] * offset_fract, + px[1] * offset_fract, + px[2] * offset_fract, + px[3] * offset_fract]; + let offset_px = [(((px[0] << 8) - next_px[0]) + prev_px[0] + 128) >> 8, + (((px[1] << 8) - next_px[1]) + prev_px[1] + 128) >> 8, + (((px[2] << 8) - next_px[2]) + prev_px[2] + 128) >> 8, + (((px[3] << 8) - next_px[3]) + prev_px[3] + 128) >> 8]; + if subpixel_mask { + // Subpixel masks assume each component is an independent weight. + dest[0] = blend_strike_pixel(dest[0], offset_px[0], offset_px[0]); + dest[1] = blend_strike_pixel(dest[1], offset_px[1], offset_px[1]); + dest[2] = blend_strike_pixel(dest[2], offset_px[2], offset_px[2]); + dest[3] = blend_strike_pixel(dest[3], offset_px[3], offset_px[3]); + } else { + // Otherwise assume we have a premultiplied alpha BGRA value. + dest[0] = blend_strike_pixel(dest[0], offset_px[0], offset_px[3]); + dest[1] = blend_strike_pixel(dest[1], offset_px[1], offset_px[3]); + dest[2] = blend_strike_pixel(dest[2], offset_px[2], offset_px[3]); + dest[3] = blend_strike_pixel(dest[3], offset_px[3], offset_px[3]); + } + // Save the remainder for blending onto the next pixel. + prev_px = next_px; + } + if offset_fract > 0 { + // When there is fractional offset, there will be a remaining value + // from the previous pixel but no next pixel, so just use that. + let dest = &mut dest_row[offset_integer + src_stride .. ]; + let offset_px = [(prev_px[0] + 128) >> 8, + (prev_px[1] + 128) >> 8, + (prev_px[2] + 128) >> 8, + (prev_px[3] + 128) >> 8]; + if subpixel_mask { + dest[0] = blend_strike_pixel(dest[0], offset_px[0], offset_px[0]); + dest[1] = blend_strike_pixel(dest[1], offset_px[1], offset_px[1]); + dest[2] = blend_strike_pixel(dest[2], offset_px[2], offset_px[2]); + dest[3] = blend_strike_pixel(dest[3], offset_px[3], offset_px[3]); + } else { + dest[0] = blend_strike_pixel(dest[0], offset_px[0], offset_px[3]); + dest[1] = blend_strike_pixel(dest[1], offset_px[1], offset_px[3]); + dest[2] = blend_strike_pixel(dest[2], offset_px[2], offset_px[3]); + dest[3] = blend_strike_pixel(dest[3], offset_px[3], offset_px[3]); + } + } + } +} + +// Applies multistrike bold to a source bitmap. This assumes the source bitmap +// is a tighly packed slice of BGRA pixel values of exactly the specified width +// and height. The specified extra strikes and pixel step control where to put +// each strike. The pixel step is allowed to have a fractional offset and does +// not strictly need to be integer. +#[allow(dead_code)] +pub fn apply_multistrike_bold( + src_bitmap: &[u8], + width: usize, + height: usize, + subpixel_mask: bool, + extra_strikes: usize, + pixel_step: f64, +) -> (Vec<u8>, usize) { + let src_stride = width * 4; + // The amount of extra width added to the bitmap from the extra strikes. + let extra_width = (extra_strikes as f64 * pixel_step).ceil() as usize; + let dest_width = width + extra_width; + let dest_stride = dest_width * 4; + // Zero out the initial bitmap so any extra width is cleared. + let mut dest_bitmap = vec![0u8; dest_stride * height]; + for (src_row, dest_row) in src_bitmap.chunks(src_stride).zip(dest_bitmap.chunks_mut(dest_stride)) { + // Copy the initial bitmap strike rows directly from the source. + dest_row[0 .. src_stride].copy_from_slice(src_row); + } + // Finally blend each extra strike in turn. + for i in 1 ..= extra_strikes { + let offset = i as f64 * pixel_step; + blend_strike(&mut dest_bitmap, src_bitmap, width, height, subpixel_mask, offset); + } + (dest_bitmap, dest_width) +} + +pub struct RasterizedGlyph { + pub top: f32, + pub left: f32, + pub width: i32, + pub height: i32, + pub scale: f32, + pub format: GlyphFormat, + pub bytes: Vec<u8>, +} + +impl RasterizedGlyph { + #[allow(dead_code)] + pub fn downscale_bitmap_if_required(&mut self, font: &FontInstance) { + // Check if the glyph is going to be downscaled in the shader. If the scaling is + // less than 0.5, that means bilinear filtering can't effectively filter the glyph + // without aliasing artifacts. + // + // Instead of fixing this by mipmapping the glyph cache texture, rather manually + // produce the appropriate mip level for individual glyphs where bilinear filtering + // will still produce acceptable results. + match self.format { + GlyphFormat::Bitmap | GlyphFormat::ColorBitmap => {}, + _ => return, + } + let (x_scale, y_scale) = font.transform.compute_scale().unwrap_or((1.0, 1.0)); + let upscaled = x_scale.max(y_scale) as f32; + let mut new_scale = self.scale; + if new_scale * upscaled <= 0.0 { + return; + } + let mut steps = 0; + while new_scale * upscaled <= 0.5 { + new_scale *= 2.0; + steps += 1; + } + // If no mipping is necessary, just bail. + if steps == 0 { + return; + } + + // Calculate the actual size of the mip level. + let new_width = (self.width as usize + (1 << steps) - 1) >> steps; + let new_height = (self.height as usize + (1 << steps) - 1) >> steps; + let mut new_bytes: Vec<u8> = Vec::with_capacity(new_width * new_height * 4); + + // Produce destination pixels by applying a box filter to the source pixels. + // The box filter corresponds to how graphics drivers may generate mipmaps. + for y in 0 .. new_height { + for x in 0 .. new_width { + // Calculate the number of source samples that contribute to the destination pixel. + let src_y = y << steps; + let src_x = x << steps; + let y_samples = (1 << steps).min(self.height as usize - src_y); + let x_samples = (1 << steps).min(self.width as usize - src_x); + let num_samples = (x_samples * y_samples) as u32; + + let mut src_idx = (src_y * self.width as usize + src_x) * 4; + // Initialize the accumulator with half an increment so that when later divided + // by the sample count, it will effectively round the accumulator to the nearest + // increment. + let mut accum = [num_samples / 2; 4]; + // Accumulate all the contributing source sampless. + for _ in 0 .. y_samples { + for _ in 0 .. x_samples { + accum[0] += self.bytes[src_idx + 0] as u32; + accum[1] += self.bytes[src_idx + 1] as u32; + accum[2] += self.bytes[src_idx + 2] as u32; + accum[3] += self.bytes[src_idx + 3] as u32; + src_idx += 4; + } + src_idx += (self.width as usize - x_samples) * 4; + } + + // Finally, divide by the sample count to get the mean value for the new pixel. + new_bytes.extend_from_slice(&[ + (accum[0] / num_samples) as u8, + (accum[1] / num_samples) as u8, + (accum[2] / num_samples) as u8, + (accum[3] / num_samples) as u8, + ]); + } + } + + // Fix the bounds for the new glyph data. + self.top /= (1 << steps) as f32; + self.left /= (1 << steps) as f32; + self.width = new_width as i32; + self.height = new_height as i32; + self.scale = new_scale; + self.bytes = new_bytes; + } +} + +pub struct FontContexts { + // These worker are mostly accessed from their corresponding worker threads. + // The goal is that there should be no noticeable contention on the mutexes. + worker_contexts: Vec<Mutex<FontContext>>, + // Stored here as a convenience to get the current thread index. + #[allow(dead_code)] + workers: Arc<ThreadPool>, + locked_mutex: Mutex<bool>, + locked_cond: Condvar, +} + +impl FontContexts { + /// Get access to any particular font context. + /// + /// The id is an index between 0 and num_worker_contexts for font contexts + /// associated to the thread pool. + pub fn lock_context(&self, id: usize) -> MutexGuard<FontContext> { + self.worker_contexts[id].lock().unwrap() + } + + // Find a context that is currently unlocked to use, otherwise defaulting + // to the first context. + pub fn lock_any_context(&self) -> MutexGuard<FontContext> { + for context in &self.worker_contexts { + if let Ok(mutex) = context.try_lock() { + return mutex; + } + } + self.lock_context(0) + } + + // number of contexts associated to workers + pub fn num_worker_contexts(&self) -> usize { + self.worker_contexts.len() + } +} + +pub trait AsyncForEach<T> { + fn async_for_each<F: Fn(MutexGuard<T>) + Send + 'static>(&self, f: F); +} + +impl AsyncForEach<FontContext> for Arc<FontContexts> { + fn async_for_each<F: Fn(MutexGuard<FontContext>) + Send + 'static>(&self, f: F) { + // Reset the locked condition. + let mut locked = self.locked_mutex.lock().unwrap(); + *locked = false; + + // Arc that can be safely moved into a spawn closure. + let font_contexts = self.clone(); + // Spawn a new thread on which to run the for-each off the main thread. + self.workers.spawn(move || { + // Lock the shared and worker contexts up front. + let mut locks = Vec::with_capacity(font_contexts.num_worker_contexts()); + for i in 0 .. font_contexts.num_worker_contexts() { + locks.push(font_contexts.lock_context(i)); + } + + // Signal the locked condition now that all contexts are locked. + *font_contexts.locked_mutex.lock().unwrap() = true; + font_contexts.locked_cond.notify_all(); + + // Now that everything is locked, proceed to processing each locked context. + for context in locks { + f(context); + } + }); + + // Wait for locked condition before resuming. Safe to proceed thereafter + // since any other thread that needs to use a FontContext will try to lock + // it first. + while !*locked { + locked = self.locked_cond.wait(locked).unwrap(); + } + } +} + +pub struct GlyphRasterizer { + #[allow(dead_code)] + workers: Arc<ThreadPool>, + font_contexts: Arc<FontContexts>, + + /// The current set of loaded fonts. + fonts: FastHashSet<FontKey>, + + /// The current number of individual glyphs waiting in pending batches. + pending_glyph_count: usize, + + /// The current number of glyph request jobs that have been kicked to worker threads. + pending_glyph_jobs: usize, + + /// The number of glyphs requested this frame. + glyph_request_count: usize, + + /// A map of current glyph request batches. + pending_glyph_requests: FastHashMap<FontInstance, SmallVec<[GlyphKey; 16]>>, + + // Receives the rendered glyphs. + glyph_rx: Receiver<GlyphRasterJob>, + glyph_tx: Sender<GlyphRasterJob>, + + // We defer removing fonts to the end of the frame so that: + // - this work is done outside of the critical path, + // - we don't have to worry about the ordering of events if a font is used on + // a frame where it is used (although it seems unlikely). + fonts_to_remove: Vec<FontKey>, + // Defer removal of font instances, as for fonts. + font_instances_to_remove: Vec<FontInstance>, + + // Whether to parallelize glyph rasterization with rayon. + enable_multithreading: bool, + + // Whether glyphs can be rasterized in r8 format when it makes sense. + can_use_r8_format: bool, +} + +impl GlyphRasterizer { + pub fn new(workers: Arc<ThreadPool>, can_use_r8_format: bool) -> Self { + let (glyph_tx, glyph_rx) = unbounded(); + + let num_workers = workers.current_num_threads(); + let mut contexts = Vec::with_capacity(num_workers); + + for _ in 0 .. num_workers { + contexts.push(Mutex::new(FontContext::new())); + } + + let font_context = FontContexts { + worker_contexts: contexts, + workers: Arc::clone(&workers), + locked_mutex: Mutex::new(false), + locked_cond: Condvar::new(), + }; + + GlyphRasterizer { + font_contexts: Arc::new(font_context), + fonts: FastHashSet::default(), + pending_glyph_jobs: 0, + pending_glyph_count: 0, + glyph_request_count: 0, + glyph_rx, + glyph_tx, + workers, + fonts_to_remove: Vec::new(), + font_instances_to_remove: Vec::new(), + enable_multithreading: true, + pending_glyph_requests: FastHashMap::default(), + can_use_r8_format, + } + } + + pub fn add_font(&mut self, font_key: FontKey, template: FontTemplate) { + if self.fonts.insert(font_key.clone()) { + // Only add font to FontContexts if not previously added. + self.font_contexts.async_for_each(move |mut context| { + context.add_font(&font_key, &template); + }); + } + } + + pub fn delete_font(&mut self, font_key: FontKey) { + self.fonts_to_remove.push(font_key); + } + + pub fn delete_fonts(&mut self, font_keys: &[FontKey]) { + self.fonts_to_remove.extend_from_slice(font_keys); + } + + pub fn delete_font_instance(&mut self, instance: &FontInstance) { + self.font_instances_to_remove.push(instance.clone()); + } + + pub fn prepare_font(&self, font: &mut FontInstance) { + FontContext::prepare_font(font); + + // Quantize the transform to minimize thrashing of the glyph cache, but + // only quantize the transform when preparing to access the glyph cache. + // This way, the glyph subpixel positions, which are calculated before + // this, can still use the precise transform which is required to match + // the subpixel positions computed for glyphs in the text run shader. + font.transform = font.transform.quantize(); + } + + pub fn has_font(&self, font_key: FontKey) -> bool { + self.fonts.contains(&font_key) + } + + pub fn get_glyph_dimensions( + &mut self, + font: &FontInstance, + glyph_index: GlyphIndex, + ) -> Option<GlyphDimensions> { + let glyph_key = GlyphKey::new( + glyph_index, + DevicePoint::zero(), + SubpixelDirection::None, + ); + + self.font_contexts + .lock_any_context() + .get_glyph_dimensions(font, &glyph_key) + } + + pub fn get_glyph_index(&mut self, font_key: FontKey, ch: char) -> Option<u32> { + self.font_contexts + .lock_any_context() + .get_glyph_index(font_key, ch) + } + + fn remove_dead_fonts(&mut self) { + if self.fonts_to_remove.is_empty() && self.font_instances_to_remove.is_empty() { + return + } + + profile_scope!("remove_dead_fonts"); + let mut fonts_to_remove = mem::replace(& mut self.fonts_to_remove, Vec::new()); + // Only remove font from FontContexts if previously added. + fonts_to_remove.retain(|font| self.fonts.remove(font)); + let font_instances_to_remove = mem::replace(& mut self.font_instances_to_remove, Vec::new()); + self.font_contexts.async_for_each(move |mut context| { + for font_key in &fonts_to_remove { + context.delete_font(font_key); + } + for instance in &font_instances_to_remove { + context.delete_font_instance(instance); + } + }); + } + + #[cfg(feature = "replay")] + pub fn reset(&mut self) { + //TODO: any signals need to be sent to the workers? + self.pending_glyph_jobs = 0; + self.pending_glyph_count = 0; + self.glyph_request_count = 0; + self.fonts_to_remove.clear(); + self.font_instances_to_remove.clear(); + } +} + +trait AddFont { + fn add_font(&mut self, font_key: &FontKey, template: &FontTemplate); +} + +impl AddFont for FontContext { + fn add_font(&mut self, font_key: &FontKey, template: &FontTemplate) { + match *template { + FontTemplate::Raw(ref bytes, index) => { + self.add_raw_font(font_key, bytes.clone(), index); + } + FontTemplate::Native(ref native_font_handle) => { + self.add_native_font(font_key, (*native_font_handle).clone()); + } + } + } +} + +#[allow(dead_code)] +pub struct GlyphRasterJob { + pub font: Arc<FontInstance>, + pub key: GlyphKey, + pub result: GlyphRasterResult, +} + +#[allow(dead_code)] +#[derive(Debug)] +pub enum GlyphRasterError { + LoadFailed, +} + +#[allow(dead_code)] +pub type GlyphRasterResult = Result<RasterizedGlyph, GlyphRasterError>; + +#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct GpuGlyphCacheKey(pub u32); + +#[cfg(test)] +mod test_glyph_rasterizer { + use crate::profiler::GlyphRasterizeProfiler; + + struct Profiler; + impl GlyphRasterizeProfiler for Profiler { + fn start_time(&mut self) {} + fn end_time(&mut self) -> f64 { + 0. + } + fn set(&mut self, _value: f64) {} + } + + #[test] + fn rasterize_200_glyphs() { + // This test loads a font from disc, the renders 4 requests containing + // 50 glyphs each, deletes the font and waits for the result. + + use rayon::ThreadPoolBuilder; + use std::fs::File; + use std::io::Read; + use api::{FontKey, FontInstanceKey, FontTemplate, IdNamespace}; + use api::units::DevicePoint; + use std::sync::Arc; + use crate::rasterizer::{FontInstance, BaseFontInstance, GlyphKey, GlyphRasterizer}; + + let worker = ThreadPoolBuilder::new() + .thread_name(|idx|{ format!("WRWorker#{}", idx) }) + .build(); + let workers = Arc::new(worker.unwrap()); + let mut glyph_rasterizer = GlyphRasterizer::new(workers, true); + let mut font_file = + File::open("../wrench/reftests/text/VeraBd.ttf").expect("Couldn't open font file"); + let mut font_data = vec![]; + font_file + .read_to_end(&mut font_data) + .expect("failed to read font file"); + + let font_key = FontKey::new(IdNamespace(0), 0); + glyph_rasterizer.add_font(font_key, FontTemplate::Raw(Arc::new(font_data), 0)); + + let font = FontInstance::from_base(Arc::new(BaseFontInstance::new( + FontInstanceKey::new(IdNamespace(0), 0), + font_key, + 32.0, + None, + None, + Vec::new(), + ))); + + let subpx_dir = font.get_subpx_dir(); + + let mut glyph_keys = Vec::with_capacity(200); + for i in 0 .. 200 { + glyph_keys.push(GlyphKey::new( + i, + DevicePoint::zero(), + subpx_dir, + )); + } + + for i in 0 .. 4 { + glyph_rasterizer.request_glyphs( + font.clone(), + &glyph_keys[(50 * i) .. (50 * (i + 1))], + |_| true, + ); + } + + glyph_rasterizer.delete_font(font_key); + + glyph_rasterizer.resolve_glyphs( + |_, _| {}, + &mut Profiler, + ); + } + + #[test] + fn rasterize_large_glyphs() { + // This test loads a font from disc and rasterize a few glyphs with a size of 200px to check + // that the texture cache handles them properly. + use rayon::ThreadPoolBuilder; + use std::fs::File; + use std::io::Read; + use api::{FontKey, FontInstanceKey, FontTemplate, IdNamespace}; + use api::units::DevicePoint; + use std::sync::Arc; + use crate::rasterizer::{FontInstance, BaseFontInstance, GlyphKey, GlyphRasterizer}; + + let worker = ThreadPoolBuilder::new() + .thread_name(|idx|{ format!("WRWorker#{}", idx) }) + .build(); + let workers = Arc::new(worker.unwrap()); + let mut glyph_rasterizer = GlyphRasterizer::new(workers, true); + let mut font_file = + File::open("../wrench/reftests/text/VeraBd.ttf").expect("Couldn't open font file"); + let mut font_data = vec![]; + font_file + .read_to_end(&mut font_data) + .expect("failed to read font file"); + + let font_key = FontKey::new(IdNamespace(0), 0); + glyph_rasterizer.add_font(font_key, FontTemplate::Raw(Arc::new(font_data), 0)); + + let font = FontInstance::from_base(Arc::new(BaseFontInstance::new( + FontInstanceKey::new(IdNamespace(0), 0), + font_key, + 200.0, + None, + None, + Vec::new(), + ))); + + let subpx_dir = font.get_subpx_dir(); + + let mut glyph_keys = Vec::with_capacity(10); + for i in 0 .. 10 { + glyph_keys.push(GlyphKey::new( + i, + DevicePoint::zero(), + subpx_dir, + )); + } + + glyph_rasterizer.request_glyphs( + font.clone(), + &glyph_keys, + |_| true, + ); + + glyph_rasterizer.delete_font(font_key); + + glyph_rasterizer.resolve_glyphs( + |_, _| {}, + &mut Profiler, + ); + } + + #[test] + fn test_subpx_quantize() { + use crate::rasterizer::SubpixelOffset; + + assert_eq!(SubpixelOffset::quantize(0.0), SubpixelOffset::Zero); + assert_eq!(SubpixelOffset::quantize(-0.0), SubpixelOffset::Zero); + + assert_eq!(SubpixelOffset::quantize(0.1), SubpixelOffset::Zero); + assert_eq!(SubpixelOffset::quantize(0.01), SubpixelOffset::Zero); + assert_eq!(SubpixelOffset::quantize(0.05), SubpixelOffset::Zero); + assert_eq!(SubpixelOffset::quantize(0.12), SubpixelOffset::Zero); + assert_eq!(SubpixelOffset::quantize(0.124), SubpixelOffset::Zero); + + assert_eq!(SubpixelOffset::quantize(0.125), SubpixelOffset::Quarter); + assert_eq!(SubpixelOffset::quantize(0.2), SubpixelOffset::Quarter); + assert_eq!(SubpixelOffset::quantize(0.25), SubpixelOffset::Quarter); + assert_eq!(SubpixelOffset::quantize(0.33), SubpixelOffset::Quarter); + assert_eq!(SubpixelOffset::quantize(0.374), SubpixelOffset::Quarter); + + assert_eq!(SubpixelOffset::quantize(0.375), SubpixelOffset::Half); + assert_eq!(SubpixelOffset::quantize(0.4), SubpixelOffset::Half); + assert_eq!(SubpixelOffset::quantize(0.5), SubpixelOffset::Half); + assert_eq!(SubpixelOffset::quantize(0.58), SubpixelOffset::Half); + assert_eq!(SubpixelOffset::quantize(0.624), SubpixelOffset::Half); + + assert_eq!(SubpixelOffset::quantize(0.625), SubpixelOffset::ThreeQuarters); + assert_eq!(SubpixelOffset::quantize(0.67), SubpixelOffset::ThreeQuarters); + assert_eq!(SubpixelOffset::quantize(0.7), SubpixelOffset::ThreeQuarters); + assert_eq!(SubpixelOffset::quantize(0.78), SubpixelOffset::ThreeQuarters); + assert_eq!(SubpixelOffset::quantize(0.874), SubpixelOffset::ThreeQuarters); + + assert_eq!(SubpixelOffset::quantize(0.875), SubpixelOffset::Zero); + assert_eq!(SubpixelOffset::quantize(0.89), SubpixelOffset::Zero); + assert_eq!(SubpixelOffset::quantize(0.91), SubpixelOffset::Zero); + assert_eq!(SubpixelOffset::quantize(0.967), SubpixelOffset::Zero); + assert_eq!(SubpixelOffset::quantize(0.999), SubpixelOffset::Zero); + + assert_eq!(SubpixelOffset::quantize(-1.0), SubpixelOffset::Zero); + assert_eq!(SubpixelOffset::quantize(1.0), SubpixelOffset::Zero); + assert_eq!(SubpixelOffset::quantize(1.5), SubpixelOffset::Half); + assert_eq!(SubpixelOffset::quantize(-1.625), SubpixelOffset::Half); + assert_eq!(SubpixelOffset::quantize(-4.33), SubpixelOffset::ThreeQuarters); + } +} diff --git a/gfx/wr/wr_glyph_rasterizer/src/telemetry.rs b/gfx/wr/wr_glyph_rasterizer/src/telemetry.rs new file mode 100644 index 0000000000..28000440b2 --- /dev/null +++ b/gfx/wr/wr_glyph_rasterizer/src/telemetry.rs @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#[cfg(feature = "gecko")] +use glean::TimerId; +#[cfg(feature = "gecko")] +use firefox_on_glean::metrics::wr; + +#[cfg(not(feature = "gecko"))] +pub struct TimerId; + +pub struct Telemetry; + +/// Defines the interface for hooking up an external telemetry reporter to WR. +#[cfg(not(feature = "gecko"))] +impl Telemetry { + // Start rasterize glyph time collection + pub fn start_rasterize_glyphs_time() -> TimerId { return TimerId {}; } + // End rasterize glyph time collection + pub fn stop_and_accumulate_rasterize_glyphs_time(_id: TimerId) { } +} + +#[cfg(feature = "gecko")] +impl Telemetry { + pub fn start_rasterize_glyphs_time() -> TimerId { wr::rasterize_glyphs_time.start() } + pub fn stop_and_accumulate_rasterize_glyphs_time(id: TimerId) { wr::rasterize_glyphs_time.stop_and_accumulate(id); } +} diff --git a/gfx/wr/wr_glyph_rasterizer/src/types.rs b/gfx/wr/wr_glyph_rasterizer/src/types.rs new file mode 100644 index 0000000000..665ab0f86b --- /dev/null +++ b/gfx/wr/wr_glyph_rasterizer/src/types.rs @@ -0,0 +1,10 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use fxhash::FxHasher; +use std::collections::{HashMap, HashSet}; +use std::hash::BuildHasherDefault; + +pub type FastHashMap<K, V> = HashMap<K, V, BuildHasherDefault<FxHasher>>; +pub type FastHashSet<K> = HashSet<K, BuildHasherDefault<FxHasher>>; |