From f323a3132ead47f886b58f9cf1d1eff93fcdf6e6 Mon Sep 17 00:00:00 2001 From: Lee Salzman Date: Tue, 14 Nov 2017 14:10:37 -0500 Subject: [PATCH] support font subpixel AA with content transforms --- Cargo.lock | 70 +++++------ webrender/Cargo.toml | 2 +- webrender/res/cs_text_run.glsl | 18 +-- webrender/res/prim_shared.glsl | 5 +- webrender/res/ps_text_run.glsl | 102 ++++++++++----- webrender/src/glyph_rasterizer.rs | 137 +++++++++++++++++--- webrender/src/platform/macos/font.rs | 75 +++++++---- webrender/src/platform/unix/font.rs | 35 ++++-- webrender/src/platform/windows/font.rs | 26 ++-- webrender/src/prim_store.rs | 23 +++- webrender/src/renderer.rs | 147 +++++++++++++++++----- webrender/src/tiling.rs | 10 +- webrender/src/util.rs | 5 + webrender_api/Cargo.toml | 2 +- wrench/reftests/text/reftest.list | 3 + wrench/reftests/text/subpixel-rotate.png | Bin 0 -> 19168 bytes wrench/reftests/text/subpixel-rotate.yaml | 10 ++ wrench/reftests/text/subpixel-scale.png | Bin 0 -> 14068 bytes wrench/reftests/text/subpixel-scale.yaml | 10 ++ wrench/reftests/text/subpixel-skew.png | Bin 0 -> 15629 bytes wrench/reftests/text/subpixel-skew.yaml | 10 ++ wrench/src/yaml_helper.rs | 37 ++++++ 22 files changed, 549 insertions(+), 178 deletions(-) create mode 100644 wrench/reftests/text/subpixel-rotate.png create mode 100644 wrench/reftests/text/subpixel-rotate.yaml create mode 100644 wrench/reftests/text/subpixel-scale.png create mode 100644 wrench/reftests/text/subpixel-scale.yaml create mode 100644 wrench/reftests/text/subpixel-skew.png create mode 100644 wrench/reftests/text/subpixel-skew.yaml diff --git a/Cargo.lock b/Cargo.lock index d45bac1e0f..c3147a11ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,31 +1,3 @@ -[root] -name = "wrench" -version = "0.2.4" -dependencies = [ - "app_units 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", - "base64 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "bincode 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", - "byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "clap 2.25.0 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", - "dwrote 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "env_logger 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "euclid 0.15.5 (registry+https://github.com/rust-lang/crates.io-index)", - "font-loader 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gleam 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)", - "image 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "osmesa-src 17.2.0-devel (git+https://github.com/servo/osmesa-src)", - "osmesa-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "ron 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "servo-glutin 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", - "webrender 0.54.0", - "yaml-rust 0.3.4 (git+https://github.com/vvuk/yaml-rust)", -] - [[package]] name = "adler32" version = "0.3.0" @@ -190,7 +162,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "block 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "core-graphics 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)", + "core-graphics 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)", "objc 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -219,7 +191,7 @@ dependencies = [ [[package]] name = "core-graphics" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -234,7 +206,7 @@ version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "core-foundation 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", - "core-graphics 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)", + "core-graphics 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", "foreign-types 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -851,7 +823,7 @@ dependencies = [ "cgl 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "cocoa 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)", "core-foundation 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", - "core-graphics 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)", + "core-graphics 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", "dwmapi-sys 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "gdi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "gl_generator 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1065,7 +1037,7 @@ dependencies = [ "bitflags 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "core-foundation 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", - "core-graphics 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)", + "core-graphics 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", "core-text 8.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "dwrote 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1098,7 +1070,7 @@ dependencies = [ "bitflags 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "core-foundation 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", - "core-graphics 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)", + "core-graphics 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", "dwrote 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "euclid 0.15.5 (registry+https://github.com/rust-lang/crates.io-index)", "ipc-channel 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1116,6 +1088,34 @@ name = "winapi-build" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "wrench" +version = "0.2.4" +dependencies = [ + "app_units 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bincode 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "dwrote 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "euclid 0.15.5 (registry+https://github.com/rust-lang/crates.io-index)", + "font-loader 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gleam 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)", + "image 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "osmesa-src 17.2.0-devel (git+https://github.com/servo/osmesa-src)", + "osmesa-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "ron 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "servo-glutin 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", + "webrender 0.54.0", + "yaml-rust 0.3.4 (git+https://github.com/vvuk/yaml-rust)", +] + [[package]] name = "ws" version = "0.7.3" @@ -1198,7 +1198,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum color_quant 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a475fc4af42d83d28adf72968d9bcfaf035a1a9381642d8e85d8a04957767b0d" "checksum core-foundation 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5909502e547762013619f4c4e01cc7393c20fe2d52d7fa471c1210adb2320dc7" "checksum core-foundation-sys 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bc9fb3d6cb663e6fd7cf1c63f9b144ee2b1e4a78595a0451dd34bff85b9a3387" -"checksum core-graphics 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)" = "096dff8fda997f2b8a1f3d71d427d2cdf6d7a18559f08c5edb7e332c4414ab4a" +"checksum core-graphics 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5dc0a78ab2ac23b6ea7b3fe5fe93b227900dc0956979735b8f68032417976dd4" "checksum core-text 8.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bcad23756dd1dc4b47bf6a914ace27aadb8fa68889db5837af2308d018d0467c" "checksum crossbeam 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "0c5ea215664ca264da8a9d9c3be80d2eaf30923c259d03e870388eb927508f97" "checksum deflate 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)" = "24c5f3de3a8e183ab9a169654b652407e5e80bed40986bcca92c2b088b9bfa80" diff --git a/webrender/Cargo.toml b/webrender/Cargo.toml index 169887285d..3587f09be3 100644 --- a/webrender/Cargo.toml +++ b/webrender/Cargo.toml @@ -47,5 +47,5 @@ dwrote = "0.4" [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.4" -core-graphics = "0.12.2" +core-graphics = "0.12.3" core-text = { version = "8.0", default-features = false } diff --git a/webrender/res/cs_text_run.glsl b/webrender/res/cs_text_run.glsl index 1d7025f5e4..3aa698ed29 100644 --- a/webrender/res/cs_text_run.glsl +++ b/webrender/res/cs_text_run.glsl @@ -25,27 +25,21 @@ void main(void) { GlyphResource res = fetch_glyph_resource(resource_address); - // Glyphs size is already in device-pixels. + // Glyph size is already in device-pixels. // The render task origin is in device-pixels. Offset that by // the glyph offset, relative to its primitive bounding rect. - vec2 size = (res.uv_rect.zw - res.uv_rect.xy) * res.scale; - vec2 local_pos = glyph.offset + vec2(res.offset.x, -res.offset.y) / uDevicePixelRatio; - vec2 origin = prim.task.common_data.task_rect.p0 + - uDevicePixelRatio * (local_pos - prim.task.content_origin); - vec4 local_rect = vec4(origin, size); + vec2 glyph_size = res.uv_rect.zw - res.uv_rect.xy; + vec2 glyph_pos = res.offset + glyph_size * aPosition.xy; + vec2 local_pos = prim.task.common_data.task_rect.p0 + glyph_pos * res.scale + + uDevicePixelRatio * (glyph.offset - prim.task.content_origin); + gl_Position = uTransform * vec4(local_pos, 0.0, 1.0); vec2 texture_size = vec2(textureSize(sColor0, 0)); vec2 st0 = res.uv_rect.xy / texture_size; vec2 st1 = res.uv_rect.zw / texture_size; - vec2 pos = mix(local_rect.xy, - local_rect.xy + local_rect.zw, - aPosition.xy); - vUv = vec3(mix(st0, st1, aPosition.xy), res.layer); vColor = prim.task.color; - - gl_Position = uTransform * vec4(pos, 0.0, 1.0); } #endif diff --git a/webrender/res/prim_shared.glsl b/webrender/res/prim_shared.glsl index d1c35bd9bd..e58b7414cd 100644 --- a/webrender/res/prim_shared.glsl +++ b/webrender/res/prim_shared.glsl @@ -552,7 +552,6 @@ vec4 get_layer_pos(vec2 pos, Layer layer) { // Compute a snapping offset in world space (adjusted to pixel ratio), // given local position on the layer and a snap rectangle. vec2 compute_snap_offset(vec2 local_pos, - RectWithSize local_clip_rect, Layer layer, RectWithSize snap_rect) { // Ensure that the snap rect is at *least* one device pixel in size. @@ -597,9 +596,9 @@ VertexInfo write_vertex(RectWithSize instance_rect, vec2 clamped_local_pos = clamp_rect(clamp_rect(local_pos, local_clip_rect), layer.local_clip_rect); /// Compute the snapping offset. - vec2 snap_offset = compute_snap_offset(clamped_local_pos, local_clip_rect, layer, snap_rect); + vec2 snap_offset = compute_snap_offset(clamped_local_pos, layer, snap_rect); - // Transform the current vertex to the world cpace. + // Transform the current vertex to world space. vec4 world_pos = layer.transform * vec4(clamped_local_pos, 0.0, 1.0); // Convert the world positions to device pixel space. diff --git a/webrender/res/ps_text_run.glsl b/webrender/res/ps_text_run.glsl index 0af4f9ede4..18d0b2922e 100644 --- a/webrender/res/ps_text_run.glsl +++ b/webrender/res/ps_text_run.glsl @@ -8,10 +8,6 @@ flat varying vec4 vColor; varying vec3 vUv; flat varying vec4 vUvBorder; -#ifdef WR_FEATURE_TRANSFORM -varying vec3 vLocalPos; -#endif - #ifdef WR_VERTEX_SHADER #define MODE_ALPHA 0 @@ -23,6 +19,42 @@ varying vec3 vLocalPos; #define MODE_SUBPX_BG_PASS2 6 #define MODE_COLOR_BITMAP 7 +VertexInfo write_text_vertex(vec2 local_pos, + RectWithSize local_clip_rect, + float z, + Layer layer, + PictureTask task, + RectWithSize snap_rect) { + // Clamp to the two local clip rects. + vec2 clamped_local_pos = clamp_rect(clamp_rect(local_pos, local_clip_rect), layer.local_clip_rect); + + // Transform the current vertex to world space. + vec4 world_pos = layer.transform * vec4(clamped_local_pos, 0.0, 1.0); + + // Convert the world positions to device pixel space. + vec2 device_pos = world_pos.xy / world_pos.w * uDevicePixelRatio; + + // Apply offsets for the render task to get correct screen location. + vec2 final_pos = device_pos - + task.content_origin + + task.common_data.task_rect.p0; + +#ifdef WR_FEATURE_GLYPH_TRANSFORM + // For transformed subpixels, we just need to align the glyph origin to a device pixel. + // Only check the layer transform's translation since the scales and axes match. + vec2 world_snap_p0 = snap_rect.p0 + layer.transform[3].xy * uDevicePixelRatio; + final_pos += floor(world_snap_p0 + 0.5) - world_snap_p0; +#elif !defined(WR_FEATURE_TRANSFORM) + // Compute the snapping offset only if the layer transform is axis-aligned. + final_pos += compute_snap_offset(clamped_local_pos, layer, snap_rect); +#endif + + gl_Position = uTransform * vec4(final_pos, z, 1.0); + + VertexInfo vi = VertexInfo(clamped_local_pos, device_pos); + return vi; +} + void main(void) { Primitive prim = load_primitive(); TextRun text = fetch_text_run(prim.specific_prim_address); @@ -35,30 +67,40 @@ void main(void) { text.subpx_dir); GlyphResource res = fetch_glyph_resource(resource_address); - vec2 local_pos = glyph.offset + - text.offset + - vec2(res.offset.x, -res.offset.y) / uDevicePixelRatio; - - RectWithSize local_rect = RectWithSize(local_pos, - (res.uv_rect.zw - res.uv_rect.xy) * res.scale / uDevicePixelRatio); - -#ifdef WR_FEATURE_TRANSFORM - TransformVertexInfo vi = write_transform_vertex(local_rect, - prim.local_clip_rect, - vec4(0.0), - prim.z, - prim.layer, - prim.task); - vLocalPos = vi.local_pos; - vec2 f = (vi.local_pos.xy / vi.local_pos.z - local_rect.p0) / local_rect.size; +#ifdef WR_FEATURE_GLYPH_TRANSFORM + // Transform from local space to glyph space. + mat2 transform = mat2(prim.layer.transform) * uDevicePixelRatio; + + // Compute the glyph rect in glyph space. + RectWithSize glyph_rect = RectWithSize(res.offset + transform * (text.offset + glyph.offset), + res.uv_rect.zw - res.uv_rect.xy); + + // Select the corner of the glyph rect that we are processing. + // Transform it from glyph space into local space. + vec2 local_pos = inverse(transform) * (glyph_rect.p0 + glyph_rect.size * aPosition.xy); +#else + // Scale from glyph space to local space. + float scale = res.scale / uDevicePixelRatio; + + // Compute the glyph rect in local space. + RectWithSize glyph_rect = RectWithSize(scale * res.offset + text.offset + glyph.offset, + scale * (res.uv_rect.zw - res.uv_rect.xy)); + + // Select the corner of the glyph rect that we are processing. + vec2 local_pos = glyph_rect.p0 + glyph_rect.size * aPosition.xy; +#endif + + VertexInfo vi = write_text_vertex(local_pos, + prim.local_clip_rect, + prim.z, + prim.layer, + prim.task, + glyph_rect); + +#ifdef WR_FEATURE_GLYPH_TRANSFORM + vec2 f = (transform * vi.local_pos - glyph_rect.p0) / glyph_rect.size; #else - VertexInfo vi = write_vertex(local_rect, - prim.local_clip_rect, - prim.z, - prim.layer, - prim.task, - local_rect); - vec2 f = (vi.local_pos - local_rect.p0) / local_rect.size; + vec2 f = (vi.local_pos - glyph_rect.p0) / glyph_rect.size; #endif write_clip(vi.screen_pos, prim.clip_area); @@ -98,11 +140,7 @@ void main(void) { vec3 tc = vec3(clamp(vUv.xy, vUvBorder.xy, vUvBorder.zw), vUv.z); vec4 mask = texture(sColor0, tc); - float alpha = 1.0; -#ifdef WR_FEATURE_TRANSFORM - init_transform_fs(vLocalPos, alpha); -#endif - alpha *= do_clip(); + float alpha = do_clip(); #ifdef WR_FEATURE_SUBPX_BG_PASS1 mask.rgb = vec3(mask.a) - mask.rgb; diff --git a/webrender/src/glyph_rasterizer.rs b/webrender/src/glyph_rasterizer.rs index 34470eed91..880090171a 100644 --- a/webrender/src/glyph_rasterizer.rs +++ b/webrender/src/glyph_rasterizer.rs @@ -7,7 +7,7 @@ use api::{IdNamespace, LayoutPoint}; use api::{ColorF, ColorU, DevicePoint, DeviceUintSize}; use api::{FontInstancePlatformOptions, FontRenderMode, FontVariation}; use api::{FontKey, FontTemplate, GlyphDimensions, GlyphKey, SubpixelDirection}; -use api::{ImageData, ImageDescriptor, ImageFormat}; +use api::{ImageData, ImageDescriptor, ImageFormat, LayerToWorldTransform}; use app_units::Au; use device::TextureFilter; use glyph_cache::{CachedGlyphInfo, GlyphCache}; @@ -17,12 +17,115 @@ use platform::font::FontContext; use profiler::TextureCacheProfileCounters; use rayon::ThreadPool; use rayon::prelude::*; +use std::cmp; use std::collections::hash_map::Entry; +use std::hash::{Hash, Hasher}; use std::mem; use std::sync::{Arc, Mutex, MutexGuard}; use std::sync::mpsc::{channel, Receiver, Sender}; use texture_cache::{TextureCache, TextureCacheHandle}; +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] +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(&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) + } + + 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, + ) + } + + 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 + } + + pub fn compute_scale(&self) -> Option<(f64, f64)> { + let det = self.determinant(); + if det != 0.0 { + let major = (self.scale_x as f64).hypot(self.skew_y as f64); + let minor = det.abs() / major; + Some((major, minor)) + } else { + None + } + } + + 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 inverse(&self) -> Option { + let det = self.determinant(); + if det != 0.0 { + let inv_det = det.recip() as f32; + Some(FontTransform::new( + self.scale_y * inv_det, + -self.skew_x * inv_det, + -self.skew_y * inv_det, + self.scale_x * inv_det + )) + } else { + None + } + } + + #[allow(dead_code)] + pub fn apply(&self, x: f32, y: f32) -> (f32, f32) { + (self.scale_x * x + self.skew_x * y, self.skew_y * x + self.scale_y * y) + } +} + +impl<'a> From<&'a LayerToWorldTransform> for FontTransform { + fn from(xform: &'a LayerToWorldTransform) -> Self { + FontTransform::new(xform.m11, xform.m21, xform.m12, xform.m22) + } +} + #[derive(Clone, Hash, PartialEq, Eq, Debug, Ord, PartialOrd)] pub struct FontInstance { pub font_key: FontKey, @@ -39,6 +142,7 @@ pub struct FontInstance { pub platform_options: Option, pub variations: Vec, pub synthetic_italics: bool, + pub transform: FontTransform, } impl FontInstance { @@ -63,6 +167,7 @@ impl FontInstance { platform_options, variations, synthetic_italics, + transform: FontTransform::identity(), } } @@ -73,27 +178,29 @@ impl FontInstance { SubpixelDirection::Vertical => (0.0, glyph.subpixel_offset.into()), } } + + pub fn get_subpixel_glyph_format(&self) -> GlyphFormat { + if self.transform.is_identity() { GlyphFormat::Subpixel } else { GlyphFormat::TransformedSubpixel } + } + + #[allow(dead_code)] + pub fn get_glyph_format(&self) -> GlyphFormat { + match self.render_mode { + FontRenderMode::Mono | FontRenderMode::Alpha => GlyphFormat::Alpha, + FontRenderMode::Subpixel => self.get_subpixel_glyph_format(), + FontRenderMode::Bitmap => GlyphFormat::ColorBitmap, + } + } } #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] pub enum GlyphFormat { - Mono, Alpha, Subpixel, + TransformedSubpixel, ColorBitmap, } -impl From for GlyphFormat { - fn from(render_mode: FontRenderMode) -> GlyphFormat { - match render_mode { - FontRenderMode::Mono => GlyphFormat::Mono, - FontRenderMode::Alpha => GlyphFormat::Alpha, - FontRenderMode::Subpixel => GlyphFormat::Subpixel, - FontRenderMode::Bitmap => GlyphFormat::ColorBitmap, - } - } -} - pub struct RasterizedGlyph { pub top: f32, pub left: f32, @@ -396,7 +503,7 @@ impl GlyphRasterizer { }, TextureFilter::Linear, ImageData::Raw(glyph_bytes.clone()), - [glyph.left, glyph.top, glyph.scale], + [glyph.left, -glyph.top, glyph.scale], None, gpu_cache, ); @@ -404,7 +511,7 @@ impl GlyphRasterizer { texture_cache_handle, glyph_bytes, size: DeviceUintSize::new(glyph.width, glyph.height), - offset: DevicePoint::new(glyph.left, glyph.top), + offset: DevicePoint::new(glyph.left, -glyph.top), scale: glyph.scale, format: glyph.format, }) diff --git a/webrender/src/platform/macos/font.rs b/webrender/src/platform/macos/font.rs index 17b8319885..617912c791 100644 --- a/webrender/src/platform/macos/font.rs +++ b/webrender/src/platform/macos/font.rs @@ -17,15 +17,14 @@ use core_graphics::color_space::CGColorSpace; use core_graphics::context::{CGContext, CGTextDrawingMode}; use core_graphics::data_provider::CGDataProvider; use core_graphics::font::{CGFont, CGGlyph}; -use core_graphics::geometry::{CGPoint, CGRect, CGSize}; +use core_graphics::geometry::{CGAffineTransform, CGPoint, CGRect, CGSize}; use core_text; use core_text::font::{CTFont, CTFontRef}; use core_text::font_descriptor::{kCTFontDefaultOrientation, kCTFontColorGlyphsTrait}; use gamma_lut::{ColorLut, GammaLut}; -use glyph_rasterizer::{FontInstance, GlyphFormat, RasterizedGlyph}; +use glyph_rasterizer::{FontInstance, RasterizedGlyph}; use internal_types::FastHashMap; use std::collections::hash_map::Entry; -use std::ptr; use std::sync::Arc; pub struct FontContext { @@ -81,11 +80,12 @@ fn should_use_white_on_black(color: ColorU) -> bool { fn get_glyph_metrics( ct_font: &CTFont, + transform: Option<&CGAffineTransform>, glyph: CGGlyph, x_offset: f64, y_offset: f64, ) -> GlyphMetrics { - let bounds = ct_font.get_bounding_rects_for_glyphs(kCTFontDefaultOrientation, &[glyph]); + 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() @@ -105,6 +105,14 @@ fn get_glyph_metrics( }; } + let mut advance = CGSize { width: 0.0, height: 0.0 }; + ct_font.get_advances_for_glyphs(kCTFontDefaultOrientation, &glyph, &mut advance, 1); + + if let Some(transform) = transform { + bounds = bounds.apply_transform(transform); + advance = advance.apply_transform(transform); + } + // First round out to pixel boundaries // CG Origin is bottom left let mut left = bounds.origin.x.floor() as i32; @@ -124,16 +132,13 @@ fn get_glyph_metrics( let width = right - left; let height = top - bottom; - let advance = - ct_font.get_advances_for_glyphs(kCTFontDefaultOrientation, &glyph, ptr::null_mut(), 1); - let metrics = GlyphMetrics { rasterized_left: left, rasterized_width: width as u32, rasterized_height: height as u32, rasterized_ascent: top, rasterized_descent: -bottom, - advance: advance as f32, + advance: advance.width as f32, }; metrics @@ -150,9 +155,9 @@ extern { fn CTFontCopyVariationAxes(font: CTFontRef) -> CFArrayRef; } -fn new_ct_font_with_variations(cg_font: &CGFont, size: Au, variations: &[FontVariation]) -> CTFont { +fn new_ct_font_with_variations(cg_font: &CGFont, size: f64, variations: &[FontVariation]) -> CTFont { unsafe { - let ct_font = core_text::font::new_from_CGFont(cg_font, size.to_f64_px()); + let ct_font = core_text::font::new_from_CGFont(cg_font, size); if variations.is_empty() { return ct_font; } @@ -243,7 +248,7 @@ fn new_ct_font_with_variations(cg_font: &CGFont, size: Au, variations: &[FontVar } let vals_dict = CFDictionary::from_CFType_pairs(&vals); let cg_var_font = cg_font.create_copy_from_variations(&vals_dict).unwrap(); - core_text::font::new_from_CGFont(&cg_var_font, size.to_f64_px()) + core_text::font::new_from_CGFont(&cg_var_font, size) } } @@ -317,7 +322,7 @@ impl FontContext { None => return None, Some(cg_font) => cg_font, }; - let ct_font = new_ct_font_with_variations(cg_font, size, variations); + let ct_font = new_ct_font_with_variations(cg_font, size.to_f64_px(), variations); entry.insert(ct_font.clone()); Some(ct_font) } @@ -328,7 +333,7 @@ impl FontContext { let character = ch as u16; let mut glyph = 0; - self.get_ct_font(font_key, Au(16 * 60), &[]) + self.get_ct_font(font_key, Au::from_px(16), &[]) .and_then(|ref ct_font| { let result = ct_font.get_glyphs_for_characters(&character, &mut glyph, 1); @@ -349,7 +354,7 @@ impl FontContext { .and_then(|ref ct_font| { let glyph = key.index as CGGlyph; let (x_offset, y_offset) = font.get_subpx_offset(key); - let metrics = get_glyph_metrics(ct_font, glyph, x_offset, y_offset); + let metrics = get_glyph_metrics(ct_font, None, glyph, x_offset, y_offset); if metrics.rasterized_width == 0 || metrics.rasterized_height == 0 { None } else { @@ -447,14 +452,29 @@ impl FontContext { font: &FontInstance, key: &GlyphKey, ) -> Option { - let ct_font = match self.get_ct_font(font.font_key, font.size, &font.variations) { + let (.., minor) = font.transform.compute_scale().unwrap_or((1.0, 1.0)); + let size = font.size.scale_by(minor as f32); + let ct_font = match self.get_ct_font(font.font_key, size, &font.variations) { Some(font) => font, None => return None, }; + let shape = font.transform.pre_scale(minor.recip() as f32, minor.recip() as f32); + let transform = if shape.is_identity() { + None + } else { + 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: 0.0, + ty: 0.0 + }) + }; let glyph = key.index as CGGlyph; let (x_offset, y_offset) = font.get_subpx_offset(key); - let metrics = get_glyph_metrics(&ct_font, glyph, x_offset, y_offset); + let metrics = get_glyph_metrics(&ct_font, transform.as_ref(), glyph, x_offset, y_offset); if metrics.rasterized_width == 0 || metrics.rasterized_height == 0 { return None; } @@ -552,12 +572,6 @@ impl FontContext { cg_context.set_allows_antialiasing(antialias); cg_context.set_should_antialias(antialias); - // CG Origin is bottom left, WR is top left. Need -y offset - let rasterization_origin = CGPoint { - x: -metrics.rasterized_left as f64 + x_offset, - y: metrics.rasterized_descent as f64 - y_offset, - }; - // Fill the background. This could be opaque white, opaque black, or // transparency. cg_context.set_rgb_fill_color(bg_color, bg_color, bg_color, bg_alpha); @@ -573,7 +587,20 @@ impl FontContext { // Set the text color and draw the glyphs. cg_context.set_rgb_fill_color(text_color, text_color, text_color, 1.0); cg_context.set_text_drawing_mode(CGTextDrawingMode::CGTextFill); - ct_font.draw_glyphs(&[glyph], &[rasterization_origin], cg_context.clone()); + + // 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, + y: metrics.rasterized_descent as f64 - y_offset, + }; + + if let Some(transform) = transform { + cg_context.set_text_matrix(&transform); + + draw_origin = draw_origin.apply_transform(&transform.invert()); + } + + ct_font.draw_glyphs(&[glyph], &[draw_origin], cg_context.clone()); let mut rasterized_pixels = cg_context.data().to_vec(); @@ -630,7 +657,7 @@ impl FontContext { width: metrics.rasterized_width, height: metrics.rasterized_height, scale: 1.0, - format: GlyphFormat::from(font.render_mode), + format: font.get_glyph_format(), bytes: rasterized_pixels, }) } diff --git a/webrender/src/platform/unix/font.rs b/webrender/src/platform/unix/font.rs index 3b191f2971..7f3c261300 100644 --- a/webrender/src/platform/unix/font.rs +++ b/webrender/src/platform/unix/font.rs @@ -14,6 +14,7 @@ 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}; 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, FT_LOAD_VERTICAL_LAYOUT}; @@ -186,15 +187,33 @@ impl FontContext { load_flags |= FT_LOAD_COLOR; load_flags |= FT_LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH; + let req_size = font.size.to_f64_px(); let mut result = if font.render_mode == FontRenderMode::Bitmap { if (load_flags & FT_LOAD_NO_BITMAP) != 0 { FT_Error(FT_Err_Cannot_Render_Glyph as i32) } else { - self.choose_bitmap_size(face.face, font.size.to_f64_px()) + unsafe { FT_Set_Transform(face.face, ptr::null_mut(), ptr::null_mut()) }; + self.choose_bitmap_size(face.face, req_size) } } else { - let char_size = font.size.to_f64_px() * 64.0 + 0.5; - unsafe { FT_Set_Char_Size(face.face, char_size as FT_F26Dot6, 0, 0, 0) } + let (major, minor) = font.transform.compute_scale().unwrap_or((1.0, 1.0)); + let shape = font.transform.pre_scale(major.recip() as f32, minor.recip() as f32); + 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, + }; + unsafe { + FT_Set_Transform(face.face, &mut ft_shape, ptr::null_mut()); + FT_Set_Char_Size( + face.face, + (req_size * major * 64.0 + 0.5) as FT_F26Dot6, + (req_size * minor * 64.0 + 0.5) as FT_F26Dot6, + 0, + 0, + ) + } }; if result.succeeded() { @@ -518,14 +537,14 @@ impl FontContext { let (format, actual_width, actual_height) = match pixel_mode { FT_Pixel_Mode::FT_PIXEL_MODE_LCD => { assert!(bitmap.width % 3 == 0); - (GlyphFormat::Subpixel, (bitmap.width / 3) as i32, bitmap.rows as i32) + (font.get_subpixel_glyph_format(), (bitmap.width / 3) as i32, bitmap.rows as i32) } FT_Pixel_Mode::FT_PIXEL_MODE_LCD_V => { assert!(bitmap.rows % 3 == 0); - (GlyphFormat::Subpixel, bitmap.width as i32, (bitmap.rows / 3) as i32) + (font.get_subpixel_glyph_format(), bitmap.width as i32, (bitmap.rows / 3) as i32) } FT_Pixel_Mode::FT_PIXEL_MODE_MONO => { - (GlyphFormat::Mono, bitmap.width as i32, bitmap.rows as i32) + (GlyphFormat::Alpha, bitmap.width as i32, bitmap.rows as i32) } FT_Pixel_Mode::FT_PIXEL_MODE_GRAY => { (GlyphFormat::Alpha, bitmap.width as i32, bitmap.rows as i32) @@ -620,8 +639,8 @@ impl FontContext { } Some(RasterizedGlyph { - left: ((dimensions.left + left) as f32 * scale).round(), - top: ((dimensions.top + top - actual_height) as f32 * scale).round(), + left: (dimensions.left + left) as f32, + top: (dimensions.top + top - actual_height) as f32, width: actual_width as u32, height: actual_height as u32, scale, diff --git a/webrender/src/platform/windows/font.rs b/webrender/src/platform/windows/font.rs index 00f39eb84e..946fe9630b 100644 --- a/webrender/src/platform/windows/font.rs +++ b/webrender/src/platform/windows/font.rs @@ -6,7 +6,7 @@ use api::{FontInstancePlatformOptions, FontKey, FontRenderMode}; use api::{ColorU, GlyphDimensions, GlyphKey, SubpixelDirection}; use dwrote; use gamma_lut::{ColorLut, GammaLut}; -use glyph_rasterizer::{FontInstance, GlyphFormat, RasterizedGlyph}; +use glyph_rasterizer::{FontInstance, RasterizedGlyph}; use internal_types::FastHashMap; use std::sync::Arc; @@ -161,9 +161,12 @@ impl FontContext { ascenderOffset: 0.0, }; + let (.., minor) = font.transform.compute_scale().unwrap_or((1.0, 1.0)); + let size = (font.size.to_f64_px() * minor) as f32; + let glyph_run = dwrote::DWRITE_GLYPH_RUN { fontFace: unsafe { face.as_ptr() }, - fontEmSize: font.size.to_f32_px(), // size in DIPs (1/96", same as CSS pixels) + fontEmSize: size, // size in DIPs (1/96", same as CSS pixels) glyphCount: 1, glyphIndices: &glyph, glyphAdvances: &advance, @@ -176,25 +179,26 @@ impl FontContext { let dwrite_render_mode = dwrite_render_mode( face, font.render_mode, - font.size.to_f32_px(), + size, dwrite_measure_mode, font.platform_options, ); let (x_offset, y_offset) = font.get_subpx_offset(key); - let transform = Some(dwrote::DWRITE_MATRIX { - m11: 1.0, - m12: 0.0, - m21: 0.0, - m22: 1.0, + let shape = font.transform.pre_scale(minor.recip() as f32, minor.recip() as f32); + let transform = 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, - }); + }; dwrote::GlyphRunAnalysis::create( &glyph_run, 1.0, - transform, + Some(transform), dwrite_render_mode, dwrite_measure_mode, 0.0, @@ -359,7 +363,7 @@ impl FontContext { width, height, scale: 1.0, - format: GlyphFormat::from(font.render_mode), + format: font.get_glyph_format(), bytes: bgra_pixels, }) } diff --git a/webrender/src/prim_store.rs b/webrender/src/prim_store.rs index bbd5797b30..68503e99ca 100644 --- a/webrender/src/prim_store.rs +++ b/webrender/src/prim_store.rs @@ -3,16 +3,16 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use api::{BorderRadius, BuiltDisplayList, ColorF, ComplexClipRegion, DeviceIntRect}; -use api::{DevicePoint, ExtendMode, GlyphInstance, GlyphKey}; +use api::{DevicePoint, ExtendMode, FontRenderMode, GlyphInstance, GlyphKey}; use api::{GradientStop, ImageKey, ImageRendering, ItemRange, ItemTag, LayerPoint, LayerRect}; -use api::{ClipMode, LayerSize, LayerVector2D, LineOrientation, LineStyle}; +use api::{ClipMode, LayerSize, LayerVector2D, LayerToWorldTransform, LineOrientation, LineStyle}; use api::{ClipAndScrollInfo, EdgeAaSegmentMask, PremultipliedColorF, TileOffset}; use api::{ClipId, LayerTransform, PipelineId, YuvColorSpace, YuvFormat}; use border::BorderCornerInstance; use clip_scroll_tree::ClipScrollTree; use clip::{ClipSourcesHandle, ClipStore}; use frame_builder::PrimitiveContext; -use glyph_rasterizer::FontInstance; +use glyph_rasterizer::{FontInstance, FontTransform}; use internal_types::FastHashMap; use gpu_cache::{GpuBlockData, GpuCache, GpuCacheAddress, GpuCacheHandle, GpuDataRequest, ToGpuBlocks}; @@ -591,9 +591,20 @@ pub struct TextRunPrimitiveCpu { impl TextRunPrimitiveCpu { - pub fn get_font(&self, device_pixel_ratio: f32) -> FontInstance { + pub fn get_font( + &self, + device_pixel_ratio: f32, + transform: &LayerToWorldTransform, + ) -> FontInstance { let mut font = self.font.clone(); font.size = font.size.scale_by(device_pixel_ratio); + if font.render_mode == FontRenderMode::Subpixel { + if transform.has_perspective_component() || !transform.has_2d_inverse() { + font.render_mode = FontRenderMode::Alpha; + } else { + font.transform = FontTransform::from(transform).quantize(); + } + } font } @@ -601,10 +612,11 @@ impl TextRunPrimitiveCpu { &mut self, resource_cache: &mut ResourceCache, device_pixel_ratio: f32, + transform: &LayerToWorldTransform, display_list: &BuiltDisplayList, gpu_cache: &mut GpuCache, ) { - let font = self.get_font(device_pixel_ratio); + let font = self.get_font(device_pixel_ratio, transform); // Cache the glyph positions, if not in the cache already. // TODO(gw): In the future, remove `glyph_instances` @@ -1110,6 +1122,7 @@ impl PrimitiveStore { text.prepare_for_render( resource_cache, prim_context.device_pixel_ratio, + &prim_context.scroll_node.world_content_transform, prim_context.display_list, gpu_cache, ); diff --git a/webrender/src/renderer.rs b/webrender/src/renderer.rs index e22ad56bde..f6da53abf1 100644 --- a/webrender/src/renderer.rs +++ b/webrender/src/renderer.rs @@ -271,9 +271,9 @@ impl Into for TextShaderMode { impl From for TextShaderMode { fn from(format: GlyphFormat) -> TextShaderMode { match format { - GlyphFormat::Mono | GlyphFormat::Alpha => TextShaderMode::Alpha, - GlyphFormat::Subpixel => { - panic!("Subpixel glyph format must be handled separately."); + GlyphFormat::Alpha => TextShaderMode::Alpha, + GlyphFormat::Subpixel | GlyphFormat::TransformedSubpixel => { + panic!("Subpixel glyph formats must be handled separately."); } GlyphFormat::ColorBitmap => TextShaderMode::ColorBitmap, } @@ -894,6 +894,7 @@ enum ShaderKind { Cache(VertexArrayKind), ClipCache, Brush, + Text, } struct LazilyCompiledShader { @@ -947,7 +948,7 @@ impl LazilyCompiledShader { if self.program.is_none() { let program = try!{ match self.kind { - ShaderKind::Primitive | ShaderKind::Brush => { + ShaderKind::Primitive | ShaderKind::Brush | ShaderKind::Text => { create_prim_shader(self.name, device, &self.features, @@ -977,11 +978,6 @@ impl LazilyCompiledShader { } } -struct PrimitiveShader { - simple: LazilyCompiledShader, - transform: LazilyCompiledShader, -} - // A brush shader supports two modes: // opaque: // Used for completely opaque primitives, @@ -1055,16 +1051,9 @@ impl BrushShader { } } -struct FileWatcher { - notifier: Box, - result_tx: Sender, -} - -impl FileWatcherHandler for FileWatcher { - fn file_changed(&self, path: PathBuf) { - self.result_tx.send(ResultMsg::RefreshShader(path)).ok(); - self.notifier.wake_up(); - } +struct PrimitiveShader { + simple: LazilyCompiledShader, + transform: LazilyCompiledShader, } impl PrimitiveShader { @@ -1120,6 +1109,87 @@ impl PrimitiveShader { } } +struct TextShader { + simple: LazilyCompiledShader, + transform: LazilyCompiledShader, + glyph_transform: LazilyCompiledShader, +} + +impl TextShader { + fn new( + name: &'static str, + device: &mut Device, + features: &[&'static str], + precache: bool, + ) -> Result { + let simple = try!{ + LazilyCompiledShader::new(ShaderKind::Text, + name, + features, + device, + precache) + }; + + let mut transform_features = features.to_vec(); + transform_features.push("TRANSFORM"); + + let transform = try!{ + LazilyCompiledShader::new(ShaderKind::Text, + name, + &transform_features, + device, + precache) + }; + + let mut glyph_transform_features = features.to_vec(); + glyph_transform_features.push("GLYPH_TRANSFORM"); + + let glyph_transform = try!{ + LazilyCompiledShader::new(ShaderKind::Text, + name, + &glyph_transform_features, + device, + precache) + }; + + Ok(TextShader { simple, transform, glyph_transform }) + } + + fn bind( + &mut self, + device: &mut Device, + glyph_format: GlyphFormat, + transform_kind: TransformedRectKind, + projection: &Transform3D, + mode: M, + renderer_errors: &mut Vec, + ) where M: Into { + match glyph_format { + GlyphFormat::Alpha | + GlyphFormat::Subpixel | + GlyphFormat::ColorBitmap => { + match transform_kind { + TransformedRectKind::AxisAligned => { + self.simple.bind(device, projection, mode, renderer_errors) + } + TransformedRectKind::Complex => { + self.transform.bind(device, projection, mode, renderer_errors) + } + } + } + GlyphFormat::TransformedSubpixel => { + self.glyph_transform.bind(device, projection, mode, renderer_errors) + } + } + } + + fn deinit(self, device: &mut Device) { + self.simple.deinit(device); + self.transform.deinit(device); + self.glyph_transform.deinit(device); + } +} + fn create_prim_shader( name: &'static str, device: &mut Device, @@ -1193,6 +1263,18 @@ fn create_clip_shader(name: &'static str, device: &mut Device) -> Result, + result_tx: Sender, +} + +impl FileWatcherHandler for FileWatcher { + fn file_changed(&self, path: PathBuf) { + self.result_tx.send(ResultMsg::RefreshShader(path)).ok(); + self.notifier.wake_up(); + } +} + #[derive(Clone, Debug, PartialEq)] pub enum ReadPixelsFormat { Rgba8, @@ -1245,8 +1327,8 @@ pub struct Renderer<'a> { // a cache shader (e.g. blur) to the screen. ps_rectangle: PrimitiveShader, ps_rectangle_clip: PrimitiveShader, - ps_text_run: PrimitiveShader, - ps_text_run_subpx_bg_pass1: PrimitiveShader, + ps_text_run: TextShader, + ps_text_run_subpx_bg_pass1: TextShader, ps_image: Vec>, ps_yuv_image: Vec>, ps_border_corner: PrimitiveShader, @@ -1509,17 +1591,17 @@ impl<'a> Renderer<'a> { }; let ps_text_run = try!{ - PrimitiveShader::new("ps_text_run", - &mut device, - &[], - options.precache_shaders) + TextShader::new("ps_text_run", + &mut device, + &[], + options.precache_shaders) }; let ps_text_run_subpx_bg_pass1 = try!{ - PrimitiveShader::new("ps_text_run", - &mut device, - &["SUBPX_BG_PASS1"], - options.precache_shaders) + TextShader::new("ps_text_run", + &mut device, + &["SUBPX_BG_PASS1"], + options.precache_shaders) }; // All image configuration. @@ -2960,6 +3042,7 @@ impl<'a> Renderer<'a> { self.ps_text_run.bind( &mut self.device, + glyph_format, transform_kind, projection, TextShaderMode::from(glyph_format), @@ -2977,6 +3060,7 @@ impl<'a> Renderer<'a> { self.ps_text_run.bind( &mut self.device, + glyph_format, transform_kind, projection, TextShaderMode::SubpixelConstantTextColor, @@ -2998,6 +3082,7 @@ impl<'a> Renderer<'a> { self.ps_text_run.bind( &mut self.device, + glyph_format, transform_kind, projection, TextShaderMode::SubpixelPass0, @@ -3014,6 +3099,7 @@ impl<'a> Renderer<'a> { self.ps_text_run.bind( &mut self.device, + glyph_format, transform_kind, projection, TextShaderMode::SubpixelPass1, @@ -3037,6 +3123,7 @@ impl<'a> Renderer<'a> { self.ps_text_run.bind( &mut self.device, + glyph_format, transform_kind, projection, TextShaderMode::SubpixelWithBgColorPass0, @@ -3053,6 +3140,7 @@ impl<'a> Renderer<'a> { self.ps_text_run_subpx_bg_pass1.bind( &mut self.device, + glyph_format, transform_kind, projection, TextShaderMode::SubpixelWithBgColorPass1, @@ -3070,6 +3158,7 @@ impl<'a> Renderer<'a> { self.ps_text_run.bind( &mut self.device, + glyph_format, transform_kind, projection, TextShaderMode::SubpixelWithBgColorPass2, diff --git a/webrender/src/tiling.rs b/webrender/src/tiling.rs index e6b984668e..74eeb09c26 100644 --- a/webrender/src/tiling.rs +++ b/webrender/src/tiling.rs @@ -543,7 +543,10 @@ fn add_to_batch( let text_cpu = &ctx.prim_store.cpu_text_runs[prim_metadata.cpu_prim_index.0]; - let font = text_cpu.get_font(ctx.device_pixel_ratio); + let font = text_cpu.get_font( + ctx.device_pixel_ratio, + &scroll_node.transform, + ); ctx.resource_cache.fetch_glyphs( font, @@ -1487,7 +1490,10 @@ impl RenderTarget for ColorRenderTarget { [sub_metadata.cpu_prim_index.0]; let text_run_cache_prims = &mut self.text_run_cache_prims; - let font = text.get_font(ctx.device_pixel_ratio); + let font = text.get_font( + ctx.device_pixel_ratio, + &LayerToWorldTransform::identity(), + ); ctx.resource_cache.fetch_glyphs( font, diff --git a/webrender/src/util.rs b/webrender/src/util.rs index acee0d5184..96304d1b46 100644 --- a/webrender/src/util.rs +++ b/webrender/src/util.rs @@ -21,6 +21,7 @@ pub trait MatrixHelpers { fn is_identity(&self) -> bool; fn preserves_2d_axis_alignment(&self) -> bool; fn has_perspective_component(&self) -> bool; + fn has_2d_inverse(&self) -> bool; fn inverse_project(&self, target: &TypedPoint2D) -> Option>; fn inverse_rect_footprint(&self, rect: &TypedRect) -> TypedRect; fn transform_kind(&self) -> TransformedRectKind; @@ -75,6 +76,10 @@ impl MatrixHelpers for TypedTransform3D { self.m14 != 0.0 || self.m24 != 0.0 || self.m34 != 0.0 || self.m44 != 1.0 } + fn has_2d_inverse(&self) -> bool { + self.m11 * self.m22 - self.m12 * self.m21 != 0.0 + } + fn inverse_project(&self, target: &TypedPoint2D) -> Option> { let m: TypedTransform2D; m = TypedTransform2D::column_major( diff --git a/webrender_api/Cargo.toml b/webrender_api/Cargo.toml index b96124e346..accb88f3a8 100644 --- a/webrender_api/Cargo.toml +++ b/webrender_api/Cargo.toml @@ -21,7 +21,7 @@ time = "0.1" [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.4" -core-graphics = "0.12.2" +core-graphics = "0.12.3" [target.'cfg(target_os = "windows")'.dependencies] dwrote = "0.4" diff --git a/wrench/reftests/text/reftest.list b/wrench/reftests/text/reftest.list index 3958f4f369..534fbd0d75 100644 --- a/wrench/reftests/text/reftest.list +++ b/wrench/reftests/text/reftest.list @@ -36,3 +36,6 @@ options(disable-aa) == transparent-no-aa.yaml transparent-no-aa-ref.yaml != diacritics.yaml diacritics-ref.yaml fuzzy(1,1) platform(linux) options(disable-subpixel) == text-masking.yaml text-masking-alpha.png fuzzy(1,44) platform(linux) == text-masking.yaml text-masking-subpx.png +platform(linux) == subpixel-rotate.yaml subpixel-rotate.png +platform(linux) == subpixel-scale.yaml subpixel-scale.png +platform(linux) == subpixel-skew.yaml subpixel-skew.png diff --git a/wrench/reftests/text/subpixel-rotate.png b/wrench/reftests/text/subpixel-rotate.png new file mode 100644 index 0000000000000000000000000000000000000000..caaedb0689f4863d6eb6de812fdb0a4a50faef95 GIT binary patch literal 19168 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 literal 0 HcmV?d00001 diff --git a/wrench/reftests/text/subpixel-skew.yaml b/wrench/reftests/text/subpixel-skew.yaml new file mode 100644 index 0000000000..7d06f722b2 --- /dev/null +++ b/wrench/reftests/text/subpixel-skew.yaml @@ -0,0 +1,10 @@ +root: + items: + - type: stacking-context + bounds: [0, 0, 480, 80] + transform: skew-x(30) + items: + - text: "a Bcd Efgh Ijklm Nopqrs Tuvwxyz" + origin: 20 50 + size: 20 + font: "FreeSans.ttf" diff --git a/wrench/src/yaml_helper.rs b/wrench/src/yaml_helper.rs index 709c00a9bc..de9e8b16fa 100644 --- a/wrench/src/yaml_helper.rs +++ b/wrench/src/yaml_helper.rs @@ -158,6 +158,15 @@ fn make_rotation( pre_transform.pre_mul(&transform).pre_mul(&post_transform) } +// Create a skew matrix, specified in degrees. +fn make_skew( + skew_x: f32, + skew_y: f32, +) -> LayoutTransform { + let alpha = Radians::new(skew_x.to_radians()); + let beta = Radians::new(skew_y.to_radians()); + LayoutTransform::create_skew(alpha, beta) +} impl YamlHelper for Yaml { fn as_f32(&self) -> Option { @@ -335,6 +344,34 @@ impl YamlHelper for Yaml { "rotate-y" if args.len() == 1 => { make_rotation(transform_origin, args[0].parse().unwrap(), 0.0, 1.0, 0.0) } + "scale" if args.len() >= 1 => { + let x = args[0].parse().unwrap(); + // Default to uniform X/Y scale if Y unspecified. + let y = args.get(1).and_then(|a| a.parse().ok()).unwrap_or(x); + // Default to no Z scale if unspecified. + let z = args.get(2).and_then(|a| a.parse().ok()).unwrap_or(1.0); + LayoutTransform::create_scale(x, y, z) + } + "scale-x" if args.len() == 1 => { + LayoutTransform::create_scale(args[0].parse().unwrap(), 1.0, 1.0) + } + "scale-y" if args.len() == 1 => { + LayoutTransform::create_scale(1.0, args[0].parse().unwrap(), 1.0) + } + "scale-z" if args.len() == 1 => { + LayoutTransform::create_scale(1.0, 1.0, args[0].parse().unwrap()) + } + "skew" if args.len() >= 1 => { + // Default to no Y skew if unspecified. + let skew_y = args.get(1).and_then(|a| a.parse().ok()).unwrap_or(0.0); + make_skew(args[0].parse().unwrap(), skew_y) + } + "skew-x" if args.len() == 1 => { + make_skew(args[0].parse().unwrap(), 0.0) + } + "skew-y" if args.len() == 1 => { + make_skew(0.0, args[0].parse().unwrap()) + } "perspective" if args.len() == 1 => { LayoutTransform::create_perspective(args[0].parse().unwrap()) }