webrender/prim_store/gradient/
radial.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
5//! Radial gradients
6//!
7//! Specification: https://drafts.csswg.org/css-images-4/#radial-gradients
8//!
9//! Radial gradients are rendered via cached render tasks and composited with the image brush.
10
11use euclid::{vec2, size2};
12use api::{ColorF, ColorU, ExtendMode, GradientStop, PremultipliedColorF};
13use api::units::*;
14use crate::pattern::{Pattern, PatternBuilder, PatternBuilderContext, PatternBuilderState, PatternKind, PatternShaderInput, PatternTextureInput};
15use crate::scene_building::IsVisible;
16use crate::frame_builder::FrameBuildingState;
17use crate::intern::{Internable, InternDebug, Handle as InternHandle};
18use crate::internal_types::LayoutPrimitiveInfo;
19use crate::prim_store::{BrushSegment, GradientTileRange, InternablePrimitive};
20use crate::prim_store::{PrimitiveInstanceKind, PrimitiveOpacity};
21use crate::prim_store::{PrimKeyCommonData, PrimTemplateCommonData, PrimitiveStore};
22use crate::prim_store::{NinePatchDescriptor, PointKey, SizeKey, FloatKey};
23use crate::render_task::{RenderTask, RenderTaskKind};
24use crate::render_task_graph::RenderTaskId;
25use crate::render_task_cache::{RenderTaskCacheKeyKind, RenderTaskCacheKey, RenderTaskParent};
26use crate::renderer::{GpuBufferAddress, GpuBufferBuilder};
27
28use std::{hash, ops::{Deref, DerefMut}};
29use super::{
30    stops_and_min_alpha, GradientStopKey, GradientGpuBlockBuilder,
31    apply_gradient_local_clip,
32};
33
34/// Hashable radial gradient parameters, for use during prim interning.
35#[cfg_attr(feature = "capture", derive(Serialize))]
36#[cfg_attr(feature = "replay", derive(Deserialize))]
37#[derive(Debug, Clone, MallocSizeOf, PartialEq)]
38pub struct RadialGradientParams {
39    pub start_radius: f32,
40    pub end_radius: f32,
41    pub ratio_xy: f32,
42}
43
44impl Eq for RadialGradientParams {}
45
46impl hash::Hash for RadialGradientParams {
47    fn hash<H: hash::Hasher>(&self, state: &mut H) {
48        self.start_radius.to_bits().hash(state);
49        self.end_radius.to_bits().hash(state);
50        self.ratio_xy.to_bits().hash(state);
51    }
52}
53
54/// Identifying key for a radial gradient.
55#[cfg_attr(feature = "capture", derive(Serialize))]
56#[cfg_attr(feature = "replay", derive(Deserialize))]
57#[derive(Debug, Clone, Eq, PartialEq, Hash, MallocSizeOf)]
58pub struct RadialGradientKey {
59    pub common: PrimKeyCommonData,
60    pub extend_mode: ExtendMode,
61    pub center: PointKey,
62    pub params: RadialGradientParams,
63    pub stretch_size: SizeKey,
64    pub stops: Vec<GradientStopKey>,
65    pub tile_spacing: SizeKey,
66    pub nine_patch: Option<Box<NinePatchDescriptor>>,
67}
68
69impl RadialGradientKey {
70    pub fn new(
71        info: &LayoutPrimitiveInfo,
72        radial_grad: RadialGradient,
73    ) -> Self {
74        RadialGradientKey {
75            common: info.into(),
76            extend_mode: radial_grad.extend_mode,
77            center: radial_grad.center,
78            params: radial_grad.params,
79            stretch_size: radial_grad.stretch_size,
80            stops: radial_grad.stops,
81            tile_spacing: radial_grad.tile_spacing,
82            nine_patch: radial_grad.nine_patch,
83        }
84    }
85}
86
87impl InternDebug for RadialGradientKey {}
88
89#[cfg_attr(feature = "capture", derive(Serialize))]
90#[cfg_attr(feature = "replay", derive(Deserialize))]
91#[derive(MallocSizeOf)]
92#[derive(Debug)]
93pub struct RadialGradientTemplate {
94    pub common: PrimTemplateCommonData,
95    pub extend_mode: ExtendMode,
96    pub params: RadialGradientParams,
97    pub center: DevicePoint,
98    pub task_size: DeviceIntSize,
99    pub scale: DeviceVector2D,
100    pub stretch_size: LayoutSize,
101    pub tile_spacing: LayoutSize,
102    pub brush_segments: Vec<BrushSegment>,
103    pub stops_opacity: PrimitiveOpacity,
104    pub stops: Vec<GradientStop>,
105    pub src_color: Option<RenderTaskId>,
106}
107
108impl PatternBuilder for RadialGradientTemplate {
109    fn build(
110        &self,
111        _sub_rect: Option<DeviceRect>,
112        _ctx: &PatternBuilderContext,
113        state: &mut PatternBuilderState,
114    ) -> Pattern {
115        // The scaling parameter is used to compensate for when we reduce the size
116        // of the render task for cached gradients. Here we aren't applying any.
117        let no_scale = DeviceVector2D::one();
118
119        radial_gradient_pattern(
120            self.center,
121            no_scale,
122            &self.params,
123            self.extend_mode,
124            &self.stops,
125            state.frame_gpu_data,
126        )
127    }
128
129    fn get_base_color(
130        &self,
131        _ctx: &PatternBuilderContext,
132    ) -> ColorF {
133        ColorF::WHITE
134    }
135
136    fn use_shared_pattern(
137        &self,
138    ) -> bool {
139        true
140    }
141}
142
143impl Deref for RadialGradientTemplate {
144    type Target = PrimTemplateCommonData;
145    fn deref(&self) -> &Self::Target {
146        &self.common
147    }
148}
149
150impl DerefMut for RadialGradientTemplate {
151    fn deref_mut(&mut self) -> &mut Self::Target {
152        &mut self.common
153    }
154}
155
156impl From<RadialGradientKey> for RadialGradientTemplate {
157    fn from(item: RadialGradientKey) -> Self {
158        let common = PrimTemplateCommonData::with_key_common(item.common);
159        let mut brush_segments = Vec::new();
160
161        if let Some(ref nine_patch) = item.nine_patch {
162            brush_segments = nine_patch.create_segments(common.prim_rect.size());
163        }
164
165        let (stops, min_alpha) = stops_and_min_alpha(&item.stops);
166
167        // Save opacity of the stops for use in
168        // selecting which pass this gradient
169        // should be drawn in.
170        let stops_opacity = PrimitiveOpacity::from_alpha(min_alpha);
171
172        let mut stretch_size: LayoutSize = item.stretch_size.into();
173        stretch_size.width = stretch_size.width.min(common.prim_rect.width());
174        stretch_size.height = stretch_size.height.min(common.prim_rect.height());
175
176        // Avoid rendering enormous gradients. Radial gradients are mostly made of soft transitions,
177        // so it is unlikely that rendering at a higher resolution that 1024 would produce noticeable
178        // differences, especially with 8 bits per channel.
179        const MAX_SIZE: f32 = 1024.0;
180        let mut task_size: DeviceSize = stretch_size.cast_unit();
181        let mut scale = vec2(1.0, 1.0);
182        if task_size.width > MAX_SIZE {
183            scale.x = task_size.width/ MAX_SIZE;
184            task_size.width = MAX_SIZE;
185        }
186        if task_size.height > MAX_SIZE {
187            scale.y = task_size.height /MAX_SIZE;
188            task_size.height = MAX_SIZE;
189        }
190
191        RadialGradientTemplate {
192            common,
193            center: DevicePoint::new(item.center.x, item.center.y),
194            extend_mode: item.extend_mode,
195            params: item.params,
196            stretch_size,
197            task_size: task_size.ceil().to_i32(),
198            scale,
199            tile_spacing: item.tile_spacing.into(),
200            brush_segments,
201            stops_opacity,
202            stops,
203            src_color: None,
204        }
205    }
206}
207
208impl RadialGradientTemplate {
209    /// Update the GPU cache for a given primitive template. This may be called multiple
210    /// times per frame, by each primitive reference that refers to this interned
211    /// template. The initial request call to the GPU cache ensures that work is only
212    /// done if the cache entry is invalid (due to first use or eviction).
213    pub fn update(
214        &mut self,
215        frame_state: &mut FrameBuildingState,
216    ) {
217        if let Some(mut request) =
218            frame_state.gpu_cache.request(&mut self.common.gpu_cache_handle) {
219            // write_prim_gpu_blocks
220            request.push(PremultipliedColorF::WHITE);
221            request.push(PremultipliedColorF::WHITE);
222            request.push([
223                self.stretch_size.width,
224                self.stretch_size.height,
225                0.0,
226                0.0,
227            ]);
228
229            // write_segment_gpu_blocks
230            for segment in &self.brush_segments {
231                // has to match VECS_PER_SEGMENT
232                request.write_segment(
233                    segment.local_rect,
234                    segment.extra_data,
235                );
236            }
237        }
238
239        let task_size = self.task_size;
240        let cache_key = RadialGradientCacheKey {
241            size: task_size,
242            center: PointKey { x: self.center.x, y: self.center.y },
243            scale: PointKey { x: self.scale.x, y: self.scale.y },
244            start_radius: FloatKey(self.params.start_radius),
245            end_radius: FloatKey(self.params.end_radius),
246            ratio_xy: FloatKey(self.params.ratio_xy),
247            extend_mode: self.extend_mode,
248            stops: self.stops.iter().map(|stop| (*stop).into()).collect(),
249        };
250
251        let task_id = frame_state.resource_cache.request_render_task(
252            Some(RenderTaskCacheKey {
253                size: task_size,
254                kind: RenderTaskCacheKeyKind::RadialGradient(cache_key),
255            }),
256            false,
257            RenderTaskParent::Surface,
258            frame_state.gpu_cache,
259            &mut frame_state.frame_gpu_data.f32,
260            frame_state.rg_builder,
261            &mut frame_state.surface_builder,
262            &mut |rg_builder, gpu_buffer_builder, _| {
263                let stops = GradientGpuBlockBuilder::build(
264                    false,
265                    gpu_buffer_builder,
266                    &self.stops,
267                );
268
269                rg_builder.add().init(RenderTask::new_dynamic(
270                    task_size,
271                    RenderTaskKind::RadialGradient(RadialGradientTask {
272                        extend_mode: self.extend_mode,
273                        center: self.center,
274                        scale: self.scale,
275                        params: self.params.clone(),
276                        stops,
277                    }),
278                ))
279            }
280        );
281
282        self.src_color = Some(task_id);
283
284        // Tile spacing is always handled by decomposing into separate draw calls so the
285        // primitive opacity is equivalent to stops opacity. This might change to being
286        // set to non-opaque in the presence of tile spacing if/when tile spacing is handled
287        // in the same way as with the image primitive.
288        self.opacity = self.stops_opacity;
289    }
290}
291
292pub type RadialGradientDataHandle = InternHandle<RadialGradient>;
293
294#[derive(Debug, MallocSizeOf)]
295#[cfg_attr(feature = "capture", derive(Serialize))]
296#[cfg_attr(feature = "replay", derive(Deserialize))]
297pub struct RadialGradient {
298    pub extend_mode: ExtendMode,
299    pub center: PointKey,
300    pub params: RadialGradientParams,
301    pub stretch_size: SizeKey,
302    pub stops: Vec<GradientStopKey>,
303    pub tile_spacing: SizeKey,
304    pub nine_patch: Option<Box<NinePatchDescriptor>>,
305}
306
307impl Internable for RadialGradient {
308    type Key = RadialGradientKey;
309    type StoreData = RadialGradientTemplate;
310    type InternData = ();
311    const PROFILE_COUNTER: usize = crate::profiler::INTERNED_RADIAL_GRADIENTS;
312}
313
314impl InternablePrimitive for RadialGradient {
315    fn into_key(
316        self,
317        info: &LayoutPrimitiveInfo,
318    ) -> RadialGradientKey {
319        RadialGradientKey::new(info, self)
320    }
321
322    fn make_instance_kind(
323        _key: RadialGradientKey,
324        data_handle: RadialGradientDataHandle,
325        _prim_store: &mut PrimitiveStore,
326    ) -> PrimitiveInstanceKind {
327        PrimitiveInstanceKind::RadialGradient {
328            data_handle,
329            visible_tiles_range: GradientTileRange::empty(),
330            cached: true,
331        }
332    }
333}
334
335impl IsVisible for RadialGradient {
336    fn is_visible(&self) -> bool {
337        true
338    }
339}
340
341#[derive(Debug)]
342#[cfg_attr(feature = "capture", derive(Serialize))]
343#[cfg_attr(feature = "replay", derive(Deserialize))]
344pub struct RadialGradientTask {
345    pub extend_mode: ExtendMode,
346    pub center: DevicePoint,
347    pub scale: DeviceVector2D,
348    pub params: RadialGradientParams,
349    pub stops: GpuBufferAddress,
350}
351
352impl RadialGradientTask {
353    pub fn to_instance(&self, target_rect: &DeviceIntRect) -> RadialGradientInstance {
354        RadialGradientInstance {
355            task_rect: target_rect.to_f32(),
356            center: self.center,
357            scale: self.scale,
358            start_radius: self.params.start_radius,
359            end_radius: self.params.end_radius,
360            ratio_xy: self.params.ratio_xy,
361            extend_mode: self.extend_mode as i32,
362            gradient_stops_address: self.stops.as_int(),
363        }
364    }
365}
366
367/// The per-instance shader input of a radial gradient render task.
368///
369/// Must match the RADIAL_GRADIENT instance description in renderer/vertex.rs.
370#[cfg_attr(feature = "capture", derive(Serialize))]
371#[cfg_attr(feature = "replay", derive(Deserialize))]
372#[repr(C)]
373#[derive(Clone, Debug)]
374pub struct RadialGradientInstance {
375    pub task_rect: DeviceRect,
376    pub center: DevicePoint,
377    pub scale: DeviceVector2D,
378    pub start_radius: f32,
379    pub end_radius: f32,
380    pub ratio_xy: f32,
381    pub extend_mode: i32,
382    pub gradient_stops_address: i32,
383}
384
385#[derive(Clone, Debug, Hash, PartialEq, Eq)]
386#[cfg_attr(feature = "capture", derive(Serialize))]
387#[cfg_attr(feature = "replay", derive(Deserialize))]
388pub struct RadialGradientCacheKey {
389    pub size: DeviceIntSize,
390    pub center: PointKey,
391    pub scale: PointKey,
392    pub start_radius: FloatKey,
393    pub end_radius: FloatKey,
394    pub ratio_xy: FloatKey,
395    pub extend_mode: ExtendMode,
396    pub stops: Vec<GradientStopKey>,
397}
398
399/// Avoid invoking the radial gradient shader on large areas where the color is
400/// constant.
401///
402/// If the extend mode is set to clamp, the "interesting" part
403/// of the gradient is only in the bounds of the gradient's ellipse, and the rest
404/// is the color of the last gradient stop.
405///
406/// Sometimes we run into radial gradient with a small radius compared to the
407/// primitive bounds, which means a large area of the primitive is a constant color
408/// This function tries to detect that, potentially shrink the gradient primitive to only
409/// the useful part and if needed insert solid color primitives around the gradient where
410/// parts of it have been removed.
411pub fn optimize_radial_gradient(
412    prim_rect: &mut LayoutRect,
413    stretch_size: &mut LayoutSize,
414    center: &mut LayoutPoint,
415    tile_spacing: &mut LayoutSize,
416    clip_rect: &LayoutRect,
417    radius: LayoutSize,
418    end_offset: f32,
419    extend_mode: ExtendMode,
420    stops: &[GradientStopKey],
421    solid_parts: &mut dyn FnMut(&LayoutRect, ColorU),
422) {
423    let offset = apply_gradient_local_clip(
424        prim_rect,
425        stretch_size,
426        tile_spacing,
427        clip_rect
428    );
429
430    *center += offset;
431
432    if extend_mode != ExtendMode::Clamp || stops.is_empty() {
433        return;
434    }
435
436    // Bounding box of the "interesting" part of the gradient.
437    let min = prim_rect.min + center.to_vector() - radius.to_vector() * end_offset;
438    let max = prim_rect.min + center.to_vector() + radius.to_vector() * end_offset;
439
440    // The (non-repeated) gradient primitive rect.
441    let gradient_rect = LayoutRect::from_origin_and_size(
442        prim_rect.min,
443        *stretch_size,
444    );
445
446    // How much internal margin between the primitive bounds and the gradient's
447    // bounding rect (areas that are a constant color).
448    let mut l = (min.x - gradient_rect.min.x).max(0.0).floor();
449    let mut t = (min.y - gradient_rect.min.y).max(0.0).floor();
450    let mut r = (gradient_rect.max.x - max.x).max(0.0).floor();
451    let mut b = (gradient_rect.max.y - max.y).max(0.0).floor();
452
453    let is_tiled = prim_rect.width() > stretch_size.width + tile_spacing.width
454        || prim_rect.height() > stretch_size.height + tile_spacing.height;
455
456    let bg_color = stops.last().unwrap().color;
457
458    if bg_color.a != 0 && is_tiled {
459        // If the primitive has repetitions, it's not enough to insert solid rects around it,
460        // so bail out.
461        return;
462    }
463
464    // If the background is fully transparent, shrinking the primitive bounds as much as possible
465    // is always a win. If the background is not transparent, we have to insert solid rectangles
466    // around the shrunk parts.
467    // If the background is transparent and the primitive is tiled, the optimization may introduce
468    // tile spacing which forces the tiling to be manually decomposed.
469    // Either way, don't bother optimizing unless it saves a significant amount of pixels.
470    if bg_color.a != 0 || (is_tiled && tile_spacing.is_empty()) {
471        let threshold = 128.0;
472        if l < threshold { l = 0.0 }
473        if t < threshold { t = 0.0 }
474        if r < threshold { r = 0.0 }
475        if b < threshold { b = 0.0 }
476    }
477
478    if l + t + r + b == 0.0 {
479        // No adjustment to make;
480        return;
481    }
482
483    // Insert solid rectangles around the gradient, in the places where the primitive will be
484    // shrunk.
485    if bg_color.a != 0 {
486        if l != 0.0 && t != 0.0 {
487            let solid_rect = LayoutRect::from_origin_and_size(
488                gradient_rect.min,
489                size2(l, t),
490            );
491            solid_parts(&solid_rect, bg_color);
492        }
493
494        if l != 0.0 && b != 0.0 {
495            let solid_rect = LayoutRect::from_origin_and_size(
496                gradient_rect.bottom_left() - vec2(0.0, b),
497                size2(l, b),
498            );
499            solid_parts(&solid_rect, bg_color);
500        }
501
502        if t != 0.0 && r != 0.0 {
503            let solid_rect = LayoutRect::from_origin_and_size(
504                gradient_rect.top_right() - vec2(r, 0.0),
505                size2(r, t),
506            );
507            solid_parts(&solid_rect, bg_color);
508        }
509
510        if r != 0.0 && b != 0.0 {
511            let solid_rect = LayoutRect::from_origin_and_size(
512                gradient_rect.bottom_right() - vec2(r, b),
513                size2(r, b),
514            );
515            solid_parts(&solid_rect, bg_color);
516        }
517
518        if l != 0.0 {
519            let solid_rect = LayoutRect::from_origin_and_size(
520                gradient_rect.min + vec2(0.0, t),
521                size2(l, gradient_rect.height() - t - b),
522            );
523            solid_parts(&solid_rect, bg_color);
524        }
525
526        if r != 0.0 {
527            let solid_rect = LayoutRect::from_origin_and_size(
528                gradient_rect.top_right() + vec2(-r, t),
529                size2(r, gradient_rect.height() - t - b),
530            );
531            solid_parts(&solid_rect, bg_color);
532        }
533
534        if t != 0.0 {
535            let solid_rect = LayoutRect::from_origin_and_size(
536                gradient_rect.min + vec2(l, 0.0),
537                size2(gradient_rect.width() - l - r, t),
538            );
539            solid_parts(&solid_rect, bg_color);
540        }
541
542        if b != 0.0 {
543            let solid_rect = LayoutRect::from_origin_and_size(
544                gradient_rect.bottom_left() + vec2(l, -b),
545                size2(gradient_rect.width() - l - r, b),
546            );
547            solid_parts(&solid_rect, bg_color);
548        }
549    }
550
551    // Shrink the gradient primitive.
552
553    prim_rect.min.x += l;
554    prim_rect.min.y += t;
555
556    stretch_size.width -= l + r;
557    stretch_size.height -= b + t;
558
559    center.x -= l;
560    center.y -= t;
561
562    tile_spacing.width += l + r;
563    tile_spacing.height += t + b;
564}
565
566pub fn radial_gradient_pattern(
567    center: DevicePoint,
568    scale: DeviceVector2D,
569    params: &RadialGradientParams,
570    extend_mode: ExtendMode,
571    stops: &[GradientStop],
572    gpu_buffer_builder: &mut GpuBufferBuilder
573) -> Pattern {
574    let mut writer = gpu_buffer_builder.f32.write_blocks(2);
575    writer.push_one([
576        center.x,
577        center.y,
578        scale.x,
579        scale.y,
580    ]);
581    writer.push_one([
582        params.start_radius,
583        params.end_radius,
584        params.ratio_xy,
585        if extend_mode == ExtendMode::Repeat { 1.0 } else { 0.0 }
586    ]);
587    let gradient_address = writer.finish();
588
589    let stops_address = GradientGpuBlockBuilder::build(
590        false,
591        &mut gpu_buffer_builder.f32,
592        &stops,
593    );
594
595    let is_opaque = stops.iter().all(|stop| stop.color.a >= 1.0);
596
597    Pattern {
598        kind: PatternKind::RadialGradient,
599        shader_input: PatternShaderInput(
600            gradient_address.as_int(),
601            stops_address.as_int(),
602        ),
603        texture_input: PatternTextureInput::default(),
604        base_color: ColorF::WHITE,
605        is_opaque,
606    }
607}