/****************************************************************************** Copyright (C) 2024 by Patrick Heyer This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ import Foundation import Metal import simd @inlinable public func unretained(_ pointer: UnsafeRawPointer) -> Instance where Instance: AnyObject { Unmanaged.fromOpaque(pointer).takeUnretainedValue() } @inlinable public func retained(_ pointer: UnsafeRawPointer) -> Instance where Instance: AnyObject { Unmanaged.fromOpaque(pointer).takeRetainedValue() } @inlinable public func OBSLog(_ level: OBSLogLevel, _ format: String, _ args: CVarArg...) { let logMessage = String.localizedStringWithFormat(format, args) logMessage.withCString { cMessage in withVaList([cMessage]) { arguments in blogva(level.rawValue, "%s", arguments) } } } /// Returns the graphics API name implemented by the "device". /// - Returns: Constant pointer to a C string with the API name /// @_cdecl("device_get_name") public func device_get_name() -> UnsafePointer { return device_name } /// Gets the graphics API identifier number for the "device". /// - Returns: Numerical identifier /// @_cdecl("device_get_type") public func device_get_type() -> Int32 { return GS_DEVICE_METAL } /// Returns a string to be used as a suffix for libobs' shader preprocessor, which will be used as part of a shaders /// identifying information. /// - Returns: Constant pointer to a C string with the suffix text @_cdecl("device_preprocessor_name") public func device_preprocessor_name() -> UnsafePointer { return preprocessor_name } /// Creates a new Metal device instance and stores an opaque pointer to a ``MetalDevice`` instance in the provided /// pointer. /// /// - Parameters: /// - devicePointer: Pointer to memory allocated by the caller to receive the pointer of the create device instance /// - adapter: Numerical identifier of a graphics display adaptor to create the device on. /// - Returns: Device creation result value defined as preprocessor macro in libobs' graphics API header /// /// This method will increment the reference count on the created ``MetalDevice`` instance to ensure it will not be /// deallocated until `libobs` actively relinquishes ownership of it via a call of `device_destroy`. /// /// > Important: As the Metal API is only supported on Apple Silicon devices, the adapter argument is effectively /// ignored (there is only ever one "adapter" in an Apple Silicon machine and thus only the "default" device is used. @_cdecl("device_create") public func device_create(devicePointer: UnsafeMutableRawPointer, adapter: UInt32) -> Int32 { guard NSProtocolFromString("MTLDevice") != nil else { OBSLog(.error, "This Mac does not support Metal.") return GS_ERROR_NOT_SUPPORTED } OBSLog(.info, "---------------------------------") guard let metalDevice = MTLCreateSystemDefaultDevice() else { OBSLog(.error, "Unable to initialize Metal device.") return GS_ERROR_FAIL } var descriptions: [String] = [] descriptions.append("Initializing Metal...") descriptions.append("\t- Name : \(metalDevice.name)") descriptions.append("\t- Unified Memory : \(metalDevice.hasUnifiedMemory ? "Yes" : "No")") descriptions.append("\t- Raytracing Support : \(metalDevice.supportsRaytracing ? "Yes" : "No")") if #available(macOS 14.0, *) { descriptions.append("\t- Architecture : \(metalDevice.architecture.name)") } OBSLog(.info, descriptions.joined(separator: "\n")) do { let device = try MetalDevice(device: metalDevice) let retained = Unmanaged.passRetained(device).toOpaque() let signalName = MetalSignalType.videoReset.rawValue let signalHandler = obs_get_signal_handler() signalName.withCString { signal_handler_connect(signalHandler, $0, metal_video_reset_handler, retained) } devicePointer.storeBytes(of: OpaquePointer(retained), as: OpaquePointer.self) } catch { OBSLog(.error, "Unable to create MetalDevice wrapper instance") return GS_ERROR_FAIL } return GS_SUCCESS } /// Uninitializes the Metal device instance created for libobs. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// /// This method will take ownership of the reference shared with `libobs` and thus return all strong references to the /// shared ``MetalDevice`` instance to pure Swift code (and thus its own memory managed). The active call to /// ``MetalDevice/shutdown()`` is necessary to ensure that internal clean up code runs _before_ `libobs` runs any of /// its own clean up code (which is not memory safe). @_cdecl("device_destroy") public func device_destroy(device: UnsafeMutableRawPointer) { let signalName = MetalSignalType.videoReset.rawValue let signalHandler = obs_get_signal_handler() signalName.withCString { signal_handler_disconnect(signalHandler, $0, metal_video_reset_handler, device) } let device: MetalDevice = retained(device) device.shutdown() } /// Returns opaque pointer to actual (wrapped) API-specific device object /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - Returns: Opaque pointer to ``MTLDevice`` object wrapped by ``MetalDevice`` instance /// /// The pointer shared by this function is unretained and is thus unsafe. It doesn't seem that anything in OBS Studio's /// codebase actually uses this function, but it is part of the graphics API and thus has to be implemented. @_cdecl("device_get_device_obj") public func device_get_device_obj(device: UnsafeMutableRawPointer) -> OpaquePointer? { let metalDevice: MetalDevice = unretained(device) let mtlDevice = metalDevice.device return OpaquePointer(Unmanaged.passUnretained(mtlDevice).toOpaque()) } /// Sets up the blend factor to be used by the current pipeline. /// /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - src: `libobs` blend type for the source /// - dest: `libobs` blend type for the destination /// /// This function uses the same blend factor for color and alpha channel. The enum values provided by `libobs` are /// converted into their appropriate ``MTLBlendFactor``variants automatically (if possible). /// /// > Important: Calling this function can trigger the creation of an entirely new render pipeline state, which is a /// costly operation. @_cdecl("device_blend_function") public func device_blend_function(device: UnsafeRawPointer, src: gs_blend_type, dest: gs_blend_type) { device_blend_function_separate( device: device, src_c: src, dest_c: dest, src_a: src, dest_a: dest ) } /// Sets up the color and alpha blend factors to be used by the current pipeline /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - src_c: `libobs` blend factor for the source color /// - dest_c: `libobs` blend factor for the destination color /// - src_a: `libobs` blend factor for the source alpha channel /// - dest_a: `libobs` blend factor for the destination alpha channel /// /// This function uses different blend factors for color and alpha channel. The enum values provided by `libobs` are /// converted into their appropriate ``MTLBlendFactor`` variants automatically (if possible). /// /// > Important: Calling this function can trigger the creation of an entirely new render pipeline state, which is a /// costly operation. @_cdecl("device_blend_function_separate") public func device_blend_function_separate( device: UnsafeRawPointer, src_c: gs_blend_type, dest_c: gs_blend_type, src_a: gs_blend_type, dest_a: gs_blend_type ) { let device: MetalDevice = unretained(device) let pipelineDescriptor = device.renderState.pipelineDescriptor guard let sourceRGBFactor = src_c.blendFactor, let sourceAlphaFactor = src_a.blendFactor, let destinationRGBFactor = dest_c.blendFactor, let destinationAlphaFactor = dest_a.blendFactor else { assertionFailure( """ device_blend_function_separate: Incompatible blend factors used. Values: - Source RGB : \(src_c) - Source Alpha : \(src_a) - Destination RGB : \(dest_c) - Destination Alpha : \(dest_a) """) return } pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = sourceRGBFactor pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = sourceAlphaFactor pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = destinationRGBFactor pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = destinationAlphaFactor } /// Sets the blend operation to be used by the current pipeline. /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - op: `libobs` blend operation name /// /// This function converts the provided `libobs` value into its appropriate ``MTLBlendOperation`` variant automatically /// (if possible). /// /// > Important: Calling this function can trigger the creation of an entirely new render pipeline state, which is a /// costly operation. @_cdecl("device_blend_op") public func device_blend_op(device: UnsafeRawPointer, op: gs_blend_op_type) { let device: MetalDevice = unretained(device) let pipelineDescriptor = device.renderState.pipelineDescriptor guard let blendOperation = op.mtlOperation else { assertionFailure("device_blend_op: Incompatible blend operation provided. Value: \(op)") return } pipelineDescriptor.colorAttachments[0].rgbBlendOperation = blendOperation } /// Returns the _current_ color space as set up by any preceding calls of the `libobs` renderer. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - Returns: Color space enum value as defined by `libobs` /// /// This color space value is commonly set by `libobs`' renderer to check the "current state", and make necessary /// switches to ensure color-correct rendering /// (e.g., to check if the renderer uses an SDR color space but the current source might provide HDR image data). This /// value is effectively just retained as a state variable for `libobs`. @_cdecl("device_get_color_space") public func device_get_color_space(device: UnsafeRawPointer) -> gs_color_space { let device: MetalDevice = unretained(device) return device.renderState.gsColorSpace } /// Signals the beginning of a new render loop iteration by `libobs` renderer. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// /// This function is the first graphics API-specific function called by `libobs` render loop and can be used as a /// signal to reset any lingering state of the prior loop iteration. /// /// For the Metal renderer this ensures that the current render target, current swap chain, as well as the list of /// active swap chains is reset. As the Metal renderer also needs to keep track of whether `libobs` is rendering any /// "displays", the associated state variable is also reset here. @_cdecl("device_begin_frame") public func device_begin_frame(device: UnsafeRawPointer) { let device: MetalDevice = unretained(device) device.renderState.useSRGBGamma = false device.renderState.renderTarget = nil device.renderState.swapChain = nil device.renderState.isInDisplaysRenderStage = false return } /// Gets a pointer to the current render target /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - Returns: Opaque pointer to ``MetalTexture`` object representing the render target /// /// OBS Studio's renderer only ever uses a single render target at the same time and switches them out if it needs to /// render a different output. Due to this single state approach, it needs to retain any "current" values before /// replacing them with (temporary) new values. It does so by retrieving pointers to the current objects set up within /// the graphics API's opaque implementation and storing them for later use. @_cdecl("device_get_render_target") public func device_get_render_target(device: UnsafeRawPointer) -> OpaquePointer? { let device: MetalDevice = unretained(device) guard let renderTarget = device.renderState.renderTarget else { return nil } return renderTarget.getUnretained() } /// Replaces the "current" render target and zstencil attachment with the objects associated by any provided non-`nil` /// pointers. /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - tex: Opaque (optional) pointer to ``MetalTexture`` instance shared with `libobs` /// - zstencil: Opaque (optional) pointer to ``MetalTexture`` instance shared with `libobs` /// /// This setter function is often used in conjunction with its associated getter function to temporarily "switch state" /// of the renderer by retaining a pointer to the "current" render target, setting up a new one, issuing a draw call, /// before restoring the original render target. /// /// This is regularly used for "texrender" instances, such as combining the chroma and luma components of a video frame /// (and uploaded as single- and dual-channel textures respectively) back into an RGB texture. This texture is then /// used as the "output" of its corresponding source in the "actual" render pass, which will use the original render /// target again. @_cdecl("device_set_render_target") public func device_set_render_target(device: UnsafeRawPointer, tex: UnsafeRawPointer?, zstencil: UnsafeRawPointer?) { device_set_render_target_with_color_space( device: device, tex: tex, zstencil: zstencil, space: GS_CS_SRGB ) } /// Replaces the "current" render target and zstencil attachment with the objects associated by any provided non-`nil` /// pointers and also updated the "current" color space used by the renderer. /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - tex: Opaque (optional) pointer to ``MetalTexture`` instance shared with `libobs` /// - zstencil: Opaque (optional) pointer to ``MetalTexture`` instance shared with `libobs` /// - space: `libobs`-based color space value /// /// This setter function is often used in conjunction with its associated getter function to temporarily "switch state" /// of the renderer by retaining a pointer to the "current" render target, setting up a new one, issuing a draw call, /// before restoring the original render target. /// /// This is regularly used for "texrender" instances, such as combining the chroma and luma components of a video frame /// (and uploaded as single- and dual-channel textures respectively) back into an RGB texture. This texture is then /// used as the "output" of its corresponding source in the "actual" render pass, which will use the original render /// target again. /// /// A `nil` pointer provided for either the render target or zstencil attachment means that the "current" value for /// either should be removed, leaving the renderer in an "invalid" state at least for the render target (using no /// zstencil attachment is a valid state however). /// /// > Important: Use this variant if you need to also update the "current" color space which might be checked by /// sources' render function to check whether linear gamma or sRGB's gamma will be used to encode color values. @_cdecl("device_set_render_target_with_color_space") public func device_set_render_target_with_color_space( device: UnsafeRawPointer, tex: UnsafeRawPointer?, zstencil: UnsafeRawPointer?, space: gs_color_space ) { let device: MetalDevice = unretained(device) if let tex { let metalTexture: MetalTexture = unretained(tex) device.renderState.renderTarget = metalTexture device.renderState.isRendertargetChanged = true } else { device.renderState.renderTarget = nil } if let zstencil { let zstencilAttachment: MetalTexture = unretained(zstencil) device.renderState.depthStencilAttachment = zstencilAttachment device.renderState.isRendertargetChanged = true } else { device.renderState.depthStencilAttachment = nil } device.renderState.gsColorSpace = space } /// Switches the current render state to use sRGB gamma encoding and decoding when reading from textures and writing /// into render targets /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - enable: Boolean to enable or disable the automatic sRGB gamma encoding and decoding /// /// OBS Studio's renderer has been retroactively updated to use sRGB color primaries _and_ gamma encoding by /// preference, but not by default. Any source has to opt-in to the use of automatic sRGB gamma encoding and decoding, /// while the default is still to use linear gamma. /// /// This method is thus used by sources to enable or disable the associated behavior and control the way color values /// generated by fragment shaders are written into the render target. @_cdecl("device_enable_framebuffer_srgb") public func device_enable_framebuffer_srgb(device: UnsafeRawPointer, enable: Bool) { let device: MetalDevice = unretained(device) if device.renderState.useSRGBGamma != enable { device.renderState.useSRGBGamma = enable device.renderState.isRendertargetChanged = true } } /// Retrieves the current render state's setting for using automatic encoding and decoding of color values using sRGB /// gamma. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - Returns: Boolean value of the sRGB gamma setting /// /// This function is used to check the current state which might have possibly been explicitly changed by calls of /// ``device_enable_framebuffer_srgb``. /// /// A source which might only be able to work with color values that have sRGB gamma already applied to them and thus /// might want to ensure that the color values provided by the fragment shader will not have the sRGB gamma curve /// encoded on them again. /// /// By calling this function, a source can check if automatic gamma encoding is enabled and then turn it off /// explicitly, which will ensure that color data is written as-is and no additional encoding will take place. @_cdecl("device_framebuffer_srgb_enabled") public func device_framebuffer_srgb_enabled(device: UnsafeRawPointer) -> Bool { let device: MetalDevice = unretained(device) return device.renderState.useSRGBGamma } /// Signals the beginning of a new scene. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// /// OBS Studio's renderer signals a new scene for each "display" and for every "video mix", which implicitly signals a /// change of output format. This usually also implies that all current textures that might have been set up for /// fragment shaders should be reset. For Metal this also requires creating a new "current" command buffer which should /// contain all GPU commands necessary to render the "scene". @_cdecl("device_begin_scene") public func device_begin_scene(device: UnsafeMutableRawPointer) { let device: MetalDevice = unretained(device) for index in 0.., depth: Float, stencil: UInt8 ) { let device: MetalDevice = unretained(device) var clearState = ClearState() if (Int32(clearFlags) & GS_CLEAR_COLOR) == 1 { clearState.colorAction = .clear clearState.clearColor = MTLClearColor( red: Double(color.pointee.x), green: Double(color.pointee.y), blue: Double(color.pointee.z), alpha: Double(color.pointee.w) ) } else { clearState.colorAction = .load } if (Int32(clearFlags) & GS_CLEAR_DEPTH) == 1 { clearState.clearDepth = Double(depth) clearState.depthAction = .clear } else { clearState.depthAction = .load } if (Int32(clearFlags) & GS_CLEAR_STENCIL) == 1 { clearState.clearStencil = UInt32(stencil) clearState.stencilAction = .clear } else { clearState.stencilAction = .load } do { try device.clear(state: clearState) } catch let error as MetalError.MTLDeviceError { OBSLog(.error, "device_clear: \(error.description)") } catch { OBSLog(.error, "device_clear: Unknown error occurred") } } /// Returns whether the current display is ready to preset a frame generated the renderer /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - Returns: Boolean value to state whether a frame generated by the renderer could actually be displayed /// /// As OBS Studio's renderer is not synced with the operating system's compositor, situations could arise where the /// renderer needs to be able to "hand off" a generated display output to the compositor but might not be able to /// because it's not "ready" to receive such a frame. If that is the case, the graphics API can check for such a state /// and return `false` here, allowing `libobs` to skip rendering the output for the "current" display entirely. /// /// In Direct3D11 the `DXGI_SWAP_EFFECT_FLIP_DISCARD` flip effect is used, which allows OBS Studio to render a preview /// into a buffer without having to care about the compositor. This is not possible in Metal as it's not the /// application that provides the output buffer, it's the compositor which provides a "drawable" surface. For each /// display there can only be a maximum of 3 drawables "in flight", a request for any consecutive drawable will stall /// the renderer. /// /// There is currently no way to check for the amount of available drawables, which could be used to return `false` /// here and would allow `libobs` to skip output rendering on its current frame and try again on the next. /// /// > Note: This check applies to the display associated with whichever "swap chain" might be "current" and is thus /// depends on swap chain state. @_cdecl("device_is_present_ready") public func device_is_present_ready(device: UnsafeRawPointer) -> Bool { return true } /// Commits the current command buffer to schedule and execute the GPU commands encoded within it and waits until they /// have been scheduled. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// /// OBS Studio's renderer will call this function when it has set up all draw commands for a given "display". It is /// usually accompanied by a call to end the current scene just before and thus marks the end of commands for the /// current command buffer. @_cdecl("device_present") public func device_present(device: UnsafeRawPointer) { let device: MetalDevice = unretained(device) device.finishPendingCommands() } /// Commits the current command buffer to schedule and execute the GPU commands encoded within it and waits until they /// have been scheduled. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// /// OBS Studio's renderer will call this function when it is finished setting up all draw commands for the video output /// texture, and also after it has used the GPU to encode a video output frame. @_cdecl("device_flush") public func device_flush(device: UnsafeRawPointer) { let device: MetalDevice = unretained(device) device.finishPendingCommands() } /// Sets the "current" cull mode to be used by the next draw call /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - mode: `libobs` cull mode identifier /// /// Converts the cull mode provided by `libobs` into its appropriate ``MTLCullMode`` variant. @_cdecl("device_set_cull_mode") public func device_set_cull_mode(device: UnsafeRawPointer, mode: gs_cull_mode) { let device: MetalDevice = unretained(device) device.renderState.cullMode = mode.mtlMode } /// Gets the "current" cull mode that was set up for the next draw call /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - Returns: `libobs` cull mode /// /// Converts the ``MTLCullMode`` set up currently into its `libobs` variation @_cdecl("device_get_cull_mode") public func device_get_cull_mode(device: UnsafeRawPointer) -> gs_cull_mode { let device: MetalDevice = unretained(device) return device.renderState.cullMode.obsMode } /// Switches blending of the next draw operation with the contents of the "current" framebuffer. /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - enable: `true` if contents should be blended, `false` otherwise /// /// This function directly enables or disables blending for the first render target set up in the current pipeline. @_cdecl("device_enable_blending") public func device_enable_blending(device: UnsafeRawPointer, enable: Bool) { let device: MetalDevice = unretained(device) device.renderState.pipelineDescriptor.colorAttachments[0].isBlendingEnabled = enable } /// Switches depth testing on the next draw operation with the contents of the current depth stencil buffer. /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - enable: `true` if depth testing should be enabled, `false` otherwise /// /// This function directly enables or disables depth texting for the depth stencil attachment set up in the current pipeline @_cdecl("device_enable_depth_test") public func device_enable_depth_test(device: UnsafeRawPointer, enable: Bool) { let device: MetalDevice = unretained(device) device.renderState.depthStencilDescriptor.isDepthWriteEnabled = enable } /// Sets the read mask in the depth stencil descriptor set up in the current pipeline /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - enable: `true` if the read mask should be `1`, `false` for a read mask of `0` /// /// The `MTLDepthStencilDescriptor` can differentiate between a front facing stencil and a back facing stencil. As /// `libobs` does not make this distinction, both values will be set to the same value. @_cdecl("device_enable_stencil_test") public func device_enable_stencil_test(device: UnsafeRawPointer, enable: Bool) { let device: MetalDevice = unretained(device) device.renderState.depthStencilDescriptor.frontFaceStencil.readMask = enable ? 1 : 0 device.renderState.depthStencilDescriptor.backFaceStencil.readMask = enable ? 1 : 0 } /// Sets the write mask in the depth stencil descriptor set up in the current pipeline /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - enable: `true` if the write mask should be `1`, `false` for a write mask of `0` /// /// The `MTLDepthStencilDescriptor` can differentiate between a front facing stencil and a back facing stencil. As /// `libobs` does not make this distinction, both values will be set to the same value. @_cdecl("device_enable_stencil_write") public func device_enable_stencil_write(device: UnsafeRawPointer, enable: Bool) { let device: MetalDevice = unretained(device) device.renderState.depthStencilDescriptor.frontFaceStencil.writeMask = enable ? 1 : 0 device.renderState.depthStencilDescriptor.backFaceStencil.writeMask = enable ? 1 : 0 } /// Sets the color write mask for the render target set up in the current pipeline /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - red: `true` if the red color channel should be written, `false` otherwise /// - green: `true` if the green color channel should be written, `false` otherwise /// - blue: `true` if the blue color channel should be written, `false` otherwise /// - alpha: `true` if the alpha channel should be written, `false` otherwise /// /// The separate `bool` values are converted into an ``MTLColorWriteMask`` which is then set up on the first render /// target of the current pipeline. @_cdecl("device_enable_color") public func device_enable_color(device: UnsafeRawPointer, red: Bool, green: Bool, blue: Bool, alpha: Bool) { let device: MetalDevice = unretained(device) var colorMask = MTLColorWriteMask() if red { colorMask.insert(.red) } if green { colorMask.insert(.green) } if blue { colorMask.insert(.blue) } if alpha { colorMask.insert(.alpha) } device.renderState.pipelineDescriptor.colorAttachments[0].writeMask = colorMask } /// Sets the depth compare function for the depth stencil descriptor to be used in the current pipeline /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - test: `libobs` enum describing the depth compare function to use /// /// The enum value provided by `libobs` is converted into a ``MTLCompareFunction``, which is then set directly as the /// compare function on the depth stencil descriptor. @_cdecl("device_depth_function") public func device_depth_function(device: UnsafeRawPointer, test: gs_depth_test) { let device: MetalDevice = unretained(device) device.renderState.depthStencilDescriptor.depthCompareFunction = test.mtlFunction } /// Sets the stencil compare functions for the specified stencil side(s) on the depth stencil descriptor in the current /// pipeline. /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - side: The stencil side(s) for which the compare function should be set up /// - test: `libobs` enum describing the stencil test function to use /// /// The enum values provided by `libobs` are first checked for the stencil side, after which the compare function value /// itself is converted into a ``MTLCompareFunction``, which is then set directly as the compare function on the depth /// stencil descriptor. @_cdecl("device_stencil_function") public func device_stencil_function(device: UnsafeRawPointer, side: gs_stencil_side, test: gs_depth_test) { let device: MetalDevice = unretained(device) let stencilCompareFunction: (MTLCompareFunction, MTLCompareFunction) if side == GS_STENCIL_FRONT { stencilCompareFunction = (test.mtlFunction, .never) } else if side == GS_STENCIL_BACK { stencilCompareFunction = (.never, test.mtlFunction) } else { stencilCompareFunction = (test.mtlFunction, test.mtlFunction) } device.renderState.depthStencilDescriptor.frontFaceStencil.stencilCompareFunction = stencilCompareFunction.0 device.renderState.depthStencilDescriptor.backFaceStencil.stencilCompareFunction = stencilCompareFunction.1 } /// Sets the stencil fail, depth fail, and depth pass operations for the specified stencil side(s) on the depth stencil /// descriptor for the current pipeline. /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - side: The stencil side(s) for which the fail and pass functions should be set up /// - fail: `libobs` enum value describing the stencil fail operation /// - zfail: `libobs` enum value describing the depth fail operation /// - zpass: `libobs` enum value describing the depth pass operation /// /// The enum values provided by `libobs` are first checked for the stencil side, after which the fail function values /// themselves are converted into their ``MTLCompareFunction`` variants, which are then set directly on the depth /// stencil descriptor. @_cdecl("device_stencil_op") public func device_stencil_op( device: UnsafeRawPointer, side: gs_stencil_side, fail: gs_stencil_op_type, zfail: gs_stencil_op_type, zpass: gs_stencil_op_type ) { let device: MetalDevice = unretained(device) let stencilFailOperation: (MTLStencilOperation, MTLStencilOperation) let depthFailOperation: (MTLStencilOperation, MTLStencilOperation) let depthPassOperation: (MTLStencilOperation, MTLStencilOperation) if side == GS_STENCIL_FRONT { stencilFailOperation = (fail.mtlOperation, .keep) depthFailOperation = (zfail.mtlOperation, .keep) depthPassOperation = (zpass.mtlOperation, .keep) } else if side == GS_STENCIL_BACK { stencilFailOperation = (.keep, fail.mtlOperation) depthFailOperation = (.keep, zfail.mtlOperation) depthPassOperation = (.keep, zpass.mtlOperation) } else { stencilFailOperation = (fail.mtlOperation, fail.mtlOperation) depthFailOperation = (zfail.mtlOperation, zfail.mtlOperation) depthPassOperation = (zpass.mtlOperation, zpass.mtlOperation) } device.renderState.depthStencilDescriptor.frontFaceStencil.stencilFailureOperation = stencilFailOperation.0 device.renderState.depthStencilDescriptor.frontFaceStencil.depthFailureOperation = depthFailOperation.0 device.renderState.depthStencilDescriptor.frontFaceStencil.depthStencilPassOperation = depthPassOperation.0 device.renderState.depthStencilDescriptor.backFaceStencil.stencilFailureOperation = stencilFailOperation.1 device.renderState.depthStencilDescriptor.backFaceStencil.depthFailureOperation = depthFailOperation.1 device.renderState.depthStencilDescriptor.backFaceStencil.depthStencilPassOperation = depthPassOperation.1 } /// Sets up the viewport for use in the current pipeline /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - x: Origin X coordinate for the viewport /// - y: Origin Y coordinate for the viewport /// - width: Width of the viewport /// - height: Height of the viewport /// /// The separate values for origin and dimension are converted into an ``MTLViewport`` which is then retained as the /// "current" viewport for later use when the pipeline is actually set up. @_cdecl("device_set_viewport") public func device_set_viewport(device: UnsafeRawPointer, x: Int32, y: Int32, width: Int32, height: Int32) { let device: MetalDevice = unretained(device) let viewPort = MTLViewport( originX: Double(x), originY: Double(y), width: Double(width), height: Double(height), znear: 0.0, zfar: 1.0 ) device.renderState.viewPort = viewPort } /// Gets the origin and dimensions of the viewport currently set up for use by the pipeline /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - rect: A pointer to a ``gs_rect`` struct in memory /// /// The function is provided a pointer to a ``gs_struct`` instance in memory which can hold the x and y values for the /// origin and dimension of the viewport. /// /// This function is usually called when some source needs to retain the current "state" of the pipeline (of which /// there can ever only be one) and overwrite the state with its own (in this case its own viewport). To be able to /// restore the prior state, the "current" state needs to be retrieved from the pipeline. @_cdecl("device_get_viewport") public func device_get_viewport(device: UnsafeRawPointer, rect: UnsafeMutablePointer) { let device: MetalDevice = unretained(device) rect.pointee.x = Int32(device.renderState.viewPort.originX) rect.pointee.y = Int32(device.renderState.viewPort.originY) rect.pointee.cx = Int32(device.renderState.viewPort.width) rect.pointee.cy = Int32(device.renderState.viewPort.height) } /// Sets up a scissor rect to be used by the current pipeline /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - rect: Pointer to a ``gs_rect`` struct in memory that contains origin and dimension of the scissor rect /// /// The ``gs_rect`` is converted into a ``MTLScissorRect`` object before saving it in the "current" render state /// for use in the next draw call. @_cdecl("device_set_scissor_rect") public func device_set_scissor_rect(device: UnsafeRawPointer, rect: UnsafePointer?) { let device: MetalDevice = unretained(device) if let rect { device.renderState.scissorRect = rect.pointee.mtlScissorRect device.renderState.scissorRectEnabled = true } else { device.renderState.scissorRect = nil device.renderState.scissorRectEnabled = false } } /// Sets up an orthographic projection matrix with the provided view frustum /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - left: Left edge of view frustum on the near plane /// - right: Right edge of view frustum on the near plane /// - top: Top edge of view frustum on the near plane /// - bottom: Bottom edge of view frustum on the near plane /// - near: Distance of near plane on the Z axis /// - far: Distance of far plane on the Z axis @_cdecl("device_ortho") public func device_ortho( device: UnsafeRawPointer, left: Float, right: Float, top: Float, bottom: Float, near: Float, far: Float ) { let device: MetalDevice = unretained(device) let rml = right - left let bmt = bottom - top let fmn = far - near device.renderState.projectionMatrix = matrix_float4x4( rows: [ SIMD4((2.0 / rml), 0.0, 0.0, 0.0), SIMD4(0.0, (2.0 / -bmt), 0.0, 0.0), SIMD4(0.0, 0.0, (1 / fmn), 0.0), SIMD4((left + right) / -rml, (bottom + top) / bmt, near / -fmn, 1.0), ] ) } /// Sets up a perspective projection matrix with the provided view frustum /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - left: Left edge of view frustum on the near plane /// - right: Right edge of view frustum on the near plane /// - top: Top edge of view frustum on the near plane /// - bottom: Bottom edge of view frustum on the near plane /// - near: Distance of near plane on the Z axis /// - far: Distance of far plane on the Z axis @_cdecl("device_frustum") public func device_frustum( device: UnsafeRawPointer, left: Float, right: Float, top: Float, bottom: Float, near: Float, far: Float ) { let device: MetalDevice = unretained(device) let rml = right - left let tmb = top - bottom let fmn = far - near device.renderState.projectionMatrix = matrix_float4x4( columns: ( SIMD4(((2 * near) / rml), 0.0, 0.0, 0.0), SIMD4(0.0, ((2 * near) / tmb), 0.0, 0.0), SIMD4(((left + right) / rml), ((top + bottom) / tmb), (-far / fmn), -1.0), SIMD4(0.0, 0.0, (-(far * near) / fmn), 0.0) ) ) } /// Requests the current projection matrix to be pushed into a projection stack /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// /// OBS Studio's renderer works with the assumption of one big "current" state stack, which requires the entire state /// to be changed to meet different rendering requirements. Part of this state is the current projection matrix, which /// might need to be replaced temporarily. This function will be called when another projection matrix will be set up /// to allow for its restoration later. @_cdecl("device_projection_push") public func device_projection_push(device: UnsafeRawPointer) { let device: MetalDevice = unretained(device) device.renderState.projections.append(device.renderState.projectionMatrix) } /// Requests the most recently pushed projection matrix to be removed from the stack and set up as the new current /// matrix /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// /// OBS Studio's renderer works with the assumption of one big "current" state stack. This requires some elements of /// this state to be temporarily retained before reinstating them after. This function will reinstate the most recently /// added matrix as the new "current" matrix. @_cdecl("device_projection_pop") public func device_projection_pop(device: UnsafeRawPointer) { let device: MetalDevice = unretained(device) device.renderState.projectionMatrix = device.renderState.projections.removeLast() } /// Checks whether the current display is capable of displaying high dynamic range content. /// /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - monitor: Opaque pointer of a platform-dependent monitor identifier /// - Returns: `true` if the display is capable of displaying high dynamic range content, `false` otherwise /// /// On macOS this capability is described by the ``NSScreen/maximumPotentialExtendedDynamicRangeColorComponentValue`` /// property, which can be checked using the ``NSWindow/screen`` property after retrieving the ``NSView/window`` /// property. @_cdecl("device_is_monitor_hdr") public func device_is_monitor_hdr(device: UnsafeRawPointer, monitor: UnsafeRawPointer) -> Bool { let device: MetalDevice = unretained(device) guard let swapChain = device.renderState.swapChain else { return false } return swapChain.edrHeadroom > 1.0 }