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, ExtendMode, GradientStop, PremultipliedColorF};
6use api::units::{LayoutRect, LayoutSize, LayoutVector2D};
7use crate::renderer::{GpuBufferAddress, GpuBufferBuilderF, GpuBufferWriterF};
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#[repr(u8)]
21#[derive(Copy, Clone, Debug)]
22pub enum GradientKind {
23    Linear = 0,
24    Radial = 1,
25    Conic = 2,
26}
27
28/// A hashable gradient stop that can be used in primitive keys.
29#[cfg_attr(feature = "capture", derive(Serialize))]
30#[cfg_attr(feature = "replay", derive(Deserialize))]
31#[derive(Debug, Copy, Clone, MallocSizeOf, PartialEq)]
32pub struct GradientStopKey {
33    pub offset: f32,
34    pub color: ColorU,
35}
36
37impl GradientStopKey {
38    pub fn empty() -> Self {
39        GradientStopKey {
40            offset: 0.0,
41            color: ColorU::new(0, 0, 0, 0),
42        }
43    }
44}
45
46impl Into<GradientStopKey> for GradientStop {
47    fn into(self) -> GradientStopKey {
48        GradientStopKey {
49            offset: self.offset,
50            color: self.color.into(),
51        }
52    }
53}
54
55// Convert `stop_keys` into a vector of `GradientStop`s, which is a more
56// convenient representation for the current gradient builder. Compute the
57// minimum stop alpha along the way.
58fn stops_and_min_alpha(stop_keys: &[GradientStopKey]) -> (Vec<GradientStop>, f32) {
59    let mut min_alpha: f32 = 1.0;
60    let stops = stop_keys.iter().map(|stop_key| {
61        let color: ColorF = stop_key.color.into();
62        min_alpha = min_alpha.min(color.a);
63
64        GradientStop {
65            offset: stop_key.offset,
66            color,
67        }
68    }).collect();
69
70    (stops, min_alpha)
71}
72
73fn write_gpu_gradient_stops_header_and_colors(
74    stops: &[GradientStop],
75    kind: GradientKind,
76    extend_mode: ExtendMode,
77    writer: &mut GpuBufferWriterF,
78) -> bool {
79    // Write the header.
80    writer.push_one([
81        (kind as u8) as f32,
82        stops.len() as f32,
83        if extend_mode == ExtendMode::Repeat { 1.0 } else { 0.0 },
84        0.0
85    ]);
86
87    // Write the stop colors.
88    let mut is_opaque = true;
89    for stop in stops {
90        writer.push_one(stop.color.premultiplied());
91        is_opaque &= stop.color.a == 1.0;
92    }
93
94    is_opaque
95}
96
97/// Builds the gpu representation for common gradient parameters and
98/// returns whether the gradient is fully opaque.
99///
100/// The format is:
101///
102/// ```ascii
103///
104/// [count, extend_mode, <padding>, color0.r, color0.g, color0.b, color0.a, ..., offset0, offset1, ..., <padding>]
105/// |_____________________________| |__________________________________________| |_______________________________|
106///        header: vec4                        colors: [vec4; n]                     offsets: [vec4; ceil(n/4)]
107/// ```
108///
109/// Packed contiguously such that each portion is 4-floats aligned to facilitate
110/// reading them from the gpu buffer.
111fn write_gpu_gradient_stops_linear(
112    stops: &[GradientStop],
113    kind: GradientKind,
114    extend_mode: ExtendMode,
115    writer: &mut GpuBufferWriterF,
116) -> bool {
117    let is_opaque = write_gpu_gradient_stops_header_and_colors(
118        stops,
119        kind,
120        extend_mode,
121        writer
122    );
123
124    for chunk in stops.chunks(4) {
125        let mut block = [0.0; 4];
126        let mut i = 0;
127        for stop in chunk {
128            block[i] = stop.offset;
129            i += 1;
130        }
131        writer.push_one(block);
132    }
133
134    is_opaque
135}
136
137// Push stop offsets in rearranged order so that the search can be carried
138// out as an implicit tree traversal.
139//
140// The structure of the tree is:
141//  - Each level is plit into 5 partitions.
142//  - The root level has one node (4 offsets -> 5 partitions).
143//  - Each level has 5 more nodes than the previous one.
144//  - Levels are pushed one by one starting from the root
145//
146// ```ascii
147// level : indices
148// ------:---------
149//   0   :                                                               24     ...
150//   1   :          4         9            14             19             |      ...
151//   2   :  0,1,2,3,|,5,6,7,8,|10,11,12,13,| ,15,16,17,18,| ,20,21,22,23,| ,25, ...
152// ```
153//
154// In the example above:
155// - The first (root) contains a single block containing the stop offsets from
156//   indices [24, 49, 74, 99].
157// - The second level contains blocks of offsets from indices [4, 9, 14, 19],
158//   [29, 34, 39, 44], etc.
159// - The third (leaf) level contains blocks from indices [0,1,2,3], [5,6,7,8],
160//   [15, 16, 17, 18], etc.
161//
162// Placeholder offsets (1.0) are used when a level has more capacity than the
163// input number of stops.
164//
165// Conceptually, blocks [0,1,2,3] and [5,6,7,8] are the first two children of
166// the node [4,9,14,19], separated by the offset from index 4.
167// Links are not explicitly represented via pointers or indices. Instead the
168// position in the buffer is sufficient to represent the level and index of the
169// stop (at the expense of having to store extra padding to round up each tree
170// level to its power-of-5-aligned size).
171//
172// This scheme is meant to make the traversal efficient loading offsets in
173// blocks of 4. The shader can converge to the leaf in very few loads.
174fn write_gpu_gradient_stops_tree(
175    stops: &[GradientStop],
176    kind: GradientKind,
177    extend_mode: ExtendMode,
178    writer: &mut GpuBufferWriterF,
179) -> bool {
180    let is_opaque = write_gpu_gradient_stops_header_and_colors(
181        stops,
182        kind,
183        extend_mode,
184        writer
185    );
186
187    let num_stops = stops.len();
188    let mut num_levels = 1;
189    let mut index_stride = 5;
190    let mut next_index_stride = 1;
191    // Number of 4-offsets blocks for the current level.
192    // The root has 1, then each level has 5 more than the previous one.
193    let mut num_blocks_for_level = 1;
194    let mut offset_blocks = 1;
195    while offset_blocks * 4 < num_stops {
196        num_blocks_for_level *= 5;
197        offset_blocks += num_blocks_for_level;
198
199        num_levels += 1;
200        index_stride *= 5;
201        next_index_stride *= 5;
202    }
203
204    // Fix offset_blocks up to account for the fact that we don't
205    // store the entirety of the last level;
206    let num_blocks_for_last_level = num_blocks_for_level.min(num_stops / 5 + 1);
207
208    // Reset num_blocks_for_level for the traversal.
209    num_blocks_for_level = 1;
210
211    // Go over each level, starting from the root.
212    for level in 0..num_levels {
213        // This scheme rounds up the number of offsets to store for each
214        // level to the next power of 5, which can represent a lot of wasted
215        // space, especially for the last levels. We need each level to start
216        // at a specific power-of-5-aligned offset so we can't get around the
217        // wasted space for all levels except the last one (which has the most
218        // waste).
219        let is_last_level = level == num_levels - 1;
220        let num_blocks = if is_last_level {
221            num_blocks_for_last_level
222        } else {
223            num_blocks_for_level
224        };
225
226        for block_idx in 0..num_blocks {
227            let mut block = [1.0; 4];
228            for i in 0..4 {
229                let linear_idx = block_idx * index_stride
230                    + i * next_index_stride
231                    + next_index_stride - 1;
232
233                if linear_idx < num_stops {
234                    block[i] = stops[linear_idx].offset;
235                }
236            }
237            writer.push_one(block);
238        }
239
240        index_stride = next_index_stride;
241        next_index_stride /= 5;
242        num_blocks_for_level *= 5;
243    }
244
245    return is_opaque;
246}
247
248fn gpu_gradient_stops_blocks(num_stops: usize, tree_traversal: bool) -> usize {
249    let header_blocks = 1;
250    let color_blocks = num_stops;
251
252    // When using a linear traversal we need 1/4th of the number of offsets,
253    // rounded up (since we store 4 stop offsets per block).
254    let mut offset_blocks = (num_stops + 3) / 4;
255
256    if tree_traversal {
257        // If this is changed, matching changes should be made to the
258        // equivalent code in write_gpu_gradient_stops_tree.
259        let mut num_blocks_for_level = 1;
260        offset_blocks = 1;
261        while offset_blocks * 4 < num_stops {
262            num_blocks_for_level *= 5;
263            offset_blocks += num_blocks_for_level;
264        }
265
266        // Fix the capacity up to account for the fact that we don't
267        // store the entirety of the last level;
268        let num_blocks_for_last_level = num_blocks_for_level.min(num_stops / 5 + 1);
269        offset_blocks -= num_blocks_for_level;
270        offset_blocks += num_blocks_for_last_level;
271    }
272
273    header_blocks + color_blocks + offset_blocks
274}
275
276impl Eq for GradientStopKey {}
277
278impl hash::Hash for GradientStopKey {
279    fn hash<H: hash::Hasher>(&self, state: &mut H) {
280        self.offset.to_bits().hash(state);
281        self.color.hash(state);
282    }
283}
284
285// The gradient entry index for the first color stop
286pub const GRADIENT_DATA_FIRST_STOP: usize = 0;
287// The gradient entry index for the last color stop
288pub const GRADIENT_DATA_LAST_STOP: usize = GRADIENT_DATA_SIZE - 1;
289
290// The start of the gradient data table
291pub const GRADIENT_DATA_TABLE_BEGIN: usize = GRADIENT_DATA_FIRST_STOP + 1;
292// The exclusive bound of the gradient data table
293pub const GRADIENT_DATA_TABLE_END: usize = GRADIENT_DATA_LAST_STOP;
294// The number of entries in the gradient data table.
295pub const GRADIENT_DATA_TABLE_SIZE: usize = 128;
296
297// The number of entries in a gradient data: GRADIENT_DATA_TABLE_SIZE + first stop entry + last stop entry
298pub const GRADIENT_DATA_SIZE: usize = GRADIENT_DATA_TABLE_SIZE + 2;
299
300/// An entry in a gradient data table representing a segment of the gradient
301/// color space.
302#[derive(Debug, Copy, Clone)]
303#[repr(C)]
304struct GradientDataEntry {
305    start_color: PremultipliedColorF,
306    end_step: PremultipliedColorF,
307}
308
309impl GradientDataEntry {
310    fn white() -> Self {
311        Self {
312            start_color: PremultipliedColorF::WHITE,
313            end_step: PremultipliedColorF::TRANSPARENT,
314        }
315    }
316}
317
318// TODO(gw): Tidy this up to be a free function / module?
319pub struct GradientGpuBlockBuilder {}
320
321impl GradientGpuBlockBuilder {
322    /// Generate a color ramp filling the indices in [start_idx, end_idx) and interpolating
323    /// from start_color to end_color.
324    fn fill_colors(
325        start_idx: usize,
326        end_idx: usize,
327        start_color: &PremultipliedColorF,
328        end_color: &PremultipliedColorF,
329        entries: &mut [GradientDataEntry; GRADIENT_DATA_SIZE],
330        prev_step: &PremultipliedColorF,
331    ) -> PremultipliedColorF {
332        // Calculate the color difference for individual steps in the ramp.
333        let inv_steps = 1.0 / (end_idx - start_idx) as f32;
334        let mut step = PremultipliedColorF {
335            r: (end_color.r - start_color.r) * inv_steps,
336            g: (end_color.g - start_color.g) * inv_steps,
337            b: (end_color.b - start_color.b) * inv_steps,
338            a: (end_color.a - start_color.a) * inv_steps,
339        };
340        // As a subtle form of compression, we ensure that the step values for
341        // each stop range are the same if and only if they belong to the same
342        // stop range. However, if two different stop ranges have the same step,
343        // we need to modify the steps so they compare unequally between ranges.
344        // This allows to quickly compare if two adjacent stops belong to the
345        // same range by comparing their steps.
346        if step == *prev_step {
347            // Modify the step alpha value as if by nextafter(). The difference
348            // here should be so small as to be unnoticeable, but yet allow it
349            // to compare differently.
350            step.a = f32::from_bits(if step.a == 0.0 { 1 } else { step.a.to_bits() + 1 });
351        }
352
353        let mut cur_color = *start_color;
354
355        // Walk the ramp writing start and end colors for each entry.
356        for index in start_idx .. end_idx {
357            let entry = &mut entries[index];
358            entry.start_color = cur_color;
359            cur_color.r += step.r;
360            cur_color.g += step.g;
361            cur_color.b += step.b;
362            cur_color.a += step.a;
363            entry.end_step = step;
364        }
365
366        step
367    }
368
369    /// Compute an index into the gradient entry table based on a gradient stop offset. This
370    /// function maps offsets from [0, 1] to indices in [GRADIENT_DATA_TABLE_BEGIN, GRADIENT_DATA_TABLE_END].
371    #[inline]
372    fn get_index(offset: f32) -> usize {
373        (offset.max(0.0).min(1.0) * GRADIENT_DATA_TABLE_SIZE as f32 +
374            GRADIENT_DATA_TABLE_BEGIN as f32)
375            .round() as usize
376    }
377
378    // Build the gradient data from the supplied stops, reversing them if necessary.
379    pub fn build(
380        reverse_stops: bool,
381        gpu_buffer_builder: &mut GpuBufferBuilderF,
382        src_stops: &[GradientStop],
383    ) -> GpuBufferAddress {
384        // Preconditions (should be ensured by DisplayListBuilder):
385        // * we have at least two stops
386        // * first stop has offset 0.0
387        // * last stop has offset 1.0
388        let mut src_stops = src_stops.into_iter();
389        let mut cur_color = match src_stops.next() {
390            Some(stop) => {
391                debug_assert_eq!(stop.offset, 0.0);
392                stop.color.premultiplied()
393            }
394            None => {
395                error!("Zero gradient stops found!");
396                PremultipliedColorF::BLACK
397            }
398        };
399
400        // A table of gradient entries, with two colors per entry, that specify the start and end color
401        // within the segment of the gradient space represented by that entry. To lookup a gradient result,
402        // first the entry index is calculated to determine which two colors to interpolate between, then
403        // the offset within that entry bucket is used to interpolate between the two colors in that entry.
404        // This layout is motivated by the fact that if one naively tries to store a single color per entry
405        // and interpolate directly between entries, then hard stops will become softened because the end
406        // color of an entry actually differs from the start color of the next entry, even though they fall
407        // at the same edge offset in the gradient space. Instead, the two-color-per-entry layout preserves
408        // hard stops, as the end color for a given entry can differ from the start color for the following
409        // entry.
410        // Colors are stored in RGBA32F format (in the GPU cache). This table requires the gradient color
411        // stops to be normalized to the range [0, 1]. The first and last entries hold the first and last
412        // color stop colors respectively, while the entries in between hold the interpolated color stop
413        // values for the range [0, 1].
414        // As a further optimization, rather than directly storing the end color, the difference of the end
415        // color from the start color is stored instead, so that an entry can be evaluated more cheaply
416        // with start+diff*offset instead of mix(start,end,offset). Further, the color difference in two
417        // adjacent entries will always be the same if they were generated from the same set of stops/run.
418        // To allow fast searching of the table, if two adjacent entries generated from different sets of
419        // stops (a boundary) have the same difference, the floating-point bits of the stop will be nudged
420        // so that they compare differently without perceptibly altering the interpolation result. This way,
421        // one can quickly scan the table and recover runs just by comparing the color differences of the
422        // current and next entry.
423        // For example, a table with 2 inside entries (startR,startG,startB):(diffR,diffG,diffB) might look
424        // like so:
425        //     first           | 0.0              | 0.5              | last
426        //     (0,0,0):(0,0,0) | (1,0,0):(-1,1,0) | (0,0,1):(0,1,-1) | (1,1,1):(0,0,0)
427        //     ^ solid black     ^ red to green     ^ blue to green    ^ solid white
428        let mut entries = [GradientDataEntry::white(); GRADIENT_DATA_SIZE];
429        let mut prev_step = cur_color;
430        if reverse_stops {
431            // Fill in the first entry (for reversed stops) with the first color stop
432            prev_step = GradientGpuBlockBuilder::fill_colors(
433                GRADIENT_DATA_LAST_STOP,
434                GRADIENT_DATA_LAST_STOP + 1,
435                &cur_color,
436                &cur_color,
437                &mut entries,
438                &prev_step,
439            );
440
441            // Fill in the center of the gradient table, generating a color ramp between each consecutive pair
442            // of gradient stops. Each iteration of a loop will fill the indices in [next_idx, cur_idx). The
443            // loop will then fill indices in [GRADIENT_DATA_TABLE_BEGIN, GRADIENT_DATA_TABLE_END).
444            let mut cur_idx = GRADIENT_DATA_TABLE_END;
445            for next in src_stops {
446                let next_color = next.color.premultiplied();
447                let next_idx = Self::get_index(1.0 - next.offset);
448
449                if next_idx < cur_idx {
450                    prev_step = GradientGpuBlockBuilder::fill_colors(
451                        next_idx,
452                        cur_idx,
453                        &next_color,
454                        &cur_color,
455                        &mut entries,
456                        &prev_step,
457                    );
458                    cur_idx = next_idx;
459                }
460
461                cur_color = next_color;
462            }
463            if cur_idx != GRADIENT_DATA_TABLE_BEGIN {
464                error!("Gradient stops abruptly at {}, auto-completing to white", cur_idx);
465            }
466
467            // Fill in the last entry (for reversed stops) with the last color stop
468            GradientGpuBlockBuilder::fill_colors(
469                GRADIENT_DATA_FIRST_STOP,
470                GRADIENT_DATA_FIRST_STOP + 1,
471                &cur_color,
472                &cur_color,
473                &mut entries,
474                &prev_step,
475            );
476        } else {
477            // Fill in the first entry with the first color stop
478            prev_step = GradientGpuBlockBuilder::fill_colors(
479                GRADIENT_DATA_FIRST_STOP,
480                GRADIENT_DATA_FIRST_STOP + 1,
481                &cur_color,
482                &cur_color,
483                &mut entries,
484                &prev_step,
485            );
486
487            // Fill in the center of the gradient table, generating a color ramp between each consecutive pair
488            // of gradient stops. Each iteration of a loop will fill the indices in [cur_idx, next_idx). The
489            // loop will then fill indices in [GRADIENT_DATA_TABLE_BEGIN, GRADIENT_DATA_TABLE_END).
490            let mut cur_idx = GRADIENT_DATA_TABLE_BEGIN;
491            for next in src_stops {
492                let next_color = next.color.premultiplied();
493                let next_idx = Self::get_index(next.offset);
494
495                if next_idx > cur_idx {
496                    prev_step = GradientGpuBlockBuilder::fill_colors(
497                        cur_idx,
498                        next_idx,
499                        &cur_color,
500                        &next_color,
501                        &mut entries,
502                        &prev_step,
503                    );
504                    cur_idx = next_idx;
505                }
506
507                cur_color = next_color;
508            }
509            if cur_idx != GRADIENT_DATA_TABLE_END {
510                error!("Gradient stops abruptly at {}, auto-completing to white", cur_idx);
511            }
512
513            // Fill in the last entry with the last color stop
514            GradientGpuBlockBuilder::fill_colors(
515                GRADIENT_DATA_LAST_STOP,
516                GRADIENT_DATA_LAST_STOP + 1,
517                &cur_color,
518                &cur_color,
519                &mut entries,
520                &prev_step,
521            );
522        }
523
524        let mut writer = gpu_buffer_builder.write_blocks(2 * entries.len());
525
526        for entry in entries {
527            writer.push_one(entry.start_color);
528            writer.push_one(entry.end_step);
529        }
530
531        writer.finish()
532    }
533}
534
535// If the gradient is not tiled we know that any content outside of the clip will not
536// be shown. Applying the clip early reduces how much of the gradient we
537// render and cache. We do this optimization separately on each axis.
538// Returns the offset between the new and old primitive rect origin, to apply to the
539// gradient parameters that are relative to the primitive origin.
540pub fn apply_gradient_local_clip(
541    prim_rect: &mut LayoutRect,
542    stretch_size: &LayoutSize,
543    tile_spacing: &LayoutSize,
544    clip_rect: &LayoutRect,
545) -> LayoutVector2D {
546    let w = prim_rect.max.x.min(clip_rect.max.x) - prim_rect.min.x;
547    let h = prim_rect.max.y.min(clip_rect.max.y) - prim_rect.min.y;
548    let is_tiled_x = w > stretch_size.width + tile_spacing.width;
549    let is_tiled_y = h > stretch_size.height + tile_spacing.height;
550
551    let mut offset = LayoutVector2D::new(0.0, 0.0);
552
553    if !is_tiled_x {
554        let diff = (clip_rect.min.x - prim_rect.min.x).min(prim_rect.width());
555        if diff > 0.0 {
556            prim_rect.min.x += diff;
557            offset.x = -diff;
558        }
559
560        let diff = prim_rect.max.x - clip_rect.max.x;
561        if diff > 0.0 {
562            prim_rect.max.x -= diff;
563        }
564    }
565
566    if !is_tiled_y {
567        let diff = (clip_rect.min.y - prim_rect.min.y).min(prim_rect.height());
568        if diff > 0.0 {
569            prim_rect.min.y += diff;
570            offset.y = -diff;
571        }
572
573        let diff = prim_rect.max.y - clip_rect.max.y;
574        if diff > 0.0 {
575            prim_rect.max.y -= diff;
576        }
577    }
578
579    offset
580}
581
582#[test]
583#[cfg(target_pointer_width = "64")]
584fn test_struct_sizes() {
585    use std::mem;
586    // The sizes of these structures are critical for performance on a number of
587    // talos stress tests. If you get a failure here on CI, there's two possibilities:
588    // (a) You made a structure smaller than it currently is. Great work! Update the
589    //     test expectations and move on.
590    // (b) You made a structure larger. This is not necessarily a problem, but should only
591    //     be done with care, and after checking if talos performance regresses badly.
592    assert_eq!(mem::size_of::<LinearGradient>(), 72, "LinearGradient size changed");
593    assert_eq!(mem::size_of::<LinearGradientTemplate>(), 144, "LinearGradientTemplate size changed");
594    assert_eq!(mem::size_of::<LinearGradientKey>(), 96, "LinearGradientKey size changed");
595
596    assert_eq!(mem::size_of::<RadialGradient>(), 72, "RadialGradient size changed");
597    assert_eq!(mem::size_of::<RadialGradientTemplate>(), 144, "RadialGradientTemplate size changed");
598    assert_eq!(mem::size_of::<RadialGradientKey>(), 96, "RadialGradientKey size changed");
599
600    assert_eq!(mem::size_of::<ConicGradient>(), 72, "ConicGradient size changed");
601    assert_eq!(mem::size_of::<ConicGradientTemplate>(), 144, "ConicGradientTemplate size changed");
602    assert_eq!(mem::size_of::<ConicGradientKey>(), 96, "ConicGradientKey size changed");
603}