diff --git a/gfx/wr/webrender/src/prim_store/gradient.rs b/gfx/wr/webrender/src/prim_store/gradient.rs index 0d935a7e28f8..336c44a29c40 100644 --- a/gfx/wr/webrender/src/prim_store/gradient.rs +++ b/gfx/wr/webrender/src/prim_store/gradient.rs @@ -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 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 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 = item.stops.iter().map(|stop| { diff --git a/gfx/wr/webrender/src/prim_store/mod.rs b/gfx/wr/webrender/src/prim_store/mod.rs index 9eac2a33bfa4..2fbfb948f9a3 100644 --- a/gfx/wr/webrender/src/prim_store/mod.rs +++ b/gfx/wr/webrender/src/prim_store/mod.rs @@ -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; + } } } diff --git a/gfx/wr/wrench/reftests/gradient/gradient_cache_repeat.yaml b/gfx/wr/wrench/reftests/gradient/gradient_cache_repeat.yaml new file mode 100644 index 000000000000..20a07a72a62a --- /dev/null +++ b/gfx/wr/wrench/reftests/gradient/gradient_cache_repeat.yaml @@ -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 ] diff --git a/gfx/wr/wrench/reftests/gradient/gradient_cache_repeat_ref.yaml b/gfx/wr/wrench/reftests/gradient/gradient_cache_repeat_ref.yaml new file mode 100644 index 000000000000..e1682622f87e --- /dev/null +++ b/gfx/wr/wrench/reftests/gradient/gradient_cache_repeat_ref.yaml @@ -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 ] diff --git a/gfx/wr/wrench/reftests/gradient/reftest.list b/gfx/wr/wrench/reftests/gradient/reftest.list index c3b3eee9cd91..c6848992670e 100644 --- a/gfx/wr/wrench/reftests/gradient/reftest.list +++ b/gfx/wr/wrench/reftests/gradient/reftest.list @@ -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