From fb3d85cff4698c3e3aaa1be6861abf45a0ed1f1d Mon Sep 17 00:00:00 2001 From: Dave Lane <42013603+ReenigneArcher@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:06:28 -0400 Subject: [PATCH] fix(nvenc): dynamic nv codec sdk selection (#5451) --- CMakeLists.txt | 4 + cmake/compile_definitions/common.cmake | 54 +- cmake/cpm/.cmake-lint-ignore | 0 cmake/cpm/CPM.cmake | 1379 ++++++++++++++++++++ cmake/dependencies/common.cmake | 1 + cmake/dependencies/nv_codec_headers.cmake | 9 + package-lock.cmake | 67 + src/nvenc/nvenc_base.cpp | 934 +++++++------ src/nvenc/nvenc_base.h | 213 ++- src/nvenc/nvenc_colorspace.h | 8 +- src/nvenc/nvenc_d3d11.cpp | 53 +- src/nvenc/nvenc_d3d11.h | 17 +- src/nvenc/nvenc_d3d11_interface.h | 36 + src/nvenc/nvenc_d3d11_native.cpp | 8 +- src/nvenc/nvenc_d3d11_native.h | 9 +- src/nvenc/nvenc_d3d11_on_cuda.cpp | 8 +- src/nvenc/nvenc_d3d11_on_cuda.h | 13 +- src/nvenc/nvenc_dynamic_factory.cpp | 129 ++ src/nvenc/nvenc_dynamic_factory.h | 106 ++ src/nvenc/nvenc_dynamic_factory_impl.cpp | 48 + src/nvenc/nvenc_dynamic_factory_versions.h | 67 + src/nvenc/nvenc_encoder.h | 75 ++ src/nvenc/nvenc_sdk.h | 125 ++ src/nvenc/nvenc_shared_dll.h | 49 + src/nvenc/nvenc_utils.cpp | 4 +- src/nvenc/nvenc_utils.h | 8 +- src/nvenc/nvenc_version.h | 53 + src/platform/common.h | 4 +- src/platform/windows/display_vram.cpp | 35 +- src/video.cpp | 5 +- tests/unit/test_nvenc_dynamic_factory.cpp | 212 +++ tests/unit/test_nvenc_version.cpp | 52 + 32 files changed, 3284 insertions(+), 501 deletions(-) create mode 100644 cmake/cpm/.cmake-lint-ignore create mode 100644 cmake/cpm/CPM.cmake create mode 100644 cmake/dependencies/nv_codec_headers.cmake create mode 100644 package-lock.cmake create mode 100644 src/nvenc/nvenc_d3d11_interface.h create mode 100644 src/nvenc/nvenc_dynamic_factory.cpp create mode 100644 src/nvenc/nvenc_dynamic_factory.h create mode 100644 src/nvenc/nvenc_dynamic_factory_impl.cpp create mode 100644 src/nvenc/nvenc_dynamic_factory_versions.h create mode 100644 src/nvenc/nvenc_encoder.h create mode 100644 src/nvenc/nvenc_sdk.h create mode 100644 src/nvenc/nvenc_shared_dll.h create mode 100644 src/nvenc/nvenc_version.h create mode 100644 tests/unit/test_nvenc_dynamic_factory.cpp create mode 100644 tests/unit/test_nvenc_version.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c94c1ffa..9ad1ef273 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,10 @@ endif() # set the module path, used for includes set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") +# CPM +include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/cpm/CPM.cmake) +CPMUsePackageLock(package-lock.cmake) + # export compile_commands.json set(CMAKE_EXPORT_COMPILE_COMMANDS ON) diff --git a/cmake/compile_definitions/common.cmake b/cmake/compile_definitions/common.cmake index d701023ef..502c9d63d 100644 --- a/cmake/compile_definitions/common.cmake +++ b/cmake/compile_definitions/common.cmake @@ -59,11 +59,53 @@ elseif(UNIX) endif() endif() -include_directories( - BEFORE SYSTEM - "${CMAKE_SOURCE_DIR}/third-party/build-deps/third-party/FFmpeg/nv-codec-headers/include" +set(NVENC_PUBLIC_SOURCES + "${CMAKE_SOURCE_DIR}/src/nvenc/nvenc_config.h" + "${CMAKE_SOURCE_DIR}/src/nvenc/nvenc_d3d11_interface.h" + "${CMAKE_SOURCE_DIR}/src/nvenc/nvenc_dynamic_factory.cpp" + "${CMAKE_SOURCE_DIR}/src/nvenc/nvenc_dynamic_factory.h" + "${CMAKE_SOURCE_DIR}/src/nvenc/nvenc_dynamic_factory_versions.h" + "${CMAKE_SOURCE_DIR}/src/nvenc/nvenc_encoded_frame.h" + "${CMAKE_SOURCE_DIR}/src/nvenc/nvenc_encoder.h" + "${CMAKE_SOURCE_DIR}/src/nvenc/nvenc_shared_dll.h" + "${CMAKE_SOURCE_DIR}/src/nvenc/nvenc_version.h" ) -file(GLOB NVENC_SOURCES CONFIGURE_DEPENDS "src/nvenc/*.cpp" "src/nvenc/*.h") +set(NVENC_SOURCES ${NVENC_PUBLIC_SOURCES}) + +if(WIN32) + set(NVENC_IMPLEMENTATION_SOURCES + "${CMAKE_SOURCE_DIR}/src/nvenc/nvenc_base.cpp" + "${CMAKE_SOURCE_DIR}/src/nvenc/nvenc_d3d11.cpp" + "${CMAKE_SOURCE_DIR}/src/nvenc/nvenc_d3d11_native.cpp" + "${CMAKE_SOURCE_DIR}/src/nvenc/nvenc_d3d11_on_cuda.cpp" + "${CMAKE_SOURCE_DIR}/src/nvenc/nvenc_dynamic_factory_impl.cpp" + "${CMAKE_SOURCE_DIR}/src/nvenc/nvenc_utils.cpp" + ) + + # Add a version-isolated NVENC implementation object library. + # add_nvenc_sdk_implementation: args = `target_name`, `sdk_version`, `sdk_include_dir` + function(add_nvenc_sdk_implementation target_name sdk_version sdk_include_dir) + add_library(${target_name} OBJECT ${NVENC_IMPLEMENTATION_SOURCES}) + target_include_directories(${target_name} BEFORE PRIVATE "${sdk_include_dir}") + target_compile_definitions(${target_name} PRIVATE + NVENC_FACTORY_SUFFIX=${sdk_version} + NVENC_NAMESPACE=nvenc_${sdk_version} + NVENC_SDK_VERSION=${sdk_version} + ) + target_compile_options(${target_name} PRIVATE ${SUNSHINE_COMPILE_OPTIONS}) + endfunction() + + add_nvenc_sdk_implementation(nvenc_sdk_1100 1100 "${NV_CODEC_HEADERS_11_INCLUDE_DIR}") + add_nvenc_sdk_implementation(nvenc_sdk_1200 1200 "${NV_CODEC_HEADERS_12_INCLUDE_DIR}") + add_nvenc_sdk_implementation(nvenc_sdk_1300 1300 "${NV_CODEC_HEADERS_13_INCLUDE_DIR}") + + list(APPEND NVENC_SOURCES + $ + $ + $ + ) +endif() + list(APPEND PLATFORM_TARGET_FILES ${NVENC_SOURCES}) set(SUNSHINE_TARGET_FILES @@ -155,6 +197,10 @@ include_directories( ${Boost_INCLUDE_DIRS} # has to be the last, or we get runtime error on macOS ffmpeg encoder ) +if(WIN32) + include_directories(BEFORE SYSTEM "${NV_CODEC_HEADERS_13_INCLUDE_DIR}") +endif() + list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${MINIUPNP_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} diff --git a/cmake/cpm/.cmake-lint-ignore b/cmake/cpm/.cmake-lint-ignore new file mode 100644 index 000000000..e69de29bb diff --git a/cmake/cpm/CPM.cmake b/cmake/cpm/CPM.cmake new file mode 100644 index 000000000..e7bb536a4 --- /dev/null +++ b/cmake/cpm/CPM.cmake @@ -0,0 +1,1379 @@ +# CPM.cmake - CMake's missing package manager +# =========================================== +# See https://github.com/cpm-cmake/CPM.cmake for usage and update instructions. +# +# MIT License +# ----------- +#[[ + Copyright (c) 2019-2023 Lars Melchior and contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +]] + +cmake_minimum_required(VERSION 3.14 FATAL_ERROR) + +# Initialize logging prefix +if(NOT CPM_INDENT) + set(CPM_INDENT + "CPM:" + CACHE INTERNAL "" + ) +endif() + +if(NOT COMMAND cpm_message) + function(cpm_message) + message(${ARGV}) + endfunction() +endif() + +if(DEFINED EXTRACTED_CPM_VERSION) + set(CURRENT_CPM_VERSION "${EXTRACTED_CPM_VERSION}${CPM_DEVELOPMENT}") +else() + set(CURRENT_CPM_VERSION 0.43.1) +endif() + +get_filename_component(CPM_CURRENT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" REALPATH) +if(CPM_DIRECTORY) + if(NOT CPM_DIRECTORY STREQUAL CPM_CURRENT_DIRECTORY) + if(CPM_VERSION VERSION_LESS CURRENT_CPM_VERSION) + message( + AUTHOR_WARNING + "${CPM_INDENT} \ +A dependency is using a more recent CPM version (${CURRENT_CPM_VERSION}) than the current project (${CPM_VERSION}). \ +It is recommended to upgrade CPM to the most recent version. \ +See https://github.com/cpm-cmake/CPM.cmake for more information." + ) + endif() + if(${CMAKE_VERSION} VERSION_LESS "3.17.0") + include(FetchContent) + endif() + return() + endif() + + get_property( + CPM_INITIALIZED GLOBAL "" + PROPERTY CPM_INITIALIZED + SET + ) + if(CPM_INITIALIZED) + return() + endif() +endif() + +if(CURRENT_CPM_VERSION MATCHES "development-version") + message( + WARNING "${CPM_INDENT} Your project is using an unstable development version of CPM.cmake. \ +Please update to a recent release if possible. \ +See https://github.com/cpm-cmake/CPM.cmake for details." + ) +endif() + +set_property(GLOBAL PROPERTY CPM_INITIALIZED true) + +macro(cpm_set_policies) + # the policy allows us to change options without caching + cmake_policy(SET CMP0077 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) + + # the policy allows us to change set(CACHE) without caching + if(POLICY CMP0126) + cmake_policy(SET CMP0126 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0126 NEW) + endif() + + # The policy uses the download time for timestamp, instead of the timestamp in the archive. This + # allows for proper rebuilds when a projects url changes + if(POLICY CMP0135) + cmake_policy(SET CMP0135 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0135 NEW) + endif() + + # treat relative git repository paths as being relative to the parent project's remote + if(POLICY CMP0150) + cmake_policy(SET CMP0150 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0150 NEW) + endif() +endmacro() +cpm_set_policies() + +option(CPM_USE_LOCAL_PACKAGES "Always try to use `find_package` to get dependencies" + $ENV{CPM_USE_LOCAL_PACKAGES} +) +option(CPM_LOCAL_PACKAGES_ONLY "Only use `find_package` to get dependencies" + $ENV{CPM_LOCAL_PACKAGES_ONLY} +) +option(CPM_DOWNLOAD_ALL "Always download dependencies from source" $ENV{CPM_DOWNLOAD_ALL}) +option(CPM_DONT_UPDATE_MODULE_PATH "Don't update the module path to allow using find_package" + $ENV{CPM_DONT_UPDATE_MODULE_PATH} +) +option(CPM_DONT_CREATE_PACKAGE_LOCK "Don't create a package lock file in the binary path" + $ENV{CPM_DONT_CREATE_PACKAGE_LOCK} +) +option(CPM_INCLUDE_ALL_IN_PACKAGE_LOCK + "Add all packages added through CPM.cmake to the package lock" + $ENV{CPM_INCLUDE_ALL_IN_PACKAGE_LOCK} +) +option(CPM_USE_NAMED_CACHE_DIRECTORIES + "Use additional directory of package name in cache on the most nested level." + $ENV{CPM_USE_NAMED_CACHE_DIRECTORIES} +) + +set(CPM_VERSION + ${CURRENT_CPM_VERSION} + CACHE INTERNAL "" +) +set(CPM_DIRECTORY + ${CPM_CURRENT_DIRECTORY} + CACHE INTERNAL "" +) +set(CPM_FILE + ${CMAKE_CURRENT_LIST_FILE} + CACHE INTERNAL "" +) +set(CPM_PACKAGES + "" + CACHE INTERNAL "" +) +set(CPM_DRY_RUN + OFF + CACHE INTERNAL "Don't download or configure dependencies (for testing)" +) + +if(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_SOURCE_CACHE_DEFAULT $ENV{CPM_SOURCE_CACHE}) +else() + set(CPM_SOURCE_CACHE_DEFAULT OFF) +endif() + +set(CPM_SOURCE_CACHE + ${CPM_SOURCE_CACHE_DEFAULT} + CACHE PATH "Directory to download CPM dependencies" +) + +if(NOT CPM_DONT_UPDATE_MODULE_PATH AND NOT DEFINED CMAKE_FIND_PACKAGE_REDIRECTS_DIR) + set(CPM_MODULE_PATH + "${CMAKE_BINARY_DIR}/CPM_modules" + CACHE INTERNAL "" + ) + # remove old modules + file(REMOVE_RECURSE ${CPM_MODULE_PATH}) + file(MAKE_DIRECTORY ${CPM_MODULE_PATH}) + # locally added CPM modules should override global packages + set(CMAKE_MODULE_PATH "${CPM_MODULE_PATH};${CMAKE_MODULE_PATH}") +endif() + +if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + set(CPM_PACKAGE_LOCK_FILE + "${CMAKE_BINARY_DIR}/cpm-package-lock.cmake" + CACHE INTERNAL "" + ) + file(WRITE ${CPM_PACKAGE_LOCK_FILE} + "# CPM Package Lock\n# This file should be committed to version control\n\n" + ) +endif() + +include(FetchContent) + +# Try to infer package name from git repository uri (path or url) +function(cpm_package_name_from_git_uri URI RESULT) + if("${URI}" MATCHES "([^/:]+)/?.git/?$") + set(${RESULT} + ${CMAKE_MATCH_1} + PARENT_SCOPE + ) + else() + unset(${RESULT} PARENT_SCOPE) + endif() +endfunction() + +# Find the shortest hash that can be used eg, if origin_hash is +# cccb77ae9609d2768ed80dd42cec54f77b1f1455 the following files will be checked, until one is found +# that is either empty (allowing us to assign origin_hash), or whose contents matches ${origin_hash} +# +# * .../cccb.hash +# * .../cccb77ae.hash +# * .../cccb77ae9609.hash +# * .../cccb77ae9609d276.hash +# * etc +# +# We will be able to use a shorter path with very high probability, but in the (rare) event that the +# first couple characters collide, we will check longer and longer substrings. +function(cpm_get_shortest_hash source_cache_dir origin_hash short_hash_output_var) + # for compatibility with caches populated by a previous version of CPM, check if a directory using + # the full hash already exists + if(EXISTS "${source_cache_dir}/${origin_hash}") + set(${short_hash_output_var} + "${origin_hash}" + PARENT_SCOPE + ) + return() + endif() + + foreach(len RANGE 4 40 4) + string(SUBSTRING "${origin_hash}" 0 ${len} short_hash) + set(hash_lock ${source_cache_dir}/${short_hash}.lock) + set(hash_fp ${source_cache_dir}/${short_hash}.hash) + # Take a lock, so we don't have a race condition with another instance of cmake. We will release + # this lock when we can, however, if there is an error, we want to ensure it gets released on + # it's own on exit from the function. + file(LOCK ${hash_lock} GUARD FUNCTION) + + # Load the contents of .../${short_hash}.hash + file(TOUCH ${hash_fp}) + file(READ ${hash_fp} hash_fp_contents) + + if(hash_fp_contents STREQUAL "") + # Write the origin hash + file(WRITE ${hash_fp} ${origin_hash}) + file(LOCK ${hash_lock} RELEASE) + break() + elseif(hash_fp_contents STREQUAL origin_hash) + file(LOCK ${hash_lock} RELEASE) + break() + else() + file(LOCK ${hash_lock} RELEASE) + endif() + endforeach() + set(${short_hash_output_var} + "${short_hash}" + PARENT_SCOPE + ) +endfunction() + +# Try to infer package name and version from a url +function(cpm_package_name_and_ver_from_url url outName outVer) + if(url MATCHES + "[/\\?]([a-zA-Z0-9_\\.-]+)\\.(tar|tar\\.gz|tar\\.bz2|tar\\.xz|tar\\.zst|zip|ZIP)(\\?|/|$)" + ) + # We matched an archive + set(filename "${CMAKE_MATCH_1}") + + if(filename MATCHES "([a-zA-Z0-9_\\.-]+)[_-]v?(([0-9]+\\.)*[0-9]+[a-zA-Z0-9]*)") + # We matched - (ie foo-1.2.3) + set(${outName} + "${CMAKE_MATCH_1}" + PARENT_SCOPE + ) + set(${outVer} + "${CMAKE_MATCH_2}" + PARENT_SCOPE + ) + elseif(filename MATCHES "(([0-9]+\\.)+[0-9]+[a-zA-Z0-9]*)") + # We couldn't find a name, but we found a version + # + # In many cases (which we don't handle here) the url would look something like + # `irrelevant/ACTUAL_PACKAGE_NAME/irrelevant/1.2.3.zip`. In such a case we can't possibly + # distinguish the package name from the irrelevant bits. Moreover if we try to match the + # package name from the filename, we'd get bogus at best. + unset(${outName} PARENT_SCOPE) + set(${outVer} + "${CMAKE_MATCH_1}" + PARENT_SCOPE + ) + else() + # Boldly assume that the file name is the package name. + # + # Yes, something like `irrelevant/ACTUAL_NAME/irrelevant/download.zip` will ruin our day, but + # such cases should be quite rare. No popular service does this... we think. + set(${outName} + "${filename}" + PARENT_SCOPE + ) + unset(${outVer} PARENT_SCOPE) + endif() + else() + # No ideas yet what to do with non-archives + unset(${outName} PARENT_SCOPE) + unset(${outVer} PARENT_SCOPE) + endif() +endfunction() + +function(cpm_find_package NAME VERSION) + string(REPLACE " " ";" EXTRA_ARGS "${ARGN}") + find_package(${NAME} ${VERSION} ${EXTRA_ARGS} QUIET) + if(${CPM_ARGS_NAME}_FOUND) + if(DEFINED ${CPM_ARGS_NAME}_VERSION) + set(VERSION ${${CPM_ARGS_NAME}_VERSION}) + endif() + cpm_message(STATUS "${CPM_INDENT} Using local package ${CPM_ARGS_NAME}@${VERSION}") + CPMRegisterPackage(${CPM_ARGS_NAME} "${VERSION}") + set(CPM_PACKAGE_FOUND + YES + PARENT_SCOPE + ) + else() + set(CPM_PACKAGE_FOUND + NO + PARENT_SCOPE + ) + endif() +endfunction() + +# Create a custom FindXXX.cmake module for a CPM package This prevents `find_package(NAME)` from +# finding the system library +function(cpm_create_module_file Name) + if(NOT CPM_DONT_UPDATE_MODULE_PATH) + if(DEFINED CMAKE_FIND_PACKAGE_REDIRECTS_DIR) + # Redirect find_package calls to the CPM package. This is what FetchContent does when you set + # OVERRIDE_FIND_PACKAGE. The CMAKE_FIND_PACKAGE_REDIRECTS_DIR works for find_package in CONFIG + # mode, unlike the Find${Name}.cmake fallback. CMAKE_FIND_PACKAGE_REDIRECTS_DIR is not defined + # in script mode, or in CMake < 3.24. + # https://cmake.org/cmake/help/latest/module/FetchContent.html#fetchcontent-find-package-integration-examples + string(TOLOWER ${Name} NameLower) + file(WRITE ${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}/${NameLower}-config.cmake + "include(\"\${CMAKE_CURRENT_LIST_DIR}/${NameLower}-extra.cmake\" OPTIONAL)\n" + "include(\"\${CMAKE_CURRENT_LIST_DIR}/${Name}Extra.cmake\" OPTIONAL)\n" + ) + file(WRITE ${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}/${NameLower}-config-version.cmake + "set(PACKAGE_VERSION_COMPATIBLE TRUE)\n" "set(PACKAGE_VERSION_EXACT TRUE)\n" + ) + else() + file(WRITE ${CPM_MODULE_PATH}/Find${Name}.cmake + "include(\"${CPM_FILE}\")\n${ARGN}\nset(${Name}_FOUND TRUE)" + ) + endif() + endif() +endfunction() + +# Find a package locally or fallback to CPMAddPackage +function(CPMFindPackage) + set(oneValueArgs NAME VERSION GIT_TAG FIND_PACKAGE_ARGUMENTS) + + cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "" ${ARGN}) + + if(NOT DEFINED CPM_ARGS_VERSION) + if(DEFINED CPM_ARGS_GIT_TAG) + cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION) + endif() + endif() + + set(downloadPackage ${CPM_DOWNLOAD_ALL}) + if(DEFINED CPM_DOWNLOAD_${CPM_ARGS_NAME}) + set(downloadPackage ${CPM_DOWNLOAD_${CPM_ARGS_NAME}}) + elseif(DEFINED ENV{CPM_DOWNLOAD_${CPM_ARGS_NAME}}) + set(downloadPackage $ENV{CPM_DOWNLOAD_${CPM_ARGS_NAME}}) + endif() + if(downloadPackage) + CPMAddPackage(${ARGN}) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) + + if(NOT CPM_PACKAGE_FOUND) + CPMAddPackage(${ARGN}) + cpm_export_variables(${CPM_ARGS_NAME}) + endif() + +endfunction() + +# checks if a package has been added before +function(cpm_check_if_package_already_added CPM_ARGS_NAME CPM_ARGS_VERSION) + if("${CPM_ARGS_NAME}" IN_LIST CPM_PACKAGES) + CPMGetPackageVersion(${CPM_ARGS_NAME} CPM_PACKAGE_VERSION) + if("${CPM_PACKAGE_VERSION}" VERSION_LESS "${CPM_ARGS_VERSION}") + message( + WARNING + "${CPM_INDENT} Requires a newer version of ${CPM_ARGS_NAME} (${CPM_ARGS_VERSION}) than currently included (${CPM_PACKAGE_VERSION})." + ) + endif() + cpm_get_fetch_properties(${CPM_ARGS_NAME}) + set(${CPM_ARGS_NAME}_ADDED NO) + set(CPM_PACKAGE_ALREADY_ADDED + YES + PARENT_SCOPE + ) + cpm_export_variables(${CPM_ARGS_NAME}) + else() + set(CPM_PACKAGE_ALREADY_ADDED + NO + PARENT_SCOPE + ) + endif() +endfunction() + +# Parse the argument of CPMAddPackage in case a single one was provided and convert it to a list of +# arguments which can then be parsed idiomatically. For example gh:foo/bar@1.2.3 will be converted +# to: GITHUB_REPOSITORY;foo/bar;VERSION;1.2.3 +function(cpm_parse_add_package_single_arg arg outArgs) + # Look for a scheme + if("${arg}" MATCHES "^([a-zA-Z]+):(.+)$") + string(TOLOWER "${CMAKE_MATCH_1}" scheme) + set(uri "${CMAKE_MATCH_2}") + + # Check for CPM-specific schemes + if(scheme STREQUAL "gh") + set(out "GITHUB_REPOSITORY;${uri}") + set(packageType "git") + elseif(scheme STREQUAL "gl") + set(out "GITLAB_REPOSITORY;${uri}") + set(packageType "git") + elseif(scheme STREQUAL "bb") + set(out "BITBUCKET_REPOSITORY;${uri}") + set(packageType "git") + # A CPM-specific scheme was not found. Looks like this is a generic URL so try to determine + # type + elseif(arg MATCHES ".git/?(@|#|$)") + set(out "GIT_REPOSITORY;${arg}") + set(packageType "git") + else() + # Fall back to a URL + set(out "URL;${arg}") + set(packageType "archive") + + # We could also check for SVN since FetchContent supports it, but SVN is so rare these days. + # We just won't bother with the additional complexity it will induce in this function. SVN is + # done by multi-arg + endif() + else() + if(arg MATCHES ".git/?(@|#|$)") + set(out "GIT_REPOSITORY;${arg}") + set(packageType "git") + else() + # Give up + message(FATAL_ERROR "${CPM_INDENT} Can't determine package type of '${arg}'") + endif() + endif() + + # For all packages we interpret @... as version. Only replace the last occurrence. Thus URIs + # containing '@' can be used + string(REGEX REPLACE "@([^@]+)$" ";VERSION;\\1" out "${out}") + + # Parse the rest according to package type + if(packageType STREQUAL "git") + # For git repos we interpret #... as a tag or branch or commit hash + string(REGEX REPLACE "#([^#]+)$" ";GIT_TAG;\\1" out "${out}") + elseif(packageType STREQUAL "archive") + # For archives we interpret #... as a URL hash. + string(REGEX REPLACE "#([^#]+)$" ";URL_HASH;\\1" out "${out}") + # We don't try to parse the version if it's not provided explicitly. cpm_get_version_from_url + # should do this at a later point + else() + # We should never get here. This is an assertion and hitting it means there's a problem with the + # code above. A packageType was set, but not handled by this if-else. + message(FATAL_ERROR "${CPM_INDENT} Unsupported package type '${packageType}' of '${arg}'") + endif() + + set(${outArgs} + ${out} + PARENT_SCOPE + ) +endfunction() + +# Check that the working directory for a git repo is clean +function(cpm_check_git_working_dir_is_clean repoPath gitTag isClean) + + find_package(Git REQUIRED) + + if(NOT GIT_EXECUTABLE) + # No git executable, assume directory is clean + set(${isClean} + TRUE + PARENT_SCOPE + ) + return() + endif() + + # check for uncommitted changes + execute_process( + COMMAND ${GIT_EXECUTABLE} status --porcelain + RESULT_VARIABLE resultGitStatus + OUTPUT_VARIABLE repoStatus + OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET + WORKING_DIRECTORY ${repoPath} + ) + if(resultGitStatus) + # not supposed to happen, assume clean anyway + message(WARNING "${CPM_INDENT} Calling git status on folder ${repoPath} failed") + set(${isClean} + TRUE + PARENT_SCOPE + ) + return() + endif() + + if(NOT "${repoStatus}" STREQUAL "") + set(${isClean} + FALSE + PARENT_SCOPE + ) + return() + endif() + + # check for committed changes + execute_process( + COMMAND ${GIT_EXECUTABLE} diff -s --exit-code ${gitTag} + RESULT_VARIABLE resultGitDiff + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_QUIET + WORKING_DIRECTORY ${repoPath} + ) + + if(${resultGitDiff} EQUAL 0) + set(${isClean} + TRUE + PARENT_SCOPE + ) + else() + set(${isClean} + FALSE + PARENT_SCOPE + ) + endif() + +endfunction() + +# Add PATCH_COMMAND to CPM_ARGS_UNPARSED_ARGUMENTS. This method consumes a list of files in ARGN +# then generates a `PATCH_COMMAND` appropriate for `ExternalProject_Add()`. This command is appended +# to the parent scope's `CPM_ARGS_UNPARSED_ARGUMENTS`. +function(cpm_add_patches) + # Return if no patch files are supplied. + if(NOT ARGN) + return() + endif() + + # Find the patch program. + find_program(PATCH_EXECUTABLE patch) + if(CMAKE_HOST_WIN32 AND NOT PATCH_EXECUTABLE) + # The Windows git executable is distributed with patch.exe. Find the path to the executable, if + # it exists, then search `../usr/bin` and `../../usr/bin` for patch.exe. + find_package(Git QUIET) + if(GIT_EXECUTABLE) + get_filename_component(extra_search_path ${GIT_EXECUTABLE} DIRECTORY) + get_filename_component(extra_search_path_1up ${extra_search_path} DIRECTORY) + get_filename_component(extra_search_path_2up ${extra_search_path_1up} DIRECTORY) + find_program( + PATCH_EXECUTABLE patch HINTS "${extra_search_path_1up}/usr/bin" + "${extra_search_path_2up}/usr/bin" + ) + endif() + endif() + if(NOT PATCH_EXECUTABLE) + message(FATAL_ERROR "Couldn't find `patch` executable to use with PATCHES keyword.") + endif() + + # Create a temporary + set(temp_list ${CPM_ARGS_UNPARSED_ARGUMENTS}) + + # Ensure each file exists (or error out) and add it to the list. + set(first_item True) + foreach(PATCH_FILE ${ARGN}) + # Make sure the patch file exists, if we can't find it, try again in the current directory. + if(NOT EXISTS "${PATCH_FILE}") + if(NOT EXISTS "${CMAKE_CURRENT_LIST_DIR}/${PATCH_FILE}") + message(FATAL_ERROR "Couldn't find patch file: '${PATCH_FILE}'") + endif() + set(PATCH_FILE "${CMAKE_CURRENT_LIST_DIR}/${PATCH_FILE}") + endif() + + # Convert to absolute path for use with patch file command. + get_filename_component(PATCH_FILE "${PATCH_FILE}" ABSOLUTE) + + # The first patch entry must be preceded by "PATCH_COMMAND" while the following items are + # preceded by "&&". + if(first_item) + set(first_item False) + list(APPEND temp_list "PATCH_COMMAND") + else() + list(APPEND temp_list "&&") + endif() + # Add the patch command to the list + list(APPEND temp_list "${PATCH_EXECUTABLE}" "-p1" "<" "${PATCH_FILE}") + endforeach() + + # Move temp out into parent scope. + set(CPM_ARGS_UNPARSED_ARGUMENTS + ${temp_list} + PARENT_SCOPE + ) + +endfunction() + +# method to overwrite internal FetchContent properties, to allow using CPM.cmake to overload +# FetchContent calls. As these are internal cmake properties, this method should be used carefully +# and may need modification in future CMake versions. Source: +# https://github.com/Kitware/CMake/blob/dc3d0b5a0a7d26d43d6cfeb511e224533b5d188f/Modules/FetchContent.cmake#L1152 +function(cpm_override_fetchcontent contentName) + cmake_parse_arguments(PARSE_ARGV 1 arg "" "SOURCE_DIR;BINARY_DIR" "") + if(NOT "${arg_UNPARSED_ARGUMENTS}" STREQUAL "") + message(FATAL_ERROR "${CPM_INDENT} Unsupported arguments: ${arg_UNPARSED_ARGUMENTS}") + endif() + + string(TOLOWER ${contentName} contentNameLower) + set(prefix "_FetchContent_${contentNameLower}") + + set(propertyName "${prefix}_sourceDir") + define_property( + GLOBAL + PROPERTY ${propertyName} + BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" + FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" + ) + set_property(GLOBAL PROPERTY ${propertyName} "${arg_SOURCE_DIR}") + + set(propertyName "${prefix}_binaryDir") + define_property( + GLOBAL + PROPERTY ${propertyName} + BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" + FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" + ) + set_property(GLOBAL PROPERTY ${propertyName} "${arg_BINARY_DIR}") + + set(propertyName "${prefix}_populated") + define_property( + GLOBAL + PROPERTY ${propertyName} + BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" + FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" + ) + set_property(GLOBAL PROPERTY ${propertyName} TRUE) +endfunction() + +# Download and add a package from source +function(CPMAddPackage) + cpm_set_policies() + + set(oneValueArgs + NAME + FORCE + VERSION + GIT_TAG + DOWNLOAD_ONLY + GITHUB_REPOSITORY + GITLAB_REPOSITORY + BITBUCKET_REPOSITORY + GIT_REPOSITORY + SOURCE_DIR + FIND_PACKAGE_ARGUMENTS + NO_CACHE + SYSTEM + GIT_SHALLOW + EXCLUDE_FROM_ALL + SOURCE_SUBDIR + CUSTOM_CACHE_KEY + ) + + set(multiValueArgs URL OPTIONS DOWNLOAD_COMMAND PATCHES) + + list(LENGTH ARGN argnLength) + + # Parse single shorthand argument + if(argnLength EQUAL 1) + cpm_parse_add_package_single_arg("${ARGN}" ARGN) + + # The shorthand syntax implies EXCLUDE_FROM_ALL and SYSTEM + set(ARGN "${ARGN};EXCLUDE_FROM_ALL;YES;SYSTEM;YES;") + + # Parse URI shorthand argument + elseif(argnLength GREATER 1 AND "${ARGV0}" STREQUAL "URI") + list(REMOVE_AT ARGN 0 1) # remove "URI gh:<...>@version#tag" + cpm_parse_add_package_single_arg("${ARGV1}" ARGV0) + + set(ARGN "${ARGV0};EXCLUDE_FROM_ALL;YES;SYSTEM;YES;${ARGN}") + endif() + + cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}") + + # Set default values for arguments + if(NOT DEFINED CPM_ARGS_VERSION) + if(DEFINED CPM_ARGS_GIT_TAG) + cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION) + endif() + endif() + + if(CPM_ARGS_DOWNLOAD_ONLY) + set(DOWNLOAD_ONLY ${CPM_ARGS_DOWNLOAD_ONLY}) + else() + set(DOWNLOAD_ONLY NO) + endif() + + if(DEFINED CPM_ARGS_GITHUB_REPOSITORY) + set(CPM_ARGS_GIT_REPOSITORY "https://github.com/${CPM_ARGS_GITHUB_REPOSITORY}.git") + elseif(DEFINED CPM_ARGS_GITLAB_REPOSITORY) + set(CPM_ARGS_GIT_REPOSITORY "https://gitlab.com/${CPM_ARGS_GITLAB_REPOSITORY}.git") + elseif(DEFINED CPM_ARGS_BITBUCKET_REPOSITORY) + set(CPM_ARGS_GIT_REPOSITORY "https://bitbucket.org/${CPM_ARGS_BITBUCKET_REPOSITORY}.git") + endif() + + if(DEFINED CPM_ARGS_GIT_REPOSITORY) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_REPOSITORY ${CPM_ARGS_GIT_REPOSITORY}) + if(NOT DEFINED CPM_ARGS_GIT_TAG) + set(CPM_ARGS_GIT_TAG v${CPM_ARGS_VERSION}) + endif() + + # If a name wasn't provided, try to infer it from the git repo + if(NOT DEFINED CPM_ARGS_NAME) + cpm_package_name_from_git_uri(${CPM_ARGS_GIT_REPOSITORY} CPM_ARGS_NAME) + endif() + endif() + + set(CPM_SKIP_FETCH FALSE) + + if(DEFINED CPM_ARGS_GIT_TAG) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_TAG ${CPM_ARGS_GIT_TAG}) + # If GIT_SHALLOW is explicitly specified, honor the value. + if(DEFINED CPM_ARGS_GIT_SHALLOW) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_SHALLOW ${CPM_ARGS_GIT_SHALLOW}) + endif() + endif() + + if(DEFINED CPM_ARGS_URL) + # If a name or version aren't provided, try to infer them from the URL + list(GET CPM_ARGS_URL 0 firstUrl) + cpm_package_name_and_ver_from_url(${firstUrl} nameFromUrl verFromUrl) + # If we fail to obtain name and version from the first URL, we could try other URLs if any. + # However multiple URLs are expected to be quite rare, so for now we won't bother. + + # If the caller provided their own name and version, they trump the inferred ones. + if(NOT DEFINED CPM_ARGS_NAME) + set(CPM_ARGS_NAME ${nameFromUrl}) + endif() + if(NOT DEFINED CPM_ARGS_VERSION) + set(CPM_ARGS_VERSION ${verFromUrl}) + endif() + + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS URL "${CPM_ARGS_URL}") + endif() + + # Check for required arguments + + if(NOT DEFINED CPM_ARGS_NAME) + message( + FATAL_ERROR + "${CPM_INDENT} 'NAME' was not provided and couldn't be automatically inferred for package added with arguments: '${ARGN}'" + ) + endif() + + # Check if package has been added before + cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") + if(CPM_PACKAGE_ALREADY_ADDED) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + if(NOT DEFINED CPM_${CPM_ARGS_NAME}_SOURCE AND DEFINED ENV{CPM_${CPM_ARGS_NAME}_SOURCE}) + # Normalize separators to support Windows paths when reading from environment variables. + file(TO_CMAKE_PATH "$ENV{CPM_${CPM_ARGS_NAME}_SOURCE}" CPM_${CPM_ARGS_NAME}_SOURCE) + message(WARNING "${CPM_INDENT} '${CPM_ARGS_NAME}' version overridden by environment variable " + "CPM_${CPM_ARGS_NAME}_SOURCE='${CPM_${CPM_ARGS_NAME}_SOURCE}'" + ) + endif() + + # Check for manual overrides + if(NOT CPM_ARGS_FORCE AND NOT "${CPM_${CPM_ARGS_NAME}_SOURCE}" STREQUAL "") + set(PACKAGE_SOURCE ${CPM_${CPM_ARGS_NAME}_SOURCE}) + set(CPM_${CPM_ARGS_NAME}_SOURCE "") + if(NOT DEFINED ENV{CPM_${CPM_ARGS_NAME}_SOURCE}) + message(WARNING "${CPM_INDENT} '${CPM_ARGS_NAME}' version overridden by CMake variable " + "CPM_${CPM_ARGS_NAME}_SOURCE='${PACKAGE_SOURCE}'" + ) + endif() + CPMAddPackage( + NAME "${CPM_ARGS_NAME}" + SOURCE_DIR "${PACKAGE_SOURCE}" + EXCLUDE_FROM_ALL "${CPM_ARGS_EXCLUDE_FROM_ALL}" + SYSTEM "${CPM_ARGS_SYSTEM}" + PATCHES "${CPM_ARGS_PATCHES}" + OPTIONS "${CPM_ARGS_OPTIONS}" + SOURCE_SUBDIR "${CPM_ARGS_SOURCE_SUBDIR}" + DOWNLOAD_ONLY "${DOWNLOAD_ONLY}" + FORCE True + ) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + # Check for available declaration + if(NOT CPM_ARGS_FORCE AND NOT "${CPM_DECLARATION_${CPM_ARGS_NAME}}" STREQUAL "") + set(declaration ${CPM_DECLARATION_${CPM_ARGS_NAME}}) + set(CPM_DECLARATION_${CPM_ARGS_NAME} "") + CPMAddPackage(${declaration}) + cpm_export_variables(${CPM_ARGS_NAME}) + # checking again to ensure version and option compatibility + cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") + return() + endif() + + if(NOT CPM_ARGS_FORCE) + if(CPM_USE_LOCAL_PACKAGES OR CPM_LOCAL_PACKAGES_ONLY) + cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) + + if(CPM_PACKAGE_FOUND) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + if(CPM_LOCAL_PACKAGES_ONLY) + message( + SEND_ERROR + "${CPM_INDENT} ${CPM_ARGS_NAME} not found via find_package(${CPM_ARGS_NAME} ${CPM_ARGS_VERSION})" + ) + endif() + endif() + endif() + + CPMRegisterPackage("${CPM_ARGS_NAME}" "${CPM_ARGS_VERSION}") + + if(DEFINED CPM_ARGS_GIT_TAG) + set(PACKAGE_INFO "${CPM_ARGS_GIT_TAG}") + elseif(DEFINED CPM_ARGS_SOURCE_DIR) + set(PACKAGE_INFO "${CPM_ARGS_SOURCE_DIR}") + else() + set(PACKAGE_INFO "${CPM_ARGS_VERSION}") + endif() + + if(DEFINED FETCHCONTENT_BASE_DIR) + # respect user's FETCHCONTENT_BASE_DIR if set + set(CPM_FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR}) + else() + set(CPM_FETCHCONTENT_BASE_DIR ${CMAKE_BINARY_DIR}/_deps) + endif() + + cpm_add_patches(${CPM_ARGS_PATCHES}) + + if(DEFINED CPM_ARGS_DOWNLOAD_COMMAND) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS DOWNLOAD_COMMAND ${CPM_ARGS_DOWNLOAD_COMMAND}) + elseif(DEFINED CPM_ARGS_SOURCE_DIR) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS SOURCE_DIR ${CPM_ARGS_SOURCE_DIR}) + if(NOT IS_ABSOLUTE ${CPM_ARGS_SOURCE_DIR}) + # Expand `CPM_ARGS_SOURCE_DIR` relative path. This is important because EXISTS doesn't work + # for relative paths. + get_filename_component( + source_directory ${CPM_ARGS_SOURCE_DIR} REALPATH BASE_DIR ${CMAKE_CURRENT_BINARY_DIR} + ) + else() + set(source_directory ${CPM_ARGS_SOURCE_DIR}) + endif() + if(NOT EXISTS ${source_directory}) + string(TOLOWER ${CPM_ARGS_NAME} lower_case_name) + # remove timestamps so CMake will re-download the dependency + file(REMOVE_RECURSE "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild") + endif() + elseif(CPM_SOURCE_CACHE AND NOT CPM_ARGS_NO_CACHE) + string(TOLOWER ${CPM_ARGS_NAME} lower_case_name) + set(origin_parameters ${CPM_ARGS_UNPARSED_ARGUMENTS}) + list(SORT origin_parameters) + if(CPM_ARGS_CUSTOM_CACHE_KEY) + # Application set a custom unique directory name + set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${CPM_ARGS_CUSTOM_CACHE_KEY}) + elseif(CPM_USE_NAMED_CACHE_DIRECTORIES) + string(SHA1 origin_hash "${origin_parameters};NEW_CACHE_STRUCTURE_TAG") + cpm_get_shortest_hash( + "${CPM_SOURCE_CACHE}/${lower_case_name}" # source cache directory + "${origin_hash}" # Input hash + origin_hash # Computed hash + ) + set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash}/${CPM_ARGS_NAME}) + else() + string(SHA1 origin_hash "${origin_parameters}") + cpm_get_shortest_hash( + "${CPM_SOURCE_CACHE}/${lower_case_name}" # source cache directory + "${origin_hash}" # Input hash + origin_hash # Computed hash + ) + set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash}) + endif() + # Expand `download_directory` relative path. This is important because EXISTS doesn't work for + # relative paths. + get_filename_component(download_directory ${download_directory} ABSOLUTE) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS SOURCE_DIR ${download_directory}) + + if(CPM_SOURCE_CACHE) + file(LOCK ${download_directory}/../cmake.lock) + endif() + + if(EXISTS ${download_directory}) + if(CPM_SOURCE_CACHE) + file(LOCK ${download_directory}/../cmake.lock RELEASE) + endif() + + cpm_store_fetch_properties( + ${CPM_ARGS_NAME} "${download_directory}" + "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-build" + ) + cpm_get_fetch_properties("${CPM_ARGS_NAME}") + + if(DEFINED CPM_ARGS_GIT_TAG AND NOT (PATCH_COMMAND IN_LIST CPM_ARGS_UNPARSED_ARGUMENTS)) + # warn if cache has been changed since checkout + cpm_check_git_working_dir_is_clean(${download_directory} ${CPM_ARGS_GIT_TAG} IS_CLEAN) + if(NOT ${IS_CLEAN}) + message( + WARNING "${CPM_INDENT} Cache for ${CPM_ARGS_NAME} (${download_directory}) is dirty" + ) + endif() + endif() + + cpm_add_subdirectory( + "${CPM_ARGS_NAME}" + "${DOWNLOAD_ONLY}" + "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" + "${${CPM_ARGS_NAME}_BINARY_DIR}" + "${CPM_ARGS_EXCLUDE_FROM_ALL}" + "${CPM_ARGS_SYSTEM}" + "${CPM_ARGS_OPTIONS}" + ) + set(PACKAGE_INFO "${PACKAGE_INFO} at ${download_directory}") + + # As the source dir is already cached/populated, we override the call to FetchContent. + set(CPM_SKIP_FETCH TRUE) + cpm_override_fetchcontent( + "${lower_case_name}" SOURCE_DIR "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" + BINARY_DIR "${${CPM_ARGS_NAME}_BINARY_DIR}" + ) + + else() + # Enable shallow clone when GIT_TAG is not a commit hash. Our guess may not be accurate, but + # it should guarantee no commit hash get mis-detected. + if(NOT DEFINED CPM_ARGS_GIT_SHALLOW) + cpm_is_git_tag_commit_hash("${CPM_ARGS_GIT_TAG}" IS_HASH) + if(NOT ${IS_HASH}) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_SHALLOW TRUE) + endif() + endif() + + # remove timestamps so CMake will re-download the dependency + file(REMOVE_RECURSE ${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild) + set(PACKAGE_INFO "${PACKAGE_INFO} to ${download_directory}") + endif() + endif() + + if(NOT "${DOWNLOAD_ONLY}") + cpm_create_module_file(${CPM_ARGS_NAME} "CPMAddPackage(\"${ARGN}\")") + endif() + + if(CPM_PACKAGE_LOCK_ENABLED) + if((CPM_ARGS_VERSION AND NOT CPM_ARGS_SOURCE_DIR) OR CPM_INCLUDE_ALL_IN_PACKAGE_LOCK) + cpm_add_to_package_lock(${CPM_ARGS_NAME} "${ARGN}") + elseif(CPM_ARGS_SOURCE_DIR) + cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "local directory") + else() + cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "${ARGN}") + endif() + endif() + + cpm_message( + STATUS "${CPM_INDENT} Adding package ${CPM_ARGS_NAME}@${CPM_ARGS_VERSION} (${PACKAGE_INFO})" + ) + + if(NOT CPM_SKIP_FETCH) + # CMake 3.28 added EXCLUDE, SYSTEM (3.25), and SOURCE_SUBDIR (3.18) to FetchContent_Declare. + # Calling FetchContent_MakeAvailable will then internally forward these options to + # add_subdirectory. Up until these changes, we had to call FetchContent_Populate and + # add_subdirectory separately, which is no longer necessary and has been deprecated as of 3.30. + # A Bug in CMake prevents us to use the non-deprecated functions until 3.30.3. + set(fetchContentDeclareExtraArgs "") + if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.30.3") + if(${CPM_ARGS_EXCLUDE_FROM_ALL}) + list(APPEND fetchContentDeclareExtraArgs EXCLUDE_FROM_ALL) + endif() + if(${CPM_ARGS_SYSTEM}) + list(APPEND fetchContentDeclareExtraArgs SYSTEM) + endif() + if(DEFINED CPM_ARGS_SOURCE_SUBDIR) + list(APPEND fetchContentDeclareExtraArgs SOURCE_SUBDIR ${CPM_ARGS_SOURCE_SUBDIR}) + endif() + # For CMake version <3.28 OPTIONS are parsed in cpm_add_subdirectory + if(CPM_ARGS_OPTIONS AND NOT DOWNLOAD_ONLY) + foreach(OPTION ${CPM_ARGS_OPTIONS}) + cpm_parse_option("${OPTION}") + set(${OPTION_KEY} "${OPTION_VALUE}") + endforeach() + endif() + endif() + cpm_declare_fetch( + "${CPM_ARGS_NAME}" ${fetchContentDeclareExtraArgs} "${CPM_ARGS_UNPARSED_ARGUMENTS}" + ) + + cpm_fetch_package("${CPM_ARGS_NAME}" ${DOWNLOAD_ONLY} populated ${CPM_ARGS_UNPARSED_ARGUMENTS}) + if(CPM_SOURCE_CACHE AND download_directory) + file(LOCK ${download_directory}/../cmake.lock RELEASE) + endif() + if(${populated} AND ${CMAKE_VERSION} VERSION_LESS "3.30.3") + cpm_add_subdirectory( + "${CPM_ARGS_NAME}" + "${DOWNLOAD_ONLY}" + "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" + "${${CPM_ARGS_NAME}_BINARY_DIR}" + "${CPM_ARGS_EXCLUDE_FROM_ALL}" + "${CPM_ARGS_SYSTEM}" + "${CPM_ARGS_OPTIONS}" + ) + endif() + cpm_get_fetch_properties("${CPM_ARGS_NAME}") + endif() + + set(${CPM_ARGS_NAME}_ADDED YES) + cpm_export_variables("${CPM_ARGS_NAME}") +endfunction() + +# Fetch a previously declared package +macro(CPMGetPackage Name) + if(DEFINED "CPM_DECLARATION_${Name}") + CPMAddPackage(NAME ${Name}) + else() + message(SEND_ERROR "${CPM_INDENT} Cannot retrieve package ${Name}: no declaration available") + endif() +endmacro() + +# export variables available to the caller to the parent scope expects ${CPM_ARGS_NAME} to be set +macro(cpm_export_variables name) + set(${name}_SOURCE_DIR + "${${name}_SOURCE_DIR}" + PARENT_SCOPE + ) + set(${name}_BINARY_DIR + "${${name}_BINARY_DIR}" + PARENT_SCOPE + ) + set(${name}_ADDED + "${${name}_ADDED}" + PARENT_SCOPE + ) + set(CPM_LAST_PACKAGE_NAME + "${name}" + PARENT_SCOPE + ) +endmacro() + +# declares a package, so that any call to CPMAddPackage for the package name will use these +# arguments instead. Previous declarations will not be overridden. +macro(CPMDeclarePackage Name) + if(NOT DEFINED "CPM_DECLARATION_${Name}") + set("CPM_DECLARATION_${Name}" "${ARGN}") + endif() +endmacro() + +function(cpm_add_to_package_lock Name) + if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + cpm_prettify_package_arguments(PRETTY_ARGN false ${ARGN}) + file(APPEND ${CPM_PACKAGE_LOCK_FILE} "# ${Name}\nCPMDeclarePackage(${Name}\n${PRETTY_ARGN})\n") + endif() +endfunction() + +function(cpm_add_comment_to_package_lock Name) + if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + cpm_prettify_package_arguments(PRETTY_ARGN true ${ARGN}) + file(APPEND ${CPM_PACKAGE_LOCK_FILE} + "# ${Name} (unversioned)\n# CPMDeclarePackage(${Name}\n${PRETTY_ARGN}#)\n" + ) + endif() +endfunction() + +# includes the package lock file if it exists and creates a target `cpm-update-package-lock` to +# update it +macro(CPMUsePackageLock file) + if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + get_filename_component(CPM_ABSOLUTE_PACKAGE_LOCK_PATH ${file} ABSOLUTE) + if(EXISTS ${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}) + include(${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}) + endif() + if(NOT TARGET cpm-update-package-lock) + add_custom_target( + cpm-update-package-lock COMMAND ${CMAKE_COMMAND} -E copy ${CPM_PACKAGE_LOCK_FILE} + ${CPM_ABSOLUTE_PACKAGE_LOCK_PATH} + ) + endif() + set(CPM_PACKAGE_LOCK_ENABLED true) + endif() +endmacro() + +# registers a package that has been added to CPM +function(CPMRegisterPackage PACKAGE VERSION) + list(APPEND CPM_PACKAGES ${PACKAGE}) + set(CPM_PACKAGES + ${CPM_PACKAGES} + CACHE INTERNAL "" + ) + set("CPM_PACKAGE_${PACKAGE}_VERSION" + ${VERSION} + CACHE INTERNAL "" + ) +endfunction() + +# retrieve the current version of the package to ${OUTPUT} +function(CPMGetPackageVersion PACKAGE OUTPUT) + set(${OUTPUT} + "${CPM_PACKAGE_${PACKAGE}_VERSION}" + PARENT_SCOPE + ) +endfunction() + +# declares a package in FetchContent_Declare +function(cpm_declare_fetch PACKAGE) + if(${CPM_DRY_RUN}) + cpm_message(STATUS "${CPM_INDENT} Package not declared (dry run)") + return() + endif() + + FetchContent_Declare(${PACKAGE} ${ARGN}) +endfunction() + +# returns properties for a package previously defined by cpm_declare_fetch +function(cpm_get_fetch_properties PACKAGE) + if(${CPM_DRY_RUN}) + return() + endif() + + set(${PACKAGE}_SOURCE_DIR + "${CPM_PACKAGE_${PACKAGE}_SOURCE_DIR}" + PARENT_SCOPE + ) + set(${PACKAGE}_BINARY_DIR + "${CPM_PACKAGE_${PACKAGE}_BINARY_DIR}" + PARENT_SCOPE + ) +endfunction() + +function(cpm_store_fetch_properties PACKAGE source_dir binary_dir) + if(${CPM_DRY_RUN}) + return() + endif() + + set(CPM_PACKAGE_${PACKAGE}_SOURCE_DIR + "${source_dir}" + CACHE INTERNAL "" + ) + set(CPM_PACKAGE_${PACKAGE}_BINARY_DIR + "${binary_dir}" + CACHE INTERNAL "" + ) +endfunction() + +# adds a package as a subdirectory if viable, according to provided options +function( + cpm_add_subdirectory + PACKAGE + DOWNLOAD_ONLY + SOURCE_DIR + BINARY_DIR + EXCLUDE + SYSTEM + OPTIONS +) + + if(NOT DOWNLOAD_ONLY AND EXISTS ${SOURCE_DIR}/CMakeLists.txt) + set(addSubdirectoryExtraArgs "") + if(EXCLUDE) + list(APPEND addSubdirectoryExtraArgs EXCLUDE_FROM_ALL) + endif() + if("${SYSTEM}" AND "${CMAKE_VERSION}" VERSION_GREATER_EQUAL "3.25") + # https://cmake.org/cmake/help/latest/prop_dir/SYSTEM.html#prop_dir:SYSTEM + list(APPEND addSubdirectoryExtraArgs SYSTEM) + endif() + if(OPTIONS) + foreach(OPTION ${OPTIONS}) + cpm_parse_option("${OPTION}") + set(${OPTION_KEY} "${OPTION_VALUE}") + endforeach() + endif() + set(CPM_OLD_INDENT "${CPM_INDENT}") + set(CPM_INDENT "${CPM_INDENT} ${PACKAGE}:") + add_subdirectory(${SOURCE_DIR} ${BINARY_DIR} ${addSubdirectoryExtraArgs}) + set(CPM_INDENT "${CPM_OLD_INDENT}") + endif() +endfunction() + +# downloads a previously declared package via FetchContent and exports the variables +# `${PACKAGE}_SOURCE_DIR` and `${PACKAGE}_BINARY_DIR` to the parent scope +function(cpm_fetch_package PACKAGE DOWNLOAD_ONLY populated) + set(${populated} + FALSE + PARENT_SCOPE + ) + if(${CPM_DRY_RUN}) + cpm_message(STATUS "${CPM_INDENT} Package ${PACKAGE} not fetched (dry run)") + return() + endif() + + FetchContent_GetProperties(${PACKAGE}) + + string(TOLOWER "${PACKAGE}" lower_case_name) + + if(NOT ${lower_case_name}_POPULATED) + if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.30.3") + if(DOWNLOAD_ONLY) + # MakeAvailable will call add_subdirectory internally which is not what we want when + # DOWNLOAD_ONLY is set. Populate will only download the dependency without adding it to the + # build + FetchContent_Populate( + ${PACKAGE} + SOURCE_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-src" + BINARY_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-build" + SUBBUILD_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild" + ${ARGN} + ) + else() + FetchContent_MakeAvailable(${PACKAGE}) + endif() + else() + FetchContent_Populate(${PACKAGE}) + endif() + set(${populated} + TRUE + PARENT_SCOPE + ) + endif() + + cpm_store_fetch_properties( + ${CPM_ARGS_NAME} ${${lower_case_name}_SOURCE_DIR} ${${lower_case_name}_BINARY_DIR} + ) + + set(${PACKAGE}_SOURCE_DIR + ${${lower_case_name}_SOURCE_DIR} + PARENT_SCOPE + ) + set(${PACKAGE}_BINARY_DIR + ${${lower_case_name}_BINARY_DIR} + PARENT_SCOPE + ) +endfunction() + +# splits a package option +function(cpm_parse_option OPTION) + string(REGEX MATCH "^[^ ]+" OPTION_KEY "${OPTION}") + string(LENGTH "${OPTION}" OPTION_LENGTH) + string(LENGTH "${OPTION_KEY}" OPTION_KEY_LENGTH) + if(OPTION_KEY_LENGTH STREQUAL OPTION_LENGTH) + # no value for key provided, assume user wants to set option to "ON" + set(OPTION_VALUE "ON") + else() + math(EXPR OPTION_KEY_LENGTH "${OPTION_KEY_LENGTH}+1") + string(SUBSTRING "${OPTION}" "${OPTION_KEY_LENGTH}" "-1" OPTION_VALUE) + string(STRIP "${OPTION_VALUE}" OPTION_VALUE) + endif() + set(OPTION_KEY + "${OPTION_KEY}" + PARENT_SCOPE + ) + set(OPTION_VALUE + "${OPTION_VALUE}" + PARENT_SCOPE + ) +endfunction() + +# guesses the package version from a git tag +function(cpm_get_version_from_git_tag GIT_TAG RESULT) + string(LENGTH ${GIT_TAG} length) + if(length EQUAL 40) + # GIT_TAG is probably a git hash + set(${RESULT} + 0 + PARENT_SCOPE + ) + else() + string(REGEX MATCH "v?([0123456789.]*).*" _ ${GIT_TAG}) + set(${RESULT} + ${CMAKE_MATCH_1} + PARENT_SCOPE + ) + endif() +endfunction() + +# guesses if the git tag is a commit hash or an actual tag or a branch name. +function(cpm_is_git_tag_commit_hash GIT_TAG RESULT) + string(LENGTH "${GIT_TAG}" length) + # full hash has 40 characters, and short hash has at least 7 characters. + if(length LESS 7 OR length GREATER 40) + set(${RESULT} + 0 + PARENT_SCOPE + ) + else() + if(${GIT_TAG} MATCHES "^[a-fA-F0-9]+$") + set(${RESULT} + 1 + PARENT_SCOPE + ) + else() + set(${RESULT} + 0 + PARENT_SCOPE + ) + endif() + endif() +endfunction() + +function(cpm_prettify_package_arguments OUT_VAR IS_IN_COMMENT) + set(oneValueArgs + NAME + FORCE + VERSION + GIT_TAG + DOWNLOAD_ONLY + GITHUB_REPOSITORY + GITLAB_REPOSITORY + BITBUCKET_REPOSITORY + GIT_REPOSITORY + SOURCE_DIR + FIND_PACKAGE_ARGUMENTS + NO_CACHE + SYSTEM + GIT_SHALLOW + EXCLUDE_FROM_ALL + SOURCE_SUBDIR + ) + set(multiValueArgs URL OPTIONS DOWNLOAD_COMMAND) + cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + foreach(oneArgName ${oneValueArgs}) + if(DEFINED CPM_ARGS_${oneArgName}) + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + if(${oneArgName} STREQUAL "SOURCE_DIR") + string(REPLACE ${CMAKE_SOURCE_DIR} "\${CMAKE_SOURCE_DIR}" CPM_ARGS_${oneArgName} + ${CPM_ARGS_${oneArgName}} + ) + endif() + string(APPEND PRETTY_OUT_VAR " ${oneArgName} ${CPM_ARGS_${oneArgName}}\n") + endif() + endforeach() + foreach(multiArgName ${multiValueArgs}) + if(DEFINED CPM_ARGS_${multiArgName}) + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + string(APPEND PRETTY_OUT_VAR " ${multiArgName}\n") + foreach(singleOption ${CPM_ARGS_${multiArgName}}) + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + string(APPEND PRETTY_OUT_VAR " \"${singleOption}\"\n") + endforeach() + endif() + endforeach() + + if(NOT "${CPM_ARGS_UNPARSED_ARGUMENTS}" STREQUAL "") + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + string(APPEND PRETTY_OUT_VAR " ") + foreach(CPM_ARGS_UNPARSED_ARGUMENT ${CPM_ARGS_UNPARSED_ARGUMENTS}) + string(APPEND PRETTY_OUT_VAR " ${CPM_ARGS_UNPARSED_ARGUMENT}") + endforeach() + string(APPEND PRETTY_OUT_VAR "\n") + endif() + + set(${OUT_VAR} + ${PRETTY_OUT_VAR} + PARENT_SCOPE + ) + +endfunction() diff --git a/cmake/dependencies/common.cmake b/cmake/dependencies/common.cmake index dc6886776..34c182cff 100644 --- a/cmake/dependencies/common.cmake +++ b/cmake/dependencies/common.cmake @@ -29,6 +29,7 @@ if(SUNSHINE_ENABLE_TRAY) endif() # common dependencies +include("${CMAKE_MODULE_PATH}/dependencies/nv_codec_headers.cmake") include("${CMAKE_MODULE_PATH}/dependencies/nlohmann_json.cmake") find_package(PkgConfig REQUIRED) find_package(Threads REQUIRED) diff --git a/cmake/dependencies/nv_codec_headers.cmake b/cmake/dependencies/nv_codec_headers.cmake new file mode 100644 index 000000000..b7910557f --- /dev/null +++ b/cmake/dependencies/nv_codec_headers.cmake @@ -0,0 +1,9 @@ +if(WIN32) + CPMGetPackage(nv_codec_headers_13) + CPMGetPackage(nv_codec_headers_11) + CPMGetPackage(nv_codec_headers_12) + + set(NV_CODEC_HEADERS_11_INCLUDE_DIR "${nv_codec_headers_11_SOURCE_DIR}/include") + set(NV_CODEC_HEADERS_12_INCLUDE_DIR "${nv_codec_headers_12_SOURCE_DIR}/include") + set(NV_CODEC_HEADERS_13_INCLUDE_DIR "${nv_codec_headers_13_SOURCE_DIR}/include") +endif() diff --git a/package-lock.cmake b/package-lock.cmake new file mode 100644 index 000000000..2542d73df --- /dev/null +++ b/package-lock.cmake @@ -0,0 +1,67 @@ +# CPM Package Lock +# This file should be committed to version control + +# The first argument of CPMDeclarePackage can be freely chosen and is used as argument in CPMGetPackage. +# The NAME argument should be package name that would also be used in a find_package call. +# Ideally, both are the same, which might not always be possible: https://github.com/cpm-cmake/CPM.cmake/issues/603 +# This is needed to support CPM_USE_LOCAL_PACKAGES + +# Renovate-bot will update the versions and hashes in this file when a new version of a dependency is released. +# The comments above each dependency are used by renovate to identify the dependencies and extract the version numbers. +# See https://github.com/LizardByte/.github/blob/master/renovate-config.json5 for the configuration of renovate. +# +# Expected dependency structure for new entries: +# - Start each block with a human-readable comment, for example `# Example dependency`. +# - Follow it with consecutive renovate metadata comments. +# - The first metadata line must start with `# renovate:` and include `datasource=` and `depName=`. +# - Optional metadata keys are `packageName=`, `versioning=`, `extractVersion=`, and `registryUrl=`. +# - Optional metadata may stay on the `# renovate:` line or continue on the next consecutive `#` lines. +# - Keep metadata keys in this order: `datasource`, `depName`, `packageName`, `versioning`, +# `extractVersion`, `registryUrl`. +# - After metadata, declare the tracked value with `set(NAME_VERSION ...)` or `set(NAME_TAG ...)`. +# - If the dependency also tracks a SHA256, keep `set(NAME_SHA256 ...)` immediately after the +# matching `NAME_VERSION` or `NAME_TAG` line with no unrelated lines between them. +# - Keep `CPMDeclarePackage(...)` below the tracked values. +# +# Example layout: +# - `# Example dependency` +# - `# renovate: datasource=github-tags depName=owner/repo` +# - `# versioning=regex:^v(?\d+)\.(?\d+)\.(?\d+)$` +# - `set(EXAMPLE_TAG v1.2.3)` +# - `set(EXAMPLE_SHA256 )` +# - `CPMDeclarePackage(...)` + +set(PATCH_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/patches") + +# NVENC SDK 11.0 headers +# renovate: datasource=github-tags depName=FFmpeg/nv-codec-headers +# versioning=regex:^n(?11)\.(?0)\.(?\d+)\.(?\d+)$ +set(NV_CODEC_HEADERS_11_TAG n11.0.10.3) +CPMDeclarePackage(nv_codec_headers_11 + NAME nv_codec_headers_11 + GIT_REPOSITORY https://github.com/FFmpeg/nv-codec-headers.git + GIT_TAG ${NV_CODEC_HEADERS_11_TAG} + DOWNLOAD_ONLY YES +) + +# NVENC SDK 12.0 headers +# renovate: datasource=github-tags depName=FFmpeg/nv-codec-headers +# versioning=regex:^n(?12)\.(?0)\.(?\d+)\.(?\d+)$ +set(NV_CODEC_HEADERS_12_TAG n12.0.16.2) +CPMDeclarePackage(nv_codec_headers_12 + NAME nv_codec_headers_12 + GIT_REPOSITORY https://github.com/FFmpeg/nv-codec-headers.git + GIT_TAG ${NV_CODEC_HEADERS_12_TAG} + DOWNLOAD_ONLY YES +) + +# NVENC SDK 13.0 headers +# renovate: datasource=github-tags depName=FFmpeg/nv-codec-headers +# versioning=regex:^n(?13)\.(?0)\.(?\d+)\.(?\d+)$ +set(NV_CODEC_HEADERS_13_TAG n13.0.19.1) +CPMDeclarePackage(nv_codec_headers_13 + NAME nv_codec_headers_13 + GIT_REPOSITORY https://github.com/FFmpeg/nv-codec-headers.git + GIT_TAG ${NV_CODEC_HEADERS_13_TAG} + DOWNLOAD_ONLY YES +) diff --git a/src/nvenc/nvenc_base.cpp b/src/nvenc/nvenc_base.cpp index f464573b1..69ab73ddb 100644 --- a/src/nvenc/nvenc_base.cpp +++ b/src/nvenc/nvenc_base.cpp @@ -6,30 +6,121 @@ #include "nvenc_base.h" // standard includes +#include +#include +#include +#include #include +#include +#include +#include // local includes +#include "nvenc_utils.h" #include "src/config.h" #include "src/logging.h" #include "src/utility.h" -/** - * @def MAKE_NVENC_VER(major, minor) - * @brief Macro for MAKE NVENC VER. - */ -#define MAKE_NVENC_VER(major, minor) ((major) | ((minor) << 24)) - -// Make sure we check backwards compatibility when bumping the Video Codec SDK version -// Things to look out for: -// - NV_ENC_*_VER definitions where the value inside NVENCAPI_STRUCT_VERSION() was increased -// - Incompatible struct changes in nvEncodeAPI.h (fields removed, semantics changed, etc.) -// - Test both old and new drivers with all supported codecs -#if NVENCAPI_VERSION != MAKE_NVENC_VER(13U, 0U) - #error Check and update NVENC code for backwards compatibility! -#endif - namespace { + using namespace NVENC_NAMESPACE; + + /** + * @brief Determine whether an NVENC buffer format stores 10-bit samples. + * + * @param buffer_format NVENC input buffer format. + * @return `true` for a 10-bit format, otherwise `false`. + */ + bool buffer_is_10bit(NV_ENC_BUFFER_FORMAT buffer_format) { + return buffer_format == NV_ENC_BUFFER_FORMAT_YUV420_10BIT || buffer_format == NV_ENC_BUFFER_FORMAT_YUV444_10BIT; + } + + /** + * @brief Determine whether an NVENC buffer format stores YUV 4:4:4 samples. + * + * @param buffer_format NVENC input buffer format. + * @return `true` for a YUV 4:4:4 format, otherwise `false`. + */ + bool buffer_is_yuv444(NV_ENC_BUFFER_FORMAT buffer_format) { + return buffer_format == NV_ENC_BUFFER_FORMAT_AYUV || + buffer_format == NV_ENC_BUFFER_FORMAT_YUV444 || + buffer_format == NV_ENC_BUFFER_FORMAT_YUV444_10BIT; + } + + /** + * @brief Determine whether a codec GUID appears in the driver-provided list. + * + * @param encode_guids Driver-provided codec GUIDs. + * @param encode_guid Codec GUID to locate. + * @return `true` when the codec is supported, otherwise `false`. + */ + bool contains_guid(const std::vector &encode_guids, const GUID &encode_guid) { + return std::ranges::any_of(encode_guids, [&](const GUID &guid) { + return std::memcmp(&encode_guid, &guid, sizeof(GUID)) == 0; + }); + } + + /** + * @brief Get the display name for a Sunshine video format. + * + * @param video_format Sunshine video format identifier. + * @return Display name including its trailing separator. + */ + std::string_view video_format_name(int video_format) { + switch (video_format) { + case 0: + return "H.264 "; + case 1: + return "HEVC "; + case 2: + return "AV1 "; + default: + return " "; + } + } + + /** + * @brief Convert an NVENC status value to its symbolic name. + * + * @param status NVENC status value. + * @return Symbolic name when known, otherwise its numeric value. + */ + std::string nvenc_status_string(NVENCSTATUS status) { + static constexpr auto names = std::to_array>({ + {NV_ENC_SUCCESS, "NV_ENC_SUCCESS"}, + {NV_ENC_ERR_NO_ENCODE_DEVICE, "NV_ENC_ERR_NO_ENCODE_DEVICE"}, + {NV_ENC_ERR_UNSUPPORTED_DEVICE, "NV_ENC_ERR_UNSUPPORTED_DEVICE"}, + {NV_ENC_ERR_INVALID_ENCODERDEVICE, "NV_ENC_ERR_INVALID_ENCODERDEVICE"}, + {NV_ENC_ERR_INVALID_DEVICE, "NV_ENC_ERR_INVALID_DEVICE"}, + {NV_ENC_ERR_DEVICE_NOT_EXIST, "NV_ENC_ERR_DEVICE_NOT_EXIST"}, + {NV_ENC_ERR_INVALID_PTR, "NV_ENC_ERR_INVALID_PTR"}, + {NV_ENC_ERR_INVALID_EVENT, "NV_ENC_ERR_INVALID_EVENT"}, + {NV_ENC_ERR_INVALID_PARAM, "NV_ENC_ERR_INVALID_PARAM"}, + {NV_ENC_ERR_INVALID_CALL, "NV_ENC_ERR_INVALID_CALL"}, + {NV_ENC_ERR_OUT_OF_MEMORY, "NV_ENC_ERR_OUT_OF_MEMORY"}, + {NV_ENC_ERR_ENCODER_NOT_INITIALIZED, "NV_ENC_ERR_ENCODER_NOT_INITIALIZED"}, + {NV_ENC_ERR_UNSUPPORTED_PARAM, "NV_ENC_ERR_UNSUPPORTED_PARAM"}, + {NV_ENC_ERR_LOCK_BUSY, "NV_ENC_ERR_LOCK_BUSY"}, + {NV_ENC_ERR_NOT_ENOUGH_BUFFER, "NV_ENC_ERR_NOT_ENOUGH_BUFFER"}, + {NV_ENC_ERR_INVALID_VERSION, "NV_ENC_ERR_INVALID_VERSION"}, + {NV_ENC_ERR_MAP_FAILED, "NV_ENC_ERR_MAP_FAILED"}, + {NV_ENC_ERR_NEED_MORE_INPUT, "NV_ENC_ERR_NEED_MORE_INPUT"}, + {NV_ENC_ERR_ENCODER_BUSY, "NV_ENC_ERR_ENCODER_BUSY"}, + {NV_ENC_ERR_EVENT_NOT_REGISTERD, "NV_ENC_ERR_EVENT_NOT_REGISTERD"}, + {NV_ENC_ERR_GENERIC, "NV_ENC_ERR_GENERIC"}, + {NV_ENC_ERR_INCOMPATIBLE_CLIENT_KEY, "NV_ENC_ERR_INCOMPATIBLE_CLIENT_KEY"}, + {NV_ENC_ERR_UNIMPLEMENTED, "NV_ENC_ERR_UNIMPLEMENTED"}, + {NV_ENC_ERR_RESOURCE_REGISTER_FAILED, "NV_ENC_ERR_RESOURCE_REGISTER_FAILED"}, + {NV_ENC_ERR_RESOURCE_NOT_REGISTERED, "NV_ENC_ERR_RESOURCE_NOT_REGISTERED"}, + {NV_ENC_ERR_RESOURCE_NOT_MAPPED, "NV_ENC_ERR_RESOURCE_NOT_MAPPED"}, + }); + + const auto item = std::ranges::find_if(names, [status](const auto &entry) { + return entry.first == status; + }); + return item == names.end() ? std::to_string(status) : std::string {item->second}; + } + GUID quality_preset_guid_from_number(unsigned number) { if (number > 7) { number = 7; @@ -91,7 +182,7 @@ namespace { } // namespace -namespace nvenc { +namespace NVENC_NAMESPACE { nvenc_base::nvenc_base(NV_ENC_DEVICE_TYPE device_type): device_type(device_type) { @@ -101,332 +192,305 @@ namespace nvenc { // Use destroy_encoder() instead } - bool nvenc_base::create_encoder(const nvenc_config &config, const video::config_t &client_config, const nvenc_colorspace_t &colorspace, NV_ENC_BUFFER_FORMAT buffer_format) { - if (!nvenc && !init_library()) { + int nvenc_base::get_encoder_cap(const GUID &encode_guid, NV_ENC_CAPS cap) const { + NV_ENC_CAPS_PARAM param = {NV_ENC_CAPS_PARAM_VER}; + param.capsToQuery = cap; + int value = 0; + if (nvenc->nvEncGetEncodeCaps(encoder, encode_guid, ¶m, &value) == NV_ENC_SUCCESS) { + return value; + } + return 0; + } + + bool nvenc_base::validate_encoder_capabilities(const GUID &encode_guid, NV_ENC_BUFFER_FORMAT buffer_format) { + const auto supported_width = get_encoder_cap(encode_guid, NV_ENC_CAPS_WIDTH_MAX); + const auto supported_height = get_encoder_cap(encode_guid, NV_ENC_CAPS_HEIGHT_MAX); + if (encoder_params.width > supported_width || encoder_params.height > supported_height) { + BOOST_LOG(error) << "NvEnc: gpu max encode resolution " << supported_width << "x" << supported_height + << ", requested " << encoder_params.width << "x" << encoder_params.height; return false; } - - if (encoder) { - destroy_encoder(); - } - auto fail_guard = util::fail_guard([this] { - destroy_encoder(); - }); - - encoder_params.width = client_config.width; - encoder_params.height = client_config.height; - encoder_params.buffer_format = buffer_format; - encoder_params.rfi = true; - - NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS session_params = {NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER}; - session_params.device = device; - session_params.deviceType = device_type; - session_params.apiVersion = NVENCAPI_VERSION; - if (nvenc_failed(nvenc->nvEncOpenEncodeSessionEx(&session_params, &encoder))) { - BOOST_LOG(error) << "NvEnc: NvEncOpenEncodeSessionEx() failed: " << last_nvenc_error_string; - return false; - } - - uint32_t encode_guid_count = 0; - if (nvenc_failed(nvenc->nvEncGetEncodeGUIDCount(encoder, &encode_guid_count))) { - BOOST_LOG(error) << "NvEnc: NvEncGetEncodeGUIDCount() failed: " << last_nvenc_error_string; - return false; - }; - - std::vector encode_guids(encode_guid_count); - if (nvenc_failed(nvenc->nvEncGetEncodeGUIDs(encoder, encode_guids.data(), (uint32_t) encode_guids.size(), &encode_guid_count))) { - BOOST_LOG(error) << "NvEnc: NvEncGetEncodeGUIDs() failed: " << last_nvenc_error_string; - return false; - } - - NV_ENC_INITIALIZE_PARAMS init_params = {NV_ENC_INITIALIZE_PARAMS_VER}; - - switch (client_config.videoFormat) { - case 0: - // H.264 - init_params.encodeGUID = NV_ENC_CODEC_H264_GUID; - break; - - case 1: - // HEVC - init_params.encodeGUID = NV_ENC_CODEC_HEVC_GUID; - break; - - case 2: - // AV1 - init_params.encodeGUID = NV_ENC_CODEC_AV1_GUID; - break; - - default: - BOOST_LOG(error) << "NvEnc: unknown video format " << client_config.videoFormat; - return false; - } - - { - auto search_predicate = [&](const GUID &guid) { - return equal_guids(init_params.encodeGUID, guid); - }; - if (std::find_if(encode_guids.begin(), encode_guids.end(), search_predicate) == encode_guids.end()) { - BOOST_LOG(error) << "NvEnc: encoding format is not supported by the gpu"; - return false; - } - } - - auto get_encoder_cap = [&](NV_ENC_CAPS cap) { - NV_ENC_CAPS_PARAM param = {NV_ENC_CAPS_PARAM_VER}; - param.capsToQuery = cap; - int value = 0; - if (int ret = nvenc->nvEncGetEncodeCaps(encoder, init_params.encodeGUID, ¶m, &value); ret == NV_ENC_SUCCESS) { - return value; - } - return 0; - }; - - auto buffer_is_10bit = [&]() { - return buffer_format == NV_ENC_BUFFER_FORMAT_YUV420_10BIT || buffer_format == NV_ENC_BUFFER_FORMAT_YUV444_10BIT; - }; - - auto buffer_is_yuv444 = [&]() { - return buffer_format == NV_ENC_BUFFER_FORMAT_AYUV || buffer_format == NV_ENC_BUFFER_FORMAT_YUV444 || buffer_format == NV_ENC_BUFFER_FORMAT_YUV444_10BIT; - }; - - { - auto supported_width = get_encoder_cap(NV_ENC_CAPS_WIDTH_MAX); - auto supported_height = get_encoder_cap(NV_ENC_CAPS_HEIGHT_MAX); - if (encoder_params.width > supported_width || encoder_params.height > supported_height) { - BOOST_LOG(error) << "NvEnc: gpu max encode resolution " << supported_width << "x" << supported_height << ", requested " << encoder_params.width << "x" << encoder_params.height; - return false; - } - } - - if (buffer_is_10bit() && !get_encoder_cap(NV_ENC_CAPS_SUPPORT_10BIT_ENCODE)) { + if (buffer_is_10bit(buffer_format) && !get_encoder_cap(encode_guid, NV_ENC_CAPS_SUPPORT_10BIT_ENCODE)) { BOOST_LOG(error) << "NvEnc: gpu doesn't support 10-bit encode"; return false; } - - if (buffer_is_yuv444() && !get_encoder_cap(NV_ENC_CAPS_SUPPORT_YUV444_ENCODE)) { + if (buffer_is_yuv444(buffer_format) && !get_encoder_cap(encode_guid, NV_ENC_CAPS_SUPPORT_YUV444_ENCODE)) { BOOST_LOG(error) << "NvEnc: gpu doesn't support YUV444 encode"; return false; } - - if (async_event_handle && !get_encoder_cap(NV_ENC_CAPS_ASYNC_ENCODE_SUPPORT)) { + if (async_event_handle && !get_encoder_cap(encode_guid, NV_ENC_CAPS_ASYNC_ENCODE_SUPPORT)) { BOOST_LOG(warning) << "NvEnc: gpu doesn't support async encode"; async_event_handle = nullptr; } + encoder_params.rfi = get_encoder_cap(encode_guid, NV_ENC_CAPS_SUPPORT_REF_PIC_INVALIDATION); + return true; + } - encoder_params.rfi = get_encoder_cap(NV_ENC_CAPS_SUPPORT_REF_PIC_INVALIDATION); - - init_params.presetGUID = quality_preset_guid_from_number(config.quality_preset); - init_params.tuningInfo = NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY; - init_params.enablePTD = 1; - init_params.enableEncodeAsync = async_event_handle ? 1 : 0; - init_params.enableWeightedPrediction = config.weighted_prediction && get_encoder_cap(NV_ENC_CAPS_SUPPORT_WEIGHTED_PREDICTION); - - init_params.encodeWidth = encoder_params.width; - init_params.darWidth = encoder_params.width; - init_params.encodeHeight = encoder_params.height; - init_params.darHeight = encoder_params.height; - const AVRational fps = video::framerate_to_rational(client_config); - init_params.frameRateNum = fps.num; - init_params.frameRateDen = fps.den; - - if (client_config.videoFormat > 0 && get_encoder_cap(NV_ENC_CAPS_NUM_ENCODER_ENGINES) > 1) { - // SFE supports HEVC/AV1 if you have more than 1 nvenc block - using enum nvenc_split_frame_encoding; - NV_ENC_SPLIT_ENCODE_MODE split_mode; - if (config.split_frame_encoding == disabled) { - split_mode = NV_ENC_SPLIT_DISABLE_MODE; - } else if (config.split_frame_encoding == force_enabled) { - split_mode = NV_ENC_SPLIT_AUTO_FORCED_MODE; - } else { - split_mode = NV_ENC_SPLIT_AUTO_MODE; - } - init_params.splitEncodeMode = split_mode; + void nvenc_base::configure_split_frame( + NV_ENC_INITIALIZE_PARAMS &init_params, + const ::nvenc::nvenc_config &config, + const video::config_t &client_config + ) const { +#if NVENC_SDK_VERSION >= 1300 + if (client_config.videoFormat <= 0 || get_encoder_cap(init_params.encodeGUID, NV_ENC_CAPS_NUM_ENCODER_ENGINES) <= 1) { + return; } - NV_ENC_PRESET_CONFIG preset_config = { - .version = NV_ENC_PRESET_CONFIG_VER, - .presetCfg = {.version = NV_ENC_CONFIG_VER}, - }; - if (nvenc_failed(nvenc->nvEncGetEncodePresetConfigEx(encoder, init_params.encodeGUID, init_params.presetGUID, init_params.tuningInfo, &preset_config))) { - BOOST_LOG(error) << "NvEnc: NvEncGetEncodePresetConfigEx() failed: " << last_nvenc_error_string; - return false; + using enum ::nvenc::nvenc_split_frame_encoding; + if (config.split_frame_encoding == disabled) { + init_params.splitEncodeMode = NV_ENC_SPLIT_DISABLE_MODE; + } else if (config.split_frame_encoding == force_enabled) { + init_params.splitEncodeMode = NV_ENC_SPLIT_AUTO_FORCED_MODE; + } else { + init_params.splitEncodeMode = NV_ENC_SPLIT_AUTO_MODE; } +#else + if (config.split_frame_encoding == ::nvenc::nvenc_split_frame_encoding::force_enabled) { + BOOST_LOG(warning) << "NvEnc: split-frame encoding requires NVENC API 13.0; ignoring forced mode"; + } +#endif + } - NV_ENC_CONFIG enc_config = preset_config.presetCfg; - enc_config.profileGUID = NV_ENC_CODEC_PROFILE_AUTOSELECT_GUID; + void nvenc_base::configure_rate_control( + NV_ENC_CONFIG &enc_config, + const ::nvenc::nvenc_config &config, + const video::config_t &client_config, + const GUID &encode_guid + ) { enc_config.gopLength = NVENC_INFINITE_GOPLENGTH; enc_config.frameIntervalP = 1; enc_config.rcParams.rateControlMode = NV_ENC_PARAMS_RC_CBR; enc_config.rcParams.zeroReorderDelay = 1; enc_config.rcParams.enableLookahead = 0; enc_config.rcParams.lowDelayKeyFrameScale = 1; - enc_config.rcParams.multiPass = config.two_pass == nvenc_two_pass::quarter_resolution ? NV_ENC_TWO_PASS_QUARTER_RESOLUTION : - config.two_pass == nvenc_two_pass::full_resolution ? NV_ENC_TWO_PASS_FULL_RESOLUTION : - NV_ENC_MULTI_PASS_DISABLED; + + using enum ::nvenc::nvenc_two_pass; + if (config.two_pass == quarter_resolution) { + enc_config.rcParams.multiPass = NV_ENC_TWO_PASS_QUARTER_RESOLUTION; + } else if (config.two_pass == full_resolution) { + enc_config.rcParams.multiPass = NV_ENC_TWO_PASS_FULL_RESOLUTION; + } else { + enc_config.rcParams.multiPass = NV_ENC_MULTI_PASS_DISABLED; + } enc_config.rcParams.enableAQ = config.adaptive_quantization; enc_config.rcParams.averageBitRate = client_config.bitrate * 1000; - - if (get_encoder_cap(NV_ENC_CAPS_SUPPORT_CUSTOM_VBV_BUF_SIZE)) { + if (get_encoder_cap(encode_guid, NV_ENC_CAPS_SUPPORT_CUSTOM_VBV_BUF_SIZE)) { enc_config.rcParams.vbvBufferSize = client_config.bitrate * 1000 / client_config.framerate; if (config.vbv_percentage_increase > 0) { enc_config.rcParams.vbvBufferSize += enc_config.rcParams.vbvBufferSize * config.vbv_percentage_increase / 100; } } + } - auto set_h264_hevc_common_format_config = [&](auto &format_config) { - format_config.repeatSPSPPS = 1; - format_config.idrPeriod = NVENC_INFINITE_GOPLENGTH; - format_config.sliceMode = 3; - format_config.sliceModeData = client_config.slicesPerFrame; - if (buffer_is_yuv444()) { - format_config.chromaFormatIDC = 3; - } - format_config.enableFillerDataInsertion = config.insert_filler_data; + void nvenc_base::configure_reference_frames( + std::uint32_t &ref_frames_option, + NV_ENC_NUM_REF_FRAMES &list0_option, + std::uint32_t default_count, + int requested_count, + const GUID &encode_guid + ) { + ref_frames_option = requested_count > 0 ? static_cast(requested_count) : default_count; + if (ref_frames_option > 0U && !get_encoder_cap(encode_guid, NV_ENC_CAPS_SUPPORT_MULTIPLE_REF_FRAMES)) { + ref_frames_option = 1; + encoder_params.rfi = false; + } + encoder_params.ref_frames_in_dpb = ref_frames_option; + // Limit each frame to one reference while keeping a larger DPB for RFI fallback. + list0_option = NV_ENC_NUM_REF_FRAMES_1; + } + + template + void nvenc_base::configure_h264_hevc_metadata( + FormatConfig &format_config, + const video::config_t &client_config, + const nvenc_colorspace_t &colorspace, + NV_ENC_BUFFER_FORMAT buffer_format, + const GUID &encode_guid + ) { + const auto configure_vui = [&](auto &vui) { + vui.videoSignalTypePresentFlag = 1; + vui.videoFormat = NV_ENC_VUI_VIDEO_FORMAT_UNSPECIFIED; + vui.videoFullRangeFlag = colorspace.full_range; + vui.colourDescriptionPresentFlag = 1; + vui.colourPrimaries = colorspace.primaries; + vui.transferCharacteristics = colorspace.tranfer_function; + vui.colourMatrix = colorspace.matrix; + vui.chromaSampleLocationFlag = buffer_is_yuv444(buffer_format) ? 0 : 1; + vui.chromaSampleLocationTop = 0; + vui.chromaSampleLocationBot = 0; + vui.bitstreamRestrictionFlag = 1; }; - auto set_ref_frames = [&](uint32_t &ref_frames_option, NV_ENC_NUM_REF_FRAMES &L0_option, uint32_t ref_frames_default) { - if (client_config.numRefFrames > 0) { - ref_frames_option = client_config.numRefFrames; - } else { - ref_frames_option = ref_frames_default; - } - if (ref_frames_option > 0 && !get_encoder_cap(NV_ENC_CAPS_SUPPORT_MULTIPLE_REF_FRAMES)) { - ref_frames_option = 1; - encoder_params.rfi = false; - } - encoder_params.ref_frames_in_dpb = ref_frames_option; - // This limits ref frames any frame can use to 1, but allows larger buffer size for fallback if some frames are invalidated through rfi - L0_option = NV_ENC_NUM_REF_FRAMES_1; - }; - - auto set_minqp_if_enabled = [&](int value) { - if (config.enable_min_qp) { - enc_config.rcParams.enableMinQP = 1; - enc_config.rcParams.minQP.qpInterP = value; - enc_config.rcParams.minQP.qpIntra = value; - } - }; - - auto fill_h264_hevc_vui = [&](auto &vui_config) { - vui_config.videoSignalTypePresentFlag = 1; - vui_config.videoFormat = NV_ENC_VUI_VIDEO_FORMAT_UNSPECIFIED; - vui_config.videoFullRangeFlag = colorspace.full_range; - vui_config.colourDescriptionPresentFlag = 1; - vui_config.colourPrimaries = colorspace.primaries; - vui_config.transferCharacteristics = colorspace.tranfer_function; - vui_config.colourMatrix = colorspace.matrix; - vui_config.chromaSampleLocationFlag = buffer_is_yuv444() ? 0 : 1; - vui_config.chromaSampleLocationTop = 0; - vui_config.chromaSampleLocationBot = 0; - - // This is critical for low decoding latency on certain devices - vui_config.bitstreamRestrictionFlag = 1; - }; - - switch (client_config.videoFormat) { - case 0: - { - // H.264 - enc_config.profileGUID = buffer_is_yuv444() ? NV_ENC_H264_PROFILE_HIGH_444_GUID : NV_ENC_H264_PROFILE_HIGH_GUID; - auto &format_config = enc_config.encodeCodecConfig.h264Config; - set_h264_hevc_common_format_config(format_config); - if (config.h264_cavlc || !get_encoder_cap(NV_ENC_CAPS_SUPPORT_CABAC)) { - format_config.entropyCodingMode = NV_ENC_H264_ENTROPY_CODING_MODE_CAVLC; - } else { - format_config.entropyCodingMode = NV_ENC_H264_ENTROPY_CODING_MODE_CABAC; - } - set_ref_frames(format_config.maxNumRefFrames, format_config.numRefL0, 5); - set_minqp_if_enabled(config.min_qp_h264); - fill_h264_hevc_vui(format_config.h264VUIParameters); - if (client_config.enableIntraRefresh == 1) { - if (get_encoder_cap(NV_ENC_CAPS_SUPPORT_INTRA_REFRESH)) { - format_config.enableIntraRefresh = 1; - format_config.intraRefreshPeriod = 300; - format_config.intraRefreshCnt = 299; - format_config.outputRecoveryPointSEI = 1; - if (get_encoder_cap(NV_ENC_CAPS_SINGLE_SLICE_INTRA_REFRESH)) { - format_config.singleSliceIntraRefresh = 1; - } else { - BOOST_LOG(warning) << "NvEnc: Single Slice Intra Refresh not supported"; - } - } else { - BOOST_LOG(error) << "NvEnc: Client asked for intra-refresh but the encoder does not support intra-refresh"; - } - } - break; - } - - case 1: - { - // HEVC - auto &format_config = enc_config.encodeCodecConfig.hevcConfig; - set_h264_hevc_common_format_config(format_config); - if (buffer_is_10bit()) { - format_config.inputBitDepth = NV_ENC_BIT_DEPTH_10; - format_config.outputBitDepth = NV_ENC_BIT_DEPTH_10; - } - set_ref_frames(format_config.maxNumRefFramesInDPB, format_config.numRefL0, 5); - set_minqp_if_enabled(config.min_qp_hevc); - fill_h264_hevc_vui(format_config.hevcVUIParameters); - if (client_config.enableIntraRefresh == 1) { - if (get_encoder_cap(NV_ENC_CAPS_SUPPORT_INTRA_REFRESH)) { - format_config.enableIntraRefresh = 1; - format_config.intraRefreshPeriod = 300; - format_config.intraRefreshCnt = 299; - format_config.outputRecoveryPointSEI = 1; - if (get_encoder_cap(NV_ENC_CAPS_SINGLE_SLICE_INTRA_REFRESH)) { - format_config.singleSliceIntraRefresh = 1; - } else { - BOOST_LOG(warning) << "NvEnc: Single Slice Intra Refresh not supported"; - } - } else { - BOOST_LOG(error) << "NvEnc: Client asked for intra-refresh but the encoder does not support intra-refresh"; - } - } - break; - } - - case 2: - { - // AV1 - auto &format_config = enc_config.encodeCodecConfig.av1Config; - format_config.repeatSeqHdr = 1; - format_config.idrPeriod = NVENC_INFINITE_GOPLENGTH; - if (buffer_is_yuv444()) { - format_config.chromaFormatIDC = 3; - } - format_config.enableBitstreamPadding = config.insert_filler_data; - if (buffer_is_10bit()) { - format_config.inputBitDepth = NV_ENC_BIT_DEPTH_10; - format_config.outputBitDepth = NV_ENC_BIT_DEPTH_10; - } - format_config.colorPrimaries = colorspace.primaries; - format_config.transferCharacteristics = colorspace.tranfer_function; - format_config.matrixCoefficients = colorspace.matrix; - format_config.colorRange = colorspace.full_range; - format_config.chromaSamplePosition = buffer_is_yuv444() ? 0 : 1; - set_ref_frames(format_config.maxNumRefFramesInDPB, format_config.numFwdRefs, 8); - set_minqp_if_enabled(config.min_qp_av1); - - if (client_config.slicesPerFrame > 1) { - // NVENC only supports slice counts that are powers of two, so we'll pick powers of two - // with bias to rows due to hopefully more similar macroblocks with a row vs a column. - format_config.numTileRows = std::pow(2, std::ceil(std::log2(client_config.slicesPerFrame) / 2)); - format_config.numTileColumns = std::pow(2, std::floor(std::log2(client_config.slicesPerFrame) / 2)); - } - break; - } + if constexpr (requires { format_config.h264VUIParameters; }) { + configure_vui(format_config.h264VUIParameters); + } else { + configure_vui(format_config.hevcVUIParameters); } - init_params.encodeConfig = &enc_config; + if (client_config.enableIntraRefresh != 1) { + return; + } + if (!get_encoder_cap(encode_guid, NV_ENC_CAPS_SUPPORT_INTRA_REFRESH)) { + BOOST_LOG(error) << "NvEnc: Client asked for intra-refresh but the encoder does not support intra-refresh"; + return; + } + format_config.enableIntraRefresh = 1; + format_config.intraRefreshPeriod = 300; + format_config.intraRefreshCnt = 299; + if constexpr (requires { format_config.outputRecoveryPointSEI; }) { + format_config.outputRecoveryPointSEI = 1; + } +#if NVENC_SDK_VERSION >= 1200 + if (get_encoder_cap(encode_guid, NV_ENC_CAPS_SINGLE_SLICE_INTRA_REFRESH)) { + format_config.singleSliceIntraRefresh = 1; + } else { + BOOST_LOG(warning) << "NvEnc: Single Slice Intra Refresh not supported"; + } +#endif + } + void nvenc_base::configure_h264( + NV_ENC_CONFIG &enc_config, + const ::nvenc::nvenc_config &config, + const video::config_t &client_config, + const nvenc_colorspace_t &colorspace, + NV_ENC_BUFFER_FORMAT buffer_format, + const GUID &encode_guid + ) { + enc_config.profileGUID = buffer_is_yuv444(buffer_format) ? NV_ENC_H264_PROFILE_HIGH_444_GUID : NV_ENC_H264_PROFILE_HIGH_GUID; + auto &format_config = enc_config.encodeCodecConfig.h264Config; + format_config.repeatSPSPPS = 1; + format_config.idrPeriod = NVENC_INFINITE_GOPLENGTH; + format_config.sliceMode = 3; + format_config.sliceModeData = client_config.slicesPerFrame; + if (buffer_is_yuv444(buffer_format)) { + format_config.chromaFormatIDC = 3; + } + format_config.enableFillerDataInsertion = config.insert_filler_data; + format_config.entropyCodingMode = config.h264_cavlc || !get_encoder_cap(encode_guid, NV_ENC_CAPS_SUPPORT_CABAC) ? + NV_ENC_H264_ENTROPY_CODING_MODE_CAVLC : + NV_ENC_H264_ENTROPY_CODING_MODE_CABAC; + configure_reference_frames(format_config.maxNumRefFrames, format_config.numRefL0, 5, client_config.numRefFrames, encode_guid); + + if (config.enable_min_qp) { + enc_config.rcParams.enableMinQP = 1; + enc_config.rcParams.minQP.qpInterP = config.min_qp_h264; + enc_config.rcParams.minQP.qpIntra = config.min_qp_h264; + } + + configure_h264_hevc_metadata(format_config, client_config, colorspace, buffer_format, encode_guid); + } + + void nvenc_base::configure_hevc( + NV_ENC_CONFIG &enc_config, + const ::nvenc::nvenc_config &config, + const video::config_t &client_config, + const nvenc_colorspace_t &colorspace, + NV_ENC_BUFFER_FORMAT buffer_format, + const GUID &encode_guid + ) { + auto &format_config = enc_config.encodeCodecConfig.hevcConfig; + format_config.repeatSPSPPS = 1; + format_config.idrPeriod = NVENC_INFINITE_GOPLENGTH; + format_config.sliceMode = 3; + format_config.sliceModeData = client_config.slicesPerFrame; + if (buffer_is_yuv444(buffer_format)) { + format_config.chromaFormatIDC = 3; + } + format_config.enableFillerDataInsertion = config.insert_filler_data; + if (buffer_is_10bit(buffer_format)) { +#if NVENC_SDK_VERSION >= 1300 + format_config.inputBitDepth = NV_ENC_BIT_DEPTH_10; + format_config.outputBitDepth = NV_ENC_BIT_DEPTH_10; +#else + format_config.pixelBitDepthMinus8 = 2; +#endif + } + configure_reference_frames(format_config.maxNumRefFramesInDPB, format_config.numRefL0, 5, client_config.numRefFrames, encode_guid); + + if (config.enable_min_qp) { + enc_config.rcParams.enableMinQP = 1; + enc_config.rcParams.minQP.qpInterP = config.min_qp_hevc; + enc_config.rcParams.minQP.qpIntra = config.min_qp_hevc; + } + + configure_h264_hevc_metadata(format_config, client_config, colorspace, buffer_format, encode_guid); + } + +#if NVENC_SDK_VERSION >= 1200 + void nvenc_base::configure_av1( + NV_ENC_CONFIG &enc_config, + const ::nvenc::nvenc_config &config, + const video::config_t &client_config, + const nvenc_colorspace_t &colorspace, + NV_ENC_BUFFER_FORMAT buffer_format, + const GUID &encode_guid + ) { + auto &format_config = enc_config.encodeCodecConfig.av1Config; + format_config.repeatSeqHdr = 1; + format_config.idrPeriod = NVENC_INFINITE_GOPLENGTH; + if (buffer_is_yuv444(buffer_format)) { + format_config.chromaFormatIDC = 3; + } + format_config.enableBitstreamPadding = config.insert_filler_data; + if (buffer_is_10bit(buffer_format)) { + #if NVENC_SDK_VERSION >= 1300 + format_config.inputBitDepth = NV_ENC_BIT_DEPTH_10; + format_config.outputBitDepth = NV_ENC_BIT_DEPTH_10; + #else + format_config.inputPixelBitDepthMinus8 = 2; + format_config.pixelBitDepthMinus8 = 2; + #endif + } + format_config.colorPrimaries = colorspace.primaries; + format_config.transferCharacteristics = colorspace.tranfer_function; + format_config.matrixCoefficients = colorspace.matrix; + format_config.colorRange = colorspace.full_range; + format_config.chromaSamplePosition = buffer_is_yuv444(buffer_format) ? 0 : 1; + configure_reference_frames(format_config.maxNumRefFramesInDPB, format_config.numFwdRefs, 8, client_config.numRefFrames, encode_guid); + + if (config.enable_min_qp) { + enc_config.rcParams.enableMinQP = 1; + enc_config.rcParams.minQP.qpInterP = config.min_qp_av1; + enc_config.rcParams.minQP.qpIntra = config.min_qp_av1; + } + if (client_config.slicesPerFrame > 1) { + // NVENC supports power-of-two tile counts, biased toward rows. + format_config.numTileRows = std::pow(2, std::ceil(std::log2(client_config.slicesPerFrame) / 2)); + format_config.numTileColumns = std::pow(2, std::floor(std::log2(client_config.slicesPerFrame) / 2)); + } + } +#endif + + void nvenc_base::configure_codec( + NV_ENC_CONFIG &enc_config, + const ::nvenc::nvenc_config &config, + const video::config_t &client_config, + const nvenc_colorspace_t &colorspace, + NV_ENC_BUFFER_FORMAT buffer_format, + const GUID &encode_guid + ) { + switch (client_config.videoFormat) { + case 0: + configure_h264(enc_config, config, client_config, colorspace, buffer_format, encode_guid); + break; + case 1: + configure_hevc(enc_config, config, client_config, colorspace, buffer_format, encode_guid); + break; +#if NVENC_SDK_VERSION >= 1200 + case 2: + configure_av1(enc_config, config, client_config, colorspace, buffer_format, encode_guid); + break; +#endif + } + } + + bool nvenc_base::initialize_encoder_resources(NV_ENC_INITIALIZE_PARAMS &init_params) { if (nvenc_failed(nvenc->nvEncInitializeEncoder(encoder, &init_params))) { BOOST_LOG(error) << "NvEnc: NvEncInitializeEncoder() failed: " << last_nvenc_error_string; return false; } - if (async_event_handle) { NV_ENC_EVENT_PARAMS event_params = {NV_ENC_EVENT_PARAMS_VER}; event_params.completionEvent = async_event_handle; @@ -442,62 +506,182 @@ namespace nvenc { return false; } output_bitstream = create_bitstream_buffer.bitstreamBuffer; + return create_and_register_input_buffer(); + } - if (!create_and_register_input_buffer()) { + void nvenc_base::log_created_encoder( + const NV_ENC_INITIALIZE_PARAMS &init_params, + const NV_ENC_CONFIG &enc_config, + const ::nvenc::nvenc_config &config, + const video::config_t &client_config, + NV_ENC_BUFFER_FORMAT buffer_format + ) const { + std::string extra; + if (init_params.enableEncodeAsync) { + extra += " async"; + } + if (buffer_is_yuv444(buffer_format)) { + extra += " yuv444"; + } + if (buffer_is_10bit(buffer_format)) { + extra += " 10-bit"; + } + if (enc_config.rcParams.multiPass != NV_ENC_MULTI_PASS_DISABLED) { + extra += " two-pass"; + } + if (config.vbv_percentage_increase > 0 && get_encoder_cap(init_params.encodeGUID, NV_ENC_CAPS_SUPPORT_CUSTOM_VBV_BUF_SIZE)) { + extra += std::format(" vbv+{}", config.vbv_percentage_increase); + } + if (encoder_params.rfi) { + extra += " rfi"; + } + if (init_params.enableWeightedPrediction) { + extra += " weighted-prediction"; + } + if (enc_config.rcParams.enableAQ) { + extra += " spatial-aq"; + } + if (enc_config.rcParams.enableMinQP) { + extra += std::format(" qpmin={}", enc_config.rcParams.minQP.qpInterP); + } + if (config.insert_filler_data) { + extra += " filler-data"; + } +#if NVENC_SDK_VERSION >= 1300 + if (client_config.videoFormat > 0 && get_encoder_cap(init_params.encodeGUID, NV_ENC_CAPS_NUM_ENCODER_ENGINES) > 1) { + if (init_params.splitEncodeMode == NV_ENC_SPLIT_AUTO_MODE) { + extra += " sfe-auto"; + } else if (init_params.splitEncodeMode == NV_ENC_SPLIT_AUTO_FORCED_MODE) { + extra += " sfe"; + } + } +#endif + + BOOST_LOG(info) << "NvEnc: created encoder v" << NVENC_SDK_VERSION << " " + << video_format_name(client_config.videoFormat) + << quality_preset_string_from_guid(init_params.presetGUID) << extra; + } + + bool nvenc_base::create_encoder( + const ::nvenc::nvenc_config &config, + const video::config_t &client_config, + const video::sunshine_colorspace_t &sunshine_colorspace, + platf::pix_fmt_e sunshine_buffer_format + ) { + if (!nvenc && !init_library()) { + return false; + } + if (encoder) { + destroy_encoder(); + } + auto fail_guard = util::fail_guard([this] { + destroy_encoder(); + }); + + const auto colorspace = nvenc_colorspace_from_sunshine_colorspace(sunshine_colorspace); + const auto buffer_format = nvenc_format_from_sunshine_format(sunshine_buffer_format); + if (buffer_format == NV_ENC_BUFFER_FORMAT_UNDEFINED) { + BOOST_LOG(error) << "NvEnc: unsupported input pixel format"; + return false; + } + encoder_params.width = client_config.width; + encoder_params.height = client_config.height; + encoder_params.buffer_format = buffer_format; + encoder_params.rfi = true; + + NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS session_params = {NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER}; + session_params.device = device; + session_params.deviceType = device_type; + session_params.apiVersion = NVENCAPI_VERSION; + if (nvenc_failed(nvenc->nvEncOpenEncodeSessionEx(&session_params, &encoder))) { + BOOST_LOG(error) << "NvEnc: NvEncOpenEncodeSessionEx() failed: " << last_nvenc_error_string; return false; } - { - auto f = stat_trackers::two_digits_after_decimal(); - BOOST_LOG(debug) << "NvEnc: requested encoded frame size " << f % (client_config.bitrate / 8. / client_config.framerate) << " kB"; + std::uint32_t encode_guid_count = 0; + if (nvenc_failed(nvenc->nvEncGetEncodeGUIDCount(encoder, &encode_guid_count))) { + BOOST_LOG(error) << "NvEnc: NvEncGetEncodeGUIDCount() failed: " << last_nvenc_error_string; + return false; + } + std::vector encode_guids(encode_guid_count); + if (nvenc_failed(nvenc->nvEncGetEncodeGUIDs( + encoder, + encode_guids.data(), + static_cast(encode_guids.size()), + &encode_guid_count + ))) { + BOOST_LOG(error) << "NvEnc: NvEncGetEncodeGUIDs() failed: " << last_nvenc_error_string; + return false; } - { - auto video_format_string = client_config.videoFormat == 0 ? "H.264 " : - client_config.videoFormat == 1 ? "HEVC " : - client_config.videoFormat == 2 ? "AV1 " : - " "; - std::string extra; - if (init_params.enableEncodeAsync) { - extra += " async"; - } - if (buffer_is_yuv444()) { - extra += " yuv444"; - } - if (buffer_is_10bit()) { - extra += " 10-bit"; - } - if (enc_config.rcParams.multiPass != NV_ENC_MULTI_PASS_DISABLED) { - extra += " two-pass"; - } - if (config.vbv_percentage_increase > 0 && get_encoder_cap(NV_ENC_CAPS_SUPPORT_CUSTOM_VBV_BUF_SIZE)) { - extra += std::format(" vbv+{}", config.vbv_percentage_increase); - } - if (encoder_params.rfi) { - extra += " rfi"; - } - if (init_params.enableWeightedPrediction) { - extra += " weighted-prediction"; - } - if (enc_config.rcParams.enableAQ) { - extra += " spatial-aq"; - } - if (enc_config.rcParams.enableMinQP) { - extra += std::format(" qpmin={}", enc_config.rcParams.minQP.qpInterP); - } - if (config.insert_filler_data) { - extra += " filler-data"; - } - if (client_config.videoFormat > 0 && get_encoder_cap(NV_ENC_CAPS_NUM_ENCODER_ENGINES) > 1) { - if (init_params.splitEncodeMode == NV_ENC_SPLIT_AUTO_MODE) { - extra += " sfe-auto"; - } else if (init_params.splitEncodeMode == NV_ENC_SPLIT_AUTO_FORCED_MODE) { - extra += " sfe"; - } - } - - BOOST_LOG(info) << "NvEnc: created encoder " << video_format_string << quality_preset_string_from_guid(init_params.presetGUID) << extra; + NV_ENC_INITIALIZE_PARAMS init_params = {NV_ENC_INITIALIZE_PARAMS_VER}; + switch (client_config.videoFormat) { + case 0: + init_params.encodeGUID = NV_ENC_CODEC_H264_GUID; + break; + case 1: + init_params.encodeGUID = NV_ENC_CODEC_HEVC_GUID; + break; +#if NVENC_SDK_VERSION >= 1200 + case 2: + init_params.encodeGUID = NV_ENC_CODEC_AV1_GUID; + break; +#endif + default: + BOOST_LOG(error) << "NvEnc: unknown video format " << client_config.videoFormat; + return false; } + if (!contains_guid(encode_guids, init_params.encodeGUID)) { + BOOST_LOG(error) << "NvEnc: encoding format is not supported by the gpu"; + return false; + } + if (!validate_encoder_capabilities(init_params.encodeGUID, buffer_format)) { + return false; + } + + init_params.presetGUID = quality_preset_guid_from_number(config.quality_preset); + init_params.tuningInfo = NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY; + init_params.enablePTD = 1; + init_params.enableEncodeAsync = async_event_handle ? 1 : 0; + init_params.enableWeightedPrediction = config.weighted_prediction && + get_encoder_cap(init_params.encodeGUID, NV_ENC_CAPS_SUPPORT_WEIGHTED_PREDICTION); + init_params.encodeWidth = encoder_params.width; + init_params.darWidth = encoder_params.width; + init_params.encodeHeight = encoder_params.height; + init_params.darHeight = encoder_params.height; + const AVRational fps = video::framerate_to_rational(client_config); + init_params.frameRateNum = fps.num; + init_params.frameRateDen = fps.den; + configure_split_frame(init_params, config, client_config); + + NV_ENC_PRESET_CONFIG preset_config = { + .version = NV_ENC_PRESET_CONFIG_VER, + .presetCfg = {.version = NV_ENC_CONFIG_VER}, + }; + if (nvenc_failed(nvenc->nvEncGetEncodePresetConfigEx( + encoder, + init_params.encodeGUID, + init_params.presetGUID, + init_params.tuningInfo, + &preset_config + ))) { + BOOST_LOG(error) << "NvEnc: NvEncGetEncodePresetConfigEx() failed: " << last_nvenc_error_string; + return false; + } + + NV_ENC_CONFIG enc_config = preset_config.presetCfg; + enc_config.profileGUID = NV_ENC_CODEC_PROFILE_AUTOSELECT_GUID; + configure_rate_control(enc_config, config, client_config, init_params.encodeGUID); + configure_codec(enc_config, config, client_config, colorspace, buffer_format, init_params.encodeGUID); + init_params.encodeConfig = &enc_config; + if (!initialize_encoder_resources(init_params)) { + return false; + } + + auto frame_size_format = stat_trackers::two_digits_after_decimal(); + BOOST_LOG(debug) << "NvEnc: requested encoded frame size " + << frame_size_format % (client_config.bitrate / 8. / client_config.framerate) << " kB"; + log_created_encoder(init_params, enc_config, config, client_config, buffer_format); encoder_state = {}; fail_guard.disable(); @@ -535,7 +719,7 @@ namespace nvenc { encoder_params = {}; } - nvenc_encoded_frame nvenc_base::encode_frame(uint64_t frame_index, bool force_idr) { + ::nvenc::nvenc_encoded_frame nvenc_base::encode_frame(uint64_t frame_index, bool force_idr) { if (!encoder) { return {}; } @@ -592,7 +776,7 @@ namespace nvenc { } auto data_pointer = (uint8_t *) lock_bitstream.bitstreamBufferPtr; - nvenc_encoded_frame encoded_frame { + ::nvenc::nvenc_encoded_frame encoded_frame { {data_pointer, data_pointer + lock_bitstream.bitstreamSizeInBytes}, lock_bitstream.outputTimeStamp, lock_bitstream.pictureType == NV_ENC_PIC_TYPE_IDR, @@ -657,46 +841,6 @@ namespace nvenc { } bool nvenc_base::nvenc_failed(NVENCSTATUS status) { - auto status_string = [](NVENCSTATUS status) -> std::string { - switch (status) { -#ifndef DOXYGEN - #define nvenc_status_case(x) \ - case x: \ - return #x; -#endif - nvenc_status_case(NV_ENC_SUCCESS); - nvenc_status_case(NV_ENC_ERR_NO_ENCODE_DEVICE); - nvenc_status_case(NV_ENC_ERR_UNSUPPORTED_DEVICE); - nvenc_status_case(NV_ENC_ERR_INVALID_ENCODERDEVICE); - nvenc_status_case(NV_ENC_ERR_INVALID_DEVICE); - nvenc_status_case(NV_ENC_ERR_DEVICE_NOT_EXIST); - nvenc_status_case(NV_ENC_ERR_INVALID_PTR); - nvenc_status_case(NV_ENC_ERR_INVALID_EVENT); - nvenc_status_case(NV_ENC_ERR_INVALID_PARAM); - nvenc_status_case(NV_ENC_ERR_INVALID_CALL); - nvenc_status_case(NV_ENC_ERR_OUT_OF_MEMORY); - nvenc_status_case(NV_ENC_ERR_ENCODER_NOT_INITIALIZED); - nvenc_status_case(NV_ENC_ERR_UNSUPPORTED_PARAM); - nvenc_status_case(NV_ENC_ERR_LOCK_BUSY); - nvenc_status_case(NV_ENC_ERR_NOT_ENOUGH_BUFFER); - nvenc_status_case(NV_ENC_ERR_INVALID_VERSION); - nvenc_status_case(NV_ENC_ERR_MAP_FAILED); - nvenc_status_case(NV_ENC_ERR_NEED_MORE_INPUT); - nvenc_status_case(NV_ENC_ERR_ENCODER_BUSY); - nvenc_status_case(NV_ENC_ERR_EVENT_NOT_REGISTERD); - nvenc_status_case(NV_ENC_ERR_GENERIC); - nvenc_status_case(NV_ENC_ERR_INCOMPATIBLE_CLIENT_KEY); - nvenc_status_case(NV_ENC_ERR_UNIMPLEMENTED); - nvenc_status_case(NV_ENC_ERR_RESOURCE_REGISTER_FAILED); - nvenc_status_case(NV_ENC_ERR_RESOURCE_NOT_REGISTERED); - nvenc_status_case(NV_ENC_ERR_RESOURCE_NOT_MAPPED); - // Newer versions of sdk may add more constants, look for them at the end of NVENCSTATUS enum -#undef nvenc_status_case - default: - return std::to_string(status); - } - }; - last_nvenc_error_string.clear(); if (status != NV_ENC_SUCCESS) { /* This API function gives broken strings more often than not @@ -705,11 +849,11 @@ namespace nvenc { if (!last_nvenc_error_string.empty()) last_nvenc_error_string += " "; } */ - last_nvenc_error_string += status_string(status); + last_nvenc_error_string += nvenc_status_string(status); return true; } return false; } -} // namespace nvenc +} // namespace NVENC_NAMESPACE diff --git a/src/nvenc/nvenc_base.h b/src/nvenc/nvenc_base.h index 9bed24165..8f9c8b01b 100644 --- a/src/nvenc/nvenc_base.h +++ b/src/nvenc/nvenc_base.h @@ -4,32 +4,33 @@ */ #pragma once -// lib includes -#include - // local includes #include "nvenc_colorspace.h" #include "nvenc_config.h" #include "nvenc_encoded_frame.h" +#include "nvenc_encoder.h" +#include "nvenc_sdk.h" #include "src/logging.h" #include "src/video.h" +#include "src/video_colorspace.h" /** * @brief Standalone NVENC encoder */ -namespace nvenc { +namespace NVENC_NAMESPACE { /** * @brief Abstract platform-agnostic base of standalone NVENC encoder. * Derived classes perform platform-specific operations. */ - class nvenc_base { + // Virtual inheritance is required because platform implementations also inherit their SDK-neutral interface. + class nvenc_base: public virtual ::nvenc::nvenc_encoder { // NOSONAR(cpp:S1011) public: /** * @param device_type Underlying device type used by derived class. */ explicit nvenc_base(NV_ENC_DEVICE_TYPE device_type); - virtual ~nvenc_base(); + ~nvenc_base() override; nvenc_base(const nvenc_base &) = delete; nvenc_base &operator=(const nvenc_base &) = delete; @@ -42,13 +43,18 @@ namespace nvenc { * @param buffer_format Platform-agnostic input surface format. * @return `true` on success, `false` on error */ - bool create_encoder(const nvenc_config &config, const video::config_t &client_config, const nvenc_colorspace_t &colorspace, NV_ENC_BUFFER_FORMAT buffer_format); + bool create_encoder( + const ::nvenc::nvenc_config &config, + const video::config_t &client_config, + const video::sunshine_colorspace_t &colorspace, + platf::pix_fmt_e buffer_format + ) override; /** * @brief Destroy the encoder. * Derived classes classes call it in the destructor. */ - void destroy_encoder(); + void destroy_encoder() override; /** * @brief Encode the next frame using platform-specific input surface. @@ -58,7 +64,7 @@ namespace nvenc { * @param force_idr Whether to encode frame as forced IDR. * @return Encoded frame. */ - nvenc_encoded_frame encode_frame(uint64_t frame_index, bool force_idr); + ::nvenc::nvenc_encoded_frame encode_frame(uint64_t frame_index, bool force_idr) override; /** * @brief Perform reference frame invalidation (RFI) procedure. @@ -67,7 +73,7 @@ namespace nvenc { * @return `true` on success, `false` on error. * After error next frame must be encoded with `force_idr = true`. */ - bool invalidate_ref_frames(uint64_t first_frame, uint64_t last_frame); + bool invalidate_ref_frames(uint64_t first_frame, uint64_t last_frame) override; protected: /** @@ -137,6 +143,191 @@ namespace nvenc { ///< Can be set in constructor or `init_library()`, must override `wait_for_async_event()`. private: + /** + * @brief Query one encoder capability. + * + * @param encode_guid Codec GUID to query. + * @param cap Capability identifier. + * @return Capability value, or zero when the query fails. + */ + int get_encoder_cap(const GUID &encode_guid, NV_ENC_CAPS cap) const; + + /** + * @brief Validate the requested input format and dimensions against encoder capabilities. + * + * @param encode_guid Selected codec GUID. + * @param buffer_format Selected NVENC input format. + * @return `true` when the request is supported, otherwise `false`. + */ + bool validate_encoder_capabilities(const GUID &encode_guid, NV_ENC_BUFFER_FORMAT buffer_format); + + /** + * @brief Configure split-frame encoding for the selected SDK. + * + * @param init_params Encoder initialization parameters to update. + * @param config NVENC encoder configuration. + * @param client_config Stream configuration requested by the client. + */ + void configure_split_frame( + NV_ENC_INITIALIZE_PARAMS &init_params, + const ::nvenc::nvenc_config &config, + const video::config_t &client_config + ) const; + + /** + * @brief Configure rate control and VBV options. + * + * @param enc_config Encoder configuration to update. + * @param config NVENC encoder configuration. + * @param client_config Stream configuration requested by the client. + * @param encode_guid Selected codec GUID. + */ + void configure_rate_control( + NV_ENC_CONFIG &enc_config, + const ::nvenc::nvenc_config &config, + const video::config_t &client_config, + const GUID &encode_guid + ); + + /** + * @brief Configure the requested reference-frame count. + * + * @param ref_frames_option Codec-specific reference-frame option. + * @param list0_option Codec-specific list-zero option. + * @param default_count Default reference-frame count. + * @param requested_count Client-requested reference-frame count. + * @param encode_guid Selected codec GUID. + */ + void configure_reference_frames( + std::uint32_t &ref_frames_option, + NV_ENC_NUM_REF_FRAMES &list0_option, + std::uint32_t default_count, + int requested_count, + const GUID &encode_guid + ); + + /** + * @brief Configure VUI metadata and intra-refresh options shared by H.264 and HEVC. + * + * @tparam FormatConfig Codec-specific NVENC configuration type. + * @param format_config Codec-specific encoder configuration to update. + * @param client_config Stream configuration requested by the client. + * @param colorspace NVENC colorspace metadata. + * @param buffer_format Selected NVENC input format. + * @param encode_guid Selected codec GUID. + */ + template + void configure_h264_hevc_metadata( + FormatConfig &format_config, + const video::config_t &client_config, + const nvenc_colorspace_t &colorspace, + NV_ENC_BUFFER_FORMAT buffer_format, + const GUID &encode_guid + ); + + /** + * @brief Configure H.264 codec options. + * + * @param enc_config Encoder configuration to update. + * @param config NVENC encoder configuration. + * @param client_config Stream configuration requested by the client. + * @param colorspace NVENC colorspace metadata. + * @param buffer_format Selected NVENC input format. + * @param encode_guid Selected codec GUID. + */ + void configure_h264( + NV_ENC_CONFIG &enc_config, + const ::nvenc::nvenc_config &config, + const video::config_t &client_config, + const nvenc_colorspace_t &colorspace, + NV_ENC_BUFFER_FORMAT buffer_format, + const GUID &encode_guid + ); + + /** + * @brief Configure HEVC codec options. + * + * @param enc_config Encoder configuration to update. + * @param config NVENC encoder configuration. + * @param client_config Stream configuration requested by the client. + * @param colorspace NVENC colorspace metadata. + * @param buffer_format Selected NVENC input format. + * @param encode_guid Selected codec GUID. + */ + void configure_hevc( + NV_ENC_CONFIG &enc_config, + const ::nvenc::nvenc_config &config, + const video::config_t &client_config, + const nvenc_colorspace_t &colorspace, + NV_ENC_BUFFER_FORMAT buffer_format, + const GUID &encode_guid + ); + +#if NVENC_SDK_VERSION >= 1200 + /** + * @brief Configure AV1 codec options. + * + * @param enc_config Encoder configuration to update. + * @param config NVENC encoder configuration. + * @param client_config Stream configuration requested by the client. + * @param colorspace NVENC colorspace metadata. + * @param buffer_format Selected NVENC input format. + * @param encode_guid Selected codec GUID. + */ + void configure_av1( + NV_ENC_CONFIG &enc_config, + const ::nvenc::nvenc_config &config, + const video::config_t &client_config, + const nvenc_colorspace_t &colorspace, + NV_ENC_BUFFER_FORMAT buffer_format, + const GUID &encode_guid + ); +#endif + + /** + * @brief Configure codec-specific encoder options. + * + * @param enc_config Encoder configuration to update. + * @param config NVENC encoder configuration. + * @param client_config Stream configuration requested by the client. + * @param colorspace NVENC colorspace metadata. + * @param buffer_format Selected NVENC input format. + * @param encode_guid Selected codec GUID. + */ + void configure_codec( + NV_ENC_CONFIG &enc_config, + const ::nvenc::nvenc_config &config, + const video::config_t &client_config, + const nvenc_colorspace_t &colorspace, + NV_ENC_BUFFER_FORMAT buffer_format, + const GUID &encode_guid + ); + + /** + * @brief Initialize the encoder and its registered input and output resources. + * + * @param init_params Completed encoder initialization parameters. + * @return `true` on success, otherwise `false`. + */ + bool initialize_encoder_resources(NV_ENC_INITIALIZE_PARAMS &init_params); + + /** + * @brief Log the selected encoder configuration. + * + * @param init_params Encoder initialization parameters. + * @param enc_config Encoder configuration. + * @param config NVENC encoder configuration. + * @param client_config Stream configuration requested by the client. + * @param buffer_format Selected NVENC input format. + */ + void log_created_encoder( + const NV_ENC_INITIALIZE_PARAMS &init_params, + const NV_ENC_CONFIG &enc_config, + const ::nvenc::nvenc_config &config, + const video::config_t &client_config, + NV_ENC_BUFFER_FORMAT buffer_format + ) const; + NV_ENC_OUTPUT_PTR output_bitstream = nullptr; struct { @@ -147,4 +338,4 @@ namespace nvenc { } encoder_state; }; -} // namespace nvenc +} // namespace NVENC_NAMESPACE diff --git a/src/nvenc/nvenc_colorspace.h b/src/nvenc/nvenc_colorspace.h index a4330a7c8..10fe264a7 100644 --- a/src/nvenc/nvenc_colorspace.h +++ b/src/nvenc/nvenc_colorspace.h @@ -4,10 +4,10 @@ */ #pragma once -// lib includes -#include +// local includes +#include "nvenc_sdk.h" -namespace nvenc { +namespace NVENC_NAMESPACE { /** * @brief YUV colorspace and color range. @@ -19,4 +19,4 @@ namespace nvenc { bool full_range; ///< Whether the video range is full-range instead of limited. }; -} // namespace nvenc +} // namespace NVENC_NAMESPACE diff --git a/src/nvenc/nvenc_d3d11.cpp b/src/nvenc/nvenc_d3d11.cpp index f3c7af174..64829e25e 100644 --- a/src/nvenc/nvenc_d3d11.cpp +++ b/src/nvenc/nvenc_d3d11.cpp @@ -8,62 +8,47 @@ #ifdef _WIN32 #include "nvenc_d3d11.h" -namespace nvenc { +namespace NVENC_NAMESPACE { - nvenc_d3d11::nvenc_d3d11(NV_ENC_DEVICE_TYPE device_type): - nvenc_base(device_type) { + nvenc_d3d11::nvenc_d3d11(NV_ENC_DEVICE_TYPE device_type, ::nvenc::shared_dll dll): + nvenc_base(device_type), + dll(std::move(dll)) { async_event_handle = CreateEvent(nullptr, FALSE, FALSE, nullptr); } nvenc_d3d11::~nvenc_d3d11() { - if (dll) { - FreeLibrary(dll); - dll = nullptr; - } if (async_event_handle) { CloseHandle(async_event_handle); } } bool nvenc_d3d11::init_library() { - if (dll) { + if (nvenc) { return true; } - #ifdef _WIN64 - constexpr auto dll_name = "nvEncodeAPI64.dll"; - #else - constexpr auto dll_name = "nvEncodeAPI.dll"; - #endif - - if ((dll = LoadLibraryEx(dll_name, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32))) { - if (auto create_instance = (decltype(NvEncodeAPICreateInstance) *) GetProcAddress(dll, "NvEncodeAPICreateInstance")) { - auto new_nvenc = std::make_unique(); - new_nvenc->version = NV_ENCODE_API_FUNCTION_LIST_VER; - if (nvenc_failed(create_instance(new_nvenc.get()))) { - BOOST_LOG(error) << "NvEnc: NvEncodeAPICreateInstance() failed: " << last_nvenc_error_string; - } else { - nvenc = std::move(new_nvenc); - return true; - } - } else { - BOOST_LOG(error) << "NvEnc: No NvEncodeAPICreateInstance() in " << dll_name; - } - } else { - BOOST_LOG(debug) << "NvEnc: Couldn't load NvEnc library " << dll_name; + auto create_instance = reinterpret_cast( + GetProcAddress(dll.get(), "NvEncodeAPICreateInstance") + ); + if (!create_instance) { + BOOST_LOG(error) << "NvEnc: No NvEncodeAPICreateInstance() in NVENC driver library"; + return false; } - if (dll) { - FreeLibrary(dll); - dll = nullptr; + auto new_nvenc = std::make_unique(); + new_nvenc->version = NV_ENCODE_API_FUNCTION_LIST_VER; + if (nvenc_failed(create_instance(new_nvenc.get()))) { + BOOST_LOG(error) << "NvEnc: NvEncodeAPICreateInstance() failed: " << last_nvenc_error_string; + return false; } - return false; + nvenc = std::move(new_nvenc); + return true; } bool nvenc_d3d11::wait_for_async_event(uint32_t timeout_ms) { return WaitForSingleObject(async_event_handle, timeout_ms) == WAIT_OBJECT_0; } -} // namespace nvenc +} // namespace NVENC_NAMESPACE #endif diff --git a/src/nvenc/nvenc_d3d11.h b/src/nvenc/nvenc_d3d11.h index 73e0ad7a7..c6ccad714 100644 --- a/src/nvenc/nvenc_d3d11.h +++ b/src/nvenc/nvenc_d3d11.h @@ -11,8 +11,10 @@ // local includes #include "nvenc_base.h" + #include "nvenc_d3d11_interface.h" + #include "nvenc_shared_dll.h" -namespace nvenc { +namespace NVENC_NAMESPACE { #ifdef DOXYGEN /** @@ -42,29 +44,30 @@ namespace nvenc { * @brief Abstract Direct3D11 NVENC encoder. * Encapsulates common code used by native and interop implementations. */ - class nvenc_d3d11: public nvenc_base { + class nvenc_d3d11: public nvenc_base, public ::nvenc::nvenc_d3d11_interface { public: /** * @brief Initialize an NVENC session wrapper for D3D11 input textures. * * @param device_type NVENC device type used by the encoder session. + * @param dll Shared NVENC driver module. */ - explicit nvenc_d3d11(NV_ENC_DEVICE_TYPE device_type); - ~nvenc_d3d11(); + explicit nvenc_d3d11(NV_ENC_DEVICE_TYPE device_type, ::nvenc::shared_dll dll); + ~nvenc_d3d11() override; /** * @brief Get input surface texture. * @return Input surface texture. */ - virtual ID3D11Texture2D *get_input_texture() = 0; + ID3D11Texture2D *get_input_texture() override = 0; protected: bool init_library() override; bool wait_for_async_event(uint32_t timeout_ms) override; private: - HMODULE dll = nullptr; + ::nvenc::shared_dll dll; }; -} // namespace nvenc +} // namespace NVENC_NAMESPACE #endif diff --git a/src/nvenc/nvenc_d3d11_interface.h b/src/nvenc/nvenc_d3d11_interface.h new file mode 100644 index 000000000..6bf699f7b --- /dev/null +++ b/src/nvenc/nvenc_d3d11_interface.h @@ -0,0 +1,36 @@ +/** + * @file src/nvenc/nvenc_d3d11_interface.h + * @brief Declarations for the SDK-neutral Direct3D11 NVENC interface. + */ +#pragma once +#ifdef _WIN32 + + // lib includes + #include + + // local includes + #include "nvenc_encoder.h" + +namespace nvenc { + + /** + * @brief SDK-neutral Direct3D11 NVENC encoder interface. + */ + // Virtual inheritance is required because concrete encoders also inherit their SDK-specific implementation. + class nvenc_d3d11_interface: public virtual nvenc_encoder { // NOSONAR(cpp:S1011) + public: + /** + * @brief Destroy the SDK-neutral Direct3D11 encoder interface. + */ + ~nvenc_d3d11_interface() override = default; + + /** + * @brief Get the input surface texture. + * + * @return Input surface texture. + */ + virtual ID3D11Texture2D *get_input_texture() = 0; + }; + +} // namespace nvenc +#endif diff --git a/src/nvenc/nvenc_d3d11_native.cpp b/src/nvenc/nvenc_d3d11_native.cpp index 02d1b3643..7af661b60 100644 --- a/src/nvenc/nvenc_d3d11_native.cpp +++ b/src/nvenc/nvenc_d3d11_native.cpp @@ -9,10 +9,10 @@ // local includes #include "nvenc_utils.h" -namespace nvenc { +namespace NVENC_NAMESPACE { - nvenc_d3d11_native::nvenc_d3d11_native(ID3D11Device *d3d_device): - nvenc_d3d11(NV_ENC_DEVICE_TYPE_DIRECTX), + nvenc_d3d11_native::nvenc_d3d11_native(ID3D11Device *d3d_device, ::nvenc::shared_dll dll): + nvenc_d3d11(NV_ENC_DEVICE_TYPE_DIRECTX, std::move(dll)), d3d_device(d3d_device) { device = d3d_device; } @@ -70,5 +70,5 @@ namespace nvenc { return true; } -} // namespace nvenc +} // namespace NVENC_NAMESPACE #endif diff --git a/src/nvenc/nvenc_d3d11_native.h b/src/nvenc/nvenc_d3d11_native.h index 0e6f039e3..f3f5d9eac 100644 --- a/src/nvenc/nvenc_d3d11_native.h +++ b/src/nvenc/nvenc_d3d11_native.h @@ -11,7 +11,7 @@ // local includes #include "nvenc_d3d11.h" -namespace nvenc { +namespace NVENC_NAMESPACE { /** * @brief Native Direct3D11 NVENC encoder. @@ -20,9 +20,10 @@ namespace nvenc { public: /** * @param d3d_device Direct3D11 device used for encoding. + * @param dll Shared NVENC driver module. */ - explicit nvenc_d3d11_native(ID3D11Device *d3d_device); - ~nvenc_d3d11_native(); + explicit nvenc_d3d11_native(ID3D11Device *d3d_device, ::nvenc::shared_dll dll); + ~nvenc_d3d11_native() override; ID3D11Texture2D *get_input_texture() override; @@ -33,5 +34,5 @@ namespace nvenc { ID3D11Texture2DPtr d3d_input_texture; }; -} // namespace nvenc +} // namespace NVENC_NAMESPACE #endif diff --git a/src/nvenc/nvenc_d3d11_on_cuda.cpp b/src/nvenc/nvenc_d3d11_on_cuda.cpp index 44123ed9e..787ccf658 100644 --- a/src/nvenc/nvenc_d3d11_on_cuda.cpp +++ b/src/nvenc/nvenc_d3d11_on_cuda.cpp @@ -9,10 +9,10 @@ // local includes #include "nvenc_utils.h" -namespace nvenc { +namespace NVENC_NAMESPACE { - nvenc_d3d11_on_cuda::nvenc_d3d11_on_cuda(ID3D11Device *d3d_device): - nvenc_d3d11(NV_ENC_DEVICE_TYPE_CUDA), + nvenc_d3d11_on_cuda::nvenc_d3d11_on_cuda(ID3D11Device *d3d_device, ::nvenc::shared_dll dll): + nvenc_d3d11(NV_ENC_DEVICE_TYPE_CUDA, std::move(dll)), d3d_device(d3d_device) { } @@ -265,5 +265,5 @@ namespace nvenc { } } -} // namespace nvenc +} // namespace NVENC_NAMESPACE #endif diff --git a/src/nvenc/nvenc_d3d11_on_cuda.h b/src/nvenc/nvenc_d3d11_on_cuda.h index 80aeb9ed8..67bc34d82 100644 --- a/src/nvenc/nvenc_d3d11_on_cuda.h +++ b/src/nvenc/nvenc_d3d11_on_cuda.h @@ -4,13 +4,11 @@ */ #pragma once #ifdef _WIN32 - // lib includes - #include - // local includes #include "nvenc_d3d11.h" + #include "nvenc_sdk.h" -namespace nvenc { +namespace NVENC_NAMESPACE { /** * @brief Interop Direct3D11 on CUDA NVENC encoder. @@ -21,9 +19,10 @@ namespace nvenc { /** * @param d3d_device Direct3D11 device that will create input surface texture. * CUDA encoding device will be derived from it. + * @param dll Shared NVENC driver module. */ - explicit nvenc_d3d11_on_cuda(ID3D11Device *d3d_device); - ~nvenc_d3d11_on_cuda(); + explicit nvenc_d3d11_on_cuda(ID3D11Device *d3d_device, ::nvenc::shared_dll dll); + ~nvenc_d3d11_on_cuda() override; ID3D11Texture2D *get_input_texture() override; @@ -84,5 +83,5 @@ namespace nvenc { size_t cuda_surface_pitch = 0; }; -} // namespace nvenc +} // namespace NVENC_NAMESPACE #endif diff --git a/src/nvenc/nvenc_dynamic_factory.cpp b/src/nvenc/nvenc_dynamic_factory.cpp new file mode 100644 index 000000000..bc40d0c36 --- /dev/null +++ b/src/nvenc/nvenc_dynamic_factory.cpp @@ -0,0 +1,129 @@ +/** + * @file src/nvenc/nvenc_dynamic_factory.cpp + * @brief Definitions for runtime NVENC SDK selection on Windows. + */ +#ifdef _WIN32 + + // this include + #include "nvenc_dynamic_factory.h" + + // standard includes + #include + #include + + // local includes + #include "nvenc_dynamic_factory_versions.h" + #include "src/logging.h" + +namespace { + + #ifdef _WIN64 + constexpr auto nvenc_dll_name = "nvEncodeAPI64.dll"; + #else + constexpr auto nvenc_dll_name = "nvEncodeAPI.dll"; + #endif + + constexpr auto minimum_driver_version = "456.71"; + + using get_max_supported_version_fn = std::uint32_t(WINAPI *)(std::uint32_t *); + +} // namespace + +namespace nvenc { + + nvenc_dynamic_factory::nvenc_dynamic_factory( + shared_dll dll, + nvenc_sdk_version sdk_version, + create_encoder_fn create_native, + create_encoder_fn create_on_cuda + ): + dll(std::move(dll)), + selected_sdk_version(sdk_version), + create_native(std::move(create_native)), + create_on_cuda(std::move(create_on_cuda)) { + } + + std::shared_ptr nvenc_dynamic_factory::get() { + return get({ + []() { + return make_shared_dll(LoadLibraryEx(nvenc_dll_name, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32)); + }, + [](HMODULE dll, const char *symbol) { + return GetProcAddress(dll, symbol); + }, + }); + } + + std::shared_ptr nvenc_dynamic_factory::get(const nvenc_runtime_api &runtime_api) { + auto dll = runtime_api.load_driver(); + if (!dll) { + BOOST_LOG(debug) << "NvEnc: Couldn't load NvEnc library " << nvenc_dll_name; + return {}; + } + + const auto get_max_version = std::bit_cast( + runtime_api.get_symbol(dll.get(), "NvEncodeAPIGetMaxSupportedVersion") + ); + if (!get_max_version) { + BOOST_LOG(error) << "NvEnc: No NvEncodeAPIGetMaxSupportedVersion() in " << nvenc_dll_name; + return {}; + } + + std::uint32_t packed_max_version = 0; + if (get_max_version(&packed_max_version) != 0U) { + BOOST_LOG(error) << "NvEnc: NvEncodeAPIGetMaxSupportedVersion() failed"; + return {}; + } + + const auto max_version = decode_nvenc_driver_version(packed_max_version); + const auto sdk_version = select_nvenc_sdk_version(max_version); + switch (sdk_version) { + case nvenc_sdk_version::sdk_13_0: + return std::make_shared( + std::move(dll), + sdk_version, + detail::create_nvenc_d3d11_native_1300, + detail::create_nvenc_d3d11_on_cuda_1300 + ); + + case nvenc_sdk_version::sdk_12_0: + return std::make_shared( + std::move(dll), + sdk_version, + detail::create_nvenc_d3d11_native_1200, + detail::create_nvenc_d3d11_on_cuda_1200 + ); + + case nvenc_sdk_version::sdk_11_0: + return std::make_shared( + std::move(dll), + sdk_version, + detail::create_nvenc_d3d11_native_1100, + detail::create_nvenc_d3d11_on_cuda_1100 + ); + + case nvenc_sdk_version::unsupported: + default: + BOOST_LOG(error) << "NvEnc: minimum required driver version is " << minimum_driver_version; + return {}; + } + } + + std::unique_ptr nvenc_dynamic_factory::create_nvenc_d3d11_native( + ID3D11Device *d3d_device + ) const { + return create_native(d3d_device, dll); + } + + std::unique_ptr nvenc_dynamic_factory::create_nvenc_d3d11_on_cuda( + ID3D11Device *d3d_device + ) const { + return create_on_cuda(d3d_device, dll); + } + + nvenc_sdk_version nvenc_dynamic_factory::sdk_version() const { + return selected_sdk_version; + } + +} // namespace nvenc +#endif diff --git a/src/nvenc/nvenc_dynamic_factory.h b/src/nvenc/nvenc_dynamic_factory.h new file mode 100644 index 000000000..87f9ca05b --- /dev/null +++ b/src/nvenc/nvenc_dynamic_factory.h @@ -0,0 +1,106 @@ +/** + * @file src/nvenc/nvenc_dynamic_factory.h + * @brief Declarations for runtime NVENC SDK selection on Windows. + */ +#pragma once +#ifdef _WIN32 + +// standard includes + #include + #include + + // local includes + #include "nvenc_d3d11_interface.h" + #include "nvenc_shared_dll.h" + #include "nvenc_version.h" + +namespace nvenc { + + /** + * @brief Windows runtime operations used to discover the installed NVENC API. + */ + struct nvenc_runtime_api { + using load_driver_fn = std::function; ///< Load the NVENC driver module. + + /** + * @brief Resolve an export from the loaded NVENC driver module. + */ + using get_symbol_fn = std::function; + + load_driver_fn load_driver; ///< Function that loads the NVENC driver module. + get_symbol_fn get_symbol; ///< Resolve a driver export. + }; + + /** + * @brief Factory bound to the newest NVENC SDK supported by the installed driver. + */ + class nvenc_dynamic_factory { + public: + /** + * @brief SDK-specific encoder constructor. + */ + using create_encoder_fn = + std::function(ID3D11Device *, shared_dll)>; + + /** + * @brief Construct a factory bound to one NVENC SDK implementation. + * + * @param dll Shared NVENC driver module. + * @param sdk_version Selected SDK version. + * @param create_native Native Direct3D11 encoder constructor. + * @param create_on_cuda CUDA-interoperability encoder constructor. + */ + nvenc_dynamic_factory( + shared_dll dll, + nvenc_sdk_version sdk_version, + create_encoder_fn create_native, + create_encoder_fn create_on_cuda + ); + + /** + * @brief Load the NVENC driver and select a compatible SDK implementation. + * + * @return Initialized factory, or an empty pointer when NVENC is unavailable. + */ + static std::shared_ptr get(); + + /** + * @brief Select a compatible SDK implementation using the supplied runtime operations. + * + * @param runtime_api Runtime operations used to load and query the NVENC driver. + * @return Initialized factory, or an empty pointer when NVENC is unavailable. + */ + static std::shared_ptr get(const nvenc_runtime_api &runtime_api); + + /** + * @brief Create a native Direct3D11 NVENC encoder. + * + * @param d3d_device Direct3D11 device used for encoding. + * @return SDK-neutral encoder instance. + */ + std::unique_ptr create_nvenc_d3d11_native(ID3D11Device *d3d_device) const; + + /** + * @brief Create a CUDA NVENC encoder with Direct3D11 input surfaces. + * + * @param d3d_device Direct3D11 device used to create input textures. + * @return SDK-neutral encoder instance. + */ + std::unique_ptr create_nvenc_d3d11_on_cuda(ID3D11Device *d3d_device) const; + + /** + * @brief Get the SDK implementation selected for this factory. + * + * @return Selected NVENC SDK version. + */ + nvenc_sdk_version sdk_version() const; + + private: + shared_dll dll; + nvenc_sdk_version selected_sdk_version; + create_encoder_fn create_native; + create_encoder_fn create_on_cuda; + }; + +} // namespace nvenc +#endif diff --git a/src/nvenc/nvenc_dynamic_factory_impl.cpp b/src/nvenc/nvenc_dynamic_factory_impl.cpp new file mode 100644 index 000000000..1bc80aa54 --- /dev/null +++ b/src/nvenc/nvenc_dynamic_factory_impl.cpp @@ -0,0 +1,48 @@ +/** + * @file src/nvenc/nvenc_dynamic_factory_impl.cpp + * @brief SDK-specific constructors used by the runtime NVENC factory. + */ +#ifdef _WIN32 + + #ifndef NVENC_NAMESPACE + #error NVENC_NAMESPACE must identify the version-specific implementation namespace + #endif + + #ifndef NVENC_FACTORY_SUFFIX + #error NVENC_FACTORY_SUFFIX must identify the version-specific constructor suffix + #endif + + // standard includes + #include + + // local includes + #include "nvenc_d3d11_native.h" + #include "nvenc_d3d11_on_cuda.h" + #include "nvenc_dynamic_factory_versions.h" + + #ifndef DOXYGEN + #define NVENC_CONCAT_IMPL(left, right) left##right + #define NVENC_CONCAT(left, right) NVENC_CONCAT_IMPL(left, right) + +namespace nvenc::detail { + + std::unique_ptr NVENC_CONCAT(create_nvenc_d3d11_native_, NVENC_FACTORY_SUFFIX)( + ID3D11Device *device, + shared_dll dll + ) { + return std::make_unique(device, std::move(dll)); + } + + std::unique_ptr NVENC_CONCAT(create_nvenc_d3d11_on_cuda_, NVENC_FACTORY_SUFFIX)( + ID3D11Device *device, + shared_dll dll + ) { + return std::make_unique(device, std::move(dll)); + } + +} // namespace nvenc::detail + + #undef NVENC_CONCAT + #undef NVENC_CONCAT_IMPL + #endif +#endif diff --git a/src/nvenc/nvenc_dynamic_factory_versions.h b/src/nvenc/nvenc_dynamic_factory_versions.h new file mode 100644 index 000000000..4ac273b68 --- /dev/null +++ b/src/nvenc/nvenc_dynamic_factory_versions.h @@ -0,0 +1,67 @@ +/** + * @file src/nvenc/nvenc_dynamic_factory_versions.h + * @brief Declarations for SDK-specific NVENC encoder constructors. + */ +#pragma once +#ifdef _WIN32 + + // standard includes + #include + + // local includes + #include "nvenc_d3d11_interface.h" + #include "nvenc_shared_dll.h" + +namespace nvenc::detail { + + /** + * @brief Create an SDK 11.0 native Direct3D11 encoder. + * + * @param device Direct3D11 device used for encoding. + * @param dll Shared NVENC driver module. + * @return SDK-neutral encoder instance. + */ + std::unique_ptr create_nvenc_d3d11_native_1100(ID3D11Device *device, shared_dll dll); + /** + * @brief Create an SDK 11.0 CUDA-interoperability encoder. + * + * @param device Direct3D11 device used for input surfaces. + * @param dll Shared NVENC driver module. + * @return SDK-neutral encoder instance. + */ + std::unique_ptr create_nvenc_d3d11_on_cuda_1100(ID3D11Device *device, shared_dll dll); + /** + * @brief Create an SDK 12.0 native Direct3D11 encoder. + * + * @param device Direct3D11 device used for encoding. + * @param dll Shared NVENC driver module. + * @return SDK-neutral encoder instance. + */ + std::unique_ptr create_nvenc_d3d11_native_1200(ID3D11Device *device, shared_dll dll); + /** + * @brief Create an SDK 12.0 CUDA-interoperability encoder. + * + * @param device Direct3D11 device used for input surfaces. + * @param dll Shared NVENC driver module. + * @return SDK-neutral encoder instance. + */ + std::unique_ptr create_nvenc_d3d11_on_cuda_1200(ID3D11Device *device, shared_dll dll); + /** + * @brief Create an SDK 13.0 native Direct3D11 encoder. + * + * @param device Direct3D11 device used for encoding. + * @param dll Shared NVENC driver module. + * @return SDK-neutral encoder instance. + */ + std::unique_ptr create_nvenc_d3d11_native_1300(ID3D11Device *device, shared_dll dll); + /** + * @brief Create an SDK 13.0 CUDA-interoperability encoder. + * + * @param device Direct3D11 device used for input surfaces. + * @param dll Shared NVENC driver module. + * @return SDK-neutral encoder instance. + */ + std::unique_ptr create_nvenc_d3d11_on_cuda_1300(ID3D11Device *device, shared_dll dll); + +} // namespace nvenc::detail +#endif diff --git a/src/nvenc/nvenc_encoder.h b/src/nvenc/nvenc_encoder.h new file mode 100644 index 000000000..4c7505830 --- /dev/null +++ b/src/nvenc/nvenc_encoder.h @@ -0,0 +1,75 @@ +/** + * @file src/nvenc/nvenc_encoder.h + * @brief Declarations for the SDK-neutral NVENC encoder interface. + */ +#pragma once + +// standard includes +#include + +// local includes +#include "nvenc_config.h" +#include "nvenc_encoded_frame.h" + +namespace platf { + enum class pix_fmt_e; +} + +namespace video { + struct config_t; + struct sunshine_colorspace_t; +} // namespace video + +namespace nvenc { + + /** + * @brief SDK-neutral standalone NVENC encoder interface. + */ + class nvenc_encoder { + public: + /** + * @brief Destroy the SDK-neutral encoder interface. + */ + virtual ~nvenc_encoder() = default; + + /** + * @brief Create the encoder. + * + * @param config NVENC encoder configuration. + * @param client_config Stream configuration requested by the client. + * @param colorspace Sunshine colorspace metadata. + * @param buffer_format Platform-agnostic input surface format. + * @return `true` on success, `false` on error. + */ + virtual bool create_encoder( + const nvenc_config &config, + const video::config_t &client_config, + const video::sunshine_colorspace_t &colorspace, + platf::pix_fmt_e buffer_format + ) = 0; + + /** + * @brief Destroy the encoder. + */ + virtual void destroy_encoder() = 0; + + /** + * @brief Encode the next frame using the platform-specific input surface. + * + * @param frame_index Frame index that uniquely identifies the frame. + * @param force_idr Whether to encode the frame as a forced IDR. + * @return Encoded frame. + */ + virtual nvenc_encoded_frame encode_frame(std::uint64_t frame_index, bool force_idr) = 0; + + /** + * @brief Invalidate reference frames in the requested range. + * + * @param first_frame First frame index of the invalidation range. + * @param last_frame Last frame index of the invalidation range. + * @return `true` on success, `false` on error. + */ + virtual bool invalidate_ref_frames(std::uint64_t first_frame, std::uint64_t last_frame) = 0; + }; + +} // namespace nvenc diff --git a/src/nvenc/nvenc_sdk.h b/src/nvenc/nvenc_sdk.h new file mode 100644 index 000000000..5479da4c5 --- /dev/null +++ b/src/nvenc/nvenc_sdk.h @@ -0,0 +1,125 @@ +/** + * @file src/nvenc/nvenc_sdk.h + * @brief Imports one NVENC SDK into its version-specific implementation namespace. + */ +#pragma once + +#ifdef DOXYGEN + /** + * @def NVENC_NAMESPACE + * @brief Namespace used for the documented NVENC implementation. + */ + #define NVENC_NAMESPACE nvenc + + /** + * @def NVENC_SDK_VERSION + * @brief NVENC SDK version used while generating documentation. + */ + // Doxygen must select the same preprocessor interface used by versioned build targets. + #define NVENC_SDK_VERSION 1300 // NOSONAR(cpp:S5028) +#endif + +#ifndef NVENC_NAMESPACE + #error NVENC_NAMESPACE must identify the version-specific implementation namespace +#endif + +#ifndef NVENC_SDK_VERSION + #error NVENC_SDK_VERSION must identify the version-specific NVENC SDK +#endif + +// Include common platform and integer declarations outside the SDK namespace. +#include +#include + +#ifdef _WIN32 + #include +#endif + +// The version-specific namespace is populated by the SDK headers included below. +namespace NVENC_NAMESPACE { // NOSONAR(cpp:S3261) +#ifndef DOXYGEN + #include + #include + + #if NVENCAPI_MAJOR_VERSION * 100 + NVENCAPI_MINOR_VERSION != NVENC_SDK_VERSION + #error NVENC_SDK_VERSION does not match the selected nv-codec-headers package + #endif +#endif + +#if NVENC_SDK_VERSION < 1200 + /** + * @brief Video format values added as named types after SDK 11.0. + */ + enum NV_ENC_VUI_VIDEO_FORMAT { + NV_ENC_VUI_VIDEO_FORMAT_COMPONENT = 0, + NV_ENC_VUI_VIDEO_FORMAT_PAL = 1, + NV_ENC_VUI_VIDEO_FORMAT_NTSC = 2, + NV_ENC_VUI_VIDEO_FORMAT_SECAM = 3, + NV_ENC_VUI_VIDEO_FORMAT_MAC = 4, + NV_ENC_VUI_VIDEO_FORMAT_UNSPECIFIED = 5, + }; + + /** + * @brief Color-primary values added as named types after SDK 11.0. + */ + enum NV_ENC_VUI_COLOR_PRIMARIES { + NV_ENC_VUI_COLOR_PRIMARIES_UNDEFINED = 0, + NV_ENC_VUI_COLOR_PRIMARIES_BT709 = 1, + NV_ENC_VUI_COLOR_PRIMARIES_UNSPECIFIED = 2, + NV_ENC_VUI_COLOR_PRIMARIES_RESERVED = 3, + NV_ENC_VUI_COLOR_PRIMARIES_BT470M = 4, + NV_ENC_VUI_COLOR_PRIMARIES_BT470BG = 5, + NV_ENC_VUI_COLOR_PRIMARIES_SMPTE170M = 6, + NV_ENC_VUI_COLOR_PRIMARIES_SMPTE240M = 7, + NV_ENC_VUI_COLOR_PRIMARIES_FILM = 8, + NV_ENC_VUI_COLOR_PRIMARIES_BT2020 = 9, + NV_ENC_VUI_COLOR_PRIMARIES_SMPTE428 = 10, + NV_ENC_VUI_COLOR_PRIMARIES_SMPTE431 = 11, + NV_ENC_VUI_COLOR_PRIMARIES_SMPTE432 = 12, + NV_ENC_VUI_COLOR_PRIMARIES_JEDEC_P22 = 22, + }; + + /** + * @brief Transfer-characteristic values added as named types after SDK 11.0. + */ + enum NV_ENC_VUI_TRANSFER_CHARACTERISTIC { + NV_ENC_VUI_TRANSFER_CHARACTERISTIC_UNDEFINED = 0, + NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT709 = 1, + NV_ENC_VUI_TRANSFER_CHARACTERISTIC_UNSPECIFIED = 2, + NV_ENC_VUI_TRANSFER_CHARACTERISTIC_RESERVED = 3, + NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT470M = 4, + NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT470BG = 5, + NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE170M = 6, + NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE240M = 7, + NV_ENC_VUI_TRANSFER_CHARACTERISTIC_LINEAR = 8, + NV_ENC_VUI_TRANSFER_CHARACTERISTIC_LOG = 9, + NV_ENC_VUI_TRANSFER_CHARACTERISTIC_LOG_SQRT = 10, + NV_ENC_VUI_TRANSFER_CHARACTERISTIC_IEC61966_2_4 = 11, + NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT1361_ECG = 12, + NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SRGB = 13, + NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT2020_10 = 14, + NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT2020_12 = 15, + NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE2084 = 16, + NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE428 = 17, + NV_ENC_VUI_TRANSFER_CHARACTERISTIC_ARIB_STD_B67 = 18, + }; + + /** + * @brief Matrix-coefficient values added as named types after SDK 11.0. + */ + enum NV_ENC_VUI_MATRIX_COEFFS { + NV_ENC_VUI_MATRIX_COEFFS_RGB = 0, + NV_ENC_VUI_MATRIX_COEFFS_BT709 = 1, + NV_ENC_VUI_MATRIX_COEFFS_UNSPECIFIED = 2, + NV_ENC_VUI_MATRIX_COEFFS_RESERVED = 3, + NV_ENC_VUI_MATRIX_COEFFS_FCC = 4, + NV_ENC_VUI_MATRIX_COEFFS_BT470BG = 5, + NV_ENC_VUI_MATRIX_COEFFS_SMPTE170M = 6, + NV_ENC_VUI_MATRIX_COEFFS_SMPTE240M = 7, + NV_ENC_VUI_MATRIX_COEFFS_YCGCO = 8, + NV_ENC_VUI_MATRIX_COEFFS_BT2020_NCL = 9, + NV_ENC_VUI_MATRIX_COEFFS_BT2020_CL = 10, + NV_ENC_VUI_MATRIX_COEFFS_SMPTE2085 = 11, + }; +#endif +} // namespace NVENC_NAMESPACE diff --git a/src/nvenc/nvenc_shared_dll.h b/src/nvenc/nvenc_shared_dll.h new file mode 100644 index 000000000..3461f1b6c --- /dev/null +++ b/src/nvenc/nvenc_shared_dll.h @@ -0,0 +1,49 @@ +/** + * @file src/nvenc/nvenc_shared_dll.h + * @brief Windows module lifetime helpers shared by NVENC factories and encoders. + */ +#pragma once +#ifdef _WIN32 + + // standard includes + #include + #include + + // platform includes + #include + +namespace nvenc { + + /** + * @brief Shared ownership wrapper for a loaded Windows module. + */ + using shared_dll = std::shared_ptr>; + + /** + * @brief Release a Windows module when its final shared owner is destroyed. + */ + struct shared_dll_deleter { + /** + * @brief Release the module. + * + * @param dll Module handle to release. + */ + void operator()(HMODULE dll) const { + if (dll) { + FreeLibrary(dll); + } + } + }; + + /** + * @brief Wrap a Windows module handle in shared ownership. + * + * @param dll Module handle returned by `LoadLibraryEx()`. + * @return Shared module handle. + */ + inline shared_dll make_shared_dll(HMODULE dll) { + return shared_dll(dll, shared_dll_deleter {}); + } + +} // namespace nvenc +#endif diff --git a/src/nvenc/nvenc_utils.cpp b/src/nvenc/nvenc_utils.cpp index cf716d7a4..3a8cf2b81 100644 --- a/src/nvenc/nvenc_utils.cpp +++ b/src/nvenc/nvenc_utils.cpp @@ -8,7 +8,7 @@ // local includes #include "nvenc_utils.h" -namespace nvenc { +namespace NVENC_NAMESPACE { #ifdef _WIN32 /** @@ -102,4 +102,4 @@ namespace nvenc { return colorspace; } -} // namespace nvenc +} // namespace NVENC_NAMESPACE diff --git a/src/nvenc/nvenc_utils.h b/src/nvenc/nvenc_utils.h index 439b493ff..f408e5918 100644 --- a/src/nvenc/nvenc_utils.h +++ b/src/nvenc/nvenc_utils.h @@ -9,15 +9,13 @@ #include #endif -// lib includes -#include - // local includes #include "nvenc_colorspace.h" +#include "nvenc_sdk.h" #include "src/platform/common.h" #include "src/video_colorspace.h" -namespace nvenc { +namespace NVENC_NAMESPACE { #ifdef _WIN32 /** @@ -45,4 +43,4 @@ namespace nvenc { */ nvenc_colorspace_t nvenc_colorspace_from_sunshine_colorspace(const video::sunshine_colorspace_t &sunshine_colorspace); -} // namespace nvenc +} // namespace NVENC_NAMESPACE diff --git a/src/nvenc/nvenc_version.h b/src/nvenc/nvenc_version.h new file mode 100644 index 000000000..2058a0abd --- /dev/null +++ b/src/nvenc/nvenc_version.h @@ -0,0 +1,53 @@ +/** + * @file src/nvenc/nvenc_version.h + * @brief NVENC SDK version selection helpers. + */ +#pragma once + +// standard includes +#include +#include + +namespace nvenc { + + /** + * @brief NVENC SDK implementations compiled into Sunshine. + */ + enum class nvenc_sdk_version : std::uint32_t { + unsupported = 0U, ///< No compatible SDK implementation is available. + sdk_11_0 = 1100U, ///< Video Codec SDK 11.0. + sdk_12_0 = 1200U, ///< Video Codec SDK 12.0. + sdk_13_0 = 1300U, ///< Video Codec SDK 13.0. + }; + + /** + * @brief Convert the packed driver API version to a comparable integer. + * + * @param version Version returned by `NvEncodeAPIGetMaxSupportedVersion()`. + * @return Version encoded as `major * 100 + minor`. + */ + constexpr std::uint32_t decode_nvenc_driver_version(std::uint32_t version) { + return (version >> 4U) * 100U + (version & 0x0FU); + } + + /** + * @brief Select the newest compiled SDK supported by the installed driver. + * + * @param max_version Maximum driver API version encoded as `major * 100 + minor`. + * @return Selected SDK implementation, or `unsupported` when the driver is too old. + */ + constexpr nvenc_sdk_version select_nvenc_sdk_version(std::uint32_t max_version) { + using enum nvenc_sdk_version; + if (max_version >= std::to_underlying(sdk_13_0)) { + return sdk_13_0; + } + if (max_version >= std::to_underlying(sdk_12_0)) { + return sdk_12_0; + } + if (max_version >= std::to_underlying(sdk_11_0)) { + return sdk_11_0; + } + return unsupported; + } + +} // namespace nvenc diff --git a/src/platform/common.h b/src/platform/common.h index 24da9f53c..9aa7c47d8 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -69,7 +69,7 @@ namespace video { } // namespace video namespace nvenc { - class nvenc_base; + class nvenc_encoder; } namespace platf { @@ -638,7 +638,7 @@ namespace platf { */ virtual bool init_encoder(const video::config_t &client_config, const video::sunshine_colorspace_t &colorspace) = 0; - nvenc::nvenc_base *nvenc = nullptr; ///< NVENC encoder instance owned by the encode device. + nvenc::nvenc_encoder *nvenc = nullptr; ///< NVENC encoder instance owned by the encode device. }; /** diff --git a/src/platform/windows/display_vram.cpp b/src/platform/windows/display_vram.cpp index 5e5f4ff97..db32d9ccf 100644 --- a/src/platform/windows/display_vram.cpp +++ b/src/platform/windows/display_vram.cpp @@ -24,9 +24,7 @@ extern "C" { #include "src/config.h" #include "src/logging.h" #include "src/nvenc/nvenc_config.h" -#include "src/nvenc/nvenc_d3d11_native.h" -#include "src/nvenc/nvenc_d3d11_on_cuda.h" -#include "src/nvenc/nvenc_utils.h" +#include "src/nvenc/nvenc_dynamic_factory.h" #include "src/video.h" #include "utf_utils.h" @@ -1240,21 +1238,25 @@ namespace platf::dxgi { * @return True when the D3D11 device resources are initialized. */ bool init_device(std::shared_ptr display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) { - buffer_format = nvenc::nvenc_format_from_sunshine_format(pix_fmt); - if (buffer_format == NV_ENC_BUFFER_FORMAT_UNDEFINED) { - BOOST_LOG(error) << "Unexpected pixel format for NvENC ["sv << from_pix_fmt(pix_fmt) << ']'; - return false; - } - if (base.init(display, adapter_p, pix_fmt)) { return false; } - if (pix_fmt == pix_fmt_e::yuv444p16) { - nvenc_d3d = std::make_unique(base.device.get()); - } else { - nvenc_d3d = std::make_unique(base.device.get()); + auto factory = nvenc::nvenc_dynamic_factory::get(); + if (!factory) { + return false; } + + if (pix_fmt == pix_fmt_e::yuv444p16) { + nvenc_d3d = factory->create_nvenc_d3d11_on_cuda(base.device.get()); + } else { + nvenc_d3d = factory->create_nvenc_d3d11_native(base.device.get()); + } + if (!nvenc_d3d) { + return false; + } + + buffer_format = pix_fmt; nvenc = nvenc_d3d.get(); return true; @@ -1272,8 +1274,7 @@ namespace platf::dxgi { return false; } - auto nvenc_colorspace = nvenc::nvenc_colorspace_from_sunshine_colorspace(colorspace); - if (!nvenc_d3d->create_encoder(config::video.nv, client_config, nvenc_colorspace, buffer_format)) { + if (!nvenc_d3d->create_encoder(config::video.nv, client_config, colorspace, buffer_format)) { return false; } @@ -1293,8 +1294,8 @@ namespace platf::dxgi { private: d3d_base_encode_device base; - std::unique_ptr nvenc_d3d; - NV_ENC_BUFFER_FORMAT buffer_format = NV_ENC_BUFFER_FORMAT_UNDEFINED; + std::unique_ptr nvenc_d3d; + platf::pix_fmt_e buffer_format = platf::pix_fmt_e::unknown; }; /** diff --git a/src/video.cpp b/src/video.cpp index 02f996df5..e8a7f11ca 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -18,6 +18,9 @@ extern "C" { #include #include #include +#if !defined(_WIN32) && !defined(__APPLE__) + #include +#endif } // local includes @@ -27,7 +30,7 @@ extern "C" { #include "globals.h" #include "input.h" #include "logging.h" -#include "nvenc/nvenc_base.h" +#include "nvenc/nvenc_encoder.h" #include "platform/common.h" #include "sync.h" #include "video.h" diff --git a/tests/unit/test_nvenc_dynamic_factory.cpp b/tests/unit/test_nvenc_dynamic_factory.cpp new file mode 100644 index 000000000..11841d3e3 --- /dev/null +++ b/tests/unit/test_nvenc_dynamic_factory.cpp @@ -0,0 +1,212 @@ +/** + * @file tests/unit/test_nvenc_dynamic_factory.cpp + * @brief Tests for the Windows runtime NVENC SDK factory. + */ +#ifdef _WIN32 + + // standard includes + #include + #include + #include + #include + #include + #include + + // lib includes + #include + + // local includes + #include "src/nvenc/nvenc_dynamic_factory.h" + +namespace { + + std::uint32_t reported_version; ///< Version returned by the fake NVENC driver. + std::uint32_t reported_status; ///< Status returned by the fake NVENC driver. + + /** + * @brief Fake implementation of `NvEncodeAPIGetMaxSupportedVersion()`. + * + * @param version Receives the configured packed API version. + * @return Configured NVENC status code. + */ + std::uint32_t WINAPI get_fake_max_supported_version(std::uint32_t *version) { + *version = reported_version; + return reported_status; + } + + /** + * @brief Minimal SDK-neutral encoder used to verify factory callbacks. + */ + class fake_nvenc_encoder final: public nvenc::nvenc_d3d11_interface { + public: + bool create_encoder( + const nvenc::nvenc_config &, + const video::config_t &, + const video::sunshine_colorspace_t &, + platf::pix_fmt_e + ) override { + return true; + } + + void destroy_encoder() override { + } + + nvenc::nvenc_encoded_frame encode_frame(std::uint64_t frame_index, bool force_idr) override { + return {{}, frame_index, force_idr, false}; + } + + bool invalidate_ref_frames(std::uint64_t, std::uint64_t) override { + return true; + } + + ID3D11Texture2D *get_input_texture() override { + return nullptr; + } + }; + + /** + * @brief Make a non-owning fake Windows module handle for factory discovery tests. + * + * @return Shared fake module handle. + */ + nvenc::shared_dll make_fake_dll() { + constexpr std::uintptr_t fake_handle_value = 1U; + return { + reinterpret_cast(fake_handle_value), + [](HMODULE) { + }, + }; + } + + /** + * @brief Make runtime operations that expose the fake version query function. + * + * @return Runtime operations for a successfully loaded fake NVENC driver. + */ + nvenc::nvenc_runtime_api make_fake_runtime_api() { + return { + []() { + return make_fake_dll(); + }, + [](HMODULE dll, const char *symbol) { + EXPECT_EQ(dll, make_fake_dll().get()); + EXPECT_EQ(std::string_view {symbol}, "NvEncodeAPIGetMaxSupportedVersion"); + return std::bit_cast(&get_fake_max_supported_version); + }, + }; + } + + TEST(NvencDynamicFactoryTest, HandlesUnavailableDriver) { + bool resolved_symbol = false; + const nvenc::nvenc_runtime_api runtime_api { + []() { + return nvenc::shared_dll {}; + }, + [&resolved_symbol](HMODULE, const char *) { + resolved_symbol = true; + return FARPROC {}; + }, + }; + + EXPECT_FALSE(nvenc::nvenc_dynamic_factory::get(runtime_api)); + EXPECT_FALSE(resolved_symbol); + } + + TEST(NvencDynamicFactoryTest, HandlesMissingVersionQuery) { + const nvenc::nvenc_runtime_api runtime_api { + []() { + return make_fake_dll(); + }, + [](HMODULE, const char *) { + return FARPROC {}; + }, + }; + + EXPECT_FALSE(nvenc::nvenc_dynamic_factory::get(runtime_api)); + } + + TEST(NvencDynamicFactoryTest, HandlesFailedVersionQuery) { + reported_version = 1300U; + reported_status = 1U; + + EXPECT_FALSE(nvenc::nvenc_dynamic_factory::get(make_fake_runtime_api())); + } + + TEST(NvencDynamicFactoryTest, RejectsUnsupportedDriver) { + reported_version = 10U << 4U; + reported_status = 0U; + + EXPECT_FALSE(nvenc::nvenc_dynamic_factory::get(make_fake_runtime_api())); + } + + TEST(NvencDynamicFactoryTest, SelectsSupportedSdkImplementations) { + using enum nvenc::nvenc_sdk_version; + constexpr std::array test_cases { + std::pair {11U << 4U, sdk_11_0}, + std::pair {12U << 4U, sdk_12_0}, + std::pair {13U << 4U, sdk_13_0}, + std::pair {14U << 4U, sdk_13_0}, + }; + reported_status = 0U; + + for (const auto &[version, expected] : test_cases) { + reported_version = version; + const auto factory = nvenc::nvenc_dynamic_factory::get(make_fake_runtime_api()); + ASSERT_TRUE(factory); + EXPECT_EQ(factory->sdk_version(), expected); + } + } + + TEST(NvencDynamicFactoryTest, UsesConfiguredEncoderConstructors) { + bool created_native = false; + bool created_on_cuda = false; + const auto dll = make_fake_dll(); + nvenc::nvenc_dynamic_factory factory { + dll, + nvenc::nvenc_sdk_version::sdk_13_0, + [&created_native, &dll](ID3D11Device *, nvenc::shared_dll callback_dll) { + created_native = true; + EXPECT_EQ(callback_dll, dll); + return std::make_unique(); + }, + [&created_on_cuda, &dll](ID3D11Device *, nvenc::shared_dll callback_dll) { + created_on_cuda = true; + EXPECT_EQ(callback_dll, dll); + return std::make_unique(); + }, + }; + + EXPECT_EQ(factory.sdk_version(), nvenc::nvenc_sdk_version::sdk_13_0); + EXPECT_TRUE(factory.create_nvenc_d3d11_native(nullptr)); + EXPECT_TRUE(factory.create_nvenc_d3d11_on_cuda(nullptr)); + EXPECT_TRUE(created_native); + EXPECT_TRUE(created_on_cuda); + } + + TEST(NvencDynamicFactoryTest, UsesDefaultWindowsRuntime) { + const auto factory = nvenc::nvenc_dynamic_factory::get(); + EXPECT_TRUE(!factory || factory->sdk_version() != nvenc::nvenc_sdk_version::unsupported); + } + + TEST(NvencSharedDllTest, OwnsLoadedModuleUntilLastReference) { + const auto handle = LoadLibraryEx("version.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); + ASSERT_NE(handle, nullptr); + + auto dll = nvenc::make_shared_dll(handle); + EXPECT_EQ(dll.get(), handle); + auto copy = dll; + EXPECT_EQ(copy.use_count(), 2); + + dll.reset(); + EXPECT_EQ(copy.use_count(), 1); + copy.reset(); + } + + TEST(NvencSharedDllTest, AcceptsNullModule) { + auto dll = nvenc::make_shared_dll(nullptr); + EXPECT_FALSE(dll); + dll.reset(); + } + +} // namespace +#endif diff --git a/tests/unit/test_nvenc_version.cpp b/tests/unit/test_nvenc_version.cpp new file mode 100644 index 000000000..077feefc5 --- /dev/null +++ b/tests/unit/test_nvenc_version.cpp @@ -0,0 +1,52 @@ +/** + * @file tests/unit/test_nvenc_version.cpp + * @brief Tests for runtime NVENC SDK version selection. + */ + +// standard includes +#include +#include + +// lib includes +#include + +// local includes +#include "src/nvenc/nvenc_version.h" + +namespace { + + /** + * @brief Expected SDK selection for a driver API version. + */ + struct nvenc_version_test_case { + std::uint32_t max_version; ///< Maximum API version reported by the driver. + nvenc::nvenc_sdk_version expected; ///< SDK implementation Sunshine should select. + }; + + TEST(NvencVersionTest, DecodesPackedDriverVersion) { + EXPECT_EQ(nvenc::decode_nvenc_driver_version((11U << 4U) | 0U), 1100U); + EXPECT_EQ(nvenc::decode_nvenc_driver_version((13U << 4U) | 1U), 1301U); + } + + TEST(NvencVersionTest, SelectsNewestCompatibleSdk) { + using enum nvenc::nvenc_sdk_version; + constexpr std::array test_cases { + nvenc_version_test_case {1000U, unsupported}, + nvenc_version_test_case {1099U, unsupported}, + nvenc_version_test_case {1100U, sdk_11_0}, + nvenc_version_test_case {1101U, sdk_11_0}, + nvenc_version_test_case {1199U, sdk_11_0}, + nvenc_version_test_case {1200U, sdk_12_0}, + nvenc_version_test_case {1201U, sdk_12_0}, + nvenc_version_test_case {1299U, sdk_12_0}, + nvenc_version_test_case {1300U, sdk_13_0}, + nvenc_version_test_case {1301U, sdk_13_0}, + nvenc_version_test_case {1400U, sdk_13_0}, + }; + + for (const auto &[max_version, expected] : test_cases) { + EXPECT_EQ(nvenc::select_nvenc_sdk_version(max_version), expected); + } + } + +} // namespace