Projects
home:darix:branches:Multimedia
obs-studio
Sign Up
Log In
Username
Password
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
Expand all
Collapse all
Changes of Revision 5
View file
obs-studio.changes
Changed
@@ -1,4 +1,46 @@ ------------------------------------------------------------------- +Fri Sep 05 21:38:34 UTC 2025 - darix <packman@nordisch.org> + +- Update to version 32.0.0~beta2: + * libobs-metal: Added README file for current state of implementation + * libobs: Update default draw effect to also provide D65P3 conversion + * frontend: Add Metal to available list of renderers in basic settings + * libobs-metal: Add Metal renderer + * CI: Update macOS runner for building to use Xcode 16.4 + * libobs: Add prerequisites for Metal and Swift support + * cmake: Enable DEBUG flag for Swift + * CI: Update swift-format configuration with more explicit rules + * libobs: Remove Qt5 module check + * Revert "libobs/util: Reject plugins linking Qt5 library for Linux" + * Revert "libobs: Assume Qt 6, always warn about Qt 5 plugins" + * Revert "libobs/util: Prevent locking mutex in child process when checking Qt5" + * cmake: Remove library compat symlink on Linux + * Revert "cmake: Avoid breaking ABI through major version bump on Linux" + * Revert "cmake: Use fixed SOVERSION everywhere" + * libobs: Re-include groups in obs_enum_scenes + * CI,build-aux: Use rebuilt CEF on Linux and macOS + * frontend/data: Remove unused context bar string + * shared/idian: Make checked status of collapsible rows public + * shared/idian: Make title and description common to all row types + * shared/idian: Pass "this" instead of "=" to lambda + * frontend: Include OBSIdianPlayground MOC + * frontend/themes: Remove Idian test rule + * rtmp-services: Remove defunct servers/services + * nv-filters: Guard function introduced in sdk >= 1.6.0 + * libobs: Use RTLD_NOW to load modules + * libobs: Fix comment typo + * frontend: Fix plugin manager module type loading + * libobs: Set module for outputs + * libobs: Remove unused obs_*_info module pointer + * libobs: Fix scene and group load state + * plugins: Ensure that graphics device type checks use graphics context + * frontend: Change crash sentinel location to separate subdirectory + * obs-ffmpeg: Null-check url query parameters + * frontend: Fix plugin manager config loading crash + * frontend: Do not enable crash log upload without log file + * frontend: Remove unneeded argument from log upload privacy notice + +------------------------------------------------------------------- Thu Aug 28 23:12:47 UTC 2025 - darix <packman@nordisch.org> - Update to version 32.0.0~beta1:
View file
obs-studio.spec
Changed
@@ -39,7 +39,7 @@ %endif Name: obs-studio -Version: 32.0.0~beta1 +Version: 32.0.0~beta2 Release: 0 Summary: A recording/broadcasting program Group: Productivity/Multimedia/Video/Editors and Convertors
View file
_service
Changed
@@ -1,7 +1,7 @@ <services> <service name="tar_scm" mode="manual"> <param name="versionformat">@PARENT_TAG@</param> - <param name="revision">32.0.0-beta1</param> + <param name="revision">32.0.0-beta2</param> <param name="url">https://github.com/obsproject/obs-studio.git</param> <param name="versionrewrite-pattern">(\.\d+)-(a-z.*)</param> <param name="versionrewrite-replacement">\1~\2</param>
View file
_servicedata
Changed
@@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/obsproject/obs-studio.git</param> - <param name="changesrevision">1239ef5f830455973af97a4cf87ed66f1ace818f</param> + <param name="changesrevision">ee212e863d6c7dcda13e039f8b710a388d1a81a5</param> </service> </servicedata> \ No newline at end of file
View file
obs-studio-32.0.0~beta1.tar.xz/.github/workflows/analyze-project.yaml -> obs-studio-32.0.0~beta2.tar.xz/.github/workflows/analyze-project.yaml
Changed
@@ -57,8 +57,8 @@ : Set Up Environment 🔧 if (( ${+RUNNER_DEBUG} )) setopt XTRACE - print '::group::Enable Xcode 16.1' - sudo xcode-select --switch /Applications/Xcode_16.1.0.app/Contents/Developer + print '::group::Enable Xcode 16.4' + sudo xcode-select --switch /Applications/Xcode_16.4.app/Contents/Developer print '::endgroup::' print '::group::Clean Homebrew Environment'
View file
obs-studio-32.0.0~beta1.tar.xz/.github/workflows/build-project.yaml -> obs-studio-32.0.0~beta2.tar.xz/.github/workflows/build-project.yaml
Changed
@@ -80,8 +80,8 @@ : Set Up Environment 🔧 if (( ${+RUNNER_DEBUG} )) setopt XTRACE - print '::group::Enable Xcode 16.1' - sudo xcode-select --switch /Applications/Xcode_16.1.0.app/Contents/Developer + print '::group::Enable Xcode 16.4' + sudo xcode-select --switch /Applications/Xcode_16.4.app/Contents/Developer print '::endgroup::' print '::group::Clean Homebrew Environment'
View file
obs-studio-32.0.0~beta1.tar.xz/.swift-format -> obs-studio-32.0.0~beta2.tar.xz/.swift-format
Changed
@@ -8,5 +8,16 @@ "maximumBlankLines": 1, "respectsExistingLineBreaks": true, "lineBreakBeforeControlFlowKeywords": false, - "lineBreakBeforeEachArgument": false + "lineBreakBeforeEachArgument": false, + "lineBreakBeforeEachGenericRequirement": false, + "lineBreakBetweenDeclarationAttributes": false, + "prioritizeKeepingFunctionOutputTogether": false, + "indentConditionalCompilationBlocks": true, + "lineBreakAroundMultilineExpressionChainComponents": false, + "fileScopedDeclarationPrivacy": {"accessLevel": "private"}, + "indentSwitchCaseLabels": false, + "spacesAroundRangeFormationOperators": false, + "noAssignmentInExpressions": { "allowedFunctions" : "XCTAssertNoThrow" }, + "multiElementCollectionTrailingCommas": true, + "indentBlankLines": false, }
View file
obs-studio-32.0.0~beta1.tar.xz/CMakeLists.txt -> obs-studio-32.0.0~beta2.tar.xz/CMakeLists.txt
Changed
@@ -25,6 +25,9 @@ add_subdirectory(libobs-winrt) endif() add_subdirectory(libobs-opengl) +if(OS_MACOS) + add_subdirectory(libobs-metal) +endif() add_subdirectory(plugins) add_subdirectory(test/test-input)
View file
obs-studio-32.0.0~beta1.tar.xz/build-aux/modules/99-cef.json -> obs-studio-32.0.0~beta2.tar.xz/build-aux/modules/99-cef.json
Changed
@@ -17,8 +17,8 @@ "sources": { "type": "archive", - "url": "https://cdn-fastly.obsproject.com/downloads/cef_binary_6533_linux_x86_64_v5.tar.xz", - "sha256": "df38ef6d8078895953d224a58dd811b83110b4f8644c5cd2b6246d04b0023ee6" + "url": "https://cdn-fastly.obsproject.com/downloads/cef_binary_6533_linux_x86_64_v6.tar.xz", + "sha256": "7963335519a19ccdc5233f7334c5ab023026e2f3e9a0cc417007c09d86608146" } }
View file
obs-studio-32.0.0~beta1.tar.xz/buildspec.json -> obs-studio-32.0.0~beta2.tar.xz/buildspec.json
Changed
@@ -30,18 +30,18 @@ "baseUrl": "https://cdn-fastly.obsproject.com/downloads", "label": "Chromium Embedded Framework", "hashes": { - "macos-x86_64": "94ff9dffd60a83a3cd30851eb5b55c1c8d00e7626bd2b8b22d332fc013643105", - "macos-arm64": "7c6c1c3706e08f470fb09c57bcfa49e760ba4a00dc59467e8c2c0d83bc99f0d5", - "ubuntu-x86_64": "df38ef6d8078895953d224a58dd811b83110b4f8644c5cd2b6246d04b0023ee6", - "ubuntu-aarch64": "b1ebcedbe63657c7f38a4d547398a4759544f75d955777eea386052abc9c9228", + "macos-x86_64": "37bf7571a48c5dfa8519817e4a90a3503a0eb30f9eadd68f4c3e783e363f272a", + "macos-arm64": "429b50e74f6c174dcfe2f14d8204b54add497eaafe117f7b69ce6bb2354d2626", + "ubuntu-x86_64": "7963335519a19ccdc5233f7334c5ab023026e2f3e9a0cc417007c09d86608146", + "ubuntu-aarch64": "642514469eaa29a5c887891084d2e73f7dc2d7405f7dfa7726b2dbc24b309999", "windows-x64": "922efbda1f2f8be9e5b2754d878a14d90afc81f04e94fc9101a7513e2b5cecc1", "windows-arm64": "df9df4bd85826b4c071c6db404fd59cf93efd9c58ec3ab64e204466ae19bb02a" }, "revision": { - "macos-x86_64": 4, - "macos-arm64": 4, - "ubuntu-x86_64": 5, - "ubuntu-aarch64": 5, + "macos-x86_64": 5, + "macos-arm64": 5, + "ubuntu-x86_64": 6, + "ubuntu-aarch64": 6, "windows-x64": 2 } }
View file
obs-studio-32.0.0~beta1.tar.xz/cmake/linux/helpers.cmake -> obs-studio-32.0.0~beta2.tar.xz/cmake/linux/helpers.cmake
Changed
@@ -19,7 +19,6 @@ endwhile() get_target_property(target_type ${target} TYPE) - set(OBS_SOVERSION 30) if(target_type STREQUAL EXECUTABLE) install(TARGETS ${target} RUNTIME DESTINATION "${OBS_EXECUTABLE_DESTINATION}" COMPONENT Runtime) @@ -60,8 +59,8 @@ set_target_properties( ${target} PROPERTIES - VERSION ${OBS_SOVERSION} - SOVERSION ${OBS_SOVERSION} + VERSION ${OBS_VERSION_CANONICAL} + SOVERSION ${OBS_VERSION_MAJOR} BUILD_RPATH "${OBS_OUTPUT_DIR}/$<CONFIG>/${OBS_LIBRARY_DESTINATION}" INSTALL_RPATH "${OBS_LIBRARY_RPATH}" ) @@ -85,36 +84,15 @@ COMMENT "Copy ${target} to library directory (${OBS_LIBRARY_DESTINATION})" VERBATIM ) - - if(target STREQUAL libobs OR target STREQUAL obs-frontend-api) - install( - FILES "$<TARGET_FILE_DIR:${target}>/$<TARGET_FILE_PREFIX:${target}>$<TARGET_FILE_BASE_NAME:${target}>.so.0" - DESTINATION "${OBS_LIBRARY_DESTINATION}" - ) - - add_custom_command( - TARGET ${target} - POST_BUILD - COMMAND - "${CMAKE_COMMAND}" -E create_symlink - "$<TARGET_FILE_PREFIX:${target}>$<TARGET_FILE_BASE_NAME:${target}>.so.${OBS_SOVERSION}" - "$<TARGET_FILE_PREFIX:${target}>$<TARGET_FILE_BASE_NAME:${target}>.so.0" - COMMAND - "${CMAKE_COMMAND}" -E copy_if_different - "$<TARGET_FILE_DIR:${target}>/$<TARGET_FILE_PREFIX:${target}>$<TARGET_FILE_BASE_NAME:${target}>.so.0" - "${OBS_OUTPUT_DIR}/$<CONFIG>/${OBS_LIBRARY_DESTINATION}" - COMMENT "Create symlink for legacy ${target}" - ) - endif() elseif(target_type STREQUAL MODULE_LIBRARY) if(target STREQUAL obs-browser) - set_target_properties(${target} PROPERTIES VERSION 0 SOVERSION ${OBS_SOVERSION}) + set_target_properties(${target} PROPERTIES VERSION 0 SOVERSION ${OBS_VERSION_MAJOR}) else() set_target_properties( ${target} PROPERTIES VERSION 0 - SOVERSION ${OBS_SOVERSION} + SOVERSION ${OBS_VERSION_MAJOR} BUILD_RPATH "${OBS_OUTPUT_DIR}/$<CONFIG>/${OBS_LIBRARY_DESTINATION}" INSTALL_RPATH "${OBS_MODULE_RPATH}" )
View file
obs-studio-32.0.0~beta1.tar.xz/cmake/macos/compilerconfig.cmake -> obs-studio-32.0.0~beta2.tar.xz/cmake/macos/compilerconfig.cmake
Changed
@@ -88,7 +88,7 @@ # * -Wno-non-virtual-dtor add_compile_definitions( - $<$<NOT:$<COMPILE_LANGUAGE:Swift>>:$<$<CONFIG:DEBUG>:DEBUG>> + $<$<CONFIG:DEBUG>:DEBUG> $<$<NOT:$<COMPILE_LANGUAGE:Swift>>:$<$<CONFIG:DEBUG>:_DEBUG>> $<$<NOT:$<COMPILE_LANGUAGE:Swift>>:SIMDE_ENABLE_OPENMP> )
View file
obs-studio-32.0.0~beta1.tar.xz/frontend/OBSApp.cpp -> obs-studio-32.0.0~beta2.tar.xz/frontend/OBSApp.cpp
Changed
@@ -292,8 +292,13 @@ #if _WIN32 config_set_default_string(appConfig, "Video", "Renderer", "Direct3D 11"); #else +#if defined(__APPLE__) && defined(__aarch64__) + // TODO: Change this value to "Metal" once the renderer has reached production quality + config_set_default_string(appConfig, "Video", "Renderer", "OpenGL"); +#else config_set_default_string(appConfig, "Video", "Renderer", "OpenGL"); #endif +#endif #ifdef _WIN32 config_set_default_bool(appConfig, "Audio", "DisableAudioDucking", true); @@ -1077,9 +1082,17 @@ const char *OBSApp::GetRenderModule() const { +#if defined(_WIN32) const char *renderer = config_get_string(appConfig, "Video", "Renderer"); return (astrcmpi(renderer, "Direct3D 11") == 0) ? DL_D3D11 : DL_OPENGL; +#elif defined(__APPLE__) && defined(__aarch64__) + const char *renderer = config_get_string(appConfig, "Video", "Renderer"); + + return (astrcmpi(renderer, "Metal (Experimental)") == 0) ? DL_METAL : DL_OPENGL; +#else + return DL_OPENGL; +#endif } static bool StartupOBS(const char *locale, profiler_name_store_t *store)
View file
obs-studio-32.0.0~beta1.tar.xz/frontend/data/locale/en-US.ini -> obs-studio-32.0.0~beta2.tar.xz/frontend/data/locale/en-US.ini
Changed
@@ -1490,7 +1490,6 @@ # Context Bar ContextBar.NoSelectedSource="No source selected" -ContextBar.ResetTransform="Reset Transform" # Context Bar Media Controls ContextBar.MediaControls.PlayMedia="Play Media"
View file
obs-studio-32.0.0~beta1.tar.xz/frontend/data/themes/Yami.obt -> obs-studio-32.0.0~beta2.tar.xz/frontend/data/themes/Yami.obt
Changed
@@ -2230,13 +2230,6 @@ border-bottom-right-radius: var(--border_radius); } -idian--Row > QWidget QLabel { - background: red; - margin: 4px; - font-weight: 500; - max-height: var(--input_height); -} - idian--Row > QLabel.description { font-size: var(--font_small); color: var(--text_muted);
View file
obs-studio-32.0.0~beta1.tar.xz/frontend/dialogs/LogUploadDialog.cpp -> obs-studio-32.0.0~beta2.tar.xz/frontend/dialogs/LogUploadDialog.cpp
Changed
@@ -45,8 +45,7 @@ ui->stackedWidget->setCurrentIndex(DialogPage::Start); - ui->privacyNotice->setText( - QTStr("LogUploadDialog.Labels.PrivacyNotice").arg(QTStr("LogUploadDialog.Buttons.ConfirmUpload"))); + ui->privacyNotice->setText(QTStr("LogUploadDialog.Labels.PrivacyNotice")); if (uploadType_ == LogFileType::CrashLog) { ui->analyzeURL->hide();
View file
obs-studio-32.0.0~beta1.tar.xz/frontend/dialogs/OBSIdianPlayground.cpp -> obs-studio-32.0.0~beta2.tar.xz/frontend/dialogs/OBSIdianPlayground.cpp
Changed
@@ -21,6 +21,8 @@ #include <QTimer> +#include "moc_OBSIdianPlayground.cpp" + using namespace idian; OBSIdianPlayground::OBSIdianPlayground(QWidget *parent) : QDialog(parent), ui(new Ui_OBSIdianPlayground) @@ -101,7 +103,8 @@ tmp->setSuffix(new ToggleSwitch); test->properties()->addRow(tmp); - CollapsibleRow *tmp2 = new CollapsibleRow("A Collapsible row!", this); + CollapsibleRow *tmp2 = new CollapsibleRow(this); + tmp2->setTitle("A Collapsible row!"); tmp2->setCheckable(true); test->addRow(tmp2);
View file
obs-studio-32.0.0~beta1.tar.xz/frontend/plugin-manager/PluginManager.cpp -> obs-studio-32.0.0~beta2.tar.xz/frontend/plugin-manager/PluginManager.cpp
Changed
@@ -94,7 +94,15 @@ auto modulesFile = getConfigFilePath_(); if (std::filesystem::exists(modulesFile)) { std::ifstream jsonFile(modulesFile); - nlohmann::json data = nlohmann::json::parse(jsonFile); + nlohmann::json data; + try { + data = nlohmann::json::parse(jsonFile); + } catch (const nlohmann::json::parse_error &error) { + modules_.clear(); + blog(LOG_ERROR, "Error loading modules config file: %s", error.what()); + blog(LOG_ERROR, "Generating new config file."); + return; + } modules_.clear(); for (auto it : data) { ModuleInfo obsModule; @@ -191,7 +199,7 @@ i = 0; while (obs_enum_output_types(i, &output_id)) { i += 1; - obs_module_t *obsModule = obs_source_get_module(output_id); + obs_module_t *obsModule = obs_output_get_module(output_id); if (!obsModule) { continue; } @@ -208,7 +216,7 @@ i = 0; while (obs_enum_encoder_types(i, &encoder_id)) { i += 1; - obs_module_t *obsModule = obs_source_get_module(encoder_id); + obs_module_t *obsModule = obs_encoder_get_module(encoder_id); if (!obsModule) { continue; } @@ -225,7 +233,7 @@ i = 0; while (obs_enum_service_types(i, &service_id)) { i += 1; - obs_module_t *obsModule = obs_source_get_module(service_id); + obs_module_t *obsModule = obs_service_get_module(service_id); if (!obsModule) { continue; }
View file
obs-studio-32.0.0~beta1.tar.xz/frontend/settings/OBSBasicSettings.cpp -> obs-studio-32.0.0~beta2.tar.xz/frontend/settings/OBSBasicSettings.cpp
Changed
@@ -609,10 +609,23 @@ ui->processPriority->addItem(QTStr(pri.name), pri.val); #else +#if defined(__APPLE__) && defined(__aarch64__) + delete ui->adapterLabel; + delete ui->adapter; + + ui->adapterLabel = nullptr; + ui->adapter = nullptr; +#else delete ui->rendererLabel; delete ui->renderer; delete ui->adapterLabel; delete ui->adapter; + + ui->rendererLabel = nullptr; + ui->renderer = nullptr; + ui->adapterLabel = nullptr; + ui->adapter = nullptr; +#endif delete ui->processPriorityLabel; delete ui->processPriority; delete ui->enableNewSocketLoop; @@ -624,10 +637,6 @@ #endif delete ui->disableAudioDucking; - ui->rendererLabel = nullptr; - ui->renderer = nullptr; - ui->adapterLabel = nullptr; - ui->adapter = nullptr; ui->processPriorityLabel = nullptr; ui->processPriority = nullptr; ui->enableNewSocketLoop = nullptr; @@ -1384,16 +1393,21 @@ void OBSBasicSettings::LoadRendererList() { -#ifdef _WIN32 +#if defined(_WIN32) || (defined(__APPLE__) && defined(__aarch64__)) const char *renderer = config_get_string(App()->GetAppConfig(), "Video", "Renderer"); - +#ifdef _WIN32 ui->renderer->addItem(QT_UTF8("Direct3D 11")); - if (opt_allow_opengl || strcmp(renderer, "OpenGL") == 0) + if (opt_allow_opengl || strcmp(renderer, "OpenGL") == 0) { ui->renderer->addItem(QT_UTF8("OpenGL")); - - int idx = ui->renderer->findText(QT_UTF8(renderer)); - if (idx == -1) - idx = 0; + } +#else + ui->renderer->addItem(QT_UTF8("OpenGL")); + ui->renderer->addItem(QT_UTF8("Metal (Experimental)")); +#endif + int index = ui->renderer->findText(QT_UTF8(renderer)); + if (index == -1) { + index = 0; + } // the video adapter selection is not currently implemented, hide for now // to avoid user confusion. was previously protected by @@ -1403,7 +1417,7 @@ ui->adapter = nullptr; ui->adapterLabel = nullptr; - ui->renderer->setCurrentIndex(idx); + ui->renderer->setCurrentIndex(index); #endif } @@ -3130,10 +3144,13 @@ { QString lastMonitoringDevice = config_get_string(main->Config(), "Audio", "MonitoringDeviceId"); -#ifdef _WIN32 - if (WidgetChanged(ui->renderer)) +#if defined(_WIN32) || (defined(__APPLE__) && defined(__aarch64__)) + if (WidgetChanged(ui->renderer)) { config_set_string(App()->GetAppConfig(), "Video", "Renderer", QT_TO_UTF8(ui->renderer->currentText())); + } +#endif +#ifdef _WIN32 std::string priority = QT_TO_UTF8(ui->processPriority->currentData().toString()); config_set_string(App()->GetAppConfig(), "General", "ProcessPriority", priority.c_str()); if (main->Active())
View file
obs-studio-32.0.0~beta1.tar.xz/frontend/utility/CrashHandler.cpp -> obs-studio-32.0.0~beta2.tar.xz/frontend/utility/CrashHandler.cpp
Changed
@@ -33,8 +33,8 @@ namespace { -constexpr std::string_view crashSentinelPath = "obs-studio"; -constexpr std::string_view crashSentinelPrefix = "crash_sentinel_"; +constexpr std::string_view crashSentinelPath = "obs-studio/.sentinel"; +constexpr std::string_view crashSentinelPrefix = "run_"; constexpr std::string_view crashUploadURL = "https://obsproject.com/logs/upload"; #ifndef NDEBUG @@ -141,6 +141,10 @@ { CrashLogUpdateResult result = updateLocalCrashLogState(); + if (result == CrashLogUpdateResult::NotAvailable) { + return false; + } + bool hasNewCrashLog = (result == CrashLogUpdateResult::Updated); bool hasNoLogUrl = lastCrashLogURL_.empty(); @@ -153,6 +157,10 @@ std::filesystem::path lastLocalCrashLogFile = findLastCrashLog(); + if (lastLocalCrashLogFile.empty() && lastCrashLogFile_.empty()) { + return CrashLogUpdateResult::NotAvailable; + } + if (lastLocalCrashLogFile != lastCrashLogFile_) { lastCrashLogFile_ = std::move(lastLocalCrashLogFile); lastCrashLogFileName_ = lastCrashLogFile_.filename().u8string(); @@ -182,8 +190,14 @@ std::filesystem::path crashSentinelPath = crashSentinelFile_.parent_path(); if (!std::filesystem::exists(crashSentinelPath)) { - blog(LOG_ERROR, "Crash sentinel location '%s' does not exist", crashSentinelPath.u8string().c_str()); - return; + try { + std::filesystem::create_directory(crashSentinelPath); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, + "Crash sentinel location '%s' does not exist and unable to create directory:\n%s.", + crashSentinelPath.u8string().c_str(), error.what()); + return; + } } for (const auto &entry : std::filesystem::directory_iterator(crashSentinelPath)) {
View file
obs-studio-32.0.0~beta1.tar.xz/frontend/utility/CrashHandler.hpp -> obs-studio-32.0.0~beta2.tar.xz/frontend/utility/CrashHandler.hpp
Changed
@@ -67,7 +67,7 @@ std::filesystem::path getCrashLogDirectory() const; void uploadLastCrashLog(); - enum class CrashLogUpdateResult { InvalidResult, NotUpdated, Updated }; + enum class CrashLogUpdateResult { InvalidResult, NotAvailable, NotUpdated, Updated }; private: void checkCrashState();
View file
obs-studio-32.0.0~beta1.tar.xz/frontend/widgets/OBSBasic_MainControls.cpp -> obs-studio-32.0.0~beta2.tar.xz/frontend/widgets/OBSBasic_MainControls.cpp
Changed
@@ -27,6 +27,9 @@ #include <dialogs/OBSBasicInteraction.hpp> #include <dialogs/OBSBasicProperties.hpp> #include <dialogs/OBSBasicTransform.hpp> +#ifdef ENABLE_IDIAN_PLAYGROUND +#include <dialogs/OBSIdianPlayground.hpp> +#endif #include <dialogs/OBSLogViewer.hpp> #ifdef __APPLE__ #include <dialogs/OBSPermissions.hpp> @@ -42,10 +45,6 @@ #endif #include <wizards/AutoConfig.hpp> -#ifdef ENABLE_IDIAN_PLAYGROUND -#include "dialogs/OBSIdianPlayground.hpp" -#endif - #include <qt-wrappers.hpp> #include <QDesktopServices>
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal
Added
+(directory)
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/CMakeLists.txt
Added
@@ -0,0 +1,70 @@ +cmake_minimum_required(VERSION 3.28...3.30) + +add_library(libobs-metal SHARED) +add_library(OBS::libobs-metal ALIAS libobs-metal) + +target_sources( + libobs-metal + PRIVATE + CVPixelFormat+Extensions.swift + MTLCullMode+Extensions.swift + MTLOrigin+Extensions.swift + MTLPixelFormat+Extensions.swift + MTLRegion+Extensions.swift + MTLSize+Extensions.swift + MTLTexture+Extensions.swift + MTLTextureDescriptor+Extensions.swift + MTLTextureType+Extensions.swift + MTLViewport+Extensions.swift + MetalBuffer.swift + MetalDevice.swift + MetalError.swift + MetalRenderState.swift + MetalShader+Extensions.swift + MetalShader.swift + MetalStageBuffer.swift + MetalTexture.swift + OBSShader.swift + OBSSwapChain.swift + Sequence+Hashable.swift + libobs+Extensions.swift + libobs+SignalHandlers.swift + libobs-metal-Bridging-Header.h + metal-indexbuffer.swift + metal-samplerstate.swift + metal-shader.swift + metal-stagesurf.swift + metal-subsystem.swift + metal-swapchain.swift + metal-texture2d.swift + metal-texture3d.swift + metal-unimplemented.swift + metal-vertexbuffer.swift + metal-zstencilbuffer.swift +) + +target_link_libraries(libobs-metal PRIVATE OBS::libobs) + +target_enable_feature(libobs "Metal renderer") + +set_property(SOURCE OBSMetalRenderer.swift APPEND PROPERTY COMPILE_FLAGS -emit-objc-header) + +set_target_properties_obs( + libobs-metal + PROPERTIES FOLDER core + VERSION 0 + PREFIX "" +) + +set_target_xcode_properties( + libobs-metal + PROPERTIES SWIFT_VERSION 6.0 + CLANG_ENABLE_OBJC_ARC YES + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION YES + GCC_WARN_SHADOW YES + CLANG_ENABLE_MODULES YES + CLANG_MODULES_AUTOLINK YES + GCC_STRICT_ALIASING YES + DEFINES_MODULE YES + SWIFT_OBJC_BRIDGING_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/libobs-metal-Bridging-Header.h" +)
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/CVPixelFormat+Extensions.swift
Added
@@ -0,0 +1,51 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +import CoreVideo +import Metal + +extension OSType { + /// Conversion of CoreVideo pixel formats into corresponding Metal pixel formats + var mtlFormat: MTLPixelFormat? { + switch self { + case kCVPixelFormatType_OneComponent8: + return .r8Unorm + case kCVPixelFormatType_OneComponent16Half: + return .r16Float + case kCVPixelFormatType_OneComponent32Float: + return .r32Float + case kCVPixelFormatType_TwoComponent8: + return .rg8Unorm + case kCVPixelFormatType_TwoComponent16Half: + return .rg16Float + case kCVPixelFormatType_TwoComponent32Float: + return .rg32Float + case kCVPixelFormatType_32BGRA: + return .bgra8Unorm + case kCVPixelFormatType_32RGBA: + return .rgba8Unorm + case kCVPixelFormatType_64RGBAHalf: + return .rgba16Float + case kCVPixelFormatType_128RGBAFloat: + return .rgba32Float + case kCVPixelFormatType_ARGB2101010LEPacked: + return .bgr10a2Unorm + default: + return nil + } + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/MTLCullMode+Extensions.swift
Added
@@ -0,0 +1,33 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +import Foundation +import Metal + +extension MTLCullMode { + /// Conversion of the cull mode into its corresponding `libobs` type + var obsMode: gs_cull_mode { + switch self { + case .back: + return GS_BACK + case .front: + return GS_FRONT + default: + return GS_NEITHER + } + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/MTLOrigin+Extensions.swift
Added
@@ -0,0 +1,25 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +import Foundation +import Metal + +extension MTLOrigin: @retroactive Equatable { + public static func == (lhs: MTLOrigin, rhs: MTLOrigin) -> Bool { + lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/MTLPixelFormat+Extensions.swift
Added
@@ -0,0 +1,406 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +import CoreGraphics +import CoreVideo +import Foundation +import Metal + +extension MTLPixelFormat { + /// Property to check whether the pixel format is an 8-bit format + var is8Bit: Bool { + switch self { + case .a8Unorm, .r8Unorm, .r8Snorm, .r8Uint, .r8Sint: + return true + case .r8Unorm_srgb: + return true + default: + return false + } + } + + /// Property to check whether the pixel format is a 16-bit format + var is16Bit: Bool { + switch self { + case .r16Unorm, .r16Snorm, .r16Uint, .r16Sint: + return true + case .rg8Unorm, .rg8Snorm, .rg8Uint, .rg8Sint: + return true + case .rg16Float: + return true + case .rg8Unorm_srgb: + return true + default: + return false + } + } + + /// Property to check whether the pixel format is a packed 16-bit format + var isPacked16Bit: Bool { + switch self { + case .b5g6r5Unorm, .a1bgr5Unorm, .abgr4Unorm, .bgr5A1Unorm: + return true + default: + return false + } + } + + /// Property to check whether the pixel format is a 32-bit format + var is32Bit: Bool { + switch self { + case .r32Uint, .r32Sint: + return true + case .r32Float: + return true + case .rg16Unorm, .rg16Snorm, .rg16Uint, .rg16Sint: + return true + case .rg16Float: + return true + case .rgba8Unorm, .rgba8Snorm, .rgba8Uint, .rgba8Sint, .bgra8Unorm: + return true + case .rgba8Unorm_srgb, .bgra8Unorm_srgb: + return true + default: + return false + } + } + + /// Property to check whether the pixel format is a packed 32-bit format + var isPacked32Bit: Bool { + switch self { + case .rgb10a2Unorm, .rgb10a2Uint, .bgr10a2Unorm: + return true + case .rg11b10Float: + return true + case .rgb9e5Float: + return true + case .bgr10_xr, .bgr10_xr_srgb: + return true + default: + return false + } + } + + /// Property to check whether the pixel format is a 64-bit format + var is64Bit: Bool { + switch self { + case .rg32Uint, .rg32Sint: + return true + case .rg32Float: + return true + case .rgba16Unorm, .rgba16Snorm, .rgba16Uint, .rgba16Sint: + return true + case .rgba16Float: + return true + case .bgra10_xr, .bgra10_xr_srgb: + return true + default: + return false + } + } + + /// Property to check whether the pixel format is a 128-bit format + var is128Bit: Bool { + switch self { + case .rgba32Uint, .rgba32Sint: + return true + case .rgba32Float: + return true + default: + return false + } + } + + /// Property to check whether the pixel format will trigger automatic sRGB gamma encoding and decoding + var isSRGB: Bool { + switch self { + case .r8Unorm_srgb, .rg8Unorm_srgb, .bgra8Unorm_srgb, .rgba8Unorm_srgb: + return true + case .bgr10_xr_srgb, .bgra10_xr_srgb: + return true + case .astc_4x4_srgb, .astc_5x4_srgb, .astc_5x5_srgb, .astc_6x5_srgb, .astc_6x6_srgb, .astc_8x5_srgb, + .astc_8x6_srgb, .astc_8x8_srgb, .astc_10x5_srgb, .astc_10x6_srgb, .astc_10x8_srgb, .astc_10x10_srgb, + .astc_12x10_srgb, .astc_12x12_srgb: + return true + case .bc1_rgba_srgb, .bc2_rgba_srgb, .bc3_rgba_srgb, .bc7_rgbaUnorm_srgb: + return true + case .eac_rgba8_srgb, .etc2_rgb8, .etc2_rgb8a1_srgb: + return true + default: + return false + } + } + + /// Property to check whether the pixel format is an extended dynamic range (EDR) format + var isEDR: Bool { + switch self { + case .bgr10_xr, .bgra10_xr, .bgr10_xr_srgb, .bgra10_xr_srgb: + return true + default: + return false + } + } + + /// Property to check whether the pixel format uses a form of texture compression + var isCompressed: Bool { + switch self { + // S3TC + case .bc1_rgba, .bc1_rgba_srgb, .bc2_rgba, .bc2_rgba_srgb, .bc3_rgba, .bc3_rgba_srgb: + return true + // RGTC + case .bc4_rUnorm, .bc4_rSnorm, .bc5_rgUnorm, .bc5_rgSnorm: + return true + // BPTC + case .bc6H_rgbFloat, .bc6H_rgbuFloat, .bc7_rgbaUnorm, .bc7_rgbaUnorm_srgb: + return true + // EAC + case .eac_r11Unorm, .eac_r11Snorm, .eac_rg11Unorm, .eac_rg11Snorm, .eac_rgba8, .eac_rgba8_srgb: + return true + // ETC + case .etc2_rgb8, .etc2_rgb8_srgb, .etc2_rgb8a1, .etc2_rgb8a1_srgb: + return true + // ASTC + case .astc_4x4_srgb, .astc_5x4_srgb, .astc_5x5_srgb, .astc_6x5_srgb, .astc_6x6_srgb, .astc_8x5_srgb, + .astc_8x6_srgb, .astc_8x8_srgb, .astc_10x5_srgb, .astc_10x6_srgb, .astc_10x8_srgb, .astc_10x10_srgb, + .astc_12x10_srgb, .astc_12x12_srgb, .astc_4x4_ldr, .astc_5x4_ldr, .astc_5x5_ldr, .astc_6x5_ldr, + .astc_6x6_ldr, .astc_8x5_ldr, .astc_8x6_ldr, .astc_8x8_ldr, .astc_10x5_ldr, .astc_10x6_ldr, .astc_10x8_ldr, + .astc_10x10_ldr, .astc_12x10_ldr, .astc_12x12_ldr: + return true + // ASTC HDR + case .astc_4x4_hdr, .astc_5x4_hdr, .astc_5x5_hdr, .astc_6x5_hdr, .astc_6x6_hdr, .astc_8x5_hdr, .astc_8x6_hdr, + .astc_8x8_hdr, .astc_10x5_hdr, .astc_10x6_hdr, .astc_10x8_hdr, .astc_10x10_hdr, .astc_12x10_hdr, + .astc_12x12_hdr: + return true + default: + return false + } + } + + /// Property to check whether the pixel format is a depth buffer format + var isDepth: Bool { + switch self { + case .depth16Unorm, .depth32Float: + return true + default: + return false + } + } + + /// Property to check whether the pixel format is depth stencil format + var isStencil: Bool { + switch self { + case .stencil8, .x24_stencil8, .x32_stencil8, .depth24Unorm_stencil8, .depth32Float_stencil8: + return true + default: + return false + } + } + + /// Returns number of color components used by the pixel format + var componentCount: Int? { + switch self { + case .a8Unorm, .r8Unorm, .r8Snorm, .r8Uint, .r8Sint, .r8Unorm_srgb: + return 1 + case .r16Unorm, .r16Snorm, .r16Uint, .r16Sint, .r16Float: + return 1 + case .r32Uint, .r32Sint, .r32Float: + return 1 + case .rg8Unorm, .rg8Snorm, .rg8Uint, .rg8Sint, .rg8Unorm_srgb: + return 2 + case .rg16Unorm, .rg16Snorm, .rg16Uint, .rg16Sint: + return 2 + case .rg32Uint, .rg32Sint, .rg32Float: + return 2 + case .b5g6r5Unorm, .rg11b10Float, .rgb9e5Float, .gbgr422, .bgrg422: + return 3 + case .a1bgr5Unorm, .abgr4Unorm, .bgr5A1Unorm: + return 4 + case .rgba8Unorm, .rgba8Snorm, .rgba8Uint, .rgba8Sint, .rgba8Unorm_srgb, .bgra8Unorm, .bgra8Unorm_srgb: + return 4 + case .rgb10a2Unorm, .rgb10a2Uint, .bgr10a2Unorm, .bgr10_xr, .bgr10_xr_srgb: + return 4 + case .rgba16Unorm, .rgba16Snorm, .rgba16Uint, .rgba16Sint, .rgba16Float: + return 4 + case .rgba32Uint, .rgba32Sint, .rgba32Float: + return 4 + case .bc4_rUnorm, .bc4_rSnorm, .eac_r11Unorm, .eac_r11Snorm: + return 1 + case .bc5_rgUnorm, .bc5_rgSnorm: + return 2 + case .bc6H_rgbFloat, .bc6H_rgbuFloat, .eac_rg11Unorm, .eac_rg11Snorm, .etc2_rgb8, .etc2_rgb8_srgb: + return 3 + case .bc1_rgba, .bc1_rgba_srgb, .bc2_rgba, .bc2_rgba_srgb, .bc3_rgba, .bc3_rgba_srgb, .etc2_rgb8a1, + .etc2_rgb8a1_srgb, .eac_rgba8, .eac_rgba8_srgb, .bc7_rgbaUnorm, .bc7_rgbaUnorm_srgb: + return 4 + default: + return nil + } + } + + /// Conversion of pixel format to `libobs` color format + var gsColorFormat: gs_color_format { + switch self { + case .a8Unorm: + return GS_A8 + case .r8Unorm: + return GS_R8 + case .rgba8Unorm: + return GS_RGBA + case .bgra8Unorm: + return GS_BGRA + case .rgb10a2Unorm: + return GS_R10G10B10A2 + case .rgba16Unorm: + return GS_RGBA16 + case .r16Unorm: + return GS_R16 + case .rgba16Float: + return GS_RGBA16F + case .rgba32Float: + return GS_RGBA32F + case .rg16Float: + return GS_RG16F + case .rg32Float: + return GS_RG32F + case .r16Float: + return GS_R16F + case .r32Float: + return GS_R32F + case .bc1_rgba: + return GS_DXT1 + case .bc2_rgba: + return GS_DXT3 + case .bc3_rgba: + return GS_DXT5 + default: + return GS_UNKNOWN + } + } + + /// Returns the bits per pixel based on the pixel format + var bitsPerPixel: Int? { + if self.is8Bit { + return 8 + } else if self.is16Bit || self.isPacked16Bit { + return 16 + } else if self.is32Bit || self.isPacked32Bit { + return 32 + } else if self.is64Bit { + return 64 + } else if self.is128Bit { + return 128 + } else { + return nil + } + } + + /// Returns the bytes per pixel based on the pixel format + var bytesPerPixel: Int? { + if self.is8Bit { + return 1 + } else if self.is16Bit || self.isPacked16Bit { + return 2 + } else if self.is32Bit { + return 4 + } else if self.isPacked32Bit { + switch self { + case .rgb10a2Unorm, .rgb10a2Uint, .bgr10a2Unorm, .rg11b10Float, .rgb9e5Float: + return 4 + case .bgr10_xr, .bgr10_xr_srgb: + return 8 + default: + return nil + } + } else if self.is64Bit { + return 8 + } else { + return nil + } + } + + /// Returns the bytes used per color component of the pixel format + var bitsPerComponent: Int? { + if !self.isCompressed { + if let bitsPerPixel = self.bitsPerPixel, let componentCount = self.componentCount { + return bitsPerPixel / componentCount + } + } + + return nil + } +} + +extension MTLPixelFormat { + /// Converts the pixel format into a compatible CoreGraphics color space + var colorSpace: CGColorSpace? { + switch self { + case .a8Unorm, .r8Unorm, .r8Snorm, .r8Uint, .r8Sint, .r16Unorm, .r16Snorm, .r16Uint, .r16Sint, + .r16Float, .r32Uint, .r32Sint, .r32Float: + return CGColorSpace(name: CGColorSpace.linearGray) + case .rg8Unorm, .rg8Snorm, .rg8Uint, .rg8Sint, .rgba8Unorm, .rgba8Snorm, .rgba8Uint, .rgba8Sint, .bgra8Unorm, + .rgba16Unorm, .rgba16Snorm, .rgba16Uint, .rgba16Sint: + return CGColorSpace(name: CGColorSpace.linearSRGB) + case .rg8Unorm_srgb, .rgba8Unorm_srgb, .bgra8Unorm_srgb: + return CGColorSpace(name: CGColorSpace.sRGB) + case .rg16Float, .rg32Float, .rgba16Float, .rgba32Float, .bgr10_xr, .bgr10a2Unorm: + return CGColorSpace(name: CGColorSpace.extendedLinearSRGB) + case .bgr10_xr_srgb: + return CGColorSpace(name: CGColorSpace.extendedSRGB) + default: + return nil + } + } +} + +extension MTLPixelFormat { + /// Initializes a ``MTLPixelFormat`` with a compatible CoreVideo video pixel format + init?(osType: OSType) { + guard let pixelFormat = osType.mtlFormat else { + return nil + } + + self = pixelFormat + } + + /// Conversion of the pixel format into a compatible CoreVideo video pixel format + var videoPixelFormat: OSType? { + switch self { + case .r8Unorm, .r8Unorm_srgb: + return kCVPixelFormatType_OneComponent8 + case .r16Float: + return kCVPixelFormatType_OneComponent16Half + case .r32Float: + return kCVPixelFormatType_OneComponent32Float + case .rg8Unorm, .rg8Unorm_srgb: + return kCVPixelFormatType_TwoComponent8 + case .rg16Float: + return kCVPixelFormatType_TwoComponent16Half + case .rg32Float: + return kCVPixelFormatType_TwoComponent32Float + case .bgra8Unorm, .bgra8Unorm_srgb: + return kCVPixelFormatType_32BGRA + case .rgba8Unorm, .rgba8Unorm_srgb: + return kCVPixelFormatType_32RGBA + case .rgba16Float: + return kCVPixelFormatType_64RGBAHalf + case .rgba32Float: + return kCVPixelFormatType_128RGBAFloat + default: + return nil + } + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/MTLRegion+Extensions.swift
Added
@@ -0,0 +1,25 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +import Foundation +import Metal + +extension MTLRegion: @retroactive Equatable { + public static func == (lhs: MTLRegion, rhs: MTLRegion) -> Bool { + lhs.origin == rhs.origin && lhs.size == rhs.size + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/MTLSize+Extensions.swift
Added
@@ -0,0 +1,25 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +import Foundation +import Metal + +extension MTLSize: @retroactive Equatable { + public static func == (lhs: MTLSize, rhs: MTLSize) -> Bool { + lhs.width == rhs.width && lhs.height == rhs.height && lhs.depth == rhs.depth + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/MTLTexture+Extensions.swift
Added
@@ -0,0 +1,76 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +import Foundation +import Metal + +extension MTLTexture { + /// Creates an opaque pointer of a ``MTLTexture`` instance and increases the reference count. + /// - Returns: Opaque pointer for the ``MTLTexture`` + func getRetained() -> OpaquePointer { + let retained = Unmanaged.passRetained(self).toOpaque() + + return OpaquePointer(retained) + } + + /// Creates an opaque pointer of a ``MTLTexture`` instance without increasing the reference count. + /// - Returns: Opaque pointer for the ``MTLTexture`` + func getUnretained() -> OpaquePointer { + let unretained = Unmanaged.passUnretained(self).toOpaque() + + return OpaquePointer(unretained) + } +} + +extension MTLTexture { + /// Convenience property to get the texture's size as a ``MTLSize`` object + var size: MTLSize { + .init( + width: self.width, + height: self.height, + depth: self.depth + ) + } + + /// Convenience property to get the texture's region as a ``MTLRegion`` object + var region: MTLRegion { + .init( + origin: .init(x: 0, y: 0, z: 0), + size: self.size + ) + } + + /// Gets a new ``MTLTextureDescriptor`` instance with the properties of the texture + var descriptor: MTLTextureDescriptor { + let descriptor = MTLTextureDescriptor() + + descriptor.textureType = self.textureType + descriptor.pixelFormat = self.pixelFormat + descriptor.width = self.width + descriptor.height = self.height + descriptor.depth = self.depth + descriptor.mipmapLevelCount = self.mipmapLevelCount + descriptor.sampleCount = self.sampleCount + descriptor.arrayLength = self.arrayLength + descriptor.storageMode = self.storageMode + descriptor.cpuCacheMode = self.cpuCacheMode + descriptor.usage = self.usage + descriptor.allowGPUOptimizedContents = self.allowGPUOptimizedContents + + return descriptor + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/MTLTextureDescriptor+Extensions.swift
Added
@@ -0,0 +1,93 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +import Metal + +extension MTLTextureDescriptor { + + /// Convenience initializer for a texture descriptor with `libobs` data + /// - Parameters: + /// - type: Metal texture type + /// - width: Width of texture + /// - height: Height of texture + /// - depth: Depth of texture + /// - colorFormat: `libobs` color format for the texture + /// - levels: Mip map levels + /// - flags: Additional usage flags as `libobs` bitfield + convenience init?( + type: MTLTextureType, width: UInt32, height: UInt32, depth: UInt32, colorFormat: gs_color_format, + levels: UInt32, flags: UInt32 + ) { + let arrayLength: Int + switch type { + case .type2D: + arrayLength = 1 + case .type3D: + arrayLength = 1 + case .typeCube: + arrayLength = 6 + default: + assertionFailure("MTLTextureDescriptor: Unsupported texture type for libobs initializer") + return nil + } + + self.init() + + self.textureType = type + self.pixelFormat = colorFormat.mtlFormat + self.width = Int(width) + self.height = Int(height) + self.depth = Int(depth) + self.sampleCount = 1 + self.arrayLength = arrayLength + self.cpuCacheMode = .defaultCache + self.allowGPUOptimizedContents = true + self.hazardTrackingMode = .default + + if (Int32(flags) & GS_BUILD_MIPMAPS) != 0 { + self.mipmapLevelCount = Int(levels) + } else { + self.mipmapLevelCount = 1 + } + + if (Int32(flags) & GS_RENDER_TARGET) != 0 { + self.storageMode = .private + self.usage = .shaderRead, .renderTarget + } else { + self.storageMode = .shared + self.usage = .shaderRead + } + } + + convenience init?(width: UInt32, height: UInt32, colorFormat: gs_zstencil_format) { + self.init() + + self.textureType = .type2D + self.pixelFormat = colorFormat.mtlFormat + self.width = Int(width) + self.height = Int(height) + self.depth = 1 + self.sampleCount = 1 + self.arrayLength = 1 + self.cpuCacheMode = .defaultCache + self.allowGPUOptimizedContents = true + self.hazardTrackingMode = .default + self.mipmapLevelCount = 1 + self.storageMode = .private + self.usage = .shaderRead + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/MTLTextureType+Extensions.swift
Added
@@ -0,0 +1,36 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +import Foundation +import Metal + +extension MTLTextureType { + /// Converts the Metal texture type into a compatible `libobs` texture type or `nil` if no compatible mapping is + /// possible. + var gsTextureType: gs_texture_type? { + switch self { + case .type2D: + return GS_TEXTURE_2D + case .type3D: + return GS_TEXTURE_3D + case .typeCube: + return GS_TEXTURE_CUBE + default: + return nil + } + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/MTLViewport+Extensions.swift
Added
@@ -0,0 +1,31 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +import Foundation +import Metal + +extension MTLViewport: @retroactive Equatable { + /// Checks two ``MTLViewPort`` objects for equality + /// - Parameters: + /// - lhs: First ``MTLViewPort``object + /// - rhs: Second ``MTLViewPort`` object + /// - Returns: `true` if the dimensions and origins of both view ports match, `false` otherwise. + public static func == (lhs: MTLViewport, rhs: MTLViewport) -> Bool { + lhs.width == rhs.width && lhs.height == rhs.height && lhs.originX == rhs.originX + && lhs.originY == rhs.originY + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/MetalBuffer.swift
Added
@@ -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(colorValues0) / 255.0, + y: Float(colorValues1) / 255.0, + z: Float(colorValues2) / 255.0, + w: Float(colorValues3) / 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: &uvCoordinatestextureSlot, data: $0, count: textureVertex.width * numVertices, + dynamic: isDynamic) + } + + #if DEBUG + uvCoordinatestextureSlot?.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 = uvCoordinatesi { + bufferList.append(uvCoordinate) + } + } + } + } + + return bufferList + } + + deinit { + gs_vbdata_destroy(vertexData) + } +} + +final class MetalIndexBuffer: MetalBuffer { + public var indexData: UnsafeMutableRawPointer? + public var count: Int + public var type: MTLIndexType + + var indices: MTLBuffer? + + init(device: MetalDevice, type: MTLIndexType, data: UnsafeMutableRawPointer?, count: Int, dynamic: Bool) { + self.indexData = data + self.count = count + self.type = type + + super.init(device: device, isDynamic: dynamic) + + if !dynamic { + setupBuffers() + } + } + + /// Sets up buffer objects for the data provided in the provided memory location + /// - Parameter data: Pointer to bytes representing index buffer data + /// + /// The provided memory location is expected to provide bytes represnting index buffer data as either unsigned + /// 16-bit integers or unsigned 32-bit integers. The size depends on the type used to create the + /// ``MetalIndexBuffer`` instance. + public func setupBuffers(_ data: UnsafeMutableRawPointer? = nil) { + guard let indexData = data ?? indexData else { + assertionFailure("MetalIndexBuffer: Unable to generate MTLBuffer without buffer data") + return + } + + let byteSize = + switch type { + case .uint16: 2 * count + case .uint32: 4 * count + @unknown default: + fatalError("MTLIndexType \(type) is not supported") + } + + indexData.withMemoryRebound(to: UInt8.self, capacity: byteSize) { + createOrUpdateBuffer(buffer: &indices, data: $0, count: byteSize, dynamic: isDynamic) + } + + #if DEBUG + if !isDynamic { + indices?.label = "Index buffer static data" + } else { + indices?.label = "Index buffer dynamic data" + } + #endif + } + + deinit { + bfree(indexData) + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/MetalDevice.swift
Added
@@ -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.colorAttachments0.isBlendingEnabled = false + clearPipelineDescriptor.vertexFunction = nopVertexFunction + clearPipelineDescriptor.fragmentFunction = nil + clearPipelineDescriptor.inputPrimitiveTopology = .point + + setupSignalHandlers() + setupDisplayLink() + } + + func dispatchSignal(type: MetalSignalType) { + if let callback = obsSignalCallbackstype { + 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.colorAttachments0.pixelFormat = renderTarget.sRGBtexture!.pixelFormat + } else { + pipelineDescriptor.colorAttachments0.pixelFormat = renderTarget.texture.pixelFormat + } + + pipelineDescriptor.colorAttachments0.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 = pipelinesstateHash { + 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 = depthStencilStatesdepthStateHash { + 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.colorAttachments0.loadAction = .clear + renderPassDescriptor.colorAttachments0.storeAction = .store + renderPassDescriptor.colorAttachments0.clearColor = state.clearColor + } else { + renderPassDescriptor.colorAttachments0.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.colorAttachments0.texture = renderTarget.sRGBtexture! + } else { + renderPassDescriptor.colorAttachments0.texture = renderTarget.texture + } + + renderTarget.hasPendingWrites = true + renderState.inFlightRenderTargets.insert(renderTarget) + + renderPassDescriptor.colorAttachments0.level = 0 + renderPassDescriptor.colorAttachments0.slice = 0 + renderPassDescriptor.colorAttachments0.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.colorAttachments0.pixelFormat = renderTarget.sRGBtexture!.pixelFormat + renderPassDescriptor.colorAttachments0.texture = renderTarget.sRGBtexture! + } else { + renderPipelineDescriptor.colorAttachments0.pixelFormat = renderTarget.texture.pixelFormat + renderPassDescriptor.colorAttachments0.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.colorAttachments0.loadAction = .load + renderPassDescriptor.depthAttachment.loadAction = .load + renderPassDescriptor.stencilAttachment.loadAction = .load + + let stateHash = renderState.pipelineDescriptor.hashValue + + let pipelineState: MTLRenderPipelineState + + if let state = pipelinesstateHash { + 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 = depthStencilStatesdepthStateHash { + depthStencilState = state + } else { + guard let state = device.makeDepthStencilState(descriptor: renderState.depthStencilDescriptor) else { + throw MetalError.MTLDeviceError.depthStencilStateCreationFailure + } + + depthStencilStates.updateValue(state, forKey: depthStateHash) + depthStencilState = state + } + + commandEncoder.setDepthStencilState(depthStencilState) + + var gsViewMatrix: matrix4 = matrix4() + gs_matrix_get(&gsViewMatrix) + + let viewMatrix = matrix_float4x4( + rows: + SIMD4(gsViewMatrix.x.x, gsViewMatrix.x.y, gsViewMatrix.x.z, gsViewMatrix.x.w), + SIMD4(gsViewMatrix.y.x, gsViewMatrix.y.y, gsViewMatrix.y.z, gsViewMatrix.y.w), + SIMD4(gsViewMatrix.z.x, gsViewMatrix.z.y, gsViewMatrix.z.z, gsViewMatrix.z.w), + SIMD4(gsViewMatrix.t.x, gsViewMatrix.t.y, gsViewMatrix.t.z, gsViewMatrix.t.w), + + ) + + renderState.viewProjectionMatrix = (viewMatrix * renderState.projectionMatrix) + + if let viewProjectionUniform = vertexShader.viewProjection { + viewProjectionUniform.setParameter( + data: &renderState.viewProjectionMatrix, size: MemoryLayout<matrix_float4x4>.size) + } + + vertexShader.uploadShaderParameters(encoder: commandEncoder) + fragmentShader.uploadShaderParameters(encoder: commandEncoder) + + if let vertexBuffer = renderState.vertexBuffer { + let buffers = vertexBuffer.getShaderBuffers(for: vertexShader) + + commandEncoder.setVertexBuffers( + buffers, + offsets: .init(repeating: 0, count: buffers.count), + range: 0..<buffers.count) + } else { + commandEncoder.setVertexBuffer(fallbackVertexBuffer, offset: 0, index: 0) + } + + for (index, texture) in renderState.textures.enumerated() { + if let texture { + commandEncoder.setFragmentTexture(texture, index: index) + } + } + + for (index, samplerState) in renderState.samplers.enumerated() { + if let samplerState { + commandEncoder.setFragmentSamplerState(samplerState, index: index) + } + } + + if let indexBuffer = renderState.indexBuffer, + let bufferData = indexBuffer.indices + { + commandEncoder.drawIndexedPrimitives( + type: primitiveType, + indexCount: (vertexCount > 0) ? vertexCount : indexBuffer.count, + indexType: indexBuffer.type, + indexBuffer: bufferData, + indexBufferOffset: 0 + ) + } else { + if let vertexBuffer = renderState.vertexBuffer, + let vertexData = vertexBuffer.vertexData + { + commandEncoder.drawPrimitives( + type: primitiveType, + vertexStart: vertexStart, + vertexCount: vertexData.pointee.num + ) + } else { + commandEncoder.drawPrimitives( + type: primitiveType, + vertexStart: vertexStart, + vertexCount: vertexCount + ) + } + } + + commandEncoder.endEncoding() + } + + /// Creates a command buffer on the render state if none exists + func ensureCommandBuffer() throws { + if renderState.commandBuffer == nil { + guard let buffer = commandQueue.makeCommandBuffer() else { + throw MetalError.MTLCommandQueueError.commandBufferCreationFailure + } + + renderState.commandBuffer = buffer + } + } + + /// Updates a memory fence used on the GPU to signal that the current render target (which is associated with a + /// ``OBSSwapChain`` is available for other GPU commands. + /// + /// This is necessary as the final output of projectors needs to be blitted into the drawables provided by the + /// `CAMetalLayer` of each ``OBSSwapChain`` at the screen refresh interval, but projectors are usually rendered + /// using tens of seperate little draw calls. + /// + /// Thus a virtual "display render stage" state is maintained by the Metal renderer, which is started when a + /// ``OBSSwapChain`` instance is loaded by `libobs` and ended when `device_end_scene` is called. + func finishDisplayRenderStage() { + let buffer = commandQueue.makeCommandBufferWithUnretainedReferences() + let encoder = buffer?.makeBlitCommandEncoder() + + guard let buffer, let encoder, let swapChain = renderState.swapChain else { + return + } + + encoder.updateFence(swapChain.fence) + encoder.endEncoding() + buffer.commit() + } + + /// Ensures that all encoded render commands in the current command buffer are committed to the command queue for + /// execution on the GPU. + /// + /// This is particularly important when textures (or texture data) is to be blitted into other textures or buffers, + /// as pending GPU commands in the existing buffer need to run before any commands that rely on the result of these + /// draw commands to have taken place. + /// + /// Within the same queue this is ensured by Metal itself, but requires the commands to be encoded and committed + /// in the desired order. + func finishPendingCommands() { + guard let commandBuffer = renderState.commandBuffer, commandBuffer.status != .committed else { + return + } + + commandBuffer.commit() + + renderState.inFlightRenderTargets.forEach { + $0.hasPendingWrites = false + } + + renderState.inFlightRenderTargets.removeAll(keepingCapacity: true) + renderState.commandBuffer = nil + } + + /// Copies the contents of a texture into another texture of identical dimensions + /// - Parameters: + /// - source: Source texture to copy from + /// - destination: Destination texture to copy to + /// + /// This function requires both textures to have been created with the same dimensions, otherwise the copy + /// operation will fail. + /// + /// If the source texture has pending writes (e.g., it was used as the render target for a clear or draw command), + /// then the current command buffer will be committed to ensure that the blit command encoded by this function + /// happens after the pending commands. + func copyTexture(source: MetalTexture, destination: MetalTexture) throws { + if source.hasPendingWrites { + finishPendingCommands() + } + + try ensureCommandBuffer() + + let buffer = renderState.commandBuffer! + let encoder = buffer.makeBlitCommandEncoder() + + guard let encoder else { + throw MetalError.MTLCommandQueueError.commandBufferCreationFailure + } + + encoder.copy(from: source.texture, to: destination.texture) + encoder.endEncoding() + } + + /// Copies the contents of a texture into a texture for CPU access + /// - Parameters: + /// - source: Source texture to copy from + /// - destination: Destination texture to copy to + /// + /// This function requires both texture to have been created with the same dimensions, otherwise the copy operation + /// will fail. + /// + /// If the source texture has pending writes (e.g., it was used as the render target for a clear or draw command), + /// then the current command buffer will be comitted to ensure that the blit command encoded by this function + /// happens after the pending commands. + /// + /// > Important: This function differs from ``copyTexture`` insofar as it will wait for the completion of all + /// commands in the command queue to ensure that the GPU has actually completed the blit into the destination + /// texture. + func stageTexture(source: MetalTexture, destination: MetalTexture) throws { + if source.hasPendingWrites { + finishPendingCommands() + } + + let buffer = commandQueue.makeCommandBufferWithUnretainedReferences() + let encoder = buffer?.makeBlitCommandEncoder() + + guard let buffer, let encoder else { + throw MetalError.MTLCommandQueueError.commandBufferCreationFailure + } + + encoder.copy(from: source.texture, to: destination.texture) + encoder.endEncoding() + buffer.commit() + buffer.waitUntilCompleted() + } + + /// Copies the contents of a texture into a buffer for CPU access + /// - Parameters: + /// - source: Source texture to copy from + /// - destination: Destination buffer to copy to + /// + /// This function requires that the destination buffer has been created with enough capacity to hold the source + /// textures pixel data. + /// + /// If the source texture has pending writes (e.g., it was used as the render target for a clear or draw command), + /// then the current command buffer will be comitted to ensure that the blit command encoded by this function + /// happens after the pending commands. + /// + /// > Important: This function will wait for the completion of all commands in the command queue to ensure that the + /// GPU has actually completed the blit into the destination buffer. + /// + func stageTextureToBuffer(source: MetalTexture, destination: MetalStageBuffer) throws { + if source.hasPendingWrites { + finishPendingCommands() + } + + let buffer = commandQueue.makeCommandBufferWithUnretainedReferences() + let encoder = buffer?.makeBlitCommandEncoder() + + guard let buffer, let encoder else { + throw MetalError.MTLCommandQueueError.commandBufferCreationFailure + } + + encoder.copy( + from: source.texture, + sourceSlice: 0, + sourceLevel: 0, + sourceOrigin: .init(x: 0, y: 0, z: 0), + sourceSize: .init(width: source.texture.width, height: source.texture.height, depth: 1), + to: destination.buffer, + destinationOffset: 0, + destinationBytesPerRow: destination.width * destination.format.bytesPerPixel!, + destinationBytesPerImage: 0) + + encoder.endEncoding() + buffer.commit() + buffer.waitUntilCompleted() + } + + /// Copies the contents of a buffer into a texture for GPU access + /// - Parameters: + /// - source: Source buffer to copy from + /// - destination: Destination texture to copy to + /// + /// This function requires that the destination texture has been created with enough capacity to hold the source + /// buffer pixel data. + /// + func stageBufferToTexture(source: MetalStageBuffer, destination: MetalTexture) throws { + let buffer = commandQueue.makeCommandBufferWithUnretainedReferences() + let encoder = buffer?.makeBlitCommandEncoder() + + guard let buffer, let encoder else { + throw MetalError.MTLCommandQueueError.commandBufferCreationFailure + } + + encoder.copy( + from: source.buffer, + sourceOffset: 0, + sourceBytesPerRow: source.width * source.format.bytesPerPixel!, + sourceBytesPerImage: 0, + sourceSize: .init(width: source.width, height: source.height, depth: 1), + to: destination.texture, + destinationSlice: 0, + destinationLevel: 0, + destinationOrigin: .init(x: 0, y: 0, z: 0) + ) + + encoder.endEncoding() + buffer.commit() + buffer.waitUntilScheduled() + } + + /// Copies a region from a source texture into a region of a destination texture + /// - Parameters: + /// - source: Source texture to copy from + /// - sourceRegion: Region of the source texture to copy from + /// - destination: Destination texture to copy to + /// - destinationRegion: Destination region to copy into + /// + /// This function requires that the destination region fits within the dimensions of the destination texture, + /// otherwise the copy operation will fail. + /// + /// If the source texture has pending writes (e.g., it was used as the render target for a clear or draw command), + /// then the current command buffer will be comitted to ensure that the blit command encoded by this function + /// happens after the pending commands. + /// + func copyTextureRegion( + source: MetalTexture, sourceRegion: MTLRegion, destination: MetalTexture, destinationRegion: MTLRegion + ) throws { + if source.hasPendingWrites { + finishPendingCommands() + } + + let buffer = commandQueue.makeCommandBufferWithUnretainedReferences() + let encoder = buffer?.makeBlitCommandEncoder() + + guard let buffer, let encoder else { + throw MetalError.MTLCommandQueueError.commandBufferCreationFailure + } + + encoder.copy( + from: source.texture, + sourceSlice: 0, + sourceLevel: 0, + sourceOrigin: sourceRegion.origin, + sourceSize: sourceRegion.size, + to: destination.texture, + destinationSlice: 0, + destinationLevel: 0, + destinationOrigin: destinationRegion.origin + ) + + encoder.endEncoding() + buffer.commit() + } + + /// Stops the `CVDisplayLink` used by the ``MetalDevice`` instance + func shutdown() { + guard let displayLink else { return } + + CVDisplayLinkStop(displayLink) + self.displayLink = nil + } + + deinit { + shutdown() + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/MetalError.swift
Added
@@ -0,0 +1,126 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +enum MetalError { + enum MTLCommandQueueError: Error, CustomStringConvertible { + case commandBufferCreationFailure + + var description: String { + switch self { + case .commandBufferCreationFailure: + "MTLCommandQueue failed to create command buffer" + } + } + } + + enum MTLDeviceError: Error, CustomStringConvertible { + case commandQueueCreationFailure + case displayLinkCreationFailure + case bufferCreationFailure(String) + case shaderCompilationFailure(String) + case pipelineStateCreationFailure + case depthStencilStateCreationFailure + case samplerStateCreationFailure + + var description: String { + switch self { + case .commandQueueCreationFailure: + "MTLDevice failed to create command queue" + case .displayLinkCreationFailure: + "MTLDevice failed to create CVDisplayLink for projector output" + case .bufferCreationFailure(_): + "MTLDevice failed to create buffer" + case .shaderCompilationFailure(_): + "MTLDevice failed to create shader library and function" + case .pipelineStateCreationFailure: + "MTLDevice failed to create render pipeline state" + case .depthStencilStateCreationFailure: + "MTLDevice failed to create depth stencil state" + case .samplerStateCreationFailure: + "MTLDevice failed to create sampler state with provided descriptor" + } + } + } + + enum MTLCommandBufferError: Error, CustomStringConvertible { + case encoderCreationFailure + + var description: String { + switch self { + case .encoderCreationFailure: + "MTLCommandBuffer failed to create command encoder" + } + } + } + + enum MetalShaderError: Error, CustomStringConvertible { + case missingVertexDescriptor + case missingSamplerDescriptors + + var description: String { + switch self { + case .missingVertexDescriptor: + "MetalShader of type vertex requires a vertex descriptor" + case .missingSamplerDescriptors: + "MetalShader of type fragment requires at least a single sampler descriptor" + } + } + } + + enum OBSShaderParserError: Error, CustomStringConvertible { + case parseFail(String) + case unsupportedType + case missingNextToken + case unexpectedToken + case missingMainFunction + + var description: String { + switch self { + case .parseFail: + "Failed to parse provided shader string" + case .unsupportedType: + "Provided GS type is not convertible to a Metal type" + case .missingNextToken: + "Required next token not found in parser token collection" + case .unexpectedToken: + "Required next token had unexpected type in parser token collection" + case .missingMainFunction: + "Shader has no main function" + } + } + } + + enum OBSShaderError: Error, CustomStringConvertible { + case unsupportedType + case parseFail(String) + case parseError(String) + case transpileError(String) + + var description: String { + switch self { + case .unsupportedType: + "Unsupported Metal shader type" + case .parseFail(_): + "OBS shader parser failed to parse effect" + case .parseError(_): + "OBS shader parser encountered warnings and/or errors while parsing effect" + case .transpileError(_): + "Transpiling OBS effects file into MSL shader failed" + } + } + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/MetalRenderState.swift
Added
@@ -0,0 +1,79 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +import Foundation +import Metal +import simd + +/// The MetalRenderState struct emulates a state object like Direct3D's `ID3D11DeviceContext`, holding references to +/// elements of a render pipeline that would be considered the "current" variant of each. +/// +/// Typical "current" state elements include (but are not limited to): +/// +/// * Variant of the render target for linear color writes +/// * Variant of the render target for color writes with automatic sRGB gamma encoding +/// * View matrix and view projection matrix +/// * Vertex buffer and optional index buffer +/// * Depth stencil attachment +/// * Vertex shader +/// * Fragment shader +/// * View port size +/// * Cull mode +/// +/// These references are swapped out by OBS for each "scene" and "scene items" within it before issuing draw calls, +/// thus actual pipelines need to be created "on demand" based on the pipeline descriptor and stored in a cache to +/// avoid the cost of pipeline validation on consecutive render passes. +struct MetalRenderState { + var viewMatrix: matrix_float4x4 + var projectionMatrix: matrix_float4x4 + var viewProjectionMatrix: matrix_float4x4 + + var renderTarget: MetalTexture? + var sRGBrenderTarget: MetalTexture? + var depthStencilAttachment: MetalTexture? + var isRendertargetChanged = false + + var vertexBuffer: MetalVertexBuffer? + var indexBuffer: MetalIndexBuffer? + + var vertexShader: MetalShader? + var fragmentShader: MetalShader? + + var viewPort = MTLViewport() + var cullMode = MTLCullMode.none + + var scissorRectEnabled: Bool + var scissorRect: MTLScissorRect? + + var gsColorSpace: gs_color_space + var useSRGBGamma = false + + var swapChain: OBSSwapChain? + var isInDisplaysRenderStage = false + + var pipelineDescriptor = MTLRenderPipelineDescriptor() + var clearPipelineDescriptor = MTLRenderPipelineDescriptor() + var renderPassDescriptor = MTLRenderPassDescriptor() + var depthStencilDescriptor = MTLDepthStencilDescriptor() + var commandBuffer: MTLCommandBuffer? + + var textures = MTLTexture?(repeating: nil, count: Int(GS_MAX_TEXTURES)) + var samplers = MTLSamplerState?(repeating: nil, count: Int(GS_MAX_TEXTURES)) + + var projections = matrix_float4x4() + var inFlightRenderTargets = Set<MetalTexture>() +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/MetalShader+Extensions.swift
Added
@@ -0,0 +1,27 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +import Foundation +import Metal + +/// Adds the comparison operator to make ``MetalShader`` instances comparable. Comparison is based on the source string +/// and function type. +extension MetalShader: Equatable { + static func == (lhs: MetalShader, rhs: MetalShader) -> Bool { + return lhs.source == rhs.source && lhs.function.functionType == rhs.function.functionType + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/MetalShader.swift
Added
@@ -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.texturesuniform.textureSlot = texture.sRGBtexture! + } else { + device.renderState.texturesuniform.textureSlot = texture.texture + } + } + + if let samplerState = uniform.samplerState { + device.renderState.samplersuniform.textureSlot = samplerState + uniform.samplerState = nil + } + } else { + if uniform.hasUpdates { + let startIndex = uniform.byteOffset + let endIndex = uniform.byteOffset + currentValues.count + + uniformData.replaceSubrange(startIndex..<endIndex, with: currentValues) + } + } + + uniform.hasUpdates = false + } + + /// Creates a new buffer with the provided data or updates an existing buffer with the provided data + /// - Parameters: + /// - buffer: Reference to a buffer variable to either receive the new buffer or provide an existing buffer + /// - data: Raw byte data array + private func createOrUpdateBuffer(buffer: inout MTLBuffer?, data: inout UInt8) { + guard let device = self.device else { return } + + let size = MemoryLayout<UInt8>.size * data.count + let alignedSize = (size + 0x0F) & ~0x0F + + if buffer != nil { + if buffer!.length == alignedSize { + buffer!.contents().copyMemory(from: data, byteCount: size) + return + } + } + + buffer = device.device.makeBuffer(bytes: data, length: alignedSize) + } + + /// Sets uniform data for a current render encoder either directly as a buffer + /// - Parameter encoder: `MTLRenderCommandEncoder` for a render pass that requires the uniform data + /// + /// Uniform data will be uploaded at index 30 (the very last available index) and is available as a single + /// contiguous block of data. Uniforms are declared as structs in the Metal Shaders and explicitly passed into + /// each function that requires access to them. + func uploadShaderParameters(encoder: MTLRenderCommandEncoder) { + for var uniform in uniforms { + updateUniform(uniform: &uniform) + } + + guard uniformSize > 0 else { + return + } + + switch function.functionType { + case .vertex: + switch uniformData.count { + case 0..<4096: encoder.setVertexBytes(&uniformData, length: uniformData.count, index: 30) + default: + createOrUpdateBuffer(buffer: &uniformBuffer, data: &uniformData) + #if DEBUG + uniformBuffer?.label = "Vertex shader uniform buffer" + #endif + encoder.setVertexBuffer(uniformBuffer, offset: 0, index: 30) + } + case .fragment: + switch uniformData.count { + case 0..<4096: encoder.setFragmentBytes(&uniformData, length: uniformData.count, index: 30) + default: + createOrUpdateBuffer(buffer: &uniformBuffer, data: &uniformData) + #if DEBUG + uniformBuffer?.label = "Fragment shader uniform buffer" + #endif + encoder.setFragmentBuffer(uniformBuffer, offset: 0, index: 30) + } + default: + fatalError("MetalShader: Unsupported shader type \(function.functionType)") + } + } + + /// Gets an opaque pointer for the ``MetalShader`` instance and increases its reference count by one + /// - Returns: `OpaquePointer` to class instance + /// + /// > Note: Use this method when the instance is to be shared via an `OpaquePointer` and needs to be retained. Any + /// opaque pointer shared this way needs to be converted into a retained reference again to ensure automatic + /// deinitialization by the Swift runtime. + func getRetained() -> OpaquePointer { + let retained = Unmanaged.passRetained(self).toOpaque() + + return OpaquePointer(retained) + } + + /// Gets an opaque pointer for the ``MetalShader`` instance without increasing its reference count + /// - Returns: `OpaquePointer` to class instance + func getUnretained() -> OpaquePointer { + let unretained = Unmanaged.passUnretained(self).toOpaque() + + return OpaquePointer(unretained) + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/MetalStageBuffer.swift
Added
@@ -0,0 +1,65 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +import Foundation +import Metal + +class MetalStageBuffer { + let device: MetalDevice + let buffer: MTLBuffer + let format: MTLPixelFormat + let width: Int + let height: Int + + init?(device: MetalDevice, width: Int, height: Int, format: MTLPixelFormat) { + self.device = device + self.width = width + self.height = height + self.format = format + + guard let bytesPerPixel = format.bytesPerPixel, + let buffer = device.device.makeBuffer( + length: width * height * bytesPerPixel, + options: .storageModeShared + ) + else { + return nil + } + + self.buffer = buffer + } + + /// Gets an opaque pointer for the ``MetalStageBuffer`` instance and increases its reference count by one + /// - Returns: `OpaquePointer` to class instance + /// + /// > Note: Use this method when the instance is to be shared via an `OpaquePointer` and needs to be retained. Any + /// opaque pointer shared this way needs to be converted into a retained reference again to ensure automatic + /// deinitialization by the Swift runtime. + func getRetained() -> OpaquePointer { + let retained = Unmanaged.passRetained(self).toOpaque() + + return OpaquePointer(retained) + } + + /// Gets an opaque pointer for the ``MetalStageBuffer`` instance without increasing its reference count + /// - Returns: `OpaquePointer` to class instance + func getUnretained() -> OpaquePointer { + let unretained = Unmanaged.passUnretained(self).toOpaque() + + return OpaquePointer(unretained) + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/MetalTexture.swift
Added
@@ -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 = dataindex 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) + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/OBSShader.swift
Added
@@ -0,0 +1,1603 @@ +/****************************************************************************** + 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 enum SampleVariant { + case load + case sample + case sampleBias + case sampleGrad + case sampleLevel +} + +private struct VariableType: OptionSet { + var rawValue: UInt + + static let typeUniform = VariableType(rawValue: 1 << 0) + static let typeStruct = VariableType(rawValue: 1 << 1) + static let typeStructMember = VariableType(rawValue: 1 << 2) + static let typeInput = VariableType(rawValue: 1 << 3) + static let typeOutput = VariableType(rawValue: 1 << 4) + static let typeTexture = VariableType(rawValue: 1 << 5) + static let typeConstant = VariableType(rawValue: 1 << 6) + +} + +private struct OBSShaderFunction { + let name: String + + var returnType: String + var typeMap: String: String + + var requiresUniformBuffers: Bool + var textures: String + var samplers: String + + var arguments: OBSShaderVariable + + let gsFunction: UnsafeMutablePointer<shader_func> +} + +private struct OBSShaderVariable { + let name: String + + var type: String + var mapping: String? + var storageType: VariableType + + var requiredBy: Set<String> + var returnedBy: Set<String> + + var isStage: Bool + var attributeId: Int? + var isConstant: Bool + var isReference: Bool + + let gsVariable: UnsafeMutablePointer<shader_var> +} + +private struct OBSShaderStruct { + let name: String + + var storageType: VariableType + var members: OBSShaderVariable + + let gsVariable: UnsafeMutablePointer<shader_struct> +} + +private struct MSLTemplates { + static let header = """ + #include <metal_stdlib> + + using namespace metal; + """ + + static let variable = "qualifier type name mapping" + + static let shaderStruct = """ + typedef struct { + variable + } typename; + """ + + static let function = "decorator type name(parameters) {content}" +} + +private typealias ParserError = MetalError.OBSShaderParserError +private typealias ShaderError = MetalError.OBSShaderError + +class OBSShader { + private let type: MTLFunctionType + private let content: String + private let fileLocation: String + + private var parser: shader_parser + private var parsed: Bool + + private var uniformsOrder = String() + private var uniforms = String: OBSShaderVariable() + private var structs = String: OBSShaderStruct() + private var functionsOrder = String() + private var functions = String: OBSShaderFunction() + private var referenceVariables = String() + + var metaData: MetalShader.ShaderData? + + init(type: MTLFunctionType, content: String, fileLocation: String) throws { + guard type == .vertex || type == .fragment else { + throw ShaderError.unsupportedType + } + + self.type = type + self.content = content + self.fileLocation = fileLocation + + self.parsed = false + + self.parser = shader_parser() + + try withUnsafeMutablePointer(to: &parser) { + shader_parser_init($0) + + let result = shader_parse($0, content.cString(using: .utf8), content.cString(using: .utf8)) + let warnings = shader_parser_geterrors($0) + + if let warnings { + throw ShaderError.parseError(String(cString: warnings)) + } + + if !result { + throw ShaderError.parseFail("Shader failed to parse: \(fileLocation)") + } else { + self.parsed = true + } + } + } + + /// Transpiles a `libobs` effect string into a Metal Shader Language (MSL) string + /// - Returns: MSL string representing the transpiled shader + func transpiled() throws -> String { + try analyzeUniforms() + try analyzeParameters() + try analyzeFunctions() + + let uniforms = try transpileUniforms() + let structs = try transpileStructs() + let functions = try transpileFunctions() + + self.metaData = try buildMetadata() + + return MSLTemplates.header, uniforms, structs, functions.joined(separator: "\n\n") + } + + /// Builds a metadata object for the current shader + /// - Returns: ``ShaderData`` object with the shader metadata + /// + /// The effects used by `libobs` are written in HLSL with some customizations to allow multiple shaders within the + /// same effects file (which is supported natively by MSL). As MSL does not support "global" variables, uniforms + /// have to be provided explicitly via buffers and the data inside those buffers needs to be laid out in the correct + /// way. + /// + /// Uniforms are converted into `struct` objects in the shader files and as MSL is based on C++14, these structs + /// will have a size, stride, and alignment, set by the compiler. Thus the uniform data used by the shader needs to + /// be laid out in the buffer according to this alignment. + /// + /// The layout of vertex buffer data also needs to be communicated using `MTLVertexDescriptor` instances for vertex + /// shaders and `MTLSamplerState` instances for fragment shaders. Both will be created and set up in a + /// ``ShaderData`` which is used to create the actual ``MetalShader`` object. + private func buildMetadata() throws -> MetalShader.ShaderData { + var uniformInfo = MetalShader.ShaderUniform() + + var textureSlot = 0 + var uniformBufferSize = 0 + + /// The order of buffers and uniforms is "load-bearing" as the order (and thus alignment and offsets) of + /// uniforms in the corresponding uniforms struct are + /// influenced by it. + for uniformName in uniformsOrder { + guard let uniform = uniformsuniformName else { + throw ParserError.parseFail("No uniform data found for '\(uniformName)'") + } + + let gsType = get_shader_param_type(uniform.gsVariable.pointee.type) + let isTexture = uniform.storageType.contains(.typeTexture) + + let byteSize: Int + let alignment: Int + let bufferOffset: Int + + if isTexture { + byteSize = 0 + alignment = 0 + bufferOffset = uniformBufferSize + } else { + byteSize = gsType.mtlSize + alignment = gsType.mtlAlignment + bufferOffset = (uniformBufferSize + (alignment - 1)) & ~(alignment - 1) + } + + let shaderUniform = MetalShader.ShaderUniform( + name: uniform.name, + gsType: gsType, + textureSlot: (isTexture ? textureSlot : 0), + samplerState: nil, + byteOffset: bufferOffset + ) + + shaderUniform.defaultValues = Array( + UnsafeMutableBufferPointer( + start: uniform.gsVariable.pointee.default_val.array, + count: uniform.gsVariable.pointee.default_val.num) + ) + + shaderUniform.currentValues = shaderUniform.defaultValues + + uniformBufferSize = bufferOffset + byteSize + + if isTexture { + textureSlot += 1 + } + + uniformInfo.append(shaderUniform) + } + + guard let mainFunction = functions"main" else { + throw ParserError.missingMainFunction + } + + let parameterMapper = { (mapping: String) -> MetalBuffer.BufferDataType? in + switch mapping { + case "POSITION": + .vertex + case "NORMAL": + .normal + case "TANGENT": + .tangent + case "COLOR": + .color + case _ where mapping.hasPrefix("TEXCOORD"): + .texcoord + default: + .none + } + } + + let descriptorMapper = { (parameter: OBSShaderVariable) -> (MTLVertexFormat, Int)? in + guard let mapping = parameter.mapping else { + return nil + } + + let type = parameter.type + + switch mapping { + case "COLOR": + return (.float4, MemoryLayout<vec4>.size) + case "POSITION", "NORMAL", "TANGENT": + return (.float4, MemoryLayout<vec4>.size) + case _ where mapping.hasPrefix("TEXCOORD"): + guard let numCoordinates = typetype.index(type.startIndex, offsetBy: 5).wholeNumberValue else { + assertionFailure("Unsupported type \(type) for texture parameter") + return nil + } + + let format: MTLVertexFormat = + switch numCoordinates { + case 0: .float + case 2: .float2 + case 3: .float3 + case 4: .float4 + default: .invalid + } + + guard format != .invalid else { + assertionFailure("OBSShader: Unsupported amount of texture coordinates '\(numCoordinates)'") + return nil + } + + return (format, MemoryLayout<Float32>.size * numCoordinates) + case "VERTEXID": + return nil + default: + assertionFailure("OBSShader: Unsupported mapping \(mapping)") + return nil + } + } + + switch type { + case .vertex: + var bufferOrder = MetalBuffer.BufferDataType() + var descriptorData = (MTLVertexFormat, Int)?() + let descriptor = MTLVertexDescriptor() + + for argument in mainFunction.arguments { + if argument.storageType.contains(.typeStruct) { + let actualStructType = argument.type.replacingOccurrences(of: "_In", with: "") + + guard let shaderStruct = structsactualStructType else { + throw ParserError.parseFail("Shader function without struct metadata encountered ") + } + + for shaderParameter in shaderStruct.members { + if let mapping = shaderParameter.mapping, let mapping = parameterMapper(mapping) { + bufferOrder.append(mapping) + } + + if let description = descriptorMapper(shaderParameter) { + descriptorData.append(description) + } + } + } else { + if let mapping = argument.mapping, let mapping = parameterMapper(mapping) { + bufferOrder.append(mapping) + } + + if let description = descriptorMapper(argument) { + descriptorData.append(description) + } + } + } + + let textureUnitCount = bufferOrder.filter({ $0 == .texcoord }).count + + for (attributeId, description) in descriptorData.filter({ $0 != nil }).enumerated() { + descriptor.attributesattributeId.bufferIndex = attributeId + descriptor.attributesattributeId.format = description!.0 + descriptor.layoutsattributeId.stride = description!.1 + } + + return MetalShader.ShaderData( + uniforms: uniformInfo, + bufferOrder: bufferOrder, + vertexDescriptor: descriptor, + samplerDescriptors: nil, + bufferSize: uniformBufferSize, + textureCount: textureUnitCount + ) + case .fragment: + var samplers = MTLSamplerDescriptor() + + for i in 0..<parser.samplers.num { + let sampler: UnsafeMutablePointer<shader_sampler>? = parser.samplers.array.advanced(by: i) + + if let sampler { + var sampler_info = gs_sampler_info() + shader_sampler_convert(sampler, &sampler_info) + + let borderColor: MTLSamplerBorderColor = + switch sampler_info.border_color { + case 0x00_00_00_FF: + .opaqueBlack + case 0xFF_FF_FF_FF: + .opaqueWhite + default: + .transparentBlack + } + + let descriptor = MTLSamplerDescriptor() + + descriptor.borderColor = borderColor + descriptor.maxAnisotropy = Int(sampler_info.max_anisotropy) + + guard + let sAddressMode = sampler_info.address_u.mtlMode, + let tAddressMode = sampler_info.address_v.mtlMode, + let rAddressMode = sampler_info.address_w.mtlMode, + let minMagFilter = sampler_info.filter.minMagFilter, + let mipFilter = sampler_info.filter.mipFilter + else { + samplers.append(descriptor) + continue + } + + descriptor.sAddressMode = sAddressMode + descriptor.tAddressMode = tAddressMode + descriptor.rAddressMode = rAddressMode + + descriptor.minFilter = minMagFilter + descriptor.magFilter = minMagFilter + descriptor.mipFilter = mipFilter + + samplers.append(descriptor) + } + } + + return MetalShader.ShaderData( + uniforms: uniformInfo, + bufferOrder: , + vertexDescriptor: nil, + samplerDescriptors: samplers, + bufferSize: uniformBufferSize, + textureCount: 0 + ) + default: + throw ShaderError.unsupportedType + } + } + + /// Analyzes shader uniform parameters parsed by the ``libobs`` shader parser. + /// + /// Each global variable declared as a "uniform" is stored as an ``OBSShaderVariable`` struct, which will be + /// extended with additional metadata by later analystics steps. + /// + /// This is necessary as MSL does not support global variables and all data needs to be explicitly provided + /// via buffer objects, which requires these "unforms" to be wrapped into a single struct and passed as an explicit + /// buffer object. + private func analyzeUniforms() throws { + for i in 0..<parser.params.num { + let uniform: UnsafeMutablePointer<shader_var>? = parser.params.array.advanced(by: i) + + guard let uniform, let name = uniform.pointee.name, let type = uniform.pointee.type else { + throw ParserError.parseFail("Uniform is missing name or type information") + } + + let mapping: String? = + if let mapping = uniform.pointee.mapping { + String(cString: mapping) + } else { + nil + } + + var data = OBSShaderVariable( + name: String(cString: name), + type: String(cString: type), + mapping: mapping, + storageType: .typeUniform, + requiredBy: , + returnedBy: , + isStage: false, + attributeId: 0, + isConstant: (uniform.pointee.var_type == SHADER_VAR_CONST), + isReference: false, + gsVariable: uniform + ) + + if self.type == .fragment { + /// A texture uniform does not contribute to the uniform buffer + if data.type.hasPrefix("texture") { + data.storageType.remove(.typeUniform) + data.storageType.insert(.typeTexture) + } + } + + uniformsOrder.append(data.name) + uniforms.updateValue(data, forKey: data.name) + + } + } + + /// Analyzes struct parameter declarations parsed by the ``libobs`` shader parser. + /// + /// Structured data declarations are used to pass data into and out of shaders. + /// + /// Whereas HLSL allows one to use "InOut" structures with attribute mappings (e.g., using the same type defintion + /// for vertex data going in and out of a vertex shader), MSL does not allow the mixing of input mappings and output + /// mappings in the same type definition. + /// + /// Thus when the same struct type is used as an input argument for a function but also used as its output type, it + /// needs to be split up into two separate types for the MSL shader. + /// + /// This function will first detect all struct type definitions in the shader file and then check if it is used as + /// an input argument or function output and update the associated ``OBSShaderVariable`` structs accordingly. + private func analyzeParameters() throws { + for i in 0..<parser.structs.num { + let shaderStruct: UnsafeMutablePointer<shader_struct>? = parser.structs.array.advanced(by: i) + + guard let shaderStruct, let name = shaderStruct.pointee.name else { + throw ParserError.parseFail("Constant data struct has no name") + } + + var parameters = OBSShaderVariable() + parameters.reserveCapacity(shaderStruct.pointee.vars.num) + + for j in 0..<shaderStruct.pointee.vars.num { + let variablePointer: UnsafeMutablePointer<shader_var>? = shaderStruct.pointee.vars.array.advanced(by: j) + + guard let variablePointer, let variableName = variablePointer.pointee.name, + let variableType = variablePointer.pointee.type + else { + throw ParserError.parseFail("Constant data variable has no name") + } + + let mapping: String? = + if let variableMapping = variablePointer.pointee.mapping { String(cString: variableMapping) } else { + nil + } + + let variable = OBSShaderVariable( + name: String(cString: variableName), + type: String(cString: variableType), + mapping: mapping, + storageType: .typeStructMember, + requiredBy: , + returnedBy: , + isStage: false, + attributeId: nil, + isConstant: false, + isReference: false, + gsVariable: variablePointer + ) + + parameters.append(variable) + } + + let data = OBSShaderStruct( + name: String(cString: name), + storageType: , + members: parameters, + gsVariable: shaderStruct + ) + + structs.updateValue(data, forKey: data.name) + } + + for i in 0..<parser.funcs.num { + let function: UnsafeMutablePointer<shader_func>? = parser.funcs.array.advanced(by: i) + + guard let function, let functionName = function.pointee.name, let returnType = function.pointee.return_type + else { + throw ParserError.parseFail("Shader function has no name or type information") + } + + var functionData = OBSShaderFunction( + name: String(cString: functionName), + returnType: String(cString: returnType), + typeMap: :, + requiresUniformBuffers: false, + textures: , + samplers: , + arguments: , + gsFunction: function, + ) + + for j in 0..<function.pointee.params.num { + let parameter: UnsafeMutablePointer<shader_var>? = function.pointee.params.array.advanced(by: j) + + guard let parameter, let parameterName = parameter.pointee.name, + let parameterType = parameter.pointee.type + else { + throw ParserError.parseFail("Function parameter has no name or type information") + } + + let mapping: String? = + if let parameterMapping = parameter.pointee.mapping { + String(cString: parameterMapping) + } else { + nil + } + + /// Most effects do not seem to use `out` or `inout` function arguments, but the lanczos scale filter + /// does. The most straight-forward way + /// to support this pattern is to use C++-style references with the `thread` storage specifier. + let isReferenceVariable = + (parameter.pointee.var_type == SHADER_VAR_OUT || parameter.pointee.var_type == SHADER_VAR_INOUT) + + var parameterData = OBSShaderVariable( + name: String(cString: parameterName), + type: String(cString: parameterType), + mapping: mapping, + storageType: .typeInput, + requiredBy: functionData.name, + returnedBy: , + isStage: false, + attributeId: nil, + isConstant: (parameter.pointee.var_type == SHADER_VAR_CONST), + isReference: isReferenceVariable, + gsVariable: parameter + ) + + if isReferenceVariable { + referenceVariables.append(parameterData.name) + } + + if parameterData.type == functionData.returnType { + parameterData.returnedBy.insert(functionData.name) + } + + if !functionData.typeMap.keys.contains(parameterData.name) { + functionData.typeMap.updateValue(parameterData.type, forKey: parameterData.name) + } + + /// Metal does not support using the same attribute mappings for structs as input to shader functions + /// and output. They need to use different + /// mappings and thus every "InOut" struct by `libobs` needs to be split up into a separate input and + /// output struct type. + for var shaderStruct in structs.values { + if shaderStruct.name == parameterData.type { + shaderStruct.storageType.insert(.typeInput) + parameterData.storageType.insert(.typeStruct) + + if shaderStruct.name == functionData.returnType { + shaderStruct.storageType.insert(.typeOutput) + parameterData.storageType.insert(.typeOutput) + parameterData.type.append("_In") + functionData.returnType.append("_Out") + } + + structs.updateValue(shaderStruct, forKey: shaderStruct.name) + } + } + + functionData.arguments.append(parameterData) + } + + if var shaderStruct = structsfunctionData.returnType { + shaderStruct.storageType.insert(.typeOutput) + structs.updateValue(shaderStruct, forKey: shaderStruct.name) + } + + functions.updateValue(functionData, forKey: functionData.name) + } + } + + /// Analyzes function data parsed by the ``libobs`` shader parser + /// + /// As MSL does not support uniforms or using the same struct type for input and output, function bodies themselves + /// need to be parsed again and checked for their usage of these types or variables. + /// + /// Due to the way that the ``libobs`` parser works, each body of a block (either within curly braces or + /// parentheses) is analyzed recursively and updating the same ``OBSShaderFunction`` struct. + /// + /// After a full analysis pass, this struct should contain information about all uniforms, textures, and samplers + /// used (or passed on) by the function. + private func analyzeFunctions() throws { + for i in 0..<parser.funcs.num { + let function: UnsafeMutablePointer<shader_func>? = parser.funcs.array.advanced(by: i) + + guard var function, var token = function.pointee.start, let functionName = function.pointee.name else { + throw ParserError.parseFail("Shader function has no name") + } + + let functionData = functionsString(cString: functionName) + + guard var functionData else { + throw ParserError.parseFail("Shader function without function meta data encountered") + } + + try analyzeFunction(function: &function, functionData: &functionData, token: &token, end: "}") + + functionData.textures = functionData.textures.unique() + functionData.samplers = functionData.samplers.unique() + + functions.updateValue(functionData, forKey: functionData.name) + functionsOrder.append(functionData.name) + } + } + + /// Analyzes a function body or source scope to check for use of global variables, textures, or samplers. + /// + /// Because MSL does not support global variables, unforms, textures, or samplers need to be passed explicitly to a + /// function. This requires scanning the entire function body (recursively in the case of separate function scopes + /// denoted by curvy brackets or parantheses) for any occurrence of a known uniform, texture, or sampler variable + /// name. + /// + /// - Parameters: + /// - function: Pointer to a ``shader_func`` element representing a parsed shader function + /// - functionData: Reference to a ``OBSShaderFunction`` struct, which will be updated by this function + /// - token: Pointer to a ``cf_token`` element used to interact with the shader parser provided by ``libobs`` + /// - end: The sentinel character at which analysis (and parsing) should stop + private func analyzeFunction( + function: inout UnsafeMutablePointer<shader_func>, functionData: inout OBSShaderFunction, + token: inout UnsafeMutablePointer<cf_token>, end: String + ) throws { + let uniformNames = + (uniforms.filter { + !$0.value.storageType.contains(.typeTexture) + }).keys + + while token.pointee.type != CFTOKEN_NONE { + token = token.successor() + + if token.pointee.str.isEqualTo(end) { + break + } + + let stringToken = token.pointee.str.getString() + + if token.pointee.type == CFTOKEN_NAME { + if uniformNames.contains(stringToken) && functionData.requiresUniformBuffers == false { + functionData.requiresUniformBuffers = true + } + + if let function = functionsstringToken { + if function.requiresUniformBuffers && functionData.requiresUniformBuffers == false { + functionData.requiresUniformBuffers = true + } + + functionData.textures.append(contentsOf: function.textures) + functionData.samplers.append(contentsOf: function.samplers) + } + + if type == .fragment { + for uniform in uniforms.values { + if stringToken == uniform.name && uniform.storageType.contains(.typeTexture) { + functionData.textures.append(stringToken) + } + } + + for i in 0..<parser.samplers.num { + let sampler: UnsafeMutablePointer<shader_sampler>? = parser.samplers.array.advanced(by: i) + + guard let sampler, let samplerName = sampler.pointee.name else { + break + } + + if stringToken == String(cString: samplerName) { + functionData.samplers.append(stringToken) + } + } + } + } else if token.pointee.type == CFTOKEN_OTHER { + if token.pointee.str.isEqualTo("{") { + try analyzeFunction(function: &function, functionData: &functionData, token: &token, end: "}") + } else if token.pointee.str.isEqualTo("(") { + try analyzeFunction(function: &function, functionData: &functionData, token: &token, end: ")") + } + } + } + } + + /// Transpiles the uniform global variables used by the shader into a `UniformData` struct that contains the + /// uniforms. + /// - Returns: String representing the uniform data struct + private func transpileUniforms() throws -> String { + var output = String() + + for uniformName in uniformsOrder { + if var uniform = uniformsuniformName { + uniform.isStage = false + uniform.attributeId = 0 + + if !uniform.storageType.contains(.typeTexture) { + let variableString = try transpileVariable(variable: uniform) + output.append("\(variableString);") + } + } + } + + if output.count > 0 { + let replacements = + ("variable", output.joined(separator: "\n")), + ("typename", "UniformData"), + + + let uniformString = replacements.reduce(into: MSLTemplates.shaderStruct) { string, replacement in + string = string.replacingOccurrences(of: replacement.0, with: replacement.1) + } + + return uniformString + } else { + return "" + } + } + + /// Transpiles the vertex data structs used by the shader + /// - Returns: String representing the vertex data structs + private func transpileStructs() throws -> String { + var output = String() + + for var shaderStruct in structs.values { + if shaderStruct.storageType.isSuperset(of: .typeInput, .typeOutput) { + /// Metal does not support using the same attribute mappings for structs as input to shader functions + /// and output. They need to use different mappings and thus every "InOut" struct by `libobs` needs to + /// be split up into a separate input and output struct type. + for suffix in "_In", "_Out" { + var variables = String() + + for (structVariableId, var structVariable) in shaderStruct.members.enumerated() { + let variableString: String + + switch suffix { + case "_In": + structVariable.storageType.formUnion(.typeInput) + structVariable.attributeId = structVariableId + variableString = try transpileVariable(variable: structVariable) + structVariable.storageType.remove(.typeInput) + case "_Out": + structVariable.storageType.formUnion(.typeOutput) + variableString = try transpileVariable(variable: structVariable) + structVariable.storageType.remove(.typeOutput) + default: + throw ParserError.parseFail("Shader struct with unknown prefix encountered") + } + + variables.append("\(variableString);") + shaderStruct.membersstructVariableId = structVariable + } + + let replacements = + ("variable", variables.joined(separator: "\n")), + ("typename", "\(shaderStruct.name)\(suffix)"), + + + let result = replacements.reduce(into: MSLTemplates.shaderStruct) { + string, replacement in + string = string.replacingOccurrences(of: replacement.0, with: replacement.1) + } + + output.append(result) + } + } else { + var variables = String() + + for (structVariableId, var structVariable) in shaderStruct.members.enumerated() { + if shaderStruct.storageType.contains(.typeInput) { + structVariable.storageType.insert(.typeInput) + structVariable.attributeId = structVariableId + } else if shaderStruct.storageType.contains(.typeOutput) { + structVariable.storageType.insert(.typeOutput) + } + + let variableString = try transpileVariable(variable: structVariable) + + structVariable.storageType.subtract(.typeInput, .typeOutput) + + variables.append("\(variableString);") + shaderStruct.membersstructVariableId = structVariable + } + + let replacements = + ("variable", variables.joined(separator: "\n")), + ("typename", shaderStruct.name), + + + let result = replacements.reduce(into: MSLTemplates.shaderStruct) { + string, replacement in + string = string.replacingOccurrences(of: replacement.0, with: replacement.1) + } + + output.append(result) + } + } + + if output.count > 0 { + return output.joined(separator: "\n\n") + } else { + return "" + } + } + + /// Transpiles a shader function into its MSL variant + /// - Returns: String representing the transpiled MSL shader function + private func transpileFunctions() throws -> String { + var output = String() + + for functionName in functionsOrder { + guard let function = functionsfunctionName, var token = function.gsFunction.pointee.start else { + throw ParserError.parseFail("Shader function has no name") + } + + var stageConsumed = false + let isMain = functionName == "main" + + var variables = String() + for var variable in function.arguments { + if isMain && !stageConsumed { + variable.isStage = true + stageConsumed = true + } + + try variables.append(transpileVariable(variable: variable)) + } + + /// As Metal has no support for global constants, the constant data needs to be wrapped into a `struct` + /// and the associated data is uploaded into a vertex buffer at a specific index (30 in this case). + /// + /// Buffers are not automatically available to shader functions but are passed into the function explicitly + ///as arguments. + /// + /// As `libobs` effects are based around a "main" entry function (something strongly discouraged by Metal), + /// each "main" function needs to receive the actual buffer as an argument and each function called _by_ + /// the main function and which internally accesses the uniform needs to have that uniform passed + /// explicitly as an argument as well. + if (uniforms.values.filter { !$0.storageType.contains(.typeTexture) }).count > 0 { + if isMain { + variables.append("constant UniformData &uniforms buffer(30)") + } else if function.requiresUniformBuffers { + variables.append("constant UniformData &uniforms") + } + } + + if type == .fragment { + var textureId = 0 + + for uniformName in uniformsOrder { + guard let uniform = uniformsuniformName else { + break + } + + if uniform.storageType.contains(.typeTexture) { + if isMain { + let variableString = try transpileVariable(variable: uniform) + + variables.append("\(variableString) texture(\(textureId))") + textureId += 1 + } else if function.textures.contains(uniform.name) { + let variableString = try transpileVariable(variable: uniform) + variables.append(variableString) + } + } + } + + var samplerId = 0 + for i in 0..<parser.samplers.num { + let sampler: UnsafeMutablePointer<shader_sampler>? = parser.samplers.array.advanced(by: i) + + if let sampler, let samplerName = sampler.pointee.name { + let name = String(cString: samplerName) + + if isMain { + let variableString = "sampler \(name) sampler(\(samplerId))" + variables.append(variableString) + samplerId += 1 + } else if function.samplers.contains(name) { + let variabelString = "sampler \(name)" + variables.append(variabelString) + } + } + } + } + + let mappedType = try convertToMTLType(gsType: function.returnType) + + let functionContent: String + var replacements = (String, String)() + + /// Metal shaders do not have "main" functions - a single shader file usually contains all shader functions + /// used by an application, each identified by their name and type decorator. This is not supported by OBS, + /// so each shader needs to have a "main" function that calls the actual shader function, which thus + /// requires a new shader library to be created for each effect file. + if isMain { + replacements = + ("name", "_main"), + ("parameters", variables.joined(separator: ", ")), + + + switch type { + case .vertex: + replacements.append(("decorator", "vertex")) + case .fragment: + replacements.append(("decorator", "fragment")) + default: + fatalError("OBSShader: Unsupported shader type \(type)") + } + + let temporaryContent = try transpileFunctionContent(token: &token, end: "}") + + if type == .fragment && isMain && mappedType == "float3" { + replacements.append(("type", "float4")) + + // TODO: Replace with Swift-native Regex once macOS 13+ is minimum target + let regex = try NSRegularExpression(pattern: "return (.+);") + functionContent = regex.stringByReplacingMatches( + in: temporaryContent, + range: NSRange(location: 0, length: temporaryContent.count), + withTemplate: "return float4($1, 1);" + ) + } else { + functionContent = temporaryContent + replacements.append(("type", mappedType)) + } + + replacements.append(("content", functionContent)) + } else { + functionContent = try transpileFunctionContent(token: &token, end: "}") + + replacements = + ("decorator", ""), + ("type", mappedType), + ("name", function.name), + ("parameters", variables.joined(separator: ", ")), + ("content", functionContent), + + } + + let result = replacements.reduce(into: MSLTemplates.function) { + string, replacement in + string = string.replacingOccurrences(of: replacement.0, with: replacement.1) + } + + output.append(result) + } + + if output.count > 0 { + return output.joined(separator: "\n\n") + } else { + return "" + } + } + + /// Transpiles a variable into its MSL variant + /// - Parameter variable: Variable to transpile + /// - Returns: String representing a transpiled variable + /// + /// Variables can either be members of a `struct` or an argument to a function. The ``OBSShaderVariable`` instance + /// has a `storageType` property which encodes the use of the variable and helps in creation of the appropriate MSL + /// string representation. + private func transpileVariable(variable: OBSShaderVariable) throws -> String { + var mappings = String() + + var metalMapping: String + var indent = 0 + + let metalType = try convertToMTLType(gsType: variable.type) + + if variable.storageType.contains(.typeUniform) { + indent = 4 + } else if variable.storageType.isSuperset(of: .typeInput, .typeStructMember) { + switch type { + case .vertex: + indent = 4 + + /// Attributes are used to associate a member of a uniform `struct` with its data in the vertex buffer + /// stage. + if let attributeId = variable.attributeId { + mappings.append("attribute(\(attributeId))") + } + case .fragment: + indent = 4 + + if let mappingPointer = variable.gsVariable.pointee.mapping, + let mappedString = convertToMTLMapping(gsMapping: String(cString: mappingPointer)) + { + mappings.append(mappedString) + } + default: + fatalError("OBSShader: Unsupported shader function type \(type)") + } + } else if variable.storageType.isSuperset(of: .typeOutput, .typeStructMember) { + indent = 4 + + if let mappingPointer = variable.gsVariable.pointee.mapping, + let mappedString = convertToMTLMapping(gsMapping: String(cString: mappingPointer)) + { + mappings.append(mappedString) + } + } else { + indent = 0 + + if variable.isStage { + if let mappingPointer = variable.gsVariable.pointee.mapping, + let mappedString = convertToMTLMapping(gsMapping: String(cString: mappingPointer)) + { + mappings.append(mappedString) + } else { + mappings.append("stage_in") + } + } + } + + if mappings.count > 0 { + metalMapping = " \(mappings.joined(separator: ", "))" + } else { + metalMapping = "" + } + + let qualifier = + if variable.storageType.contains(.typeConstant) { + " constant " + } else if variable.isReference { + " thread " + } else { + "" + } + + let name = + if variable.isReference { + "&\(variable.name)" + } else { variable.name } + + let result = "\(String(repeating: " ", count: indent))\(qualifier)\(metalType) \(name)\(metalMapping)" + + return result + } + + /// Transpiles the body of a function into its MSL representation + /// - Parameters: + /// - token: Stateful `libobs` parser token pointer + /// - end: String representing which ends function body parsing if matched + /// - Returns: String representing the body of a MSL shader function + /// + /// OBS effect function content needs to be transpiled into MSL function content token by token, as each token + /// needs to be matched not only against direct translations (e.g., a HLSL function name into its appropriate MSL + /// variant) but also to detect if a token represents a uniform variable which will not be available as a global + /// variable in MSL, but instead will only exist as part of the `uniform` struct that was explicitly passed into + /// the function. + /// + /// Similarly, if a function call is encountered, the function's metadata needs to be checked for use of such a + /// uniform and the call signature extended to explicitly pass the data into the called function. + /// + /// Because Metal does not implicitly or automagically coerce types (but the effects files sometimes rely on this), + /// some arguments and parameters need to be explicitly wrapped in casts to wider types (e.g., a `float3` is + /// returned from a fragment shader, but fragment shaders _have to_ provide a `float4`). + /// + /// There are many such conversions necessary, as MSL is more strict than HLSL or GLSL when it comes to type safety. + private func transpileFunctionContent(token: inout UnsafeMutablePointer<cf_token>, end: String) throws -> String { + var content = String() + + while token.pointee.type != CFTOKEN_NONE { + token = token.successor() + + if token.pointee.str.isEqualTo(end) { + break + } + + let stringToken = token.pointee.str.getString() + + if token.pointee.type == CFTOKEN_NAME { + let type = try convertToMTLType(gsType: stringToken) + + if stringToken == "obs_glsl_compile" { + content.append("false") + continue + } + + if type != stringToken { + content.append(type) + continue + } + + if let intrinsic = try convertToMTLIntrinsic(intrinsic: stringToken) { + content.append(intrinsic) + continue + } + + if stringToken == "mul" { + try content.append(convertToMTLMultiplication(token: &token)) + continue + } else if stringToken == "mad" { + try content.append(convertToMTLMultiplyAdd(token: &token)) + continue + } else { + var skip = false + for uniform in uniforms.values { + if uniform.name == stringToken && uniform.storageType.contains(.typeTexture) { + try content.append(createSampler(token: &token)) + skip = true + break + } + } + + if skip { + continue + } + } + + if uniforms.keys.contains(stringToken) { + let priorToken = token.predecessor() + let priorString = priorToken.pointee.str.getString() + + if priorString != "." { + content.append("uniforms.\(stringToken)") + continue + } + } + + var skip = false + for shaderStruct in structs.values { + if shaderStruct.name == stringToken { + if shaderStruct.storageType.isSuperset(of: .typeInput, .typeOutput) { + content.append("\(stringToken)_Out") + skip = true + break + } + } + } + + if skip { + continue + } + + if let comparison = try convertToMTLComparison(token: &token) { + content.append(comparison) + continue + } + + content.append(stringToken) + } else if token.pointee.type == CFTOKEN_OTHER { + if token.pointee.str.isEqualTo("{") { + let blockContent = try transpileFunctionContent(token: &token, end: "}") + content.append("{\(blockContent)}") + continue + } else if token.pointee.str.isEqualTo("(") { + let priorToken = token.predecessor() + let functionName = priorToken.pointee.str.getString() + + var functionParameters = String() + + let parameters = try transpileFunctionContent(token: &token, end: ")") + + if functionName == "int3" { + let intParameters = parameters.split( + separator: ",", maxSplits: 3, omittingEmptySubsequences: true) + + switch intParameters.count { + case 3: + functionParameters.append( + "int(\(intParameters0)), int(\(intParameters1)), int(\(intParameters2))") + case 2: + functionParameters.append("int2(\(intParameters0)), int(\(intParameters1))") + case 1: + functionParameters.append("\(intParameters)") + default: + throw ParserError.parseFail("int3 constructor with invalid amount of arguments encountered") + } + } else { + functionParameters.append(parameters) + } + + if let additionalArguments = generateAdditionalArguments(for: functionName) { + functionParameters.append(additionalArguments) + } + + content.append("(\(functionParameters.joined(separator: ", ")))") + continue + } + + content.append(stringToken) + } else { + content.append(stringToken) + } + } + + return content.joined() + } + + /// Converts a HLSL-like type into a MSL type if possible + /// - Parameter gsType: HLSL-like type string + /// - Returns: MSL type string + private func convertToMTLType(gsType: String) throws -> String { + switch gsType { + case "texture2d": + return "texture2d<float>" + case "texture3d": + return "texture3d<float>" + case "texture_cube": + return "texturecube<float>" + case "texture_rect": + throw ParserError.unsupportedType + case "half2": + return "float2" + case "half3": + return "float3" + case "half4": + return "float4" + case "half": + return "float" + case "min16float2": + return "half2" + case "min16float3": + return "half3" + case "min16float4": + return "half4" + case "min16float": + return "half" + case "min10float": + throw ParserError.unsupportedType + case "double": + throw ParserError.unsupportedType + case "min16int2": + return "short2" + case "min16int3": + return "short3" + case "min16int4": + return "short4" + case "min16int": + return "short" + case "min16uint2": + return "ushort2" + case "min16uint3": + return "ushort3" + case "min16uint4": + return "ushort4" + case "min16uint": + return "ushort" + case "min13int": + throw ParserError.unsupportedType + default: + return gsType + } + } + + /// Converts an HLSL-like uniform mapping into a MSL attribute decoration if possible + /// - Parameter gsMapping: HLSL-like mapping + /// - Returns: MSL attribute string + private func convertToMTLMapping(gsMapping: String) -> String? { + switch gsMapping { + case "POSITION": + return "position" + case "VERTEXID": + return "vertex_id" + default: + return nil + } + } + + /// Converts a HLSL-like comparison to a vector-safe MSL comparison operation + /// - Parameter token: Start token of the comparison in the function body + /// - Returns: MSL comparison operation + /// + /// A comparison operation that involves a vector will always result in a boolean vector in MSL (and not a scalar + /// vector). Thus any functions that compares two vectors will also result in a vector + /// (e.g., float2 == float2 -> bool2). This will break when a ternary expression is used, as the first element of + /// it needs to be as scalar boolean in MSL. + /// + /// Wrapping the comparison in `all` ensures that a single scalar `true` is returned if all elements of the + /// resulting boolean vectors are `true` as well. + private func convertToMTLComparison(token: inout UnsafeMutablePointer<cf_token>) throws -> String? { + var isComparator = false + + let nextToken = token.successor() + + if nextToken.pointee.type == CFTOKEN_OTHER { + let comparators = "==", "!=", "<", "<=", ">=", ">" + + for comparator in comparators { + if nextToken.pointee.str.isEqualTo(comparator) { + isComparator = true + break + } + } + } + + if isComparator { + var cfp = parser.cfp + cfp.cur_token = token + + let lhs = cfp.cur_token.pointee.str.getString() + + guard cfp.advanceToken() else { throw ParserError.missingNextToken } + + let comparator = cfp.cur_token.pointee.str.getString() + + guard cfp.advanceToken() else { throw ParserError.missingNextToken } + + let rhs = cfp.cur_token.pointee.str.getString() + + return "all(\(lhs) \(comparator) \(rhs))" + } else { + return nil + } + } + + /// Converts HLSL-like intrinsic into its MSL representation + /// - Parameter intrinsic: HLSL-like intrinsic string + /// - Returns: MSL intrinsic string + private func convertToMTLIntrinsic(intrinsic: String) throws -> String? { + switch intrinsic { + case "clip": + throw ParserError.unsupportedType + case "ddx": + return "dfdx" + case "ddy": + return "dfdy" + case "frac": + return "fract" + case "lerp": + return "mix" + default: + return nil + } + } + + /// Converts a HLSL-like multiplication function call into a direct multiplication + /// - Parameter token: Start token of the multiplication in the function body + /// - Returns: MSL multiplication string + private func convertToMTLMultiplication(token: inout UnsafeMutablePointer<cf_token>) throws -> String { + var cfp = parser.cfp + cfp.cur_token = token + + guard cfp.advanceToken() else { throw ParserError.missingNextToken } + guard cfp.tokenIsEqualTo("(") else { throw ParserError.unexpectedToken } + guard cfp.hasNextToken() else { throw ParserError.missingNextToken } + + let lhs = try transpileFunctionContent(token: &cfp.cur_token, end: ",") + + guard cfp.advanceToken() else { throw ParserError.missingNextToken } + + cfp.cur_token = cfp.cur_token.predecessor() + + let rhs = try transpileFunctionContent(token: &cfp.cur_token, end: ")") + + token = cfp.cur_token + + return "(\(lhs)) * (\(rhs))" + } + + /// Converts a HLSL-like multiply+add function call into a direct multiplication followed by addition + /// - Parameter token: Start token of the multiply+add in the function body + /// - Returns: MSL multiplication and addition string + private func convertToMTLMultiplyAdd(token: inout UnsafeMutablePointer<cf_token>) throws -> String { + var cfp = parser.cfp + cfp.cur_token = token + + guard cfp.advanceToken() else { throw ParserError.missingNextToken } + guard cfp.tokenIsEqualTo("(") else { throw ParserError.unexpectedToken } + guard cfp.hasNextToken() else { throw ParserError.missingNextToken } + + let first = try transpileFunctionContent(token: &cfp.cur_token, end: ",") + + guard cfp.hasNextToken() else { throw ParserError.missingNextToken } + + let second = try transpileFunctionContent(token: &cfp.cur_token, end: ",") + + guard cfp.hasNextToken() else { throw ParserError.missingNextToken } + + let third = try transpileFunctionContent(token: &cfp.cur_token, end: ")") + + token = cfp.cur_token + + return "((\(first)) * (\(second))) + (\(third))" + } + + /// Creates an MSL sampler call from a HLSL-like sampler call + /// - Parameter token: Start token of the sampler call in the function + /// - Returns: String of an MSL sampler call + private func createSampler(token: inout UnsafeMutablePointer<cf_token>) throws -> String { + var cfp = parser.cfp + cfp.cur_token = token + + let stringToken = token.pointee.str.getString() + + guard cfp.advanceToken() else { throw ParserError.missingNextToken } + guard cfp.tokenIsEqualTo(".") else { throw ParserError.unexpectedToken } + guard cfp.advanceToken() else { throw ParserError.missingNextToken } + guard cfp.cur_token.pointee.type == CFTOKEN_NAME else { throw ParserError.unexpectedToken } + + let textureCall: String + + if cfp.tokenIsEqualTo("Sample") { + textureCall = try createTextureCall(token: &cfp.cur_token, callType: .sample) + } else if cfp.tokenIsEqualTo("SampleBias") { + textureCall = try createTextureCall(token: &cfp.cur_token, callType: .sampleBias) + } else if cfp.tokenIsEqualTo("SampleGrad") { + textureCall = try createTextureCall(token: &cfp.cur_token, callType: .sampleGrad) + } else if cfp.tokenIsEqualTo("SampleLevel") { + textureCall = try createTextureCall(token: &cfp.cur_token, callType: .sampleLevel) + } else if cfp.tokenIsEqualTo("Load") { + textureCall = try createTextureCall(token: &cfp.cur_token, callType: .load) + } else { + throw ParserError.missingNextToken + } + + token = cfp.cur_token + return "\(stringToken).\(textureCall)" + } + + /// Creates a MSL sampler call based on the sampling type + /// - Parameters: + /// - token: Start token of the sampler call arguments in the function body + /// - callType: Type of sampling used + /// - Returns: String of an MSL sampler call + private func createTextureCall(token: inout UnsafeMutablePointer<cf_token>, callType: SampleVariant) throws + -> String + { + var cfp = parser.cfp + cfp.cur_token = token + + guard cfp.advanceToken() else { throw ParserError.missingNextToken } + guard cfp.tokenIsEqualTo("(") else { throw ParserError.unexpectedToken } + guard cfp.hasNextToken() else { throw ParserError.missingNextToken } + + switch callType { + case .sample: + let first = try transpileFunctionContent(token: &cfp.cur_token, end: ",") + guard cfp.hasNextToken() else { throw ParserError.missingNextToken } + + let second = try transpileFunctionContent(token: &cfp.cur_token, end: ")") + + token = cfp.cur_token + return "sample(\(first), \(second))" + case .sampleBias: + let first = try transpileFunctionContent(token: &cfp.cur_token, end: ",") + guard cfp.hasNextToken() else { throw ParserError.missingNextToken } + + let second = try transpileFunctionContent(token: &cfp.cur_token, end: ",") + guard cfp.hasNextToken() else { throw ParserError.missingNextToken } + + let third = try transpileFunctionContent(token: &cfp.cur_token, end: ")") + + token = cfp.cur_token + return "sample(\(first), \(second), bias(\(third)))" + case .sampleGrad: + let first = try transpileFunctionContent(token: &cfp.cur_token, end: ",") + guard cfp.hasNextToken() else { throw ParserError.missingNextToken } + + let second = try transpileFunctionContent(token: &cfp.cur_token, end: ",") + guard cfp.hasNextToken() else { throw ParserError.missingNextToken } + + let third = try transpileFunctionContent(token: &cfp.cur_token, end: ",") + guard cfp.hasNextToken() else { throw ParserError.missingNextToken } + + let fourth = try transpileFunctionContent(token: &cfp.cur_token, end: ")") + + token = cfp.cur_token + return "sample(\(first), \(second), gradient2d(\(third), \(fourth)))" + case .sampleLevel: + let first = try transpileFunctionContent(token: &cfp.cur_token, end: ",") + guard cfp.hasNextToken() else { throw ParserError.missingNextToken } + + let second = try transpileFunctionContent(token: &cfp.cur_token, end: ",") + guard cfp.hasNextToken() else { throw ParserError.missingNextToken } + + let third = try transpileFunctionContent(token: &cfp.cur_token, end: ")") + + token = cfp.cur_token + return "sample(\(first), \(second), level(\(third)))" + case .load: + let first = try transpileFunctionContent(token: &cfp.cur_token, end: ")") + + let loadCall: String + + /// Many load calls in OBS effects files rely on implicit type conversion, which is not allowed in MSL in + /// addition to `read` calls only accepting a `uint2` followed by a `uint`. Any instance of a `int3` thus + /// needs to be converted into the appropriate variant compatible with the `read` call. + if first.hasPrefix("int3(") { + let loadParameters = first + first.index(first.startIndex, offsetBy: 5)..<first.index(first.endIndex, offsetBy: -1) + .split(separator: ",", maxSplits: 3, omittingEmptySubsequences: true) + + switch loadParameters.count { + case 3: + loadCall = "read(uint2(\(loadParameters0), \(loadParameters1)), uint(\(loadParameters2)))" + case 2: + loadCall = "read(uint2(\(loadParameters0)), uint(\(loadParameters1)))" + case 1: + loadCall = "read(uint2(\(loadParameters0).xy), 0)" + default: + throw ParserError.parseFail("int3 constructor with invalid number of arguments encountered") + } + } else { + loadCall = "read(uint2(\(first).xy), 0)" + } + + token = cfp.cur_token + return loadCall + } + } + + /// Generates the explicit arguments that need to be passed into MSL shader functions in place of direct access to + /// uniform globals which are not supported by Metal. + /// - Parameter functionName: Name of the function to generate the additional arguments for + /// - Returns: String of additional arguments to be put into the function signature + private func generateAdditionalArguments(for functionName: String) -> String? { + var output = String() + + for function in functions.values { + if function.name != functionName { + continue + } + + if function.requiresUniformBuffers { + output.append("uniforms") + } + + for texture in function.textures { + for uniform in uniforms.values { + if uniform.name == texture && uniform.storageType.contains(.typeTexture) { + output.append(texture) + } + } + } + + for sampler in function.samplers { + for i in 0..<parser.samplers.num { + let samplerPointer: UnsafeMutablePointer<shader_sampler>? = parser.samplers.array.advanced(by: i) + + if let samplerPointer { + if sampler == String(cString: samplerPointer.pointee.name) { + output.append(sampler) + } + } + } + } + } + + if output.count > 0 { + return output.joined(separator: ", ") + } + + return nil + } + + deinit { + withUnsafeMutablePointer(to: &parser) { + shader_parser_free($0) + } + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/OBSSwapChain.swift
Added
@@ -0,0 +1,125 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +import AppKit +import CoreVideo +import Foundation +import Metal + +class OBSSwapChain { + enum ColorRange { + case sdr + case hdrPQ + case hdrHLG + } + + private weak var device: MetalDevice? + private var view: NSView? + + var colorRange: ColorRange + var edrHeadroom: CGFloat = 0.0 + let layer: CAMetalLayer + var renderTarget: MetalTexture? + var viewSize: MTLSize + var fence: MTLFence + var discard: Bool = false + + init?(device: MetalDevice, size: MTLSize, colorSpace: gs_color_format) { + self.device = device + self.viewSize = size + self.layer = CAMetalLayer() + self.layer.framebufferOnly = false + self.layer.device = device.device + self.layer.drawableSize = CGSize(width: viewSize.width, height: viewSize.height) + self.layer.pixelFormat = .bgra8Unorm_srgb + self.layer.colorspace = CGColorSpace(name: CGColorSpace.sRGB) + self.layer.wantsExtendedDynamicRangeContent = false + self.layer.edrMetadata = nil + self.layer.displaySyncEnabled = false + self.colorRange = .sdr + + guard let fence = device.device.makeFence() else { return nil } + + self.fence = fence + } + + /// Updates the provided view to use the `CAMetalLayer` managed by the ``OBSSwapChain`` + /// - Parameter view: `NSView` instance to update + /// + /// > Important: This function has to be called from the main thread + @MainActor + func updateView(_ view: NSView) { + self.view = view + view.layer = self.layer + view.wantsLayer = true + + updateEdrHeadroom() + } + + /// Updates the EDR headroom value on the ``OBSSwapChain`` with the value from the screen the managed `NSView` is + /// associated with. + /// + /// This is necessary to ensure that the projector uses the appropriate SDR or EDR output depending on the screen + /// the view is on. + @MainActor + func updateEdrHeadroom() { + guard let view = self.view else { + return + } + + if let screen = view.window?.screen { + self.edrHeadroom = screen.maximumPotentialExtendedDynamicRangeColorComponentValue + } else { + self.edrHeadroom = CGFloat(1.0) + } + } + + /// Resizes the drawable of the managed `CAMetalLayer` to the provided size + /// - Parameter size: Desired new size of the drawable + /// + /// This is usually achieved via a delegate method directly on the associated `NSView` instance, but because the + /// view is managed by Qt, the resize event is routed manually into the ``OBSSwapChain`` instance by `libobs`. + func resize(_ size: MTLSize) { + guard viewSize.width != size.width || viewSize.height != size.height else { return } + + viewSize = size + layer.drawableSize = CGSize( + width: viewSize.width, + height: viewSize.height) + renderTarget = nil + } + + /// Gets an opaque pointer for the ``OBSSwapChain`` instance and increases its reference count by one + /// - Returns: `OpaquePointer` to class instance + /// + /// > Note: Use this method when the instance is to be shared via an `OpaquePointer` and needs to be retained. Any + /// opaque pointer shared this way needs to be converted into a retained reference again to ensure automatic + /// deinitialization by the Swift runtime. + func getRetained() -> OpaquePointer { + let retained = Unmanaged.passRetained(self).toOpaque() + + return OpaquePointer(retained) + } + + /// Gets an opaque pointer for the ``OBSSwapChain`` instance without increasing its reference count + /// - Returns: `OpaquePointer` to class instance + func getUnretained() -> OpaquePointer { + let unretained = Unmanaged.passUnretained(self).toOpaque() + + return OpaquePointer(unretained) + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/README.md
Added
@@ -0,0 +1,83 @@ +libobs-metal +============ + +This is an alpha quality implementation of a Metal renderer backend for OBS exclusive to Apple Silicon Macs. It supports all default source types, filters, and transitions provided by OBS Studio + +## Overview + +* The renderer backend is implemented entirely in Swift +* A C interface header is generated automatically via the `-emit-objc-header` compile flag and `@cdecl("<FUNCTION NAME>")` decorators are used to expose desired functions to `libobs` +* Only Metal Version 3 is supported (this is by design) +* Only Apple Silicon Macs are supported (this is by design) + +## Implemented functionality + +* Default source types are supported: + * Color Source + * Image Source + * Media Source + * SCK Capture Source + * Browser Source + * Capture Card and Video Capture Device Source + * Text (Freetype 2) +* Default transitions are supported: + * Cut + * Fade + * Stinger + * Fade To Color + * Luma Wipe +* Default filters are supported: + * Apply LUT + * Chroma Key + * Color Correction + * Crop/Pad + * Image Mask/Blend + * Luma Key + * Scaling/Aspect Ratio + * Scroll + * Sharpen +* sRGB-aware rendering is enabled by default +* HDR output in previews and projectors is supported on screens which have EDR support +* HDR output is not tonemapped by OBS - if the screen has EDR support, the previews will always output content in their actual format +* Recording and streaming with VideoToolbox encoders works +* Preview, separate projectors, and multi-view all work (with caveats, see below) + +## Known Issues + +* Previews can stutter or be stuck with low FPS - will not be fully fixed before alpha release (see below) +* Not all possible encoder configurations have been tested +* Performance is not optimized (see below) + +## The State Of Previews + +To manually render contents into a window using Metal one has to use a `CAMetalLayer` that is set to be a `NSView`'s backing layer. This layer can provide a `CAMetalDrawable` object which the compositor will use when it renders a new frame of the desktop. This drawable can provide a texture that OBS Studio can render into to generate output like the main preview. + +Because Metal is much more integrated with macOS than OpenGL and designed with energy efficiency in mind, a `CAMetalLayer` will never provide more drawables than necessary, which means that there can be at most 3 drawables "in flight". If all available drawables are in use (either by OBS Studio to render into or by the compositor to render the desktop output) a request for a new drawable will block until an old drawable expires and a new one has been generated. + +This means that if OBS renders at a higher framerate than the operating system's compositor, it will exhaust this budget and OBS Studio's renderer will be stalled and will have to wait until a new drawable is available. This effectively means that OBS Studio's maximum frame rate is limited to the operating system's screen refresh interval. + +The current implementation avoids the issue of stalling OBS Studio's video render framerate, at the cost of possible framerate issues with the preview itself. OBS will always render a preview at its own framerate (which can be higher but also lower than the operating system's refresh interval) and callback provided to macOS will be used instead to copy (or "blit") this preview texture into a drawable that is only kept around as short as necessary to finish this copy operation. + +This decouples the update of previews from the rendering of their contents, but obviously makes this blit operation dependent on a projector having finished rendering, as otherwise the callback might blit an incomplete preview or multi-view. It is this synchronization that can lead to slow and "choppy" frame rates if the refresh interval of the operating system and the interval at which OBS can finish rendering a preview are too misaligned. + +**Note:** This is a known issue and work on a fix or better implementation of preview rendering is in progress. As the way `CAMetalLayer` works is the opposite of the way `DXGISwapChain`s work, it requires a lot more resource management and housekeeping in the Metal backend to get right. + +## On Performance + +Compiled in Release configuration the Metal renderer already has about the same CPU impact and render times as the OpenGL renderer on an M1 Mac even though neither the Swift code nor the Metal code has been optimized in any way. The late generation (and switches) of pipeline states and buffers is a costly operation and the way OBS Studio's renderer operates puts a natural ceiling on the performance improvements the Metal renderer could achieve (as it does lots of small render operations but with a lot of context switching between CPU and GPU). + +In Debug mode the performance is a bit worse, but that's in part due to Xcode using the debug variant of the Metal framework, which allows inspection and reflection on all Metal types, including live previews of textures, buffers, debugging of shaders, and more. + +Usually one would prefer to upload all data in big batches (preferably into a big `MTLHeap` object) and then pick and choose elements for each render pass to limit the switch between CPU and GPU, but this is not compatible with how OBS Studio's renderer works at this moment. + +**Note:** All these observations are based on OBS Studio's own CPU and render time statistics which are flawed as the clock speeds of either CPU and GPU are not taken into account. + +## Required Fixes and Workarounds + +* Metal Shader Language is stricter than HLSL and GLSL and does not allow type punning or implicit casting - all type conversions have to be explicit - and commonly allows only a specific set of types for vector data, colour data, or UV coordinates + * The transpiler has to force conversions to unsigned integers and unsigned integer vectors for texture `Load` calls because `libobs` shaders depend on the implicit conversion of a 32-bit float vector to integer values when passed to the texture's load command (`read` in MSL) + * Metal has no support for BGRX/RGBX formats, color always has to be specified using a vector of 4 floats, some `libobs` shaders assume BGRX and only provide a `float3` value in their pixel shaders. Transpiled Metal shaders instead return a `float4` with a `1.0` alpha value + * This might not be exhaustive, as other - so far untested - shaders might depend on other implicit conversions of HLSL/GLSL and will require additional workarounds and wrapping of existing code to return the correct types expected by MSL +* Metal does not support unpacking `UInt32` values into a `float4` in vertex data provided via the `stage_in` attribute to benefit from vertex fetch (where the pipeline itself is made aware of the buffer layout via a vertex descriptor and thus fetches the data from the buffer as needed) vs. the classic "vertex push" method + * This is commonly used in `libobs` to provide color buffer data - to fix this, the values are unpacked and converted into a `float4` when the GPU buffers are created for a vertex buffer +* There is no explicit clear command in Metal, as clears are implemented as a command that is run when a render target (or more precisely a tile of the render target) is loaded into tile memory for a render pass. If no render pass occurs, no load command is executed and the render target is not cleared. OBS Studio depends on a "clear" call actually clearing the texture, thus an explicit (but lightweight) draw call is scheduled to ensure that the render target is loaded, cleared, and stored.
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/Sequence+Hashable.swift
Added
@@ -0,0 +1,25 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +extension Sequence where Iterator.Element: Hashable { + /// Filters a `Sequence` to only contain its unique elements, retaining order for unique elements. + /// - Returns: Filtered `Sequence` with unique elements of original `Sequence` + func unique() -> Iterator.Element { + var seen: Set<Iterator.Element> = + return filter { seen.insert($0).inserted } + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/libobs+Extensions.swift
Added
@@ -0,0 +1,486 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +import Foundation +import Metal +import simd + +public enum OBSLogLevel: Int32 { + case error = 100 + case warning = 200 + case info = 300 + case debug = 400 +} + +extension strref { + mutating func getString() -> String { + let buffer = UnsafeRawBufferPointer(start: self.array, count: self.len) + + let string = String(decoding: buffer, as: UTF8.self) + + return string + } + + mutating func isEqualTo(_ comparison: String) -> Bool { + return strref_cmp(&self, comparison.cString(using: .utf8)) == 0 + } + + mutating func isEqualToCString(_ comparison: UnsafeMutablePointer<CChar>?) -> Bool { + if let comparison { + let result = withUnsafeMutablePointer(to: &self) { + strref_cmp($0, comparison) == 0 + } + + return result + } + + return false + } +} + +extension cf_parser { + mutating func advanceToken() -> Bool { + let result = withUnsafeMutablePointer(to: &self) { + cf_next_token($0) + } + + return result + } + + mutating func hasNextToken() -> Bool { + let result = withUnsafeMutablePointer(to: &self) { + var nextToken: UnsafeMutablePointer<cf_token>? + + switch $0.pointee.cur_token.pointee.type { + case CFTOKEN_SPACETAB, CFTOKEN_NEWLINE, CFTOKEN_NONE: + nextToken = $0.pointee.cur_token + default: + nextToken = $0.pointee.cur_token.advanced(by: 1) + } + + if var nextToken { + while nextToken.pointee.type == CFTOKEN_SPACETAB || nextToken.pointee.type == CFTOKEN_NEWLINE { + nextToken = nextToken.successor() + } + + return nextToken.pointee.type != CFTOKEN_NONE + } else { + return false + } + } + + return result + } + + mutating func tokenIsEqualTo(_ comparison: String) -> Bool { + let result = withUnsafeMutablePointer(to: &self) { + cf_token_is($0, comparison.cString(using: .utf8)) + } + + return result + } +} + +extension gs_shader_param_type { + var size: Int { + switch self { + case GS_SHADER_PARAM_BOOL, GS_SHADER_PARAM_INT, GS_SHADER_PARAM_FLOAT: + return MemoryLayout<Float32>.size + case GS_SHADER_PARAM_INT2, GS_SHADER_PARAM_VEC2: + return MemoryLayout<Float32>.size * 2 + case GS_SHADER_PARAM_INT3, GS_SHADER_PARAM_VEC3: + return MemoryLayout<Float32>.size * 3 + case GS_SHADER_PARAM_INT4, GS_SHADER_PARAM_VEC4: + return MemoryLayout<Float32>.size * 4 + case GS_SHADER_PARAM_MATRIX4X4: + return MemoryLayout<Float32>.size * 4 * 4 + case GS_SHADER_PARAM_TEXTURE: + return MemoryLayout<gs_shader_texture>.size + case GS_SHADER_PARAM_STRING, GS_SHADER_PARAM_UNKNOWN: + return 0 + default: + return 0 + } + } + + var mtlSize: Int { + switch self { + case GS_SHADER_PARAM_BOOL, GS_SHADER_PARAM_INT, GS_SHADER_PARAM_FLOAT: + return MemoryLayout<simd_float1>.size + case GS_SHADER_PARAM_INT2, GS_SHADER_PARAM_VEC2: + return MemoryLayout<simd_float2>.size + case GS_SHADER_PARAM_INT3, GS_SHADER_PARAM_VEC3: + return MemoryLayout<simd_float3>.size + case GS_SHADER_PARAM_INT4, GS_SHADER_PARAM_VEC4: + return MemoryLayout<simd_float4>.size + case GS_SHADER_PARAM_MATRIX4X4: + return MemoryLayout<simd_float4x4>.size + case GS_SHADER_PARAM_TEXTURE: + return MemoryLayout<gs_shader_texture>.size + case GS_SHADER_PARAM_STRING, GS_SHADER_PARAM_UNKNOWN: + return 0 + default: + return 0 + } + } + + var mtlAlignment: Int { + switch self { + case GS_SHADER_PARAM_BOOL, GS_SHADER_PARAM_INT, GS_SHADER_PARAM_FLOAT: + return MemoryLayout<simd_float1>.alignment + case GS_SHADER_PARAM_INT2, GS_SHADER_PARAM_VEC2: + return MemoryLayout<simd_float2>.alignment + case GS_SHADER_PARAM_INT3, GS_SHADER_PARAM_VEC3: + return MemoryLayout<simd_float3>.alignment + case GS_SHADER_PARAM_INT4, GS_SHADER_PARAM_VEC4: + return MemoryLayout<simd_float4>.alignment + case GS_SHADER_PARAM_MATRIX4X4: + return MemoryLayout<simd_float4x4>.alignment + case GS_SHADER_PARAM_TEXTURE: + return 0 + case GS_SHADER_PARAM_STRING, GS_SHADER_PARAM_UNKNOWN: + return 0 + default: + return 0 + + } + } +} + +extension gs_color_format { + var sRGBVariant: MTLPixelFormat? { + switch self { + case GS_RGBA: + return .rgba8Unorm_srgb + case GS_BGRX, GS_BGRA: + return .bgra8Unorm_srgb + default: + return nil + } + } + + var mtlFormat: MTLPixelFormat { + switch self { + case GS_A8: + return .a8Unorm + case GS_R8: + return .r8Unorm + case GS_R8G8: + return .rg8Unorm + case GS_R16: + return .r16Unorm + case GS_R16F: + return .r16Float + case GS_RG16: + return .rg16Unorm + case GS_RG16F: + return .rg16Float + case GS_R32F: + return .r32Float + case GS_RG32F: + return .rg32Float + case GS_RGBA: + return .rgba8Unorm + case GS_BGRX, GS_BGRA: + return .bgra8Unorm + case GS_R10G10B10A2: + return .rgb10a2Unorm + case GS_RGBA16: + return .rgba16Unorm + case GS_RGBA16F: + return .rgba16Float + case GS_RGBA32F: + return .rgba32Float + case GS_DXT1: + return .bc1_rgba + case GS_DXT3: + return .bc2_rgba + case GS_DXT5: + return .bc3_rgba + default: + return .invalid + } + } +} + +extension gs_color_space { + var colorFormat: gs_color_format { + switch self { + case GS_CS_SRGB_16F, GS_CS_709_SCRGB: + return GS_RGBA16F + default: + return GS_RGBA + } + } + + var pixelFormat: MTLPixelFormat? { + switch self { + case GS_CS_SRGB: + .bgra8Unorm_srgb + case GS_CS_709_SCRGB: + nil + case GS_CS_709_EXTENDED: + .bgra10_xr_srgb + case GS_CS_SRGB_16F: + nil + default: + nil + } + } +} + +extension gs_depth_test { + var mtlFunction: MTLCompareFunction { + switch self { + case GS_NEVER: + return .never + case GS_LESS: + return .less + case GS_LEQUAL: + return .lessEqual + case GS_EQUAL: + return .equal + case GS_GEQUAL: + return .greaterEqual + case GS_GREATER: + return .greater + case GS_NOTEQUAL: + return .notEqual + case GS_ALWAYS: + return .always + default: + return .never + } + } +} + +extension gs_stencil_op_type { + var mtlOperation: MTLStencilOperation { + switch self { + case GS_KEEP: + return .keep + case GS_ZERO: + return .zero + case GS_REPLACE: + return .replace + case GS_INCR: + return .incrementWrap + case GS_DECR: + return .decrementWrap + case GS_INVERT: + return .invert + default: + return .keep + } + } +} + +extension gs_blend_type { + var blendFactor: MTLBlendFactor? { + switch self { + case GS_BLEND_ZERO: + return .zero + case GS_BLEND_ONE: + return .one + case GS_BLEND_SRCCOLOR: + return .sourceColor + case GS_BLEND_INVSRCCOLOR: + return .oneMinusSourceColor + case GS_BLEND_SRCALPHA: + return .sourceAlpha + case GS_BLEND_INVSRCALPHA: + return .oneMinusSourceAlpha + case GS_BLEND_DSTCOLOR: + return .destinationColor + case GS_BLEND_INVDSTCOLOR: + return .oneMinusDestinationColor + case GS_BLEND_DSTALPHA: + return .destinationAlpha + case GS_BLEND_INVDSTALPHA: + return .oneMinusDestinationAlpha + case GS_BLEND_SRCALPHASAT: + return .sourceAlphaSaturated + default: + return nil + } + } +} + +extension gs_blend_op_type { + var mtlOperation: MTLBlendOperation? { + switch self { + case GS_BLEND_OP_ADD: + return .add + case GS_BLEND_OP_MAX: + return .max + case GS_BLEND_OP_MIN: + return .min + case GS_BLEND_OP_SUBTRACT: + return .subtract + case GS_BLEND_OP_REVERSE_SUBTRACT: + return .reverseSubtract + default: + return nil + } + } +} + +extension gs_cull_mode { + var mtlMode: MTLCullMode { + switch self { + case GS_BACK: + return .back + case GS_FRONT: + return .front + default: + return .none + } + } +} + +extension gs_draw_mode { + var mtlPrimitive: MTLPrimitiveType? { + switch self { + case GS_POINTS: + return .point + case GS_LINES: + return .line + case GS_LINESTRIP: + return .lineStrip + case GS_TRIS: + return .triangle + case GS_TRISTRIP: + return .triangleStrip + default: + return nil + } + } +} + +extension gs_rect { + var mtlViewPort: MTLViewport { + MTLViewport( + originX: Double(self.x), + originY: Double(self.y), + width: Double(self.cx), + height: Double(self.cy), + znear: 0.0, + zfar: 1.0) + } + + var mtlScissorRect: MTLScissorRect { + MTLScissorRect( + x: Int(self.x), + y: Int(self.y), + width: Int(self.cx), + height: Int(self.cy)) + } +} + +extension gs_zstencil_format { + var mtlFormat: MTLPixelFormat { + switch self { + case GS_ZS_NONE: + return .invalid + case GS_Z16: + return .depth16Unorm + case GS_Z24_S8: + return .depth24Unorm_stencil8 + case GS_Z32F: + return .depth32Float + case GS_Z32F_S8X24: + return .depth32Float_stencil8 + default: + return .invalid + } + } +} + +extension gs_index_type { + var mtlType: MTLIndexType? { + switch self { + case GS_UNSIGNED_LONG: + return .uint16 + case GS_UNSIGNED_SHORT: + return .uint32 + default: + return nil + } + } + + var byteSize: Int { + guard let indexType = self.mtlType else { + return 0 + } + + let byteSize = + if indexType == .uint16 { + 2 + } else { + 4 + } + + return byteSize + } +} + +extension gs_address_mode { + var mtlMode: MTLSamplerAddressMode? { + switch self { + case GS_ADDRESS_WRAP: + return .repeat + case GS_ADDRESS_CLAMP: + return .clampToEdge + case GS_ADDRESS_MIRROR: + return .mirrorRepeat + case GS_ADDRESS_BORDER: + return .clampToBorderColor + case GS_ADDRESS_MIRRORONCE: + return .mirrorClampToEdge + default: + return nil + } + } +} + +extension gs_sample_filter { + var minMagFilter: MTLSamplerMinMagFilter? { + switch self { + case GS_FILTER_POINT, GS_FILTER_MIN_MAG_POINT_MIP_LINEAR, GS_FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT, + GS_FILTER_MIN_POINT_MAG_MIP_LINEAR: + return .nearest + case GS_FILTER_LINEAR, GS_FILTER_MIN_LINEAR_MAG_MIP_POINT, GS_FILTER_MIN_LINEAR_MAG_POINT_MIP_LINEAR, + GS_FILTER_MIN_MAG_LINEAR_MIP_POINT, GS_FILTER_ANISOTROPIC: + return .linear + default: + return nil + } + } + + var mipFilter: MTLSamplerMipFilter? { + switch self { + case GS_FILTER_POINT, GS_FILTER_MIN_MAG_POINT_MIP_LINEAR, GS_FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT, + GS_FILTER_MIN_POINT_MAG_MIP_LINEAR: + return .nearest + case GS_FILTER_LINEAR, GS_FILTER_MIN_LINEAR_MAG_MIP_POINT, GS_FILTER_MIN_LINEAR_MAG_POINT_MIP_LINEAR, + GS_FILTER_MIN_MAG_LINEAR_MIP_POINT, GS_FILTER_ANISOTROPIC: + return .linear + default: + return nil + } + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/libobs+SignalHandlers.swift
Added
@@ -0,0 +1,34 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +import Foundation + +enum MetalSignalType: String { + case videoReset = "video_reset" +} + +/// Dispatches the video reset event to the ``MetalDevice`` instance +/// - Parameters: +/// - param: Opaque pointer to a ``MetalDevice`` instance +/// - _: Unused pointer to signal callback data +public func metal_video_reset_handler(_ param: UnsafeMutableRawPointer?, _: UnsafeMutablePointer<calldata>?) { + guard let param else { return } + + let metalDevice = unsafeBitCast(param, to: MetalDevice.self) + + metalDevice.dispatchSignal(type: .videoReset) +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/libobs-metal-Bridging-Header.h
Added
@@ -0,0 +1,32 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +#import <util/base.h> +#import <util/cf-parser.h> +#import <util/cf-lexer.h> + +#import <obs.h> + +#import <graphics/graphics.h> +#import <graphics/device-exports.h> +#import <graphics/vec2.h> +#import <graphics/matrix3.h> +#import <graphics/matrix4.h> +#import <graphics/shader-parser.h> + +static const char *const device_name = "Metal"; +static const char *const preprocessor_name = "_Metal";
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/metal-indexbuffer.swift
Added
@@ -0,0 +1,158 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +import Foundation +import Metal + +/// Creates a ``MetalIndexBuffer`` object to share with `libobs` and hold the provided indices +/// +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - type: Size of each index value (16 bit or 32 bit) +/// - indices: Opaque pointer to index buffer data set up by `libobs` +/// - num: Count of vertices present at the memory address provided by the `indices` argument +/// - flags: Bit field of `libobs` buffer flags +/// - Returns: Opaque pointer to a retained ``MetalIndexBuffer`` instance if valid index type was provided, `nil` +/// otherwise +/// +/// > Note: The ownership of the memory pointed to by `indices` is implicitly transferred to the ``MetalIndexBuffer`` +/// instance, but is not managed by Swift. +@_cdecl("device_indexbuffer_create") +public func device_indexbuffer_create( + device: UnsafeRawPointer, type: gs_index_type, indices: UnsafeMutableRawPointer, num: UInt32, flags: UInt32 +) -> OpaquePointer? { + let device: MetalDevice = unretained(device) + + guard let indexType = type.mtlType else { + return nil + } + + let indexBuffer = MetalIndexBuffer( + device: device, + type: indexType, + data: indices, + count: Int(num), + dynamic: (Int32(flags) & GS_DYNAMIC) != 0 + ) + + return indexBuffer.getRetained() +} + +/// Sets up a ``MetalIndexBuffer`` as the index buffer for the current pipeline +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - indexbuffer: Opaque pointer to ``MetalIndexBuffer`` instance shared with `libobs` +/// +/// > Note: The reference count of the ``MetalIndexBuffer`` instance will not be increased by this call. +/// +/// > Important: If a `nil` pointer is provided as the index buffer, the index buffer will be _unset_. +@_cdecl("device_load_indexbuffer") +public func device_load_indexbuffer(device: UnsafeRawPointer, indexbuffer: UnsafeRawPointer?) { + let device: MetalDevice = unretained(device) + + if let indexbuffer { + device.renderState.indexBuffer = unretained(indexbuffer) + } else { + device.renderState.indexBuffer = nil + } +} + +/// Requests the deinitialization of a shared ``MetalIndexBuffer`` instance +/// - Parameter indexBuffer: Opaque pointer to ``MetalIndexBuffer`` instance shared with `libobs` +/// +/// The deinitialization is handled automatically by Swift after the ownership of the instance has been transferred +/// into the function and becomes the last strong reference to it. After the function leaves its scope, the object will +/// be deinitialized and deallocated automatically. +/// +/// > Note: The index buffer data memory is implicitly owned by the ``MetalIndexBuffer`` instance and will be manually +/// cleaned up and deallocated by the instance's `deinit` method. +@_cdecl("gs_indexbuffer_destroy") +public func gs_indexbuffer_destroy(indexBuffer: UnsafeRawPointer) { + let _ = retained(indexBuffer) as MetalIndexBuffer +} + +/// Requests the index buffer's current data to be transferred into GPU memory +/// - Parameter indexBuffer: Opaque pointer to ``MetalIndexBuffer`` instance shared with `libobs` +/// +/// This function will call `gs_indexbuffer_flush_direct` with `nil` data pointer. +@_cdecl("gs_indexbuffer_flush") +public func gs_indexbuffer_flush(indexBuffer: UnsafeRawPointer) { + gs_indexbuffer_flush_direct(indexBuffer: indexBuffer, data: nil) +} + +/// Requests the index buffer to be updated with the provided data and then transferred into GPU memory +/// - Parameters: +/// - indexBuffer: Opaque pointer to ``MetalIndexBuffer`` instance shared with `libobs` +/// - data: Opaque pointer to index buffer data set up by `libobs` +/// +/// This function is called to ensure that the index buffer data that is contained in the memory pointed at by the +/// `data` argument is uploaded into GPU memory. If a `nil` pointer is provided instead, the data provided to the +/// instance during creation will be used instead. +@_cdecl("gs_indexbuffer_flush_direct") +public func gs_indexbuffer_flush_direct(indexBuffer: UnsafeRawPointer, data: UnsafeMutableRawPointer?) { + let indexBuffer: MetalIndexBuffer = unretained(indexBuffer) + + indexBuffer.setupBuffers(data) +} + +/// Returns an opaque pointer to the index buffer data associated with the ``MetalIndexBuffer`` instance +/// - Parameter indexBuffer: Opaque pointer to ``MetalIndexBuffer`` instance shared with `libobs` +/// - Returns: Opaque pointer to index buffer data in memory +/// +/// The returned opaque pointer represents the unchanged memory address that was provided for the creation of the index +/// buffer object. +/// +/// > Warning: There is only limited memory safety associated with this pointer. It is implicitly owned and its +/// lifetime is managed by the ``MetalIndexBuffer`` instance, but it was originally created by `libobs`. +@_cdecl("gs_indexbuffer_get_data") +public func gs_indexbuffer_get_data(indexBuffer: UnsafeRawPointer) -> UnsafeMutableRawPointer? { + let indexBuffer: MetalIndexBuffer = unretained(indexBuffer) + + return indexBuffer.indexData +} + +/// Returns the number of indices associated with the ``MetalIndexBuffer`` instance +/// - Parameter indexBuffer: Opaque pointer to ``MetalIndexBuffer`` instance shared with `libobs` +/// - Returns: Number of index buffers +/// +/// > Note: This returns the same number that was provided for the creation of the index buffer object. +@_cdecl("gs_indexbuffer_get_num_indices") +public func gs_indexbuffer_get_num_indices(indexBuffer: UnsafeRawPointer) -> UInt32 { + let indexBuffer: MetalIndexBuffer = unretained(indexBuffer) + + return UInt32(indexBuffer.count) +} + +/// Gets the index buffer type as a `libobs` enum value +/// - Parameter indexBuffer: Opaque pointer to ``MetalIndexBuffer`` instance shared with `libobs` +/// - Returns: Index buffer type as identified by the `gs_index_type` enum +/// +/// > Warning: As the `gs_index_type` enumeration does not provide an "invalid" value (and thus `0` becomes a valied +/// value), this function has no way to communicate an incompatible index buffer type that might be introduced at a +/// later point. +@_cdecl("gs_indexbuffer_get_type") +public func gs_indexbuffer_get_type(indexBuffer: UnsafeRawPointer) -> gs_index_type { + let indexBuffer: MetalIndexBuffer = unretained(indexBuffer) + + switch indexBuffer.type { + case .uint16: return GS_UNSIGNED_SHORT + case .uint32: return GS_UNSIGNED_LONG + @unknown default: + assertionFailure("gs_indexbuffer_get_type: Unsupported index buffer type \(indexBuffer.type)") + return GS_UNSIGNED_SHORT + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/metal-samplerstate.swift
Added
@@ -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.samplersInt(unit) = samplerState +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/metal-shader.swift
Added
@@ -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.texturesindex = nil + device.renderState.samplersindex = 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.uniformsInt(param) + let unretained = Unmanaged.passUnretained(uniform).toOpaque() + + return OpaquePointer(unretained) +} + +/// Gets a uniform parameter from the ``MetalShader`` by its name +/// - Parameters: +/// - shader: Opaque pointer to ``MetalShader`` instance shared with `libobs` +/// - param: C character array pointer with the name of the requested uniform parameter +/// - Returns: Opaque pointer to a ``ShaderUniform`` instance if any uniform with the provided name was found or `nil` +/// otherwise +/// +/// > Important: The opaque pointer for the ``ShaderUniform`` instance is passe unretained and as such can become +/// invalid when its owning ``MetalShader`` instance either is deinitialized itself or is replaced in the uniforms +/// array. +/// +@_cdecl("gs_shader_get_param_by_name") +public func gs_shader_get_param_by_name(shader: UnsafeRawPointer, param: UnsafeMutablePointer<CChar>) -> OpaquePointer? +{ + let shader: MetalShader = unretained(shader) + + let paramName = String(cString: param) + + for uniform in shader.uniforms { + if uniform.name == paramName { + let unretained = Unmanaged.passUnretained(uniform).toOpaque() + return OpaquePointer(unretained) + } + } + + return nil +} + +/// Gets the uniform parameter associated with the view projection matrix used by the ``MetalShader`` +/// - Parameter shader: Opaque pointer to ``MetalShader`` instance shared with `libobs` +/// - Returns: Opaque pointer to a ``ShaderUniform`` instance if a uniform for the view projection matrix was found +/// or `nil` otherwise +/// +/// The uniform for the view projection matrix has the associated name `viewProj` in the Metal renderer, thus a +/// name-based lookup is used to find the associated ``ShaderUniform`` instance. +/// +/// > Important: The opaque pointer for the ``ShaderUniform`` instance is passe unretained and as such can become +/// invalid when its owning ``MetalShader`` instance either is deinitialized itself or is replaced in the uniforms +/// array. +/// +@_cdecl("gs_shader_get_viewproj_matrix") +public func gs_shader_get_viewproj_matrix(shader: UnsafeRawPointer) -> OpaquePointer? { + let shader: MetalShader = unretained(shader) + let paramName = "viewProj" + + for uniform in shader.uniforms { + if uniform.name == paramName { + let unretained = Unmanaged.passUnretained(uniform).toOpaque() + return OpaquePointer(unretained) + } + } + + return nil +} + +/// Gets the uniform parameter associated with the world projection matrix used by the ``MetalShader`` +/// - Parameter shader: Opaque pointer to ``MetalShader`` instance shared with `libobs` +/// - Returns: Opaque pointer to a ``ShaderUniform`` instance if a uniform for the world projection matrix was found +/// or `nil` otherwise +/// +/// The uniform for the view projection matrix has the associated name `worldProj` in the Metal renderer, thus a +/// name-based lookup is used to find the associated ``ShaderUniform`` instance. +/// +/// > Important: The opaque pointer for the ``ShaderUniform`` instance is passe unretained and as such can become +/// invalid when its owning ``MetalShader`` instance either is deinitialized itself or is replaced in the uniforms +/// array. +@_cdecl("gs_shader_get_world_matrix") +public func gs_shader_get_world_matrix(shader: UnsafeRawPointer) -> OpaquePointer? { + let shader: MetalShader = unretained(shader) + let paramName = "worldProj" + + for uniform in shader.uniforms { + if uniform.name == paramName { + let unretained = Unmanaged.passUnretained(uniform).toOpaque() + return OpaquePointer(unretained) + } + } + + return nil +} + +/// Gets the name and uniform type from the ``ShaderUniform`` instance +/// - Parameters: +/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` +/// - info: Pointer to a `gs_shader_param_info` struct pre-allocated by `libobs` +/// +/// > Warning: The C character array pointer holding the name of the uniform is managed by Swift and might become +/// invalid at any point in time. +@_cdecl("gs_shader_get_param_info") +public func gs_shader_get_param_info(shaderParam: UnsafeRawPointer, info: UnsafeMutablePointer<gs_shader_param_info>) { + let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) + + shaderUniform.name.withCString { + info.pointee.name = $0 + } + info.pointee.type = shaderUniform.gsType +} + +/// Sets a boolean value on the ``ShaderUniform`` instance +/// - Parameters: +/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` +/// - val: Boolean value to set for the uniform +@_cdecl("gs_shader_set_bool") +public func gs_shader_set_bool(shaderParam: UnsafeRawPointer, val: Bool) { + let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) + + withUnsafePointer(to: val) { + shaderUniform.setParameter(data: $0, size: MemoryLayout<Int32>.size) + } +} + +/// Sets a 32-bit floating point value on the ``ShaderUniform`` instance +/// - Parameters: +/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` +/// - val: 32-bit floating point value to set for the uniform +@_cdecl("gs_shader_set_float") +public func gs_shader_set_float(shaderParam: UnsafeRawPointer, val: Float32) { + let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) + + withUnsafePointer(to: val) { + shaderUniform.setParameter(data: $0, size: MemoryLayout<Float32>.size) + } +} + +/// Sets a 32-bit signed integer value on the ``ShaderUniform`` instance +/// - Parameters: +/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` +/// - val: 32-bit signed integer value to set for the uniform +@_cdecl("gs_shader_set_int") +public func gs_shader_set_int(shaderParam: UnsafeRawPointer, val: Int32) { + let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) + + withUnsafePointer(to: val) { + shaderUniform.setParameter(data: $0, size: MemoryLayout<Int32>.size) + } +} + +/// Sets a 3x3 matrix of 32-bit floating point values on the ``ShaderUniform``instance +/// - Parameters: +/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` +/// - val: A 3x3 matrix of 32-bit floating point values +/// +/// The 3x3 matrix is converted into a 4x4 matrix (padded with zeros) before actually being set as the uniform data +@_cdecl("gs_shader_set_matrix3") +public func gs_shader_set_matrix3(shaderParam: UnsafeRawPointer, val: UnsafePointer<matrix3>) { + let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) + + var newMatrix = matrix4() + matrix4_from_matrix3(&newMatrix, val) + + shaderUniform.setParameter(data: &newMatrix, size: MemoryLayout<matrix4>.size) +} + +/// Sets a 4x4 matrix of 32-bit floating point values on the ``ShaderUniform`` instance +/// - Parameters: +/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` +/// - val: A 4x4 matrix of 32-bit floating point values +@_cdecl("gs_shader_set_matrix4") +public func gs_shader_set_matrix4(shaderParam: UnsafeRawPointer, val: UnsafePointer<matrix4>) { + let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) + + shaderUniform.setParameter(data: val, size: MemoryLayout<matrix4>.size) +} + +/// Sets a vector of 2 32-bit floating point values on the ``ShaderUniform`` instance +/// - Parameters: +/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` +/// - val: A vector of 2 32-bit floating point values +@_cdecl("gs_shader_set_vec2") +public func gs_shader_set_vec2(shaderParam: UnsafeRawPointer, val: UnsafePointer<vec2>) { + let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) + + shaderUniform.setParameter(data: val, size: MemoryLayout<vec2>.size) +} + +/// Sets a vector of 3 32-bit floating point values on the ``ShaderUniform`` instance +/// - Parameters: +/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` +/// - val: A vector of 3 32-bit floating point values +@_cdecl("gs_shader_set_vec3") +public func gs_shader_set_vec3(shaderParam: UnsafeRawPointer, val: UnsafePointer<vec3>) { + let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) + + shaderUniform.setParameter(data: val, size: MemoryLayout<vec3>.size) +} + +/// Sets a vector of 4 32-bit floating point values on the ``ShaderUniform`` instance +/// - Parameters: +/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` +/// - val: A vector of 4 32-bit floating point values +@_cdecl("gs_shader_set_vec4") +public func gs_shader_set_vec4(shaderParam: UnsafeRawPointer, val: UnsafePointer<vec4>) { + let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) + + shaderUniform.setParameter(data: val, size: MemoryLayout<vec4>.size) +} + +/// Sets up the data of a `gs_shader_texture` struct as a uniform on the ``ShaderUniform`` instance +/// - Parameters: +/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` +/// - val: A pointer to a `gs_shader_struct` containing an opaque pointer to the actual ``MetalTexture`` instance +/// and an sRGB gamma state flag +/// +/// The struct's data is copied verbatim into the uniform, which allows reconstruction of the pointer at a later point +/// as long as the actual ``MetalTexture`` instance still exists. +@_cdecl("gs_shader_set_texture") +public func gs_shader_set_texture(shaderParam: UnsafeRawPointer, val: UnsafePointer<gs_shader_texture>?) { + let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) + + if let val { + shaderUniform.setParameter(data: val, size: MemoryLayout<gs_shader_texture>.size) + } +} + +/// Sets an arbitrary value on the ``ShaderUniform`` instance +/// - Parameters: +/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` +/// - val: Opaque pointer to some unknown data for use as the uniform +/// - size: The size of the data available at the memory pointed to by the `val` argument +/// +/// The ``ShaderUniform`` itself is set up to hold a specific uniform type, each of which is associated with a size of +/// bytes required for it. If the size of the data pointed to by `val` does not fit into this size, the uniform will +/// not be updated. +/// +/// If the ``ShaderUniform`` expects a texture parameter, the pointer will be bound as memory of a `gs_shader_texture` +/// instance before setting it up. +@_cdecl("gs_shader_set_val") +public func gs_shader_set_val(shaderParam: UnsafeRawPointer, val: UnsafeRawPointer, size: UInt32) { + let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) + + let size = Int(size) + let valueSize = shaderUniform.gsType.size + + guard valueSize == size else { + assertionFailure("gs_shader_set_val: Required size of uniform does not match size of input") + return + } + + if shaderUniform.gsType == GS_SHADER_PARAM_TEXTURE { + let shaderTexture = val.bindMemory(to: gs_shader_texture.self, capacity: 1) + + shaderUniform.setParameter(data: shaderTexture, size: valueSize) + } else { + let bytes = val.bindMemory(to: UInt8.self, capacity: valueSize) + shaderUniform.setParameter(data: bytes, size: valueSize) + } +} + +/// Resets the ``ShaderUniform``'s current data with its default data +/// - Parameter shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` +/// +/// Each ``ShaderUniform`` is optionally set up with a set of default data (stored as an array of bytes) which is +/// simply copied into the current values. +@_cdecl("gs_shader_set_default") +public func gs_shader_set_default(shaderParam: UnsafeRawPointer) { + let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) + + if let defaultValues = shaderUniform.defaultValues { + shaderUniform.currentValues = Array(defaultValues) + } +} + +/// Sets up the ``MTLSamplerState`` as the sampler state for the ``ShaderUniform`` +/// - Parameters: +/// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` +/// - sampler: Opaque pointer to ``MTLSamplerState`` instance shared with `libobs` +/// +/// If the uniform represents a texture for use in the associated shader, this function will also set up the provided +/// ``MTLSamplerState`` for the associated texture's texture slot. +@_cdecl("gs_shader_set_next_sampler") +public func gs_shader_set_next_sampler(shaderParam: UnsafeRawPointer, sampler: UnsafeRawPointer) { + let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) + + let samplerState = Unmanaged<MTLSamplerState>.fromOpaque(sampler).takeUnretainedValue() + + shaderUniform.samplerState = samplerState +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/metal-stagesurf.swift
Added
@@ -0,0 +1,130 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +import Foundation +import Metal + +/// Creates a ``MetalStageBuffer`` instance for use as a stage surface by `libobs` +/// - Parameters: +/// - device: Opaque pointer to ``MetalStageBuffer`` instance shared with `libobs` +/// - width: Number of data rows +/// - height: Number of data columns +/// - format: Color format of the stage surface texture as defined by `libobs`'s `gs_color_format` struct +/// - Returns: A ``MetalStageBuffer`` instance that wraps a `MTLBuffer` or a `nil` pointer otherwise +/// +/// Stage surfaces are used by `libobs` for transfer of image data from the GPU to the CPU. The most common use case is +/// to block transfer (blit) the video output texture into a staging texture and then downloading the texture data from +/// the staging texture into CPU memory. +@_cdecl("device_stagesurface_create") +public func device_stagesurface_create(device: UnsafeRawPointer, width: UInt32, height: UInt32, format: gs_color_format) + -> OpaquePointer? +{ + let device: MetalDevice = unretained(device) + + guard + let buffer = MetalStageBuffer( + device: device, + width: Int(width), + height: Int(height), + format: format.mtlFormat + ) + else { + OBSLog(.error, "device_stagesurface_create: Unable to create MetalStageBuffer with provided format \(format)") + return nil + } + + return buffer.getRetained() +} + +/// Requests the deinitialization of the ``MetalStageBuffer`` instance that was shared with `libobs` +/// - Parameter stagesurf: Opaque pointer to ``MetalStageBuffer`` instance shared with `libobs` +/// +/// The ownership of the shared pointer is transferred into this function and the instance is placed under Swift's +/// memory management again. +@_cdecl("gs_stagesurface_destroy") +public func gs_stagesurface_destroy(stagesurf: UnsafeRawPointer) { + let _ = retained(stagesurf) as MetalStageBuffer +} + +/// Gets the "width" of the staging texture +/// - Parameter stagesurf: Opaque pointer to ``MetalStageBuffer`` instance shared with `libobs` +/// - Returns: Amount of data rows in the buffer representing the width of an image +@_cdecl("gs_stagesurface_get_width") +public func gs_stagesurface_get_width(stagesurf: UnsafeRawPointer) -> UInt32 { + let stageSurface: MetalStageBuffer = unretained(stagesurf) + + return UInt32(stageSurface.width) +} + +/// Gets the "height" of the staging texture +/// - Parameter stagesurf: Opaque pointer to ``MetalStageBuffer`` instance shared with `libobs` +/// - Returns: Amount of data columns in the buffer representing the height of an image +@_cdecl("gs_stagesurface_get_height") +public func gs_stagesurface_get_height(stagesurf: UnsafeRawPointer) -> UInt32 { + let stageSurface: MetalStageBuffer = unretained(stagesurf) + + return UInt32(stageSurface.height) +} + +/// Gets the color format of the staged image data +/// - Parameter stagesurf: Opaque pointer to ``MetalStageBuffer`` instance shared with `libobs` +/// - Returns: Color format in `libobs`'s own color format struct +/// +/// The Metal color format is automatically converted into its corresponding `gs_color_format` variant. +@_cdecl("gs_stagesurface_get_color_format") +public func gs_stagesurface_get_height(stagesurf: UnsafeRawPointer) -> gs_color_format { + let stageSurface: MetalStageBuffer = unretained(stagesurf) + + return stageSurface.format.gsColorFormat +} + +/// Provides a pointer to memory that contains the buffer's raw data. +/// - Parameters: +/// - stagesurf: Opaque pointer to ``MetalStageBuffer`` instance shared with `libobs` +/// - ptr: Opaque pointer to memory which itself can hold a pointer to the actual image data +/// - linesize: Opaque pointer to memory which itself can hold the row size of the image data +/// - Returns: `true` if the data can be provided, `false` otherwise +/// +/// Metal does not provide "map" and "unmap" operations as they exist in Direct3D11, as resource management and +/// synchronization needs to be handled explicitly by the application. To reduce unnecessary copy operations, the +/// original texture's data was copied into a `MTLBuffer` (instead of another texture) using a block transfer on the +/// GPU. +/// +/// As the Metal renderer is only available on Apple Silicon machines, this means that the buffer itself is available +/// for direct access by the CPU and thus a pointer to the raw bytes of the buffer can be shared with `libobs`. +@_cdecl("gs_stagesurface_map") +public func gs_stagesurface_map( + stagesurf: UnsafeRawPointer, ptr: UnsafeMutablePointer<UnsafeMutableRawPointer>, + linesize: UnsafeMutablePointer<UInt32> +) -> Bool { + let stageSurface: MetalStageBuffer = unretained(stagesurf) + + ptr.pointee = stageSurface.buffer.contents() + linesize.pointee = UInt32(stageSurface.width * stageSurface.format.bytesPerPixel!) + + return true +} + +/// Signals that the downloaded image data of the stage texture is not needed anymore. +/// +/// - Parameter stagesurf: Opaque pointer to ``MetalStageBuffer`` instance shared with `libobs` +/// +/// This function has no effect as the `MTLBuffer` used by the ``MetalStageBuffer`` does not need to be "unmapped". +@_cdecl("gs_stagesurface_unmap") +public func gs_stagesurface_unmap(stagesurf: UnsafeRawPointer) { + return +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/metal-subsystem.swift
Added
@@ -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.colorAttachments0.sourceRGBBlendFactor = sourceRGBFactor + pipelineDescriptor.colorAttachments0.sourceAlphaBlendFactor = sourceAlphaFactor + pipelineDescriptor.colorAttachments0.destinationRGBBlendFactor = destinationRGBFactor + pipelineDescriptor.colorAttachments0.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.colorAttachments0.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.texturesInt(index) = nil + device.renderState.samplersInt(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.colorAttachments0.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.colorAttachments0.writeMask = colorMask +} + +/// Sets the depth compare function for the depth stencil descriptor to be used in the current pipeline +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - test: `libobs` enum describing the depth compare function to use +/// +/// The enum value provided by `libobs` is converted into a ``MTLCompareFunction``, which is then set directly as the +/// compare function on the depth stencil descriptor. +@_cdecl("device_depth_function") +public func device_depth_function(device: UnsafeRawPointer, test: gs_depth_test) { + let device: MetalDevice = unretained(device) + + device.renderState.depthStencilDescriptor.depthCompareFunction = test.mtlFunction +} + +/// Sets the stencil compare functions for the specified stencil side(s) on the depth stencil descriptor in the current +/// pipeline. +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - side: The stencil side(s) for which the compare function should be set up +/// - test: `libobs` enum describing the stencil test function to use +/// +/// The enum values provided by `libobs` are first checked for the stencil side, after which the compare function value +/// itself is converted into a ``MTLCompareFunction``, which is then set directly as the compare function on the depth +/// stencil descriptor. +@_cdecl("device_stencil_function") +public func device_stencil_function(device: UnsafeRawPointer, side: gs_stencil_side, test: gs_depth_test) { + let device: MetalDevice = unretained(device) + + let stencilCompareFunction: (MTLCompareFunction, MTLCompareFunction) + + if side == GS_STENCIL_FRONT { + stencilCompareFunction = (test.mtlFunction, .never) + } else if side == GS_STENCIL_BACK { + stencilCompareFunction = (.never, test.mtlFunction) + } else { + stencilCompareFunction = (test.mtlFunction, test.mtlFunction) + } + + device.renderState.depthStencilDescriptor.frontFaceStencil.stencilCompareFunction = stencilCompareFunction.0 + device.renderState.depthStencilDescriptor.backFaceStencil.stencilCompareFunction = stencilCompareFunction.1 +} + +/// Sets the stencil fail, depth fail, and depth pass operations for the specified stencil side(s) on the depth stencil +/// descriptor for the current pipeline. +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - side: The stencil side(s) for which the fail and pass functions should be set up +/// - fail: `libobs` enum value describing the stencil fail operation +/// - zfail: `libobs` enum value describing the depth fail operation +/// - zpass: `libobs` enum value describing the depth pass operation +/// +/// The enum values provided by `libobs` are first checked for the stencil side, after which the fail function values +/// themselves are converted into their ``MTLCompareFunction`` variants, which are then set directly on the depth +/// stencil descriptor. +@_cdecl("device_stencil_op") +public func device_stencil_op( + device: UnsafeRawPointer, side: gs_stencil_side, fail: gs_stencil_op_type, zfail: gs_stencil_op_type, + zpass: gs_stencil_op_type +) { + let device: MetalDevice = unretained(device) + + let stencilFailOperation: (MTLStencilOperation, MTLStencilOperation) + let depthFailOperation: (MTLStencilOperation, MTLStencilOperation) + let depthPassOperation: (MTLStencilOperation, MTLStencilOperation) + + if side == GS_STENCIL_FRONT { + stencilFailOperation = (fail.mtlOperation, .keep) + depthFailOperation = (zfail.mtlOperation, .keep) + depthPassOperation = (zpass.mtlOperation, .keep) + } else if side == GS_STENCIL_BACK { + stencilFailOperation = (.keep, fail.mtlOperation) + depthFailOperation = (.keep, zfail.mtlOperation) + depthPassOperation = (.keep, zpass.mtlOperation) + } else { + stencilFailOperation = (fail.mtlOperation, fail.mtlOperation) + depthFailOperation = (zfail.mtlOperation, zfail.mtlOperation) + depthPassOperation = (zpass.mtlOperation, zpass.mtlOperation) + } + + device.renderState.depthStencilDescriptor.frontFaceStencil.stencilFailureOperation = stencilFailOperation.0 + device.renderState.depthStencilDescriptor.frontFaceStencil.depthFailureOperation = depthFailOperation.0 + device.renderState.depthStencilDescriptor.frontFaceStencil.depthStencilPassOperation = depthPassOperation.0 + + device.renderState.depthStencilDescriptor.backFaceStencil.stencilFailureOperation = stencilFailOperation.1 + device.renderState.depthStencilDescriptor.backFaceStencil.depthFailureOperation = depthFailOperation.1 + device.renderState.depthStencilDescriptor.backFaceStencil.depthStencilPassOperation = depthPassOperation.1 +} + +/// Sets up the viewport for use in the current pipeline +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - x: Origin X coordinate for the viewport +/// - y: Origin Y coordinate for the viewport +/// - width: Width of the viewport +/// - height: Height of the viewport +/// +/// The separate values for origin and dimension are converted into an ``MTLViewport`` which is then retained as the +/// "current" viewport for later use when the pipeline is actually set up. +@_cdecl("device_set_viewport") +public func device_set_viewport(device: UnsafeRawPointer, x: Int32, y: Int32, width: Int32, height: Int32) { + let device: MetalDevice = unretained(device) + + let viewPort = MTLViewport( + originX: Double(x), + originY: Double(y), + width: Double(width), + height: Double(height), + znear: 0.0, + zfar: 1.0 + ) + + device.renderState.viewPort = viewPort +} + +/// Gets the origin and dimensions of the viewport currently set up for use by the pipeline +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - rect: A pointer to a ``gs_rect`` struct in memory +/// +/// The function is provided a pointer to a ``gs_struct`` instance in memory which can hold the x and y values for the +/// origin and dimension of the viewport. +/// +/// This function is usually called when some source needs to retain the current "state" of the pipeline (of which +/// there can ever only be one) and overwrite the state with its own (in this case its own viewport). To be able to +/// restore the prior state, the "current" state needs to be retrieved from the pipeline. +@_cdecl("device_get_viewport") +public func device_get_viewport(device: UnsafeRawPointer, rect: UnsafeMutablePointer<gs_rect>) { + let device: MetalDevice = unretained(device) + + rect.pointee.x = Int32(device.renderState.viewPort.originX) + rect.pointee.y = Int32(device.renderState.viewPort.originY) + rect.pointee.cx = Int32(device.renderState.viewPort.width) + rect.pointee.cy = Int32(device.renderState.viewPort.height) +} + +/// Sets up a scissor rect to be used by the current pipeline +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - rect: Pointer to a ``gs_rect`` struct in memory that contains origin and dimension of the scissor rect +/// +/// The ``gs_rect`` is converted into a ``MTLScissorRect`` object before saving it in the "current" render state +/// for use in the next draw call. +@_cdecl("device_set_scissor_rect") +public func device_set_scissor_rect(device: UnsafeRawPointer, rect: UnsafePointer<gs_rect>?) { + let device: MetalDevice = unretained(device) + + if let rect { + device.renderState.scissorRect = rect.pointee.mtlScissorRect + device.renderState.scissorRectEnabled = true + } else { + device.renderState.scissorRect = nil + device.renderState.scissorRectEnabled = false + } +} + +/// Sets up an orthographic projection matrix with the provided view frustum +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - left: Left edge of view frustum on the near plane +/// - right: Right edge of view frustum on the near plane +/// - top: Top edge of view frustum on the near plane +/// - bottom: Bottom edge of view frustum on the near plane +/// - near: Distance of near plane on the Z axis +/// - far: Distance of far plane on the Z axis +@_cdecl("device_ortho") +public func device_ortho( + device: UnsafeRawPointer, left: Float, right: Float, top: Float, bottom: Float, near: Float, far: Float +) { + let device: MetalDevice = unretained(device) + + let rml = right - left + let bmt = bottom - top + let fmn = far - near + + device.renderState.projectionMatrix = matrix_float4x4( + rows: + SIMD4((2.0 / rml), 0.0, 0.0, 0.0), + SIMD4(0.0, (2.0 / -bmt), 0.0, 0.0), + SIMD4(0.0, 0.0, (1 / fmn), 0.0), + SIMD4((left + right) / -rml, (bottom + top) / bmt, near / -fmn, 1.0), + + ) +} + +/// Sets up a perspective projection matrix with the provided view frustum +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - left: Left edge of view frustum on the near plane +/// - right: Right edge of view frustum on the near plane +/// - top: Top edge of view frustum on the near plane +/// - bottom: Bottom edge of view frustum on the near plane +/// - near: Distance of near plane on the Z axis +/// - far: Distance of far plane on the Z axis +@_cdecl("device_frustum") +public func device_frustum( + device: UnsafeRawPointer, left: Float, right: Float, top: Float, bottom: Float, near: Float, far: Float +) { + let device: MetalDevice = unretained(device) + + let rml = right - left + let tmb = top - bottom + let fmn = far - near + + device.renderState.projectionMatrix = matrix_float4x4( + columns: ( + SIMD4(((2 * near) / rml), 0.0, 0.0, 0.0), + SIMD4(0.0, ((2 * near) / tmb), 0.0, 0.0), + SIMD4(((left + right) / rml), ((top + bottom) / tmb), (-far / fmn), -1.0), + SIMD4(0.0, 0.0, (-(far * near) / fmn), 0.0) + ) + ) +} + +/// Requests the current projection matrix to be pushed into a projection stack +/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// +/// OBS Studio's renderer works with the assumption of one big "current" state stack, which requires the entire state +/// to be changed to meet different rendering requirements. Part of this state is the current projection matrix, which +/// might need to be replaced temporarily. This function will be called when another projection matrix will be set up +/// to allow for its restoration later. +@_cdecl("device_projection_push") +public func device_projection_push(device: UnsafeRawPointer) { + let device: MetalDevice = unretained(device) + + device.renderState.projections.append(device.renderState.projectionMatrix) +} + +/// Requests the most recently pushed projection matrix to be removed from the stack and set up as the new current +/// matrix +/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// +/// OBS Studio's renderer works with the assumption of one big "current" state stack. This requires some elements of +/// this state to be temporarily retained before reinstating them after. This function will reinstate the most recently +/// added matrix as the new "current" matrix. +@_cdecl("device_projection_pop") +public func device_projection_pop(device: UnsafeRawPointer) { + let device: MetalDevice = unretained(device) + + device.renderState.projectionMatrix = device.renderState.projections.removeLast() +} + +/// Checks whether the current display is capable of displaying high dynamic range content. +/// +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - monitor: Opaque pointer of a platform-dependent monitor identifier +/// - Returns: `true` if the display is capable of displaying high dynamic range content, `false` otherwise +/// +/// On macOS this capability is described by the ``NSScreen/maximumPotentialExtendedDynamicRangeColorComponentValue`` +/// property, which can be checked using the ``NSWindow/screen`` property after retrieving the ``NSView/window`` +/// property. +@_cdecl("device_is_monitor_hdr") +public func device_is_monitor_hdr(device: UnsafeRawPointer, monitor: UnsafeRawPointer) -> Bool { + let device: MetalDevice = unretained(device) + + guard let swapChain = device.renderState.swapChain else { + return false + } + + return swapChain.edrHeadroom > 1.0 +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/metal-swapchain.swift
Added
@@ -0,0 +1,269 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +import AppKit +import Foundation + +/// Creates a ``OBSSwapChain`` instance for use as a pseudo swap chain implementation to be shared with `libobs` +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - data: Pointer to platform-specific `gs_init_data` struct +/// - Returns: Opaque pointer to a new ``OBSSwapChain`` on success or `nil` on error +/// +/// As interaction with UI elements needs to happen on the main thread of macOS, this function is marked with +/// `@MainActor`. This is also necessary because ``OBSSwapChain/updateView`` itself interacts with the ``NSView`` +/// instance passed via the `data` argument and also has to occur on the main thread. +/// +/// As applications cannot manage their own swap chain on macOS, the ``OBSSwapChain`` class merely wraps the +/// management of the ``CAMetalLayer`` that will be associated with the ``NSView`` and handles the drawables used to +/// render their contents. +/// +/// > Important: This function can only be called from the main thread. +@MainActor +@_cdecl("device_swapchain_create") +public func device_swapchain_create(device: UnsafeMutableRawPointer, data: UnsafePointer<gs_init_data>) + -> OpaquePointer? +{ + let device: MetalDevice = unretained(device) + + let view = data.pointee.window.view.takeUnretainedValue() as! NSView + let size = MTLSize( + width: Int(data.pointee.cx), + height: Int(data.pointee.cy), + depth: 0 + ) + + guard let swapChain = OBSSwapChain(device: device, size: size, colorSpace: data.pointee.format) else { return nil } + + swapChain.updateView(view) + + device.swapChainQueue.sync { + device.swapChains.append(swapChain) + } + + return swapChain.getRetained() +} + +/// Updates the internal size parameter and dimension of the ``CAMetalLayer`` managed by the ``OBSSwapChain`` instance +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - width: Width to update the layer's dimensions to +/// - height: Height to update the layer's dimensions to +/// +/// As the relationship between the ``CAMetalLayer`` and the ``NSView`` it is associated with is managed indirectly, +/// the metal layer cannot directly react to size changes (even though it would be possible to do so). Instead +/// ``AppKit`` will report a size change to the application, which will be picked up by Qt, who will emit a size +/// change event on the main loop, which will update internal state of the ``OBSQTDisplay`` class. These changes are +/// asynchronously picked up by `libobs` render loop, which will then call this function. +@_cdecl("device_resize") +public func device_resize(device: UnsafeMutableRawPointer, width: UInt32, height: UInt32) { + let device: MetalDevice = unretained(device) + + guard let swapChain = device.renderState.swapChain else { + return + } + + swapChain.resize(.init(width: Int(width), height: Int(height), depth: 0)) +} + +/// This function does nothing on Metal +/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// +/// The intended purpose of this function is to update the render target in the "current" swap chain with the color +/// space of its "display" and thus pick up changes in color spaces between different screens. +/// +/// On macOS this just requires updating the EDR headroom for the screen the view might be associated with, as the +/// actual color space and EDR capabilities are evaluated on every render loop. +/// +/// > Important: This function can only be called from the main thread. +@_cdecl("device_update_color_space") +public func device_update_color_space(device: UnsafeRawPointer) { + let device: MetalDevice = unretained(device) + + guard device.renderState.swapChain != nil else { + return + } + + nonisolated(unsafe) let swapChain = device.renderState.swapChain! + + Task { @MainActor in + swapChain.updateEdrHeadroom() + } +} + +/// Gets the dimensions of the ``CAMetalLayer`` managed by the ``OBSSwapChain`` instance set up in the current pipeline +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - cx: Pointer to memory for the width of the layer +/// - cy: Pointer to memory for the height of the layer +@_cdecl("device_get_size") +public func device_get_size( + device: UnsafeMutableRawPointer, cx: UnsafeMutablePointer<UInt32>, cy: UnsafeMutablePointer<UInt32> +) { + let device: MetalDevice = unretained(device) + + guard let swapChain = device.renderState.swapChain else { + cx.pointee = 0 + cy.pointee = 0 + return + } + + cx.pointee = UInt32(swapChain.viewSize.width) + cy.pointee = UInt32(swapChain.viewSize.height) +} + +/// Gets the width of the ``CAMetalLayer`` managed by the ``OBSSwapChain`` instance set up in the current pipeline +/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - Returns: Width of the layer +@_cdecl("device_get_width") +public func device_get_width(device: UnsafeRawPointer) -> UInt32 { + let device: MetalDevice = unretained(device) + + guard let swapChain = device.renderState.swapChain else { + return 0 + } + + return UInt32(swapChain.viewSize.width) +} + +/// Gets the height of the ``CAMetalLayer`` managed by the ``OBSSwapChain`` instance set up in the current pipeline +/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - Returns: Height of the layer +@_cdecl("device_get_height") +public func device_get_height(device: UnsafeRawPointer) -> UInt32 { + let device: MetalDevice = unretained(device) + + guard let swapChain = device.renderState.swapChain else { + return 0 + } + + return UInt32(swapChain.viewSize.height) +} + +/// Sets up the ``OBSSwapChain`` for use in the current pipeline +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - swap: Opaque pointer to ``OBSSwapChain`` instance shared with `libobs` +/// +/// The first call of this function in any render loop marks the "begin" of OBS Studio's display render stage. There +/// will only ever be one "current" swap chain in use by `libobs` and there is no dedicated call to "reset" or unload +/// the current swap chain, instead a new swap chain is loaded or the "scene end" function is called. +@_cdecl("device_load_swapchain") +public func device_load_swapchain(device: UnsafeRawPointer, swap: UnsafeRawPointer) { + let device: MetalDevice = unretained(device) + let swapChain: OBSSwapChain = unretained(swap) + + if swapChain.edrHeadroom > 1.0 { + var videoInfo: obs_video_info = obs_video_info() + obs_get_video_info(&videoInfo) + + let videoColorSpace = videoInfo.colorspace + + switch videoColorSpace { + case VIDEO_CS_2100_PQ: + if swapChain.colorRange != .hdrPQ { + // TODO: Investigate whether it's viable to use PQ or HLG tone mapping for the preview + // Use the following code to enable it for either: + // 2100 PQ: + // let maxLuminance = obs_get_video_hdr_nominal_peak_level() + // swapChain.layer.edrMetadata = .hdr10( + // minLuminance: 0.0001, maxLuminance: maxLuminance, opticalOutputScale: 10000) + // HLG: + // swapChain.layer.edrMetadata = .hlg + swapChain.layer.pixelFormat = .rgba16Float + swapChain.layer.colorspace = CGColorSpace(name: CGColorSpace.extendedLinearSRGB) + swapChain.layer.wantsExtendedDynamicRangeContent = true + swapChain.layer.edrMetadata = nil + swapChain.colorRange = .hdrPQ + swapChain.renderTarget = nil + } + case VIDEO_CS_2100_HLG: + if swapChain.colorRange != .hdrHLG { + swapChain.layer.pixelFormat = .rgba16Float + swapChain.layer.colorspace = CGColorSpace(name: CGColorSpace.extendedLinearSRGB) + swapChain.layer.wantsExtendedDynamicRangeContent = true + swapChain.layer.edrMetadata = nil + swapChain.colorRange = .hdrHLG + swapChain.renderTarget = nil + } + default: + if swapChain.colorRange != .sdr { + swapChain.layer.pixelFormat = .bgra8Unorm_srgb + swapChain.layer.colorspace = CGColorSpace(name: CGColorSpace.sRGB) + swapChain.layer.wantsExtendedDynamicRangeContent = false + swapChain.layer.edrMetadata = nil + swapChain.colorRange = .sdr + swapChain.renderTarget = nil + } + } + } else { + if swapChain.colorRange != .sdr { + swapChain.layer.pixelFormat = .bgra8Unorm_srgb + swapChain.layer.colorspace = CGColorSpace(name: CGColorSpace.sRGB) + swapChain.layer.wantsExtendedDynamicRangeContent = false + swapChain.layer.edrMetadata = nil + swapChain.colorRange = .sdr + swapChain.renderTarget = nil + } + } + + switch swapChain.colorRange { + case .hdrHLG, .hdrPQ: + device.renderState.gsColorSpace = GS_CS_709_EXTENDED + device.renderState.useSRGBGamma = false + case .sdr: + device.renderState.gsColorSpace = GS_CS_SRGB + device.renderState.useSRGBGamma = true + } + + if let renderTarget = swapChain.renderTarget { + device.renderState.renderTarget = renderTarget + } else { + let descriptor = MTLTextureDescriptor.texture2DDescriptor( + pixelFormat: swapChain.layer.pixelFormat, + width: Int(swapChain.layer.drawableSize.width), + height: Int(swapChain.layer.drawableSize.height), + mipmapped: false) + + descriptor.usage = .renderTarget + + guard let renderTarget = MetalTexture(device: device, descriptor: descriptor) else { + return + } + + swapChain.renderTarget = renderTarget + device.renderState.renderTarget = renderTarget + } + + device.renderState.depthStencilAttachment = nil + device.renderState.isRendertargetChanged = true + device.renderState.isInDisplaysRenderStage = true + + device.renderState.swapChain = swapChain +} + +/// Requests deinitialization of the ``OBSSwapChain`` instance shared with `libobs` +/// - Parameter texture: Opaque pointer to ``OBSSwapChain`` instance shared with `libobs` +/// +/// The ownership of the shared pointer is transferred into this function and the instance is placed under Swift's +/// memory management again. +@_cdecl("gs_swapchain_destroy") +public func gs_swapchain_destroy(swapChain: UnsafeMutableRawPointer) { + let swapChain = retained(swapChain) as OBSSwapChain + + swapChain.discard = true +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/metal-texture2d.swift
Added
@@ -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.texturesInt(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.texturesInt(unit) = texture.sRGBtexture! + } else { + device.renderState.texturesInt(unit) = texture.texture + } +} + +/// Copies image data from a region in the source ``MetalTexture`` into a destination ``MetalTexture`` at the provided +/// origin +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - dst: Opaque pointer to ``MetalTexture`` instance shared with `libobs`, used as destination for the copy operation +/// - dst_x: X coordinate of the origin in the destination texture +/// - dst_y: Y coordinate of the origin in the destination texture +/// - src: Opaque pointer to ``MetalTexture`` instance shared with `libobs`, used as source for the copy operation +/// - src_x: X coordinate of the origin in the source texture +/// - src_y: Y coordinate of the origin in the source texture +/// - src_w: Width of the region in the source texture +/// - src_h: Height of the region in the source texture +/// +/// This function will fail if the destination texture's dimensions aren't large enough to hold the region copied from +/// the source texture. This check will use the desired origin within the destination texture and the region's size +/// into account and checks whether the total dimensions of the destination are large enough (starting at the +/// destination origin) to hold the source's region. +/// +/// > Important: Execution will **not** be blocked, the copy operation will be committed to the command queue and +/// executed at some point after this function returns. +@_cdecl("device_copy_texture_region") +public func device_copy_texture_region( + device: UnsafeRawPointer, dst: UnsafeRawPointer, dst_x: UInt32, dst_y: UInt32, src: UnsafeRawPointer, src_x: UInt32, + src_y: UInt32, src_w: UInt32, src_h: UInt32 +) { + let device: MetalDevice = unretained(device) + let source: MetalTexture = unretained(src) + let destination: MetalTexture = unretained(dst) + + var sourceRegion = MTLRegion( + origin: .init(x: Int(src_x), y: Int(src_y), z: 0), + size: .init(width: Int(src_w), height: Int(src_h), depth: 1) + ) + + let destinationRegion = MTLRegion( + origin: .init(x: Int(dst_x), y: Int(dst_y), z: 0), + size: .init(width: destination.texture.width, height: destination.texture.height, depth: 1) + ) + + if sourceRegion.size.width == 0 { + sourceRegion.size.width = source.texture.width - sourceRegion.origin.x + } + + if sourceRegion.size.height == 0 { + sourceRegion.size.height = source.texture.height - sourceRegion.origin.y + } + + guard + destinationRegion.size.width - destinationRegion.origin.x > sourceRegion.size.width + && destinationRegion.size.height - destinationRegion.origin.y > sourceRegion.size.height + else { + OBSLog( + .error, + "device_copy_texture_region: Destination texture \(destinationRegion.size) is not large enough to hold source region (\(sourceRegion.size) at origin \(destinationRegion.origin)" + ) + return + } + + do { + try device.copyTextureRegion( + source: source, + sourceRegion: sourceRegion, + destination: destination, + destinationRegion: destinationRegion) + } catch let error as MetalError.MTLDeviceError { + OBSLog(.error, "device_clear: \(error.description)") + } catch { + OBSLog(.error, "device_clear: Unknown error occurred") + } +} + +/// Copies the image data from the source ``MetalTexture`` into the destination ``MetalTexture`` +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - dst: Opaque pointer to ``MetalTexture`` instance shared with `libobs`, used as destination for the copy +/// operation +/// - src: Opaque pointer to ``MetalTexture`` instance shared with `libobs`, used as source for the copy operation +/// +/// > Warning: This function requires that the source and destination texture dimensions are identical, otherwise the +/// copy operation will fail. +/// +/// > Important: Execution will **not** be blocked, the copy operation will be committed to the command queue and +/// executed at some point after this function returns. +@_cdecl("device_copy_texture") +public func device_copy_texture(device: UnsafeRawPointer, dst: UnsafeRawPointer, src: UnsafeRawPointer) { + let device: MetalDevice = unretained(device) + let source: MetalTexture = unretained(src) + let destination: MetalTexture = unretained(dst) + + do { + try device.copyTexture(source: source, destination: destination) + } catch let error as MetalError.MTLDeviceError { + OBSLog(.error, "device_clear: \(error.description)") + } catch { + OBSLog(.error, "device_clear: Unknown error occurred") + } +} + +/// Copies the image data from the source ``MetalTexture`` into the destination ``MetalTexture`` and blocks execution +/// until the copy operation has finished. +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - dst: Opaque pointer to ``MetalStageBuffer`` instance shared with `libobs`, used as destination for the copy +/// operation +/// - src: Opaque pointer to ``MetalTexture`` instance shared with `libobs`, used as source for the copy operation +/// +/// > Important: Execution will be blocked by waiting for the blit command encoder to finish the copy operation. +@_cdecl("device_stage_texture") +public func device_stage_texture(device: UnsafeRawPointer, dst: UnsafeRawPointer, src: UnsafeRawPointer) { + let device: MetalDevice = unretained(device) + let source: MetalTexture = unretained(src) + let destination: MetalStageBuffer = unretained(dst) + + do { + try device.stageTextureToBuffer(source: source, destination: destination) + } catch let error as MetalError.MTLDeviceError { + OBSLog(.error, "device_clear: \(error.description)") + } catch { + OBSLog(.error, "device_clear: Unknown error occurred") + } +} + +/// Gets the width of the texture wrapped by the ``MetalTexture`` instance +/// - Parameter tex: Opaque pointer to ``MetalTexture`` instance shared with `libobs` +/// - Returns: Width of the texture +@_cdecl("gs_texture_get_width") +public func device_texture_get_width(tex: UnsafeRawPointer) -> UInt32 { + let texture: MetalTexture = unretained(tex) + + return UInt32(texture.texture.width) +} + +/// Gets the height of the texture wrapped by the ``MetalTexture`` instance +/// - Parameter tex: Opaque pointer to ``MetalTexture`` instance shared with `libobs` +/// - Returns: Height of the texture +@_cdecl("gs_texture_get_height") +public func device_texture_get_height(tex: UnsafeRawPointer) -> UInt32 { + let texture: MetalTexture = unretained(tex) + + return UInt32(texture.texture.height) +} + +/// Gets the color format of the texture wrapped by the ``MetalTexture`` instance +/// - Parameter tex: Opaque pointer to ``MetalTexture`` instance shared with `libobs` +/// - Returns: Color format as defined by the `gs_color_format` enumeration +@_cdecl("gs_texture_get_color_format") +public func gs_texture_get_color_format(tex: UnsafeRawPointer) -> gs_color_format { + let texture: MetalTexture = unretained(tex) + + return texture.texture.pixelFormat.gsColorFormat +} + +/// Allocates memory for an update of the texture's image data wrapped by the ``MetalTexture`` instance. +/// - Parameters: +/// - tex: Opaque pointer to ``MetalTexture`` instance shared with `libobs` +/// - ptr: Pointer to memory for the raw image data +/// - linesize: Pointer to integer for the row size of the texture +/// - Returns: `true` if the mapping memory was allocated successfully, `false` otherwise +/// +/// Metal does not provide "map" and "unmap" operations as they exist in Direct3D11, as resource management and +/// synchronization needs to be handled explicitly by the application. Thus "mapping" just means that enough memory for +/// raw image data is allocated and an unmanaged pointer to that memory is shared with `libobs` for writing the image data. +/// +/// To ensure that the data written into the memory provided by this function is actually used to update the texture, +/// the corresponding function `gs_texture_unmap` needs to be used. +/// +/// > Important: This function can only be used to **push** new image data into the texture. To _pull_ image data from +/// the texture, use a stage surface instead. +@_cdecl("gs_texture_map") +public func gs_texture_map( + tex: UnsafeRawPointer, ptr: UnsafeMutablePointer<UnsafeMutableRawPointer>, linesize: UnsafeMutablePointer<UInt32> +) -> Bool { + let texture: MetalTexture = unretained(tex) + + guard texture.texture.textureType == .type2D, let device = texture.device else { + return false + } + + let stageBuffer: MetalStageBuffer + + if texture.stageBuffer == nil + || (texture.stageBuffer!.width != texture.texture.width + && texture.stageBuffer!.height != texture.texture.height) + { + guard + let buffer = MetalStageBuffer( + device: device, + width: texture.texture.width, + height: texture.texture.height, + format: texture.texture.pixelFormat + ) + else { + OBSLog(.error, "gs_texture_map: Unable to create MetalStageBuffer for mapping texture") + return false + } + + texture.stageBuffer = buffer + stageBuffer = buffer + } else { + stageBuffer = texture.stageBuffer! + } + + ptr.pointee = stageBuffer.buffer.contents() + linesize.pointee = UInt32(stageBuffer.width * stageBuffer.format.bytesPerPixel!) + + return true +} + +/// Writes back raw image data into the texture wrapped by the ``MetalTexture`` instance +/// - Parameter tex: Opaque pointer to ``MetalTexture`` instance shared with `libobs` +/// +/// This function needs to be used in tandem with `gs_texture_map`, which allocates memory for raw image data that +/// should be used in an update of the wrapped `MTLTexture`. This function will then actually replace the image data +/// in the texture with that raw image data and deallocate the memory that was allocated during `gs_texture_map`. +@_cdecl("gs_texture_unmap") +public func gs_texture_unmap(tex: UnsafeRawPointer) { + let texture: MetalTexture = unretained(tex) + + guard texture.texture.textureType == .type2D, let stageBuffer = texture.stageBuffer, let device = texture.device + else { + return + } + + do { + try device.stageBufferToTexture(source: stageBuffer, destination: texture) + } catch let error as MetalError.MTLDeviceError { + OBSLog(.error, "gs_texture_unmap: \(error.description)") + } catch { + OBSLog(.error, "gs_texture_unmap: Unknown error occurred") + } +} + +/// Gets an opaque pointer to the ``MTLTexture`` instance wrapped by the provided ``MetalTexture`` instance +/// - Parameter tex: Opaque pointer to ``MetalTexture`` instance shared with `libobs` +/// - Returns: Opaque pointer to ``MTLTexture`` instance +/// +/// > Important: The opaque pointer returned by this function is **unretained**, which means that the ``MTLTexture`` +/// instance it refers to might be deinitialized at any point when no other Swift code holds a strong reference to it. +@_cdecl("gs_texture_get_obj") +public func gs_texture_get_obj(tex: UnsafeRawPointer) -> OpaquePointer { + let texture: MetalTexture = unretained(tex) + + let unretained = Unmanaged.passUnretained(texture.texture).toOpaque() + + return OpaquePointer(unretained) +} + +/// Requests deinitialization of the ``MetalTexture`` instance shared with `libobs` +/// - Parameter cubetex: Opaque pointer to ``MetalTexture`` instance shared with `libobs` +/// +/// The ownership of the shared pointer is transferred into this function and the instance is placed under +/// Swift's memory management again. +@_cdecl("gs_cubetexture_destroy") +public func gs_cubetexture_destroy(cubetex: UnsafeRawPointer) { + let _ = retained(cubetex) as MetalTexture +} + +/// Gets the edge size of the cube texture wrapped by the ``MetalTexture`` instance +/// - Parameter cubetex: Opaque pointer to ``MetalTexture`` instance shared with `libobs` +/// - Returns: Edge size of the cube +@_cdecl("gs_cubetexture_get_size") +public func gs_cubetexture_get_size(cubetex: UnsafeRawPointer) -> UInt32 { + let texture: MetalTexture = unretained(cubetex) + + return UInt32(texture.texture.width) +} + +/// Gets the color format of the cube texture wrapped by the ``MetalTexture`` instance +/// - Parameter cubetex: Opaque pointer to ``MetalTexture`` instance shared with `libobs` +/// - Returns: Color format value +@_cdecl("gs_cubetexture_get_color_format") +public func gs_cubetexture_get_color_format(cubetex: UnsafeRawPointer) -> gs_color_format { + let texture: MetalTexture = unretained(cubetex) + + return texture.texture.pixelFormat.gsColorFormat +} + +/// Gets the device capability state for shared textures +/// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - Returns: Always `true` +/// +/// While Metal provides a specific "shared texture" type, OBS Studio understands this to mean "textures shared between +/// processes", which is usually achieved using ``IOSurface`` references on macOS. Metal textures can be created from +/// these references, so this is always `true`. +@_cdecl("device_shared_texture_available") +public func device_shared_texture_available(device: UnsafeRawPointer) -> Bool { + return true +} + +/// Creates a ``MetalTexture`` wrapping an ``MTLTexture`` that was created using the provided ``IOSurface`` reference. +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - iosurf: ``IOSurface`` reference to use as the image data source for the texture +/// - Returns: An opaque pointer to a ``MetalTexture`` instance on success, `nil` otherwise +/// +/// If the provided ``IOSurface`` uses a video image format that has no compatible ``Metal`` pixel format, creation of +/// the texture will fail. +@_cdecl("device_texture_create_from_iosurface") +public func device_texture_create_from_iosurface(device: UnsafeRawPointer, iosurf: IOSurfaceRef) -> OpaquePointer? { + let device: MetalDevice = unretained(device) + + let texture = MetalTexture(device: device, surface: iosurf) + + guard let texture else { + return nil + } + + return texture.getRetained() +} + +/// Replaces the current ``IOSurface``-based ``MTLTexture`` wrapped by the provided ``MetalTexture`` instance with a +/// new instance. +/// - Parameters: +/// - texture: Opaque pointer to ``MetalTexture`` instance shared with `libobs` +/// - iosurf: ``IOSurface`` reference to use as the image data source for the texture +/// - Returns: An opaque pointer to a ``MetalTexture`` instance on success, `nil` otherwise +/// +/// The "rebind" mentioned in the function name is limited to the ``MTLTexture`` instance wrapped inside the +/// ``MetalTexture`` instance, as textures are immutable objects (but their underlying data is mutable). This allows +/// `libobs` to hold onto the same opaque ``MetalTexture`` pointer even though the backing surface might have changed. +@_cdecl("gs_texture_rebind_iosurface") +public func gs_texture_rebind_iosurface(texture: UnsafeRawPointer, iosurf: IOSurfaceRef) -> Bool { + let texture: MetalTexture = unretained(texture) + + return texture.rebind(surface: iosurf) +} + +/// Creates a new ``MetalTexture`` instance with an opaque shared texture "handle" +/// - Parameters: +/// - device: Opaque pointer to ``MetalTexture`` instance shared with `libobs` +/// - handle: Arbitrary handle value that needs to be reinterpreted into the correct platform specific shared +/// reference type +/// - Returns: An opaque pointer to a ``MetalTexture`` instance on success, `nil` otherwise +/// +/// The "handle" is a generalised argument used on all platforms and needs to be converted into a platform-specific +/// type before the "shared" texture can be created. In case of macOS this means converting the unsigned integer into +/// a ``IOSurface`` address. +/// +/// > Warning: As the handle is a 32-bit integer, this can break on 64-bit systems if the ``IOSurface`` pointer +/// address does not fit into a 32-bit number. +@_cdecl("device_texture_open_shared") +public func device_texture_open_shared(device: UnsafeRawPointer, handle: UInt32) -> OpaquePointer? { + if let reference = IOSurfaceLookupFromMachPort(handle) { + let texture = device_texture_create_from_iosurface(device: device, iosurf: reference) + + return texture + } else { + return nil + } +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/metal-texture3d.swift
Added
@@ -0,0 +1,113 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +import Foundation +import Metal + +/// Creates a three-dimensional ``MetalTexture`` instance with the specified usage options and the raw image data +/// (if provided) +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - size: Desired size of the texture +/// - color_format: Desired color format of the texture as described by `gs_color_format` +/// - levels: Amount of mip map levels to generate for the texture +/// - data: Optional pointer to raw pixel data per mip map level +/// - flags: Texture resource use information encoded as `libobs` bitfield +/// - Returns: Opaque pointer to a created ``MetalTexture`` instance or a `NULL` pointer on error +/// +/// This function will create a new ``MTLTexture`` wrapped within a ``MetalTexture`` class and also upload any pixel +/// data if non-`NULL` pointers have been provided via the `data` argument. +/// +/// > Important: If mipmap generation is requested, execution will be blocked by waiting for the blit command encoder +/// to generate the mipmaps. +@_cdecl("device_voltexture_create") +public func device_voltexture_create( + device: UnsafeRawPointer, width: UInt32, height: UInt32, depth: UInt32, color_format: gs_color_format, + levels: UInt32, data: UnsafePointer<UnsafePointer<UInt8>?>?, flags: UInt32 +) -> OpaquePointer? { + let device = Unmanaged<MetalDevice>.fromOpaque(device).takeUnretainedValue() + + let descriptor = MTLTextureDescriptor.init( + type: .type3D, + width: width, + height: height, + depth: depth, + colorFormat: color_format, + levels: levels, + flags: flags + ) + + guard let descriptor, let texture = MetalTexture(device: device, descriptor: descriptor) else { + return nil + } + + if let data { + texture.upload(data: data, mipmapLevels: descriptor.mipmapLevelCount) + } + + return texture.getRetained() +} + +/// Requests deinitialization of the ``MetalTexture`` instance shared with `libobs` +/// - Parameter texture: Opaque pointer to ``MetalTexture`` instance shared with `libobs` +/// +/// The ownership of the shared pointer is transferred into this function and the instance is placed under +/// Swift's memory management again. +@_cdecl("gs_voltexture_destroy") +public func gs_voltexture_destroy(voltex: UnsafeRawPointer) { + let _ = retained(voltex) as MetalTexture +} + +/// Gets the width of the texture wrapped by the ``MetalTexture`` instance +/// - Parameter voltex: Opaque pointer to ``MetalTexture`` instance shared with `libobs` +/// - Returns: Width of the texture +@_cdecl("gs_voltexture_get_width") +public func gs_voltexture_get_width(voltex: UnsafeRawPointer) -> UInt32 { + let texture: MetalTexture = unretained(voltex) + + return UInt32(texture.texture.width) +} + +/// Gets the height of the texture wrapped by the ``MetalTexture`` instance +/// - Parameter voltex: Opaque pointer to ``MetalTexture`` instance shared with `libobs` +/// - Returns: Height of the texture +@_cdecl("gs_voltexture_get_height") +public func gs_voltexture_get_height(voltex: UnsafeRawPointer) -> UInt32 { + let texture: MetalTexture = unretained(voltex) + + return UInt32(texture.texture.height) +} + +/// Gets the depth of the texture wrapped by the ``Metaltexture`` instance +/// - Parameter voltex: Opaque pointer to ``MetalTexture`` instance shared with `libobs` +/// - Returns: Depth of the texture +@_cdecl("gs_voltexture_get_depth") +public func gs_voltexture_get_depth(voltex: UnsafeRawPointer) -> UInt32 { + let texture: MetalTexture = unretained(voltex) + + return UInt32(texture.texture.depth) +} + +/// Gets the color format of the texture wrapped by the ``MetalTexture`` instance +/// - Parameter voltex: Opaque pointer to ``MetalTexture`` instance shared with `libobs` +/// - Returns: Color format as defined by the `gs_color_format` enumeration +@_cdecl("gs_voltexture_get_color_format") +public func gs_voltexture_get_color_format(voltex: UnsafeRawPointer) -> gs_color_format { + let texture: MetalTexture = unretained(voltex) + + return texture.texture.pixelFormat.gsColorFormat +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/metal-unimplemented.swift
Added
@@ -0,0 +1,97 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +@_cdecl("device_load_default_samplerstate") +public func device_load_default_samplerstate(device: UnsafeRawPointer, b_3d: Bool, unit: Int) { + return +} + +@_cdecl("device_enter_context") +public func device_enter_context(device: UnsafeMutableRawPointer) { + return +} + +@_cdecl("device_leave_context") +public func device_leave_context(device: UnsafeMutableRawPointer) { + return +} + +@_cdecl("device_timer_create") +public func device_timer_create(device: UnsafeRawPointer) { + return +} + +@_cdecl("device_timer_range_create") +public func device_timer_range_create(device: UnsafeRawPointer) { +} + +@_cdecl("gs_timer_destroy") +public func gs_timer_destroy(timer: UnsafeRawPointer) { + return +} + +@_cdecl("gs_timer_begin") +public func gs_timer_begin(timer: UnsafeRawPointer) { + return +} + +@_cdecl("gs_timer_end") +public func gs_timer_end(timer: UnsafeRawPointer) { + return +} + +@_cdecl("gs_timer_get_data") +public func gs_timer_get_data(timer: UnsafeRawPointer) -> Bool { + return false +} + +@_cdecl("gs_timer_range_destroy") +public func gs_timer_range_destroy(range: UnsafeRawPointer) { + return +} + +@_cdecl("gs_timer_range_begin") +public func gs_timer_range_begin(range: UnsafeRawPointer) { + return +} + +@_cdecl("gs_timer_range_end") +public func gs_timer_range_end(range: UnsafeRawPointer) { + return +} + +@_cdecl("gs_timer_range_get_data") +public func gs_timer_range_get_data(range: UnsafeRawPointer, disjoint: Bool, frequency: UInt64) -> Bool { + return false +} + +@_cdecl("device_debug_marker_begin") +public func device_debug_marker_begin(device: UnsafeRawPointer, monitor: UnsafeMutableRawPointer) { + return +} + +@_cdecl("device_debug_marker_end") +public func device_debug_marker_end(device: UnsafeRawPointer) { + return +} + +@_cdecl("device_set_cube_render_target") +public func device_set_cube_render_target( + device: UnsafeRawPointer, cubetex: UnsafeRawPointer, side: Int, zstencil: UnsafeRawPointer +) { + return +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/metal-vertexbuffer.swift
Added
@@ -0,0 +1,115 @@ +/****************************************************************************** + Copyright (C) 2024 by Patrick Heyer <PatTheMav@users.noreply.github.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + ******************************************************************************/ + +/// Creates a new ``MetalVertexBuffer`` instance with the given vertex buffer data and usage flags +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - data: Pointer to `gs_vb_data` vertex buffer data created by `libobs` +/// - flags: Usage flags encoded as `libobs` bitmask +/// - Returns: Opaque pointer to a new ``MetalVertexBuffer`` instance if successful, `nil` otherwise +/// +/// > Note: The ownership of the memory pointed to by `data` is implicitly transferred to the ``MetalVertexBuffer`` +/// instance, but is not managed by Swift. +@_cdecl("device_vertexbuffer_create") +public func device_vertexbuffer_create(device: UnsafeRawPointer, data: UnsafeMutablePointer<gs_vb_data>, flags: UInt32) + -> OpaquePointer +{ + let device: MetalDevice = unretained(device) + + let vertexBuffer = MetalVertexBuffer( + device: device, + data: data, + dynamic: (Int32(flags) & GS_DYNAMIC) != 0 + ) + + return vertexBuffer.getRetained() +} + +/// Requests the deinitialization of a shared ``MetalVertexBuffer`` instance +/// - Parameter indexBuffer: Opaque pointer to ``MetalVertexBuffer`` instance shared with `libobs` +/// +/// The deinitialization is handled automatically by Swift after the ownership of the instance has been transferred +/// into the function and becomes the last strong reference to it. After the function leaves its scope, the object will +/// be deinitialized and deallocated automatically. +/// +/// > Note: The vertex buffer data memory is implicitly owned by the ``MetalVertexBuffer`` instance and will be +/// manually cleaned up and deallocated by the instance's ``deinit`` method. +@_cdecl("gs_vertexbuffer_destroy") +public func gs_vertexbuffer_destroy(vertBuffer: UnsafeRawPointer) { + let _ = retained(vertBuffer) as MetalVertexBuffer +} + +/// Sets up a ``MetalVertexBuffer`` as the vertex buffer for the current pipeline +/// - Parameters: +/// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` +/// - vertbuffer: Opaque pointer to ``MetalVertexBuffer`` instance shared with `libobs` +/// +/// > Note: The reference count of the ``MetalVertexBuffer`` instance will not be increased by this call. +/// +/// > Important: If a `nil` pointer is provided as the vertex buffer, the index buffer will be _unset_. +@_cdecl("device_load_vertexbuffer") +public func device_load_vertexbuffer(device: UnsafeRawPointer, vertBuffer: UnsafeMutableRawPointer?) { + let device: MetalDevice = unretained(device) + + if let vertBuffer { + device.renderState.vertexBuffer = unretained(vertBuffer) + } else { + device.renderState.vertexBuffer = nil + } +} + +/// Requests the vertex buffer's current data to be transferred into GPU memory +/// - Parameter vertBuffer: Opaque pointer to ``MetalVertexBuffer`` instance shared with `libobs` +/// +/// This function will call `gs_vertexbuffer_flush_direct` with a `nil` pointer as the data pointer. +@_cdecl("gs_vertexbuffer_flush") +public func gs_vertexbuffer_flush(vertbuffer: UnsafeRawPointer) { + gs_vertexbuffer_flush_direct(vertbuffer: vertbuffer, data: nil) +} + +/// Requests the vertex buffer to be updated with the provided data and then transferred into GPU memory +/// - Parameters: +/// - vertBuffer: Opaque pointer to ``MetalVertexBuffer`` instance shared with `libobs` +/// - data: Opaque pointer to vertex buffer data set up by `libobs` +/// +/// This function is called to ensure that the vertex buffer data that is contained in the memory pointed at by the +/// `data` argument is uploaded into GPU memory. +/// +/// If a `nil` pointer is provided instead, the data provided to the instance during creation will be used instead. +@_cdecl("gs_vertexbuffer_flush_direct") +public func gs_vertexbuffer_flush_direct(vertbuffer: UnsafeRawPointer, data: UnsafeMutablePointer<gs_vb_data>?) { + let vertexBuffer: MetalVertexBuffer = unretained(vertbuffer) + + vertexBuffer.setupBuffers(data: data) +} + +/// Returns an opaque pointer to the vertex buffer data associated with the ``MetalVertexBuffer`` instance +/// - Parameter vertBuffer: Opaque pointer to ``MetalVertexBuffer`` instance shared with `libobs` +/// - Returns: Opaque pointer to index buffer data in memory +/// +/// The returned opaque pointer represents the unchanged memory address that was provided for the creation of the index +/// buffer object. +/// +/// > Warning: There is only limited memory safety associated with this pointer. It is implicitly owned and its +/// lifetime is managed by the ``MetalVertexBuffer`` +/// instance, but it was originally created by `libobs`. +@_cdecl("gs_vertexbuffer_get_data") +public func gs_vertexbuffer_get_data(vertBuffer: UnsafeRawPointer) -> UnsafeMutablePointer<gs_vb_data>? { + let vertexBuffer: MetalVertexBuffer = unretained(vertBuffer) + + return vertexBuffer.vertexData +}
View file
obs-studio-32.0.0~beta2.tar.xz/libobs-metal/metal-zstencilbuffer.swift
Added
@@ -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 +}
View file
obs-studio-32.0.0~beta1.tar.xz/libobs/CMakeLists.txt -> obs-studio-32.0.0~beta2.tar.xz/libobs/CMakeLists.txt
Changed
@@ -15,10 +15,6 @@ find_package(ZLIB REQUIRED) find_package(Uthash REQUIRED) -if(ENABLE_UI) - find_package(Qt6 REQUIRED Core) -endif() - find_package(jansson REQUIRED) if(NOT TARGET OBS::caption) add_subdirectory("${CMAKE_SOURCE_DIR}/deps/libcaption" "${CMAKE_BINARY_DIR}/deps/libcaption")
View file
obs-studio-32.0.0~beta1.tar.xz/libobs/cmake/os-macos.cmake -> obs-studio-32.0.0~beta2.tar.xz/libobs/cmake/os-macos.cmake
Changed
@@ -26,7 +26,7 @@ util/threading-posix.h ) -target_compile_options(libobs PUBLIC -Wno-strict-prototypes -Wno-shorten-64-to-32) +target_compile_options(libobs PUBLIC "$<$<NOT:$<COMPILE_LANGUAGE:Swift>>:-Wno-strict-prototypes;-Wno-shorten-64-to-32>") set_property(SOURCE obs-cocoa.m util/platform-cocoa.m PROPERTY COMPILE_OPTIONS -fobjc-arc) set_property(TARGET libobs PROPERTY FRAMEWORK TRUE)
View file
obs-studio-32.0.0~beta1.tar.xz/libobs/data/default.effect -> obs-studio-32.0.0~beta2.tar.xz/libobs/data/default.effect
Changed
@@ -136,6 +136,14 @@ return rgba; } +float4 PSDrawD65P3(VertInOut vert_in) : TARGET +{ + float4 rgba = image.Sample(def_sampler, vert_in.uv); + rgba.rgb = srgb_nonlinear_to_linear(rgba.rgb); + rgba.rgb = d65p3_to_rec709(rgba.rgb); + return rgba; +} + technique Draw { pass @@ -252,3 +260,12 @@ pixel_shader = PSDrawTonemapPQ(vert_in); } } + +technique DrawD65P3 +{ + pass + { + vertex_shader = VSDefault(vert_in); + pixel_shader = PSDrawD65P3(vert_in); + } +}
View file
obs-studio-32.0.0~beta1.tar.xz/libobs/graphics/graphics.h -> obs-studio-32.0.0~beta2.tar.xz/libobs/graphics/graphics.h
Changed
@@ -500,6 +500,7 @@ #define GS_DEVICE_OPENGL 1 #define GS_DEVICE_DIRECT3D_11 2 +#define GS_DEVICE_METAL 3 EXPORT const char *gs_get_device_name(void); EXPORT const char *gs_get_driver_version(void);
View file
obs-studio-32.0.0~beta1.tar.xz/libobs/obs-encoder.h -> obs-studio-32.0.0~beta2.tar.xz/libobs/obs-encoder.h
Changed
@@ -344,9 +344,6 @@ bool (*encode_texture2)(void *data, struct encoder_texture *texture, int64_t pts, uint64_t lock_key, uint64_t *next_key, struct encoder_packet *packet, bool *received_packet); - - /** Pointer to module that generated this encoder **/ - obs_module_t *module; }; EXPORT void obs_register_encoder_s(const struct obs_encoder_info *info, size_t size);
View file
obs-studio-32.0.0~beta1.tar.xz/libobs/obs-module.c -> obs-studio-32.0.0~beta2.tar.xz/libobs/obs-module.c
Changed
@@ -442,7 +442,7 @@ da_push_back(obs->disabled_modules, &item); } -extern void get_plugin_info(const char *path, bool *is_obs_plugin, bool *can_load); +extern void get_plugin_info(const char *path, bool *is_obs_plugin); struct fail_info { struct dstr fail_modules; @@ -497,9 +497,8 @@ obs_module_t *disabled_module; bool is_obs_plugin; - bool can_load_obs_plugin; - get_plugin_info(info->bin_path, &is_obs_plugin, &can_load_obs_plugin); + get_plugin_info(info->bin_path, &is_obs_plugin); if (!is_obs_plugin) { blog(LOG_WARNING, "Skipping module '%s', not an OBS plugin", info->bin_path); @@ -518,14 +517,6 @@ return; } - if (!can_load_obs_plugin) { - blog(LOG_WARNING, - "Skipping module '%s' due to possible " - "import conflicts", - info->bin_path); - goto load_failure; - } - int code = obs_open_module(&module, info->bin_path, info->data_path); switch (code) { case MODULE_MISSING_EXPORTS: @@ -990,7 +981,6 @@ /* NOTE: The assignment of data.module must occur before memcpy! */ if (loadingModule) { - data.module = loadingModule; char *source_id = bstrdup(info->id); da_push_back(loadingModule->sources, &source_id); } @@ -1117,6 +1107,11 @@ strlist_free(protocols); } + if (loadingModule) { + char *output_id = bstrdup(info->id); + da_push_back(loadingModule->outputs, &output_id); + } + return; error:
View file
obs-studio-32.0.0~beta1.tar.xz/libobs/obs-module.h -> obs-studio-32.0.0~beta2.tar.xz/libobs/obs-module.h
Changed
@@ -180,8 +180,8 @@ /** Optional: Returns a description of the module */ MODULE_EXPORT const char *obs_module_description(void); -/** Returns the module's unique ID, or null if it doesn't have one */ +/** Returns the module's unique ID, or NULL if it doesn't have one */ MODULE_EXPORT const char *obs_get_module_id(obs_module_t *module); -/** Returns the module's semver verison number or null if it doesn't have one */ +/** Returns the module's semver version number or NULL if it doesn't have one */ MODULE_EXPORT const char *obs_get_module_version(obs_module_t *module);
View file
obs-studio-32.0.0~beta1.tar.xz/libobs/obs-output.h -> obs-studio-32.0.0~beta2.tar.xz/libobs/obs-output.h
Changed
@@ -85,9 +85,6 @@ /* required if OBS_OUTPUT_SERVICE */ const char *protocols; - - /* Pointer to module that generated this output */ - obs_module_t *module; }; EXPORT void obs_register_output_s(const struct obs_output_info *info, size_t size);
View file
obs-studio-32.0.0~beta1.tar.xz/libobs/obs-scene.c -> obs-studio-32.0.0~beta2.tar.xz/libobs/obs-scene.c
Changed
@@ -3963,11 +3963,21 @@ return source && strcmp(source->info.id, group_info.id) == 0; } +bool obs_source_type_is_group(const char *id) +{ + return id && strcmp(id, group_info.id) == 0; +} + bool obs_source_is_scene(const obs_source_t *source) { return source && strcmp(source->info.id, scene_info.id) == 0; } +bool obs_source_type_is_scene(const char *id) +{ + return id && strcmp(id, scene_info.id) == 0; +} + bool obs_scene_is_group(const obs_scene_t *scene) { return scene ? scene->is_group : false;
View file
obs-studio-32.0.0~beta1.tar.xz/libobs/obs-service.h -> obs-studio-32.0.0~beta2.tar.xz/libobs/obs-service.h
Changed
@@ -104,9 +104,6 @@ const char *(*get_connect_info)(void *data, uint32_t type); bool (*can_try_to_connect)(void *data); - - /* Pointer to module that generated this service */ - obs_module_t *module; }; EXPORT void obs_register_service_s(const struct obs_service_info *info, size_t size);
View file
obs-studio-32.0.0~beta1.tar.xz/libobs/obs-source.c -> obs-studio-32.0.0~beta2.tar.xz/libobs/obs-source.c
Changed
@@ -157,6 +157,12 @@ enum obs_module_load_state obs_source_load_state(const char *id) { + if (!id) + return OBS_MODULE_INVALID; + + if (obs_source_type_is_scene(id) || obs_source_type_is_group(id)) + return OBS_MODULE_ENABLED; + obs_module_t *module = obs_source_get_module(id); if (!module) { return OBS_MODULE_MISSING;
View file
obs-studio-32.0.0~beta1.tar.xz/libobs/obs-source.h -> obs-studio-32.0.0~beta2.tar.xz/libobs/obs-source.h
Changed
@@ -551,9 +551,6 @@ * @param source Source that the filter is being added to */ void (*filter_add)(void *data, obs_source_t *source); - - /** Pointer to module that generated this source **/ - obs_module_t *module; }; EXPORT void obs_register_source_s(const struct obs_source_info *info, size_t size);
View file
obs-studio-32.0.0~beta1.tar.xz/libobs/obs.c -> obs-studio-32.0.0~beta2.tar.xz/libobs/obs.c
Changed
@@ -1852,7 +1852,7 @@ while (source) { obs_source_t *s = obs_source_get_ref(source); if (s) { - if (obs_source_is_scene(source) && !enum_proc(param, s)) { + if (source->info.type == OBS_SOURCE_TYPE_SCENE && !enum_proc(param, s)) { obs_source_release(s); break; } @@ -2258,7 +2258,7 @@ if (!*v_id) v_id = id; - if (strcmp(id, scene_info.id) == 0 || strcmp(id, group_info.id) == 0) { + if (obs_source_type_is_scene(id) || obs_source_type_is_group(id)) { const char *canvas_uuid = obs_data_get_string(source_data, "canvas_uuid"); canvas = obs_get_canvas_by_uuid(canvas_uuid); /* Fall back to main canvas if canvas cannot be found. */
View file
obs-studio-32.0.0~beta1.tar.xz/libobs/obs.h -> obs-studio-32.0.0~beta2.tar.xz/libobs/obs.h
Changed
@@ -1710,6 +1710,7 @@ size_t item_order_size); EXPORT bool obs_source_is_scene(const obs_source_t *source); +EXPORT bool obs_source_type_is_scene(const char *id); /** Adds/creates a new scene item for a source */ EXPORT obs_sceneitem_t *obs_scene_add(obs_scene_t *scene, obs_source_t *source); @@ -1839,6 +1840,7 @@ EXPORT obs_sceneitem_t *obs_sceneitem_get_group(obs_scene_t *scene, obs_sceneitem_t *item); EXPORT bool obs_source_is_group(const obs_source_t *source); +EXPORT bool obs_source_type_is_group(const char *id); EXPORT bool obs_scene_is_group(const obs_scene_t *scene); EXPORT void obs_sceneitem_group_enum_items(obs_sceneitem_t *group,
View file
obs-studio-32.0.0~beta1.tar.xz/libobs/util/platform-nix.c -> obs-studio-32.0.0~beta2.tar.xz/libobs/util/platform-nix.c
Changed
@@ -14,14 +14,6 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -#include "obsconfig.h" - -#if !defined(__APPLE__) -#define _GNU_SOURCE -#include <link.h> -#include <stdlib.h> -#endif - #include <stdio.h> #include <errno.h> #include <sys/types.h> @@ -37,6 +29,8 @@ #include <signal.h> #include <uuid/uuid.h> +#include "obsconfig.h" + #if !defined(__APPLE__) #include <sys/times.h> #include <sys/wait.h> @@ -82,7 +76,7 @@ dstr_cat(&dylib_name, ".so"); #ifdef __APPLE__ - int dlopen_flags = RTLD_LAZY | RTLD_FIRST; + int dlopen_flags = RTLD_NOW | RTLD_FIRST; if (dstr_find(&dylib_name, "Python")) { dlopen_flags = dlopen_flags | RTLD_GLOBAL; } else { @@ -90,7 +84,7 @@ } void *res = dlopen(dylib_name.array, dlopen_flags); #else - void *res = dlopen(dylib_name.array, RTLD_LAZY); + void *res = dlopen(dylib_name.array, RTLD_NOW); #endif if (!res) blog(LOG_ERROR, "os_dlopen(%s->%s): %s\n", path, dylib_name.array, dlerror()); @@ -110,51 +104,9 @@ dlclose(module); } -#if !defined(__APPLE__) -int module_has_qt5_check(const char *path) -{ - void *mod = os_dlopen(path); - if (mod == NULL) { - return 1; - } - - struct link_map *list = NULL; - if (dlinfo(mod, RTLD_DI_LINKMAP, &list) == 0) { - for (struct link_map *ptr = list; ptr; ptr = ptr->l_next) { - if (strstr(ptr->l_name, "libQt5") != NULL) { - return 0; - } - } - } - - return 1; -} - -bool has_qt5_dependency(const char *path) -{ - pid_t pid = fork(); - if (pid == 0) { - base_set_log_handler(NULL, NULL); - _exit(module_has_qt5_check(path)); - } - if (pid < 0) { - return false; - } - int status; - if (waitpid(pid, &status, 0) < 0) { - return false; - } - return WIFEXITED(status) && WEXITSTATUS(status) == 0; -} -#endif - -void get_plugin_info(const char *path, bool *is_obs_plugin, bool *can_load) +void get_plugin_info(const char *path, bool *is_obs_plugin) { *is_obs_plugin = true; - *can_load = true; -#if !defined(__APPLE__) - *can_load = !has_qt5_dependency(path); -#endif UNUSED_PARAMETER(path); }
View file
obs-studio-32.0.0~beta1.tar.xz/libobs/util/platform-windows.c -> obs-studio-32.0.0~beta2.tar.xz/libobs/util/platform-windows.c
Changed
@@ -27,7 +27,6 @@ #include "platform.h" #include "darray.h" #include "dstr.h" -#include "obsconfig.h" #include "util_uint64.h" #include "windows/win-registry.h" #include "windows/win-version.h" @@ -134,63 +133,6 @@ FreeLibrary(module); } -static bool has_qt5_import(VOID *base, PIMAGE_NT_HEADERS nt_headers) -{ - __try { - PIMAGE_DATA_DIRECTORY data_dir; - data_dir = &nt_headers->OptionalHeader.DataDirectoryIMAGE_DIRECTORY_ENTRY_IMPORT; - - if (data_dir->Size == 0) - return false; - - PIMAGE_SECTION_HEADER section, last_section; - section = IMAGE_FIRST_SECTION(nt_headers); - last_section = section; - - /* find the section that contains the export directory */ - int i; - for (i = 0; i < nt_headers->FileHeader.NumberOfSections; i++) { - if (section->VirtualAddress <= data_dir->VirtualAddress) { - last_section = section; - section++; - continue; - } else { - break; - } - } - - /* double check in case we exited early */ - if (last_section->VirtualAddress > data_dir->VirtualAddress || - section->VirtualAddress <= data_dir->VirtualAddress) - return false; - - section = last_section; - - /* get a pointer to the import directory */ - PIMAGE_IMPORT_DESCRIPTOR import; - import = (PIMAGE_IMPORT_DESCRIPTOR)((byte *)base + data_dir->VirtualAddress - section->VirtualAddress + - section->PointerToRawData); - - while (import->Name != 0) { - char *name = (char *)((byte *)base + import->Name - section->VirtualAddress + - section->PointerToRawData); - - /* qt5? bingo, reject this library */ - if (astrcmpi_n(name, "qt5", 3) == 0) { - return true; - } - - import++; - } - - } __except (EXCEPTION_EXECUTE_HANDLER) { - /* we failed somehow, for compatibility assume no qt5 import */ - return false; - } - - return false; -} - static bool has_obs_export(VOID *base, PIMAGE_NT_HEADERS nt_headers) { __try { @@ -256,7 +198,7 @@ return false; } -void get_plugin_info(const char *path, bool *is_obs_plugin, bool *can_load) +void get_plugin_info(const char *path, bool *is_obs_plugin) { struct dstr dll_name; wchar_t *wpath; @@ -269,7 +211,6 @@ PIMAGE_NT_HEADERS nt_headers; *is_obs_plugin = false; - *can_load = false; if (!path) return; @@ -312,15 +253,10 @@ *is_obs_plugin = has_obs_export(base, nt_headers); - if (*is_obs_plugin) { - *can_load = !has_qt5_import(base, nt_headers); - } - } __except (EXCEPTION_EXECUTE_HANDLER) { /* we failed somehow, for compatibility let's assume it * was a valid plugin and let the loader deal with it */ *is_obs_plugin = true; - *can_load = true; goto cleanup; } @@ -338,11 +274,10 @@ bool os_is_obs_plugin(const char *path) { bool is_obs_plugin; - bool can_load; - get_plugin_info(path, &is_obs_plugin, &can_load); + get_plugin_info(path, &is_obs_plugin); - return is_obs_plugin && can_load; + return is_obs_plugin; } union time_data {
View file
obs-studio-32.0.0~beta1.tar.xz/plugins/mac-avcapture/plugin-main.m -> obs-studio-32.0.0~beta2.tar.xz/plugins/mac-avcapture/plugin-main.m
Changed
@@ -35,11 +35,14 @@ capture_info->settings = settings; capture_info->source = source; + obs_enter_graphics(); if (gs_get_device_type() == GS_DEVICE_OPENGL) { capture_info->effect = obs_get_base_effect(OBS_EFFECT_DEFAULT_RECT); } else { capture_info->effect = obs_get_base_effect(OBS_EFFECT_DEFAULT); } + obs_leave_graphics(); + capture_info->frameSize = CGRectZero; if (!capture_info->effect) {
View file
obs-studio-32.0.0~beta1.tar.xz/plugins/mac-capture/mac-display-capture.m -> obs-studio-32.0.0~beta2.tar.xz/plugins/mac-capture/mac-display-capture.m
Changed
@@ -255,6 +255,8 @@ dc->source = source; dc->hide_cursor = !obs_data_get_bool(settings, "show_cursor"); + obs_enter_graphics(); + if (gs_get_device_type() == GS_DEVICE_OPENGL) { dc->effect = obs_get_base_effect(OBS_EFFECT_DEFAULT_RECT); } else { @@ -264,8 +266,6 @@ if (!dc->effect) goto fail; - obs_enter_graphics(); - struct gs_sampler_info info = { .filter = GS_FILTER_LINEAR, .address_u = GS_ADDRESS_CLAMP,
View file
obs-studio-32.0.0~beta1.tar.xz/plugins/mac-capture/mac-sck-video-capture.m -> obs-studio-32.0.0~beta2.tar.xz/plugins/mac-capture/mac-sck-video-capture.m
Changed
@@ -291,11 +291,13 @@ sc->capture_delegate = ScreenCaptureDelegate alloc init; sc->capture_delegate.sc = sc; + obs_enter_graphics(); if (gs_get_device_type() == GS_DEVICE_OPENGL) { sc->effect = obs_get_base_effect(OBS_EFFECT_DEFAULT_RECT); } else { sc->effect = obs_get_base_effect(OBS_EFFECT_DEFAULT); } + obs_leave_graphics(); if (!sc->effect) goto fail; @@ -311,7 +313,6 @@ return sc; fail: - obs_leave_graphics(); sck_video_capture_destroy(sc); return NULL; }
View file
obs-studio-32.0.0~beta1.tar.xz/plugins/mac-syphon/syphon.m -> obs-studio-32.0.0~beta2.tar.xz/plugins/mac-syphon/syphon.m
Changed
@@ -312,13 +312,13 @@ obs_enter_graphics(); s->sampler = gs_samplerstate_create(&info); s->vertbuffer = create_vertbuffer(); - obs_leave_graphics(); if (gs_get_device_type() == GS_DEVICE_OPENGL) { s->effect = obs_get_base_effect(OBS_EFFECT_DEFAULT_RECT); } else { s->effect = obs_get_base_effect(OBS_EFFECT_DEFAULT); } + obs_leave_graphics(); return s->sampler != NULL && s->vertbuffer != NULL && s->effect != NULL; }
View file
obs-studio-32.0.0~beta1.tar.xz/plugins/nv-filters/nvidia-audiofx-filter.c -> obs-studio-32.0.0~beta2.tar.xz/plugins/nv-filters/nvidia-audiofx-filter.c
Changed
@@ -130,10 +130,15 @@ { struct nvidia_audio_data *ng = data; + if (!ng) + return; + if (ng->nvidia_sdk_dir_found) pthread_mutex_lock(&ng->nvafx_mutex); - NvAFX_UninitializeLogger(); + if (nvafx_new_sdk) + NvAFX_UninitializeLogger(); + for (size_t i = 0; i < ng->channels; i++) { if (ng->handle0) { if (NvAFX_DestroyEffect) {
View file
obs-studio-32.0.0~beta1.tar.xz/plugins/obs-ffmpeg/obs-ffmpeg-mpegts.c -> obs-studio-32.0.0~beta2.tar.xz/plugins/obs-ffmpeg/obs-ffmpeg-mpegts.c
Changed
@@ -863,8 +863,8 @@ const char *p; char buf1024; p = strchr(config->url, '?'); - if (av_find_info_tag(buf, sizeof(buf), "payload_size", p) || - av_find_info_tag(buf, sizeof(buf), "pkt_size", p)) { + if (p && (av_find_info_tag(buf, sizeof(buf), "payload_size", p) || + av_find_info_tag(buf, sizeof(buf), "pkt_size", p))) { config->srt_pkt_size = strtol(buf, NULL, 10); } return true;
View file
obs-studio-32.0.0~beta1.tar.xz/plugins/rtmp-services/data/package.json -> obs-studio-32.0.0~beta2.tar.xz/plugins/rtmp-services/data/package.json
Changed
@@ -1,11 +1,11 @@ { "$schema": "schema/package-schema.json", "url": "https://obsproject.com/obs2_update/rtmp-services/v5", - "version": 273, + "version": 274, "files": { "name": "services.json", - "version": 273 + "version": 274 } }
View file
obs-studio-32.0.0~beta1.tar.xz/plugins/rtmp-services/data/services.json -> obs-studio-32.0.0~beta2.tar.xz/plugins/rtmp-services/data/services.json
Changed
@@ -2409,24 +2409,6 @@ }, { - "name": "Live Streamer Cafe", - "more_info_link": "https://livestreamercafe.com/help.php", - "stream_key_link": "https://livestreamercafe.com/profile.php", - "servers": - { - "name": "Live Streamer Cafe Server", - "url": "rtmp://tophicles.com/live" - } - , - "recommended": { - "keyint": 2, - "max video bitrate": 6000 - }, - "supported video codecs": - "h264" - - }, - { "name": "Enchant.events", "more_info_link": "https://docs.enchant.events/knowledge-base-y4pOb", "servers":
View file
obs-studio-32.0.0~beta1.tar.xz/shared/qt/idian/components/ComboBox.cpp -> obs-studio-32.0.0~beta2.tar.xz/shared/qt/idian/components/ComboBox.cpp
Changed
@@ -46,7 +46,7 @@ // // All my efforts have failed so we get this instead. allowOpeningPopup = false; - QTimer::singleShot(120, this, =() { allowOpeningPopup = true; }); + QTimer::singleShot(120, this, this() { allowOpeningPopup = true; }); QComboBox::hidePopup(); }
View file
obs-studio-32.0.0~beta1.tar.xz/shared/qt/idian/include/Idian/Row.hpp -> obs-studio-32.0.0~beta2.tar.xz/shared/qt/idian/include/Idian/Row.hpp
Changed
@@ -40,6 +40,9 @@ public: GenericRow(QWidget *parent = nullptr) : QFrame(parent), Utils(this) { setAccessibleName(""); }; + + virtual void setTitle(const QString &title) = 0; + virtual void setDescription(const QString &description) = 0; }; // Row widget containing one or more controls @@ -61,8 +64,8 @@ void setPrefixEnabled(bool enabled); void setSuffixEnabled(bool enabled); - void setTitle(QString name); - void setDescription(QString description); + virtual void setTitle(const QString &title) override; + virtual void setDescription(const QString &description) override; void showTitle(bool visible); void showDescription(bool visible); @@ -171,14 +174,22 @@ Q_OBJECT public: - CollapsibleRow(const QString &name, QWidget *parent = nullptr); - CollapsibleRow(const QString &name, const QString &desc = nullptr, QWidget *parent = nullptr); + CollapsibleRow(QWidget *parent = nullptr); void setCheckable(bool check); bool isCheckable() { return checkable; } + void setChecked(bool checked); + bool isChecked() { return toggleSwitch->isChecked(); }; + + virtual void setTitle(const QString &title) override; + virtual void setDescription(const QString &description) override; + void addRow(GenericRow *actionRow); +signals: + void toggled(bool checked); + private: void toggleVisibility();
View file
obs-studio-32.0.0~beta1.tar.xz/shared/qt/idian/widgets/Group.cpp -> obs-studio-32.0.0~beta2.tar.xz/shared/qt/idian/widgets/Group.cpp
Changed
@@ -116,7 +116,7 @@ toggleSwitch = new ToggleSwitch(true); controlLayout->addWidget(toggleSwitch); connect(toggleSwitch, &ToggleSwitch::toggled, this, - =(bool checked) { propertyList->setEnabled(checked); }); + this(bool checked) { propertyList->setEnabled(checked); }); } if (!checkable && toggleSwitch) {
View file
obs-studio-32.0.0~beta1.tar.xz/shared/qt/idian/widgets/Row.cpp -> obs-studio-32.0.0~beta2.tar.xz/shared/qt/idian/widgets/Row.cpp
Changed
@@ -112,14 +112,14 @@ suffix_->setVisible(enabled); } -void Row::setTitle(QString name) +void Row::setTitle(const QString &name) { nameLabel->setText(name); setAccessibleName(name); showTitle(true); } -void Row::setDescription(QString description) +void Row::setDescription(const QString &description) { descriptionLabel->setText(description); setAccessibleDescription(description); @@ -260,7 +260,7 @@ } // Row variant that can be expanded to show another properties list -CollapsibleRow::CollapsibleRow(const QString &name, QWidget *parent) : GenericRow(parent) +CollapsibleRow::CollapsibleRow(QWidget *parent) : GenericRow(parent) { layout = new QVBoxLayout; layout->setContentsMargins(0, 0, 0, 0); @@ -274,7 +274,6 @@ rowWidget->setLayout(rowLayout); actionRow = new Row(); - actionRow->setTitle(name); actionRow->setChangeCursor(false); rowLayout->addWidget(actionRow); @@ -301,15 +300,9 @@ actionRow->setFocusProxy(expandButton); connect(expandButton, &QAbstractButton::clicked, this, &CollapsibleRow::toggleVisibility); - connect(actionRow, &Row::clicked, expandButton, &QAbstractButton::click); } -CollapsibleRow::CollapsibleRow(const QString &name, const QString &desc, QWidget *parent) : CollapsibleRow(name, parent) -{ - actionRow->setDescription(desc); -} - void CollapsibleRow::setCheckable(bool check) { checkable = check; @@ -322,6 +315,7 @@ actionRow->setSuffix(toggleSwitch, false); connect(toggleSwitch, &ToggleSwitch::toggled, propertyList, &PropertiesList::setEnabled); + connect(toggleSwitch, &ToggleSwitch::toggled, this, &CollapsibleRow::toggled); } if (!checkable && toggleSwitch) { @@ -332,6 +326,25 @@ } } +void CollapsibleRow::setChecked(bool checked) +{ + if (!isCheckable()) { + throw std::logic_error("Called setChecked on a non-checkable row."); + } + + toggleSwitch->setChecked(checked); +} + +void CollapsibleRow::setTitle(const QString &name) +{ + actionRow->setTitle(name); +} + +void CollapsibleRow::setDescription(const QString &description) +{ + actionRow->setDescription(description); +} + void CollapsibleRow::toggleVisibility() { bool visible = !propertyList->isVisible();
Locations
Projects
Search
Status Monitor
Help
Open Build Service
OBS Manuals
API Documentation
OBS Portal
Reporting a Bug
Contact
Mailing List
Forums
Chat (IRC)
Twitter
Open Build Service (OBS)
is an
openSUSE project
.