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.
1416typedef 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,111 +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- if (set_gpu_preference_on_self (i)) {
390- return true ;
391- }
392- else {
393- return false ;
394- }
395- }
396- }
397-
398- // If no valid configuration was found, return false
399- return false ;
400- }
401-
402- // On hybrid graphics systems, Windows will change the order of GPUs reported by
403- // DXGI in accordance with the user's GPU preference. If the selected GPU is a
404- // render-only device with no displays, DXGI will add virtual outputs to the
405- // that device to avoid confusing applications. While this works properly for most
406- // applications, it breaks the Desktop Duplication API because DXGI doesn't proxy
407- // the virtual DXGIOutput to the real GPU it is attached to. When trying to call
408- // DuplicateOutput() on one of these virtual outputs, it fails with DXGI_ERROR_UNSUPPORTED
409- // (even if you try sneaky stuff like passing the ID3D11Device for the iGPU and the
410- // virtual DXGIOutput from the dGPU). Because the GPU preference is once-per-process,
411- // we spawn a helper tool to probe for us before we set our own GPU preference.
412- bool
413- probe_for_gpu_preference (const std::string &display_name) {
414- static bool set_gpu_preference = false ;
415-
416- // If we've already been through here, there's nothing to do this time.
417- if (set_gpu_preference) {
418- return true ;
419- }
420-
421- // Try probing with different GPU preferences and verify_frame_capture flag
422- if (validate_and_test_gpu_preference (display_name, true )) {
423- set_gpu_preference = true ;
424- return true ;
425- }
426-
427- // If no valid configuration was found, try again with verify_frame_capture == false
428- if (validate_and_test_gpu_preference (display_name, false )) {
429- set_gpu_preference = true ;
430- return true ;
431- }
432-
433- // If neither worked, return false
434- return false ;
435- }
436-
437344 /* *
438345 * @brief Tests to determine if the Desktop Duplication API can capture the given output.
439346 * @details When testing for enumeration only, we avoid resyncing the thread desktop.
@@ -506,6 +413,27 @@ namespace platf::dxgi {
506413 return false ;
507414 }
508415
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+
509437 int
510438 display_base_t::init (const ::video::config_t &config, const std::string &display_name) {
511439 std::once_flag windows_cpp_once_flag;
@@ -515,13 +443,22 @@ namespace platf::dxgi {
515443
516444 typedef BOOL (*User32_SetProcessDpiAwarenessContext)(DPI_AWARENESS_CONTEXT value);
517445
518- auto user32 = LoadLibraryA (" user32.dll" );
519- auto f = (User32_SetProcessDpiAwarenessContext) GetProcAddress (user32, " SetProcessDpiAwarenessContext" );
520- if (f) {
521- 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);
522454 }
523455
524- 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+ }
525462 });
526463
527464 // Get rectangle of full desktop for absolute mouse coordinates
@@ -530,11 +467,6 @@ namespace platf::dxgi {
530467
531468 HRESULT status;
532469
533- // We must set the GPU preference before calling any DXGI APIs!
534- if (!probe_for_gpu_preference (display_name)) {
535- BOOST_LOG (warning) << " Failed to set GPU preference. Capture may not work!" sv;
536- }
537-
538470 status = CreateDXGIFactory1 (IID_IDXGIFactory1, (void **) &factory);
539471 if (FAILED (status)) {
540472 BOOST_LOG (error) << " Failed to create DXGIFactory1 [0x" sv << util::hex (status).to_string_view () << ' ]' ;
@@ -1101,12 +1033,6 @@ namespace platf {
11011033
11021034 BOOST_LOG (debug) << " Detecting monitors..." sv;
11031035
1104- // We must set the GPU preference before calling any DXGI APIs!
1105- const auto output_name { display_device::map_output_name (config::video.output_name ) };
1106- if (!dxgi::probe_for_gpu_preference (output_name)) {
1107- BOOST_LOG (warning) << " Failed to set GPU preference. Capture may not work!" sv;
1108- }
1109-
11101036 // We sync the thread desktop once before we start the enumeration process
11111037 // to ensure test_dxgi_duplication() returns consistent results for all GPUs
11121038 // even if the current desktop changes during our enumeration process.
0 commit comments