123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786 |
- /******************************************************************************
- Copyright (C) 2024 by Patrick Heyer <[email protected]>
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 2 of the License, or
- (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
- ******************************************************************************/
- import AppKit
- import Foundation
- 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<CVTimeStamp>,
- _ outputTime: UnsafePointer<CVTimeStamp>,
- _ flagsIn: CVOptionFlags,
- _ flagsOut: UnsafeMutablePointer<CVOptionFlags>,
- _ 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<matrix_float4x4>.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..<buffers.count)
- } else {
- commandEncoder.setVertexBuffer(fallbackVertexBuffer, offset: 0, index: 0)
- }
- for (index, texture) in renderState.textures.enumerated() {
- if let texture {
- commandEncoder.setFragmentTexture(texture, index: index)
- }
- }
- for (index, samplerState) in renderState.samplers.enumerated() {
- if let samplerState {
- commandEncoder.setFragmentSamplerState(samplerState, index: index)
- }
- }
- if let indexBuffer = renderState.indexBuffer,
- let bufferData = indexBuffer.indices
- {
- commandEncoder.drawIndexedPrimitives(
- type: primitiveType,
- indexCount: (vertexCount > 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()
- }
- }
|