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}