diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c7bba1e38..ca00dc525c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -329,6 +329,10 @@ endif() add_subdirectory(src) +if(SCORE_STATIC_PLUGINS AND TARGET EncoderTester) + target_link_libraries(EncoderTester PRIVATE ${SCORE_PLUGINS_LIST}) +endif() + include(cmake/ScoreFeatureCheck.cmake) if(EXISTS Documentation/Models/score.qmodel) diff --git a/cmake/ScoreAvndHelper.cmake b/cmake/ScoreAvndHelper.cmake index 02b718c557..29e3c3bc39 100644 --- a/cmake/ScoreAvndHelper.cmake +++ b/cmake/ScoreAvndHelper.cmake @@ -22,42 +22,54 @@ endfunction() function(avnd_score_plugin_finalize) cmake_parse_arguments(AVND "CUSTOM_PLUGIN;MODULE" "BASE_TARGET;PLUGIN_VERSION;PLUGIN_UUID" "" ${ARGN}) + # Each addon's generated files live in their own directory, exposed only to + # targets that link the addon. score_static_plugins.hpp registers plug-ins + # behind `#if __has_include()`, so a header sitting in + # CMAKE_BINARY_DIR -- which is on every target's include path -- makes that + # guard true everywhere, and any target compiling that header then references + # a constructor it does not link. Small unit tests are the ones that break. + set(AVND_GEN_DIR "${CMAKE_BINARY_DIR}/score_addons/${AVND_BASE_TARGET}") + if(NOT AVND_CUSTOM_PLUGIN) # Generate the score_plugin_foo.{h,c}pp configure_file( "${SCORE_AVND_SOURCE_DIR}/plugin_prototype.hpp.in" - "${CMAKE_BINARY_DIR}/${AVND_BASE_TARGET}.hpp" + "${AVND_GEN_DIR}/${AVND_BASE_TARGET}.hpp" @ONLY NEWLINE_STYLE LF ) if(AVND_MODULE) configure_file( "${SCORE_AVND_SOURCE_DIR}/module_plugin_prototype.cpp.in" - "${CMAKE_BINARY_DIR}/${AVND_BASE_TARGET}.cpp" + "${AVND_GEN_DIR}/${AVND_BASE_TARGET}.cpp" @ONLY NEWLINE_STYLE LF ) target_sources(${AVND_BASE_TARGET} PRIVATE FILE_SET CXX_MODULES FILES - "${CMAKE_BINARY_DIR}/${AVND_BASE_TARGET}.cpp" + "${AVND_GEN_DIR}/${AVND_BASE_TARGET}.cpp" ) else() configure_file( "${SCORE_AVND_SOURCE_DIR}/plugin_prototype.cpp.in" - "${CMAKE_BINARY_DIR}/${AVND_BASE_TARGET}.cpp" + "${AVND_GEN_DIR}/${AVND_BASE_TARGET}.cpp" @ONLY NEWLINE_STYLE LF ) target_sources(${AVND_BASE_TARGET} PRIVATE - "${CMAKE_BINARY_DIR}/${AVND_BASE_TARGET}.cpp" + "${AVND_GEN_DIR}/${AVND_BASE_TARGET}.cpp" ) endif() else() file(CONFIGURE OUTPUT - "${CMAKE_BINARY_DIR}/include.${AVND_BASE_TARGET}.cpp" + "${AVND_GEN_DIR}/include.${AVND_BASE_TARGET}.cpp" CONTENT "${AVND_ADDITIONAL_CLASSES}\nstatic void all_custom_factories(auto& fx, auto& ctx, auto& key) { ${AVND_CUSTOM_FACTORIES} }\n" NEWLINE_STYLE LF) endif() + # PUBLIC: the addon's own generated .cpp includes the header, and so must the + # app that links it -- but nothing else. + target_include_directories(${AVND_BASE_TARGET} PUBLIC "${AVND_GEN_DIR}") + setup_score_plugin(${AVND_BASE_TARGET}) target_link_libraries(${AVND_BASE_TARGET} PUBLIC score_plugin_engine score_plugin_avnd) diff --git a/src/app/main.cpp b/src/app/main.cpp index b824fb0c0d..128bffe84c 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -274,8 +274,12 @@ static void setup_x11(int argc, char** argv) helper_dylibs.run_under_x11 = true; helper_dylibs.xwayland = wayland; - // EGL is the only way to get zero-copy with dma-buf import - qputenv("QT_XCB_GL_INTEGRATION", "xcb_egl"); + // EGL is the only way to get zero-copy with dma-buf import, so it is + // the default -- but NVIDIA's GPUDirect-for-Video (libdvp) interops + // only with GLX, so a user who needs DVP must be able to ask for + // xcb_glx and have it stick. + if(qEnvironmentVariableIsEmpty("QT_XCB_GL_INTEGRATION")) + qputenv("QT_XCB_GL_INTEGRATION", "xcb_egl"); } } }; diff --git a/src/plugins/score-plugin-gfx/3rdparty/nv-dvp-bridge/CMakeLists.txt b/src/plugins/score-plugin-gfx/3rdparty/nv-dvp-bridge/CMakeLists.txt new file mode 100644 index 0000000000..8cbc91aeec --- /dev/null +++ b/src/plugins/score-plugin-gfx/3rdparty/nv-dvp-bridge/CMakeLists.txt @@ -0,0 +1,70 @@ +# nv-dvp-bridge: NVIDIA "GPUDirect for Video" runtime-loaded shim + +# C API wrapper. Cross-platform: Windows uses dvp.dll; Linux uses +# libdvp.so.1. Consumers (AJA, planned DeckLink) link this target via +# its score-plugin-gfx parent. +# +# The bridge itself has no NVIDIA-SDK headers as a build dependency — +# all DVP entry points are dlsym'd at runtime from the NVIDIA library. + +add_library(score_nv_dvp_bridge STATIC + "${CMAKE_CURRENT_SOURCE_DIR}/nv_dvp_bridge.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/nv_dvp_bridge.h" + "${CMAKE_CURRENT_SOURCE_DIR}/dvpapi_shim.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/dvpapi_shim.h" +) + +target_include_directories(score_nv_dvp_bridge + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" +) + +# Qt is used to identify the current GL context: libdvp's GL entry points +# only exist for GLX, and QNativeInterface::QGLXContext is the reliable way +# to know whether that is what we have. +target_link_libraries(score_nv_dvp_bridge PRIVATE ${QT_PREFIX}::Gui) + +if(WIN32) + # D3D11 path uses ID3D11Device / ID3D11Texture2D via d3d11.h. + target_link_libraries(score_nv_dvp_bridge PRIVATE d3d11) +else() + # Linux dlopen path needs libdl. + target_link_libraries(score_nv_dvp_bridge PRIVATE ${CMAKE_DL_LIBS}) +endif() + +target_compile_definitions(score_nv_dvp_bridge PUBLIC + SCORE_HAS_NV_DVP_BRIDGE=1 + # Bridge is built as a STATIC lib and linked into the consumer. The + # INLINE define makes NV_DVP_API empty (no dllexport/dllimport + # decoration) for both the bridge build and its consumers on Windows. + NV_DVP_BRIDGE_INLINE=1 +) + +set_property(TARGET score_nv_dvp_bridge PROPERTY POSITION_INDEPENDENT_CODE ON) + +# Optional dvp.dll fetch on Windows. Mirrors the previous AJA addon's +# SCORE_FETCH_DVP_DLL option so existing build invocations keep working. +if(WIN32 AND SCORE_FETCH_DVP_DLL) + set(_dvp_dll "${CMAKE_BINARY_DIR}/3rdparty/dvp/dvp.dll") + if(NOT EXISTS "${_dvp_dll}") + file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/3rdparty/dvp") + message(STATUS "nv-dvp-bridge: fetching dvp.dll v1.70 from PlusToolkit/PlusLib") + file(DOWNLOAD + "https://github.com/PlusToolkit/PlusLib/raw/master/Tools/NVidia/dvp170/bin/x64/dvp.dll" + "${_dvp_dll}" + EXPECTED_HASH SHA256=602f2cefecb9b7c67d4ffebb3b15e11291bacf5a7342fcec6d52dd0118cbaddd + STATUS _dvp_dl_status + SHOW_PROGRESS) + list(GET _dvp_dl_status 0 _dvp_dl_rc) + if(NOT _dvp_dl_rc EQUAL 0) + message(WARNING "nv-dvp-bridge: dvp.dll download failed: ${_dvp_dl_status}") + endif() + endif() + if(EXISTS "${_dvp_dll}") + # Expose path so the consuming addon can copy_if_different next to its DLL. + set(SCORE_NV_DVP_DLL "${_dvp_dll}" CACHE INTERNAL "Fetched dvp.dll path") + endif() +elseif(WIN32 AND AJA_DVP_DLL AND EXISTS "${AJA_DVP_DLL}") + set(SCORE_NV_DVP_DLL "${AJA_DVP_DLL}" CACHE INTERNAL "User-supplied dvp.dll path") +endif() + +message(STATUS "score-plugin-gfx: nv-dvp-bridge enabled") diff --git a/src/plugins/score-plugin-gfx/3rdparty/nv-dvp-bridge/dvpapi_shim.cpp b/src/plugins/score-plugin-gfx/3rdparty/nv-dvp-bridge/dvpapi_shim.cpp new file mode 100755 index 0000000000..719816d3bc --- /dev/null +++ b/src/plugins/score-plugin-gfx/3rdparty/nv-dvp-bridge/dvpapi_shim.cpp @@ -0,0 +1,407 @@ +/* + * NVIDIA "GPUDirect for Video" (DVP) runtime-loaded shim. + * See dvpapi_shim.h for design + license details. + * + * Adapted from Blender's intern/gpudirect/dvpapi.cpp (GPL-2.0+): + * Copyright (C) 2015 Blender Foundation. All rights reserved. + * + * Cross-platform — BOTH platforms ship libdvp as a C++ library (no + * `extern "C"`), so the shim resolves mangled symbol names on both: + * - Windows: MSVC C++ mangling. Names verified against `dvp.dll` v1.70 + * (SHA256 602f2cef…), available either from the PlusToolkit/PlusLib + * mirror or bundled with the Blackmagic DeckLink SDK at + * `decklink/Win/Samples/bin/dvp.dll`. Surface: D3D9 / D3D10 / D3D11, + * OpenGL, **CUDA** (dvpInitCUDAContext, dvpMemcpyCuda, …), plus + * sync primitives. We currently resolve only the GL + D3D11 subset. + * - Linux: GCC Itanium ABI mangling. Names verified against + * `libdvp.so.1` (same v1.70 ABI) shipped inside the DeckLink SDK at + * `decklink/Linux/Samples/NVIDIA_GPUDirect/x86_64/libdvp.so.1`. + * Surface: OpenGL + CUDA + `dvpCreateGPUBufferGL` (no D3D11, which + * doesn't exist on Linux). We currently resolve only the GL subset. + * + * The CUDA path (`dvpInit/Close/Bind/UnbindFromCUDACtx`, `dvpMemcpyCuda`, + * `dvpMapBuffer{Wait,End}CUDAStream`) is **cross-platform** on both + * Windows and Linux — score's existing GPU-direct pipeline uses + * `CudaInterop` (which is independent of DVP), but the DVP CUDA + * surface is a viable second path. + * + * To dump symbols for cross-checking after an SDK ABI change: + * dumpbin -exports dvp.dll (Windows) + * nm -D --defined-only libdvp.so.1 | sort (Linux) + */ + +#include "dvpapi_shim.h" + +#if defined(_WIN32) +# define WIN32_LEAN_AND_MEAN +# include +#else +# include +#endif + +#include +#include +#include +#include + +/* ============================================================================ + * Global function pointer storage (definitions for the externs in the + * header). All start nulled; nv_dvp_load_runtime fills them. + * ============================================================================ */ + +extern "C" { + +PFN_dvpInitGLContext dvpInitGLContext = nullptr; +PFN_dvpCloseGLContext dvpCloseGLContext = nullptr; +PFN_dvpGetLibraryVersion dvpGetLibraryVersion = nullptr; +PFN_dvpBegin dvpBegin = nullptr; +PFN_dvpEnd dvpEnd = nullptr; +PFN_dvpCreateBuffer dvpCreateBuffer = nullptr; +PFN_dvpDestroyBuffer dvpDestroyBuffer = nullptr; +PFN_dvpFreeBuffer dvpFreeBuffer = nullptr; +PFN_dvpMemcpyLined dvpMemcpyLined = nullptr; +PFN_dvpMemcpy dvpMemcpy = nullptr; +PFN_dvpImportSyncObject dvpImportSyncObject = nullptr; +PFN_dvpFreeSyncObject dvpFreeSyncObject = nullptr; +PFN_dvpSyncObjClientWaitPartial dvpSyncObjClientWaitPartial = nullptr; +PFN_dvpMapBufferEndAPI dvpMapBufferEndAPI = nullptr; +PFN_dvpMapBufferWaitDVP dvpMapBufferWaitDVP = nullptr; +PFN_dvpMapBufferEndDVP dvpMapBufferEndDVP = nullptr; +PFN_dvpMapBufferWaitAPI dvpMapBufferWaitAPI = nullptr; +PFN_dvpBindToGLCtx dvpBindToGLCtx = nullptr; +PFN_dvpUnbindFromGLCtx dvpUnbindFromGLCtx = nullptr; +PFN_dvpCreateGPUTextureGL dvpCreateGPUTextureGL = nullptr; +PFN_dvpGetRequiredConstantsGLCtx dvpGetRequiredConstantsGLCtx = nullptr; + +PFN_dvpInitCUDAContext dvpInitCUDAContext = nullptr; +PFN_dvpCloseCUDAContext dvpCloseCUDAContext = nullptr; +PFN_dvpBindToCUDACtx dvpBindToCUDACtx = nullptr; +PFN_dvpUnbindFromCUDACtx dvpUnbindFromCUDACtx = nullptr; +PFN_dvpCreateGPUCUDAArray dvpCreateGPUCUDAArray = nullptr; +PFN_dvpCreateGPUCUDADevicePtr dvpCreateGPUCUDADevicePtr = nullptr; +PFN_dvpMapBufferWaitCUDAStream dvpMapBufferWaitCUDAStream = nullptr; +PFN_dvpMapBufferEndCUDAStream dvpMapBufferEndCUDAStream = nullptr; +PFN_dvpGetRequiredConstantsCUDACtx dvpGetRequiredConstantsCUDACtx = nullptr; + +PFN_dvpSyncObjClientWaitComplete dvpSyncObjClientWaitComplete = nullptr; +PFN_dvpSyncObjCompletion dvpSyncObjCompletion = nullptr; + +#if defined(_WIN32) +PFN_dvpInitD3D11Device dvpInitD3D11Device = nullptr; +PFN_dvpCloseD3D11Device dvpCloseD3D11Device = nullptr; +PFN_dvpCreateGPUD3D11Resource dvpCreateGPUD3D11Resource = nullptr; +PFN_dvpBindToD3D11Device dvpBindToD3D11Device = nullptr; +PFN_dvpUnbindFromD3D11Device dvpUnbindFromD3D11Device = nullptr; +PFN_dvpGetRequiredConstantsD3D11Device dvpGetRequiredConstantsD3D11Device = nullptr; +#endif + +} // extern "C" + +namespace +{ + +constexpr uint32_t kRequiredMajor = 1; +constexpr uint32_t kRequiredMinor = 63; + +#if defined(_WIN32) +HMODULE g_module = nullptr; +#else +void* g_module = nullptr; +#endif + +char g_error[512] = {}; +std::atomic g_glOk{false}; +std::atomic g_d3d11Ok{false}; +std::atomic g_cudaOk{false}; +std::once_flag g_onceFlag; + +// Lookup the symbol `name` in the loaded library; report a fix-it +// message on failure. Naming convention differs per platform — see +// the two-arg overload that picks the right name. +bool resolveSym(const char* name, void** outFn) +{ +#if defined(_WIN32) + FARPROC p = ::GetProcAddress(g_module, name); +#else + void* p = ::dlsym(g_module, name); +#endif + if(!p) + { + if(g_error[0] == '\0') + { +#if defined(_WIN32) + std::snprintf(g_error, sizeof(g_error), + "GetProcAddress failed: %s", name); +#else + const char* errMsg = ::dlerror(); + std::snprintf(g_error, sizeof(g_error), + "dlsym failed: %s (%s)", name, + errMsg ? errMsg : "no detail"); +#endif + } + return false; + } + *outFn = reinterpret_cast(p); + return true; +} + +// Resolve a symbol given its two mangled forms. Both Windows and Linux +// ship libdvp as a C++ library, so each platform has its own mangling +// scheme: +// - Windows: MSVC C++ mangling (`?dvpBegin@@YA?AW4DVPStatus@@XZ` style). +// - Linux: GCC Itanium ABI mangling (`_Z8dvpBeginv` style). +// Caller passes both; the active-platform name is used. +bool resolve(const char* mangledWin, const char* mangledLinux, void** outFn) +{ +#if defined(_WIN32) + (void)mangledLinux; + return resolveSym(mangledWin, outFn); +#else + (void)mangledWin; + return resolveSym(mangledLinux, outFn); +#endif +} + +void doLoad() +{ +#if defined(_WIN32) + g_module = ::LoadLibraryA("dvp.dll"); + if(!g_module) + { + const DWORD err = ::GetLastError(); + // ERROR_MOD_NOT_FOUND is reported both when dvp.dll is absent and when it + // is present but one of ITS imports cannot be resolved. NVIDIA ship DVP + // v1.70 linked against the Visual C++ 2010 runtime, so on a machine + // without that redistributable the library is right there and still fails + // to load -- a distinction the bare error number hides. + if(err == ERROR_MOD_NOT_FOUND) + std::snprintf( + g_error, sizeof(g_error), + "LoadLibraryA(\"dvp.dll\") failed (Win32 error 126, " + "ERROR_MOD_NOT_FOUND): dvp.dll is missing, or it was found but a " + "dependency was not. DVP v1.70 imports MSVCR100.dll -- install the " + "Microsoft Visual C++ 2010 x64 Redistributable if dvp.dll exists."); + else + std::snprintf(g_error, sizeof(g_error), + "LoadLibraryA(\"dvp.dll\") failed (Win32 error %lu)", err); + return; + } +#else + // Try the versioned soname first, then unversioned fallback. + g_module = ::dlopen("libdvp.so.1", RTLD_NOW); + if(!g_module) + g_module = ::dlopen("libdvp.so", RTLD_NOW); + if(!g_module) + { + const char* errMsg = ::dlerror(); + std::snprintf(g_error, sizeof(g_error), + "dlopen(\"libdvp.so[.1]\") failed: %s", + errMsg ? errMsg : "no detail"); + return; + } +#endif + + /* === Common (non-API-specific) DVP entry points. === + * Windows MSVC mangling (left arg) vs Linux Itanium mangling (right arg). + * Linux names verified against the libdvp.so.1 in DeckLink SDK 14.x's + * `NVIDIA_GPUDirect/x86_64/`. Re-derive after ABI changes with: + * nm -D --defined-only libdvp.so.1 | sort + */ + bool common = true; + common &= resolve("?dvpGetLibrayVersion@@YA?AW4DVPStatus@@PEAI0@Z", + "_Z19dvpGetLibrayVersionPjS_", + reinterpret_cast(&dvpGetLibraryVersion)); + common &= resolve("?dvpBegin@@YA?AW4DVPStatus@@XZ", + "_Z8dvpBeginv", + reinterpret_cast(&dvpBegin)); + common &= resolve("?dvpEnd@@YA?AW4DVPStatus@@XZ", + "_Z6dvpEndv", + reinterpret_cast(&dvpEnd)); + common &= resolve( + "?dvpCreateBuffer@@YA?AW4DVPStatus@@PEAUDVPSysmemBufferDescRec@@PEA_K@Z", + "_Z15dvpCreateBufferP22DVPSysmemBufferDescRecPm", + reinterpret_cast(&dvpCreateBuffer)); + common &= resolve("?dvpDestroyBuffer@@YA?AW4DVPStatus@@_K@Z", + "_Z16dvpDestroyBufferm", + reinterpret_cast(&dvpDestroyBuffer)); + common &= resolve("?dvpFreeBuffer@@YA?AW4DVPStatus@@_K@Z", + "_Z13dvpFreeBufferm", + reinterpret_cast(&dvpFreeBuffer)); + common &= resolve("?dvpMemcpyLined@@YA?AW4DVPStatus@@_K0I000III@Z", + "_Z14dvpMemcpyLinedmmjmmmjjj", + reinterpret_cast(&dvpMemcpyLined)); + /* dvpMemcpy is exported as dvpMemcpy2D in both dvp.dll and libdvp.so.1; + same signature so we re-target the symbol. Blender's shim does the + same on Windows. */ + common &= resolve("?dvpMemcpy2D@@YA?AW4DVPStatus@@_K0I000IIIII@Z", + "_Z11dvpMemcpy2Dmmjmmmjjjjj", + reinterpret_cast(&dvpMemcpy)); + common &= resolve( + "?dvpImportSyncObject@@YA?AW4DVPStatus@@PEAUDVPSyncObjectDescRec@@PEA_K@Z", + "_Z19dvpImportSyncObjectP20DVPSyncObjectDescRecPm", + reinterpret_cast(&dvpImportSyncObject)); + common &= resolve("?dvpFreeSyncObject@@YA?AW4DVPStatus@@_K@Z", + "_Z17dvpFreeSyncObjectm", + reinterpret_cast(&dvpFreeSyncObject)); + common &= resolve( + "?dvpSyncObjClientWaitPartial@@YA?AW4DVPStatus@@_KI0@Z", + "_Z27dvpSyncObjClientWaitPartialmjm", + reinterpret_cast(&dvpSyncObjClientWaitPartial)); + common &= resolve("?dvpMapBufferEndAPI@@YA?AW4DVPStatus@@_K@Z", + "_Z18dvpMapBufferEndAPIm", + reinterpret_cast(&dvpMapBufferEndAPI)); + common &= resolve("?dvpMapBufferWaitDVP@@YA?AW4DVPStatus@@_K@Z", + "_Z19dvpMapBufferWaitDVPm", + reinterpret_cast(&dvpMapBufferWaitDVP)); + common &= resolve("?dvpMapBufferEndDVP@@YA?AW4DVPStatus@@_K@Z", + "_Z18dvpMapBufferEndDVPm", + reinterpret_cast(&dvpMapBufferEndDVP)); + common &= resolve("?dvpMapBufferWaitAPI@@YA?AW4DVPStatus@@_K@Z", + "_Z19dvpMapBufferWaitAPIm", + reinterpret_cast(&dvpMapBufferWaitAPI)); + /* Additional sync primitives — present in v1.70 but not used by + * Blender's shim. dvpSyncObjClientWaitComplete is the blocking + * counterpart to *Partial; dvpSyncObjCompletion returns the current + * value. Optional; resolution failure isn't fatal. */ + (void)resolve("?dvpSyncObjClientWaitComplete@@YA?AW4DVPStatus@@_K0@Z", + "_Z28dvpSyncObjClientWaitCompletemm", + reinterpret_cast(&dvpSyncObjClientWaitComplete)); + (void)resolve("?dvpSyncObjCompletion@@YA?AW4DVPStatus@@_KPEA_K@Z", + "_Z20dvpSyncObjCompletionmPm", + reinterpret_cast(&dvpSyncObjCompletion)); + + /* Verify the library version - matches Blender's check. */ + if(common && dvpGetLibraryVersion) + { + uint32_t major = 0, minor = 0; + DVPStatus s = dvpGetLibraryVersion(&major, &minor); + if(s != DVP_STATUS_OK + || major != kRequiredMajor || minor < kRequiredMinor) + { + if(g_error[0] == '\0') + std::snprintf(g_error, sizeof(g_error), + "DVP runtime version mismatch: got %u.%u, need %u.%u+", + major, minor, kRequiredMajor, kRequiredMinor); + return; + } + } + + /* === OpenGL entry points === */ + bool gl = common; + gl &= resolve("?dvpInitGLContext@@YA?AW4DVPStatus@@I@Z", + "_Z16dvpInitGLContextj", + reinterpret_cast(&dvpInitGLContext)); + gl &= resolve("?dvpCloseGLContext@@YA?AW4DVPStatus@@XZ", + "_Z17dvpCloseGLContextv", + reinterpret_cast(&dvpCloseGLContext)); + gl &= resolve("?dvpBindToGLCtx@@YA?AW4DVPStatus@@_K@Z", + "_Z14dvpBindToGLCtxm", + reinterpret_cast(&dvpBindToGLCtx)); + gl &= resolve("?dvpUnbindFromGLCtx@@YA?AW4DVPStatus@@_K@Z", + "_Z18dvpUnbindFromGLCtxm", + reinterpret_cast(&dvpUnbindFromGLCtx)); + gl &= resolve("?dvpCreateGPUTextureGL@@YA?AW4DVPStatus@@IPEA_K@Z", + "_Z21dvpCreateGPUTextureGLjPm", + reinterpret_cast(&dvpCreateGPUTextureGL)); + gl &= resolve( + "?dvpGetRequiredConstantsGLCtx@@YA?AW4DVPStatus@@PEAI00000@Z", + "_Z28dvpGetRequiredConstantsGLCtxPjS_S_S_S_S_", + reinterpret_cast(&dvpGetRequiredConstantsGLCtx)); + + g_glOk.store(gl, std::memory_order_release); + + /* === CUDA entry points (cross-platform — v1.70 dvp.dll v1.70 and + * libdvp.so.1 v1.70 both export the full CUDA surface). === */ + bool cuda = common; + cuda &= resolve("?dvpInitCUDAContext@@YA?AW4DVPStatus@@I@Z", + "_Z18dvpInitCUDAContextj", + reinterpret_cast(&dvpInitCUDAContext)); + cuda &= resolve("?dvpCloseCUDAContext@@YA?AW4DVPStatus@@XZ", + "_Z19dvpCloseCUDAContextv", + reinterpret_cast(&dvpCloseCUDAContext)); + cuda &= resolve("?dvpBindToCUDACtx@@YA?AW4DVPStatus@@_K@Z", + "_Z16dvpBindToCUDACtxm", + reinterpret_cast(&dvpBindToCUDACtx)); + cuda &= resolve("?dvpUnbindFromCUDACtx@@YA?AW4DVPStatus@@_K@Z", + "_Z20dvpUnbindFromCUDACtxm", + reinterpret_cast(&dvpUnbindFromCUDACtx)); + cuda &= resolve( + "?dvpCreateGPUCUDAArray@@YA?AW4DVPStatus@@PEAUCUarray_st@@PEA_K@Z", + "_Z21dvpCreateGPUCUDAArrayP10CUarray_stPm", + reinterpret_cast(&dvpCreateGPUCUDAArray)); + cuda &= resolve( + "?dvpCreateGPUCUDADevicePtr@@YA?AW4DVPStatus@@_KPEA_K@Z", + "_Z25dvpCreateGPUCUDADevicePtryPm", + reinterpret_cast(&dvpCreateGPUCUDADevicePtr)); + cuda &= resolve( + "?dvpMapBufferWaitCUDAStream@@YA?AW4DVPStatus@@_KPEAUCUstream_st@@@Z", + "_Z26dvpMapBufferWaitCUDAStreammP11CUstream_st", + reinterpret_cast(&dvpMapBufferWaitCUDAStream)); + cuda &= resolve( + "?dvpMapBufferEndCUDAStream@@YA?AW4DVPStatus@@_KPEAUCUstream_st@@@Z", + "_Z25dvpMapBufferEndCUDAStreammP11CUstream_st", + reinterpret_cast(&dvpMapBufferEndCUDAStream)); + cuda &= resolve( + "?dvpGetRequiredConstantsCUDACtx@@YA?AW4DVPStatus@@PEAI00000@Z", + "_Z30dvpGetRequiredConstantsCUDACtxPjS_S_S_S_S_", + reinterpret_cast(&dvpGetRequiredConstantsCUDACtx)); + g_cudaOk.store(cuda, std::memory_order_release); + +#if defined(_WIN32) + /* === D3D11 entry points (Windows only — MSVC mangled names) === */ + bool d3d11 = common; + d3d11 &= resolveSym( + "?dvpInitD3D11Device@@YA?AW4DVPStatus@@PEAUID3D11Device@@I@Z", + reinterpret_cast(&dvpInitD3D11Device)); + d3d11 &= resolveSym( + "?dvpCloseD3D11Device@@YA?AW4DVPStatus@@PEAUID3D11Device@@@Z", + reinterpret_cast(&dvpCloseD3D11Device)); + d3d11 &= resolveSym( + "?dvpCreateGPUD3D11Resource@@YA?AW4DVPStatus@@PEAUID3D11Resource@@PEA_K@Z", + reinterpret_cast(&dvpCreateGPUD3D11Resource)); + d3d11 &= resolveSym( + "?dvpBindToD3D11Device@@YA?AW4DVPStatus@@_KPEAUID3D11Device@@@Z", + reinterpret_cast(&dvpBindToD3D11Device)); + d3d11 &= resolveSym( + "?dvpUnbindFromD3D11Device@@YA?AW4DVPStatus@@_KPEAUID3D11Device@@@Z", + reinterpret_cast(&dvpUnbindFromD3D11Device)); + d3d11 &= resolveSym( + "?dvpGetRequiredConstantsD3D11Device@@YA?AW4DVPStatus@@PEAI00000PEAUID3D11Device@@@Z", + reinterpret_cast(&dvpGetRequiredConstantsD3D11Device)); + g_d3d11Ok.store(d3d11, std::memory_order_release); +#endif +} + +} // namespace + +extern "C" { + +bool nv_dvp_load_runtime(void) +{ + std::call_once(g_onceFlag, doLoad); + return g_glOk.load(std::memory_order_acquire) + || g_d3d11Ok.load(std::memory_order_acquire) + || g_cudaOk.load(std::memory_order_acquire); +} + +bool nv_dvp_have_gl(void) +{ + return g_glOk.load(std::memory_order_acquire); +} + +bool nv_dvp_have_d3d11(void) +{ + return g_d3d11Ok.load(std::memory_order_acquire); +} + +bool nv_dvp_have_cuda(void) +{ + return g_cudaOk.load(std::memory_order_acquire); +} + +const char* nv_dvp_get_runtime_error(void) +{ + return g_error; +} + +} // extern "C" diff --git a/src/plugins/score-plugin-gfx/3rdparty/nv-dvp-bridge/dvpapi_shim.h b/src/plugins/score-plugin-gfx/3rdparty/nv-dvp-bridge/dvpapi_shim.h new file mode 100755 index 0000000000..a1c6f94371 --- /dev/null +++ b/src/plugins/score-plugin-gfx/3rdparty/nv-dvp-bridge/dvpapi_shim.h @@ -0,0 +1,340 @@ +/* + * NVIDIA "GPUDirect for Video" (DVP) runtime-loaded shim. + * + * Adapted from Blender's intern/gpudirect/dvpapi.{h,cpp}: + * Copyright (C) 2015 Blender Foundation. All rights reserved. + * Licensed under the GNU GPL v2 or later. + * + * Modifications for ossia score: + * - Replaced BLI_dynlib with bare LoadLibraryA / GetProcAddress (Win32) + * and dlopen / dlsym (Linux). Zero Blender dependency. + * - Added D3D11 entry points (Windows only) using MSVC mangled names. + * - Linux symbol lookup uses GCC Itanium-ABI mangled names — + * `libdvp.so.1` is a C++ library too (no `extern "C"`), so + * `dlsym(handle, "dvpBegin")` returns NULL; we resolve + * `dlsym(handle, "_Z8dvpBeginv")` instead. Names verified against + * the libdvp.so.1 shipped with DeckLink SDK 14.x Linux samples + * (path: `decklink/Linux/Samples/NVIDIA_GPUDirect/x86_64/libdvp.so.1`). + * To re-derive after an ABI change: `nm -D --defined-only libdvp.so.1`. + * - The shim makes runtime-load failures explicit (reports which + * symbol failed to resolve) so future SDK updates can fix names + * individually. + * + * Runtime sources: + * - Windows: `dvp.dll` from NVIDIA's "GPUDirect for Video" SDK (free, + * NVIDIA developer login). The CMakeLists has an opt-in fetch + * (`SCORE_FETCH_DVP_DLL=ON`) that downloads v1.70 from + * PlusToolkit/PlusLib for development. + * - Linux: `libdvp.so.1` shipped inside the Blackmagic DeckLink SDK + * (`decklink/Linux/Samples/NVIDIA_GPUDirect/x86_64/libdvp.so.1`) or + * packaged with NVIDIA's video-codec SDK. The shipped binary uses + * Itanium C++ mangling — see the per-symbol comments below. + * + * Without the runtime on the system, `nv_dvp_load_runtime()` returns + * false and consumers (AJA DVP strategies, planned DeckLink DVP path) + * refuse to initialize. + */ + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ============================================================================ + * NVIDIA DVP types (subset used by the AJA DVP path). + * Mirrors the layout in NVIDIA's DVPAPI.h v1.63. + * ============================================================================ */ + +typedef uint64_t DVPBufferHandle; +typedef uint64_t DVPSyncObjectHandle; + +typedef enum +{ + DVP_STATUS_OK = 0, + DVP_STATUS_INVALID_PARAMETER = 1, + DVP_STATUS_UNSUPPORTED = 2, + DVP_STATUS_END_ENUMERATION = 3, + DVP_STATUS_INVALID_DEVICE = 4, + DVP_STATUS_OUT_OF_MEMORY = 5, + DVP_STATUS_INVALID_OPERATION = 6, + DVP_STATUS_TIMEOUT = 7, + DVP_STATUS_INVALID_CONTEXT = 8, + DVP_STATUS_INVALID_RESOURCE_TYPE = 9, + DVP_STATUS_INVALID_FORMAT_OR_TYPE = 10, + DVP_STATUS_DEVICE_UNINITIALIZED = 11, + DVP_STATUS_UNSIGNALED = 12, + DVP_STATUS_SYNC_ERROR = 13, + DVP_STATUS_SYNC_STILL_BOUND = 14, + DVP_STATUS_ERROR = -1 +} DVPStatus; + +typedef enum +{ + DVP_BUFFER, + DVP_DEPTH_COMPONENT, + DVP_RGBA, + DVP_BGRA, + DVP_RED, + DVP_GREEN, + DVP_BLUE, + DVP_ALPHA, + DVP_RGB, + DVP_BGR, + DVP_LUMINANCE, + DVP_LUMINANCE_ALPHA, + DVP_CUDA_1_CHANNEL, + DVP_CUDA_2_CHANNELS, + DVP_CUDA_4_CHANNELS, + DVP_RGBA_INTEGER, + DVP_BGRA_INTEGER, + DVP_RED_INTEGER, + DVP_GREEN_INTEGER, + DVP_BLUE_INTEGER, + DVP_ALPHA_INTEGER, + DVP_RGB_INTEGER, + DVP_BGR_INTEGER, + DVP_LUMINANCE_INTEGER, + DVP_LUMINANCE_ALPHA_INTEGER +} DVPBufferFormats; + +typedef enum +{ + DVP_UNSIGNED_BYTE, + DVP_BYTE, + DVP_UNSIGNED_SHORT, + DVP_SHORT, + DVP_UNSIGNED_INT, + DVP_INT, + DVP_FLOAT, + DVP_HALF_FLOAT, + DVP_UNSIGNED_BYTE_3_3_2, + DVP_UNSIGNED_BYTE_2_3_3_REV, + DVP_UNSIGNED_SHORT_5_6_5, + DVP_UNSIGNED_SHORT_5_6_5_REV, + DVP_UNSIGNED_SHORT_4_4_4_4, + DVP_UNSIGNED_SHORT_4_4_4_4_REV, + DVP_UNSIGNED_SHORT_5_5_5_1, + DVP_UNSIGNED_SHORT_1_5_5_5_REV, + DVP_UNSIGNED_INT_8_8_8_8, + DVP_UNSIGNED_INT_8_8_8_8_REV, + DVP_UNSIGNED_INT_10_10_10_2, + DVP_UNSIGNED_INT_2_10_10_10_REV +} DVPBufferTypes; + +typedef struct DVPSysmemBufferDescRec +{ + uint32_t width; + uint32_t height; + uint32_t stride; + uint32_t size; + DVPBufferFormats format; + DVPBufferTypes type; + void* bufAddr; +} DVPSysmemBufferDesc; + +#define DVP_SYNC_OBJECT_FLAGS_USE_EVENTS 0x00000001 + +typedef struct DVPSyncObjectDescRec +{ + uint32_t* sem; + uint32_t flags; + DVPStatus (*externalClientWaitFunc)( + DVPSyncObjectHandle sync, uint32_t value, bool GEQ, uint64_t timeout); +} DVPSyncObjectDesc; + +#define DVP_TIMEOUT_IGNORED 0xFFFFFFFFFFFFFFFFull + +/* Forward declarations of D3D11 types — we don't include d3d11.h from a + * vendored shim. Translation units that actually call the D3D11 entry + * points include d3d11.h before this header (or after; either order + * works because the function pointer types take struct pointers and + * struct redeclarations are legal). Windows-only — the D3D11 entry + * points themselves are gated below. */ +#if defined(_WIN32) +struct ID3D11Device; +struct ID3D11Resource; +#endif + +/* Forward declarations of CUDA driver-API types. Identical to the + * declarations in Gfx/Graph/interop/CudaFunctions.hpp; C++ permits + * multiple typedef-name declarations naming the same type, so including + * both headers in the same translation unit is safe. */ +typedef struct CUstream_st* CUstream; +typedef struct CUarray_st* CUarray; +#if defined(_WIN64) || defined(__LP64__) +typedef unsigned long long CUdeviceptr; +#else +typedef unsigned int CUdeviceptr; +#endif + +/* ============================================================================ + * Function pointers (resolved at runtime by nv_dvp_load_runtime). + * + * Macros redirect the SDK-style names (dvpBegin etc.) to the underlying + * function pointers - this keeps consumer code looking like it links + * against the SDK while in fact dispatching through dlsym/GetProcAddress. + * ============================================================================ */ + +typedef DVPStatus (*PFN_dvpInitGLContext)(uint32_t flags); +typedef DVPStatus (*PFN_dvpCloseGLContext)(void); +typedef DVPStatus (*PFN_dvpGetLibraryVersion)(uint32_t* major, uint32_t* minor); +typedef DVPStatus (*PFN_dvpBegin)(void); +typedef DVPStatus (*PFN_dvpEnd)(void); +typedef DVPStatus (*PFN_dvpCreateBuffer)( + DVPSysmemBufferDesc* desc, DVPBufferHandle* hBuf); +typedef DVPStatus (*PFN_dvpDestroyBuffer)(DVPBufferHandle hBuf); +typedef DVPStatus (*PFN_dvpFreeBuffer)(DVPBufferHandle hBuf); +typedef DVPStatus (*PFN_dvpMemcpyLined)( + DVPBufferHandle src, DVPSyncObjectHandle srcSync, uint32_t srcAcq, + uint64_t timeout, DVPBufferHandle dst, DVPSyncObjectHandle dstSync, + uint32_t dstRel, uint32_t startLine, uint32_t numLines); +/* Resolved from dvpMemcpy2D, which takes ELEVEN parameters -- the mangled + * names the shim looks up decode to (m,m,j,m,m,m,j,j,j,j,j) on Linux and the + * same count on Windows. This typedef previously declared ten, so any call + * through it would have passed one argument short and let the callee read + * garbage. Nothing in the tree calls it (only dvpMemcpyLined is used), so the + * mismatch never fired, but an exported typedef is a loaded gun. */ +typedef DVPStatus (*PFN_dvpMemcpy)( + DVPBufferHandle src, DVPSyncObjectHandle srcSync, uint32_t srcAcq, + uint64_t timeout, DVPBufferHandle dst, DVPSyncObjectHandle dstSync, + uint32_t dstRel, uint32_t startingY, uint32_t startingX, uint32_t height, + uint32_t width); +typedef DVPStatus (*PFN_dvpImportSyncObject)( + DVPSyncObjectDesc* desc, DVPSyncObjectHandle* syncObject); +typedef DVPStatus (*PFN_dvpFreeSyncObject)(DVPSyncObjectHandle syncObject); +typedef DVPStatus (*PFN_dvpSyncObjClientWaitPartial)( + DVPSyncObjectHandle sync, uint32_t value, uint64_t timeout); +typedef DVPStatus (*PFN_dvpMapBufferEndAPI)(DVPBufferHandle hBuf); +typedef DVPStatus (*PFN_dvpMapBufferWaitDVP)(DVPBufferHandle hBuf); +typedef DVPStatus (*PFN_dvpMapBufferEndDVP)(DVPBufferHandle hBuf); +typedef DVPStatus (*PFN_dvpMapBufferWaitAPI)(DVPBufferHandle hBuf); +typedef DVPStatus (*PFN_dvpBindToGLCtx)(DVPBufferHandle hBuf); +typedef DVPStatus (*PFN_dvpUnbindFromGLCtx)(DVPBufferHandle hBuf); +typedef DVPStatus (*PFN_dvpCreateGPUTextureGL)( + uint32_t glTexId, DVPBufferHandle* hBuf); +typedef DVPStatus (*PFN_dvpGetRequiredConstantsGLCtx)( + uint32_t* bufferAddrAlignment, uint32_t* bufferGPUStrideAlignment, + uint32_t* semaphoreAddrAlignment, uint32_t* semaphoreAllocSize, + uint32_t* semaphorePayloadOffset, uint32_t* semaphorePayloadSize); + +/* CUDA — cross-platform (Windows v1.70 dvp.dll and Linux libdvp.so.1 + * both export the same surface). Used for sysmem↔CUDA-resource DMA + * with CUstream-side synchronisation. */ +typedef DVPStatus (*PFN_dvpInitCUDAContext)(uint32_t flags); +typedef DVPStatus (*PFN_dvpCloseCUDAContext)(void); +typedef DVPStatus (*PFN_dvpBindToCUDACtx)(DVPBufferHandle hBuf); +typedef DVPStatus (*PFN_dvpUnbindFromCUDACtx)(DVPBufferHandle hBuf); +typedef DVPStatus (*PFN_dvpCreateGPUCUDAArray)( + CUarray array, DVPBufferHandle* hBuf); +typedef DVPStatus (*PFN_dvpCreateGPUCUDADevicePtr)( + CUdeviceptr devPtr, DVPBufferHandle* hBuf); +typedef DVPStatus (*PFN_dvpMapBufferWaitCUDAStream)( + DVPBufferHandle hBuf, CUstream stream); +typedef DVPStatus (*PFN_dvpMapBufferEndCUDAStream)( + DVPBufferHandle hBuf, CUstream stream); +typedef DVPStatus (*PFN_dvpGetRequiredConstantsCUDACtx)( + uint32_t* bufferAddrAlignment, uint32_t* bufferGPUStrideAlignment, + uint32_t* semaphoreAddrAlignment, uint32_t* semaphoreAllocSize, + uint32_t* semaphorePayloadOffset, uint32_t* semaphorePayloadSize); + +/* Additional sync primitives (cross-platform, exported by v1.70): + * SyncObjClientWaitComplete blocks until the sync completes (vs the + * partial variant we already use); SyncObjCompletion returns the + * current completion value. */ +typedef DVPStatus (*PFN_dvpSyncObjClientWaitComplete)( + DVPSyncObjectHandle sync, uint64_t timeout); +typedef DVPStatus (*PFN_dvpSyncObjCompletion)( + DVPSyncObjectHandle sync, uint64_t* completionValue); + +/* D3D11 — Windows-only extension over Blender's GL-only shim. */ +#if defined(_WIN32) +typedef DVPStatus (*PFN_dvpInitD3D11Device)( + struct ID3D11Device* dev, uint32_t flags); +typedef DVPStatus (*PFN_dvpCloseD3D11Device)(struct ID3D11Device* dev); +typedef DVPStatus (*PFN_dvpCreateGPUD3D11Resource)( + struct ID3D11Resource* res, DVPBufferHandle* hBuf); +typedef DVPStatus (*PFN_dvpBindToD3D11Device)( + DVPBufferHandle hBuf, struct ID3D11Device* dev); +typedef DVPStatus (*PFN_dvpUnbindFromD3D11Device)( + DVPBufferHandle hBuf, struct ID3D11Device* dev); +typedef DVPStatus (*PFN_dvpGetRequiredConstantsD3D11Device)( + uint32_t* bufferAddrAlignment, uint32_t* bufferGPUStrideAlignment, + uint32_t* semaphoreAddrAlignment, uint32_t* semaphoreAllocSize, + uint32_t* semaphorePayloadOffset, uint32_t* semaphorePayloadSize, + struct ID3D11Device* dev); +#endif + +extern PFN_dvpInitGLContext dvpInitGLContext; +extern PFN_dvpCloseGLContext dvpCloseGLContext; +extern PFN_dvpGetLibraryVersion dvpGetLibraryVersion; +extern PFN_dvpBegin dvpBegin; +extern PFN_dvpEnd dvpEnd; +extern PFN_dvpCreateBuffer dvpCreateBuffer; +extern PFN_dvpDestroyBuffer dvpDestroyBuffer; +extern PFN_dvpFreeBuffer dvpFreeBuffer; +extern PFN_dvpMemcpyLined dvpMemcpyLined; +extern PFN_dvpMemcpy dvpMemcpy; +extern PFN_dvpImportSyncObject dvpImportSyncObject; +extern PFN_dvpFreeSyncObject dvpFreeSyncObject; +extern PFN_dvpSyncObjClientWaitPartial dvpSyncObjClientWaitPartial; +extern PFN_dvpMapBufferEndAPI dvpMapBufferEndAPI; +extern PFN_dvpMapBufferWaitDVP dvpMapBufferWaitDVP; +extern PFN_dvpMapBufferEndDVP dvpMapBufferEndDVP; +extern PFN_dvpMapBufferWaitAPI dvpMapBufferWaitAPI; +extern PFN_dvpBindToGLCtx dvpBindToGLCtx; +extern PFN_dvpUnbindFromGLCtx dvpUnbindFromGLCtx; +extern PFN_dvpCreateGPUTextureGL dvpCreateGPUTextureGL; +extern PFN_dvpGetRequiredConstantsGLCtx dvpGetRequiredConstantsGLCtx; + +extern PFN_dvpInitCUDAContext dvpInitCUDAContext; +extern PFN_dvpCloseCUDAContext dvpCloseCUDAContext; +extern PFN_dvpBindToCUDACtx dvpBindToCUDACtx; +extern PFN_dvpUnbindFromCUDACtx dvpUnbindFromCUDACtx; +extern PFN_dvpCreateGPUCUDAArray dvpCreateGPUCUDAArray; +extern PFN_dvpCreateGPUCUDADevicePtr dvpCreateGPUCUDADevicePtr; +extern PFN_dvpMapBufferWaitCUDAStream dvpMapBufferWaitCUDAStream; +extern PFN_dvpMapBufferEndCUDAStream dvpMapBufferEndCUDAStream; +extern PFN_dvpGetRequiredConstantsCUDACtx dvpGetRequiredConstantsCUDACtx; + +extern PFN_dvpSyncObjClientWaitComplete dvpSyncObjClientWaitComplete; +extern PFN_dvpSyncObjCompletion dvpSyncObjCompletion; + +#if defined(_WIN32) +extern PFN_dvpInitD3D11Device dvpInitD3D11Device; +extern PFN_dvpCloseD3D11Device dvpCloseD3D11Device; +extern PFN_dvpCreateGPUD3D11Resource dvpCreateGPUD3D11Resource; +extern PFN_dvpBindToD3D11Device dvpBindToD3D11Device; +extern PFN_dvpUnbindFromD3D11Device dvpUnbindFromD3D11Device; +extern PFN_dvpGetRequiredConstantsD3D11Device dvpGetRequiredConstantsD3D11Device; +#endif + +/* ============================================================================ + * Runtime loader + * ============================================================================ */ + +/** Try to load dvp.dll and resolve required GL + D3D11 entry points. + * Returns true on full success. On partial failure (e.g. dvp.dll loaded + * but a D3D11 symbol couldn't be resolved), the failed function pointer + * remains null and nv_dvp_get_runtime_error() reports which one. */ +bool nv_dvp_load_runtime(void); + +/** Are GL DVP entry points all resolved? Cheap; returns cached result. */ +bool nv_dvp_have_gl(void); + +/** Are D3D11 DVP entry points all resolved? Windows-only; always false + * on Linux. */ +bool nv_dvp_have_d3d11(void); + +/** Are CUDA DVP entry points all resolved? Cross-platform; both v1.70 + * dvp.dll and libdvp.so.1 export the CUDA suite. */ +bool nv_dvp_have_cuda(void); + +/** Last loader-time error message, NUL-terminated. Empty if no error. */ +const char* nv_dvp_get_runtime_error(void); + +#ifdef __cplusplus +} +#endif diff --git a/src/plugins/score-plugin-gfx/3rdparty/nv-dvp-bridge/nv_dvp_bridge.cpp b/src/plugins/score-plugin-gfx/3rdparty/nv-dvp-bridge/nv_dvp_bridge.cpp new file mode 100755 index 0000000000..9a004ef82a --- /dev/null +++ b/src/plugins/score-plugin-gfx/3rdparty/nv-dvp-bridge/nv_dvp_bridge.cpp @@ -0,0 +1,1083 @@ +/** + * @file nv_dvp_bridge.cpp + * @brief Implementation of nv_dvp_bridge.h. Wraps NVIDIA's "GPUDirect for + * Video" (DVP) SDK so score's AJA GPU-direct output path can DMA + * encoder output from GPU memory to AJA-DMA-locked sysmem on Windows. + * + * Reference implementation: AJA's `ntv2glTextureTransferNV.cpp` in + * `3rdparty/libajantv2/demos/NVIDIA/common/`. This bridge collapses the + * demo's chunked-async transfer into one synchronous call per frame; the + * AJA strategies in score-plugin-gfx provide pipelining via their slot + * ring + the AJAConsumerThread instead of via DVP-side chunking. + */ + +#include "nv_dvp_bridge.h" +#include "dvpapi_shim.h" + +#if defined(_WIN32) +#include +#include // _aligned_malloc / _aligned_free +#endif + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ + +/* DVP semaphores must be 32-bit ints aligned to alignment-bytes that + * `dvpGetRequiredConstants*` reports. `_semaphoreAllocSize` is the + * minimum byte count to allocate; we add `_semaphoreAddrAlignment` as + * slack and round up the resulting pointer. Same shape as AJA's demo. */ +struct SyncSlot +{ + volatile uint32_t* sem{}; + volatile uint32_t* semOrg{}; + uint32_t releaseValue{0}; + uint32_t acquireValue{0}; + DVPSyncObjectHandle obj{}; +}; + +struct ResourceState +{ + enum class Kind : uint8_t { Texture, Buffer }; + Kind kind{}; + DVPBufferHandle dvp{}; + + /* Geometry: needed for `dvpMemcpyLined` which takes an explicit line + * count. Both source and destination of a memcpy must agree on the + * line count, so the strategy registers the texture and buffer with + * matching (width, height) and the bridge passes height to the DMA. */ + uint32_t width{}; + uint32_t height{}; + + /* Buffer-only: one sync slot tracks DVP DMA completion (waited on by + * AJA before reading). For Texture, sync is implicit via API-side + * map/unmap calls; we do not allocate a slot. */ + SyncSlot bufSync; + + /* Strong references (texture only) so we can dvpUnbind on shutdown. */ +#if defined(_WIN32) + ID3D11Texture2D* d3d11Texture{}; +#endif +}; + +} // namespace + +struct NvDvpContext_t +{ + enum class Backend : uint8_t { D3D11, GL, CUDA }; + Backend backend{}; + + /* D3D11-only */ +#if defined(_WIN32) + ID3D11Device* d3d11Device{}; +#endif + + uint32_t bufferAddrAlignment{4096}; + uint32_t bufferGPUStrideAlignment{4096}; + uint32_t semaphoreAddrAlignment{16}; + uint32_t semaphoreAllocSize{16}; + + std::mutex mtx; + std::string lastError; + + /* Resource registry. Pointer-stable; handed back to the caller as + * an opaque NvDvpResourceHandle. */ + std::map> resources; + + bool initialized{false}; +}; + +namespace +{ + +/* ---------------------------------------------------------------------- + * Helpers + * ---------------------------------------------------------------------- */ + +inline ResourceState* asResource(NvDvpResourceHandle h) +{ + return reinterpret_cast(h); +} + +inline NvDvpResourceHandle asHandle(ResourceState* r) +{ + return reinterpret_cast(r); +} + +void initSyncSlot(NvDvpContext_t* ctx, SyncSlot& s) +{ + /* Aligned-up allocation for DVP semaphore memory. AJA's demo uses + * malloc + manual alignment, same here. */ + s.semOrg = static_cast( + std::calloc(1, ctx->semaphoreAllocSize + ctx->semaphoreAddrAlignment)); + uintptr_t v = reinterpret_cast(s.semOrg); + v += ctx->semaphoreAddrAlignment - 1; + v &= ~(uintptr_t(ctx->semaphoreAddrAlignment) - 1); + s.sem = reinterpret_cast(v); + *s.sem = 0; + + DVPSyncObjectDesc desc{}; + desc.externalClientWaitFunc = nullptr; + desc.flags = 0; + desc.sem = const_cast(s.sem); + dvpImportSyncObject(&desc, &s.obj); +} + +void freeSyncSlot(SyncSlot& s) +{ + if(s.obj) + dvpFreeSyncObject(s.obj); + s.obj = 0; + if(s.semOrg) + std::free(const_cast(s.semOrg)); + s.semOrg = nullptr; + s.sem = nullptr; +} + +uint32_t bytesPerPixel(NvDvpFormat fmt) +{ + switch(fmt) + { + case NV_DVP_FORMAT_RGBA8: + case NV_DVP_FORMAT_BGRA8: + return 4u; + } + return 4u; +} + +DVPBufferFormats toDvpFormat(NvDvpFormat f) +{ + switch(f) + { + case NV_DVP_FORMAT_RGBA8: + return DVP_RGBA; + case NV_DVP_FORMAT_BGRA8: + return DVP_BGRA; + } + return DVP_RGBA; +} + +/* Sticky last-init error. On init failure we destroy the context (the caller's + * out_ctx stays null), so the per-context lastError is lost and a subsequent + * nv_dvp_get_error_string(nullptr) would fall back to the generic "Invalid + * context" string. Stash the real failure (incl. DVP status) here so callers + * get the actual cause (e.g. "dvpInitGLContext failed (DVP status=-1)" — the + * runtime rejecting a non-Quadro GPU). */ +std::mutex& initErrorMutex() +{ + static std::mutex m; + return m; +} +std::string& lastInitErrorStorage() +{ + static std::string e; + return e; +} +void setInitError(const char* msg, DVPStatus status) +{ + std::lock_guard lk{initErrorMutex()}; + char buf[160]; + std::snprintf(buf, sizeof(buf), "%s (DVP status=%d)", msg, int(status)); + lastInitErrorStorage().assign(buf); +} + +/* True when the current GL context is a GLX one. + * + * Every libdvp GL entry point binds to the *current* context and walks a + * dispatch table that exists only for GLX; given EGL (score's default, since + * EGL is what makes dma-buf import zero-copy) or no context at all, those + * pointers are null and calling them segfaults inside libdvp. Qt knows which + * backend it created, so ask it -- same approach EglDmaBufImport uses in the + * other direction with QEGLContext. A Qt without GLX support cannot have a + * GLX context current, hence the false. */ +bool hasCurrentGlxContext() +{ +#if QT_CONFIG(xcb_glx_plugin) + auto* ctx = QOpenGLContext::currentContext(); + return ctx && ctx->nativeInterface(); +#else + return false; +#endif +} + +/* Set ctx->lastError. Called under ctx->mtx. */ +void setError(NvDvpContext_t* ctx, const char* msg, DVPStatus status = DVP_STATUS_OK) +{ + if(!ctx) + return; + if(status != DVP_STATUS_OK) + { + char buf[160]; + std::snprintf(buf, sizeof(buf), "%s (DVP status=%d)", msg, int(status)); + ctx->lastError.assign(buf); + } + else + { + ctx->lastError.assign(msg); + } +} + +#define DVP_CHECK(call, ctx, retval) \ + do \ + { \ + DVPStatus _st = (call); \ + if(_st != DVP_STATUS_OK) \ + { \ + setError((ctx), #call, _st); \ + return (retval); \ + } \ + } while(0) + +} // namespace + +/* ============================================================================ + * Public API + * ============================================================================ */ + +extern "C" { + +NV_DVP_API bool nv_dvp_available(void) +{ + /* Try to load dvp.dll. If it's not present (no NVIDIA "GPUDirect for + * Video" SDK installed and dvp.dll not in PATH), this returns false + * and the AJA strategies refuse to use the DVP path. */ + return nv_dvp_load_runtime(); +} + +NV_DVP_API const char* nv_dvp_get_error_string(NvDvpContextHandle ctx) +{ + if(!ctx) + { + /* Loader-time error (dvp.dll missing, version mismatch, mangled-name + * mismatch on a required entry point) is sticky — surface it even if + * the caller never got a context. */ + const char* loadErr = nv_dvp_get_runtime_error(); + if(loadErr && loadErr[0]) + return loadErr; + /* Init-time error (e.g. dvpInit*Context rejecting a non-Quadro GPU) is + * also sticky: the context is destroyed on failure so it can't carry the + * message itself. */ + { + std::lock_guard lk{initErrorMutex()}; + if(!lastInitErrorStorage().empty()) + return lastInitErrorStorage().c_str(); + } + return "Invalid context"; + } + return ctx->lastError.c_str(); +} + +NV_DVP_API NvDvpError nv_dvp_init_d3d11( + NvDvpContextHandle* out_ctx, void* d3d11_device) +{ +#if !defined(_WIN32) + (void)d3d11_device; + if(out_ctx) + *out_ctx = nullptr; + return NV_DVP_ERROR_UNKNOWN; // D3D11 unavailable on non-Windows +#else + if(!out_ctx || !d3d11_device) + return NV_DVP_ERROR_INVALID_PARAMETER; + + *out_ctx = nullptr; + + if(!nv_dvp_load_runtime() || !nv_dvp_have_d3d11()) + return NV_DVP_ERROR_INIT_FAILED; + + auto ctx = std::make_unique(); + ctx->backend = NvDvpContext_t::Backend::D3D11; + ctx->d3d11Device = static_cast(d3d11_device); + + if(auto _st = dvpInitD3D11Device(ctx->d3d11Device, 0); _st != DVP_STATUS_OK) + { + fprintf( + stderr, "[nv-dvp] dvpInitD3D11Device failed: DVP_STATUS=%d\n", (int)_st); + setError(ctx.get(), "dvpInitD3D11Device failed", _st); + setInitError("dvpInitD3D11Device failed", _st); + return NV_DVP_ERROR_INIT_FAILED; + } + + uint32_t unused = 0; + if(dvpGetRequiredConstantsD3D11Device( + &ctx->bufferAddrAlignment, &ctx->bufferGPUStrideAlignment, + &ctx->semaphoreAddrAlignment, &ctx->semaphoreAllocSize, + &unused, &unused, ctx->d3d11Device) + != DVP_STATUS_OK) + { + dvpCloseD3D11Device(ctx->d3d11Device); + setError(ctx.get(), "dvpGetRequiredConstantsD3D11Device failed"); + return NV_DVP_ERROR_INIT_FAILED; + } + + ctx->initialized = true; + *out_ctx = ctx.release(); + return NV_DVP_SUCCESS; +#endif +} + +NV_DVP_API NvDvpError nv_dvp_init_gl(NvDvpContextHandle* out_ctx) +{ + if(!out_ctx) + return NV_DVP_ERROR_INVALID_PARAMETER; + + *out_ctx = nullptr; + + if(!nv_dvp_load_runtime() || !nv_dvp_have_gl()) + return NV_DVP_ERROR_INIT_FAILED; + + /* libdvp reports success even when it has silently degraded to CUDA-only + * (it returns DVP_STATUS_OK from dvpInitGLContext and + * dvpGetRequiredConstantsGLCtx alike), so there is nothing to test after + * the fact -- the context type is the only reliable signal. */ + if(!hasCurrentGlxContext()) + { + fprintf( + stderr, + "[nv-dvp] GPUDirect-for-Video disabled: no GLX context.\n" + " libdvp interops only with GLX, and score defaults to EGL\n" + " (EGL is what makes dma-buf import zero-copy). To use DVP,\n" + " start score with QT_XCB_GL_INTEGRATION=xcb_glx on an X11\n" + " session. Falling back to CPU staging.\n"); + setInitError( + "no GLX context: set QT_XCB_GL_INTEGRATION=xcb_glx", + DVP_STATUS_UNSUPPORTED); + return NV_DVP_ERROR_INIT_FAILED; + } + + auto ctx = std::make_unique(); + ctx->backend = NvDvpContext_t::Backend::GL; + + /* DVP_DEVICE_FLAGS_SHARE_APP_CONTEXT (1): make libdvp share the app's GL + * context instead of creating its own internal one. Without it, only the + * FIRST dvpInitGLContext'd context in the process can transfer — a second + * one (e.g. an output node and a capture node each with their own QRhi) + * gets DVP_STATUS_ERROR from every dvpMemcpyLined while all setup calls + * report success. Verified with a standalone two-context probe. */ + if(auto _st = dvpInitGLContext(1); _st != DVP_STATUS_OK) + { + fprintf(stderr, "[nv-dvp] dvpInitGLContext failed: DVP_STATUS=%d\n", (int)_st); + setError(ctx.get(), "dvpInitGLContext failed", _st); + setInitError("dvpInitGLContext failed", _st); + return NV_DVP_ERROR_INIT_FAILED; + } + + uint32_t unused = 0; + if(dvpGetRequiredConstantsGLCtx( + &ctx->bufferAddrAlignment, &ctx->bufferGPUStrideAlignment, + &ctx->semaphoreAddrAlignment, &ctx->semaphoreAllocSize, + &unused, &unused) + != DVP_STATUS_OK) + { + dvpCloseGLContext(); + setError(ctx.get(), "dvpGetRequiredConstantsGLCtx failed"); + return NV_DVP_ERROR_INIT_FAILED; + } + + ctx->initialized = true; + *out_ctx = ctx.release(); + return NV_DVP_SUCCESS; +} + +NV_DVP_API NvDvpError nv_dvp_init_cuda(NvDvpContextHandle* out_ctx) +{ + if(!out_ctx) + return NV_DVP_ERROR_INVALID_PARAMETER; + + *out_ctx = nullptr; + + if(!nv_dvp_load_runtime() || !nv_dvp_have_cuda()) + return NV_DVP_ERROR_INIT_FAILED; + + auto ctx = std::make_unique(); + ctx->backend = NvDvpContext_t::Backend::CUDA; + + if(auto _st = dvpInitCUDAContext(0); _st != DVP_STATUS_OK) + { + fprintf( + stderr, "[nv-dvp] dvpInitCUDAContext failed: DVP_STATUS=%d\n", (int)_st); + setError(ctx.get(), "dvpInitCUDAContext failed", _st); + setInitError("dvpInitCUDAContext failed", _st); + return NV_DVP_ERROR_INIT_FAILED; + } + + uint32_t unused = 0; + if(dvpGetRequiredConstantsCUDACtx( + &ctx->bufferAddrAlignment, &ctx->bufferGPUStrideAlignment, + &ctx->semaphoreAddrAlignment, &ctx->semaphoreAllocSize, + &unused, &unused) + != DVP_STATUS_OK) + { + dvpCloseCUDAContext(); + setError(ctx.get(), "dvpGetRequiredConstantsCUDACtx failed"); + return NV_DVP_ERROR_INIT_FAILED; + } + + ctx->initialized = true; + *out_ctx = ctx.release(); + return NV_DVP_SUCCESS; +} + +NV_DVP_API void nv_dvp_shutdown(NvDvpContextHandle ctx) +{ + if(!ctx) + return; + std::lock_guard lock(ctx->mtx); + + const bool glCurrent = hasCurrentGlxContext(); + + /* Drop all registered resources. Textures returned from + * dvpCreateGPU*Resource were never explicitly bound (see comment in + * register_*_texture), so only sysmem buffers need the unbind. */ + for(auto& kv : ctx->resources) + { + auto* r = kv.second.get(); + if(r->kind == ResourceState::Kind::Buffer) + { + switch(ctx->backend) + { + case NvDvpContext_t::Backend::D3D11: +#if defined(_WIN32) + dvpUnbindFromD3D11Device(r->dvp, ctx->d3d11Device); +#endif + break; + case NvDvpContext_t::Backend::GL: + if(glCurrent) + dvpUnbindFromGLCtx(r->dvp); + break; + case NvDvpContext_t::Backend::CUDA: + dvpUnbindFromCUDACtx(r->dvp); + break; + } + } + if(r->dvp) + dvpFreeBuffer(r->dvp); + freeSyncSlot(r->bufSync); + } + ctx->resources.clear(); + + if(ctx->initialized) + { + switch(ctx->backend) + { + case NvDvpContext_t::Backend::D3D11: +#if defined(_WIN32) + dvpCloseD3D11Device(ctx->d3d11Device); +#endif + break; + case NvDvpContext_t::Backend::GL: + /* dvpCloseGLContext unbinds from the *current* GL context and + * segfaults inside libdvp when there is none -- which is what + * teardown looks like once the QRhi has dropped the context, seen at + * Graph::~Graph on the Quadro box. Leaking libdvp's context in that + * window is better than taking the process down at exit. */ + if(glCurrent) + dvpCloseGLContext(); + break; + case NvDvpContext_t::Backend::CUDA: + dvpCloseCUDAContext(); + break; + } + } + delete ctx; +} + +/* dvpBegin / dvpEnd is not held across the lifetime of the strategy: + * NVIDIA's docs require dvpMapBufferEndAPI / dvpMapBufferWaitAPI to be + * called *outside* a dvpBegin/dvpEnd pair, while WaitDVP/EndDVP/ + * Memcpy/ClientWait must be called *inside*. AJA's GL demo solves this + * by running the DVP DMA work on a separate thread that holds the + * begin/end pair; in our single-threaded model the bridge instead + * brackets each transfer call with its own dvpBegin/dvpEnd internally. + * + * These thread_begin / thread_end functions are kept for ABI + * compatibility but are no-ops. */ +NV_DVP_API NvDvpError nv_dvp_thread_begin(NvDvpContextHandle ctx) +{ + if(!ctx) + return NV_DVP_ERROR_INVALID_PARAMETER; + return NV_DVP_SUCCESS; +} + +NV_DVP_API NvDvpError nv_dvp_thread_end(NvDvpContextHandle ctx) +{ + if(!ctx) + return NV_DVP_ERROR_INVALID_PARAMETER; + return NV_DVP_SUCCESS; +} + +NV_DVP_API NvDvpError nv_dvp_register_d3d11_texture( + NvDvpContextHandle ctx, void* d3d11_texture, NvDvpFormat /*format*/, + uint32_t width, uint32_t height, NvDvpResourceHandle* out_handle) +{ +#if !defined(_WIN32) + (void)ctx; (void)d3d11_texture; (void)width; (void)height; + if(out_handle) + *out_handle = nullptr; + return NV_DVP_ERROR_UNKNOWN; +#else + if(!ctx || !d3d11_texture || !out_handle || width == 0 || height == 0 + || ctx->backend != NvDvpContext_t::Backend::D3D11) + return NV_DVP_ERROR_INVALID_PARAMETER; + + *out_handle = nullptr; + std::lock_guard lock(ctx->mtx); + + auto r = std::make_unique(); + r->kind = ResourceState::Kind::Texture; + r->width = width; + r->height = height; + r->d3d11Texture = static_cast(d3d11_texture); + + /* dvpCreateGPUD3D11Resource takes any ID3D11Resource*. ID3D11Texture2D + * derives from ID3D11Resource so the implicit upcast is fine. The + * resulting buffer handle is implicitly bound to the device that + * dvpInitD3D11Device was called against - we do NOT (and must not) + * also call dvpBindToD3D11Device on it. AJA's reference demo + * (ntv2glTextureTransferNV.cpp:RegisterTexture) only binds the + * sysmem buffers, never the GPU resources. */ + if(dvpCreateGPUD3D11Resource(r->d3d11Texture, &r->dvp) + != DVP_STATUS_OK) + { + setError(ctx, "dvpCreateGPUD3D11Resource failed"); + return NV_DVP_ERROR_INTEROP_FAILED; + } + + auto* raw = r.get(); + ctx->resources.emplace(raw, std::move(r)); + *out_handle = asHandle(raw); + return NV_DVP_SUCCESS; +#endif +} + +NV_DVP_API NvDvpError nv_dvp_register_gl_texture( + NvDvpContextHandle ctx, uint32_t gl_texture_id, NvDvpFormat /*format*/, + uint32_t width, uint32_t height, NvDvpResourceHandle* out_handle) +{ + if(!ctx || !out_handle || gl_texture_id == 0 || width == 0 || height == 0 + || ctx->backend != NvDvpContext_t::Backend::GL) + return NV_DVP_ERROR_INVALID_PARAMETER; + + *out_handle = nullptr; + std::lock_guard lock(ctx->mtx); + + auto r = std::make_unique(); + r->kind = ResourceState::Kind::Texture; + r->width = width; + r->height = height; + + if(dvpCreateGPUTextureGL(gl_texture_id, &r->dvp) != DVP_STATUS_OK) + { + setError(ctx, "dvpCreateGPUTextureGL failed"); + return NV_DVP_ERROR_INTEROP_FAILED; + } + /* GL textures are implicitly bound to the GL context dvpInitGLContext + * was called against; no separate dvpBindToGLCtx for GPU textures. */ + + /* Texture-destination copies (buffer->texture) need a sync object to + * signal on completion — dvpMemcpyLined rejects a null dstSync. + * (Blender/UPBGE's VideoDeckLink allocates a "gpu sync" per texture + * for exactly this.) */ + initSyncSlot(ctx, r->bufSync); + + auto* raw = r.get(); + ctx->resources.emplace(raw, std::move(r)); + *out_handle = asHandle(raw); + return NV_DVP_SUCCESS; +} + +NV_DVP_API NvDvpError nv_dvp_register_cuda_device_ptr( + NvDvpContextHandle ctx, uint64_t cuda_device_ptr, NvDvpFormat /*format*/, + uint32_t width, uint32_t height, NvDvpResourceHandle* out_handle) +{ + if(!ctx || !out_handle || cuda_device_ptr == 0 + || width == 0 || height == 0 + || ctx->backend != NvDvpContext_t::Backend::CUDA) + return NV_DVP_ERROR_INVALID_PARAMETER; + + *out_handle = nullptr; + std::lock_guard lock(ctx->mtx); + + auto r = std::make_unique(); + r->kind = ResourceState::Kind::Texture; + r->width = width; + r->height = height; + + /* CUDA flat device pointers are implicitly bound to the CUDA context + * dvpInitCUDAContext was called against - no dvpBindToCUDACtx for + * GPU resources, only sysmem. Same convention as GL/D3D11 GPU + * resources in DVP. */ + if(dvpCreateGPUCUDADevicePtr( + static_cast(cuda_device_ptr), &r->dvp) + != DVP_STATUS_OK) + { + setError(ctx, "dvpCreateGPUCUDADevicePtr failed"); + return NV_DVP_ERROR_INTEROP_FAILED; + } + + auto* raw = r.get(); + ctx->resources.emplace(raw, std::move(r)); + *out_handle = asHandle(raw); + return NV_DVP_SUCCESS; +} + +NV_DVP_API NvDvpError nv_dvp_register_cuda_array( + NvDvpContextHandle ctx, void* cuda_array, NvDvpFormat /*format*/, + uint32_t width, uint32_t height, NvDvpResourceHandle* out_handle) +{ + if(!ctx || !out_handle || !cuda_array || width == 0 || height == 0 + || ctx->backend != NvDvpContext_t::Backend::CUDA) + return NV_DVP_ERROR_INVALID_PARAMETER; + + *out_handle = nullptr; + std::lock_guard lock(ctx->mtx); + + auto r = std::make_unique(); + r->kind = ResourceState::Kind::Texture; + r->width = width; + r->height = height; + + if(dvpCreateGPUCUDAArray(static_cast(cuda_array), &r->dvp) + != DVP_STATUS_OK) + { + setError(ctx, "dvpCreateGPUCUDAArray failed"); + return NV_DVP_ERROR_INTEROP_FAILED; + } + + auto* raw = r.get(); + ctx->resources.emplace(raw, std::move(r)); + *out_handle = asHandle(raw); + return NV_DVP_SUCCESS; +} + +NV_DVP_API NvDvpError nv_dvp_register_sysmem_buffer( + NvDvpContextHandle ctx, void* sysmem_ptr, NvDvpFormat format, + uint32_t width, uint32_t height, uint32_t stride_bytes, + NvDvpResourceHandle* out_handle) +{ + if(!ctx || !sysmem_ptr || !out_handle || width == 0 || height == 0) + return NV_DVP_ERROR_INVALID_PARAMETER; + + /* Caller is responsible for sysmem_ptr being aligned to + * ctx->bufferAddrAlignment and stride to bufferGPUStrideAlignment. + * We don't enforce here because score's strategies use _aligned_malloc + * with alignment >= 4096 and align stride to the same, which is + * always >= what dvpGetRequiredConstants reports on current drivers. */ + + std::lock_guard lock(ctx->mtx); + + auto r = std::make_unique(); + r->kind = ResourceState::Kind::Buffer; + r->width = width; + r->height = height; + + DVPSysmemBufferDesc desc{}; + desc.width = width; + desc.height = height; + desc.stride = stride_bytes; + desc.size = uint32_t(stride_bytes) * height; + desc.format = toDvpFormat(format); + desc.type = DVP_UNSIGNED_BYTE; + desc.bufAddr = sysmem_ptr; + + (void)bytesPerPixel; /* size already supplied by caller via stride */ + + if(dvpCreateBuffer(&desc, &r->dvp) != DVP_STATUS_OK) + { + setError(ctx, "dvpCreateBuffer (sysmem) failed"); + return NV_DVP_ERROR_ALLOC_FAILED; + } + DVPStatus bindStatus = DVP_STATUS_OK; + const char* bindErr = nullptr; + switch(ctx->backend) + { + case NvDvpContext_t::Backend::D3D11: +#if defined(_WIN32) + bindStatus = dvpBindToD3D11Device(r->dvp, ctx->d3d11Device); + bindErr = "dvpBindToD3D11Device (sysmem) failed"; +#endif + break; + case NvDvpContext_t::Backend::GL: + bindStatus = dvpBindToGLCtx(r->dvp); + bindErr = "dvpBindToGLCtx (sysmem) failed"; + break; + case NvDvpContext_t::Backend::CUDA: + bindStatus = dvpBindToCUDACtx(r->dvp); + bindErr = "dvpBindToCUDACtx (sysmem) failed"; + break; + } + if(bindStatus != DVP_STATUS_OK) + { + dvpFreeBuffer(r->dvp); + setError(ctx, bindErr ? bindErr : "DVP sysmem bind failed", bindStatus); + return NV_DVP_ERROR_INTEROP_FAILED; + } + + initSyncSlot(ctx, r->bufSync); + + auto* raw = r.get(); + ctx->resources.emplace(raw, std::move(r)); + *out_handle = asHandle(raw); + return NV_DVP_SUCCESS; +} + +NV_DVP_API void nv_dvp_unregister( + NvDvpContextHandle ctx, NvDvpResourceHandle handle) +{ + if(!ctx || !handle) + return; + std::lock_guard lock(ctx->mtx); + + auto* r = asResource(handle); + auto it = ctx->resources.find(r); + if(it == ctx->resources.end()) + return; + + /* Textures (from dvpCreateGPU*Resource) are never explicitly bound, + * so they don't need unbinding. Only sysmem buffers do. */ + if(r->kind == ResourceState::Kind::Buffer) + { + switch(ctx->backend) + { + case NvDvpContext_t::Backend::D3D11: +#if defined(_WIN32) + dvpUnbindFromD3D11Device(r->dvp, ctx->d3d11Device); +#endif + break; + case NvDvpContext_t::Backend::GL: + // Same gate as nv_dvp_shutdown: unbinding needs the GLX context the + // handle was bound to. This runs FIRST in every teardown -- callers + // unregister each resource before shutting the context down -- so + // leaving it ungated made the gate in nv_dvp_shutdown unreachable. + if(hasCurrentGlxContext()) + dvpUnbindFromGLCtx(r->dvp); + break; + case NvDvpContext_t::Backend::CUDA: + dvpUnbindFromCUDACtx(r->dvp); + break; + } + } + + if(r->dvp) + dvpFreeBuffer(r->dvp); + freeSyncSlot(r->bufSync); + ctx->resources.erase(it); +} + +NV_DVP_API NvDvpError nv_dvp_acquire_texture( + NvDvpContextHandle ctx, NvDvpResourceHandle texture) +{ + if(!ctx || !texture) + return NV_DVP_ERROR_INVALID_PARAMETER; + auto* r = asResource(texture); + if(r->kind != ResourceState::Kind::Texture) + return NV_DVP_ERROR_INVALID_HANDLE; + + std::lock_guard lock(ctx->mtx); + /* dvpMapBufferWaitAPI inserts a wait on the API command queue (D3D11 + * deferred or GL command stream) until any pending DVP DMA completes. + * It does NOT block the CPU. */ + DVP_CHECK(dvpMapBufferWaitAPI(r->dvp), ctx, NV_DVP_ERROR_SYNC_FAILED); + return NV_DVP_SUCCESS; +} + +NV_DVP_API NvDvpError nv_dvp_release_texture( + NvDvpContextHandle ctx, NvDvpResourceHandle texture) +{ + if(!ctx || !texture) + return NV_DVP_ERROR_INVALID_PARAMETER; + auto* r = asResource(texture); + if(r->kind != ResourceState::Kind::Texture) + return NV_DVP_ERROR_INVALID_HANDLE; + + std::lock_guard lock(ctx->mtx); + /* dvpMapBufferEndAPI signals the API queue is done writing the + * texture; the next DVP DMA can proceed. */ + DVP_CHECK(dvpMapBufferEndAPI(r->dvp), ctx, NV_DVP_ERROR_SYNC_FAILED); + return NV_DVP_SUCCESS; +} + +NV_DVP_API NvDvpError nv_dvp_copy_texture_to_buffer( + NvDvpContextHandle ctx, NvDvpResourceHandle src_texture, + NvDvpResourceHandle dst_buffer) +{ + if(!ctx || !src_texture || !dst_buffer) + return NV_DVP_ERROR_INVALID_PARAMETER; + + auto* tex = asResource(src_texture); + auto* buf = asResource(dst_buffer); + if(tex->kind != ResourceState::Kind::Texture + || buf->kind != ResourceState::Kind::Buffer) + return NV_DVP_ERROR_INVALID_HANDLE; + + std::lock_guard lock(ctx->mtx); + + /* WaitDVP / Memcpy / EndDVP / ClientWaitPartial all must run inside a + * dvpBegin/dvpEnd pair (NVIDIA dvpapi.h docs). EndAPI / WaitAPI - + * called by nv_dvp_release_texture / nv_dvp_acquire_texture - must + * run *outside* such a pair, so we bracket only the DVP-side ops + * here, not the API-side ones. */ + DVP_CHECK(dvpBegin(), ctx, NV_DVP_ERROR_SYNC_FAILED); + + /* Lock the texture for DVP. The caller is expected to have called + * nv_dvp_release_texture (dvpMapBufferEndAPI) before this so the + * API queue has signalled it's done writing; if not, WaitDVP will + * stall waiting for that signal. */ + DVPStatus status = dvpMapBufferWaitDVP(tex->dvp); + if(status == DVP_STATUS_OK) + { + /* DMA into the sysmem buffer. The AJA strategy guarantees (via its + * producer-consumer pattern + AJAConsumerThread) that we only ever + * reuse a buffer AJA has finished reading; the ClientWaitPartial + * below blocks us until DMA is fully complete. */ + buf->bufSync.releaseValue++; + status = dvpMemcpyLined( + tex->dvp, /*srcSync=*/0, /*srcWaitValue=*/0, DVP_TIMEOUT_IGNORED, + buf->dvp, buf->bufSync.obj, buf->bufSync.releaseValue, + /*startingLine=*/0, /*numberOfLines=*/buf->height); + } + if(status == DVP_STATUS_OK) + status = dvpMapBufferEndDVP(tex->dvp); + if(status == DVP_STATUS_OK) + status = dvpSyncObjClientWaitPartial( + buf->bufSync.obj, buf->bufSync.releaseValue, DVP_TIMEOUT_IGNORED); + + /* Always close the dvpBegin scope, even on failure, otherwise + * subsequent acquire/release calls would see a dangling open scope. */ + DVPStatus endStatus = dvpEnd(); + if(status != DVP_STATUS_OK) + { + setError(ctx, "DVP texture->buffer transfer failed", status); + return NV_DVP_ERROR_TRANSFER_FAILED; + } + if(endStatus != DVP_STATUS_OK) + { + setError(ctx, "dvpEnd after texture->buffer", endStatus); + return NV_DVP_ERROR_SYNC_FAILED; + } + + return NV_DVP_SUCCESS; +} + +NV_DVP_API NvDvpError nv_dvp_copy_buffer_to_texture( + NvDvpContextHandle ctx, NvDvpResourceHandle src_buffer, + NvDvpResourceHandle dst_texture) +{ + if(!ctx || !src_buffer || !dst_texture) + return NV_DVP_ERROR_INVALID_PARAMETER; + + auto* buf = asResource(src_buffer); + auto* tex = asResource(dst_texture); + if(buf->kind != ResourceState::Kind::Buffer + || tex->kind != ResourceState::Kind::Texture) + return NV_DVP_ERROR_INVALID_HANDLE; + + std::lock_guard lock(ctx->mtx); + + /* Upload sequence per Blender/UPBGE VideoDeckLink (the reference libdvp + * consumer): EndAPI marks the API stream done with the texture BEFORE the + * DVP scope; the copy waits on the sysmem buffer's sync (CPU-signalled + * "data ready") and signals the texture's own sync on completion; WaitAPI + * after the scope makes the API stream wait for the copy before sampling. + * Deviating from this exact shape makes dvpMemcpyLined return + * DVP_STATUS_ERROR. */ + const char* step = "dvpMapBufferEndAPI(tex)"; + DVPStatus status = dvpMapBufferEndAPI(tex->dvp); + if(status != DVP_STATUS_OK) + { + setError(ctx, step, status); + return NV_DVP_ERROR_TRANSFER_FAILED; + } + + DVP_CHECK(dvpBegin(), ctx, NV_DVP_ERROR_SYNC_FAILED); + + step = "dvpMapBufferWaitDVP(tex)"; + status = dvpMapBufferWaitDVP(tex->dvp); + if(status == DVP_STATUS_OK) + { + step = "dvpMemcpyLined(buf->tex)"; + buf->bufSync.acquireValue++; + *buf->bufSync.sem = buf->bufSync.acquireValue; + tex->bufSync.releaseValue++; + status = dvpMemcpyLined( + buf->dvp, buf->bufSync.obj, buf->bufSync.acquireValue, + DVP_TIMEOUT_IGNORED, tex->dvp, tex->bufSync.obj, + tex->bufSync.releaseValue, + /*startingLine=*/0, /*numberOfLines=*/tex->height); + } + if(status == DVP_STATUS_OK) + { + step = "dvpMapBufferEndDVP(tex)"; + status = dvpMapBufferEndDVP(tex->dvp); + } + + DVPStatus endStatus = dvpEnd(); + if(status != DVP_STATUS_OK) + { + setError(ctx, step, status); + return NV_DVP_ERROR_TRANSFER_FAILED; + } + if(endStatus != DVP_STATUS_OK) + { + setError(ctx, "dvpEnd after buffer->texture", endStatus); + return NV_DVP_ERROR_SYNC_FAILED; + } + + status = dvpMapBufferWaitAPI(tex->dvp); + if(status != DVP_STATUS_OK) + { + setError(ctx, "dvpMapBufferWaitAPI(tex)", status); + return NV_DVP_ERROR_SYNC_FAILED; + } + + return NV_DVP_SUCCESS; +} + +/* ============================================================================ + * Per-frame CUDA transfers + * + * The CUDA-stream sync pattern differs from the GL/D3D11 case: instead of + * the API-side dvpMapBufferEndAPI / dvpMapBufferWaitAPI calls (which are + * implicit at queue boundaries on GL/D3D11), the CUDA path uses + * dvpMapBufferWaitCUDAStream / dvpMapBufferEndCUDAStream which insert + * wait/signal operations into a user-supplied CUstream. These stream + * sync calls are themselves *inside* the dvpBegin/dvpEnd pair (per + * NVIDIA's dvpapi.h docs), unlike the API variants. + * ============================================================================ */ + +NV_DVP_API NvDvpError nv_dvp_copy_buffer_to_cuda( + NvDvpContextHandle ctx, NvDvpResourceHandle src_buffer, + NvDvpResourceHandle dst_cuda, void* cuda_stream) +{ + if(!ctx || !src_buffer || !dst_cuda + || ctx->backend != NvDvpContext_t::Backend::CUDA) + return NV_DVP_ERROR_INVALID_PARAMETER; + + auto* buf = asResource(src_buffer); + auto* dst = asResource(dst_cuda); + if(buf->kind != ResourceState::Kind::Buffer + || dst->kind != ResourceState::Kind::Texture) + return NV_DVP_ERROR_INVALID_HANDLE; + + std::lock_guard lock(ctx->mtx); + + DVP_CHECK(dvpBegin(), ctx, NV_DVP_ERROR_SYNC_FAILED); + + CUstream stream = static_cast(cuda_stream); + DVPStatus status = dvpMapBufferWaitCUDAStream(dst->dvp, stream); + if(status == DVP_STATUS_OK) + { + buf->bufSync.releaseValue++; + status = dvpMemcpyLined( + buf->dvp, /*srcSync=*/0, /*srcWaitValue=*/0, DVP_TIMEOUT_IGNORED, + dst->dvp, buf->bufSync.obj, buf->bufSync.releaseValue, + /*startingLine=*/0, /*numberOfLines=*/dst->height); + } + if(status == DVP_STATUS_OK) + status = dvpMapBufferEndCUDAStream(dst->dvp, stream); + if(status == DVP_STATUS_OK) + status = dvpSyncObjClientWaitPartial( + buf->bufSync.obj, buf->bufSync.releaseValue, DVP_TIMEOUT_IGNORED); + + DVPStatus endStatus = dvpEnd(); + if(status != DVP_STATUS_OK) + { + setError(ctx, "DVP buffer->CUDA transfer failed", status); + return NV_DVP_ERROR_TRANSFER_FAILED; + } + if(endStatus != DVP_STATUS_OK) + { + setError(ctx, "dvpEnd after buffer->CUDA", endStatus); + return NV_DVP_ERROR_SYNC_FAILED; + } + + return NV_DVP_SUCCESS; +} + +NV_DVP_API NvDvpError nv_dvp_copy_cuda_to_buffer( + NvDvpContextHandle ctx, NvDvpResourceHandle src_cuda, + NvDvpResourceHandle dst_buffer, void* cuda_stream) +{ + if(!ctx || !src_cuda || !dst_buffer + || ctx->backend != NvDvpContext_t::Backend::CUDA) + return NV_DVP_ERROR_INVALID_PARAMETER; + + auto* src = asResource(src_cuda); + auto* buf = asResource(dst_buffer); + if(src->kind != ResourceState::Kind::Texture + || buf->kind != ResourceState::Kind::Buffer) + return NV_DVP_ERROR_INVALID_HANDLE; + + std::lock_guard lock(ctx->mtx); + + DVP_CHECK(dvpBegin(), ctx, NV_DVP_ERROR_SYNC_FAILED); + + CUstream stream = static_cast(cuda_stream); + DVPStatus status = dvpMapBufferWaitCUDAStream(src->dvp, stream); + if(status == DVP_STATUS_OK) + { + buf->bufSync.releaseValue++; + status = dvpMemcpyLined( + src->dvp, /*srcSync=*/0, /*srcWaitValue=*/0, DVP_TIMEOUT_IGNORED, + buf->dvp, buf->bufSync.obj, buf->bufSync.releaseValue, + /*startingLine=*/0, /*numberOfLines=*/buf->height); + } + if(status == DVP_STATUS_OK) + status = dvpMapBufferEndCUDAStream(src->dvp, stream); + if(status == DVP_STATUS_OK) + status = dvpSyncObjClientWaitPartial( + buf->bufSync.obj, buf->bufSync.releaseValue, DVP_TIMEOUT_IGNORED); + + DVPStatus endStatus = dvpEnd(); + if(status != DVP_STATUS_OK) + { + setError(ctx, "DVP CUDA->buffer transfer failed", status); + return NV_DVP_ERROR_TRANSFER_FAILED; + } + if(endStatus != DVP_STATUS_OK) + { + setError(ctx, "dvpEnd after CUDA->buffer", endStatus); + return NV_DVP_ERROR_SYNC_FAILED; + } + + return NV_DVP_SUCCESS; +} + +/* ============================================================================ + * Cross-platform 4K-aligned allocation + * ============================================================================ */ + +NV_DVP_API void* nv_dvp_aligned_alloc(uint64_t bytes) +{ +#if defined(_WIN32) + return ::_aligned_malloc(static_cast(bytes), 4096); +#else + void* p = nullptr; + /* posix_memalign requires alignment to be a power of 2 and a multiple + * of sizeof(void*); 4096 satisfies both. */ + if(::posix_memalign(&p, 4096, static_cast(bytes)) != 0) + return nullptr; + return p; +#endif +} + +NV_DVP_API void nv_dvp_aligned_free(void* ptr) +{ + if(!ptr) + return; +#if defined(_WIN32) + ::_aligned_free(ptr); +#else + std::free(ptr); +#endif +} + +} // extern "C" diff --git a/src/plugins/score-plugin-gfx/3rdparty/nv-dvp-bridge/nv_dvp_bridge.h b/src/plugins/score-plugin-gfx/3rdparty/nv-dvp-bridge/nv_dvp_bridge.h new file mode 100755 index 0000000000..20cb81318f --- /dev/null +++ b/src/plugins/score-plugin-gfx/3rdparty/nv-dvp-bridge/nv_dvp_bridge.h @@ -0,0 +1,301 @@ +/** + * @file nv_dvp_bridge.h + * @brief C API for AJA <-> NVIDIA "GPUDirect for Video" (DVP) on Windows. + * + * Despite the name, NVIDIA's GPUDirect for Video does not implement true + * GPU<->card peer-to-peer DMA on Windows; it is a high-throughput, + * hardware-synchronised DMA path between a registered backend GPU + * resource (D3D11 texture / OpenGL texture) and a registered, page-locked + * system-memory buffer. AJA's `dvplowlatencydemo` uses this path. + * + * Per output frame on score's AJA GPU-direct output path: + * + * 1. QRhi encoder (V210 / UYVY / BGRA fragment encoder) writes the AJA + * pixel format into a QRhi RGBA8 texture whose dimensions match the + * encoded frame layout (e.g. 1280x1080 for 1920x1080 v210). + * 2. The bridge performs a `dvpMemcpyLined` from the texture into a + * page-locked, AJA-DMA-locked system-memory buffer. The DVP DMA + * engine handles ordering between QRhi's queue and the copy via + * DVP sync objects. + * 3. AJA's `AutoCirculateTransfer` ships the system buffer over PCIe + * to the SDI card. + * + * The bridge expects the NVIDIA GPUDirect for Video SDK headers + * (`DVPAPI.h`, `dvpapi_d3d11.h`, `dvpapi_gl.h`) to be available at build + * time. Without them, score-plugin-gfx skips this bridge and AJA falls + * back to encoder + CPU staging via QRhi readback. + * + * Thread model: every thread that calls a transfer function must bracket + * its work with `nv_dvp_thread_begin` / `nv_dvp_thread_end` once. The + * thread that calls `nv_dvp_init_*` does the API binding (e.g. + * `dvpInitD3D11Device`); the underlying GL or D3D11 device must be + * usable from the calling thread at init time. + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/* Symbol visibility: the bridge's .cpp is compiled directly into the + * AJA addon (NV_DVP_BRIDGE_INLINE=1) so no DLL boundary is needed. + * The DLL build mode is preserved in case a future use needs it. + */ +#if defined(NV_DVP_BRIDGE_INLINE) + #define NV_DVP_API +#elif defined(_WIN32) + #ifdef NV_DVP_BRIDGE_EXPORTS + #define NV_DVP_API __declspec(dllexport) + #else + #define NV_DVP_API __declspec(dllimport) + #endif +#else + #define NV_DVP_API +#endif + +/* Opaque handle types */ +typedef struct NvDvpContext_t* NvDvpContextHandle; +typedef struct NvDvpResource_t* NvDvpResourceHandle; /* texture or buffer */ + +typedef enum NvDvpError { + NV_DVP_SUCCESS = 0, + NV_DVP_ERROR_NOT_INITIALIZED, + NV_DVP_ERROR_INIT_FAILED, + NV_DVP_ERROR_INVALID_PARAMETER, + NV_DVP_ERROR_INVALID_HANDLE, + NV_DVP_ERROR_INTEROP_FAILED, + NV_DVP_ERROR_TRANSFER_FAILED, + NV_DVP_ERROR_SYNC_FAILED, + NV_DVP_ERROR_ALLOC_FAILED, + NV_DVP_ERROR_UNKNOWN +} NvDvpError; + +/** Pixel format the DVP DMA understands. Only formats actually emitted + * by score's AJA encoders / used by AJA frame buffer formats are + * exposed. The bridge maps these to the matching DVP_* enum. */ +typedef enum NvDvpFormat { + NV_DVP_FORMAT_RGBA8 = 0, /**< DVP_RGBA + DVP_UNSIGNED_BYTE */ + NV_DVP_FORMAT_BGRA8 /**< DVP_BGRA + DVP_UNSIGNED_BYTE */ +} NvDvpFormat; + +/* ============================================================================ + * Context Management + * + * Each context binds DVP to a single backend (D3D11 device or GL context). + * To use both D3D11 and GL in the same process, create two contexts. + * ============================================================================ */ + +/** Bind DVP to an existing D3D11 device. The device must remain valid + * for the lifetime of the context. The bridge does not AddRef the + * device; the caller (QRhi in score's case) owns it. */ +NV_DVP_API NvDvpError nv_dvp_init_d3d11( + NvDvpContextHandle* out_ctx, + void* d3d11_device); /* ID3D11Device* */ + +/** Bind DVP to the current OpenGL context. The caller must have made + * the context current on the calling thread before calling this; DVP + * resolves required entry points from that context. */ +NV_DVP_API NvDvpError nv_dvp_init_gl(NvDvpContextHandle* out_ctx); + +/** Bind DVP to the current CUDA primary context. + * + * Cross-platform (Win + Linux). Caller must have a CUDA context + * active on the calling thread — `cuCtxSetCurrent(ctx)` from the + * shared `CudaFunctions` table is the typical setup. DVP's CUDA path + * is what enables sysmem↔CUDA-resource DMA with CUstream-side sync, + * useful for vendors without true GPU-direct P2P (e.g. DeckLink) and + * for cases where CUDA-stream integration is cleaner than a manual + * sync-object pair. */ +NV_DVP_API NvDvpError nv_dvp_init_cuda(NvDvpContextHandle* out_ctx); + +/** Tear down the DVP context. Must be called from the same thread that + * was last responsible for `nv_dvp_thread_begin` on this context if + * any thread is still inside `begin`/`end`. */ +NV_DVP_API void nv_dvp_shutdown(NvDvpContextHandle ctx); + + +/** Whether DVP is available at runtime. Returns false on non-NVIDIA + * GPUs and when the DVP runtime DLL isn't present. Cheap to call. */ +NV_DVP_API bool nv_dvp_available(void); + +NV_DVP_API const char* nv_dvp_get_error_string(NvDvpContextHandle ctx); + +/* ============================================================================ + * Per-thread scope + * + * `dvpBegin` / `dvpEnd` is per-thread; AJA's demos call once at thread + * startup, not per frame. Score's AJA strategies call begin from + * AJANode's render thread (the one that does the offscreen frame + + * encoder dispatch) once and end on shutdown. + * ============================================================================ */ + +NV_DVP_API NvDvpError nv_dvp_thread_begin(NvDvpContextHandle ctx); +NV_DVP_API NvDvpError nv_dvp_thread_end(NvDvpContextHandle ctx); + +/* ============================================================================ + * Resource registration + * + * Textures and buffers are registered once and reused across many + * transfers. The returned handle owns DVP-side state including a sync + * object pair that tracks DMA ordering against API access. + * ============================================================================ */ + +/** Register a D3D11 texture (typically a colour-attachment texture + * produced by QRhi's encoder render target). The texture must be + * D3D11_USAGE_DEFAULT, ALLOW the bind flags QRhi sets for render + * targets, and have format + dimensions exactly matching the + * arguments. */ +NV_DVP_API NvDvpError nv_dvp_register_d3d11_texture( + NvDvpContextHandle ctx, + void* d3d11_texture, /* ID3D11Texture2D* */ + NvDvpFormat format, + uint32_t width, + uint32_t height, + NvDvpResourceHandle* out_handle); + +/** Register an OpenGL 2D texture by GL id. The OpenGL context bound + * at `nv_dvp_init_gl` time must be current when this is called. */ +NV_DVP_API NvDvpError nv_dvp_register_gl_texture( + NvDvpContextHandle ctx, + uint32_t gl_texture_id, + NvDvpFormat format, + uint32_t width, + uint32_t height, + NvDvpResourceHandle* out_handle); + +/** Register a CUDA-allocated GPU **flat device pointer** with DVP. + * Useful for true P2P GPU buffers (CUDA-imported VkBuffer, CUDA- + * allocated cudaMalloc region, etc) that need DVP-mediated transfers + * with stream-side sync. `cuda_device_ptr` is passed as `uint64_t` + * to avoid pulling `` into consumers. */ +NV_DVP_API NvDvpError nv_dvp_register_cuda_device_ptr( + NvDvpContextHandle ctx, + uint64_t cuda_device_ptr, + NvDvpFormat format, + uint32_t width, + uint32_t height, + NvDvpResourceHandle* out_handle); + +/** Register a CUDA array (typically the level-0 array of a CUDA-imported + * mipmapped array — see `cuda_interop_import_vulkan_image`) with DVP. */ +NV_DVP_API NvDvpError nv_dvp_register_cuda_array( + NvDvpContextHandle ctx, + void* cuda_array, /* CUarray opaque pointer */ + NvDvpFormat format, + uint32_t width, + uint32_t height, + NvDvpResourceHandle* out_handle); + +/** Register a system-memory buffer. The caller owns the memory; the + * bridge stores the pointer and trusts it stays valid until + * unregister. Recommended allocation: `nv_dvp_aligned_alloc(size)` which + * returns 4K-aligned memory cross-platform (`_aligned_malloc` on + * Windows, `posix_memalign` on POSIX). Vendor-side pinning (e.g. + * AJA `DMABufferLock(inMap=true)`) is the caller's responsibility — + * the bridge does not pin. */ +NV_DVP_API NvDvpError nv_dvp_register_sysmem_buffer( + NvDvpContextHandle ctx, + void* sysmem_ptr, + NvDvpFormat format, + uint32_t width, + uint32_t height, + uint32_t stride_bytes, + NvDvpResourceHandle* out_handle); + +NV_DVP_API void nv_dvp_unregister( + NvDvpContextHandle ctx, NvDvpResourceHandle handle); + +/* ============================================================================ + * Cross-platform 4K-aligned allocation helper + * + * DVP wants `bufferAddrAlignment` (typically 4K). NVIDIA's documentation + * recommends page-aligned allocation regardless of the reported value. + * Windows uses `_aligned_malloc` / `_aligned_free`; POSIX uses + * `posix_memalign` paired with plain `free`. This helper hides the split. + * + * NULL is returned on allocation failure. + * ============================================================================ */ + +NV_DVP_API void* nv_dvp_aligned_alloc(uint64_t bytes); +NV_DVP_API void nv_dvp_aligned_free(void* ptr); + +/* ============================================================================ + * API-side sync: bracket GL/D3D access to a registered texture + * + * Between `nv_dvp_acquire_texture` and `nv_dvp_release_texture` the + * texture is owned by the API (QRhi) for rendering. Outside that range + * the bridge owns it for DVP DMA. Calling `copy_texture_to_buffer` + * implicitly releases the texture for DVP, then re-acquires it after + * the DMA completes; explicit acquire/release is only needed when QRhi + * wants to render to the texture between transfers without a transfer + * call in between (typical for the input pipeline overwriting the + * texture each frame). + * ============================================================================ */ + +NV_DVP_API NvDvpError nv_dvp_acquire_texture( + NvDvpContextHandle ctx, NvDvpResourceHandle texture); + +NV_DVP_API NvDvpError nv_dvp_release_texture( + NvDvpContextHandle ctx, NvDvpResourceHandle texture); + +/* ============================================================================ + * Per-frame transfers + * + * These are *synchronous* from the caller's perspective: the function + * returns when the DMA is complete and the destination is consistent. + * Synchronous transfer simplifies the AJA strategy at the cost of one + * frame of pipelining headroom; AJA's own dvplowlatencydemo uses + * asynchronous transfers + a multi-frame circular buffer for that + * reason. Score's AJANode already runs the AJA writes on a separate + * `AJAConsumerThread`, so the synchronous bridge call only stalls the + * render thread for the duration of the DMA (~ms on a typical PCIe). + * ============================================================================ */ + +/** OUTPUT path: copy from a registered texture to a registered sysmem + * buffer. Blocks until the destination is consistent. */ +NV_DVP_API NvDvpError nv_dvp_copy_texture_to_buffer( + NvDvpContextHandle ctx, + NvDvpResourceHandle src_texture, + NvDvpResourceHandle dst_buffer); + +/** INPUT path (capture): copy from a registered sysmem buffer to a + * registered texture. Blocks until the destination is consistent. */ +NV_DVP_API NvDvpError nv_dvp_copy_buffer_to_texture( + NvDvpContextHandle ctx, + NvDvpResourceHandle src_buffer, + NvDvpResourceHandle dst_texture); + +/* ============================================================================ + * Per-frame CUDA transfers + * + * Same shape as the texture variants but using CUstream-side sync. The + * caller provides a `CUstream` (or NULL for the default stream) and + * DVP's `dvpMapBufferWaitCUDAStream` / `dvpMapBufferEndCUDAStream` + * insert wait/signal operations into that stream. This is the path + * vendors without GPU-direct P2P (DeckLink, possibly some Magewell + * configurations) use to push captured frames into a CUDA-mapped + * texture without a CPU memcpy. + * + * `cuda_stream` is passed as `void*` (CUstream is opaque) — pass NULL + * for the default stream. + * ============================================================================ */ + +NV_DVP_API NvDvpError nv_dvp_copy_buffer_to_cuda( + NvDvpContextHandle ctx, + NvDvpResourceHandle src_buffer, + NvDvpResourceHandle dst_cuda, + void* cuda_stream); + +NV_DVP_API NvDvpError nv_dvp_copy_cuda_to_buffer( + NvDvpContextHandle ctx, + NvDvpResourceHandle src_cuda, + NvDvpResourceHandle dst_buffer, + void* cuda_stream); + +#ifdef __cplusplus +} +#endif diff --git a/src/plugins/score-plugin-gfx/CMakeLists.txt b/src/plugins/score-plugin-gfx/CMakeLists.txt index 0b4dab58f3..eb89aa8679 100644 --- a/src/plugins/score-plugin-gfx/CMakeLists.txt +++ b/src/plugins/score-plugin-gfx/CMakeLists.txt @@ -15,6 +15,8 @@ score_common_setup() find_package(${QT_VERSION} REQUIRED Gui) +add_subdirectory(3rdparty/nv-dvp-bridge) + if(NOT TARGET avformat AND NOT EMSCRIPTEN) find_package(FFmpeg COMPONENTS AVCODEC AVFORMAT AVUTIL AVDEVICE) endif() @@ -53,9 +55,11 @@ set(SYPHON_SRCS set(VTB_SRCS Gfx/Graph/decoders/HWVideoToolbox_metal.hpp Gfx/Graph/decoders/HWVideoToolbox_metal.mm + Gfx/Graph/interop/TextureShareMetal.mm ) set_source_files_properties( Gfx/Graph/decoders/HWVideoToolbox_metal.mm + Gfx/Graph/interop/TextureShareMetal.mm PROPERTIES SKIP_UNITY_BUILD_INCLUSION ON ) @@ -195,6 +199,7 @@ set(HDRS Gfx/Graph/GeometryFilterNode.hpp Gfx/Graph/GeometryFilterNodeRenderer.hpp Gfx/Graph/RhiComputeBarrier.hpp + Gfx/Graph/RhiTextureReadback.hpp Gfx/Graph/RhiClearBuffer.hpp Gfx/Graph/GPUBufferScatter.hpp Gfx/Graph/RenderedCSFNode.hpp @@ -206,8 +211,11 @@ set(HDRS Gfx/Graph/Node.hpp Gfx/Graph/NodeRenderer.hpp Gfx/Graph/OutputNode.hpp - Gfx/Graph/PreviewNode.hpp Gfx/Graph/RenderClock.hpp + Gfx/Graph/DMACaptureInputNode.hpp + Gfx/Graph/DirectVideoOutputBackend.hpp + Gfx/Graph/DirectVideoOutputNode.hpp + Gfx/Graph/PreviewNode.hpp Gfx/Graph/SceneGPUState.hpp Gfx/Graph/GpuResourceRegistry.hpp Gfx/Graph/VertexFallbackDefaults.hpp @@ -242,7 +250,12 @@ set(HDRS Gfx/Graph/Window.hpp Gfx/Graph/decoders/DXV.hpp - Gfx/Graph/decoders/DMABufImport.hpp + Gfx/Graph/interop/DMABufImport.hpp + Gfx/Graph/interop/DmaBufImportCapture.hpp + Gfx/Graph/interop/DrmFourcc.hpp + Gfx/Graph/interop/DrmPrimeWrap.hpp + Gfx/Graph/interop/EglDmaBufImport.hpp + Gfx/Graph/interop/EglDmaBufExport.hpp Gfx/Graph/encoders/ColorSpaceOut.hpp Gfx/Graph/encoders/GPUVideoEncoder.hpp Gfx/Graph/encoders/UYVY.hpp @@ -250,12 +263,75 @@ set(HDRS Gfx/Graph/encoders/I420.hpp Gfx/Graph/encoders/BGRA.hpp Gfx/Graph/encoders/V210.hpp + Gfx/Graph/encoders/YUV422P10.hpp + Gfx/Graph/encoders/P010.hpp Gfx/Graph/encoders/V210Compute.hpp Gfx/Graph/encoders/UYVYCompute.hpp Gfx/Graph/encoders/BGRACompute.hpp Gfx/Graph/encoders/ComputeEncoder.hpp + Gfx/Graph/encoders/PackedRGB.hpp + Gfx/Graph/encoders/YUY2.hpp + Gfx/Graph/encoders/YUVPlanar.hpp + Gfx/Graph/encoders/WireEncoderFactory.hpp + + Gfx/Graph/interop/CudaFunctions.hpp + Gfx/Graph/interop/CudaInterop.h + Gfx/Graph/interop/CudaInterop.cpp + Gfx/Graph/interop/VkExternalMemoryHelpers.hpp + Gfx/Graph/interop/VulkanCudaBounce.hpp + Gfx/Graph/interop/VulkanCudaBounce.cpp + Gfx/Graph/interop/VkExternalMemoryHelpers.cpp + Gfx/Graph/interop/D3D12HostImportUpload.hpp + Gfx/Graph/interop/VkHostImportUpload.hpp + Gfx/Graph/interop/D3D12HostImportUpload.cpp + Gfx/Graph/interop/VkHostImportUpload.cpp + Gfx/Graph/interop/TextureShare.hpp + Gfx/Graph/interop/TextureShare.cpp + Gfx/Graph/interop/AVHWFrameToQRhi.hpp + Gfx/Graph/interop/ImportedGpuBufferRing.hpp + Gfx/Graph/interop/ImportedGpuBufferRing.cpp + Gfx/Graph/interop/InteropFence.hpp + Gfx/Graph/interop/InteropFence.cpp + Gfx/Graph/interop/ComputeRingDispatcher.hpp + Gfx/Graph/interop/VideoOutputStrategy.hpp + Gfx/Graph/interop/VideoCaptureStrategy.hpp + Gfx/Graph/interop/CaptureStrategyCommon.hpp + Gfx/Graph/interop/CpuStagedCapture.hpp + Gfx/Graph/interop/RdmaRingDepth.hpp + Gfx/Graph/interop/VulkanRhiContext.hpp + # Shared, vendor-neutral NVIDIA-DVP shim templates (parameterized by a + # per-vendor DMA-lock policy). Header-only; consumed by capture-card addons. + Gfx/Graph/interop/DmaLockPolicy.hpp + Gfx/Graph/interop/DvpCaptureGl.hpp + Gfx/Graph/interop/DvpCaptureD3D11.hpp + Gfx/Graph/interop/DvpOutputGl.hpp + Gfx/Graph/interop/DvpOutputD3D11.hpp + Gfx/Graph/interop/GLCaptureUpload.hpp + Gfx/Graph/interop/RdmaVideoOutput.hpp + Gfx/Graph/interop/RdmaVideoOutput.cpp + Gfx/Graph/interop/CpuStagedVideoOutput.hpp + Gfx/Graph/interop/CpuStagedVideoOutput.cpp + Gfx/Graph/interop/HostFramePool.hpp + Gfx/Graph/interop/HostFramePool.cpp + Gfx/Graph/interop/VendorDmaRegistrar.hpp + Gfx/Graph/interop/PacedFramePump.hpp + Gfx/Graph/interop/PacedFramePump.cpp + Gfx/Graph/interop/VideoOutputStrategySelect.hpp Gfx/Graph/interop/VideoPixelFormatAV.hpp Gfx/Graph/interop/VideoPixelFormatAV.cpp + Gfx/Graph/interop/GpuCapabilities.hpp + Gfx/Graph/interop/GpuCapabilities.cpp + Gfx/Graph/interop/CudaVmmAllocator.hpp + Gfx/Graph/interop/CudaVmmAllocator.cpp + Gfx/Graph/interop/StageProfiler.hpp + Gfx/Graph/interop/RdmaGpuBuffer.hpp + Gfx/Graph/interop/RdmaGpuBuffer.cpp + Gfx/Graph/interop/AmdPinnedBuffers.hpp + Gfx/Graph/interop/AmdPinnedBuffers.cpp + Gfx/Graph/interop/HostPinnedRing.hpp + Gfx/Graph/interop/HostPinnedRing.cpp + Gfx/Graph/interop/VkCudaSemaphore.hpp + Gfx/Graph/interop/VkCudaSemaphore.cpp Gfx/Graph/interop/VideoPixelFormat.hpp Gfx/Graph/interop/V4L2PixelFormat.hpp Gfx/Graph/interop/DirectShowPixelFormat.hpp @@ -272,6 +348,7 @@ set(HDRS Gfx/Graph/decoders/GPUVideoDecoder.hpp Gfx/Graph/decoders/GPUVideoDecoderFactory.hpp Gfx/Graph/decoders/HAP.hpp + Gfx/Graph/decoders/WireDecoderFactory.hpp Gfx/Graph/decoders/HWTransfer.hpp Gfx/Graph/decoders/HWVAAPI.hpp Gfx/Graph/decoders/HWCUDA.hpp @@ -283,6 +360,7 @@ set(HDRS Gfx/Graph/decoders/HWVulkanShared.hpp Gfx/Graph/VulkanVideoDevice.hpp Gfx/Graph/decoders/NV12.hpp + Gfx/Graph/decoders/NV12ExternalOES.hpp Gfx/Graph/decoders/NV16.hpp Gfx/Graph/decoders/NV24.hpp Gfx/Graph/decoders/P010.hpp @@ -399,6 +477,8 @@ set(SRCS Gfx/Graph/decoders/DXV.cpp Gfx/Graph/decoders/GPUVideoDecoder.cpp + Gfx/Graph/decoders/NV12ExternalOES.cpp + Gfx/Graph/decoders/BayerExternalOES.cpp Gfx/Graph/decoders/GPUVideoDecoderFactory.cpp Gfx/Graph/decoders/HAP.cpp Gfx/Graph/BackgroundNode.cpp @@ -407,6 +487,7 @@ set(SRCS Gfx/Graph/GeometryFilterNode.cpp Gfx/Graph/GeometryFilterNodeRenderer.cpp Gfx/Graph/RhiComputeBarrier.cpp + Gfx/Graph/RhiTextureReadback.cpp Gfx/Graph/RhiClearBuffer.cpp Gfx/Graph/GPUBufferScatter.cpp Gfx/Graph/RenderedCSFNode.cpp @@ -418,8 +499,11 @@ set(SRCS Gfx/Graph/Node.cpp Gfx/Graph/NodeRenderer.cpp Gfx/Graph/OutputNode.cpp - Gfx/Graph/PreviewNode.cpp Gfx/Graph/RenderClock.cpp + Gfx/Graph/DMACaptureInputNode.cpp + Gfx/Graph/DirectVideoOutputBackend.cpp + Gfx/Graph/DirectVideoOutputNode.cpp + Gfx/Graph/PreviewNode.cpp Gfx/Graph/SceneGPUState.cpp Gfx/Graph/GpuResourceRegistry.cpp Gfx/Graph/VertexFallbackDefaults.cpp @@ -527,6 +611,10 @@ target_link_libraries(${PROJECT_NAME} PUBLIC ${QT_PREFIX}::ShaderTools ${QT_PREFIX}::ShaderToolsPrivate ${QT_PREFIX}::GuiPrivate ${QT_PREFIX}::Quick "$" ) + +# nv-dvp-bridge: HostPinnedRing's DVP backend uses the bridge C API +# directly; needs the include path and the static lib. +target_link_libraries(${PROJECT_NAME} PRIVATE score_nv_dvp_bridge) if(TARGET ${QT_PREFIX}::Svg) target_link_libraries(${PROJECT_NAME} PUBLIC "${QT_PREFIX}::Svg") endif() @@ -709,6 +797,30 @@ if(NOT EMSCRIPTEN) endif() endif() +if(NOT EMSCRIPTEN AND NOT APPLE AND NOT WIN32 AND TARGET pipewire::pipewire) + target_sources(${PROJECT_NAME} PRIVATE + Gfx/Pipewire/PipewireFormats.hpp + Gfx/Pipewire/PipewireInputDevice.hpp + Gfx/Pipewire/PipewireInputDevice.cpp + Gfx/Pipewire/PipewireOutputDevice.hpp + Gfx/Pipewire/PipewireOutputDevice.cpp + ) + set_source_files_properties( + Gfx/Pipewire/PipewireInputDevice.cpp + Gfx/Pipewire/PipewireOutputDevice.cpp + PROPERTIES SKIP_UNITY_BUILD_INCLUSION ON + ) + # Include-only: pull pipewire's INTERFACE_INCLUDE_DIRECTORIES without + # linking libpipewire-0.3 (DT_NEEDED would defeat dlopen). The shared + # layer in libremidi resolves every pw_* symbol at runtime. + get_target_property(_pw_includes pipewire::pipewire INTERFACE_INCLUDE_DIRECTORIES) + if(_pw_includes) + target_include_directories(${PROJECT_NAME} PRIVATE ${_pw_includes}) + endif() + target_compile_definitions(${PROJECT_NAME} PRIVATE SCORE_HAS_PIPEWIRE_VIDEO_IO) + list(APPEND SCORE_FEATURES_LIST pipewire_video) +endif() + if(freenect2_FOUND) target_compile_definitions(${PROJECT_NAME} PRIVATE HAS_FREENECT2) target_include_directories(${PROJECT_NAME} PRIVATE ${freenect2_INCLUDE_DIR}) @@ -722,13 +834,137 @@ endif() # Target-specific options setup_score_plugin(${PROJECT_NAME}) +# --- Direct DRM/KMS output (Linux only; libdrm + gbm) ------------------------- +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + find_package(PkgConfig QUIET) + if(PkgConfig_FOUND) + pkg_check_modules(SCORE_LIBDRM QUIET libdrm) + pkg_check_modules(SCORE_GBM QUIET gbm) + endif() + if(SCORE_LIBDRM_FOUND) + target_sources(score_plugin_gfx PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/Gfx/Graph/interop/DrmKmsDevice.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Gfx/Graph/interop/DrmKmsDevice.hpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Gfx/Graph/KmsOutputNode.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Gfx/Graph/KmsOutputNode.hpp") + # Headers only: libdrm is dlopen'd at runtime (DrmFunctions.hpp), so a + # machine without it still builds and simply reports the rung unavailable. + target_include_directories(score_plugin_gfx PUBLIC ${SCORE_LIBDRM_INCLUDE_DIRS}) + target_compile_definitions(score_plugin_gfx PRIVATE SCORE_HAS_DRM_KMS=1) + + add_executable(DrmOutputTest tests/DrmOutputTest.cpp) + target_link_libraries(DrmOutputTest PRIVATE score_plugin_gfx) + target_include_directories(DrmOutputTest PRIVATE ${SCORE_LIBDRM_INCLUDE_DIRS}) + + add_executable(KmsOutputTest tests/KmsOutputTest.cpp) + target_link_libraries(KmsOutputTest PRIVATE + score_plugin_gfx ${QT_PREFIX}::Gui ${QT_PREFIX}::Widgets) + target_include_directories(KmsOutputTest PRIVATE ${SCORE_LIBDRM_INCLUDE_DIRS}) + message(STATUS "score-plugin-gfx: direct DRM/KMS output enabled") + else() + message(STATUS "score-plugin-gfx: DRM/KMS output disabled (libdrm headers missing)") + endif() +endif() + if(SCORE_ISF_TESTER) add_executable(ISFTester tests/ISFTester.cpp) target_link_libraries(ISFTester PRIVATE score_plugin_gfx) endif() +# Capture-correction tests. Both are gated: score_add_test does not exist at all +# in a deployment build (an unguarded call is a configure error, not a skipped +# test), and there is no reason for WASM or Flatpak to build a GPU test they +# cannot run. +if(SCORE_TESTING) + # The real GLSL against the CPU reference. Standalone rather than Catch2 + # because it needs a QGuiApplication and a GPU; skips cleanly without one. + add_executable(CaptureAdjustShaderTest tests/CaptureAdjustShaderTest.cpp) + target_link_libraries(CaptureAdjustShaderTest PRIVATE + score_plugin_gfx ${QT_PREFIX}::Gui) + + # The maths, the struct layout and the cross-thread slot: no GPU, no camera, + # no display. + score_add_test(CaptureAdjustTest + SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/tests/CaptureAdjustTest.cpp" + PLUGINS score_plugin_gfx) +endif() + if(SCORE_VIDEO_TESTER) add_executable(VideoTester tests/VideoTester.cpp) target_link_libraries(VideoTester PRIVATE score_plugin_gfx) + +# Headless-friendly variant used by tests/integration/video-decoder-sweep.sh +add_executable(VideoDecoderTester tests/VideoDecoderTester.cpp) +target_link_libraries(VideoDecoderTester PRIVATE score_plugin_gfx) +endif() + +# Offscreen unit test for the GPU video encoders (no AJA/libav/gst needed). +# In static-plugin builds score_init_static_plugins() references every +# configured plugin, so the top-level CMakeLists completes the link with +# SCORE_PLUGINS_LIST once all plugins have been declared. +# App-free unit tests: no display, no GPU, runnable everywhere. Deliberately +# outside the encoder-tester option: turning that off must not take the unit +# tests with it. +if(SCORE_TESTING) + add_subdirectory(Tests) +endif() + +# EncoderTester needs a GPU and an RHI, so it can never run in the wasm job, +# and it is a bare add_executable that never goes through +# setup_score_common_features() -- which is where emscripten's --bind is added. +# Linking it under a static-plugin wasm build therefore fails on the _emval_* +# symbols Qt6Core/WebSockets/Gui pull in through embind. +option(SCORE_ENCODER_TESTER "Build the GPU encoder offscreen self-test" ON) +if(SCORE_ENCODER_TESTER AND NOT EMSCRIPTEN) +add_executable(EncoderTester tests/EncoderTester.cpp) +target_link_libraries(EncoderTester PRIVATE + score_plugin_gfx score_plugin_media ${QT_PREFIX}::Gui) + +# App-free unit tests: no display, no GPU, runnable everywhere. +if(SCORE_TESTING) + add_subdirectory(Tests) +endif() + +# Register the self-test as a ctest when the test suite is enabled. It needs +# a GL-capable display (llvmpipe is fine): label it "gui" like the other +# display-dependent tests so it can be excluded with `ctest -LE gui`. +if(SCORE_TESTING) + add_test(NAME EncoderTester COMMAND EncoderTester) + set_tests_properties(EncoderTester PROPERTIES + WORKING_DIRECTORY "${SCORE_ROOT_BINARY_DIR}" + ENVIRONMENT "SCORE_AUDIO_BACKEND=dummy;ASAN_OPTIONS=detect_leaks=0:halt_on_error=0" + LABELS "gui") +endif() +endif() + +# Offscreen self-test + micro-benchmark for the host-memory texture readback. +option(SCORE_READBACK_TESTER "Build the host-memory readback self-test" ON) +if(SCORE_READBACK_TESTER AND NOT SCORE_STATIC_PLUGINS) +add_executable(ReadbackTester tests/ReadbackTester.cpp) +target_link_libraries(ReadbackTester PRIVATE + score_plugin_gfx score_plugin_media ${QT_PREFIX}::Gui) + +if(SCORE_TESTING) + add_test(NAME ReadbackTester COMMAND ReadbackTester) + set_tests_properties(ReadbackTester PROPERTIES + WORKING_DIRECTORY "${SCORE_ROOT_BINARY_DIR}" + ENVIRONMENT "SCORE_AUDIO_BACKEND=dummy;ASAN_OPTIONS=detect_leaks=0:halt_on_error=0" + LABELS "gui") +endif() +endif() + +# PipeWire video round-trip harness: drives the real Gfx/Pipewire producer + +# consumer against the live daemon across all pixel formats and transports. +option(SCORE_PIPEWIRE_TESTER "Build the PipeWire video round-trip harness" ON) +if(SCORE_PIPEWIRE_TESTER + AND NOT EMSCRIPTEN AND NOT APPLE AND NOT WIN32 AND NOT SCORE_STATIC_PLUGINS + AND TARGET pipewire::pipewire) +add_executable(PipewireRoundtrip tests/PipewireRoundtrip.cpp) +# pipewire::pipewire is include-only (score dlopens libpipewire); the +# harness's raw reference producer calls libpipewire directly, so link it. +find_library(PIPEWIRE_LIBRARY NAMES pipewire-0.3) +target_link_libraries(PipewireRoundtrip PRIVATE + score_plugin_gfx score_plugin_media ${QT_PREFIX}::Gui ${QT_PREFIX}::Widgets + pipewire::pipewire ${PIPEWIRE_LIBRARY} swscale) endif() diff --git a/src/plugins/score-plugin-gfx/Gfx/CameraDevice.v4l2.cpp b/src/plugins/score-plugin-gfx/Gfx/CameraDevice.v4l2.cpp index 6881622d80..fea600d27a 100644 --- a/src/plugins/score-plugin-gfx/Gfx/CameraDevice.v4l2.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/CameraDevice.v4l2.cpp @@ -16,20 +16,7 @@ extern "C" { #include #include -// libv4l2.h is sometimes not here... -extern "C" -{ -int v4l2_open(const char *file, int oflag, ...); -int v4l2_close(int fd); -int v4l2_dup(int fd); -int v4l2_ioctl(int fd, unsigned long int request, ...); -ssize_t v4l2_read(int fd, void *buffer, size_t n); -ssize_t v4l2_write(int fd, const void *buffer, size_t n); -void *v4l2_mmap(void *start, size_t length, int prot, int flags, - int fd, int64_t offset); -int v4l2_munmap(void *_start, size_t length); -} - +#include #include #include @@ -89,33 +76,7 @@ AVPixelFormat ff_fmt_v4l2ff(uint32_t v4l2_fmt, AVCodecID codec_id) return toAVPixelFormat(chromaSwappedTwin(layout)); } -class libv4l2 -{ -public: - decltype(&::v4l2_ioctl) ioctl{}; - decltype(&::v4l2_open) open{}; - decltype(&::v4l2_close) close{}; - static const libv4l2& instance() - { - static const libv4l2 self; - return self; - } - -private: - libv4l2() - : library("libv4l2.so.0") - { - open = library.symbol("v4l2_open"); - close = library.symbol("v4l2_close"); - ioctl = library.symbol("v4l2_ioctl"); - - assert(open); - assert(close); - assert(ioctl); - } - - ossia::dylib_loader library; -}; +using libv4l2 = score::gfx::v4l2::Libv4l2; static QString v4l2_pretty_name(const AVDeviceInfo& dev) { diff --git a/src/plugins/score-plugin-gfx/Gfx/CaptureControlTree.hpp b/src/plugins/score-plugin-gfx/Gfx/CaptureControlTree.hpp new file mode 100644 index 0000000000..839638a583 --- /dev/null +++ b/src/plugins/score-plugin-gfx/Gfx/CaptureControlTree.hpp @@ -0,0 +1,188 @@ +#pragma once + +/** + * @file CaptureControlTree.hpp + * @brief Publishes a capture node's corrections as `/render/`. + * + * The counterpart to the driver-discovered `/controls/` group: those are + * whatever the hardware happens to expose and differ per camera, these are + * declared by score and are the same everywhere. Keeping them apart is what + * stops a driver that publishes its own `gamma` from colliding with ours. + * + * Nothing here is V4L2-specific -- it drives a CaptureAdjustSlot, so any + * DMACaptureInputNode gets the same group whatever the vendor. + */ + +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace Gfx +{ + +/** + * @brief Builds `/render/` for one capture stream. + * + * Holds the working copy the controls edit. Each control owns one field, so a + * write is a read-modify-write of the whole struct -- serialised here, because + * two controls written at once from different threads would otherwise lose one + * of the two edits. + */ +class CaptureControlTree +{ +public: + CaptureControlTree( + score::gfx::CaptureAdjustSlot& slot, ossia::net::device_base& dev, + ossia::net::node_base& parent, std::string group = "render") + : m_slot{slot} + { + m_value = slot.value(); + + auto edit = [this](auto&& fn) { + std::lock_guard lk{m_mutex}; + fn(m_value); + m_slot.set(m_value); + }; + + std::vector controls; + + { + TreeControl t; + t.name = "scale_mode"; + t.description + = "How the frame is fitted to the viewport: Original, BlackBars, " + "Fill, Stretch"; + t.type = ossia::val_type::STRING; + t.domain = ossia::make_domain( + std::vector{"Original", "BlackBars", "Fill", "Stretch"}); + t.initial = std::string{"Stretch"}; + t.onSet = [edit](const ossia::value& v) { + const auto s = v.target(); + if(!s) + return; + // A name we do not know leaves the mode alone. Falling through to a + // default would mean an OSC client with a typo silently resetting the + // fit -- and the domain does not stop that, since a domain describes + // the values rather than enforcing them. + std::optional mode; + if(*s == "Original") + mode = score::gfx::ScaleMode::Original; + else if(*s == "BlackBars") + mode = score::gfx::ScaleMode::BlackBars; + else if(*s == "Fill") + mode = score::gfx::ScaleMode::Fill; + else if(*s == "Stretch") + mode = score::gfx::ScaleMode::Stretch; + if(!mode) + return; + edit([m = *mode](score::gfx::CaptureAdjust& a) { a.scaleMode = m; }); + }; + controls.push_back(std::move(t)); + } + + // Black level and white balance are per-channel, so one vec3 each rather + // than six scalars: they are set together in practice, and six nodes would + // mean six round-trips through the slot for one adjustment. + { + TreeControl t; + t.name = "black_level"; + t.description = "Sensor pedestal per channel, normalised, subtracted " + "before any gain"; + t.type = ossia::val_type::VEC3F; + t.domain = ossia::make_domain(0.f, 1.f); + t.initial = ossia::vec3f{0.f, 0.f, 0.f}; + t.onSet = [edit](const ossia::value& v) { + if(auto p = v.target()) + edit([p = *p](score::gfx::CaptureAdjust& a) { + for(int i = 0; i < 3; ++i) + a.blackLevel[i] = p[i]; + }); + }; + controls.push_back(std::move(t)); + } + + { + TreeControl t; + t.name = "white_balance"; + t.description = "Per-channel gain. A Bayer sensor reads green without it"; + t.type = ossia::val_type::VEC3F; + t.domain = ossia::make_domain(0.f, 8.f); + t.initial = ossia::vec3f{1.f, 1.f, 1.f}; + t.onSet = [edit](const ossia::value& v) { + if(auto p = v.target()) + edit([p = *p](score::gfx::CaptureAdjust& a) { + for(int i = 0; i < 3; ++i) + a.whiteBalance[i] = p[i]; + }); + }; + controls.push_back(std::move(t)); + } + + const auto scalar + = [&](std::string name, std::string desc, float lo, float hi, float init, + auto member) { + TreeControl t; + t.name = std::move(name); + t.description = std::move(desc); + t.type = ossia::val_type::FLOAT; + t.domain = ossia::make_domain(lo, hi); + t.initial = init; + t.onSet = [edit, member](const ossia::value& v) { + const auto f = ossia::convert(v); + // A domain describes values, it does not enforce them, so a client can + // send anything. NaN would survive every clamp in the shader -- it is + // not greater than or less than anything -- and take the frame with it. + if(!std::isfinite(f)) + return; + edit([f, member](score::gfx::CaptureAdjust& a) { a.*member = f; }); + }; + controls.push_back(std::move(t)); + }; + + scalar("exposure", "Linear multiplier applied after white balance", 0.f, + 16.f, 1.f, &score::gfx::CaptureAdjust::exposure); + // 1 is the identity and the default: raising it to 2.2 approximates sRGB, + // which is usually what makes a linear sensor frame stop looking dark. + scalar("gamma", "Encoding exponent; 1 leaves the signal linear, 2.2 is " + "roughly sRGB", 0.1f, 4.f, 1.f, + &score::gfx::CaptureAdjust::gamma); + scalar("saturation", "0 collapses to luma, 1 leaves colour alone", 0.f, 4.f, + 1.f, &score::gfx::CaptureAdjust::saturation); + + m_params = addControlGroup(dev, parent, group, controls); + } + + ~CaptureControlTree() + { + // The parameters outlive this object -- the device owns them and is + // destroyed after -- and their callbacks capture `this`. Cut the link + // before it can dangle. + for(auto* p : m_params) + if(p) + p->callbacks_clear(); + } + + CaptureControlTree(const CaptureControlTree&) = delete; + CaptureControlTree& operator=(const CaptureControlTree&) = delete; + + std::size_t count() const noexcept { return m_params.size(); } + +private: + score::gfx::CaptureAdjustSlot& m_slot; + std::mutex m_mutex; + score::gfx::CaptureAdjust m_value; + std::vector m_params; +}; + +} // namespace Gfx diff --git a/src/plugins/score-plugin-gfx/Gfx/ControlTree.hpp b/src/plugins/score-plugin-gfx/Gfx/ControlTree.hpp new file mode 100644 index 0000000000..eaccfd93fd --- /dev/null +++ b/src/plugins/score-plugin-gfx/Gfx/ControlTree.hpp @@ -0,0 +1,109 @@ +#pragma once + +/** + * @file ControlTree.hpp + * @brief Turn a list of control descriptions into device-tree nodes. + * + * Two very different things end up as settings under a video device: what the + * driver publishes (gain, exposure, white balance -- discovered at runtime and + * different on every camera) and what score itself offers (scale mode, and the + * demosaic's own corrections). They share no vocabulary, so this takes the one + * thing they do have in common -- a name, a type, a domain and something to do + * on write -- and builds the nodes from that. + * + * Kept free of V4L2 so the score-side group is not obliged to invent a fake + * driver control to describe itself. + */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace Gfx +{ + +/// One settable thing, described independently of where it came from. +struct TreeControl +{ + std::string name; ///< tree-safe; becomes the address component + std::string description; ///< shown in the explorer + + ossia::val_type type{}; + ossia::domain domain; + ossia::value initial; + ossia::access_mode access{ossia::access_mode::BI}; + + /// Called when something writes the parameter, on the writing thread. + /// Empty for a read-only value that is only ever pushed outward. + std::function onSet; +}; + +/** + * @brief Creates `//` for each control. + * + * @returns the created parameters, in the order of @p controls, so the caller + * can push values back into them later. An entry is null when its node could + * not be created -- a name collision, in practice. + */ +inline std::vector addControlGroup( + ossia::net::device_base& dev, ossia::net::node_base& parent, + const std::string& group, const std::vector& controls) +{ + std::vector out; + out.reserve(controls.size()); + + auto groupNode = std::make_unique(group, dev, parent); + auto* groupPtr = parent.add_child(std::move(groupNode)); + if(!groupPtr) + { + out.resize(controls.size(), nullptr); + return out; + } + + for(const auto& c : controls) + { + auto node = std::make_unique(c.name, dev, *groupPtr); + auto* param = node->create_parameter(c.type); + if(!param) + { + out.push_back(nullptr); + continue; + } + + if(c.domain) + param->set_domain(c.domain); + param->set_access(c.access); + + if(!c.description.empty()) + ossia::net::set_description(*node, c.description); + + // The initial value is set before the callback is installed: it describes + // what the hardware already holds, and writing it back would be a + // round-trip through the driver for no reason -- and, for a control whose + // write performs an action, an unwanted action. + if(c.initial.valid()) + param->push_value(c.initial); + + if(c.onSet) + { + auto cb = c.onSet; + param->add_callback(std::move(cb)); + } + + // add_child takes ownership; the node keeps the parameter alive. + auto* added = groupPtr->add_child(std::move(node)); + out.push_back(added ? param : nullptr); + } + + return out; +} + +} // namespace Gfx diff --git a/src/plugins/score-plugin-gfx/Gfx/GStreamer/GStreamerAudioBuffer.hpp b/src/plugins/score-plugin-gfx/Gfx/GStreamer/GStreamerAudioBuffer.hpp new file mode 100644 index 0000000000..e543e8d19a --- /dev/null +++ b/src/plugins/score-plugin-gfx/Gfx/GStreamer/GStreamerAudioBuffer.hpp @@ -0,0 +1,127 @@ +#pragma once + +/** + * @file GStreamerAudioBuffer.hpp + * @brief Lock-free bridge between GStreamer's chunk size and the engine tick. + * + * Lives in a header rather than in GStreamerDevice.cpp because the resize / + * span re-point invariant it carries is an audio-thread lifetime rule, and a + * rule nothing can exercise is a rule nothing keeps. + */ + +#include +#include + +#include +#include +#include +#include +#include + +namespace Gfx::GStreamer +{ + +// Audio: GStreamer delivers large chunks (e.g. 1024 samples). +// The audio engine reads small chunks (e.g. 64 samples). +// We use a lock-free ring buffer to bridge the two. +struct AudioBuffer +{ + int sample_rate{48000}; + int num_channels{2}; + + // Ring buffer per channel, written by GStreamer thread, read by audio engine + static constexpr std::size_t ring_size = 65536; + + // Max block the audio thread may resize the output storage to; the + // parameter reserves this up front so the per-tick resize never reallocates + // (a realloc would free a buffer the audio thread is reading through). + static constexpr std::size_t max_block = 1 << 15; + std::vector> ring; // [channel][ring_size] + std::atomic write_pos{0}; + std::atomic read_pos{0}; + + // Backing storage for audio spans — audio engine reads from here + std::vector* output_data{}; + + void init(int nchannels) + { + num_channels = nchannels; + ring.resize(nchannels); + for(auto& ch : ring) + ch.resize(ring_size, 0.f); + } + + // Called by GStreamer thread: write deinterleaved samples into ring + void write(const float* interleaved, int num_samples, int channels) + { + int nch = std::min(channels, num_channels); + auto wp = write_pos.load(std::memory_order_relaxed); + for(int s = 0; s < num_samples; s++) + { + for(int ch = 0; ch < nch; ch++) + ring[ch][(wp + s) % ring_size] = interleaved[s * channels + ch]; + } + write_pos.store(wp + num_samples, std::memory_order_release); + } + + // Points at the parameter's audio spans so read_into_output can re-point + // them after a resize. A raw pointer (not a std::function) so that clearing + // or using it during teardown can never throw on the audio thread. + ossia::small_vector, 8>* output_spans{}; + + // Called by audio engine (indirectly): copy from ring into output spans + void read_into_output(int block_size) + { + if(!output_data) + return; + + // The engine tick size can differ from the configured buffer size + // (e.g. PipeWire dynamic quantum); the storage follows it, but never + // beyond the capacity reserved at construction (so no reallocation). + if(block_size > (int)max_block) + block_size = max_block; + bool resized = false; + for(auto& v : *output_data) + { + if(std::ssize(v) != block_size) + { + v.resize(block_size); + resized = true; + } + } + if(resized && output_spans && output_data) + { + const std::size_t n = std::min(output_spans->size(), output_data->size()); + for(std::size_t i = 0; i < n; i++) + (*output_spans)[i] = (*output_data)[i]; + } + + auto rp = read_pos.load(std::memory_order_relaxed); + auto wp = write_pos.load(std::memory_order_acquire); + + // How many samples are available? + std::size_t available = (wp >= rp) ? (wp - rp) : 0; + + int nch = std::min((int)output_data->size(), num_channels); + if(available >= (std::size_t)block_size) + { + // Copy block_size samples from ring to output + for(int ch = 0; ch < nch; ch++) + { + auto& dst = (*output_data)[ch]; + auto& src = ring[ch]; + for(int s = 0; s < block_size; s++) + dst[s] = src[(rp + s) % ring_size]; + } + read_pos.store(rp + block_size, std::memory_order_release); + } + else + { + // Underrun: output silence + for(int ch = 0; ch < nch; ch++) + std::fill_n((*output_data)[ch].data(), block_size, 0.f); + } + } +}; + +} diff --git a/src/plugins/score-plugin-gfx/Gfx/GStreamer/GStreamerDevice.cpp b/src/plugins/score-plugin-gfx/Gfx/GStreamer/GStreamerDevice.cpp index 93d6af325f..d1feac8e3b 100644 --- a/src/plugins/score-plugin-gfx/Gfx/GStreamer/GStreamerDevice.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/GStreamer/GStreamerDevice.cpp @@ -32,7 +32,9 @@ extern "C" { #include } +#include #include +#include #include