mirror of
https://github.com/obsproject/obs-studio.git
synced 2025-12-23 22:48:02 -05:00
libobs-metal: Add Metal renderer
This commit is contained in:
@@ -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)
|
||||
|
||||
70
libobs-metal/CMakeLists.txt
Normal file
70
libobs-metal/CMakeLists.txt
Normal 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"
|
||||
)
|
||||
51
libobs-metal/CVPixelFormat+Extensions.swift
Normal file
51
libobs-metal/CVPixelFormat+Extensions.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
33
libobs-metal/MTLCullMode+Extensions.swift
Normal file
33
libobs-metal/MTLCullMode+Extensions.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
25
libobs-metal/MTLOrigin+Extensions.swift
Normal file
25
libobs-metal/MTLOrigin+Extensions.swift
Normal 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
|
||||
}
|
||||
}
|
||||
406
libobs-metal/MTLPixelFormat+Extensions.swift
Normal file
406
libobs-metal/MTLPixelFormat+Extensions.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
25
libobs-metal/MTLRegion+Extensions.swift
Normal file
25
libobs-metal/MTLRegion+Extensions.swift
Normal 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
|
||||
}
|
||||
}
|
||||
25
libobs-metal/MTLSize+Extensions.swift
Normal file
25
libobs-metal/MTLSize+Extensions.swift
Normal 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
|
||||
}
|
||||
}
|
||||
76
libobs-metal/MTLTexture+Extensions.swift
Normal file
76
libobs-metal/MTLTexture+Extensions.swift
Normal 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
|
||||
}
|
||||
}
|
||||
93
libobs-metal/MTLTextureDescriptor+Extensions.swift
Normal file
93
libobs-metal/MTLTextureDescriptor+Extensions.swift
Normal 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]
|
||||
}
|
||||
}
|
||||
36
libobs-metal/MTLTextureType+Extensions.swift
Normal file
36
libobs-metal/MTLTextureType+Extensions.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
31
libobs-metal/MTLViewport+Extensions.swift
Normal file
31
libobs-metal/MTLViewport+Extensions.swift
Normal 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
|
||||
}
|
||||
}
|
||||
308
libobs-metal/MetalBuffer.swift
Normal file
308
libobs-metal/MetalBuffer.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
786
libobs-metal/MetalDevice.swift
Normal file
786
libobs-metal/MetalDevice.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
126
libobs-metal/MetalError.swift
Normal file
126
libobs-metal/MetalError.swift
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
libobs-metal/MetalRenderState.swift
Normal file
79
libobs-metal/MetalRenderState.swift
Normal 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>()
|
||||
}
|
||||
27
libobs-metal/MetalShader+Extensions.swift
Normal file
27
libobs-metal/MetalShader+Extensions.swift
Normal 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
|
||||
}
|
||||
}
|
||||
287
libobs-metal/MetalShader.swift
Normal file
287
libobs-metal/MetalShader.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
65
libobs-metal/MetalStageBuffer.swift
Normal file
65
libobs-metal/MetalStageBuffer.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
433
libobs-metal/MetalTexture.swift
Normal file
433
libobs-metal/MetalTexture.swift
Normal 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
1603
libobs-metal/OBSShader.swift
Normal file
File diff suppressed because it is too large
Load Diff
125
libobs-metal/OBSSwapChain.swift
Normal file
125
libobs-metal/OBSSwapChain.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
25
libobs-metal/Sequence+Hashable.swift
Normal file
25
libobs-metal/Sequence+Hashable.swift
Normal 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 }
|
||||
}
|
||||
}
|
||||
486
libobs-metal/libobs+Extensions.swift
Normal file
486
libobs-metal/libobs+Extensions.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
34
libobs-metal/libobs+SignalHandlers.swift
Normal file
34
libobs-metal/libobs+SignalHandlers.swift
Normal 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)
|
||||
}
|
||||
32
libobs-metal/libobs-metal-Bridging-Header.h
Normal file
32
libobs-metal/libobs-metal-Bridging-Header.h
Normal 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";
|
||||
158
libobs-metal/metal-indexbuffer.swift
Normal file
158
libobs-metal/metal-indexbuffer.swift
Normal 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
|
||||
}
|
||||
}
|
||||
100
libobs-metal/metal-samplerstate.swift
Normal file
100
libobs-metal/metal-samplerstate.swift
Normal 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
|
||||
}
|
||||
593
libobs-metal/metal-shader.swift
Normal file
593
libobs-metal/metal-shader.swift
Normal 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
|
||||
}
|
||||
130
libobs-metal/metal-stagesurf.swift
Normal file
130
libobs-metal/metal-stagesurf.swift
Normal 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
|
||||
}
|
||||
985
libobs-metal/metal-subsystem.swift
Normal file
985
libobs-metal/metal-subsystem.swift
Normal 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
|
||||
}
|
||||
269
libobs-metal/metal-swapchain.swift
Normal file
269
libobs-metal/metal-swapchain.swift
Normal 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
|
||||
}
|
||||
528
libobs-metal/metal-texture2d.swift
Normal file
528
libobs-metal/metal-texture2d.swift
Normal 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
|
||||
}
|
||||
}
|
||||
113
libobs-metal/metal-texture3d.swift
Normal file
113
libobs-metal/metal-texture3d.swift
Normal 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
|
||||
}
|
||||
97
libobs-metal/metal-unimplemented.swift
Normal file
97
libobs-metal/metal-unimplemented.swift
Normal 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
|
||||
}
|
||||
115
libobs-metal/metal-vertexbuffer.swift
Normal file
115
libobs-metal/metal-vertexbuffer.swift
Normal 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
|
||||
}
|
||||
69
libobs-metal/metal-zstencilbuffer.swift
Normal file
69
libobs-metal/metal-zstencilbuffer.swift
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user