diff options
Diffstat (limited to '')
-rw-r--r-- | gfx/wr/webrender/src/prim_store/gradient/mod.rs | 392 |
1 files changed, 392 insertions, 0 deletions
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"); +} |