Files
wgpu/tests/gpu-tests/external_texture.rs
2025-02-23 14:06:34 -05:00

354 lines
14 KiB
Rust

#![cfg(all(target_arch = "wasm32", not(target_os = "emscripten")))]
use wasm_bindgen::JsCast;
use wgpu::ExternalImageSource;
use wgpu_test::{fail_if, gpu_test, GpuTestConfiguration};
#[gpu_test]
static IMAGE_BITMAP_IMPORT: GpuTestConfiguration =
GpuTestConfiguration::new().run_async(|ctx| async move {
let image_encoded = include_bytes!("3x3_colors.png");
// Create an array-of-arrays for Blob's constructor
let array = js_sys::Array::new();
array.push(&js_sys::Uint8Array::from(&image_encoded[..]));
// We're passing an array of Uint8Arrays
let blob = web_sys::Blob::new_with_u8_array_sequence(&array).unwrap();
// Parse the image from the blob
// Because we need to call the function in a way that isn't bound by
// web_sys, we need to manually construct the options struct and call
// the function.
let image_bitmap_function: js_sys::Function = web_sys::window()
.unwrap()
.get("createImageBitmap")
.unwrap()
.dyn_into()
.unwrap();
let options_arg = js_sys::Object::new();
js_sys::Reflect::set(
&options_arg,
&wasm_bindgen::JsValue::from_str("premultiplyAlpha"),
&wasm_bindgen::JsValue::from_str("none"),
)
.unwrap();
let image_bitmap_promise: js_sys::Promise = image_bitmap_function
.call2(&wasm_bindgen::JsValue::UNDEFINED, &blob, &options_arg)
.unwrap()
.dyn_into()
.unwrap();
// Wait for the parsing to be done
let image_bitmap: web_sys::ImageBitmap =
wasm_bindgen_futures::JsFuture::from(image_bitmap_promise)
.await
.unwrap()
.dyn_into()
.unwrap();
// Sanity checks
assert_eq!(image_bitmap.width(), 3);
assert_eq!(image_bitmap.height(), 3);
// Due to restrictions with premultiplication with ImageBitmaps, we also create an HtmlCanvasElement
// by drawing the image bitmap onto the canvas.
let canvas: web_sys::HtmlCanvasElement = web_sys::window()
.unwrap()
.document()
.unwrap()
.create_element("canvas")
.unwrap()
.dyn_into()
.unwrap();
canvas.set_width(3);
canvas.set_height(3);
let d2_context: web_sys::CanvasRenderingContext2d = canvas
.get_context("2d")
.unwrap()
.unwrap()
.dyn_into()
.unwrap();
d2_context
.draw_image_with_image_bitmap(&image_bitmap, 0.0, 0.0)
.unwrap();
// Decode it cpu side
let raw_image = image::load_from_memory_with_format(image_encoded, image::ImageFormat::Png)
.unwrap()
.into_rgba8();
// Set of test cases to test with image import
#[derive(Debug, Copy, Clone)]
enum TestCase {
// Import the image as normal
Normal,
// Sets the FlipY flag. Deals with global state on GLES, so run before other tests to ensure it's reset.
//
// Only works on canvases.
FlipY,
// Sets the premultiplied alpha flag. Deals with global state on GLES, so run before other tests to ensure it's reset.
//
// Only works on canvases.
Premultiplied,
// Sets the color space to P3.
//
// Only works on canvases.
ColorSpace,
// Sets the premultiplied alpha flag. Deals with global state on GLES, so run before other tests to ensure it's reset.
// Set both the input offset and output offset to 1 in x, so the first column is omitted.
TrimLeft,
// Set the size to 2 in x, so the last column is omitted
TrimRight,
// Set only the output offset to 1, so the second column gets the first column's data.
SlideRight,
// Try to copy from out of bounds of the source image
SourceOutOfBounds,
// Try to copy from out of bounds of the destination image
DestOutOfBounds,
// Try to copy more than one slice from the source
MultiSliceCopy,
// Copy into the second slice of a 2D array texture,
SecondSliceCopy,
}
let sources = [
ExternalImageSource::ImageBitmap(image_bitmap),
ExternalImageSource::HTMLCanvasElement(canvas),
];
let cases = [
TestCase::Normal,
TestCase::FlipY,
TestCase::Premultiplied,
TestCase::ColorSpace,
TestCase::TrimLeft,
TestCase::TrimRight,
TestCase::SlideRight,
TestCase::SourceOutOfBounds,
TestCase::DestOutOfBounds,
TestCase::MultiSliceCopy,
TestCase::SecondSliceCopy,
];
for source in sources {
for case in cases {
// Copy the data, so we can modify it for tests
let mut raw_image = raw_image.clone();
// The origin used for the external copy on the source side.
let mut src_origin = wgpu::Origin2d::ZERO;
// If the source should be flipped in Y
let mut src_flip_y = false;
// The origin used for the external copy on the destination side.
let mut dest_origin = wgpu::Origin3d::ZERO;
// The layer the external image's data should end up in.
let mut dest_data_layer = 0;
// Color space the destination is in.
let mut dest_color_space = wgpu::PredefinedColorSpace::Srgb;
// If the destination image is premultiplied.
let mut dest_premultiplied = false;
// Size of the external copy
let mut copy_size = wgpu::Extent3d {
width: 3,
height: 3,
depth_or_array_layers: 1,
};
// Width of the destination texture
let mut dest_width = 3;
// Layer count of the destination texture
let mut dest_layers = 1;
// If the test is supposed to be valid call to copyExternal.
let mut valid = true;
match case {
TestCase::Normal => {}
TestCase::FlipY => {
valid = !matches!(source, wgpu::ExternalImageSource::ImageBitmap(_));
src_flip_y = true;
for x in 0..3 {
let top = raw_image[(x, 0)];
let bottom = raw_image[(x, 2)];
raw_image[(x, 0)] = bottom;
raw_image[(x, 2)] = top;
}
}
TestCase::Premultiplied => {
valid = !matches!(source, wgpu::ExternalImageSource::ImageBitmap(_));
dest_premultiplied = true;
for pixel in raw_image.pixels_mut() {
let mut float_pix = pixel.0.map(|v| v as f32 / 255.0);
float_pix[0] *= float_pix[3];
float_pix[1] *= float_pix[3];
float_pix[2] *= float_pix[3];
pixel.0 = float_pix.map(|v| (v * 255.0).round() as u8);
}
}
TestCase::ColorSpace => {
valid = ctx
.adapter_downlevel_capabilities
.flags
.contains(wgpu::DownlevelFlags::UNRESTRICTED_EXTERNAL_TEXTURE_COPIES);
dest_color_space = wgpu::PredefinedColorSpace::DisplayP3;
// As we don't test, we don't bother converting the color spaces
// in the image as that's relatively annoying.
}
TestCase::TrimLeft => {
valid = ctx
.adapter_downlevel_capabilities
.flags
.contains(wgpu::DownlevelFlags::UNRESTRICTED_EXTERNAL_TEXTURE_COPIES);
src_origin.x = 1;
dest_origin.x = 1;
copy_size.width = 2;
for y in 0..3 {
raw_image[(0, y)].0 = [0; 4];
}
}
TestCase::TrimRight => {
copy_size.width = 2;
for y in 0..3 {
raw_image[(2, y)].0 = [0; 4];
}
}
TestCase::SlideRight => {
dest_origin.x = 1;
copy_size.width = 2;
for x in (1..3).rev() {
for y in 0..3 {
raw_image[(x, y)].0 = raw_image[(x - 1, y)].0;
}
}
for y in 0..3 {
raw_image[(0, y)].0 = [0; 4];
}
}
TestCase::SourceOutOfBounds => {
valid = false;
// It's now in bounds for the destination
dest_width = 4;
copy_size.width = 4;
}
TestCase::DestOutOfBounds => {
valid = false;
// It's now out bounds for the destination
dest_width = 2;
}
TestCase::MultiSliceCopy => {
valid = false;
copy_size.depth_or_array_layers = 2;
dest_layers = 2;
}
TestCase::SecondSliceCopy => {
dest_origin.z = 1;
dest_data_layer = 1;
dest_layers = 2;
}
}
let texture = ctx.device.create_texture(&wgpu::TextureDescriptor {
label: Some("import dest"),
size: wgpu::Extent3d {
width: dest_width,
height: 3,
depth_or_array_layers: dest_layers,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
fail_if(
&ctx.device,
!valid,
|| {
ctx.queue.copy_external_image_to_texture(
&wgpu::CopyExternalImageSourceInfo {
source: source.clone(),
origin: src_origin,
flip_y: src_flip_y,
},
wgpu::CopyExternalImageDestInfo {
texture: &texture,
mip_level: 0,
origin: dest_origin,
aspect: wgpu::TextureAspect::All,
color_space: dest_color_space,
premultiplied_alpha: dest_premultiplied,
},
copy_size,
);
},
None,
);
let readback_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("readback buffer"),
size: 4 * 64 * 3,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut encoder = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: 0,
y: 0,
z: dest_data_layer,
},
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback_buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(256),
rows_per_image: None,
},
},
wgpu::Extent3d {
width: dest_width,
height: 3,
depth_or_array_layers: 1,
},
);
ctx.queue.submit(Some(encoder.finish()));
readback_buffer
.slice(..)
.map_async(wgpu::MapMode::Read, |_| ());
ctx.async_poll(wgpu::PollType::wait()).await.unwrap();
let buffer = readback_buffer.slice(..).get_mapped_range();
// 64 because of 256 byte alignment / 4.
let gpu_image = image::RgbaImage::from_vec(64, 3, buffer.to_vec()).unwrap();
let gpu_image_cropped =
image::imageops::crop_imm(&gpu_image, 0, 0, 3, 3).to_image();
if valid {
assert_eq!(
raw_image, gpu_image_cropped,
"Failed on test case {case:?} {source:?}"
);
} else {
assert_ne!(
raw_image, gpu_image_cropped,
"Failed on test case {case:?} {source:?}"
);
}
}
}
});