Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions vello_gpu_shaders/shaders/helpers/extend.wesl
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,22 @@ fn extend_mode(t: f32, mode: u32, max: f32) -> f32 {
return clamp(t, 0.0, max - 1.0);
}
case EXTEND_REPEAT: {
return extend_mode_normalized(t / max, mode) * max;
return euclid_mod(floor(t), max);
}
case EXTEND_REFLECT, default: {
return extend_mode_normalized(t / max, mode) * max;
let period = max + max;
let r = euclid_mod(floor(t), period);
return select(r, period - 1.0 - r, r >= max);
}
}
}

fn euclid_mod(t: f32, m: f32) -> f32 {
let q = round(t / m);
let r = fma(q, -m, t);
return select(r, r + m, r < 0.0);
}

fn extend_mode_normalized(t: f32, mode: u32) -> f32 {
switch mode {
case EXTEND_PAD: {
Expand Down
103 changes: 74 additions & 29 deletions vello_gpu_shaders/shaders/helpers/external_texture.wesl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import package::helpers::image::{
IMAGE_QUALITY_MEDIUM,
cubic_weights
};
import package::helpers::extend::extend_mode;

fn external_axis_index(coord: f32, extend_mode_id: u32, size: f32) -> i32 {
return i32(min(extend_mode(coord, extend_mode_id, size), size - 1.0));
}

// This is the same as `helpers::image::bilinear_sample`, but for a 2D texture
// instead of an atlas texture array.
Expand All @@ -16,15 +21,24 @@ fn external_bilinear_sample(
coords: vec2<f32>,
image_offset: vec2<f32>,
image_size: vec2<f32>,
image_extend_modes: vec2<u32>,
) -> vec4<f32> {
let image_max = image_offset + image_size - vec2(1.0);
let clamped_coords = clamp(coords, image_offset, image_max);
let coord_quad = vec4(floor(clamped_coords), ceil(clamped_coords));
let coord_frac = fract(coords);
let a = textureLoad(external_texture, vec2<i32>(coord_quad.xy), 0);
let b = textureLoad(external_texture, vec2<i32>(coord_quad.xw), 0);
let c = textureLoad(external_texture, vec2<i32>(coord_quad.zy), 0);
let d = textureLoad(external_texture, vec2<i32>(coord_quad.zw), 0);
let shifted_coords = coords + vec2(0.5);
let base_coords = floor(shifted_coords);
let coord_frac = shifted_coords - base_coords;
let x = vec2<i32>(
external_axis_index(base_coords.x - 1.0, image_extend_modes.x, image_size.x),
external_axis_index(base_coords.x, image_extend_modes.x, image_size.x),
);
let y = vec2<i32>(
external_axis_index(base_coords.y - 1.0, image_extend_modes.y, image_size.y),
external_axis_index(base_coords.y, image_extend_modes.y, image_size.y),
);
let offset = vec2<i32>(image_offset);
let a = textureLoad(external_texture, offset + vec2<i32>(x.x, y.x), 0);
let b = textureLoad(external_texture, offset + vec2<i32>(x.x, y.y), 0);
let c = textureLoad(external_texture, offset + vec2<i32>(x.y, y.x), 0);
let d = textureLoad(external_texture, offset + vec2<i32>(x.y, y.y), 0);
return mix(mix(a, b, coord_frac.y), mix(c, d, coord_frac.y), coord_frac.x);
}

Expand All @@ -35,31 +49,47 @@ fn external_bicubic_sample(
coords: vec2<f32>,
image_offset: vec2<f32>,
image_size: vec2<f32>,
image_extend_modes: vec2<u32>,
) -> vec4<f32> {
let image_max = image_offset + image_size - vec2(1.0);
let frac_coords = fract(coords + 0.5);
let shifted_coords = coords + vec2(0.5);
let base_coords = floor(shifted_coords);
let frac_coords = shifted_coords - base_coords;
let cx = cubic_weights(frac_coords.x);
let cy = cubic_weights(frac_coords.y);

let s00 = textureLoad(external_texture, vec2<i32>(clamp(coords + vec2(-1.5, -1.5), image_offset, image_max)), 0);
let s10 = textureLoad(external_texture, vec2<i32>(clamp(coords + vec2(-0.5, -1.5), image_offset, image_max)), 0);
let s20 = textureLoad(external_texture, vec2<i32>(clamp(coords + vec2(0.5, -1.5), image_offset, image_max)), 0);
let s30 = textureLoad(external_texture, vec2<i32>(clamp(coords + vec2(1.5, -1.5), image_offset, image_max)), 0);
let x = vec4<i32>(
external_axis_index(base_coords.x - 2.0, image_extend_modes.x, image_size.x),
external_axis_index(base_coords.x - 1.0, image_extend_modes.x, image_size.x),
external_axis_index(base_coords.x, image_extend_modes.x, image_size.x),
external_axis_index(base_coords.x + 1.0, image_extend_modes.x, image_size.x),
);
let y = vec4<i32>(
external_axis_index(base_coords.y - 2.0, image_extend_modes.y, image_size.y),
external_axis_index(base_coords.y - 1.0, image_extend_modes.y, image_size.y),
external_axis_index(base_coords.y, image_extend_modes.y, image_size.y),
external_axis_index(base_coords.y + 1.0, image_extend_modes.y, image_size.y),
);
let offset = vec2<i32>(image_offset);

let s01 = textureLoad(external_texture, vec2<i32>(clamp(coords + vec2(-1.5, -0.5), image_offset, image_max)), 0);
let s11 = textureLoad(external_texture, vec2<i32>(clamp(coords + vec2(-0.5, -0.5), image_offset, image_max)), 0);
let s21 = textureLoad(external_texture, vec2<i32>(clamp(coords + vec2(0.5, -0.5), image_offset, image_max)), 0);
let s31 = textureLoad(external_texture, vec2<i32>(clamp(coords + vec2(1.5, -0.5), image_offset, image_max)), 0);
let s00 = textureLoad(external_texture, offset + vec2<i32>(x.x, y.x), 0);
let s10 = textureLoad(external_texture, offset + vec2<i32>(x.y, y.x), 0);
let s20 = textureLoad(external_texture, offset + vec2<i32>(x.z, y.x), 0);
let s30 = textureLoad(external_texture, offset + vec2<i32>(x.w, y.x), 0);

let s02 = textureLoad(external_texture, vec2<i32>(clamp(coords + vec2(-1.5, 0.5), image_offset, image_max)), 0);
let s12 = textureLoad(external_texture, vec2<i32>(clamp(coords + vec2(-0.5, 0.5), image_offset, image_max)), 0);
let s22 = textureLoad(external_texture, vec2<i32>(clamp(coords + vec2(0.5, 0.5), image_offset, image_max)), 0);
let s32 = textureLoad(external_texture, vec2<i32>(clamp(coords + vec2(1.5, 0.5), image_offset, image_max)), 0);
let s01 = textureLoad(external_texture, offset + vec2<i32>(x.x, y.y), 0);
let s11 = textureLoad(external_texture, offset + vec2<i32>(x.y, y.y), 0);
let s21 = textureLoad(external_texture, offset + vec2<i32>(x.z, y.y), 0);
let s31 = textureLoad(external_texture, offset + vec2<i32>(x.w, y.y), 0);

let s03 = textureLoad(external_texture, vec2<i32>(clamp(coords + vec2(-1.5, 1.5), image_offset, image_max)), 0);
let s13 = textureLoad(external_texture, vec2<i32>(clamp(coords + vec2(-0.5, 1.5), image_offset, image_max)), 0);
let s23 = textureLoad(external_texture, vec2<i32>(clamp(coords + vec2(0.5, 1.5), image_offset, image_max)), 0);
let s33 = textureLoad(external_texture, vec2<i32>(clamp(coords + vec2(1.5, 1.5), image_offset, image_max)), 0);
let s02 = textureLoad(external_texture, offset + vec2<i32>(x.x, y.z), 0);
let s12 = textureLoad(external_texture, offset + vec2<i32>(x.y, y.z), 0);
let s22 = textureLoad(external_texture, offset + vec2<i32>(x.z, y.z), 0);
let s32 = textureLoad(external_texture, offset + vec2<i32>(x.w, y.z), 0);

let s03 = textureLoad(external_texture, offset + vec2<i32>(x.x, y.w), 0);
let s13 = textureLoad(external_texture, offset + vec2<i32>(x.y, y.w), 0);
let s23 = textureLoad(external_texture, offset + vec2<i32>(x.z, y.w), 0);
let s33 = textureLoad(external_texture, offset + vec2<i32>(x.w, y.w), 0);

let row0 = cx.x * s00 + cx.y * s10 + cx.z * s20 + cx.w * s30;
let row1 = cx.x * s01 + cx.y * s11 + cx.z * s21 + cx.w * s31;
Expand All @@ -78,17 +108,32 @@ fn sample_external_image(
coords: vec2<f32>,
image_offset: vec2<f32>,
image_size: vec2<f32>,
image_extend_modes: vec2<u32>,
) -> vec4<f32> {
if quality == IMAGE_QUALITY_HIGH {
return external_bicubic_sample(external_texture, coords, image_offset, image_size);
return external_bicubic_sample(
external_texture,
coords,
image_offset,
image_size,
image_extend_modes,
);
}
if quality == IMAGE_QUALITY_MEDIUM {
return external_bilinear_sample(
external_texture,
coords - vec2(0.5),
coords,
image_offset,
image_size,
image_extend_modes,
);
}
return textureLoad(external_texture, vec2<u32>(coords), 0);
return textureLoad(
external_texture,
vec2<i32>(image_offset) + vec2<i32>(
external_axis_index(coords.x, image_extend_modes.x, image_size.x),
external_axis_index(coords.y, image_extend_modes.y, image_size.y),
),
0,
);
}
9 changes: 2 additions & 7 deletions vello_gpu_shaders/shaders/render.wesl
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@

import package::helpers::alpha::unpack_alphas_from_channel;
import package::helpers::blurred_rounded_rect::calculate_blurred_rounded_rect;
import package::helpers::extend::extend_mode;
import package::helpers::external_texture::sample_external_image;
import package::helpers::gradient::{
apply_gradient_transform,
Expand Down Expand Up @@ -395,21 +394,17 @@ fn fs_main(
let image_tint = unpack4x8unorm(packed_tint);
let is_multiply = image_texel2.z == TINT_MODE_MULTIPLY;
let local_xy = sample_xy - image_offset;
let extended_xy = vec2<f32>(
extend_mode(local_xy.x, image_extend_modes.x, image_size.x),
extend_mode(local_xy.y, image_extend_modes.y, image_size.y)
);

// TODO: add a fast path for images where we are using bilinear sampling and want transparent pixels,
// using GPU-native bilinear sampling

let final_xy = image_offset + extended_xy;
let sample_color = sample_external_image(
external_texture_0,
image_quality,
final_xy,
local_xy,
image_offset,
image_size,
image_extend_modes,
);

final_color = alpha * select(
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions vello_tests/snapshots/external_texture_skewed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The border in the new snapshot is expected. The test uses bilinear sampling with repeat. This means that for example, for the top-right part, as we approach the right border of the yellow pixel, it should slowly start to fade into the red of the top-left pixel, hence why it becomes orange.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions vello_tests/snapshots/image_bicubic_repeat_x_pad_y.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 26 additions & 3 deletions vello_tests/src/renderer.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

#[cfg(not(all(target_arch = "wasm32", feature = "webgl")))]
use std::collections::HashMap;
use std::sync::Arc;

Expand Down Expand Up @@ -100,6 +99,8 @@ pub trait Renderer: Sized {
pub struct CpuRenderer {
ctx: RenderContext,
resources: Resources,
external_textures: HashMap<TextureId, Arc<Pixmap>>,
next_external_texture_id: u64,
render_mode: RenderMode,
target: Pixmap,
target_init: GpuTargetInit<'static>,
Expand All @@ -119,6 +120,8 @@ impl Renderer for CpuRenderer {
Self {
ctx: RenderContext::new_with(width, height, settings),
resources: Resources::new(),
external_textures: HashMap::new(),
next_external_texture_id: 1,
render_mode,
target: Pixmap::new(width, height),
target_init: GpuTargetInit::Clear(ClearSettings::default()),
Expand Down Expand Up @@ -214,6 +217,23 @@ impl Renderer for CpuRenderer {
}

fn set_paint(&mut self, paint: impl Into<PaintType>) {
let mut paint = paint.into();
if let PaintType::Image(image) = &mut paint
&& let ImageSource::ExternalTexture {
id, source_region, ..
} = &image.image
{
let pixmap = self
.external_textures
.get(id)
.unwrap_or_else(|| panic!("External texture {id:?} not found in test registry"));
assert_eq!(
*source_region,
RectU16::new(0, 0, pixmap.width(), pixmap.height()),
"CPU test renderer only supports full external texture regions"
);
image.image = ImageSource::Pixmap(Arc::clone(pixmap));
}
self.ctx.set_paint(paint);
}

Expand Down Expand Up @@ -283,8 +303,11 @@ impl Renderer for CpuRenderer {
self.target.clone()
}

fn register_external_texture(&mut self, _: Arc<Pixmap>) -> TextureId {
unimplemented!("external textures are only supported by GPU renderer tests")
fn register_external_texture(&mut self, pixmap: Arc<Pixmap>) -> TextureId {
let texture_id = TextureId(self.next_external_texture_id);
self.next_external_texture_id += 1;
self.external_textures.insert(texture_id, pixmap);
texture_id
}

fn get_image_source(&mut self, pixmap: Arc<Pixmap>) -> ImageSource {
Expand Down
Loading
Loading