From 41034330a76aa8e8f2591a8aa667800d1e69bc23 Mon Sep 17 00:00:00 2001 From: chyyran Date: Tue, 24 Sep 2024 19:14:59 -0400 Subject: [PATCH] test(mtl): Add Metal render test --- librashader-test/Cargo.toml | 2 +- librashader-test/src/render/mod.rs | 24 ++-- librashader-test/src/render/mtl.rs | 177 +++++++++++++++++++++++++++++ 3 files changed, 193 insertions(+), 10 deletions(-) create mode 100644 librashader-test/src/render/mtl.rs diff --git a/librashader-test/Cargo.toml b/librashader-test/Cargo.toml index 3c0589f..12d9f6b 100644 --- a/librashader-test/Cargo.toml +++ b/librashader-test/Cargo.toml @@ -29,7 +29,7 @@ wgpu = ["librashader/runtime-wgpu", "dep:wgpu", "dep:wgpu-types"] d3d11 = ["librashader/runtime-d3d11", "dep:windows"] d3d12 = ["librashader/runtime-d3d12", "dep:windows"] - +metal = ["librashader/runtime-metal", "dep:objc2", "dep:objc2-metal"] [target.'cfg(windows)'.dependencies.windows] workspace = true diff --git a/librashader-test/src/render/mod.rs b/librashader-test/src/render/mod.rs index 679a497..8317027 100644 --- a/librashader-test/src/render/mod.rs +++ b/librashader-test/src/render/mod.rs @@ -13,6 +13,9 @@ pub mod vk; #[cfg(feature = "wgpu")] pub mod wgpu; +#[cfg(feature = "metal")] +pub mod mtl; + use std::path::Path; /// Test harness to set up a device, render a triangle, and apply a shader @@ -39,10 +42,7 @@ pub trait RenderTest { #[cfg(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 image::codecs::png::PngEncoder; use std::fs::File; @@ -65,31 +65,37 @@ mod test { #[test] #[cfg(feature = "d3d11")] pub fn test_d3d11() -> anyhow::Result<()> { - do_test::() + do_test::() } #[test] #[cfg(feature = "wgpu")] pub fn test_wgpu() -> anyhow::Result<()> { - do_test::() + do_test::() } #[test] #[cfg(feature = "vulkan")] pub fn test_vk() -> anyhow::Result<()> { - do_test::() + do_test::() } #[test] #[cfg(feature = "opengl")] pub fn test_gl3() -> anyhow::Result<()> { - do_test::() + do_test::() } #[test] #[cfg(feature = "opengl")] pub fn test_gl4() -> anyhow::Result<()> { - do_test::() + do_test::() + } + + #[test] + #[cfg(feature = "metal")] + pub fn test_metal() -> anyhow::Result<()> { + do_test::() } pub fn compare() -> anyhow::Result<()> { diff --git a/librashader-test/src/render/mtl.rs b/librashader-test/src/render/mtl.rs new file mode 100644 index 0000000..f32fcd3 --- /dev/null +++ b/librashader-test/src/render/mtl.rs @@ -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>, + texture: Retained>, + image_bytes: Image, +} + +impl RenderTest for Metal { + fn new(path: impl AsRef) -> anyhow::Result + where + Self: Sized, + { + Metal::new(path) + } + + fn render(&self, path: impl AsRef, frame_count: usize) -> anyhow::Result { + 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) -> anyhow::Result { + let image: Image = 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, + }) + } + } +}