MetalTexture.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  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 CoreVideo
  15. import Foundation
  16. import Metal
  17. private let bgraSurfaceFormat = kCVPixelFormatType_32BGRA // 0x42_47_52_41
  18. private let l10rSurfaceFormat = kCVPixelFormatType_ARGB2101010LEPacked // 0x6C_31_30_72
  19. enum MetalTextureMapMode {
  20. case unmapped
  21. case read
  22. case write
  23. }
  24. /// Struct used for data exchange between ``MetalTexture`` and `libobs` API functions during mapping and unmapping of
  25. /// textures.
  26. struct MetalTextureMapping {
  27. let mode: MetalTextureMapMode
  28. let rowSize: Int
  29. let data: UnsafeMutableRawPointer
  30. }
  31. /// Convenience class for managing ``MTLTexture`` objects
  32. class MetalTexture {
  33. private let descriptor: MTLTextureDescriptor
  34. private var mappingMode: MetalTextureMapMode
  35. private let resourceID: UUID
  36. weak var device: MetalDevice?
  37. var data: UnsafeMutableRawPointer?
  38. var hasPendingWrites: Bool = false
  39. var sRGBtexture: MTLTexture?
  40. var texture: MTLTexture
  41. var stageBuffer: MetalStageBuffer?
  42. /// Binds the provided `IOSurfaceRef` to a new `MTLTexture` instance
  43. /// - Parameters:
  44. /// - device: `MTLDevice` instance to use for texture object creation
  45. /// - surface: `IOSurfaceRef` reference to an existing `IOSurface`
  46. /// - Returns: `MTLTexture` instance if texture was created successfully, `nil` otherwise
  47. private static func bindSurface(device: MetalDevice, surface: IOSurfaceRef) -> MTLTexture? {
  48. guard let pixelFormat = MTLPixelFormat.init(osType: IOSurfaceGetPixelFormat(surface)) else {
  49. assertionFailure("MetalDevice: IOSurface pixel format is not supported")
  50. return nil
  51. }
  52. let descriptor = MTLTextureDescriptor.texture2DDescriptor(
  53. pixelFormat: pixelFormat,
  54. width: IOSurfaceGetWidth(surface),
  55. height: IOSurfaceGetHeight(surface),
  56. mipmapped: false
  57. )
  58. descriptor.usage = [.shaderRead]
  59. let texture = device.device.makeTexture(descriptor: descriptor, iosurface: surface, plane: 0)
  60. return texture
  61. }
  62. /// Creates a new ``MetalDevice`` instance with the provided `MTLTextureDescriptor`
  63. /// - Parameters:
  64. /// - device: `MTLDevice` instance to use for texture object creation
  65. /// - descriptor: `MTLTextureDescriptor` to use for texture object creation
  66. init?(device: MetalDevice, descriptor: MTLTextureDescriptor) {
  67. self.device = device
  68. let texture = device.device.makeTexture(descriptor: descriptor)
  69. guard let texture else {
  70. assertionFailure(
  71. "MetalTexture: Failed to create texture with size \(descriptor.width)x\(descriptor.height)")
  72. return nil
  73. }
  74. self.texture = texture
  75. self.resourceID = UUID()
  76. self.mappingMode = .unmapped
  77. self.descriptor = texture.descriptor
  78. updateSRGBView()
  79. }
  80. /// Creates a new ``MetalDevice`` instance with the provided `IOSurfaceRef`
  81. /// - Parameters:
  82. /// - device: `MTLDevice` instance to use for texture object creation
  83. /// - surface: `IOSurfaceRef` to use for texture object creation
  84. init?(device: MetalDevice, surface: IOSurfaceRef) {
  85. self.device = device
  86. let texture = MetalTexture.bindSurface(device: device, surface: surface)
  87. guard let texture else {
  88. assertionFailure("MetalTexture: Failed to create texture with IOSurface")
  89. return nil
  90. }
  91. self.texture = texture
  92. self.resourceID = UUID()
  93. self.mappingMode = .unmapped
  94. self.descriptor = texture.descriptor
  95. updateSRGBView()
  96. }
  97. /// Creates a new ``MetalDevice`` instance with the provided `MTLTexture`
  98. /// - Parameters:
  99. /// - device: `MTLDevice` instance to use for future texture operations
  100. /// - surface: `MTLTexture` to wrap in the ``MetalDevice`` instance
  101. init?(device: MetalDevice, texture: MTLTexture) {
  102. self.device = device
  103. self.texture = texture
  104. self.resourceID = UUID()
  105. self.mappingMode = .unmapped
  106. self.descriptor = texture.descriptor
  107. updateSRGBView()
  108. }
  109. /// Creates a new ``MetalDevice`` instance with a placeholder texture
  110. /// - Parameters:
  111. /// - device: `MTLDevice` instance to use for future texture operations
  112. ///
  113. /// This constructor creates a "placeholder" object that can be shared with `libobs` or updated with an actual
  114. /// `MTLTexture` later.
  115. init?(device: MetalDevice) {
  116. self.device = device
  117. let descriptor = MTLTextureDescriptor.texture2DDescriptor(
  118. pixelFormat: .bgra8Unorm, width: 2, height: 2, mipmapped: false)
  119. guard let texture = device.device.makeTexture(descriptor: descriptor) else {
  120. assertionFailure("MetalTexture: Failed to create placeholder texture object")
  121. return nil
  122. }
  123. self.texture = texture
  124. self.sRGBtexture = nil
  125. self.resourceID = UUID()
  126. self.mappingMode = .unmapped
  127. self.descriptor = texture.descriptor
  128. }
  129. /// Updates the ``MetalTexture`` with a new `IOSurfaceRef`
  130. /// - Parameter surface: Updated `IOSurfaceRef` to a new `IOSurface`
  131. /// - Returns: `true` if update was successful, `false` otherwise
  132. ///
  133. /// "Rebinding" was used with the OpenGL backend, but is not available in Metal. Instead a new `MTLTexture` is
  134. /// created with the provided `IOSurfaceRef` and the ``MetalTexture`` is updated accordingly.
  135. ///
  136. func rebind(surface: IOSurfaceRef) -> Bool {
  137. guard let device = self.device, let texture = MetalTexture.bindSurface(device: device, surface: surface) else {
  138. assertionFailure("MetalTexture: Failed to rebind IOSurface to texture")
  139. return false
  140. }
  141. self.texture = texture
  142. updateSRGBView()
  143. return true
  144. }
  145. /// Creates a `MTLTextureView` for the texture wrapped by the ``MetalTexture`` instance with a corresponding sRGB
  146. /// pixel format, if the texture's pixel format has an appropriate sRGB variant.
  147. func updateSRGBView() {
  148. guard !texture.isFramebufferOnly else {
  149. self.sRGBtexture = nil
  150. return
  151. }
  152. let sRGBFormat: MTLPixelFormat? =
  153. switch texture.pixelFormat {
  154. case .bgra8Unorm: .bgra8Unorm_srgb
  155. case .rgba8Unorm: .rgba8Unorm_srgb
  156. case .r8Unorm: .r8Unorm_srgb
  157. case .rg8Unorm: .rg8Unorm_srgb
  158. case .bgra10_xr: .bgra10_xr_srgb
  159. default: nil
  160. }
  161. if let sRGBFormat {
  162. self.sRGBtexture = texture.makeTextureView(pixelFormat: sRGBFormat)
  163. } else {
  164. self.sRGBtexture = nil
  165. }
  166. }
  167. /// Downloads pixel data from the wrapped `MTLTexture` to the memory location provided by a pointer.
  168. /// - Parameters:
  169. /// - data: Pointer to memory that should receive the texture data
  170. /// - mipmapLevel: Mipmap level of the texture to copy data from
  171. ///
  172. /// > Important: The access of texture data is neither protected nor synchronized. If any draw calls to the texture
  173. /// take place while this function is executed, the downloaded data will reflect this. Use explicit synchronization
  174. /// before initiating a download to prevent this.
  175. func download(data: UnsafeMutableRawPointer, mipmapLevel: Int = 0) {
  176. let mipmapWidth = texture.width >> mipmapLevel
  177. let mipmapHeight = texture.height >> mipmapLevel
  178. let rowSize = mipmapWidth * texture.pixelFormat.bytesPerPixel!
  179. let region = MTLRegionMake2D(0, 0, mipmapWidth, mipmapHeight)
  180. texture.getBytes(data, bytesPerRow: rowSize, from: region, mipmapLevel: mipmapLevel)
  181. }
  182. /// Uploads pixel data into the wrappred `MTLTexture` from the memory location provided by a pointer.
  183. /// - Parameters:
  184. /// - data: Pointer to memory that contains the texture data
  185. /// - mipmapLevels: Mipmap level of the texture to copy data into
  186. ///
  187. /// > Important: The write access of texture data is neither protected nor synchronized. If any draw calls use this
  188. /// texture for reading or writing while this function is executed, the upload might have been incomplete or the
  189. /// data might have been overwritten by the GPU. Use explicit synchronization before initiaitng an upload to
  190. /// prevent this.
  191. func upload(data: UnsafePointer<UnsafePointer<UInt8>?>, mipmapLevels: Int) {
  192. let bytesPerPixel = texture.pixelFormat.bytesPerPixel!
  193. switch texture.textureType {
  194. case .type2D, .typeCube:
  195. let textureCount = if texture.textureType == .typeCube { 6 } else { 1 }
  196. let data = UnsafeBufferPointer(start: data, count: (textureCount * mipmapLevels))
  197. for i in 0..<textureCount {
  198. for mipmapLevel in 0..<mipmapLevels {
  199. let index = mipmapLevels * i + mipmapLevel
  200. guard let data = data[index] else { break }
  201. let mipmapWidth = texture.width >> mipmapLevel
  202. let mipmapHeight = texture.height >> mipmapLevel
  203. let rowSize = mipmapWidth * bytesPerPixel
  204. let region = MTLRegionMake2D(0, 0, mipmapWidth, mipmapHeight)
  205. texture.replace(
  206. region: region, mipmapLevel: mipmapLevel, slice: i, withBytes: data, bytesPerRow: rowSize,
  207. bytesPerImage: 0)
  208. }
  209. }
  210. case .type3D:
  211. let data = UnsafeBufferPointer(start: data, count: mipmapLevels)
  212. for (mipmapLevel, mipmapData) in data.enumerated() {
  213. guard let mipmapData else { break }
  214. let mipmapWidth = texture.width >> mipmapLevel
  215. let mipmapHeight = texture.height >> mipmapLevel
  216. let mipmapDepth = texture.depth >> mipmapLevel
  217. let rowSize = mipmapWidth * bytesPerPixel
  218. let imageSize = rowSize * mipmapHeight
  219. let region = MTLRegionMake3D(0, 0, 0, mipmapWidth, mipmapHeight, mipmapDepth)
  220. texture.replace(
  221. region: region,
  222. mipmapLevel: mipmapLevel,
  223. slice: 0,
  224. withBytes: mipmapData,
  225. bytesPerRow: rowSize,
  226. bytesPerImage: imageSize
  227. )
  228. }
  229. default:
  230. fatalError("MetalTexture: Unsupported texture type \(texture.textureType)")
  231. }
  232. if texture.mipmapLevelCount > 1 {
  233. let device = self.device!
  234. try? device.ensureCommandBuffer()
  235. guard let buffer = device.renderState.commandBuffer,
  236. let encoder = buffer.makeBlitCommandEncoder()
  237. else {
  238. assertionFailure("MetalTexture: Failed to create command buffer for mipmap generation")
  239. return
  240. }
  241. encoder.generateMipmaps(for: texture)
  242. encoder.endEncoding()
  243. }
  244. }
  245. /// Emulates the "map" operation available in Direct3D, providing a pointer for texture uploads or downloads
  246. /// - Parameters:
  247. /// - mode: Map mode to use (writing or reading)
  248. /// - mipmapLevel: Mip map level to map
  249. /// - Returns: A ``MetalTextureMapping`` struct that provides the result of the mapping
  250. ///
  251. /// In Direct3D a "map" operation will do many things at once depending on the current state of its pipelines and
  252. /// the mapping mode used:
  253. /// * When mapped for writing, Direct3D will provide a pointer to CPU memory into which an application can write
  254. /// new texture data.
  255. /// * When mapped for reading, Direct3D will provide a pointer to CPU memory into which it has copied the contents
  256. /// of the texture
  257. ///
  258. /// In either case, the texture will be blocked from access by the GPU until it is unmapped again. In some cases a
  259. /// "map" operation will also implicitly initiate a "flush" operation to ensure that pending GPU commands involving
  260. /// this texture are submitted before it becomes unavailable.
  261. ///
  262. /// Metal does not provide such a convenience method and because `libobs` operates under the assumption that it has
  263. /// to copy its own data into a memory location provided by Direct3D, this has to be emulated explicitly here,
  264. /// albeit without the blocking of access to the texture.
  265. ///
  266. /// This function always needs to be balanced by an appropriate ``unmap`` call.
  267. func map(mode: MetalTextureMapMode, mipmapLevel: Int = 0) -> MetalTextureMapping? {
  268. guard mappingMode == .unmapped else {
  269. assertionFailure("MetalTexture: Attempted to map already-mapped texture.")
  270. return nil
  271. }
  272. let mipmapWidth = texture.width >> mipmapLevel
  273. let mipmapHeight = texture.height >> mipmapLevel
  274. let rowSize = mipmapWidth * texture.pixelFormat.bytesPerPixel!
  275. let dataSize = rowSize * mipmapHeight
  276. // TODO: Evaluate whether a blit to/from a `MTLBuffer` with its `contents` pointer shared is more efficient
  277. let data = UnsafeMutableRawBufferPointer.allocate(byteCount: dataSize, alignment: MemoryLayout<UInt8>.alignment)
  278. guard let baseAddress = data.baseAddress else {
  279. return nil
  280. }
  281. if mode == .read {
  282. download(data: baseAddress, mipmapLevel: mipmapLevel)
  283. }
  284. self.data = baseAddress
  285. self.mappingMode = mode
  286. let mapping = MetalTextureMapping(
  287. mode: mode,
  288. rowSize: rowSize,
  289. data: baseAddress
  290. )
  291. return mapping
  292. }
  293. /// Emulates the "unmap" operation available in Direct3D
  294. /// - Parameter mipmapLevel: The mipmap level that is to be unmapped
  295. ///
  296. /// This function will replace the contents of the "mapped" texture with the data written into the memory provided
  297. /// by the "mapping".
  298. ///
  299. /// As such this function has to always balance the corresponding ``map`` call to ensure that the data written into
  300. /// the provided memory location is written into the texture and the memory itself is deallocated.
  301. func unmap(mipmapLevel: Int = 0) {
  302. guard mappingMode != .unmapped else {
  303. assertionFailure("MetalTexture: Attempted to unmap an unmapped texture")
  304. return
  305. }
  306. let mipmapWidth = texture.width >> mipmapLevel
  307. let mipmapHeight = texture.height >> mipmapLevel
  308. let rowSize = mipmapWidth * texture.pixelFormat.bytesPerPixel!
  309. let region = MTLRegionMake2D(0, 0, mipmapWidth, mipmapHeight)
  310. if let textureData = self.data {
  311. if self.mappingMode == .write {
  312. texture.replace(
  313. region: region,
  314. mipmapLevel: mipmapLevel,
  315. withBytes: textureData,
  316. bytesPerRow: rowSize
  317. )
  318. }
  319. textureData.deallocate()
  320. self.data = nil
  321. }
  322. self.mappingMode = .unmapped
  323. }
  324. /// Gets an opaque pointer for the ``MetalTexture`` instance and increases its reference count by one
  325. /// - Returns: `OpaquePointer` to class instance
  326. ///
  327. /// > Note: Use this method when the instance is to be shared via an `OpaquePointer` and needs to be retained. Any
  328. /// opaque pointer shared this way needs to be converted into a retained reference again to ensure automatic
  329. /// deinitialization by the Swift runtime.
  330. func getRetained() -> OpaquePointer {
  331. let retained = Unmanaged.passRetained(self).toOpaque()
  332. return OpaquePointer(retained)
  333. }
  334. /// Gets an opaque pointer for the ``MetalTexture`` instance without increasing its reference count
  335. /// - Returns: `OpaquePointer` to class instance
  336. func getUnretained() -> OpaquePointer {
  337. let unretained = Unmanaged.passUnretained(self).toOpaque()
  338. return OpaquePointer(unretained)
  339. }
  340. }
  341. /// Extends the ``MetalTexture`` class with comparison operators and a hash function to enable the use inside a `Set`
  342. /// collection
  343. extension MetalTexture: Hashable {
  344. static func == (lhs: MetalTexture, rhs: MetalTexture) -> Bool {
  345. lhs.resourceID == rhs.resourceID
  346. }
  347. static func != (lhs: MetalTexture, rhs: MetalTexture) -> Bool {
  348. lhs.resourceID != rhs.resourceID
  349. }
  350. func hash(into hasher: inout Hasher) {
  351. hasher.combine(resourceID)
  352. }
  353. }