OBSForCaiic/obs-studio/libobs-metal/metal-swapchain.swift
2026-09-18 06:08:44 +08:00

270 lines
11 KiB
Swift

/******************************************************************************
Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com>
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 AppKit
import Foundation
/// Creates a ``OBSSwapChain`` instance for use as a pseudo swap chain implementation to be shared with `libobs`
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - data: Pointer to platform-specific `gs_init_data` struct
/// - Returns: Opaque pointer to a new ``OBSSwapChain`` on success or `nil` on error
///
/// As interaction with UI elements needs to happen on the main thread of macOS, this function is marked with
/// `@MainActor`. This is also necessary because ``OBSSwapChain/updateView`` itself interacts with the ``NSView``
/// instance passed via the `data` argument and also has to occur on the main thread.
///
/// As applications cannot manage their own swap chain on macOS, the ``OBSSwapChain`` class merely wraps the
/// management of the ``CAMetalLayer`` that will be associated with the ``NSView`` and handles the drawables used to
/// render their contents.
///
/// > Important: This function can only be called from the main thread.
@MainActor
@_cdecl("device_swapchain_create")
public func device_swapchain_create(device: UnsafeMutableRawPointer, data: UnsafePointer<gs_init_data>)
-> OpaquePointer?
{
let device: MetalDevice = unretained(device)
let view = data.pointee.window.view.takeUnretainedValue() as! NSView
let size = MTLSize(
width: Int(data.pointee.cx),
height: Int(data.pointee.cy),
depth: 0
)
guard let swapChain = OBSSwapChain(device: device, size: size, colorSpace: data.pointee.format) else { return nil }
swapChain.updateView(view)
device.swapChainQueue.sync {
device.swapChains.append(swapChain)
}
return swapChain.getRetained()
}
/// Updates the internal size parameter and dimension of the ``CAMetalLayer`` managed by the ``OBSSwapChain`` instance
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - width: Width to update the layer's dimensions to
/// - height: Height to update the layer's dimensions to
///
/// As the relationship between the ``CAMetalLayer`` and the ``NSView`` it is associated with is managed indirectly,
/// the metal layer cannot directly react to size changes (even though it would be possible to do so). Instead
/// ``AppKit`` will report a size change to the application, which will be picked up by Qt, who will emit a size
/// change event on the main loop, which will update internal state of the ``OBSQTDisplay`` class. These changes are
/// asynchronously picked up by `libobs` render loop, which will then call this function.
@_cdecl("device_resize")
public func device_resize(device: UnsafeMutableRawPointer, width: UInt32, height: UInt32) {
let device: MetalDevice = unretained(device)
guard let swapChain = device.renderState.swapChain else {
return
}
swapChain.resize(.init(width: Int(width), height: Int(height), depth: 0))
}
/// This function does nothing on Metal
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
///
/// The intended purpose of this function is to update the render target in the "current" swap chain with the color
/// space of its "display" and thus pick up changes in color spaces between different screens.
///
/// On macOS this just requires updating the EDR headroom for the screen the view might be associated with, as the
/// actual color space and EDR capabilities are evaluated on every render loop.
///
/// > Important: This function can only be called from the main thread.
@_cdecl("device_update_color_space")
public func device_update_color_space(device: UnsafeRawPointer) {
let device: MetalDevice = unretained(device)
guard device.renderState.swapChain != nil else {
return
}
nonisolated(unsafe) let swapChain = device.renderState.swapChain!
Task { @MainActor in
swapChain.updateEdrHeadroom()
}
}
/// Gets the dimensions of the ``CAMetalLayer`` managed by the ``OBSSwapChain`` instance set up in the current pipeline
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - cx: Pointer to memory for the width of the layer
/// - cy: Pointer to memory for the height of the layer
@_cdecl("device_get_size")
public func device_get_size(
device: UnsafeMutableRawPointer, cx: UnsafeMutablePointer<UInt32>, cy: UnsafeMutablePointer<UInt32>
) {
let device: MetalDevice = unretained(device)
guard let swapChain = device.renderState.swapChain else {
cx.pointee = 0
cy.pointee = 0
return
}
cx.pointee = UInt32(swapChain.viewSize.width)
cy.pointee = UInt32(swapChain.viewSize.height)
}
/// Gets the width of the ``CAMetalLayer`` managed by the ``OBSSwapChain`` instance set up in the current pipeline
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - Returns: Width of the layer
@_cdecl("device_get_width")
public func device_get_width(device: UnsafeRawPointer) -> UInt32 {
let device: MetalDevice = unretained(device)
guard let swapChain = device.renderState.swapChain else {
return 0
}
return UInt32(swapChain.viewSize.width)
}
/// Gets the height of the ``CAMetalLayer`` managed by the ``OBSSwapChain`` instance set up in the current pipeline
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - Returns: Height of the layer
@_cdecl("device_get_height")
public func device_get_height(device: UnsafeRawPointer) -> UInt32 {
let device: MetalDevice = unretained(device)
guard let swapChain = device.renderState.swapChain else {
return 0
}
return UInt32(swapChain.viewSize.height)
}
/// Sets up the ``OBSSwapChain`` for use in the current pipeline
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - swap: Opaque pointer to ``OBSSwapChain`` instance shared with `libobs`
///
/// The first call of this function in any render loop marks the "begin" of OBS Studio's display render stage. There
/// will only ever be one "current" swap chain in use by `libobs` and there is no dedicated call to "reset" or unload
/// the current swap chain, instead a new swap chain is loaded or the "scene end" function is called.
@_cdecl("device_load_swapchain")
public func device_load_swapchain(device: UnsafeRawPointer, swap: UnsafeRawPointer) {
let device: MetalDevice = unretained(device)
let swapChain: OBSSwapChain = unretained(swap)
if swapChain.edrHeadroom > 1.0 {
var videoInfo: obs_video_info = obs_video_info()
obs_get_video_info(&videoInfo)
let videoColorSpace = videoInfo.colorspace
switch videoColorSpace {
case VIDEO_CS_2100_PQ:
if swapChain.colorRange != .hdrPQ {
// TODO: Investigate whether it's viable to use PQ or HLG tone mapping for the preview
// Use the following code to enable it for either:
// 2100 PQ:
// let maxLuminance = obs_get_video_hdr_nominal_peak_level()
// swapChain.layer.edrMetadata = .hdr10(
// minLuminance: 0.0001, maxLuminance: maxLuminance, opticalOutputScale: 10000)
// HLG:
// swapChain.layer.edrMetadata = .hlg
swapChain.layer.pixelFormat = .rgba16Float
swapChain.layer.colorspace = CGColorSpace(name: CGColorSpace.extendedLinearSRGB)
swapChain.layer.wantsExtendedDynamicRangeContent = true
swapChain.layer.edrMetadata = nil
swapChain.colorRange = .hdrPQ
swapChain.renderTarget = nil
}
case VIDEO_CS_2100_HLG:
if swapChain.colorRange != .hdrHLG {
swapChain.layer.pixelFormat = .rgba16Float
swapChain.layer.colorspace = CGColorSpace(name: CGColorSpace.extendedLinearSRGB)
swapChain.layer.wantsExtendedDynamicRangeContent = true
swapChain.layer.edrMetadata = nil
swapChain.colorRange = .hdrHLG
swapChain.renderTarget = nil
}
default:
if swapChain.colorRange != .sdr {
swapChain.layer.pixelFormat = .bgra8Unorm_srgb
swapChain.layer.colorspace = CGColorSpace(name: CGColorSpace.sRGB)
swapChain.layer.wantsExtendedDynamicRangeContent = false
swapChain.layer.edrMetadata = nil
swapChain.colorRange = .sdr
swapChain.renderTarget = nil
}
}
} else {
if swapChain.colorRange != .sdr {
swapChain.layer.pixelFormat = .bgra8Unorm_srgb
swapChain.layer.colorspace = CGColorSpace(name: CGColorSpace.sRGB)
swapChain.layer.wantsExtendedDynamicRangeContent = false
swapChain.layer.edrMetadata = nil
swapChain.colorRange = .sdr
swapChain.renderTarget = nil
}
}
switch swapChain.colorRange {
case .hdrHLG, .hdrPQ:
device.renderState.gsColorSpace = GS_CS_709_EXTENDED
device.renderState.useSRGBGamma = false
case .sdr:
device.renderState.gsColorSpace = GS_CS_SRGB
device.renderState.useSRGBGamma = true
}
if let renderTarget = swapChain.renderTarget {
device.renderState.renderTarget = renderTarget
} else {
let descriptor = MTLTextureDescriptor.texture2DDescriptor(
pixelFormat: swapChain.layer.pixelFormat,
width: Int(swapChain.layer.drawableSize.width),
height: Int(swapChain.layer.drawableSize.height),
mipmapped: false)
descriptor.usage = [.renderTarget]
guard let renderTarget = MetalTexture(device: device, descriptor: descriptor) else {
return
}
swapChain.renderTarget = renderTarget
device.renderState.renderTarget = renderTarget
}
device.renderState.depthStencilAttachment = nil
device.renderState.isRendertargetChanged = true
device.renderState.isInDisplaysRenderStage = true
device.renderState.swapChain = swapChain
}
/// Requests deinitialization of the ``OBSSwapChain`` instance shared with `libobs`
/// - Parameter texture: Opaque pointer to ``OBSSwapChain`` instance shared with `libobs`
///
/// The ownership of the shared pointer is transferred into this function and the instance is placed under Swift's
/// memory management again.
@_cdecl("gs_swapchain_destroy")
public func gs_swapchain_destroy(swapChain: UnsafeMutableRawPointer) {
let swapChain = retained(swapChain) as OBSSwapChain
swapChain.discard = true
}