test(wgpu): Add WGPU render test
Also rename triangle -> render
This commit is contained in:
parent
2904a2ac10
commit
6de2de8d12
15
Cargo.lock
generated
15
Cargo.lock
generated
|
@ -1471,6 +1471,18 @@ dependencies = [
|
|||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image-compare"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96cd73af13ae2e7220a1c02fe7d6bb53be50612ba7fabbb5c88e7753645f1f3c"
|
||||
dependencies = [
|
||||
"image",
|
||||
"itertools",
|
||||
"rayon",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image-webp"
|
||||
version = "0.1.3"
|
||||
|
@ -1952,10 +1964,13 @@ dependencies = [
|
|||
"ash",
|
||||
"gfx-maths",
|
||||
"image",
|
||||
"image-compare",
|
||||
"librashader",
|
||||
"librashader-runtime",
|
||||
"objc2 0.5.2",
|
||||
"objc2-metal",
|
||||
"parking_lot",
|
||||
"pollster",
|
||||
"wgpu",
|
||||
"wgpu-types",
|
||||
"windows 0.58.0",
|
||||
|
|
|
@ -13,6 +13,9 @@ image = "0.25.2"
|
|||
gfx-maths = "0.2.8"
|
||||
|
||||
ash = "0.38.0+1.3.281"
|
||||
pollster = "0.3.0"
|
||||
parking_lot = "0.12.3"
|
||||
image-compare = "0.4.1"
|
||||
|
||||
[target.'cfg(windows)'.dependencies.windows]
|
||||
workspace = true
|
||||
|
|
|
@ -1,16 +1,3 @@
|
|||
mod triangle;
|
||||
|
||||
pub fn add(left: u64, right: u64) -> u64 {
|
||||
left + right
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = add(2, 2);
|
||||
assert_eq!(result, 4);
|
||||
}
|
||||
}
|
||||
/// Render tests
|
||||
pub mod render;
|
||||
|
|
66
librashader-test/src/render/mod.rs
Normal file
66
librashader-test/src/render/mod.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
pub mod d3d11;
|
||||
pub mod wgpu;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
/// Test harness to set up a device, render a triangle, and apply a shader
|
||||
pub trait RenderTest {
|
||||
/// Render a shader onto an image buffer, applying the provided shader.
|
||||
///
|
||||
/// The test should render in linear colour space for proper comparison against
|
||||
/// backends.
|
||||
///
|
||||
/// For testing purposes, it is often that a single image will be reused with multiple
|
||||
/// shader presets, so the actual image that a shader will be applied to
|
||||
/// will often be part of the test harness object.
|
||||
fn render(
|
||||
&self,
|
||||
path: impl AsRef<Path>,
|
||||
frame_count: usize,
|
||||
) -> anyhow::Result<image::RgbaImage>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::render::RenderTest;
|
||||
use image::codecs::png::PngEncoder;
|
||||
use std::fs::File;
|
||||
|
||||
const IMAGE_PATH: &str = "../triangle.png";
|
||||
const FILTER_PATH: &str = "../test/shaders_slang/crt/crt-royale.slangp";
|
||||
|
||||
#[test]
|
||||
pub fn test_d3d11() -> anyhow::Result<()> {
|
||||
let d3d11 = super::d3d11::Direct3D11::new(IMAGE_PATH)?;
|
||||
let image = d3d11.render(FILTER_PATH, 100)?;
|
||||
|
||||
let out = File::create("out.png")?;
|
||||
image.write_with_encoder(PngEncoder::new(out))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_wgpu() -> anyhow::Result<()> {
|
||||
let wgpu = super::wgpu::Wgpu::new(IMAGE_PATH)?;
|
||||
let image = wgpu.render(FILTER_PATH, 100)?;
|
||||
|
||||
let out = File::create("out.png")?;
|
||||
image.write_with_encoder(PngEncoder::new(out))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn compare() -> anyhow::Result<()> {
|
||||
let d3d11 = super::d3d11::Direct3D11::new(IMAGE_PATH)?;
|
||||
let wgpu = super::wgpu::Wgpu::new(IMAGE_PATH)?;
|
||||
|
||||
let wgpu_image = wgpu.render(FILTER_PATH, 100)?;
|
||||
let d3d11_image = d3d11.render(FILTER_PATH, 100)?;
|
||||
|
||||
let similarity = image_compare::rgba_hybrid_compare(&wgpu_image, &d3d11_image)?;
|
||||
|
||||
assert!(similarity.score > 0.95);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
241
librashader-test/src/render/wgpu.rs
Normal file
241
librashader-test/src/render/wgpu.rs
Normal file
|
@ -0,0 +1,241 @@
|
|||
use crate::render::RenderTest;
|
||||
use anyhow::anyhow;
|
||||
use image::RgbaImage;
|
||||
use librashader::runtime::wgpu::*;
|
||||
use librashader::runtime::Viewport;
|
||||
use librashader_runtime::image::{Image, UVDirection};
|
||||
use std::io::{Cursor, Write};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use wgpu::{Adapter, Device, Instance, Queue, Texture};
|
||||
use wgpu_types::{
|
||||
BufferAddress, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, ImageCopyBuffer,
|
||||
ImageDataLayout, Maintain, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
|
||||
};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
|
||||
pub struct Wgpu {
|
||||
instance: Instance,
|
||||
adapter: Adapter,
|
||||
device: Arc<Device>,
|
||||
queue: Arc<Queue>,
|
||||
image: Image,
|
||||
texture: Arc<Texture>,
|
||||
}
|
||||
|
||||
struct BufferDimensions {
|
||||
width: usize,
|
||||
height: usize,
|
||||
unpadded_bytes_per_row: usize,
|
||||
padded_bytes_per_row: usize,
|
||||
}
|
||||
|
||||
impl BufferDimensions {
|
||||
fn new(width: usize, height: usize) -> Self {
|
||||
let bytes_per_pixel = size_of::<u32>();
|
||||
let unpadded_bytes_per_row = width * bytes_per_pixel;
|
||||
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
|
||||
let padded_bytes_per_row_padding = (align - unpadded_bytes_per_row % align) % align;
|
||||
let padded_bytes_per_row = unpadded_bytes_per_row + padded_bytes_per_row_padding;
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
unpadded_bytes_per_row,
|
||||
padded_bytes_per_row,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderTest for Wgpu {
|
||||
fn render(&self, path: impl AsRef<Path>, frame_count: usize) -> anyhow::Result<RgbaImage> {
|
||||
let mut chain = FilterChain::load_from_path(
|
||||
path,
|
||||
Arc::clone(&self.device),
|
||||
Arc::clone(&self.queue),
|
||||
Some(&FilterChainOptions {
|
||||
force_no_mipmaps: false,
|
||||
enable_cache: false,
|
||||
adapter_info: None,
|
||||
}),
|
||||
)?;
|
||||
|
||||
let mut cmd = self
|
||||
.device
|
||||
.create_command_encoder(&CommandEncoderDescriptor { label: None });
|
||||
|
||||
let output_tex = self.device.create_texture(&TextureDescriptor {
|
||||
label: None,
|
||||
size: self.texture.size(),
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba8Unorm,
|
||||
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::COPY_SRC,
|
||||
view_formats: &[wgpu::TextureFormat::Rgba8Unorm],
|
||||
});
|
||||
|
||||
let buffer_dimensions =
|
||||
BufferDimensions::new(output_tex.width() as usize, output_tex.height() as usize);
|
||||
let output_buf = Arc::new(self.device.create_buffer(&BufferDescriptor {
|
||||
label: None,
|
||||
size: (buffer_dimensions.padded_bytes_per_row * buffer_dimensions.height)
|
||||
as BufferAddress, // 4bpp
|
||||
usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ,
|
||||
mapped_at_creation: false,
|
||||
}));
|
||||
|
||||
let view = output_tex.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let output = WgpuOutputView::new_from_raw(
|
||||
&view,
|
||||
output_tex.size().into(),
|
||||
TextureFormat::Rgba8Unorm,
|
||||
);
|
||||
|
||||
chain.frame(
|
||||
Arc::clone(&self.texture),
|
||||
&Viewport::new_render_target_sized_origin(output, None)?,
|
||||
&mut cmd,
|
||||
frame_count,
|
||||
None,
|
||||
)?;
|
||||
|
||||
cmd.copy_texture_to_buffer(
|
||||
output_tex.as_image_copy(),
|
||||
ImageCopyBuffer {
|
||||
buffer: &output_buf,
|
||||
layout: ImageDataLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(buffer_dimensions.padded_bytes_per_row as u32),
|
||||
rows_per_image: None,
|
||||
},
|
||||
},
|
||||
output_tex.size(),
|
||||
);
|
||||
|
||||
let si = self.queue.submit([cmd.finish()]);
|
||||
self.device.poll(Maintain::WaitForSubmissionIndex(si));
|
||||
|
||||
let capturable = Arc::clone(&output_buf);
|
||||
|
||||
let mut pixels = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
let pixels_async = Arc::clone(&pixels);
|
||||
output_buf
|
||||
.slice(..)
|
||||
.map_async(wgpu::MapMode::Read, move |r| {
|
||||
if r.is_ok() {
|
||||
let buffer = capturable.slice(..).get_mapped_range();
|
||||
let mut pixels = pixels_async.lock();
|
||||
pixels.resize(buffer.len(), 0);
|
||||
|
||||
let mut cursor = Cursor::new(pixels.deref_mut());
|
||||
for chunk in buffer.chunks(buffer_dimensions.padded_bytes_per_row) {
|
||||
cursor
|
||||
.write_all(&chunk[..buffer_dimensions.unpadded_bytes_per_row])
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
cursor.into_inner();
|
||||
}
|
||||
capturable.unmap();
|
||||
});
|
||||
|
||||
self.device.poll(Maintain::Wait);
|
||||
|
||||
if pixels.lock().len() == 0 {
|
||||
return Err(anyhow!("failed to copy pixels from buffer"));
|
||||
}
|
||||
|
||||
let image = RgbaImage::from_raw(
|
||||
output_tex.width(),
|
||||
output_tex.height(),
|
||||
pixels.lock().to_vec(),
|
||||
)
|
||||
.ok_or(anyhow!("Unable to create image from data"))?;
|
||||
|
||||
Ok(image)
|
||||
}
|
||||
}
|
||||
|
||||
impl Wgpu {
|
||||
pub fn new(image: impl AsRef<Path>) -> anyhow::Result<Self> {
|
||||
pollster::block_on(async {
|
||||
let instance = wgpu::Instance::default();
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::default(),
|
||||
compatible_surface: None,
|
||||
force_fallback_adapter: false,
|
||||
})
|
||||
.await
|
||||
.ok_or(anyhow!("Couldn't request WGPU adapter"))?;
|
||||
|
||||
let (device, queue) = adapter
|
||||
.request_device(
|
||||
&wgpu::DeviceDescriptor {
|
||||
required_features: wgpu::Features::ADDRESS_MODE_CLAMP_TO_BORDER
|
||||
| wgpu::Features::PIPELINE_CACHE
|
||||
| wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES
|
||||
| wgpu::Features::FLOAT32_FILTERABLE,
|
||||
required_limits: wgpu::Limits::default(),
|
||||
label: None,
|
||||
memory_hints: Default::default(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let (image, texture) = Self::load_image(&device, &queue, image)?;
|
||||
|
||||
Ok(Self {
|
||||
instance,
|
||||
adapter,
|
||||
device: Arc::new(device),
|
||||
queue: Arc::new(queue),
|
||||
image,
|
||||
texture: Arc::new(texture),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn load_image(
|
||||
device: &Device,
|
||||
queue: &Queue,
|
||||
path: impl AsRef<Path>,
|
||||
) -> anyhow::Result<(Image, Texture)> {
|
||||
let image = Image::load(path, UVDirection::TopLeft)?;
|
||||
let texture = device.create_texture(&TextureDescriptor {
|
||||
size: image.size.into(),
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
view_formats: &[wgpu::TextureFormat::Rgba8Unorm],
|
||||
label: None,
|
||||
});
|
||||
|
||||
queue.write_texture(
|
||||
wgpu::ImageCopyTexture {
|
||||
texture: &texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
&image.bytes,
|
||||
wgpu::ImageDataLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(4 * image.size.width),
|
||||
rows_per_image: None,
|
||||
},
|
||||
image.size.into(),
|
||||
);
|
||||
|
||||
let si = queue.submit([]);
|
||||
|
||||
device.poll(Maintain::WaitForSubmissionIndex(si));
|
||||
|
||||
Ok((image, texture))
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
mod d3d11;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
/// Test harness to set up a device, render a triangle, and apply a shader
|
||||
pub trait TriangleTest {
|
||||
/// Render a triangle to an image buffer, applying the provided shader.
|
||||
///
|
||||
/// The test should render in linear colour space for proper comparison against
|
||||
/// backends.
|
||||
fn triangle(
|
||||
&self,
|
||||
image: impl AsRef<Path>,
|
||||
path: impl AsRef<Path>,
|
||||
frame_count: usize,
|
||||
) -> anyhow::Result<image::RgbaImage>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::triangle::TriangleTest;
|
||||
use image::codecs::png::PngEncoder;
|
||||
use std::fs::File;
|
||||
|
||||
const IMAGE_PATH: &str = "../triangle.png";
|
||||
const FILTER_PATH: &str = "../test/shaders_slang/crt/crt-royale.slangp";
|
||||
|
||||
#[test]
|
||||
pub fn test_d3d11() -> anyhow::Result<()> {
|
||||
let d3d11 = super::d3d11::Direct3D11::new()?;
|
||||
let image = d3d11.triangle(IMAGE_PATH, FILTER_PATH, 100)?;
|
||||
|
||||
let out = File::create("out.png")?;
|
||||
image.write_with_encoder(PngEncoder::new(out))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue