metal-swapchain.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. /******************************************************************************
  2. Copyright (C) 2024 by Patrick Heyer <[email protected]>
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU General Public License as published by
  5. the Free Software Foundation, either version 2 of the License, or
  6. (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU General Public License for more details.
  11. You should have received a copy of the GNU General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. ******************************************************************************/
  14. import AppKit
  15. import Foundation
  16. /// Creates a ``OBSSwapChain`` instance for use as a pseudo swap chain implementation to be shared with `libobs`
  17. /// - Parameters:
  18. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  19. /// - data: Pointer to platform-specific `gs_init_data` struct
  20. /// - Returns: Opaque pointer to a new ``OBSSwapChain`` on success or `nil` on error
  21. ///
  22. /// As interaction with UI elements needs to happen on the main thread of macOS, this function is marked with
  23. /// `@MainActor`. This is also necessary because ``OBSSwapChain/updateView`` itself interacts with the ``NSView``
  24. /// instance passed via the `data` argument and also has to occur on the main thread.
  25. ///
  26. /// As applications cannot manage their own swap chain on macOS, the ``OBSSwapChain`` class merely wraps the
  27. /// management of the ``CAMetalLayer`` that will be associated with the ``NSView`` and handles the drawables used to
  28. /// render their contents.
  29. ///
  30. /// > Important: This function can only be called from the main thread.
  31. @MainActor
  32. @_cdecl("device_swapchain_create")
  33. public func device_swapchain_create(device: UnsafeMutableRawPointer, data: UnsafePointer<gs_init_data>)
  34. -> OpaquePointer?
  35. {
  36. let device: MetalDevice = unretained(device)
  37. let view = data.pointee.window.view.takeUnretainedValue() as! NSView
  38. let size = MTLSize(
  39. width: Int(data.pointee.cx),
  40. height: Int(data.pointee.cy),
  41. depth: 0
  42. )
  43. guard let swapChain = OBSSwapChain(device: device, size: size, colorSpace: data.pointee.format) else { return nil }
  44. swapChain.updateView(view)
  45. device.swapChainQueue.sync {
  46. device.swapChains.append(swapChain)
  47. }
  48. return swapChain.getRetained()
  49. }
  50. /// Updates the internal size parameter and dimension of the ``CAMetalLayer`` managed by the ``OBSSwapChain`` instance
  51. /// - Parameters:
  52. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  53. /// - width: Width to update the layer's dimensions to
  54. /// - height: Height to update the layer's dimensions to
  55. ///
  56. /// As the relationship between the ``CAMetalLayer`` and the ``NSView`` it is associated with is managed indirectly,
  57. /// the metal layer cannot directly react to size changes (even though it would be possible to do so). Instead
  58. /// ``AppKit`` will report a size change to the application, which will be picked up by Qt, who will emit a size
  59. /// change event on the main loop, which will update internal state of the ``OBSQTDisplay`` class. These changes are
  60. /// asynchronously picked up by `libobs` render loop, which will then call this function.
  61. @_cdecl("device_resize")
  62. public func device_resize(device: UnsafeMutableRawPointer, width: UInt32, height: UInt32) {
  63. let device: MetalDevice = unretained(device)
  64. guard let swapChain = device.renderState.swapChain else {
  65. return
  66. }
  67. swapChain.resize(.init(width: Int(width), height: Int(height), depth: 0))
  68. }
  69. /// This function does nothing on Metal
  70. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  71. ///
  72. /// The intended purpose of this function is to update the render target in the "current" swap chain with the color
  73. /// space of its "display" and thus pick up changes in color spaces between different screens.
  74. ///
  75. /// On macOS this just requires updating the EDR headroom for the screen the view might be associated with, as the
  76. /// actual color space and EDR capabilities are evaluated on every render loop.
  77. ///
  78. /// > Important: This function can only be called from the main thread.
  79. @_cdecl("device_update_color_space")
  80. public func device_update_color_space(device: UnsafeRawPointer) {
  81. let device: MetalDevice = unretained(device)
  82. guard device.renderState.swapChain != nil else {
  83. return
  84. }
  85. nonisolated(unsafe) let swapChain = device.renderState.swapChain!
  86. Task { @MainActor in
  87. swapChain.updateEdrHeadroom()
  88. }
  89. }
  90. /// Gets the dimensions of the ``CAMetalLayer`` managed by the ``OBSSwapChain`` instance set up in the current pipeline
  91. /// - Parameters:
  92. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  93. /// - cx: Pointer to memory for the width of the layer
  94. /// - cy: Pointer to memory for the height of the layer
  95. @_cdecl("device_get_size")
  96. public func device_get_size(
  97. device: UnsafeMutableRawPointer, cx: UnsafeMutablePointer<UInt32>, cy: UnsafeMutablePointer<UInt32>
  98. ) {
  99. let device: MetalDevice = unretained(device)
  100. guard let swapChain = device.renderState.swapChain else {
  101. cx.pointee = 0
  102. cy.pointee = 0
  103. return
  104. }
  105. cx.pointee = UInt32(swapChain.viewSize.width)
  106. cy.pointee = UInt32(swapChain.viewSize.height)
  107. }
  108. /// Gets the width of the ``CAMetalLayer`` managed by the ``OBSSwapChain`` instance set up in the current pipeline
  109. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  110. /// - Returns: Width of the layer
  111. @_cdecl("device_get_width")
  112. public func device_get_width(device: UnsafeRawPointer) -> UInt32 {
  113. let device: MetalDevice = unretained(device)
  114. guard let swapChain = device.renderState.swapChain else {
  115. return 0
  116. }
  117. return UInt32(swapChain.viewSize.width)
  118. }
  119. /// Gets the height of the ``CAMetalLayer`` managed by the ``OBSSwapChain`` instance set up in the current pipeline
  120. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  121. /// - Returns: Height of the layer
  122. @_cdecl("device_get_height")
  123. public func device_get_height(device: UnsafeRawPointer) -> UInt32 {
  124. let device: MetalDevice = unretained(device)
  125. guard let swapChain = device.renderState.swapChain else {
  126. return 0
  127. }
  128. return UInt32(swapChain.viewSize.height)
  129. }
  130. /// Sets up the ``OBSSwapChain`` for use in the current pipeline
  131. /// - Parameters:
  132. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  133. /// - swap: Opaque pointer to ``OBSSwapChain`` instance shared with `libobs`
  134. ///
  135. /// The first call of this function in any render loop marks the "begin" of OBS Studio's display render stage. There
  136. /// will only ever be one "current" swap chain in use by `libobs` and there is no dedicated call to "reset" or unload
  137. /// the current swap chain, instead a new swap chain is loaded or the "scene end" function is called.
  138. @_cdecl("device_load_swapchain")
  139. public func device_load_swapchain(device: UnsafeRawPointer, swap: UnsafeRawPointer) {
  140. let device: MetalDevice = unretained(device)
  141. let swapChain: OBSSwapChain = unretained(swap)
  142. if swapChain.edrHeadroom > 1.0 {
  143. var videoInfo: obs_video_info = obs_video_info()
  144. obs_get_video_info(&videoInfo)
  145. let videoColorSpace = videoInfo.colorspace
  146. switch videoColorSpace {
  147. case VIDEO_CS_2100_PQ:
  148. if swapChain.colorRange != .hdrPQ {
  149. // TODO: Investigate whether it's viable to use PQ or HLG tone mapping for the preview
  150. // Use the following code to enable it for either:
  151. // 2100 PQ:
  152. // let maxLuminance = obs_get_video_hdr_nominal_peak_level()
  153. // swapChain.layer.edrMetadata = .hdr10(
  154. // minLuminance: 0.0001, maxLuminance: maxLuminance, opticalOutputScale: 10000)
  155. // HLG:
  156. // swapChain.layer.edrMetadata = .hlg
  157. swapChain.layer.pixelFormat = .rgba16Float
  158. swapChain.layer.colorspace = CGColorSpace(name: CGColorSpace.extendedLinearSRGB)
  159. swapChain.layer.wantsExtendedDynamicRangeContent = true
  160. swapChain.layer.edrMetadata = nil
  161. swapChain.colorRange = .hdrPQ
  162. swapChain.renderTarget = nil
  163. }
  164. case VIDEO_CS_2100_HLG:
  165. if swapChain.colorRange != .hdrHLG {
  166. swapChain.layer.pixelFormat = .rgba16Float
  167. swapChain.layer.colorspace = CGColorSpace(name: CGColorSpace.extendedLinearSRGB)
  168. swapChain.layer.wantsExtendedDynamicRangeContent = true
  169. swapChain.layer.edrMetadata = nil
  170. swapChain.colorRange = .hdrHLG
  171. swapChain.renderTarget = nil
  172. }
  173. default:
  174. if swapChain.colorRange != .sdr {
  175. swapChain.layer.pixelFormat = .bgra8Unorm_srgb
  176. swapChain.layer.colorspace = CGColorSpace(name: CGColorSpace.sRGB)
  177. swapChain.layer.wantsExtendedDynamicRangeContent = false
  178. swapChain.layer.edrMetadata = nil
  179. swapChain.colorRange = .sdr
  180. swapChain.renderTarget = nil
  181. }
  182. }
  183. } else {
  184. if swapChain.colorRange != .sdr {
  185. swapChain.layer.pixelFormat = .bgra8Unorm_srgb
  186. swapChain.layer.colorspace = CGColorSpace(name: CGColorSpace.sRGB)
  187. swapChain.layer.wantsExtendedDynamicRangeContent = false
  188. swapChain.layer.edrMetadata = nil
  189. swapChain.colorRange = .sdr
  190. swapChain.renderTarget = nil
  191. }
  192. }
  193. switch swapChain.colorRange {
  194. case .hdrHLG, .hdrPQ:
  195. device.renderState.gsColorSpace = GS_CS_709_EXTENDED
  196. device.renderState.useSRGBGamma = false
  197. case .sdr:
  198. device.renderState.gsColorSpace = GS_CS_SRGB
  199. device.renderState.useSRGBGamma = true
  200. }
  201. if let renderTarget = swapChain.renderTarget {
  202. device.renderState.renderTarget = renderTarget
  203. } else {
  204. let descriptor = MTLTextureDescriptor.texture2DDescriptor(
  205. pixelFormat: swapChain.layer.pixelFormat,
  206. width: Int(swapChain.layer.drawableSize.width),
  207. height: Int(swapChain.layer.drawableSize.height),
  208. mipmapped: false)
  209. descriptor.usage = [.renderTarget]
  210. guard let renderTarget = MetalTexture(device: device, descriptor: descriptor) else {
  211. return
  212. }
  213. swapChain.renderTarget = renderTarget
  214. device.renderState.renderTarget = renderTarget
  215. }
  216. device.renderState.depthStencilAttachment = nil
  217. device.renderState.isRendertargetChanged = true
  218. device.renderState.isInDisplaysRenderStage = true
  219. device.renderState.swapChain = swapChain
  220. }
  221. /// Requests deinitialization of the ``OBSSwapChain`` instance shared with `libobs`
  222. /// - Parameter texture: Opaque pointer to ``OBSSwapChain`` instance shared with `libobs`
  223. ///
  224. /// The ownership of the shared pointer is transferred into this function and the instance is placed under Swift's
  225. /// memory management again.
  226. @_cdecl("gs_swapchain_destroy")
  227. public func gs_swapchain_destroy(swapChain: UnsafeMutableRawPointer) {
  228. let swapChain = retained(swapChain) as OBSSwapChain
  229. swapChain.discard = true
  230. }