Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cmake/compile_definitions/windows.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
27 changes: 27 additions & 0 deletions examples/capture_plugin_nvfbc/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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"
)
228 changes: 228 additions & 0 deletions examples/capture_plugin_nvfbc/sunshine_nvfbc_plugin.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
/**
* @file examples/capture_plugin_nvfbc/sunshine_nvfbc_plugin.cpp
* @brief Example NvFBC capture plugin for Sunshine (Windows).
*
* This is a SKELETON implementation showing how to build an NvFBC capture
* plugin DLL for Sunshine. You need to fill in the actual NvFBC API calls
* based on the Windows NvFBC API definitions (from NVIDIA Grid SDK or
* reverse-engineered from keylase/nvidia-patch's nvfbcwrp).
*
* 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 driver patched for NvFBC (keylase/nvidia-patch)
* - NvFBC64.dll present in system (installed with NVIDIA driver)
*/

#include <cstring>
#include <vector>
#include <Windows.h>

// Include the Sunshine capture plugin API
#include "src/platform/windows/capture_plugin/capture_plugin_api.h"

// ============================================================================
// Windows NvFBC API definitions (reverse-engineered / from Grid SDK)
// These must match the actual driver's NvFBC interface.
// Refer to: https://github.com/keylase/nvidia-patch/blob/master/win/nvfbcwrp/
// ============================================================================

// NvFBC function pointer type
typedef void *(*NvFBCCreateInstance_t)(unsigned int magic);

// TODO: Define the actual Windows NvFBC structures here:
// - NVFBC_SESSION_HANDLE
// - NVFBC_CREATE_PARAMS
// - NVFBC_TOSYS_SETUP_PARAMS
// - NVFBC_TOSYS_GRAB_FRAME_PARAMS
// etc.

// Magic private data to bypass consumer GPU check
static const unsigned int MAGIC_PRIVATE_DATA[4] = {
0xAEF57AC5, 0x401D1A39, 0x1B856BBE, 0x9ED0CEBA
};

// ============================================================================
// Plugin session state
// ============================================================================

struct nvfbc_session {
HMODULE nvfbc_dll; // NvFBC64.dll handle
NvFBCCreateInstance_t create_fn; // NvFBCCreateInstance function
void *fbc_handle; // NvFBC session handle

int width;
int height;
int framerate;

// Frame buffer for ToSys capture
std::vector<uint8_t> frame_buffer;
bool frame_ready;
bool interrupted;
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interrupted field is a plain bool, but per the API documentation in capture_plugin_api.h (line 201), sunshine_capture_interrupt is "called from a different thread." This creates a data race when next_frame reads s->interrupted on the capture thread while interrupt writes it from another thread. This should be std::atomic<bool> to ensure thread safety.

Even though this is example/skeleton code, it will likely be copied by plugin authors and should demonstrate correct patterns.

Copilot uses AI. Check for mistakes.
};

// ============================================================================
// 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.1.0";
info->author = "Community";

// Support system memory (ToSys) and CUDA (ToCuda)
info->supported_mem_types = (1 << SUNSHINE_MEM_SYSTEM) | (1 << SUNSHINE_MEM_CUDA);

return 0;
}

SUNSHINE_CAPTURE_EXPORT int
sunshine_capture_enum_displays(
sunshine_mem_type_e mem_type,
sunshine_display_info_t *displays,
int max_displays) {
// NvFBC captures the entire desktop, so we 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;
Comment on lines +223 to +229
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

GetSystemMetrics SM_CXSCREEN vs SM_CXVIRTUALSCREEN Windows API documentation

💡 Result:

SM_CXSCREEN returns the width of the primary display monitor's screen in pixels. SM_CXVIRTUALSCREEN returns the width of the entire virtual screen, which is the bounding rectangle encompassing all display monitors in a multi-monitor setup. In single-monitor configurations, these values are identical. GetSystemMetrics is not DPI-aware by default; for per-monitor DPI-aware apps, use GetSystemMetricsForDpi instead.

Citations:


🏁 Script executed:

# Search for the specific file mentioned in the review
fd "sunshine_nvfbc_plugin.cpp" --type f

Repository: AlkaidLab/foundation-sunshine

Length of output: 128


🏁 Script executed:

# Also search for any related display enumeration code
rg "GetSystemMetrics.*SM_CXSCREEN|SM_CXVIRTUALSCREEN" -A 3 -B 3

Repository: AlkaidLab/foundation-sunshine

Length of output: 1773


全桌面尺寸获取错误,多显示器场景下会报告错误分辨率。

代码注释说"NvFBC captures the full desktop",但 SM_CXSCREEN/SM_CYSCREEN 只返回主显示器的尺寸。在多显示器扩展桌面配置中,应使用 SM_CXVIRTUALSCREEN/SM_CYVIRTUALSCREEN 来获取整个虚拟屏幕的尺寸。项目中 src/platform/windows/display_base.cpp 已正确采用这一模式。

建议修改
-    displays[0].width = GetSystemMetrics(SM_CXSCREEN);
-    displays[0].height = GetSystemMetrics(SM_CYSCREEN);
+    displays[0].width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
+    displays[0].height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/capture_plugin_nvfbc/sunshine_nvfbc_plugin.cpp` around lines 223 -
229, The code sets displays[0].width/height using
GetSystemMetrics(SM_CXSCREEN/SM_CYSCREEN) which returns only the primary monitor
size; replace those calls with GetSystemMetrics(SM_CXVIRTUALSCREEN) and
GetSystemMetrics(SM_CYVIRTUALSCREEN) so displays[0] reports the full virtual
desktop size (matching the approach in src/platform/windows/display_base.cpp);
update the two calls that assign displays[0].width and displays[0].height
accordingly.

}
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) {
if (!config || !session) return -1;

auto *s = new nvfbc_session {};
s->width = config->width;
s->height = config->height;
s->framerate = config->framerate;
s->frame_ready = false;
s->interrupted = false;

// Load NvFBC64.dll
s->nvfbc_dll = LoadLibraryExA("NvFBC64.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);
if (!s->nvfbc_dll) {
delete s;
return -1;
}

s->create_fn = reinterpret_cast<NvFBCCreateInstance_t>(
GetProcAddress(s->nvfbc_dll, "NvFBCCreateInstance"));
if (!s->create_fn) {
FreeLibrary(s->nvfbc_dll);
delete s;
return -1;
}

// TODO: Initialize NvFBC session with MAGIC_PRIVATE_DATA
// This requires the actual Windows NvFBC API structures.
//
// Pseudocode:
// NVFBC_CREATE_PARAMS create_params = {};
// create_params.privateData = MAGIC_PRIVATE_DATA;
// create_params.privateDataSize = sizeof(MAGIC_PRIVATE_DATA);
// auto status = nvFBCCreate(&create_params, &s->fbc_handle);
//
// NVFBC_TOSYS_SETUP_PARAMS setup = {};
// setup.bufferFormat = NVFBC_BUFFER_FORMAT_BGRA;
// setup.ppBuffer = &s->frame_buffer_ptr;
// status = nvFBCToSysSetUp(s->fbc_handle, &setup);

// Allocate frame buffer for ToSys mode
s->frame_buffer.resize(s->width * s->height * 4); // BGRA

*session = reinterpret_cast<sunshine_capture_session_t>(s);
return 0;
Comment on lines +271 to +286
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

在真正接通 GrabFrame 前,不要把会话标记为可用。

Line 284 把 session_valid 设成了 true 并返回成功,但 Lines 311-359 目前只会返回 SUNSHINE_CAPTURE_TIMEOUT。结合 src/platform/windows/capture_plugin/display_plugin.cpp 的 Lines 138-143,上层会一直循环空帧,用户看到的是“初始化成功但永远没画面”。

🛠️ 一种更安全的临时处理
-  s->session_valid = true;
-  *session = reinterpret_cast<sunshine_capture_session_t>(s);
-  return 0;
+  FreeLibrary(s->nvfbc_dll);
+  delete s;
+  return -1;

Also applies to: 311-359

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/capture_plugin_nvfbc/sunshine_nvfbc_plugin.cpp` around lines 271 -
286, Do not mark the session as valid until we can actually grab frames: change
the logic around sunshine_nvfbc_plugin.cpp so s->session_valid is not set to
true and a successful 0 is not returned immediately in the setup path (where
*session is assigned and return 0 currently occurs); instead leave session_valid
false and return an appropriate "not ready" error (or propagate the GrabFrame
timeout behavior) and only set s->session_valid = true inside the first
successful GrabFrame/ToSys setup completion (the code path currently returning
SUNSHINE_CAPTURE_TIMEOUT in lines ~311-359 should be the state the caller sees
until a real frame is obtained); reference s->session_valid,
sunshine_capture_session_t, and the GrabFrame/INvFBCToSys setup code when making
the change.

}

SUNSHINE_CAPTURE_EXPORT void
sunshine_capture_destroy_session(sunshine_capture_session_t session) {
if (!session) return;

auto *s = reinterpret_cast<nvfbc_session *>(session);

// TODO: Destroy NvFBC session
// nvFBCRelease(s->fbc_handle);

if (s->nvfbc_dll) {
FreeLibrary(s->nvfbc_dll);
}

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<nvfbc_session *>(session);

if (s->interrupted) {
return SUNSHINE_CAPTURE_INTERRUPTED;
}

// TODO: Actual NvFBC grab frame call
//
// Pseudocode:
// NVFBC_TOSYS_GRAB_FRAME_PARAMS grab = {};
// grab.dwFlags = NVFBC_TOSYS_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY;
// grab.dwTimeoutMs = timeout_ms;
// auto status = nvFBCToSysGrabFrame(s->fbc_handle, &grab);
//
// if (status == NVFBC_SUCCESS) {
// frame->data = s->frame_buffer_ptr; // Pointer set by NvFBC
// frame->width = s->width;
// ...
// return SUNSHINE_CAPTURE_OK;
// }

// Placeholder: return timeout
return SUNSHINE_CAPTURE_TIMEOUT;
}

SUNSHINE_CAPTURE_EXPORT void
sunshine_capture_release_frame(
sunshine_capture_session_t session,
sunshine_frame_t *frame) {
// NvFBC ToSys: frames are managed by NvFBC internally, no release needed
// NvFBC ToCuda: may need to unlock CUDA resource
(void) session;
(void) frame;
}

SUNSHINE_CAPTURE_EXPORT int
sunshine_capture_is_hdr(sunshine_capture_session_t session) {
// NvFBC typically does not support HDR
(void) session;
return 0;
}

SUNSHINE_CAPTURE_EXPORT void
sunshine_capture_interrupt(sunshine_capture_session_t session) {
if (!session) return;

auto *s = reinterpret_cast<nvfbc_session *>(session);
s->interrupted = true;
}

} // extern "C"
Loading
Loading