diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake index 2fb12026135..886f8648a6b 100644 --- a/cmake/compile_definitions/windows.cmake +++ b/cmake/compile_definitions/windows.cmake @@ -78,6 +78,11 @@ set(PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/windows/display_ram.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/display_wgc.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/display_amd.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/capture_plugin/capture_plugin_api.h" + "${CMAKE_SOURCE_DIR}/src/platform/windows/capture_plugin/capture_plugin_loader.h" + "${CMAKE_SOURCE_DIR}/src/platform/windows/capture_plugin/capture_plugin_loader.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/capture_plugin/display_plugin.h" + "${CMAKE_SOURCE_DIR}/src/platform/windows/capture_plugin/display_plugin.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/audio.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/mic_write.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/device_hdr_states.cpp" diff --git a/examples/capture_plugin_nvfbc/CMakeLists.txt b/examples/capture_plugin_nvfbc/CMakeLists.txt new file mode 100644 index 00000000000..dc26bacf014 --- /dev/null +++ b/examples/capture_plugin_nvfbc/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.20) +project(sunshine_nvfbc_plugin LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Path to Sunshine source root (for plugin API header) +set(SUNSHINE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH + "Path to Sunshine source root") + +add_library(sunshine_nvfbc SHARED + sunshine_nvfbc_plugin.cpp +) + +target_include_directories(sunshine_nvfbc PRIVATE + "${SUNSHINE_SOURCE_DIR}" +) + +# NvFBC does not have a public import library on Windows. +# The plugin loads NvFBC64.dll at runtime via LoadLibrary. + +# Output to plugins/ directory for easy deployment +set_target_properties(sunshine_nvfbc PROPERTIES + OUTPUT_NAME "sunshine_nvfbc" + PREFIX "" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/plugins" +) diff --git a/examples/capture_plugin_nvfbc/NOTICE.md b/examples/capture_plugin_nvfbc/NOTICE.md new file mode 100644 index 00000000000..54f4bca4ca6 --- /dev/null +++ b/examples/capture_plugin_nvfbc/NOTICE.md @@ -0,0 +1,51 @@ +# NvFBC Capture Plugin — Legal Notice + +## Disclaimer + +This plugin interfaces with NVIDIA Frame Buffer Capture (NvFBC), a +proprietary NVIDIA technology. By using this plugin, you acknowledge +and agree to the following: + +1. **Authorization Required**: NvFBC access may be restricted to specific + GPU product lines (e.g., NVIDIA Quadro, Tesla, Grid). Using NvFBC on + unsupported hardware may violate NVIDIA's End User License Agreement + (EULA) for GeForce drivers. + +2. **No Bypass Mechanisms Included**: This plugin does NOT include any + authentication keys, private data, or technical measures to circumvent + NvFBC access restrictions. Users must independently obtain appropriate + NvFBC authorization. + +3. **External Private Data**: If your NvFBC configuration requires private + authentication data, it must be supplied externally via: + - A file at `plugins/nvfbc_auth.bin` (next to the plugin DLL) + - The `NVFBC_PRIVDATA_FILE` environment variable pointing to the file + + This plugin does not generate, distribute, or embed such data. + +4. **User Responsibility**: Users are solely responsible for ensuring their + use of NvFBC complies with all applicable laws, regulations, and license + agreements, including but not limited to the NVIDIA EULA, DMCA (US), + EU Copyright Directive, and local intellectual property laws. + +5. **No Warranty**: This plugin is provided "AS IS" without warranty of any + kind. The authors disclaim all liability for any damages arising from + its use. + +## Third-Party Attributions + +- **NvFBCCreateParams structure**: Based on definitions from + [keylase/nvidia-patch](https://github.com/keylase/nvidia-patch) + (MIT License) +- **NVFBCRESULT error codes**: Based on publicly documented NVIDIA + NvFBC error codes +- **Sunshine Capture Plugin API**: Part of the Sunshine project + (GPL-3.0 License) + +## NVIDIA Capture SDK + +For official NvFBC API headers and documentation, obtain the NVIDIA +Capture SDK from: https://developer.nvidia.com/capture-sdk + +The Capture SDK provides the complete INvFBCToSys interface definition +needed to fully implement the capture functionality in this plugin. diff --git a/examples/capture_plugin_nvfbc/nvfbc_win_defs.h b/examples/capture_plugin_nvfbc/nvfbc_win_defs.h new file mode 100644 index 00000000000..fa9d6868ff5 --- /dev/null +++ b/examples/capture_plugin_nvfbc/nvfbc_win_defs.h @@ -0,0 +1,134 @@ +/** + * @file nvfbc_win_defs.h + * @brief Windows NvFBC API type definitions. + * + * Based on publicly available information: + * - keylase/nvidia-patch nvfbcdefs.h (MIT license) for NvFBCCreateParams + * - NVIDIA public documentation for error codes and buffer formats + * + * IMPORTANT: + * This file does NOT contain proprietary NVIDIA Capture SDK headers. + * The INvFBCToSys interface is NOT defined here — users must provide + * their own interface definitions from an authorized source (NVIDIA + * Capture SDK, Grid SDK, etc.). + * + * No authentication credentials, private keys, or bypass mechanisms + * are included. Users are responsible for obtaining appropriate access + * to NvFBC functionality through legitimate channels. + */ +#pragma once + +#include +#include + +// ============================================================================ +// NvFBC version and macros (from keylase/nvidia-patch nvfbcdefs.h, MIT license) +// ============================================================================ + +typedef unsigned long NvU32; + +#define NVFBC_DLL_VERSION 0x70 + +#define NVFBC_STRUCT_VERSION(typeName, ver) \ + (NvU32)(sizeof(typeName) | ((ver) << 16) | (NVFBC_DLL_VERSION << 24)) + +#define NVFBCAPI __stdcall + +// ============================================================================ +// Error codes (NVFBCRESULT) — documented in NVIDIA public references +// ============================================================================ + +typedef enum _NVFBCRESULT { + NVFBC_SUCCESS = 0, + NVFBC_ERROR_GENERIC = -1, + NVFBC_ERROR_INVALID_PARAM = -2, + NVFBC_ERROR_INVALIDATED_SESSION = -3, + NVFBC_ERROR_PROTECTED_CONTENT = -4, + NVFBC_ERROR_DRIVER_FAILURE = -5, + NVFBC_ERROR_CUDA_FAILURE = -6, + NVFBC_ERROR_UNSUPPORTED = -7, + NVFBC_ERROR_HW_ENC_FAILURE = -8, + NVFBC_ERROR_INCOMPATIBLE_DRIVER = -9, + NVFBC_ERROR_UNSUPPORTED_PLATFORM = -10, + NVFBC_ERROR_OUT_OF_MEMORY = -11, + NVFBC_ERROR_INVALID_PTR = -12, + NVFBC_ERROR_INCOMPATIBLE_VERSION = -13, + NVFBC_ERROR_OPT_CAPTURE_FAILURE = -14, + NVFBC_ERROR_INSUFFICIENT_PRIVILEGES = -15, + NVFBC_ERROR_INVALID_CALL = -16, + NVFBC_ERROR_SYSTEM_ERROR = -17, + NVFBC_ERROR_INVALID_TARGET = -18, + NVFBC_ERROR_NVAPI_FAILURE = -19, + NVFBC_ERROR_DYNAMIC_DISABLE = -20, + NVFBC_ERROR_IPC_FAILURE = -21, + NVFBC_ERROR_CURSOR_CAPTURE_FAILURE = -22, +} NVFBCRESULT; + +// ============================================================================ +// Buffer formats — common across NvFBC platforms +// ============================================================================ + +typedef enum _NVFBC_BUFFER_FORMAT { + NVFBC_BUFFER_FORMAT_ARGB = 0, + NVFBC_BUFFER_FORMAT_RGB = 1, + NVFBC_BUFFER_FORMAT_NV12 = 2, + NVFBC_BUFFER_FORMAT_YUV444P = 3, + NVFBC_BUFFER_FORMAT_RGBA = 4, + NVFBC_BUFFER_FORMAT_BGRA = 5, +} NVFBC_BUFFER_FORMAT; + +// ============================================================================ +// Interface type IDs for NvFBC_CreateEx +// ============================================================================ + +typedef enum _NVFBC_INTERFACE_TYPE { + NVFBC_TO_SYS = 0, + NVFBC_TO_CUDA = 1, + NVFBC_TO_DX9VID = 2, + NVFBC_TO_HW_ENC = 3, +} NVFBC_INTERFACE_TYPE; + +// ============================================================================ +// NvFBC_CreateEx parameters (from keylase/nvidia-patch, MIT license) +// ============================================================================ + +typedef struct _NvFBCCreateParams { + NvU32 dwVersion; + NvU32 dwInterfaceType; + NvU32 dwMaxDisplayWidth; + NvU32 dwMaxDisplayHeight; + void *pDevice; + void *pPrivateData; + NvU32 dwPrivateDataSize; + NvU32 dwInterfaceVersion; + void *pNvFBC; + NvU32 dwAdapterIdx; + NvU32 dwNvFBCVersion; + void *cudaCtx; + void *pPrivateData2; + NvU32 dwPrivateData2Size; + NvU32 dwReserved[55]; + void *pReserved[27]; +} NvFBCCreateParams; + +#define NVFBC_CREATE_PARAMS_VER NVFBC_STRUCT_VERSION(NvFBCCreateParams, 2) + +// ============================================================================ +// Function pointer types for NvFBC64.dll exports +// ============================================================================ + +typedef NVFBCRESULT(NVFBCAPI *NvFBC_CreateFunctionExType)(void *pCreateParams); +typedef NVFBCRESULT(NVFBCAPI *NvFBC_GetStatusExFunctionType)(void *pStatusParams); + +// ============================================================================ +// Frame grab info (populated after successful capture) +// ============================================================================ + +typedef struct _NvFBCFrameGrabInfo { + NvU32 dwWidth; + NvU32 dwHeight; + NvU32 dwBufferWidth; + NvU32 dwReserved; + NvU32 bIsNewFrame; + NvU32 dwReservedFields[27]; +} NvFBCFrameGrabInfo; diff --git a/examples/capture_plugin_nvfbc/sunshine_nvfbc_plugin.cpp b/examples/capture_plugin_nvfbc/sunshine_nvfbc_plugin.cpp new file mode 100644 index 00000000000..4e996f09461 --- /dev/null +++ b/examples/capture_plugin_nvfbc/sunshine_nvfbc_plugin.cpp @@ -0,0 +1,387 @@ +/** + * @file examples/capture_plugin_nvfbc/sunshine_nvfbc_plugin.cpp + * @brief NvFBC capture plugin for Sunshine (Windows). + * + * Uses NvFBC (NVIDIA Frame Buffer Capture) to capture the desktop with + * near-zero latency, bypassing Desktop Duplication API overhead. + * + * Build: Compile as a DLL named "sunshine_nvfbc.dll" and place in + * Sunshine's "plugins/" directory. + * + * Usage: Set capture = nvfbc in sunshine.conf + * + * Prerequisites: + * - NVIDIA GPU with appropriate NvFBC access (Grid/Quadro license, + * or driver patched via keylase/nvidia-patch) + * - NvFBC64.dll present in system (shipped with NVIDIA driver) + * - Private data file at plugins/nvfbc_auth.bin (16 bytes) + * OR environment variable NVFBC_PRIVDATA_FILE pointing to the file + * + * LEGAL NOTICE: + * NvFBC access on consumer GPUs requires driver modification. + * This plugin does NOT include any bypass mechanisms or private keys. + * Users are responsible for ensuring they have appropriate authorization + * to use NvFBC on their hardware. See NOTICE file for details. + * + * NOTE on INvFBCToSys interface: + * The plugin requires NVIDIA Capture SDK headers for the INvFBCToSys + * vtable layout. The current implementation uses a minimal interface + * definition. If GrabFrame causes issues, replace with official SDK headers. + */ + +#include +#include +#include +#include +#include + +// NvFBC Windows API definitions (public types only) +#include "nvfbc_win_defs.h" + +// Sunshine capture plugin ABI +#include "src/platform/windows/capture_plugin/capture_plugin_api.h" + +// ============================================================================ +// Private data loading — external file, NOT hardcoded +// ============================================================================ + +// Expected private data size (4 x uint32 = 16 bytes) +static constexpr size_t NVFBC_PRIVDATA_SIZE = 16; + +/** + * Load NvFBC private data from an external binary file. + * + * Search order: + * 1. Environment variable NVFBC_PRIVDATA_FILE (absolute path) + * 2. plugins/nvfbc_auth.bin (next to sunshine.exe) + * + * Returns true if exactly 16 bytes were read. + */ +static bool +load_private_data(uint8_t *out_data, size_t out_size) { + if (out_size < NVFBC_PRIVDATA_SIZE) return false; + + std::string path; + + // Check environment variable first + char env_buf[MAX_PATH] = {}; + DWORD env_len = GetEnvironmentVariableA("NVFBC_PRIVDATA_FILE", env_buf, sizeof(env_buf)); + if (env_len > 0 && env_len < sizeof(env_buf)) { + path = env_buf; + } + + // Fallback: plugins/nvfbc_auth.bin next to the DLL + if (path.empty()) { + HMODULE hSelf = nullptr; + GetModuleHandleExA( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&load_private_data), + &hSelf); + if (hSelf) { + char dll_path[MAX_PATH] = {}; + GetModuleFileNameA(hSelf, dll_path, MAX_PATH); + // Navigate to parent directory (plugins/) and then look for nvfbc_auth.bin + std::string dir(dll_path); + auto sep = dir.find_last_of("\\/"); + if (sep != std::string::npos) { + dir = dir.substr(0, sep + 1); + } + path = dir + "nvfbc_auth.bin"; + } + } + + if (path.empty()) return false; + + // Read exactly NVFBC_PRIVDATA_SIZE bytes + HANDLE hFile = CreateFileA( + path.c_str(), GENERIC_READ, FILE_SHARE_READ, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (hFile == INVALID_HANDLE_VALUE) return false; + + DWORD bytes_read = 0; + BOOL ok = ReadFile(hFile, out_data, static_cast(NVFBC_PRIVDATA_SIZE), &bytes_read, nullptr); + CloseHandle(hFile); + + return ok && bytes_read == NVFBC_PRIVDATA_SIZE; +} + +// ============================================================================ +// Plugin session state +// ============================================================================ + +struct nvfbc_session { + // NvFBC library + HMODULE nvfbc_dll = nullptr; + NvFBC_CreateFunctionExType createEx_fn = nullptr; + + // NvFBC interface (opaque — actual type depends on Capture SDK) + void *nvfbc_interface = nullptr; + + // Private data loaded from external file + uint8_t private_data[NVFBC_PRIVDATA_SIZE] = {}; + bool has_private_data = false; + + // Capture buffer (managed by NvFBC after SetUp) + void *frame_buffer = nullptr; + + // Config + int width = 0; + int height = 0; + int framerate = 0; + + // State + std::atomic interrupted {false}; + bool session_valid = false; +}; + +// ============================================================================ +// Internal: Load NvFBC library and resolve exports +// ============================================================================ + +static bool +load_nvfbc_library(nvfbc_session *s) { + s->nvfbc_dll = LoadLibraryExW(L"NvFBC64.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); + if (!s->nvfbc_dll) { + return false; + } + + s->createEx_fn = reinterpret_cast( + GetProcAddress(s->nvfbc_dll, "NvFBC_CreateEx")); + + if (!s->createEx_fn) { + FreeLibrary(s->nvfbc_dll); + s->nvfbc_dll = nullptr; + return false; + } + + return true; +} + +// ============================================================================ +// Internal: Create NvFBC session via NvFBC_CreateEx +// +// NOTE: This creates the interface object. The interface type (ToSys/ToCuda) +// determines what operations are available. The returned pNvFBC pointer is +// a COM-like interface object whose vtable depends on the Capture SDK version. +// +// To use the interface, you need the official NVIDIA Capture SDK headers +// that define INvFBCToSys or equivalent interfaces. Without those headers, +// the pNvFBC pointer cannot be safely dereferenced. +// ============================================================================ + +static NVFBCRESULT +create_nvfbc_session(nvfbc_session *s) { + NvFBCCreateParams params {}; + params.dwVersion = NVFBC_CREATE_PARAMS_VER; + params.dwInterfaceType = NVFBC_TO_SYS; + + // Attach private data if loaded from external file + if (s->has_private_data) { + params.pPrivateData = s->private_data; + params.dwPrivateDataSize = NVFBC_PRIVDATA_SIZE; + } + + NVFBCRESULT res = s->createEx_fn(¶ms); + if (res != NVFBC_SUCCESS) { + return res; + } + + s->nvfbc_interface = params.pNvFBC; + if (!s->nvfbc_interface) { + return NVFBC_ERROR_GENERIC; + } + + return NVFBC_SUCCESS; +} + +// ============================================================================ +// Plugin API implementation +// ============================================================================ + +extern "C" { + +SUNSHINE_CAPTURE_EXPORT int +sunshine_capture_get_info(sunshine_capture_plugin_info_t *info) { + if (!info) return -1; + + info->abi_version = SUNSHINE_CAPTURE_PLUGIN_ABI_VERSION; + info->name = "nvfbc"; + info->version = "0.2.0"; + info->author = "Community"; + info->supported_mem_types = (1 << SUNSHINE_MEM_SYSTEM); + + return 0; +} + +SUNSHINE_CAPTURE_EXPORT int +sunshine_capture_enum_displays( + sunshine_mem_type_e mem_type, + sunshine_display_info_t *displays, + int max_displays) { + (void) mem_type; + + // NvFBC captures the full desktop; expose one display + if (displays && max_displays > 0) { + strncpy(displays[0].name, "NvFBC Desktop", sizeof(displays[0].name) - 1); + displays[0].name[sizeof(displays[0].name) - 1] = '\0'; + displays[0].width = GetSystemMetrics(SM_CXSCREEN); + displays[0].height = GetSystemMetrics(SM_CYSCREEN); + displays[0].is_primary = 1; + } + return 1; +} + +SUNSHINE_CAPTURE_EXPORT int +sunshine_capture_create_session( + sunshine_mem_type_e mem_type, + const char *display_name, + const sunshine_video_config_t *config, + sunshine_capture_session_t *session) { + (void) mem_type; + (void) display_name; + + if (!config || !session) return -1; + + auto *s = new (std::nothrow) nvfbc_session {}; + if (!s) return -1; + + s->width = config->width; + s->height = config->height; + s->framerate = config->framerate; + + // Step 1: Load private data from external file + s->has_private_data = load_private_data(s->private_data, sizeof(s->private_data)); + // Note: Session creation may still succeed without private data on + // Quadro/Grid GPUs that have NvFBC enabled natively. + + // Step 2: Load NvFBC64.dll + if (!load_nvfbc_library(s)) { + delete s; + return -1; // NvFBC DLL not found + } + + // Step 3: Create NvFBC interface via NvFBC_CreateEx + NVFBCRESULT res = create_nvfbc_session(s); + if (res != NVFBC_SUCCESS) { + FreeLibrary(s->nvfbc_dll); + delete s; + return -1; + } + + // Step 4: SetUp and GrabFrame require NVIDIA Capture SDK headers + // to properly call the INvFBCToSys interface methods. + // + // With official SDK headers: + // auto *toSys = static_cast(s->nvfbc_interface); + // NVFBC_TOSYS_SETUP_PARAMS setup = { ... }; + // setup.eBufferFormat = NVFBC_BUFFER_FORMAT_BGRA; + // setup.ppBuffer = &s->frame_buffer; + // toSys->NvFBCToSysSetUp(&setup); + // + // Without SDK headers, the interface pointer cannot be used. + // TODO: Add official Capture SDK headers and uncomment the above. + + s->session_valid = true; + *session = reinterpret_cast(s); + return 0; +} + +SUNSHINE_CAPTURE_EXPORT void +sunshine_capture_destroy_session(sunshine_capture_session_t session) { + if (!session) return; + + auto *s = reinterpret_cast(session); + + // Release NvFBC interface + // With official SDK headers: + // if (s->nvfbc_interface) { + // auto *toSys = static_cast(s->nvfbc_interface); + // toSys->NvFBCToSysRelease(); + // } + s->nvfbc_interface = nullptr; + + if (s->nvfbc_dll) { + FreeLibrary(s->nvfbc_dll); + s->nvfbc_dll = nullptr; + } + + delete s; +} + +SUNSHINE_CAPTURE_EXPORT sunshine_capture_result_e +sunshine_capture_next_frame( + sunshine_capture_session_t session, + sunshine_frame_t *frame, + int timeout_ms) { + if (!session || !frame) return SUNSHINE_CAPTURE_ERROR; + + auto *s = reinterpret_cast(session); + + if (s->interrupted.load(std::memory_order_relaxed)) { + return SUNSHINE_CAPTURE_INTERRUPTED; + } + + if (!s->session_valid || !s->nvfbc_interface) { + return SUNSHINE_CAPTURE_ERROR; + } + + // GrabFrame requires NVIDIA Capture SDK headers for INvFBCToSys. + // + // With official SDK headers: + // auto *toSys = static_cast(s->nvfbc_interface); + // NvFBCFrameGrabInfo grab_info {}; + // NVFBC_TOSYS_GRAB_FRAME_PARAMS grab {}; + // grab.dwVersion = NVFBC_TOSYS_GRAB_FRAME_PARAMS_VER; + // grab.dwFlags = NVFBC_TOSYS_NOWAIT; + // grab.pFrameGrabInfo = &grab_info; + // grab.dwTargetWidth = s->width; + // grab.dwTargetHeight = s->height; + // grab.dwTimeoutMs = timeout_ms; + // + // NVFBCRESULT res = toSys->NvFBCToSysGrabFrame(&grab); + // + // if (res == NVFBC_SUCCESS && grab_info.bIsNewFrame) { + // frame->data = static_cast(s->frame_buffer); + // frame->width = grab_info.dwWidth; + // frame->height = grab_info.dwHeight; + // frame->pitch = grab_info.dwBufferWidth * 4; + // frame->pixel_format = SUNSHINE_PIX_FMT_BGRA; + // frame->gpu_handle = nullptr; + // return SUNSHINE_CAPTURE_OK; + // } + // if (res == NVFBC_ERROR_INVALIDATED_SESSION) { + // s->session_valid = false; + // return SUNSHINE_CAPTURE_REINIT; + // } + // + // TODO: Replace with actual calls once SDK headers are available. + + return SUNSHINE_CAPTURE_TIMEOUT; +} + +SUNSHINE_CAPTURE_EXPORT void +sunshine_capture_release_frame( + sunshine_capture_session_t session, + sunshine_frame_t *frame) { + // NvFBC ToSys: frame buffer is managed by NvFBC internally. + // The ppBuffer pointer from SetUp stays valid until session is destroyed. + (void) session; + (void) frame; +} + +SUNSHINE_CAPTURE_EXPORT int +sunshine_capture_is_hdr(sunshine_capture_session_t session) { + // NvFBC ToSys does not support HDR output + (void) session; + return 0; +} + +SUNSHINE_CAPTURE_EXPORT void +sunshine_capture_interrupt(sunshine_capture_session_t session) { + if (!session) return; + + auto *s = reinterpret_cast(session); + s->interrupted.store(true, std::memory_order_relaxed); +} + +} // extern "C" diff --git a/src/platform/windows/capture_plugin/capture_plugin_api.h b/src/platform/windows/capture_plugin/capture_plugin_api.h new file mode 100644 index 00000000000..e810eb11b23 --- /dev/null +++ b/src/platform/windows/capture_plugin/capture_plugin_api.h @@ -0,0 +1,241 @@ +/** + * @file src/platform/windows/capture_plugin/capture_plugin_api.h + * @brief Capture plugin C ABI interface for Sunshine. + * + * This header defines the C ABI interface that capture plugin DLLs must implement. + * Using pure C interface ensures cross-compiler compatibility (MSVC, MinGW, Clang). + * + * Plugin DLLs are loaded from the "plugins/" directory next to sunshine.exe. + * Each plugin must export the functions prefixed with "sunshine_capture_". + */ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// ============================================================================ +// Version and ABI constants +// ============================================================================ + +/** Current plugin ABI version. Bump when the interface changes incompatibly. */ +#define SUNSHINE_CAPTURE_PLUGIN_ABI_VERSION 1 + +// ============================================================================ +// Opaque handle types +// ============================================================================ + +/** Opaque handle to a capture session created by the plugin. */ +typedef struct sunshine_capture_session *sunshine_capture_session_t; + +// ============================================================================ +// Enumerations (matching Sunshine internal types) +// ============================================================================ + +typedef enum { + SUNSHINE_MEM_SYSTEM = 0, /**< System memory (software encoding) */ + SUNSHINE_MEM_DXGI = 1, /**< DXGI / D3D11 (hardware encoding) */ + SUNSHINE_MEM_CUDA = 2, /**< CUDA (NVENC direct) */ +} sunshine_mem_type_e; + +typedef enum { + SUNSHINE_PIX_FMT_NV12 = 0, /**< NV12 */ + SUNSHINE_PIX_FMT_P010 = 1, /**< P010 (10-bit) */ + SUNSHINE_PIX_FMT_AYUV = 2, /**< AYUV (4:4:4) */ + SUNSHINE_PIX_FMT_Y410 = 3, /**< Y410 (4:4:4 10-bit) */ +} sunshine_pix_fmt_e; + +typedef enum { + SUNSHINE_CAPTURE_OK = 0, /**< Success */ + SUNSHINE_CAPTURE_REINIT = 1, /**< Need reinit (display mode change, etc.) */ + SUNSHINE_CAPTURE_TIMEOUT = 2, /**< Timeout waiting for frame */ + SUNSHINE_CAPTURE_INTERRUPTED = 3, /**< Capture interrupted (stop signal) */ + SUNSHINE_CAPTURE_ERROR = -1, /**< Fatal error */ +} sunshine_capture_result_e; + +// ============================================================================ +// Data structures +// ============================================================================ + +/** Plugin information returned by sunshine_capture_get_info(). */ +typedef struct { + uint32_t abi_version; /**< Must be SUNSHINE_CAPTURE_PLUGIN_ABI_VERSION */ + const char *name; /**< Human-readable plugin name (e.g., "NvFBC") */ + const char *version; /**< Plugin version string (e.g., "1.0.0") */ + const char *author; /**< Plugin author */ + + /** + * Bitmask of supported memory types. + * Set bit 0 for SUNSHINE_MEM_SYSTEM, bit 1 for SUNSHINE_MEM_DXGI, bit 2 for SUNSHINE_MEM_CUDA. + */ + uint32_t supported_mem_types; +} sunshine_capture_plugin_info_t; + +/** Video configuration passed to the plugin when creating a session. */ +typedef struct { + int width; /**< Target capture width */ + int height; /**< Target capture height */ + int framerate; /**< Target framerate */ + int dynamic_range; /**< 0 = SDR, 1 = HDR10 PQ, 2 = HDR HLG */ +} sunshine_video_config_t; + +/** Frame data returned by sunshine_capture_next_frame(). */ +typedef struct { + uint8_t *data; /**< Pointer to frame data (system memory) or NULL for GPU frames */ + int width; /**< Frame width */ + int height; /**< Frame height */ + int pixel_pitch; /**< Bytes per pixel */ + int row_pitch; /**< Bytes per row */ + int64_t timestamp_ns; /**< Frame timestamp in nanoseconds (steady clock) */ + + /** + * For DXGI memory type: ID3D11Texture2D* handle. + * For CUDA memory type: CUdeviceptr handle. + * Cast to appropriate type based on memory type. + */ + void *gpu_handle; + + /** For DXGI: subresource index within the texture. */ + uint32_t gpu_subresource; +} sunshine_frame_t; + +/** Display information for enumeration. */ +typedef struct { + char name[256]; /**< Display identifier string */ + int width; /**< Display width */ + int height; /**< Display height */ + int is_primary; /**< Whether this is the primary display */ +} sunshine_display_info_t; + +// ============================================================================ +// Plugin exported functions +// ============================================================================ + +/** + * @brief Get plugin information. + * @param[out] info Filled with plugin info. name/version/author pointers must + * remain valid for the lifetime of the DLL. + * @return 0 on success, non-zero on failure. + * + * Plugin DLL must export as: sunshine_capture_get_info + */ +typedef int (*sunshine_capture_get_info_fn)(sunshine_capture_plugin_info_t *info); + +/** + * @brief Enumerate available displays. + * @param mem_type Requested memory type for capture. + * @param[out] displays Array to fill with display info. + * @param max_displays Maximum number of entries in the displays array. + * @return Number of displays found (may exceed max_displays to indicate truncation). + * + * Plugin DLL must export as: sunshine_capture_enum_displays + */ +typedef int (*sunshine_capture_enum_displays_fn)( + sunshine_mem_type_e mem_type, + sunshine_display_info_t *displays, + int max_displays); + +/** + * @brief Create a capture session. + * @param mem_type Requested memory type. + * @param display_name Display to capture (from enum_displays, or empty for default). + * @param config Video configuration. + * @param[out] session Handle to the created session. + * @return 0 on success, non-zero on failure. + * + * Plugin DLL must export as: sunshine_capture_create_session + */ +typedef int (*sunshine_capture_create_session_fn)( + sunshine_mem_type_e mem_type, + const char *display_name, + const sunshine_video_config_t *config, + sunshine_capture_session_t *session); + +/** + * @brief Destroy a capture session and release all resources. + * @param session The session to destroy. + * + * Plugin DLL must export as: sunshine_capture_destroy_session + */ +typedef void (*sunshine_capture_destroy_session_fn)(sunshine_capture_session_t session); + +/** + * @brief Capture the next frame. + * @param session The capture session. + * @param[out] frame Filled with frame data on success. + * @param timeout_ms Timeout in milliseconds (0 = no wait, -1 = infinite). + * @return SUNSHINE_CAPTURE_OK on success, or an error code. + * + * Plugin DLL must export as: sunshine_capture_next_frame + */ +typedef sunshine_capture_result_e (*sunshine_capture_next_frame_fn)( + sunshine_capture_session_t session, + sunshine_frame_t *frame, + int timeout_ms); + +/** + * @brief Release a frame after processing. + * Must be called after each successful sunshine_capture_next_frame(). + * @param session The capture session. + * @param frame The frame to release. + * + * Plugin DLL must export as: sunshine_capture_release_frame + */ +typedef void (*sunshine_capture_release_frame_fn)( + sunshine_capture_session_t session, + sunshine_frame_t *frame); + +/** + * @brief Check if HDR is active on the captured display. + * @param session The capture session. + * @return 1 if HDR, 0 if SDR. + * + * Plugin DLL must export as: sunshine_capture_is_hdr + */ +typedef int (*sunshine_capture_is_hdr_fn)(sunshine_capture_session_t session); + +/** + * @brief Signal the capture session to stop. + * This is called from a different thread to interrupt a blocking next_frame() call. + * @param session The capture session. + * + * Plugin DLL must export as: sunshine_capture_interrupt + */ +typedef void (*sunshine_capture_interrupt_fn)(sunshine_capture_session_t session); + +// ============================================================================ +// Function table (populated by the plugin loader) +// ============================================================================ + +/** Complete function table loaded from a plugin DLL. */ +typedef struct { + sunshine_capture_get_info_fn get_info; + sunshine_capture_enum_displays_fn enum_displays; + sunshine_capture_create_session_fn create_session; + sunshine_capture_destroy_session_fn destroy_session; + sunshine_capture_next_frame_fn next_frame; + sunshine_capture_release_frame_fn release_frame; + sunshine_capture_is_hdr_fn is_hdr; + sunshine_capture_interrupt_fn interrupt; +} sunshine_capture_plugin_vtable_t; + +// ============================================================================ +// Convenience macros for plugin implementation +// ============================================================================ + +/** + * Use these macros in your plugin .cpp to export the required functions. + * Example: + * SUNSHINE_CAPTURE_EXPORT int sunshine_capture_get_info(sunshine_capture_plugin_info_t *info) { ... } + */ +#ifdef _WIN32 + #define SUNSHINE_CAPTURE_EXPORT __declspec(dllexport) +#else + #define SUNSHINE_CAPTURE_EXPORT __attribute__((visibility("default"))) +#endif + +#ifdef __cplusplus +} +#endif diff --git a/src/platform/windows/capture_plugin/capture_plugin_loader.cpp b/src/platform/windows/capture_plugin/capture_plugin_loader.cpp new file mode 100644 index 00000000000..cb5471ca610 --- /dev/null +++ b/src/platform/windows/capture_plugin/capture_plugin_loader.cpp @@ -0,0 +1,160 @@ +/** + * @file src/platform/windows/capture_plugin/capture_plugin_loader.cpp + * @brief Implementation of capture plugin loader. + */ +#include "capture_plugin_loader.h" + +#include +#include + +#include + +#include "src/logging.h" + +namespace platf::capture_plugin { + + namespace { + + /** + * @brief Resolve a single function from the DLL. + * @return Function pointer, or nullptr if not found. + */ + template + Fn + resolve_fn(HMODULE dll, const char *name) { + auto fn = reinterpret_cast(GetProcAddress(dll, name)); + if (!fn) { + BOOST_LOG(warning) << "Plugin missing export: " << name; + } + return fn; + } + + /** + * @brief Get the plugins directory path. + */ + std::filesystem::path + get_plugins_dir() { + wchar_t module_path[MAX_PATH]; + GetModuleFileNameW(nullptr, module_path, MAX_PATH); + return std::filesystem::path(module_path).parent_path() / "plugins"; + } + + } // namespace + + std::unique_ptr + load_plugin(const std::filesystem::path &dll_path) { + BOOST_LOG(info) << "Loading capture plugin: " << dll_path.string(); + + auto dll = LoadLibraryExW(dll_path.c_str(), nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); + if (!dll) { + BOOST_LOG(error) << "Failed to load plugin DLL: " << dll_path.string() + << " (error " << GetLastError() << ")"; + return nullptr; + } + + auto plugin = std::make_unique(); + plugin->dll_handle = dll; + plugin->dll_path = dll_path.string(); + + // Resolve required function: get_info + plugin->vtable.get_info = resolve_fn(dll, "sunshine_capture_get_info"); + if (!plugin->vtable.get_info) { + BOOST_LOG(error) << "Plugin missing required export: sunshine_capture_get_info"; + FreeLibrary(dll); + return nullptr; + } + + // Get plugin info and validate ABI version + sunshine_capture_plugin_info_t plugin_info {}; + if (plugin->vtable.get_info(&plugin_info) != 0) { + BOOST_LOG(error) << "Plugin sunshine_capture_get_info() failed"; + FreeLibrary(dll); + return nullptr; + } + + if (plugin_info.abi_version != SUNSHINE_CAPTURE_PLUGIN_ABI_VERSION) { + BOOST_LOG(error) << "Plugin ABI version mismatch: expected " + << SUNSHINE_CAPTURE_PLUGIN_ABI_VERSION + << ", got " << plugin_info.abi_version; + FreeLibrary(dll); + return nullptr; + } + + plugin->name = plugin_info.name ? plugin_info.name : "unknown"; + plugin->version = plugin_info.version ? plugin_info.version : "0.0.0"; + plugin->supported_mem_types = plugin_info.supported_mem_types; + + // Resolve remaining functions + plugin->vtable.enum_displays = resolve_fn(dll, "sunshine_capture_enum_displays"); + plugin->vtable.create_session = resolve_fn(dll, "sunshine_capture_create_session"); + plugin->vtable.destroy_session = resolve_fn(dll, "sunshine_capture_destroy_session"); + plugin->vtable.next_frame = resolve_fn(dll, "sunshine_capture_next_frame"); + plugin->vtable.release_frame = resolve_fn(dll, "sunshine_capture_release_frame"); + plugin->vtable.is_hdr = resolve_fn(dll, "sunshine_capture_is_hdr"); + plugin->vtable.interrupt = resolve_fn(dll, "sunshine_capture_interrupt"); + + // Validate required functions + if (!plugin->vtable.create_session || !plugin->vtable.destroy_session || !plugin->vtable.next_frame) { + BOOST_LOG(error) << "Plugin missing required exports (create_session, destroy_session, or next_frame)"; + FreeLibrary(dll); + return nullptr; + } + + BOOST_LOG(info) << "Loaded capture plugin: " << plugin->name << " v" << plugin->version; + return plugin; + } + + void + unload_plugin(loaded_plugin_t *plugin) { + if (plugin && plugin->dll_handle) { + BOOST_LOG(info) << "Unloading capture plugin: " << plugin->name; + FreeLibrary(static_cast(plugin->dll_handle)); + plugin->dll_handle = nullptr; + } + } + + std::vector> + discover_plugins() { + std::vector> plugins; + + auto plugins_dir = get_plugins_dir(); + if (!std::filesystem::exists(plugins_dir)) { + BOOST_LOG(debug) << "No plugins directory found at: " << plugins_dir.string(); + return plugins; + } + + BOOST_LOG(info) << "Scanning for capture plugins in: " << plugins_dir.string(); + + for (const auto &entry : std::filesystem::directory_iterator(plugins_dir)) { + if (!entry.is_regular_file()) continue; + + auto ext = entry.path().extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); + if (ext != ".dll") continue; + + auto plugin = load_plugin(entry.path()); + if (plugin) { + plugins.push_back(std::move(plugin)); + } + } + + BOOST_LOG(info) << "Discovered " << plugins.size() << " capture plugin(s)"; + return plugins; + } + + loaded_plugin_t * + find_plugin(const std::vector> &plugins, const std::string &name) { + for (const auto &plugin : plugins) { + // Case-insensitive comparison + auto plugin_name = plugin->name; + auto search_name = name; + std::transform(plugin_name.begin(), plugin_name.end(), plugin_name.begin(), ::tolower); + std::transform(search_name.begin(), search_name.end(), search_name.begin(), ::tolower); + if (plugin_name == search_name) { + return plugin.get(); + } + } + return nullptr; + } + +} // namespace platf::capture_plugin diff --git a/src/platform/windows/capture_plugin/capture_plugin_loader.h b/src/platform/windows/capture_plugin/capture_plugin_loader.h new file mode 100644 index 00000000000..cac804f9b45 --- /dev/null +++ b/src/platform/windows/capture_plugin/capture_plugin_loader.h @@ -0,0 +1,60 @@ +/** + * @file src/platform/windows/capture_plugin/capture_plugin_loader.h + * @brief Capture plugin loader - discovers and loads plugin DLLs. + */ +#pragma once + +#include +#include +#include +#include + +#include "capture_plugin_api.h" + +namespace platf::capture_plugin { + + /** + * @brief Represents a loaded capture plugin DLL. + */ + struct loaded_plugin_t { + std::string name; ///< Plugin name from get_info() + std::string version; ///< Plugin version string + std::string dll_path; ///< Full path to the DLL + uint32_t supported_mem_types; ///< Bitmask of supported memory types + + sunshine_capture_plugin_vtable_t vtable; ///< Function table + + void *dll_handle; ///< Platform-specific DLL handle (HMODULE on Windows) + }; + + /** + * @brief Load a single plugin DLL. + * @param dll_path Path to the DLL file. + * @return Loaded plugin info, or nullptr on failure. + */ + std::unique_ptr + load_plugin(const std::filesystem::path &dll_path); + + /** + * @brief Unload a plugin and free the DLL. + */ + void + unload_plugin(loaded_plugin_t *plugin); + + /** + * @brief Discover and load all plugins from the plugins directory. + * @return List of successfully loaded plugins. + */ + std::vector> + discover_plugins(); + + /** + * @brief Find a loaded plugin by name (case-insensitive). + * @param plugins The list of loaded plugins. + * @param name Plugin name to search for. + * @return Pointer to the plugin, or nullptr if not found. + */ + loaded_plugin_t * + find_plugin(const std::vector> &plugins, const std::string &name); + +} // namespace platf::capture_plugin diff --git a/src/platform/windows/capture_plugin/display_plugin.cpp b/src/platform/windows/capture_plugin/display_plugin.cpp new file mode 100644 index 00000000000..4a85a6ad816 --- /dev/null +++ b/src/platform/windows/capture_plugin/display_plugin.cpp @@ -0,0 +1,192 @@ +/** + * @file src/platform/windows/capture_plugin/display_plugin.cpp + * @brief Implementation of the plugin-backed display adapter. + */ +#include "display_plugin.h" + +#include +#include + +#include "src/logging.h" +#include "src/video.h" + +using namespace std::literals; + +namespace platf::capture_plugin { + + namespace { + + sunshine_mem_type_e + to_plugin_mem_type(mem_type_e type) { + switch (type) { + case mem_type_e::system: + return SUNSHINE_MEM_SYSTEM; + case mem_type_e::dxgi: + return SUNSHINE_MEM_DXGI; + case mem_type_e::cuda: + return SUNSHINE_MEM_CUDA; + default: + return SUNSHINE_MEM_SYSTEM; + } + } + + } // namespace + + display_plugin_t::display_plugin_t(loaded_plugin_t *plugin, mem_type_e mem_type): + plugin_(plugin), mem_type_(mem_type) { + } + + display_plugin_t::~display_plugin_t() { + stop_flag_ = true; + + if (session_) { + if (plugin_->vtable.interrupt) { + plugin_->vtable.interrupt(session_); + } + plugin_->vtable.destroy_session(session_); + session_ = nullptr; + } + } + + int + display_plugin_t::init(const ::video::config_t &config, const std::string &display_name) { + sunshine_video_config_t plugin_config {}; + plugin_config.width = config.width; + plugin_config.height = config.height; + plugin_config.framerate = config.framerate; + plugin_config.dynamic_range = config.dynamicRange; + + framerate_ = config.framerate; + + width = config.width; + height = config.height; + env_width = config.width; + env_height = config.height; + + auto result = plugin_->vtable.create_session( + to_plugin_mem_type(mem_type_), + display_name.c_str(), + &plugin_config, + &session_); + + if (result != 0 || !session_) { + BOOST_LOG(error) << "Plugin " << plugin_->name << " failed to create capture session"; + return -1; + } + + BOOST_LOG(info) << "Plugin " << plugin_->name << " capture session created (" + << config.width << "x" << config.height << "@" << config.framerate << ")"; + return 0; + } + + capture_e + display_plugin_t::capture( + const push_captured_image_cb_t &push_captured_image_cb, + const pull_free_image_cb_t &pull_free_image_cb, + bool *cursor) { + if (!session_) return capture_e::error; + + stop_flag_ = false; + auto frame_interval = std::chrono::nanoseconds(1'000'000'000 / framerate_); + auto next_frame_time = std::chrono::steady_clock::now(); + + while (!stop_flag_) { + // Wait for frame timing + auto now = std::chrono::steady_clock::now(); + if (now < next_frame_time) { + std::this_thread::sleep_for(next_frame_time - now); + } + next_frame_time += frame_interval; + + // Get a free image from the pool + std::shared_ptr img; + if (!pull_free_image_cb(img) || !img) { + return capture_e::interrupted; + } + + // Capture next frame from plugin + auto plugin_img = std::static_pointer_cast(img); + plugin_img->release(); // Release any previous frame data + + sunshine_frame_t frame {}; + auto result = plugin_->vtable.next_frame(session_, &frame, 100); + + if (result == SUNSHINE_CAPTURE_OK) { + // Copy frame data into img + plugin_img->data = frame.data; + plugin_img->width = frame.width; + plugin_img->height = frame.height; + plugin_img->pixel_pitch = frame.pixel_pitch; + plugin_img->row_pitch = frame.row_pitch; + plugin_img->plugin_frame = frame; + plugin_img->session = session_; + plugin_img->vtable = &plugin_->vtable; + plugin_img->needs_release = true; + + if (frame.timestamp_ns > 0) { + plugin_img->frame_timestamp = std::chrono::steady_clock::time_point( + std::chrono::nanoseconds(frame.timestamp_ns)); + } + else { + plugin_img->frame_timestamp = std::chrono::steady_clock::now(); + } + + if (!push_captured_image_cb(std::move(img), true)) { + return capture_e::ok; + } + } + else if (result == SUNSHINE_CAPTURE_TIMEOUT) { + // No new frame, push empty + if (!push_captured_image_cb(std::move(img), false)) { + return capture_e::ok; + } + } + else if (result == SUNSHINE_CAPTURE_REINIT) { + return capture_e::reinit; + } + else if (result == SUNSHINE_CAPTURE_INTERRUPTED) { + return capture_e::interrupted; + } + else { + BOOST_LOG(error) << "Plugin " << plugin_->name << " capture error"; + return capture_e::error; + } + } + + return capture_e::ok; + } + + std::shared_ptr + display_plugin_t::alloc_img() { + auto img = std::make_shared(); + img->width = width; + img->height = height; + img->pixel_pitch = 4; // Default BGRA + img->row_pitch = img->width * img->pixel_pitch; + return img; + } + + int + display_plugin_t::dummy_img(img_t *img) { + // Fill with black + if (img->data && img->row_pitch > 0 && img->height > 0) { + std::memset(img->data, 0, img->row_pitch * img->height); + } + return 0; + } + + bool + display_plugin_t::is_hdr() { + if (session_ && plugin_->vtable.is_hdr) { + return plugin_->vtable.is_hdr(session_) != 0; + } + return false; + } + + bool + display_plugin_t::is_codec_supported(std::string_view name, const ::video::config_t &config) { + // Plugin backends support all codecs by default + return true; + } + +} // namespace platf::capture_plugin diff --git a/src/platform/windows/capture_plugin/display_plugin.h b/src/platform/windows/capture_plugin/display_plugin.h new file mode 100644 index 00000000000..92b59fd75ee --- /dev/null +++ b/src/platform/windows/capture_plugin/display_plugin.h @@ -0,0 +1,96 @@ +/** + * @file src/platform/windows/capture_plugin/display_plugin.h + * @brief Plugin-backed display adapter that bridges capture_plugin_api to display_t. + */ +#pragma once + +#include +#include + +#include "capture_plugin_api.h" +#include "capture_plugin_loader.h" +#include "src/platform/common.h" + +namespace platf::capture_plugin { + + /** + * @brief Image type that wraps plugin frame data. + */ + struct plugin_img_t: public img_t { + /** Plugin session that owns this frame (needed for release). */ + sunshine_capture_session_t session = nullptr; + + /** Plugin vtable for calling release_frame. */ + const sunshine_capture_plugin_vtable_t *vtable = nullptr; + + /** The raw frame data from the plugin. */ + sunshine_frame_t plugin_frame {}; + + /** Whether this frame needs to be released back to the plugin. */ + bool needs_release = false; + + ~plugin_img_t() override { + release(); + } + + void + release() { + if (needs_release && vtable && vtable->release_frame && session) { + vtable->release_frame(session, &plugin_frame); + needs_release = false; + } + } + }; + + /** + * @brief Display backend that delegates capture to an external plugin DLL. + * + * This adapter implements platf::display_t by forwarding calls to the + * plugin's C ABI functions. It handles the translation between Sunshine's + * internal types and the plugin's simplified types. + */ + class display_plugin_t: public display_t { + public: + explicit display_plugin_t(loaded_plugin_t *plugin, mem_type_e mem_type); + ~display_plugin_t() override; + + /** + * @brief Initialize the plugin capture session. + */ + int + init(const ::video::config_t &config, const std::string &display_name); + + // display_t interface + capture_e + capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override; + + std::shared_ptr + alloc_img() override; + + int + dummy_img(img_t *img) override; + + bool + is_hdr() override; + + bool + is_codec_supported(std::string_view name, const ::video::config_t &config) override; + + private: + /** The loaded plugin providing capture functionality. */ + loaded_plugin_t *plugin_; + + /** Active capture session. */ + sunshine_capture_session_t session_ = nullptr; + + /** Memory type requested by Sunshine. */ + mem_type_e mem_type_; + + /** Stop flag for the capture loop. */ + std::atomic stop_flag_ { false }; + + /** Frame rate for timing. */ + int framerate_ = 60; + }; + +} // namespace platf::capture_plugin diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index 476d01e5bcf..735442afbcc 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -27,6 +27,8 @@ typedef enum _D3DKMT_GPU_PREFERENCE_QUERY_STATE: DWORD { } D3DKMT_GPU_PREFERENCE_QUERY_STATE; #include "display.h" +#include "capture_plugin/capture_plugin_loader.h" +#include "capture_plugin/display_plugin.h" #include "display_device/windows_utils.h" #include "misc.h" #include "src/config.h" @@ -1128,6 +1130,17 @@ namespace platf { ret = try_init(std::make_shared()); } } + else { + // Try loading as a capture plugin + static auto plugins = capture_plugin::discover_plugins(); + auto *plugin = capture_plugin::find_plugin(plugins, type); + if (plugin) { + auto disp = std::make_shared(plugin, hwdevice_type); + if (!disp->init(config, display_name)) { + ret = disp; + } + } + } if (ret) { return ret; diff --git a/src_assets/common/assets/web/configs/tabs/Advanced.vue b/src_assets/common/assets/web/configs/tabs/Advanced.vue index 07d88430912..a31313c9d45 100644 --- a/src_assets/common/assets/web/configs/tabs/Advanced.vue +++ b/src_assets/common/assets/web/configs/tabs/Advanced.vue @@ -19,10 +19,20 @@ const isWGCSelected = computed(() => { return props.platform === 'windows' && config.value.capture === 'wgc' }) +// 检查是否选择了 NvFBC +const isNvFBCSelected = computed(() => { + return props.platform === 'windows' && config.value.capture === 'nvfbc' +}) + // Sunshine 运行模式状态 const isUserMode = ref(false) const isCheckingMode = ref(false) +// NvFBC 状态 +const nvfbcStatus = ref(null) +const isCheckingNvfbc = ref(false) +const isInstallingNvfbc = ref(false) + const showMessage = (message, type = 'info') => { // 尝试使用 window.showToast(如果可用) if (typeof window.showToast === 'function') { @@ -95,10 +105,48 @@ const toggleSunshineMode = async () => { } } +// 检查 NvFBC 环境状态 +const checkNvfbcStatus = async () => { + if (!isTauri.value) return + isCheckingNvfbc.value = true + try { + nvfbcStatus.value = await window.__TAURI__.core.invoke('check_nvfbc_status') + } catch (error) { + console.error('检查 NvFBC 状态失败:', error) + nvfbcStatus.value = null + } finally { + isCheckingNvfbc.value = false + } +} + +// 一键安装 NvFBC +const installNvfbc = async () => { + if (!isTauri.value) { + showMessage(t('config.nvfbc_control_panel_only'), 'error') + return + } + isInstallingNvfbc.value = true + try { + const msg = await window.__TAURI__.core.invoke('setup_nvfbc') + showMessage(msg || t('config.nvfbc_install_success'), 'success') + // 延迟后重新检查(等待注册表和 UAC 完成) + setTimeout(() => checkNvfbcStatus(), 3000) + setTimeout(() => checkNvfbcStatus(), 8000) + } catch (error) { + console.error('安装 NvFBC 失败:', error) + showMessage(t('config.nvfbc_install_failed') + ': ' + (error.message || error), 'error') + } finally { + isInstallingNvfbc.value = false + } +} + onMounted(() => { if (isTauri.value && isWGCSelected.value) { checkSunshineMode() } + if (isTauri.value && isNvFBCSelected.value) { + checkNvfbcStatus() + } }) watch(isWGCSelected, (newValue) => { @@ -106,6 +154,12 @@ watch(isWGCSelected, (newValue) => { checkSunshineMode() } }) + +watch(isNvFBCSelected, (newValue) => { + if (newValue && isTauri.value) { + checkNvfbcStatus() + } +}) @@ -191,6 +246,44 @@ watch(isWGCSelected, (newValue) => { : $t('config.wgc_switch_to_user_mode') }} +
{{ $t('config.capture_desc') }} @@ -200,6 +293,66 @@ watch(isWGCSelected, (newValue) => { {{ $t('config.wgc_user_mode_available') }} {{ $t('config.wgc_service_mode_warning') }} + + + +
diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json index 992a0d9a2a5..492a51198de 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -533,6 +533,23 @@ "wgc_switch_to_user_mode": "Switch to User Mode", "wgc_switch_to_user_mode_tooltip": "WGC capture requires running in user mode. Click this button to switch to user mode.", "wgc_user_mode_available": "Currently running in user mode. WGC capture is available.", + "nvfbc_install": "Install NvFBC", + "nvfbc_installed": "Installed", + "nvfbc_installing": "Installing...", + "nvfbc_checking": "Checking...", + "nvfbc_checking_status": "Checking NvFBC environment...", + "nvfbc_install_tooltip": "One-click install NvFBC capture components (nvfbcwrp + registry)", + "nvfbc_install_success": "NvFBC environment configured successfully", + "nvfbc_install_failed": "Failed to install NvFBC", + "nvfbc_control_panel_only": "This feature is only available in Sunshine Control Panel", + "nvfbc_gpu_found": "NVIDIA GPU detected", + "nvfbc_gpu_not_found": "No NVIDIA GPU detected. NvFBC requires an NVIDIA graphics card.", + "nvfbc_registry_enabled": "NvFBC registry key enabled", + "nvfbc_registry_disabled": "NvFBC registry key not set. Click Install to configure automatically.", + "nvfbc_wrapper_installed": "NvFBC wrapper installed", + "nvfbc_wrapper_missing": "NvFBC wrapper missing. Click Install to download automatically.", + "nvfbc_plugin_installed": "NvFBC capture plugin ready", + "nvfbc_plugin_missing": "NvFBC capture plugin missing. Please place sunshine_nvfbc.dll in the plugins/ directory.", "window_title": "Window Title", "window_title_desc": "The title of the window to capture (partial match, case-insensitive). If left empty, the current running application name will be used automatically.", "window_title_placeholder": "e.g., Application Name" diff --git a/src_assets/common/assets/web/public/assets/locale/zh.json b/src_assets/common/assets/web/public/assets/locale/zh.json index 0567099a317..5eaa2376f3d 100644 --- a/src_assets/common/assets/web/public/assets/locale/zh.json +++ b/src_assets/common/assets/web/public/assets/locale/zh.json @@ -533,6 +533,23 @@ "wgc_switch_to_user_mode": "切换到用户模式", "wgc_switch_to_user_mode_tooltip": "WGC 捕获需要在用户模式下运行。点击此按钮切换到用户模式。", "wgc_user_mode_available": "当前以用户模式运行,WGC 捕获可用。", + "nvfbc_install": "安装 NvFBC", + "nvfbc_installed": "已安装", + "nvfbc_installing": "安装中...", + "nvfbc_checking": "检查中...", + "nvfbc_checking_status": "正在检查 NvFBC 环境...", + "nvfbc_install_tooltip": "一键安装 NvFBC 捕获所需组件(nvfbcwrp + 注册表)", + "nvfbc_install_success": "NvFBC 环境配置完成", + "nvfbc_install_failed": "安装 NvFBC 失败", + "nvfbc_control_panel_only": "此功能仅在 Sunshine Control Panel 中可用", + "nvfbc_gpu_found": "已检测到 NVIDIA GPU", + "nvfbc_gpu_not_found": "未检测到 NVIDIA GPU,NvFBC 需要 NVIDIA 显卡", + "nvfbc_registry_enabled": "NvFBC 注册表已启用", + "nvfbc_registry_disabled": "NvFBC 注册表未启用,点击安装按钮自动配置", + "nvfbc_wrapper_installed": "NvFBC wrapper 已安装", + "nvfbc_wrapper_missing": "NvFBC wrapper 未安装,点击安装按钮自动下载", + "nvfbc_plugin_installed": "NvFBC 捕获插件已就绪", + "nvfbc_plugin_missing": "NvFBC 捕获插件缺失,请将 sunshine_nvfbc.dll 放入 plugins/ 目录", "window_title": "窗口标题", "window_title_desc": "要捕获的窗口标题(部分匹配,不区分大小写)。如果留空,将自动使用当前运行的应用名称。", "window_title_placeholder": "例如:yuanshen" diff --git a/src_assets/common/sunshine-control-panel b/src_assets/common/sunshine-control-panel index baae67eed37..9f1af167202 160000 --- a/src_assets/common/sunshine-control-panel +++ b/src_assets/common/sunshine-control-panel @@ -1 +1 @@ -Subproject commit baae67eed3740d509cdf800c43f67c3bd20b3fde +Subproject commit 9f1af167202b078d81ac003d427e7f0b5631ee95