mirror of
https://github.com/obsproject/obs-studio.git
synced 2026-03-27 02:42:26 -04:00
785 lines
32 KiB
Swift
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()
|
|
}
|
|
}
|