OBSSwapChain.swift 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  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 AppKit
  15. import CoreVideo
  16. import Foundation
  17. import Metal
  18. class OBSSwapChain {
  19. enum ColorRange {
  20. case sdr
  21. case hdrPQ
  22. case hdrHLG
  23. }
  24. private weak var device: MetalDevice?
  25. private var view: NSView?
  26. var colorRange: ColorRange
  27. var edrHeadroom: CGFloat = 0.0
  28. let layer: CAMetalLayer
  29. var renderTarget: MetalTexture?
  30. var viewSize: MTLSize
  31. var fence: MTLFence
  32. var discard: Bool = false
  33. init?(device: MetalDevice, size: MTLSize, colorSpace: gs_color_format) {
  34. self.device = device
  35. self.viewSize = size
  36. self.layer = CAMetalLayer()
  37. self.layer.framebufferOnly = false
  38. self.layer.device = device.device
  39. self.layer.drawableSize = CGSize(width: viewSize.width, height: viewSize.height)
  40. self.layer.pixelFormat = .bgra8Unorm_srgb
  41. self.layer.colorspace = CGColorSpace(name: CGColorSpace.sRGB)
  42. self.layer.wantsExtendedDynamicRangeContent = false
  43. self.layer.edrMetadata = nil
  44. self.layer.displaySyncEnabled = false
  45. self.colorRange = .sdr
  46. guard let fence = device.device.makeFence() else { return nil }
  47. self.fence = fence
  48. }
  49. /// Updates the provided view to use the `CAMetalLayer` managed by the ``OBSSwapChain``
  50. /// - Parameter view: `NSView` instance to update
  51. ///
  52. /// > Important: This function has to be called from the main thread
  53. @MainActor
  54. func updateView(_ view: NSView) {
  55. self.view = view
  56. view.layer = self.layer
  57. view.wantsLayer = true
  58. updateEdrHeadroom()
  59. }
  60. /// Updates the EDR headroom value on the ``OBSSwapChain`` with the value from the screen the managed `NSView` is
  61. /// associated with.
  62. ///
  63. /// This is necessary to ensure that the projector uses the appropriate SDR or EDR output depending on the screen
  64. /// the view is on.
  65. @MainActor
  66. func updateEdrHeadroom() {
  67. guard let view = self.view else {
  68. return
  69. }
  70. if let screen = view.window?.screen {
  71. self.edrHeadroom = screen.maximumPotentialExtendedDynamicRangeColorComponentValue
  72. } else {
  73. self.edrHeadroom = CGFloat(1.0)
  74. }
  75. }
  76. /// Resizes the drawable of the managed `CAMetalLayer` to the provided size
  77. /// - Parameter size: Desired new size of the drawable
  78. ///
  79. /// This is usually achieved via a delegate method directly on the associated `NSView` instance, but because the
  80. /// view is managed by Qt, the resize event is routed manually into the ``OBSSwapChain`` instance by `libobs`.
  81. func resize(_ size: MTLSize) {
  82. guard viewSize.width != size.width || viewSize.height != size.height else { return }
  83. viewSize = size
  84. layer.drawableSize = CGSize(
  85. width: viewSize.width,
  86. height: viewSize.height)
  87. renderTarget = nil
  88. }
  89. /// Gets an opaque pointer for the ``OBSSwapChain`` instance and increases its reference count by one
  90. /// - Returns: `OpaquePointer` to class instance
  91. ///
  92. /// > Note: Use this method when the instance is to be shared via an `OpaquePointer` and needs to be retained. Any
  93. /// opaque pointer shared this way needs to be converted into a retained reference again to ensure automatic
  94. /// deinitialization by the Swift runtime.
  95. func getRetained() -> OpaquePointer {
  96. let retained = Unmanaged.passRetained(self).toOpaque()
  97. return OpaquePointer(retained)
  98. }
  99. /// Gets an opaque pointer for the ``OBSSwapChain`` instance without increasing its reference count
  100. /// - Returns: `OpaquePointer` to class instance
  101. func getUnretained() -> OpaquePointer {
  102. let unretained = Unmanaged.passUnretained(self).toOpaque()
  103. return OpaquePointer(unretained)
  104. }
  105. }