Files
obs-studio/libobs-metal/MetalDevice.swift
2026-01-14 13:47:30 -05:00

785 lines
32 KiB
Swift

/******************************************************************************
Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com>
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.present(drawable)
}
}
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 separate 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 committed 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 committed 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 committed 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()
}
}