Skip to content

Commit 8392bdc

Browse files
authored
feat(capture/windows): hook APIs to avoid output reparenting that breaks 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
1 parent c369e8e commit 8392bdc

File tree

14 files changed

+51
-519
lines changed

14 files changed

+51
-519
lines changed

.codeql-prebuild-cpp-Windows.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ dependencies=(
1111
"mingw-w64-ucrt-x86_64-cmake"
1212
"mingw-w64-ucrt-x86_64-cppwinrt"
1313
"mingw-w64-ucrt-x86_64-curl-winssl"
14+
"mingw-w64-ucrt-x86_64-MinHook"
1415
"mingw-w64-ucrt-x86_64-miniupnpc"
1516
"mingw-w64-ucrt-x86_64-nlohmann-json"
1617
"mingw-w64-ucrt-x86_64-nodejs"

.github/workflows/CI.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,7 @@ jobs:
885885
mingw-w64-ucrt-x86_64-cppwinrt
886886
mingw-w64-ucrt-x86_64-curl-winssl
887887
mingw-w64-ucrt-x86_64-graphviz
888+
mingw-w64-ucrt-x86_64-MinHook
888889
mingw-w64-ucrt-x86_64-miniupnpc
889890
mingw-w64-ucrt-x86_64-nlohmann-json
890891
mingw-w64-ucrt-x86_64-nodejs

cmake/dependencies/common.cmake

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ include_directories(SYSTEM ${MINIUPNP_INCLUDE_DIRS})
2828
# ffmpeg pre-compiled binaries
2929
if(NOT DEFINED FFMPEG_PREPARED_BINARIES)
3030
if(WIN32)
31-
set(FFMPEG_PLATFORM_LIBRARIES mfplat ole32 strmiids mfuuid vpl)
31+
set(FFMPEG_PLATFORM_LIBRARIES mfplat ole32 strmiids mfuuid vpl MinHook)
3232
elseif(UNIX AND NOT APPLE)
3333
set(FFMPEG_PLATFORM_LIBRARIES numa va va-drm va-x11 X11)
3434
endif()

cmake/packaging/windows.cmake

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ install(TARGETS dxgi-info RUNTIME DESTINATION "tools" COMPONENT dxgi)
1111
install(TARGETS audio-info RUNTIME DESTINATION "tools" COMPONENT audio)
1212

1313
# Mandatory tools
14-
install(TARGETS ddprobe RUNTIME DESTINATION "tools" COMPONENT application)
1514
install(TARGETS sunshinesvc RUNTIME DESTINATION "tools" COMPONENT application)
1615

1716
# Mandatory scripts

docs/building.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ dependencies=(
9090
"mingw-w64-ucrt-x86_64-curl-winssl"
9191
"mingw-w64-ucrt-x86_64-doxygen" # Optional, for docs... better to install official Doxygen
9292
"mingw-w64-ucrt-x86_64-graphviz" # Optional, for docs
93+
"mingw-w64-ucrt-x86_64-MinHook"
9394
"mingw-w64-ucrt-x86_64-miniupnpc"
9495
"mingw-w64-ucrt-x86_64-nlohmann-json"
9596
"mingw-w64-ucrt-x86_64-nodejs"

docs/configuration.md

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -848,37 +848,6 @@ editing the `conf` file in a text editor. Use the examples as reference.
848848
</tr>
849849
</table>
850850

851-
### gpu_preference
852-
853-
<table>
854-
<tr>
855-
<td>Description</td>
856-
<td colspan="2">
857-
Specify the GPU preference for the Sunshine process.
858-
<br>
859-
<br>
860-
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.
861-
<br>
862-
Setting it to 0 will allow Windows to try and select the best GPU.
863-
<br>
864-
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).
865-
@note{Applies to Windows only.}
866-
</td>
867-
</tr>
868-
<tr>
869-
<td>Default</td>
870-
<td colspan="2">@code{}
871-
-1
872-
@endcode</td>
873-
</tr>
874-
<tr>
875-
<td>Example</td>
876-
<td colspan="2">@code{}
877-
2
878-
@endcode</td>
879-
</tr>
880-
</table>
881-
882851
### output_name
883852

884853
<table>

src/config.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,6 @@ namespace config {
468468
{}, // capture
469469
{}, // encoder
470470
{}, // adapter_name
471-
-1, // gpu_preference
472471
{}, // output_name
473472

474473
{
@@ -1122,7 +1121,6 @@ namespace config {
11221121
string_f(vars, "capture", video.capture);
11231122
string_f(vars, "encoder", video.encoder);
11241123
string_f(vars, "adapter_name", video.adapter_name);
1125-
int_f(vars, "gpu_preference", video.gpu_preference);
11261124
string_f(vars, "output_name", video.output_name);
11271125

11281126
generic_f(vars, "dd_configuration_option", video.dd.configuration_option, dd::config_option_from_view);

src/config.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ namespace config {
7777
std::string capture;
7878
std::string encoder;
7979
std::string adapter_name;
80-
int gpu_preference;
8180
std::string output_name;
8281

8382
struct dd_t {

src/platform/windows/display_base.cpp

Lines changed: 47 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,22 @@
99
#include <boost/algorithm/string/join.hpp>
1010
#include <boost/process/v1.hpp>
1111

12+
#include <MinHook.h>
13+
1214
// We have to include boost/process/v1.hpp before display.h due to WinSock.h,
1315
// but that prevents the definition of NTSTATUS so we must define it ourself.
1416
typedef long NTSTATUS;
1517

18+
// Definition from the WDK's d3dkmthk.h
19+
typedef enum _D3DKMT_GPU_PREFERENCE_QUERY_STATE: DWORD {
20+
D3DKMT_GPU_PREFERENCE_STATE_UNINITIALIZED, ///< The GPU preference isn't initialized.
21+
D3DKMT_GPU_PREFERENCE_STATE_HIGH_PERFORMANCE, ///< The highest performing GPU is preferred.
22+
D3DKMT_GPU_PREFERENCE_STATE_MINIMUM_POWER, ///< The minimum-powered GPU is preferred.
23+
D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED, ///< A GPU preference isn't specified.
24+
D3DKMT_GPU_PREFERENCE_STATE_NOT_FOUND, ///< A GPU preference isn't found.
25+
D3DKMT_GPU_PREFERENCE_STATE_USER_SPECIFIED_GPU ///< A specific GPU is preferred.
26+
} D3DKMT_GPU_PREFERENCE_QUERY_STATE;
27+
1628
#include "display.h"
1729
#include "misc.h"
1830
#include "src/config.h"
@@ -329,115 +341,6 @@ namespace platf::dxgi {
329341
return capture_e::ok;
330342
}
331343

332-
bool
333-
set_gpu_preference_on_self(int preference) {
334-
// The GPU preferences key uses app path as the value name.
335-
WCHAR sunshine_path[MAX_PATH];
336-
GetModuleFileNameW(NULL, sunshine_path, ARRAYSIZE(sunshine_path));
337-
338-
WCHAR value_data[128];
339-
swprintf_s(value_data, L"GpuPreference=%d;", preference);
340-
341-
auto status = RegSetKeyValueW(HKEY_CURRENT_USER,
342-
L"Software\\Microsoft\\DirectX\\UserGpuPreferences",
343-
sunshine_path,
344-
REG_SZ,
345-
value_data,
346-
(wcslen(value_data) + 1) * sizeof(WCHAR));
347-
if (status != ERROR_SUCCESS) {
348-
BOOST_LOG(error) << "Failed to set GPU preference: "sv << status;
349-
return false;
350-
}
351-
352-
BOOST_LOG(info) << "Set GPU preference: "sv << preference;
353-
return true;
354-
}
355-
356-
bool
357-
validate_and_test_gpu_preference(const std::string &display_name, bool verify_frame_capture) {
358-
std::string cmd = "tools\\ddprobe.exe";
359-
360-
// We start at 1 because 0 is automatic selection which can be overridden by
361-
// the GPU driver control panel options. Since ddprobe.exe can have different
362-
// GPU driver overrides than Sunshine.exe, we want to avoid a scenario where
363-
// autoselection might work for ddprobe.exe but not for us.
364-
for (int i = 1; i < 5; i++) {
365-
// Run the probe tool. It returns the status of DuplicateOutput().
366-
//
367-
// Arg format: [GPU preference] [Display name] [--verify-frame-capture]
368-
HRESULT result;
369-
std::vector<std::string> args = { std::to_string(i), display_name };
370-
try {
371-
if (verify_frame_capture) {
372-
args.emplace_back("--verify-frame-capture");
373-
}
374-
result = bp::system(cmd, bp::args(args), bp::std_out > bp::null, bp::std_err > bp::null);
375-
}
376-
catch (bp::process_error &e) {
377-
BOOST_LOG(error) << "Failed to start ddprobe.exe: "sv << e.what();
378-
return false;
379-
}
380-
381-
BOOST_LOG(info) << "ddprobe.exe " << boost::algorithm::join(args, " ") << " returned 0x"
382-
<< util::hex(result).to_string_view();
383-
384-
// E_ACCESSDENIED can happen at the login screen. If we get this error,
385-
// we know capture would have been supported, because DXGI_ERROR_UNSUPPORTED
386-
// would have been raised first if it wasn't.
387-
if (result == S_OK || result == E_ACCESSDENIED) {
388-
// We found a working GPU preference, so set ourselves to use that.
389-
return set_gpu_preference_on_self(i);
390-
}
391-
}
392-
393-
// If no valid configuration was found, return false
394-
return false;
395-
}
396-
397-
// On hybrid graphics systems, Windows will change the order of GPUs reported by
398-
// DXGI in accordance with the user's GPU preference. If the selected GPU is a
399-
// render-only device with no displays, DXGI will add virtual outputs to the
400-
// that device to avoid confusing applications. While this works properly for most
401-
// applications, it breaks the Desktop Duplication API because DXGI doesn't proxy
402-
// the virtual DXGIOutput to the real GPU it is attached to. When trying to call
403-
// DuplicateOutput() on one of these virtual outputs, it fails with DXGI_ERROR_UNSUPPORTED
404-
// (even if you try sneaky stuff like passing the ID3D11Device for the iGPU and the
405-
// virtual DXGIOutput from the dGPU). Because the GPU preference is once-per-process,
406-
// we spawn a helper tool to probe for us before we set our own GPU preference.
407-
bool
408-
probe_for_gpu_preference(const std::string &display_name) {
409-
static bool set_gpu_preference = false;
410-
411-
// If we've already been through here, there's nothing to do this time.
412-
if (set_gpu_preference) {
413-
return true;
414-
}
415-
416-
// If the GPU preference was manually specified, we can skip the probe.
417-
if (config::video.gpu_preference >= 0) {
418-
if (set_gpu_preference_on_self(config::video.gpu_preference)) {
419-
set_gpu_preference = true;
420-
return true;
421-
}
422-
}
423-
else {
424-
// Try probing with different GPU preferences and verify_frame_capture flag
425-
if (validate_and_test_gpu_preference(display_name, true)) {
426-
set_gpu_preference = true;
427-
return true;
428-
}
429-
430-
// If no valid configuration was found, try again with verify_frame_capture == false
431-
if (validate_and_test_gpu_preference(display_name, false)) {
432-
set_gpu_preference = true;
433-
return true;
434-
}
435-
}
436-
437-
// If neither worked, return false
438-
return false;
439-
}
440-
441344
/**
442345
* @brief Tests to determine if the Desktop Duplication API can capture the given output.
443346
* @details When testing for enumeration only, we avoid resyncing the thread desktop.
@@ -510,6 +413,27 @@ namespace platf::dxgi {
510413
return false;
511414
}
512415

416+
/**
417+
* @brief Hook for NtGdiDdDDIGetCachedHybridQueryValue() from win32u.dll.
418+
* @param gpuPreference A pointer to the location where the preference will be written.
419+
* @return Always STATUS_SUCCESS if valid arguments are provided.
420+
*/
421+
NTSTATUS
422+
__stdcall NtGdiDdDDIGetCachedHybridQueryValueHook(D3DKMT_GPU_PREFERENCE_QUERY_STATE *gpuPreference) {
423+
// By faking a cached GPU preference state of D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED, this will
424+
// prevent DXGI from performing the normal GPU preference resolution that looks at the registry,
425+
// power settings, and the hybrid adapter DDI interface to pick a GPU. Instead, we will not be
426+
// bound to any specific GPU. This will prevent DXGI from performing output reparenting (moving
427+
// outputs from their true location to the render GPU), which breaks DDA.
428+
if (gpuPreference) {
429+
*gpuPreference = D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED;
430+
return 0; // STATUS_SUCCESS
431+
}
432+
else {
433+
return STATUS_INVALID_PARAMETER;
434+
}
435+
}
436+
513437
int
514438
display_base_t::init(const ::video::config_t &config, const std::string &display_name) {
515439
std::once_flag windows_cpp_once_flag;
@@ -519,13 +443,22 @@ namespace platf::dxgi {
519443

520444
typedef BOOL (*User32_SetProcessDpiAwarenessContext)(DPI_AWARENESS_CONTEXT value);
521445

522-
auto user32 = LoadLibraryA("user32.dll");
523-
auto f = (User32_SetProcessDpiAwarenessContext) GetProcAddress(user32, "SetProcessDpiAwarenessContext");
524-
if (f) {
525-
f(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
446+
{
447+
auto user32 = LoadLibraryA("user32.dll");
448+
auto f = (User32_SetProcessDpiAwarenessContext) GetProcAddress(user32, "SetProcessDpiAwarenessContext");
449+
if (f) {
450+
f(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
451+
}
452+
453+
FreeLibrary(user32);
526454
}
527455

528-
FreeLibrary(user32);
456+
{
457+
// We aren't calling MH_Uninitialize(), but that's okay because this hook lasts for the life of the process
458+
MH_Initialize();
459+
MH_CreateHookApi(L"win32u.dll", "NtGdiDdDDIGetCachedHybridQueryValue", (void *) NtGdiDdDDIGetCachedHybridQueryValueHook, nullptr);
460+
MH_EnableHook(MH_ALL_HOOKS);
461+
}
529462
});
530463

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

535468
HRESULT status;
536469

537-
// We must set the GPU preference before calling any DXGI APIs!
538-
if (!probe_for_gpu_preference(display_name)) {
539-
BOOST_LOG(warning) << "Failed to set GPU preference. Capture may not work!"sv;
540-
}
541-
542470
status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory);
543471
if (FAILED(status)) {
544472
BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']';
@@ -1105,12 +1033,6 @@ namespace platf {
11051033

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

1108-
// We must set the GPU preference before calling any DXGI APIs!
1109-
const auto output_name { display_device::map_output_name(config::video.output_name) };
1110-
if (!dxgi::probe_for_gpu_preference(output_name)) {
1111-
BOOST_LOG(warning) << "Failed to set GPU preference. Capture may not work!"sv;
1112-
}
1113-
11141036
// We sync the thread desktop once before we start the enumeration process
11151037
// to ensure test_dxgi_duplication() returns consistent results for all GPUs
11161038
// even if the current desktop changes during our enumeration process.

src_assets/common/assets/web/config.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,6 @@ <h1 class="my-4">{{ $t('config.configuration') }}</h1>
167167
"virtual_sink": "",
168168
"install_steam_audio_drivers": "enabled",
169169
"adapter_name": "",
170-
"gpu_preference": -1,
171170
"output_name": "",
172171
"dd_configuration_option": "verify_only",
173172
"dd_resolution_option": "auto",

0 commit comments

Comments
 (0)