ref:fb3d85cff4698c3e3aaa1be6861abf45a0ed1f1d

fix(nvenc): dynamic nv codec sdk selection (#5451)

SHA: fb3d85cff4698c3e3aaa1be6861abf45a0ed1f1d
Author: Dave Lane <42013603+ReenigneArcher@users.noreply.github.com>
Date: 2026-07-27 01:06
Parents: 7cb9207
32 files changed +3263 -480
Type
cmake/compile_definitions/common.cmake +50 −4
@@ -59,11 +59,53 @@
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"
)
set(NVENC_SOURCES ${NVENC_PUBLIC_SOURCES})
file(GLOB NVENC_SOURCES CONFIGURE_DEPENDS "src/nvenc/*.cpp" "src/nvenc/*.h")
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
$<TARGET_OBJECTS:nvenc_sdk_1100>
$<TARGET_OBJECTS:nvenc_sdk_1200>
$<TARGET_OBJECTS:nvenc_sdk_1300>
)
endif()
list(APPEND PLATFORM_TARGET_FILES ${NVENC_SOURCES})
set(SUNSHINE_TARGET_FILES
@@ -154,6 +196,10 @@
${FFMPEG_INCLUDE_DIRS}
${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/cpm/.cmake-lint-ignore +0 −0
cmake/cpm/CPM.cmake +1379 −0
@@ -1,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 <name>-<version> (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()
cmake/dependencies/common.cmake +1 −0
@@ -29,6 +29,7 @@
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)
cmake/dependencies/nv_codec_headers.cmake +9 −0
@@ -1,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()
CMakeLists.txt +4 −0
@@ -38,5 +38,9 @@
# 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)
package-lock.cmake +67 −0
@@ -1,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(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)$`
# - `set(EXAMPLE_TAG v1.2.3)`
# - `set(EXAMPLE_SHA256 <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(?<major>11)\.(?<minor>0)\.(?<patch>\d+)\.(?<build>\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(?<major>12)\.(?<minor>0)\.(?<patch>\d+)\.(?<build>\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(?<major>13)\.(?<minor>0)\.(?<patch>\d+)\.(?<build>\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
)
src/nvenc/nvenc_base.cpp +522 −378
@@ -6,29 +6,120 @@
#include "nvenc_base.h"
// standard includes
#include <algorithm>
#include <array>
#include <cmath>
#include <cstring>
#include <format>
#include <string_view>
#include <utility>
#include <vector>
// local includes
#include "nvenc_utils.h"
#include "src/config.h"
#include "src/logging.h"
#include "src/utility.h"
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<GUID> &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 ";
/**
* @def MAKE_NVENC_VER(major, minor)
* @brief Macro for MAKE NVENC VER.
*/
#define MAKE_NVENC_VER(major, minor) ((major) | ((minor) << 24))
case 1:
return "HEVC ";
case 2:
return "AV1 ";
default:
return " ";
}
}
// 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
/**
* @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<std::pair<NVENCSTATUS, std::string_view>>({
{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};
namespace {
}
GUID quality_preset_guid_from_number(unsigned number) {
if (number > 7) {
@@ -91,7 +182,7 @@
} // namespace
namespace nvenc {
namespace NVENC_NAMESPACE {
nvenc_base::nvenc_base(NV_ENC_DEVICE_TYPE device_type):
device_type(device_type) {
@@ -101,11 +192,385 @@
// Use destroy_encoder() instead
}
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;
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 value = 0;
if (nvenc->nvEncGetEncodeCaps(encoder, encode_guid, &param, &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 (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(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(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;
}
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;
}
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
}
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;
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(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;
}
}
}
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<std::uint32_t>(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<typename FormatConfig>
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;
};
if constexpr (requires { format_config.h264VUIParameters; }) {
configure_vui(format_config.h264VUIParameters);
} else {
configure_vui(format_config.hevcVUIParameters);
}
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;
if (nvenc_failed(nvenc->nvEncRegisterAsyncEvent(encoder, &event_params))) {
BOOST_LOG(error) << "NvEnc: NvEncRegisterAsyncEvent() failed: " << last_nvenc_error_string;
return false;
}
}
NV_ENC_CREATE_BITSTREAM_BUFFER create_bitstream_buffer = {NV_ENC_CREATE_BITSTREAM_BUFFER_VER};
if (nvenc_failed(nvenc->nvEncCreateBitstreamBuffer(encoder, &create_bitstream_buffer))) {
BOOST_LOG(error) << "NvEnc: NvEncCreateBitstreamBuffer() failed: " << last_nvenc_error_string;
return false;
}
output_bitstream = create_bitstream_buffer.bitstreamBuffer;
return 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();
}
@@ -113,6 +578,12 @@
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;
@@ -127,101 +598,53 @@
return false;
}
std::uint32_t encode_guid_count = 0;
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<GUID> encode_guids(encode_guid_count);
if (nvenc_failed(nvenc->nvEncGetEncodeGUIDs(
if (nvenc_failed(nvenc->nvEncGetEncodeGUIDs(encoder, encode_guids.data(), (uint32_t) encode_guids.size(), &encode_guid_count))) {
encoder,
encode_guids.data(),
static_cast<std::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;
#if NVENC_SDK_VERSION >= 1200
case 2:
// AV1
init_params.encodeGUID = NV_ENC_CODEC_AV1_GUID;
break;
#endif
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, &param, &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 (!contains_guid(encode_guids, init_params.encodeGUID)) {
BOOST_LOG(error) << "NvEnc: encoding format is not supported by the gpu";
}
}
if (buffer_is_10bit() && !get_encoder_cap(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)) {
BOOST_LOG(error) << "NvEnc: gpu doesn't support YUV444 encode";
if (!validate_encoder_capabilities(init_params.encodeGUID, buffer_format)) {
return false;
}
if (async_event_handle && !get_encoder_cap(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(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(init_params.encodeGUID, NV_ENC_CAPS_SUPPORT_WEIGHTED_PREDICTION);
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;
@@ -229,276 +652,37 @@
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;
}
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))) {
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;
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;
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)) {
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;
};
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 {
configure_rate_control(enc_config, config, client_config, init_params.encodeGUID);
configure_codec(enc_config, config, client_config, colorspace, buffer_format, init_params.encodeGUID);
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;
}
}
init_params.encodeConfig = &enc_config;
if (nvenc_failed(nvenc->nvEncInitializeEncoder(encoder, &init_params))) {
BOOST_LOG(error) << "NvEnc: NvEncInitializeEncoder() failed: " << last_nvenc_error_string;
if (!initialize_encoder_resources(init_params)) {
return false;
}
if (async_event_handle) {
NV_ENC_EVENT_PARAMS event_params = {NV_ENC_EVENT_PARAMS_VER};
event_params.completionEvent = async_event_handle;
if (nvenc_failed(nvenc->nvEncRegisterAsyncEvent(encoder, &event_params))) {
BOOST_LOG(error) << "NvEnc: NvEncRegisterAsyncEvent() failed: " << last_nvenc_error_string;
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);
}
NV_ENC_CREATE_BITSTREAM_BUFFER create_bitstream_buffer = {NV_ENC_CREATE_BITSTREAM_BUFFER_VER};
if (nvenc_failed(nvenc->nvEncCreateBitstreamBuffer(encoder, &create_bitstream_buffer))) {
BOOST_LOG(error) << "NvEnc: NvEncCreateBitstreamBuffer() failed: " << last_nvenc_error_string;
return false;
}
output_bitstream = create_bitstream_buffer.bitstreamBuffer;
if (!create_and_register_input_buffer()) {
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";
}
{
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;
}
encoder_state = {};
fail_guard.disable();
return true;
@@ -535,7 +719,7 @@
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 @@
}
auto data_pointer = (uint8_t *) lock_bitstream.bitstreamBufferPtr;
::nvenc::nvenc_encoded_frame encoded_frame {
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 @@
}
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 @@
if (!last_nvenc_error_string.empty()) last_nvenc_error_string += " ";
}
*/
last_nvenc_error_string += nvenc_status_string(status);
last_nvenc_error_string += status_string(status);
return true;
}
return false;
}
} // namespace NVENC_NAMESPACE
} // namespace nvenc
src/nvenc/nvenc_base.h +202 −11
@@ -4,32 +4,33 @@
*/
#pragma once
// lib includes
#include <ffnvcodec/nvEncodeAPI.h>
// 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 @@
* @param buffer_format Platform-agnostic input surface format.
* @return `true` on success, `false` on error
*/
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
bool create_encoder(const nvenc_config &config, const video::config_t &client_config, const nvenc_colorspace_t &colorspace, NV_ENC_BUFFER_FORMAT buffer_format);
) override;
/**
* @brief Destroy the encoder.
* Derived classes classes call it in the destructor.
*/
void destroy_encoder() override;
void destroy_encoder();
/**
* @brief Encode the next frame using platform-specific input surface.
@@ -58,7 +64,7 @@
* @param force_idr Whether to encode frame as forced IDR.
* @return Encoded frame.
*/
::nvenc::nvenc_encoded_frame encode_frame(uint64_t frame_index, bool force_idr) override;
nvenc_encoded_frame encode_frame(uint64_t frame_index, bool force_idr);
/**
* @brief Perform reference frame invalidation (RFI) procedure.
@@ -67,7 +73,7 @@
* @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 @@
///< 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<typename FormatConfig>
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 @@
} encoder_state;
};
} // namespace NVENC_NAMESPACE
} // namespace nvenc
src/nvenc/nvenc_colorspace.h +4 −4
@@ -4,10 +4,10 @@
*/
#pragma once
// lib includes
#include <ffnvcodec/nvEncodeAPI.h>
// local includes
#include "nvenc_sdk.h"
namespace nvenc {
namespace NVENC_NAMESPACE {
/**
* @brief YUV colorspace and color range.
@@ -19,4 +19,4 @@
bool full_range; ///< Whether the video range is full-range instead of limited.
};
} // namespace nvenc
} // namespace NVENC_NAMESPACE
src/nvenc/nvenc_d3d11.cpp +19 −34
@@ -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::shared_dll dll):
nvenc_base(device_type),
nvenc_d3d11::nvenc_d3d11(NV_ENC_DEVICE_TYPE device_type):
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;
}
auto create_instance = reinterpret_cast<decltype(NvEncodeAPICreateInstance) *>(
#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")) {
GetProcAddress(dll.get(), "NvEncodeAPICreateInstance")
);
if (!create_instance) {
BOOST_LOG(error) << "NvEnc: No NvEncodeAPICreateInstance() in NVENC driver library";
return false;
auto new_nvenc = std::make_unique<NV_ENCODE_API_FUNCTION_LIST>();
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;
}
if (dll) {
FreeLibrary(dll);
auto new_nvenc = std::make_unique<NV_ENCODE_API_FUNCTION_LIST>();
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;
dll = nullptr;
}
nvenc = std::move(new_nvenc);
return false;
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
} // namespace nvenc
#endif
src/nvenc/nvenc_d3d11.h +10 −7
@@ -11,8 +11,10 @@
// local includes
#include "nvenc_base.h"
#include "nvenc_d3d11_interface.h"
#include "nvenc_shared_dll.h"
namespace NVENC_NAMESPACE {
namespace nvenc {
#ifdef DOXYGEN
/**
@@ -42,29 +44,30 @@
* @brief Abstract Direct3D11 NVENC encoder.
* Encapsulates common code used by native and interop implementations.
*/
class nvenc_d3d11: public nvenc_base, public ::nvenc::nvenc_d3d11_interface {
class nvenc_d3d11: public nvenc_base {
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::shared_dll dll);
~nvenc_d3d11() override;
explicit nvenc_d3d11(NV_ENC_DEVICE_TYPE device_type);
~nvenc_d3d11();
/**
* @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
src/nvenc/nvenc_d3d11_interface.h +36 −0
@@ -1,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 <d3d11.h>
// 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
src/nvenc/nvenc_d3d11_native.cpp +4 −4
@@ -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 @@
return true;
}
} // namespace nvenc
} // namespace NVENC_NAMESPACE
#endif
src/nvenc/nvenc_d3d11_native.h +5 −4
@@ -11,7 +11,7 @@
// local includes
#include "nvenc_d3d11.h"
namespace nvenc {
namespace NVENC_NAMESPACE {
/**
* @brief Native Direct3D11 NVENC encoder.
@@ -20,8 +20,9 @@
public:
/**
* @param d3d_device Direct3D11 device used for encoding.
* @param dll Shared NVENC driver module.
*/
explicit nvenc_d3d11_native(ID3D11Device *d3d_device, ::nvenc::shared_dll dll);
~nvenc_d3d11_native() override;
explicit nvenc_d3d11_native(ID3D11Device *d3d_device);
~nvenc_d3d11_native();
ID3D11Texture2D *get_input_texture() override;
@@ -33,5 +34,5 @@
ID3D11Texture2DPtr d3d_input_texture;
};
} // namespace nvenc
} // namespace NVENC_NAMESPACE
#endif
src/nvenc/nvenc_d3d11_on_cuda.cpp +4 −4
@@ -9,9 +9,9 @@
// 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
#endif
src/nvenc/nvenc_d3d11_on_cuda.h +6 −7
@@ -4,13 +4,11 @@
*/
#pragma once
#ifdef _WIN32
// lib includes
#include <ffnvcodec/dynlink_cuda.h>
// local includes
#include "nvenc_d3d11.h"
#include "nvenc_sdk.h"
namespace nvenc {
namespace NVENC_NAMESPACE {
/**
* @brief Interop Direct3D11 on CUDA NVENC encoder.
@@ -21,8 +19,9 @@
/**
* @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 @@
size_t cuda_surface_pitch = 0;
};
} // namespace NVENC_NAMESPACE
} // namespace nvenc
#endif
src/nvenc/nvenc_dynamic_factory.cpp +129 −0
@@ -1,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 <bit>
#include <utility>
// 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> 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> 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<get_max_supported_version_fn>(
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<nvenc_dynamic_factory>(
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<nvenc_dynamic_factory>(
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<nvenc_dynamic_factory>(
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_d3d11_interface> nvenc_dynamic_factory::create_nvenc_d3d11_native(
ID3D11Device *d3d_device
) const {
return create_native(d3d_device, dll);
}
std::unique_ptr<nvenc_d3d11_interface> 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
src/nvenc/nvenc_dynamic_factory.h +106 −0
@@ -1,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 <functional>
#include <memory>
// 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<shared_dll()>; ///< Load the NVENC driver module.
/**
* @brief Resolve an export from the loaded NVENC driver module.
*/
using get_symbol_fn = std::function<FARPROC(HMODULE, const char *)>;
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<std::unique_ptr<nvenc_d3d11_interface>(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<nvenc_dynamic_factory> 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<nvenc_dynamic_factory> 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<nvenc_d3d11_interface> 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<nvenc_d3d11_interface> 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
src/nvenc/nvenc_dynamic_factory_impl.cpp +48 −0
@@ -1,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 <utility>
// 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_d3d11_interface> NVENC_CONCAT(create_nvenc_d3d11_native_, NVENC_FACTORY_SUFFIX)(
ID3D11Device *device,
shared_dll dll
) {
return std::make_unique<NVENC_NAMESPACE::nvenc_d3d11_native>(device, std::move(dll));
}
std::unique_ptr<nvenc_d3d11_interface> NVENC_CONCAT(create_nvenc_d3d11_on_cuda_, NVENC_FACTORY_SUFFIX)(
ID3D11Device *device,
shared_dll dll
) {
return std::make_unique<NVENC_NAMESPACE::nvenc_d3d11_on_cuda>(device, std::move(dll));
}
} // namespace nvenc::detail
#undef NVENC_CONCAT
#undef NVENC_CONCAT_IMPL
#endif
#endif
src/nvenc/nvenc_dynamic_factory_versions.h +67 −0
@@ -1,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 <memory>
// 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<nvenc_d3d11_interface> 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<nvenc_d3d11_interface> 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<nvenc_d3d11_interface> 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<nvenc_d3d11_interface> 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<nvenc_d3d11_interface> 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<nvenc_d3d11_interface> create_nvenc_d3d11_on_cuda_1300(ID3D11Device *device, shared_dll dll);
} // namespace nvenc::detail
#endif
src/nvenc/nvenc_encoder.h +75 −0
@@ -1,0 +1,75 @@
/**
* @file src/nvenc/nvenc_encoder.h
* @brief Declarations for the SDK-neutral NVENC encoder interface.
*/
#pragma once
// standard includes
#include <cstdint>
// 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
src/nvenc/nvenc_sdk.h +125 −0
@@ -1,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 <cstddef>
#include <cstdint>
#ifdef _WIN32
#include <Windows.h>
#endif
// The version-specific namespace is populated by the SDK headers included below.
namespace NVENC_NAMESPACE { // NOSONAR(cpp:S3261)
#ifndef DOXYGEN
#include <ffnvcodec/dynlink_cuda.h>
#include <ffnvcodec/nvEncodeAPI.h>
#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
src/nvenc/nvenc_shared_dll.h +49 −0
@@ -1,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 <memory>
#include <type_traits>
// platform includes
#include <Windows.h>
namespace nvenc {
/**
* @brief Shared ownership wrapper for a loaded Windows module.
*/
using shared_dll = std::shared_ptr<std::remove_pointer_t<HMODULE>>;
/**
* @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
src/nvenc/nvenc_utils.cpp +2 −2
@@ -8,7 +8,7 @@
// local includes
#include "nvenc_utils.h"
namespace nvenc {
namespace NVENC_NAMESPACE {
#ifdef _WIN32
/**
@@ -102,4 +102,4 @@
return colorspace;
}
} // namespace nvenc
} // namespace NVENC_NAMESPACE
src/nvenc/nvenc_utils.h +3 −5
@@ -9,15 +9,13 @@
#include <dxgiformat.h>
#endif
// lib includes
#include <ffnvcodec/nvEncodeAPI.h>
// 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 @@
*/
nvenc_colorspace_t nvenc_colorspace_from_sunshine_colorspace(const video::sunshine_colorspace_t &sunshine_colorspace);
} // namespace nvenc
} // namespace NVENC_NAMESPACE
src/nvenc/nvenc_version.h +53 −0
@@ -1,0 +1,53 @@
/**
* @file src/nvenc/nvenc_version.h
* @brief NVENC SDK version selection helpers.
*/
#pragma once
// standard includes
#include <cstdint>
#include <utility>
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
src/platform/common.h +2 −2
@@ -69,7 +69,7 @@
} // namespace video
namespace nvenc {
class nvenc_base;
class nvenc_encoder;
}
namespace platf {
@@ -638,7 +638,7 @@
*/
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.
};
/**
src/platform/windows/display_vram.cpp +14 −13
@@ -24,8 +24,6 @@
#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 @@
* @return True when the D3D11 device resources are initialized.
*/
bool init_device(std::shared_ptr<platf::display_t> 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) << ']';
if (base.init(display, adapter_p, pix_fmt)) {
return false;
}
if (base.init(display, adapter_p, pix_fmt)) {
auto factory = nvenc::nvenc_dynamic_factory::get();
if (!factory) {
return false;
}
if (pix_fmt == pix_fmt_e::yuv444p16) {
nvenc_d3d = std::make_unique<nvenc::nvenc_d3d11_on_cuda>(base.device.get());
nvenc_d3d = factory->create_nvenc_d3d11_on_cuda(base.device.get());
} else {
nvenc_d3d = factory->create_nvenc_d3d11_native(base.device.get());
nvenc_d3d = std::make_unique<nvenc::nvenc_d3d11_native>(base.device.get());
}
if (!nvenc_d3d) {
return false;
}
buffer_format = pix_fmt;
nvenc = nvenc_d3d.get();
return true;
@@ -1272,7 +1274,6 @@
return false;
}
if (!nvenc_d3d->create_encoder(config::video.nv, client_config, colorspace, buffer_format)) {
auto nvenc_colorspace = nvenc::nvenc_colorspace_from_sunshine_colorspace(colorspace);
if (!nvenc_d3d->create_encoder(config::video.nv, client_config, nvenc_colorspace, buffer_format)) {
return false;
}
@@ -1293,8 +1294,8 @@
private:
d3d_base_encode_device base;
std::unique_ptr<nvenc::nvenc_d3d11> nvenc_d3d;
NV_ENC_BUFFER_FORMAT buffer_format = NV_ENC_BUFFER_FORMAT_UNDEFINED;
std::unique_ptr<nvenc::nvenc_d3d11_interface> nvenc_d3d;
platf::pix_fmt_e buffer_format = platf::pix_fmt_e::unknown;
};
/**
src/video.cpp +4 −1
@@ -18,6 +18,9 @@
#include <libavutil/mastering_display_metadata.h>
#include <libavutil/opt.h>
#include <libavutil/pixdesc.h>
#if !defined(_WIN32) && !defined(__APPLE__)
#include <ffnvcodec/nvEncodeAPI.h>
#endif
}
// local includes
@@ -27,7 +30,7 @@
#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"
tests/unit/test_nvenc_dynamic_factory.cpp +212 −0
@@ -1,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 <array>
#include <bit>
#include <cstdint>
#include <memory>
#include <string_view>
#include <utility>
// lib includes
#include <gtest/gtest.h>
// 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<HMODULE>(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<FARPROC>(&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<fake_nvenc_encoder>();
},
[&created_on_cuda, &dll](ID3D11Device *, nvenc::shared_dll callback_dll) {
created_on_cuda = true;
EXPECT_EQ(callback_dll, dll);
return std::make_unique<fake_nvenc_encoder>();
},
};
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
tests/unit/test_nvenc_version.cpp +52 −0
@@ -1,0 +1,52 @@
/**
* @file tests/unit/test_nvenc_version.cpp
* @brief Tests for runtime NVENC SDK version selection.
*/
// standard includes
#include <array>
#include <cstdint>
// lib includes
#include <gtest/gtest.h>
// 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