/****************************************************************************** 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 Foundation import Metal enum MetalBufferType { case vertex case index } /// The MetalBuffer class serves as the super class for both vertex and index buffer objects. /// /// It provides convenience functions to pass buffer instances as retained and unretained opaque pointers and provides /// a generic buffer factory method. class MetalBuffer { enum BufferDataType { case vertex case normal case tangent case color case texcoord } private let device: MTLDevice fileprivate let isDynamic: Bool init(device: MetalDevice, isDynamic: Bool) { self.device = device.device self.isDynamic = isDynamic } /// Creates a new buffer with the provided data or updates an existing buffer with the provided data /// - Parameters: /// - buffer: Reference to a buffer variable to either receive the new buffer or provide an existing buffer /// - data: Pointer to raw data of provided type `T` /// - count: Byte size of data to be written into the buffer /// - dynamic: `true` if underlying buffer is dynamically updated for each frame, `false` otherwise. /// /// > Note: Some sources (like the `text-freetype2` source) generate "dynamic" buffers but don't update them at /// every frame and instead treat them as "static" buffers. For this reason `MTLBuffer` objects have to be cached /// and re-used per `MetalBuffer` instance and cannot be dynamically provided from a pool of buffers of a `MTLHeap`. fileprivate func createOrUpdateBuffer( buffer: inout MTLBuffer?, data: UnsafeMutablePointer, count: Int, dynamic: Bool ) { let size = MemoryLayout.size * count let alignedSize = (size + 15) & ~15 if buffer != nil { if dynamic && buffer!.length == alignedSize { buffer!.contents().copyMemory(from: data, byteCount: size) return } } buffer = device.makeBuffer( bytes: data, length: alignedSize, options: [.cpuCacheModeWriteCombined, .storageModeShared]) } /// Gets an opaque pointer for the ``MetalBuffer`` instance and increases its reference count by one /// - Returns: `OpaquePointer` to class instance /// /// > Note: Use this method when the instance is to be shared via an `OpaquePointer` and needs to be retained. Any /// opaque pointer shared this way needs to be converted into a retained reference again to ensure automatic /// deinitialization by the Swift runtime. func getRetained() -> OpaquePointer { let retained = Unmanaged.passRetained(self).toOpaque() return OpaquePointer(retained) } /// Gets an opaque pointer for the ``MetalBuffer`` instance without increasing its reference count /// - Returns: `OpaquePointer` to class instance func getUnretained() -> OpaquePointer { let unretained = Unmanaged.passUnretained(self).toOpaque() return OpaquePointer(unretained) } } final class MetalVertexBuffer: MetalBuffer { public var vertexData: UnsafeMutablePointer? private var points: MTLBuffer? private var normals: MTLBuffer? private var tangents: MTLBuffer? private var vertexColors: MTLBuffer? private var uvCoordinates: [MTLBuffer?] init(device: MetalDevice, data: UnsafeMutablePointer, dynamic: Bool) { self.vertexData = data self.uvCoordinates = Array(repeating: nil, count: data.pointee.num_tex) super.init(device: device, isDynamic: dynamic) if !dynamic { setupBuffers() } } /// Sets up buffer objects for the data provided in the provided `gs_vb_data` structure /// - Parameter data: Pointer to a `gs_vb_data` instance /// /// The provided `gs_vb_data` instance is expected to: /// * Always contain vertex data /// * Optionally contain normals data /// * Optionally contain tangents data /// * Optionally contain color data /// * Optionally contain either 2 or 4 texture coordinates per vertex /// /// > Note: The color data needs to be converted from the packed UInt32 format used by `libobs` into a normalized /// vector of Float32 values as Metal does not support implicit conversion of these types when vertex data is /// provided in a single buffer to a vertex shader. public func setupBuffers(data: UnsafeMutablePointer? = nil) { guard let data = data ?? self.vertexData else { assertionFailure("MetalBuffer: Unable to create MTLBuffers without vertex data") return } let numVertices = data.pointee.num createOrUpdateBuffer(buffer: &points, data: data.pointee.points, count: numVertices, dynamic: isDynamic) #if DEBUG points?.label = "Vertex buffer points data" #endif if let normalsData = data.pointee.normals { createOrUpdateBuffer(buffer: &normals, data: normalsData, count: numVertices, dynamic: isDynamic) #if DEBUG normals?.label = "Vertex buffer normals data" #endif } if let tangentsData = data.pointee.tangents { createOrUpdateBuffer(buffer: &tangents, data: tangentsData, count: numVertices, dynamic: isDynamic) #if DEBUG tangents?.label = "Vertex buffer tangents data" #endif } if let colorsData = data.pointee.colors { var unpackedColors = [SIMD4]() unpackedColors.reserveCapacity(4) for i in 0..(start: $0, count: 4) let color = SIMD4( x: Float(colorValues[0]) / 255.0, y: Float(colorValues[1]) / 255.0, z: Float(colorValues[2]) / 255.0, w: Float(colorValues[3]) / 255.0 ) unpackedColors.append(color) } } unpackedColors.withUnsafeMutableBufferPointer { createOrUpdateBuffer( buffer: &vertexColors, data: $0.baseAddress!, count: numVertices, dynamic: isDynamic) } #if DEBUG vertexColors?.label = "Vertex buffer colors data" #endif } guard data.pointee.num_tex > 0 else { return } let textureVertices = UnsafeMutableBufferPointer( start: data.pointee.tvarray, count: data.pointee.num_tex) for (textureSlot, textureVertex) in textureVertices.enumerated() { textureVertex.array.withMemoryRebound(to: Float32.self, capacity: textureVertex.width * numVertices) { createOrUpdateBuffer( buffer: &uvCoordinates[textureSlot], data: $0, count: textureVertex.width * numVertices, dynamic: isDynamic) } #if DEBUG uvCoordinates[textureSlot]?.label = "Vertex buffer texture uv data (texture slot \(textureSlot))" #endif } } /// Gets a collection of all ` MTLBuffer` objects created for the vertex data contained in the ``MetalBuffer``. /// - Parameter shader: ``MetalShader`` instance for which the buffers will be used /// - Returns: Array for `MTLBuffer`s in the order required by the shader /// /// > Important: To ensure that the data in the buffers is aligned with the structures declared in the shaders, /// each ``MetalShader`` provides a "buffer order". The corresponding collection will contain the associated /// ``MTLBuffer`` objects in this order. public func getShaderBuffers(for shader: MetalShader) -> [MTLBuffer] { var bufferList = [MTLBuffer]() for bufferType in shader.bufferOrder { switch bufferType { case .vertex: if let points { bufferList.append(points) } case .normal: if let normals { bufferList.append(normals) } case .tangent: if let tangents { bufferList.append(tangents) } case .color: if let vertexColors { bufferList.append(vertexColors) } case .texcoord: guard shader.textureCount == uvCoordinates.count else { assertionFailure( "MetalBuffer: Amount of available texture uv coordinates not sufficient for vertex shader") break } for i in 0..