Bug 1624468 - Add a fast path for more gradient types in WR r=gw

Add support for repeating gradients in the cached fast path.

Documentation:
https://bugzilla.mozilla.org/attachment.cgi?id=9138638

Differential Revision: https://phabricator.services.mozilla.com/D69476

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Bert Peers 2020-04-08 22:19:11 +00:00
Родитель 7b5b3c8ae8
Коммит 03acaa106c
5 изменённых файлов: 493 добавлений и 125 удалений

Просмотреть файл

@ -19,6 +19,7 @@ use crate::prim_store::{PrimKeyCommonData, PrimTemplateCommonData, PrimitiveStor
use crate::prim_store::{NinePatchDescriptor, PointKey, SizeKey, InternablePrimitive};
use std::{hash, ops::{Deref, DerefMut}};
use crate::util::pack_as_float;
use crate::texture_cache::TEXTURE_REGION_DIMENSIONS;
/// The maximum number of stops a gradient may have to use the fast path.
pub const GRADIENT_FP_STOPS: usize = 4;
@ -151,9 +152,7 @@ impl From<LinearGradientKey> for LinearGradientTemplate {
// gradient in a smaller task, and drawing as an image.
// TODO(gw): Aim to reduce the constraints on fast path gradients in future,
// although this catches the vast majority of gradients on real pages.
let supports_caching =
// No repeating support in fast path
item.extend_mode == ExtendMode::Clamp &&
let mut supports_caching =
// Gradient must cover entire primitive
item.tile_spacing.w + item.stretch_size.w >= common.prim_size.width &&
item.tile_spacing.h + item.stretch_size.h >= common.prim_size.height &&
@ -163,6 +162,31 @@ impl From<LinearGradientKey> for LinearGradientTemplate {
// Fast path not supported on segmented (border-image) gradients.
item.nine_patch.is_none();
// if we support caching and the gradient uses repeat, we might potentially
// emit a lot of quads to cover the primitive. each quad will still cover
// the entire gradient along the other axis, so the effect is linear in
// display resolution, not quadratic (unlike say a tiny background image
// tiling the display). in addition, excessive minification may lead to
// texture trashing. so use the minification as a proxy heuristic for both
// cases.
//
// note that the actual number of quads may be further increased due to
// hard-stops and/or more than GRADIENT_FP_STOPS stops per gradient.
if supports_caching && item.extend_mode == ExtendMode::Repeat {
let single_repeat_size =
if item.start_point.x.approx_eq(&item.end_point.x) {
item.end_point.y - item.start_point.y
} else {
item.end_point.x - item.start_point.x
};
let downscaling = single_repeat_size as f32 / TEXTURE_REGION_DIMENSIONS as f32;
if downscaling < 0.1 {
// if a single copy of the gradient is this small relative to its baked
// gradient cache, we have bad texture caching and/or too many quads.
supports_caching = false;
}
}
// Convert the stops to more convenient representation
// for the current gradient builder.
let stops: Vec<GradientStop> = item.stops.iter().map(|stop| {

Просмотреть файл

@ -6,7 +6,7 @@ use api::{BorderRadius, ClipMode, ColorF, ColorU};
use api::{ImageRendering, RepeatMode, PrimitiveFlags};
use api::{PremultipliedColorF, PropertyBinding, Shadow, GradientStop};
use api::{BoxShadowClipMode, LineStyle, LineOrientation, BorderStyle};
use api::{PrimitiveKeyKind};
use api::{PrimitiveKeyKind, ExtendMode};
use api::units::*;
use crate::border::{get_max_scale_for_border, build_border_instances};
use crate::border::BorderSegmentCacheKey;
@ -3267,17 +3267,20 @@ impl PrimitiveStore {
// size of the gradient task is the length of a texture cache
// region, for maximum accuracy, and a minimal size on the
// axis that doesn't matter.
let (size, orientation, start_point, end_point) = if prim_data.start_point.x.approx_eq(&prim_data.end_point.x) {
let start_point = -prim_data.start_point.y / gradient_size.height;
let end_point = (prim_data.common.prim_size.height - prim_data.start_point.y) / gradient_size.height;
let size = DeviceIntSize::new(16, TEXTURE_REGION_DIMENSIONS);
(size, LineOrientation::Vertical, start_point, end_point)
} else {
let start_point = -prim_data.start_point.x / gradient_size.width;
let end_point = (prim_data.common.prim_size.width - prim_data.start_point.x) / gradient_size.width;
let size = DeviceIntSize::new(TEXTURE_REGION_DIMENSIONS, 16);
(size, LineOrientation::Horizontal, start_point, end_point)
};
let (size, orientation, prim_start_offset, prim_end_offset) =
if prim_data.start_point.x.approx_eq(&prim_data.end_point.x) {
let prim_start_offset = -prim_data.start_point.y / gradient_size.height;
let prim_end_offset = (prim_data.common.prim_size.height - prim_data.start_point.y)
/ gradient_size.height;
let size = DeviceIntSize::new(16, TEXTURE_REGION_DIMENSIONS);
(size, LineOrientation::Vertical, prim_start_offset, prim_end_offset)
} else {
let prim_start_offset = -prim_data.start_point.x / gradient_size.width;
let prim_end_offset = (prim_data.common.prim_size.width - prim_data.start_point.x)
/ gradient_size.width;
let size = DeviceIntSize::new(TEXTURE_REGION_DIMENSIONS, 16);
(size, LineOrientation::Horizontal, prim_start_offset, prim_end_offset)
};
// Build the cache key, including information about the stops.
let mut stops = vec![GradientStopKey::empty(); prim_data.stops.len()];
@ -3298,123 +3301,224 @@ impl PrimitiveStore {
}
}
// To support clamping, we need to make sure that quads are emitted for the
// segments before and after the 0.0...1.0 range of offsets. The loop below
// can handle that by duplicating the first and last point if necessary:
if start_point < 0.0 {
stops.insert(0, GradientStopKey {
offset: start_point,
color : stops[0].color
});
}
if end_point > 1.0 {
stops.push( GradientStopKey {
offset: end_point,
color : stops[stops.len()-1].color
});
}
gradient.cache_segments.clear();
let mut first_stop = 0;
// look for an inclusive range of stops [first_stop, last_stop].
// once first_stop points at (or past) the last stop, we're done.
while first_stop < stops.len()-1 {
// emit render task caches and image rectangles to draw a gradient
// with offsets from start_offset to end_offset.
//
// the primitive is covered by a gradient that ranges from
// prim_start_offset to prim_end_offset.
//
// when clamping, these two pairs of offsets will always be the same.
// when repeating, however, we march across the primitive, blitting
// copies of the gradient along the way. each copy has a range from
// 0.0 to 1.0 (assuming it's fully visible), but where it appears on
// the primitive changes as we go. this position is also expressed
// as an offset: gradient_offset_base. that is, in terms of stops,
// we draw a gradient from start_offset to end_offset. its actual
// location on the primitive is at start_offset + gradient_offset_base.
//
// either way, we need a while-loop to draw the gradient as well
// because it might have more than 4 stops (the maximum of a cached
// segment) and/or hard stops. so we have a walk-within-the-walk from
// start_offset to end_offset caching up to GRADIENT_FP_STOPS stops at a
// time.
fn emit_segments(start_offset: f32, // start and end offset together are
end_offset: f32, // always a subrange of 0..1
gradient_offset_base: f32,
prim_start_offset: f32, // the offsets of the entire gradient as it
prim_end_offset: f32, // covers the entire primitive.
prim_origin_in: LayoutPoint,
prim_size_in: LayoutSize,
task_size: DeviceIntSize,
is_opaque: bool,
stops: &[GradientStopKey],
orientation: LineOrientation,
frame_state: &mut FrameBuildingState,
gradient: &mut LinearGradientPrimitive)
{
// these prints are used to generate documentation examples, so
// leaving them in but commented out:
//println!("emit_segments call:");
//println!("\tstart_offset: {}, end_offset: {}", start_offset, end_offset);
//println!("\tprim_start_offset: {}, prim_end_offset: {}", prim_start_offset, prim_end_offset);
//println!("\tgradient_offset_base: {}", gradient_offset_base);
let mut first_stop = 0;
// look for an inclusive range of stops [first_stop, last_stop].
// once first_stop points at (or past) the last stop, we're done.
while first_stop < stops.len()-1 {
// if the entire segment starts at an offset that's past the primitive's
// end_point, we're done.
if stops[first_stop].offset > end_point {
break;
}
// accumulate stops until we have GRADIENT_FP_STOPS of them, or we hit
// a hard stop:
let mut last_stop = first_stop;
let mut hard_stop = false; // did we stop on a hard stop?
while last_stop < stops.len()-1 &&
last_stop - first_stop + 1 < GRADIENT_FP_STOPS
{
if stops[last_stop+1].offset == stops[last_stop].offset {
hard_stop = true;
break;
// if the entire sub-gradient starts at an offset that's past the
// segment's end offset, we're done.
if stops[first_stop].offset > end_offset {
return;
}
last_stop = last_stop + 1;
}
// accumulate stops until we have GRADIENT_FP_STOPS of them, or we hit
// a hard stop:
let mut last_stop = first_stop;
let mut hard_stop = false; // did we stop on a hard stop?
while last_stop < stops.len()-1 &&
last_stop - first_stop + 1 < GRADIENT_FP_STOPS
{
if stops[last_stop+1].offset == stops[last_stop].offset {
hard_stop = true;
break;
}
let num_stops = last_stop - first_stop + 1;
// repeated hard stops at the same offset, skip
if num_stops == 0 {
first_stop = last_stop + 1;
continue;
}
// if the last stop offset is before start_point, the segment's not visible:
if stops[last_stop].offset < start_point {
first_stop = if hard_stop { last_stop+1 } else { last_stop };
continue;
}
let segment_start_point = start_point.max(stops[first_stop].offset);
let segment_end_point = end_point .min(stops[last_stop ].offset);
let mut segment_stops = [GradientStopKey::empty(); GRADIENT_FP_STOPS];
for i in 0..num_stops {
segment_stops[i] = stops[first_stop + i];
}
let cache_key = GradientCacheKey {
orientation,
start_stop_point: VectorKey {
x: segment_start_point,
y: segment_end_point,
},
stops: segment_stops,
};
let mut prim_origin = prim_instance.prim_origin;
let mut prim_size = prim_data.common.prim_size;
let inv_length = 1.0 / ( end_point - start_point );
if orientation == LineOrientation::Horizontal {
prim_origin.x += ( segment_start_point - start_point ) * inv_length * prim_size.width;
prim_size.width *= ( segment_end_point - segment_start_point ) * inv_length;
} else {
prim_origin.y += ( segment_start_point - start_point ) * inv_length * prim_size.height;
prim_size.height *= ( segment_end_point - segment_start_point ) * inv_length;
}
let local_rect = LayoutRect::new( prim_origin, prim_size );
// Request the render task each frame.
gradient.cache_segments.push(
CachedGradientSegment {
handle: frame_state.resource_cache.request_render_task(
RenderTaskCacheKey {
size,
kind: RenderTaskCacheKeyKind::Gradient(cache_key),
},
frame_state.gpu_cache,
frame_state.render_tasks,
None,
prim_data.stops_opacity.is_opaque,
|render_tasks| {
render_tasks.add().init(RenderTask::new_gradient(
size,
segment_stops,
orientation,
segment_start_point,
segment_end_point,
))
}),
local_rect: local_rect,
last_stop = last_stop + 1;
}
);
// if ending on a hardstop, skip past it for the start of the next run:
first_stop = if hard_stop { last_stop + 1 } else { last_stop };
let num_stops = last_stop - first_stop + 1;
// repeated hard stops at the same offset, skip
if num_stops == 0 {
first_stop = last_stop + 1;
continue;
}
// if the last_stop offset is before start_offset, the segment's not visible:
if stops[last_stop].offset < start_offset {
first_stop = if hard_stop { last_stop+1 } else { last_stop };
continue;
}
let segment_start_point = start_offset.max(stops[first_stop].offset);
let segment_end_point = end_offset .min(stops[last_stop ].offset);
let mut segment_stops = [GradientStopKey::empty(); GRADIENT_FP_STOPS];
for i in 0..num_stops {
segment_stops[i] = stops[first_stop + i];
}
let cache_key = GradientCacheKey {
orientation,
start_stop_point: VectorKey {
x: segment_start_point,
y: segment_end_point,
},
stops: segment_stops,
};
let mut prim_origin = prim_origin_in;
let mut prim_size = prim_size_in;
// the primitive is covered by a segment from overall_start to
// overall_end; scale and shift based on the length of the actual
// segment that we're drawing:
let inv_length = 1.0 / ( prim_end_offset - prim_start_offset );
if orientation == LineOrientation::Horizontal {
prim_origin.x += ( segment_start_point + gradient_offset_base - prim_start_offset )
* inv_length * prim_size.width;
prim_size.width *= ( segment_end_point - segment_start_point )
* inv_length; // 2 gradient_offset_bases cancel out
} else {
prim_origin.y += ( segment_start_point + gradient_offset_base - prim_start_offset )
* inv_length * prim_size.height;
prim_size.height *= ( segment_end_point - segment_start_point )
* inv_length; // 2 gradient_offset_bases cancel out
}
// <= 0 can happen if a hardstop lands exactly on an edge
if prim_size.area() > 0.0 {
let local_rect = LayoutRect::new( prim_origin, prim_size );
// documentation example traces:
//println!("\t\tcaching from offset {} to {}", segment_start_point, segment_end_point);
//println!("\t\tand blitting to {:?}", local_rect);
// Request the render task each frame.
gradient.cache_segments.push(
CachedGradientSegment {
handle: frame_state.resource_cache.request_render_task(
RenderTaskCacheKey {
size: task_size,
kind: RenderTaskCacheKeyKind::Gradient(cache_key),
},
frame_state.gpu_cache,
frame_state.render_tasks,
None,
is_opaque,
|render_tasks| {
render_tasks.add().init(RenderTask::new_gradient(
task_size,
segment_stops,
orientation,
segment_start_point,
segment_end_point,
))
}),
local_rect: local_rect,
}
);
}
// if ending on a hardstop, skip past it for the start of the next run:
first_stop = if hard_stop { last_stop + 1 } else { last_stop };
}
}
if prim_data.extend_mode == ExtendMode::Clamp ||
( prim_start_offset >= 0.0 && prim_end_offset <= 1.0 ) // repeat doesn't matter
{
// To support clamping, we need to make sure that quads are emitted for the
// segments before and after the 0.0...1.0 range of offsets. emit_segments
// can handle that by duplicating the first and last point if necessary:
if prim_start_offset < 0.0 {
stops.insert(0, GradientStopKey {
offset: prim_start_offset,
color : stops[0].color
});
}
if prim_end_offset > 1.0 {
stops.push( GradientStopKey {
offset: prim_end_offset,
color : stops[stops.len()-1].color
});
}
emit_segments(prim_start_offset, prim_end_offset,
0.0,
prim_start_offset, prim_end_offset,
prim_instance.prim_origin,
prim_data.common.prim_size,
size,
prim_data.stops_opacity.is_opaque,
&stops,
orientation,
frame_state,
gradient);
}
else
{
let mut segment_start_point = prim_start_offset;
while segment_start_point < prim_end_offset {
// gradient stops are expressed in the range 0.0 ... 1.0, so to blit
// a copy of the gradient, snap to the integer just before the offset
// we want ...
let gradient_offset_base = segment_start_point.floor();
// .. and then draw from a start offset in range 0 to 1 ...
let repeat_start = segment_start_point - gradient_offset_base;
// .. up to the next integer, but clamped to the primitive's real
// end offset:
let repeat_end = (gradient_offset_base + 1.0).min(prim_end_offset) - gradient_offset_base;
emit_segments(repeat_start, repeat_end,
gradient_offset_base,
prim_start_offset, prim_end_offset,
prim_instance.prim_origin,
prim_data.common.prim_size,
size,
prim_data.stops_opacity.is_opaque,
&stops,
orientation,
frame_state,
gradient);
segment_start_point = repeat_end + gradient_offset_base;
}
}
}

Просмотреть файл

@ -0,0 +1,119 @@
---
root:
items:
# non-repeating
- type: gradient
bounds: 100 50 500 10
start: 100 0
end: 200 0
repeat: false
stops: [0.0, green,
0.5, green,
0.5, blue,
1.0, blue ]
# repeat 4 times
- type: gradient
bounds: 100 100 500 10
start: 100 0
end: 200 0
repeat: true
stops: [0.0, green,
0.5, green,
0.5, blue,
1.0, blue ]
# same but start doesn't line up with 0
- type: gradient
bounds: 100 150 500 10
start: 125 0
end: 225 0
repeat: true
stops: [0.0, green,
0.5, green,
0.5, blue,
1.0, blue ]
# more hard stops, non-uniform distribution
- type: gradient
bounds: 100 250 500 10
start: 200 0
end: 300 0
repeat: false
stops: [0.0, green,
0.25, green,
0.25, red,
0.75, red,
0.75, blue,
1.0, blue ]
# repeat the hard stops
- type: gradient
bounds: 100 300 500 10
start: 200 0
end: 300 0
repeat: true
stops: [0.0, green,
0.25, green,
0.25, red,
0.75, red,
0.75, blue,
1.0, blue ]
# same but start doesn't line up with 0
- type: gradient
bounds: 100 350 500 10
start: 175 0
end: 275 0
repeat: true
stops: [0.0, green,
0.25, green,
0.25, red,
0.75, red,
0.75, blue,
1.0, blue ]
# the entire gradient from 0 to 1 is
# "offscreen", we're only seeing its
# repeats. the gradient is 100 wide
# and ends at -75, so the first
# three-quarters of it would be hidden,
# that is, it should start with blue.
- type: gradient
bounds: 100 400 500 10
start: -175 0
end: -75 0
repeat: true
stops: [0.0, green,
0.25, green,
0.25, red,
0.75, red,
0.75, blue,
1.0, blue ]
# same but over on the right
- type: gradient
bounds: 100 450 500 10
start: 575 0
end: 675 0
repeat: true
stops: [0.0, green,
0.25, green,
0.25, red,
0.75, red,
0.75, blue,
1.0, blue ]
# a repeat, but not really because only part
# of the gradient is visible
- type: gradient
bounds: 100 500 500 10
start: -50 0
end: 550 0
repeat: true
stops: [0.0, green,
0.25, green,
0.25, red,
0.75, red,
0.75, blue,
1.0, blue ]

Просмотреть файл

@ -0,0 +1,119 @@
---
root:
items:
# non-repeating
- type: gradient
bounds: 100 50 500 10
start: 100 0
end: 200 0.001
repeat: false
stops: [0.0, green,
0.5, green,
0.5, blue,
1.0, blue ]
# repeat 4 times
- type: gradient
bounds: 100 100 500 10
start: 100 0
end: 200 0.001
repeat: true
stops: [0.0, green,
0.5, green,
0.5, blue,
1.0, blue ]
# same but start doesn't line up with 0
- type: gradient
bounds: 100 150 500 10
start: 125 0
end: 225 0.001
repeat: true
stops: [0.0, green,
0.5, green,
0.5, blue,
1.0, blue ]
# more hard stops, non-uniform distribution
- type: gradient
bounds: 100 250 500 10
start: 200 0
end: 300 0.001
repeat: false
stops: [0.0, green,
0.25, green,
0.25, red,
0.75, red,
0.75, blue,
1.0, blue ]
# repeat the hard stops
- type: gradient
bounds: 100 300 500 10
start: 200 0
end: 300 0.001
repeat: true
stops: [0.0, green,
0.25, green,
0.25, red,
0.75, red,
0.75, blue,
1.0, blue ]
# same but start doesn't line up with 0
- type: gradient
bounds: 100 350 500 10
start: 175 0
end: 275 0.001
repeat: true
stops: [0.0, green,
0.25, green,
0.25, red,
0.75, red,
0.75, blue,
1.0, blue ]
# the entire gradient from 0 to 1 is
# "offscreen", we're only seeing its
# repeats. the gradient is 100 wide
# and ends at -75, so the first
# three-quarters of it would be hidden,
# that is, it should start with blue.
- type: gradient
bounds: 100 400 500 10
start: -175 0
end: -75 0.001
repeat: true
stops: [0.0, green,
0.25, green,
0.25, red,
0.75, red,
0.75, blue,
1.0, blue ]
# same but over on the right
- type: gradient
bounds: 100 450 500 10
start: 575 0
end: 675 0.001
repeat: true
stops: [0.0, green,
0.25, green,
0.25, red,
0.75, red,
0.75, blue,
1.0, blue ]
# a repeat, but not really because only part
# of the gradient is visible
- type: gradient
bounds: 100 500 500 10
start: -50 0
end: 550 0.001
repeat: true
stops: [0.0, green,
0.25, green,
0.25, red,
0.75, red,
0.75, blue,
1.0, blue ]

Просмотреть файл

@ -76,7 +76,8 @@ fuzzy(1,3) == tiling-conic-3.yaml tiling-conic-3-ref.yaml
== linear-adjust-tile-size.yaml linear-adjust-tile-size-ref.yaml
platform(linux,mac) == linear-aligned-border-radius.yaml linear-aligned-border-radius.png
platform(linux,mac) == repeat-border-radius.yaml repeat-border-radius.png
# interpolation fuzz from sampling texture-baked gradient ramps
platform(linux,mac) fuzzy-range(<=1,*1404) == repeat-border-radius.yaml repeat-border-radius.png
== conic.yaml conic-ref.yaml
fuzzy(1,56) == conic-simple.yaml conic-simple.png
@ -94,3 +95,4 @@ fuzzy-range(<=1,*169000) == gradient_cache_5stops_vertical.yaml gradient_cache_5
== gradient_cache_hardstop.yaml gradient_cache_hardstop_ref.yaml
== gradient_cache_hardstop_clip.yaml gradient_cache_hardstop_clip_ref.yaml
== gradient_cache_clamp.yaml gradient_cache_clamp_ref.yaml
== gradient_cache_repeat.yaml gradient_cache_repeat_ref.yaml