diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /gfx/wr/webrender/src/prim_store/gradient | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'gfx/wr/webrender/src/prim_store/gradient')
-rw-r--r-- | gfx/wr/webrender/src/prim_store/gradient/conic.rs | 399 | ||||
-rw-r--r-- | gfx/wr/webrender/src/prim_store/gradient/linear.rs | 750 | ||||
-rw-r--r-- | gfx/wr/webrender/src/prim_store/gradient/mod.rs | 392 | ||||
-rw-r--r-- | gfx/wr/webrender/src/prim_store/gradient/radial.rs | 531 |
4 files changed, 2072 insertions, 0 deletions
diff --git a/gfx/wr/webrender/src/prim_store/gradient/conic.rs b/gfx/wr/webrender/src/prim_store/gradient/conic.rs new file mode 100644 index 0000000000..d9c3f5d350 --- /dev/null +++ b/gfx/wr/webrender/src/prim_store/gradient/conic.rs @@ -0,0 +1,399 @@ +/* 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/. */ + +//! Conic gradients +//! +//! Specification: https://drafts.csswg.org/css-images-4/#conic-gradients +//! +//! Conic gradients are rendered via cached render tasks and composited with the image brush. + +use euclid::vec2; +use api::{ExtendMode, GradientStop, PremultipliedColorF}; +use api::units::*; +use crate::scene_building::IsVisible; +use crate::frame_builder::FrameBuildingState; +use crate::intern::{Internable, InternDebug, Handle as InternHandle}; +use crate::internal_types::LayoutPrimitiveInfo; +use crate::prim_store::{BrushSegment, GradientTileRange}; +use crate::prim_store::{PrimitiveInstanceKind, PrimitiveOpacity, FloatKey}; +use crate::prim_store::{PrimKeyCommonData, PrimTemplateCommonData, PrimitiveStore}; +use crate::prim_store::{NinePatchDescriptor, PointKey, SizeKey, InternablePrimitive}; +use crate::render_task::{RenderTask, RenderTaskKind}; +use crate::render_task_graph::RenderTaskId; +use crate::render_task_cache::{RenderTaskCacheKeyKind, RenderTaskCacheKey, RenderTaskParent}; +use crate::renderer::GpuBufferAddress; +use crate::picture::{SurfaceIndex}; + +use std::{hash, ops::{Deref, DerefMut}}; +use super::{stops_and_min_alpha, GradientStopKey, GradientGpuBlockBuilder}; + +/// Hashable conic gradient parameters, for use during prim interning. +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[derive(Debug, Clone, MallocSizeOf, PartialEq)] +pub struct ConicGradientParams { + pub angle: f32, // in radians + pub start_offset: f32, + pub end_offset: f32, +} + +impl Eq for ConicGradientParams {} + +impl hash::Hash for ConicGradientParams { + fn hash<H: hash::Hasher>(&self, state: &mut H) { + self.angle.to_bits().hash(state); + self.start_offset.to_bits().hash(state); + self.end_offset.to_bits().hash(state); + } +} + +/// Identifying key for a line decoration. +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[derive(Debug, Clone, Eq, PartialEq, Hash, MallocSizeOf)] +pub struct ConicGradientKey { + pub common: PrimKeyCommonData, + pub extend_mode: ExtendMode, + pub center: PointKey, + pub params: ConicGradientParams, + pub stretch_size: SizeKey, + pub stops: Vec<GradientStopKey>, + pub tile_spacing: SizeKey, + pub nine_patch: Option<Box<NinePatchDescriptor>>, +} + +impl ConicGradientKey { + pub fn new( + info: &LayoutPrimitiveInfo, + conic_grad: ConicGradient, + ) -> Self { + ConicGradientKey { + common: info.into(), + extend_mode: conic_grad.extend_mode, + center: conic_grad.center, + params: conic_grad.params, + stretch_size: conic_grad.stretch_size, + stops: conic_grad.stops, + tile_spacing: conic_grad.tile_spacing, + nine_patch: conic_grad.nine_patch, + } + } +} + +impl InternDebug for ConicGradientKey {} + +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[derive(MallocSizeOf)] +pub struct ConicGradientTemplate { + pub common: PrimTemplateCommonData, + pub extend_mode: ExtendMode, + pub center: DevicePoint, + pub params: ConicGradientParams, + pub task_size: DeviceIntSize, + pub scale: DeviceVector2D, + pub stretch_size: LayoutSize, + pub tile_spacing: LayoutSize, + pub brush_segments: Vec<BrushSegment>, + pub stops_opacity: PrimitiveOpacity, + pub stops: Vec<GradientStop>, + pub src_color: Option<RenderTaskId>, +} + +impl Deref for ConicGradientTemplate { + type Target = PrimTemplateCommonData; + fn deref(&self) -> &Self::Target { + &self.common + } +} + +impl DerefMut for ConicGradientTemplate { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.common + } +} + +impl From<ConicGradientKey> for ConicGradientTemplate { + fn from(item: ConicGradientKey) -> Self { + let common = PrimTemplateCommonData::with_key_common(item.common); + let mut brush_segments = Vec::new(); + + if let Some(ref nine_patch) = item.nine_patch { + brush_segments = nine_patch.create_segments(common.prim_rect.size()); + } + + let (stops, min_alpha) = stops_and_min_alpha(&item.stops); + + // Save opacity of the stops for use in + // selecting which pass this gradient + // should be drawn in. + let stops_opacity = PrimitiveOpacity::from_alpha(min_alpha); + + let mut stretch_size: LayoutSize = item.stretch_size.into(); + stretch_size.width = stretch_size.width.min(common.prim_rect.width()); + stretch_size.height = stretch_size.height.min(common.prim_rect.height()); + + fn approx_eq(a: f32, b: f32) -> bool { (a - b).abs() < 0.01 } + + // Attempt to detect some of the common configurations with hard gradient stops. Allow + // those a higher maximum resolution to avoid the worst cases of aliasing artifacts with + // large conic gradients. A better solution would be to go back to rendering very large + // conic gradients via a brush shader instead of caching all of them (unclear whether + // it is important enough to warrant the better solution). + let mut has_hard_stops = false; + let mut prev_stop = None; + let offset_range = item.params.end_offset - item.params.start_offset; + for stop in &stops { + if offset_range <= 0.0 { + break; + } + if let Some(prev_offset) = prev_stop { + // Check whether two consecutive stops are very close (hard stops). + if stop.offset < prev_offset + 0.005 / offset_range { + // a is the angle of the stop normalized into 0-1 space and repeating in the 0-0.25 range. + // If close to 0.0 or 0.25 it means the stop is vertical or horizontal. For those, the lower + // resolution isn't a big issue. + let a = item.params.angle / (2.0 * std::f32::consts::PI) + + item.params.start_offset + + stop.offset / offset_range; + let a = a.rem_euclid(0.25); + + if !approx_eq(a, 0.0) && !approx_eq(a, 0.25) { + has_hard_stops = true; + break; + } + } + } + prev_stop = Some(stop.offset); + } + + let max_size = if has_hard_stops { + 2048.0 + } else { + 1024.0 + }; + + // Avoid rendering enormous gradients. Radial gradients are mostly made of soft transitions, + // so it is unlikely that rendering at a higher resolution that 1024 would produce noticeable + // differences, especially with 8 bits per channel. + let mut task_size: DeviceSize = stretch_size.cast_unit(); + let mut scale = vec2(1.0, 1.0); + if task_size.width > max_size { + scale.x = task_size.width / max_size; + task_size.width = max_size; + } + if task_size.height > max_size { + scale.y = task_size.height / max_size; + task_size.height = max_size; + } + + ConicGradientTemplate { + common, + center: DevicePoint::new(item.center.x, item.center.y), + extend_mode: item.extend_mode, + params: item.params, + stretch_size, + task_size: task_size.ceil().to_i32(), + scale, + tile_spacing: item.tile_spacing.into(), + brush_segments, + stops_opacity, + stops, + src_color: None, + } + } +} + +impl ConicGradientTemplate { + /// Update the GPU cache for a given primitive template. This may be called multiple + /// times per frame, by each primitive reference that refers to this interned + /// template. The initial request call to the GPU cache ensures that work is only + /// done if the cache entry is invalid (due to first use or eviction). + pub fn update( + &mut self, + frame_state: &mut FrameBuildingState, + parent_surface: SurfaceIndex, + ) { + if let Some(mut request) = + frame_state.gpu_cache.request(&mut self.common.gpu_cache_handle) { + // write_prim_gpu_blocks + request.push(PremultipliedColorF::WHITE); + request.push(PremultipliedColorF::WHITE); + request.push([ + self.stretch_size.width, + self.stretch_size.height, + 0.0, + 0.0, + ]); + + // write_segment_gpu_blocks + for segment in &self.brush_segments { + // has to match VECS_PER_SEGMENT + request.write_segment( + segment.local_rect, + segment.extra_data, + ); + } + } + + let cache_key = ConicGradientCacheKey { + size: self.task_size, + center: PointKey { x: self.center.x, y: self.center.y }, + scale: PointKey { x: self.scale.x, y: self.scale.y }, + start_offset: FloatKey(self.params.start_offset), + end_offset: FloatKey(self.params.end_offset), + angle: FloatKey(self.params.angle), + extend_mode: self.extend_mode, + stops: self.stops.iter().map(|stop| (*stop).into()).collect(), + }; + + let task_id = frame_state.resource_cache.request_render_task( + RenderTaskCacheKey { + size: self.task_size, + kind: RenderTaskCacheKeyKind::ConicGradient(cache_key), + }, + frame_state.gpu_cache, + frame_state.frame_gpu_data, + frame_state.rg_builder, + None, + false, + RenderTaskParent::Surface(parent_surface), + &mut frame_state.surface_builder, + |rg_builder, gpu_buffer_builder| { + let stops = GradientGpuBlockBuilder::build( + false, + gpu_buffer_builder, + &self.stops, + ); + + rg_builder.add().init(RenderTask::new_dynamic( + self.task_size, + RenderTaskKind::ConicGradient(ConicGradientTask { + extend_mode: self.extend_mode, + scale: self.scale, + center: self.center, + params: self.params.clone(), + stops, + }), + )) + } + ); + + self.src_color = Some(task_id); + + // Tile spacing is always handled by decomposing into separate draw calls so the + // primitive opacity is equivalent to stops opacity. This might change to being + // set to non-opaque in the presence of tile spacing if/when tile spacing is handled + // in the same way as with the image primitive. + self.opacity = self.stops_opacity; + } +} + +pub type ConicGradientDataHandle = InternHandle<ConicGradient>; + +#[derive(Debug, MallocSizeOf)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct ConicGradient { + pub extend_mode: ExtendMode, + pub center: PointKey, + pub params: ConicGradientParams, + pub stretch_size: SizeKey, + pub stops: Vec<GradientStopKey>, + pub tile_spacing: SizeKey, + pub nine_patch: Option<Box<NinePatchDescriptor>>, +} + +impl Internable for ConicGradient { + type Key = ConicGradientKey; + type StoreData = ConicGradientTemplate; + type InternData = (); + const PROFILE_COUNTER: usize = crate::profiler::INTERNED_CONIC_GRADIENTS; +} + +impl InternablePrimitive for ConicGradient { + fn into_key( + self, + info: &LayoutPrimitiveInfo, + ) -> ConicGradientKey { + ConicGradientKey::new(info, self) + } + + fn make_instance_kind( + _key: ConicGradientKey, + data_handle: ConicGradientDataHandle, + _prim_store: &mut PrimitiveStore, + _reference_frame_relative_offset: LayoutVector2D, + ) -> PrimitiveInstanceKind { + PrimitiveInstanceKind::ConicGradient { + data_handle, + visible_tiles_range: GradientTileRange::empty(), + } + } +} + +impl IsVisible for ConicGradient { + fn is_visible(&self) -> bool { + true + } +} + +#[derive(Debug)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct ConicGradientTask { + pub extend_mode: ExtendMode, + pub center: DevicePoint, + pub scale: DeviceVector2D, + pub params: ConicGradientParams, + pub stops: GpuBufferAddress, +} + +impl ConicGradientTask { + pub fn to_instance(&self, target_rect: &DeviceIntRect) -> ConicGradientInstance { + ConicGradientInstance { + task_rect: target_rect.to_f32(), + center: self.center, + scale: self.scale, + start_offset: self.params.start_offset, + end_offset: self.params.end_offset, + angle: self.params.angle, + extend_mode: self.extend_mode as i32, + gradient_stops_address: self.stops.as_int(), + } + } +} + +/// The per-instance shader input of a radial gradient render task. +/// +/// Must match the RADIAL_GRADIENT instance description in renderer/vertex.rs. +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[repr(C)] +#[derive(Clone, Debug)] +pub struct ConicGradientInstance { + pub task_rect: DeviceRect, + pub center: DevicePoint, + pub scale: DeviceVector2D, + pub start_offset: f32, + pub end_offset: f32, + pub angle: f32, + pub extend_mode: i32, + pub gradient_stops_address: i32, +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct ConicGradientCacheKey { + pub size: DeviceIntSize, + pub center: PointKey, + pub scale: PointKey, + pub start_offset: FloatKey, + pub end_offset: FloatKey, + pub angle: FloatKey, + pub extend_mode: ExtendMode, + pub stops: Vec<GradientStopKey>, +} + diff --git a/gfx/wr/webrender/src/prim_store/gradient/linear.rs b/gfx/wr/webrender/src/prim_store/gradient/linear.rs new file mode 100644 index 0000000000..85da4b670a --- /dev/null +++ b/gfx/wr/webrender/src/prim_store/gradient/linear.rs @@ -0,0 +1,750 @@ +/* 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/. */ + +//! Linear gradients +//! +//! Specification: https://drafts.csswg.org/css-images-4/#linear-gradients +//! +//! Linear gradients are rendered via cached render tasks and composited with the image brush. + +use euclid::approxeq::ApproxEq; +use euclid::{point2, vec2, size2}; +use api::{ExtendMode, GradientStop, LineOrientation, PremultipliedColorF, ColorF, ColorU}; +use api::units::*; +use crate::scene_building::IsVisible; +use crate::frame_builder::FrameBuildingState; +use crate::intern::{Internable, InternDebug, Handle as InternHandle}; +use crate::internal_types::LayoutPrimitiveInfo; +use crate::image_tiling::simplify_repeated_primitive; +use crate::prim_store::{BrushSegment, GradientTileRange}; +use crate::prim_store::{PrimitiveInstanceKind, PrimitiveOpacity}; +use crate::prim_store::{PrimKeyCommonData, PrimTemplateCommonData, PrimitiveStore}; +use crate::prim_store::{NinePatchDescriptor, PointKey, SizeKey, InternablePrimitive}; +use crate::render_task::{RenderTask, RenderTaskKind}; +use crate::render_task_graph::RenderTaskId; +use crate::render_task_cache::{RenderTaskCacheKeyKind, RenderTaskCacheKey, RenderTaskParent}; +use crate::renderer::GpuBufferAddress; +use crate::segment::EdgeAaSegmentMask; +use crate::picture::{SurfaceIndex}; +use crate::util::pack_as_float; +use super::{stops_and_min_alpha, GradientStopKey, GradientGpuBlockBuilder, apply_gradient_local_clip}; +use std::ops::{Deref, DerefMut}; +use std::mem::swap; + +pub const MAX_CACHED_SIZE: f32 = 1024.0; + +/// Identifying key for a linear gradient. +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[derive(Debug, Clone, Eq, PartialEq, Hash, MallocSizeOf)] +pub struct LinearGradientKey { + pub common: PrimKeyCommonData, + pub extend_mode: ExtendMode, + pub start_point: PointKey, + pub end_point: PointKey, + pub stretch_size: SizeKey, + pub tile_spacing: SizeKey, + pub stops: Vec<GradientStopKey>, + pub reverse_stops: bool, + pub cached: bool, + pub nine_patch: Option<Box<NinePatchDescriptor>>, + pub edge_aa_mask: EdgeAaSegmentMask, +} + +impl LinearGradientKey { + pub fn new( + info: &LayoutPrimitiveInfo, + linear_grad: LinearGradient, + ) -> Self { + LinearGradientKey { + common: info.into(), + extend_mode: linear_grad.extend_mode, + start_point: linear_grad.start_point, + end_point: linear_grad.end_point, + stretch_size: linear_grad.stretch_size, + tile_spacing: linear_grad.tile_spacing, + stops: linear_grad.stops, + reverse_stops: linear_grad.reverse_stops, + cached: linear_grad.cached, + nine_patch: linear_grad.nine_patch, + edge_aa_mask: linear_grad.edge_aa_mask, + } + } +} + +impl InternDebug for LinearGradientKey {} + +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[derive(Debug, MallocSizeOf)] +pub struct LinearGradientTemplate { + pub common: PrimTemplateCommonData, + pub extend_mode: ExtendMode, + pub start_point: DevicePoint, + pub end_point: DevicePoint, + pub task_size: DeviceIntSize, + pub scale: DeviceVector2D, + pub stretch_size: LayoutSize, + pub tile_spacing: LayoutSize, + pub stops_opacity: PrimitiveOpacity, + pub stops: Vec<GradientStop>, + pub brush_segments: Vec<BrushSegment>, + pub reverse_stops: bool, + pub is_fast_path: bool, + pub cached: bool, + pub src_color: Option<RenderTaskId>, +} + +impl Deref for LinearGradientTemplate { + type Target = PrimTemplateCommonData; + fn deref(&self) -> &Self::Target { + &self.common + } +} + +impl DerefMut for LinearGradientTemplate { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.common + } +} + +/// Perform a few optimizations to the gradient that are relevant to scene building. +/// +/// Returns true if the gradient was decomposed into fast-path primitives, indicating +/// that we shouldn't emit a regular gradient primitive after this returns. +pub fn optimize_linear_gradient( + prim_rect: &mut LayoutRect, + tile_size: &mut LayoutSize, + mut tile_spacing: LayoutSize, + clip_rect: &LayoutRect, + start: &mut LayoutPoint, + end: &mut LayoutPoint, + extend_mode: ExtendMode, + stops: &mut [GradientStopKey], + // Callback called for each fast-path segment (rect, start end, stops). + callback: &mut dyn FnMut(&LayoutRect, LayoutPoint, LayoutPoint, &[GradientStopKey], EdgeAaSegmentMask) +) -> bool { + // First sanitize the gradient parameters. See if we can remove repetitions, + // tighten the primitive bounds, etc. + + simplify_repeated_primitive(&tile_size, &mut tile_spacing, prim_rect); + + let vertical = start.x.approx_eq(&end.x); + let horizontal = start.y.approx_eq(&end.y); + + let mut horizontally_tiled = prim_rect.width() > tile_size.width; + let mut vertically_tiled = prim_rect.height() > tile_size.height; + + // Check whether the tiling is equivalent to stretching on either axis. + // Stretching the gradient is more efficient than repeating it. + if vertically_tiled && horizontal && tile_spacing.height == 0.0 { + tile_size.height = prim_rect.height(); + vertically_tiled = false; + } + + if horizontally_tiled && vertical && tile_spacing.width == 0.0 { + tile_size.width = prim_rect.width(); + horizontally_tiled = false; + } + + let offset = apply_gradient_local_clip( + prim_rect, + &tile_size, + &tile_spacing, + &clip_rect + ); + + // The size of gradient render tasks depends on the tile_size. No need to generate + // large stretch sizes that will be clipped to the bounds of the primitive. + tile_size.width = tile_size.width.min(prim_rect.width()); + tile_size.height = tile_size.height.min(prim_rect.height()); + + *start += offset; + *end += offset; + + // Next, in the case of axis-aligned gradients, see if it is worth + // decomposing the gradient into multiple gradients with only two + // gradient stops per segment to get a faster shader. + + if extend_mode != ExtendMode::Clamp || stops.is_empty() { + return false; + } + + if !vertical && !horizontal { + return false; + } + + if vertical && horizontal { + return false; + } + + if !tile_spacing.is_empty() || vertically_tiled || horizontally_tiled { + return false; + } + + // If the gradient is small, no need to bother with decomposing it. + if (horizontal && tile_size.width < 256.0) + || (vertical && tile_size.height < 256.0) { + + return false; + } + + // Flip x and y if need be so that we only deal with the horizontal case. + + // From now on don't return false. We are going modifying the caller's + // variables and not bother to restore them. If the control flow changes, + // Make sure to to restore &mut parameters to sensible values before + // returning false. + + let adjust_rect = &mut |rect: &mut LayoutRect| { + if vertical { + swap(&mut rect.min.x, &mut rect.min.y); + swap(&mut rect.max.x, &mut rect.max.y); + } + }; + + let adjust_size = &mut |size: &mut LayoutSize| { + if vertical { swap(&mut size.width, &mut size.height); } + }; + + let adjust_point = &mut |p: &mut LayoutPoint| { + if vertical { swap(&mut p.x, &mut p.y); } + }; + + let clip_rect = match clip_rect.intersection(prim_rect) { + Some(clip) => clip, + None => { + return false; + } + }; + + adjust_rect(prim_rect); + adjust_point(start); + adjust_point(end); + adjust_size(tile_size); + + let length = (end.x - start.x).abs(); + + // Decompose the gradient into simple segments. This lets us: + // - separate opaque from semi-transparent segments, + // - compress long segments into small render tasks, + // - make sure hard stops stay so even if the primitive is large. + + let reverse_stops = start.x > end.x; + + // Handle reverse stops so we can assume stops are arranged in increasing x. + if reverse_stops { + stops.reverse(); + swap(start, end); + } + + // Use fake gradient stop to emulate the potential constant color sections + // before and after the gradient endpoints. + let mut prev = *stops.first().unwrap(); + let mut last = *stops.last().unwrap(); + + // Set the offsets of the fake stops to position them at the edges of the primitive. + prev.offset = -start.x / length; + last.offset = (tile_size.width - start.x) / length; + if reverse_stops { + prev.offset = 1.0 - prev.offset; + last.offset = 1.0 - last.offset; + } + + let (side_edges, first_edge, last_edge) = if vertical { + ( + EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::RIGHT, + EdgeAaSegmentMask::TOP, + EdgeAaSegmentMask::BOTTOM + ) + } else { + ( + EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::BOTTOM, + EdgeAaSegmentMask::LEFT, + EdgeAaSegmentMask::RIGHT + ) + }; + + let mut is_first = true; + let last_offset = last.offset; + for stop in stops.iter().chain((&[last]).iter()) { + let prev_stop = prev; + prev = *stop; + + if prev_stop.color.a == 0 && stop.color.a == 0 { + continue; + } + + + let prev_offset = if reverse_stops { 1.0 - prev_stop.offset } else { prev_stop.offset }; + let offset = if reverse_stops { 1.0 - stop.offset } else { stop.offset }; + + // In layout space, relative to the primitive. + let segment_start = start.x + prev_offset * length; + let segment_end = start.x + offset * length; + let segment_length = segment_end - segment_start; + + if segment_length <= 0.0 { + continue; + } + + let mut segment_rect = *prim_rect; + segment_rect.min.x += segment_start; + segment_rect.max.x = segment_rect.min.x + segment_length; + + let mut start = point2(0.0, 0.0); + let mut end = point2(segment_length, 0.0); + + adjust_point(&mut start); + adjust_point(&mut end); + adjust_rect(&mut segment_rect); + + let origin_before_clip = segment_rect.min; + segment_rect = match segment_rect.intersection(&clip_rect) { + Some(rect) => rect, + None => { + continue; + } + }; + let offset = segment_rect.min - origin_before_clip; + + // Account for the clipping since start and end are relative to the origin. + start -= offset; + end -= offset; + + let mut edge_flags = side_edges; + if is_first { + edge_flags |= first_edge; + is_first = false; + } + if stop.offset == last_offset { + edge_flags |= last_edge; + } + + callback( + &segment_rect, + start, + end, + &[ + GradientStopKey { offset: 0.0, .. prev_stop }, + GradientStopKey { offset: 1.0, .. *stop }, + ], + edge_flags, + ); + } + + true +} + +impl From<LinearGradientKey> for LinearGradientTemplate { + fn from(item: LinearGradientKey) -> Self { + + let mut common = PrimTemplateCommonData::with_key_common(item.common); + common.edge_aa_mask = item.edge_aa_mask; + + let (mut stops, min_alpha) = stops_and_min_alpha(&item.stops); + + let mut brush_segments = Vec::new(); + + if let Some(ref nine_patch) = item.nine_patch { + brush_segments = nine_patch.create_segments(common.prim_rect.size()); + } + + // Save opacity of the stops for use in + // selecting which pass this gradient + // should be drawn in. + let stops_opacity = PrimitiveOpacity::from_alpha(min_alpha); + + let start_point = DevicePoint::new(item.start_point.x, item.start_point.y); + let end_point = DevicePoint::new(item.end_point.x, item.end_point.y); + let tile_spacing: LayoutSize = item.tile_spacing.into(); + let stretch_size: LayoutSize = item.stretch_size.into(); + let mut task_size: DeviceSize = stretch_size.cast_unit(); + + let horizontal = start_point.y.approx_eq(&end_point.y); + let vertical = start_point.x.approx_eq(&end_point.x); + + if horizontal { + // Completely horizontal, we can stretch the gradient vertically. + task_size.height = 1.0; + } + + if vertical { + // Completely vertical, we can stretch the gradient horizontally. + task_size.width = 1.0; + } + + // See if we can render the gradient using a special fast-path shader. + // The fast path path only works with two gradient stops. + let mut is_fast_path = false; + if item.cached && stops.len() == 2 && brush_segments.is_empty() { + if horizontal + && stretch_size.width >= common.prim_rect.width() + && start_point.x.approx_eq(&0.0) + && end_point.x.approx_eq(&stretch_size.width) { + is_fast_path = true; + task_size.width = task_size.width.min(256.0); + } + if vertical + && stretch_size.height >= common.prim_rect.height() + && start_point.y.approx_eq(&0.0) + && end_point.y.approx_eq(&stretch_size.height) { + is_fast_path = true; + task_size.height = task_size.height.min(256.0); + } + + if stops[0].color == stops[1].color { + is_fast_path = true; + task_size = size2(1.0, 1.0); + } + + if is_fast_path && item.reverse_stops { + // The fast path doesn't use the gradient gpu blocks builder so handle + // reversed stops here. + stops.swap(0, 1); + } + } + + // Avoid rendering enormous gradients. Linear gradients are mostly made of soft transitions, + // so it is unlikely that rendering at a higher resolution than 1024 would produce noticeable + // differences, especially with 8 bits per channel. + + let mut scale = vec2(1.0, 1.0); + + if task_size.width > MAX_CACHED_SIZE { + scale.x = task_size.width / MAX_CACHED_SIZE; + task_size.width = MAX_CACHED_SIZE; + } + + if task_size.height > MAX_CACHED_SIZE { + scale.y = task_size.height / MAX_CACHED_SIZE; + task_size.height = MAX_CACHED_SIZE; + } + + LinearGradientTemplate { + common, + extend_mode: item.extend_mode, + start_point, + end_point, + task_size: task_size.ceil().to_i32(), + scale, + stretch_size, + tile_spacing, + stops_opacity, + stops, + brush_segments, + reverse_stops: item.reverse_stops, + is_fast_path, + cached: item.cached, + src_color: None, + } + } +} + +impl LinearGradientTemplate { + /// Update the GPU cache for a given primitive template. This may be called multiple + /// times per frame, by each primitive reference that refers to this interned + /// template. The initial request call to the GPU cache ensures that work is only + /// done if the cache entry is invalid (due to first use or eviction). + pub fn update( + &mut self, + frame_state: &mut FrameBuildingState, + parent_surface: SurfaceIndex, + ) { + if let Some(mut request) = frame_state.gpu_cache.request( + &mut self.common.gpu_cache_handle + ) { + + // Write_prim_gpu_blocks + if self.cached { + // We are using the image brush. + request.push(PremultipliedColorF::WHITE); + request.push(PremultipliedColorF::WHITE); + request.push([ + self.stretch_size.width, + self.stretch_size.height, + 0.0, + 0.0, + ]); + } else { + // We are using the gradient brush. + request.push([ + self.start_point.x, + self.start_point.y, + self.end_point.x, + self.end_point.y, + ]); + request.push([ + pack_as_float(self.extend_mode as u32), + self.stretch_size.width, + self.stretch_size.height, + 0.0, + ]); + } + + // write_segment_gpu_blocks + for segment in &self.brush_segments { + // has to match VECS_PER_SEGMENT + request.write_segment( + segment.local_rect, + segment.extra_data, + ); + } + } + + // Tile spacing is always handled by decomposing into separate draw calls so the + // primitive opacity is equivalent to stops opacity. This might change to being + // set to non-opaque in the presence of tile spacing if/when tile spacing is handled + // in the same way as with the image primitive. + self.opacity = self.stops_opacity; + + if !self.cached { + return; + } + + let task_id = if self.is_fast_path { + let orientation = if self.task_size.width > self.task_size.height { + LineOrientation::Horizontal + } else { + LineOrientation::Vertical + }; + + let gradient = FastLinearGradientTask { + color0: self.stops[0].color.into(), + color1: self.stops[1].color.into(), + orientation, + }; + + frame_state.resource_cache.request_render_task( + RenderTaskCacheKey { + size: self.task_size, + kind: RenderTaskCacheKeyKind::FastLinearGradient(gradient), + }, + frame_state.gpu_cache, + frame_state.frame_gpu_data, + frame_state.rg_builder, + None, + false, + RenderTaskParent::Surface(parent_surface), + &mut frame_state.surface_builder, + |rg_builder, _| { + rg_builder.add().init(RenderTask::new_dynamic( + self.task_size, + RenderTaskKind::FastLinearGradient(gradient), + )) + } + ) + } else { + let cache_key = LinearGradientCacheKey { + size: self.task_size, + start: PointKey { x: self.start_point.x, y: self.start_point.y }, + end: PointKey { x: self.end_point.x, y: self.end_point.y }, + scale: PointKey { x: self.scale.x, y: self.scale.y }, + extend_mode: self.extend_mode, + stops: self.stops.iter().map(|stop| (*stop).into()).collect(), + reversed_stops: self.reverse_stops, + }; + + frame_state.resource_cache.request_render_task( + RenderTaskCacheKey { + size: self.task_size, + kind: RenderTaskCacheKeyKind::LinearGradient(cache_key), + }, + frame_state.gpu_cache, + frame_state.frame_gpu_data, + frame_state.rg_builder, + None, + false, + RenderTaskParent::Surface(parent_surface), + &mut frame_state.surface_builder, + |rg_builder, gpu_buffer_builder| { + let stops = Some(GradientGpuBlockBuilder::build( + self.reverse_stops, + gpu_buffer_builder, + &self.stops, + )); + + rg_builder.add().init(RenderTask::new_dynamic( + self.task_size, + RenderTaskKind::LinearGradient(LinearGradientTask { + start: self.start_point, + end: self.end_point, + scale: self.scale, + extend_mode: self.extend_mode, + stops: stops.unwrap(), + }), + )) + } + ) + }; + + self.src_color = Some(task_id); + } +} + +pub type LinearGradientDataHandle = InternHandle<LinearGradient>; + +#[derive(Debug, MallocSizeOf)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct LinearGradient { + pub extend_mode: ExtendMode, + pub start_point: PointKey, + pub end_point: PointKey, + pub stretch_size: SizeKey, + pub tile_spacing: SizeKey, + pub stops: Vec<GradientStopKey>, + pub reverse_stops: bool, + pub nine_patch: Option<Box<NinePatchDescriptor>>, + pub cached: bool, + pub edge_aa_mask: EdgeAaSegmentMask, +} + +impl Internable for LinearGradient { + type Key = LinearGradientKey; + type StoreData = LinearGradientTemplate; + type InternData = (); + const PROFILE_COUNTER: usize = crate::profiler::INTERNED_LINEAR_GRADIENTS; +} + +impl InternablePrimitive for LinearGradient { + fn into_key( + self, + info: &LayoutPrimitiveInfo, + ) -> LinearGradientKey { + LinearGradientKey::new(info, self) + } + + fn make_instance_kind( + key: LinearGradientKey, + data_handle: LinearGradientDataHandle, + _prim_store: &mut PrimitiveStore, + _reference_frame_relative_offset: LayoutVector2D, + ) -> PrimitiveInstanceKind { + if key.cached { + PrimitiveInstanceKind::CachedLinearGradient { + data_handle, + visible_tiles_range: GradientTileRange::empty(), + } + } else { + PrimitiveInstanceKind::LinearGradient { + data_handle, + visible_tiles_range: GradientTileRange::empty(), + } + } + } +} + +impl IsVisible for LinearGradient { + fn is_visible(&self) -> bool { + true + } +} + +#[derive(Debug)] +#[cfg_attr(feature = "capture", derive(Serialize))] +pub struct LinearGradientPrimitive { + pub cache_segments: Vec<CachedGradientSegment>, + pub visible_tiles_range: GradientTileRange, +} + +#[derive(Debug)] +#[cfg_attr(feature = "capture", derive(Serialize))] +pub struct CachedGradientSegment { + pub render_task: RenderTaskId, + pub local_rect: LayoutRect, +} + + +#[derive(Copy, Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct FastLinearGradientTask { + pub color0: ColorU, + pub color1: ColorU, + pub orientation: LineOrientation, +} + +impl FastLinearGradientTask { + pub fn to_instance(&self, target_rect: &DeviceIntRect) -> FastLinearGradientInstance { + FastLinearGradientInstance { + task_rect: target_rect.to_f32(), + color0: ColorF::from(self.color0).premultiplied(), + color1: ColorF::from(self.color1).premultiplied(), + axis_select: match self.orientation { + LineOrientation::Horizontal => 0.0, + LineOrientation::Vertical => 1.0, + }, + } + } +} + +pub type FastLinearGradientCacheKey = FastLinearGradientTask; + +/// The per-instance shader input of a fast-path linear gradient render task. +/// +/// Must match the FAST_LINEAR_GRADIENT instance description in renderer/vertex.rs. +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[repr(C)] +#[derive(Clone, Debug)] +pub struct FastLinearGradientInstance { + pub task_rect: DeviceRect, + pub color0: PremultipliedColorF, + pub color1: PremultipliedColorF, + pub axis_select: f32, +} + +#[derive(Debug)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct LinearGradientTask { + pub start: DevicePoint, + pub end: DevicePoint, + pub scale: DeviceVector2D, + pub extend_mode: ExtendMode, + pub stops: GpuBufferAddress, +} + +impl LinearGradientTask { + pub fn to_instance(&self, target_rect: &DeviceIntRect) -> LinearGradientInstance { + LinearGradientInstance { + task_rect: target_rect.to_f32(), + start: self.start, + end: self.end, + scale: self.scale, + extend_mode: self.extend_mode as i32, + gradient_stops_address: self.stops.as_int(), + } + } +} + +/// The per-instance shader input of a linear gradient render task. +/// +/// Must match the LINEAR_GRADIENT instance description in renderer/vertex.rs. +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[repr(C)] +#[derive(Clone, Debug)] +pub struct LinearGradientInstance { + pub task_rect: DeviceRect, + pub start: DevicePoint, + pub end: DevicePoint, + pub scale: DeviceVector2D, + pub extend_mode: i32, + pub gradient_stops_address: i32, +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct LinearGradientCacheKey { + pub size: DeviceIntSize, + pub start: PointKey, + pub end: PointKey, + pub scale: PointKey, + pub extend_mode: ExtendMode, + pub stops: Vec<GradientStopKey>, + pub reversed_stops: bool, +} diff --git a/gfx/wr/webrender/src/prim_store/gradient/mod.rs b/gfx/wr/webrender/src/prim_store/gradient/mod.rs new file mode 100644 index 0000000000..d0b922c579 --- /dev/null +++ b/gfx/wr/webrender/src/prim_store/gradient/mod.rs @@ -0,0 +1,392 @@ +/* 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 api::{ColorF, ColorU, GradientStop, PremultipliedColorF}; +use api::units::{LayoutRect, LayoutSize, LayoutVector2D}; +use crate::renderer::{GpuBufferAddress, GpuBufferBuilder}; +use std::hash; + +mod linear; +mod radial; +mod conic; + +pub use linear::MAX_CACHED_SIZE as LINEAR_MAX_CACHED_SIZE; + +pub use linear::*; +pub use radial::*; +pub use conic::*; + +/// A hashable gradient stop that can be used in primitive keys. +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[derive(Debug, Copy, Clone, MallocSizeOf, PartialEq)] +pub struct GradientStopKey { + pub offset: f32, + pub color: ColorU, +} + +impl GradientStopKey { + pub fn empty() -> Self { + GradientStopKey { + offset: 0.0, + color: ColorU::new(0, 0, 0, 0), + } + } +} + +impl Into<GradientStopKey> for GradientStop { + fn into(self) -> GradientStopKey { + GradientStopKey { + offset: self.offset, + color: self.color.into(), + } + } +} + +// Convert `stop_keys` into a vector of `GradientStop`s, which is a more +// convenient representation for the current gradient builder. Compute the +// minimum stop alpha along the way. +fn stops_and_min_alpha(stop_keys: &[GradientStopKey]) -> (Vec<GradientStop>, f32) { + let mut min_alpha: f32 = 1.0; + let stops = stop_keys.iter().map(|stop_key| { + let color: ColorF = stop_key.color.into(); + min_alpha = min_alpha.min(color.a); + + GradientStop { + offset: stop_key.offset, + color, + } + }).collect(); + + (stops, min_alpha) +} + +impl Eq for GradientStopKey {} + +impl hash::Hash for GradientStopKey { + fn hash<H: hash::Hasher>(&self, state: &mut H) { + self.offset.to_bits().hash(state); + self.color.hash(state); + } +} + +// The gradient entry index for the first color stop +pub const GRADIENT_DATA_FIRST_STOP: usize = 0; +// The gradient entry index for the last color stop +pub const GRADIENT_DATA_LAST_STOP: usize = GRADIENT_DATA_SIZE - 1; + +// The start of the gradient data table +pub const GRADIENT_DATA_TABLE_BEGIN: usize = GRADIENT_DATA_FIRST_STOP + 1; +// The exclusive bound of the gradient data table +pub const GRADIENT_DATA_TABLE_END: usize = GRADIENT_DATA_LAST_STOP; +// The number of entries in the gradient data table. +pub const GRADIENT_DATA_TABLE_SIZE: usize = 128; + +// The number of entries in a gradient data: GRADIENT_DATA_TABLE_SIZE + first stop entry + last stop entry +pub const GRADIENT_DATA_SIZE: usize = GRADIENT_DATA_TABLE_SIZE + 2; + +/// An entry in a gradient data table representing a segment of the gradient +/// color space. +#[derive(Debug, Copy, Clone)] +#[repr(C)] +struct GradientDataEntry { + start_color: PremultipliedColorF, + end_step: PremultipliedColorF, +} + +impl GradientDataEntry { + fn white() -> Self { + Self { + start_color: PremultipliedColorF::WHITE, + end_step: PremultipliedColorF::TRANSPARENT, + } + } +} + +// TODO(gw): Tidy this up to be a free function / module? +pub struct GradientGpuBlockBuilder {} + +impl GradientGpuBlockBuilder { + /// Generate a color ramp filling the indices in [start_idx, end_idx) and interpolating + /// from start_color to end_color. + fn fill_colors( + start_idx: usize, + end_idx: usize, + start_color: &PremultipliedColorF, + end_color: &PremultipliedColorF, + entries: &mut [GradientDataEntry; GRADIENT_DATA_SIZE], + prev_step: &PremultipliedColorF, + ) -> PremultipliedColorF { + // Calculate the color difference for individual steps in the ramp. + let inv_steps = 1.0 / (end_idx - start_idx) as f32; + let mut step = PremultipliedColorF { + r: (end_color.r - start_color.r) * inv_steps, + g: (end_color.g - start_color.g) * inv_steps, + b: (end_color.b - start_color.b) * inv_steps, + a: (end_color.a - start_color.a) * inv_steps, + }; + // As a subtle form of compression, we ensure that the step values for + // each stop range are the same if and only if they belong to the same + // stop range. However, if two different stop ranges have the same step, + // we need to modify the steps so they compare unequally between ranges. + // This allows to quickly compare if two adjacent stops belong to the + // same range by comparing their steps. + if step == *prev_step { + // Modify the step alpha value as if by nextafter(). The difference + // here should be so small as to be unnoticeable, but yet allow it + // to compare differently. + step.a = f32::from_bits(if step.a == 0.0 { 1 } else { step.a.to_bits() + 1 }); + } + + let mut cur_color = *start_color; + + // Walk the ramp writing start and end colors for each entry. + for index in start_idx .. end_idx { + let entry = &mut entries[index]; + entry.start_color = cur_color; + cur_color.r += step.r; + cur_color.g += step.g; + cur_color.b += step.b; + cur_color.a += step.a; + entry.end_step = step; + } + + step + } + + /// Compute an index into the gradient entry table based on a gradient stop offset. This + /// function maps offsets from [0, 1] to indices in [GRADIENT_DATA_TABLE_BEGIN, GRADIENT_DATA_TABLE_END]. + #[inline] + fn get_index(offset: f32) -> usize { + (offset.max(0.0).min(1.0) * GRADIENT_DATA_TABLE_SIZE as f32 + + GRADIENT_DATA_TABLE_BEGIN as f32) + .round() as usize + } + + // Build the gradient data from the supplied stops, reversing them if necessary. + pub fn build( + reverse_stops: bool, + gpu_buffer_builder: &mut GpuBufferBuilder, + src_stops: &[GradientStop], + ) -> GpuBufferAddress { + // Preconditions (should be ensured by DisplayListBuilder): + // * we have at least two stops + // * first stop has offset 0.0 + // * last stop has offset 1.0 + let mut src_stops = src_stops.into_iter(); + let mut cur_color = match src_stops.next() { + Some(stop) => { + debug_assert_eq!(stop.offset, 0.0); + stop.color.premultiplied() + } + None => { + error!("Zero gradient stops found!"); + PremultipliedColorF::BLACK + } + }; + + // A table of gradient entries, with two colors per entry, that specify the start and end color + // within the segment of the gradient space represented by that entry. To lookup a gradient result, + // first the entry index is calculated to determine which two colors to interpolate between, then + // the offset within that entry bucket is used to interpolate between the two colors in that entry. + // This layout is motivated by the fact that if one naively tries to store a single color per entry + // and interpolate directly between entries, then hard stops will become softened because the end + // color of an entry actually differs from the start color of the next entry, even though they fall + // at the same edge offset in the gradient space. Instead, the two-color-per-entry layout preserves + // hard stops, as the end color for a given entry can differ from the start color for the following + // entry. + // Colors are stored in RGBA32F format (in the GPU cache). This table requires the gradient color + // stops to be normalized to the range [0, 1]. The first and last entries hold the first and last + // color stop colors respectively, while the entries in between hold the interpolated color stop + // values for the range [0, 1]. + // As a further optimization, rather than directly storing the end color, the difference of the end + // color from the start color is stored instead, so that an entry can be evaluated more cheaply + // with start+diff*offset instead of mix(start,end,offset). Further, the color difference in two + // adjacent entries will always be the same if they were generated from the same set of stops/run. + // To allow fast searching of the table, if two adjacent entries generated from different sets of + // stops (a boundary) have the same difference, the floating-point bits of the stop will be nudged + // so that they compare differently without perceptibly altering the interpolation result. This way, + // one can quickly scan the table and recover runs just by comparing the color differences of the + // current and next entry. + // For example, a table with 2 inside entries (startR,startG,startB):(diffR,diffG,diffB) might look + // like so: + // first | 0.0 | 0.5 | last + // (0,0,0):(0,0,0) | (1,0,0):(-1,1,0) | (0,0,1):(0,1,-1) | (1,1,1):(0,0,0) + // ^ solid black ^ red to green ^ blue to green ^ solid white + let mut entries = [GradientDataEntry::white(); GRADIENT_DATA_SIZE]; + let mut prev_step = cur_color; + if reverse_stops { + // Fill in the first entry (for reversed stops) with the first color stop + prev_step = GradientGpuBlockBuilder::fill_colors( + GRADIENT_DATA_LAST_STOP, + GRADIENT_DATA_LAST_STOP + 1, + &cur_color, + &cur_color, + &mut entries, + &prev_step, + ); + + // Fill in the center of the gradient table, generating a color ramp between each consecutive pair + // of gradient stops. Each iteration of a loop will fill the indices in [next_idx, cur_idx). The + // loop will then fill indices in [GRADIENT_DATA_TABLE_BEGIN, GRADIENT_DATA_TABLE_END). + let mut cur_idx = GRADIENT_DATA_TABLE_END; + for next in src_stops { + let next_color = next.color.premultiplied(); + let next_idx = Self::get_index(1.0 - next.offset); + + if next_idx < cur_idx { + prev_step = GradientGpuBlockBuilder::fill_colors( + next_idx, + cur_idx, + &next_color, + &cur_color, + &mut entries, + &prev_step, + ); + cur_idx = next_idx; + } + + cur_color = next_color; + } + if cur_idx != GRADIENT_DATA_TABLE_BEGIN { + error!("Gradient stops abruptly at {}, auto-completing to white", cur_idx); + } + + // Fill in the last entry (for reversed stops) with the last color stop + GradientGpuBlockBuilder::fill_colors( + GRADIENT_DATA_FIRST_STOP, + GRADIENT_DATA_FIRST_STOP + 1, + &cur_color, + &cur_color, + &mut entries, + &prev_step, + ); + } else { + // Fill in the first entry with the first color stop + prev_step = GradientGpuBlockBuilder::fill_colors( + GRADIENT_DATA_FIRST_STOP, + GRADIENT_DATA_FIRST_STOP + 1, + &cur_color, + &cur_color, + &mut entries, + &prev_step, + ); + + // Fill in the center of the gradient table, generating a color ramp between each consecutive pair + // of gradient stops. Each iteration of a loop will fill the indices in [cur_idx, next_idx). The + // loop will then fill indices in [GRADIENT_DATA_TABLE_BEGIN, GRADIENT_DATA_TABLE_END). + let mut cur_idx = GRADIENT_DATA_TABLE_BEGIN; + for next in src_stops { + let next_color = next.color.premultiplied(); + let next_idx = Self::get_index(next.offset); + + if next_idx > cur_idx { + prev_step = GradientGpuBlockBuilder::fill_colors( + cur_idx, + next_idx, + &cur_color, + &next_color, + &mut entries, + &prev_step, + ); + cur_idx = next_idx; + } + + cur_color = next_color; + } + if cur_idx != GRADIENT_DATA_TABLE_END { + error!("Gradient stops abruptly at {}, auto-completing to white", cur_idx); + } + + // Fill in the last entry with the last color stop + GradientGpuBlockBuilder::fill_colors( + GRADIENT_DATA_LAST_STOP, + GRADIENT_DATA_LAST_STOP + 1, + &cur_color, + &cur_color, + &mut entries, + &prev_step, + ); + } + + let mut writer = gpu_buffer_builder.write_blocks(2 * entries.len()); + + for entry in entries { + writer.push_one(entry.start_color); + writer.push_one(entry.end_step); + } + + writer.finish() + } +} + +// If the gradient is not tiled we know that any content outside of the clip will not +// be shown. Applying the clip early reduces how much of the gradient we +// render and cache. We do this optimization separately on each axis. +// Returns the offset between the new and old primitive rect origin, to apply to the +// gradient parameters that are relative to the primitive origin. +pub fn apply_gradient_local_clip( + prim_rect: &mut LayoutRect, + stretch_size: &LayoutSize, + tile_spacing: &LayoutSize, + clip_rect: &LayoutRect, +) -> LayoutVector2D { + let w = prim_rect.max.x.min(clip_rect.max.x) - prim_rect.min.x; + let h = prim_rect.max.y.min(clip_rect.max.y) - prim_rect.min.y; + let is_tiled_x = w > stretch_size.width + tile_spacing.width; + let is_tiled_y = h > stretch_size.height + tile_spacing.height; + + let mut offset = LayoutVector2D::new(0.0, 0.0); + + if !is_tiled_x { + let diff = (clip_rect.min.x - prim_rect.min.x).min(prim_rect.width()); + if diff > 0.0 { + prim_rect.min.x += diff; + offset.x = -diff; + } + + let diff = prim_rect.max.x - clip_rect.max.x; + if diff > 0.0 { + prim_rect.max.x -= diff; + } + } + + if !is_tiled_y { + let diff = (clip_rect.min.y - prim_rect.min.y).min(prim_rect.height()); + if diff > 0.0 { + prim_rect.min.y += diff; + offset.y = -diff; + } + + let diff = prim_rect.max.y - clip_rect.max.y; + if diff > 0.0 { + prim_rect.max.y -= diff; + } + } + + offset +} + +#[test] +#[cfg(target_pointer_width = "64")] +fn test_struct_sizes() { + use std::mem; + // The sizes of these structures are critical for performance on a number of + // talos stress tests. If you get a failure here on CI, there's two possibilities: + // (a) You made a structure smaller than it currently is. Great work! Update the + // test expectations and move on. + // (b) You made a structure larger. This is not necessarily a problem, but should only + // be done with care, and after checking if talos performance regresses badly. + assert_eq!(mem::size_of::<LinearGradient>(), 72, "LinearGradient size changed"); + assert_eq!(mem::size_of::<LinearGradientTemplate>(), 144, "LinearGradientTemplate size changed"); + assert_eq!(mem::size_of::<LinearGradientKey>(), 88, "LinearGradientKey size changed"); + + assert_eq!(mem::size_of::<RadialGradient>(), 72, "RadialGradient size changed"); + assert_eq!(mem::size_of::<RadialGradientTemplate>(), 144, "RadialGradientTemplate size changed"); + assert_eq!(mem::size_of::<RadialGradientKey>(), 96, "RadialGradientKey size changed"); + + assert_eq!(mem::size_of::<ConicGradient>(), 72, "ConicGradient size changed"); + assert_eq!(mem::size_of::<ConicGradientTemplate>(), 144, "ConicGradientTemplate size changed"); + assert_eq!(mem::size_of::<ConicGradientKey>(), 96, "ConicGradientKey size changed"); +} diff --git a/gfx/wr/webrender/src/prim_store/gradient/radial.rs b/gfx/wr/webrender/src/prim_store/gradient/radial.rs new file mode 100644 index 0000000000..f3f20f9a55 --- /dev/null +++ b/gfx/wr/webrender/src/prim_store/gradient/radial.rs @@ -0,0 +1,531 @@ +/* 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/. */ + +//! Radial gradients +//! +//! Specification: https://drafts.csswg.org/css-images-4/#radial-gradients +//! +//! Radial gradients are rendered via cached render tasks and composited with the image brush. + +use euclid::{vec2, size2}; +use api::{ExtendMode, GradientStop, PremultipliedColorF, ColorU}; +use api::units::*; +use crate::scene_building::IsVisible; +use crate::frame_builder::FrameBuildingState; +use crate::intern::{Internable, InternDebug, Handle as InternHandle}; +use crate::internal_types::LayoutPrimitiveInfo; +use crate::prim_store::{BrushSegment, GradientTileRange, InternablePrimitive}; +use crate::prim_store::{PrimitiveInstanceKind, PrimitiveOpacity}; +use crate::prim_store::{PrimKeyCommonData, PrimTemplateCommonData, PrimitiveStore}; +use crate::prim_store::{NinePatchDescriptor, PointKey, SizeKey, FloatKey}; +use crate::render_task::{RenderTask, RenderTaskKind}; +use crate::render_task_graph::RenderTaskId; +use crate::render_task_cache::{RenderTaskCacheKeyKind, RenderTaskCacheKey, RenderTaskParent}; +use crate::renderer::GpuBufferAddress; +use crate::picture::{SurfaceIndex}; + +use std::{hash, ops::{Deref, DerefMut}}; +use super::{ + stops_and_min_alpha, GradientStopKey, GradientGpuBlockBuilder, + apply_gradient_local_clip, +}; + +/// Hashable radial gradient parameters, for use during prim interning. +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[derive(Debug, Clone, MallocSizeOf, PartialEq)] +pub struct RadialGradientParams { + pub start_radius: f32, + pub end_radius: f32, + pub ratio_xy: f32, +} + +impl Eq for RadialGradientParams {} + +impl hash::Hash for RadialGradientParams { + fn hash<H: hash::Hasher>(&self, state: &mut H) { + self.start_radius.to_bits().hash(state); + self.end_radius.to_bits().hash(state); + self.ratio_xy.to_bits().hash(state); + } +} + +/// Identifying key for a radial gradient. +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[derive(Debug, Clone, Eq, PartialEq, Hash, MallocSizeOf)] +pub struct RadialGradientKey { + pub common: PrimKeyCommonData, + pub extend_mode: ExtendMode, + pub center: PointKey, + pub params: RadialGradientParams, + pub stretch_size: SizeKey, + pub stops: Vec<GradientStopKey>, + pub tile_spacing: SizeKey, + pub nine_patch: Option<Box<NinePatchDescriptor>>, +} + +impl RadialGradientKey { + pub fn new( + info: &LayoutPrimitiveInfo, + radial_grad: RadialGradient, + ) -> Self { + RadialGradientKey { + common: info.into(), + extend_mode: radial_grad.extend_mode, + center: radial_grad.center, + params: radial_grad.params, + stretch_size: radial_grad.stretch_size, + stops: radial_grad.stops, + tile_spacing: radial_grad.tile_spacing, + nine_patch: radial_grad.nine_patch, + } + } +} + +impl InternDebug for RadialGradientKey {} + +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[derive(MallocSizeOf)] +#[derive(Debug)] +pub struct RadialGradientTemplate { + pub common: PrimTemplateCommonData, + pub extend_mode: ExtendMode, + pub params: RadialGradientParams, + pub center: DevicePoint, + pub task_size: DeviceIntSize, + pub scale: DeviceVector2D, + pub stretch_size: LayoutSize, + pub tile_spacing: LayoutSize, + pub brush_segments: Vec<BrushSegment>, + pub stops_opacity: PrimitiveOpacity, + pub stops: Vec<GradientStop>, + pub src_color: Option<RenderTaskId>, +} + +impl Deref for RadialGradientTemplate { + type Target = PrimTemplateCommonData; + fn deref(&self) -> &Self::Target { + &self.common + } +} + +impl DerefMut for RadialGradientTemplate { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.common + } +} + +impl From<RadialGradientKey> for RadialGradientTemplate { + fn from(item: RadialGradientKey) -> Self { + let common = PrimTemplateCommonData::with_key_common(item.common); + let mut brush_segments = Vec::new(); + + if let Some(ref nine_patch) = item.nine_patch { + brush_segments = nine_patch.create_segments(common.prim_rect.size()); + } + + let (stops, min_alpha) = stops_and_min_alpha(&item.stops); + + // Save opacity of the stops for use in + // selecting which pass this gradient + // should be drawn in. + let stops_opacity = PrimitiveOpacity::from_alpha(min_alpha); + + let mut stretch_size: LayoutSize = item.stretch_size.into(); + stretch_size.width = stretch_size.width.min(common.prim_rect.width()); + stretch_size.height = stretch_size.height.min(common.prim_rect.height()); + + // Avoid rendering enormous gradients. Radial gradients are mostly made of soft transitions, + // so it is unlikely that rendering at a higher resolution that 1024 would produce noticeable + // differences, especially with 8 bits per channel. + const MAX_SIZE: f32 = 1024.0; + let mut task_size: DeviceSize = stretch_size.cast_unit(); + let mut scale = vec2(1.0, 1.0); + if task_size.width > MAX_SIZE { + scale.x = task_size.width/ MAX_SIZE; + task_size.width = MAX_SIZE; + } + if task_size.height > MAX_SIZE { + scale.y = task_size.height /MAX_SIZE; + task_size.height = MAX_SIZE; + } + + RadialGradientTemplate { + common, + center: DevicePoint::new(item.center.x, item.center.y), + extend_mode: item.extend_mode, + params: item.params, + stretch_size, + task_size: task_size.ceil().to_i32(), + scale, + tile_spacing: item.tile_spacing.into(), + brush_segments, + stops_opacity, + stops, + src_color: None, + } + } +} + +impl RadialGradientTemplate { + /// Update the GPU cache for a given primitive template. This may be called multiple + /// times per frame, by each primitive reference that refers to this interned + /// template. The initial request call to the GPU cache ensures that work is only + /// done if the cache entry is invalid (due to first use or eviction). + pub fn update( + &mut self, + frame_state: &mut FrameBuildingState, + parent_surface: SurfaceIndex, + ) { + if let Some(mut request) = + frame_state.gpu_cache.request(&mut self.common.gpu_cache_handle) { + // write_prim_gpu_blocks + request.push(PremultipliedColorF::WHITE); + request.push(PremultipliedColorF::WHITE); + request.push([ + self.stretch_size.width, + self.stretch_size.height, + 0.0, + 0.0, + ]); + + // write_segment_gpu_blocks + for segment in &self.brush_segments { + // has to match VECS_PER_SEGMENT + request.write_segment( + segment.local_rect, + segment.extra_data, + ); + } + } + + let task_size = self.task_size; + let cache_key = RadialGradientCacheKey { + size: task_size, + center: PointKey { x: self.center.x, y: self.center.y }, + scale: PointKey { x: self.scale.x, y: self.scale.y }, + start_radius: FloatKey(self.params.start_radius), + end_radius: FloatKey(self.params.end_radius), + ratio_xy: FloatKey(self.params.ratio_xy), + extend_mode: self.extend_mode, + stops: self.stops.iter().map(|stop| (*stop).into()).collect(), + }; + + let task_id = frame_state.resource_cache.request_render_task( + RenderTaskCacheKey { + size: task_size, + kind: RenderTaskCacheKeyKind::RadialGradient(cache_key), + }, + frame_state.gpu_cache, + frame_state.frame_gpu_data, + frame_state.rg_builder, + None, + false, + RenderTaskParent::Surface(parent_surface), + &mut frame_state.surface_builder, + |rg_builder, gpu_buffer_builder| { + let stops = GradientGpuBlockBuilder::build( + false, + gpu_buffer_builder, + &self.stops, + ); + + rg_builder.add().init(RenderTask::new_dynamic( + task_size, + RenderTaskKind::RadialGradient(RadialGradientTask { + extend_mode: self.extend_mode, + center: self.center, + scale: self.scale, + params: self.params.clone(), + stops, + }), + )) + } + ); + + self.src_color = Some(task_id); + + // Tile spacing is always handled by decomposing into separate draw calls so the + // primitive opacity is equivalent to stops opacity. This might change to being + // set to non-opaque in the presence of tile spacing if/when tile spacing is handled + // in the same way as with the image primitive. + self.opacity = self.stops_opacity; + } +} + +pub type RadialGradientDataHandle = InternHandle<RadialGradient>; + +#[derive(Debug, MallocSizeOf)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct RadialGradient { + pub extend_mode: ExtendMode, + pub center: PointKey, + pub params: RadialGradientParams, + pub stretch_size: SizeKey, + pub stops: Vec<GradientStopKey>, + pub tile_spacing: SizeKey, + pub nine_patch: Option<Box<NinePatchDescriptor>>, +} + +impl Internable for RadialGradient { + type Key = RadialGradientKey; + type StoreData = RadialGradientTemplate; + type InternData = (); + const PROFILE_COUNTER: usize = crate::profiler::INTERNED_RADIAL_GRADIENTS; +} + +impl InternablePrimitive for RadialGradient { + fn into_key( + self, + info: &LayoutPrimitiveInfo, + ) -> RadialGradientKey { + RadialGradientKey::new(info, self) + } + + fn make_instance_kind( + _key: RadialGradientKey, + data_handle: RadialGradientDataHandle, + _prim_store: &mut PrimitiveStore, + _reference_frame_relative_offset: LayoutVector2D, + ) -> PrimitiveInstanceKind { + PrimitiveInstanceKind::RadialGradient { + data_handle, + visible_tiles_range: GradientTileRange::empty(), + } + } +} + +impl IsVisible for RadialGradient { + fn is_visible(&self) -> bool { + true + } +} + +#[derive(Debug)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct RadialGradientTask { + pub extend_mode: ExtendMode, + pub center: DevicePoint, + pub scale: DeviceVector2D, + pub params: RadialGradientParams, + pub stops: GpuBufferAddress, +} + +impl RadialGradientTask { + pub fn to_instance(&self, target_rect: &DeviceIntRect) -> RadialGradientInstance { + RadialGradientInstance { + task_rect: target_rect.to_f32(), + center: self.center, + scale: self.scale, + start_radius: self.params.start_radius, + end_radius: self.params.end_radius, + ratio_xy: self.params.ratio_xy, + extend_mode: self.extend_mode as i32, + gradient_stops_address: self.stops.as_int(), + } + } +} + +/// The per-instance shader input of a radial gradient render task. +/// +/// Must match the RADIAL_GRADIENT instance description in renderer/vertex.rs. +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[repr(C)] +#[derive(Clone, Debug)] +pub struct RadialGradientInstance { + pub task_rect: DeviceRect, + pub center: DevicePoint, + pub scale: DeviceVector2D, + pub start_radius: f32, + pub end_radius: f32, + pub ratio_xy: f32, + pub extend_mode: i32, + pub gradient_stops_address: i32, +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct RadialGradientCacheKey { + pub size: DeviceIntSize, + pub center: PointKey, + pub scale: PointKey, + pub start_radius: FloatKey, + pub end_radius: FloatKey, + pub ratio_xy: FloatKey, + pub extend_mode: ExtendMode, + pub stops: Vec<GradientStopKey>, +} + +/// Avoid invoking the radial gradient shader on large areas where the color is +/// constant. +/// +/// If the extend mode is set to clamp, the "interesting" part +/// of the gradient is only in the bounds of the gradient's ellipse, and the rest +/// is the color of the last gradient stop. +/// +/// Sometimes we run into radial gradient with a small radius compared to the +/// primitive bounds, which means a large area of the primitive is a constant color +/// This function tries to detect that, potentially shrink the gradient primitive to only +/// the useful part and if needed insert solid color primitives around the gradient where +/// parts of it have been removed. +pub fn optimize_radial_gradient( + prim_rect: &mut LayoutRect, + stretch_size: &mut LayoutSize, + center: &mut LayoutPoint, + tile_spacing: &mut LayoutSize, + clip_rect: &LayoutRect, + radius: LayoutSize, + end_offset: f32, + extend_mode: ExtendMode, + stops: &[GradientStopKey], + solid_parts: &mut dyn FnMut(&LayoutRect, ColorU), +) { + let offset = apply_gradient_local_clip( + prim_rect, + stretch_size, + tile_spacing, + clip_rect + ); + + *center += offset; + + if extend_mode != ExtendMode::Clamp || stops.is_empty() { + return; + } + + // Bounding box of the "interesting" part of the gradient. + let min = prim_rect.min + center.to_vector() - radius.to_vector() * end_offset; + let max = prim_rect.min + center.to_vector() + radius.to_vector() * end_offset; + + // The (non-repeated) gradient primitive rect. + let gradient_rect = LayoutRect::from_origin_and_size( + prim_rect.min, + *stretch_size, + ); + + // How much internal margin between the primitive bounds and the gradient's + // bounding rect (areas that are a constant color). + let mut l = (min.x - gradient_rect.min.x).max(0.0).floor(); + let mut t = (min.y - gradient_rect.min.y).max(0.0).floor(); + let mut r = (gradient_rect.max.x - max.x).max(0.0).floor(); + let mut b = (gradient_rect.max.y - max.y).max(0.0).floor(); + + let is_tiled = prim_rect.width() > stretch_size.width + tile_spacing.width + || prim_rect.height() > stretch_size.height + tile_spacing.height; + + let bg_color = stops.last().unwrap().color; + + if bg_color.a != 0 && is_tiled { + // If the primitive has repetitions, it's not enough to insert solid rects around it, + // so bail out. + return; + } + + // If the background is fully transparent, shrinking the primitive bounds as much as possible + // is always a win. If the background is not transparent, we have to insert solid rectangles + // around the shrunk parts. + // If the background is transparent and the primitive is tiled, the optimization may introduce + // tile spacing which forces the tiling to be manually decomposed. + // Either way, don't bother optimizing unless it saves a significant amount of pixels. + if bg_color.a != 0 || (is_tiled && tile_spacing.is_empty()) { + let threshold = 128.0; + if l < threshold { l = 0.0 } + if t < threshold { t = 0.0 } + if r < threshold { r = 0.0 } + if b < threshold { b = 0.0 } + } + + if l + t + r + b == 0.0 { + // No adjustment to make; + return; + } + + // Insert solid rectangles around the gradient, in the places where the primitive will be + // shrunk. + if bg_color.a != 0 { + if l != 0.0 && t != 0.0 { + let solid_rect = LayoutRect::from_origin_and_size( + gradient_rect.min, + size2(l, t), + ); + solid_parts(&solid_rect, bg_color); + } + + if l != 0.0 && b != 0.0 { + let solid_rect = LayoutRect::from_origin_and_size( + gradient_rect.bottom_left() - vec2(0.0, b), + size2(l, b), + ); + solid_parts(&solid_rect, bg_color); + } + + if t != 0.0 && r != 0.0 { + let solid_rect = LayoutRect::from_origin_and_size( + gradient_rect.top_right() - vec2(r, 0.0), + size2(r, t), + ); + solid_parts(&solid_rect, bg_color); + } + + if r != 0.0 && b != 0.0 { + let solid_rect = LayoutRect::from_origin_and_size( + gradient_rect.bottom_right() - vec2(r, b), + size2(r, b), + ); + solid_parts(&solid_rect, bg_color); + } + + if l != 0.0 { + let solid_rect = LayoutRect::from_origin_and_size( + gradient_rect.min + vec2(0.0, t), + size2(l, gradient_rect.height() - t - b), + ); + solid_parts(&solid_rect, bg_color); + } + + if r != 0.0 { + let solid_rect = LayoutRect::from_origin_and_size( + gradient_rect.top_right() + vec2(-r, t), + size2(r, gradient_rect.height() - t - b), + ); + solid_parts(&solid_rect, bg_color); + } + + if t != 0.0 { + let solid_rect = LayoutRect::from_origin_and_size( + gradient_rect.min + vec2(l, 0.0), + size2(gradient_rect.width() - l - r, t), + ); + solid_parts(&solid_rect, bg_color); + } + + if b != 0.0 { + let solid_rect = LayoutRect::from_origin_and_size( + gradient_rect.bottom_left() + vec2(l, -b), + size2(gradient_rect.width() - l - r, b), + ); + solid_parts(&solid_rect, bg_color); + } + } + + // Shrink the gradient primitive. + + prim_rect.min.x += l; + prim_rect.min.y += t; + + stretch_size.width -= l + r; + stretch_size.height -= b + t; + + center.x -= l; + center.y -= t; + + tile_spacing.width += l + r; + tile_spacing.height += t + b; +} |