commit 7182a1824edfb2471e0167378aabc4673f3542c1
parent b3959a19bfa025823ed9151b1938a9cef0478a5d
Author: Nicolas Silva <nical@fastmail.com>
Date: Mon, 20 Oct 2025 07:03:53 +0000
Bug 1994764 - Add support for tile repetitions with quad gradients. r=gfx-reviewers,lsalzman
This implements tile repetitions by decomposing the primitive on the CPU. It's not ideal when there are a lot of repetitions but the non-quad code paths for gradients also use that approach so it's probably fine in practice.
Differential Revision: https://phabricator.services.mozilla.com/D268909
Diffstat:
2 files changed, 219 insertions(+), 51 deletions(-)
diff --git a/gfx/wr/webrender/src/prepare.rs b/gfx/wr/webrender/src/prepare.rs
@@ -241,10 +241,6 @@ fn prepare_prim_for_render(
let prim_instance = &mut prim_instances[prim_instance_index];
if !is_passthrough {
- fn may_need_repetition(stretch_size: LayoutSize, prim_rect: LayoutRect) -> bool {
- stretch_size.width < prim_rect.width() ||
- stretch_size.height < prim_rect.height()
- }
// Bug 1887841: At the moment the quad shader does not support repetitions.
// Bug 1888349: Some primitives have brush segments that aren't handled by
// the quad infrastructure yet.
@@ -252,20 +248,17 @@ fn prepare_prim_for_render(
PrimitiveInstanceKind::Rectangle { .. } => false,
PrimitiveInstanceKind::LinearGradient { data_handle, .. } => {
let prim_data = &data_stores.linear_grad[*data_handle];
- !prim_data.brush_segments.is_empty() ||
- may_need_repetition(prim_data.stretch_size, prim_data.common.prim_rect)
+ !prim_data.brush_segments.is_empty()
|| !frame_context.fb_config.precise_linear_gradients
}
PrimitiveInstanceKind::RadialGradient { data_handle, .. } => {
let prim_data = &data_stores.radial_grad[*data_handle];
- !prim_data.brush_segments.is_empty() ||
- may_need_repetition(prim_data.stretch_size, prim_data.common.prim_rect)
+ !prim_data.brush_segments.is_empty()
}
// TODO(bug 1899546) Enable quad conic gradients with SWGL.
PrimitiveInstanceKind::ConicGradient { data_handle, .. } if !frame_context.fb_config.is_software => {
let prim_data = &data_stores.conic_grad[*data_handle];
- !prim_data.brush_segments.is_empty() ||
- may_need_repetition(prim_data.stretch_size, prim_data.common.prim_rect)
+ !prim_data.brush_segments.is_empty()
}
_ => true,
};
@@ -765,11 +758,12 @@ fn prepare_interned_prim_for_render(
PrimitiveInstanceKind::LinearGradient { data_handle, ref mut visible_tiles_range, use_legacy_path: cached, .. } => {
profile_scope!("LinearGradient");
let prim_data = &mut data_stores.linear_grad[*data_handle];
-
if !*cached {
- quad::prepare_quad(
+ quad::prepare_repeatable_quad(
prim_data,
&prim_data.common.prim_rect,
+ prim_data.stretch_size,
+ prim_data.tile_spacing,
prim_instance_index,
prim_spatial_node_index,
&prim_instance.vis.clip_chain,
@@ -876,14 +870,16 @@ fn prepare_interned_prim_for_render(
}
}
}
- PrimitiveInstanceKind::RadialGradient { data_handle, ref mut visible_tiles_range, use_legacy_path: cached, .. } => {
+ PrimitiveInstanceKind::RadialGradient { data_handle, ref mut visible_tiles_range, use_legacy_path, .. } => {
profile_scope!("RadialGradient");
let prim_data = &mut data_stores.radial_grad[*data_handle];
- if !*cached {
- quad::prepare_quad(
+ if !*use_legacy_path {
+ quad::prepare_repeatable_quad(
prim_data,
&prim_data.common.prim_rect,
+ prim_data.stretch_size,
+ prim_data.tile_spacing,
prim_instance_index,
prim_spatial_node_index,
&prim_instance.vis.clip_chain,
@@ -927,14 +923,16 @@ fn prepare_interned_prim_for_render(
}
}
}
- PrimitiveInstanceKind::ConicGradient { data_handle, ref mut visible_tiles_range, use_legacy_path: cached, .. } => {
+ PrimitiveInstanceKind::ConicGradient { data_handle, ref mut visible_tiles_range, use_legacy_path, .. } => {
profile_scope!("ConicGradient");
let prim_data = &mut data_stores.conic_grad[*data_handle];
- if !*cached {
- quad::prepare_quad(
+ if !*use_legacy_path {
+ quad::prepare_repeatable_quad(
prim_data,
&prim_data.common.prim_rect,
+ prim_data.stretch_size,
+ prim_data.tile_spacing,
prim_instance_index,
prim_spatial_node_index,
&prim_instance.vis.clip_chain,
diff --git a/gfx/wr/webrender/src/quad.rs b/gfx/wr/webrender/src/quad.rs
@@ -19,9 +19,10 @@ use crate::render_task_graph::{RenderTaskGraph, RenderTaskGraphBuilder, RenderTa
use crate::renderer::{BlendMode, GpuBufferAddress, GpuBufferBuilder, GpuBufferBuilderF};
use crate::segment::EdgeAaSegmentMask;
use crate::space::SpaceMapper;
-use crate::spatial_tree::{SpatialNodeIndex, SpatialTree};
+use crate::spatial_tree::{CoordinateSpaceMapping, SpatialNodeIndex, SpatialTree};
use crate::surface::SurfaceBuilder;
use crate::util::{extract_inner_rect_k, MaxRect, ScaleOffset};
+use crate::visibility::compute_conservative_visible_rect;
const MIN_AA_SEGMENTS_SIZE: f32 = 4.0;
const MIN_QUAD_SPLIT_SIZE: f32 = 256.0;
@@ -76,50 +77,220 @@ pub fn prepare_quad(
pic_state: &mut PictureState,
scratch: &mut PrimitiveScratchBuffer,
) {
- let map_prim_to_raster = frame_context.spatial_tree.get_relative_transform(
+ let pattern_ctx = PatternBuilderContext {
+ scene_properties: frame_context.scene_properties,
+ spatial_tree: frame_context.spatial_tree,
+ fb_config: frame_context.fb_config,
+ };
+
+ let shared_pattern = if pattern_builder.use_shared_pattern() {
+ Some(pattern_builder.build(
+ None,
+ &pattern_ctx,
+ &mut PatternBuilderState {
+ frame_gpu_data: frame_state.frame_gpu_data,
+ rg_builder: frame_state.rg_builder,
+ clip_store: frame_state.clip_store,
+ },
+ ))
+ } else {
+ None
+ };
+
+ // TODO: It would be worth hoisting this out of prepare_quad and
+ // prepare_repeatable_quad.
+ let map_prim_to_raster = pattern_ctx.spatial_tree.get_relative_transform(
prim_spatial_node_index,
pic_context.raster_spatial_node_index,
);
- let ctx = PatternBuilderContext {
+ let can_use_nine_patch = map_prim_to_raster.is_2d_scale_translation()
+ && pattern_builder.can_use_nine_patch();
+
+ let strategy = get_prim_render_strategy(
+ prim_spatial_node_index,
+ clip_chain,
+ frame_state.clip_store,
+ interned_clips,
+ can_use_nine_patch,
+ pattern_ctx.spatial_tree,
+ );
+
+ prepare_quad_impl(
+ strategy,
+ pattern_builder,
+ shared_pattern.as_ref(),
+ local_rect,
+ prim_instance_index,
+ prim_spatial_node_index,
+ clip_chain,
+ device_pixel_scale,
+
+ &map_prim_to_raster,
+ &pattern_ctx,
+ pic_context,
+ targets,
+ interned_clips,
+
+ frame_state,
+ pic_state,
+ scratch,
+ )
+}
+
+pub fn prepare_repeatable_quad(
+ pattern_builder: &dyn PatternBuilder,
+ local_rect: &LayoutRect,
+ stretch_size: LayoutSize,
+ tile_spacing: LayoutSize,
+ prim_instance_index: PrimitiveInstanceIndex,
+ prim_spatial_node_index: SpatialNodeIndex,
+ clip_chain: &ClipChainInstance,
+ device_pixel_scale: DevicePixelScale,
+
+ frame_context: &FrameBuildingContext,
+ pic_context: &PictureContext,
+ targets: &[CommandBufferIndex],
+ interned_clips: &DataStore<ClipIntern>,
+
+ frame_state: &mut FrameBuildingState,
+ pic_state: &mut PictureState,
+ scratch: &mut PrimitiveScratchBuffer,
+) {
+ let pattern_ctx = PatternBuilderContext {
scene_properties: frame_context.scene_properties,
spatial_tree: frame_context.spatial_tree,
fb_config: frame_context.fb_config,
};
- let mut state = PatternBuilderState {
- frame_gpu_data: frame_state.frame_gpu_data,
- rg_builder: frame_state.rg_builder,
- clip_store: frame_state.clip_store,
- };
-
let shared_pattern = if pattern_builder.use_shared_pattern() {
Some(pattern_builder.build(
None,
- &ctx,
- &mut state,
+ &pattern_ctx,
+ &mut PatternBuilderState {
+ frame_gpu_data: frame_state.frame_gpu_data,
+ rg_builder: frame_state.rg_builder,
+ clip_store: frame_state.clip_store,
+ },
))
} else {
None
};
- let prim_is_2d_scale_translation = map_prim_to_raster.is_2d_scale_translation();
- let prim_is_2d_axis_aligned = map_prim_to_raster.is_2d_axis_aligned();
+ let map_prim_to_raster = pattern_ctx.spatial_tree.get_relative_transform(
+ prim_spatial_node_index,
+ pic_context.raster_spatial_node_index,
+ );
- // TODO(gw): Can't support 9-patch for box-shadows for now as should_create_task
- // assumes pattern is solid. This is a temporary hack until as once that's
- // fixed we can select 9-patch for box-shadows
- let can_use_nine_patch = prim_is_2d_scale_translation && pattern_builder.can_use_nine_patch();
+ let can_use_nine_patch = map_prim_to_raster.is_2d_scale_translation()
+ && pattern_builder.can_use_nine_patch();
+ // This could move back into preapre_quad_impl if it took the tile's
+ // coverage rect into account rather than the whole primitive's, but
+ // for now it does the latter so we might as well not do the work
+ // multiple times.
let strategy = get_prim_render_strategy(
prim_spatial_node_index,
clip_chain,
- state.clip_store,
+ frame_state.clip_store,
interned_clips,
can_use_nine_patch,
+ pattern_ctx.spatial_tree,
+ );
+
+ let needs_repetition = stretch_size.width < local_rect.width()
+ || stretch_size.height < local_rect.height();
+
+ if !needs_repetition {
+ // Most common path.
+ prepare_quad_impl(
+ strategy,
+ pattern_builder,
+ shared_pattern.as_ref(),
+ local_rect,
+ prim_instance_index,
+ prim_spatial_node_index,
+ clip_chain,
+ device_pixel_scale,
+ &map_prim_to_raster,
+ &pattern_ctx,
+ pic_context,
+ targets,
+ interned_clips,
+ frame_state,
+ pic_state,
+ scratch,
+ );
+
+ return;
+ }
+
+ // TODO: In some cases it would be a lot more efficient to bake the repeated
+ // pattern into a texture and use a repeating image shader instead of duplicating
+ // the primitive, especially with a high number of repetitions.
+
+ let visible_rect = compute_conservative_visible_rect(
+ clip_chain,
+ frame_state.current_dirty_region().combined,
+ frame_state.current_dirty_region().visibility_spatial_node,
+ prim_spatial_node_index,
frame_context.spatial_tree,
);
+ let stride = stretch_size + tile_spacing;
+ let repetitions = crate::image_tiling::repetitions(&local_rect, &visible_rect, stride);
+ for tile in repetitions {
+ let tile_rect = LayoutRect::from_origin_and_size(tile.origin, stretch_size);
+ prepare_quad_impl(
+ strategy,
+ pattern_builder,
+ shared_pattern.as_ref(),
+ &tile_rect,
+ prim_instance_index,
+ prim_spatial_node_index,
+ clip_chain,
+ device_pixel_scale,
+ &map_prim_to_raster,
+ &pattern_ctx,
+ pic_context,
+ targets,
+ interned_clips,
+ frame_state,
+ pic_state,
+ scratch,
+ );
+ }
+}
+
+fn prepare_quad_impl(
+ strategy: QuadRenderStrategy,
+ pattern_builder: &dyn PatternBuilder,
+ shared_pattern: Option<&Pattern>,
+ local_rect: &LayoutRect,
+ prim_instance_index: PrimitiveInstanceIndex,
+ prim_spatial_node_index: SpatialNodeIndex,
+ clip_chain: &ClipChainInstance,
+ device_pixel_scale: DevicePixelScale,
+
+ map_prim_to_raster: &CoordinateSpaceMapping<LayoutPixel, LayoutPixel>,
+ ctx: &PatternBuilderContext,
+ pic_context: &PictureContext,
+ targets: &[CommandBufferIndex],
+ interned_clips: &DataStore<ClipIntern>,
+
+ frame_state: &mut FrameBuildingState,
+ pic_state: &mut PictureState,
+ scratch: &mut PrimitiveScratchBuffer,
+) {
+ let mut state = PatternBuilderState {
+ frame_gpu_data: frame_state.frame_gpu_data,
+ rg_builder: frame_state.rg_builder,
+ clip_store: frame_state.clip_store,
+ };
+
+ let prim_is_2d_scale_translation = map_prim_to_raster.is_2d_scale_translation();
+ let prim_is_2d_axis_aligned = map_prim_to_raster.is_2d_axis_aligned();
+
let mut quad_flags = QuadFlags::empty();
// Only use AA edge instances if the primitive is large enough to require it
@@ -146,11 +317,11 @@ pub fn prepare_quad(
let transform_id = frame_state.transforms.get_id(
prim_spatial_node_index,
pic_context.raster_spatial_node_index,
- frame_context.spatial_tree,
+ ctx.spatial_tree,
);
if let QuadRenderStrategy::Direct = strategy {
- let pattern = shared_pattern.unwrap_or_else(|| {
+ let pattern = shared_pattern.cloned().unwrap_or_else(|| {
pattern_builder.build(
None,
&ctx,
@@ -202,7 +373,7 @@ pub fn prepare_quad(
let surface = &mut frame_state.surfaces[pic_context.surface_index.0];
let Some(clipped_surface_rect) = surface.get_surface_rect(
- &clip_chain.pic_coverage_rect, frame_context.spatial_tree
+ &clip_chain.pic_coverage_rect, ctx.spatial_tree
) else {
return;
};
@@ -210,7 +381,7 @@ pub fn prepare_quad(
match strategy {
QuadRenderStrategy::Direct => {}
QuadRenderStrategy::Indirect => {
- let pattern = shared_pattern.unwrap_or_else(|| {
+ let pattern = shared_pattern.cloned().unwrap_or_else(|| {
pattern_builder.build(
None,
&ctx,
@@ -272,18 +443,18 @@ pub fn prepare_quad(
// - in layout space for the render task,
// - in device space for the instances that draw into the destination picture.
let clip_coverage_rect = surface
- .map_to_device_rect(&clip_chain.pic_coverage_rect, frame_context.spatial_tree);
+ .map_to_device_rect(&clip_chain.pic_coverage_rect, ctx.spatial_tree);
let clipped_surface_rect = clipped_surface_rect.to_f32();
surface.map_local_to_picture.set_target_spatial_node(
prim_spatial_node_index,
- frame_context.spatial_tree,
+ ctx.spatial_tree,
);
let Some(pic_rect) = surface.map_local_to_picture.map(local_rect) else { return };
let unclipped_surface_rect = surface.map_to_device_rect(
- &pic_rect, frame_context.spatial_tree
+ &pic_rect, ctx.spatial_tree
).round_out();
// Set up the tile classifier for the params of this quad
@@ -303,7 +474,7 @@ pub fn prepare_quad(
prim_spatial_node_index,
clip_node.item.spatial_node_index,
pic_context.visibility_spatial_node_index,
- frame_context.spatial_tree,
+ ctx.spatial_tree,
);
// For now, we only handle axis-aligned mappings
@@ -464,7 +635,7 @@ pub fn prepare_quad(
if is_direct {
scratch.quad_direct_segments.push(QuadSegment { rect: rect.cast_unit(), task_id: RenderTaskId::INVALID });
} else {
- let pattern = match shared_pattern {
+ let pattern = match shared_pattern.cloned() {
Some(ref shared_pattern) => shared_pattern.clone(),
None => {
pattern_builder.build(
@@ -519,7 +690,7 @@ pub fn prepare_quad(
let device_prim_rect: DeviceRect = local_to_device.map_rect(&local_rect);
let pattern = match shared_pattern {
- Some(ref shared_pattern) => shared_pattern.clone(),
+ Some(shared_pattern) => shared_pattern.clone(),
None => {
pattern_builder.build(
Some(device_prim_rect),
@@ -561,7 +732,7 @@ pub fn prepare_quad(
// - in layout space for the render task,
// - in device space for the instances that draw into the destination picture.
let clip_coverage_rect = surface
- .map_to_device_rect(&clip_chain.pic_coverage_rect, frame_context.spatial_tree);
+ .map_to_device_rect(&clip_chain.pic_coverage_rect, ctx.spatial_tree);
let local_to_device = map_prim_to_raster.as_2d_scale_offset()
.expect("bug: nine-patch segments should be axis-aligned only")
@@ -584,12 +755,12 @@ pub fn prepare_quad(
let surface_rect_0 = surface.map_to_device_rect(
&pic_corner_0,
- frame_context.spatial_tree,
+ ctx.spatial_tree,
).round_out().to_i32();
let surface_rect_1 = surface.map_to_device_rect(
&pic_corner_1,
- frame_context.spatial_tree,
+ ctx.spatial_tree,
).round_out().to_i32();
let p0 = surface_rect_0.min;
@@ -650,7 +821,6 @@ pub fn prepare_quad(
if should_create_task(mode, x, y) {
let pattern = shared_pattern
- .as_ref()
.expect("bug: nine-patch expects shared pattern, for now");
if pattern.is_opaque {
@@ -668,7 +838,7 @@ pub fn prepare_quad(
);
let task_id = add_render_task_with_mask(
- &pattern,
+ pattern,
device_rect.size(),
device_rect.min.to_f32(),
clip_chain.clips_range,