Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image x/y extend, alpha, nearest neighbor sampling #766

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ skrifa = "0.26.0"
# The version of kurbo used below should be kept in sync
# with the version of kurbo used by peniko.
# peniko = "0.2.0"
peniko = { version = "0.2.0", git = "https://github.com/linebender/peniko.git", rev = "3462e19" }
peniko = { version = "0.2.0", git = "https://github.com/linebender/peniko.git", rev = "98b2b62" }
DJMcNab marked this conversation as resolved.
Show resolved Hide resolved
# FIXME: This can be removed once peniko supports the schemars feature.
kurbo = "0.11.1"
futures-intrusive = "0.5.0"
Expand Down
4 changes: 2 additions & 2 deletions examples/scenes/src/images.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use vello::peniko::{Blob, Format, Image};
use vello::peniko::{Blob, Image, ImageFormat};

/// Simple hack to support loading images for examples.
#[derive(Default)]
Expand Down Expand Up @@ -50,5 +50,5 @@ fn decode_image(data: &[u8]) -> anyhow::Result<Image> {
let height = image.height();
let data = Arc::new(image.into_rgba8().into_vec());
let blob = Blob::new(data);
Ok(Image::new(blob, Format::Rgba8, width, height))
Ok(Image::new(blob, ImageFormat::Rgba8, width, height))
}
41 changes: 32 additions & 9 deletions examples/scenes/src/test_scenes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ export_scenes!(
many_draw_objects(many_draw_objects),
blurred_rounded_rect(blurred_rounded_rect),
image_sampling(image_sampling),
image_extend_modes(image_extend_modes)
image_extend_modes(image_extend_modes),
image_extend_modes_nearest_neighbor(image_extend_modes_nearest_neighbor),
);

/// Implementations for the test scenes.
Expand Down Expand Up @@ -659,7 +660,8 @@ mod impls {
let piet_logo = params
.images
.from_bytes(FLOWER_IMAGE.as_ptr() as usize, FLOWER_IMAGE)
.unwrap();
.unwrap()
.with_alpha(((params.time * 0.5 + 200.0).sin() as f32 + 1.0) * 0.5);

use PathEl::*;
let rect = Rect::from_origin_size(Point::new(0.0, 0.0), (1000.0, 1000.0));
Expand Down Expand Up @@ -766,7 +768,7 @@ mod impls {
);
scene.draw_image(
&piet_logo,
Affine::translate((800.0, 50.0)) * Affine::rotate(20f64.to_radians()),
Affine::translate((750.0, 70.0)) * Affine::rotate(20f64.to_radians()),
);
}

Expand Down Expand Up @@ -1779,7 +1781,7 @@ mod impls {
blob.extend(c.premultiply().to_rgba8().to_u8_array());
});
let data = Blob::new(Arc::new(blob));
let image = Image::new(data, Format::Rgba8, 2, 2);
let image = Image::new(data, ImageFormat::Rgba8, 2, 2);

scene.draw_image(
&image,
Expand Down Expand Up @@ -1810,6 +1812,16 @@ mod impls {
pub(super) fn image_extend_modes(scene: &mut Scene, params: &mut SceneParams) {
params.resolution = Some(Vec2::new(1500., 1500.));
params.base_color = Some(palette::css::WHITE);
image_extend_modes_helper(scene, ImageQuality::Medium);
}

pub(super) fn image_extend_modes_nearest_neighbor(scene: &mut Scene, params: &mut SceneParams) {
Copy link
Member

Choose a reason for hiding this comment

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

We could impl this in a way similar to longpathdash, which is another parameterised test scene

params.resolution = Some(Vec2::new(1500., 1500.));
params.base_color = Some(palette::css::WHITE);
image_extend_modes_helper(scene, ImageQuality::Low);
}

fn image_extend_modes_helper(scene: &mut Scene, quality: ImageQuality) {
let mut blob: Vec<u8> = Vec::new();
[
palette::css::RED,
Expand All @@ -1822,30 +1834,41 @@ mod impls {
blob.extend(c.premultiply().to_rgba8().to_u8_array());
});
let data = Blob::new(Arc::new(blob));
let image = Image::new(data, Format::Rgba8, 2, 2);
let image = image.with_extend(Extend::Pad);
let image = Image::new(data, ImageFormat::Rgba8, 2, 2).with_quality(quality);
let brush_offset = Some(Affine::translate((2., 2.)));
// Pad extend mode
let image = image.with_extend(Extend::Pad);
scene.fill(
Fill::NonZero,
Affine::scale(100.).then_translate((100., 100.).into()),
&image,
Some(Affine::translate((2., 2.)).then_scale(100.)),
brush_offset,
&Rect::new(0., 0., 6., 6.),
);
let image = image.with_extend(Extend::Reflect);
scene.fill(
Fill::NonZero,
Affine::scale(100.).then_translate((100., 800.).into()),
&image,
Some(Affine::translate((2., 2.))),
brush_offset,
&Rect::new(0., 0., 6., 6.),
);
let image = image.with_extend(Extend::Repeat);
scene.fill(
Fill::NonZero,
Affine::scale(100.).then_translate((800., 100.).into()),
&image,
Some(Affine::translate((2., 2.))),
brush_offset,
&Rect::new(0., 0., 6., 6.),
);
let image = image
.with_x_extend(Extend::Repeat)
.with_y_extend(Extend::Reflect);
scene.fill(
Fill::NonZero,
Affine::scale(100.).then_translate((800., 800.).into()),
&image,
brush_offset,
&Rect::new(0., 0., 6., 6.),
);
}
Expand Down
6 changes: 3 additions & 3 deletions vello/src/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ impl<'a> DrawGlyphs<'a> {
Image::new(
// TODO: The design of the Blob type forces the double boxing
Blob::new(Arc::new(data)),
peniko::Format::Rgba8,
peniko::ImageFormat::Rgba8,
bitmap.width,
bitmap.height,
)
Expand Down Expand Up @@ -583,7 +583,7 @@ impl<'a> DrawGlyphs<'a> {
Image::new(
// TODO: The design of the Blob type forces the double boxing
Blob::new(Arc::new(buf)),
peniko::Format::Rgba8,
peniko::ImageFormat::Rgba8,
bitmap.width,
bitmap.height,
)
Expand Down Expand Up @@ -614,7 +614,7 @@ impl<'a> DrawGlyphs<'a> {
Image::new(
// TODO: The design of the Blob type forces the double boxing
Blob::new(Arc::new(data)),
peniko::Format::Rgba8,
peniko::ImageFormat::Rgba8,
bitmap.width,
bitmap.height,
)
Expand Down
4 changes: 3 additions & 1 deletion vello_encoding/src/draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ impl DrawTag {
pub const SWEEP_GRADIENT: Self = Self(0x254);

/// Image fill.
pub const IMAGE: Self = Self(0x248);
pub const IMAGE: Self = Self(0x28C); // info: 10, scene: 3

/// Blurred rounded rectangle.
pub const BLUR_RECT: Self = Self(0x2d4); // info: 11, scene: 5 (DrawBlurRoundedRect)
Expand Down Expand Up @@ -164,6 +164,8 @@ pub struct DrawImage {
pub xy: u32,
/// Packed image dimensions.
pub width_height: u32,
/// Packed quality, extend mode and 8-bit alpha (bits `qqxxyyaaaaaaaa`).
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// Packed quality, extend mode and 8-bit alpha (bits `qqxxyyaaaaaaaa`).
/// Packed quality, extend mode and 8-bit alpha (bits `qqxxyyaaaaaaaa`, 18 unused prefix bits).

pub sample_alpha: u32,
}

/// Draw data for a blurred rounded rectangle.
Expand Down
6 changes: 5 additions & 1 deletion vello_encoding/src/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ impl Encoding {

/// Encodes an image brush.
pub fn encode_image(&mut self, image: &Image, alpha: f32) {
let _alpha = alpha * image.alpha;
let alpha = (alpha * image.alpha * 255.0).round() as u8;
DJMcNab marked this conversation as resolved.
Show resolved Hide resolved
// TODO: feed the alpha multiplier through the full pipeline for consistency
// with other brushes?
// Tracked in https://github.com/linebender/vello/issues/692
Expand All @@ -415,6 +415,10 @@ impl Encoding {
.extend_from_slice(bytemuck::bytes_of(&DrawImage {
xy: 0,
width_height: (image.width << 16) | (image.height & 0xFFFF),
sample_alpha: ((image.quality as u32) << 12)
| ((image.x_extend as u32) << 10)
| ((image.y_extend as u32) << 8)
Copy link
Member

Choose a reason for hiding this comment

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

Maybe? It would be worth debug_asserting that these all fit. Since these enums are exhaustive, having an exhaustive match #[cfg(test)]ed somewhere would also be sufficient.

| alpha as u32,
}));
}

Expand Down
1 change: 1 addition & 0 deletions vello_shaders/shader/draw_leaf.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ fn main(
info[di + 6u] = bitcast<u32>(inv.translate.y);
info[di + 7u] = scene[dd];
info[di + 8u] = scene[dd + 1u];
info[di + 9u] = scene[dd + 2u];
}
case DRAWTAG_BLURRED_ROUNDED_RECT: {
info[di] = draw_flags;
Expand Down
77 changes: 56 additions & 21 deletions vello_shaders/shader/fine.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ var<storage> segments: array<Segment>;

const GRADIENT_WIDTH = 512;

const IMAGE_QUALITY_LOW = 0u;
const IMAGE_QUALITY_MEDIUM = 1u;
const IMAGE_QUALITY_HIGH = 2u;

@group(0) @binding(2)
var<storage> ptcl: array<u32>;

Expand Down Expand Up @@ -805,12 +809,17 @@ fn read_image(cmd_ix: u32) -> CmdImage {
let xlat = vec2(bitcast<f32>(info[info_offset + 4u]), bitcast<f32>(info[info_offset + 5u]));
let xy = info[info_offset + 6u];
let width_height = info[info_offset + 7u];
let sample_alpha = info[info_offset + 8u];
let alpha = f32(sample_alpha & 0xFFu) / 255.0;
let quality = sample_alpha >> 12u;
let x_extend = (sample_alpha >> 10u) & 0x3u;
let y_extend = (sample_alpha >> 8u) & 0x3u;
// The following are not intended to be bitcasts
let x = f32(xy >> 16u);
let y = f32(xy & 0xffffu);
let width = f32(width_height >> 16u);
let height = f32(width_height & 0xffffu);
return CmdImage(matrx, xlat, vec2(x, y), vec2(width, height));
return CmdImage(matrx, xlat, vec2(x, y), vec2(width, height), x_extend, y_extend, quality, alpha);
}

fn read_end_clip(cmd_ix: u32) -> CmdEndClip {
Expand Down Expand Up @@ -1146,26 +1155,52 @@ fn main(
case CMD_IMAGE: {
let image = read_image(cmd_ix);
let atlas_max = image.atlas_offset + image.extents - vec2(1.0);
for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) {
// We only need to load from the textures if the value will be used.
if area[i] != 0.0 {
let my_xy = vec2(xy.x + f32(i), xy.y);
let atlas_uv = image.matrx.xy * my_xy.x + image.matrx.zw * my_xy.y + image.xlat + image.atlas_offset - vec2(0.5);
// This currently only implements the Pad extend mode
// TODO: Support repeat and reflect
// TODO: If the image couldn't be added to the atlas (i.e. was too big), this isn't robust
let atlas_uv_clamped = clamp(atlas_uv, image.atlas_offset, atlas_max);
// We know that the floor and ceil are within the atlas area because atlas_max and
// atlas_offset are integers
let uv_quad = vec4(floor(atlas_uv_clamped), ceil(atlas_uv_clamped));
let uv_frac = fract(atlas_uv);
let a = premul_alpha(textureLoad(image_atlas, vec2<i32>(uv_quad.xy), 0));
let b = premul_alpha(textureLoad(image_atlas, vec2<i32>(uv_quad.xw), 0));
let c = premul_alpha(textureLoad(image_atlas, vec2<i32>(uv_quad.zy), 0));
let d = premul_alpha(textureLoad(image_atlas, vec2<i32>(uv_quad.zw), 0));
let fg_rgba = mix(mix(a, b, uv_frac.y), mix(c, d, uv_frac.y), uv_frac.x);
let fg_i = fg_rgba * area[i];
rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i;
let extents_inv = vec2(1.0) / image.extents;
switch image.quality {
case IMAGE_QUALITY_LOW: {
for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) {
// We only need to load from the textures if the value will be used.
if area[i] != 0.0 {
let my_xy = vec2(xy.x + f32(i), xy.y);
var atlas_uv = image.matrx.xy * my_xy.x + image.matrx.zw * my_xy.y + image.xlat;
atlas_uv.x = extend_mode(atlas_uv.x * extents_inv.x, image.x_extend_mode) * image.extents.x;
atlas_uv.y = extend_mode(atlas_uv.y * extents_inv.y, image.y_extend_mode) * image.extents.y;
atlas_uv = atlas_uv + image.atlas_offset;
// TODO: If the image couldn't be added to the atlas (i.e. was too big), this isn't robust
let atlas_uv_clamped = clamp(atlas_uv, image.atlas_offset, atlas_max);
let fg_rgba = premul_alpha(textureLoad(image_atlas, vec2<i32>(atlas_uv_clamped), 0));
Copy link
Member

@DJMcNab DJMcNab Dec 12, 2024

Choose a reason for hiding this comment

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

I think it would be useful to give a small comment as a hint here.

Suggested change
let fg_rgba = premul_alpha(textureLoad(image_atlas, vec2<i32>(atlas_uv_clamped), 0));
// Nearest Neighbor
let fg_rgba = premul_alpha(textureLoad(image_atlas, vec2<i32>(atlas_uv_clamped), 0));

let r = extend_mode(atlas_uv.x * extents_inv.x, image.x_extend_mode);
let g = extend_mode(atlas_uv.y * extents_inv.y, image.y_extend_mode);
let fg_rgba2 = vec4(r, g, 0.0, 1.0);
Comment on lines +1172 to +1174
Copy link
Member

Choose a reason for hiding this comment

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

What does this do?

let fg_i = fg_rgba * area[i] * image.alpha;
rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i;
}
}
}
case IMAGE_QUALITY_MEDIUM, default: {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
case IMAGE_QUALITY_MEDIUM, default: {
// We don't have an implementation for `IMAGE_QUALITY_HIGH` yet, just use the same as medium
case IMAGE_QUALITY_MEDIUM, default: {

for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) {
// We only need to load from the textures if the value will be used.
if area[i] != 0.0 {
let my_xy = vec2(xy.x + f32(i), xy.y);
var atlas_uv = image.matrx.xy * my_xy.x + image.matrx.zw * my_xy.y + image.xlat;
atlas_uv.x = extend_mode(atlas_uv.x * extents_inv.x, image.x_extend_mode) * image.extents.x;
atlas_uv.y = extend_mode(atlas_uv.y * extents_inv.y, image.y_extend_mode) * image.extents.y;
atlas_uv = atlas_uv + image.atlas_offset - vec2(0.5);
// TODO: If the image couldn't be added to the atlas (i.e. was too big), this isn't robust
let atlas_uv_clamped = clamp(atlas_uv, image.atlas_offset, atlas_max);
// We know that the floor and ceil are within the atlas area because atlas_max and
// atlas_offset are integers
let uv_quad = vec4(floor(atlas_uv_clamped), ceil(atlas_uv_clamped));
let uv_frac = fract(atlas_uv);
let a = premul_alpha(textureLoad(image_atlas, vec2<i32>(uv_quad.xy), 0));
let b = premul_alpha(textureLoad(image_atlas, vec2<i32>(uv_quad.xw), 0));
let c = premul_alpha(textureLoad(image_atlas, vec2<i32>(uv_quad.zy), 0));
let d = premul_alpha(textureLoad(image_atlas, vec2<i32>(uv_quad.zw), 0));
let fg_rgba = mix(mix(a, b, uv_frac.y), mix(c, d, uv_frac.y), uv_frac.x);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
let fg_rgba = mix(mix(a, b, uv_frac.y), mix(c, d, uv_frac.y), uv_frac.x);
// Bilinear sampling
let fg_rgba = mix(mix(a, b, uv_frac.y), mix(c, d, uv_frac.y), uv_frac.x);

let fg_i = fg_rgba * area[i] * image.alpha;
rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i;
}
}
}
}
cmd_ix += 2u;
Expand Down
2 changes: 1 addition & 1 deletion vello_shaders/shader/shared/drawtag.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const DRAWTAG_FILL_COLOR = 0x44u;
const DRAWTAG_FILL_LIN_GRADIENT = 0x114u;
const DRAWTAG_FILL_RAD_GRADIENT = 0x29cu;
const DRAWTAG_FILL_SWEEP_GRADIENT = 0x254u;
const DRAWTAG_FILL_IMAGE = 0x248u;
const DRAWTAG_FILL_IMAGE = 0x28Cu;
const DRAWTAG_BLURRED_ROUNDED_RECT = 0x2d4u;
const DRAWTAG_BEGIN_CLIP = 0x9u;
const DRAWTAG_END_CLIP = 0x21u;
Expand Down
4 changes: 4 additions & 0 deletions vello_shaders/shader/shared/ptcl.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ struct CmdImage {
xlat: vec2<f32>,
atlas_offset: vec2<f32>,
extents: vec2<f32>,
x_extend_mode: u32,
y_extend_mode: u32,
quality: u32,
alpha: f32,
}

struct CmdEndClip {
Expand Down
1 change: 1 addition & 0 deletions vello_shaders/src/cpu/draw_leaf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ fn draw_leaf_main(
info[di + 6] = f32::to_bits(xform.0[5]);
info[di + 7] = scene[dd as usize];
info[di + 8] = scene[dd as usize + 1];
info[di + 9] = scene[dd as usize + 2];
}
DrawTag::BLUR_RECT => {
info[di] = draw_flags;
Expand Down
6 changes: 3 additions & 3 deletions vello_tests/src/compare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use anyhow::{anyhow, bail, Result};
use image::DynamicImage;
use nv_flip::FlipPool;
use vello::{
peniko::{Format, Image},
peniko::{Image, ImageFormat},
Scene,
};

Expand Down Expand Up @@ -105,8 +105,8 @@ pub async fn compare_gpu_cpu(scene: Scene, mut params: TestParams) -> Result<Gpu
assert!(gpu_rendered.width == cpu_rendered.width && gpu_rendered.height == cpu_rendered.height,);

// Compare the images using nv-flip
assert_eq!(cpu_rendered.format, Format::Rgba8);
assert_eq!(gpu_rendered.format, Format::Rgba8);
assert_eq!(cpu_rendered.format, ImageFormat::Rgba8);
assert_eq!(gpu_rendered.format, ImageFormat::Rgba8);
let gpu_rendered_data: DynamicImage = image::RgbaImage::from_raw(
cpu_rendered.width,
cpu_rendered.height,
Expand Down
4 changes: 2 additions & 2 deletions vello_tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ use std::sync::Arc;
use anyhow::{anyhow, bail, Result};
use scenes::{ExampleScene, ImageCache, SceneParams, SimpleText};
use vello::kurbo::{Affine, Vec2};
use vello::peniko::{color::palette, Blob, Color, Format, Image};
use vello::peniko::{color::palette, Blob, Color, Image, ImageFormat};
use vello::wgpu::{
self, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, ImageCopyBuffer,
TextureDescriptor, TextureFormat, TextureUsages,
Expand Down Expand Up @@ -190,7 +190,7 @@ pub async fn get_scene_image(params: &TestParams, scene: &Scene) -> Result<Image
result_unpadded.extend(&data[start..start + (width * 4) as usize]);
}
let data = Blob::new(Arc::new(result_unpadded));
let image = Image::new(data, Format::Rgba8, width, height);
let image = Image::new(data, ImageFormat::Rgba8, width, height);
Ok(image)
}

Expand Down
4 changes: 2 additions & 2 deletions vello_tests/src/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::{
use image::{DynamicImage, ImageError};
use nv_flip::FlipPool;
use vello::{
peniko::{Format, Image},
peniko::{Image, ImageFormat},
Scene,
};

Expand Down Expand Up @@ -284,7 +284,7 @@ pub fn snapshot_test_image(
unreachable!();
}
// Compare the images using nv-flip
assert_eq!(raw_rendered.format, Format::Rgba8);
assert_eq!(raw_rendered.format, ImageFormat::Rgba8);
let rendered_data: DynamicImage = image::RgbaImage::from_raw(
raw_rendered.width,
raw_rendered.height,
Expand Down
Loading
Loading