metal-subsystem.swift 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985
  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 Foundation
  15. import Metal
  16. import simd
  17. @inlinable
  18. public func unretained<Instance>(_ pointer: UnsafeRawPointer) -> Instance where Instance: AnyObject {
  19. Unmanaged<Instance>.fromOpaque(pointer).takeUnretainedValue()
  20. }
  21. @inlinable
  22. public func retained<Instance>(_ pointer: UnsafeRawPointer) -> Instance where Instance: AnyObject {
  23. Unmanaged<Instance>.fromOpaque(pointer).takeRetainedValue()
  24. }
  25. @inlinable
  26. public func OBSLog(_ level: OBSLogLevel, _ format: String, _ args: CVarArg...) {
  27. let logMessage = String.localizedStringWithFormat(format, args)
  28. logMessage.withCString { cMessage in
  29. withVaList([cMessage]) { arguments in
  30. blogva(level.rawValue, "%s", arguments)
  31. }
  32. }
  33. }
  34. /// Returns the graphics API name implemented by the "device".
  35. /// - Returns: Constant pointer to a C string with the API name
  36. ///
  37. @_cdecl("device_get_name")
  38. public func device_get_name() -> UnsafePointer<CChar> {
  39. return device_name
  40. }
  41. /// Gets the graphics API identifier number for the "device".
  42. /// - Returns: Numerical identifier
  43. ///
  44. @_cdecl("device_get_type")
  45. public func device_get_type() -> Int32 {
  46. return GS_DEVICE_METAL
  47. }
  48. /// Returns a string to be used as a suffix for libobs' shader preprocessor, which will be used as part of a shaders
  49. /// identifying information.
  50. /// - Returns: Constant pointer to a C string with the suffix text
  51. @_cdecl("device_preprocessor_name")
  52. public func device_preprocessor_name() -> UnsafePointer<CChar> {
  53. return preprocessor_name
  54. }
  55. /// Creates a new Metal device instance and stores an opaque pointer to a ``MetalDevice`` instance in the provided
  56. /// pointer.
  57. ///
  58. /// - Parameters:
  59. /// - devicePointer: Pointer to memory allocated by the caller to receive the pointer of the create device instance
  60. /// - adapter: Numerical identifier of a graphics display adaptor to create the device on.
  61. /// - Returns: Device creation result value defined as preprocessor macro in libobs' graphics API header
  62. ///
  63. /// This method will increment the reference count on the created ``MetalDevice`` instance to ensure it will not be
  64. /// deallocated until `libobs` actively relinquishes ownership of it via a call of `device_destroy`.
  65. ///
  66. /// > Important: As the Metal API is only supported on Apple Silicon devices, the adapter argument is effectively
  67. /// ignored (there is only ever one "adapter" in an Apple Silicon machine and thus only the "default" device is used.
  68. @_cdecl("device_create")
  69. public func device_create(devicePointer: UnsafeMutableRawPointer, adapter: UInt32) -> Int32 {
  70. guard NSProtocolFromString("MTLDevice") != nil else {
  71. OBSLog(.error, "This Mac does not support Metal.")
  72. return GS_ERROR_NOT_SUPPORTED
  73. }
  74. OBSLog(.info, "---------------------------------")
  75. guard let metalDevice = MTLCreateSystemDefaultDevice() else {
  76. OBSLog(.error, "Unable to initialize Metal device.")
  77. return GS_ERROR_FAIL
  78. }
  79. var descriptions: [String] = []
  80. descriptions.append("Initializing Metal...")
  81. descriptions.append("\t- Name : \(metalDevice.name)")
  82. descriptions.append("\t- Unified Memory : \(metalDevice.hasUnifiedMemory ? "Yes" : "No")")
  83. descriptions.append("\t- Raytracing Support : \(metalDevice.supportsRaytracing ? "Yes" : "No")")
  84. if #available(macOS 14.0, *) {
  85. descriptions.append("\t- Architecture : \(metalDevice.architecture.name)")
  86. }
  87. OBSLog(.info, descriptions.joined(separator: "\n"))
  88. do {
  89. let device = try MetalDevice(device: metalDevice)
  90. let retained = Unmanaged.passRetained(device).toOpaque()
  91. let signalName = MetalSignalType.videoReset.rawValue
  92. let signalHandler = obs_get_signal_handler()
  93. signalName.withCString {
  94. signal_handler_connect(signalHandler, $0, metal_video_reset_handler, retained)
  95. }
  96. devicePointer.storeBytes(of: OpaquePointer(retained), as: OpaquePointer.self)
  97. } catch {
  98. OBSLog(.error, "Unable to create MetalDevice wrapper instance")
  99. return GS_ERROR_FAIL
  100. }
  101. return GS_SUCCESS
  102. }
  103. /// Uninitializes the Metal device instance created for libobs.
  104. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  105. ///
  106. /// This method will take ownership of the reference shared with `libobs` and thus return all strong references to the
  107. /// shared ``MetalDevice`` instance to pure Swift code (and thus its own memory managed). The active call to
  108. /// ``MetalDevice/shutdown()`` is necessary to ensure that internal clean up code runs _before_ `libobs` runs any of
  109. /// its own clean up code (which is not memory safe).
  110. @_cdecl("device_destroy")
  111. public func device_destroy(device: UnsafeMutableRawPointer) {
  112. let signalName = MetalSignalType.videoReset.rawValue
  113. let signalHandler = obs_get_signal_handler()
  114. signalName.withCString {
  115. signal_handler_disconnect(signalHandler, $0, metal_video_reset_handler, device)
  116. }
  117. let device: MetalDevice = retained(device)
  118. device.shutdown()
  119. }
  120. /// Returns opaque pointer to actual (wrapped) API-specific device object
  121. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  122. /// - Returns: Opaque pointer to ``MTLDevice`` object wrapped by ``MetalDevice`` instance
  123. ///
  124. /// The pointer shared by this function is unretained and is thus unsafe. It doesn't seem that anything in OBS Studio's
  125. /// codebase actually uses this function, but it is part of the graphics API and thus has to be implemented.
  126. @_cdecl("device_get_device_obj")
  127. public func device_get_device_obj(device: UnsafeMutableRawPointer) -> OpaquePointer? {
  128. let metalDevice: MetalDevice = unretained(device)
  129. let mtlDevice = metalDevice.device
  130. return OpaquePointer(Unmanaged.passUnretained(mtlDevice).toOpaque())
  131. }
  132. /// Sets up the blend factor to be used by the current pipeline.
  133. ///
  134. /// - Parameters:
  135. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  136. /// - src: `libobs` blend type for the source
  137. /// - dest: `libobs` blend type for the destination
  138. ///
  139. /// This function uses the same blend factor for color and alpha channel. The enum values provided by `libobs` are
  140. /// converted into their appropriate ``MTLBlendFactor``variants automatically (if possible).
  141. ///
  142. /// > Important: Calling this function can trigger the creation of an entirely new render pipeline state, which is a
  143. /// costly operation.
  144. @_cdecl("device_blend_function")
  145. public func device_blend_function(device: UnsafeRawPointer, src: gs_blend_type, dest: gs_blend_type) {
  146. device_blend_function_separate(
  147. device: device,
  148. src_c: src,
  149. dest_c: dest,
  150. src_a: src,
  151. dest_a: dest
  152. )
  153. }
  154. /// Sets up the color and alpha blend factors to be used by the current pipeline
  155. /// - Parameters:
  156. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  157. /// - src_c: `libobs` blend factor for the source color
  158. /// - dest_c: `libobs` blend factor for the destination color
  159. /// - src_a: `libobs` blend factor for the source alpha channel
  160. /// - dest_a: `libobs` blend factor for the destination alpha channel
  161. ///
  162. /// This function uses different blend factors for color and alpha channel. The enum values provided by `libobs` are
  163. /// converted into their appropriate ``MTLBlendFactor`` variants automatically (if possible).
  164. ///
  165. /// > Important: Calling this function can trigger the creation of an entirely new render pipeline state, which is a
  166. /// costly operation.
  167. @_cdecl("device_blend_function_separate")
  168. public func device_blend_function_separate(
  169. device: UnsafeRawPointer, src_c: gs_blend_type, dest_c: gs_blend_type, src_a: gs_blend_type, dest_a: gs_blend_type
  170. ) {
  171. let device: MetalDevice = unretained(device)
  172. let pipelineDescriptor = device.renderState.pipelineDescriptor
  173. guard let sourceRGBFactor = src_c.blendFactor,
  174. let sourceAlphaFactor = src_a.blendFactor,
  175. let destinationRGBFactor = dest_c.blendFactor,
  176. let destinationAlphaFactor = dest_a.blendFactor
  177. else {
  178. assertionFailure(
  179. """
  180. device_blend_function_separate: Incompatible blend factors used. Values:
  181. - Source RGB : \(src_c)
  182. - Source Alpha : \(src_a)
  183. - Destination RGB : \(dest_c)
  184. - Destination Alpha : \(dest_a)
  185. """)
  186. return
  187. }
  188. pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = sourceRGBFactor
  189. pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = sourceAlphaFactor
  190. pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = destinationRGBFactor
  191. pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = destinationAlphaFactor
  192. }
  193. /// Sets the blend operation to be used by the current pipeline.
  194. /// - Parameters:
  195. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  196. /// - op: `libobs` blend operation name
  197. ///
  198. /// This function converts the provided `libobs` value into its appropriate ``MTLBlendOperation`` variant automatically
  199. /// (if possible).
  200. ///
  201. /// > Important: Calling this function can trigger the creation of an entirely new render pipeline state, which is a
  202. /// costly operation.
  203. @_cdecl("device_blend_op")
  204. public func device_blend_op(device: UnsafeRawPointer, op: gs_blend_op_type) {
  205. let device: MetalDevice = unretained(device)
  206. let pipelineDescriptor = device.renderState.pipelineDescriptor
  207. guard let blendOperation = op.mtlOperation else {
  208. assertionFailure("device_blend_op: Incompatible blend operation provided. Value: \(op)")
  209. return
  210. }
  211. pipelineDescriptor.colorAttachments[0].rgbBlendOperation = blendOperation
  212. }
  213. /// Returns the _current_ color space as set up by any preceding calls of the `libobs` renderer.
  214. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  215. /// - Returns: Color space enum value as defined by `libobs`
  216. ///
  217. /// This color space value is commonly set by `libobs`' renderer to check the "current state", and make necessary
  218. /// switches to ensure color-correct rendering
  219. /// (e.g., to check if the renderer uses an SDR color space but the current source might provide HDR image data). This
  220. /// value is effectively just retained as a state variable for `libobs`.
  221. @_cdecl("device_get_color_space")
  222. public func device_get_color_space(device: UnsafeRawPointer) -> gs_color_space {
  223. let device: MetalDevice = unretained(device)
  224. return device.renderState.gsColorSpace
  225. }
  226. /// Signals the beginning of a new render loop iteration by `libobs` renderer.
  227. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  228. ///
  229. /// This function is the first graphics API-specific function called by `libobs` render loop and can be used as a
  230. /// signal to reset any lingering state of the prior loop iteration.
  231. ///
  232. /// For the Metal renderer this ensures that the current render target, current swap chain, as well as the list of
  233. /// active swap chains is reset. As the Metal renderer also needs to keep track of whether `libobs` is rendering any
  234. /// "displays", the associated state variable is also reset here.
  235. @_cdecl("device_begin_frame")
  236. public func device_begin_frame(device: UnsafeRawPointer) {
  237. let device: MetalDevice = unretained(device)
  238. device.renderState.useSRGBGamma = false
  239. device.renderState.renderTarget = nil
  240. device.renderState.swapChain = nil
  241. device.renderState.isInDisplaysRenderStage = false
  242. return
  243. }
  244. /// Gets a pointer to the current render target
  245. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  246. /// - Returns: Opaque pointer to ``MetalTexture`` object representing the render target
  247. ///
  248. /// OBS Studio's renderer only ever uses a single render target at the same time and switches them out if it needs to
  249. /// render a different output. Due to this single state approach, it needs to retain any "current" values before
  250. /// replacing them with (temporary) new values. It does so by retrieving pointers to the current objects set up within
  251. /// the graphics API's opaque implementation and storing them for later use.
  252. @_cdecl("device_get_render_target")
  253. public func device_get_render_target(device: UnsafeRawPointer) -> OpaquePointer? {
  254. let device: MetalDevice = unretained(device)
  255. guard let renderTarget = device.renderState.renderTarget else {
  256. return nil
  257. }
  258. return renderTarget.getUnretained()
  259. }
  260. /// Replaces the "current" render target and zstencil attachment with the objects associated by any provided non-`nil`
  261. /// pointers.
  262. /// - Parameters:
  263. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  264. /// - tex: Opaque (optional) pointer to ``MetalTexture`` instance shared with `libobs`
  265. /// - zstencil: Opaque (optional) pointer to ``MetalTexture`` instance shared with `libobs`
  266. ///
  267. /// This setter function is often used in conjunction with its associated getter function to temporarily "switch state"
  268. /// of the renderer by retaining a pointer to the "current" render target, setting up a new one, issuing a draw call,
  269. /// before restoring the original render target.
  270. ///
  271. /// This is regularly used for "texrender" instances, such as combining the chroma and luma components of a video frame
  272. /// (and uploaded as single- and dual-channel textures respectively) back into an RGB texture. This texture is then
  273. /// used as the "output" of its corresponding source in the "actual" render pass, which will use the original render
  274. /// target again.
  275. @_cdecl("device_set_render_target")
  276. public func device_set_render_target(device: UnsafeRawPointer, tex: UnsafeRawPointer?, zstencil: UnsafeRawPointer?) {
  277. device_set_render_target_with_color_space(
  278. device: device,
  279. tex: tex,
  280. zstencil: zstencil,
  281. space: GS_CS_SRGB
  282. )
  283. }
  284. /// Replaces the "current" render target and zstencil attachment with the objects associated by any provided non-`nil`
  285. /// pointers and also updated the "current" color space used by the renderer.
  286. /// - Parameters:
  287. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  288. /// - tex: Opaque (optional) pointer to ``MetalTexture`` instance shared with `libobs`
  289. /// - zstencil: Opaque (optional) pointer to ``MetalTexture`` instance shared with `libobs`
  290. /// - space: `libobs`-based color space value
  291. ///
  292. /// This setter function is often used in conjunction with its associated getter function to temporarily "switch state"
  293. /// of the renderer by retaining a pointer to the "current" render target, setting up a new one, issuing a draw call,
  294. /// before restoring the original render target.
  295. ///
  296. /// This is regularly used for "texrender" instances, such as combining the chroma and luma components of a video frame
  297. /// (and uploaded as single- and dual-channel textures respectively) back into an RGB texture. This texture is then
  298. /// used as the "output" of its corresponding source in the "actual" render pass, which will use the original render
  299. /// target again.
  300. ///
  301. /// A `nil` pointer provided for either the render target or zstencil attachment means that the "current" value for
  302. /// either should be removed, leaving the renderer in an "invalid" state at least for the render target (using no
  303. /// zstencil attachment is a valid state however).
  304. ///
  305. /// > Important: Use this variant if you need to also update the "current" color space which might be checked by
  306. /// sources' render function to check whether linear gamma or sRGB's gamma will be used to encode color values.
  307. @_cdecl("device_set_render_target_with_color_space")
  308. public func device_set_render_target_with_color_space(
  309. device: UnsafeRawPointer, tex: UnsafeRawPointer?, zstencil: UnsafeRawPointer?, space: gs_color_space
  310. ) {
  311. let device: MetalDevice = unretained(device)
  312. if let tex {
  313. let metalTexture: MetalTexture = unretained(tex)
  314. device.renderState.renderTarget = metalTexture
  315. device.renderState.isRendertargetChanged = true
  316. } else {
  317. device.renderState.renderTarget = nil
  318. }
  319. if let zstencil {
  320. let zstencilAttachment: MetalTexture = unretained(zstencil)
  321. device.renderState.depthStencilAttachment = zstencilAttachment
  322. device.renderState.isRendertargetChanged = true
  323. } else {
  324. device.renderState.depthStencilAttachment = nil
  325. }
  326. device.renderState.gsColorSpace = space
  327. }
  328. /// Switches the current render state to use sRGB gamma encoding and decoding when reading from textures and writing
  329. /// into render targets
  330. /// - Parameters:
  331. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  332. /// - enable: Boolean to enable or disable the automatic sRGB gamma encoding and decoding
  333. ///
  334. /// OBS Studio's renderer has been retroactively updated to use sRGB color primaries _and_ gamma encoding by
  335. /// preference, but not by default. Any source has to opt-in to the use of automatic sRGB gamma encoding and decoding,
  336. /// while the default is still to use linear gamma.
  337. ///
  338. /// This method is thus used by sources to enable or disable the associated behavior and control the way color values
  339. /// generated by fragment shaders are written into the render target.
  340. @_cdecl("device_enable_framebuffer_srgb")
  341. public func device_enable_framebuffer_srgb(device: UnsafeRawPointer, enable: Bool) {
  342. let device: MetalDevice = unretained(device)
  343. if device.renderState.useSRGBGamma != enable {
  344. device.renderState.useSRGBGamma = enable
  345. device.renderState.isRendertargetChanged = true
  346. }
  347. }
  348. /// Retrieves the current render state's setting for using automatic encoding and decoding of color values using sRGB
  349. /// gamma.
  350. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  351. /// - Returns: Boolean value of the sRGB gamma setting
  352. ///
  353. /// This function is used to check the current state which might have possibly been explicitly changed by calls of
  354. /// ``device_enable_framebuffer_srgb``.
  355. ///
  356. /// A source which might only be able to work with color values that have sRGB gamma already applied to them and thus
  357. /// might want to ensure that the color values provided by the fragment shader will not have the sRGB gamma curve
  358. /// encoded on them again.
  359. ///
  360. /// By calling this function, a source can check if automatic gamma encoding is enabled and then turn it off
  361. /// explicitly, which will ensure that color data is written as-is and no additional encoding will take place.
  362. @_cdecl("device_framebuffer_srgb_enabled")
  363. public func device_framebuffer_srgb_enabled(device: UnsafeRawPointer) -> Bool {
  364. let device: MetalDevice = unretained(device)
  365. return device.renderState.useSRGBGamma
  366. }
  367. /// Signals the beginning of a new scene.
  368. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  369. ///
  370. /// OBS Studio's renderer signals a new scene for each "display" and for every "video mix", which implicitly signals a
  371. /// change of output format. This usually also implies that all current textures that might have been set up for
  372. /// fragment shaders should be reset. For Metal this also requires creating a new "current" command buffer which should
  373. /// contain all GPU commands necessary to render the "scene".
  374. @_cdecl("device_begin_scene")
  375. public func device_begin_scene(device: UnsafeMutableRawPointer) {
  376. let device: MetalDevice = unretained(device)
  377. for index in 0..<GS_MAX_TEXTURES {
  378. device.renderState.textures[Int(index)] = nil
  379. device.renderState.samplers[Int(index)] = nil
  380. }
  381. }
  382. /// Signals the end of a scene.
  383. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  384. ///
  385. /// OBS Studio's renderer signals the end of a scene for each "display" and for every "video mix", which implicitly
  386. /// marks the end of the output at a different format. As the Metal renderer needs a way to detect if all draw commands
  387. /// for a given "display" have ended (and there is no bespoke signal for that in the API), it uses an internal state
  388. /// variable to track if a display had been loaded for the "current" pipeline state and resets it at the "end of scene"
  389. /// signal.
  390. @_cdecl("device_end_scene")
  391. public func device_end_scene(device: UnsafeRawPointer) {
  392. let device: MetalDevice = unretained(device)
  393. if device.renderState.isInDisplaysRenderStage {
  394. device.finishDisplayRenderStage()
  395. device.renderState.isInDisplaysRenderStage = false
  396. }
  397. }
  398. /// Schedules a draw command on the GPU using all "state" variables set up by OBS Studio's renderer up to this point.
  399. /// - Parameters:
  400. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  401. /// - drawMode: Primitive type to draw as specified by `libobs`
  402. /// - startVertex: Start index of vertex to begin drawing with
  403. /// - numVertices: Count of vertices to draw
  404. ///
  405. /// Due to OBS Studio's design this function will usually render only a very low amount of vertices (commonly only 4
  406. /// of them) and very often those vertices are already loaded up as vertex buffers for use by the vertex shader. In
  407. /// those cases `libobs` does not seem to provide a vertex count and implicitly expects the graphics API implementation
  408. /// to deduct the vertex count from the amount of vertices available in its vertex data struct.
  409. ///
  410. /// In other cases a vertex shader will not use any buffers but calculate the vertex positions based on vertex ID and
  411. /// a non-null vertex count has to be provided.
  412. @_cdecl("device_draw")
  413. public func device_draw(device: UnsafeRawPointer, drawMode: gs_draw_mode, startVertex: UInt32, numVertices: UInt32) {
  414. let device: MetalDevice = unretained(device)
  415. guard let primitiveType = drawMode.mtlPrimitive else {
  416. OBSLog(.error, "device_draw: Unsupported draw mode provided: \(drawMode)")
  417. return
  418. }
  419. do {
  420. try device.draw(primitiveType: primitiveType, vertexStart: Int(startVertex), vertexCount: Int(numVertices))
  421. } catch let error as MetalError.MTLDeviceError {
  422. OBSLog(.error, "device_draw: \(error.description)")
  423. } catch {
  424. OBSLog(.error, "device_draw: Unknown error occurred")
  425. }
  426. }
  427. /// Sets up a load action for the "current" frame buffer and depth stencil attachment to simulate the "clear" behavior
  428. /// of other graphics APIs.
  429. /// - Parameters:
  430. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  431. /// - clearFlags: Bit field provided by `libobs` to mark the clear operations to handle
  432. /// - color: The RGBA color to use for clearing the frame buffer
  433. /// - depth: The depth to clear from the depth stencil attachment
  434. /// - stencil: The stencil to clear from the depth stencil attachment
  435. ///
  436. /// In APIs like OpenGL or Direct3D11 render targets have to be explicitly cleared. In OpenGL this is achieved by
  437. /// calling `glClear()` which will schedule a clear operation. Similarly Direct3D11 requires a call to
  438. /// `ClearRenderTargetView` with a specific `ID3D11RenderTargetView` to do the same.
  439. ///
  440. /// Metal does not provide an explicit command to "clear the screen" (as one does not render directly to screens
  441. /// anymore with these APIs). Instead Metal provides "load commands" and "store commands" which describe what should
  442. /// happen to a render target when it is loaded for rendering and unloaded after rendering.
  443. ///
  444. /// Thus a "clear" is a "load command" for a render target or depth stencil attachment that is automatically executed
  445. /// by Metal when it loads or stores them and thus requires Metal to do an explicit (empty) draw call to ensure that
  446. /// the load and store commands are executed even when no other draw calls will follow.
  447. @_cdecl("device_clear")
  448. public func device_clear(
  449. device: UnsafeRawPointer, clearFlags: UInt32, color: UnsafePointer<vec4>, depth: Float, stencil: UInt8
  450. ) {
  451. let device: MetalDevice = unretained(device)
  452. var clearState = ClearState()
  453. if (Int32(clearFlags) & GS_CLEAR_COLOR) == 1 {
  454. clearState.colorAction = .clear
  455. clearState.clearColor = MTLClearColor(
  456. red: Double(color.pointee.x),
  457. green: Double(color.pointee.y),
  458. blue: Double(color.pointee.z),
  459. alpha: Double(color.pointee.w)
  460. )
  461. } else {
  462. clearState.colorAction = .load
  463. }
  464. if (Int32(clearFlags) & GS_CLEAR_DEPTH) == 1 {
  465. clearState.clearDepth = Double(depth)
  466. clearState.depthAction = .clear
  467. } else {
  468. clearState.depthAction = .load
  469. }
  470. if (Int32(clearFlags) & GS_CLEAR_STENCIL) == 1 {
  471. clearState.clearStencil = UInt32(stencil)
  472. clearState.stencilAction = .clear
  473. } else {
  474. clearState.stencilAction = .load
  475. }
  476. do {
  477. try device.clear(state: clearState)
  478. } catch let error as MetalError.MTLDeviceError {
  479. OBSLog(.error, "device_clear: \(error.description)")
  480. } catch {
  481. OBSLog(.error, "device_clear: Unknown error occurred")
  482. }
  483. }
  484. /// Returns whether the current display is ready to preset a frame generated the renderer
  485. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  486. /// - Returns: Boolean value to state whether a frame generated by the renderer could actually be displayed
  487. ///
  488. /// As OBS Studio's renderer is not synced with the operating system's compositor, situations could arise where the
  489. /// renderer needs to be able to "hand off" a generated display output to the compositor but might not be able to
  490. /// because it's not "ready" to receive such a frame. If that is the case, the graphics API can check for such a state
  491. /// and return `false` here, allowing `libobs` to skip rendering the output for the "current" display entirely.
  492. ///
  493. /// In Direct3D11 the `DXGI_SWAP_EFFECT_FLIP_DISCARD` flip effect is used, which allows OBS Studio to render a preview
  494. /// into a buffer without having to care about the compositor. This is not possible in Metal as it's not the
  495. /// application that provides the output buffer, it's the compositor which provides a "drawable" surface. For each
  496. /// display there can only be a maximum of 3 drawables "in flight", a request for any consecutive drawable will stall
  497. /// the renderer.
  498. ///
  499. /// There is currently no way to check for the amount of available drawables, which could be used to return `false`
  500. /// here and would allow `libobs` to skip output rendering on its current frame and try again on the next.
  501. ///
  502. /// > Note: This check applies to the display associated with whichever "swap chain" might be "current" and is thus
  503. /// depends on swap chain state.
  504. @_cdecl("device_is_present_ready")
  505. public func device_is_present_ready(device: UnsafeRawPointer) -> Bool {
  506. return true
  507. }
  508. /// Commits the current command buffer to schedule and execute the GPU commands encoded within it and waits until they
  509. /// have been scheduled.
  510. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  511. ///
  512. /// OBS Studio's renderer will call this function when it has set up all draw commands for a given "display". It is
  513. /// usually accompanied by a call to end the current scene just before and thus marks the end of commands for the
  514. /// current command buffer.
  515. @_cdecl("device_present")
  516. public func device_present(device: UnsafeRawPointer) {
  517. let device: MetalDevice = unretained(device)
  518. device.finishPendingCommands()
  519. }
  520. /// Commits the current command buffer to schedule and execute the GPU commands encoded within it and waits until they
  521. /// have been scheduled.
  522. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  523. ///
  524. /// OBS Studio's renderer will call this function when it is finished setting up all draw commands for the video output
  525. /// texture, and also after it has used the GPU to encode a video output frame.
  526. @_cdecl("device_flush")
  527. public func device_flush(device: UnsafeRawPointer) {
  528. let device: MetalDevice = unretained(device)
  529. device.finishPendingCommands()
  530. }
  531. /// Sets the "current" cull mode to be used by the next draw call
  532. /// - Parameters:
  533. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  534. /// - mode: `libobs` cull mode identifier
  535. ///
  536. /// Converts the cull mode provided by `libobs` into its appropriate ``MTLCullMode`` variant.
  537. @_cdecl("device_set_cull_mode")
  538. public func device_set_cull_mode(device: UnsafeRawPointer, mode: gs_cull_mode) {
  539. let device: MetalDevice = unretained(device)
  540. device.renderState.cullMode = mode.mtlMode
  541. }
  542. /// Gets the "current" cull mode that was set up for the next draw call
  543. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  544. /// - Returns: `libobs` cull mode
  545. ///
  546. /// Converts the ``MTLCullMode`` set up currently into its `libobs` variation
  547. @_cdecl("device_get_cull_mode")
  548. public func device_get_cull_mode(device: UnsafeRawPointer) -> gs_cull_mode {
  549. let device: MetalDevice = unretained(device)
  550. return device.renderState.cullMode.obsMode
  551. }
  552. /// Switches blending of the next draw operation with the contents of the "current" framebuffer.
  553. /// - Parameters:
  554. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  555. /// - enable: `true` if contents should be blended, `false` otherwise
  556. ///
  557. /// This function directly enables or disables blending for the first render target set up in the current pipeline.
  558. @_cdecl("device_enable_blending")
  559. public func device_enable_blending(device: UnsafeRawPointer, enable: Bool) {
  560. let device: MetalDevice = unretained(device)
  561. device.renderState.pipelineDescriptor.colorAttachments[0].isBlendingEnabled = enable
  562. }
  563. /// Switches depth testing on the next draw operation with the contents of the current depth stencil buffer.
  564. /// - Parameters:
  565. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  566. /// - enable: `true` if depth testing should be enabled, `false` otherwise
  567. ///
  568. /// This function directly enables or disables depth texting for the depth stencil attachment set up in the current pipeline
  569. @_cdecl("device_enable_depth_test")
  570. public func device_enable_depth_test(device: UnsafeRawPointer, enable: Bool) {
  571. let device: MetalDevice = unretained(device)
  572. device.renderState.depthStencilDescriptor.isDepthWriteEnabled = enable
  573. }
  574. /// Sets the read mask in the depth stencil descriptor set up in the current pipeline
  575. /// - Parameters:
  576. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  577. /// - enable: `true` if the read mask should be `1`, `false` for a read mask of `0`
  578. ///
  579. /// The `MTLDepthStencilDescriptor` can differentiate between a front facing stencil and a back facing stencil. As
  580. /// `libobs` does not make this distinction, both values will be set to the same value.
  581. @_cdecl("device_enable_stencil_test")
  582. public func device_enable_stencil_test(device: UnsafeRawPointer, enable: Bool) {
  583. let device: MetalDevice = unretained(device)
  584. device.renderState.depthStencilDescriptor.frontFaceStencil.readMask = enable ? 1 : 0
  585. device.renderState.depthStencilDescriptor.backFaceStencil.readMask = enable ? 1 : 0
  586. }
  587. /// Sets the write mask in the depth stencil descriptor set up in the current pipeline
  588. /// - Parameters:
  589. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  590. /// - enable: `true` if the write mask should be `1`, `false` for a write mask of `0`
  591. ///
  592. /// The `MTLDepthStencilDescriptor` can differentiate between a front facing stencil and a back facing stencil. As
  593. /// `libobs` does not make this distinction, both values will be set to the same value.
  594. @_cdecl("device_enable_stencil_write")
  595. public func device_enable_stencil_write(device: UnsafeRawPointer, enable: Bool) {
  596. let device: MetalDevice = unretained(device)
  597. device.renderState.depthStencilDescriptor.frontFaceStencil.writeMask = enable ? 1 : 0
  598. device.renderState.depthStencilDescriptor.backFaceStencil.writeMask = enable ? 1 : 0
  599. }
  600. /// Sets the color write mask for the render target set up in the current pipeline
  601. /// - Parameters:
  602. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  603. /// - red: `true` if the red color channel should be written, `false` otherwise
  604. /// - green: `true` if the green color channel should be written, `false` otherwise
  605. /// - blue: `true` if the blue color channel should be written, `false` otherwise
  606. /// - alpha: `true` if the alpha channel should be written, `false` otherwise
  607. ///
  608. /// The separate `bool` values are converted into an ``MTLColorWriteMask`` which is then set up on the first render
  609. /// target of the current pipeline.
  610. @_cdecl("device_enable_color")
  611. public func device_enable_color(device: UnsafeRawPointer, red: Bool, green: Bool, blue: Bool, alpha: Bool) {
  612. let device: MetalDevice = unretained(device)
  613. var colorMask = MTLColorWriteMask()
  614. if red {
  615. colorMask.insert(.red)
  616. }
  617. if green {
  618. colorMask.insert(.green)
  619. }
  620. if blue {
  621. colorMask.insert(.blue)
  622. }
  623. if alpha {
  624. colorMask.insert(.alpha)
  625. }
  626. device.renderState.pipelineDescriptor.colorAttachments[0].writeMask = colorMask
  627. }
  628. /// Sets the depth compare function for the depth stencil descriptor to be used in the current pipeline
  629. /// - Parameters:
  630. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  631. /// - test: `libobs` enum describing the depth compare function to use
  632. ///
  633. /// The enum value provided by `libobs` is converted into a ``MTLCompareFunction``, which is then set directly as the
  634. /// compare function on the depth stencil descriptor.
  635. @_cdecl("device_depth_function")
  636. public func device_depth_function(device: UnsafeRawPointer, test: gs_depth_test) {
  637. let device: MetalDevice = unretained(device)
  638. device.renderState.depthStencilDescriptor.depthCompareFunction = test.mtlFunction
  639. }
  640. /// Sets the stencil compare functions for the specified stencil side(s) on the depth stencil descriptor in the current
  641. /// pipeline.
  642. /// - Parameters:
  643. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  644. /// - side: The stencil side(s) for which the compare function should be set up
  645. /// - test: `libobs` enum describing the stencil test function to use
  646. ///
  647. /// The enum values provided by `libobs` are first checked for the stencil side, after which the compare function value
  648. /// itself is converted into a ``MTLCompareFunction``, which is then set directly as the compare function on the depth
  649. /// stencil descriptor.
  650. @_cdecl("device_stencil_function")
  651. public func device_stencil_function(device: UnsafeRawPointer, side: gs_stencil_side, test: gs_depth_test) {
  652. let device: MetalDevice = unretained(device)
  653. let stencilCompareFunction: (MTLCompareFunction, MTLCompareFunction)
  654. if side == GS_STENCIL_FRONT {
  655. stencilCompareFunction = (test.mtlFunction, .never)
  656. } else if side == GS_STENCIL_BACK {
  657. stencilCompareFunction = (.never, test.mtlFunction)
  658. } else {
  659. stencilCompareFunction = (test.mtlFunction, test.mtlFunction)
  660. }
  661. device.renderState.depthStencilDescriptor.frontFaceStencil.stencilCompareFunction = stencilCompareFunction.0
  662. device.renderState.depthStencilDescriptor.backFaceStencil.stencilCompareFunction = stencilCompareFunction.1
  663. }
  664. /// Sets the stencil fail, depth fail, and depth pass operations for the specified stencil side(s) on the depth stencil
  665. /// descriptor for the current pipeline.
  666. /// - Parameters:
  667. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  668. /// - side: The stencil side(s) for which the fail and pass functions should be set up
  669. /// - fail: `libobs` enum value describing the stencil fail operation
  670. /// - zfail: `libobs` enum value describing the depth fail operation
  671. /// - zpass: `libobs` enum value describing the depth pass operation
  672. ///
  673. /// The enum values provided by `libobs` are first checked for the stencil side, after which the fail function values
  674. /// themselves are converted into their ``MTLCompareFunction`` variants, which are then set directly on the depth
  675. /// stencil descriptor.
  676. @_cdecl("device_stencil_op")
  677. public func device_stencil_op(
  678. device: UnsafeRawPointer, side: gs_stencil_side, fail: gs_stencil_op_type, zfail: gs_stencil_op_type,
  679. zpass: gs_stencil_op_type
  680. ) {
  681. let device: MetalDevice = unretained(device)
  682. let stencilFailOperation: (MTLStencilOperation, MTLStencilOperation)
  683. let depthFailOperation: (MTLStencilOperation, MTLStencilOperation)
  684. let depthPassOperation: (MTLStencilOperation, MTLStencilOperation)
  685. if side == GS_STENCIL_FRONT {
  686. stencilFailOperation = (fail.mtlOperation, .keep)
  687. depthFailOperation = (zfail.mtlOperation, .keep)
  688. depthPassOperation = (zpass.mtlOperation, .keep)
  689. } else if side == GS_STENCIL_BACK {
  690. stencilFailOperation = (.keep, fail.mtlOperation)
  691. depthFailOperation = (.keep, zfail.mtlOperation)
  692. depthPassOperation = (.keep, zpass.mtlOperation)
  693. } else {
  694. stencilFailOperation = (fail.mtlOperation, fail.mtlOperation)
  695. depthFailOperation = (zfail.mtlOperation, zfail.mtlOperation)
  696. depthPassOperation = (zpass.mtlOperation, zpass.mtlOperation)
  697. }
  698. device.renderState.depthStencilDescriptor.frontFaceStencil.stencilFailureOperation = stencilFailOperation.0
  699. device.renderState.depthStencilDescriptor.frontFaceStencil.depthFailureOperation = depthFailOperation.0
  700. device.renderState.depthStencilDescriptor.frontFaceStencil.depthStencilPassOperation = depthPassOperation.0
  701. device.renderState.depthStencilDescriptor.backFaceStencil.stencilFailureOperation = stencilFailOperation.1
  702. device.renderState.depthStencilDescriptor.backFaceStencil.depthFailureOperation = depthFailOperation.1
  703. device.renderState.depthStencilDescriptor.backFaceStencil.depthStencilPassOperation = depthPassOperation.1
  704. }
  705. /// Sets up the viewport for use in the current pipeline
  706. /// - Parameters:
  707. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  708. /// - x: Origin X coordinate for the viewport
  709. /// - y: Origin Y coordinate for the viewport
  710. /// - width: Width of the viewport
  711. /// - height: Height of the viewport
  712. ///
  713. /// The separate values for origin and dimension are converted into an ``MTLViewport`` which is then retained as the
  714. /// "current" viewport for later use when the pipeline is actually set up.
  715. @_cdecl("device_set_viewport")
  716. public func device_set_viewport(device: UnsafeRawPointer, x: Int32, y: Int32, width: Int32, height: Int32) {
  717. let device: MetalDevice = unretained(device)
  718. let viewPort = MTLViewport(
  719. originX: Double(x),
  720. originY: Double(y),
  721. width: Double(width),
  722. height: Double(height),
  723. znear: 0.0,
  724. zfar: 1.0
  725. )
  726. device.renderState.viewPort = viewPort
  727. }
  728. /// Gets the origin and dimensions of the viewport currently set up for use by the pipeline
  729. /// - Parameters:
  730. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  731. /// - rect: A pointer to a ``gs_rect`` struct in memory
  732. ///
  733. /// The function is provided a pointer to a ``gs_struct`` instance in memory which can hold the x and y values for the
  734. /// origin and dimension of the viewport.
  735. ///
  736. /// This function is usually called when some source needs to retain the current "state" of the pipeline (of which
  737. /// there can ever only be one) and overwrite the state with its own (in this case its own viewport). To be able to
  738. /// restore the prior state, the "current" state needs to be retrieved from the pipeline.
  739. @_cdecl("device_get_viewport")
  740. public func device_get_viewport(device: UnsafeRawPointer, rect: UnsafeMutablePointer<gs_rect>) {
  741. let device: MetalDevice = unretained(device)
  742. rect.pointee.x = Int32(device.renderState.viewPort.originX)
  743. rect.pointee.y = Int32(device.renderState.viewPort.originY)
  744. rect.pointee.cx = Int32(device.renderState.viewPort.width)
  745. rect.pointee.cy = Int32(device.renderState.viewPort.height)
  746. }
  747. /// Sets up a scissor rect to be used by the current pipeline
  748. /// - Parameters:
  749. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  750. /// - rect: Pointer to a ``gs_rect`` struct in memory that contains origin and dimension of the scissor rect
  751. ///
  752. /// The ``gs_rect`` is converted into a ``MTLScissorRect`` object before saving it in the "current" render state
  753. /// for use in the next draw call.
  754. @_cdecl("device_set_scissor_rect")
  755. public func device_set_scissor_rect(device: UnsafeRawPointer, rect: UnsafePointer<gs_rect>?) {
  756. let device: MetalDevice = unretained(device)
  757. if let rect {
  758. device.renderState.scissorRect = rect.pointee.mtlScissorRect
  759. device.renderState.scissorRectEnabled = true
  760. } else {
  761. device.renderState.scissorRect = nil
  762. device.renderState.scissorRectEnabled = false
  763. }
  764. }
  765. /// Sets up an orthographic projection matrix with the provided view frustum
  766. /// - Parameters:
  767. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  768. /// - left: Left edge of view frustum on the near plane
  769. /// - right: Right edge of view frustum on the near plane
  770. /// - top: Top edge of view frustum on the near plane
  771. /// - bottom: Bottom edge of view frustum on the near plane
  772. /// - near: Distance of near plane on the Z axis
  773. /// - far: Distance of far plane on the Z axis
  774. @_cdecl("device_ortho")
  775. public func device_ortho(
  776. device: UnsafeRawPointer, left: Float, right: Float, top: Float, bottom: Float, near: Float, far: Float
  777. ) {
  778. let device: MetalDevice = unretained(device)
  779. let rml = right - left
  780. let bmt = bottom - top
  781. let fmn = far - near
  782. device.renderState.projectionMatrix = matrix_float4x4(
  783. rows: [
  784. SIMD4((2.0 / rml), 0.0, 0.0, 0.0),
  785. SIMD4(0.0, (2.0 / -bmt), 0.0, 0.0),
  786. SIMD4(0.0, 0.0, (1 / fmn), 0.0),
  787. SIMD4((left + right) / -rml, (bottom + top) / bmt, near / -fmn, 1.0),
  788. ]
  789. )
  790. }
  791. /// Sets up a perspective projection matrix with the provided view frustum
  792. /// - Parameters:
  793. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  794. /// - left: Left edge of view frustum on the near plane
  795. /// - right: Right edge of view frustum on the near plane
  796. /// - top: Top edge of view frustum on the near plane
  797. /// - bottom: Bottom edge of view frustum on the near plane
  798. /// - near: Distance of near plane on the Z axis
  799. /// - far: Distance of far plane on the Z axis
  800. @_cdecl("device_frustum")
  801. public func device_frustum(
  802. device: UnsafeRawPointer, left: Float, right: Float, top: Float, bottom: Float, near: Float, far: Float
  803. ) {
  804. let device: MetalDevice = unretained(device)
  805. let rml = right - left
  806. let tmb = top - bottom
  807. let fmn = far - near
  808. device.renderState.projectionMatrix = matrix_float4x4(
  809. columns: (
  810. SIMD4(((2 * near) / rml), 0.0, 0.0, 0.0),
  811. SIMD4(0.0, ((2 * near) / tmb), 0.0, 0.0),
  812. SIMD4(((left + right) / rml), ((top + bottom) / tmb), (-far / fmn), -1.0),
  813. SIMD4(0.0, 0.0, (-(far * near) / fmn), 0.0)
  814. )
  815. )
  816. }
  817. /// Requests the current projection matrix to be pushed into a projection stack
  818. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  819. ///
  820. /// OBS Studio's renderer works with the assumption of one big "current" state stack, which requires the entire state
  821. /// to be changed to meet different rendering requirements. Part of this state is the current projection matrix, which
  822. /// might need to be replaced temporarily. This function will be called when another projection matrix will be set up
  823. /// to allow for its restoration later.
  824. @_cdecl("device_projection_push")
  825. public func device_projection_push(device: UnsafeRawPointer) {
  826. let device: MetalDevice = unretained(device)
  827. device.renderState.projections.append(device.renderState.projectionMatrix)
  828. }
  829. /// Requests the most recently pushed projection matrix to be removed from the stack and set up as the new current
  830. /// matrix
  831. /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  832. ///
  833. /// OBS Studio's renderer works with the assumption of one big "current" state stack. This requires some elements of
  834. /// this state to be temporarily retained before reinstating them after. This function will reinstate the most recently
  835. /// added matrix as the new "current" matrix.
  836. @_cdecl("device_projection_pop")
  837. public func device_projection_pop(device: UnsafeRawPointer) {
  838. let device: MetalDevice = unretained(device)
  839. device.renderState.projectionMatrix = device.renderState.projections.removeLast()
  840. }
  841. /// Checks whether the current display is capable of displaying high dynamic range content.
  842. ///
  843. /// - Parameters:
  844. /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
  845. /// - monitor: Opaque pointer of a platform-dependent monitor identifier
  846. /// - Returns: `true` if the display is capable of displaying high dynamic range content, `false` otherwise
  847. ///
  848. /// On macOS this capability is described by the ``NSScreen/maximumPotentialExtendedDynamicRangeColorComponentValue``
  849. /// property, which can be checked using the ``NSWindow/screen`` property after retrieving the ``NSView/window``
  850. /// property.
  851. @_cdecl("device_is_monitor_hdr")
  852. public func device_is_monitor_hdr(device: UnsafeRawPointer, monitor: UnsafeRawPointer) -> Bool {
  853. let device: MetalDevice = unretained(device)
  854. guard let swapChain = device.renderState.swapChain else {
  855. return false
  856. }
  857. return swapChain.edrHeadroom > 1.0
  858. }