test(mtl): Add Metal render test

This commit is contained in:
chyyran 2024-09-24 19:14:59 -04:00 committed by Ronny Chan
parent a7836923d7
commit 41034330a7
3 changed files with 193 additions and 10 deletions

View file

@ -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

View file

@ -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<()> {

View 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,
})
}
}
}