test(mtl): Add Metal render test
This commit is contained in:
parent
a7836923d7
commit
41034330a7
|
@ -29,7 +29,7 @@ wgpu = ["librashader/runtime-wgpu", "dep:wgpu", "dep:wgpu-types"]
|
||||||
|
|
||||||
d3d11 = ["librashader/runtime-d3d11", "dep:windows"]
|
d3d11 = ["librashader/runtime-d3d11", "dep:windows"]
|
||||||
d3d12 = ["librashader/runtime-d3d12", "dep:windows"]
|
d3d12 = ["librashader/runtime-d3d12", "dep:windows"]
|
||||||
|
metal = ["librashader/runtime-metal", "dep:objc2", "dep:objc2-metal"]
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies.windows]
|
[target.'cfg(windows)'.dependencies.windows]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
@ -13,6 +13,9 @@ pub mod vk;
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu")]
|
||||||
pub mod wgpu;
|
pub mod wgpu;
|
||||||
|
|
||||||
|
#[cfg(feature = "metal")]
|
||||||
|
pub mod mtl;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
/// Test harness to set up a device, render a triangle, and apply a shader
|
/// Test harness to set up a device, render a triangle, and apply a shader
|
||||||
|
@ -39,10 +42,7 @@ pub trait RenderTest {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::render::d3d11::Direct3D11;
|
|
||||||
use crate::render::gl::{OpenGl3, OpenGl4};
|
|
||||||
use crate::render::vk::Vulkan;
|
|
||||||
use crate::render::wgpu::Wgpu;
|
|
||||||
use crate::render::RenderTest;
|
use crate::render::RenderTest;
|
||||||
use image::codecs::png::PngEncoder;
|
use image::codecs::png::PngEncoder;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
@ -65,31 +65,37 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "d3d11")]
|
#[cfg(feature = "d3d11")]
|
||||||
pub fn test_d3d11() -> anyhow::Result<()> {
|
pub fn test_d3d11() -> anyhow::Result<()> {
|
||||||
do_test::<Direct3D11>()
|
do_test::<crate::render::d3d11::Direct3D11>()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu")]
|
||||||
pub fn test_wgpu() -> anyhow::Result<()> {
|
pub fn test_wgpu() -> anyhow::Result<()> {
|
||||||
do_test::<Wgpu>()
|
do_test::<crate::render::wgpu::Wgpu>()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "vulkan")]
|
#[cfg(feature = "vulkan")]
|
||||||
pub fn test_vk() -> anyhow::Result<()> {
|
pub fn test_vk() -> anyhow::Result<()> {
|
||||||
do_test::<Vulkan>()
|
do_test::<crate::render::vk::Vulkan>()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "opengl")]
|
#[cfg(feature = "opengl")]
|
||||||
pub fn test_gl3() -> anyhow::Result<()> {
|
pub fn test_gl3() -> anyhow::Result<()> {
|
||||||
do_test::<OpenGl3>()
|
do_test::<crate::render::gl::OpenGl3>()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "opengl")]
|
#[cfg(feature = "opengl")]
|
||||||
pub fn test_gl4() -> anyhow::Result<()> {
|
pub fn test_gl4() -> anyhow::Result<()> {
|
||||||
do_test::<OpenGl4>()
|
do_test::<crate::render::gl::OpenGl4>()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "metal")]
|
||||||
|
pub fn test_metal() -> anyhow::Result<()> {
|
||||||
|
do_test::<crate::render::mtl::Metal>()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compare<A: RenderTest, B: RenderTest>() -> anyhow::Result<()> {
|
pub fn compare<A: RenderTest, B: RenderTest>() -> anyhow::Result<()> {
|
||||||
|
|
177
librashader-test/src/render/mtl.rs
Normal file
177
librashader-test/src/render/mtl.rs
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
use crate::render::RenderTest;
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use image::RgbaImage;
|
||||||
|
use librashader::runtime::mtl::{FilterChain, FilterChainOptions};
|
||||||
|
use librashader::runtime::Viewport;
|
||||||
|
use librashader_runtime::image::{Image, PixelFormat, UVDirection, BGRA8, RGBA8};
|
||||||
|
use objc2::ffi::NSUInteger;
|
||||||
|
use objc2::rc::Retained;
|
||||||
|
use objc2::runtime::ProtocolObject;
|
||||||
|
use objc2_metal::{
|
||||||
|
MTLCommandBuffer, MTLCommandQueue, MTLCreateSystemDefaultDevice, MTLDevice, MTLOrigin,
|
||||||
|
MTLPixelFormat, MTLRegion, MTLSize, MTLStorageMode, MTLTexture, MTLTextureDescriptor,
|
||||||
|
MTLTextureUsage,
|
||||||
|
};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::ptr::NonNull;
|
||||||
|
|
||||||
|
pub struct Metal {
|
||||||
|
device: Retained<ProtocolObject<dyn MTLDevice>>,
|
||||||
|
texture: Retained<ProtocolObject<dyn MTLTexture>>,
|
||||||
|
image_bytes: Image<BGRA8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderTest for Metal {
|
||||||
|
fn new(path: impl AsRef<Path>) -> anyhow::Result<Self>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
Metal::new(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self, path: impl AsRef<Path>, frame_count: usize) -> anyhow::Result<RgbaImage> {
|
||||||
|
let queue = self
|
||||||
|
.device
|
||||||
|
.newCommandQueue()
|
||||||
|
.ok_or_else(|| anyhow!("Unable to create command queue"))?;
|
||||||
|
|
||||||
|
let cmd = queue
|
||||||
|
.commandBuffer()
|
||||||
|
.ok_or_else(|| anyhow!("Unable to create command buffer"))?;
|
||||||
|
|
||||||
|
let mut filter_chain = FilterChain::load_from_path(
|
||||||
|
path,
|
||||||
|
&queue,
|
||||||
|
Some(&FilterChainOptions {
|
||||||
|
force_no_mipmaps: false,
|
||||||
|
}),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let render_texture = unsafe {
|
||||||
|
let texture_descriptor =
|
||||||
|
MTLTextureDescriptor::texture2DDescriptorWithPixelFormat_width_height_mipmapped(
|
||||||
|
MTLPixelFormat::BGRA8Unorm,
|
||||||
|
self.image_bytes.size.width as NSUInteger,
|
||||||
|
self.image_bytes.size.height as NSUInteger,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
texture_descriptor.setSampleCount(1);
|
||||||
|
texture_descriptor.setStorageMode(
|
||||||
|
if cfg!(all(target_arch = "aarch64", target_vendor = "apple")) {
|
||||||
|
MTLStorageMode::Shared
|
||||||
|
} else {
|
||||||
|
MTLStorageMode::Managed
|
||||||
|
},
|
||||||
|
);
|
||||||
|
texture_descriptor.setUsage(MTLTextureUsage::ShaderWrite);
|
||||||
|
|
||||||
|
let texture = self
|
||||||
|
.device
|
||||||
|
.newTextureWithDescriptor(&texture_descriptor)
|
||||||
|
.ok_or_else(|| anyhow!("Failed to create texture"))?;
|
||||||
|
|
||||||
|
texture
|
||||||
|
};
|
||||||
|
|
||||||
|
filter_chain.frame(
|
||||||
|
&self.texture,
|
||||||
|
&Viewport::new_render_target_sized_origin(render_texture.as_ref(), None)?,
|
||||||
|
cmd.as_ref(),
|
||||||
|
frame_count,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
cmd.commit();
|
||||||
|
unsafe {
|
||||||
|
cmd.waitUntilCompleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
let region = MTLRegion {
|
||||||
|
origin: MTLOrigin { x: 0, y: 0, z: 0 },
|
||||||
|
size: MTLSize {
|
||||||
|
width: self.image_bytes.size.width as usize,
|
||||||
|
height: self.image_bytes.size.height as usize,
|
||||||
|
depth: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
// should be the same size
|
||||||
|
let mut buffer = vec![0u8; self.image_bytes.bytes.len()];
|
||||||
|
render_texture.getBytes_bytesPerRow_fromRegion_mipmapLevel(
|
||||||
|
NonNull::new(buffer.as_mut_ptr().cast()).unwrap(),
|
||||||
|
4 * self.image_bytes.size.width as usize,
|
||||||
|
region,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// swap the BGRA back to RGBA.
|
||||||
|
BGRA8::convert(&mut buffer);
|
||||||
|
|
||||||
|
let image = RgbaImage::from_raw(
|
||||||
|
render_texture.width() as u32,
|
||||||
|
render_texture.height() as u32,
|
||||||
|
Vec::from(buffer),
|
||||||
|
)
|
||||||
|
.ok_or(anyhow!("Unable to create image from data"))?;
|
||||||
|
|
||||||
|
Ok(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Metal {
|
||||||
|
pub fn new(image_path: impl AsRef<Path>) -> anyhow::Result<Self> {
|
||||||
|
let image: Image<BGRA8> = Image::load(image_path, UVDirection::TopLeft)?;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let device = Retained::from_raw(MTLCreateSystemDefaultDevice())
|
||||||
|
.ok_or_else(|| anyhow!("Unable to create default Metal device"))?;
|
||||||
|
|
||||||
|
let texture_descriptor =
|
||||||
|
MTLTextureDescriptor::texture2DDescriptorWithPixelFormat_width_height_mipmapped(
|
||||||
|
MTLPixelFormat::BGRA8Unorm,
|
||||||
|
image.size.width as NSUInteger,
|
||||||
|
image.size.height as NSUInteger,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
texture_descriptor.setSampleCount(1);
|
||||||
|
texture_descriptor.setStorageMode(
|
||||||
|
if cfg!(all(target_arch = "aarch64", target_vendor = "apple")) {
|
||||||
|
MTLStorageMode::Shared
|
||||||
|
} else {
|
||||||
|
MTLStorageMode::Managed
|
||||||
|
},
|
||||||
|
);
|
||||||
|
texture_descriptor.setUsage(MTLTextureUsage::ShaderRead);
|
||||||
|
|
||||||
|
let texture = device
|
||||||
|
.newTextureWithDescriptor(&texture_descriptor)
|
||||||
|
.ok_or_else(|| anyhow!("Failed to create texture"))?;
|
||||||
|
|
||||||
|
let region = MTLRegion {
|
||||||
|
origin: MTLOrigin { x: 0, y: 0, z: 0 },
|
||||||
|
size: MTLSize {
|
||||||
|
width: image.size.width as usize,
|
||||||
|
height: image.size.height as usize,
|
||||||
|
depth: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
texture.replaceRegion_mipmapLevel_withBytes_bytesPerRow(
|
||||||
|
region,
|
||||||
|
0,
|
||||||
|
// SAFETY: replaceRegion withBytes is const.
|
||||||
|
NonNull::new_unchecked(image.bytes.as_slice().as_ptr() as *mut _),
|
||||||
|
4 * image.size.width as usize,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
device,
|
||||||
|
texture,
|
||||||
|
image_bytes: image,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue