/****************************************************************************** Copyright (C) 2024 by Patrick Heyer This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ import AppKit import Foundation import Metal import simd /// Describes which clear actions to take when an explicit clear is requested struct ClearState { var colorAction: MTLLoadAction = .dontCare var depthAction: MTLLoadAction = .dontCare var stencilAction: MTLLoadAction = .dontCare var clearColor: MTLClearColor = MTLClearColor() var clearDepth: Double = 0.0 var clearStencil: UInt32 = 0 var clearTarget: MetalTexture? = nil } /// Object wrapping an `MTLDevice` object and providing convenience functions for interaction with `libobs` class MetalDevice { private let identityMatrix = matrix_float4x4.init(diagonal: SIMD4(1.0, 1.0, 1.0, 1.0)) private let fallbackVertexBuffer: MTLBuffer private var nopVertexFunction: MTLFunction private var pipelines = [Int: MTLRenderPipelineState]() private var depthStencilStates = [Int: MTLDepthStencilState]() private var obsSignalCallbacks = [MetalSignalType: () -> Void]() private var displayLink: CVDisplayLink? let device: MTLDevice let commandQueue: MTLCommandQueue var renderState: MetalRenderState var swapChains = [OBSSwapChain]() let swapChainQueue = DispatchQueue(label: "swapchainUpdateQueue", qos: .userInteractive) init(device: MTLDevice) throws { self.device = device guard let commandQueue = device.makeCommandQueue() else { throw MetalError.MTLDeviceError.commandQueueCreationFailure } guard let buffer = device.makeBuffer(length: 1, options: .storageModePrivate) else { throw MetalError.MTLDeviceError.bufferCreationFailure("Fallback vertex buffer") } let nopVertexSource = "[[vertex]] float4 vsNop() { return (float4)0; }" let compileOptions = MTLCompileOptions() if #available(macOS 15, *) { compileOptions.mathMode = .fast } else { compileOptions.fastMathEnabled = true } guard let library = try? device.makeLibrary(source: nopVertexSource, options: compileOptions), let function = library.makeFunction(name: "vsNop") else { throw MetalError.MTLDeviceError.shaderCompilationFailure("Vertex NOP shader") } CVDisplayLinkCreateWithActiveCGDisplays(&displayLink) if displayLink == nil { throw MetalError.MTLDeviceError.displayLinkCreationFailure } self.commandQueue = commandQueue self.nopVertexFunction = function self.fallbackVertexBuffer = buffer self.renderState = MetalRenderState( viewMatrix: identityMatrix, projectionMatrix: identityMatrix, viewProjectionMatrix: identityMatrix, scissorRectEnabled: false, gsColorSpace: GS_CS_SRGB ) let clearPipelineDescriptor = renderState.clearPipelineDescriptor clearPipelineDescriptor.colorAttachments[0].isBlendingEnabled = false clearPipelineDescriptor.vertexFunction = nopVertexFunction clearPipelineDescriptor.fragmentFunction = nil clearPipelineDescriptor.inputPrimitiveTopology = .point setupSignalHandlers() setupDisplayLink() } func dispatchSignal(type: MetalSignalType) { if let callback = obsSignalCallbacks[type] { callback() } } /// Creates signal handlers for specific OBS signals and adds them to a collection of signal handlers using the signal name as their key private func setupSignalHandlers() { let videoResetCallback = { [self] in guard let displayLink else { return } CVDisplayLinkStop(displayLink) CVDisplayLinkStart(displayLink) } obsSignalCallbacks.updateValue(videoResetCallback, forKey: MetalSignalType.videoReset) } /// Sets up the `CVDisplayLink` used by the ``MetalDevice`` to synchronize projector output with the operating /// system's screen refresh rate. private func setupDisplayLink() { func displayLinkCallback( displayLink: CVDisplayLink, _ now: UnsafePointer, _ outputTime: UnsafePointer, _ flagsIn: CVOptionFlags, _ flagsOut: UnsafeMutablePointer, _ displayLinkContext: UnsafeMutableRawPointer? ) -> CVReturn { guard let displayLinkContext else { return kCVReturnSuccess } let metalDevice = unsafeBitCast(displayLinkContext, to: MetalDevice.self) metalDevice.blitSwapChains() return kCVReturnSuccess } let opaqueSelf = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) CVDisplayLinkSetOutputCallback(displayLink!, displayLinkCallback, opaqueSelf) } /// Iterates over all ``OBSSwapChain`` instances present on the ``MetalDevice`` instance and encodes a block /// transfer command on the GPU to copy the contents of the projector rendered by `libobs`'s render loop into the /// drawable provided by a `CAMetalLayer`. func blitSwapChains() { guard swapChains.count > 0 else { return } guard let commandBuffer = commandQueue.makeCommandBuffer(), let encoder = commandBuffer.makeBlitCommandEncoder() else { return } self.swapChainQueue.sync { swapChains = swapChains.filter { $0.discard == false } } for swapChain in swapChains { guard let renderTarget = swapChain.renderTarget, let drawable = swapChain.layer.nextDrawable() else { continue } guard renderTarget.texture.width == drawable.texture.width, renderTarget.texture.height == drawable.texture.height, renderTarget.texture.pixelFormat == drawable.texture.pixelFormat else { continue } autoreleasepool { encoder.waitForFence(swapChain.fence) encoder.copy(from: renderTarget.texture, to: drawable.texture) commandBuffer.addScheduledHandler { _ in drawable.present() } } } encoder.endEncoding() commandBuffer.commit() } /// Simulates an explicit "clear" command commonly used in OpenGL or Direct3D11 implementations. /// - Parameter state: A ``ClearState`` object holding the requested clear actions /// /// Metal (like Direct3D12 and Vulkan) does not have an explicit clear command anymore. Devices with M- and /// A-series SOCs have deferred tile-based GPUs which do not load render targets as single large textures, but /// instead interact with textures via tiles. A load and store command is executed every time this occurs and a /// clear is achieved via a load command. /// /// If no actual rendering occurs however, no load or store commands are executed, and a render target will be /// "untouched". This would lead to issues in situations like switching to an empty scene, as the lack of any /// sources would trigger no draw calls. /// /// Thus an explicit draw call needs to be scheduled to achieve the same outcome as the explicit "clear" call in /// legacy APIs. This is achieved using the most lightweight pipeline possible: /// * A single vertex shader that returns 0 for all points /// * No fragment shader /// * Just load and store commands /// /// While this is indeed more inefficient than the "native" approach, it is the best way to ensure expected /// output with `libobs` rendering system. /// func clear(state: ClearState) throws { try ensureCommandBuffer() let commandBuffer = renderState.commandBuffer! guard let renderTarget = renderState.renderTarget else { return } let pipelineDescriptor = renderState.clearPipelineDescriptor if renderState.useSRGBGamma && renderTarget.sRGBtexture != nil { pipelineDescriptor.colorAttachments[0].pixelFormat = renderTarget.sRGBtexture!.pixelFormat } else { pipelineDescriptor.colorAttachments[0].pixelFormat = renderTarget.texture.pixelFormat } pipelineDescriptor.colorAttachments[0].isBlendingEnabled = false if let depthStencilAttachment = renderState.depthStencilAttachment { pipelineDescriptor.depthAttachmentPixelFormat = depthStencilAttachment.texture.pixelFormat pipelineDescriptor.stencilAttachmentPixelFormat = depthStencilAttachment.texture.pixelFormat } else { pipelineDescriptor.depthAttachmentPixelFormat = .invalid pipelineDescriptor.stencilAttachmentPixelFormat = .invalid } let stateHash = pipelineDescriptor.hashValue let renderPipelineState: MTLRenderPipelineState if let pipelineState = pipelines[stateHash] { renderPipelineState = pipelineState } else { do { let pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor) pipelines.updateValue(pipelineState, forKey: stateHash) renderPipelineState = pipelineState } catch { throw MetalError.MTLDeviceError.pipelineStateCreationFailure } } let depthStencilDescriptor = MTLDepthStencilDescriptor() depthStencilDescriptor.isDepthWriteEnabled = false let depthStateHash = depthStencilDescriptor.hashValue let depthStencilState: MTLDepthStencilState if let state = depthStencilStates[depthStateHash] { depthStencilState = state } else { guard let state = device.makeDepthStencilState(descriptor: depthStencilDescriptor) else { throw MetalError.MTLDeviceError.depthStencilStateCreationFailure } depthStencilStates.updateValue(state, forKey: depthStateHash) depthStencilState = state } let renderPassDescriptor = MTLRenderPassDescriptor() if state.colorAction == .clear { renderPassDescriptor.colorAttachments[0].loadAction = .clear renderPassDescriptor.colorAttachments[0].storeAction = .store renderPassDescriptor.colorAttachments[0].clearColor = state.clearColor } else { renderPassDescriptor.colorAttachments[0].loadAction = state.colorAction } if state.depthAction == .clear { renderPassDescriptor.depthAttachment.loadAction = .clear renderPassDescriptor.depthAttachment.storeAction = .store renderPassDescriptor.depthAttachment.clearDepth = state.clearDepth } else { renderPassDescriptor.depthAttachment.loadAction = state.depthAction } if state.stencilAction == .clear { renderPassDescriptor.stencilAttachment.loadAction = .clear renderPassDescriptor.stencilAttachment.storeAction = .store renderPassDescriptor.stencilAttachment.clearStencil = state.clearStencil } else { renderPassDescriptor.stencilAttachment.loadAction = state.stencilAction } if renderState.useSRGBGamma && renderTarget.sRGBtexture != nil { renderPassDescriptor.colorAttachments[0].texture = renderTarget.sRGBtexture! } else { renderPassDescriptor.colorAttachments[0].texture = renderTarget.texture } renderTarget.hasPendingWrites = true renderState.inFlightRenderTargets.insert(renderTarget) renderPassDescriptor.colorAttachments[0].level = 0 renderPassDescriptor.colorAttachments[0].slice = 0 renderPassDescriptor.colorAttachments[0].depthPlane = 0 if let zstencilAttachment = renderState.depthStencilAttachment { renderPassDescriptor.depthAttachment.texture = zstencilAttachment.texture renderPassDescriptor.stencilAttachment.texture = zstencilAttachment.texture } else { renderPassDescriptor.depthAttachment.texture = nil renderPassDescriptor.stencilAttachment.texture = nil } guard let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { throw MetalError.MTLCommandBufferError.encoderCreationFailure } encoder.setRenderPipelineState(renderPipelineState) if renderState.depthStencilAttachment != nil { encoder.setDepthStencilState(depthStencilState) } encoder.setCullMode(.none) encoder.drawPrimitives(type: .point, vertexStart: 0, vertexCount: 1, instanceCount: 1, baseInstance: 0) encoder.endEncoding() } /// Schedules a draw call on the GPU with the information currently set up in the ``MetalRenderState`.` /// - Parameters: /// - primitiveType: Type of primitives to render /// - vertexStart: Start index for the vertices to be drawn /// - vertexCount: Amount of vertices to be drawn /// /// Modern APIs like Metal have moved away from the "magic state" mental model used by legacy APIs like OpenGL or /// Direct3D11 which required the APIs to validate the "global state" at every draw call. Instead Metal requires /// the creation of a pipeline object which is immutable after creation and thus has to run validation once and can /// then run draw calls directly. /// /// Due to the nature of OBS Studio, the pipeline state can change constantly, as blending, filtering, and /// conversion of data can constantly be changed by users of the program, which means that the combination of blend /// modes, shaders, and attachments can change constantly. /// /// To avoid a costly re-creation of pipelines for every draw call, pipelines are cached after creation and if a /// draw call uses an established pipeline, it will be reused from cache instead. While this cannot avoid the cost /// of creating new pipelines during runtime, it mitigates the cost for consecutive draw calls. func draw(primitiveType: MTLPrimitiveType, vertexStart: Int, vertexCount: Int) throws { try ensureCommandBuffer() let commandBuffer = renderState.commandBuffer! guard let renderTarget = renderState.renderTarget else { return } guard renderState.vertexBuffer != nil || vertexCount > 0 else { assertionFailure("MetalDevice: Attempted to render without a vertex buffer set") return } guard let vertexShader = renderState.vertexShader else { assertionFailure("MetalDevice: Attempted to render without vertex shader set") return } guard let fragmentShader = renderState.fragmentShader else { assertionFailure("MetalDevice: Attempted to render without fragment shader set") return } let renderPipelineDescriptor = renderState.pipelineDescriptor let renderPassDescriptor = renderState.renderPassDescriptor if renderState.isRendertargetChanged { if renderState.useSRGBGamma && renderTarget.sRGBtexture != nil { renderPipelineDescriptor.colorAttachments[0].pixelFormat = renderTarget.sRGBtexture!.pixelFormat renderPassDescriptor.colorAttachments[0].texture = renderTarget.sRGBtexture! } else { renderPipelineDescriptor.colorAttachments[0].pixelFormat = renderTarget.texture.pixelFormat renderPassDescriptor.colorAttachments[0].texture = renderTarget.texture } renderTarget.hasPendingWrites = true renderState.inFlightRenderTargets.insert(renderTarget) if let zstencilAttachment = renderState.depthStencilAttachment { renderPipelineDescriptor.depthAttachmentPixelFormat = zstencilAttachment.texture.pixelFormat renderPipelineDescriptor.stencilAttachmentPixelFormat = zstencilAttachment.texture.pixelFormat renderPassDescriptor.depthAttachment.texture = zstencilAttachment.texture renderPassDescriptor.stencilAttachment.texture = zstencilAttachment.texture } else { renderPipelineDescriptor.depthAttachmentPixelFormat = .invalid renderPipelineDescriptor.stencilAttachmentPixelFormat = .invalid renderPassDescriptor.depthAttachment.texture = nil renderPassDescriptor.stencilAttachment.texture = nil } } renderPassDescriptor.colorAttachments[0].loadAction = .load renderPassDescriptor.depthAttachment.loadAction = .load renderPassDescriptor.stencilAttachment.loadAction = .load let stateHash = renderState.pipelineDescriptor.hashValue let pipelineState: MTLRenderPipelineState if let state = pipelines[stateHash] { pipelineState = state } else { do { let state = try device.makeRenderPipelineState(descriptor: renderPipelineDescriptor) pipelines.updateValue(state, forKey: stateHash) pipelineState = state } catch { throw MetalError.MTLDeviceError.pipelineStateCreationFailure } } guard let commandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { throw MetalError.MTLCommandBufferError.encoderCreationFailure } commandEncoder.setRenderPipelineState(pipelineState) if let effect: OpaquePointer = gs_get_effect() { gs_effect_update_params(effect) } commandEncoder.setViewport(renderState.viewPort) commandEncoder.setFrontFacing(.counterClockwise) commandEncoder.setCullMode(renderState.cullMode) if let scissorRect = renderState.scissorRect, renderState.scissorRectEnabled { commandEncoder.setScissorRect(scissorRect) } let depthStateHash = renderState.depthStencilDescriptor.hashValue let depthStencilState: MTLDepthStencilState if let state = depthStencilStates[depthStateHash] { depthStencilState = state } else { guard let state = device.makeDepthStencilState(descriptor: renderState.depthStencilDescriptor) else { throw MetalError.MTLDeviceError.depthStencilStateCreationFailure } depthStencilStates.updateValue(state, forKey: depthStateHash) depthStencilState = state } commandEncoder.setDepthStencilState(depthStencilState) var gsViewMatrix: matrix4 = matrix4() gs_matrix_get(&gsViewMatrix) let viewMatrix = matrix_float4x4( rows: [ SIMD4(gsViewMatrix.x.x, gsViewMatrix.x.y, gsViewMatrix.x.z, gsViewMatrix.x.w), SIMD4(gsViewMatrix.y.x, gsViewMatrix.y.y, gsViewMatrix.y.z, gsViewMatrix.y.w), SIMD4(gsViewMatrix.z.x, gsViewMatrix.z.y, gsViewMatrix.z.z, gsViewMatrix.z.w), SIMD4(gsViewMatrix.t.x, gsViewMatrix.t.y, gsViewMatrix.t.z, gsViewMatrix.t.w), ] ) renderState.viewProjectionMatrix = (viewMatrix * renderState.projectionMatrix) if let viewProjectionUniform = vertexShader.viewProjection { viewProjectionUniform.setParameter( data: &renderState.viewProjectionMatrix, size: MemoryLayout.size) } vertexShader.uploadShaderParameters(encoder: commandEncoder) fragmentShader.uploadShaderParameters(encoder: commandEncoder) if let vertexBuffer = renderState.vertexBuffer { let buffers = vertexBuffer.getShaderBuffers(for: vertexShader) commandEncoder.setVertexBuffers( buffers, offsets: .init(repeating: 0, count: buffers.count), range: 0.. 0) ? vertexCount : indexBuffer.count, indexType: indexBuffer.type, indexBuffer: bufferData, indexBufferOffset: 0 ) } else { if let vertexBuffer = renderState.vertexBuffer, let vertexData = vertexBuffer.vertexData { commandEncoder.drawPrimitives( type: primitiveType, vertexStart: vertexStart, vertexCount: vertexData.pointee.num ) } else { commandEncoder.drawPrimitives( type: primitiveType, vertexStart: vertexStart, vertexCount: vertexCount ) } } commandEncoder.endEncoding() } /// Creates a command buffer on the render state if none exists func ensureCommandBuffer() throws { if renderState.commandBuffer == nil { guard let buffer = commandQueue.makeCommandBuffer() else { throw MetalError.MTLCommandQueueError.commandBufferCreationFailure } renderState.commandBuffer = buffer } } /// Updates a memory fence used on the GPU to signal that the current render target (which is associated with a /// ``OBSSwapChain`` is available for other GPU commands. /// /// This is necessary as the final output of projectors needs to be blitted into the drawables provided by the /// `CAMetalLayer` of each ``OBSSwapChain`` at the screen refresh interval, but projectors are usually rendered /// using tens of seperate little draw calls. /// /// Thus a virtual "display render stage" state is maintained by the Metal renderer, which is started when a /// ``OBSSwapChain`` instance is loaded by `libobs` and ended when `device_end_scene` is called. func finishDisplayRenderStage() { let buffer = commandQueue.makeCommandBufferWithUnretainedReferences() let encoder = buffer?.makeBlitCommandEncoder() guard let buffer, let encoder, let swapChain = renderState.swapChain else { return } encoder.updateFence(swapChain.fence) encoder.endEncoding() buffer.commit() } /// Ensures that all encoded render commands in the current command buffer are committed to the command queue for /// execution on the GPU. /// /// This is particularly important when textures (or texture data) is to be blitted into other textures or buffers, /// as pending GPU commands in the existing buffer need to run before any commands that rely on the result of these /// draw commands to have taken place. /// /// Within the same queue this is ensured by Metal itself, but requires the commands to be encoded and committed /// in the desired order. func finishPendingCommands() { guard let commandBuffer = renderState.commandBuffer, commandBuffer.status != .committed else { return } commandBuffer.commit() renderState.inFlightRenderTargets.forEach { $0.hasPendingWrites = false } renderState.inFlightRenderTargets.removeAll(keepingCapacity: true) renderState.commandBuffer = nil } /// Copies the contents of a texture into another texture of identical dimensions /// - Parameters: /// - source: Source texture to copy from /// - destination: Destination texture to copy to /// /// This function requires both textures to have been created with the same dimensions, otherwise the copy /// operation will fail. /// /// If the source texture has pending writes (e.g., it was used as the render target for a clear or draw command), /// then the current command buffer will be committed to ensure that the blit command encoded by this function /// happens after the pending commands. func copyTexture(source: MetalTexture, destination: MetalTexture) throws { if source.hasPendingWrites { finishPendingCommands() } try ensureCommandBuffer() let buffer = renderState.commandBuffer! let encoder = buffer.makeBlitCommandEncoder() guard let encoder else { throw MetalError.MTLCommandQueueError.commandBufferCreationFailure } encoder.copy(from: source.texture, to: destination.texture) encoder.endEncoding() } /// Copies the contents of a texture into a texture for CPU access /// - Parameters: /// - source: Source texture to copy from /// - destination: Destination texture to copy to /// /// This function requires both texture to have been created with the same dimensions, otherwise the copy operation /// will fail. /// /// If the source texture has pending writes (e.g., it was used as the render target for a clear or draw command), /// then the current command buffer will be comitted to ensure that the blit command encoded by this function /// happens after the pending commands. /// /// > Important: This function differs from ``copyTexture`` insofar as it will wait for the completion of all /// commands in the command queue to ensure that the GPU has actually completed the blit into the destination /// texture. func stageTexture(source: MetalTexture, destination: MetalTexture) throws { if source.hasPendingWrites { finishPendingCommands() } let buffer = commandQueue.makeCommandBufferWithUnretainedReferences() let encoder = buffer?.makeBlitCommandEncoder() guard let buffer, let encoder else { throw MetalError.MTLCommandQueueError.commandBufferCreationFailure } encoder.copy(from: source.texture, to: destination.texture) encoder.endEncoding() buffer.commit() buffer.waitUntilCompleted() } /// Copies the contents of a texture into a buffer for CPU access /// - Parameters: /// - source: Source texture to copy from /// - destination: Destination buffer to copy to /// /// This function requires that the destination buffer has been created with enough capacity to hold the source /// textures pixel data. /// /// If the source texture has pending writes (e.g., it was used as the render target for a clear or draw command), /// then the current command buffer will be comitted to ensure that the blit command encoded by this function /// happens after the pending commands. /// /// > Important: This function will wait for the completion of all commands in the command queue to ensure that the /// GPU has actually completed the blit into the destination buffer. /// func stageTextureToBuffer(source: MetalTexture, destination: MetalStageBuffer) throws { if source.hasPendingWrites { finishPendingCommands() } let buffer = commandQueue.makeCommandBufferWithUnretainedReferences() let encoder = buffer?.makeBlitCommandEncoder() guard let buffer, let encoder else { throw MetalError.MTLCommandQueueError.commandBufferCreationFailure } encoder.copy( from: source.texture, sourceSlice: 0, sourceLevel: 0, sourceOrigin: .init(x: 0, y: 0, z: 0), sourceSize: .init(width: source.texture.width, height: source.texture.height, depth: 1), to: destination.buffer, destinationOffset: 0, destinationBytesPerRow: destination.width * destination.format.bytesPerPixel!, destinationBytesPerImage: 0) encoder.endEncoding() buffer.commit() buffer.waitUntilCompleted() } /// Copies the contents of a buffer into a texture for GPU access /// - Parameters: /// - source: Source buffer to copy from /// - destination: Destination texture to copy to /// /// This function requires that the destination texture has been created with enough capacity to hold the source /// buffer pixel data. /// func stageBufferToTexture(source: MetalStageBuffer, destination: MetalTexture) throws { let buffer = commandQueue.makeCommandBufferWithUnretainedReferences() let encoder = buffer?.makeBlitCommandEncoder() guard let buffer, let encoder else { throw MetalError.MTLCommandQueueError.commandBufferCreationFailure } encoder.copy( from: source.buffer, sourceOffset: 0, sourceBytesPerRow: source.width * source.format.bytesPerPixel!, sourceBytesPerImage: 0, sourceSize: .init(width: source.width, height: source.height, depth: 1), to: destination.texture, destinationSlice: 0, destinationLevel: 0, destinationOrigin: .init(x: 0, y: 0, z: 0) ) encoder.endEncoding() buffer.commit() buffer.waitUntilScheduled() } /// Copies a region from a source texture into a region of a destination texture /// - Parameters: /// - source: Source texture to copy from /// - sourceRegion: Region of the source texture to copy from /// - destination: Destination texture to copy to /// - destinationRegion: Destination region to copy into /// /// This function requires that the destination region fits within the dimensions of the destination texture, /// otherwise the copy operation will fail. /// /// If the source texture has pending writes (e.g., it was used as the render target for a clear or draw command), /// then the current command buffer will be comitted to ensure that the blit command encoded by this function /// happens after the pending commands. /// func copyTextureRegion( source: MetalTexture, sourceRegion: MTLRegion, destination: MetalTexture, destinationRegion: MTLRegion ) throws { if source.hasPendingWrites { finishPendingCommands() } let buffer = commandQueue.makeCommandBufferWithUnretainedReferences() let encoder = buffer?.makeBlitCommandEncoder() guard let buffer, let encoder else { throw MetalError.MTLCommandQueueError.commandBufferCreationFailure } encoder.copy( from: source.texture, sourceSlice: 0, sourceLevel: 0, sourceOrigin: sourceRegion.origin, sourceSize: sourceRegion.size, to: destination.texture, destinationSlice: 0, destinationLevel: 0, destinationOrigin: destinationRegion.origin ) encoder.endEncoding() buffer.commit() } /// Stops the `CVDisplayLink` used by the ``MetalDevice`` instance func shutdown() { guard let displayLink else { return } CVDisplayLinkStop(displayLink) self.displayLink = nil } deinit { shutdown() } }