rt(mtl): implement texture and buffer abstractions

This commit is contained in:
chyyran 2024-02-11 17:54:13 -05:00 committed by Ronny Chan
parent 6780397d49
commit ba3154b92d
14 changed files with 425 additions and 26 deletions

View file

@ -29,7 +29,6 @@ pub mod metal;
mod viewport;
pub use viewport::Viewport;
use num_traits::AsPrimitive;

View file

@ -1,5 +1,5 @@
use crate::{FilterMode, ImageFormat, Size, WrapMode};
use icrate::Metal;
use crate::{Size, ImageFormat, FilterMode, WrapMode};
impl From<ImageFormat> for Metal::MTLPixelFormat {
fn from(format: ImageFormat) -> Self {
@ -61,7 +61,6 @@ impl From<Size<u32>> for Metal::MTLViewport {
}
}
impl From<WrapMode> for Metal::MTLSamplerAddressMode {
fn from(value: WrapMode) -> Self {
match value {
@ -91,5 +90,3 @@ impl FilterMode {
}
}
}

View file

@ -24,7 +24,7 @@ librashader-runtime = { path = "../librashader-runtime" , version = "0.2.0-beta.
rustc-hash = "1.1.0"
thiserror = "1.0"
array-concat = "0.5.2"
bytemuck = "1.14.3"
bytemuck = { version = "1.12.3", features = ["derive"] }
[dev-dependencies]

View file

@ -0,0 +1,54 @@
use crate::error::FilterChainError;
use icrate::Foundation::NSRange;
use icrate::Metal::{
MTLBuffer, MTLDevice, MTLResourceStorageModeManaged, MTLResourceStorageModeShared,
};
use objc2::rc::Id;
use objc2::runtime::ProtocolObject;
use std::ops::{Deref, DerefMut};
pub struct MetalBuffer {
buffer: Id<ProtocolObject<dyn MTLBuffer>>,
size: usize,
}
impl MetalBuffer {
pub fn new(device: &ProtocolObject<dyn MTLDevice>, size: usize) -> Result<Self> {
let resource_mode = if cfg!(target_os = "ios") {
MTLResourceStorageModeShared
} else {
MTLResourceStorageModeManaged
};
let buffer = device
.newBufferWithLength_options(size, resource_mode)
.ok_or(FilterChainError::BufferError)?;
Ok(Self { buffer, size })
}
pub fn flush(&self) {
// We don't know what was actually written to so...
self.buffer.didModifyRange(NSRange {
location: 0,
length: self.size,
})
}
}
impl Deref for MetalBuffer {
type Target = [u8];
fn deref(&self) -> &Self::Target {
// SAFETY: the lifetime of this reference must be longer than of the MetalBuffer.
// Additionally, `MetalBuffer.buffer` is never lent out directly
unsafe { std::slice::from_raw_parts(self.buffer.contents().as_ptr().cast(), self.size) }
}
}
impl DerefMut for MetalBuffer {
fn deref_mut(&mut self) -> &mut Self::Target {
// SAFETY: the lifetime of this reference must be longer than of the MetalBuffer.
// Additionally, `MetalBuffer.buffer` is never lent out directly
unsafe { std::slice::from_raw_parts_mut(self.buffer.contents().as_ptr().cast(), self.size) }
}
}

View file

@ -65,20 +65,20 @@ pub struct DrawQuad {
impl DrawQuad {
pub fn new(device: &ProtocolObject<dyn MTLDevice>) -> Result<DrawQuad> {
let vbo_data: &'static [u8] = bytemuck::cast_slice(&VBO_DATA);
let Some(buffer) = (unsafe {
device.newBufferWithBytes_length_options(
// SAFETY: this pointer is const.
// https://developer.apple.com/documentation/metal/mtldevice/1433429-newbufferwithbytes
NonNull::new_unchecked(vbo_data.as_ptr() as *mut c_void),
vbo_data.len(),
if cfg!(target_os = "ios") {
MTLResourceStorageModeShared
} else {
MTLResourceStorageModeManaged
},
)
}) else {
return Err(FilterChainError::BufferError);
let buffer = unsafe {
device
.newBufferWithBytes_length_options(
// SAFETY: this pointer is const.
// https://developer.apple.com/documentation/metal/mtldevice/1433429-newbufferwithbytes
NonNull::new_unchecked(vbo_data.as_ptr() as *mut c_void),
vbo_data.len(),
if cfg!(target_os = "ios") {
MTLResourceStorageModeShared
} else {
MTLResourceStorageModeManaged
},
)
.ok_or(FilterChainError::BufferError)?
};
Ok(DrawQuad { buffer })

View file

@ -26,11 +26,15 @@ pub enum FilterChainError {
#[error("buffer creation error")]
BufferError,
#[error("metal error")]
MetalError(Id<NSError>),
MetalError(#[from] Id<NSError>),
#[error("couldn't find entry for shader")]
ShaderWrongEntryName,
#[error("couldn't create render pass")]
FailedToCreateRenderPass,
#[error("couldn't create texture")]
FailedToCreateTexture,
#[error("couldn't create command buffer")]
FailedToCreateCommandBuffer,
}
/// Result type for Metal filter chains.

View file

@ -0,0 +1,115 @@
use crate::draw_quad::DrawQuad;
use crate::error;
use crate::error::FilterChainError;
use crate::filter_pass::FilterPass;
use crate::luts::LutTexture;
use crate::options::FilterChainOptionsMetal;
use crate::samplers::SamplerSet;
use crate::texture::{MetalTexture, OwnedImage};
use icrate::Metal::{MTLCommandBuffer, MTLCommandQueue, MTLDevice};
use librashader_presets::context::VideoDriver;
use librashader_presets::{ShaderPassConfig, ShaderPreset, TextureConfig};
use librashader_reflect::back::targets::{MSL, WGSL};
use librashader_reflect::back::CompileReflectShader;
use librashader_reflect::front::{Glslang, SpirvCompilation};
use librashader_reflect::reflect::cross::SpirvCross;
use librashader_reflect::reflect::naga::Naga;
use librashader_reflect::reflect::presets::{CompilePresetTarget, ShaderPassArtifact};
use librashader_reflect::reflect::semantics::ShaderSemantics;
use objc2::rc::Id;
use objc2::runtime::ProtocolObject;
use rustc_hash::FxHashMap;
use std::collections::VecDeque;
use std::path::Path;
use std::sync::Arc;
type ShaderPassMeta =
ShaderPassArtifact<impl CompileReflectShader<MSL, SpirvCompilation, SpirvCross> + Send>;
fn compile_passes(
shaders: Vec<ShaderPassConfig>,
textures: &[TextureConfig],
) -> Result<(Vec<ShaderPassMeta>, ShaderSemantics), FilterChainError> {
let (passes, semantics) =
MSL::compile_preset_passes::<Glslang, SpirvCompilation, SpirvCross, FilterChainError>(
shaders, &textures,
)?;
Ok((passes, semantics))
}
/// A wgpu filter chain.
pub struct FilterChainMetal {
pub(crate) common: FilterCommon,
passes: Box<[FilterPass]>,
output_framebuffers: Box<[OwnedImage]>,
feedback_framebuffers: Box<[OwnedImage]>,
history_framebuffers: VecDeque<OwnedImage>,
disable_mipmaps: bool,
}
pub struct FilterMutable {
pub passes_enabled: usize,
pub(crate) parameters: FxHashMap<String, f32>,
}
pub(crate) struct FilterCommon {
pub output_textures: Box<[Option<MetalTexture>]>,
pub feedback_textures: Box<[Option<MetalTexture>]>,
pub history_textures: Box<[Option<MetalTexture>]>,
pub luts: FxHashMap<usize, LutTexture>,
pub samplers: SamplerSet,
pub config: FilterMutable,
pub internal_frame_count: i32,
pub(crate) draw_quad: DrawQuad,
device: Id<ProtocolObject<dyn MTLDevice>>,
queue: Id<ProtocolObject<dyn MTLCommandQueue>>,
}
impl FilterChainMetal {
/// Load the shader preset at the given path into a filter chain.
pub fn load_from_path(
path: impl AsRef<Path>,
queue: Id<ProtocolObject<dyn MTLCommandQueue>>,
options: Option<&FilterChainOptionsMetal>,
) -> error::Result<FilterChainMetal> {
// load passes from preset
let preset = ShaderPreset::try_parse_with_driver_context(path, VideoDriver::Metal)?;
Self::load_from_preset(preset, queue, options)
}
/// Load a filter chain from a pre-parsed `ShaderPreset`.
pub fn load_from_preset(
preset: ShaderPreset,
queue: Id<ProtocolObject<dyn MTLCommandQueue>>,
options: Option<&FilterChainOptionsMetal>,
) -> error::Result<FilterChainMetal> {
let cmd = queue
.commandBuffer()
.ok_or(FilterChainError::FailedToCreateCommandBuffer)?;
let filter_chain = Self::load_from_preset_deferred(preset, queue, cmd, options)?;
cmd.commit();
unsafe { cmd.waitUntilCompleted() };
Ok(filter_chain)
}
/// Load a filter chain from a pre-parsed `ShaderPreset`, deferring and GPU-side initialization
/// to the caller. This function therefore requires no external synchronization of the device queue.
///
/// ## Safety
/// The provided command buffer must be ready for recording.
/// The caller is responsible for ending the command buffer and immediately submitting it to a
/// graphics queue. The command buffer must be completely executed before calling [`frame`](Self::frame).
pub fn load_from_preset_deferred(
preset: ShaderPreset,
queue: Id<ProtocolObject<dyn MTLCommandQueue>>,
cmd: Id<ProtocolObject<dyn MTLCommandBuffer>>,
options: Option<&FilterChainOptionsMetal>,
) -> error::Result<FilterChainMetal> {
let device = queue.device();
let (passes, semantics) = compile_passes(preset.shaders, &preset.textures)?;
let samplers = SamplerSet::new(&device)?;
}
}

View file

@ -0,0 +1 @@
pub struct FilterPass {}

View file

@ -30,7 +30,7 @@ pub struct PipelineLayoutObjects {
}
trait MslEntryPoint {
fn entry_point() -> NSString;
fn entry_point() -> Id<NSString>;
}
impl MslEntryPoint for CrossMslContext {
@ -112,7 +112,7 @@ impl PipelineLayoutObjects {
ca.setSourceAlphaBlendFactor(MTLBlendFactorSourceAlpha);
ca.setSourceRGBBlendFactor(MTLBlendFactorSourceAlpha);
ca.setDestinationAlphaBlendFactor(MTLBlendFactorOneMinusSourceAlpha);
ca.setDetinationRGBBlendFactor(MTLBlendFactorOneMinusSourceAlpha);
ca.setDestinationRGBBlendFactor(MTLBlendFactorOneMinusSourceAlpha);
ca
}
@ -153,10 +153,10 @@ impl MetalGraphicsPipeline {
render_pass_format: MTLPixelFormat,
) -> Result<Self> {
let layout = PipelineLayoutObjects::new(shader_assembly, device)?;
let pipeline = layout.create_pipeline(render_pass_format)?;
Ok(Self {
layout,
render_pipeline: layout.create_pipeline(render_pass_format)?,
render_pipeline: pipeline,
})
}

View file

@ -1,5 +1,11 @@
#![cfg(target_vendor = "apple")]
mod buffer;
mod draw_quad;
mod error;
mod filter_chain;
mod filter_pass;
mod graphics_pipeline;
mod luts;
mod options;
mod samplers;
mod texture;

View file

@ -0,0 +1,79 @@
use crate::error::{FilterChainError, Result};
use crate::samplers::SamplerSet;
use crate::texture::MetalTexture;
use icrate::Metal::{
MTLBlitCommandEncoder, MTLCommandBuffer, MTLCommandEncoder, MTLDevice, MTLOrigin,
MTLPixelFormatBGRA8Unorm, MTLRegion, MTLSize, MTLTexture, MTLTextureDescriptor,
MTLTextureUsageShaderRead,
};
use librashader_presets::TextureConfig;
use librashader_runtime::image::{Image, BGRA8};
use librashader_runtime::scaling::MipmapSize;
use objc2::rc::Id;
use objc2::runtime::ProtocolObject;
use std::ffi::c_void;
use std::ptr::NonNull;
pub(crate) struct LutTexture(MetalTexture);
impl LutTexture {
pub fn new(
device: &ProtocolObject<dyn MTLDevice>,
image: Image<BGRA8>,
cmd: &ProtocolObject<dyn MTLCommandBuffer>,
config: &TextureConfig,
) -> Result<Self> {
let descriptor = unsafe {
let descriptor =
MTLTextureDescriptor::texture2DDescriptorWithPixelFormat_width_height_mipmapped(
MTLPixelFormatBGRA8Unorm,
image.size.width as usize,
image.size.height as usize,
config.mipmap,
);
descriptor.setSampleCount(1);
descriptor.setMipmapLevelCount(if config.mipmap {
image.size.calculate_miplevels() as usize
} else {
1
});
descriptor.setUsage(MTLTextureUsageShaderRead);
descriptor
};
let texture = device
.newTextureWithDescriptor(&descriptor)
.ok_or(FilterChainError::FailedToCreateTexture)?;
unsafe {
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 c_void),
4 * image.size.width as usize,
)
}
if config.mipmap {
if let Some(encoder) = cmd.blitCommandEncoder() {
encoder.generateMipmapsForTexture(&texture);
encoder.endEncoding();
}
}
Ok(LutTexture(texture))
}
}

View file

@ -0,0 +1,20 @@
//! Metal shader runtime options.
/// Options for each Vulkan shader frame.
#[repr(C)]
#[derive(Default, Debug, Clone)]
pub struct FrameOptionsMetal {
/// Whether or not to clear the history buffers.
pub clear_history: bool,
/// The direction of rendering.
/// -1 indicates that the frames are played in reverse order.
pub frame_direction: i32,
}
/// Options for filter chain creation.
#[repr(C)]
#[derive(Default, Debug, Clone)]
pub struct FilterChainOptionsMetal {
/// Whether or not to explicitly disable mipmap generation regardless of shader preset settings.
pub force_no_mipmaps: bool,
}

View file

@ -0,0 +1,125 @@
use crate::error::{FilterChainError, Result};
use icrate::Metal::{
MTLBlitCommandEncoder, MTLCommandBuffer, MTLCommandEncoder, MTLDevice, MTLPixelFormat,
MTLPixelFormatBGRA8Unorm, MTLTexture, MTLTextureDescriptor, MTLTextureUsageRenderTarget,
MTLTextureUsageShaderRead, MTLTextureUsageShaderWrite,
};
use librashader_common::{FilterMode, ImageFormat, Size, WrapMode};
use librashader_presets::Scale2D;
use librashader_runtime::scaling::{MipmapSize, ViewportSize};
use objc2::rc::Id;
use objc2::runtime::ProtocolObject;
use std::sync::Arc;
pub type MetalTexture = Id<ProtocolObject<dyn MTLTexture>>;
pub struct OwnedImage {
image: MetalTexture,
max_miplevels: u32,
size: Size<u32>,
}
impl OwnedImage {
pub fn new(
device: &ProtocolObject<dyn MTLDevice>,
size: Size<u32>,
max_miplevels: u32,
format: ImageFormat,
) -> Result<Self> {
let format: MTLPixelFormat = format.into();
let descriptor = unsafe {
let descriptor =
MTLTextureDescriptor::texture2DDescriptorWithPixelFormat_width_height_mipmapped(
format,
size.width as usize,
size.height as usize,
max_miplevels <= 1,
);
descriptor.setSampleCount(1);
descriptor.setMipmapLevelCount(if max_miplevels <= 1 {
size.calculate_miplevels() as usize
} else {
1
});
descriptor.setUsage(
MTLTextureUsageShaderRead
| MTLTextureUsageShaderWrite
| MTLTextureUsageRenderTarget,
);
descriptor
};
Ok(Self {
image: device
.newTextureWithDescriptor(&descriptor)
.ok_or(FilterChainError::FailedToCreateTexture)?,
max_miplevels,
size,
})
}
pub fn scale(
&mut self,
device: &ProtocolObject<dyn MTLDevice>,
scaling: Scale2D,
format: ImageFormat,
viewport_size: &Size<u32>,
source_size: &Size<u32>,
mipmap: bool,
) -> Size<u32> {
let size = source_size.scale_viewport(scaling, *viewport_size);
let format: MTLPixelFormat = format.into();
if self.size != size
|| (mipmap && self.max_miplevels == 1)
|| (!mipmap && self.max_miplevels != 1)
|| format != self.image.pixelFormat()
{
let mut new = OwnedImage::new(device, size, self.max_miplevels, format.into())?;
std::mem::swap(self, &mut new);
}
size
}
// pub(crate) fn as_input(&self, filter: FilterMode, wrap_mode: WrapMode) -> InputImage {
// InputImage {
// image: Arc::clone(&self.image),
// view: Arc::clone(&self.view),
// wrap_mode,
// filter_mode: filter,
// mip_filter: filter,
// }
// }
pub fn copy_from(
&self,
other: &ProtocolObject<dyn MTLTexture>,
cmd: Id<ProtocolObject<dyn MTLCommandBuffer>>,
) -> Result<()> {
let encoder = cmd
.blitCommandEncoder()
.ok_or(FilterChainError::FailedToCreateCommandBuffer)?;
unsafe {
encoder.copyFromTexture_toTexture(other, &self.image);
}
encoder.generateMipmapsForTexture(&self.image);
encoder.endEncoding();
Ok(())
}
pub fn clear(&self, cmd: Id<ProtocolObject<dyn MTLCommandBuffer>>) {
// let render = cmd.renderCommandEncoder()
// .ok_or(FilterChainError::FailedToCreateCommandBuffer)?;
// render.
// cmd.clear_texture(&self.image, &wgpu::ImageSubresourceRange::default());
}
/// caller must end the blit encoder after.
pub fn generate_mipmaps(&self, mipmapper: &ProtocolObject<dyn MTLBlitCommandEncoder>) {
mipmapper.generateMipmapsForTexture(&self.image);
}
}

View file

@ -59,4 +59,3 @@ macro_rules! impl_filter_chain_parameters {
}
};
}