Skip to content

Commit b300160

Browse files
shader: implement bicubic image sampling
Adapt the sparse-strips bicubic kernel to main Vello's single 2D atlas and premultiplied image path, and add a dedicated snapshot scene that distinguishes Low, Medium, and High image sampling. This addresses part of #1545.
1 parent 3fc4bb6 commit b300160

4 files changed

Lines changed: 186 additions & 1 deletion

File tree

examples/scenes/src/test_scenes.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export_scenes!(
110110
fn many_draw_objects(many_draw_objects)
111111
fn blurred_rounded_rect(blurred_rounded_rect)
112112
fn image_sampling(image_sampling)
113+
fn image_sampling_bicubic(image_sampling_bicubic)
113114
fn image_extend_modes_bilinear(impls::image_extend_modes(ImageQuality::Medium), "image_extend_modes (bilinear)", false)
114115
fn image_extend_modes_nearest_neighbor(impls::image_extend_modes(ImageQuality::Low), "image_extend_modes (nearest neighbor)", false)
115116
fn luminance_mask(luminance_mask)
@@ -1963,6 +1964,62 @@ mod impls {
19631964
);
19641965
}
19651966

1967+
pub(super) fn image_sampling_bicubic(scene: &mut Scene, params: &mut SceneParams<'_>) {
1968+
params.resolution = Some(Vec2::new(1400., 900.));
1969+
params.base_color = Some(palette::css::WHITE);
1970+
1971+
let mut blob: Vec<u8> = Vec::with_capacity(16 * 16 * 4);
1972+
for y in 0..16 {
1973+
for x in 0..16 {
1974+
let is_checker = ((x / 2) + (y / 2)) % 2 == 0;
1975+
let mut color = if is_checker {
1976+
palette::css::BLACK
1977+
} else {
1978+
palette::css::WHITE
1979+
};
1980+
if x == 8 || y == 8 {
1981+
color = palette::css::RED;
1982+
}
1983+
if x == y || x + y == 15 {
1984+
color = palette::css::BLUE;
1985+
}
1986+
if (x == 2 && y == 13) || (x == 13 && y == 2) {
1987+
color = palette::css::LIME;
1988+
}
1989+
blob.extend(color.to_rgba8().to_u8_array());
1990+
}
1991+
}
1992+
let data = Blob::new(Arc::new(blob));
1993+
let image = ImageData {
1994+
data,
1995+
format: ImageFormat::Rgba8,
1996+
width: 16,
1997+
height: 16,
1998+
alpha_type: ImageAlphaType::Alpha,
1999+
};
2000+
2001+
let image_low = ImageBrush::new(image.clone()).with_quality(ImageQuality::Low);
2002+
let image_medium = ImageBrush::new(image.clone()).with_quality(ImageQuality::Medium);
2003+
let image_high = ImageBrush::new(image).with_quality(ImageQuality::High);
2004+
2005+
let transforms = [
2006+
Affine::translate((-8.0, -8.0))
2007+
.then_rotate(PI / 5.0)
2008+
.then_scale_non_uniform(18.0, 14.0)
2009+
.then_translate((250.0, 270.0).into()),
2010+
Affine::translate((250.0, 670.0))
2011+
* Affine::scale_non_uniform(20.0, 10.0)
2012+
* Affine::skew(0.35, -0.15)
2013+
* Affine::translate((-8.0, -8.0)),
2014+
];
2015+
2016+
for transform in transforms {
2017+
scene.draw_image(&image_low, transform);
2018+
scene.draw_image(&image_medium, transform.then_translate((420.0, 0.0).into()));
2019+
scene.draw_image(&image_high, transform.then_translate((840.0, 0.0).into()));
2020+
}
2021+
}
2022+
19662023
pub(super) fn image_extend_modes(
19672024
quality: ImageQuality,
19682025
) -> impl FnMut(&mut Scene, &mut SceneParams<'_>) {

vello_shaders/shader/fine.wgsl

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,110 @@ fn extend_mode(t: f32, mode: u32, max: f32) -> f32 {
892892
}
893893
}
894894

895+
// Cubic resampler logic borrowed from Skia (same as CPU cubic_resampler function)
896+
// Mitchell-Netravali cubic filter coefficients with parameters B=1/3 and C=1/3
897+
const MF: array<vec4<f32>, 4> = array<vec4<f32>, 4>(
898+
vec4<f32>(
899+
(1.0 / 6.0) / 3.0,
900+
-(3.0 / 6.0) / 3.0 - 1.0 / 3.0,
901+
(3.0 / 6.0) / 3.0 + 2.0 * 1.0 / 3.0,
902+
-(1.0 / 6.0) / 3.0 - 1.0 / 3.0
903+
),
904+
vec4<f32>(
905+
1.0 - (2.0 / 6.0) / 3.0,
906+
0.0,
907+
-3.0 + (12.0 / 6.0) / 3.0 + 1.0 / 3.0,
908+
2.0 - (9.0 / 6.0) / 3.0 - 1.0 / 3.0
909+
),
910+
vec4<f32>(
911+
(1.0 / 6.0) / 3.0,
912+
(3.0 / 6.0) / 3.0 + 1.0 / 3.0,
913+
3.0 - (15.0 / 6.0) / 3.0 - 2.0 * 1.0 / 3.0,
914+
-2.0 + (9.0 / 6.0) / 3.0 + 1.0 / 3.0
915+
),
916+
vec4<f32>(
917+
0.0,
918+
0.0,
919+
-1.0 / 3.0,
920+
(1.0 / 6.0) / 3.0 + 1.0 / 3.0
921+
)
922+
);
923+
924+
// Calculate the weights for a single fractional value (same as CPU weights function)
925+
fn cubic_weights(fract: f32) -> vec4<f32> {
926+
return vec4<f32>(
927+
single_weight(fract, MF[0][0], MF[0][1], MF[0][2], MF[0][3]),
928+
single_weight(fract, MF[1][0], MF[1][1], MF[1][2], MF[1][3]),
929+
single_weight(fract, MF[2][0], MF[2][1], MF[2][2], MF[2][3]),
930+
single_weight(fract, MF[3][0], MF[3][1], MF[3][2], MF[3][3])
931+
);
932+
}
933+
934+
// Calculate a weight based on the fractional value t and the cubic coefficients
935+
// This matches the CPU implementation exactly
936+
fn single_weight(t: f32, a: f32, b: f32, c: f32, d: f32) -> f32 {
937+
return t * (t * (t * d + c) + b) + a;
938+
}
939+
940+
// Bicubic filtering using Mitchell filter with B=1/3, C=1/3
941+
//
942+
// Cubic resampling consists of sampling the 16 surrounding pixels of the target point and
943+
// interpolating them with a cubic filter. The generated matrix is 4x4 and represent the coefficients
944+
// of the cubic function used to calculate weights based on the `x_fract` and `y_fract` of the
945+
// location we are looking at.
946+
//
947+
// This is adapted from the sparse-strips shader for the main Vello image path:
948+
// - the atlas is a single `texture_2d`, not a texture array
949+
// - each tap is premultiplied before filtering, matching the existing bilinear path
950+
fn bicubic_sample(
951+
coords: vec2<f32>,
952+
atlas_offset: vec2<f32>,
953+
atlas_max: vec2<f32>,
954+
alpha_type: u32,
955+
) -> vec4<f32> {
956+
let frac_coords = fract(coords + vec2(0.5));
957+
// Get cubic weights for x and y directions
958+
let cx = cubic_weights(frac_coords.x);
959+
let cy = cubic_weights(frac_coords.y);
960+
961+
// Sample 4x4 grid around coords
962+
let s00 = maybe_premul_alpha(textureLoad(image_atlas, vec2<i32>(clamp(coords + vec2(-1.5, -1.5), atlas_offset, atlas_max)), 0), alpha_type);
963+
let s10 = maybe_premul_alpha(textureLoad(image_atlas, vec2<i32>(clamp(coords + vec2(-0.5, -1.5), atlas_offset, atlas_max)), 0), alpha_type);
964+
let s20 = maybe_premul_alpha(textureLoad(image_atlas, vec2<i32>(clamp(coords + vec2(0.5, -1.5), atlas_offset, atlas_max)), 0), alpha_type);
965+
let s30 = maybe_premul_alpha(textureLoad(image_atlas, vec2<i32>(clamp(coords + vec2(1.5, -1.5), atlas_offset, atlas_max)), 0), alpha_type);
966+
967+
let s01 = maybe_premul_alpha(textureLoad(image_atlas, vec2<i32>(clamp(coords + vec2(-1.5, -0.5), atlas_offset, atlas_max)), 0), alpha_type);
968+
let s11 = maybe_premul_alpha(textureLoad(image_atlas, vec2<i32>(clamp(coords + vec2(-0.5, -0.5), atlas_offset, atlas_max)), 0), alpha_type);
969+
let s21 = maybe_premul_alpha(textureLoad(image_atlas, vec2<i32>(clamp(coords + vec2(0.5, -0.5), atlas_offset, atlas_max)), 0), alpha_type);
970+
let s31 = maybe_premul_alpha(textureLoad(image_atlas, vec2<i32>(clamp(coords + vec2(1.5, -0.5), atlas_offset, atlas_max)), 0), alpha_type);
971+
972+
let s02 = maybe_premul_alpha(textureLoad(image_atlas, vec2<i32>(clamp(coords + vec2(-1.5, 0.5), atlas_offset, atlas_max)), 0), alpha_type);
973+
let s12 = maybe_premul_alpha(textureLoad(image_atlas, vec2<i32>(clamp(coords + vec2(-0.5, 0.5), atlas_offset, atlas_max)), 0), alpha_type);
974+
let s22 = maybe_premul_alpha(textureLoad(image_atlas, vec2<i32>(clamp(coords + vec2(0.5, 0.5), atlas_offset, atlas_max)), 0), alpha_type);
975+
let s32 = maybe_premul_alpha(textureLoad(image_atlas, vec2<i32>(clamp(coords + vec2(1.5, 0.5), atlas_offset, atlas_max)), 0), alpha_type);
976+
977+
let s03 = maybe_premul_alpha(textureLoad(image_atlas, vec2<i32>(clamp(coords + vec2(-1.5, 1.5), atlas_offset, atlas_max)), 0), alpha_type);
978+
let s13 = maybe_premul_alpha(textureLoad(image_atlas, vec2<i32>(clamp(coords + vec2(-0.5, 1.5), atlas_offset, atlas_max)), 0), alpha_type);
979+
let s23 = maybe_premul_alpha(textureLoad(image_atlas, vec2<i32>(clamp(coords + vec2(0.5, 1.5), atlas_offset, atlas_max)), 0), alpha_type);
980+
let s33 = maybe_premul_alpha(textureLoad(image_atlas, vec2<i32>(clamp(coords + vec2(1.5, 1.5), atlas_offset, atlas_max)), 0), alpha_type);
981+
982+
// Interpolate in x direction for each row
983+
let row0 = cx.x * s00 + cx.y * s10 + cx.z * s20 + cx.w * s30;
984+
let row1 = cx.x * s01 + cx.y * s11 + cx.z * s21 + cx.w * s31;
985+
let row2 = cx.x * s02 + cx.y * s12 + cx.z * s22 + cx.w * s32;
986+
let row3 = cx.x * s03 + cx.y * s13 + cx.z * s23 + cx.w * s33;
987+
// Interpolate in y direction
988+
let result = cy.x * row0 + cy.y * row1 + cy.z * row2 + cy.w * row3;
989+
990+
// Clamp each component to [0,1] and ensure color components don't exceed alpha
991+
return vec4<f32>(
992+
min(clamp(result.r, 0.0, 1.0), result.a),
993+
min(clamp(result.g, 0.0, 1.0), result.a),
994+
min(clamp(result.b, 0.0, 1.0), result.a),
995+
clamp(result.a, 0.0, 1.0)
996+
);
997+
}
998+
895999
const PIXELS_PER_THREAD = 4u;
8961000

8971001
#ifndef msaa
@@ -1235,7 +1339,6 @@ fn main(
12351339
}
12361340
}
12371341
case IMAGE_QUALITY_MEDIUM, default: {
1238-
// We don't have an implementation for `IMAGE_QUALITY_HIGH` yet, just use the same as medium
12391342
for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) {
12401343
// We only need to load from the textures if the value will be used.
12411344
if area[i] != 0.0 {
@@ -1261,6 +1364,20 @@ fn main(
12611364
}
12621365
}
12631366
}
1367+
case IMAGE_QUALITY_HIGH: {
1368+
for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) {
1369+
if area[i] != 0.0 {
1370+
let my_xy = vec2(xy.x + f32(i), xy.y);
1371+
var atlas_uv = image.matrx.xy * my_xy.x + image.matrx.zw * my_xy.y + image.xlat;
1372+
atlas_uv.x = extend_mode(atlas_uv.x, image.x_extend_mode, image.extents.x);
1373+
atlas_uv.y = extend_mode(atlas_uv.y, image.y_extend_mode, image.extents.y);
1374+
atlas_uv = atlas_uv + image.atlas_offset;
1375+
let fg_rgba = bicubic_sample(atlas_uv, image.atlas_offset, atlas_max, image.alpha_type);
1376+
let fg_i = pixel_format(fg_rgba * area[i] * image.alpha, image.format);
1377+
rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i;
1378+
}
1379+
}
1380+
}
12641381
}
12651382
cmd_ix += 2u;
12661383
}
Lines changed: 3 additions & 0 deletions
Loading

vello_tests/tests/snapshot_test_scenes.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,14 @@ fn snapshot_image_sampling() {
130130
snapshot_test_scene(test_scene, params);
131131
}
132132

133+
#[test]
134+
#[cfg_attr(skip_gpu_tests, ignore)]
135+
fn snapshot_image_sampling_bicubic() {
136+
let test_scene = test_scenes::image_sampling_bicubic();
137+
let params = TestParams::new("image_sampling_bicubic", 520, 336);
138+
snapshot_test_scene(test_scene, params);
139+
}
140+
133141
#[test]
134142
#[cfg_attr(skip_gpu_tests, ignore)]
135143
fn snapshot_image_extend_modes_bilinear() {

0 commit comments

Comments
 (0)