Skip to content

Commit

Permalink
feat(capture/windows): hook APIs to avoid output reparenting that bre…
Browse files Browse the repository at this point in the history
…aks DDA (#3530)

* Revert "feat(ddprobe): allow to manually specify gpu preference (#3521)"

This reverts commit 6a233cb.

* Keep display revert delay input type change from 6a233cb

* Remove ddprobe

* feat(capture/windows): hook APIs to avoid output reparenting that breaks DDA
  • Loading branch information
cgutman authored Jan 12, 2025
1 parent c369e8e commit 8392bdc
Show file tree
Hide file tree
Showing 14 changed files with 51 additions and 519 deletions.
1 change: 1 addition & 0 deletions .codeql-prebuild-cpp-Windows.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dependencies=(
"mingw-w64-ucrt-x86_64-cmake"
"mingw-w64-ucrt-x86_64-cppwinrt"
"mingw-w64-ucrt-x86_64-curl-winssl"
"mingw-w64-ucrt-x86_64-MinHook"
"mingw-w64-ucrt-x86_64-miniupnpc"
"mingw-w64-ucrt-x86_64-nlohmann-json"
"mingw-w64-ucrt-x86_64-nodejs"
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,7 @@ jobs:
mingw-w64-ucrt-x86_64-cppwinrt
mingw-w64-ucrt-x86_64-curl-winssl
mingw-w64-ucrt-x86_64-graphviz
mingw-w64-ucrt-x86_64-MinHook
mingw-w64-ucrt-x86_64-miniupnpc
mingw-w64-ucrt-x86_64-nlohmann-json
mingw-w64-ucrt-x86_64-nodejs
Expand Down
2 changes: 1 addition & 1 deletion cmake/dependencies/common.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ include_directories(SYSTEM ${MINIUPNP_INCLUDE_DIRS})
# ffmpeg pre-compiled binaries
if(NOT DEFINED FFMPEG_PREPARED_BINARIES)
if(WIN32)
set(FFMPEG_PLATFORM_LIBRARIES mfplat ole32 strmiids mfuuid vpl)
set(FFMPEG_PLATFORM_LIBRARIES mfplat ole32 strmiids mfuuid vpl MinHook)
elseif(UNIX AND NOT APPLE)
set(FFMPEG_PLATFORM_LIBRARIES numa va va-drm va-x11 X11)
endif()
Expand Down
1 change: 0 additions & 1 deletion cmake/packaging/windows.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ install(TARGETS dxgi-info RUNTIME DESTINATION "tools" COMPONENT dxgi)
install(TARGETS audio-info RUNTIME DESTINATION "tools" COMPONENT audio)

# Mandatory tools
install(TARGETS ddprobe RUNTIME DESTINATION "tools" COMPONENT application)
install(TARGETS sunshinesvc RUNTIME DESTINATION "tools" COMPONENT application)

# Mandatory scripts
Expand Down
1 change: 1 addition & 0 deletions docs/building.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ dependencies=(
"mingw-w64-ucrt-x86_64-curl-winssl"
"mingw-w64-ucrt-x86_64-doxygen" # Optional, for docs... better to install official Doxygen
"mingw-w64-ucrt-x86_64-graphviz" # Optional, for docs
"mingw-w64-ucrt-x86_64-MinHook"
"mingw-w64-ucrt-x86_64-miniupnpc"
"mingw-w64-ucrt-x86_64-nlohmann-json"
"mingw-w64-ucrt-x86_64-nodejs"
Expand Down
31 changes: 0 additions & 31 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -848,37 +848,6 @@ editing the `conf` file in a text editor. Use the examples as reference.
</tr>
</table>

### gpu_preference

<table>
<tr>
<td>Description</td>
<td colspan="2">
Specify the GPU preference for the Sunshine process.
<br>
<br>
If set to negative number (-1 by default), Sunshine will try to detect the best GPU for the streamed display, but if it fails you will get a black screen.
<br>
Setting it to 0 will allow Windows to try and select the best GPU.
<br>
Setting it to 1 and above will prioritize the GPU that matches this number (the number has to be guessed, but it starts at 1 and increases).
@note{Applies to Windows only.}
</td>
</tr>
<tr>
<td>Default</td>
<td colspan="2">@code{}
-1
@endcode</td>
</tr>
<tr>
<td>Example</td>
<td colspan="2">@code{}
2
@endcode</td>
</tr>
</table>

### output_name

<table>
Expand Down
2 changes: 0 additions & 2 deletions src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,6 @@ namespace config {
{}, // capture
{}, // encoder
{}, // adapter_name
-1, // gpu_preference
{}, // output_name

{
Expand Down Expand Up @@ -1122,7 +1121,6 @@ namespace config {
string_f(vars, "capture", video.capture);
string_f(vars, "encoder", video.encoder);
string_f(vars, "adapter_name", video.adapter_name);
int_f(vars, "gpu_preference", video.gpu_preference);
string_f(vars, "output_name", video.output_name);

generic_f(vars, "dd_configuration_option", video.dd.configuration_option, dd::config_option_from_view);
Expand Down
1 change: 0 additions & 1 deletion src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ namespace config {
std::string capture;
std::string encoder;
std::string adapter_name;
int gpu_preference;
std::string output_name;

struct dd_t {
Expand Down
172 changes: 47 additions & 125 deletions src/platform/windows/display_base.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,22 @@
#include <boost/algorithm/string/join.hpp>
#include <boost/process/v1.hpp>

#include <MinHook.h>

// We have to include boost/process/v1.hpp before display.h due to WinSock.h,
// but that prevents the definition of NTSTATUS so we must define it ourself.
typedef long NTSTATUS;

// Definition from the WDK's d3dkmthk.h
typedef enum _D3DKMT_GPU_PREFERENCE_QUERY_STATE: DWORD {
D3DKMT_GPU_PREFERENCE_STATE_UNINITIALIZED, ///< The GPU preference isn't initialized.
D3DKMT_GPU_PREFERENCE_STATE_HIGH_PERFORMANCE, ///< The highest performing GPU is preferred.
D3DKMT_GPU_PREFERENCE_STATE_MINIMUM_POWER, ///< The minimum-powered GPU is preferred.
D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED, ///< A GPU preference isn't specified.
D3DKMT_GPU_PREFERENCE_STATE_NOT_FOUND, ///< A GPU preference isn't found.
D3DKMT_GPU_PREFERENCE_STATE_USER_SPECIFIED_GPU ///< A specific GPU is preferred.
} D3DKMT_GPU_PREFERENCE_QUERY_STATE;

#include "display.h"
#include "misc.h"
#include "src/config.h"
Expand Down Expand Up @@ -329,115 +341,6 @@ namespace platf::dxgi {
return capture_e::ok;
}

bool
set_gpu_preference_on_self(int preference) {
// The GPU preferences key uses app path as the value name.
WCHAR sunshine_path[MAX_PATH];
GetModuleFileNameW(NULL, sunshine_path, ARRAYSIZE(sunshine_path));

WCHAR value_data[128];
swprintf_s(value_data, L"GpuPreference=%d;", preference);

auto status = RegSetKeyValueW(HKEY_CURRENT_USER,
L"Software\\Microsoft\\DirectX\\UserGpuPreferences",
sunshine_path,
REG_SZ,
value_data,
(wcslen(value_data) + 1) * sizeof(WCHAR));
if (status != ERROR_SUCCESS) {
BOOST_LOG(error) << "Failed to set GPU preference: "sv << status;
return false;
}

BOOST_LOG(info) << "Set GPU preference: "sv << preference;
return true;
}

bool
validate_and_test_gpu_preference(const std::string &display_name, bool verify_frame_capture) {
std::string cmd = "tools\\ddprobe.exe";

// We start at 1 because 0 is automatic selection which can be overridden by
// the GPU driver control panel options. Since ddprobe.exe can have different
// GPU driver overrides than Sunshine.exe, we want to avoid a scenario where
// autoselection might work for ddprobe.exe but not for us.
for (int i = 1; i < 5; i++) {
// Run the probe tool. It returns the status of DuplicateOutput().
//
// Arg format: [GPU preference] [Display name] [--verify-frame-capture]
HRESULT result;
std::vector<std::string> args = { std::to_string(i), display_name };
try {
if (verify_frame_capture) {
args.emplace_back("--verify-frame-capture");
}
result = bp::system(cmd, bp::args(args), bp::std_out > bp::null, bp::std_err > bp::null);
}
catch (bp::process_error &e) {
BOOST_LOG(error) << "Failed to start ddprobe.exe: "sv << e.what();
return false;
}

BOOST_LOG(info) << "ddprobe.exe " << boost::algorithm::join(args, " ") << " returned 0x"
<< util::hex(result).to_string_view();

// E_ACCESSDENIED can happen at the login screen. If we get this error,
// we know capture would have been supported, because DXGI_ERROR_UNSUPPORTED
// would have been raised first if it wasn't.
if (result == S_OK || result == E_ACCESSDENIED) {
// We found a working GPU preference, so set ourselves to use that.
return set_gpu_preference_on_self(i);
}
}

// If no valid configuration was found, return false
return false;
}

// On hybrid graphics systems, Windows will change the order of GPUs reported by
// DXGI in accordance with the user's GPU preference. If the selected GPU is a
// render-only device with no displays, DXGI will add virtual outputs to the
// that device to avoid confusing applications. While this works properly for most
// applications, it breaks the Desktop Duplication API because DXGI doesn't proxy
// the virtual DXGIOutput to the real GPU it is attached to. When trying to call
// DuplicateOutput() on one of these virtual outputs, it fails with DXGI_ERROR_UNSUPPORTED
// (even if you try sneaky stuff like passing the ID3D11Device for the iGPU and the
// virtual DXGIOutput from the dGPU). Because the GPU preference is once-per-process,
// we spawn a helper tool to probe for us before we set our own GPU preference.
bool
probe_for_gpu_preference(const std::string &display_name) {
static bool set_gpu_preference = false;

// If we've already been through here, there's nothing to do this time.
if (set_gpu_preference) {
return true;
}

// If the GPU preference was manually specified, we can skip the probe.
if (config::video.gpu_preference >= 0) {
if (set_gpu_preference_on_self(config::video.gpu_preference)) {
set_gpu_preference = true;
return true;
}
}
else {
// Try probing with different GPU preferences and verify_frame_capture flag
if (validate_and_test_gpu_preference(display_name, true)) {
set_gpu_preference = true;
return true;
}

// If no valid configuration was found, try again with verify_frame_capture == false
if (validate_and_test_gpu_preference(display_name, false)) {
set_gpu_preference = true;
return true;
}
}

// If neither worked, return false
return false;
}

/**
* @brief Tests to determine if the Desktop Duplication API can capture the given output.
* @details When testing for enumeration only, we avoid resyncing the thread desktop.
Expand Down Expand Up @@ -510,6 +413,27 @@ namespace platf::dxgi {
return false;
}

/**
* @brief Hook for NtGdiDdDDIGetCachedHybridQueryValue() from win32u.dll.
* @param gpuPreference A pointer to the location where the preference will be written.
* @return Always STATUS_SUCCESS if valid arguments are provided.
*/
NTSTATUS
__stdcall NtGdiDdDDIGetCachedHybridQueryValueHook(D3DKMT_GPU_PREFERENCE_QUERY_STATE *gpuPreference) {
// By faking a cached GPU preference state of D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED, this will
// prevent DXGI from performing the normal GPU preference resolution that looks at the registry,
// power settings, and the hybrid adapter DDI interface to pick a GPU. Instead, we will not be
// bound to any specific GPU. This will prevent DXGI from performing output reparenting (moving
// outputs from their true location to the render GPU), which breaks DDA.
if (gpuPreference) {
*gpuPreference = D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED;
return 0; // STATUS_SUCCESS
}
else {
return STATUS_INVALID_PARAMETER;
}
}

int
display_base_t::init(const ::video::config_t &config, const std::string &display_name) {
std::once_flag windows_cpp_once_flag;
Expand All @@ -519,13 +443,22 @@ namespace platf::dxgi {

typedef BOOL (*User32_SetProcessDpiAwarenessContext)(DPI_AWARENESS_CONTEXT value);

auto user32 = LoadLibraryA("user32.dll");
auto f = (User32_SetProcessDpiAwarenessContext) GetProcAddress(user32, "SetProcessDpiAwarenessContext");
if (f) {
f(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
{
auto user32 = LoadLibraryA("user32.dll");
auto f = (User32_SetProcessDpiAwarenessContext) GetProcAddress(user32, "SetProcessDpiAwarenessContext");
if (f) {
f(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
}

FreeLibrary(user32);
}

FreeLibrary(user32);
{
// We aren't calling MH_Uninitialize(), but that's okay because this hook lasts for the life of the process
MH_Initialize();
MH_CreateHookApi(L"win32u.dll", "NtGdiDdDDIGetCachedHybridQueryValue", (void *) NtGdiDdDDIGetCachedHybridQueryValueHook, nullptr);
MH_EnableHook(MH_ALL_HOOKS);
}
});

// Get rectangle of full desktop for absolute mouse coordinates
Expand All @@ -534,11 +467,6 @@ namespace platf::dxgi {

HRESULT status;

// We must set the GPU preference before calling any DXGI APIs!
if (!probe_for_gpu_preference(display_name)) {
BOOST_LOG(warning) << "Failed to set GPU preference. Capture may not work!"sv;
}

status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory);
if (FAILED(status)) {
BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']';
Expand Down Expand Up @@ -1105,12 +1033,6 @@ namespace platf {

BOOST_LOG(debug) << "Detecting monitors..."sv;

// We must set the GPU preference before calling any DXGI APIs!
const auto output_name { display_device::map_output_name(config::video.output_name) };
if (!dxgi::probe_for_gpu_preference(output_name)) {
BOOST_LOG(warning) << "Failed to set GPU preference. Capture may not work!"sv;
}

// We sync the thread desktop once before we start the enumeration process
// to ensure test_dxgi_duplication() returns consistent results for all GPUs
// even if the current desktop changes during our enumeration process.
Expand Down
1 change: 0 additions & 1 deletion src_assets/common/assets/web/config.html
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,6 @@ <h1 class="my-4">{{ $t('config.configuration') }}</h1>
"virtual_sink": "",
"install_steam_audio_drivers": "enabled",
"adapter_name": "",
"gpu_preference": -1,
"output_name": "",
"dd_configuration_option": "verify_only",
"dd_resolution_option": "auto",
Expand Down
12 changes: 0 additions & 12 deletions src_assets/common/assets/web/configs/tabs/AudioVideo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,6 @@ const config = ref(props.config)
:config="config"
/>

<PlatformLayout :platform="platform">
<template #windows>
<!-- GPU Preference -->
<div class="mb-3">
<label for="gpu_preference" class="form-label">{{ $t('config.gpu_preference') }}</label>
<input type="number" class="form-control" id="gpu_preference" placeholder="-1" min="-1"
v-model="config.gpu_preference" />
<div class="form-text">{{ $t('config.gpu_preference_desc') }}</div>
</div>
</template>
</PlatformLayout>

<DisplayOutputSelector
:platform="platform"
:config="config"
Expand Down
2 changes: 0 additions & 2 deletions src_assets/common/assets/web/public/assets/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,6 @@
"fec_percentage": "FEC Percentage",
"fec_percentage_desc": "Percentage of error correcting packets per data packet in each video frame. Higher values can correct for more network packet loss, but at the cost of increasing bandwidth usage.",
"ffmpeg_auto": "auto -- let ffmpeg decide (default)",
"gpu_preference": "GPU Preference",
"gpu_preference_desc": "Specify the GPU preference for the Sunshine process. If set to negative number (-1 by default), Sunshine will try to detect the best GPU for the streamed display, but if it fails you will get a black screen. Setting it to 0 will allow Windows to try and select the best GPU. Setting it to 1 and above will prioritize the GPU that matches this number (the number has to be guessed, but it starts at 1 and increases).",
"file_apps": "Apps File",
"file_apps_desc": "The file where current apps of Sunshine are stored.",
"file_state": "State File",
Expand Down
9 changes: 0 additions & 9 deletions tools/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,3 @@ target_link_libraries(sunshinesvc
wtsapi32
${PLATFORM_LIBRARIES})
target_compile_options(sunshinesvc PRIVATE ${SUNSHINE_COMPILE_OPTIONS})

add_executable(ddprobe ddprobe.cpp)
set_target_properties(ddprobe PROPERTIES CXX_STANDARD 20)
target_link_libraries(ddprobe
${CMAKE_THREAD_LIBS_INIT}
dxgi
d3d11
${PLATFORM_LIBRARIES})
target_compile_options(ddprobe PRIVATE ${SUNSHINE_COMPILE_OPTIONS})
Loading

0 comments on commit 8392bdc

Please sign in to comment.