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"]
|
||||
d3d12 = ["librashader/runtime-d3d12", "dep:windows"]
|
||||
|
||||
metal = ["librashader/runtime-metal", "dep:objc2", "dep:objc2-metal"]
|
||||
|
||||
[target.'cfg(windows)'.dependencies.windows]
|
||||
workspace = true
|
||||
|
|
|
@ -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::<Direct3D11>()
|
||||
do_test::<crate::render::d3d11::Direct3D11>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "wgpu")]
|
||||
pub fn test_wgpu() -> anyhow::Result<()> {
|
||||
do_test::<Wgpu>()
|
||||
do_test::<crate::render::wgpu::Wgpu>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "vulkan")]
|
||||
pub fn test_vk() -> anyhow::Result<()> {
|
||||
do_test::<Vulkan>()
|
||||
do_test::<crate::render::vk::Vulkan>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "opengl")]
|
||||
pub fn test_gl3() -> anyhow::Result<()> {
|
||||
do_test::<OpenGl3>()
|
||||
do_test::<crate::render::gl::OpenGl3>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "opengl")]
|
||||
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<()> {
|
||||
|
|
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