libobs-metal: Add Metal renderer

This commit is contained in:
PatTheMav
2025-06-20 21:43:11 +02:00
committed by Ryan Foster
parent 4ff872c9bb
commit d8b19c3c25
37 changed files with 8447 additions and 0 deletions

View File

@@ -25,6 +25,9 @@ if(OS_WINDOWS)
add_subdirectory(libobs-winrt)
endif()
add_subdirectory(libobs-opengl)
if(OS_MACOS)
add_subdirectory(libobs-metal)
endif()
add_subdirectory(plugins)
add_subdirectory(test/test-input)

View File

@@ -0,0 +1,70 @@
cmake_minimum_required(VERSION 3.28...3.30)
add_library(libobs-metal SHARED)
add_library(OBS::libobs-metal ALIAS libobs-metal)
target_sources(
libobs-metal
PRIVATE
CVPixelFormat+Extensions.swift
MTLCullMode+Extensions.swift
MTLOrigin+Extensions.swift
MTLPixelFormat+Extensions.swift
MTLRegion+Extensions.swift
MTLSize+Extensions.swift
MTLTexture+Extensions.swift
MTLTextureDescriptor+Extensions.swift
MTLTextureType+Extensions.swift
MTLViewport+Extensions.swift
MetalBuffer.swift
MetalDevice.swift
MetalError.swift
MetalRenderState.swift
MetalShader+Extensions.swift
MetalShader.swift
MetalStageBuffer.swift
MetalTexture.swift
OBSShader.swift
OBSSwapChain.swift
Sequence+Hashable.swift
libobs+Extensions.swift
libobs+SignalHandlers.swift
libobs-metal-Bridging-Header.h
metal-indexbuffer.swift
metal-samplerstate.swift
metal-shader.swift
metal-stagesurf.swift
metal-subsystem.swift
metal-swapchain.swift
metal-texture2d.swift
metal-texture3d.swift
metal-unimplemented.swift
metal-vertexbuffer.swift
metal-zstencilbuffer.swift
)
target_link_libraries(libobs-metal PRIVATE OBS::libobs)
target_enable_feature(libobs "Metal renderer")
set_property(SOURCE OBSMetalRenderer.swift APPEND PROPERTY COMPILE_FLAGS -emit-objc-header)
set_target_properties_obs(
libobs-metal
PROPERTIES FOLDER core
VERSION 0
PREFIX ""
)
set_target_xcode_properties(
libobs-metal
PROPERTIES SWIFT_VERSION 6.0
CLANG_ENABLE_OBJC_ARC YES
CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION YES
GCC_WARN_SHADOW YES
CLANG_ENABLE_MODULES YES
CLANG_MODULES_AUTOLINK YES
GCC_STRICT_ALIASING YES
DEFINES_MODULE YES
SWIFT_OBJC_BRIDGING_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/libobs-metal-Bridging-Header.h"
)

View File

@@ -0,0 +1,51 @@
/******************************************************************************
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 CoreVideo
import Metal
extension OSType {
/// Conversion of CoreVideo pixel formats into corresponding Metal pixel formats
var mtlFormat: MTLPixelFormat? {
switch self {
case kCVPixelFormatType_OneComponent8:
return .r8Unorm
case kCVPixelFormatType_OneComponent16Half:
return .r16Float
case kCVPixelFormatType_OneComponent32Float:
return .r32Float
case kCVPixelFormatType_TwoComponent8:
return .rg8Unorm
case kCVPixelFormatType_TwoComponent16Half:
return .rg16Float
case kCVPixelFormatType_TwoComponent32Float:
return .rg32Float
case kCVPixelFormatType_32BGRA:
return .bgra8Unorm
case kCVPixelFormatType_32RGBA:
return .rgba8Unorm
case kCVPixelFormatType_64RGBAHalf:
return .rgba16Float
case kCVPixelFormatType_128RGBAFloat:
return .rgba32Float
case kCVPixelFormatType_ARGB2101010LEPacked:
return .bgr10a2Unorm
default:
return nil
}
}
}

View File

@@ -0,0 +1,33 @@
/******************************************************************************
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 Foundation
import Metal
extension MTLCullMode {
/// Conversion of the cull mode into its corresponding `libobs` type
var obsMode: gs_cull_mode {
switch self {
case .back:
return GS_BACK
case .front:
return GS_FRONT
default:
return GS_NEITHER
}
}
}

View File

@@ -0,0 +1,25 @@
/******************************************************************************
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 Foundation
import Metal
extension MTLOrigin: @retroactive Equatable {
public static func == (lhs: MTLOrigin, rhs: MTLOrigin) -> Bool {
lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z
}
}

View File

@@ -0,0 +1,406 @@
/******************************************************************************
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 CoreGraphics
import CoreVideo
import Foundation
import Metal
extension MTLPixelFormat {
/// Property to check whether the pixel format is an 8-bit format
var is8Bit: Bool {
switch self {
case .a8Unorm, .r8Unorm, .r8Snorm, .r8Uint, .r8Sint:
return true
case .r8Unorm_srgb:
return true
default:
return false
}
}
/// Property to check whether the pixel format is a 16-bit format
var is16Bit: Bool {
switch self {
case .r16Unorm, .r16Snorm, .r16Uint, .r16Sint:
return true
case .rg8Unorm, .rg8Snorm, .rg8Uint, .rg8Sint:
return true
case .rg16Float:
return true
case .rg8Unorm_srgb:
return true
default:
return false
}
}
/// Property to check whether the pixel format is a packed 16-bit format
var isPacked16Bit: Bool {
switch self {
case .b5g6r5Unorm, .a1bgr5Unorm, .abgr4Unorm, .bgr5A1Unorm:
return true
default:
return false
}
}
/// Property to check whether the pixel format is a 32-bit format
var is32Bit: Bool {
switch self {
case .r32Uint, .r32Sint:
return true
case .r32Float:
return true
case .rg16Unorm, .rg16Snorm, .rg16Uint, .rg16Sint:
return true
case .rg16Float:
return true
case .rgba8Unorm, .rgba8Snorm, .rgba8Uint, .rgba8Sint, .bgra8Unorm:
return true
case .rgba8Unorm_srgb, .bgra8Unorm_srgb:
return true
default:
return false
}
}
/// Property to check whether the pixel format is a packed 32-bit format
var isPacked32Bit: Bool {
switch self {
case .rgb10a2Unorm, .rgb10a2Uint, .bgr10a2Unorm:
return true
case .rg11b10Float:
return true
case .rgb9e5Float:
return true
case .bgr10_xr, .bgr10_xr_srgb:
return true
default:
return false
}
}
/// Property to check whether the pixel format is a 64-bit format
var is64Bit: Bool {
switch self {
case .rg32Uint, .rg32Sint:
return true
case .rg32Float:
return true
case .rgba16Unorm, .rgba16Snorm, .rgba16Uint, .rgba16Sint:
return true
case .rgba16Float:
return true
case .bgra10_xr, .bgra10_xr_srgb:
return true
default:
return false
}
}
/// Property to check whether the pixel format is a 128-bit format
var is128Bit: Bool {
switch self {
case .rgba32Uint, .rgba32Sint:
return true
case .rgba32Float:
return true
default:
return false
}
}
/// Property to check whether the pixel format will trigger automatic sRGB gamma encoding and decoding
var isSRGB: Bool {
switch self {
case .r8Unorm_srgb, .rg8Unorm_srgb, .bgra8Unorm_srgb, .rgba8Unorm_srgb:
return true
case .bgr10_xr_srgb, .bgra10_xr_srgb:
return true
case .astc_4x4_srgb, .astc_5x4_srgb, .astc_5x5_srgb, .astc_6x5_srgb, .astc_6x6_srgb, .astc_8x5_srgb,
.astc_8x6_srgb, .astc_8x8_srgb, .astc_10x5_srgb, .astc_10x6_srgb, .astc_10x8_srgb, .astc_10x10_srgb,
.astc_12x10_srgb, .astc_12x12_srgb:
return true
case .bc1_rgba_srgb, .bc2_rgba_srgb, .bc3_rgba_srgb, .bc7_rgbaUnorm_srgb:
return true
case .eac_rgba8_srgb, .etc2_rgb8, .etc2_rgb8a1_srgb:
return true
default:
return false
}
}
/// Property to check whether the pixel format is an extended dynamic range (EDR) format
var isEDR: Bool {
switch self {
case .bgr10_xr, .bgra10_xr, .bgr10_xr_srgb, .bgra10_xr_srgb:
return true
default:
return false
}
}
/// Property to check whether the pixel format uses a form of texture compression
var isCompressed: Bool {
switch self {
// S3TC
case .bc1_rgba, .bc1_rgba_srgb, .bc2_rgba, .bc2_rgba_srgb, .bc3_rgba, .bc3_rgba_srgb:
return true
// RGTC
case .bc4_rUnorm, .bc4_rSnorm, .bc5_rgUnorm, .bc5_rgSnorm:
return true
// BPTC
case .bc6H_rgbFloat, .bc6H_rgbuFloat, .bc7_rgbaUnorm, .bc7_rgbaUnorm_srgb:
return true
// EAC
case .eac_r11Unorm, .eac_r11Snorm, .eac_rg11Unorm, .eac_rg11Snorm, .eac_rgba8, .eac_rgba8_srgb:
return true
// ETC
case .etc2_rgb8, .etc2_rgb8_srgb, .etc2_rgb8a1, .etc2_rgb8a1_srgb:
return true
// ASTC
case .astc_4x4_srgb, .astc_5x4_srgb, .astc_5x5_srgb, .astc_6x5_srgb, .astc_6x6_srgb, .astc_8x5_srgb,
.astc_8x6_srgb, .astc_8x8_srgb, .astc_10x5_srgb, .astc_10x6_srgb, .astc_10x8_srgb, .astc_10x10_srgb,
.astc_12x10_srgb, .astc_12x12_srgb, .astc_4x4_ldr, .astc_5x4_ldr, .astc_5x5_ldr, .astc_6x5_ldr,
.astc_6x6_ldr, .astc_8x5_ldr, .astc_8x6_ldr, .astc_8x8_ldr, .astc_10x5_ldr, .astc_10x6_ldr, .astc_10x8_ldr,
.astc_10x10_ldr, .astc_12x10_ldr, .astc_12x12_ldr:
return true
// ASTC HDR
case .astc_4x4_hdr, .astc_5x4_hdr, .astc_5x5_hdr, .astc_6x5_hdr, .astc_6x6_hdr, .astc_8x5_hdr, .astc_8x6_hdr,
.astc_8x8_hdr, .astc_10x5_hdr, .astc_10x6_hdr, .astc_10x8_hdr, .astc_10x10_hdr, .astc_12x10_hdr,
.astc_12x12_hdr:
return true
default:
return false
}
}
/// Property to check whether the pixel format is a depth buffer format
var isDepth: Bool {
switch self {
case .depth16Unorm, .depth32Float:
return true
default:
return false
}
}
/// Property to check whether the pixel format is depth stencil format
var isStencil: Bool {
switch self {
case .stencil8, .x24_stencil8, .x32_stencil8, .depth24Unorm_stencil8, .depth32Float_stencil8:
return true
default:
return false
}
}
/// Returns number of color components used by the pixel format
var componentCount: Int? {
switch self {
case .a8Unorm, .r8Unorm, .r8Snorm, .r8Uint, .r8Sint, .r8Unorm_srgb:
return 1
case .r16Unorm, .r16Snorm, .r16Uint, .r16Sint, .r16Float:
return 1
case .r32Uint, .r32Sint, .r32Float:
return 1
case .rg8Unorm, .rg8Snorm, .rg8Uint, .rg8Sint, .rg8Unorm_srgb:
return 2
case .rg16Unorm, .rg16Snorm, .rg16Uint, .rg16Sint:
return 2
case .rg32Uint, .rg32Sint, .rg32Float:
return 2
case .b5g6r5Unorm, .rg11b10Float, .rgb9e5Float, .gbgr422, .bgrg422:
return 3
case .a1bgr5Unorm, .abgr4Unorm, .bgr5A1Unorm:
return 4
case .rgba8Unorm, .rgba8Snorm, .rgba8Uint, .rgba8Sint, .rgba8Unorm_srgb, .bgra8Unorm, .bgra8Unorm_srgb:
return 4
case .rgb10a2Unorm, .rgb10a2Uint, .bgr10a2Unorm, .bgr10_xr, .bgr10_xr_srgb:
return 4
case .rgba16Unorm, .rgba16Snorm, .rgba16Uint, .rgba16Sint, .rgba16Float:
return 4
case .rgba32Uint, .rgba32Sint, .rgba32Float:
return 4
case .bc4_rUnorm, .bc4_rSnorm, .eac_r11Unorm, .eac_r11Snorm:
return 1
case .bc5_rgUnorm, .bc5_rgSnorm:
return 2
case .bc6H_rgbFloat, .bc6H_rgbuFloat, .eac_rg11Unorm, .eac_rg11Snorm, .etc2_rgb8, .etc2_rgb8_srgb:
return 3
case .bc1_rgba, .bc1_rgba_srgb, .bc2_rgba, .bc2_rgba_srgb, .bc3_rgba, .bc3_rgba_srgb, .etc2_rgb8a1,
.etc2_rgb8a1_srgb, .eac_rgba8, .eac_rgba8_srgb, .bc7_rgbaUnorm, .bc7_rgbaUnorm_srgb:
return 4
default:
return nil
}
}
/// Conversion of pixel format to `libobs` color format
var gsColorFormat: gs_color_format {
switch self {
case .a8Unorm:
return GS_A8
case .r8Unorm:
return GS_R8
case .rgba8Unorm:
return GS_RGBA
case .bgra8Unorm:
return GS_BGRA
case .rgb10a2Unorm:
return GS_R10G10B10A2
case .rgba16Unorm:
return GS_RGBA16
case .r16Unorm:
return GS_R16
case .rgba16Float:
return GS_RGBA16F
case .rgba32Float:
return GS_RGBA32F
case .rg16Float:
return GS_RG16F
case .rg32Float:
return GS_RG32F
case .r16Float:
return GS_R16F
case .r32Float:
return GS_R32F
case .bc1_rgba:
return GS_DXT1
case .bc2_rgba:
return GS_DXT3
case .bc3_rgba:
return GS_DXT5
default:
return GS_UNKNOWN
}
}
/// Returns the bits per pixel based on the pixel format
var bitsPerPixel: Int? {
if self.is8Bit {
return 8
} else if self.is16Bit || self.isPacked16Bit {
return 16
} else if self.is32Bit || self.isPacked32Bit {
return 32
} else if self.is64Bit {
return 64
} else if self.is128Bit {
return 128
} else {
return nil
}
}
/// Returns the bytes per pixel based on the pixel format
var bytesPerPixel: Int? {
if self.is8Bit {
return 1
} else if self.is16Bit || self.isPacked16Bit {
return 2
} else if self.is32Bit {
return 4
} else if self.isPacked32Bit {
switch self {
case .rgb10a2Unorm, .rgb10a2Uint, .bgr10a2Unorm, .rg11b10Float, .rgb9e5Float:
return 4
case .bgr10_xr, .bgr10_xr_srgb:
return 8
default:
return nil
}
} else if self.is64Bit {
return 8
} else {
return nil
}
}
/// Returns the bytes used per color component of the pixel format
var bitsPerComponent: Int? {
if !self.isCompressed {
if let bitsPerPixel = self.bitsPerPixel, let componentCount = self.componentCount {
return bitsPerPixel / componentCount
}
}
return nil
}
}
extension MTLPixelFormat {
/// Converts the pixel format into a compatible CoreGraphics color space
var colorSpace: CGColorSpace? {
switch self {
case .a8Unorm, .r8Unorm, .r8Snorm, .r8Uint, .r8Sint, .r16Unorm, .r16Snorm, .r16Uint, .r16Sint,
.r16Float, .r32Uint, .r32Sint, .r32Float:
return CGColorSpace(name: CGColorSpace.linearGray)
case .rg8Unorm, .rg8Snorm, .rg8Uint, .rg8Sint, .rgba8Unorm, .rgba8Snorm, .rgba8Uint, .rgba8Sint, .bgra8Unorm,
.rgba16Unorm, .rgba16Snorm, .rgba16Uint, .rgba16Sint:
return CGColorSpace(name: CGColorSpace.linearSRGB)
case .rg8Unorm_srgb, .rgba8Unorm_srgb, .bgra8Unorm_srgb:
return CGColorSpace(name: CGColorSpace.sRGB)
case .rg16Float, .rg32Float, .rgba16Float, .rgba32Float, .bgr10_xr, .bgr10a2Unorm:
return CGColorSpace(name: CGColorSpace.extendedLinearSRGB)
case .bgr10_xr_srgb:
return CGColorSpace(name: CGColorSpace.extendedSRGB)
default:
return nil
}
}
}
extension MTLPixelFormat {
/// Initializes a ``MTLPixelFormat`` with a compatible CoreVideo video pixel format
init?(osType: OSType) {
guard let pixelFormat = osType.mtlFormat else {
return nil
}
self = pixelFormat
}
/// Conversion of the pixel format into a compatible CoreVideo video pixel format
var videoPixelFormat: OSType? {
switch self {
case .r8Unorm, .r8Unorm_srgb:
return kCVPixelFormatType_OneComponent8
case .r16Float:
return kCVPixelFormatType_OneComponent16Half
case .r32Float:
return kCVPixelFormatType_OneComponent32Float
case .rg8Unorm, .rg8Unorm_srgb:
return kCVPixelFormatType_TwoComponent8
case .rg16Float:
return kCVPixelFormatType_TwoComponent16Half
case .rg32Float:
return kCVPixelFormatType_TwoComponent32Float
case .bgra8Unorm, .bgra8Unorm_srgb:
return kCVPixelFormatType_32BGRA
case .rgba8Unorm, .rgba8Unorm_srgb:
return kCVPixelFormatType_32RGBA
case .rgba16Float:
return kCVPixelFormatType_64RGBAHalf
case .rgba32Float:
return kCVPixelFormatType_128RGBAFloat
default:
return nil
}
}
}

View File

@@ -0,0 +1,25 @@
/******************************************************************************
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 Foundation
import Metal
extension MTLRegion: @retroactive Equatable {
public static func == (lhs: MTLRegion, rhs: MTLRegion) -> Bool {
lhs.origin == rhs.origin && lhs.size == rhs.size
}
}

View File

@@ -0,0 +1,25 @@
/******************************************************************************
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 Foundation
import Metal
extension MTLSize: @retroactive Equatable {
public static func == (lhs: MTLSize, rhs: MTLSize) -> Bool {
lhs.width == rhs.width && lhs.height == rhs.height && lhs.depth == rhs.depth
}
}

View File

@@ -0,0 +1,76 @@
/******************************************************************************
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 Foundation
import Metal
extension MTLTexture {
/// Creates an opaque pointer of a ``MTLTexture`` instance and increases the reference count.
/// - Returns: Opaque pointer for the ``MTLTexture``
func getRetained() -> OpaquePointer {
let retained = Unmanaged.passRetained(self).toOpaque()
return OpaquePointer(retained)
}
/// Creates an opaque pointer of a ``MTLTexture`` instance without increasing the reference count.
/// - Returns: Opaque pointer for the ``MTLTexture``
func getUnretained() -> OpaquePointer {
let unretained = Unmanaged.passUnretained(self).toOpaque()
return OpaquePointer(unretained)
}
}
extension MTLTexture {
/// Convenience property to get the texture's size as a ``MTLSize`` object
var size: MTLSize {
.init(
width: self.width,
height: self.height,
depth: self.depth
)
}
/// Convenience property to get the texture's region as a ``MTLRegion`` object
var region: MTLRegion {
.init(
origin: .init(x: 0, y: 0, z: 0),
size: self.size
)
}
/// Gets a new ``MTLTextureDescriptor`` instance with the properties of the texture
var descriptor: MTLTextureDescriptor {
let descriptor = MTLTextureDescriptor()
descriptor.textureType = self.textureType
descriptor.pixelFormat = self.pixelFormat
descriptor.width = self.width
descriptor.height = self.height
descriptor.depth = self.depth
descriptor.mipmapLevelCount = self.mipmapLevelCount
descriptor.sampleCount = self.sampleCount
descriptor.arrayLength = self.arrayLength
descriptor.storageMode = self.storageMode
descriptor.cpuCacheMode = self.cpuCacheMode
descriptor.usage = self.usage
descriptor.allowGPUOptimizedContents = self.allowGPUOptimizedContents
return descriptor
}
}

View File

@@ -0,0 +1,93 @@
/******************************************************************************
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 Metal
extension MTLTextureDescriptor {
/// Convenience initializer for a texture descriptor with `libobs` data
/// - Parameters:
/// - type: Metal texture type
/// - width: Width of texture
/// - height: Height of texture
/// - depth: Depth of texture
/// - colorFormat: `libobs` color format for the texture
/// - levels: Mip map levels
/// - flags: Additional usage flags as `libobs` bitfield
convenience init?(
type: MTLTextureType, width: UInt32, height: UInt32, depth: UInt32, colorFormat: gs_color_format,
levels: UInt32, flags: UInt32
) {
let arrayLength: Int
switch type {
case .type2D:
arrayLength = 1
case .type3D:
arrayLength = 1
case .typeCube:
arrayLength = 6
default:
assertionFailure("MTLTextureDescriptor: Unsupported texture type for libobs initializer")
return nil
}
self.init()
self.textureType = type
self.pixelFormat = colorFormat.mtlFormat
self.width = Int(width)
self.height = Int(height)
self.depth = Int(depth)
self.sampleCount = 1
self.arrayLength = arrayLength
self.cpuCacheMode = .defaultCache
self.allowGPUOptimizedContents = true
self.hazardTrackingMode = .default
if (Int32(flags) & GS_BUILD_MIPMAPS) != 0 {
self.mipmapLevelCount = Int(levels)
} else {
self.mipmapLevelCount = 1
}
if (Int32(flags) & GS_RENDER_TARGET) != 0 {
self.storageMode = .private
self.usage = [.shaderRead, .renderTarget]
} else {
self.storageMode = .shared
self.usage = [.shaderRead]
}
}
convenience init?(width: UInt32, height: UInt32, colorFormat: gs_zstencil_format) {
self.init()
self.textureType = .type2D
self.pixelFormat = colorFormat.mtlFormat
self.width = Int(width)
self.height = Int(height)
self.depth = 1
self.sampleCount = 1
self.arrayLength = 1
self.cpuCacheMode = .defaultCache
self.allowGPUOptimizedContents = true
self.hazardTrackingMode = .default
self.mipmapLevelCount = 1
self.storageMode = .private
self.usage = [.shaderRead]
}
}

View File

@@ -0,0 +1,36 @@
/******************************************************************************
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 Foundation
import Metal
extension MTLTextureType {
/// Converts the Metal texture type into a compatible `libobs` texture type or `nil` if no compatible mapping is
/// possible.
var gsTextureType: gs_texture_type? {
switch self {
case .type2D:
return GS_TEXTURE_2D
case .type3D:
return GS_TEXTURE_3D
case .typeCube:
return GS_TEXTURE_CUBE
default:
return nil
}
}
}

View File

@@ -0,0 +1,31 @@
/******************************************************************************
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 Foundation
import Metal
extension MTLViewport: @retroactive Equatable {
/// Checks two ``MTLViewPort`` objects for equality
/// - Parameters:
/// - lhs: First ``MTLViewPort``object
/// - rhs: Second ``MTLViewPort`` object
/// - Returns: `true` if the dimensions and origins of both view ports match, `false` otherwise.
public static func == (lhs: MTLViewport, rhs: MTLViewport) -> Bool {
lhs.width == rhs.width && lhs.height == rhs.height && lhs.originX == rhs.originX
&& lhs.originY == rhs.originY
}
}

View File

@@ -0,0 +1,308 @@
/******************************************************************************
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 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<T>(
buffer: inout MTLBuffer?, data: UnsafeMutablePointer<T>, count: Int, dynamic: Bool
) {
let size = MemoryLayout<T>.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<gs_vb_data>?
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<gs_vb_data>, 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<gs_vb_data>? = 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<Float>]()
unpackedColors.reserveCapacity(4)
for i in 0..<numVertices {
let vertexColor = colorsData.advanced(by: i)
vertexColor.withMemoryRebound(to: UInt8.self, capacity: 4) {
let colorValues = UnsafeBufferPointer<UInt8>(start: $0, count: 4)
let color = SIMD4<Float>(
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<gs_tvertarray>(
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..<shader.textureCount {
if let uvCoordinate = uvCoordinates[i] {
bufferList.append(uvCoordinate)
}
}
}
}
return bufferList
}
deinit {
gs_vbdata_destroy(vertexData)
}
}
final class MetalIndexBuffer: MetalBuffer {
public var indexData: UnsafeMutableRawPointer?
public var count: Int
public var type: MTLIndexType
var indices: MTLBuffer?
init(device: MetalDevice, type: MTLIndexType, data: UnsafeMutableRawPointer?, count: Int, dynamic: Bool) {
self.indexData = data
self.count = count
self.type = type
super.init(device: device, isDynamic: dynamic)
if !dynamic {
setupBuffers()
}
}
/// Sets up buffer objects for the data provided in the provided memory location
/// - Parameter data: Pointer to bytes representing index buffer data
///
/// The provided memory location is expected to provide bytes represnting index buffer data as either unsigned
/// 16-bit integers or unsigned 32-bit integers. The size depends on the type used to create the
/// ``MetalIndexBuffer`` instance.
public func setupBuffers(_ data: UnsafeMutableRawPointer? = nil) {
guard let indexData = data ?? indexData else {
assertionFailure("MetalIndexBuffer: Unable to generate MTLBuffer without buffer data")
return
}
let byteSize =
switch type {
case .uint16: 2 * count
case .uint32: 4 * count
@unknown default:
fatalError("MTLIndexType \(type) is not supported")
}
indexData.withMemoryRebound(to: UInt8.self, capacity: byteSize) {
createOrUpdateBuffer(buffer: &indices, data: $0, count: byteSize, dynamic: isDynamic)
}
#if DEBUG
if !isDynamic {
indices?.label = "Index buffer static data"
} else {
indices?.label = "Index buffer dynamic data"
}
#endif
}
deinit {
bfree(indexData)
}
}

View File

@@ -0,0 +1,786 @@
/******************************************************************************
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.addScheduledHandler { _ in
drawable.present()
}
}
}
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 seperate 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 comitted 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 comitted 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 comitted 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()
}
}

View File

@@ -0,0 +1,126 @@
/******************************************************************************
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/>.
******************************************************************************/
enum MetalError {
enum MTLCommandQueueError: Error, CustomStringConvertible {
case commandBufferCreationFailure
var description: String {
switch self {
case .commandBufferCreationFailure:
"MTLCommandQueue failed to create command buffer"
}
}
}
enum MTLDeviceError: Error, CustomStringConvertible {
case commandQueueCreationFailure
case displayLinkCreationFailure
case bufferCreationFailure(String)
case shaderCompilationFailure(String)
case pipelineStateCreationFailure
case depthStencilStateCreationFailure
case samplerStateCreationFailure
var description: String {
switch self {
case .commandQueueCreationFailure:
"MTLDevice failed to create command queue"
case .displayLinkCreationFailure:
"MTLDevice failed to create CVDisplayLink for projector output"
case .bufferCreationFailure(_):
"MTLDevice failed to create buffer"
case .shaderCompilationFailure(_):
"MTLDevice failed to create shader library and function"
case .pipelineStateCreationFailure:
"MTLDevice failed to create render pipeline state"
case .depthStencilStateCreationFailure:
"MTLDevice failed to create depth stencil state"
case .samplerStateCreationFailure:
"MTLDevice failed to create sampler state with provided descriptor"
}
}
}
enum MTLCommandBufferError: Error, CustomStringConvertible {
case encoderCreationFailure
var description: String {
switch self {
case .encoderCreationFailure:
"MTLCommandBuffer failed to create command encoder"
}
}
}
enum MetalShaderError: Error, CustomStringConvertible {
case missingVertexDescriptor
case missingSamplerDescriptors
var description: String {
switch self {
case .missingVertexDescriptor:
"MetalShader of type vertex requires a vertex descriptor"
case .missingSamplerDescriptors:
"MetalShader of type fragment requires at least a single sampler descriptor"
}
}
}
enum OBSShaderParserError: Error, CustomStringConvertible {
case parseFail(String)
case unsupportedType
case missingNextToken
case unexpectedToken
case missingMainFunction
var description: String {
switch self {
case .parseFail:
"Failed to parse provided shader string"
case .unsupportedType:
"Provided GS type is not convertible to a Metal type"
case .missingNextToken:
"Required next token not found in parser token collection"
case .unexpectedToken:
"Required next token had unexpected type in parser token collection"
case .missingMainFunction:
"Shader has no main function"
}
}
}
enum OBSShaderError: Error, CustomStringConvertible {
case unsupportedType
case parseFail(String)
case parseError(String)
case transpileError(String)
var description: String {
switch self {
case .unsupportedType:
"Unsupported Metal shader type"
case .parseFail(_):
"OBS shader parser failed to parse effect"
case .parseError(_):
"OBS shader parser encountered warnings and/or errors while parsing effect"
case .transpileError(_):
"Transpiling OBS effects file into MSL shader failed"
}
}
}
}

View File

@@ -0,0 +1,79 @@
/******************************************************************************
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 Foundation
import Metal
import simd
/// The MetalRenderState struct emulates a state object like Direct3D's `ID3D11DeviceContext`, holding references to
/// elements of a render pipeline that would be considered the "current" variant of each.
///
/// Typical "current" state elements include (but are not limited to):
///
/// * Variant of the render target for linear color writes
/// * Variant of the render target for color writes with automatic sRGB gamma encoding
/// * View matrix and view projection matrix
/// * Vertex buffer and optional index buffer
/// * Depth stencil attachment
/// * Vertex shader
/// * Fragment shader
/// * View port size
/// * Cull mode
///
/// These references are swapped out by OBS for each "scene" and "scene items" within it before issuing draw calls,
/// thus actual pipelines need to be created "on demand" based on the pipeline descriptor and stored in a cache to
/// avoid the cost of pipeline validation on consecutive render passes.
struct MetalRenderState {
var viewMatrix: matrix_float4x4
var projectionMatrix: matrix_float4x4
var viewProjectionMatrix: matrix_float4x4
var renderTarget: MetalTexture?
var sRGBrenderTarget: MetalTexture?
var depthStencilAttachment: MetalTexture?
var isRendertargetChanged = false
var vertexBuffer: MetalVertexBuffer?
var indexBuffer: MetalIndexBuffer?
var vertexShader: MetalShader?
var fragmentShader: MetalShader?
var viewPort = MTLViewport()
var cullMode = MTLCullMode.none
var scissorRectEnabled: Bool
var scissorRect: MTLScissorRect?
var gsColorSpace: gs_color_space
var useSRGBGamma = false
var swapChain: OBSSwapChain?
var isInDisplaysRenderStage = false
var pipelineDescriptor = MTLRenderPipelineDescriptor()
var clearPipelineDescriptor = MTLRenderPipelineDescriptor()
var renderPassDescriptor = MTLRenderPassDescriptor()
var depthStencilDescriptor = MTLDepthStencilDescriptor()
var commandBuffer: MTLCommandBuffer?
var textures = [MTLTexture?](repeating: nil, count: Int(GS_MAX_TEXTURES))
var samplers = [MTLSamplerState?](repeating: nil, count: Int(GS_MAX_TEXTURES))
var projections = [matrix_float4x4]()
var inFlightRenderTargets = Set<MetalTexture>()
}

View File

@@ -0,0 +1,27 @@
/******************************************************************************
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 Foundation
import Metal
/// Adds the comparison operator to make ``MetalShader`` instances comparable. Comparison is based on the source string
/// and function type.
extension MetalShader: Equatable {
static func == (lhs: MetalShader, rhs: MetalShader) -> Bool {
return lhs.source == rhs.source && lhs.function.functionType == rhs.function.functionType
}
}

View File

@@ -0,0 +1,287 @@
/******************************************************************************
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 Foundation
import Metal
class MetalShader {
/// This class wraps a single uniform shader variable, which will hold the data associated with the uniform updated
/// by `libobs` at each render loop, which is then converted and set as vertex or fragment bytes for a render pass
/// by the ``MetalDevice/draw`` function.
class ShaderUniform {
let name: String
let gsType: gs_shader_param_type
fileprivate let textureSlot: Int
var samplerState: MTLSamplerState?
fileprivate let byteOffset: Int
var currentValues: [UInt8]?
var defaultValues: [UInt8]?
fileprivate var hasUpdates: Bool
init(
name: String, gsType: gs_shader_param_type, textureSlot: Int, samplerState: MTLSamplerState?,
byteOffset: Int
) {
self.name = name
self.gsType = gsType
self.textureSlot = textureSlot
self.samplerState = samplerState
self.byteOffset = byteOffset
self.currentValues = nil
self.defaultValues = nil
self.hasUpdates = false
}
/// Sets the data for the shader uniform
/// - Parameters:
/// - data: Pointer to data of type `T`
/// - size: Size of data available at the pointer provided by `data`
///
/// This function will reinterpet the data provided by the pointer as raw bytes and store it as raw bytes on
/// the Uniform.
public func setParameter<T>(data: UnsafePointer<T>?, size: Int) {
guard let data else {
assertionFailure(
"MetalShader.ShaderUniform: Attempted to set a shader parameter with an empty data pointer")
return
}
data.withMemoryRebound(to: UInt8.self, capacity: size) {
self.currentValues = Array(UnsafeBufferPointer<UInt8>(start: $0, count: size))
}
hasUpdates = true
}
}
/// This struct serves as a data container to communicate shader meta data between the ``OBSShader`` shader
/// transpiler and the actual ``MetalShader`` instances created with them.
struct ShaderData {
let uniforms: [ShaderUniform]
let bufferOrder: [MetalBuffer.BufferDataType]
let vertexDescriptor: MTLVertexDescriptor?
let samplerDescriptors: [MTLSamplerDescriptor]?
let bufferSize: Int
let textureCount: Int
}
private weak var device: MetalDevice?
let source: String
private var uniformData: [UInt8]
private var uniformSize: Int
private var uniformBuffer: MTLBuffer?
private let library: MTLLibrary
let function: MTLFunction
var uniforms: [ShaderUniform]
var vertexDescriptor: MTLVertexDescriptor?
var textureCount = 0
var samplers: [MTLSamplerState]?
let type: MTLFunctionType
let bufferOrder: [MetalBuffer.BufferDataType]
var viewProjection: ShaderUniform?
init(device: MetalDevice, source: String, type: MTLFunctionType, data: ShaderData) throws {
self.device = device
self.source = source
self.type = type
self.uniforms = data.uniforms
self.bufferOrder = data.bufferOrder
self.uniformSize = (data.bufferSize + 0x0F) & ~0x0F
self.uniformData = [UInt8](repeating: 0, count: self.uniformSize)
self.textureCount = data.textureCount
switch type {
case .vertex:
guard let descriptor = data.vertexDescriptor else {
throw MetalError.MetalShaderError.missingVertexDescriptor
}
self.vertexDescriptor = descriptor
self.viewProjection = self.uniforms.first(where: { $0.name == "ViewProj" })
case .fragment:
guard let samplerDescriptors = data.samplerDescriptors else {
throw MetalError.MetalShaderError.missingSamplerDescriptors
}
var samplers = [MTLSamplerState]()
samplers.reserveCapacity(samplerDescriptors.count)
for descriptor in samplerDescriptors {
guard let samplerState = device.device.makeSamplerState(descriptor: descriptor) else {
throw MetalError.MTLDeviceError.samplerStateCreationFailure
}
samplers.append(samplerState)
}
self.samplers = samplers
default:
fatalError("MetalShader: Unsupported shader type \(type)")
}
do {
library = try device.device.makeLibrary(source: source, options: nil)
} catch {
throw MetalError.MTLDeviceError.shaderCompilationFailure("Failed to create shader library")
}
guard let function = library.makeFunction(name: "_main") else {
throw MetalError.MTLDeviceError.shaderCompilationFailure("Failed to create '_main' function")
}
self.function = function
}
/// Updates the Metal-specific data associated with a ``ShaderUniform`` with the raw bytes provided by `libobs`
/// - Parameter uniform: Inout reference to the ``ShaderUniform`` instance
///
/// Uniform data is provided by `libobs` precisely in the format required by the shader (and interpreted by
/// `libobs`), which means that the raw bytes stored on the ``ShaderUniform`` are usually already in the correct
/// order and can be used without reinterpretation.
///
/// The exception to this rule is data for textures, which represents a copy of a `gs_shader_texture` struct that
/// itself contains the pointer address of an `OpaquePointer` for a ``MetalTexture`` instance.
private func updateUniform(uniform: inout ShaderUniform) {
guard let device = self.device else { return }
guard let currentValues = uniform.currentValues else { return }
if uniform.gsType == GS_SHADER_PARAM_TEXTURE {
var textureObject: OpaquePointer?
var isSrgb = false
currentValues.withUnsafeBufferPointer {
$0.baseAddress?.withMemoryRebound(to: gs_shader_texture.self, capacity: 1) {
textureObject = $0.pointee.tex
isSrgb = $0.pointee.srgb
}
}
if let textureObject {
let texture: MetalTexture = unretained(UnsafeRawPointer(textureObject))
if texture.sRGBtexture != nil, isSrgb {
device.renderState.textures[uniform.textureSlot] = texture.sRGBtexture!
} else {
device.renderState.textures[uniform.textureSlot] = texture.texture
}
}
if let samplerState = uniform.samplerState {
device.renderState.samplers[uniform.textureSlot] = samplerState
uniform.samplerState = nil
}
} else {
if uniform.hasUpdates {
let startIndex = uniform.byteOffset
let endIndex = uniform.byteOffset + currentValues.count
uniformData.replaceSubrange(startIndex..<endIndex, with: currentValues)
}
}
uniform.hasUpdates = false
}
/// 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: Raw byte data array
private func createOrUpdateBuffer(buffer: inout MTLBuffer?, data: inout [UInt8]) {
guard let device = self.device else { return }
let size = MemoryLayout<UInt8>.size * data.count
let alignedSize = (size + 0x0F) & ~0x0F
if buffer != nil {
if buffer!.length == alignedSize {
buffer!.contents().copyMemory(from: data, byteCount: size)
return
}
}
buffer = device.device.makeBuffer(bytes: data, length: alignedSize)
}
/// Sets uniform data for a current render encoder either directly as a buffer
/// - Parameter encoder: `MTLRenderCommandEncoder` for a render pass that requires the uniform data
///
/// Uniform data will be uploaded at index 30 (the very last available index) and is available as a single
/// contiguous block of data. Uniforms are declared as structs in the Metal Shaders and explicitly passed into
/// each function that requires access to them.
func uploadShaderParameters(encoder: MTLRenderCommandEncoder) {
for var uniform in uniforms {
updateUniform(uniform: &uniform)
}
guard uniformSize > 0 else {
return
}
switch function.functionType {
case .vertex:
switch uniformData.count {
case 0..<4096: encoder.setVertexBytes(&uniformData, length: uniformData.count, index: 30)
default:
createOrUpdateBuffer(buffer: &uniformBuffer, data: &uniformData)
#if DEBUG
uniformBuffer?.label = "Vertex shader uniform buffer"
#endif
encoder.setVertexBuffer(uniformBuffer, offset: 0, index: 30)
}
case .fragment:
switch uniformData.count {
case 0..<4096: encoder.setFragmentBytes(&uniformData, length: uniformData.count, index: 30)
default:
createOrUpdateBuffer(buffer: &uniformBuffer, data: &uniformData)
#if DEBUG
uniformBuffer?.label = "Fragment shader uniform buffer"
#endif
encoder.setFragmentBuffer(uniformBuffer, offset: 0, index: 30)
}
default:
fatalError("MetalShader: Unsupported shader type \(function.functionType)")
}
}
/// Gets an opaque pointer for the ``MetalShader`` 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 ``MetalShader`` instance without increasing its reference count
/// - Returns: `OpaquePointer` to class instance
func getUnretained() -> OpaquePointer {
let unretained = Unmanaged.passUnretained(self).toOpaque()
return OpaquePointer(unretained)
}
}

View File

@@ -0,0 +1,65 @@
/******************************************************************************
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 Foundation
import Metal
class MetalStageBuffer {
let device: MetalDevice
let buffer: MTLBuffer
let format: MTLPixelFormat
let width: Int
let height: Int
init?(device: MetalDevice, width: Int, height: Int, format: MTLPixelFormat) {
self.device = device
self.width = width
self.height = height
self.format = format
guard let bytesPerPixel = format.bytesPerPixel,
let buffer = device.device.makeBuffer(
length: width * height * bytesPerPixel,
options: .storageModeShared
)
else {
return nil
}
self.buffer = buffer
}
/// Gets an opaque pointer for the ``MetalStageBuffer`` 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 ``MetalStageBuffer`` instance without increasing its reference count
/// - Returns: `OpaquePointer` to class instance
func getUnretained() -> OpaquePointer {
let unretained = Unmanaged.passUnretained(self).toOpaque()
return OpaquePointer(unretained)
}
}

View File

@@ -0,0 +1,433 @@
/******************************************************************************
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 CoreVideo
import Foundation
import Metal
private let bgraSurfaceFormat = kCVPixelFormatType_32BGRA // 0x42_47_52_41
private let l10rSurfaceFormat = kCVPixelFormatType_ARGB2101010LEPacked // 0x6C_31_30_72
enum MetalTextureMapMode {
case unmapped
case read
case write
}
/// Struct used for data exchange between ``MetalTexture`` and `libobs` API functions during mapping and unmapping of
/// textures.
struct MetalTextureMapping {
let mode: MetalTextureMapMode
let rowSize: Int
let data: UnsafeMutableRawPointer
}
/// Convenience class for managing ``MTLTexture`` objects
class MetalTexture {
private let descriptor: MTLTextureDescriptor
private var mappingMode: MetalTextureMapMode
private let resourceID: UUID
weak var device: MetalDevice?
var data: UnsafeMutableRawPointer?
var hasPendingWrites: Bool = false
var sRGBtexture: MTLTexture?
var texture: MTLTexture
var stageBuffer: MetalStageBuffer?
/// Binds the provided `IOSurfaceRef` to a new `MTLTexture` instance
/// - Parameters:
/// - device: `MTLDevice` instance to use for texture object creation
/// - surface: `IOSurfaceRef` reference to an existing `IOSurface`
/// - Returns: `MTLTexture` instance if texture was created successfully, `nil` otherwise
private static func bindSurface(device: MetalDevice, surface: IOSurfaceRef) -> MTLTexture? {
guard let pixelFormat = MTLPixelFormat.init(osType: IOSurfaceGetPixelFormat(surface)) else {
assertionFailure("MetalDevice: IOSurface pixel format is not supported")
return nil
}
let descriptor = MTLTextureDescriptor.texture2DDescriptor(
pixelFormat: pixelFormat,
width: IOSurfaceGetWidth(surface),
height: IOSurfaceGetHeight(surface),
mipmapped: false
)
descriptor.usage = [.shaderRead]
let texture = device.device.makeTexture(descriptor: descriptor, iosurface: surface, plane: 0)
return texture
}
/// Creates a new ``MetalDevice`` instance with the provided `MTLTextureDescriptor`
/// - Parameters:
/// - device: `MTLDevice` instance to use for texture object creation
/// - descriptor: `MTLTextureDescriptor` to use for texture object creation
init?(device: MetalDevice, descriptor: MTLTextureDescriptor) {
self.device = device
let texture = device.device.makeTexture(descriptor: descriptor)
guard let texture else {
assertionFailure(
"MetalTexture: Failed to create texture with size \(descriptor.width)x\(descriptor.height)")
return nil
}
self.texture = texture
self.resourceID = UUID()
self.mappingMode = .unmapped
self.descriptor = texture.descriptor
updateSRGBView()
}
/// Creates a new ``MetalDevice`` instance with the provided `IOSurfaceRef`
/// - Parameters:
/// - device: `MTLDevice` instance to use for texture object creation
/// - surface: `IOSurfaceRef` to use for texture object creation
init?(device: MetalDevice, surface: IOSurfaceRef) {
self.device = device
let texture = MetalTexture.bindSurface(device: device, surface: surface)
guard let texture else {
assertionFailure("MetalTexture: Failed to create texture with IOSurface")
return nil
}
self.texture = texture
self.resourceID = UUID()
self.mappingMode = .unmapped
self.descriptor = texture.descriptor
updateSRGBView()
}
/// Creates a new ``MetalDevice`` instance with the provided `MTLTexture`
/// - Parameters:
/// - device: `MTLDevice` instance to use for future texture operations
/// - surface: `MTLTexture` to wrap in the ``MetalDevice`` instance
init?(device: MetalDevice, texture: MTLTexture) {
self.device = device
self.texture = texture
self.resourceID = UUID()
self.mappingMode = .unmapped
self.descriptor = texture.descriptor
updateSRGBView()
}
/// Creates a new ``MetalDevice`` instance with a placeholder texture
/// - Parameters:
/// - device: `MTLDevice` instance to use for future texture operations
///
/// This constructor creates a "placeholder" object that can be shared with `libobs` or updated with an actual
/// `MTLTexture` later.
init?(device: MetalDevice) {
self.device = device
let descriptor = MTLTextureDescriptor.texture2DDescriptor(
pixelFormat: .bgra8Unorm, width: 2, height: 2, mipmapped: false)
guard let texture = device.device.makeTexture(descriptor: descriptor) else {
assertionFailure("MetalTexture: Failed to create placeholder texture object")
return nil
}
self.texture = texture
self.sRGBtexture = nil
self.resourceID = UUID()
self.mappingMode = .unmapped
self.descriptor = texture.descriptor
}
/// Updates the ``MetalTexture`` with a new `IOSurfaceRef`
/// - Parameter surface: Updated `IOSurfaceRef` to a new `IOSurface`
/// - Returns: `true` if update was successful, `false` otherwise
///
/// "Rebinding" was used with the OpenGL backend, but is not available in Metal. Instead a new `MTLTexture` is
/// created with the provided `IOSurfaceRef` and the ``MetalTexture`` is updated accordingly.
///
func rebind(surface: IOSurfaceRef) -> Bool {
guard let device = self.device, let texture = MetalTexture.bindSurface(device: device, surface: surface) else {
assertionFailure("MetalTexture: Failed to rebind IOSurface to texture")
return false
}
self.texture = texture
updateSRGBView()
return true
}
/// Creates a `MTLTextureView` for the texture wrapped by the ``MetalTexture`` instance with a corresponding sRGB
/// pixel format, if the texture's pixel format has an appropriate sRGB variant.
func updateSRGBView() {
guard !texture.isFramebufferOnly else {
self.sRGBtexture = nil
return
}
let sRGBFormat: MTLPixelFormat? =
switch texture.pixelFormat {
case .bgra8Unorm: .bgra8Unorm_srgb
case .rgba8Unorm: .rgba8Unorm_srgb
case .r8Unorm: .r8Unorm_srgb
case .rg8Unorm: .rg8Unorm_srgb
case .bgra10_xr: .bgra10_xr_srgb
default: nil
}
if let sRGBFormat {
self.sRGBtexture = texture.makeTextureView(pixelFormat: sRGBFormat)
} else {
self.sRGBtexture = nil
}
}
/// Downloads pixel data from the wrapped `MTLTexture` to the memory location provided by a pointer.
/// - Parameters:
/// - data: Pointer to memory that should receive the texture data
/// - mipmapLevel: Mipmap level of the texture to copy data from
///
/// > Important: The access of texture data is neither protected nor synchronized. If any draw calls to the texture
/// take place while this function is executed, the downloaded data will reflect this. Use explicit synchronization
/// before initiating a download to prevent this.
func download(data: UnsafeMutableRawPointer, mipmapLevel: Int = 0) {
let mipmapWidth = texture.width >> mipmapLevel
let mipmapHeight = texture.height >> mipmapLevel
let rowSize = mipmapWidth * texture.pixelFormat.bytesPerPixel!
let region = MTLRegionMake2D(0, 0, mipmapWidth, mipmapHeight)
texture.getBytes(data, bytesPerRow: rowSize, from: region, mipmapLevel: mipmapLevel)
}
/// Uploads pixel data into the wrappred `MTLTexture` from the memory location provided by a pointer.
/// - Parameters:
/// - data: Pointer to memory that contains the texture data
/// - mipmapLevels: Mipmap level of the texture to copy data into
///
/// > Important: The write access of texture data is neither protected nor synchronized. If any draw calls use this
/// texture for reading or writing while this function is executed, the upload might have been incomplete or the
/// data might have been overwritten by the GPU. Use explicit synchronization before initiaitng an upload to
/// prevent this.
func upload(data: UnsafePointer<UnsafePointer<UInt8>?>, mipmapLevels: Int) {
let bytesPerPixel = texture.pixelFormat.bytesPerPixel!
switch texture.textureType {
case .type2D, .typeCube:
let textureCount = if texture.textureType == .typeCube { 6 } else { 1 }
let data = UnsafeBufferPointer(start: data, count: (textureCount * mipmapLevels))
for i in 0..<textureCount {
for mipmapLevel in 0..<mipmapLevels {
let index = mipmapLevels * i + mipmapLevel
guard let data = data[index] else { break }
let mipmapWidth = texture.width >> mipmapLevel
let mipmapHeight = texture.height >> mipmapLevel
let rowSize = mipmapWidth * bytesPerPixel
let region = MTLRegionMake2D(0, 0, mipmapWidth, mipmapHeight)
texture.replace(
region: region, mipmapLevel: mipmapLevel, slice: i, withBytes: data, bytesPerRow: rowSize,
bytesPerImage: 0)
}
}
case .type3D:
let data = UnsafeBufferPointer(start: data, count: mipmapLevels)
for (mipmapLevel, mipmapData) in data.enumerated() {
guard let mipmapData else { break }
let mipmapWidth = texture.width >> mipmapLevel
let mipmapHeight = texture.height >> mipmapLevel
let mipmapDepth = texture.depth >> mipmapLevel
let rowSize = mipmapWidth * bytesPerPixel
let imageSize = rowSize * mipmapHeight
let region = MTLRegionMake3D(0, 0, 0, mipmapWidth, mipmapHeight, mipmapDepth)
texture.replace(
region: region,
mipmapLevel: mipmapLevel,
slice: 0,
withBytes: mipmapData,
bytesPerRow: rowSize,
bytesPerImage: imageSize
)
}
default:
fatalError("MetalTexture: Unsupported texture type \(texture.textureType)")
}
if texture.mipmapLevelCount > 1 {
let device = self.device!
try? device.ensureCommandBuffer()
guard let buffer = device.renderState.commandBuffer,
let encoder = buffer.makeBlitCommandEncoder()
else {
assertionFailure("MetalTexture: Failed to create command buffer for mipmap generation")
return
}
encoder.generateMipmaps(for: texture)
encoder.endEncoding()
}
}
/// Emulates the "map" operation available in Direct3D, providing a pointer for texture uploads or downloads
/// - Parameters:
/// - mode: Map mode to use (writing or reading)
/// - mipmapLevel: Mip map level to map
/// - Returns: A ``MetalTextureMapping`` struct that provides the result of the mapping
///
/// In Direct3D a "map" operation will do many things at once depending on the current state of its pipelines and
/// the mapping mode used:
/// * When mapped for writing, Direct3D will provide a pointer to CPU memory into which an application can write
/// new texture data.
/// * When mapped for reading, Direct3D will provide a pointer to CPU memory into which it has copied the contents
/// of the texture
///
/// In either case, the texture will be blocked from access by the GPU until it is unmapped again. In some cases a
/// "map" operation will also implicitly initiate a "flush" operation to ensure that pending GPU commands involving
/// this texture are submitted before it becomes unavailable.
///
/// Metal does not provide such a convenience method and because `libobs` operates under the assumption that it has
/// to copy its own data into a memory location provided by Direct3D, this has to be emulated explicitly here,
/// albeit without the blocking of access to the texture.
///
/// This function always needs to be balanced by an appropriate ``unmap`` call.
func map(mode: MetalTextureMapMode, mipmapLevel: Int = 0) -> MetalTextureMapping? {
guard mappingMode == .unmapped else {
assertionFailure("MetalTexture: Attempted to map already-mapped texture.")
return nil
}
let mipmapWidth = texture.width >> mipmapLevel
let mipmapHeight = texture.height >> mipmapLevel
let rowSize = mipmapWidth * texture.pixelFormat.bytesPerPixel!
let dataSize = rowSize * mipmapHeight
// TODO: Evaluate whether a blit to/from a `MTLBuffer` with its `contents` pointer shared is more efficient
let data = UnsafeMutableRawBufferPointer.allocate(byteCount: dataSize, alignment: MemoryLayout<UInt8>.alignment)
guard let baseAddress = data.baseAddress else {
return nil
}
if mode == .read {
download(data: baseAddress, mipmapLevel: mipmapLevel)
}
self.data = baseAddress
self.mappingMode = mode
let mapping = MetalTextureMapping(
mode: mode,
rowSize: rowSize,
data: baseAddress
)
return mapping
}
/// Emulates the "unmap" operation available in Direct3D
/// - Parameter mipmapLevel: The mipmap level that is to be unmapped
///
/// This function will replace the contents of the "mapped" texture with the data written into the memory provided
/// by the "mapping".
///
/// As such this function has to always balance the corresponding ``map`` call to ensure that the data written into
/// the provided memory location is written into the texture and the memory itself is deallocated.
func unmap(mipmapLevel: Int = 0) {
guard mappingMode != .unmapped else {
assertionFailure("MetalTexture: Attempted to unmap an unmapped texture")
return
}
let mipmapWidth = texture.width >> mipmapLevel
let mipmapHeight = texture.height >> mipmapLevel
let rowSize = mipmapWidth * texture.pixelFormat.bytesPerPixel!
let region = MTLRegionMake2D(0, 0, mipmapWidth, mipmapHeight)
if let textureData = self.data {
if self.mappingMode == .write {
texture.replace(
region: region,
mipmapLevel: mipmapLevel,
withBytes: textureData,
bytesPerRow: rowSize
)
}
textureData.deallocate()
self.data = nil
}
self.mappingMode = .unmapped
}
/// Gets an opaque pointer for the ``MetalTexture`` 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 ``MetalTexture`` instance without increasing its reference count
/// - Returns: `OpaquePointer` to class instance
func getUnretained() -> OpaquePointer {
let unretained = Unmanaged.passUnretained(self).toOpaque()
return OpaquePointer(unretained)
}
}
/// Extends the ``MetalTexture`` class with comparison operators and a hash function to enable the use inside a `Set`
/// collection
extension MetalTexture: Hashable {
static func == (lhs: MetalTexture, rhs: MetalTexture) -> Bool {
lhs.resourceID == rhs.resourceID
}
static func != (lhs: MetalTexture, rhs: MetalTexture) -> Bool {
lhs.resourceID != rhs.resourceID
}
func hash(into hasher: inout Hasher) {
hasher.combine(resourceID)
}
}

1603
libobs-metal/OBSShader.swift Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
/******************************************************************************
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 CoreVideo
import Foundation
import Metal
class OBSSwapChain {
enum ColorRange {
case sdr
case hdrPQ
case hdrHLG
}
private weak var device: MetalDevice?
private var view: NSView?
var colorRange: ColorRange
var edrHeadroom: CGFloat = 0.0
let layer: CAMetalLayer
var renderTarget: MetalTexture?
var viewSize: MTLSize
var fence: MTLFence
var discard: Bool = false
init?(device: MetalDevice, size: MTLSize, colorSpace: gs_color_format) {
self.device = device
self.viewSize = size
self.layer = CAMetalLayer()
self.layer.framebufferOnly = false
self.layer.device = device.device
self.layer.drawableSize = CGSize(width: viewSize.width, height: viewSize.height)
self.layer.pixelFormat = .bgra8Unorm_srgb
self.layer.colorspace = CGColorSpace(name: CGColorSpace.sRGB)
self.layer.wantsExtendedDynamicRangeContent = false
self.layer.edrMetadata = nil
self.layer.displaySyncEnabled = false
self.colorRange = .sdr
guard let fence = device.device.makeFence() else { return nil }
self.fence = fence
}
/// Updates the provided view to use the `CAMetalLayer` managed by the ``OBSSwapChain``
/// - Parameter view: `NSView` instance to update
///
/// > Important: This function has to be called from the main thread
@MainActor
func updateView(_ view: NSView) {
self.view = view
view.layer = self.layer
view.wantsLayer = true
updateEdrHeadroom()
}
/// Updates the EDR headroom value on the ``OBSSwapChain`` with the value from the screen the managed `NSView` is
/// associated with.
///
/// This is necessary to ensure that the projector uses the appropriate SDR or EDR output depending on the screen
/// the view is on.
@MainActor
func updateEdrHeadroom() {
guard let view = self.view else {
return
}
if let screen = view.window?.screen {
self.edrHeadroom = screen.maximumPotentialExtendedDynamicRangeColorComponentValue
} else {
self.edrHeadroom = CGFloat(1.0)
}
}
/// Resizes the drawable of the managed `CAMetalLayer` to the provided size
/// - Parameter size: Desired new size of the drawable
///
/// This is usually achieved via a delegate method directly on the associated `NSView` instance, but because the
/// view is managed by Qt, the resize event is routed manually into the ``OBSSwapChain`` instance by `libobs`.
func resize(_ size: MTLSize) {
guard viewSize.width != size.width || viewSize.height != size.height else { return }
viewSize = size
layer.drawableSize = CGSize(
width: viewSize.width,
height: viewSize.height)
renderTarget = nil
}
/// Gets an opaque pointer for the ``OBSSwapChain`` 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 ``OBSSwapChain`` instance without increasing its reference count
/// - Returns: `OpaquePointer` to class instance
func getUnretained() -> OpaquePointer {
let unretained = Unmanaged.passUnretained(self).toOpaque()
return OpaquePointer(unretained)
}
}

View File

@@ -0,0 +1,25 @@
/******************************************************************************
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/>.
******************************************************************************/
extension Sequence where Iterator.Element: Hashable {
/// Filters a `Sequence` to only contain its unique elements, retaining order for unique elements.
/// - Returns: Filtered `Sequence` with unique elements of original `Sequence`
func unique() -> [Iterator.Element] {
var seen: Set<Iterator.Element> = []
return filter { seen.insert($0).inserted }
}
}

View File

@@ -0,0 +1,486 @@
/******************************************************************************
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 Foundation
import Metal
import simd
public enum OBSLogLevel: Int32 {
case error = 100
case warning = 200
case info = 300
case debug = 400
}
extension strref {
mutating func getString() -> String {
let buffer = UnsafeRawBufferPointer(start: self.array, count: self.len)
let string = String(decoding: buffer, as: UTF8.self)
return string
}
mutating func isEqualTo(_ comparison: String) -> Bool {
return strref_cmp(&self, comparison.cString(using: .utf8)) == 0
}
mutating func isEqualToCString(_ comparison: UnsafeMutablePointer<CChar>?) -> Bool {
if let comparison {
let result = withUnsafeMutablePointer(to: &self) {
strref_cmp($0, comparison) == 0
}
return result
}
return false
}
}
extension cf_parser {
mutating func advanceToken() -> Bool {
let result = withUnsafeMutablePointer(to: &self) {
cf_next_token($0)
}
return result
}
mutating func hasNextToken() -> Bool {
let result = withUnsafeMutablePointer(to: &self) {
var nextToken: UnsafeMutablePointer<cf_token>?
switch $0.pointee.cur_token.pointee.type {
case CFTOKEN_SPACETAB, CFTOKEN_NEWLINE, CFTOKEN_NONE:
nextToken = $0.pointee.cur_token
default:
nextToken = $0.pointee.cur_token.advanced(by: 1)
}
if var nextToken {
while nextToken.pointee.type == CFTOKEN_SPACETAB || nextToken.pointee.type == CFTOKEN_NEWLINE {
nextToken = nextToken.successor()
}
return nextToken.pointee.type != CFTOKEN_NONE
} else {
return false
}
}
return result
}
mutating func tokenIsEqualTo(_ comparison: String) -> Bool {
let result = withUnsafeMutablePointer(to: &self) {
cf_token_is($0, comparison.cString(using: .utf8))
}
return result
}
}
extension gs_shader_param_type {
var size: Int {
switch self {
case GS_SHADER_PARAM_BOOL, GS_SHADER_PARAM_INT, GS_SHADER_PARAM_FLOAT:
return MemoryLayout<Float32>.size
case GS_SHADER_PARAM_INT2, GS_SHADER_PARAM_VEC2:
return MemoryLayout<Float32>.size * 2
case GS_SHADER_PARAM_INT3, GS_SHADER_PARAM_VEC3:
return MemoryLayout<Float32>.size * 3
case GS_SHADER_PARAM_INT4, GS_SHADER_PARAM_VEC4:
return MemoryLayout<Float32>.size * 4
case GS_SHADER_PARAM_MATRIX4X4:
return MemoryLayout<Float32>.size * 4 * 4
case GS_SHADER_PARAM_TEXTURE:
return MemoryLayout<gs_shader_texture>.size
case GS_SHADER_PARAM_STRING, GS_SHADER_PARAM_UNKNOWN:
return 0
default:
return 0
}
}
var mtlSize: Int {
switch self {
case GS_SHADER_PARAM_BOOL, GS_SHADER_PARAM_INT, GS_SHADER_PARAM_FLOAT:
return MemoryLayout<simd_float1>.size
case GS_SHADER_PARAM_INT2, GS_SHADER_PARAM_VEC2:
return MemoryLayout<simd_float2>.size
case GS_SHADER_PARAM_INT3, GS_SHADER_PARAM_VEC3:
return MemoryLayout<simd_float3>.size
case GS_SHADER_PARAM_INT4, GS_SHADER_PARAM_VEC4:
return MemoryLayout<simd_float4>.size
case GS_SHADER_PARAM_MATRIX4X4:
return MemoryLayout<simd_float4x4>.size
case GS_SHADER_PARAM_TEXTURE:
return MemoryLayout<gs_shader_texture>.size
case GS_SHADER_PARAM_STRING, GS_SHADER_PARAM_UNKNOWN:
return 0
default:
return 0
}
}
var mtlAlignment: Int {
switch self {
case GS_SHADER_PARAM_BOOL, GS_SHADER_PARAM_INT, GS_SHADER_PARAM_FLOAT:
return MemoryLayout<simd_float1>.alignment
case GS_SHADER_PARAM_INT2, GS_SHADER_PARAM_VEC2:
return MemoryLayout<simd_float2>.alignment
case GS_SHADER_PARAM_INT3, GS_SHADER_PARAM_VEC3:
return MemoryLayout<simd_float3>.alignment
case GS_SHADER_PARAM_INT4, GS_SHADER_PARAM_VEC4:
return MemoryLayout<simd_float4>.alignment
case GS_SHADER_PARAM_MATRIX4X4:
return MemoryLayout<simd_float4x4>.alignment
case GS_SHADER_PARAM_TEXTURE:
return 0
case GS_SHADER_PARAM_STRING, GS_SHADER_PARAM_UNKNOWN:
return 0
default:
return 0
}
}
}
extension gs_color_format {
var sRGBVariant: MTLPixelFormat? {
switch self {
case GS_RGBA:
return .rgba8Unorm_srgb
case GS_BGRX, GS_BGRA:
return .bgra8Unorm_srgb
default:
return nil
}
}
var mtlFormat: MTLPixelFormat {
switch self {
case GS_A8:
return .a8Unorm
case GS_R8:
return .r8Unorm
case GS_R8G8:
return .rg8Unorm
case GS_R16:
return .r16Unorm
case GS_R16F:
return .r16Float
case GS_RG16:
return .rg16Unorm
case GS_RG16F:
return .rg16Float
case GS_R32F:
return .r32Float
case GS_RG32F:
return .rg32Float
case GS_RGBA:
return .rgba8Unorm
case GS_BGRX, GS_BGRA:
return .bgra8Unorm
case GS_R10G10B10A2:
return .rgb10a2Unorm
case GS_RGBA16:
return .rgba16Unorm
case GS_RGBA16F:
return .rgba16Float
case GS_RGBA32F:
return .rgba32Float
case GS_DXT1:
return .bc1_rgba
case GS_DXT3:
return .bc2_rgba
case GS_DXT5:
return .bc3_rgba
default:
return .invalid
}
}
}
extension gs_color_space {
var colorFormat: gs_color_format {
switch self {
case GS_CS_SRGB_16F, GS_CS_709_SCRGB:
return GS_RGBA16F
default:
return GS_RGBA
}
}
var pixelFormat: MTLPixelFormat? {
switch self {
case GS_CS_SRGB:
.bgra8Unorm_srgb
case GS_CS_709_SCRGB:
nil
case GS_CS_709_EXTENDED:
.bgra10_xr_srgb
case GS_CS_SRGB_16F:
nil
default:
nil
}
}
}
extension gs_depth_test {
var mtlFunction: MTLCompareFunction {
switch self {
case GS_NEVER:
return .never
case GS_LESS:
return .less
case GS_LEQUAL:
return .lessEqual
case GS_EQUAL:
return .equal
case GS_GEQUAL:
return .greaterEqual
case GS_GREATER:
return .greater
case GS_NOTEQUAL:
return .notEqual
case GS_ALWAYS:
return .always
default:
return .never
}
}
}
extension gs_stencil_op_type {
var mtlOperation: MTLStencilOperation {
switch self {
case GS_KEEP:
return .keep
case GS_ZERO:
return .zero
case GS_REPLACE:
return .replace
case GS_INCR:
return .incrementWrap
case GS_DECR:
return .decrementWrap
case GS_INVERT:
return .invert
default:
return .keep
}
}
}
extension gs_blend_type {
var blendFactor: MTLBlendFactor? {
switch self {
case GS_BLEND_ZERO:
return .zero
case GS_BLEND_ONE:
return .one
case GS_BLEND_SRCCOLOR:
return .sourceColor
case GS_BLEND_INVSRCCOLOR:
return .oneMinusSourceColor
case GS_BLEND_SRCALPHA:
return .sourceAlpha
case GS_BLEND_INVSRCALPHA:
return .oneMinusSourceAlpha
case GS_BLEND_DSTCOLOR:
return .destinationColor
case GS_BLEND_INVDSTCOLOR:
return .oneMinusDestinationColor
case GS_BLEND_DSTALPHA:
return .destinationAlpha
case GS_BLEND_INVDSTALPHA:
return .oneMinusDestinationAlpha
case GS_BLEND_SRCALPHASAT:
return .sourceAlphaSaturated
default:
return nil
}
}
}
extension gs_blend_op_type {
var mtlOperation: MTLBlendOperation? {
switch self {
case GS_BLEND_OP_ADD:
return .add
case GS_BLEND_OP_MAX:
return .max
case GS_BLEND_OP_MIN:
return .min
case GS_BLEND_OP_SUBTRACT:
return .subtract
case GS_BLEND_OP_REVERSE_SUBTRACT:
return .reverseSubtract
default:
return nil
}
}
}
extension gs_cull_mode {
var mtlMode: MTLCullMode {
switch self {
case GS_BACK:
return .back
case GS_FRONT:
return .front
default:
return .none
}
}
}
extension gs_draw_mode {
var mtlPrimitive: MTLPrimitiveType? {
switch self {
case GS_POINTS:
return .point
case GS_LINES:
return .line
case GS_LINESTRIP:
return .lineStrip
case GS_TRIS:
return .triangle
case GS_TRISTRIP:
return .triangleStrip
default:
return nil
}
}
}
extension gs_rect {
var mtlViewPort: MTLViewport {
MTLViewport(
originX: Double(self.x),
originY: Double(self.y),
width: Double(self.cx),
height: Double(self.cy),
znear: 0.0,
zfar: 1.0)
}
var mtlScissorRect: MTLScissorRect {
MTLScissorRect(
x: Int(self.x),
y: Int(self.y),
width: Int(self.cx),
height: Int(self.cy))
}
}
extension gs_zstencil_format {
var mtlFormat: MTLPixelFormat {
switch self {
case GS_ZS_NONE:
return .invalid
case GS_Z16:
return .depth16Unorm
case GS_Z24_S8:
return .depth24Unorm_stencil8
case GS_Z32F:
return .depth32Float
case GS_Z32F_S8X24:
return .depth32Float_stencil8
default:
return .invalid
}
}
}
extension gs_index_type {
var mtlType: MTLIndexType? {
switch self {
case GS_UNSIGNED_LONG:
return .uint16
case GS_UNSIGNED_SHORT:
return .uint32
default:
return nil
}
}
var byteSize: Int {
guard let indexType = self.mtlType else {
return 0
}
let byteSize =
if indexType == .uint16 {
2
} else {
4
}
return byteSize
}
}
extension gs_address_mode {
var mtlMode: MTLSamplerAddressMode? {
switch self {
case GS_ADDRESS_WRAP:
return .repeat
case GS_ADDRESS_CLAMP:
return .clampToEdge
case GS_ADDRESS_MIRROR:
return .mirrorRepeat
case GS_ADDRESS_BORDER:
return .clampToBorderColor
case GS_ADDRESS_MIRRORONCE:
return .mirrorClampToEdge
default:
return nil
}
}
}
extension gs_sample_filter {
var minMagFilter: MTLSamplerMinMagFilter? {
switch self {
case GS_FILTER_POINT, GS_FILTER_MIN_MAG_POINT_MIP_LINEAR, GS_FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT,
GS_FILTER_MIN_POINT_MAG_MIP_LINEAR:
return .nearest
case GS_FILTER_LINEAR, GS_FILTER_MIN_LINEAR_MAG_MIP_POINT, GS_FILTER_MIN_LINEAR_MAG_POINT_MIP_LINEAR,
GS_FILTER_MIN_MAG_LINEAR_MIP_POINT, GS_FILTER_ANISOTROPIC:
return .linear
default:
return nil
}
}
var mipFilter: MTLSamplerMipFilter? {
switch self {
case GS_FILTER_POINT, GS_FILTER_MIN_MAG_POINT_MIP_LINEAR, GS_FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT,
GS_FILTER_MIN_POINT_MAG_MIP_LINEAR:
return .nearest
case GS_FILTER_LINEAR, GS_FILTER_MIN_LINEAR_MAG_MIP_POINT, GS_FILTER_MIN_LINEAR_MAG_POINT_MIP_LINEAR,
GS_FILTER_MIN_MAG_LINEAR_MIP_POINT, GS_FILTER_ANISOTROPIC:
return .linear
default:
return nil
}
}
}

View File

@@ -0,0 +1,34 @@
/******************************************************************************
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 Foundation
enum MetalSignalType: String {
case videoReset = "video_reset"
}
/// Dispatches the video reset event to the ``MetalDevice`` instance
/// - Parameters:
/// - param: Opaque pointer to a ``MetalDevice`` instance
/// - _: Unused pointer to signal callback data
public func metal_video_reset_handler(_ param: UnsafeMutableRawPointer?, _: UnsafeMutablePointer<calldata>?) {
guard let param else { return }
let metalDevice = unsafeBitCast(param, to: MetalDevice.self)
metalDevice.dispatchSignal(type: .videoReset)
}

View File

@@ -0,0 +1,32 @@
/******************************************************************************
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 <util/base.h>
#import <util/cf-parser.h>
#import <util/cf-lexer.h>
#import <obs.h>
#import <graphics/graphics.h>
#import <graphics/device-exports.h>
#import <graphics/vec2.h>
#import <graphics/matrix3.h>
#import <graphics/matrix4.h>
#import <graphics/shader-parser.h>
static const char *const device_name = "Metal";
static const char *const preprocessor_name = "_Metal";

View File

@@ -0,0 +1,158 @@
/******************************************************************************
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 Foundation
import Metal
/// Creates a ``MetalIndexBuffer`` object to share with `libobs` and hold the provided indices
///
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - type: Size of each index value (16 bit or 32 bit)
/// - indices: Opaque pointer to index buffer data set up by `libobs`
/// - num: Count of vertices present at the memory address provided by the `indices` argument
/// - flags: Bit field of `libobs` buffer flags
/// - Returns: Opaque pointer to a retained ``MetalIndexBuffer`` instance if valid index type was provided, `nil`
/// otherwise
///
/// > Note: The ownership of the memory pointed to by `indices` is implicitly transferred to the ``MetalIndexBuffer``
/// instance, but is not managed by Swift.
@_cdecl("device_indexbuffer_create")
public func device_indexbuffer_create(
device: UnsafeRawPointer, type: gs_index_type, indices: UnsafeMutableRawPointer, num: UInt32, flags: UInt32
) -> OpaquePointer? {
let device: MetalDevice = unretained(device)
guard let indexType = type.mtlType else {
return nil
}
let indexBuffer = MetalIndexBuffer(
device: device,
type: indexType,
data: indices,
count: Int(num),
dynamic: (Int32(flags) & GS_DYNAMIC) != 0
)
return indexBuffer.getRetained()
}
/// Sets up a ``MetalIndexBuffer`` as the index buffer for the current pipeline
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - indexbuffer: Opaque pointer to ``MetalIndexBuffer`` instance shared with `libobs`
///
/// > Note: The reference count of the ``MetalIndexBuffer`` instance will not be increased by this call.
///
/// > Important: If a `nil` pointer is provided as the index buffer, the index buffer will be _unset_.
@_cdecl("device_load_indexbuffer")
public func device_load_indexbuffer(device: UnsafeRawPointer, indexbuffer: UnsafeRawPointer?) {
let device: MetalDevice = unretained(device)
if let indexbuffer {
device.renderState.indexBuffer = unretained(indexbuffer)
} else {
device.renderState.indexBuffer = nil
}
}
/// Requests the deinitialization of a shared ``MetalIndexBuffer`` instance
/// - Parameter indexBuffer: Opaque pointer to ``MetalIndexBuffer`` instance shared with `libobs`
///
/// The deinitialization is handled automatically by Swift after the ownership of the instance has been transferred
/// into the function and becomes the last strong reference to it. After the function leaves its scope, the object will
/// be deinitialized and deallocated automatically.
///
/// > Note: The index buffer data memory is implicitly owned by the ``MetalIndexBuffer`` instance and will be manually
/// cleaned up and deallocated by the instance's `deinit` method.
@_cdecl("gs_indexbuffer_destroy")
public func gs_indexbuffer_destroy(indexBuffer: UnsafeRawPointer) {
let _ = retained(indexBuffer) as MetalIndexBuffer
}
/// Requests the index buffer's current data to be transferred into GPU memory
/// - Parameter indexBuffer: Opaque pointer to ``MetalIndexBuffer`` instance shared with `libobs`
///
/// This function will call `gs_indexbuffer_flush_direct` with `nil` data pointer.
@_cdecl("gs_indexbuffer_flush")
public func gs_indexbuffer_flush(indexBuffer: UnsafeRawPointer) {
gs_indexbuffer_flush_direct(indexBuffer: indexBuffer, data: nil)
}
/// Requests the index buffer to be updated with the provided data and then transferred into GPU memory
/// - Parameters:
/// - indexBuffer: Opaque pointer to ``MetalIndexBuffer`` instance shared with `libobs`
/// - data: Opaque pointer to index buffer data set up by `libobs`
///
/// This function is called to ensure that the index buffer data that is contained in the memory pointed at by the
/// `data` argument is uploaded into GPU memory. If a `nil` pointer is provided instead, the data provided to the
/// instance during creation will be used instead.
@_cdecl("gs_indexbuffer_flush_direct")
public func gs_indexbuffer_flush_direct(indexBuffer: UnsafeRawPointer, data: UnsafeMutableRawPointer?) {
let indexBuffer: MetalIndexBuffer = unretained(indexBuffer)
indexBuffer.setupBuffers(data)
}
/// Returns an opaque pointer to the index buffer data associated with the ``MetalIndexBuffer`` instance
/// - Parameter indexBuffer: Opaque pointer to ``MetalIndexBuffer`` instance shared with `libobs`
/// - Returns: Opaque pointer to index buffer data in memory
///
/// The returned opaque pointer represents the unchanged memory address that was provided for the creation of the index
/// buffer object.
///
/// > Warning: There is only limited memory safety associated with this pointer. It is implicitly owned and its
/// lifetime is managed by the ``MetalIndexBuffer`` instance, but it was originally created by `libobs`.
@_cdecl("gs_indexbuffer_get_data")
public func gs_indexbuffer_get_data(indexBuffer: UnsafeRawPointer) -> UnsafeMutableRawPointer? {
let indexBuffer: MetalIndexBuffer = unretained(indexBuffer)
return indexBuffer.indexData
}
/// Returns the number of indices associated with the ``MetalIndexBuffer`` instance
/// - Parameter indexBuffer: Opaque pointer to ``MetalIndexBuffer`` instance shared with `libobs`
/// - Returns: Number of index buffers
///
/// > Note: This returns the same number that was provided for the creation of the index buffer object.
@_cdecl("gs_indexbuffer_get_num_indices")
public func gs_indexbuffer_get_num_indices(indexBuffer: UnsafeRawPointer) -> UInt32 {
let indexBuffer: MetalIndexBuffer = unretained(indexBuffer)
return UInt32(indexBuffer.count)
}
/// Gets the index buffer type as a `libobs` enum value
/// - Parameter indexBuffer: Opaque pointer to ``MetalIndexBuffer`` instance shared with `libobs`
/// - Returns: Index buffer type as identified by the `gs_index_type` enum
///
/// > Warning: As the `gs_index_type` enumeration does not provide an "invalid" value (and thus `0` becomes a valied
/// value), this function has no way to communicate an incompatible index buffer type that might be introduced at a
/// later point.
@_cdecl("gs_indexbuffer_get_type")
public func gs_indexbuffer_get_type(indexBuffer: UnsafeRawPointer) -> gs_index_type {
let indexBuffer: MetalIndexBuffer = unretained(indexBuffer)
switch indexBuffer.type {
case .uint16: return GS_UNSIGNED_SHORT
case .uint32: return GS_UNSIGNED_LONG
@unknown default:
assertionFailure("gs_indexbuffer_get_type: Unsupported index buffer type \(indexBuffer.type)")
return GS_UNSIGNED_SHORT
}
}

View File

@@ -0,0 +1,100 @@
/******************************************************************************
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 Foundation
import Metal
/// Creates a new ``MTLSamplerDescriptor`` to share as an opaque pointer with `libobs`
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - info: Sampler information encoded as a `gs_sampler_info` struct
/// - Returns: Opaque pointer to a new ``MTLSamplerDescriptor`` instance on success, `nil` otherwise
@_cdecl("device_samplerstate_create")
public func device_samplerstate_create(device: UnsafeRawPointer, info: gs_sampler_info) -> OpaquePointer? {
let device: MetalDevice = unretained(device)
guard let sAddressMode = info.address_u.mtlMode,
let tAddressMode = info.address_v.mtlMode,
let rAddressMode = info.address_w.mtlMode
else {
assertionFailure("device_samplerstate_create: Invalid address modes provided")
return nil
}
guard let minFilter = info.filter.minMagFilter, let magFilter = info.filter.minMagFilter,
let mipFilter = info.filter.mipFilter
else {
assertionFailure("device_samplerstate_create: Invalid filter modes provided")
return nil
}
let descriptor = MTLSamplerDescriptor()
descriptor.sAddressMode = sAddressMode
descriptor.tAddressMode = tAddressMode
descriptor.rAddressMode = rAddressMode
descriptor.minFilter = minFilter
descriptor.magFilter = magFilter
descriptor.mipFilter = mipFilter
descriptor.maxAnisotropy = max(16, min(1, Int(info.max_anisotropy)))
descriptor.compareFunction = .always
descriptor.borderColor =
if (info.border_color & 0x00_00_00_FF) == 0 {
.transparentBlack
} else if info.border_color == 0xFF_FF_FF_FF {
.opaqueWhite
} else {
.opaqueBlack
}
guard let samplerState = device.device.makeSamplerState(descriptor: descriptor) else {
assertionFailure("device_samplerstate_create: Unable to create sampler state")
return nil
}
let retained = Unmanaged.passRetained(samplerState).toOpaque()
return OpaquePointer(retained)
}
/// Requests the deinitialization of the ``MTLSamplerState`` instance shared with `libobs`
/// - Parameter samplerstate: Opaque pointer to ``MTLSamplerState`` instance shared with `libobs`
///
/// Ownership of the ``MTLSamplerState`` instance will be transferred into the function and if this was the last
/// strong reference to it, the object will be automatically deinitialized and deallocated by Swift.
@_cdecl("gs_samplerstate_destroy")
public func gs_samplerstate_destroy(samplerstate: UnsafeRawPointer) {
let _ = retained(samplerstate) as MTLSamplerState
}
/// Loads the provided ``MTLSamplerState`` into the current pipeline's sampler array at the requested texture unit
/// number
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - samplerstate: Opaque pointer to ``MTLSamplerState`` instance shared with `libobs`
/// - unit: Number identifying the "texture slot" used by OBS Studio's renderer.
///
/// Texture slot numbers are equivalent to array index and represent a direct mapping between samplers and textures.
@_cdecl("device_load_samplerstate")
public func device_load_samplerstate(device: UnsafeRawPointer, samplerstate: UnsafeRawPointer, unit: UInt32) {
let device: MetalDevice = unretained(device)
let samplerState: MTLSamplerState = unretained(samplerstate)
device.renderState.samplers[Int(unit)] = samplerState
}

View File

@@ -0,0 +1,593 @@
/******************************************************************************
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 Foundation
import Metal
private typealias ParserError = MetalError.OBSShaderParserError
private typealias ShaderError = MetalError.OBSShaderError
private typealias MetalShaderError = MetalError.MetalShaderError
/// Creates a ``MetalShader`` instance from the given shader string for use as a vertex shader.
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - shader: C character pointer with the contents of the `libobs` effect file
/// - file: C character pointer with the contents of the `libobs` effect file location
/// - error_string: Pointer for another C character pointer with the contents of an error description
/// - Returns: Opaque pointer to a new ``MetalShader`` instance on success or `nil` on error
///
/// The string pointed to by the `data` argument is a re-compiled shader string created from the associated "effect"
/// file (which will contain multiple effects). Each effect is made up of several passes (though usually only a single
/// pass is defined), each of which contains a vertex and fragment shader. This function is then called with just the
/// vertex shader string.
///
/// This vertex shader string needs to be parsed again and transpiled into a Metal shader string, which is handled by
/// the ``OBSShader`` class. The transpiled string is then used to create the actual ``MetalShader`` instance.
@_cdecl("device_vertexshader_create")
public func device_vertexshader_create(
device: UnsafeRawPointer, shader: UnsafePointer<CChar>, file: UnsafePointer<CChar>,
error_string: UnsafeMutablePointer<UnsafeMutablePointer<CChar>>
) -> OpaquePointer? {
let device: MetalDevice = unretained(device)
let content = String(cString: shader)
let fileLocation = String(cString: file)
do {
let obsShader = try OBSShader(type: .vertex, content: content, fileLocation: fileLocation)
let transpiled = try obsShader.transpiled()
guard let metaData = obsShader.metaData else {
OBSLog(.error, "device_vertexshader_create: No required metadata found for transpiled shader")
return nil
}
let metalShader = try MetalShader(device: device, source: transpiled, type: .vertex, data: metaData)
return metalShader.getRetained()
} catch let error as ParserError {
switch error {
case .parseFail(let description):
OBSLog(.error, "device_vertexshader_create: Error parsing shader.\n\(description)")
default:
OBSLog(.error, "device_vertexshader_create: Error parsing shader.\n\(error.description)")
}
} catch let error as ShaderError {
switch error {
case .transpileError(let description):
OBSLog(.error, "device_vertexshader_create: Error transpiling shader.\n\(description)")
case .parseError(let description):
OBSLog(.error, "device_vertexshader_create: OBS parser error.\n\(description)")
case .parseFail(let description):
OBSLog(.error, "device_vertexshader_create: OBS parser failure.\n\(description)")
default:
OBSLog(.error, "device_vertexshader_create: OBS shader error.\n\(error.description)")
}
} catch {
switch error {
case let error as MetalShaderError:
OBSLog(.error, "device_vertexshader_create: Error compiling shader.\n\(error.description)")
case let error as MetalError.MTLDeviceError:
OBSLog(.error, "device_vertexshader_create: Device error compiling shader.\n\(error.description)")
default:
OBSLog(.error, "device_vertexshader_create: Unknown error occurred")
}
}
return nil
}
/// Creates a ``MetalShader`` instance from the given shader string for use as a fragment shader.
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - shader: C character pointer with the contents of the `libobs` effect file
/// - file: C character pointer with the contents of the `libobs` effect file location
/// - error_string: Pointer for another C character pointer with the contents of an error description
/// - Returns: Opaque pointer to a new ``MetalShader`` instance on success or `nil` on error
///
/// The string pointed to by the `data` argument is a re-compiled shader string created from the associated "effect"
/// file (which will contain multiple effects). Each effect is made up of several passes (though usually only a single
/// pass is defined), each of which contains a vertex and fragment shader. This function is then called with just the
/// vertex shader string.
///
/// This fragment shader string needs to be parsed again and transpiled into a Metal shader string, which is handled by
/// the ``OBSShader`` class. The transpiled string is then used to create the actual ``MetalShader`` instance.
@_cdecl("device_pixelshader_create")
public func device_pixelshader_create(
device: UnsafeRawPointer, shader: UnsafePointer<CChar>, file: UnsafePointer<CChar>,
error_string: UnsafeMutablePointer<UnsafeMutablePointer<CChar>>
) -> OpaquePointer? {
let device: MetalDevice = unretained(device)
let content = String(cString: shader)
let fileLocation = String(cString: file)
do {
let obsShader = try OBSShader(type: .fragment, content: content, fileLocation: fileLocation)
let transpiled = try obsShader.transpiled()
guard let metaData = obsShader.metaData else {
OBSLog(.error, "device_pixelshader_create: No required metadata found for transpiled shader")
return nil
}
let metalShader = try MetalShader(device: device, source: transpiled, type: .fragment, data: metaData)
return metalShader.getRetained()
} catch let error as ParserError {
switch error {
case .parseFail(let description):
OBSLog(.error, "device_vertexshader_create: Error parsing shader.\n\(description)")
default:
OBSLog(.error, "device_vertexshader_create: Error parsing shader.\n\(error.description)")
}
} catch let error as ShaderError {
switch error {
case .transpileError(let description):
OBSLog(.error, "device_vertexshader_create: Error transpiling shader.\n\(description)")
case .parseError(let description):
OBSLog(.error, "device_vertexshader_create: OBS parser error.\n\(description)")
case .parseFail(let description):
OBSLog(.error, "device_vertexshader_create: OBS parser failure.\n\(description)")
default:
OBSLog(.error, "device_vertexshader_create: OBS shader error.\n\(error.description)")
}
} catch {
switch error {
case let error as MetalShaderError:
OBSLog(.error, "device_vertexshader_create: Error compiling shader.\n\(error.description)")
case let error as MetalError.MTLDeviceError:
OBSLog(.error, "device_vertexshader_create: Device error compiling shader.\n\(error.description)")
default:
OBSLog(.error, "device_vertexshader_create: Unknown error occurred")
}
}
return nil
}
/// Loads the ``MetalShader`` instance for use as the vertex shader for the current render pipeline descriptor.
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - vertShader: Opaque pointer to ``MetalShader`` instance shared with `libobs`
///
/// This function will simply set up the ``MTLFunction`` wrapped by the ``MetalShader`` instance as the current
/// pipeline descriptor's `vertexFunction`. The Metal renderer will lazily create new render pipeline states for each
/// permutation of pipeline descriptors, which is a comparatively costly operation but will only occur once for any
/// such permutation.
///
/// > Note: If a `NULL` pointer is passed for the `vertShader` argument, the vertex function on the current render
/// pipeline descriptor will be _unset_.
///
@_cdecl("device_load_vertexshader")
public func device_load_vertexshader(device: UnsafeRawPointer, vertShader: UnsafeRawPointer?) {
let device: MetalDevice = unretained(device)
if let vertShader {
let shader: MetalShader = unretained(vertShader)
guard shader.type == .vertex else {
assertionFailure("device_load_vertexshader: Invalid shader type \(shader.type)")
return
}
device.renderState.vertexShader = shader
device.renderState.pipelineDescriptor.vertexFunction = shader.function
device.renderState.pipelineDescriptor.vertexDescriptor = shader.vertexDescriptor
} else {
device.renderState.vertexShader = nil
device.renderState.pipelineDescriptor.vertexFunction = nil
device.renderState.pipelineDescriptor.vertexDescriptor = nil
}
}
/// Loads the ``MetalShader`` instance for use as the fragment shader for the current render pipeline descriptor.
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - vertShader: Opaque pointer to ``MetalShader`` instance shared with `libobs`
///
/// This function will simply set up the ``MTLFunction`` wrapped by the ``MetalShader`` instance as the current
/// pipeline descriptor's `fragmentFunction`. The Metal renderer will lazily create new render pipeline states for
/// each permutation of pipeline descriptors, which is a comparatively costly operation but will only occur once for
/// any such permutation.
///
/// As any fragment function is potentially associated with a number of textures and associated sampler states, the
/// associated arrays are reset whenever a new fragment function is set up.
///
/// > Note: If a `NULL` pointer is passed for the `pixelShader` argument, the fragment function on the current render
/// pipeline descriptor will be _unset_.
///
@_cdecl("device_load_pixelshader")
public func device_load_pixelshader(device: UnsafeRawPointer, pixelShader: UnsafeRawPointer?) {
let device: MetalDevice = unretained(device)
for index in 0..<Int(GS_MAX_TEXTURES) {
device.renderState.textures[index] = nil
device.renderState.samplers[index] = nil
}
if let pixelShader {
let shader: MetalShader = unretained(pixelShader)
guard shader.type == .fragment else {
assertionFailure("device_load_pixelshader: Invalid shader type \(shader.type)")
return
}
device.renderState.fragmentShader = shader
device.renderState.pipelineDescriptor.fragmentFunction = shader.function
if let samplers = shader.samplers {
device.renderState.samplers.replaceSubrange(0..<samplers.count, with: samplers)
}
} else {
device.renderState.pipelineDescriptor.fragmentFunction = nil
}
}
/// Gets the ``MetalShader`` set up as the current vertex shader for the pipeline
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - Returns: Opaque pointer to ``MetalShader`` instance if a vertex shader is currently set up or `nil` otherwise
@_cdecl("device_get_vertex_shader")
public func device_get_vertex_shader(device: UnsafeRawPointer) -> OpaquePointer? {
let device: MetalDevice = unretained(device)
if let shader = device.renderState.vertexShader {
return shader.getUnretained()
} else {
return nil
}
}
/// Gets the ``MetalShader`` set up as the current fragment shader for the pipeline
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - Returns: Opaque pointer to ``MetalShader`` instance if a fragment shader is currently set up or `nil` otherwise
@_cdecl("device_get_pixel_shader")
public func device_get_pixel_shader(device: UnsafeRawPointer) -> OpaquePointer? {
let device: MetalDevice = unretained(device)
if let shader = device.renderState.fragmentShader {
return shader.getUnretained()
} else {
return nil
}
}
/// Requests the deinitialization of the ``MetalShader`` instance shared with `libobs`
/// - Parameter shader: Opaque pointer to ``MetalShader`` instance shared with `libobs`
///
/// Ownership of the ``MetalShader`` instance will be transferred into the function and if this was the last strong
/// reference to it, the object will be automatically deinitialized and deallocated by Swift.
@_cdecl("gs_shader_destroy")
public func gs_shader_destroy(shader: UnsafeRawPointer) {
let _ = retained(shader) as MetalShader
}
/// Gets the number of uniform parameters used on the ``MetalShader``
/// - Parameter shader: Opaque pointer to ``MetalShader`` instance shared with `libobs`
/// - Returns: Number of uniforms
@_cdecl("gs_shader_get_num_params")
public func gs_shader_get_num_params(shader: UnsafeRawPointer) -> UInt32 {
let shader: MetalShader = unretained(shader)
return UInt32(shader.uniforms.count)
}
/// Gets a uniform parameter from the ``MetalShader`` by its array index
/// - Parameters:
/// - shader: Opaque pointer to ``MetalShader`` instance shared with `libobs`
/// - param: Array index of uniform parameter to get
/// - Returns: Opaque pointer to a ``ShaderUniform`` instance if index within uniform array bounds or `nil` otherwise
///
/// This function requires that the array indices of the uniforms array do not change for a ``MetalShader`` and also
/// that the exact order of uniforms is identical between `libobs`'s interpretation of the effects file and the
/// transpiled shader's analysis of the uniforms.
///
/// > Important: The opaque pointer for the ``ShaderUniform`` instance is passe unretained and as such can become
/// invalid when its owning ``MetalShader`` instance either is deinitialized itself or is replaced in the uniforms
/// array.
@_cdecl("gs_shader_get_param_by_idx")
public func gs_shader_get_param_by_idx(shader: UnsafeRawPointer, param: UInt32) -> OpaquePointer? {
let shader: MetalShader = unretained(shader)
guard param < shader.uniforms.count else {
return nil
}
let uniform = shader.uniforms[Int(param)]
let unretained = Unmanaged.passUnretained(uniform).toOpaque()
return OpaquePointer(unretained)
}
/// Gets a uniform parameter from the ``MetalShader`` by its name
/// - Parameters:
/// - shader: Opaque pointer to ``MetalShader`` instance shared with `libobs`
/// - param: C character array pointer with the name of the requested uniform parameter
/// - Returns: Opaque pointer to a ``ShaderUniform`` instance if any uniform with the provided name was found or `nil`
/// otherwise
///
/// > Important: The opaque pointer for the ``ShaderUniform`` instance is passe unretained and as such can become
/// invalid when its owning ``MetalShader`` instance either is deinitialized itself or is replaced in the uniforms
/// array.
///
@_cdecl("gs_shader_get_param_by_name")
public func gs_shader_get_param_by_name(shader: UnsafeRawPointer, param: UnsafeMutablePointer<CChar>) -> OpaquePointer?
{
let shader: MetalShader = unretained(shader)
let paramName = String(cString: param)
for uniform in shader.uniforms {
if uniform.name == paramName {
let unretained = Unmanaged.passUnretained(uniform).toOpaque()
return OpaquePointer(unretained)
}
}
return nil
}
/// Gets the uniform parameter associated with the view projection matrix used by the ``MetalShader``
/// - Parameter shader: Opaque pointer to ``MetalShader`` instance shared with `libobs`
/// - Returns: Opaque pointer to a ``ShaderUniform`` instance if a uniform for the view projection matrix was found
/// or `nil` otherwise
///
/// The uniform for the view projection matrix has the associated name `viewProj` in the Metal renderer, thus a
/// name-based lookup is used to find the associated ``ShaderUniform`` instance.
///
/// > Important: The opaque pointer for the ``ShaderUniform`` instance is passe unretained and as such can become
/// invalid when its owning ``MetalShader`` instance either is deinitialized itself or is replaced in the uniforms
/// array.
///
@_cdecl("gs_shader_get_viewproj_matrix")
public func gs_shader_get_viewproj_matrix(shader: UnsafeRawPointer) -> OpaquePointer? {
let shader: MetalShader = unretained(shader)
let paramName = "viewProj"
for uniform in shader.uniforms {
if uniform.name == paramName {
let unretained = Unmanaged.passUnretained(uniform).toOpaque()
return OpaquePointer(unretained)
}
}
return nil
}
/// Gets the uniform parameter associated with the world projection matrix used by the ``MetalShader``
/// - Parameter shader: Opaque pointer to ``MetalShader`` instance shared with `libobs`
/// - Returns: Opaque pointer to a ``ShaderUniform`` instance if a uniform for the world projection matrix was found
/// or `nil` otherwise
///
/// The uniform for the view projection matrix has the associated name `worldProj` in the Metal renderer, thus a
/// name-based lookup is used to find the associated ``ShaderUniform`` instance.
///
/// > Important: The opaque pointer for the ``ShaderUniform`` instance is passe unretained and as such can become
/// invalid when its owning ``MetalShader`` instance either is deinitialized itself or is replaced in the uniforms
/// array.
@_cdecl("gs_shader_get_world_matrix")
public func gs_shader_get_world_matrix(shader: UnsafeRawPointer) -> OpaquePointer? {
let shader: MetalShader = unretained(shader)
let paramName = "worldProj"
for uniform in shader.uniforms {
if uniform.name == paramName {
let unretained = Unmanaged.passUnretained(uniform).toOpaque()
return OpaquePointer(unretained)
}
}
return nil
}
/// Gets the name and uniform type from the ``ShaderUniform`` instance
/// - Parameters:
/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs`
/// - info: Pointer to a `gs_shader_param_info` struct pre-allocated by `libobs`
///
/// > Warning: The C character array pointer holding the name of the uniform is managed by Swift and might become
/// invalid at any point in time.
@_cdecl("gs_shader_get_param_info")
public func gs_shader_get_param_info(shaderParam: UnsafeRawPointer, info: UnsafeMutablePointer<gs_shader_param_info>) {
let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam)
shaderUniform.name.withCString {
info.pointee.name = $0
}
info.pointee.type = shaderUniform.gsType
}
/// Sets a boolean value on the ``ShaderUniform`` instance
/// - Parameters:
/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs`
/// - val: Boolean value to set for the uniform
@_cdecl("gs_shader_set_bool")
public func gs_shader_set_bool(shaderParam: UnsafeRawPointer, val: Bool) {
let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam)
withUnsafePointer(to: val) {
shaderUniform.setParameter(data: $0, size: MemoryLayout<Int32>.size)
}
}
/// Sets a 32-bit floating point value on the ``ShaderUniform`` instance
/// - Parameters:
/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs`
/// - val: 32-bit floating point value to set for the uniform
@_cdecl("gs_shader_set_float")
public func gs_shader_set_float(shaderParam: UnsafeRawPointer, val: Float32) {
let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam)
withUnsafePointer(to: val) {
shaderUniform.setParameter(data: $0, size: MemoryLayout<Float32>.size)
}
}
/// Sets a 32-bit signed integer value on the ``ShaderUniform`` instance
/// - Parameters:
/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs`
/// - val: 32-bit signed integer value to set for the uniform
@_cdecl("gs_shader_set_int")
public func gs_shader_set_int(shaderParam: UnsafeRawPointer, val: Int32) {
let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam)
withUnsafePointer(to: val) {
shaderUniform.setParameter(data: $0, size: MemoryLayout<Int32>.size)
}
}
/// Sets a 3x3 matrix of 32-bit floating point values on the ``ShaderUniform``instance
/// - Parameters:
/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs`
/// - val: A 3x3 matrix of 32-bit floating point values
///
/// The 3x3 matrix is converted into a 4x4 matrix (padded with zeros) before actually being set as the uniform data
@_cdecl("gs_shader_set_matrix3")
public func gs_shader_set_matrix3(shaderParam: UnsafeRawPointer, val: UnsafePointer<matrix3>) {
let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam)
var newMatrix = matrix4()
matrix4_from_matrix3(&newMatrix, val)
shaderUniform.setParameter(data: &newMatrix, size: MemoryLayout<matrix4>.size)
}
/// Sets a 4x4 matrix of 32-bit floating point values on the ``ShaderUniform`` instance
/// - Parameters:
/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs`
/// - val: A 4x4 matrix of 32-bit floating point values
@_cdecl("gs_shader_set_matrix4")
public func gs_shader_set_matrix4(shaderParam: UnsafeRawPointer, val: UnsafePointer<matrix4>) {
let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam)
shaderUniform.setParameter(data: val, size: MemoryLayout<matrix4>.size)
}
/// Sets a vector of 2 32-bit floating point values on the ``ShaderUniform`` instance
/// - Parameters:
/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs`
/// - val: A vector of 2 32-bit floating point values
@_cdecl("gs_shader_set_vec2")
public func gs_shader_set_vec2(shaderParam: UnsafeRawPointer, val: UnsafePointer<vec2>) {
let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam)
shaderUniform.setParameter(data: val, size: MemoryLayout<vec2>.size)
}
/// Sets a vector of 3 32-bit floating point values on the ``ShaderUniform`` instance
/// - Parameters:
/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs`
/// - val: A vector of 3 32-bit floating point values
@_cdecl("gs_shader_set_vec3")
public func gs_shader_set_vec3(shaderParam: UnsafeRawPointer, val: UnsafePointer<vec3>) {
let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam)
shaderUniform.setParameter(data: val, size: MemoryLayout<vec3>.size)
}
/// Sets a vector of 4 32-bit floating point values on the ``ShaderUniform`` instance
/// - Parameters:
/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs`
/// - val: A vector of 4 32-bit floating point values
@_cdecl("gs_shader_set_vec4")
public func gs_shader_set_vec4(shaderParam: UnsafeRawPointer, val: UnsafePointer<vec4>) {
let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam)
shaderUniform.setParameter(data: val, size: MemoryLayout<vec4>.size)
}
/// Sets up the data of a `gs_shader_texture` struct as a uniform on the ``ShaderUniform`` instance
/// - Parameters:
/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs`
/// - val: A pointer to a `gs_shader_struct` containing an opaque pointer to the actual ``MetalTexture`` instance
/// and an sRGB gamma state flag
///
/// The struct's data is copied verbatim into the uniform, which allows reconstruction of the pointer at a later point
/// as long as the actual ``MetalTexture`` instance still exists.
@_cdecl("gs_shader_set_texture")
public func gs_shader_set_texture(shaderParam: UnsafeRawPointer, val: UnsafePointer<gs_shader_texture>?) {
let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam)
if let val {
shaderUniform.setParameter(data: val, size: MemoryLayout<gs_shader_texture>.size)
}
}
/// Sets an arbitrary value on the ``ShaderUniform`` instance
/// - Parameters:
/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs`
/// - val: Opaque pointer to some unknown data for use as the uniform
/// - size: The size of the data available at the memory pointed to by the `val` argument
///
/// The ``ShaderUniform`` itself is set up to hold a specific uniform type, each of which is associated with a size of
/// bytes required for it. If the size of the data pointed to by `val` does not fit into this size, the uniform will
/// not be updated.
///
/// If the ``ShaderUniform`` expects a texture parameter, the pointer will be bound as memory of a `gs_shader_texture`
/// instance before setting it up.
@_cdecl("gs_shader_set_val")
public func gs_shader_set_val(shaderParam: UnsafeRawPointer, val: UnsafeRawPointer, size: UInt32) {
let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam)
let size = Int(size)
let valueSize = shaderUniform.gsType.size
guard valueSize == size else {
assertionFailure("gs_shader_set_val: Required size of uniform does not match size of input")
return
}
if shaderUniform.gsType == GS_SHADER_PARAM_TEXTURE {
let shaderTexture = val.bindMemory(to: gs_shader_texture.self, capacity: 1)
shaderUniform.setParameter(data: shaderTexture, size: valueSize)
} else {
let bytes = val.bindMemory(to: UInt8.self, capacity: valueSize)
shaderUniform.setParameter(data: bytes, size: valueSize)
}
}
/// Resets the ``ShaderUniform``'s current data with its default data
/// - Parameter shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs`
///
/// Each ``ShaderUniform`` is optionally set up with a set of default data (stored as an array of bytes) which is
/// simply copied into the current values.
@_cdecl("gs_shader_set_default")
public func gs_shader_set_default(shaderParam: UnsafeRawPointer) {
let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam)
if let defaultValues = shaderUniform.defaultValues {
shaderUniform.currentValues = Array(defaultValues)
}
}
/// Sets up the ``MTLSamplerState`` as the sampler state for the ``ShaderUniform``
/// - Parameters:
/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs`
/// - sampler: Opaque pointer to ``MTLSamplerState`` instance shared with `libobs`
///
/// If the uniform represents a texture for use in the associated shader, this function will also set up the provided
/// ``MTLSamplerState`` for the associated texture's texture slot.
@_cdecl("gs_shader_set_next_sampler")
public func gs_shader_set_next_sampler(shaderParam: UnsafeRawPointer, sampler: UnsafeRawPointer) {
let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam)
let samplerState = Unmanaged<MTLSamplerState>.fromOpaque(sampler).takeUnretainedValue()
shaderUniform.samplerState = samplerState
}

View File

@@ -0,0 +1,130 @@
/******************************************************************************
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 Foundation
import Metal
/// Creates a ``MetalStageBuffer`` instance for use as a stage surface by `libobs`
/// - Parameters:
/// - device: Opaque pointer to ``MetalStageBuffer`` instance shared with `libobs`
/// - width: Number of data rows
/// - height: Number of data columns
/// - format: Color format of the stage surface texture as defined by `libobs`'s `gs_color_format` struct
/// - Returns: A ``MetalStageBuffer`` instance that wraps a `MTLBuffer` or a `nil` pointer otherwise
///
/// Stage surfaces are used by `libobs` for transfer of image data from the GPU to the CPU. The most common use case is
/// to block transfer (blit) the video output texture into a staging texture and then downloading the texture data from
/// the staging texture into CPU memory.
@_cdecl("device_stagesurface_create")
public func device_stagesurface_create(device: UnsafeRawPointer, width: UInt32, height: UInt32, format: gs_color_format)
-> OpaquePointer?
{
let device: MetalDevice = unretained(device)
guard
let buffer = MetalStageBuffer(
device: device,
width: Int(width),
height: Int(height),
format: format.mtlFormat
)
else {
OBSLog(.error, "device_stagesurface_create: Unable to create MetalStageBuffer with provided format \(format)")
return nil
}
return buffer.getRetained()
}
/// Requests the deinitialization of the ``MetalStageBuffer`` instance that was shared with `libobs`
/// - Parameter stagesurf: Opaque pointer to ``MetalStageBuffer`` instance shared with `libobs`
///
/// The ownership of the shared pointer is transferred into this function and the instance is placed under Swift's
/// memory management again.
@_cdecl("gs_stagesurface_destroy")
public func gs_stagesurface_destroy(stagesurf: UnsafeRawPointer) {
let _ = retained(stagesurf) as MetalStageBuffer
}
/// Gets the "width" of the staging texture
/// - Parameter stagesurf: Opaque pointer to ``MetalStageBuffer`` instance shared with `libobs`
/// - Returns: Amount of data rows in the buffer representing the width of an image
@_cdecl("gs_stagesurface_get_width")
public func gs_stagesurface_get_width(stagesurf: UnsafeRawPointer) -> UInt32 {
let stageSurface: MetalStageBuffer = unretained(stagesurf)
return UInt32(stageSurface.width)
}
/// Gets the "height" of the staging texture
/// - Parameter stagesurf: Opaque pointer to ``MetalStageBuffer`` instance shared with `libobs`
/// - Returns: Amount of data columns in the buffer representing the height of an image
@_cdecl("gs_stagesurface_get_height")
public func gs_stagesurface_get_height(stagesurf: UnsafeRawPointer) -> UInt32 {
let stageSurface: MetalStageBuffer = unretained(stagesurf)
return UInt32(stageSurface.height)
}
/// Gets the color format of the staged image data
/// - Parameter stagesurf: Opaque pointer to ``MetalStageBuffer`` instance shared with `libobs`
/// - Returns: Color format in `libobs`'s own color format struct
///
/// The Metal color format is automatically converted into its corresponding `gs_color_format` variant.
@_cdecl("gs_stagesurface_get_color_format")
public func gs_stagesurface_get_height(stagesurf: UnsafeRawPointer) -> gs_color_format {
let stageSurface: MetalStageBuffer = unretained(stagesurf)
return stageSurface.format.gsColorFormat
}
/// Provides a pointer to memory that contains the buffer's raw data.
/// - Parameters:
/// - stagesurf: Opaque pointer to ``MetalStageBuffer`` instance shared with `libobs`
/// - ptr: Opaque pointer to memory which itself can hold a pointer to the actual image data
/// - linesize: Opaque pointer to memory which itself can hold the row size of the image data
/// - Returns: `true` if the data can be provided, `false` otherwise
///
/// Metal does not provide "map" and "unmap" operations as they exist in Direct3D11, as resource management and
/// synchronization needs to be handled explicitly by the application. To reduce unnecessary copy operations, the
/// original texture's data was copied into a `MTLBuffer` (instead of another texture) using a block transfer on the
/// GPU.
///
/// As the Metal renderer is only available on Apple Silicon machines, this means that the buffer itself is available
/// for direct access by the CPU and thus a pointer to the raw bytes of the buffer can be shared with `libobs`.
@_cdecl("gs_stagesurface_map")
public func gs_stagesurface_map(
stagesurf: UnsafeRawPointer, ptr: UnsafeMutablePointer<UnsafeMutableRawPointer>,
linesize: UnsafeMutablePointer<UInt32>
) -> Bool {
let stageSurface: MetalStageBuffer = unretained(stagesurf)
ptr.pointee = stageSurface.buffer.contents()
linesize.pointee = UInt32(stageSurface.width * stageSurface.format.bytesPerPixel!)
return true
}
/// Signals that the downloaded image data of the stage texture is not needed anymore.
///
/// - Parameter stagesurf: Opaque pointer to ``MetalStageBuffer`` instance shared with `libobs`
///
/// This function has no effect as the `MTLBuffer` used by the ``MetalStageBuffer`` does not need to be "unmapped".
@_cdecl("gs_stagesurface_unmap")
public func gs_stagesurface_unmap(stagesurf: UnsafeRawPointer) {
return
}

View File

@@ -0,0 +1,985 @@
/******************************************************************************
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 Foundation
import Metal
import simd
@inlinable
public func unretained<Instance>(_ pointer: UnsafeRawPointer) -> Instance where Instance: AnyObject {
Unmanaged<Instance>.fromOpaque(pointer).takeUnretainedValue()
}
@inlinable
public func retained<Instance>(_ pointer: UnsafeRawPointer) -> Instance where Instance: AnyObject {
Unmanaged<Instance>.fromOpaque(pointer).takeRetainedValue()
}
@inlinable
public func OBSLog(_ level: OBSLogLevel, _ format: String, _ args: CVarArg...) {
let logMessage = String.localizedStringWithFormat(format, args)
logMessage.withCString { cMessage in
withVaList([cMessage]) { arguments in
blogva(level.rawValue, "%s", arguments)
}
}
}
/// Returns the graphics API name implemented by the "device".
/// - Returns: Constant pointer to a C string with the API name
///
@_cdecl("device_get_name")
public func device_get_name() -> UnsafePointer<CChar> {
return device_name
}
/// Gets the graphics API identifier number for the "device".
/// - Returns: Numerical identifier
///
@_cdecl("device_get_type")
public func device_get_type() -> Int32 {
return GS_DEVICE_METAL
}
/// Returns a string to be used as a suffix for libobs' shader preprocessor, which will be used as part of a shaders
/// identifying information.
/// - Returns: Constant pointer to a C string with the suffix text
@_cdecl("device_preprocessor_name")
public func device_preprocessor_name() -> UnsafePointer<CChar> {
return preprocessor_name
}
/// Creates a new Metal device instance and stores an opaque pointer to a ``MetalDevice`` instance in the provided
/// pointer.
///
/// - Parameters:
/// - devicePointer: Pointer to memory allocated by the caller to receive the pointer of the create device instance
/// - adapter: Numerical identifier of a graphics display adaptor to create the device on.
/// - Returns: Device creation result value defined as preprocessor macro in libobs' graphics API header
///
/// This method will increment the reference count on the created ``MetalDevice`` instance to ensure it will not be
/// deallocated until `libobs` actively relinquishes ownership of it via a call of `device_destroy`.
///
/// > Important: As the Metal API is only supported on Apple Silicon devices, the adapter argument is effectively
/// ignored (there is only ever one "adapter" in an Apple Silicon machine and thus only the "default" device is used.
@_cdecl("device_create")
public func device_create(devicePointer: UnsafeMutableRawPointer, adapter: UInt32) -> Int32 {
guard NSProtocolFromString("MTLDevice") != nil else {
OBSLog(.error, "This Mac does not support Metal.")
return GS_ERROR_NOT_SUPPORTED
}
OBSLog(.info, "---------------------------------")
guard let metalDevice = MTLCreateSystemDefaultDevice() else {
OBSLog(.error, "Unable to initialize Metal device.")
return GS_ERROR_FAIL
}
var descriptions: [String] = []
descriptions.append("Initializing Metal...")
descriptions.append("\t- Name : \(metalDevice.name)")
descriptions.append("\t- Unified Memory : \(metalDevice.hasUnifiedMemory ? "Yes" : "No")")
descriptions.append("\t- Raytracing Support : \(metalDevice.supportsRaytracing ? "Yes" : "No")")
if #available(macOS 14.0, *) {
descriptions.append("\t- Architecture : \(metalDevice.architecture.name)")
}
OBSLog(.info, descriptions.joined(separator: "\n"))
do {
let device = try MetalDevice(device: metalDevice)
let retained = Unmanaged.passRetained(device).toOpaque()
let signalName = MetalSignalType.videoReset.rawValue
let signalHandler = obs_get_signal_handler()
signalName.withCString {
signal_handler_connect(signalHandler, $0, metal_video_reset_handler, retained)
}
devicePointer.storeBytes(of: OpaquePointer(retained), as: OpaquePointer.self)
} catch {
OBSLog(.error, "Unable to create MetalDevice wrapper instance")
return GS_ERROR_FAIL
}
return GS_SUCCESS
}
/// Uninitializes the Metal device instance created for libobs.
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
///
/// This method will take ownership of the reference shared with `libobs` and thus return all strong references to the
/// shared ``MetalDevice`` instance to pure Swift code (and thus its own memory managed). The active call to
/// ``MetalDevice/shutdown()`` is necessary to ensure that internal clean up code runs _before_ `libobs` runs any of
/// its own clean up code (which is not memory safe).
@_cdecl("device_destroy")
public func device_destroy(device: UnsafeMutableRawPointer) {
let signalName = MetalSignalType.videoReset.rawValue
let signalHandler = obs_get_signal_handler()
signalName.withCString {
signal_handler_disconnect(signalHandler, $0, metal_video_reset_handler, device)
}
let device: MetalDevice = retained(device)
device.shutdown()
}
/// Returns opaque pointer to actual (wrapped) API-specific device object
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - Returns: Opaque pointer to ``MTLDevice`` object wrapped by ``MetalDevice`` instance
///
/// The pointer shared by this function is unretained and is thus unsafe. It doesn't seem that anything in OBS Studio's
/// codebase actually uses this function, but it is part of the graphics API and thus has to be implemented.
@_cdecl("device_get_device_obj")
public func device_get_device_obj(device: UnsafeMutableRawPointer) -> OpaquePointer? {
let metalDevice: MetalDevice = unretained(device)
let mtlDevice = metalDevice.device
return OpaquePointer(Unmanaged.passUnretained(mtlDevice).toOpaque())
}
/// Sets up the blend factor to be used by the current pipeline.
///
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - src: `libobs` blend type for the source
/// - dest: `libobs` blend type for the destination
///
/// This function uses the same blend factor for color and alpha channel. The enum values provided by `libobs` are
/// converted into their appropriate ``MTLBlendFactor``variants automatically (if possible).
///
/// > Important: Calling this function can trigger the creation of an entirely new render pipeline state, which is a
/// costly operation.
@_cdecl("device_blend_function")
public func device_blend_function(device: UnsafeRawPointer, src: gs_blend_type, dest: gs_blend_type) {
device_blend_function_separate(
device: device,
src_c: src,
dest_c: dest,
src_a: src,
dest_a: dest
)
}
/// Sets up the color and alpha blend factors to be used by the current pipeline
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - src_c: `libobs` blend factor for the source color
/// - dest_c: `libobs` blend factor for the destination color
/// - src_a: `libobs` blend factor for the source alpha channel
/// - dest_a: `libobs` blend factor for the destination alpha channel
///
/// This function uses different blend factors for color and alpha channel. The enum values provided by `libobs` are
/// converted into their appropriate ``MTLBlendFactor`` variants automatically (if possible).
///
/// > Important: Calling this function can trigger the creation of an entirely new render pipeline state, which is a
/// costly operation.
@_cdecl("device_blend_function_separate")
public func device_blend_function_separate(
device: UnsafeRawPointer, src_c: gs_blend_type, dest_c: gs_blend_type, src_a: gs_blend_type, dest_a: gs_blend_type
) {
let device: MetalDevice = unretained(device)
let pipelineDescriptor = device.renderState.pipelineDescriptor
guard let sourceRGBFactor = src_c.blendFactor,
let sourceAlphaFactor = src_a.blendFactor,
let destinationRGBFactor = dest_c.blendFactor,
let destinationAlphaFactor = dest_a.blendFactor
else {
assertionFailure(
"""
device_blend_function_separate: Incompatible blend factors used. Values:
- Source RGB : \(src_c)
- Source Alpha : \(src_a)
- Destination RGB : \(dest_c)
- Destination Alpha : \(dest_a)
""")
return
}
pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = sourceRGBFactor
pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = sourceAlphaFactor
pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = destinationRGBFactor
pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = destinationAlphaFactor
}
/// Sets the blend operation to be used by the current pipeline.
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - op: `libobs` blend operation name
///
/// This function converts the provided `libobs` value into its appropriate ``MTLBlendOperation`` variant automatically
/// (if possible).
///
/// > Important: Calling this function can trigger the creation of an entirely new render pipeline state, which is a
/// costly operation.
@_cdecl("device_blend_op")
public func device_blend_op(device: UnsafeRawPointer, op: gs_blend_op_type) {
let device: MetalDevice = unretained(device)
let pipelineDescriptor = device.renderState.pipelineDescriptor
guard let blendOperation = op.mtlOperation else {
assertionFailure("device_blend_op: Incompatible blend operation provided. Value: \(op)")
return
}
pipelineDescriptor.colorAttachments[0].rgbBlendOperation = blendOperation
}
/// Returns the _current_ color space as set up by any preceding calls of the `libobs` renderer.
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - Returns: Color space enum value as defined by `libobs`
///
/// This color space value is commonly set by `libobs`' renderer to check the "current state", and make necessary
/// switches to ensure color-correct rendering
/// (e.g., to check if the renderer uses an SDR color space but the current source might provide HDR image data). This
/// value is effectively just retained as a state variable for `libobs`.
@_cdecl("device_get_color_space")
public func device_get_color_space(device: UnsafeRawPointer) -> gs_color_space {
let device: MetalDevice = unretained(device)
return device.renderState.gsColorSpace
}
/// Signals the beginning of a new render loop iteration by `libobs` renderer.
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
///
/// This function is the first graphics API-specific function called by `libobs` render loop and can be used as a
/// signal to reset any lingering state of the prior loop iteration.
///
/// For the Metal renderer this ensures that the current render target, current swap chain, as well as the list of
/// active swap chains is reset. As the Metal renderer also needs to keep track of whether `libobs` is rendering any
/// "displays", the associated state variable is also reset here.
@_cdecl("device_begin_frame")
public func device_begin_frame(device: UnsafeRawPointer) {
let device: MetalDevice = unretained(device)
device.renderState.useSRGBGamma = false
device.renderState.renderTarget = nil
device.renderState.swapChain = nil
device.renderState.isInDisplaysRenderStage = false
return
}
/// Gets a pointer to the current render target
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - Returns: Opaque pointer to ``MetalTexture`` object representing the render target
///
/// OBS Studio's renderer only ever uses a single render target at the same time and switches them out if it needs to
/// render a different output. Due to this single state approach, it needs to retain any "current" values before
/// replacing them with (temporary) new values. It does so by retrieving pointers to the current objects set up within
/// the graphics API's opaque implementation and storing them for later use.
@_cdecl("device_get_render_target")
public func device_get_render_target(device: UnsafeRawPointer) -> OpaquePointer? {
let device: MetalDevice = unretained(device)
guard let renderTarget = device.renderState.renderTarget else {
return nil
}
return renderTarget.getUnretained()
}
/// Replaces the "current" render target and zstencil attachment with the objects associated by any provided non-`nil`
/// pointers.
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - tex: Opaque (optional) pointer to ``MetalTexture`` instance shared with `libobs`
/// - zstencil: Opaque (optional) pointer to ``MetalTexture`` instance shared with `libobs`
///
/// This setter function is often used in conjunction with its associated getter function to temporarily "switch state"
/// of the renderer by retaining a pointer to the "current" render target, setting up a new one, issuing a draw call,
/// before restoring the original render target.
///
/// This is regularly used for "texrender" instances, such as combining the chroma and luma components of a video frame
/// (and uploaded as single- and dual-channel textures respectively) back into an RGB texture. This texture is then
/// used as the "output" of its corresponding source in the "actual" render pass, which will use the original render
/// target again.
@_cdecl("device_set_render_target")
public func device_set_render_target(device: UnsafeRawPointer, tex: UnsafeRawPointer?, zstencil: UnsafeRawPointer?) {
device_set_render_target_with_color_space(
device: device,
tex: tex,
zstencil: zstencil,
space: GS_CS_SRGB
)
}
/// Replaces the "current" render target and zstencil attachment with the objects associated by any provided non-`nil`
/// pointers and also updated the "current" color space used by the renderer.
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - tex: Opaque (optional) pointer to ``MetalTexture`` instance shared with `libobs`
/// - zstencil: Opaque (optional) pointer to ``MetalTexture`` instance shared with `libobs`
/// - space: `libobs`-based color space value
///
/// This setter function is often used in conjunction with its associated getter function to temporarily "switch state"
/// of the renderer by retaining a pointer to the "current" render target, setting up a new one, issuing a draw call,
/// before restoring the original render target.
///
/// This is regularly used for "texrender" instances, such as combining the chroma and luma components of a video frame
/// (and uploaded as single- and dual-channel textures respectively) back into an RGB texture. This texture is then
/// used as the "output" of its corresponding source in the "actual" render pass, which will use the original render
/// target again.
///
/// A `nil` pointer provided for either the render target or zstencil attachment means that the "current" value for
/// either should be removed, leaving the renderer in an "invalid" state at least for the render target (using no
/// zstencil attachment is a valid state however).
///
/// > Important: Use this variant if you need to also update the "current" color space which might be checked by
/// sources' render function to check whether linear gamma or sRGB's gamma will be used to encode color values.
@_cdecl("device_set_render_target_with_color_space")
public func device_set_render_target_with_color_space(
device: UnsafeRawPointer, tex: UnsafeRawPointer?, zstencil: UnsafeRawPointer?, space: gs_color_space
) {
let device: MetalDevice = unretained(device)
if let tex {
let metalTexture: MetalTexture = unretained(tex)
device.renderState.renderTarget = metalTexture
device.renderState.isRendertargetChanged = true
} else {
device.renderState.renderTarget = nil
}
if let zstencil {
let zstencilAttachment: MetalTexture = unretained(zstencil)
device.renderState.depthStencilAttachment = zstencilAttachment
device.renderState.isRendertargetChanged = true
} else {
device.renderState.depthStencilAttachment = nil
}
device.renderState.gsColorSpace = space
}
/// Switches the current render state to use sRGB gamma encoding and decoding when reading from textures and writing
/// into render targets
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - enable: Boolean to enable or disable the automatic sRGB gamma encoding and decoding
///
/// OBS Studio's renderer has been retroactively updated to use sRGB color primaries _and_ gamma encoding by
/// preference, but not by default. Any source has to opt-in to the use of automatic sRGB gamma encoding and decoding,
/// while the default is still to use linear gamma.
///
/// This method is thus used by sources to enable or disable the associated behavior and control the way color values
/// generated by fragment shaders are written into the render target.
@_cdecl("device_enable_framebuffer_srgb")
public func device_enable_framebuffer_srgb(device: UnsafeRawPointer, enable: Bool) {
let device: MetalDevice = unretained(device)
if device.renderState.useSRGBGamma != enable {
device.renderState.useSRGBGamma = enable
device.renderState.isRendertargetChanged = true
}
}
/// Retrieves the current render state's setting for using automatic encoding and decoding of color values using sRGB
/// gamma.
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - Returns: Boolean value of the sRGB gamma setting
///
/// This function is used to check the current state which might have possibly been explicitly changed by calls of
/// ``device_enable_framebuffer_srgb``.
///
/// A source which might only be able to work with color values that have sRGB gamma already applied to them and thus
/// might want to ensure that the color values provided by the fragment shader will not have the sRGB gamma curve
/// encoded on them again.
///
/// By calling this function, a source can check if automatic gamma encoding is enabled and then turn it off
/// explicitly, which will ensure that color data is written as-is and no additional encoding will take place.
@_cdecl("device_framebuffer_srgb_enabled")
public func device_framebuffer_srgb_enabled(device: UnsafeRawPointer) -> Bool {
let device: MetalDevice = unretained(device)
return device.renderState.useSRGBGamma
}
/// Signals the beginning of a new scene.
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
///
/// OBS Studio's renderer signals a new scene for each "display" and for every "video mix", which implicitly signals a
/// change of output format. This usually also implies that all current textures that might have been set up for
/// fragment shaders should be reset. For Metal this also requires creating a new "current" command buffer which should
/// contain all GPU commands necessary to render the "scene".
@_cdecl("device_begin_scene")
public func device_begin_scene(device: UnsafeMutableRawPointer) {
let device: MetalDevice = unretained(device)
for index in 0..<GS_MAX_TEXTURES {
device.renderState.textures[Int(index)] = nil
device.renderState.samplers[Int(index)] = nil
}
}
/// Signals the end of a scene.
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
///
/// OBS Studio's renderer signals the end of a scene for each "display" and for every "video mix", which implicitly
/// marks the end of the output at a different format. As the Metal renderer needs a way to detect if all draw commands
/// for a given "display" have ended (and there is no bespoke signal for that in the API), it uses an internal state
/// variable to track if a display had been loaded for the "current" pipeline state and resets it at the "end of scene"
/// signal.
@_cdecl("device_end_scene")
public func device_end_scene(device: UnsafeRawPointer) {
let device: MetalDevice = unretained(device)
if device.renderState.isInDisplaysRenderStage {
device.finishDisplayRenderStage()
device.renderState.isInDisplaysRenderStage = false
}
}
/// Schedules a draw command on the GPU using all "state" variables set up by OBS Studio's renderer up to this point.
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - drawMode: Primitive type to draw as specified by `libobs`
/// - startVertex: Start index of vertex to begin drawing with
/// - numVertices: Count of vertices to draw
///
/// Due to OBS Studio's design this function will usually render only a very low amount of vertices (commonly only 4
/// of them) and very often those vertices are already loaded up as vertex buffers for use by the vertex shader. In
/// those cases `libobs` does not seem to provide a vertex count and implicitly expects the graphics API implementation
/// to deduct the vertex count from the amount of vertices available in its vertex data struct.
///
/// In other cases a vertex shader will not use any buffers but calculate the vertex positions based on vertex ID and
/// a non-null vertex count has to be provided.
@_cdecl("device_draw")
public func device_draw(device: UnsafeRawPointer, drawMode: gs_draw_mode, startVertex: UInt32, numVertices: UInt32) {
let device: MetalDevice = unretained(device)
guard let primitiveType = drawMode.mtlPrimitive else {
OBSLog(.error, "device_draw: Unsupported draw mode provided: \(drawMode)")
return
}
do {
try device.draw(primitiveType: primitiveType, vertexStart: Int(startVertex), vertexCount: Int(numVertices))
} catch let error as MetalError.MTLDeviceError {
OBSLog(.error, "device_draw: \(error.description)")
} catch {
OBSLog(.error, "device_draw: Unknown error occurred")
}
}
/// Sets up a load action for the "current" frame buffer and depth stencil attachment to simulate the "clear" behavior
/// of other graphics APIs.
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - clearFlags: Bit field provided by `libobs` to mark the clear operations to handle
/// - color: The RGBA color to use for clearing the frame buffer
/// - depth: The depth to clear from the depth stencil attachment
/// - stencil: The stencil to clear from the depth stencil attachment
///
/// In APIs like OpenGL or Direct3D11 render targets have to be explicitly cleared. In OpenGL this is achieved by
/// calling `glClear()` which will schedule a clear operation. Similarly Direct3D11 requires a call to
/// `ClearRenderTargetView` with a specific `ID3D11RenderTargetView` to do the same.
///
/// Metal does not provide an explicit command to "clear the screen" (as one does not render directly to screens
/// anymore with these APIs). Instead Metal provides "load commands" and "store commands" which describe what should
/// happen to a render target when it is loaded for rendering and unloaded after rendering.
///
/// Thus a "clear" is a "load command" for a render target or depth stencil attachment that is automatically executed
/// by Metal when it loads or stores them and thus requires Metal to do an explicit (empty) draw call to ensure that
/// the load and store commands are executed even when no other draw calls will follow.
@_cdecl("device_clear")
public func device_clear(
device: UnsafeRawPointer, clearFlags: UInt32, color: UnsafePointer<vec4>, depth: Float, stencil: UInt8
) {
let device: MetalDevice = unretained(device)
var clearState = ClearState()
if (Int32(clearFlags) & GS_CLEAR_COLOR) == 1 {
clearState.colorAction = .clear
clearState.clearColor = MTLClearColor(
red: Double(color.pointee.x),
green: Double(color.pointee.y),
blue: Double(color.pointee.z),
alpha: Double(color.pointee.w)
)
} else {
clearState.colorAction = .load
}
if (Int32(clearFlags) & GS_CLEAR_DEPTH) == 1 {
clearState.clearDepth = Double(depth)
clearState.depthAction = .clear
} else {
clearState.depthAction = .load
}
if (Int32(clearFlags) & GS_CLEAR_STENCIL) == 1 {
clearState.clearStencil = UInt32(stencil)
clearState.stencilAction = .clear
} else {
clearState.stencilAction = .load
}
do {
try device.clear(state: clearState)
} catch let error as MetalError.MTLDeviceError {
OBSLog(.error, "device_clear: \(error.description)")
} catch {
OBSLog(.error, "device_clear: Unknown error occurred")
}
}
/// Returns whether the current display is ready to preset a frame generated the renderer
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - Returns: Boolean value to state whether a frame generated by the renderer could actually be displayed
///
/// As OBS Studio's renderer is not synced with the operating system's compositor, situations could arise where the
/// renderer needs to be able to "hand off" a generated display output to the compositor but might not be able to
/// because it's not "ready" to receive such a frame. If that is the case, the graphics API can check for such a state
/// and return `false` here, allowing `libobs` to skip rendering the output for the "current" display entirely.
///
/// In Direct3D11 the `DXGI_SWAP_EFFECT_FLIP_DISCARD` flip effect is used, which allows OBS Studio to render a preview
/// into a buffer without having to care about the compositor. This is not possible in Metal as it's not the
/// application that provides the output buffer, it's the compositor which provides a "drawable" surface. For each
/// display there can only be a maximum of 3 drawables "in flight", a request for any consecutive drawable will stall
/// the renderer.
///
/// There is currently no way to check for the amount of available drawables, which could be used to return `false`
/// here and would allow `libobs` to skip output rendering on its current frame and try again on the next.
///
/// > Note: This check applies to the display associated with whichever "swap chain" might be "current" and is thus
/// depends on swap chain state.
@_cdecl("device_is_present_ready")
public func device_is_present_ready(device: UnsafeRawPointer) -> Bool {
return true
}
/// Commits the current command buffer to schedule and execute the GPU commands encoded within it and waits until they
/// have been scheduled.
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
///
/// OBS Studio's renderer will call this function when it has set up all draw commands for a given "display". It is
/// usually accompanied by a call to end the current scene just before and thus marks the end of commands for the
/// current command buffer.
@_cdecl("device_present")
public func device_present(device: UnsafeRawPointer) {
let device: MetalDevice = unretained(device)
device.finishPendingCommands()
}
/// Commits the current command buffer to schedule and execute the GPU commands encoded within it and waits until they
/// have been scheduled.
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
///
/// OBS Studio's renderer will call this function when it is finished setting up all draw commands for the video output
/// texture, and also after it has used the GPU to encode a video output frame.
@_cdecl("device_flush")
public func device_flush(device: UnsafeRawPointer) {
let device: MetalDevice = unretained(device)
device.finishPendingCommands()
}
/// Sets the "current" cull mode to be used by the next draw call
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - mode: `libobs` cull mode identifier
///
/// Converts the cull mode provided by `libobs` into its appropriate ``MTLCullMode`` variant.
@_cdecl("device_set_cull_mode")
public func device_set_cull_mode(device: UnsafeRawPointer, mode: gs_cull_mode) {
let device: MetalDevice = unretained(device)
device.renderState.cullMode = mode.mtlMode
}
/// Gets the "current" cull mode that was set up for the next draw call
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - Returns: `libobs` cull mode
///
/// Converts the ``MTLCullMode`` set up currently into its `libobs` variation
@_cdecl("device_get_cull_mode")
public func device_get_cull_mode(device: UnsafeRawPointer) -> gs_cull_mode {
let device: MetalDevice = unretained(device)
return device.renderState.cullMode.obsMode
}
/// Switches blending of the next draw operation with the contents of the "current" framebuffer.
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - enable: `true` if contents should be blended, `false` otherwise
///
/// This function directly enables or disables blending for the first render target set up in the current pipeline.
@_cdecl("device_enable_blending")
public func device_enable_blending(device: UnsafeRawPointer, enable: Bool) {
let device: MetalDevice = unretained(device)
device.renderState.pipelineDescriptor.colorAttachments[0].isBlendingEnabled = enable
}
/// Switches depth testing on the next draw operation with the contents of the current depth stencil buffer.
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - enable: `true` if depth testing should be enabled, `false` otherwise
///
/// This function directly enables or disables depth texting for the depth stencil attachment set up in the current pipeline
@_cdecl("device_enable_depth_test")
public func device_enable_depth_test(device: UnsafeRawPointer, enable: Bool) {
let device: MetalDevice = unretained(device)
device.renderState.depthStencilDescriptor.isDepthWriteEnabled = enable
}
/// Sets the read mask in the depth stencil descriptor set up in the current pipeline
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - enable: `true` if the read mask should be `1`, `false` for a read mask of `0`
///
/// The `MTLDepthStencilDescriptor` can differentiate between a front facing stencil and a back facing stencil. As
/// `libobs` does not make this distinction, both values will be set to the same value.
@_cdecl("device_enable_stencil_test")
public func device_enable_stencil_test(device: UnsafeRawPointer, enable: Bool) {
let device: MetalDevice = unretained(device)
device.renderState.depthStencilDescriptor.frontFaceStencil.readMask = enable ? 1 : 0
device.renderState.depthStencilDescriptor.backFaceStencil.readMask = enable ? 1 : 0
}
/// Sets the write mask in the depth stencil descriptor set up in the current pipeline
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - enable: `true` if the write mask should be `1`, `false` for a write mask of `0`
///
/// The `MTLDepthStencilDescriptor` can differentiate between a front facing stencil and a back facing stencil. As
/// `libobs` does not make this distinction, both values will be set to the same value.
@_cdecl("device_enable_stencil_write")
public func device_enable_stencil_write(device: UnsafeRawPointer, enable: Bool) {
let device: MetalDevice = unretained(device)
device.renderState.depthStencilDescriptor.frontFaceStencil.writeMask = enable ? 1 : 0
device.renderState.depthStencilDescriptor.backFaceStencil.writeMask = enable ? 1 : 0
}
/// Sets the color write mask for the render target set up in the current pipeline
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - red: `true` if the red color channel should be written, `false` otherwise
/// - green: `true` if the green color channel should be written, `false` otherwise
/// - blue: `true` if the blue color channel should be written, `false` otherwise
/// - alpha: `true` if the alpha channel should be written, `false` otherwise
///
/// The separate `bool` values are converted into an ``MTLColorWriteMask`` which is then set up on the first render
/// target of the current pipeline.
@_cdecl("device_enable_color")
public func device_enable_color(device: UnsafeRawPointer, red: Bool, green: Bool, blue: Bool, alpha: Bool) {
let device: MetalDevice = unretained(device)
var colorMask = MTLColorWriteMask()
if red {
colorMask.insert(.red)
}
if green {
colorMask.insert(.green)
}
if blue {
colorMask.insert(.blue)
}
if alpha {
colorMask.insert(.alpha)
}
device.renderState.pipelineDescriptor.colorAttachments[0].writeMask = colorMask
}
/// Sets the depth compare function for the depth stencil descriptor to be used in the current pipeline
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - test: `libobs` enum describing the depth compare function to use
///
/// The enum value provided by `libobs` is converted into a ``MTLCompareFunction``, which is then set directly as the
/// compare function on the depth stencil descriptor.
@_cdecl("device_depth_function")
public func device_depth_function(device: UnsafeRawPointer, test: gs_depth_test) {
let device: MetalDevice = unretained(device)
device.renderState.depthStencilDescriptor.depthCompareFunction = test.mtlFunction
}
/// Sets the stencil compare functions for the specified stencil side(s) on the depth stencil descriptor in the current
/// pipeline.
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - side: The stencil side(s) for which the compare function should be set up
/// - test: `libobs` enum describing the stencil test function to use
///
/// The enum values provided by `libobs` are first checked for the stencil side, after which the compare function value
/// itself is converted into a ``MTLCompareFunction``, which is then set directly as the compare function on the depth
/// stencil descriptor.
@_cdecl("device_stencil_function")
public func device_stencil_function(device: UnsafeRawPointer, side: gs_stencil_side, test: gs_depth_test) {
let device: MetalDevice = unretained(device)
let stencilCompareFunction: (MTLCompareFunction, MTLCompareFunction)
if side == GS_STENCIL_FRONT {
stencilCompareFunction = (test.mtlFunction, .never)
} else if side == GS_STENCIL_BACK {
stencilCompareFunction = (.never, test.mtlFunction)
} else {
stencilCompareFunction = (test.mtlFunction, test.mtlFunction)
}
device.renderState.depthStencilDescriptor.frontFaceStencil.stencilCompareFunction = stencilCompareFunction.0
device.renderState.depthStencilDescriptor.backFaceStencil.stencilCompareFunction = stencilCompareFunction.1
}
/// Sets the stencil fail, depth fail, and depth pass operations for the specified stencil side(s) on the depth stencil
/// descriptor for the current pipeline.
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - side: The stencil side(s) for which the fail and pass functions should be set up
/// - fail: `libobs` enum value describing the stencil fail operation
/// - zfail: `libobs` enum value describing the depth fail operation
/// - zpass: `libobs` enum value describing the depth pass operation
///
/// The enum values provided by `libobs` are first checked for the stencil side, after which the fail function values
/// themselves are converted into their ``MTLCompareFunction`` variants, which are then set directly on the depth
/// stencil descriptor.
@_cdecl("device_stencil_op")
public func device_stencil_op(
device: UnsafeRawPointer, side: gs_stencil_side, fail: gs_stencil_op_type, zfail: gs_stencil_op_type,
zpass: gs_stencil_op_type
) {
let device: MetalDevice = unretained(device)
let stencilFailOperation: (MTLStencilOperation, MTLStencilOperation)
let depthFailOperation: (MTLStencilOperation, MTLStencilOperation)
let depthPassOperation: (MTLStencilOperation, MTLStencilOperation)
if side == GS_STENCIL_FRONT {
stencilFailOperation = (fail.mtlOperation, .keep)
depthFailOperation = (zfail.mtlOperation, .keep)
depthPassOperation = (zpass.mtlOperation, .keep)
} else if side == GS_STENCIL_BACK {
stencilFailOperation = (.keep, fail.mtlOperation)
depthFailOperation = (.keep, zfail.mtlOperation)
depthPassOperation = (.keep, zpass.mtlOperation)
} else {
stencilFailOperation = (fail.mtlOperation, fail.mtlOperation)
depthFailOperation = (zfail.mtlOperation, zfail.mtlOperation)
depthPassOperation = (zpass.mtlOperation, zpass.mtlOperation)
}
device.renderState.depthStencilDescriptor.frontFaceStencil.stencilFailureOperation = stencilFailOperation.0
device.renderState.depthStencilDescriptor.frontFaceStencil.depthFailureOperation = depthFailOperation.0
device.renderState.depthStencilDescriptor.frontFaceStencil.depthStencilPassOperation = depthPassOperation.0
device.renderState.depthStencilDescriptor.backFaceStencil.stencilFailureOperation = stencilFailOperation.1
device.renderState.depthStencilDescriptor.backFaceStencil.depthFailureOperation = depthFailOperation.1
device.renderState.depthStencilDescriptor.backFaceStencil.depthStencilPassOperation = depthPassOperation.1
}
/// Sets up the viewport for use in the current pipeline
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - x: Origin X coordinate for the viewport
/// - y: Origin Y coordinate for the viewport
/// - width: Width of the viewport
/// - height: Height of the viewport
///
/// The separate values for origin and dimension are converted into an ``MTLViewport`` which is then retained as the
/// "current" viewport for later use when the pipeline is actually set up.
@_cdecl("device_set_viewport")
public func device_set_viewport(device: UnsafeRawPointer, x: Int32, y: Int32, width: Int32, height: Int32) {
let device: MetalDevice = unretained(device)
let viewPort = MTLViewport(
originX: Double(x),
originY: Double(y),
width: Double(width),
height: Double(height),
znear: 0.0,
zfar: 1.0
)
device.renderState.viewPort = viewPort
}
/// Gets the origin and dimensions of the viewport currently set up for use by the pipeline
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - rect: A pointer to a ``gs_rect`` struct in memory
///
/// The function is provided a pointer to a ``gs_struct`` instance in memory which can hold the x and y values for the
/// origin and dimension of the viewport.
///
/// This function is usually called when some source needs to retain the current "state" of the pipeline (of which
/// there can ever only be one) and overwrite the state with its own (in this case its own viewport). To be able to
/// restore the prior state, the "current" state needs to be retrieved from the pipeline.
@_cdecl("device_get_viewport")
public func device_get_viewport(device: UnsafeRawPointer, rect: UnsafeMutablePointer<gs_rect>) {
let device: MetalDevice = unretained(device)
rect.pointee.x = Int32(device.renderState.viewPort.originX)
rect.pointee.y = Int32(device.renderState.viewPort.originY)
rect.pointee.cx = Int32(device.renderState.viewPort.width)
rect.pointee.cy = Int32(device.renderState.viewPort.height)
}
/// Sets up a scissor rect to be used by the current pipeline
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - rect: Pointer to a ``gs_rect`` struct in memory that contains origin and dimension of the scissor rect
///
/// The ``gs_rect`` is converted into a ``MTLScissorRect`` object before saving it in the "current" render state
/// for use in the next draw call.
@_cdecl("device_set_scissor_rect")
public func device_set_scissor_rect(device: UnsafeRawPointer, rect: UnsafePointer<gs_rect>?) {
let device: MetalDevice = unretained(device)
if let rect {
device.renderState.scissorRect = rect.pointee.mtlScissorRect
device.renderState.scissorRectEnabled = true
} else {
device.renderState.scissorRect = nil
device.renderState.scissorRectEnabled = false
}
}
/// Sets up an orthographic projection matrix with the provided view frustum
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - left: Left edge of view frustum on the near plane
/// - right: Right edge of view frustum on the near plane
/// - top: Top edge of view frustum on the near plane
/// - bottom: Bottom edge of view frustum on the near plane
/// - near: Distance of near plane on the Z axis
/// - far: Distance of far plane on the Z axis
@_cdecl("device_ortho")
public func device_ortho(
device: UnsafeRawPointer, left: Float, right: Float, top: Float, bottom: Float, near: Float, far: Float
) {
let device: MetalDevice = unretained(device)
let rml = right - left
let bmt = bottom - top
let fmn = far - near
device.renderState.projectionMatrix = matrix_float4x4(
rows: [
SIMD4((2.0 / rml), 0.0, 0.0, 0.0),
SIMD4(0.0, (2.0 / -bmt), 0.0, 0.0),
SIMD4(0.0, 0.0, (1 / fmn), 0.0),
SIMD4((left + right) / -rml, (bottom + top) / bmt, near / -fmn, 1.0),
]
)
}
/// Sets up a perspective projection matrix with the provided view frustum
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - left: Left edge of view frustum on the near plane
/// - right: Right edge of view frustum on the near plane
/// - top: Top edge of view frustum on the near plane
/// - bottom: Bottom edge of view frustum on the near plane
/// - near: Distance of near plane on the Z axis
/// - far: Distance of far plane on the Z axis
@_cdecl("device_frustum")
public func device_frustum(
device: UnsafeRawPointer, left: Float, right: Float, top: Float, bottom: Float, near: Float, far: Float
) {
let device: MetalDevice = unretained(device)
let rml = right - left
let tmb = top - bottom
let fmn = far - near
device.renderState.projectionMatrix = matrix_float4x4(
columns: (
SIMD4(((2 * near) / rml), 0.0, 0.0, 0.0),
SIMD4(0.0, ((2 * near) / tmb), 0.0, 0.0),
SIMD4(((left + right) / rml), ((top + bottom) / tmb), (-far / fmn), -1.0),
SIMD4(0.0, 0.0, (-(far * near) / fmn), 0.0)
)
)
}
/// Requests the current projection matrix to be pushed into a projection stack
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
///
/// OBS Studio's renderer works with the assumption of one big "current" state stack, which requires the entire state
/// to be changed to meet different rendering requirements. Part of this state is the current projection matrix, which
/// might need to be replaced temporarily. This function will be called when another projection matrix will be set up
/// to allow for its restoration later.
@_cdecl("device_projection_push")
public func device_projection_push(device: UnsafeRawPointer) {
let device: MetalDevice = unretained(device)
device.renderState.projections.append(device.renderState.projectionMatrix)
}
/// Requests the most recently pushed projection matrix to be removed from the stack and set up as the new current
/// matrix
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
///
/// OBS Studio's renderer works with the assumption of one big "current" state stack. This requires some elements of
/// this state to be temporarily retained before reinstating them after. This function will reinstate the most recently
/// added matrix as the new "current" matrix.
@_cdecl("device_projection_pop")
public func device_projection_pop(device: UnsafeRawPointer) {
let device: MetalDevice = unretained(device)
device.renderState.projectionMatrix = device.renderState.projections.removeLast()
}
/// Checks whether the current display is capable of displaying high dynamic range content.
///
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - monitor: Opaque pointer of a platform-dependent monitor identifier
/// - Returns: `true` if the display is capable of displaying high dynamic range content, `false` otherwise
///
/// On macOS this capability is described by the ``NSScreen/maximumPotentialExtendedDynamicRangeColorComponentValue``
/// property, which can be checked using the ``NSWindow/screen`` property after retrieving the ``NSView/window``
/// property.
@_cdecl("device_is_monitor_hdr")
public func device_is_monitor_hdr(device: UnsafeRawPointer, monitor: UnsafeRawPointer) -> Bool {
let device: MetalDevice = unretained(device)
guard let swapChain = device.renderState.swapChain else {
return false
}
return swapChain.edrHeadroom > 1.0
}

View File

@@ -0,0 +1,269 @@
/******************************************************************************
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
/// Creates a ``OBSSwapChain`` instance for use as a pseudo swap chain implementation to be shared with `libobs`
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - data: Pointer to platform-specific `gs_init_data` struct
/// - Returns: Opaque pointer to a new ``OBSSwapChain`` on success or `nil` on error
///
/// As interaction with UI elements needs to happen on the main thread of macOS, this function is marked with
/// `@MainActor`. This is also necessary because ``OBSSwapChain/updateView`` itself interacts with the ``NSView``
/// instance passed via the `data` argument and also has to occur on the main thread.
///
/// As applications cannot manage their own swap chain on macOS, the ``OBSSwapChain`` class merely wraps the
/// management of the ``CAMetalLayer`` that will be associated with the ``NSView`` and handles the drawables used to
/// render their contents.
///
/// > Important: This function can only be called from the main thread.
@MainActor
@_cdecl("device_swapchain_create")
public func device_swapchain_create(device: UnsafeMutableRawPointer, data: UnsafePointer<gs_init_data>)
-> OpaquePointer?
{
let device: MetalDevice = unretained(device)
let view = data.pointee.window.view.takeUnretainedValue() as! NSView
let size = MTLSize(
width: Int(data.pointee.cx),
height: Int(data.pointee.cy),
depth: 0
)
guard let swapChain = OBSSwapChain(device: device, size: size, colorSpace: data.pointee.format) else { return nil }
swapChain.updateView(view)
device.swapChainQueue.sync {
device.swapChains.append(swapChain)
}
return swapChain.getRetained()
}
/// Updates the internal size parameter and dimension of the ``CAMetalLayer`` managed by the ``OBSSwapChain`` instance
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - width: Width to update the layer's dimensions to
/// - height: Height to update the layer's dimensions to
///
/// As the relationship between the ``CAMetalLayer`` and the ``NSView`` it is associated with is managed indirectly,
/// the metal layer cannot directly react to size changes (even though it would be possible to do so). Instead
/// ``AppKit`` will report a size change to the application, which will be picked up by Qt, who will emit a size
/// change event on the main loop, which will update internal state of the ``OBSQTDisplay`` class. These changes are
/// asynchronously picked up by `libobs` render loop, which will then call this function.
@_cdecl("device_resize")
public func device_resize(device: UnsafeMutableRawPointer, width: UInt32, height: UInt32) {
let device: MetalDevice = unretained(device)
guard let swapChain = device.renderState.swapChain else {
return
}
swapChain.resize(.init(width: Int(width), height: Int(height), depth: 0))
}
/// This function does nothing on Metal
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
///
/// The intended purpose of this function is to update the render target in the "current" swap chain with the color
/// space of its "display" and thus pick up changes in color spaces between different screens.
///
/// On macOS this just requires updating the EDR headroom for the screen the view might be associated with, as the
/// actual color space and EDR capabilities are evaluated on every render loop.
///
/// > Important: This function can only be called from the main thread.
@_cdecl("device_update_color_space")
public func device_update_color_space(device: UnsafeRawPointer) {
let device: MetalDevice = unretained(device)
guard device.renderState.swapChain != nil else {
return
}
nonisolated(unsafe) let swapChain = device.renderState.swapChain!
Task { @MainActor in
swapChain.updateEdrHeadroom()
}
}
/// Gets the dimensions of the ``CAMetalLayer`` managed by the ``OBSSwapChain`` instance set up in the current pipeline
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - cx: Pointer to memory for the width of the layer
/// - cy: Pointer to memory for the height of the layer
@_cdecl("device_get_size")
public func device_get_size(
device: UnsafeMutableRawPointer, cx: UnsafeMutablePointer<UInt32>, cy: UnsafeMutablePointer<UInt32>
) {
let device: MetalDevice = unretained(device)
guard let swapChain = device.renderState.swapChain else {
cx.pointee = 0
cy.pointee = 0
return
}
cx.pointee = UInt32(swapChain.viewSize.width)
cy.pointee = UInt32(swapChain.viewSize.height)
}
/// Gets the width of the ``CAMetalLayer`` managed by the ``OBSSwapChain`` instance set up in the current pipeline
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - Returns: Width of the layer
@_cdecl("device_get_width")
public func device_get_width(device: UnsafeRawPointer) -> UInt32 {
let device: MetalDevice = unretained(device)
guard let swapChain = device.renderState.swapChain else {
return 0
}
return UInt32(swapChain.viewSize.width)
}
/// Gets the height of the ``CAMetalLayer`` managed by the ``OBSSwapChain`` instance set up in the current pipeline
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - Returns: Height of the layer
@_cdecl("device_get_height")
public func device_get_height(device: UnsafeRawPointer) -> UInt32 {
let device: MetalDevice = unretained(device)
guard let swapChain = device.renderState.swapChain else {
return 0
}
return UInt32(swapChain.viewSize.height)
}
/// Sets up the ``OBSSwapChain`` for use in the current pipeline
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - swap: Opaque pointer to ``OBSSwapChain`` instance shared with `libobs`
///
/// The first call of this function in any render loop marks the "begin" of OBS Studio's display render stage. There
/// will only ever be one "current" swap chain in use by `libobs` and there is no dedicated call to "reset" or unload
/// the current swap chain, instead a new swap chain is loaded or the "scene end" function is called.
@_cdecl("device_load_swapchain")
public func device_load_swapchain(device: UnsafeRawPointer, swap: UnsafeRawPointer) {
let device: MetalDevice = unretained(device)
let swapChain: OBSSwapChain = unretained(swap)
if swapChain.edrHeadroom > 1.0 {
var videoInfo: obs_video_info = obs_video_info()
obs_get_video_info(&videoInfo)
let videoColorSpace = videoInfo.colorspace
switch videoColorSpace {
case VIDEO_CS_2100_PQ:
if swapChain.colorRange != .hdrPQ {
// TODO: Investigate whether it's viable to use PQ or HLG tone mapping for the preview
// Use the following code to enable it for either:
// 2100 PQ:
// let maxLuminance = obs_get_video_hdr_nominal_peak_level()
// swapChain.layer.edrMetadata = .hdr10(
// minLuminance: 0.0001, maxLuminance: maxLuminance, opticalOutputScale: 10000)
// HLG:
// swapChain.layer.edrMetadata = .hlg
swapChain.layer.pixelFormat = .rgba16Float
swapChain.layer.colorspace = CGColorSpace(name: CGColorSpace.extendedLinearSRGB)
swapChain.layer.wantsExtendedDynamicRangeContent = true
swapChain.layer.edrMetadata = nil
swapChain.colorRange = .hdrPQ
swapChain.renderTarget = nil
}
case VIDEO_CS_2100_HLG:
if swapChain.colorRange != .hdrHLG {
swapChain.layer.pixelFormat = .rgba16Float
swapChain.layer.colorspace = CGColorSpace(name: CGColorSpace.extendedLinearSRGB)
swapChain.layer.wantsExtendedDynamicRangeContent = true
swapChain.layer.edrMetadata = nil
swapChain.colorRange = .hdrHLG
swapChain.renderTarget = nil
}
default:
if swapChain.colorRange != .sdr {
swapChain.layer.pixelFormat = .bgra8Unorm_srgb
swapChain.layer.colorspace = CGColorSpace(name: CGColorSpace.sRGB)
swapChain.layer.wantsExtendedDynamicRangeContent = false
swapChain.layer.edrMetadata = nil
swapChain.colorRange = .sdr
swapChain.renderTarget = nil
}
}
} else {
if swapChain.colorRange != .sdr {
swapChain.layer.pixelFormat = .bgra8Unorm_srgb
swapChain.layer.colorspace = CGColorSpace(name: CGColorSpace.sRGB)
swapChain.layer.wantsExtendedDynamicRangeContent = false
swapChain.layer.edrMetadata = nil
swapChain.colorRange = .sdr
swapChain.renderTarget = nil
}
}
switch swapChain.colorRange {
case .hdrHLG, .hdrPQ:
device.renderState.gsColorSpace = GS_CS_709_EXTENDED
device.renderState.useSRGBGamma = false
case .sdr:
device.renderState.gsColorSpace = GS_CS_SRGB
device.renderState.useSRGBGamma = true
}
if let renderTarget = swapChain.renderTarget {
device.renderState.renderTarget = renderTarget
} else {
let descriptor = MTLTextureDescriptor.texture2DDescriptor(
pixelFormat: swapChain.layer.pixelFormat,
width: Int(swapChain.layer.drawableSize.width),
height: Int(swapChain.layer.drawableSize.height),
mipmapped: false)
descriptor.usage = [.renderTarget]
guard let renderTarget = MetalTexture(device: device, descriptor: descriptor) else {
return
}
swapChain.renderTarget = renderTarget
device.renderState.renderTarget = renderTarget
}
device.renderState.depthStencilAttachment = nil
device.renderState.isRendertargetChanged = true
device.renderState.isInDisplaysRenderStage = true
device.renderState.swapChain = swapChain
}
/// Requests deinitialization of the ``OBSSwapChain`` instance shared with `libobs`
/// - Parameter texture: Opaque pointer to ``OBSSwapChain`` instance shared with `libobs`
///
/// The ownership of the shared pointer is transferred into this function and the instance is placed under Swift's
/// memory management again.
@_cdecl("gs_swapchain_destroy")
public func gs_swapchain_destroy(swapChain: UnsafeMutableRawPointer) {
let swapChain = retained(swapChain) as OBSSwapChain
swapChain.discard = true
}

View File

@@ -0,0 +1,528 @@
/******************************************************************************
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 Foundation
import Metal
/// Creates a two-dimensional ``MetalTexture`` instance with the specified usage options and the raw image data (if
/// provided)
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - width: Desired width of the texture
/// - height: Desired height of the texture
/// - color_format: Desired color format of the texture as described by `gs_color_format`
/// - levels: Amount of mip map levels to generate for the texture
/// - data: Optional pointer to raw pixel data per mip map level
/// - flags: Texture resource use information encoded as `libobs` bitfield
/// - Returns: Opaque pointer to a created ``MetalTexture`` instance or a `nil` pointer on error
///
/// This function will create a new ``MTLTexture`` wrapped within a ``MetalTexture`` class and also upload any pixel
/// data if non-`nil` pointers have been provided via the `data` argument.
///
/// > Important: If mipmap generation is requested, execution will be blocked by waiting for the blit command encoder
/// to generate the mipmaps.
@_cdecl("device_texture_create")
public func device_texture_create(
device: UnsafeRawPointer, width: UInt32, height: UInt32, color_format: gs_color_format, levels: UInt32,
data: UnsafePointer<UnsafePointer<UInt8>?>?, flags: UInt32
) -> OpaquePointer? {
let device: MetalDevice = unretained(device)
let descriptor = MTLTextureDescriptor.init(
type: .type2D,
width: width,
height: height,
depth: 1,
colorFormat: color_format,
levels: levels,
flags: flags
)
guard let descriptor, let texture = MetalTexture(device: device, descriptor: descriptor) else {
return nil
}
if let data {
texture.upload(data: data, mipmapLevels: descriptor.mipmapLevelCount)
}
return texture.getRetained()
}
/// Creates a ``MetalTexture`` instance for a cube texture with the specified usage options and the raw image data (if provided)
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - size: Desized edge length for the cube
/// - color_format: Desired color format of the texture as described by `gs_color_format`
/// - levels: Amount of mip map levels to generate for the texture
/// - data: Optional pointer to raw pixel data per mip map level
/// - flags: Texture resource use information encoded as `libobs` bitfield
/// - Returns: Opaque pointer to created ``MetalTexture`` instance or a `nil` pointer on error
///
/// This function will create a new ``MTLTexture`` wrapped within a ``MetalTexture`` class and also upload any pixel
/// data if non-`nil` pointers have
/// been provided via the `data` argument.
///
/// > Important: If mipmap generation is requested, execution will be blocked by waiting for the blit command encoder
/// to generate the mipmaps.
@_cdecl("device_cubetexture_create")
public func device_cubetexture_create(
device: UnsafeRawPointer, size: UInt32, color_format: gs_color_format, levels: UInt32,
data: UnsafePointer<UnsafePointer<UInt8>?>?, flags: UInt32
) -> OpaquePointer? {
let device: MetalDevice = unretained(device)
let descriptor = MTLTextureDescriptor.init(
type: .typeCube,
width: size,
height: size,
depth: 1,
colorFormat: color_format,
levels: levels,
flags: flags
)
guard let descriptor, let texture = MetalTexture(device: device, descriptor: descriptor) else {
return nil
}
if let data {
texture.upload(data: data, mipmapLevels: descriptor.mipmapLevelCount)
}
return texture.getRetained()
}
/// Requests deinitialization of the ``MetalTexture`` instance shared with `libobs`
/// - Parameter texture: Opaque pointer to ``MetalTexture`` instance shared with `libobs`
///
/// The ownership of the shared pointer is transferred into this function and the instance is placed under Swift's
/// memory management again.
@_cdecl("gs_texture_destroy")
public func gs_texture_destroy(texture: UnsafeRawPointer) {
let _ = retained(texture) as MetalTexture
}
/// Gets the type of the texture wrapped by the ``MetalTexture`` instance
/// - Parameter texture: Opaque pointer to ``MetalTexture`` instance shared with `libobs`
/// - Returns: Texture type identified by `gs_texture_type` enum value
///
/// > Warning: As `libobs` has no enum value for "invalid texture type", there is no way for this function to signal
/// that the wrapped texture has an incompatible ``MTLTextureType``. Instead of crashing the program (which would
/// avoid undefined behavior), this function will return the 2D texture type value instead, which is incorrect, but is
/// more in line with how OBS Studio handles undefined behavior.
@_cdecl("device_get_texture_type")
public func device_get_texture_type(texture: UnsafeRawPointer) -> gs_texture_type {
let texture: MetalTexture = unretained(texture)
return texture.texture.textureType.gsTextureType ?? GS_TEXTURE_2D
}
/// Requests the ``MetalTexture`` instance to be loaded as one of the current pipeline's fragment attachments in the
/// specified texture slot
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - tex: Opaque pointer to ``MetalTexture`` instance shared with `libobs`
/// - unit: Texture slot for fragment attachment
///
/// OBS Studio expects pipelines to support fragment attachments for textures and samplers up to the amount defined in
/// the `GS_MAX_TEXTURES` preprocessor directive. The order of this calls can be arbitrary, so at any point in time a
/// request to load a texture into slot "5" can take place, even if slots 0 to 4 are empty.
@_cdecl("device_load_texture")
public func device_load_texture(device: UnsafeRawPointer, tex: UnsafeRawPointer, unit: UInt32) {
let device: MetalDevice = unretained(device)
let texture: MetalTexture = unretained(tex)
device.renderState.textures[Int(unit)] = texture.texture
}
/// Requests an sRGB variant of a ``MetalTexture`` instance to be set as one of the current pipeline's fragment
/// attachments in the specified texture slot.
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - tex: Opaque pointer to ``MetalTexture`` instance shared with `libobs`
/// - unit: Texture slot for fragment attachment
/// OBS Studio expects pipelines to support fragment attachments for textures and samplers up to the amount defined in
/// the `GS_MAX_TEXTURES` preprocessor directive. The order of this calls can be arbitrary, so at any point in time a
/// request to load a texture into slot "5" can take place, even if slots 0 to 4 are empty.
///
/// > Important: This variant of the texture load functions expects a texture whose color values are already sRGB gamma
/// encoded and thus also expects that the color values used in the fragment shader will have been automatically
/// decoded into linear gamma. If the ``MetalTexture`` instance has no dedicated ``MetalTexture/sRGBtexture`` instance,
/// it will use the normal ``MetalTexture/texture`` instance instead.
@_cdecl("device_load_texture_srgb")
public func device_load_texture_srgb(device: UnsafeRawPointer, tex: UnsafeRawPointer, unit: UInt32) {
let device: MetalDevice = unretained(device)
let texture: MetalTexture = unretained(tex)
if texture.sRGBtexture != nil {
device.renderState.textures[Int(unit)] = texture.sRGBtexture!
} else {
device.renderState.textures[Int(unit)] = texture.texture
}
}
/// Copies image data from a region in the source ``MetalTexture`` into a destination ``MetalTexture`` at the provided
/// origin
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - dst: Opaque pointer to ``MetalTexture`` instance shared with `libobs`, used as destination for the copy operation
/// - dst_x: X coordinate of the origin in the destination texture
/// - dst_y: Y coordinate of the origin in the destination texture
/// - src: Opaque pointer to ``MetalTexture`` instance shared with `libobs`, used as source for the copy operation
/// - src_x: X coordinate of the origin in the source texture
/// - src_y: Y coordinate of the origin in the source texture
/// - src_w: Width of the region in the source texture
/// - src_h: Height of the region in the source texture
///
/// This function will fail if the destination texture's dimensions aren't large enough to hold the region copied from
/// the source texture. This check will use the desired origin within the destination texture and the region's size
/// into account and checks whether the total dimensions of the destination are large enough (starting at the
/// destination origin) to hold the source's region.
///
/// > Important: Execution will **not** be blocked, the copy operation will be committed to the command queue and
/// executed at some point after this function returns.
@_cdecl("device_copy_texture_region")
public func device_copy_texture_region(
device: UnsafeRawPointer, dst: UnsafeRawPointer, dst_x: UInt32, dst_y: UInt32, src: UnsafeRawPointer, src_x: UInt32,
src_y: UInt32, src_w: UInt32, src_h: UInt32
) {
let device: MetalDevice = unretained(device)
let source: MetalTexture = unretained(src)
let destination: MetalTexture = unretained(dst)
var sourceRegion = MTLRegion(
origin: .init(x: Int(src_x), y: Int(src_y), z: 0),
size: .init(width: Int(src_w), height: Int(src_h), depth: 1)
)
let destinationRegion = MTLRegion(
origin: .init(x: Int(dst_x), y: Int(dst_y), z: 0),
size: .init(width: destination.texture.width, height: destination.texture.height, depth: 1)
)
if sourceRegion.size.width == 0 {
sourceRegion.size.width = source.texture.width - sourceRegion.origin.x
}
if sourceRegion.size.height == 0 {
sourceRegion.size.height = source.texture.height - sourceRegion.origin.y
}
guard
destinationRegion.size.width - destinationRegion.origin.x > sourceRegion.size.width
&& destinationRegion.size.height - destinationRegion.origin.y > sourceRegion.size.height
else {
OBSLog(
.error,
"device_copy_texture_region: Destination texture \(destinationRegion.size) is not large enough to hold source region (\(sourceRegion.size) at origin \(destinationRegion.origin)"
)
return
}
do {
try device.copyTextureRegion(
source: source,
sourceRegion: sourceRegion,
destination: destination,
destinationRegion: destinationRegion)
} catch let error as MetalError.MTLDeviceError {
OBSLog(.error, "device_clear: \(error.description)")
} catch {
OBSLog(.error, "device_clear: Unknown error occurred")
}
}
/// Copies the image data from the source ``MetalTexture`` into the destination ``MetalTexture``
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - dst: Opaque pointer to ``MetalTexture`` instance shared with `libobs`, used as destination for the copy
/// operation
/// - src: Opaque pointer to ``MetalTexture`` instance shared with `libobs`, used as source for the copy operation
///
/// > Warning: This function requires that the source and destination texture dimensions are identical, otherwise the
/// copy operation will fail.
///
/// > Important: Execution will **not** be blocked, the copy operation will be committed to the command queue and
/// executed at some point after this function returns.
@_cdecl("device_copy_texture")
public func device_copy_texture(device: UnsafeRawPointer, dst: UnsafeRawPointer, src: UnsafeRawPointer) {
let device: MetalDevice = unretained(device)
let source: MetalTexture = unretained(src)
let destination: MetalTexture = unretained(dst)
do {
try device.copyTexture(source: source, destination: destination)
} catch let error as MetalError.MTLDeviceError {
OBSLog(.error, "device_clear: \(error.description)")
} catch {
OBSLog(.error, "device_clear: Unknown error occurred")
}
}
/// Copies the image data from the source ``MetalTexture`` into the destination ``MetalTexture`` and blocks execution
/// until the copy operation has finished.
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - dst: Opaque pointer to ``MetalStageBuffer`` instance shared with `libobs`, used as destination for the copy
/// operation
/// - src: Opaque pointer to ``MetalTexture`` instance shared with `libobs`, used as source for the copy operation
///
/// > Important: Execution will be blocked by waiting for the blit command encoder to finish the copy operation.
@_cdecl("device_stage_texture")
public func device_stage_texture(device: UnsafeRawPointer, dst: UnsafeRawPointer, src: UnsafeRawPointer) {
let device: MetalDevice = unretained(device)
let source: MetalTexture = unretained(src)
let destination: MetalStageBuffer = unretained(dst)
do {
try device.stageTextureToBuffer(source: source, destination: destination)
} catch let error as MetalError.MTLDeviceError {
OBSLog(.error, "device_clear: \(error.description)")
} catch {
OBSLog(.error, "device_clear: Unknown error occurred")
}
}
/// Gets the width of the texture wrapped by the ``MetalTexture`` instance
/// - Parameter tex: Opaque pointer to ``MetalTexture`` instance shared with `libobs`
/// - Returns: Width of the texture
@_cdecl("gs_texture_get_width")
public func device_texture_get_width(tex: UnsafeRawPointer) -> UInt32 {
let texture: MetalTexture = unretained(tex)
return UInt32(texture.texture.width)
}
/// Gets the height of the texture wrapped by the ``MetalTexture`` instance
/// - Parameter tex: Opaque pointer to ``MetalTexture`` instance shared with `libobs`
/// - Returns: Height of the texture
@_cdecl("gs_texture_get_height")
public func device_texture_get_height(tex: UnsafeRawPointer) -> UInt32 {
let texture: MetalTexture = unretained(tex)
return UInt32(texture.texture.height)
}
/// Gets the color format of the texture wrapped by the ``MetalTexture`` instance
/// - Parameter tex: Opaque pointer to ``MetalTexture`` instance shared with `libobs`
/// - Returns: Color format as defined by the `gs_color_format` enumeration
@_cdecl("gs_texture_get_color_format")
public func gs_texture_get_color_format(tex: UnsafeRawPointer) -> gs_color_format {
let texture: MetalTexture = unretained(tex)
return texture.texture.pixelFormat.gsColorFormat
}
/// Allocates memory for an update of the texture's image data wrapped by the ``MetalTexture`` instance.
/// - Parameters:
/// - tex: Opaque pointer to ``MetalTexture`` instance shared with `libobs`
/// - ptr: Pointer to memory for the raw image data
/// - linesize: Pointer to integer for the row size of the texture
/// - Returns: `true` if the mapping memory was allocated successfully, `false` otherwise
///
/// Metal does not provide "map" and "unmap" operations as they exist in Direct3D11, as resource management and
/// synchronization needs to be handled explicitly by the application. Thus "mapping" just means that enough memory for
/// raw image data is allocated and an unmanaged pointer to that memory is shared with `libobs` for writing the image data.
///
/// To ensure that the data written into the memory provided by this function is actually used to update the texture,
/// the corresponding function `gs_texture_unmap` needs to be used.
///
/// > Important: This function can only be used to **push** new image data into the texture. To _pull_ image data from
/// the texture, use a stage surface instead.
@_cdecl("gs_texture_map")
public func gs_texture_map(
tex: UnsafeRawPointer, ptr: UnsafeMutablePointer<UnsafeMutableRawPointer>, linesize: UnsafeMutablePointer<UInt32>
) -> Bool {
let texture: MetalTexture = unretained(tex)
guard texture.texture.textureType == .type2D, let device = texture.device else {
return false
}
let stageBuffer: MetalStageBuffer
if texture.stageBuffer == nil
|| (texture.stageBuffer!.width != texture.texture.width
&& texture.stageBuffer!.height != texture.texture.height)
{
guard
let buffer = MetalStageBuffer(
device: device,
width: texture.texture.width,
height: texture.texture.height,
format: texture.texture.pixelFormat
)
else {
OBSLog(.error, "gs_texture_map: Unable to create MetalStageBuffer for mapping texture")
return false
}
texture.stageBuffer = buffer
stageBuffer = buffer
} else {
stageBuffer = texture.stageBuffer!
}
ptr.pointee = stageBuffer.buffer.contents()
linesize.pointee = UInt32(stageBuffer.width * stageBuffer.format.bytesPerPixel!)
return true
}
/// Writes back raw image data into the texture wrapped by the ``MetalTexture`` instance
/// - Parameter tex: Opaque pointer to ``MetalTexture`` instance shared with `libobs`
///
/// This function needs to be used in tandem with `gs_texture_map`, which allocates memory for raw image data that
/// should be used in an update of the wrapped `MTLTexture`. This function will then actually replace the image data
/// in the texture with that raw image data and deallocate the memory that was allocated during `gs_texture_map`.
@_cdecl("gs_texture_unmap")
public func gs_texture_unmap(tex: UnsafeRawPointer) {
let texture: MetalTexture = unretained(tex)
guard texture.texture.textureType == .type2D, let stageBuffer = texture.stageBuffer, let device = texture.device
else {
return
}
do {
try device.stageBufferToTexture(source: stageBuffer, destination: texture)
} catch let error as MetalError.MTLDeviceError {
OBSLog(.error, "gs_texture_unmap: \(error.description)")
} catch {
OBSLog(.error, "gs_texture_unmap: Unknown error occurred")
}
}
/// Gets an opaque pointer to the ``MTLTexture`` instance wrapped by the provided ``MetalTexture`` instance
/// - Parameter tex: Opaque pointer to ``MetalTexture`` instance shared with `libobs`
/// - Returns: Opaque pointer to ``MTLTexture`` instance
///
/// > Important: The opaque pointer returned by this function is **unretained**, which means that the ``MTLTexture``
/// instance it refers to might be deinitialized at any point when no other Swift code holds a strong reference to it.
@_cdecl("gs_texture_get_obj")
public func gs_texture_get_obj(tex: UnsafeRawPointer) -> OpaquePointer {
let texture: MetalTexture = unretained(tex)
let unretained = Unmanaged.passUnretained(texture.texture).toOpaque()
return OpaquePointer(unretained)
}
/// Requests deinitialization of the ``MetalTexture`` instance shared with `libobs`
/// - Parameter cubetex: Opaque pointer to ``MetalTexture`` instance shared with `libobs`
///
/// The ownership of the shared pointer is transferred into this function and the instance is placed under
/// Swift's memory management again.
@_cdecl("gs_cubetexture_destroy")
public func gs_cubetexture_destroy(cubetex: UnsafeRawPointer) {
let _ = retained(cubetex) as MetalTexture
}
/// Gets the edge size of the cube texture wrapped by the ``MetalTexture`` instance
/// - Parameter cubetex: Opaque pointer to ``MetalTexture`` instance shared with `libobs`
/// - Returns: Edge size of the cube
@_cdecl("gs_cubetexture_get_size")
public func gs_cubetexture_get_size(cubetex: UnsafeRawPointer) -> UInt32 {
let texture: MetalTexture = unretained(cubetex)
return UInt32(texture.texture.width)
}
/// Gets the color format of the cube texture wrapped by the ``MetalTexture`` instance
/// - Parameter cubetex: Opaque pointer to ``MetalTexture`` instance shared with `libobs`
/// - Returns: Color format value
@_cdecl("gs_cubetexture_get_color_format")
public func gs_cubetexture_get_color_format(cubetex: UnsafeRawPointer) -> gs_color_format {
let texture: MetalTexture = unretained(cubetex)
return texture.texture.pixelFormat.gsColorFormat
}
/// Gets the device capability state for shared textures
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - Returns: Always `true`
///
/// While Metal provides a specific "shared texture" type, OBS Studio understands this to mean "textures shared between
/// processes", which is usually achieved using ``IOSurface`` references on macOS. Metal textures can be created from
/// these references, so this is always `true`.
@_cdecl("device_shared_texture_available")
public func device_shared_texture_available(device: UnsafeRawPointer) -> Bool {
return true
}
/// Creates a ``MetalTexture`` wrapping an ``MTLTexture`` that was created using the provided ``IOSurface`` reference.
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - iosurf: ``IOSurface`` reference to use as the image data source for the texture
/// - Returns: An opaque pointer to a ``MetalTexture`` instance on success, `nil` otherwise
///
/// If the provided ``IOSurface`` uses a video image format that has no compatible ``Metal`` pixel format, creation of
/// the texture will fail.
@_cdecl("device_texture_create_from_iosurface")
public func device_texture_create_from_iosurface(device: UnsafeRawPointer, iosurf: IOSurfaceRef) -> OpaquePointer? {
let device: MetalDevice = unretained(device)
let texture = MetalTexture(device: device, surface: iosurf)
guard let texture else {
return nil
}
return texture.getRetained()
}
/// Replaces the current ``IOSurface``-based ``MTLTexture`` wrapped by the provided ``MetalTexture`` instance with a
/// new instance.
/// - Parameters:
/// - texture: Opaque pointer to ``MetalTexture`` instance shared with `libobs`
/// - iosurf: ``IOSurface`` reference to use as the image data source for the texture
/// - Returns: An opaque pointer to a ``MetalTexture`` instance on success, `nil` otherwise
///
/// The "rebind" mentioned in the function name is limited to the ``MTLTexture`` instance wrapped inside the
/// ``MetalTexture`` instance, as textures are immutable objects (but their underlying data is mutable). This allows
/// `libobs` to hold onto the same opaque ``MetalTexture`` pointer even though the backing surface might have changed.
@_cdecl("gs_texture_rebind_iosurface")
public func gs_texture_rebind_iosurface(texture: UnsafeRawPointer, iosurf: IOSurfaceRef) -> Bool {
let texture: MetalTexture = unretained(texture)
return texture.rebind(surface: iosurf)
}
/// Creates a new ``MetalTexture`` instance with an opaque shared texture "handle"
/// - Parameters:
/// - device: Opaque pointer to ``MetalTexture`` instance shared with `libobs`
/// - handle: Arbitrary handle value that needs to be reinterpreted into the correct platform specific shared
/// reference type
/// - Returns: An opaque pointer to a ``MetalTexture`` instance on success, `nil` otherwise
///
/// The "handle" is a generalised argument used on all platforms and needs to be converted into a platform-specific
/// type before the "shared" texture can be created. In case of macOS this means converting the unsigned integer into
/// a ``IOSurface`` address.
///
/// > Warning: As the handle is a 32-bit integer, this can break on 64-bit systems if the ``IOSurface`` pointer
/// address does not fit into a 32-bit number.
@_cdecl("device_texture_open_shared")
public func device_texture_open_shared(device: UnsafeRawPointer, handle: UInt32) -> OpaquePointer? {
if let reference = IOSurfaceLookupFromMachPort(handle) {
let texture = device_texture_create_from_iosurface(device: device, iosurf: reference)
return texture
} else {
return nil
}
}

View File

@@ -0,0 +1,113 @@
/******************************************************************************
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 Foundation
import Metal
/// Creates a three-dimensional ``MetalTexture`` instance with the specified usage options and the raw image data
/// (if provided)
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - size: Desired size of the texture
/// - color_format: Desired color format of the texture as described by `gs_color_format`
/// - levels: Amount of mip map levels to generate for the texture
/// - data: Optional pointer to raw pixel data per mip map level
/// - flags: Texture resource use information encoded as `libobs` bitfield
/// - Returns: Opaque pointer to a created ``MetalTexture`` instance or a `NULL` pointer on error
///
/// This function will create a new ``MTLTexture`` wrapped within a ``MetalTexture`` class and also upload any pixel
/// data if non-`NULL` pointers have been provided via the `data` argument.
///
/// > Important: If mipmap generation is requested, execution will be blocked by waiting for the blit command encoder
/// to generate the mipmaps.
@_cdecl("device_voltexture_create")
public func device_voltexture_create(
device: UnsafeRawPointer, width: UInt32, height: UInt32, depth: UInt32, color_format: gs_color_format,
levels: UInt32, data: UnsafePointer<UnsafePointer<UInt8>?>?, flags: UInt32
) -> OpaquePointer? {
let device = Unmanaged<MetalDevice>.fromOpaque(device).takeUnretainedValue()
let descriptor = MTLTextureDescriptor.init(
type: .type3D,
width: width,
height: height,
depth: depth,
colorFormat: color_format,
levels: levels,
flags: flags
)
guard let descriptor, let texture = MetalTexture(device: device, descriptor: descriptor) else {
return nil
}
if let data {
texture.upload(data: data, mipmapLevels: descriptor.mipmapLevelCount)
}
return texture.getRetained()
}
/// Requests deinitialization of the ``MetalTexture`` instance shared with `libobs`
/// - Parameter texture: Opaque pointer to ``MetalTexture`` instance shared with `libobs`
///
/// The ownership of the shared pointer is transferred into this function and the instance is placed under
/// Swift's memory management again.
@_cdecl("gs_voltexture_destroy")
public func gs_voltexture_destroy(voltex: UnsafeRawPointer) {
let _ = retained(voltex) as MetalTexture
}
/// Gets the width of the texture wrapped by the ``MetalTexture`` instance
/// - Parameter voltex: Opaque pointer to ``MetalTexture`` instance shared with `libobs`
/// - Returns: Width of the texture
@_cdecl("gs_voltexture_get_width")
public func gs_voltexture_get_width(voltex: UnsafeRawPointer) -> UInt32 {
let texture: MetalTexture = unretained(voltex)
return UInt32(texture.texture.width)
}
/// Gets the height of the texture wrapped by the ``MetalTexture`` instance
/// - Parameter voltex: Opaque pointer to ``MetalTexture`` instance shared with `libobs`
/// - Returns: Height of the texture
@_cdecl("gs_voltexture_get_height")
public func gs_voltexture_get_height(voltex: UnsafeRawPointer) -> UInt32 {
let texture: MetalTexture = unretained(voltex)
return UInt32(texture.texture.height)
}
/// Gets the depth of the texture wrapped by the ``Metaltexture`` instance
/// - Parameter voltex: Opaque pointer to ``MetalTexture`` instance shared with `libobs`
/// - Returns: Depth of the texture
@_cdecl("gs_voltexture_get_depth")
public func gs_voltexture_get_depth(voltex: UnsafeRawPointer) -> UInt32 {
let texture: MetalTexture = unretained(voltex)
return UInt32(texture.texture.depth)
}
/// Gets the color format of the texture wrapped by the ``MetalTexture`` instance
/// - Parameter voltex: Opaque pointer to ``MetalTexture`` instance shared with `libobs`
/// - Returns: Color format as defined by the `gs_color_format` enumeration
@_cdecl("gs_voltexture_get_color_format")
public func gs_voltexture_get_color_format(voltex: UnsafeRawPointer) -> gs_color_format {
let texture: MetalTexture = unretained(voltex)
return texture.texture.pixelFormat.gsColorFormat
}

View File

@@ -0,0 +1,97 @@
/******************************************************************************
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/>.
******************************************************************************/
@_cdecl("device_load_default_samplerstate")
public func device_load_default_samplerstate(device: UnsafeRawPointer, b_3d: Bool, unit: Int) {
return
}
@_cdecl("device_enter_context")
public func device_enter_context(device: UnsafeMutableRawPointer) {
return
}
@_cdecl("device_leave_context")
public func device_leave_context(device: UnsafeMutableRawPointer) {
return
}
@_cdecl("device_timer_create")
public func device_timer_create(device: UnsafeRawPointer) {
return
}
@_cdecl("device_timer_range_create")
public func device_timer_range_create(device: UnsafeRawPointer) {
}
@_cdecl("gs_timer_destroy")
public func gs_timer_destroy(timer: UnsafeRawPointer) {
return
}
@_cdecl("gs_timer_begin")
public func gs_timer_begin(timer: UnsafeRawPointer) {
return
}
@_cdecl("gs_timer_end")
public func gs_timer_end(timer: UnsafeRawPointer) {
return
}
@_cdecl("gs_timer_get_data")
public func gs_timer_get_data(timer: UnsafeRawPointer) -> Bool {
return false
}
@_cdecl("gs_timer_range_destroy")
public func gs_timer_range_destroy(range: UnsafeRawPointer) {
return
}
@_cdecl("gs_timer_range_begin")
public func gs_timer_range_begin(range: UnsafeRawPointer) {
return
}
@_cdecl("gs_timer_range_end")
public func gs_timer_range_end(range: UnsafeRawPointer) {
return
}
@_cdecl("gs_timer_range_get_data")
public func gs_timer_range_get_data(range: UnsafeRawPointer, disjoint: Bool, frequency: UInt64) -> Bool {
return false
}
@_cdecl("device_debug_marker_begin")
public func device_debug_marker_begin(device: UnsafeRawPointer, monitor: UnsafeMutableRawPointer) {
return
}
@_cdecl("device_debug_marker_end")
public func device_debug_marker_end(device: UnsafeRawPointer) {
return
}
@_cdecl("device_set_cube_render_target")
public func device_set_cube_render_target(
device: UnsafeRawPointer, cubetex: UnsafeRawPointer, side: Int, zstencil: UnsafeRawPointer
) {
return
}

View File

@@ -0,0 +1,115 @@
/******************************************************************************
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/>.
******************************************************************************/
/// Creates a new ``MetalVertexBuffer`` instance with the given vertex buffer data and usage flags
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - data: Pointer to `gs_vb_data` vertex buffer data created by `libobs`
/// - flags: Usage flags encoded as `libobs` bitmask
/// - Returns: Opaque pointer to a new ``MetalVertexBuffer`` instance if successful, `nil` otherwise
///
/// > Note: The ownership of the memory pointed to by `data` is implicitly transferred to the ``MetalVertexBuffer``
/// instance, but is not managed by Swift.
@_cdecl("device_vertexbuffer_create")
public func device_vertexbuffer_create(device: UnsafeRawPointer, data: UnsafeMutablePointer<gs_vb_data>, flags: UInt32)
-> OpaquePointer
{
let device: MetalDevice = unretained(device)
let vertexBuffer = MetalVertexBuffer(
device: device,
data: data,
dynamic: (Int32(flags) & GS_DYNAMIC) != 0
)
return vertexBuffer.getRetained()
}
/// Requests the deinitialization of a shared ``MetalVertexBuffer`` instance
/// - Parameter indexBuffer: Opaque pointer to ``MetalVertexBuffer`` instance shared with `libobs`
///
/// The deinitialization is handled automatically by Swift after the ownership of the instance has been transferred
/// into the function and becomes the last strong reference to it. After the function leaves its scope, the object will
/// be deinitialized and deallocated automatically.
///
/// > Note: The vertex buffer data memory is implicitly owned by the ``MetalVertexBuffer`` instance and will be
/// manually cleaned up and deallocated by the instance's ``deinit`` method.
@_cdecl("gs_vertexbuffer_destroy")
public func gs_vertexbuffer_destroy(vertBuffer: UnsafeRawPointer) {
let _ = retained(vertBuffer) as MetalVertexBuffer
}
/// Sets up a ``MetalVertexBuffer`` as the vertex buffer for the current pipeline
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - vertbuffer: Opaque pointer to ``MetalVertexBuffer`` instance shared with `libobs`
///
/// > Note: The reference count of the ``MetalVertexBuffer`` instance will not be increased by this call.
///
/// > Important: If a `nil` pointer is provided as the vertex buffer, the index buffer will be _unset_.
@_cdecl("device_load_vertexbuffer")
public func device_load_vertexbuffer(device: UnsafeRawPointer, vertBuffer: UnsafeMutableRawPointer?) {
let device: MetalDevice = unretained(device)
if let vertBuffer {
device.renderState.vertexBuffer = unretained(vertBuffer)
} else {
device.renderState.vertexBuffer = nil
}
}
/// Requests the vertex buffer's current data to be transferred into GPU memory
/// - Parameter vertBuffer: Opaque pointer to ``MetalVertexBuffer`` instance shared with `libobs`
///
/// This function will call `gs_vertexbuffer_flush_direct` with a `nil` pointer as the data pointer.
@_cdecl("gs_vertexbuffer_flush")
public func gs_vertexbuffer_flush(vertbuffer: UnsafeRawPointer) {
gs_vertexbuffer_flush_direct(vertbuffer: vertbuffer, data: nil)
}
/// Requests the vertex buffer to be updated with the provided data and then transferred into GPU memory
/// - Parameters:
/// - vertBuffer: Opaque pointer to ``MetalVertexBuffer`` instance shared with `libobs`
/// - data: Opaque pointer to vertex buffer data set up by `libobs`
///
/// This function is called to ensure that the vertex buffer data that is contained in the memory pointed at by the
/// `data` argument is uploaded into GPU memory.
///
/// If a `nil` pointer is provided instead, the data provided to the instance during creation will be used instead.
@_cdecl("gs_vertexbuffer_flush_direct")
public func gs_vertexbuffer_flush_direct(vertbuffer: UnsafeRawPointer, data: UnsafeMutablePointer<gs_vb_data>?) {
let vertexBuffer: MetalVertexBuffer = unretained(vertbuffer)
vertexBuffer.setupBuffers(data: data)
}
/// Returns an opaque pointer to the vertex buffer data associated with the ``MetalVertexBuffer`` instance
/// - Parameter vertBuffer: Opaque pointer to ``MetalVertexBuffer`` instance shared with `libobs`
/// - Returns: Opaque pointer to index buffer data in memory
///
/// The returned opaque pointer represents the unchanged memory address that was provided for the creation of the index
/// buffer object.
///
/// > Warning: There is only limited memory safety associated with this pointer. It is implicitly owned and its
/// lifetime is managed by the ``MetalVertexBuffer``
/// instance, but it was originally created by `libobs`.
@_cdecl("gs_vertexbuffer_get_data")
public func gs_vertexbuffer_get_data(vertBuffer: UnsafeRawPointer) -> UnsafeMutablePointer<gs_vb_data>? {
let vertexBuffer: MetalVertexBuffer = unretained(vertBuffer)
return vertexBuffer.vertexData
}

View File

@@ -0,0 +1,69 @@
/******************************************************************************
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 Foundation
import Metal
/// Creates ``MetalTexture`` for use as a depth stencil attachment
/// - Parameters:
/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - width: Desired width of the texture
/// - height: Desired height of the texture
/// - color_format: Desired color format of the depth stencil attachment as described by `gs_zstencil_format`
/// - Returns: Opaque pointer to a created ``MetalTexture`` instance or a `NULL` pointer on error
@_cdecl("device_zstencil_create")
public func device_zstencil_create(device: UnsafeRawPointer, width: UInt32, height: UInt32, format: gs_zstencil_format)
-> OpaquePointer?
{
let device: MetalDevice = unretained(device)
let descriptor = MTLTextureDescriptor.init(
width: width,
height: height,
colorFormat: format
)
guard let descriptor, let texture = MetalTexture(device: device, descriptor: descriptor) else {
return nil
}
return texture.getRetained()
}
/// Gets the ``MetalTexture`` instance used as the depth stencil attachment for the current pipeline
/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs`
/// - Returns: Opaque pointer to ``MetalTexture`` instance if any is set, `nil` otherwise
@_cdecl("device_get_zstencil_target")
public func device_get_zstencil_target(device: UnsafeRawPointer) -> OpaquePointer? {
let device: MetalDevice = unretained(device)
guard let stencilAttachment = device.renderState.depthStencilAttachment else {
return nil
}
return stencilAttachment.getUnretained()
}
/// Requests deinitialization of the ``MetalTexture`` instance shared with `libobs`
/// - Parameter zstencil: Opaque pointer to ``MetalTexture`` instance shared with `libobs`
///
/// The ownership of the shared pointer is transferred into this function and the instance is placed under Swift's
/// memory management again.
@_cdecl("gs_zstencil_destroy")
public func gs_zstencil_destroy(zstencil: UnsafeRawPointer) {
let _ = retained(zstencil) as MetalTexture
}