123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985 |
- /******************************************************************************
- Copyright (C) 2024 by Patrick Heyer <[email protected]>
- 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 <http://www.gnu.org/licenses/>.
- ******************************************************************************/
- import Foundation
- import Metal
- import simd
- @inlinable
- public func unretained<Instance>(_ pointer: UnsafeRawPointer) -> Instance where Instance: AnyObject {
- Unmanaged<Instance>.fromOpaque(pointer).takeUnretainedValue()
- }
- @inlinable
- public func retained<Instance>(_ pointer: UnsafeRawPointer) -> Instance where Instance: AnyObject {
- Unmanaged<Instance>.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<CChar> {
- 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<CChar> {
- 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..<GS_MAX_TEXTURES {
- device.renderState.textures[Int(index)] = nil
- device.renderState.samplers[Int(index)] = nil
- }
- }
- /// Signals the end of a scene.
- /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
- ///
- /// OBS Studio's renderer signals the end of a scene for each "display" and for every "video mix", which implicitly
- /// marks the end of the output at a different format. As the Metal renderer needs a way to detect if all draw commands
- /// for a given "display" have ended (and there is no bespoke signal for that in the API), it uses an internal state
- /// variable to track if a display had been loaded for the "current" pipeline state and resets it at the "end of scene"
- /// signal.
- @_cdecl("device_end_scene")
- public func device_end_scene(device: UnsafeRawPointer) {
- let device: MetalDevice = unretained(device)
- if device.renderState.isInDisplaysRenderStage {
- device.finishDisplayRenderStage()
- device.renderState.isInDisplaysRenderStage = false
- }
- }
- /// Schedules a draw command on the GPU using all "state" variables set up by OBS Studio's renderer up to this point.
- /// - Parameters:
- /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
- /// - drawMode: Primitive type to draw as specified by `libobs`
- /// - startVertex: Start index of vertex to begin drawing with
- /// - numVertices: Count of vertices to draw
- ///
- /// Due to OBS Studio's design this function will usually render only a very low amount of vertices (commonly only 4
- /// of them) and very often those vertices are already loaded up as vertex buffers for use by the vertex shader. In
- /// those cases `libobs` does not seem to provide a vertex count and implicitly expects the graphics API implementation
- /// to deduct the vertex count from the amount of vertices available in its vertex data struct.
- ///
- /// In other cases a vertex shader will not use any buffers but calculate the vertex positions based on vertex ID and
- /// a non-null vertex count has to be provided.
- @_cdecl("device_draw")
- public func device_draw(device: UnsafeRawPointer, drawMode: gs_draw_mode, startVertex: UInt32, numVertices: UInt32) {
- let device: MetalDevice = unretained(device)
- guard let primitiveType = drawMode.mtlPrimitive else {
- OBSLog(.error, "device_draw: Unsupported draw mode provided: \(drawMode)")
- return
- }
- do {
- try device.draw(primitiveType: primitiveType, vertexStart: Int(startVertex), vertexCount: Int(numVertices))
- } catch let error as MetalError.MTLDeviceError {
- OBSLog(.error, "device_draw: \(error.description)")
- } catch {
- OBSLog(.error, "device_draw: Unknown error occurred")
- }
- }
- /// Sets up a load action for the "current" frame buffer and depth stencil attachment to simulate the "clear" behavior
- /// of other graphics APIs.
- /// - Parameters:
- /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
- /// - clearFlags: Bit field provided by `libobs` to mark the clear operations to handle
- /// - color: The RGBA color to use for clearing the frame buffer
- /// - depth: The depth to clear from the depth stencil attachment
- /// - stencil: The stencil to clear from the depth stencil attachment
- ///
- /// In APIs like OpenGL or Direct3D11 render targets have to be explicitly cleared. In OpenGL this is achieved by
- /// calling `glClear()` which will schedule a clear operation. Similarly Direct3D11 requires a call to
- /// `ClearRenderTargetView` with a specific `ID3D11RenderTargetView` to do the same.
- ///
- /// Metal does not provide an explicit command to "clear the screen" (as one does not render directly to screens
- /// anymore with these APIs). Instead Metal provides "load commands" and "store commands" which describe what should
- /// happen to a render target when it is loaded for rendering and unloaded after rendering.
- ///
- /// Thus a "clear" is a "load command" for a render target or depth stencil attachment that is automatically executed
- /// by Metal when it loads or stores them and thus requires Metal to do an explicit (empty) draw call to ensure that
- /// the load and store commands are executed even when no other draw calls will follow.
- @_cdecl("device_clear")
- public func device_clear(
- device: UnsafeRawPointer, clearFlags: UInt32, color: UnsafePointer<vec4>, 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<gs_rect>) {
- 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<gs_rect>?) {
- 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
- }
|