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