MetalDevice.swift 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786
  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. import Metal
  17. import simd
  18. /// Describes which clear actions to take when an explicit clear is requested
  19. struct ClearState {
  20. var colorAction: MTLLoadAction = .dontCare
  21. var depthAction: MTLLoadAction = .dontCare
  22. var stencilAction: MTLLoadAction = .dontCare
  23. var clearColor: MTLClearColor = MTLClearColor()
  24. var clearDepth: Double = 0.0
  25. var clearStencil: UInt32 = 0
  26. var clearTarget: MetalTexture? = nil
  27. }
  28. /// Object wrapping an `MTLDevice` object and providing convenience functions for interaction with `libobs`
  29. class MetalDevice {
  30. private let identityMatrix = matrix_float4x4.init(diagonal: SIMD4(1.0, 1.0, 1.0, 1.0))
  31. private let fallbackVertexBuffer: MTLBuffer
  32. private var nopVertexFunction: MTLFunction
  33. private var pipelines = [Int: MTLRenderPipelineState]()
  34. private var depthStencilStates = [Int: MTLDepthStencilState]()
  35. private var obsSignalCallbacks = [MetalSignalType: () -> Void]()
  36. private var displayLink: CVDisplayLink?
  37. let device: MTLDevice
  38. let commandQueue: MTLCommandQueue
  39. var renderState: MetalRenderState
  40. var swapChains = [OBSSwapChain]()
  41. let swapChainQueue = DispatchQueue(label: "swapchainUpdateQueue", qos: .userInteractive)
  42. init(device: MTLDevice) throws {
  43. self.device = device
  44. guard let commandQueue = device.makeCommandQueue() else {
  45. throw MetalError.MTLDeviceError.commandQueueCreationFailure
  46. }
  47. guard let buffer = device.makeBuffer(length: 1, options: .storageModePrivate) else {
  48. throw MetalError.MTLDeviceError.bufferCreationFailure("Fallback vertex buffer")
  49. }
  50. let nopVertexSource = "[[vertex]] float4 vsNop() { return (float4)0; }"
  51. let compileOptions = MTLCompileOptions()
  52. if #available(macOS 15, *) {
  53. compileOptions.mathMode = .fast
  54. } else {
  55. compileOptions.fastMathEnabled = true
  56. }
  57. guard let library = try? device.makeLibrary(source: nopVertexSource, options: compileOptions),
  58. let function = library.makeFunction(name: "vsNop")
  59. else {
  60. throw MetalError.MTLDeviceError.shaderCompilationFailure("Vertex NOP shader")
  61. }
  62. CVDisplayLinkCreateWithActiveCGDisplays(&displayLink)
  63. if displayLink == nil {
  64. throw MetalError.MTLDeviceError.displayLinkCreationFailure
  65. }
  66. self.commandQueue = commandQueue
  67. self.nopVertexFunction = function
  68. self.fallbackVertexBuffer = buffer
  69. self.renderState = MetalRenderState(
  70. viewMatrix: identityMatrix,
  71. projectionMatrix: identityMatrix,
  72. viewProjectionMatrix: identityMatrix,
  73. scissorRectEnabled: false,
  74. gsColorSpace: GS_CS_SRGB
  75. )
  76. let clearPipelineDescriptor = renderState.clearPipelineDescriptor
  77. clearPipelineDescriptor.colorAttachments[0].isBlendingEnabled = false
  78. clearPipelineDescriptor.vertexFunction = nopVertexFunction
  79. clearPipelineDescriptor.fragmentFunction = nil
  80. clearPipelineDescriptor.inputPrimitiveTopology = .point
  81. setupSignalHandlers()
  82. setupDisplayLink()
  83. }
  84. func dispatchSignal(type: MetalSignalType) {
  85. if let callback = obsSignalCallbacks[type] {
  86. callback()
  87. }
  88. }
  89. /// Creates signal handlers for specific OBS signals and adds them to a collection of signal handlers using the signal name as their key
  90. private func setupSignalHandlers() {
  91. let videoResetCallback = { [self] in
  92. guard let displayLink else { return }
  93. CVDisplayLinkStop(displayLink)
  94. CVDisplayLinkStart(displayLink)
  95. }
  96. obsSignalCallbacks.updateValue(videoResetCallback, forKey: MetalSignalType.videoReset)
  97. }
  98. /// Sets up the `CVDisplayLink` used by the ``MetalDevice`` to synchronize projector output with the operating
  99. /// system's screen refresh rate.
  100. private func setupDisplayLink() {
  101. func displayLinkCallback(
  102. displayLink: CVDisplayLink,
  103. _ now: UnsafePointer<CVTimeStamp>,
  104. _ outputTime: UnsafePointer<CVTimeStamp>,
  105. _ flagsIn: CVOptionFlags,
  106. _ flagsOut: UnsafeMutablePointer<CVOptionFlags>,
  107. _ displayLinkContext: UnsafeMutableRawPointer?
  108. ) -> CVReturn {
  109. guard let displayLinkContext else { return kCVReturnSuccess }
  110. let metalDevice = unsafeBitCast(displayLinkContext, to: MetalDevice.self)
  111. metalDevice.blitSwapChains()
  112. return kCVReturnSuccess
  113. }
  114. let opaqueSelf = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
  115. CVDisplayLinkSetOutputCallback(displayLink!, displayLinkCallback, opaqueSelf)
  116. }
  117. /// Iterates over all ``OBSSwapChain`` instances present on the ``MetalDevice`` instance and encodes a block
  118. /// transfer command on the GPU to copy the contents of the projector rendered by `libobs`'s render loop into the
  119. /// drawable provided by a `CAMetalLayer`.
  120. func blitSwapChains() {
  121. guard swapChains.count > 0 else { return }
  122. guard let commandBuffer = commandQueue.makeCommandBuffer(),
  123. let encoder = commandBuffer.makeBlitCommandEncoder()
  124. else {
  125. return
  126. }
  127. self.swapChainQueue.sync {
  128. swapChains = swapChains.filter { $0.discard == false }
  129. }
  130. for swapChain in swapChains {
  131. guard let renderTarget = swapChain.renderTarget, let drawable = swapChain.layer.nextDrawable() else {
  132. continue
  133. }
  134. guard renderTarget.texture.width == drawable.texture.width,
  135. renderTarget.texture.height == drawable.texture.height,
  136. renderTarget.texture.pixelFormat == drawable.texture.pixelFormat
  137. else {
  138. continue
  139. }
  140. autoreleasepool {
  141. encoder.waitForFence(swapChain.fence)
  142. encoder.copy(from: renderTarget.texture, to: drawable.texture)
  143. commandBuffer.addScheduledHandler { _ in
  144. drawable.present()
  145. }
  146. }
  147. }
  148. encoder.endEncoding()
  149. commandBuffer.commit()
  150. }
  151. /// Simulates an explicit "clear" command commonly used in OpenGL or Direct3D11 implementations.
  152. /// - Parameter state: A ``ClearState`` object holding the requested clear actions
  153. ///
  154. /// Metal (like Direct3D12 and Vulkan) does not have an explicit clear command anymore. Devices with M- and
  155. /// A-series SOCs have deferred tile-based GPUs which do not load render targets as single large textures, but
  156. /// instead interact with textures via tiles. A load and store command is executed every time this occurs and a
  157. /// clear is achieved via a load command.
  158. ///
  159. /// If no actual rendering occurs however, no load or store commands are executed, and a render target will be
  160. /// "untouched". This would lead to issues in situations like switching to an empty scene, as the lack of any
  161. /// sources would trigger no draw calls.
  162. ///
  163. /// Thus an explicit draw call needs to be scheduled to achieve the same outcome as the explicit "clear" call in
  164. /// legacy APIs. This is achieved using the most lightweight pipeline possible:
  165. /// * A single vertex shader that returns 0 for all points
  166. /// * No fragment shader
  167. /// * Just load and store commands
  168. ///
  169. /// While this is indeed more inefficient than the "native" approach, it is the best way to ensure expected
  170. /// output with `libobs` rendering system.
  171. ///
  172. func clear(state: ClearState) throws {
  173. try ensureCommandBuffer()
  174. let commandBuffer = renderState.commandBuffer!
  175. guard let renderTarget = renderState.renderTarget else {
  176. return
  177. }
  178. let pipelineDescriptor = renderState.clearPipelineDescriptor
  179. if renderState.useSRGBGamma && renderTarget.sRGBtexture != nil {
  180. pipelineDescriptor.colorAttachments[0].pixelFormat = renderTarget.sRGBtexture!.pixelFormat
  181. } else {
  182. pipelineDescriptor.colorAttachments[0].pixelFormat = renderTarget.texture.pixelFormat
  183. }
  184. pipelineDescriptor.colorAttachments[0].isBlendingEnabled = false
  185. if let depthStencilAttachment = renderState.depthStencilAttachment {
  186. pipelineDescriptor.depthAttachmentPixelFormat = depthStencilAttachment.texture.pixelFormat
  187. pipelineDescriptor.stencilAttachmentPixelFormat = depthStencilAttachment.texture.pixelFormat
  188. } else {
  189. pipelineDescriptor.depthAttachmentPixelFormat = .invalid
  190. pipelineDescriptor.stencilAttachmentPixelFormat = .invalid
  191. }
  192. let stateHash = pipelineDescriptor.hashValue
  193. let renderPipelineState: MTLRenderPipelineState
  194. if let pipelineState = pipelines[stateHash] {
  195. renderPipelineState = pipelineState
  196. } else {
  197. do {
  198. let pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor)
  199. pipelines.updateValue(pipelineState, forKey: stateHash)
  200. renderPipelineState = pipelineState
  201. } catch {
  202. throw MetalError.MTLDeviceError.pipelineStateCreationFailure
  203. }
  204. }
  205. let depthStencilDescriptor = MTLDepthStencilDescriptor()
  206. depthStencilDescriptor.isDepthWriteEnabled = false
  207. let depthStateHash = depthStencilDescriptor.hashValue
  208. let depthStencilState: MTLDepthStencilState
  209. if let state = depthStencilStates[depthStateHash] {
  210. depthStencilState = state
  211. } else {
  212. guard let state = device.makeDepthStencilState(descriptor: depthStencilDescriptor) else {
  213. throw MetalError.MTLDeviceError.depthStencilStateCreationFailure
  214. }
  215. depthStencilStates.updateValue(state, forKey: depthStateHash)
  216. depthStencilState = state
  217. }
  218. let renderPassDescriptor = MTLRenderPassDescriptor()
  219. if state.colorAction == .clear {
  220. renderPassDescriptor.colorAttachments[0].loadAction = .clear
  221. renderPassDescriptor.colorAttachments[0].storeAction = .store
  222. renderPassDescriptor.colorAttachments[0].clearColor = state.clearColor
  223. } else {
  224. renderPassDescriptor.colorAttachments[0].loadAction = state.colorAction
  225. }
  226. if state.depthAction == .clear {
  227. renderPassDescriptor.depthAttachment.loadAction = .clear
  228. renderPassDescriptor.depthAttachment.storeAction = .store
  229. renderPassDescriptor.depthAttachment.clearDepth = state.clearDepth
  230. } else {
  231. renderPassDescriptor.depthAttachment.loadAction = state.depthAction
  232. }
  233. if state.stencilAction == .clear {
  234. renderPassDescriptor.stencilAttachment.loadAction = .clear
  235. renderPassDescriptor.stencilAttachment.storeAction = .store
  236. renderPassDescriptor.stencilAttachment.clearStencil = state.clearStencil
  237. } else {
  238. renderPassDescriptor.stencilAttachment.loadAction = state.stencilAction
  239. }
  240. if renderState.useSRGBGamma && renderTarget.sRGBtexture != nil {
  241. renderPassDescriptor.colorAttachments[0].texture = renderTarget.sRGBtexture!
  242. } else {
  243. renderPassDescriptor.colorAttachments[0].texture = renderTarget.texture
  244. }
  245. renderTarget.hasPendingWrites = true
  246. renderState.inFlightRenderTargets.insert(renderTarget)
  247. renderPassDescriptor.colorAttachments[0].level = 0
  248. renderPassDescriptor.colorAttachments[0].slice = 0
  249. renderPassDescriptor.colorAttachments[0].depthPlane = 0
  250. if let zstencilAttachment = renderState.depthStencilAttachment {
  251. renderPassDescriptor.depthAttachment.texture = zstencilAttachment.texture
  252. renderPassDescriptor.stencilAttachment.texture = zstencilAttachment.texture
  253. } else {
  254. renderPassDescriptor.depthAttachment.texture = nil
  255. renderPassDescriptor.stencilAttachment.texture = nil
  256. }
  257. guard let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
  258. throw MetalError.MTLCommandBufferError.encoderCreationFailure
  259. }
  260. encoder.setRenderPipelineState(renderPipelineState)
  261. if renderState.depthStencilAttachment != nil {
  262. encoder.setDepthStencilState(depthStencilState)
  263. }
  264. encoder.setCullMode(.none)
  265. encoder.drawPrimitives(type: .point, vertexStart: 0, vertexCount: 1, instanceCount: 1, baseInstance: 0)
  266. encoder.endEncoding()
  267. }
  268. /// Schedules a draw call on the GPU with the information currently set up in the ``MetalRenderState`.`
  269. /// - Parameters:
  270. /// - primitiveType: Type of primitives to render
  271. /// - vertexStart: Start index for the vertices to be drawn
  272. /// - vertexCount: Amount of vertices to be drawn
  273. ///
  274. /// Modern APIs like Metal have moved away from the "magic state" mental model used by legacy APIs like OpenGL or
  275. /// Direct3D11 which required the APIs to validate the "global state" at every draw call. Instead Metal requires
  276. /// the creation of a pipeline object which is immutable after creation and thus has to run validation once and can
  277. /// then run draw calls directly.
  278. ///
  279. /// Due to the nature of OBS Studio, the pipeline state can change constantly, as blending, filtering, and
  280. /// conversion of data can constantly be changed by users of the program, which means that the combination of blend
  281. /// modes, shaders, and attachments can change constantly.
  282. ///
  283. /// To avoid a costly re-creation of pipelines for every draw call, pipelines are cached after creation and if a
  284. /// draw call uses an established pipeline, it will be reused from cache instead. While this cannot avoid the cost
  285. /// of creating new pipelines during runtime, it mitigates the cost for consecutive draw calls.
  286. func draw(primitiveType: MTLPrimitiveType, vertexStart: Int, vertexCount: Int) throws {
  287. try ensureCommandBuffer()
  288. let commandBuffer = renderState.commandBuffer!
  289. guard let renderTarget = renderState.renderTarget else {
  290. return
  291. }
  292. guard renderState.vertexBuffer != nil || vertexCount > 0 else {
  293. assertionFailure("MetalDevice: Attempted to render without a vertex buffer set")
  294. return
  295. }
  296. guard let vertexShader = renderState.vertexShader else {
  297. assertionFailure("MetalDevice: Attempted to render without vertex shader set")
  298. return
  299. }
  300. guard let fragmentShader = renderState.fragmentShader else {
  301. assertionFailure("MetalDevice: Attempted to render without fragment shader set")
  302. return
  303. }
  304. let renderPipelineDescriptor = renderState.pipelineDescriptor
  305. let renderPassDescriptor = renderState.renderPassDescriptor
  306. if renderState.isRendertargetChanged {
  307. if renderState.useSRGBGamma && renderTarget.sRGBtexture != nil {
  308. renderPipelineDescriptor.colorAttachments[0].pixelFormat = renderTarget.sRGBtexture!.pixelFormat
  309. renderPassDescriptor.colorAttachments[0].texture = renderTarget.sRGBtexture!
  310. } else {
  311. renderPipelineDescriptor.colorAttachments[0].pixelFormat = renderTarget.texture.pixelFormat
  312. renderPassDescriptor.colorAttachments[0].texture = renderTarget.texture
  313. }
  314. renderTarget.hasPendingWrites = true
  315. renderState.inFlightRenderTargets.insert(renderTarget)
  316. if let zstencilAttachment = renderState.depthStencilAttachment {
  317. renderPipelineDescriptor.depthAttachmentPixelFormat = zstencilAttachment.texture.pixelFormat
  318. renderPipelineDescriptor.stencilAttachmentPixelFormat = zstencilAttachment.texture.pixelFormat
  319. renderPassDescriptor.depthAttachment.texture = zstencilAttachment.texture
  320. renderPassDescriptor.stencilAttachment.texture = zstencilAttachment.texture
  321. } else {
  322. renderPipelineDescriptor.depthAttachmentPixelFormat = .invalid
  323. renderPipelineDescriptor.stencilAttachmentPixelFormat = .invalid
  324. renderPassDescriptor.depthAttachment.texture = nil
  325. renderPassDescriptor.stencilAttachment.texture = nil
  326. }
  327. }
  328. renderPassDescriptor.colorAttachments[0].loadAction = .load
  329. renderPassDescriptor.depthAttachment.loadAction = .load
  330. renderPassDescriptor.stencilAttachment.loadAction = .load
  331. let stateHash = renderState.pipelineDescriptor.hashValue
  332. let pipelineState: MTLRenderPipelineState
  333. if let state = pipelines[stateHash] {
  334. pipelineState = state
  335. } else {
  336. do {
  337. let state = try device.makeRenderPipelineState(descriptor: renderPipelineDescriptor)
  338. pipelines.updateValue(state, forKey: stateHash)
  339. pipelineState = state
  340. } catch {
  341. throw MetalError.MTLDeviceError.pipelineStateCreationFailure
  342. }
  343. }
  344. guard let commandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
  345. else {
  346. throw MetalError.MTLCommandBufferError.encoderCreationFailure
  347. }
  348. commandEncoder.setRenderPipelineState(pipelineState)
  349. if let effect: OpaquePointer = gs_get_effect() {
  350. gs_effect_update_params(effect)
  351. }
  352. commandEncoder.setViewport(renderState.viewPort)
  353. commandEncoder.setFrontFacing(.counterClockwise)
  354. commandEncoder.setCullMode(renderState.cullMode)
  355. if let scissorRect = renderState.scissorRect, renderState.scissorRectEnabled {
  356. commandEncoder.setScissorRect(scissorRect)
  357. }
  358. let depthStateHash = renderState.depthStencilDescriptor.hashValue
  359. let depthStencilState: MTLDepthStencilState
  360. if let state = depthStencilStates[depthStateHash] {
  361. depthStencilState = state
  362. } else {
  363. guard let state = device.makeDepthStencilState(descriptor: renderState.depthStencilDescriptor) else {
  364. throw MetalError.MTLDeviceError.depthStencilStateCreationFailure
  365. }
  366. depthStencilStates.updateValue(state, forKey: depthStateHash)
  367. depthStencilState = state
  368. }
  369. commandEncoder.setDepthStencilState(depthStencilState)
  370. var gsViewMatrix: matrix4 = matrix4()
  371. gs_matrix_get(&gsViewMatrix)
  372. let viewMatrix = matrix_float4x4(
  373. rows: [
  374. SIMD4(gsViewMatrix.x.x, gsViewMatrix.x.y, gsViewMatrix.x.z, gsViewMatrix.x.w),
  375. SIMD4(gsViewMatrix.y.x, gsViewMatrix.y.y, gsViewMatrix.y.z, gsViewMatrix.y.w),
  376. SIMD4(gsViewMatrix.z.x, gsViewMatrix.z.y, gsViewMatrix.z.z, gsViewMatrix.z.w),
  377. SIMD4(gsViewMatrix.t.x, gsViewMatrix.t.y, gsViewMatrix.t.z, gsViewMatrix.t.w),
  378. ]
  379. )
  380. renderState.viewProjectionMatrix = (viewMatrix * renderState.projectionMatrix)
  381. if let viewProjectionUniform = vertexShader.viewProjection {
  382. viewProjectionUniform.setParameter(
  383. data: &renderState.viewProjectionMatrix, size: MemoryLayout<matrix_float4x4>.size)
  384. }
  385. vertexShader.uploadShaderParameters(encoder: commandEncoder)
  386. fragmentShader.uploadShaderParameters(encoder: commandEncoder)
  387. if let vertexBuffer = renderState.vertexBuffer {
  388. let buffers = vertexBuffer.getShaderBuffers(for: vertexShader)
  389. commandEncoder.setVertexBuffers(
  390. buffers,
  391. offsets: .init(repeating: 0, count: buffers.count),
  392. range: 0..<buffers.count)
  393. } else {
  394. commandEncoder.setVertexBuffer(fallbackVertexBuffer, offset: 0, index: 0)
  395. }
  396. for (index, texture) in renderState.textures.enumerated() {
  397. if let texture {
  398. commandEncoder.setFragmentTexture(texture, index: index)
  399. }
  400. }
  401. for (index, samplerState) in renderState.samplers.enumerated() {
  402. if let samplerState {
  403. commandEncoder.setFragmentSamplerState(samplerState, index: index)
  404. }
  405. }
  406. if let indexBuffer = renderState.indexBuffer,
  407. let bufferData = indexBuffer.indices
  408. {
  409. commandEncoder.drawIndexedPrimitives(
  410. type: primitiveType,
  411. indexCount: (vertexCount > 0) ? vertexCount : indexBuffer.count,
  412. indexType: indexBuffer.type,
  413. indexBuffer: bufferData,
  414. indexBufferOffset: 0
  415. )
  416. } else {
  417. if let vertexBuffer = renderState.vertexBuffer,
  418. let vertexData = vertexBuffer.vertexData
  419. {
  420. commandEncoder.drawPrimitives(
  421. type: primitiveType,
  422. vertexStart: vertexStart,
  423. vertexCount: vertexData.pointee.num
  424. )
  425. } else {
  426. commandEncoder.drawPrimitives(
  427. type: primitiveType,
  428. vertexStart: vertexStart,
  429. vertexCount: vertexCount
  430. )
  431. }
  432. }
  433. commandEncoder.endEncoding()
  434. }
  435. /// Creates a command buffer on the render state if none exists
  436. func ensureCommandBuffer() throws {
  437. if renderState.commandBuffer == nil {
  438. guard let buffer = commandQueue.makeCommandBuffer() else {
  439. throw MetalError.MTLCommandQueueError.commandBufferCreationFailure
  440. }
  441. renderState.commandBuffer = buffer
  442. }
  443. }
  444. /// Updates a memory fence used on the GPU to signal that the current render target (which is associated with a
  445. /// ``OBSSwapChain`` is available for other GPU commands.
  446. ///
  447. /// This is necessary as the final output of projectors needs to be blitted into the drawables provided by the
  448. /// `CAMetalLayer` of each ``OBSSwapChain`` at the screen refresh interval, but projectors are usually rendered
  449. /// using tens of seperate little draw calls.
  450. ///
  451. /// Thus a virtual "display render stage" state is maintained by the Metal renderer, which is started when a
  452. /// ``OBSSwapChain`` instance is loaded by `libobs` and ended when `device_end_scene` is called.
  453. func finishDisplayRenderStage() {
  454. let buffer = commandQueue.makeCommandBufferWithUnretainedReferences()
  455. let encoder = buffer?.makeBlitCommandEncoder()
  456. guard let buffer, let encoder, let swapChain = renderState.swapChain else {
  457. return
  458. }
  459. encoder.updateFence(swapChain.fence)
  460. encoder.endEncoding()
  461. buffer.commit()
  462. }
  463. /// Ensures that all encoded render commands in the current command buffer are committed to the command queue for
  464. /// execution on the GPU.
  465. ///
  466. /// This is particularly important when textures (or texture data) is to be blitted into other textures or buffers,
  467. /// as pending GPU commands in the existing buffer need to run before any commands that rely on the result of these
  468. /// draw commands to have taken place.
  469. ///
  470. /// Within the same queue this is ensured by Metal itself, but requires the commands to be encoded and committed
  471. /// in the desired order.
  472. func finishPendingCommands() {
  473. guard let commandBuffer = renderState.commandBuffer, commandBuffer.status != .committed else {
  474. return
  475. }
  476. commandBuffer.commit()
  477. renderState.inFlightRenderTargets.forEach {
  478. $0.hasPendingWrites = false
  479. }
  480. renderState.inFlightRenderTargets.removeAll(keepingCapacity: true)
  481. renderState.commandBuffer = nil
  482. }
  483. /// Copies the contents of a texture into another texture of identical dimensions
  484. /// - Parameters:
  485. /// - source: Source texture to copy from
  486. /// - destination: Destination texture to copy to
  487. ///
  488. /// This function requires both textures to have been created with the same dimensions, otherwise the copy
  489. /// operation will fail.
  490. ///
  491. /// If the source texture has pending writes (e.g., it was used as the render target for a clear or draw command),
  492. /// then the current command buffer will be committed to ensure that the blit command encoded by this function
  493. /// happens after the pending commands.
  494. func copyTexture(source: MetalTexture, destination: MetalTexture) throws {
  495. if source.hasPendingWrites {
  496. finishPendingCommands()
  497. }
  498. try ensureCommandBuffer()
  499. let buffer = renderState.commandBuffer!
  500. let encoder = buffer.makeBlitCommandEncoder()
  501. guard let encoder else {
  502. throw MetalError.MTLCommandQueueError.commandBufferCreationFailure
  503. }
  504. encoder.copy(from: source.texture, to: destination.texture)
  505. encoder.endEncoding()
  506. }
  507. /// Copies the contents of a texture into a texture for CPU access
  508. /// - Parameters:
  509. /// - source: Source texture to copy from
  510. /// - destination: Destination texture to copy to
  511. ///
  512. /// This function requires both texture to have been created with the same dimensions, otherwise the copy operation
  513. /// will fail.
  514. ///
  515. /// If the source texture has pending writes (e.g., it was used as the render target for a clear or draw command),
  516. /// then the current command buffer will be comitted to ensure that the blit command encoded by this function
  517. /// happens after the pending commands.
  518. ///
  519. /// > Important: This function differs from ``copyTexture`` insofar as it will wait for the completion of all
  520. /// commands in the command queue to ensure that the GPU has actually completed the blit into the destination
  521. /// texture.
  522. func stageTexture(source: MetalTexture, destination: MetalTexture) throws {
  523. if source.hasPendingWrites {
  524. finishPendingCommands()
  525. }
  526. let buffer = commandQueue.makeCommandBufferWithUnretainedReferences()
  527. let encoder = buffer?.makeBlitCommandEncoder()
  528. guard let buffer, let encoder else {
  529. throw MetalError.MTLCommandQueueError.commandBufferCreationFailure
  530. }
  531. encoder.copy(from: source.texture, to: destination.texture)
  532. encoder.endEncoding()
  533. buffer.commit()
  534. buffer.waitUntilCompleted()
  535. }
  536. /// Copies the contents of a texture into a buffer for CPU access
  537. /// - Parameters:
  538. /// - source: Source texture to copy from
  539. /// - destination: Destination buffer to copy to
  540. ///
  541. /// This function requires that the destination buffer has been created with enough capacity to hold the source
  542. /// textures pixel data.
  543. ///
  544. /// If the source texture has pending writes (e.g., it was used as the render target for a clear or draw command),
  545. /// then the current command buffer will be comitted to ensure that the blit command encoded by this function
  546. /// happens after the pending commands.
  547. ///
  548. /// > Important: This function will wait for the completion of all commands in the command queue to ensure that the
  549. /// GPU has actually completed the blit into the destination buffer.
  550. ///
  551. func stageTextureToBuffer(source: MetalTexture, destination: MetalStageBuffer) throws {
  552. if source.hasPendingWrites {
  553. finishPendingCommands()
  554. }
  555. let buffer = commandQueue.makeCommandBufferWithUnretainedReferences()
  556. let encoder = buffer?.makeBlitCommandEncoder()
  557. guard let buffer, let encoder else {
  558. throw MetalError.MTLCommandQueueError.commandBufferCreationFailure
  559. }
  560. encoder.copy(
  561. from: source.texture,
  562. sourceSlice: 0,
  563. sourceLevel: 0,
  564. sourceOrigin: .init(x: 0, y: 0, z: 0),
  565. sourceSize: .init(width: source.texture.width, height: source.texture.height, depth: 1),
  566. to: destination.buffer,
  567. destinationOffset: 0,
  568. destinationBytesPerRow: destination.width * destination.format.bytesPerPixel!,
  569. destinationBytesPerImage: 0)
  570. encoder.endEncoding()
  571. buffer.commit()
  572. buffer.waitUntilCompleted()
  573. }
  574. /// Copies the contents of a buffer into a texture for GPU access
  575. /// - Parameters:
  576. /// - source: Source buffer to copy from
  577. /// - destination: Destination texture to copy to
  578. ///
  579. /// This function requires that the destination texture has been created with enough capacity to hold the source
  580. /// buffer pixel data.
  581. ///
  582. func stageBufferToTexture(source: MetalStageBuffer, destination: MetalTexture) throws {
  583. let buffer = commandQueue.makeCommandBufferWithUnretainedReferences()
  584. let encoder = buffer?.makeBlitCommandEncoder()
  585. guard let buffer, let encoder else {
  586. throw MetalError.MTLCommandQueueError.commandBufferCreationFailure
  587. }
  588. encoder.copy(
  589. from: source.buffer,
  590. sourceOffset: 0,
  591. sourceBytesPerRow: source.width * source.format.bytesPerPixel!,
  592. sourceBytesPerImage: 0,
  593. sourceSize: .init(width: source.width, height: source.height, depth: 1),
  594. to: destination.texture,
  595. destinationSlice: 0,
  596. destinationLevel: 0,
  597. destinationOrigin: .init(x: 0, y: 0, z: 0)
  598. )
  599. encoder.endEncoding()
  600. buffer.commit()
  601. buffer.waitUntilScheduled()
  602. }
  603. /// Copies a region from a source texture into a region of a destination texture
  604. /// - Parameters:
  605. /// - source: Source texture to copy from
  606. /// - sourceRegion: Region of the source texture to copy from
  607. /// - destination: Destination texture to copy to
  608. /// - destinationRegion: Destination region to copy into
  609. ///
  610. /// This function requires that the destination region fits within the dimensions of the destination texture,
  611. /// otherwise the copy operation will fail.
  612. ///
  613. /// If the source texture has pending writes (e.g., it was used as the render target for a clear or draw command),
  614. /// then the current command buffer will be comitted to ensure that the blit command encoded by this function
  615. /// happens after the pending commands.
  616. ///
  617. func copyTextureRegion(
  618. source: MetalTexture, sourceRegion: MTLRegion, destination: MetalTexture, destinationRegion: MTLRegion
  619. ) throws {
  620. if source.hasPendingWrites {
  621. finishPendingCommands()
  622. }
  623. let buffer = commandQueue.makeCommandBufferWithUnretainedReferences()
  624. let encoder = buffer?.makeBlitCommandEncoder()
  625. guard let buffer, let encoder else {
  626. throw MetalError.MTLCommandQueueError.commandBufferCreationFailure
  627. }
  628. encoder.copy(
  629. from: source.texture,
  630. sourceSlice: 0,
  631. sourceLevel: 0,
  632. sourceOrigin: sourceRegion.origin,
  633. sourceSize: sourceRegion.size,
  634. to: destination.texture,
  635. destinationSlice: 0,
  636. destinationLevel: 0,
  637. destinationOrigin: destinationRegion.origin
  638. )
  639. encoder.endEncoding()
  640. buffer.commit()
  641. }
  642. /// Stops the `CVDisplayLink` used by the ``MetalDevice`` instance
  643. func shutdown() {
  644. guard let displayLink else { return }
  645. CVDisplayLinkStop(displayLink)
  646. self.displayLink = nil
  647. }
  648. deinit {
  649. shutdown()
  650. }
  651. }