Skip to content

feat(windows): add capture plugin framework#519

Open
qiin2333 wants to merge 4 commits intomasterfrom
feature/capture-plugin-framework
Open

feat(windows): add capture plugin framework#519
qiin2333 wants to merge 4 commits intomasterfrom
feature/capture-plugin-framework

Conversation

@qiin2333
Copy link
Copy Markdown
Collaborator

@qiin2333 qiin2333 commented Mar 9, 2026

Summary

Add an extensible capture plugin system that allows external DLLs to provide screen capture backends for Sunshine on Windows.

Changes

Plugin System Core

  • capture_plugin_api.h: Pure C ABI interface (v1) for cross-compiler compatibility (MSVC/MinGW/Clang)
  • capture_plugin_loader: Runtime discovery and loading of plugin DLLs from plugins/ directory
  • display_plugin: Adapter bridging plugin C ABI to Sunshine's display_t interface
  • display() factory: Extended to fall back to plugin loader for unrecognized capture type names

Example Plugin

  • examples/capture_plugin_nvfbc/: NvFBC skeleton plugin with standalone CMakeLists.txt, demonstrating the full plugin API

Build Integration

  • Added capture_plugin source files to Windows platform CMake build

Usage

  1. Build a capture plugin DLL implementing the C ABI in capture_plugin_api.h
  2. Place the DLL in Sunshine's plugins/ directory
  3. Set capture = <plugin_name> in sunshine.conf

Architecture

sunshine.conf (capture = nvfbc)
       │
       ▼
display() factory ──► discover_plugins() ──► LoadLibrary(sunshine_nvfbc.dll)
       │                                              │
       ▼                                              ▼
display_plugin_t ◄──── C ABI bridge ────► plugin vtable (get_info, create_session, next_frame, ...)
       │
       ▼
  display_t interface (Sunshine's encoding pipeline)

Next Steps

  • Fill in NvFBC API calls in the skeleton plugin (Windows NvFBC structures from keylase/nvidia-patch)
  • Add plugin configuration options to Sunshine's web UI
  • Test with actual NvFBC-patched driver

Summary by CodeRabbit

发布说明

新功能

  • 新增 NVIDIA NvFBC 屏幕捕获支持,用户可在高级设置中选择此方式进行屏幕录制
  • 新增 NvFBC 一键安装和检查功能,自动验证 NVIDIA GPU、驱动注册表配置及必要组件的安装状态

Add an extensible capture plugin system that allows external DLLs to
provide screen capture backends for Sunshine on Windows.

Plugin system components:
- capture_plugin_api.h: Pure C ABI interface (v1) for plugin DLLs
- capture_plugin_loader: Discovery and loading of plugin DLLs from plugins/ dir
- display_plugin: Adapter bridging plugin C ABI to display_t interface
- display() factory: Falls back to plugin loader for unknown capture types

Example plugin:
- examples/capture_plugin_nvfbc/: NvFBC skeleton plugin with CMakeLists.txt

Usage: Set 'capture = <plugin_name>' in config, place DLL in plugins/ dir.
Copilot AI review requested due to automatic review settings March 9, 2026 07:24
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds an extensible capture plugin framework for Sunshine on Windows, allowing external DLLs to provide screen capture backends via a C ABI interface. The architecture includes a pure C API header for cross-compiler compatibility, a runtime DLL loader that discovers plugins from a plugins/ directory, and an adapter class that bridges the plugin's C ABI to Sunshine's internal display_t interface. An example NvFBC skeleton plugin is included for reference.

Changes:

  • New C ABI interface (capture_plugin_api.h) defining plugin structures, enums, and function signatures for capture session management
  • Plugin loader (capture_plugin_loader.cpp) for discovering and loading DLLs, plus a display_plugin_t adapter class implementing Sunshine's display_t interface
  • Integration into the display() factory function to fall back to plugin loading for unrecognized capture type names, with build integration in the Windows CMake config

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/platform/windows/capture_plugin/capture_plugin_api.h Defines the C ABI interface (v1) with plugin info, session management, frame capture, and vtable structures
src/platform/windows/capture_plugin/capture_plugin_loader.h Header for plugin discovery, loading, and lookup functions
src/platform/windows/capture_plugin/capture_plugin_loader.cpp Implementation of DLL loading via LoadLibraryExW, ABI version validation, and directory scanning
src/platform/windows/capture_plugin/display_plugin.h Header for display_plugin_t adapter and plugin_img_t image wrapper
src/platform/windows/capture_plugin/display_plugin.cpp Implementation of plugin-to-display_t bridge, capture loop, and frame management
src/platform/windows/display_base.cpp Extended display() factory to fall back to plugin loader for unknown capture types
cmake/compile_definitions/windows.cmake Added capture plugin source files to Windows platform build
examples/capture_plugin_nvfbc/sunshine_nvfbc_plugin.cpp Skeleton NvFBC plugin demonstrating the full plugin API
examples/capture_plugin_nvfbc/CMakeLists.txt Standalone CMake build file for the example plugin

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +19 to +28
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)
};
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.

loaded_plugin_t holds a raw dll_handle (HMODULE) but has no destructor to call FreeLibrary. When the unique_ptr<loaded_plugin_t> objects in the static vector are destroyed at program shutdown, the DLL handles leak. While the OS will reclaim them at process exit, this is still poor resource management.

Consider either adding a destructor to loaded_plugin_t that calls FreeLibrary, or using a custom deleter on the unique_ptr that calls unload_plugin.

Copilot uses AI. Check for mistakes.
Comment on lines +128 to +143
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;
}
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.

All DLLs in the plugins/ directory are loaded eagerly when discover_plugins() is first called, even if only one specific plugin was requested. Loading a DLL executes its DllMain, which means any DLL placed in the plugins directory gets arbitrary code execution.

Consider only loading the specific plugin DLL that matches the requested capture type name (e.g., look for sunshine_<name>.dll), rather than enumerating and loading all DLLs. This reduces the attack surface significantly and also improves startup time.

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +99
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;
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 capture loop implements its own sleep-based frame pacing (sleep_for + next_frame_time) on top of calling the plugin's next_frame() with a hardcoded 100ms timeout. This double-timing approach adds unnecessary latency: the loop sleeps until the next frame interval, then blocks for up to 100ms more in next_frame().

If the plugin's next_frame() blocks until a frame is available (which is the common pattern for capture APIs), the adapter's sleep pacing is counterproductive. Consider letting the plugin handle its own timing via next_frame() with an appropriate timeout derived from framerate_, and removing the sleep-based pacing. This would be more consistent with how the existing display_base_t::capture works, where the underlying capture API (dup.next_frame()) handles timing.

Copilot uses AI. Check for mistakes.
bool
is_codec_supported(std::string_view name, const ::video::config_t &config) override;

private:
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.

display_plugin_t does not override make_avcodec_encode_device() or make_nvenc_encode_device(). The base display_t implementations return nullptr, which causes make_encode_device() in src/video.cpp:2806-2811 to return an empty unique_ptr. This in turn causes make_synced_session() to fail (line 2842-2843), preventing the encoding pipeline from functioning at all.

For system memory plugins, you should override make_avcodec_encode_device to return a basic avcodec_encode_device_t (same as display_ram_t::make_avcodec_encode_device does at src/platform/windows/display_ram.cpp:402-403). For GPU memory types (DXGI/CUDA), appropriate encode device creation would be needed as well.

Suggested change
private:
private:
/**
* @brief Create an AVCodec encode device for this display backend.
*
* For plugin-backed capture that delivers frames in system memory, we can
* use a basic avcodec_encode_device_t, equivalent to the RAM display
* implementation.
*/
std::unique_ptr<avcodec_encode_device_t>
make_avcodec_encode_device(const avcodec_t &, const ::video::config_t &) override {
return std::make_unique<avcodec_encode_device_t>();
}

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +49
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;
}
}
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.

There's a potential use-after-free issue. display_plugin_t::~display_plugin_t() destroys the plugin session via destroy_session(session_), but plugin_img_t objects in the image pool may still hold references to this session (via plugin_img_t::session). When those images are eventually destroyed, their ~plugin_img_t() calls release() which attempts to call vtable->release_frame(session, ...) on the already-destroyed session.

To fix this, either:

  1. Ensure all outstanding plugin_img_t objects are released before destroying the session (e.g., clear the image pool), or
  2. Use a shared ownership mechanism (e.g., shared_ptr) for the session, or
  3. Set a flag that prevents plugin_img_t::release() from calling into the plugin after the session is destroyed.

Copilot uses AI. Check for mistakes.
Comment on lines +159 to +176
std::shared_ptr<img_t>
display_plugin_t::alloc_img() {
auto img = std::make_shared<plugin_img_t>();
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;
}
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.

alloc_img() creates a plugin_img_t but never allocates a backing buffer for data, leaving it as nullptr (the default from img_t). The callers of alloc_img() / dummy_img() in src/video.cpp (lines 2653-2654, 2935-2936, 3302) expect alloc_img() to return an image that, after dummy_img(), has valid data for use in session->convert().

Currently, dummy_img() just checks img->data which is null, does nothing, and returns success. This will cause downstream code to operate on a null data pointer.

The fix is to allocate a frame buffer in alloc_img() (e.g., img->data = new uint8_t[width * height * 4]{}) or have dummy_img() allocate the buffer when it's null. You'll also need to ensure the buffer is freed in plugin_img_t's destructor.

Copilot uses AI. Check for mistakes.
Comment on lines +1133 to +1143
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<capture_plugin::display_plugin_t>(plugin, hwdevice_type);
if (!disp->init(config, display_name)) {
ret = disp;
}
}
}
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 else block will also be reached for known capture type names that don't match their additional conditions. For example, type == "amd" with hwdevice_type != mem_type_e::dxgi, or type == "wgc" when is_running_as_system_user is true, will fall through to the plugin loader and attempt to find plugins named "amd" or "wgc".

While this is unlikely to match an actual plugin, it's confusing and could cause unintended behavior if someone names a plugin "amd" or "wgc". Consider adding an explicit check that the type is not a known built-in name before attempting plugin lookup, or restructuring the logic so that plugin lookup only occurs for truly unrecognized type names.

Copilot uses AI. Check for mistakes.
// 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.
qiin2333 added 2 commits March 9, 2026 15:55
- Remove all hardcoded magic private data / bypass mechanisms
- Load NvFBC private data from external file (nvfbc_auth.bin or env var)
- Remove INvFBCToSys vtable definition (requires official Capture SDK)
- Add NOTICE.md with legal disclaimer and usage requirements
- Keep NvFBC_CreateEx flow as reference implementation
- Mark INvFBCToSys method calls as TODO pending official SDK headers
Copilot AI review requested due to automatic review settings March 9, 2026 09:23
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Add NvFBC (Plugin) option to Windows capture dropdown
- Add install button with status check (GPU, registry, wrapper, plugin)
- Add Tauri backend: check_nvfbc_status, setup_nvfbc commands
- Add i18n keys for zh/en locales
- Update sunshine-control-panel submodule
@qiin2333 qiin2333 force-pushed the master branch 2 times, most recently from e6bbe21 to d74f9ed Compare March 28, 2026 13:24
@ShadowLemoon
Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

📝 Walkthrough

总体概述

该拉取请求为 Windows 上的 Sunshine 添加了可插拔式屏幕捕捉插件系统,包括通用插件加载器基础设施、稳定的 C ABI 接口定义以及 NVIDIA NvFBC 作为第一个参考实现,并集成了相应的 UI 组件用于插件管理和状态检查。

变更

文件/组合 变更摘要
插件API与加载器基础设施
src/platform/windows/capture_plugin/capture_plugin_api.h, src/platform/windows/capture_plugin/capture_plugin_loader.h, src/platform/windows/capture_plugin/capture_plugin_loader.cpp
定义跨编译器兼容的 DLL 导出接口(ABI 版本控制、会话句柄、内存类型、像素格式、帧数据结构);实现 DLL 动态加载、导出符号解析、ABI 校验和插件发现机制。
插件驱动的显示适配器
src/platform/windows/capture_plugin/display_plugin.h, src/platform/windows/capture_plugin/display_plugin.cpp
创建继承自 display_t 的插件适配器类,管理会话生命周期、帧获取与释放、内存类型转换,集成插件回调和帧速率控制。
NvFBC 插件示例实现
examples/capture_plugin_nvfbc/nvfbc_win_defs.h, examples/capture_plugin_nvfbc/sunshine_nvfbc_plugin.cpp, examples/capture_plugin_nvfbc/CMakeLists.txt, examples/capture_plugin_nvfbc/NOTICE.md
定义 NvFBC API 类型与接口(错误码、缓冲格式、参数结构);实现插件导出函数处理会话管理、帧捕捉(TODO:实际捕捉逻辑待完成)、私钥加载;提供编译配置与法律声明。
Windows 构建集成
cmake/compile_definitions/windows.cmake, src/platform/windows/display_base.cpp
更新 CMake 将插件相关源文件纳入平台构建目标;扩展显示选择逻辑,当非内置类型时尝试解析和加载匹配的捕捉插件。
UI 与本地化
src_assets/common/assets/web/configs/tabs/Advanced.vue, src_assets/common/assets/web/public/assets/locale/en.json, src_assets/common/assets/web/public/assets/locale/zh.json, src_assets/common/sunshine-control-panel
在高级选项卡添加 NvFBC 选择、安装检查、状态面板;为英文和中文提供 UI 字符串和诊断文本;更新 Control Panel 子模块。

时序图

sequenceDiagram
    participant App as Sunshine App
    participant Loader as 插件加载器
    participant DLL as 插件 DLL<br/>(NvFBC)
    participant Display as Display Plugin
    participant NvFBC as NvFBC 库
    
    App->>Loader: discover_plugins()
    Loader->>DLL: LoadLibraryExW()
    DLL-->>Loader: DLL 句柄
    Loader->>DLL: 解析 sunshine_capture_get_info
    DLL-->>Loader: 函数指针 + 插件元数据
    Loader->>Loader: 校验 ABI 版本
    Loader-->>App: loaded_plugin_t
    
    App->>Display: display_plugin_t::init()
    Display->>DLL: vtable.create_session()
    DLL->>NvFBC: NvFBC_CreateEx()
    NvFBC-->>DLL: 会话句柄
    DLL-->>Display: 会话 ID
    Display-->>App: 初始化成功
    
    loop 帧捕捉
        App->>Display: capture()
        Display->>DLL: vtable.next_frame()
        DLL->>NvFBC: 捕捉帧数据
        NvFBC-->>DLL: 帧缓冲
        DLL-->>Display: sunshine_frame_t
        Display->>Display: 填充 plugin_img_t
        Display-->>App: 图像回调
    end
    
    App->>Display: 销毁
    Display->>DLL: vtable.destroy_session()
    DLL->>NvFBC: 释放会话
    DLL->>DLL: FreeLibrary()
Loading

代码审查工作量估计

🎯 4 (复杂) | ⏱️ ~50 分钟

该变更涉及多个相互关联的新组件(插件加载器、显示适配器、NvFBC 实现),包含复杂的 DLL 加载、符号解析、会话管理和帧处理逻辑。跨越多个不同文件类型(C++ 核心库、示例插件、UI、CMake),需要对插件 ABI、生命周期、内存管理和错误处理的综合理解。

诗歌

🐰 插件之梦悄然落成,
NvFBC 显卡齐奋力,
加载器轻轻握手 dll,
帧帧捕捉如流云绽,
Sunshine 再添新羽翼!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.30% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确地概括了PR的主要变更:为Windows添加捕获插件框架系统。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/capture-plugin-framework
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feature/capture-plugin-framework

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (3)
src_assets/common/assets/web/configs/tabs/Advanced.vue (2)

108-141: 考虑合并重复的状态检查调用。

installNvfbc 中使用了两次 setTimeout 来延迟检查状态(3秒和8秒),与 WGC 的实现模式相似。但可以考虑抽取为可复用的工具函数:

♻️ 可选重构建议
// 可复用的延迟重检查函数
const scheduleRechecks = (checkFn, delays = [3000, 8000]) => {
  delays.forEach(delay => setTimeout(checkFn, delay))
}

// 在 installNvfbc 中使用
const installNvfbc = async () => {
  // ... existing code ...
  scheduleRechecks(checkNvfbcStatus)
}

这是一个可选的代码整洁性改进,当前实现功能正确。

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

In `@src_assets/common/assets/web/configs/tabs/Advanced.vue` around lines 108 -
141, The installNvfbc function currently schedules duplicate rechecks with two
setTimeout calls; extract a small helper like scheduleRechecks(checkFn, delays =
[3000, 8000]) that iterates delays and calls setTimeout(checkFn, delay), then
replace the two setTimeouts in installNvfbc with a single call
scheduleRechecks(checkNvfbcStatus); update any other places (e.g., WGC-related
code) to reuse scheduleRechecks to avoid duplication and keep checkNvfbcStatus
unchanged.

249-286: 按钮状态条件过长,建议抽取为计算属性。

多处重复检查 nvfbcStatus && nvfbcStatus.wrapper_installed && nvfbcStatus.registry_enabled && nvfbcStatus.plugin_installed,可读性较差。

♻️ 建议的重构
// 在 script setup 中添加计算属性
const isNvfbcReady = computed(() => {
  return nvfbcStatus.value &&
    nvfbcStatus.value.wrapper_installed &&
    nvfbcStatus.value.registry_enabled &&
    nvfbcStatus.value.plugin_installed
})

// 模板中使用
// :class="['btn', isNvfbcReady ? 'btn-success' : 'btn-primary']"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src_assets/common/assets/web/configs/tabs/Advanced.vue` around lines 249 -
286, Extract the repeated long condition into a computed property named
isNvfbcReady in the component's script setup (use nvfbcStatus as the source) and
then replace all occurrences of "nvfbcStatus && nvfbcStatus.wrapper_installed &&
nvfbcStatus.registry_enabled && nvfbcStatus.plugin_installed" in the template
with isNvfbcReady; update the button's :class expression, the icon v-else-if,
and the text conditional that currently checks nvfbcStatus to use isNvfbcReady
alongside the existing isInstallingNvfbc, isCheckingNvfbc and installNvfbc
handler to preserve behavior.
examples/capture_plugin_nvfbc/CMakeLists.txt (1)

7-17: 考虑验证 SUNSHINE_SOURCE_DIR 路径有效性。

当前实现假设插件在 examples/capture_plugin_nvfbc/ 目录下构建,../.. 指向 Sunshine 源码根目录。如果从其他位置构建,可能找不到 capture_plugin_api.h

建议添加路径验证:

🛠️ 建议的修改
 set(SUNSHINE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH
     "Path to Sunshine source root")
+
+if(NOT EXISTS "${SUNSHINE_SOURCE_DIR}/src/platform/windows/capture_plugin/capture_plugin_api.h")
+  message(FATAL_ERROR "Cannot find capture_plugin_api.h. Please set SUNSHINE_SOURCE_DIR correctly.")
+endif()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/capture_plugin_nvfbc/CMakeLists.txt` around lines 7 - 17, Validate
the SUNSHINE_SOURCE_DIR before using it: check that "${SUNSHINE_SOURCE_DIR}"
exists and that the expected header (e.g., capture_plugin_api.h) can be found
before calling target_include_directories for target sunshine_nvfbc, and if not
present emit a clear CMake error (message(FATAL_ERROR)) with guidance on how to
set SUNSHINE_SOURCE_DIR (or optionally try to locate the header with find_path
and fallback). Ensure references to SUNSHINE_SOURCE_DIR, capture_plugin_api.h
and the target name sunshine_nvfbc are used so the validation ties directly to
the include setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/capture_plugin_nvfbc/sunshine_nvfbc_plugin.cpp`:
- Around line 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.
- Around line 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.

In `@src/platform/windows/capture_plugin/capture_plugin_api.h`:
- Line 124: The public ABI lacks an explicit Windows calling convention which
can break cross-compiler/plugin calls; define a calling-convention macro (e.g.,
SUNSHINE_CAPTURE_CALL set to __cdecl or chosen convention) and apply it
uniformly to all function pointer types and exported functions (for example,
update sunshine_capture_get_info_fn and any other sunshine_capture_* function
pointer typedefs and export declarations to use SUNSHINE_CAPTURE_CALL) so the
host and plugins share the same calling convention and avoid
GetProcAddress/stack corruption issues.
- Around line 84-102: sunshine_frame_t.timestamp_ns currently claims to be a
"steady clock nanoseconds" value but exposing steady_clock epoch across C ABI is
not portable; stop reconstructing std::chrono::steady_clock::time_point directly
from frame.timestamp_ns in display_plugin.cpp (the plugin_img->frame_timestamp
assignment). Instead, change the ABI semantics: document timestamp_ns as either
(a) 0 meaning "unset—host must fill timestamp using its local monotonic clock
via existing qpc_time_difference logic" or (b) an OS-level monotonic tick value
(e.g., QueryPerformanceCounter ticks or nanoseconds tied to the host's monotonic
clock) and then convert it to a steady_clock time_point using host-specific
conversion (use qpc_time_difference and QPC frequency where available). Update
the sunshine_frame_t comment to remove any claim about std::chrono::steady_clock
epoch, modify the consumer code around plugin_img->frame_timestamp to check for
timestamp_ns == 0 and use host qpc_time_difference to populate the timestamp,
and if non-zero perform an OS-specific conversion rather than direct
std::chrono::steady_clock reconstruction.

In `@src/platform/windows/capture_plugin/capture_plugin_loader.cpp`:
- Around line 35-40: The call to GetModuleFileNameW in get_plugins_dir does not
check its return value and can silently truncate paths longer than MAX_PATH;
update get_plugins_dir to call GetModuleFileNameW with a dynamically sized
wchar_t buffer (e.g., std::vector<wchar_t>) and loop, doubling the buffer size
until the returned length is less than the buffer size (and nonzero), or stop on
error; if GetModuleFileNameW returns zero, handle/check GetLastError and
propagate or log an error instead of using a truncated module_path; reference
get_plugins_dir, GetModuleFileNameW, MAX_PATH and module_path when locating the
code to change.
- Around line 128-139: The directory iteration over
std::filesystem::directory_iterator in the plugin loading loop can throw (e.g.,
permissions or missing path); wrap the iteration in a try-catch that catches
std::filesystem::filesystem_error (and optionally std::exception) around the
loop that uses directory_iterator, log a clear error via your logger and either
abort plugin loading or return/continue gracefully, and ensure resources remain
consistent; update the block that calls load_plugin(entry.path()) and pushes
into plugins to run only when no exception was thrown.

In `@src/platform/windows/capture_plugin/display_plugin.cpp`:
- Around line 39-48: The destructor currently calls
plugin_->vtable.destroy_session(session_) immediately, which can race with
in-flight frames returned via capture() because plugin_img_t::~plugin_img_t()
still calls release_frame() using the cached session/vtable; instead, set
stop_flag_ and call plugin_->vtable.interrupt(session_) as now, then wait for
any background next_frame() to exit (join the worker thread or wait on a
condition) and for in-flight frames to be returned (track outstanding frame
count or use a shared ownership/weak_ptr scheme), only after outstanding frames
are zero and next_frame() has stopped call
plugin_->vtable.destroy_session(session_) and then set session_ = nullptr;
update display_plugin_t::~display_plugin_t(), any thread/worker join logic, and
ensure plugin_img_t holds only safe references (weak/shared) to session/vtable
to avoid use-after-free.
- Around line 19-30: The adapter must reject or explicitly handle memory types
it doesn't support instead of silently mapping them to system memory; update the
code path that converts and forwards mem_type_ to the plugin by (1) changing
to_plugin_mem_type(mem_type_e) to not default to SUNSHINE_MEM_SYSTEM — return an
explicit invalid/unsupported value or assert/error on unknown enums, and (2) add
a runtime check before calling the plugin (where mem_type_ is passed) to
validate adapter support for that mem type and return an error if unsupported.
Also ensure the plugin buffer population (the block that currently only copies
CPU pointer and pitch and mentions gpu_handle/gpu_subresource) copies or
forwards gpu_handle and gpu_subresource for DXGI/CUDA paths so GPU-backed memory
isn’t misrouted; locate references to mem_type_, to_plugin_mem_type(), and the
code that sets gpu_handle/gpu_subresource and update those to propagate GPU
fields for non-system types and to fail early for unsupported/unknown types.

---

Nitpick comments:
In `@examples/capture_plugin_nvfbc/CMakeLists.txt`:
- Around line 7-17: Validate the SUNSHINE_SOURCE_DIR before using it: check that
"${SUNSHINE_SOURCE_DIR}" exists and that the expected header (e.g.,
capture_plugin_api.h) can be found before calling target_include_directories for
target sunshine_nvfbc, and if not present emit a clear CMake error
(message(FATAL_ERROR)) with guidance on how to set SUNSHINE_SOURCE_DIR (or
optionally try to locate the header with find_path and fallback). Ensure
references to SUNSHINE_SOURCE_DIR, capture_plugin_api.h and the target name
sunshine_nvfbc are used so the validation ties directly to the include setup.

In `@src_assets/common/assets/web/configs/tabs/Advanced.vue`:
- Around line 108-141: The installNvfbc function currently schedules duplicate
rechecks with two setTimeout calls; extract a small helper like
scheduleRechecks(checkFn, delays = [3000, 8000]) that iterates delays and calls
setTimeout(checkFn, delay), then replace the two setTimeouts in installNvfbc
with a single call scheduleRechecks(checkNvfbcStatus); update any other places
(e.g., WGC-related code) to reuse scheduleRechecks to avoid duplication and keep
checkNvfbcStatus unchanged.
- Around line 249-286: Extract the repeated long condition into a computed
property named isNvfbcReady in the component's script setup (use nvfbcStatus as
the source) and then replace all occurrences of "nvfbcStatus &&
nvfbcStatus.wrapper_installed && nvfbcStatus.registry_enabled &&
nvfbcStatus.plugin_installed" in the template with isNvfbcReady; update the
button's :class expression, the icon v-else-if, and the text conditional that
currently checks nvfbcStatus to use isNvfbcReady alongside the existing
isInstallingNvfbc, isCheckingNvfbc and installNvfbc handler to preserve
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2035ff09-ed1e-438d-ad74-bdcf5d5bc6f6

📥 Commits

Reviewing files that changed from the base of the PR and between d74f9ed and f07169e.

📒 Files selected for processing (15)
  • cmake/compile_definitions/windows.cmake
  • examples/capture_plugin_nvfbc/CMakeLists.txt
  • examples/capture_plugin_nvfbc/NOTICE.md
  • examples/capture_plugin_nvfbc/nvfbc_win_defs.h
  • examples/capture_plugin_nvfbc/sunshine_nvfbc_plugin.cpp
  • src/platform/windows/capture_plugin/capture_plugin_api.h
  • src/platform/windows/capture_plugin/capture_plugin_loader.cpp
  • src/platform/windows/capture_plugin/capture_plugin_loader.h
  • src/platform/windows/capture_plugin/display_plugin.cpp
  • src/platform/windows/capture_plugin/display_plugin.h
  • src/platform/windows/display_base.cpp
  • src_assets/common/assets/web/configs/tabs/Advanced.vue
  • src_assets/common/assets/web/public/assets/locale/en.json
  • src_assets/common/assets/web/public/assets/locale/zh.json
  • src_assets/common/sunshine-control-panel

Comment on lines +223 to +229
// 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;
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.

Comment on lines +271 to +286
// 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<INvFBCToSys *>(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<sunshine_capture_session_t>(s);
return 0;
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.

Comment on lines +84 to +102
/** 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;
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

🧩 Analysis chain

🏁 Script executed:

find . -name "capture_plugin_api.h" -o -name "display_plugin.cpp" | head -20

Repository: AlkaidLab/foundation-sunshine

Length of output: 188


🏁 Script executed:

cat -n src/platform/windows/capture_plugin/capture_plugin_api.h | sed -n '84,102p'

Repository: AlkaidLab/foundation-sunshine

Length of output: 927


🏁 Script executed:

cat -n src/platform/windows/capture_plugin/display_plugin.cpp | sed -n '120,135p'

Repository: AlkaidLab/foundation-sunshine

Length of output: 784


🏁 Script executed:

rg "timestamp_ns" --type cpp -B 3 -A 3

Repository: AlkaidLab/foundation-sunshine

Length of output: 1610


🏁 Script executed:

rg "steady_clock" src/platform/windows/ -B 2 -A 2

Repository: AlkaidLab/foundation-sunshine

Length of output: 14812


🏁 Script executed:

rg "timestamp_ns\s*=" --type cpp -B 5 -A 2

Repository: AlkaidLab/foundation-sunshine

Length of output: 55


🏁 Script executed:

find . -name "*.cpp" -path "*/capture_plugin/*" | head -20

Repository: AlkaidLab/foundation-sunshine

Length of output: 193


🏁 Script executed:

grep -r "sunshine_frame_t" --include="*.cpp" -B 2 -A 2

Repository: AlkaidLab/foundation-sunshine

Length of output: 1494


🏁 Script executed:

cat -n examples/capture_plugin_nvfbc/sunshine_nvfbc_plugin.cpp | grep -A 30 "timestamp_ns"

Repository: AlkaidLab/foundation-sunshine

Length of output: 55


🏁 Script executed:

rg "\.timestamp_ns\s*=" --type cpp -B 3 -A 1

Repository: AlkaidLab/foundation-sunshine

Length of output: 55


🏁 Script executed:

cat -n examples/capture_plugin_nvfbc/sunshine_nvfbc_plugin.cpp | head -200

Repository: AlkaidLab/foundation-sunshine

Length of output: 8054


🏁 Script executed:

rg "frame\." src/platform/windows/capture_plugin/ --type cpp -A 2 | head -50

Repository: AlkaidLab/foundation-sunshine

Length of output: 1823


🏁 Script executed:

find examples -name "*.cpp" | xargs grep -l "timestamp_ns" 2>/dev/null

Repository: AlkaidLab/foundation-sunshine

Length of output: 57


🏁 Script executed:

cat -n examples/capture_plugin_nvfbc/sunshine_nvfbc_plugin.cpp | sed -n '200,350p'

Repository: AlkaidLab/foundation-sunshine

Length of output: 5709


🏁 Script executed:

rg "steady_clock" --type cpp -l | head -20

Repository: AlkaidLab/foundation-sunshine

Length of output: 589


🏁 Script executed:

cat -n src/platform/windows/capture_plugin/display_plugin.cpp | sed -n '100,150p'

Repository: AlkaidLab/foundation-sunshine

Length of output: 2163


🏁 Script executed:

cat -n src/platform/windows/capture_plugin/display_plugin.cpp | sed -n '1,50p'

Repository: AlkaidLab/foundation-sunshine

Length of output: 1534


🏁 Script executed:

grep -r "next_frame" src/platform/windows/capture_plugin/ --include="*.h" -A 3

Repository: AlkaidLab/foundation-sunshine

Length of output: 2279


🏁 Script executed:

rg "plugin.*vtable" src/platform/windows/capture_plugin/ -B 2 -A 2

Repository: AlkaidLab/foundation-sunshine

Length of output: 7179


🏁 Script executed:

rg "time_since_epoch|steady_clock::now" src/platform/windows/ --type cpp -B 2 -A 2 | head -60

Repository: AlkaidLab/foundation-sunshine

Length of output: 3892


🏁 Script executed:

cat -n src/platform/windows/capture_plugin/capture_plugin_api.h | sed -n '1,50p'

Repository: AlkaidLab/foundation-sunshine

Length of output: 2295


🏁 Script executed:

grep -r "QPC\|QueryPerformance" src/platform/windows/ --include="*.cpp" --include="*.h" | head -20

Repository: AlkaidLab/foundation-sunshine

Length of output: 500


🏁 Script executed:

cat -n src/platform/windows/misc.cpp | grep -A 10 -B 10 "QueryPerformance"

Repository: AlkaidLab/foundation-sunshine

Length of output: 1178


🏁 Script executed:

cat -n src/platform/windows/display_ram.cpp | grep -A 5 -B 5 "qpc_time_difference"

Repository: AlkaidLab/foundation-sunshine

Length of output: 634


🏁 Script executed:

rg "qpc_time_difference|qpc_counter" src/platform/windows/ --type cpp --type h -B 3 -A 3

Repository: AlkaidLab/foundation-sunshine

Length of output: 3777


C ABI 中不应暴露 steady_clock 的 epoch 表示。

timestamp_ns 被定义为"steady clock 纳秒",但此头文件声称支持跨编译器兼容性(MSVC、MinGW、Clang)。然而 std::chrono::steady_clock::time_since_epoch().count() 的具体值在不同工具链中并无标准保证,不同编译器可能使用不同的 epoch 表示。

在 display_plugin.cpp 第 126-128 行,宿主直接将 int64_t 值重建为 steady_clock::time_point

plugin_img->frame_timestamp = std::chrono::steady_clock::time_point(
  std::chrono::nanoseconds(frame.timestamp_ns));

这样做假设了插件和宿主使用相同的 steady_clock 内部表示,跨编译器时此假设不成立。

建议改用 OS 级时间语义,例如用 QueryPerformanceCounter 的 ticks 加频率表达时间,或允许 timestamp_ns = 0 让宿主负责填充时间戳。宿主代码已有相应基础设施(qpc_time_difference)可支持此方案。

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

In `@src/platform/windows/capture_plugin/capture_plugin_api.h` around lines 84 -
102, sunshine_frame_t.timestamp_ns currently claims to be a "steady clock
nanoseconds" value but exposing steady_clock epoch across C ABI is not portable;
stop reconstructing std::chrono::steady_clock::time_point directly from
frame.timestamp_ns in display_plugin.cpp (the plugin_img->frame_timestamp
assignment). Instead, change the ABI semantics: document timestamp_ns as either
(a) 0 meaning "unset—host must fill timestamp using its local monotonic clock
via existing qpc_time_difference logic" or (b) an OS-level monotonic tick value
(e.g., QueryPerformanceCounter ticks or nanoseconds tied to the host's monotonic
clock) and then convert it to a steady_clock time_point using host-specific
conversion (use qpc_time_difference and QPC frequency where available). Update
the sunshine_frame_t comment to remove any claim about std::chrono::steady_clock
epoch, modify the consumer code around plugin_img->frame_timestamp to check for
timestamp_ns == 0 and use host qpc_time_difference to populate the timestamp,
and if non-zero perform an OS-specific conversion rather than direct
std::chrono::steady_clock reconstruction.

*
* Plugin DLL must export as: sunshine_capture_get_info
*/
typedef int (*sunshine_capture_get_info_fn)(sunshine_capture_plugin_info_t *info);
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

🧩 Analysis chain

🏁 Script executed:

find . -name "capture_plugin_api.h" -type f

Repository: AlkaidLab/foundation-sunshine

Length of output: 131


🏁 Script executed:

head -250 src/platform/windows/capture_plugin/capture_plugin_api.h | cat -n

Repository: AlkaidLab/foundation-sunshine

Length of output: 10813


🏁 Script executed:

grep -n "CALL\|__stdcall\|__cdecl\|__fastcall\|calling" src/platform/windows/capture_plugin/capture_plugin_api.h

Repository: AlkaidLab/foundation-sunshine

Length of output: 55


🏁 Script executed:

sed -n '120,130p' src/platform/windows/capture_plugin/capture_plugin_api.h | cat -n

Repository: AlkaidLab/foundation-sunshine

Length of output: 569


🏁 Script executed:

sed -n '130,180p' src/platform/windows/capture_plugin/capture_plugin_api.h | cat -n

Repository: AlkaidLab/foundation-sunshine

Length of output: 2240


🏁 Script executed:

sed -n '180,240p' src/platform/windows/capture_plugin/capture_plugin_api.h | cat -n

Repository: AlkaidLab/foundation-sunshine

Length of output: 2713


公开 ABI 缺少固定调用约定。

此处只有 extern "C" 但未指定 Windows 调用约定。extern "C" 仅处理名称修饰,不控制调用约定。若插件以 /Gz (stdcall) 或其他调用约定编译,与宿主的默认调用约定不符,会导致 GetProcAddress 解析失败或调用栈损坏。头部声称"跨编译器兼容性"但未通过明确的调用约定保证。建议定义 SUNSHINE_CAPTURE_CALL 宏(如 #define SUNSHINE_CAPTURE_CALL __cdecl),并将所有函数指针类型和导出函数声明统一采用此宏。

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

In `@src/platform/windows/capture_plugin/capture_plugin_api.h` at line 124, The
public ABI lacks an explicit Windows calling convention which can break
cross-compiler/plugin calls; define a calling-convention macro (e.g.,
SUNSHINE_CAPTURE_CALL set to __cdecl or chosen convention) and apply it
uniformly to all function pointer types and exported functions (for example,
update sunshine_capture_get_info_fn and any other sunshine_capture_* function
pointer typedefs and export declarations to use SUNSHINE_CAPTURE_CALL) so the
host and plugins share the same calling convention and avoid
GetProcAddress/stack corruption issues.

Comment on lines +35 to +40
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";
}
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

GetModuleFileNameW 返回值未检查,可能存在路径截断风险。

当路径超过 MAX_PATH 字符时,GetModuleFileNameW 会截断路径但不会报错。建议检查返回值:

🛡️ 建议的修复
 std::filesystem::path
 get_plugins_dir() {
   wchar_t module_path[MAX_PATH];
-  GetModuleFileNameW(nullptr, module_path, MAX_PATH);
+  DWORD len = GetModuleFileNameW(nullptr, module_path, MAX_PATH);
+  if (len == 0 || len >= MAX_PATH) {
+    BOOST_LOG(error) << "Failed to get module path or path too long";
+    return {};
+  }
   return std::filesystem::path(module_path).parent_path() / "plugins";
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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";
}
std::filesystem::path
get_plugins_dir() {
wchar_t module_path[MAX_PATH];
DWORD len = GetModuleFileNameW(nullptr, module_path, MAX_PATH);
if (len == 0 || len >= MAX_PATH) {
BOOST_LOG(error) << "Failed to get module path or path too long";
return {};
}
return std::filesystem::path(module_path).parent_path() / "plugins";
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/platform/windows/capture_plugin/capture_plugin_loader.cpp` around lines
35 - 40, The call to GetModuleFileNameW in get_plugins_dir does not check its
return value and can silently truncate paths longer than MAX_PATH; update
get_plugins_dir to call GetModuleFileNameW with a dynamically sized wchar_t
buffer (e.g., std::vector<wchar_t>) and loop, doubling the buffer size until the
returned length is less than the buffer size (and nonzero), or stop on error; if
GetModuleFileNameW returns zero, handle/check GetLastError and propagate or log
an error instead of using a truncated module_path; reference get_plugins_dir,
GetModuleFileNameW, MAX_PATH and module_path when locating the code to change.

Comment on lines +128 to +139
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));
}
}
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

目录遍历缺少异常处理。

std::filesystem::directory_iterator 在权限不足或路径不可访问时可能抛出异常。建议添加 try-catch:

🛡️ 建议的修复
-  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));
+  try {
+    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));
+      }
     }
+  } catch (const std::filesystem::filesystem_error &e) {
+    BOOST_LOG(error) << "Error scanning plugins directory: " << e.what();
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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));
}
}
try {
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));
}
}
} catch (const std::filesystem::filesystem_error &e) {
BOOST_LOG(error) << "Error scanning plugins directory: " << e.what();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/platform/windows/capture_plugin/capture_plugin_loader.cpp` around lines
128 - 139, The directory iteration over std::filesystem::directory_iterator in
the plugin loading loop can throw (e.g., permissions or missing path); wrap the
iteration in a try-catch that catches std::filesystem::filesystem_error (and
optionally std::exception) around the loop that uses directory_iterator, log a
clear error via your logger and either abort plugin loading or return/continue
gracefully, and ensure resources remain consistent; update the block that calls
load_plugin(entry.path()) and pushes into plugins to run only when no exception
was thrown.

Comment on lines +19 to +30
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;
}
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

这里应先拒绝当前适配器还吃不下的内存类型。

Line 66 会把 mem_type_ 直接传给插件,但 Lines 114-124 只拷贝了 CPU 指针和 pitch,gpu_handle/gpu_subresource 完全没被消费;同时 to_plugin_mem_type() 的默认分支还会把未知类型静默回落到 SUNSHINE_MEM_SYSTEM。这会把 DXGI/CUDA 或未来新增类型伪装成 system-memory 路径,最终在编码链路里失配。

Also applies to: 66-70, 114-124

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

In `@src/platform/windows/capture_plugin/display_plugin.cpp` around lines 19 - 30,
The adapter must reject or explicitly handle memory types it doesn't support
instead of silently mapping them to system memory; update the code path that
converts and forwards mem_type_ to the plugin by (1) changing
to_plugin_mem_type(mem_type_e) to not default to SUNSHINE_MEM_SYSTEM — return an
explicit invalid/unsupported value or assert/error on unknown enums, and (2) add
a runtime check before calling the plugin (where mem_type_ is passed) to
validate adapter support for that mem type and return an error if unsupported.
Also ensure the plugin buffer population (the block that currently only copies
CPU pointer and pitch and mentions gpu_handle/gpu_subresource) copies or
forwards gpu_handle and gpu_subresource for DXGI/CUDA paths so GPU-backed memory
isn’t misrouted; locate references to mem_type_, to_plugin_mem_type(), and the
code that sets gpu_handle/gpu_subresource and update those to propagate GPU
fields for non-system types and to fail early for unsupported/unknown types.

Comment on lines +39 to +48
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;
}
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 | 🔴 Critical

不要在析构里立刻销毁 session_

capture() 会把 plugin_img_t 通过 shared_ptr 交给外部,而 plugin_img_t::~plugin_img_t() 仍会用缓存的 session/vtablerelease_frame()(见 src/platform/windows/capture_plugin/display_plugin.h 的 Lines 19-42)。这里直接 destroy_session(session_),会让在途帧或稍后归还的帧回调到已销毁会话;另外析构也没有等待正在执行的 next_frame() 退出。

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

In `@src/platform/windows/capture_plugin/display_plugin.cpp` around lines 39 - 48,
The destructor currently calls plugin_->vtable.destroy_session(session_)
immediately, which can race with in-flight frames returned via capture() because
plugin_img_t::~plugin_img_t() still calls release_frame() using the cached
session/vtable; instead, set stop_flag_ and call
plugin_->vtable.interrupt(session_) as now, then wait for any background
next_frame() to exit (join the worker thread or wait on a condition) and for
in-flight frames to be returned (track outstanding frame count or use a shared
ownership/weak_ptr scheme), only after outstanding frames are zero and
next_frame() has stopped call plugin_->vtable.destroy_session(session_) and then
set session_ = nullptr; update display_plugin_t::~display_plugin_t(), any
thread/worker join logic, and ensure plugin_img_t holds only safe references
(weak/shared) to session/vtable to avoid use-after-free.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants