webrender/prim_store/gradient/
mod.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use api::{ColorF, ColorU, GradientStop, PremultipliedColorF};
6use api::units::{LayoutRect, LayoutSize, LayoutVector2D};
7use crate::renderer::{GpuBufferAddress, GpuBufferBuilderF};
8use std::hash;
9
10mod linear;
11mod radial;
12mod conic;
13
14pub use linear::MAX_CACHED_SIZE as LINEAR_MAX_CACHED_SIZE;
15
16pub use linear::*;
17pub use radial::*;
18pub use conic::*;
19
20/// A hashable gradient stop that can be used in primitive keys.
21#[cfg_attr(feature = "capture", derive(Serialize))]
22#[cfg_attr(feature = "replay", derive(Deserialize))]
23#[derive(Debug, Copy, Clone, MallocSizeOf, PartialEq)]
24pub struct GradientStopKey {
25    pub offset: f32,
26    pub color: ColorU,
27}
28
29impl GradientStopKey {
30    pub fn empty() -> Self {
31        GradientStopKey {
32            offset: 0.0,
33            color: ColorU::new(0, 0, 0, 0),
34        }
35    }
36}
37
38impl Into<GradientStopKey> for GradientStop {
39    fn into(self) -> GradientStopKey {
40        GradientStopKey {
41            offset: self.offset,
42            color: self.color.into(),
43        }
44    }
45}
46
47// Convert `stop_keys` into a vector of `GradientStop`s, which is a more
48// convenient representation for the current gradient builder. Compute the
49// minimum stop alpha along the way.
50fn stops_and_min_alpha(stop_keys: &[GradientStopKey]) -> (Vec<GradientStop>, f32) {
51    let mut min_alpha: f32 = 1.0;
52    let stops = stop_keys.iter().map(|stop_key| {
53        let color: ColorF = stop_key.color.into();
54        min_alpha = min_alpha.min(color.a);
55
56        GradientStop {
57            offset: stop_key.offset,
58            color,
59        }
60    }).collect();
61
62    (stops, min_alpha)
63}
64
65impl Eq for GradientStopKey {}
66
67impl hash::Hash for GradientStopKey {
68    fn hash<H: hash::Hasher>(&self, state: &mut H) {
69        self.offset.to_bits().hash(state);
70        self.color.hash(state);
71    }
72}
73
74// The gradient entry index for the first color stop
75pub const GRADIENT_DATA_FIRST_STOP: usize = 0;
76// The gradient entry index for the last color stop
77pub const GRADIENT_DATA_LAST_STOP: usize = GRADIENT_DATA_SIZE - 1;
78
79// The start of the gradient data table
80pub const GRADIENT_DATA_TABLE_BEGIN: usize = GRADIENT_DATA_FIRST_STOP + 1;
81// The exclusive bound of the gradient data table
82pub const GRADIENT_DATA_TABLE_END: usize = GRADIENT_DATA_LAST_STOP;
83// The number of entries in the gradient data table.
84pub const GRADIENT_DATA_TABLE_SIZE: usize = 128;
85
86// The number of entries in a gradient data: GRADIENT_DATA_TABLE_SIZE + first stop entry + last stop entry
87pub const GRADIENT_DATA_SIZE: usize = GRADIENT_DATA_TABLE_SIZE + 2;
88
89/// An entry in a gradient data table representing a segment of the gradient
90/// color space.
91#[derive(Debug, Copy, Clone)]
92#[repr(C)]
93struct GradientDataEntry {
94    start_color: PremultipliedColorF,
95    end_step: PremultipliedColorF,
96}
97
98impl GradientDataEntry {
99    fn white() -> Self {
100        Self {
101            start_color: PremultipliedColorF::WHITE,
102            end_step: PremultipliedColorF::TRANSPARENT,
103        }
104    }
105}
106
107// TODO(gw): Tidy this up to be a free function / module?
108pub struct GradientGpuBlockBuilder {}
109
110impl GradientGpuBlockBuilder {
111    /// Generate a color ramp filling the indices in [start_idx, end_idx) and interpolating
112    /// from start_color to end_color.
113    fn fill_colors(
114        start_idx: usize,
115        end_idx: usize,
116        start_color: &PremultipliedColorF,
117        end_color: &PremultipliedColorF,
118        entries: &mut [GradientDataEntry; GRADIENT_DATA_SIZE],
119        prev_step: &PremultipliedColorF,
120    ) -> PremultipliedColorF {
121        // Calculate the color difference for individual steps in the ramp.
122        let inv_steps = 1.0 / (end_idx - start_idx) as f32;
123        let mut step = PremultipliedColorF {
124            r: (end_color.r - start_color.r) * inv_steps,
125            g: (end_color.g - start_color.g) * inv_steps,
126            b: (end_color.b - start_color.b) * inv_steps,
127            a: (end_color.a - start_color.a) * inv_steps,
128        };
129        // As a subtle form of compression, we ensure that the step values for
130        // each stop range are the same if and only if they belong to the same
131        // stop range. However, if two different stop ranges have the same step,
132        // we need to modify the steps so they compare unequally between ranges.
133        // This allows to quickly compare if two adjacent stops belong to the
134        // same range by comparing their steps.
135        if step == *prev_step {
136            // Modify the step alpha value as if by nextafter(). The difference
137            // here should be so small as to be unnoticeable, but yet allow it
138            // to compare differently.
139            step.a = f32::from_bits(if step.a == 0.0 { 1 } else { step.a.to_bits() + 1 });
140        }
141
142        let mut cur_color = *start_color;
143
144        // Walk the ramp writing start and end colors for each entry.
145        for index in start_idx .. end_idx {
146            let entry = &mut entries[index];
147            entry.start_color = cur_color;
148            cur_color.r += step.r;
149            cur_color.g += step.g;
150            cur_color.b += step.b;
151            cur_color.a += step.a;
152            entry.end_step = step;
153        }
154
155        step
156    }
157
158    /// Compute an index into the gradient entry table based on a gradient stop offset. This
159    /// function maps offsets from [0, 1] to indices in [GRADIENT_DATA_TABLE_BEGIN, GRADIENT_DATA_TABLE_END].
160    #[inline]
161    fn get_index(offset: f32) -> usize {
162        (offset.max(0.0).min(1.0) * GRADIENT_DATA_TABLE_SIZE as f32 +
163            GRADIENT_DATA_TABLE_BEGIN as f32)
164            .round() as usize
165    }
166
167    // Build the gradient data from the supplied stops, reversing them if necessary.
168    pub fn build(
169        reverse_stops: bool,
170        gpu_buffer_builder: &mut GpuBufferBuilderF,
171        src_stops: &[GradientStop],
172    ) -> GpuBufferAddress {
173        // Preconditions (should be ensured by DisplayListBuilder):
174        // * we have at least two stops
175        // * first stop has offset 0.0
176        // * last stop has offset 1.0
177        let mut src_stops = src_stops.into_iter();
178        let mut cur_color = match src_stops.next() {
179            Some(stop) => {
180                debug_assert_eq!(stop.offset, 0.0);
181                stop.color.premultiplied()
182            }
183            None => {
184                error!("Zero gradient stops found!");
185                PremultipliedColorF::BLACK
186            }
187        };
188
189        // A table of gradient entries, with two colors per entry, that specify the start and end color
190        // within the segment of the gradient space represented by that entry. To lookup a gradient result,
191        // first the entry index is calculated to determine which two colors to interpolate between, then
192        // the offset within that entry bucket is used to interpolate between the two colors in that entry.
193        // This layout is motivated by the fact that if one naively tries to store a single color per entry
194        // and interpolate directly between entries, then hard stops will become softened because the end
195        // color of an entry actually differs from the start color of the next entry, even though they fall
196        // at the same edge offset in the gradient space. Instead, the two-color-per-entry layout preserves
197        // hard stops, as the end color for a given entry can differ from the start color for the following
198        // entry.
199        // Colors are stored in RGBA32F format (in the GPU cache). This table requires the gradient color
200        // stops to be normalized to the range [0, 1]. The first and last entries hold the first and last
201        // color stop colors respectively, while the entries in between hold the interpolated color stop
202        // values for the range [0, 1].
203        // As a further optimization, rather than directly storing the end color, the difference of the end
204        // color from the start color is stored instead, so that an entry can be evaluated more cheaply
205        // with start+diff*offset instead of mix(start,end,offset). Further, the color difference in two
206        // adjacent entries will always be the same if they were generated from the same set of stops/run.
207        // To allow fast searching of the table, if two adjacent entries generated from different sets of
208        // stops (a boundary) have the same difference, the floating-point bits of the stop will be nudged
209        // so that they compare differently without perceptibly altering the interpolation result. This way,
210        // one can quickly scan the table and recover runs just by comparing the color differences of the
211        // current and next entry.
212        // For example, a table with 2 inside entries (startR,startG,startB):(diffR,diffG,diffB) might look
213        // like so:
214        //     first           | 0.0              | 0.5              | last
215        //     (0,0,0):(0,0,0) | (1,0,0):(-1,1,0) | (0,0,1):(0,1,-1) | (1,1,1):(0,0,0)
216        //     ^ solid black     ^ red to green     ^ blue to green    ^ solid white
217        let mut entries = [GradientDataEntry::white(); GRADIENT_DATA_SIZE];
218        let mut prev_step = cur_color;
219        if reverse_stops {
220            // Fill in the first entry (for reversed stops) with the first color stop
221            prev_step = GradientGpuBlockBuilder::fill_colors(
222                GRADIENT_DATA_LAST_STOP,
223                GRADIENT_DATA_LAST_STOP + 1,
224                &cur_color,
225                &cur_color,
226                &mut entries,
227                &prev_step,
228            );
229
230            // Fill in the center of the gradient table, generating a color ramp between each consecutive pair
231            // of gradient stops. Each iteration of a loop will fill the indices in [next_idx, cur_idx). The
232            // loop will then fill indices in [GRADIENT_DATA_TABLE_BEGIN, GRADIENT_DATA_TABLE_END).
233            let mut cur_idx = GRADIENT_DATA_TABLE_END;
234            for next in src_stops {
235                let next_color = next.color.premultiplied();
236                let next_idx = Self::get_index(1.0 - next.offset);
237
238                if next_idx < cur_idx {
239                    prev_step = GradientGpuBlockBuilder::fill_colors(
240                        next_idx,
241                        cur_idx,
242                        &next_color,
243                        &cur_color,
244                        &mut entries,
245                        &prev_step,
246                    );
247                    cur_idx = next_idx;
248                }
249
250                cur_color = next_color;
251            }
252            if cur_idx != GRADIENT_DATA_TABLE_BEGIN {
253                error!("Gradient stops abruptly at {}, auto-completing to white", cur_idx);
254            }
255
256            // Fill in the last entry (for reversed stops) with the last color stop
257            GradientGpuBlockBuilder::fill_colors(
258                GRADIENT_DATA_FIRST_STOP,
259                GRADIENT_DATA_FIRST_STOP + 1,
260                &cur_color,
261                &cur_color,
262                &mut entries,
263                &prev_step,
264            );
265        } else {
266            // Fill in the first entry with the first color stop
267            prev_step = GradientGpuBlockBuilder::fill_colors(
268                GRADIENT_DATA_FIRST_STOP,
269                GRADIENT_DATA_FIRST_STOP + 1,
270                &cur_color,
271                &cur_color,
272                &mut entries,
273                &prev_step,
274            );
275
276            // Fill in the center of the gradient table, generating a color ramp between each consecutive pair
277            // of gradient stops. Each iteration of a loop will fill the indices in [cur_idx, next_idx). The
278            // loop will then fill indices in [GRADIENT_DATA_TABLE_BEGIN, GRADIENT_DATA_TABLE_END).
279            let mut cur_idx = GRADIENT_DATA_TABLE_BEGIN;
280            for next in src_stops {
281                let next_color = next.color.premultiplied();
282                let next_idx = Self::get_index(next.offset);
283
284                if next_idx > cur_idx {
285                    prev_step = GradientGpuBlockBuilder::fill_colors(
286                        cur_idx,
287                        next_idx,
288                        &cur_color,
289                        &next_color,
290                        &mut entries,
291                        &prev_step,
292                    );
293                    cur_idx = next_idx;
294                }
295
296                cur_color = next_color;
297            }
298            if cur_idx != GRADIENT_DATA_TABLE_END {
299                error!("Gradient stops abruptly at {}, auto-completing to white", cur_idx);
300            }
301
302            // Fill in the last entry with the last color stop
303            GradientGpuBlockBuilder::fill_colors(
304                GRADIENT_DATA_LAST_STOP,
305                GRADIENT_DATA_LAST_STOP + 1,
306                &cur_color,
307                &cur_color,
308                &mut entries,
309                &prev_step,
310            );
311        }
312
313        let mut writer = gpu_buffer_builder.write_blocks(2 * entries.len());
314
315        for entry in entries {
316            writer.push_one(entry.start_color);
317            writer.push_one(entry.end_step);
318        }
319
320        writer.finish()
321    }
322}
323
324// If the gradient is not tiled we know that any content outside of the clip will not
325// be shown. Applying the clip early reduces how much of the gradient we
326// render and cache. We do this optimization separately on each axis.
327// Returns the offset between the new and old primitive rect origin, to apply to the
328// gradient parameters that are relative to the primitive origin.
329pub fn apply_gradient_local_clip(
330    prim_rect: &mut LayoutRect,
331    stretch_size: &LayoutSize,
332    tile_spacing: &LayoutSize,
333    clip_rect: &LayoutRect,
334) -> LayoutVector2D {
335    let w = prim_rect.max.x.min(clip_rect.max.x) - prim_rect.min.x;
336    let h = prim_rect.max.y.min(clip_rect.max.y) - prim_rect.min.y;
337    let is_tiled_x = w > stretch_size.width + tile_spacing.width;
338    let is_tiled_y = h > stretch_size.height + tile_spacing.height;
339
340    let mut offset = LayoutVector2D::new(0.0, 0.0);
341
342    if !is_tiled_x {
343        let diff = (clip_rect.min.x - prim_rect.min.x).min(prim_rect.width());
344        if diff > 0.0 {
345            prim_rect.min.x += diff;
346            offset.x = -diff;
347        }
348
349        let diff = prim_rect.max.x - clip_rect.max.x;
350        if diff > 0.0 {
351            prim_rect.max.x -= diff;
352        }
353    }
354
355    if !is_tiled_y {
356        let diff = (clip_rect.min.y - prim_rect.min.y).min(prim_rect.height());
357        if diff > 0.0 {
358            prim_rect.min.y += diff;
359            offset.y = -diff;
360        }
361
362        let diff = prim_rect.max.y - clip_rect.max.y;
363        if diff > 0.0 {
364            prim_rect.max.y -= diff;
365        }
366    }
367
368    offset
369}
370
371#[test]
372#[cfg(target_pointer_width = "64")]
373fn test_struct_sizes() {
374    use std::mem;
375    // The sizes of these structures are critical for performance on a number of
376    // talos stress tests. If you get a failure here on CI, there's two possibilities:
377    // (a) You made a structure smaller than it currently is. Great work! Update the
378    //     test expectations and move on.
379    // (b) You made a structure larger. This is not necessarily a problem, but should only
380    //     be done with care, and after checking if talos performance regresses badly.
381    assert_eq!(mem::size_of::<LinearGradient>(), 72, "LinearGradient size changed");
382    assert_eq!(mem::size_of::<LinearGradientTemplate>(), 144, "LinearGradientTemplate size changed");
383    assert_eq!(mem::size_of::<LinearGradientKey>(), 88, "LinearGradientKey size changed");
384
385    assert_eq!(mem::size_of::<RadialGradient>(), 72, "RadialGradient size changed");
386    assert_eq!(mem::size_of::<RadialGradientTemplate>(), 144, "RadialGradientTemplate size changed");
387    assert_eq!(mem::size_of::<RadialGradientKey>(), 96, "RadialGradientKey size changed");
388
389    assert_eq!(mem::size_of::<ConicGradient>(), 72, "ConicGradient size changed");
390    assert_eq!(mem::size_of::<ConicGradientTemplate>(), 144, "ConicGradientTemplate size changed");
391    assert_eq!(mem::size_of::<ConicGradientKey>(), 96, "ConicGradientKey size changed");
392}