Skip to main content

librashader_runtime_mtl/
filter_chain.rs

1use crate::buffer::MetalBuffer;
2use crate::draw_quad::DrawQuad;
3use crate::error;
4use crate::error::FilterChainError;
5use crate::filter_pass::FilterPass;
6use crate::graphics_pipeline::MetalGraphicsPipeline;
7use crate::luts::LutTexture;
8use crate::options::{FilterChainOptionsMetal, FrameOptionsMetal};
9use crate::samplers::SamplerSet;
10use crate::texture::{get_texture_size, InputTexture, MetalTextureRef, OwnedTexture};
11use librashader_common::map::FastHashMap;
12use librashader_common::{ImageFormat, Size, Viewport};
13use librashader_presets::context::VideoDriver;
14use librashader_presets::{ShaderFeatures, ShaderPreset};
15use librashader_reflect::back::msl::MslVersion;
16use librashader_reflect::back::targets::MSL;
17use librashader_reflect::back::{CompileReflectShader, CompileShader};
18use librashader_reflect::front::SpirvCompilation;
19use librashader_reflect::reflect::cross::SpirvCross;
20use librashader_reflect::reflect::presets::{CompilePresetTarget, ShaderPassArtifact};
21use librashader_reflect::reflect::semantics::ShaderSemantics;
22use librashader_reflect::reflect::ReflectShader;
23use librashader_runtime::binding::BindingUtil;
24use librashader_runtime::framebuffer::{FramebufferInit, FramebufferPool};
25use librashader_runtime::image::{ImageError, LoadedTexture, UVDirection, BGRA8};
26use librashader_runtime::quad::QuadType;
27use librashader_runtime::render_target::RenderTarget;
28use librashader_runtime::scaling::ScaleFramebuffer;
29use librashader_runtime::uniforms::UniformStorage;
30use objc2::rc::Retained;
31use objc2::runtime::ProtocolObject;
32use objc2_foundation::NSString;
33use objc2_metal::{
34    MTLCommandBuffer, MTLCommandEncoder, MTLCommandQueue, MTLDevice, MTLLoadAction, MTLPixelFormat,
35    MTLRenderPassDescriptor, MTLResource, MTLStoreAction, MTLTexture,
36};
37use rayon::prelude::*;
38use std::collections::VecDeque;
39use std::fmt::{Debug, Formatter};
40use std::path::Path;
41
42mod compile {
43    use super::*;
44    use librashader_pack::{PassResource, TextureResource};
45
46    #[cfg(feature = "nightly")]
47    pub type ShaderPassMeta =
48        ShaderPassArtifact<impl CompileReflectShader<MSL, SpirvCompilation, SpirvCross> + Send>;
49
50    #[cfg(not(feature = "nightly"))]
51    pub type ShaderPassMeta =
52        ShaderPassArtifact<Box<dyn CompileReflectShader<MSL, SpirvCompilation, SpirvCross> + Send>>;
53
54    #[cfg_attr(feature = "nightly", define_opaque(ShaderPassMeta))]
55    pub fn compile_passes(
56        shaders: Vec<PassResource>,
57        textures: &[TextureResource],
58    ) -> Result<(Vec<ShaderPassMeta>, ShaderSemantics), FilterChainError> {
59        let (passes, semantics) = MSL::compile_preset_passes::<
60            SpirvCompilation,
61            SpirvCross,
62            FilterChainError,
63        >(shaders, textures.iter().map(|t| &t.meta))?;
64        Ok((passes, semantics))
65    }
66}
67
68use compile::{compile_passes, ShaderPassMeta};
69use librashader_pack::{ShaderPresetPack, TextureResource};
70use librashader_runtime::parameters::RuntimeParameters;
71
72/// A Metal filter chain.
73pub struct FilterChainMetal {
74    pub(crate) common: FilterCommon,
75    passes: Box<[FilterPass]>,
76    output_framebuffers: FramebufferPool<OwnedTexture>,
77    feedback_framebuffers: FramebufferPool<OwnedTexture>,
78    history_framebuffers: VecDeque<OwnedTexture>,
79    /// Metal does not allow us to push the input texture to history
80    /// before recording framebuffers, so we double-buffer it.
81    ///
82    /// First we swap OriginalHistory1 with the contents of this buffer (which were written to
83    /// in the previous frame)
84    ///
85    /// Then we blit the original to the buffer.
86    prev_frame_history_buffer: OwnedTexture,
87    disable_mipmaps: bool,
88    default_options: FrameOptionsMetal,
89    draw_last_pass_feedback: bool,
90}
91
92impl Debug for FilterChainMetal {
93    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
94        f.write_fmt(format_args!("FilterChainMetal"))
95    }
96}
97
98pub(crate) struct FilterCommon {
99    pub output_textures: Box<[Option<InputTexture>]>,
100    pub feedback_textures: Box<[Option<InputTexture>]>,
101    pub history_textures: Box<[Option<InputTexture>]>,
102    pub luts: FastHashMap<usize, LutTexture>,
103    pub samplers: SamplerSet,
104    pub config: RuntimeParameters,
105    pub(crate) draw_quad: DrawQuad,
106    device: Retained<ProtocolObject<dyn MTLDevice>>,
107}
108
109impl FilterChainMetal {
110    /// Load the shader preset at the given path into a filter chain.
111    pub fn load_from_path(
112        path: impl AsRef<Path>,
113        features: ShaderFeatures,
114        queue: &ProtocolObject<dyn MTLCommandQueue>,
115        options: Option<&FilterChainOptionsMetal>,
116    ) -> error::Result<FilterChainMetal> {
117        // load passes from preset
118        let preset =
119            ShaderPreset::try_parse_with_driver_context(path, features, VideoDriver::Metal)?;
120        Self::load_from_preset(preset, queue, options)
121    }
122
123    /// Load a filter chain from a pre-parsed `ShaderPreset`.
124    pub fn load_from_preset(
125        preset: ShaderPreset,
126        queue: &ProtocolObject<dyn MTLCommandQueue>,
127        options: Option<&FilterChainOptionsMetal>,
128    ) -> error::Result<FilterChainMetal> {
129        let preset = ShaderPresetPack::load_from_preset::<FilterChainError>(preset)?;
130        Self::load_from_pack(preset, queue, options)
131    }
132
133    /// Load a filter chain from a pre-parsed `ShaderPreset`.
134    pub fn load_from_pack(
135        preset: ShaderPresetPack,
136        queue: &ProtocolObject<dyn MTLCommandQueue>,
137        options: Option<&FilterChainOptionsMetal>,
138    ) -> error::Result<FilterChainMetal> {
139        let cmd = queue
140            .commandBuffer()
141            .ok_or(FilterChainError::FailedToCreateCommandBuffer)?;
142
143        let filter_chain =
144            Self::load_from_pack_deferred_internal(preset, queue.device(), &cmd, options)?;
145
146        cmd.commit();
147        cmd.waitUntilCompleted();
148
149        Ok(filter_chain)
150    }
151
152    fn load_luts(
153        device: &ProtocolObject<dyn MTLDevice>,
154        cmd: &ProtocolObject<dyn MTLCommandBuffer>,
155        textures: Vec<TextureResource>,
156    ) -> error::Result<FastHashMap<usize, LutTexture>> {
157        let mut luts = FastHashMap::default();
158
159        let mipmapper = cmd
160            .blitCommandEncoder()
161            .ok_or(FilterChainError::FailedToCreateCommandBuffer)?;
162
163        let textures = textures
164            .into_par_iter()
165            .map(|texture| LoadedTexture::<BGRA8>::from_texture(texture, UVDirection::TopLeft))
166            .collect::<Result<Vec<LoadedTexture<BGRA8>>, ImageError>>()?;
167        for (index, LoadedTexture { meta, image }) in textures.into_iter().enumerate() {
168            let texture = LutTexture::new(device, image, &meta, &mipmapper)?;
169            luts.insert(index, texture);
170        }
171
172        mipmapper.endEncoding();
173        Ok(luts)
174    }
175
176    fn init_passes(
177        device: &Retained<ProtocolObject<dyn MTLDevice>>,
178        passes: Vec<ShaderPassMeta>,
179        semantics: &ShaderSemantics,
180    ) -> error::Result<Box<[FilterPass]>> {
181        // todo: fix this to allow send
182        let filters: Vec<error::Result<FilterPass>> = passes
183            .into_iter()
184            .enumerate()
185            .map(|(index, (config, mut reflect))| {
186                let reflection = reflect.reflect(index, semantics)?;
187                let msl = reflect.compile(Some(MslVersion::new(2, 0, 0)))?;
188
189                let ubo_size = reflection.ubo.as_ref().map_or(0, |ubo| ubo.size as usize);
190                let push_size = reflection
191                    .push_constant
192                    .as_ref()
193                    .map_or(0, |push| push.size);
194
195                let uniform_storage = UniformStorage::new_with_storage(
196                    MetalBuffer::new(&device, ubo_size, "ubo")?,
197                    MetalBuffer::new(&device, push_size as usize, "pcb")?,
198                );
199
200                let uniform_bindings = reflection.meta.create_binding_map(|param| param.offset());
201
202                let render_pass_format: MTLPixelFormat =
203                    if let Some(format) = config.meta.get_format_override() {
204                        format.into()
205                    } else {
206                        config.data.format.into()
207                    };
208
209                let graphics_pipeline = MetalGraphicsPipeline::new(
210                    &device,
211                    &msl,
212                    if render_pass_format == MTLPixelFormat(0) {
213                        MTLPixelFormat::RGBA8Unorm
214                    } else {
215                        render_pass_format
216                    },
217                )?;
218
219                Ok(FilterPass {
220                    reflection,
221                    uniform_storage,
222                    uniform_bindings,
223                    source: config.data,
224                    meta: config.meta,
225                    graphics_pipeline,
226                })
227            })
228            .collect();
229        //
230        let filters: error::Result<Vec<FilterPass>> = filters.into_iter().collect();
231        let filters = filters?;
232        Ok(filters.into_boxed_slice())
233    }
234
235    fn push_history(
236        &mut self,
237        cmd: &ProtocolObject<dyn MTLCommandBuffer>,
238        input: &ProtocolObject<dyn MTLTexture>,
239    ) -> error::Result<()> {
240        // If there's no history, there's no need to do any of this.
241        let Some(mut back) = self.history_framebuffers.pop_back() else {
242            return Ok(());
243        };
244
245        // Push the previous frame as OriginalHistory1
246        std::mem::swap(&mut back, &mut self.prev_frame_history_buffer);
247        self.history_framebuffers.push_front(back);
248
249        // Copy the current frame into prev_frame_history_buffer, which will be
250        // pushed to OriginalHistory1 in the next frame.
251        let back = &mut self.prev_frame_history_buffer;
252        let mipmapper = cmd
253            .blitCommandEncoder()
254            .ok_or(FilterChainError::FailedToCreateCommandBuffer)?;
255        if back.texture.height() != input.height()
256            || back.texture.width() != input.width()
257            || input.pixelFormat() != back.texture.pixelFormat()
258        {
259            let size = Size {
260                width: input.width() as u32,
261                height: input.height() as u32,
262            };
263
264            let _old_back = std::mem::replace(
265                back,
266                OwnedTexture::new(&self.common.device, size, 1, input.pixelFormat())?,
267            );
268        }
269
270        back.copy_from(&mipmapper, input)?;
271        mipmapper.endEncoding();
272        Ok(())
273    }
274
275    /// Load a filter chain from a pre-parsed `ShaderPreset`, deferring and GPU-side initialization
276    /// to the caller. This function therefore requires no external synchronization of the device queue.
277    ///
278    /// ## Safety
279    /// The provided command buffer must be ready for recording.
280    /// The caller is responsible for ending the command buffer and immediately submitting it to a
281    /// graphics queue. The command buffer must be completely executed before calling [`frame`](Self::frame).
282    pub fn load_from_preset_deferred(
283        preset: ShaderPreset,
284        queue: &ProtocolObject<dyn MTLCommandQueue>,
285        cmd: &ProtocolObject<dyn MTLCommandBuffer>,
286        options: Option<&FilterChainOptionsMetal>,
287    ) -> error::Result<FilterChainMetal> {
288        let preset = ShaderPresetPack::load_from_preset::<FilterChainError>(preset)?;
289        Self::load_from_pack_deferred(preset, queue, cmd, options)
290    }
291
292    /// Load a filter chain from a pre-parsed `ShaderPreset`, deferring and GPU-side initialization
293    /// to the caller. This function therefore requires no external synchronization of the device queue.
294    ///
295    /// ## Safety
296    /// The provided command buffer must be ready for recording.
297    /// The caller is responsible for ending the command buffer and immediately submitting it to a
298    /// graphics queue. The command buffer must be completely executed before calling [`frame`](Self::frame).
299    pub fn load_from_pack_deferred(
300        preset: ShaderPresetPack,
301        queue: &ProtocolObject<dyn MTLCommandQueue>,
302        cmd: &ProtocolObject<dyn MTLCommandBuffer>,
303        options: Option<&FilterChainOptionsMetal>,
304    ) -> error::Result<FilterChainMetal> {
305        Self::load_from_pack_deferred_internal(preset, queue.device(), &cmd, options)
306    }
307
308    /// Load a filter chain from a pre-parsed `ShaderPreset`, deferring and GPU-side initialization
309    /// to the caller. This function therefore requires no external synchronization of the device queue.
310    ///
311    /// ## Safety
312    /// The provided command buffer must be ready for recording.
313    /// The caller is responsible for ending the command buffer and immediately submitting it to a
314    /// graphics queue. The command buffer must be completely executed before calling [`frame`](Self::frame).
315    fn load_from_pack_deferred_internal(
316        preset: ShaderPresetPack,
317        device: Retained<ProtocolObject<dyn MTLDevice>>,
318        cmd: &ProtocolObject<dyn MTLCommandBuffer>,
319        options: Option<&FilterChainOptionsMetal>,
320    ) -> error::Result<FilterChainMetal> {
321        let config = RuntimeParameters::new(&preset);
322        let (passes, semantics) = compile_passes(preset.passes, &preset.textures)?;
323
324        let filters = Self::init_passes(&device, passes, &semantics)?;
325
326        let samplers = SamplerSet::new(&device)?;
327        let luts = FilterChainMetal::load_luts(&device, &cmd, preset.textures)?;
328        let framebuffer_gen = || {
329            Ok::<_, error::FilterChainError>(OwnedTexture::new(
330                &device,
331                Size::new(1, 1),
332                1,
333                ImageFormat::R8G8B8A8Unorm.into(),
334            )?)
335        };
336        let input_gen = || None;
337        let framebuffer_init = FramebufferInit::new(
338            filters.iter().map(|f| &f.reflection.meta),
339            &framebuffer_gen,
340            &input_gen,
341        );
342        let (output_framebuffers, output_textures) = framebuffer_init.init_output_framebuffers()?;
343        //
344        // initialize feedback framebuffers
345        let (feedback_framebuffers, feedback_textures) = framebuffer_init.init_feedback_framebuffers()?;
346        //
347        // initialize history
348        let (history_framebuffers, history_textures) = framebuffer_init.init_history()?;
349
350        let history_buffer = framebuffer_gen()?;
351
352        let draw_quad = DrawQuad::new(&device)?;
353        Ok(FilterChainMetal {
354            draw_last_pass_feedback: framebuffer_init.uses_final_pass_as_feedback(),
355            common: FilterCommon {
356                luts,
357                samplers,
358                config,
359                draw_quad,
360                device,
361                output_textures,
362                feedback_textures,
363                history_textures,
364            },
365            passes: filters,
366            output_framebuffers,
367            feedback_framebuffers,
368            history_framebuffers,
369            prev_frame_history_buffer: history_buffer,
370            disable_mipmaps: options.map(|f| f.force_no_mipmaps).unwrap_or(false),
371            default_options: Default::default(),
372        })
373    }
374
375    /// Records shader rendering commands to the provided command encoder.
376    ///
377    /// SAFETY: The `MTLCommandBuffer` provided must not have an active encoder.
378    pub fn frame(
379        &mut self,
380        input: &ProtocolObject<dyn MTLTexture>,
381        viewport: &Viewport<MetalTextureRef>,
382        cmd: &ProtocolObject<dyn MTLCommandBuffer>,
383        frame_count: usize,
384        options: Option<&FrameOptionsMetal>,
385    ) -> error::Result<()> {
386        let max = std::cmp::min(self.passes.len(), self.common.config.passes_enabled());
387        if let Some(options) = &options {
388            let clear_desc = MTLRenderPassDescriptor::new();
389            if options.clear_history {
390                for (index, history) in self.history_framebuffers.iter().enumerate() {
391                    unsafe {
392                        let ca = clear_desc
393                            .colorAttachments()
394                            .objectAtIndexedSubscript(index);
395                        ca.setTexture(Some(&history.texture));
396                        ca.setLoadAction(MTLLoadAction::Clear);
397                        ca.setStoreAction(MTLStoreAction::Store);
398                    }
399                }
400
401                let clearpass = cmd
402                    .renderCommandEncoderWithDescriptor(&clear_desc)
403                    .ok_or(FilterChainError::FailedToCreateCommandBuffer)?;
404                clearpass.endEncoding();
405            }
406        }
407
408        self.push_history(&cmd, &input)?;
409
410        let passes = &mut self.passes[0..max];
411        if passes.is_empty() {
412            return Ok(());
413        }
414
415        let filter = passes[0].meta.filter;
416        let wrap_mode = passes[0].meta.wrap_mode;
417
418        // update history
419        for (texture, image) in self
420            .common
421            .history_textures
422            .iter_mut()
423            .zip(self.history_framebuffers.iter())
424        {
425            *texture = Some(image.as_input(filter, wrap_mode)?);
426        }
427
428        let original = InputTexture {
429            texture: input
430                .newTextureViewWithPixelFormat(input.pixelFormat())
431                .ok_or(FilterChainError::FailedToCreateTexture)?,
432            wrap_mode,
433            filter_mode: filter,
434            mip_filter: filter,
435        };
436
437        let mut source = original.try_clone()?;
438
439        source
440            .texture
441            .setLabel(Some(&*NSString::from_str("librashader_sourcetex")));
442
443        let passes_len = passes.len();
444        let options = options.unwrap_or(&self.default_options);
445
446        // swap output and feedback **before** recording command buffers
447        for index in 0..passes_len {
448            if self.feedback_framebuffers.contains(index) {
449                std::mem::swap(&mut self.output_framebuffers[index], &mut self.feedback_framebuffers[index]);
450            }
451        }
452
453        let source_size: Size<u32> = get_texture_size(&source.texture).into();
454        let viewport_size = get_texture_size(viewport.output);
455        let original_size: Size<u32> = get_texture_size(&original.texture).into();
456
457        // The device acts as the scaling context. Clone it into a local so the
458        // context borrow stays disjoint from the `&mut self.common` borrows taken
459        // by the callbacks below.
460        let scale_context = self.common.device.clone();
461
462        // rescale feedback buffers and refresh their bound textures.
463        OwnedTexture::scale_feedback_framebuffers_with_context(
464            source_size,
465            viewport_size,
466            original_size,
467            &mut self.feedback_framebuffers,
468            passes,
469            &scale_context,
470            |index, pass, feedback| {
471                self.common.feedback_textures[index] =
472                    Some(feedback.as_input(pass.meta.filter, pass.meta.wrap_mode)?);
473                Ok(())
474            },
475        )?;
476
477        OwnedTexture::scale_output_framebuffers_with_context(
478            source_size,
479            viewport_size,
480            original_size,
481            &mut self.output_framebuffers,
482            passes,
483            &scale_context,
484            |index, pass, target, size| {
485                source.filter_mode = pass.meta.filter;
486                source.wrap_mode = pass.meta.wrap_mode;
487                source.mip_filter = pass.meta.filter;
488
489                if index != passes_len - 1 {
490                    let out = RenderTarget::identity(target.texture.as_ref())?;
491                    pass.draw(
492                        &cmd,
493                        index,
494                        &self.common,
495                        pass.meta.get_frame_count(frame_count),
496                        options,
497                        viewport,
498                        &original,
499                        &source,
500                        &out,
501                        None,
502                        QuadType::Offscreen,
503                    )?;
504
505                    if target.max_miplevels > 1 && !self.disable_mipmaps {
506                        target.generate_mipmaps(&cmd)?;
507                    }
508
509                    self.common.output_textures[index] =
510                        Some(target.as_input(pass.meta.filter, pass.meta.wrap_mode)?);
511                    source = self.common.output_textures[index]
512                        .as_ref()
513                        .map(InputTexture::try_clone)
514                        .unwrap()?;
515                    return Ok(());
516                }
517
518                if !pass
519                    .graphics_pipeline
520                    .has_format(viewport.output.pixelFormat())
521                {
522                    // need to recompile
523                    pass.graphics_pipeline
524                        .recompile(&self.common.device, viewport.output.pixelFormat())?;
525                }
526
527                // When feedback is enabled, render the last pass to the intermediate
528                // framebuffer first then render to the viewport with the OutputSize semantic
529                // overridden to the FB scale.
530                //
531                // Shaders need to see the pass's declared scale rather than the viewport size,
532                // or they won't render correctly for feedback.
533                let output_size_override = if self.draw_last_pass_feedback {
534                    let out = RenderTarget::viewport_with_output(target.texture.as_ref(), viewport);
535                    pass.draw(
536                        &cmd,
537                        index,
538                        &self.common,
539                        pass.meta.get_frame_count(frame_count),
540                        options,
541                        viewport,
542                        &original,
543                        &source,
544                        &out,
545                        None,
546                        QuadType::Final,
547                    )?;
548                    Some(size)
549                } else {
550                    None
551                };
552
553                let out = RenderTarget::viewport(viewport);
554                pass.draw(
555                    &cmd,
556                    index,
557                    &self.common,
558                    pass.meta.get_frame_count(frame_count),
559                    options,
560                    viewport,
561                    &original,
562                    &source,
563                    &out,
564                    output_size_override,
565                    QuadType::Final,
566                )?;
567                Ok(())
568            },
569        )?;
570
571        Ok(())
572    }
573}