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

CI and local smoke testing #439

Merged
merged 13 commits into from
Feb 15, 2024
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ jobs:
strategy:
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
include:
- os: ubuntu-latest
gpu: 'yes'
- os: macos-latest
gpu: 'no'
- os: windows-latest
# TODO: The windows runners theoretically have CPU fallback for GPUs, but
# this failed in initial testing
gpu: 'no'
name: cargo clippy + test
steps:
- uses: actions/checkout@v4
Expand All @@ -57,6 +66,16 @@ jobs:
if: matrix.os == 'ubuntu-latest'
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev

# Adapted from https://github.com/bevyengine/bevy/blob/b446374392adc70aceb92621b080d1a6cf7a7392/.github/workflows/validation-jobs.yml#L74-L79
- name: install xvfb, llvmpipe and lavapipe
if: matrix.os == 'ubuntu-latest'
# https://launchpad.net/~kisak/+archive/ubuntu/turtle
run: |
sudo apt-get update -y -qq
sudo add-apt-repository ppa:kisak/turtle -y
sudo apt-get update
sudo apt install -y xvfb libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers

- name: cargo clippy (no default features)
run: cargo clippy --workspace --lib --bins --no-default-features -- -D warnings

Expand All @@ -78,6 +97,8 @@ jobs:
# At the time of writing, we don't have any tests. Nevertheless, it's better to still run this
- name: cargo test
run: cargo test --workspace --all-features
env:
VELLO_CI_GPU_SUPPORT: ${{ matrix.gpu }}

clippy-stable-wasm:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ resolver = "2"
members = [
"crates/encoding",
"crates/shaders",
"crates/tests",

"integrations/vello_svg",

Expand Down
11 changes: 2 additions & 9 deletions crates/encoding/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ pub struct RenderConfig {

impl RenderConfig {
pub fn new(layout: &Layout, width: u32, height: u32, base_color: &peniko::Color) -> Self {
let new_width = next_multiple_of(width, TILE_WIDTH);
let new_height = next_multiple_of(height, TILE_HEIGHT);
let new_width = width.next_multiple_of(TILE_WIDTH);
let new_height = height.next_multiple_of(TILE_HEIGHT);
let width_in_tiles = new_width / TILE_WIDTH;
let height_in_tiles = new_height / TILE_HEIGHT;
let n_path_tags = layout.path_tags_size();
Expand Down Expand Up @@ -352,10 +352,3 @@ impl BufferSizes {
const fn align_up(len: u32, alignment: u32) -> u32 {
len + (len.wrapping_neg() & (alignment - 1))
}

const fn next_multiple_of(val: u32, rhs: u32) -> u32 {
match val % rhs {
0 => val,
r => val + (rhs - r),
}
}
19 changes: 19 additions & 0 deletions crates/tests/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "vello_tests"
edition.workspace = true
version.workspace = true
license.workspace = true
repository.workspace = true
publish = false

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
vello = { path = "../.." }
image = "0.24.5"
anyhow = { workspace = true }

wgpu = { workspace = true }
pollster = { workspace = true }
png = "0.17.7"
futures-intrusive = "0.5.0"
17 changes: 17 additions & 0 deletions crates/tests/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use std::env;

fn main() {
println!("cargo:rerun-if-env-changed=VELLO_CI_GPU_SUPPORT");
if let Ok(mut value) = env::var("VELLO_CI_GPU_SUPPORT") {
value.make_ascii_lowercase();
match &*value {
"yes" | "y" => {}
"no" | "n" => {
println!("cargo:rustc-cfg=skip_gpu_tests");
}
_ => {
println!("cargo:cargo:warning=VELLO_CI_GPU_SUPPORT should be set to yes/y or no/n");
}
}
}
}
2 changes: 2 additions & 0 deletions crates/tests/debug_outputs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.gitignore
189 changes: 189 additions & 0 deletions crates/tests/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
use std::{env, fs::File, path::Path, sync::Arc};

use anyhow::{anyhow, bail, Result};
use vello::{
block_on_wgpu,
peniko::{Blob, Color, Format, Image},
util::RenderContext,
RendererOptions, Scene,
};
use wgpu::{
BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, ImageCopyBuffer,
TextureDescriptor, TextureFormat, TextureUsages,
};

pub fn decode_image(data: &[u8]) -> Result<Image> {
let image = image::io::Reader::new(std::io::Cursor::new(data))
.with_guessed_format()?
.decode()?;
let width = image.width();
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))
}

pub struct TestParams {
pub width: u32,
pub height: u32,
pub base_colour: Color,
pub use_cpu: bool,
pub name: String,
}

impl TestParams {
pub fn new(name: impl Into<String>, width: u32, height: u32) -> Self {
TestParams {
width,
height,
base_colour: Color::BLACK,
use_cpu: false,
name: name.into(),
}
}
}

pub fn render_sync(scene: Scene, params: &TestParams) -> Result<Image> {
pollster::block_on(render(scene, params))
}

pub async fn render(scene: Scene, params: &TestParams) -> Result<Image> {
let mut context = RenderContext::new()
.or_else(|_| bail!("Got non-Send/Sync error from creating render context"))?;
let device_id = context
.device(None)
.await
.ok_or_else(|| anyhow!("No compatible device found"))?;
let device_handle = &mut context.devices[device_id];
let device = &device_handle.device;
let queue = &device_handle.queue;
let mut renderer = vello::Renderer::new(
device,
RendererOptions {
surface_format: None,
use_cpu: params.use_cpu,
antialiasing_support: vello::AaSupport::area_only(),
},
)
.or_else(|_| bail!("Got non-Send/Sync error from creating renderer"))?;

let width = params.width;
let height = params.height;
let render_params = vello::RenderParams {
base_color: params.base_colour,
width,
height,
antialiasing_method: vello::AaConfig::Area,
};
let size = Extent3d {
width,
height,
depth_or_array_layers: 1,
};
let target = device.create_texture(&TextureDescriptor {
label: Some("Target texture"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::STORAGE_BINDING | TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = target.create_view(&wgpu::TextureViewDescriptor::default());
renderer
.render_to_texture(device, queue, &scene, &view, &render_params)
.or_else(|_| bail!("Got non-Send/Sync error from rendering"))?;
let padded_byte_width = (width * 4).next_multiple_of(256);
let buffer_size = padded_byte_width as u64 * height as u64;
let buffer = device.create_buffer(&BufferDescriptor {
label: Some("val"),
size: buffer_size,
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("Copy out buffer"),
});
encoder.copy_texture_to_buffer(
target.as_image_copy(),
ImageCopyBuffer {
buffer: &buffer,
layout: wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(padded_byte_width),
rows_per_image: None,
},
},
size,
);
queue.submit([encoder.finish()]);
let buf_slice = buffer.slice(..);

let (sender, receiver) = futures_intrusive::channel::shared::oneshot_channel();
buf_slice.map_async(wgpu::MapMode::Read, move |v| sender.send(v).unwrap());
if let Some(recv_result) = block_on_wgpu(device, receiver.receive()) {
recv_result?;
} else {
bail!("channel was closed");
}

let data = buf_slice.get_mapped_range();
let mut result_unpadded = Vec::<u8>::with_capacity((width * height * 4).try_into()?);
for row in 0..height {
let start = (row * padded_byte_width).try_into()?;
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);
if should_debug_png(&params.name, params.use_cpu) {
let suffix = if params.use_cpu { "cpu" } else { "gpu" };
let name = format!("{}_{suffix}", &params.name);
debug_png(&image, &name, params)?;
}
Ok(image)
}

pub fn debug_png(image: &Image, name: &str, params: &TestParams) -> Result<()> {
let width = params.width;
let height = params.height;
let out_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("debug_outputs")
.join(name)
.with_extension("png");
let mut file = File::create(&out_path)?;
let mut encoder = png::Encoder::new(&mut file, width, height);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder.write_header()?;
writer.write_image_data(image.data.data())?;
writer.finish()?;
println!("Wrote result ({width}x{height}) to {out_path:?}");

Ok(())
}

pub fn should_debug_png(name: &str, use_cpu: bool) -> bool {
if let Ok(val) = env::var("VELLO_DEBUG_TEST") {
if val.eq_ignore_ascii_case("all")
|| val.eq_ignore_ascii_case("cpu") && use_cpu
|| val.eq_ignore_ascii_case("gpu") && !use_cpu
{
return true;
}
for test in val.split(',') {
if use_cpu {
let test_name = test.trim_end_matches("_cpu");
if test_name.eq_ignore_ascii_case(name) {
return true;
}
} else {
let test_name = test.trim_end_matches("_gpu");
if test_name.eq_ignore_ascii_case(name) {
return true;
}
}
}
}
false
}
55 changes: 55 additions & 0 deletions crates/tests/tests/smoke.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use vello::{
kurbo::{Affine, Rect},
peniko::{Brush, Color, Format},
Scene,
};
use vello_tests::TestParams;

#[test]
#[cfg_attr(skip_gpu_tests, ignore)]
fn simple_square_gpu() {
simple_square(false)
}

#[test]
// The fine shader still requires a GPU, and so we still get a wgpu device
// skip this for now
#[cfg_attr(skip_gpu_tests, ignore)]
fn simple_square_cpu() {
simple_square(true)
}

fn simple_square(use_cpu: bool) {
let mut scene = Scene::new();
scene.fill(
vello::peniko::Fill::NonZero,
Affine::IDENTITY,
&Brush::Solid(Color::RED),
None,
&Rect::from_center_size((100., 100.), (50., 50.)),
);
let params = TestParams {
use_cpu,
..TestParams::new("simple_square", 150, 150)
};
let image = vello_tests::render_sync(scene, &params).unwrap();
assert_eq!(image.format, Format::Rgba8);
let mut red_count = 0;
let mut black_count = 0;
for pixel in image.data.data().chunks_exact(4) {
let &[r, g, b, a] = pixel else { unreachable!() };
let is_red = r == 255 && g == 0 && b == 0 && a == 255;
let is_black = r == 0 && g == 0 && b == 0 && a == 255;
if !is_red && !is_black {
panic!("{pixel:?}");
}
match (is_red, is_black) {
(true, true) => unreachable!(),
(true, false) => red_count += 1,
(false, true) => black_count += 1,
(false, false) => panic!("Got unexpected pixel {pixel:?}"),
}
}
assert_eq!(red_count, 50 * 50);
assert_eq!(black_count, 150 * 150 - 50 * 50);
}
9 changes: 1 addition & 8 deletions examples/headless/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,7 @@ async fn render(mut scenes: SceneSet, index: usize, args: &Args) -> Result<()> {
renderer
.render_to_texture(device, queue, &scene, &view, &render_params)
.or_else(|_| bail!("Got non-Send/Sync error from rendering"))?;
// (width * 4).next_multiple_of(256)
let padded_byte_width = {
let w = width * 4;
match w % 256 {
0 => w,
r => w + (256 - r),
}
};
let padded_byte_width = (width * 4).next_multiple_of(256);
let buffer_size = padded_byte_width as u64 * height as u64;
let buffer = device.create_buffer(&BufferDescriptor {
label: Some("val"),
Expand Down
10 changes: 7 additions & 3 deletions src/cpu_shader/flatten.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,8 +526,12 @@ fn compute_tag_monoid(ix: usize, pathtags: &[u32], tag_monoids: &[PathMonoid]) -
}
// We no longer encode an initial transform and style so these
// are off by one.
tm.trans_ix -= 1;
tm.style_ix -= core::mem::size_of::<Style>() as u32 / 4;
// We wrap here because these values will return to positive values later
// (when we add style_base)
tm.trans_ix = tm.trans_ix.wrapping_sub(1);
tm.style_ix = tm
.style_ix
.wrapping_sub(core::mem::size_of::<Style>() as u32 / 4);
PathTagData {
tag_byte,
monoid: tm,
Expand Down Expand Up @@ -641,7 +645,7 @@ fn flatten_main(
let path_ix = tag.monoid.path_ix;
let style_ix = tag.monoid.style_ix;
let trans_ix = tag.monoid.trans_ix;
let style_flags = scene[(config.layout.style_base + style_ix) as usize];
let style_flags = scene[(config.layout.style_base.wrapping_add(style_ix)) as usize];
if (tag.tag_byte & PATH_TAG_PATH) != 0 {
let out = &mut path_bboxes[path_ix as usize];
out.draw_flags = if (style_flags & Style::FLAGS_FILL_BIT) == 0 {
Expand Down