From c1efd22bea24e293fff8e2ecb8bb1cefa5f9bcdc Mon Sep 17 00:00:00 2001 From: Glenn Watson Date: Thu, 4 May 2017 12:53:41 +1000 Subject: [PATCH] Support border corners with differing styles. This handles all border corners styles except those with dashed and/or dotted styles. Those require changes to the border clip mask shader, which will be done in a follow up. The basic idea is to draw the corner twice, once for each style and mask out the other side of the corner. This results in more pixels being drawn than necessary, and can sometimes result in the AA between the corner segments being slightly incorrect. Those issues can be fixed at a later time. --- webrender/res/prim_shared.glsl | 4 +- webrender/res/ps_border_corner.vs.glsl | 66 +++++++++++++---- webrender/res/ps_border_edge.vs.glsl | 20 ++++-- webrender/src/border.rs | 78 ++++++++++++++------- webrender/src/prim_store.rs | 2 + webrender/src/tiling.rs | 20 +++++- wrench/reftests/border/border-suite-3.png | Bin 0 -> 12192 bytes wrench/reftests/border/border-suite-3.yaml | 57 +++++++++++++++ wrench/reftests/border/reftest.list | 1 + 9 files changed, 200 insertions(+), 48 deletions(-) create mode 100644 wrench/reftests/border/border-suite-3.png create mode 100644 wrench/reftests/border/border-suite-3.yaml diff --git a/webrender/res/prim_shared.glsl b/webrender/res/prim_shared.glsl index 4c65969f57..9fbe2a9b87 100644 --- a/webrender/res/prim_shared.glsl +++ b/webrender/res/prim_shared.glsl @@ -324,8 +324,8 @@ struct Border { vec4 radii[2]; }; -vec4 get_effective_border_widths(Border border) { - switch (int(border.style.x)) { +vec4 get_effective_border_widths(Border border, int style) { + switch (style) { case BORDER_STYLE_DOUBLE: // Calculate the width of a border segment in a style: double // border. Round to the nearest CSS pixel. diff --git a/webrender/res/ps_border_corner.vs.glsl b/webrender/res/ps_border_corner.vs.glsl index b0091b989b..60bf0be0e9 100644 --- a/webrender/res/ps_border_corner.vs.glsl +++ b/webrender/res/ps_border_corner.vs.glsl @@ -3,6 +3,11 @@ * 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/. */ +// Matches BorderCornerSide enum in border.rs +#define SIDE_BOTH 0 +#define SIDE_FIRST 1 +#define SIDE_SECOND 2 + vec2 get_radii(vec2 radius, vec2 invalid) { if (all(greaterThan(radius, vec2(0.0)))) { return radius; @@ -11,14 +16,14 @@ vec2 get_radii(vec2 radius, vec2 invalid) { return invalid; } -void set_radii(float style, +void set_radii(int style, vec2 radii, vec2 widths, vec2 adjusted_widths) { vRadii0.xy = get_radii(radii, 2.0 * widths); vRadii0.zw = get_radii(radii - widths, -widths); - switch (int(style)) { + switch (style) { case BORDER_STYLE_RIDGE: case BORDER_STYLE_GROOVE: vRadii1.xy = radii - adjusted_widths; @@ -42,7 +47,7 @@ void set_edge_line(vec2 border_width, vColorEdgeLine = vec4(outer_corner, vec2(-gradient.y, gradient.x)); } -void write_color(vec4 color0, vec4 color1, int style, vec2 delta) { +void write_color(vec4 color0, vec4 color1, int style, vec2 delta, int instance_kind) { vec4 modulate; switch (style) { @@ -64,20 +69,40 @@ void write_color(vec4 color0, vec4 color1, int style, vec2 delta) { break; } + // Optionally mask out one side of the border corner, + // depending on the instance kind. + switch (instance_kind) { + case SIDE_FIRST: + color0.a = 0.0; + break; + case SIDE_SECOND: + color1.a = 0.0; + break; + } + vColor00 = vec4(color0.rgb * modulate.x, color0.a); vColor01 = vec4(color0.rgb * modulate.y, color0.a); vColor10 = vec4(color1.rgb * modulate.z, color1.a); vColor11 = vec4(color1.rgb * modulate.w, color1.a); } +int select_style(int color_select, vec2 style) { + switch (color_select) { + case SIDE_BOTH: + return int(style.x); + case SIDE_FIRST: + return int(style.x); + case SIDE_SECOND: + return int(style.y); + } +} + void main(void) { Primitive prim = load_primitive(); Border border = fetch_border(prim.prim_index); int sub_part = prim.sub_index; BorderCorners corners = get_border_corners(border, prim.local_rect); - vec4 adjusted_widths = get_effective_border_widths(border); - vec4 inv_adjusted_widths = border.widths - adjusted_widths; vec2 p0, p1; // TODO(gw): We'll need to pass through multiple styles @@ -87,6 +112,9 @@ void main(void) { vec4 color0, color1; vec2 color_delta; + // TODO(gw): Now that all border styles are supported, the switch + // statement below can be tidied up quite a bit. + switch (sub_part) { case 0: { p0 = corners.tl_outer; @@ -95,14 +123,16 @@ void main(void) { color1 = border.colors[1]; vClipCenter = corners.tl_outer + border.radii[0].xy; vClipSign = vec2(1.0); - set_radii(border.style.x, + style = select_style(prim.user_data.x, border.style.yx); + vec4 adjusted_widths = get_effective_border_widths(border, style); + vec4 inv_adjusted_widths = border.widths - adjusted_widths; + set_radii(style, border.radii[0].xy, border.widths.xy, adjusted_widths.xy); set_edge_line(border.widths.xy, corners.tl_outer, vec2(1.0, 1.0)); - style = int(border.style.x); edge_distances = vec4(p0 + adjusted_widths.xy, p0 + inv_adjusted_widths.xy); color_delta = vec2(1.0); @@ -115,14 +145,16 @@ void main(void) { color1 = border.colors[2]; vClipCenter = corners.tr_outer + vec2(-border.radii[0].z, border.radii[0].w); vClipSign = vec2(-1.0, 1.0); - set_radii(border.style.y, + style = select_style(prim.user_data.x, border.style.zy); + vec4 adjusted_widths = get_effective_border_widths(border, style); + vec4 inv_adjusted_widths = border.widths - adjusted_widths; + set_radii(style, border.radii[0].zw, border.widths.zy, adjusted_widths.zy); set_edge_line(border.widths.zy, corners.tr_outer, vec2(-1.0, 1.0)); - style = int(border.style.y); edge_distances = vec4(p1.x - adjusted_widths.z, p0.y + adjusted_widths.y, p1.x - border.widths.z + adjusted_widths.z, @@ -137,14 +169,16 @@ void main(void) { color1 = border.colors[3]; vClipCenter = corners.br_outer - border.radii[1].xy; vClipSign = vec2(-1.0, -1.0); - set_radii(border.style.z, + style = select_style(prim.user_data.x, border.style.wz); + vec4 adjusted_widths = get_effective_border_widths(border, style); + vec4 inv_adjusted_widths = border.widths - adjusted_widths; + set_radii(style, border.radii[1].xy, border.widths.zw, adjusted_widths.zw); set_edge_line(border.widths.zw, corners.br_outer, vec2(-1.0, -1.0)); - style = int(border.style.z); edge_distances = vec4(p1.x - adjusted_widths.z, p1.y - adjusted_widths.w, p1.x - border.widths.z + adjusted_widths.z, @@ -159,14 +193,16 @@ void main(void) { color1 = border.colors[0]; vClipCenter = corners.bl_outer + vec2(border.radii[1].z, -border.radii[1].w); vClipSign = vec2(1.0, -1.0); - set_radii(border.style.w, + style = select_style(prim.user_data.x, border.style.xw); + vec4 adjusted_widths = get_effective_border_widths(border, style); + vec4 inv_adjusted_widths = border.widths - adjusted_widths; + set_radii(style, border.radii[1].zw, border.widths.xw, adjusted_widths.xw); set_edge_line(border.widths.xw, corners.bl_outer, vec2(1.0, -1.0)); - style = int(border.style.w); edge_distances = vec4(p0.x + adjusted_widths.x, p1.y - adjusted_widths.w, p0.x + inv_adjusted_widths.x, @@ -176,7 +212,7 @@ void main(void) { } } - switch (int(style)) { + switch (style) { case BORDER_STYLE_DOUBLE: { vEdgeDistance = edge_distances; vAlphaSelect = 0.0; @@ -205,7 +241,7 @@ void main(void) { } } - write_color(color0, color1, style, color_delta); + write_color(color0, color1, style, color_delta, prim.user_data.x); RectWithSize segment_rect; segment_rect.p0 = p0; diff --git a/webrender/res/ps_border_edge.vs.glsl b/webrender/res/ps_border_edge.vs.glsl index 490fc751aa..53dc3a506b 100644 --- a/webrender/res/ps_border_edge.vs.glsl +++ b/webrender/res/ps_border_edge.vs.glsl @@ -103,14 +103,17 @@ void main(void) { Border border = fetch_border(prim.prim_index); int sub_part = prim.sub_index; BorderCorners corners = get_border_corners(border, prim.local_rect); - vec4 adjusted_widths = get_effective_border_widths(border); vec4 color = border.colors[sub_part]; + // TODO(gw): Now that all border styles are supported, the switch + // statement below can be tidied up quite a bit. + RectWithSize segment_rect; switch (sub_part) { - case 0: + case 0: { segment_rect.p0 = vec2(corners.tl_outer.x, corners.tl_inner.y); segment_rect.size = vec2(border.widths.x, corners.bl_inner.y - corners.tl_inner.y); + vec4 adjusted_widths = get_effective_border_widths(border, int(border.style.x)); 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); @@ -120,9 +123,11 @@ void main(void) { segment_rect.p0.y, segment_rect.p0.x + 0.5 * segment_rect.size.x); break; - case 1: + } + case 1: { segment_rect.p0 = vec2(corners.tl_inner.x, corners.tl_outer.y); segment_rect.size = vec2(corners.tr_inner.x - corners.tl_inner.x, border.widths.y); + vec4 adjusted_widths = get_effective_border_widths(border, int(border.style.y)); 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); @@ -132,9 +137,11 @@ void main(void) { segment_rect.p0.x, segment_rect.p0.y + 0.5 * segment_rect.size.y); break; - case 2: + } + case 2: { segment_rect.p0 = vec2(corners.tr_outer.x - border.widths.z, corners.tr_inner.y); segment_rect.size = vec2(border.widths.z, corners.br_inner.y - corners.tr_inner.y); + vec4 adjusted_widths = get_effective_border_widths(border, int(border.style.z)); 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); @@ -144,9 +151,11 @@ void main(void) { segment_rect.p0.y, segment_rect.p0.x + 0.5 * segment_rect.size.x); break; - case 3: + } + case 3: { segment_rect.p0 = vec2(corners.bl_inner.x, corners.bl_outer.y - border.widths.w); segment_rect.size = vec2(corners.br_inner.x - corners.bl_inner.x, border.widths.w); + vec4 adjusted_widths = get_effective_border_widths(border, int(border.style.w)); 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); @@ -156,6 +165,7 @@ void main(void) { segment_rect.p0.x, segment_rect.p0.y + 0.5 * segment_rect.size.y); break; + } } #ifdef WR_FEATURE_TRANSFORM diff --git a/webrender/src/border.rs b/webrender/src/border.rs index 2b41a75e01..bac5230549 100644 --- a/webrender/src/border.rs +++ b/webrender/src/border.rs @@ -11,6 +11,20 @@ use util::{lerp, pack_as_float}; use webrender_traits::{BorderSide, BorderStyle, BorderWidths, ClipAndScrollInfo, ClipRegion}; use webrender_traits::{ColorF, LayerPoint, LayerRect, LayerSize, NormalBorder}; +#[repr(u8)] +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum BorderCornerInstance { + Single, // Single instance needed - corner styles are same or similar. + Double, // Different corner styles. Draw two instances, one per style. +} + +#[repr(C)] +pub enum BorderCornerSide { + Both, + First, + Second, +} + #[repr(C)] enum BorderCorner { TopLeft, @@ -23,7 +37,7 @@ enum BorderCorner { pub enum BorderCornerKind { None, Solid, - Clip, + Clip(BorderCornerInstance), Mask(BorderCornerClipData, LayerSize, LayerSize, BorderCornerClipKind), Unhandled, } @@ -129,7 +143,7 @@ impl NormalBorderHelpers for NormalBorder { if edge0.color == edge1.color && radius.width == 0.0 && radius.height == 0.0 { BorderCornerKind::Solid } else { - BorderCornerKind::Clip + BorderCornerKind::Clip(BorderCornerInstance::Single) } } @@ -139,9 +153,11 @@ impl NormalBorderHelpers for NormalBorder { (BorderStyle::Inset, BorderStyle::Inset) | (BorderStyle::Double, BorderStyle::Double) | (BorderStyle::Groove, BorderStyle::Groove) | - (BorderStyle::Ridge, BorderStyle::Ridge) => BorderCornerKind::Clip, + (BorderStyle::Ridge, BorderStyle::Ridge) => { + BorderCornerKind::Clip(BorderCornerInstance::Single) + } - // Dashed border corners get drawn into a clip mask. + // Dashed and dotted border corners get drawn into a clip mask. (BorderStyle::Dashed, BorderStyle::Dashed) => { BorderCornerKind::new_mask(BorderCornerClipKind::Dash, width0, @@ -150,7 +166,6 @@ impl NormalBorderHelpers for NormalBorder { *radius, *border_rect) } - (BorderStyle::Dotted, BorderStyle::Dotted) => { BorderCornerKind::new_mask(BorderCornerClipKind::Dot, width0, @@ -160,16 +175,20 @@ impl NormalBorderHelpers for NormalBorder { *border_rect) } - // 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. - (BorderStyle::Dotted, _) | (_, BorderStyle::Dotted) => BorderCornerKind::Unhandled, - (BorderStyle::Dashed, _) | (_, BorderStyle::Dashed) => BorderCornerKind::Unhandled, - (BorderStyle::Double, _) | (_, BorderStyle::Double) => BorderCornerKind::Unhandled, - (BorderStyle::Groove, _) | (_, BorderStyle::Groove) => BorderCornerKind::Unhandled, - (BorderStyle::Ridge, _) | (_, BorderStyle::Ridge) => BorderCornerKind::Unhandled, - (BorderStyle::Outset, _) | (_, BorderStyle::Outset) => BorderCornerKind::Unhandled, - (BorderStyle::Inset, _) | (_, BorderStyle::Inset) => BorderCornerKind::Unhandled, + // TODO(gw): Handle border corners with both dots and dashes. + // Once these are handled, the old border path can + // be removed. + (BorderStyle::Dotted, _) | + (_, BorderStyle::Dotted) | + (BorderStyle::Dashed, _) | + (_, BorderStyle::Dashed) => BorderCornerKind::Unhandled, + + // Everything else can be handled by drawing the corner twice, + // where the shader outputs zero alpha for the side it's not + // drawing. This is somewhat inefficient in terms of pixels + // written, but it's a fairly rare case, and we can optimize + // this case later. + _ => BorderCornerKind::Clip(BorderCornerInstance::Double), } } @@ -205,6 +224,7 @@ impl FrameBuilder { clip_and_scroll: ClipAndScrollInfo, clip_region: &ClipRegion, use_new_border_path: bool, + corner_instances: [BorderCornerInstance; 4], extra_clips: &[ClipSource]) { let radius = &border.radius; let left = &border.left; @@ -220,6 +240,7 @@ impl FrameBuilder { let prim_cpu = BorderPrimitiveCpu { use_new_border_path: use_new_border_path, + corner_instances: corner_instances, }; let prim_gpu = BorderPrimitiveGpu { @@ -313,6 +334,7 @@ impl FrameBuilder { clip_and_scroll, clip_region, false, + [BorderCornerInstance::Single; 4], &[]); return; } @@ -382,14 +404,21 @@ impl FrameBuilder { } 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, kind) = corner { - let clip_source = BorderCornerClipSource::new(corner_data, - corner_radius, - widths, - kind); - extra_clips.push(ClipSource::BorderCorner(clip_source)); + let mut corner_instances = [BorderCornerInstance::Single; 4]; + + for (i, corner) in corners.iter().enumerate() { + match corner { + &BorderCornerKind::Mask(corner_data, corner_radius, widths, kind) => { + let clip_source = BorderCornerClipSource::new(corner_data, + corner_radius, + widths, + kind); + extra_clips.push(ClipSource::BorderCorner(clip_source)); + } + &BorderCornerKind::Clip(instance_kind) => { + corner_instances[i] = instance_kind; + } + _ => {} } } @@ -399,6 +428,7 @@ impl FrameBuilder { clip_and_scroll, clip_region, true, + corner_instances, &extra_clips); } } @@ -712,4 +742,4 @@ impl DotInfo { diameter: diameter, } } -} \ No newline at end of file +} diff --git a/webrender/src/prim_store.rs b/webrender/src/prim_store.rs index d02182dc03..36b5a035d2 100644 --- a/webrender/src/prim_store.rs +++ b/webrender/src/prim_store.rs @@ -4,6 +4,7 @@ use app_units::Au; use border::{BorderCornerClipData, BorderCornerDashClipData, BorderCornerDotClipData}; +use border::BorderCornerInstance; use euclid::{Size2D}; use gpu_store::GpuStoreAddress; use internal_types::{SourceTexture, PackedTexel}; @@ -219,6 +220,7 @@ pub struct BorderPrimitiveCpu { // TODO(gw): Remove this when all border kinds are switched // over to the new border path! pub use_new_border_path: bool, + pub corner_instances: [BorderCornerInstance; 4], } #[derive(Debug, Clone)] diff --git a/webrender/src/tiling.rs b/webrender/src/tiling.rs index 26f78611a0..d0092b8910 100644 --- a/webrender/src/tiling.rs +++ b/webrender/src/tiling.rs @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use app_units::Au; +use border::{BorderCornerInstance, BorderCornerSide}; use device::TextureId; use fnv::FnvHasher; use gpu_store::GpuStoreAddress; @@ -425,8 +426,23 @@ impl AlphaRenderItem { let edge_key = AlphaBatchKey::new(AlphaBatchKind::BorderEdge, flags, blend_mode, textures); batch_list.with_suitable_batch(&corner_key, item_bounding_rect, |batch| { - for border_segment in 0..4 { - batch.add_instance(base_instance.build(border_segment, 0, 0)); + for (i, instance_kind) in border_cpu.corner_instances.iter().enumerate() { + let sub_index = i as i32; + match *instance_kind { + BorderCornerInstance::Single => { + batch.add_instance(base_instance.build(sub_index, + BorderCornerSide::Both as i32, + 0)); + } + BorderCornerInstance::Double => { + batch.add_instance(base_instance.build(sub_index, + BorderCornerSide::First as i32, + 0)); + batch.add_instance(base_instance.build(sub_index, + BorderCornerSide::Second as i32, + 0)); + } + } } }); diff --git a/wrench/reftests/border/border-suite-3.png b/wrench/reftests/border/border-suite-3.png new file mode 100644 index 0000000000000000000000000000000000000000..8b8dea1406a78f68cc7749f6bd15d06782d68c2f GIT binary patch literal 12192 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 Z