Rasterize direct gradients at native scale

This commit is contained in:
SGavrl committed 2026-08-03 20:20:52 +00:00
1 parent c8a5477296
commit cfac7f2003
1 file changed
+181 -11
+181 -11
View File
@@ -1870,16 +1870,53 @@ pub fn paint_prepared_region_with_scroll(
/// intermediate surfaces retain the proven CSS-raster + resample path until /// intermediate surfaces retain the proven CSS-raster + resample path until
/// those layers can carry an explicit source/device transform. /// those layers can carry an explicit source/device transform.
fn native_raster_scale_supported(tree: &DomTree, laid: &crate::DomLayout) -> bool { fn native_raster_scale_supported(tree: &DomTree, laid: &crate::DomLayout) -> bool {
fn direct_gradient_geometry_supported(style: &crate::LayoutStyle) -> bool {
if style.background_size.is_some()
|| style.background_size_expression.is_some()
|| style.background_size_fit.is_some()
{
return false;
}
// The direct vector painter is scale-safe only when no logical-pixel
// tile surface is needed. Equal positioning and clipping boxes make
// the implicit gradient tile exactly the painted path; position and
// repeat then have no leftover space on which to operate.
let geometry = background_geometry(
&crate::Rect {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
},
style,
);
(geometry.origin_rect.x - geometry.clip_rect.x).abs() <= 0.01
&& (geometry.origin_rect.y - geometry.clip_rect.y).abs() <= 0.01
&& (geometry.origin_rect.width - geometry.clip_rect.width).abs() <= 0.01
&& (geometry.origin_rect.height - geometry.clip_rect.height).abs() <= 0.01
}
fn style_supported(style: &crate::LayoutStyle) -> bool { fn style_supported(style: &crate::LayoutStyle) -> bool {
let has_gradient = style.background_gradient.is_some()
|| style.background_radial_gradient.is_some()
|| !style.background_gradient_layers.is_empty();
let gradients_supported = style.background_conic_gradient.is_none()
&& style
.background_gradient_layers
.iter()
.all(|layer| match layer {
crate::BackgroundGradientLayer::Linear { repeating, .. } => !repeating,
crate::BackgroundGradientLayer::Radial { .. } => true,
crate::BackgroundGradientLayer::Conic { .. } => false,
})
&& (!has_gradient || direct_gradient_geometry_supported(style));
let simple = !has_authored_transform(style) let simple = !has_authored_transform(style)
&& style.opacity.is_none_or(|opacity| opacity >= 1.0) && style.opacity.is_none_or(|opacity| opacity >= 1.0)
&& style.clip_path.is_none() && style.clip_path.is_none()
&& style.background_image.is_none() && style.background_image.is_none()
&& style.mask_image.is_none() && style.mask_image.is_none()
&& style.background_gradient.is_none() && gradients_supported
&& style.background_radial_gradient.is_none()
&& style.background_conic_gradient.is_none()
&& style.background_gradient_layers.is_empty()
&& !style.background_clip_text && !style.background_clip_text
&& style.box_shadow.is_none() && style.box_shadow.is_none()
&& style.content_image.is_none() && style.content_image.is_none()
@@ -2641,6 +2678,7 @@ fn paint_laid_dom_scrolled(
root_font_size, root_font_size,
viewport, viewport,
background_mask.as_ref(), background_mask.as_ref(),
raster_scale,
); );
} }
} else { } else {
@@ -2653,6 +2691,7 @@ fn paint_laid_dom_scrolled(
*center, *center,
stops, stops,
background_mask.as_ref(), background_mask.as_ref(),
raster_scale,
); );
} }
} }
@@ -2677,6 +2716,7 @@ fn paint_laid_dom_scrolled(
*angle, *angle,
stops, stops,
background_mask.as_ref(), background_mask.as_ref(),
raster_scale,
); );
} }
} }
@@ -3302,6 +3342,7 @@ fn paint_inline_fragment_decorations(
root_font_size, root_font_size,
viewport, viewport,
background_mask.as_ref(), background_mask.as_ref(),
raster_scale,
); );
} }
} else { } else {
@@ -3316,6 +3357,7 @@ fn paint_inline_fragment_decorations(
*center, *center,
stops, stops,
background_mask.as_ref(), background_mask.as_ref(),
raster_scale,
); );
} }
if let Some((angle, center, stops)) = &fragment_style.background_conic_gradient { if let Some((angle, center, stops)) = &fragment_style.background_conic_gradient {
@@ -3341,6 +3383,7 @@ fn paint_inline_fragment_decorations(
*angle, *angle,
stops, stops,
background_mask.as_ref(), background_mask.as_ref(),
raster_scale,
); );
} }
} }
@@ -5238,6 +5281,7 @@ fn paint_linear_gradient(
angle: f32, angle: f32,
stops: &[([u8; 4], Option<f32>)], stops: &[([u8; 4], Option<f32>)],
clip: Option<&tiny_skia::Mask>, clip: Option<&tiny_skia::Mask>,
raster_scale: f32,
) { ) {
if stops.len() < 2 { if stops.len() < 2 {
return; return;
@@ -5270,7 +5314,13 @@ fn paint_linear_gradient(
let mut paint = Paint::default(); let mut paint = Paint::default();
paint.shader = shader; paint.shader = shader;
paint.anti_alias = true; paint.anti_alias = true;
pixmap.fill_path(path, &paint, FillRule::Winding, Transform::identity(), clip); pixmap.fill_path(
path,
&paint,
FillRule::Winding,
raster_transform(raster_scale),
clip,
);
} }
} }
@@ -5286,6 +5336,7 @@ fn paint_linear_gradient_layer(
rem: f32, rem: f32,
viewport: (f32, f32), viewport: (f32, f32),
clip: Option<&tiny_skia::Mask>, clip: Option<&tiny_skia::Mask>,
raster_scale: f32,
) { ) {
if stops.len() < 2 { if stops.len() < 2 {
return; return;
@@ -5351,7 +5402,13 @@ fn paint_linear_gradient_layer(
let color = gradient_stop_color(stops, last_index); let color = gradient_stop_color(stops, last_index);
let mut paint = Paint::default(); let mut paint = Paint::default();
paint.set_color(Color::from_rgba8(color[0], color[1], color[2], color[3])); paint.set_color(Color::from_rgba8(color[0], color[1], color[2], color[3]));
pixmap.fill_path(path, &paint, FillRule::Winding, Transform::identity(), clip); pixmap.fill_path(
path,
&paint,
FillRule::Winding,
raster_transform(raster_scale),
clip,
);
return; return;
} }
let (start, end, spread, normalize) = if repeating { let (start, end, spread, normalize) = if repeating {
@@ -5393,7 +5450,13 @@ fn paint_linear_gradient_layer(
let mut paint = Paint::default(); let mut paint = Paint::default();
paint.shader = shader; paint.shader = shader;
paint.anti_alias = true; paint.anti_alias = true;
pixmap.fill_path(path, &paint, FillRule::Winding, Transform::identity(), clip); pixmap.fill_path(
path,
&paint,
FillRule::Winding,
raster_transform(raster_scale),
clip,
);
} }
} }
@@ -5494,6 +5557,7 @@ fn paint_radial_gradient(
center: (f32, f32), center: (f32, f32),
stops: &[([u8; 4], Option<f32>)], stops: &[([u8; 4], Option<f32>)],
clip: Option<&tiny_skia::Mask>, clip: Option<&tiny_skia::Mask>,
raster_scale: f32,
) { ) {
if stops.len() < 2 { if stops.len() < 2 {
return; return;
@@ -5532,7 +5596,13 @@ fn paint_radial_gradient(
let mut paint = Paint::default(); let mut paint = Paint::default();
paint.shader = shader; paint.shader = shader;
paint.anti_alias = true; paint.anti_alias = true;
pixmap.fill_path(path, &paint, FillRule::Winding, Transform::identity(), clip); pixmap.fill_path(
path,
&paint,
FillRule::Winding,
raster_transform(raster_scale),
clip,
);
} }
} }
@@ -5546,6 +5616,7 @@ fn paint_background_gradient_layers(
root_font_size: f32, root_font_size: f32,
viewport: (f32, f32), viewport: (f32, f32),
clip: Option<&tiny_skia::Mask>, clip: Option<&tiny_skia::Mask>,
raster_scale: f32,
) { ) {
let layers = &style.background_gradient_layers; let layers = &style.background_gradient_layers;
let em = style.font_size.unwrap_or(16.0); let em = style.font_size.unwrap_or(16.0);
@@ -5562,6 +5633,7 @@ fn paint_background_gradient_layers(
// coordinates and would also ignore `background-repeat: no-repeat`. // coordinates and would also ignore `background-repeat: no-repeat`.
|| clip_differs_from_origin; || clip_differs_from_origin;
if needs_tile && tile_size.0 > 0.0 && tile_size.1 > 0.0 { if needs_tile && tile_size.0 > 0.0 && tile_size.1 > 0.0 {
debug_assert_eq!(raster_scale, 1.0);
let width = tile_size.0.ceil().clamp(1.0, 4096.0) as u32; let width = tile_size.0.ceil().clamp(1.0, 4096.0) as u32;
let height = tile_size.1.ceil().clamp(1.0, 4096.0) as u32; let height = tile_size.1.ceil().clamp(1.0, 4096.0) as u32;
if let Some(mut tile) = Pixmap::new(width, height) { if let Some(mut tile) = Pixmap::new(width, height) {
@@ -5589,6 +5661,7 @@ fn paint_background_gradient_layers(
root_font_size, root_font_size,
viewport, viewport,
None, None,
1.0,
); );
let tile_x = origin_rect.x let tile_x = origin_rect.x
+ style + style
@@ -5682,6 +5755,7 @@ fn paint_background_gradient_layers(
root_font_size, root_font_size,
viewport, viewport,
clip, clip,
raster_scale,
); );
} }
@@ -5696,6 +5770,7 @@ fn paint_gradient_layer_stack(
root_font_size: f32, root_font_size: f32,
viewport: (f32, f32), viewport: (f32, f32),
clip: Option<&tiny_skia::Mask>, clip: Option<&tiny_skia::Mask>,
raster_scale: f32,
) { ) {
// CSS lists the topmost background first. Paint back-to-front so every // CSS lists the topmost background first. Paint back-to-front so every
// translucent layer composites over the layers authored after it. // translucent layer composites over the layers authored after it.
@@ -5719,10 +5794,19 @@ fn paint_gradient_layer_stack(
root_font_size, root_font_size,
viewport, viewport,
clip, clip,
raster_scale,
); );
} }
crate::BackgroundGradientLayer::Radial { center, stops } => { crate::BackgroundGradientLayer::Radial { center, stops } => {
paint_radial_gradient(pixmap, path, sampling_rect, *center, stops, clip); paint_radial_gradient(
pixmap,
path,
sampling_rect,
*center,
stops,
clip,
raster_scale,
);
} }
crate::BackgroundGradientLayer::Conic { crate::BackgroundGradientLayer::Conic {
angle, angle,
@@ -6231,6 +6315,7 @@ fn paint_in_flow_generated_box(
root_font_size, root_font_size,
viewport, viewport,
background_mask.as_ref(), background_mask.as_ref(),
raster_scale,
); );
} }
} else { } else {
@@ -6243,6 +6328,7 @@ fn paint_in_flow_generated_box(
*center, *center,
stops, stops,
background_mask.as_ref(), background_mask.as_ref(),
raster_scale,
); );
} }
} }
@@ -6267,6 +6353,7 @@ fn paint_in_flow_generated_box(
*angle, *angle,
stops, stops,
background_mask.as_ref(), background_mask.as_ref(),
raster_scale,
); );
} }
} }
@@ -6452,6 +6539,7 @@ fn paint_positioned_pseudo(
root_font_size, root_font_size,
viewport, viewport,
background_mask.as_ref(), background_mask.as_ref(),
raster_scale,
); );
} }
} else { } else {
@@ -6464,6 +6552,7 @@ fn paint_positioned_pseudo(
*center, *center,
stops, stops,
background_mask.as_ref(), background_mask.as_ref(),
raster_scale,
); );
} }
} }
@@ -6488,6 +6577,7 @@ fn paint_positioned_pseudo(
*angle, *angle,
stops, stops,
background_mask.as_ref(), background_mask.as_ref(),
raster_scale,
); );
} }
} }
@@ -11646,17 +11736,97 @@ mod tests {
assert!(dark_text_pixels > 10, "native 2x text must remain painted"); assert!(dark_text_pixels > 10, "native 2x text must remain painted");
} }
#[test]
fn direct_vector_gradients_rasterize_natively_at_two_x() {
let tree = parse_html(
r#"<html style="margin:0"><body style="margin:0">
<div style="width:20px;height:16px;background:linear-gradient(90deg,#f00 0%,#00f 100%)"></div>
<div style="width:20px;height:16px;background:radial-gradient(circle at 50% 50%,#fff 0%,#000 100%)"></div>
</body></html>"#,
);
let mut resources = RenderResourceCache::with_loader(|_url: &str| None);
let mut prepared =
prepare_dom(&tree, (30.0, 40.0), None, &mut resources).expect("prepared render");
assert!(
native_raster_scale_supported(&tree, &prepared.layout),
"direct non-repeating linear/radial gradients should use device-scale paint"
);
let scroll = prepared.resolve_scroll_state(&tree, (0.0, 0.0), &HashMap::new());
let one_x = paint_prepared_region_with_scroll(
&tree,
&mut prepared,
&mut resources,
&scroll,
CaptureRegion::new(0.0, 0.0, 30.0, 40.0, 1.0),
)
.unwrap();
let two_x = paint_prepared_region_with_scroll(
&tree,
&mut prepared,
&mut resources,
&scroll,
CaptureRegion::new(0.0, 0.0, 30.0, 40.0, 2.0),
)
.unwrap();
assert_eq!((one_x.width(), one_x.height()), (30, 40));
assert_eq!((two_x.width(), two_x.height()), (60, 80));
let linear_left = two_x.pixel(1, 8).unwrap();
let linear_right = two_x.pixel(38, 8).unwrap();
assert!(
linear_left.red() > 220 && linear_left.blue() < 40,
"linear gradient start must remain red at 2x: {linear_left:?}"
);
assert!(
linear_right.blue() > 220 && linear_right.red() < 40,
"linear gradient end must remain blue at 2x: {linear_right:?}"
);
let radial_center = two_x.pixel(20, 48).unwrap();
let radial_edge = two_x.pixel(1, 48).unwrap();
assert!(
radial_center.red() > 230,
"radial center must remain light at 2x: {radial_center:?}"
);
assert!(
radial_edge.red() < 100,
"radial edge must remain dark at 2x: {radial_edge:?}"
);
let source =
image::RgbaImage::from_raw(one_x.width(), one_x.height(), one_x.data().to_vec())
.unwrap();
let post_scaled = image::imageops::resize(
&source,
two_x.width(),
two_x.height(),
image::imageops::FilterType::Lanczos3,
);
let differing = two_x
.data()
.iter()
.zip(post_scaled.as_raw())
.filter(|(native, post)| native != post)
.count();
assert!(
differing > 200,
"2x vector gradient samples must be rerasterized, not resize-equivalent ({differing} differing channels)"
);
}
#[test] #[test]
fn effectful_region_capture_keeps_the_proven_bounded_resample_fallback() { fn effectful_region_capture_keeps_the_proven_bounded_resample_fallback() {
let tree = parse_html( let tree = parse_html(
r#"<html style="margin:0"><body style="margin:0"> r#"<html style="margin:0"><body style="margin:0">
<div style="width:20px;height:20px;background:linear-gradient(red,blue)"></div> <div style="width:20px;height:20px;background:conic-gradient(red,blue,red)"></div>
</body></html>"#, </body></html>"#,
); );
let mut resources = RenderResourceCache::with_loader(|_url: &str| None); let mut resources = RenderResourceCache::with_loader(|_url: &str| None);
let mut prepared = let mut prepared =
prepare_dom(&tree, (30.0, 30.0), None, &mut resources).expect("prepared render"); prepare_dom(&tree, (30.0, 30.0), None, &mut resources).expect("prepared render");
assert!(!native_raster_scale_supported(&tree, &prepared.layout)); assert!(
!native_raster_scale_supported(&tree, &prepared.layout),
"sampled conic gradients must retain the logical-pixel fallback"
);
let scroll = prepared.resolve_scroll_state(&tree, (0.0, 0.0), &HashMap::new()); let scroll = prepared.resolve_scroll_state(&tree, (0.0, 0.0), &HashMap::new());
let one_x = paint_prepared_region_with_scroll( let one_x = paint_prepared_region_with_scroll(
&tree, &tree,