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
31 changes: 30 additions & 1 deletion sparse_strips/vello_hybrid/src/render/webgl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1829,7 +1829,36 @@ fn create_texture(gl: &WebGl2RenderingContext) -> WebGlTexture {
/// Create a texture array with nearest neighbor sampling and
/// clamp-to-edge wrapping.
fn create_texture_array(gl: &WebGl2RenderingContext) -> WebGlTexture {
create_texture_inner(gl, WebGl2RenderingContext::TEXTURE_2D_ARRAY)
let target = WebGl2RenderingContext::TEXTURE_2D_ARRAY;
let texture = gl.create_texture().unwrap();
gl.active_texture(WebGl2RenderingContext::TEXTURE0);
gl.bind_texture(target, Some(&texture));
// The filter and wrap modes are irrelevant because the shader
// (`render_strips.wgsl`) exclusively uses `textureLoad`, which bypasses
// the sampler entirely.
gl.tex_parameteri(
target,
WebGl2RenderingContext::TEXTURE_MIN_FILTER,
WebGl2RenderingContext::LINEAR as i32,
);
gl.tex_parameteri(
target,
WebGl2RenderingContext::TEXTURE_MAG_FILTER,
WebGl2RenderingContext::LINEAR as i32,
);
gl.tex_parameteri(
target,
WebGl2RenderingContext::TEXTURE_WRAP_S,
WebGl2RenderingContext::CLAMP_TO_EDGE as i32,
);
gl.tex_parameteri(
target,
WebGl2RenderingContext::TEXTURE_WRAP_T,
WebGl2RenderingContext::CLAMP_TO_EDGE as i32,
);
gl.tex_parameteri(target, WebGl2RenderingContext::TEXTURE_MAX_LEVEL, 0);

texture
}

fn create_texture_inner(gl: &WebGl2RenderingContext, target: u32) -> WebGlTexture {
Expand Down
61 changes: 46 additions & 15 deletions sparse_strips/vello_hybrid/src/render/wgpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,8 @@ struct GpuResources {
atlas_texture_array: Texture,
/// View for atlas texture array
atlas_texture_array_view: TextureView,
/// Bilinear sampler for GPU-native image sampling
atlas_sampler: Sampler,
/// Bind group for atlas textures (as texture array)
atlas_bind_group: BindGroup,
/// Filter atlas textures and their associated views/bind groups.
Expand Down Expand Up @@ -963,18 +965,33 @@ impl Programs {
let atlas_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Atlas Texture Bind Group Layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2Array,
multisampled: false,
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2Array,
multisampled: false,
},
count: None,
},
count: None,
}],
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});

let atlas_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Atlas Bilinear Sampler"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
..Default::default()
});

let encoded_paints_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Encoded Paints Bind Group Layout"),
Expand Down Expand Up @@ -1347,15 +1364,20 @@ impl Programs {
device,
&atlas_bind_group_layout,
&atlas_texture_array_view,
&atlas_sampler,
);

// Create a 1x1 stub atlas texture array for use during render_to_atlas.
// This avoids the read-write conflict that occurs when the real atlas is both
// a shader input (bind group) and render target in the same pass.
let (_stub_atlas_texture, stub_atlas_view) =
Self::create_atlas_texture_array(device, 1, 1, 1);
let stub_atlas_bind_group =
Self::create_atlas_bind_group(device, &atlas_bind_group_layout, &stub_atlas_view);
let stub_atlas_bind_group = Self::create_atlas_bind_group(
device,
&atlas_bind_group_layout,
&stub_atlas_view,
&atlas_sampler,
);

const INITIAL_ENCODED_PAINTS_TEXTURE_HEIGHT: u32 = 1;
let encoded_paints_data = vec![
Expand Down Expand Up @@ -1432,6 +1454,7 @@ impl Programs {
alphas_texture,
atlas_texture_array,
atlas_texture_array_view,
atlas_sampler,
atlas_bind_group,
filter_atlas,
stub_atlas_bind_group,
Expand Down Expand Up @@ -1612,14 +1635,21 @@ impl Programs {
device: &Device,
atlas_bind_group_layout: &BindGroupLayout,
atlas_texture_array_view: &TextureView,
atlas_sampler: &Sampler,
) -> BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Atlas Bind Group"),
layout: atlas_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(atlas_texture_array_view),
}],
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(atlas_texture_array_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(atlas_sampler),
},
],
})
}

Expand Down Expand Up @@ -2002,6 +2032,7 @@ impl Programs {
device,
atlas_bind_group_layout,
&new_atlas_texture_array_view,
&resources.atlas_sampler,
);

// Replace the old resources
Expand Down
38 changes: 25 additions & 13 deletions sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@ var<uniform> config: Config;
@group(1) @binding(0)
var atlas_texture_array: texture_2d_array<f32>;

@group(1) @binding(1)
var atlas_sampler: sampler;

@group(2) @binding(0)
var encoded_paints_texture: texture_2d<u32>;

Expand Down Expand Up @@ -407,7 +410,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
encoded_image.image_padding,
);
} else if encoded_image.quality == IMAGE_QUALITY_MEDIUM {
let final_xy = image_offset + extended_xy - vec2(0.5);
let final_xy = image_offset + extended_xy;
sample_color = bilinear_sample(
atlas_texture_array,
final_xy,
Expand Down Expand Up @@ -933,10 +936,26 @@ fn extend_mode_normalized(t: f32, mode: u32) -> f32 {
}
}

// Bilinear filtering
// Convert atlas-space pixel coordinates to normalized UVs suitable for textureSample.
//
// Bilinear filtering consists of sampling the 4 surrounding pixels of the target point and
// interpolating them with a bilinear filter.
// To prevent bleeding from neighboring atlas images, we clamp the UVs so that the boundary
// samples land on the CENTER of the boundary texels, not their edges.
//
// The input `sample_xy` is in atlas pixel space (after extend-mode and offset are applied).
fn atlas_to_normalized_uv(
sample_xy: vec2<f32>,
image_offset: vec2<f32>,
image_size: vec2<f32>,
extend_modes: vec2<u32>,
) -> vec2<f32> {
let inv_atlas_dim = 1.0 / vec2<f32>(textureDimensions(atlas_texture_array));
let uv_min = (image_offset + 0.5) * inv_atlas_dim;
let uv_max = (image_offset + image_size - 0.5) * inv_atlas_dim;
let uv = sample_xy * inv_atlas_dim;
return clamp(uv, uv_min, uv_max);
}

// Bilinear filtering via hardware textureSample.
fn bilinear_sample(
tex: texture_2d_array<f32>,
coords: vec2<f32>,
Expand All @@ -946,15 +965,8 @@ fn bilinear_sample(
extend_modes: vec2<u32>,
image_padding: f32,
) -> vec4<f32> {
let atlas_max = image_offset + image_size - vec2(1.0);
let atlas_uv_clamped = clamp(coords, image_offset, atlas_max);
let uv_quad = vec4(floor(atlas_uv_clamped), ceil(atlas_uv_clamped));
let uv_frac = fract(coords);
let a = textureLoad(tex, vec2<i32>(uv_quad.xy), atlas_idx, 0);
let b = textureLoad(tex, vec2<i32>(uv_quad.xw), atlas_idx, 0);
let c = textureLoad(tex, vec2<i32>(uv_quad.zy), atlas_idx, 0);
let d = textureLoad(tex, vec2<i32>(uv_quad.zw), atlas_idx, 0);
return mix(mix(a, b, uv_frac.y), mix(c, d, uv_frac.y), uv_frac.x);
let uv = atlas_to_normalized_uv(coords, image_offset, image_size, extend_modes);
return textureSample(tex, atlas_sampler, uv, atlas_idx);
}

// Bicubic filtering using Mitchell filter with B=1/3, C=1/3
Expand Down
12 changes: 6 additions & 6 deletions sparse_strips/vello_sparse_tests/tests/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ fn image_bilinear_identity(ctx: &mut impl Renderer) {
);
}

#[vello_test]
#[vello_test(hybrid_tolerance = 2)]
fn image_bilinear_2x_scale(ctx: &mut impl Renderer) {
let image_source = rgb_img_2x2(ctx);
quality(
Expand All @@ -384,7 +384,7 @@ fn image_bilinear_2x_scale(ctx: &mut impl Renderer) {
);
}

#[vello_test]
#[vello_test(hybrid_tolerance = 2)]
fn image_bilinear_5x_scale(ctx: &mut impl Renderer) {
let image_source = rgb_img_2x2(ctx);
quality(
Expand All @@ -396,7 +396,7 @@ fn image_bilinear_5x_scale(ctx: &mut impl Renderer) {
);
}

#[vello_test]
#[vello_test(hybrid_tolerance = 2)]
fn image_bilinear_10x_scale(ctx: &mut impl Renderer) {
let image_source = rgb_img_2x2(ctx);
quality(
Expand All @@ -408,7 +408,7 @@ fn image_bilinear_10x_scale(ctx: &mut impl Renderer) {
);
}

#[vello_test]
#[vello_test(hybrid_tolerance = 2)]
fn image_bilinear_with_rotation(ctx: &mut impl Renderer) {
let image_source = rgb_img_2x2(ctx);
quality(
Expand All @@ -420,7 +420,7 @@ fn image_bilinear_with_rotation(ctx: &mut impl Renderer) {
);
}

#[vello_test]
#[vello_test(hybrid_tolerance = 2)]
fn image_bilinear_with_translation(ctx: &mut impl Renderer) {
let image_source = rgb_img_2x2(ctx);
quality(
Expand All @@ -432,7 +432,7 @@ fn image_bilinear_with_translation(ctx: &mut impl Renderer) {
);
}

#[vello_test]
#[vello_test(hybrid_tolerance = 2)]
fn image_bilinear_10x_scale_2(ctx: &mut impl Renderer) {
let image_source = rgb_img_2x3(ctx);
quality(
Expand Down
Loading