From 924def50c37d6df340623bd9a2da115ed683931b Mon Sep 17 00:00:00 2001 From: Glenn Watson Date: Fri, 28 Apr 2017 09:59:53 +1000 Subject: [PATCH] Support dashed borders with new border path. Dashed border edges are handled by the standard ps_border_edge shader. Dashed border corners instead create a clip mask for the border and draw each dash stroke to the clip mask. The math for determining the correct dash strokes is too complex to evaluate efficiently in a shader, doing it this way allows us to support arbitrary kinds of dash strokes. In the future, we will either specialize this to a border render task (to avoid the clip mask target allocation for the whole border rect), or make use of upcoming optimization work in the clip mask code to handle this case more efficiently. This will also serve as the basis for dotted border corners, which are also too complex to evaluate inside a shader. For now, we just treat everything as an ellipse when evaluating the dash strokes. This gives the correct result for ellipses and circle radii, but is very inefficient for circles. As a follow up, we will include a fast path for corners where the x/y radii are equal. Deciding the exact dash length etc is not well specified. The current code is a rough approximation to what Gecko does. In the future we can iterate on this and make it closer to Gecko's rules for dash length. --- webrender/res/cs_clip_border.fs.glsl | 30 +++ webrender/res/cs_clip_border.glsl | 12 + webrender/res/cs_clip_border.vs.glsl | 68 ++++++ webrender/res/prim_shared.glsl | 4 + webrender/res/ps_border_corner.fs.glsl | 5 - webrender/res/ps_border_edge.fs.glsl | 10 +- webrender/res/ps_border_edge.glsl | 1 + webrender/res/ps_border_edge.vs.glsl | 28 +++ webrender/src/border.rs | 251 +++++++++++++++++++-- webrender/src/ellipse.rs | 94 ++++++++ webrender/src/lib.rs | 1 + webrender/src/mask_cache.rs | 84 ++++--- webrender/src/prim_store.rs | 5 +- webrender/src/render_task.rs | 2 +- webrender/src/renderer.rs | 20 ++ webrender/src/tiling.rs | 16 +- wrench/reftests/border/border-suite-2.png | Bin 19778 -> 31729 bytes wrench/reftests/border/border-suite-2.yaml | 54 +++++ 18 files changed, 627 insertions(+), 58 deletions(-) create mode 100644 webrender/res/cs_clip_border.fs.glsl create mode 100644 webrender/res/cs_clip_border.glsl create mode 100644 webrender/res/cs_clip_border.vs.glsl create mode 100644 webrender/src/ellipse.rs diff --git a/webrender/res/cs_clip_border.fs.glsl b/webrender/res/cs_clip_border.fs.glsl new file mode 100644 index 0000000000..1a51066d53 --- /dev/null +++ b/webrender/res/cs_clip_border.fs.glsl @@ -0,0 +1,30 @@ +#line 1 +/* 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/. */ + +void main(void) { + vec2 local_pos = vPos.xy / vPos.z; + + // Get local space position relative to the clip center. + vec2 clip_relative_pos = local_pos - vClipCenter; + + // Get the signed distances to the two clip lines. + float d0 = distance_to_line(vPoint_Tangent0.xy, + vPoint_Tangent0.zw, + clip_relative_pos); + float d1 = distance_to_line(vPoint_Tangent1.xy, + vPoint_Tangent1.zw, + clip_relative_pos); + + // Get AA widths based on zoom / scale etc. + vec2 fw = fwidth(local_pos); + float afwidth = length(fw); + + // Apply AA over half a device pixel for the clip. + float d = smoothstep(-0.5 * afwidth, + 0.5 * afwidth, + max(d0, -d1)); + + oFragColor = vec4(d, 0.0, 0.0, 1.0); +} diff --git a/webrender/res/cs_clip_border.glsl b/webrender/res/cs_clip_border.glsl new file mode 100644 index 0000000000..3f0234522c --- /dev/null +++ b/webrender/res/cs_clip_border.glsl @@ -0,0 +1,12 @@ +#line 1 + +/* 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/. */ + +varying vec3 vPos; + +flat varying vec2 vClipCenter; + +flat varying vec4 vPoint_Tangent0; +flat varying vec4 vPoint_Tangent1; diff --git a/webrender/res/cs_clip_border.vs.glsl b/webrender/res/cs_clip_border.vs.glsl new file mode 100644 index 0000000000..83e282a6d6 --- /dev/null +++ b/webrender/res/cs_clip_border.vs.glsl @@ -0,0 +1,68 @@ +#line 1 +/* 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/. */ + +// Header for a border corner clip. +struct BorderCorner { + RectWithSize rect; + vec2 clip_center; + vec2 sign_modifier; +}; + +BorderCorner fetch_border_corner(int index) { + vec4 data[2] = fetch_data_2(index); + return BorderCorner(RectWithSize(data[0].xy, data[0].zw), + data[1].xy, + data[1].zw); +} + +// Per-dash clip information. +// TODO: Expand this to handle dots in the future! +struct BorderClip { + vec4 point_tangent_0; + vec4 point_tangent_1; +}; + +BorderClip fetch_border_clip(int index) { + vec4 data[2] = fetch_data_2(index); + return BorderClip(data[0], data[1]); +} + +void main(void) { + CacheClipInstance cci = fetch_clip_item(gl_InstanceID); + ClipArea area = fetch_clip_area(cci.render_task_index); + Layer layer = fetch_layer(cci.layer_index); + + // Fetch the header information for this corner clip. + BorderCorner corner = fetch_border_corner(cci.data_index); + vClipCenter = corner.clip_center; + + // Fetch the information about this particular dash. + BorderClip clip = fetch_border_clip(cci.data_index + cci.segment_index + 1); + vPoint_Tangent0 = clip.point_tangent_0 * corner.sign_modifier.xyxy; + vPoint_Tangent1 = clip.point_tangent_1 * corner.sign_modifier.xyxy; + + // Get local vertex position for the corner rect. + // TODO(gw): We could reduce the number of pixels written here + // by calculating a tight fitting bounding box of the dash itself. + vec2 pos = corner.rect.p0 + aPosition.xy * corner.rect.size; + + // Transform to world pos + vec4 world_pos = layer.transform * vec4(pos, 0.0, 1.0); + world_pos.xyz /= world_pos.w; + + // Scale into device pixels. + vec2 device_pos = world_pos.xy * uDevicePixelRatio; + + // Position vertex within the render task area. + vec2 final_pos = device_pos - + area.screen_origin_target_index.xy + + area.task_bounds.xy; + + // Calculate the local space position for this vertex. + vec4 layer_pos = get_layer_pos(world_pos.xy, layer); + vPos = layer_pos.xyw; + + gl_Position = uTransform * vec4(final_pos, 0.0, 1.0); +} diff --git a/webrender/res/prim_shared.glsl b/webrender/res/prim_shared.glsl index 56ebdc161f..619934c6e1 100644 --- a/webrender/res/prim_shared.glsl +++ b/webrender/res/prim_shared.glsl @@ -103,6 +103,10 @@ RectWithEndpoint intersect_rect(RectWithEndpoint a, RectWithEndpoint b) { return RectWithEndpoint(p.xy, max(p.xy, p.zw)); } +float distance_to_line(vec2 p0, vec2 perp_dir, vec2 p) { + vec2 dir_to_p0 = p0 - p; + return dot(normalize(perp_dir), dir_to_p0); +} // TODO: convert back to RectWithEndPoint if driver issues are resolved, if ever. flat varying vec4 vClipMaskUvBounds; diff --git a/webrender/res/ps_border_corner.fs.glsl b/webrender/res/ps_border_corner.fs.glsl index 31589c3c60..43c95106fa 100644 --- a/webrender/res/ps_border_corner.fs.glsl +++ b/webrender/res/ps_border_corner.fs.glsl @@ -54,11 +54,6 @@ float sdEllipse( vec2 p, in vec2 ab ) { return length(r - p ) * sign(p.y-r.y); } -float distance_to_line(vec2 p0, vec2 perp_dir, vec2 p) { - vec2 dir_to_p0 = p0 - p; - return dot(normalize(perp_dir), dir_to_p0); -} - float distance_to_ellipse(vec2 p, vec2 radii) { // sdEllipse fails on exact circles, so handle equal // radii here. The branch coherency should make this diff --git a/webrender/res/ps_border_edge.fs.glsl b/webrender/res/ps_border_edge.fs.glsl index bd8791f4bf..6a9134d3b9 100644 --- a/webrender/res/ps_border_edge.fs.glsl +++ b/webrender/res/ps_border_edge.fs.glsl @@ -24,11 +24,11 @@ void main(void) { // no effect. // Select the x/y coord, depending on which axis this edge is. - float pos = mix(local_pos.x, local_pos.y, vAxisSelect); + vec2 pos = mix(local_pos.xy, local_pos.yx, vAxisSelect); // Get signed distance from each of the inner edges. - float d0 = pos - vEdgeDistance.x; - float d1 = vEdgeDistance.y - pos; + float d0 = pos.x - vEdgeDistance.x; + float d1 = vEdgeDistance.y - pos.x; // SDF union to select both outer edges. float d = min(d0, d1); @@ -42,6 +42,8 @@ void main(void) { // TODO(gw): Support AA for groove/ridge border edge with transforms. vec4 color = mix(vColor0, vColor1, bvec4(d0 * vEdgeDistance.y > 0.0)); - //oFragColor = vec4(d0 * vEdgeDistance.y, -d0 * vEdgeDistance.y, 0, 1.0); + // Apply dashing parameters. + alpha = min(alpha, step(mod(pos.y - vDashParams.x, vDashParams.y), vDashParams.z)); + oFragColor = color * vec4(1.0, 1.0, 1.0, alpha); } diff --git a/webrender/res/ps_border_edge.glsl b/webrender/res/ps_border_edge.glsl index fbffd1873d..5053b00ba1 100644 --- a/webrender/res/ps_border_edge.glsl +++ b/webrender/res/ps_border_edge.glsl @@ -7,6 +7,7 @@ flat varying vec4 vColor1; flat varying vec2 vEdgeDistance; flat varying float vAxisSelect; flat varying float vAlphaSelect; +flat varying vec3 vDashParams; #ifdef WR_FEATURE_TRANSFORM varying vec3 vLocalPos; diff --git a/webrender/res/ps_border_edge.vs.glsl b/webrender/res/ps_border_edge.vs.glsl index c35c703da4..35f5b2e787 100644 --- a/webrender/res/ps_border_edge.vs.glsl +++ b/webrender/res/ps_border_edge.vs.glsl @@ -56,6 +56,30 @@ void write_color(vec4 color, float style, bool flip) { vColor1 = vec4(color.rgb * modulate.y, color.a); } +void write_dash_params(float style, + float border_width, + float edge_length, + float edge_offset) { + // x = offset + // y = dash on + off length + // z = dash length + switch (int(style)) { + case BORDER_STYLE_DASHED: { + float desired_dash_length = border_width * 3.0; + // Consider half total length since there is an equal on/off for each dash. + float dash_count = ceil(0.5 * edge_length / desired_dash_length); + float dash_length = 0.5 * edge_length / dash_count; + vDashParams = vec3(edge_offset - 0.5 * dash_length, + 2.0 * dash_length, + dash_length); + break; + } + default: + vDashParams = vec3(1.0); + break; + } +} + void main(void) { Primitive prim = load_primitive(); Border border = fetch_border(prim.prim_index); @@ -72,6 +96,7 @@ void main(void) { write_edge_distance(segment_rect.p0.x, border.widths.x, adjusted_widths.x, border.style.x, 0.0, 1.0); write_alpha_select(border.style.x); write_color(color, border.style.x, false); + write_dash_params(border.style.x, border.widths.x, segment_rect.size.y, segment_rect.p0.y); break; case 1: segment_rect.p0 = vec2(corners.tl_inner.x, corners.tl_outer.y); @@ -79,6 +104,7 @@ void main(void) { write_edge_distance(segment_rect.p0.y, border.widths.y, adjusted_widths.y, border.style.y, 1.0, 1.0); write_alpha_select(border.style.y); write_color(color, border.style.y, false); + write_dash_params(border.style.y, border.widths.y, segment_rect.size.x, segment_rect.p0.x); break; case 2: segment_rect.p0 = vec2(corners.tr_outer.x - border.widths.z, corners.tr_inner.y); @@ -86,6 +112,7 @@ void main(void) { write_edge_distance(segment_rect.p0.x, border.widths.z, adjusted_widths.z, border.style.z, 0.0, -1.0); write_alpha_select(border.style.z); write_color(color, border.style.z, true); + write_dash_params(border.style.z, border.widths.z, segment_rect.size.y, segment_rect.p0.y); break; case 3: segment_rect.p0 = vec2(corners.bl_inner.x, corners.bl_outer.y - border.widths.w); @@ -93,6 +120,7 @@ void main(void) { write_edge_distance(segment_rect.p0.y, border.widths.w, adjusted_widths.w, border.style.w, 1.0, -1.0); write_alpha_select(border.style.w); write_color(color, border.style.w, true); + write_dash_params(border.style.w, border.widths.w, segment_rect.size.x, segment_rect.p0.x); break; } diff --git a/webrender/src/border.rs b/webrender/src/border.rs index 594514a135..21d2d533de 100644 --- a/webrender/src/border.rs +++ b/webrender/src/border.rs @@ -2,18 +2,28 @@ * 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 ellipse::Ellipse; use frame_builder::FrameBuilder; -use prim_store::{BorderPrimitiveCpu, BorderPrimitiveGpu, PrimitiveContainer}; +use mask_cache::{ClipSource}; +use prim_store::{BorderPrimitiveCpu, BorderPrimitiveGpu, GpuBlock32, PrimitiveContainer}; use tiling::PrimitiveFlags; use util::pack_as_float; use webrender_traits::{BorderSide, BorderStyle, BorderWidths, ClipAndScrollInfo, ClipRegion}; use webrender_traits::{ColorF, LayerPoint, LayerRect, LayerSize, NormalBorder}; -#[derive(Copy, Clone, Debug, PartialEq)] +enum BorderCorner { + TopLeft, + TopRight, + BottomLeft, + BottomRight, +} + +#[derive(Clone, Debug, PartialEq)] pub enum BorderCornerKind { None, Solid, Clip, + Mask(BorderCornerClipData, LayerSize, LayerSize), Unhandled, } @@ -25,13 +35,15 @@ pub enum BorderEdgeKind { Unhandled, } -pub trait NormalBorderHelpers { +trait NormalBorderHelpers { fn get_corner(&self, edge0: &BorderSide, width0: f32, edge1: &BorderSide, width1: f32, - radius: &LayerSize) -> BorderCornerKind; + radius: &LayerSize, + corner: BorderCorner, + border_rect: &LayerRect) -> BorderCornerKind; fn get_edge(&self, edge: &BorderSide, @@ -44,7 +56,9 @@ impl NormalBorderHelpers for NormalBorder { width0: f32, edge1: &BorderSide, width1: f32, - radius: &LayerSize) -> BorderCornerKind { + radius: &LayerSize, + corner: BorderCorner, + border_rect: &LayerRect) -> BorderCornerKind { // If either width is zero, a corner isn't formed. if width0 == 0.0 || width1 == 0.0 { return BorderCornerKind::None; @@ -78,6 +92,45 @@ impl NormalBorderHelpers for NormalBorder { (BorderStyle::Groove, BorderStyle::Groove) | (BorderStyle::Ridge, BorderStyle::Ridge) => BorderCornerKind::Clip, + // Dashed border corners get drawn into a clip mask. + (BorderStyle::Dashed, BorderStyle::Dashed) => { + let size = LayerSize::new(width0.max(radius.width), width1.max(radius.height)); + let (origin, clip_center, sign_modifier) = match corner { + BorderCorner::TopLeft => { + let origin = border_rect.origin; + let clip_center = origin + size; + (origin, clip_center, LayerPoint::new(-1.0, -1.0)) + } + BorderCorner::TopRight => { + let origin = LayerPoint::new(border_rect.origin.x + + border_rect.size.width - + size.width, + border_rect.origin.y); + let clip_center = origin + LayerSize::new(0.0, size.height); + (origin, clip_center, LayerPoint::new(1.0, -1.0)) + } + BorderCorner::BottomRight => { + let origin = border_rect.origin + (border_rect.size - size); + let clip_center = origin; + (origin, clip_center, LayerPoint::new(1.0, 1.0)) + } + BorderCorner::BottomLeft => { + let origin = LayerPoint::new(border_rect.origin.x, + border_rect.origin.y + + border_rect.size.height - + size.height); + let clip_center = origin + LayerSize::new(size.width, 0.0); + (origin, clip_center, LayerPoint::new(-1.0, 1.0)) + } + }; + let clip_data = BorderCornerClipData { + corner_rect: LayerRect::new(origin, size), + clip_center: clip_center, + sign_modifier: sign_modifier, + }; + BorderCornerKind::Mask(clip_data, *radius, LayerSize::new(width0, width1)) + } + // Assume complex for these cases. // TODO(gw): There are some cases in here that can be handled with a fast path. // For example, with inset/outset borders, two of the four corners are solid. @@ -108,10 +161,10 @@ impl NormalBorderHelpers for NormalBorder { BorderStyle::Double | BorderStyle::Groove | - BorderStyle::Ridge => (BorderEdgeKind::Clip, width), + BorderStyle::Ridge | + BorderStyle::Dashed => (BorderEdgeKind::Clip, width), - BorderStyle::Dotted | - BorderStyle::Dashed => (BorderEdgeKind::Unhandled, width), + BorderStyle::Dotted => (BorderEdgeKind::Unhandled, width), } } } @@ -123,7 +176,8 @@ impl FrameBuilder { widths: &BorderWidths, clip_and_scroll: ClipAndScrollInfo, clip_region: &ClipRegion, - use_new_border_path: bool) { + use_new_border_path: bool, + extra_clips: &[ClipSource]) { let radius = &border.radius; let left = &border.left; let right = &border.right; @@ -163,7 +217,7 @@ impl FrameBuilder { self.add_primitive(clip_and_scroll, &rect, clip_region, - &[], + extra_clips, PrimitiveContainer::Border(prim_cpu, prim_gpu)); } @@ -193,10 +247,34 @@ impl FrameBuilder { let bottom = &border.bottom; let corners = [ - border.get_corner(left, widths.left, top, widths.top, &radius.top_left), - border.get_corner(top, widths.top, right, widths.right, &radius.top_right), - border.get_corner(right, widths.right, bottom, widths.bottom, &radius.bottom_right), - border.get_corner(bottom, widths.bottom, left, widths.left, &radius.bottom_left), + border.get_corner(left, + widths.left, + top, + widths.top, + &radius.top_left, + BorderCorner::TopLeft, + rect), + border.get_corner(right, + widths.right, + top, + widths.top, + &radius.top_right, + BorderCorner::TopRight, + rect), + border.get_corner(right, + widths.right, + bottom, + widths.bottom, + &radius.bottom_right, + BorderCorner::BottomRight, + rect), + border.get_corner(left, + widths.left, + bottom, + widths.bottom, + &radius.bottom_left, + BorderCorner::BottomLeft, + rect), ]; // If any of the corners are unhandled, fall back to slow path for now. @@ -206,7 +284,8 @@ impl FrameBuilder { widths, clip_and_scroll, clip_region, - false); + false, + &[]); return; } @@ -229,7 +308,8 @@ impl FrameBuilder { widths, clip_and_scroll, clip_region, - false); + false, + &[]); return; } @@ -284,12 +364,25 @@ impl FrameBuilder { PrimitiveFlags::None); } } else { + // Create clip masks for border corners, if required. + let mut extra_clips = Vec::new(); + + for corner in corners.iter() { + if let &BorderCornerKind::Mask(corner_data, corner_radius, widths) = corner { + let clip_source = BorderCornerClipSource::new(corner_data, + corner_radius, + widths); + extra_clips.push(ClipSource::BorderCorner(clip_source)); + } + } + self.add_normal_border_primitive(rect, border, widths, clip_and_scroll, clip_region, - true); + true, + &extra_clips); } } } @@ -327,3 +420,127 @@ impl BorderSideHelpers for BorderSide { } } } + +/// The source data for a border corner clip mask. +#[derive(Debug, Clone)] +pub struct BorderCornerClipSource { + pub corner_data: BorderCornerClipData, + pub dash_count: usize, + dash_arc_length: f32, + ellipse: Ellipse, +} + +impl BorderCornerClipSource { + pub fn new(corner_data: BorderCornerClipData, + corner_radius: LayerSize, + widths: LayerSize) -> BorderCornerClipSource { + let ellipse = Ellipse::new(corner_radius); + + // Work out a dash length (and therefore dash count) + // based on the width of the border edges. The "correct" + // dash length is not mentioned in the CSS borders + // spec. The calculation below is similar, but not exactly + // the same as what Gecko uses. + // TODO(gw): Iterate on this to get it closer to what Gecko + // uses for dash length. + + // Approximate the total arc length of the quarter ellipse. + let total_arc_length = ellipse.get_quarter_arc_length(); + + // The desired dash length is ~3x the border width. + let average_border_width = 0.5 * (widths.width + widths.height); + let desired_dash_arc_length = average_border_width * 3.0; + + // Get the ideal number of dashes for that arc length. + // This is scaled by 0.5 since there is an on/off length + // for each dash. + let desired_count = 0.5 * total_arc_length / desired_dash_arc_length; + + // Round that up to the nearest integer, so that the dash length + // doesn't exceed the ratio above. + let actual_count = desired_count.ceil(); + + // Get the correct dash arc length. + let dash_arc_length = 0.5 * total_arc_length / actual_count; + + // Get the number of dashes we'll need to fit. + let dash_count = actual_count as usize; + + BorderCornerClipSource { + corner_data: corner_data, + dash_count: dash_count, + ellipse: ellipse, + dash_arc_length: dash_arc_length, + } + } + + pub fn populate_gpu_data(&self, slice: &mut [GpuBlock32]) { + let (header, dashes) = slice.split_first_mut().unwrap(); + *header = self.corner_data.into(); + + let mut current_arc_length = self.dash_arc_length * 0.5; + for dash_index in 0..self.dash_count { + let arc_length0 = current_arc_length; + current_arc_length += self.dash_arc_length; + + let arc_length1 = current_arc_length; + current_arc_length += self.dash_arc_length; + + let dash_data = BorderCornerDashClipData::new(arc_length0, + arc_length1, + &self.ellipse); + dashes[dash_index] = dash_data.into(); + } + } +} + +/// Represents the common GPU data for writing a +/// clip mask for a border corner. +#[derive(Debug, Copy, Clone, PartialEq)] +#[repr(C)] +pub struct BorderCornerClipData { + /// Local space rect of the border corner. + corner_rect: LayerRect, + /// Local space point that is the center of the + /// circle or ellipse that we are clipping against. + clip_center: LayerPoint, + /// A constant that flips the local space points + /// and tangents of the ellipse for this specific + /// corner. This is used since the ellipse points + /// and tangents are always generated for a single + /// quadrant only. + sign_modifier: LayerPoint, +} + +/// Represents the GPU data for drawing a single dash +/// to a clip mask. A dash clip is defined by two lines. +/// We store a point on the ellipse curve, and a tangent +/// to that point, which allows for efficient line-distance +/// calculations in the fragment shader. +#[derive(Debug, Clone)] +#[repr(C)] +pub struct BorderCornerDashClipData { + pub point0: LayerPoint, + pub tangent0: LayerPoint, + pub point1: LayerPoint, + pub tangent1: LayerPoint, +} + +impl BorderCornerDashClipData { + pub fn new(arc_length0: f32, + arc_length1: f32, + ellipse: &Ellipse) -> BorderCornerDashClipData { + let alpha = ellipse.find_angle_for_arc_length(arc_length0); + let beta = ellipse.find_angle_for_arc_length(arc_length1); + + let (p0, t0) = ellipse.get_point_and_tangent(alpha); + let (p1, t1) = ellipse.get_point_and_tangent(beta); + + BorderCornerDashClipData { + point0: p0, + tangent0: t0, + point1: p1, + tangent1: t1, + } + } +} diff --git a/webrender/src/ellipse.rs b/webrender/src/ellipse.rs new file mode 100644 index 0000000000..8511f159d0 --- /dev/null +++ b/webrender/src/ellipse.rs @@ -0,0 +1,94 @@ +/* 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 webrender_traits::{LayerPoint, LayerSize}; +use std::f32::consts::FRAC_PI_2; + +/// Number of steps to integrate arc length over. +const STEP_COUNT: usize = 20; + +/// Represents an ellipse centred at a local space origin. +#[derive(Debug, Clone)] +pub struct Ellipse { + pub radius: LayerSize, +} + +impl Ellipse { + pub fn new(radius: LayerSize) -> Ellipse { + Ellipse { + radius: radius, + } + } + + /// Use Simpsons rule to approximate the arc length of + /// part of an ellipse. Note that this only works over + /// the range of [0, pi/2]. + // TODO(gw): This is a simplistic way to estimate the + // arc length of an ellipse segment. We can probably use + // a faster / more accurate method! + fn get_simpson_length(&self, theta: f32) -> f32 { + let df = theta / STEP_COUNT as f32; + let mut sum = 0.0; + + for i in 0..(STEP_COUNT+1) { + let (sin_theta, cos_theta) = (i as f32 * df).sin_cos(); + let a = self.radius.width * sin_theta; + let b = self.radius.height * cos_theta; + let y = (a*a + b*b).sqrt(); + let q = if i == 0 || i == STEP_COUNT { + 1.0 + } else if i % 2 == 0 { + 2.0 + } else { + 4.0 + }; + + sum += q * y; + } + + (df / 3.0) * sum + } + + /// Binary search to estimate the angle of an ellipse + /// for a given arc length. This only searches over the + /// first quadrant of an ellipse. + pub fn find_angle_for_arc_length(&self, arc_length: f32) -> f32 { + let epsilon = 0.01; + let mut low = 0.0; + let mut high = FRAC_PI_2; + let mut theta = 0.0; + + while low <= high { + theta = 0.5 * (low + high); + let length = self.get_simpson_length(theta); + + if (length - arc_length).abs() < epsilon { + break; + } else if length < arc_length { + low = theta; + } else { + high = theta; + } + } + + theta + } + + /// Approximate the total length of the first quadrant of + /// this ellipse. + pub fn get_quarter_arc_length(&self) -> f32 { + self.get_simpson_length(FRAC_PI_2) + } + + /// Get a point and tangent on this ellipse from a given angle. + /// This only works for the first quadrant of the ellipse. + pub fn get_point_and_tangent(&self, theta: f32) -> (LayerPoint, LayerPoint) { + let (sin_theta, cos_theta) = theta.sin_cos(); + let point = LayerPoint::new(self.radius.width * cos_theta, + self.radius.height * sin_theta); + let tangent = LayerPoint::new(-self.radius.width * sin_theta, + self.radius.height * cos_theta); + (point, tangent) + } +} diff --git a/webrender/src/lib.rs b/webrender/src/lib.rs index 48f6ac8503..1a5d93f0de 100644 --- a/webrender/src/lib.rs +++ b/webrender/src/lib.rs @@ -52,6 +52,7 @@ mod debug_colors; mod debug_font_data; mod debug_render; mod device; +mod ellipse; mod frame; mod frame_builder; mod freelist; diff --git a/webrender/src/mask_cache.rs b/webrender/src/mask_cache.rs index 9c94d4f5a8..5cfbfa699f 100644 --- a/webrender/src/mask_cache.rs +++ b/webrender/src/mask_cache.rs @@ -2,6 +2,7 @@ * 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 border::BorderCornerClipSource; use gpu_store::GpuStoreAddress; use prim_store::{ClipData, GpuBlock32, PrimitiveStore}; use prim_store::{CLIP_DATA_GPU_SIZE, MASK_DATA_GPU_SIZE}; @@ -47,12 +48,19 @@ pub enum ClipSource { // for clip/scroll nodes, but false for primitives, where // the clip rect is handled in local space. Region(ClipRegion, RegionMode), + + // TODO(gw): This currently only handles dashed style + // clips, where the border style is dashed for both + // adjacent border edges. Expand to handle dotted style + // and different styles per edge. + BorderCorner(BorderCornerClipSource), } impl ClipSource { pub fn image_mask(&self) -> Option { match *self { - ClipSource::Complex(..) => None, + ClipSource::Complex(..) | + ClipSource::BorderCorner{..} => None, ClipSource::Region(ref region, _) => region.image_mask, } } @@ -112,9 +120,10 @@ pub enum MaskBounds { #[derive(Clone, Debug)] pub struct MaskCacheInfo { - pub clip_range: ClipAddressRange, - pub effective_clip_count: usize, + pub complex_clip_range: ClipAddressRange, + pub effective_complex_clip_count: usize, pub image: Option<(ImageMask, GpuStoreAddress)>, + pub border_corners: Vec<(BorderCornerClipSource, GpuStoreAddress)>, pub bounds: Option, pub is_aligned: bool, } @@ -130,42 +139,50 @@ impl MaskCacheInfo { } let mut image = None; - let mut clip_count = 0; + let mut border_corners = Vec::new(); + let mut complex_clip_count = 0; // Work out how much clip data space we need to allocate // and if we have an image mask. for clip in clips { match *clip { ClipSource::Complex(..) => { - clip_count += 1; - }, + complex_clip_count += 1; + } ClipSource::Region(ref region, region_mode) => { if let Some(info) = region.image_mask { debug_assert!(image.is_none()); // TODO(gw): Support >1 image mask! image = Some((info, clip_store.alloc(MASK_DATA_GPU_SIZE))); } - clip_count += region.complex.length; + complex_clip_count += region.complex.length; if region_mode == RegionMode::IncludeRect { - clip_count += 1; + complex_clip_count += 1; } - }, + } + ClipSource::BorderCorner(ref source) => { + // One block for the corner header, plus one + // block per dash to clip out. + let gpu_address = clip_store.alloc(1 + source.dash_count); + border_corners.push((source.clone(), gpu_address)); + } } } - let clip_range = ClipAddressRange { - start: if clip_count > 0 { - clip_store.alloc(CLIP_DATA_GPU_SIZE * clip_count) + let complex_clip_range = ClipAddressRange { + start: if complex_clip_count > 0 { + clip_store.alloc(CLIP_DATA_GPU_SIZE * complex_clip_count) } else { GpuStoreAddress(0) }, - item_count: clip_count, + item_count: complex_clip_count, }; Some(MaskCacheInfo { - clip_range: clip_range, - effective_clip_count: clip_range.item_count, + complex_clip_range: complex_clip_range, + effective_complex_clip_count: complex_clip_range.item_count, image: image, + border_corners: border_corners, bounds: None, is_aligned: true, }) @@ -186,8 +203,9 @@ impl MaskCacheInfo { LayerSize::new(2.0 * MAX_CLIP, 2.0 * MAX_CLIP))); let mut local_inner: Option = None; let mut has_clip_out = false; + let mut has_border_clip = false; - self.effective_clip_count = 0; + self.effective_complex_clip_count = 0; self.is_aligned = is_aligned; for source in sources { @@ -198,9 +216,9 @@ impl MaskCacheInfo { if mode == ClipMode::ClipOut { has_clip_out = true; } - debug_assert!(self.effective_clip_count < self.clip_range.item_count); - let address = self.clip_range.start + self.effective_clip_count * CLIP_DATA_GPU_SIZE; - self.effective_clip_count += 1; + debug_assert!(self.effective_complex_clip_count < self.complex_clip_range.item_count); + let address = self.complex_clip_range.start + self.effective_complex_clip_count * CLIP_DATA_GPU_SIZE; + self.effective_complex_clip_count += 1; let slice = clip_store.get_slice_mut(address, CLIP_DATA_GPU_SIZE); let data = ClipData::uniform(rect, radius, mode); @@ -223,17 +241,17 @@ impl MaskCacheInfo { let clips = aux_lists.complex_clip_regions(®ion.complex); if !self.is_aligned && region_mode == RegionMode::IncludeRect { // we have an extra clip rect coming from the transformed layer - debug_assert!(self.effective_clip_count < self.clip_range.item_count); - let address = self.clip_range.start + self.effective_clip_count * CLIP_DATA_GPU_SIZE; - self.effective_clip_count += 1; + debug_assert!(self.effective_complex_clip_count < self.complex_clip_range.item_count); + let address = self.complex_clip_range.start + self.effective_complex_clip_count * CLIP_DATA_GPU_SIZE; + self.effective_complex_clip_count += 1; let slice = clip_store.get_slice_mut(address, CLIP_DATA_GPU_SIZE); PrimitiveStore::populate_clip_data(slice, ClipData::uniform(region.main, 0.0, ClipMode::Clip)); } - debug_assert!(self.effective_clip_count + clips.len() <= self.clip_range.item_count); - let address = self.clip_range.start + self.effective_clip_count * CLIP_DATA_GPU_SIZE; - self.effective_clip_count += clips.len(); + debug_assert!(self.effective_complex_clip_count + clips.len() <= self.complex_clip_range.item_count); + let address = self.complex_clip_range.start + self.effective_complex_clip_count * CLIP_DATA_GPU_SIZE; + self.effective_complex_clip_count += clips.len(); let slice = clip_store.get_slice_mut(address, CLIP_DATA_GPU_SIZE * clips.len()); for (clip, chunk) in clips.iter().zip(slice.chunks_mut(CLIP_DATA_GPU_SIZE)) { @@ -244,12 +262,20 @@ impl MaskCacheInfo { .and_then(|ref inner| r.intersection(inner))); } } + ClipSource::BorderCorner{..} => {} } } + for &(ref source, gpu_address) in &self.border_corners { + has_border_clip = true; + let slice = clip_store.get_slice_mut(gpu_address, + 1 + source.dash_count); + source.populate_gpu_data(slice); + } + // Work out the type of mask geometry we have, based on the // list of clip sources above. - if has_clip_out { + if has_clip_out || has_border_clip { // For clip-out, the mask rect is not known. self.bounds = Some(MaskBounds::None); } else { @@ -288,10 +314,12 @@ impl MaskCacheInfo { } } - /// Check if this `MaskCacheInfo` actually carries any masks. `effective_clip_count` + /// Check if this `MaskCacheInfo` actually carries any masks. `effective_complex_clip_count` /// can change during the `update` call depending on the transformation, so the mask may /// appear to be empty. pub fn is_masking(&self) -> bool { - self.image.is_some() || self.effective_clip_count != 0 + self.image.is_some() || + self.effective_complex_clip_count != 0 || + !self.border_corners.is_empty() } } diff --git a/webrender/src/prim_store.rs b/webrender/src/prim_store.rs index 167d68561d..eb99e1c598 100644 --- a/webrender/src/prim_store.rs +++ b/webrender/src/prim_store.rs @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use app_units::Au; +use border::{BorderCornerClipData, BorderCornerDashClipData}; use euclid::{Size2D}; use gpu_store::GpuStoreAddress; use internal_types::{SourceTexture, PackedTexel}; @@ -1042,6 +1043,7 @@ impl PrimitiveStore { let (rect, is_complex) = match source { ClipSource::Complex(rect, radius, _) => (rect, radius > 0.0), ClipSource::Region(ref region, _) => (region.main, region.is_complex()), + ClipSource::BorderCorner{..} => panic!("Not supported!"), }; self.gpu_geometry.get_mut(GpuStoreAddress(index.0 as i32)) .local_clip_rect = rect; @@ -1340,7 +1342,8 @@ define_gpu_block!(GpuBlock16: [f32; 4] = TextRunPrimitiveGpu, ImagePrimitiveGpu, YuvImagePrimitiveGpu ); define_gpu_block!(GpuBlock32: [f32; 8] = - GradientStopGpu, ClipCorner, ClipRect, ImageMaskData + GradientStopGpu, ClipCorner, ClipRect, ImageMaskData, + BorderCornerClipData, BorderCornerDashClipData ); define_gpu_block!(GpuBlock64: [f32; 16] = GradientPrimitiveGpu, RadialGradientPrimitiveGpu, BoxShadowPrimitiveGpu diff --git a/webrender/src/render_task.rs b/webrender/src/render_task.rs index 5db91128f7..f2c6f2dc2f 100644 --- a/webrender/src/render_task.rs +++ b/webrender/src/render_task.rs @@ -236,7 +236,7 @@ impl RenderTask { if inner_rect.is_some() && clips.len() == 1 { let (_, ref clip_info) = clips[0]; if clip_info.image.is_none() && - clip_info.effective_clip_count == 1 && + clip_info.effective_complex_clip_count == 1 && clip_info.is_aligned { geometry_kind = MaskGeometryKind::CornersOnly; } diff --git a/webrender/src/renderer.rs b/webrender/src/renderer.rs index 2cda9f470d..0fbf7e10c5 100644 --- a/webrender/src/renderer.rs +++ b/webrender/src/renderer.rs @@ -497,6 +497,7 @@ pub struct Renderer { /// of these shaders are also used by the primitive shaders. cs_clip_rectangle: LazilyCompiledShader, cs_clip_image: LazilyCompiledShader, + cs_clip_border: LazilyCompiledShader, // The are "primitive shaders". These shaders draw and blend // final results on screen. They are aware of tile boundaries. @@ -683,6 +684,14 @@ impl Renderer { options.precache_shaders) }; + let cs_clip_border = try!{ + LazilyCompiledShader::new(ShaderKind::ClipCache, + "cs_clip_border", + &[], + &mut device, + options.precache_shaders) + }; + let ps_rectangle = try!{ PrimitiveShader::new("ps_rectangle", &mut device, @@ -1040,6 +1049,7 @@ impl Renderer { cs_text_run: cs_text_run, cs_blur: cs_blur, cs_clip_rectangle: cs_clip_rectangle, + cs_clip_border: cs_clip_border, cs_clip_image: cs_clip_image, ps_rectangle: ps_rectangle, ps_rectangle_clip: ps_rectangle_clip, @@ -1801,6 +1811,16 @@ impl Renderer { &textures, &projection); } + // draw special border clips + if !target.clip_batcher.borders.is_empty() { + let _gm2 = GpuMarker::new(self.device.rc_gl(), "clip borders"); + let shader = self.cs_clip_border.get(&mut self.device).unwrap(); + self.draw_instanced_batch(&target.clip_batcher.borders, + vao, + shader, + &BatchTextures::no_texture(), + &projection); + } } } diff --git a/webrender/src/tiling.rs b/webrender/src/tiling.rs index 695427ddb1..1f567e8e76 100644 --- a/webrender/src/tiling.rs +++ b/webrender/src/tiling.rs @@ -630,6 +630,7 @@ pub struct ClipBatcher { pub rectangles: Vec, /// Image draws apply the image masking. pub images: HashMap>, + pub borders: Vec, } impl ClipBatcher { @@ -637,6 +638,7 @@ impl ClipBatcher { ClipBatcher { rectangles: Vec::new(), images: HashMap::new(), + borders: Vec::new(), } } @@ -654,8 +656,8 @@ impl ClipBatcher { segment: 0, }; - for clip_index in 0..info.effective_clip_count as usize { - let offset = info.clip_range.start.0 + ((CLIP_DATA_GPU_SIZE * clip_index) as i32); + for clip_index in 0..info.effective_complex_clip_count as usize { + let offset = info.complex_clip_range.start.0 + ((CLIP_DATA_GPU_SIZE * clip_index) as i32); match geometry_kind { MaskGeometryKind::Default => { self.rectangles.push(CacheClipInstance { @@ -700,6 +702,16 @@ impl ClipBatcher { ..instance }) } + + for &(ref source, gpu_address) in &info.border_corners { + for dash_index in 0..source.dash_count { + self.borders.push(CacheClipInstance { + address: gpu_address, + segment: dash_index as i32, + ..instance + }) + } + } } } } diff --git a/wrench/reftests/border/border-suite-2.png b/wrench/reftests/border/border-suite-2.png index 71f21fbc046d815dfdd526eceacdf4b6c2ccfd13..6df896ad8679a53b7001ca5e20e43943ba079b49 100644 GIT binary patch literal 31729 zcmbT7bzD?W7w~CRN|5d!2nL-?2q=xTAf3`6EZrd@B3+V8BVED*OLt03EU+{y-QBzw z{5{Y6y#Ky@K78)oyE}Jg&N*{t=A7>Yt0>9fJ)(Ssf`Wo4CoB051qH1J_<#EV3wY9< z%r635Fr38Y)E@wU-VaQ_p`g&C$Vt9YcTd@x^|Fr}Z#=s_l^{Yxl|%a~KcX_0fuU~q zaDsG9gIxsk6}HYQg!2Mh7JOLJsY;HSx`eWUO0j>IdQz#k5{BOk_IxUWIVkw8`m+{h zn_{2T6T;wk?1VA&7~&5uPrHA8)jKgOX-r4VT^=S+Wq+LTO&jtQeR>In8w}}a&_8}a z|K+jxgMTDr)|OmQfE(b`q}(IDyX%8!^&c%H_s`^{{(m1a>BEez3Z&#}V>2J*`Zh$I{F-Sz6# zxP1IxuhWpAZJ90-Nkl=AcK!yK0L%ZiVLh?V~klkCrKBi3V*)?J=gTMD^^=&R+bG<(6| znH@|P{p2AaTL!41See9rV0@RfXc|vY*{@%(!k2S@zuVENdK)r}ABLkB#Q?|I4%Ovk zvp2(-quP(C2|Co+ozp8Vf*vOp6x!Ia#}1|)3*lCik>z1 z>|{$MR`7ux>%^Wi8rZ-i@3q5Z<_+7S>9Xr|b1-l@1t~nd$t3U@ey8C`j zT53Pkok8<%a%7@n3tgridU7JJ$5``*o58Yqt+QnAK`C-T-}O%kV-osqvHNNbL_k%w zb%%n7TG;m`In=kYES@wr`fQWP_=iX`U=h{GKY~q^pAo;I6^UB95N=TRRK-X@*n{c5 zh-h7!7?77>9BdAJYjWP66@w3c#3|; z&-tZe-sJkF3Z^$|B74TTAo%@je>R78p`$T1YVGp2coXf{91G^^zO&Em#UExI>@(Tp zUox-HTp(kKo31JpWt_S|*aQ_WXZDCl1ZEFr6DF?YH7>*wD+2xYwGXn%+Q#|UH8#F| z`%hzh>M5^j-Q$gyw%47EoiUk2lUitMB0`NwHfg4x1sJ&k_G?Vz@>$gnBe3GX7lFFG zqS~snsr(=2i@RZOgzNfs_&j`k8)0D^s%x1Jv%El;a*$cz(ElW?Lx&RAA{P!197H@$ zN#U6t*%%kgnGbpIK$pj1tw_@R(aab%K$i|hvBh`EaAvP3goIes8$aas@oIF6!_l~= zAg5cYZTF`CMU4v-ultd2{S#B03l-^K%%2v zODsRF6RP{EV49?~k+CKQMqWt@L){vpB1hsszrHon-Igaq+Kf8y2PE zxi!b=y^vh~&XOnA?3AqU&E8B2^+Mu_1jGi8q{=y{s;_zues;2bNt3tDe1uSEv0L

ECYhsJ^Q(qpKNq+q)25+t_98%?=kaNxpPsTkvYp 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 Zn1O>Tp4Lt1M=&->QdIU0Ttv_Be*xDa6n_8! literal 19778 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 HZ$0@3JyFm! diff --git a/wrench/reftests/border/border-suite-2.yaml b/wrench/reftests/border/border-suite-2.yaml index ddda497b07..be03f1faa9 100644 --- a/wrench/reftests/border/border-suite-2.yaml +++ b/wrench/reftests/border/border-suite-2.yaml @@ -89,3 +89,57 @@ root: style: ridge color: [ red, green, blue, yellow ] radius: 50 + + - type: border + bounds: [ 10, 230, 100, 100 ] + width: 1 + border-type: normal + style: dashed + color: [ red, green, blue, black ] + radius: 16 + - type: border + bounds: [ 120, 230, 100, 100 ] + width: 2 + border-type: normal + style: dashed + color: [ red, green, blue, black ] + radius: 32 + - type: border + bounds: [ 230, 230, 100, 100 ] + width: 3 + border-type: normal + style: dashed + color: [ red, green, blue, black ] + radius: 32 + - type: border + bounds: [ 340, 230, 100, 100 ] + width: 8 + border-type: normal + style: dashed + color: [ red, green, blue, black ] + radius: 32 + + - type: border + bounds: [ 10, 340, 200, 200 ] + width: [4, 8, 16, 8] + border-type: normal + style: dashed + color: [ red, green, blue, black ] + radius: { + top-left: [32, 64], + top-right: [32, 32], + bottom-left: [64, 32], + bottom-right: [32, 32], + } + - type: border + bounds: [ 230, 340, 200, 200 ] + width: 4 + border-type: normal + style: dashed + color: [ red, green, blue, black ] + radius: { + top-left: [64, 128], + top-right: [16, 32], + bottom-left: [40, 18], + bottom-right: [100, 50], + }