1
0

MetalBuffer.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. /******************************************************************************
  2. Copyright (C) 2024 by Patrick Heyer <[email protected]>
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU General Public License as published by
  5. the Free Software Foundation, either version 2 of the License, or
  6. (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU General Public License for more details.
  11. You should have received a copy of the GNU General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. ******************************************************************************/
  14. import Foundation
  15. import Metal
  16. enum MetalBufferType {
  17. case vertex
  18. case index
  19. }
  20. /// The MetalBuffer class serves as the super class for both vertex and index buffer objects.
  21. ///
  22. /// It provides convenience functions to pass buffer instances as retained and unretained opaque pointers and provides
  23. /// a generic buffer factory method.
  24. class MetalBuffer {
  25. enum BufferDataType {
  26. case vertex
  27. case normal
  28. case tangent
  29. case color
  30. case texcoord
  31. }
  32. private let device: MTLDevice
  33. fileprivate let isDynamic: Bool
  34. init(device: MetalDevice, isDynamic: Bool) {
  35. self.device = device.device
  36. self.isDynamic = isDynamic
  37. }
  38. /// Creates a new buffer with the provided data or updates an existing buffer with the provided data
  39. /// - Parameters:
  40. /// - buffer: Reference to a buffer variable to either receive the new buffer or provide an existing buffer
  41. /// - data: Pointer to raw data of provided type `T`
  42. /// - count: Byte size of data to be written into the buffer
  43. /// - dynamic: `true` if underlying buffer is dynamically updated for each frame, `false` otherwise.
  44. ///
  45. /// > Note: Some sources (like the `text-freetype2` source) generate "dynamic" buffers but don't update them at
  46. /// every frame and instead treat them as "static" buffers. For this reason `MTLBuffer` objects have to be cached
  47. /// and re-used per `MetalBuffer` instance and cannot be dynamically provided from a pool of buffers of a `MTLHeap`.
  48. fileprivate func createOrUpdateBuffer<T>(
  49. buffer: inout MTLBuffer?, data: UnsafeMutablePointer<T>, count: Int, dynamic: Bool
  50. ) {
  51. let size = MemoryLayout<T>.size * count
  52. let alignedSize = (size + 15) & ~15
  53. if buffer != nil {
  54. if dynamic && buffer!.length == alignedSize {
  55. buffer!.contents().copyMemory(from: data, byteCount: size)
  56. return
  57. }
  58. }
  59. buffer = device.makeBuffer(
  60. bytes: data, length: alignedSize, options: [.cpuCacheModeWriteCombined, .storageModeShared])
  61. }
  62. /// Gets an opaque pointer for the ``MetalBuffer`` instance and increases its reference count by one
  63. /// - Returns: `OpaquePointer` to class instance
  64. ///
  65. /// > Note: Use this method when the instance is to be shared via an `OpaquePointer` and needs to be retained. Any
  66. /// opaque pointer shared this way needs to be converted into a retained reference again to ensure automatic
  67. /// deinitialization by the Swift runtime.
  68. func getRetained() -> OpaquePointer {
  69. let retained = Unmanaged.passRetained(self).toOpaque()
  70. return OpaquePointer(retained)
  71. }
  72. /// Gets an opaque pointer for the ``MetalBuffer`` instance without increasing its reference count
  73. /// - Returns: `OpaquePointer` to class instance
  74. func getUnretained() -> OpaquePointer {
  75. let unretained = Unmanaged.passUnretained(self).toOpaque()
  76. return OpaquePointer(unretained)
  77. }
  78. }
  79. final class MetalVertexBuffer: MetalBuffer {
  80. public var vertexData: UnsafeMutablePointer<gs_vb_data>?
  81. private var points: MTLBuffer?
  82. private var normals: MTLBuffer?
  83. private var tangents: MTLBuffer?
  84. private var vertexColors: MTLBuffer?
  85. private var uvCoordinates: [MTLBuffer?]
  86. init(device: MetalDevice, data: UnsafeMutablePointer<gs_vb_data>, dynamic: Bool) {
  87. self.vertexData = data
  88. self.uvCoordinates = Array(repeating: nil, count: data.pointee.num_tex)
  89. super.init(device: device, isDynamic: dynamic)
  90. if !dynamic {
  91. setupBuffers()
  92. }
  93. }
  94. /// Sets up buffer objects for the data provided in the provided `gs_vb_data` structure
  95. /// - Parameter data: Pointer to a `gs_vb_data` instance
  96. ///
  97. /// The provided `gs_vb_data` instance is expected to:
  98. /// * Always contain vertex data
  99. /// * Optionally contain normals data
  100. /// * Optionally contain tangents data
  101. /// * Optionally contain color data
  102. /// * Optionally contain either 2 or 4 texture coordinates per vertex
  103. ///
  104. /// > Note: The color data needs to be converted from the packed UInt32 format used by `libobs` into a normalized
  105. /// vector of Float32 values as Metal does not support implicit conversion of these types when vertex data is
  106. /// provided in a single buffer to a vertex shader.
  107. public func setupBuffers(data: UnsafeMutablePointer<gs_vb_data>? = nil) {
  108. guard let data = data ?? self.vertexData else {
  109. assertionFailure("MetalBuffer: Unable to create MTLBuffers without vertex data")
  110. return
  111. }
  112. let numVertices = data.pointee.num
  113. createOrUpdateBuffer(buffer: &points, data: data.pointee.points, count: numVertices, dynamic: isDynamic)
  114. #if DEBUG
  115. points?.label = "Vertex buffer points data"
  116. #endif
  117. if let normalsData = data.pointee.normals {
  118. createOrUpdateBuffer(buffer: &normals, data: normalsData, count: numVertices, dynamic: isDynamic)
  119. #if DEBUG
  120. normals?.label = "Vertex buffer normals data"
  121. #endif
  122. }
  123. if let tangentsData = data.pointee.tangents {
  124. createOrUpdateBuffer(buffer: &tangents, data: tangentsData, count: numVertices, dynamic: isDynamic)
  125. #if DEBUG
  126. tangents?.label = "Vertex buffer tangents data"
  127. #endif
  128. }
  129. if let colorsData = data.pointee.colors {
  130. var unpackedColors = [SIMD4<Float>]()
  131. unpackedColors.reserveCapacity(4)
  132. for i in 0..<numVertices {
  133. let vertexColor = colorsData.advanced(by: i)
  134. vertexColor.withMemoryRebound(to: UInt8.self, capacity: 4) {
  135. let colorValues = UnsafeBufferPointer<UInt8>(start: $0, count: 4)
  136. let color = SIMD4<Float>(
  137. x: Float(colorValues[0]) / 255.0,
  138. y: Float(colorValues[1]) / 255.0,
  139. z: Float(colorValues[2]) / 255.0,
  140. w: Float(colorValues[3]) / 255.0
  141. )
  142. unpackedColors.append(color)
  143. }
  144. }
  145. unpackedColors.withUnsafeMutableBufferPointer {
  146. createOrUpdateBuffer(
  147. buffer: &vertexColors, data: $0.baseAddress!, count: numVertices, dynamic: isDynamic)
  148. }
  149. #if DEBUG
  150. vertexColors?.label = "Vertex buffer colors data"
  151. #endif
  152. }
  153. guard data.pointee.num_tex > 0 else {
  154. return
  155. }
  156. let textureVertices = UnsafeMutableBufferPointer<gs_tvertarray>(
  157. start: data.pointee.tvarray, count: data.pointee.num_tex)
  158. for (textureSlot, textureVertex) in textureVertices.enumerated() {
  159. textureVertex.array.withMemoryRebound(to: Float32.self, capacity: textureVertex.width * numVertices) {
  160. createOrUpdateBuffer(
  161. buffer: &uvCoordinates[textureSlot], data: $0, count: textureVertex.width * numVertices,
  162. dynamic: isDynamic)
  163. }
  164. #if DEBUG
  165. uvCoordinates[textureSlot]?.label = "Vertex buffer texture uv data (texture slot \(textureSlot))"
  166. #endif
  167. }
  168. }
  169. /// Gets a collection of all ` MTLBuffer` objects created for the vertex data contained in the ``MetalBuffer``.
  170. /// - Parameter shader: ``MetalShader`` instance for which the buffers will be used
  171. /// - Returns: Array for `MTLBuffer`s in the order required by the shader
  172. ///
  173. /// > Important: To ensure that the data in the buffers is aligned with the structures declared in the shaders,
  174. /// each ``MetalShader`` provides a "buffer order". The corresponding collection will contain the associated
  175. /// ``MTLBuffer`` objects in this order.
  176. public func getShaderBuffers(for shader: MetalShader) -> [MTLBuffer] {
  177. var bufferList = [MTLBuffer]()
  178. for bufferType in shader.bufferOrder {
  179. switch bufferType {
  180. case .vertex:
  181. if let points {
  182. bufferList.append(points)
  183. }
  184. case .normal:
  185. if let normals { bufferList.append(normals) }
  186. case .tangent:
  187. if let tangents { bufferList.append(tangents) }
  188. case .color:
  189. if let vertexColors { bufferList.append(vertexColors) }
  190. case .texcoord:
  191. guard shader.textureCount == uvCoordinates.count else {
  192. assertionFailure(
  193. "MetalBuffer: Amount of available texture uv coordinates not sufficient for vertex shader")
  194. break
  195. }
  196. for i in 0..<shader.textureCount {
  197. if let uvCoordinate = uvCoordinates[i] {
  198. bufferList.append(uvCoordinate)
  199. }
  200. }
  201. }
  202. }
  203. return bufferList
  204. }
  205. deinit {
  206. gs_vbdata_destroy(vertexData)
  207. }
  208. }
  209. final class MetalIndexBuffer: MetalBuffer {
  210. public var indexData: UnsafeMutableRawPointer?
  211. public var count: Int
  212. public var type: MTLIndexType
  213. var indices: MTLBuffer?
  214. init(device: MetalDevice, type: MTLIndexType, data: UnsafeMutableRawPointer?, count: Int, dynamic: Bool) {
  215. self.indexData = data
  216. self.count = count
  217. self.type = type
  218. super.init(device: device, isDynamic: dynamic)
  219. if !dynamic {
  220. setupBuffers()
  221. }
  222. }
  223. /// Sets up buffer objects for the data provided in the provided memory location
  224. /// - Parameter data: Pointer to bytes representing index buffer data
  225. ///
  226. /// The provided memory location is expected to provide bytes represnting index buffer data as either unsigned
  227. /// 16-bit integers or unsigned 32-bit integers. The size depends on the type used to create the
  228. /// ``MetalIndexBuffer`` instance.
  229. public func setupBuffers(_ data: UnsafeMutableRawPointer? = nil) {
  230. guard let indexData = data ?? indexData else {
  231. assertionFailure("MetalIndexBuffer: Unable to generate MTLBuffer without buffer data")
  232. return
  233. }
  234. let byteSize =
  235. switch type {
  236. case .uint16: 2 * count
  237. case .uint32: 4 * count
  238. @unknown default:
  239. fatalError("MTLIndexType \(type) is not supported")
  240. }
  241. indexData.withMemoryRebound(to: UInt8.self, capacity: byteSize) {
  242. createOrUpdateBuffer(buffer: &indices, data: $0, count: byteSize, dynamic: isDynamic)
  243. }
  244. #if DEBUG
  245. if !isDynamic {
  246. indices?.label = "Index buffer static data"
  247. } else {
  248. indices?.label = "Index buffer dynamic data"
  249. }
  250. #endif
  251. }
  252. deinit {
  253. bfree(indexData)
  254. }
  255. }