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}