Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

缩放时模拟D3D独占全屏 #245

Closed
codehz opened this issue Dec 17, 2021 · 14 comments
Closed

缩放时模拟D3D独占全屏 #245

codehz opened this issue Dec 17, 2021 · 14 comments
Labels
enhancement New feature or request

Comments

@codehz
Copy link

codehz commented Dec 17, 2021

Expected behavior 预期的功能

游戏模式可以影响其他app的行为,例如不推送通知或者隐藏某些悬浮窗。。
看起来现在检测游戏模式是通过 SHQueryUserNotificationState 获取结果是否等于 QUNS_RUNNING_D3D_FULL_SCREEN 来判断的(例如 https://github.com/microsoft/PowerToys/blob/main/src/common/utils/game_mode.h ),但是这要求必须有一个全屏独占窗口,显然Magpie现在是没法满足这个条件的,不确定是否有其他的实现途径(

Alternative behavior (optional) 近似的功能(可选)

No response

@codehz codehz added the enhancement New feature or request label Dec 17, 2021
@Blinue
Copy link
Owner

Blinue commented Dec 18, 2021

SHQueryUserNotificationState 只能检测独占式全屏,检测它的原因是系统无法在独占全屏和正常桌面间快速切换,但magpie没有这个问题。

一些用户可能在全屏时不希望被打扰,这个没有好的解决办法,独占全屏应该没法模拟

@codehz
Copy link
Author

codehz commented Dec 19, 2021

一些用户可能在全屏时不希望被打扰,这个没有好的解决办法,独占全屏应该没法模拟

如果游戏模式确实无法达成,那是否可以自动启动专注模式?(以及退出)
虽然没有公开 api ,但是有这个 https://github.com/stefnotch/dnd 通过 ZwUpdateWnfStateData 切换专注模式

@Blinue
Copy link
Owner

Blinue commented Dec 19, 2021

我会尝试一下,不过专注模式只能影响系统通知,其他应用不遵守也没办法

@codehz
Copy link
Author

codehz commented Dec 19, 2021

一些用户可能在全屏时不希望被打扰,这个没有好的解决办法,独占全屏应该没法模拟

研究了一下 QueryUserNotificationState 的实现方式,发现它最终是调用 IsDirectXAppRunningFullScreen 来测试是否独占全屏,而这个函数的实现非常简单,就是尝试获取 Local\__DDrawExclMode__ 的 Mutex,如果失败(WAIT_TIMEOUT),就证明有独占全屏

也就是说,只需要 OpenMutexA(SYNCHRONIZE, FALSE, "__DDrawExclMode__"); 并在全屏期间使用 WaitForSingleObject 占用这个 mutex ,即可确保 QueryUserNotificationState 得到 QUNS_RUNNING_D3D_FULL_SCREEN , 可以接受的副作用是其他独占全屏的程序无法启动

(但是并不影响通知推送,因此可以结合上述专注模式来避免通知)

@Blinue
Copy link
Owner

Blinue commented Dec 19, 2021

mpv 曾经做过这方面的 hack mpv-player/mpv@3d8ca93

这种方法很脆弱,但也有尝试的价值

@codehz
Copy link
Author

codehz commented Dec 19, 2021

mpv 曾经做过这方面的 hack mpv-player/mpv@3d8ca93

这种方法很脆弱,但也有尝试的价值

mpv的目的是不太一样的,这里显然只是为了让其他(会检测状态的)app认为有全屏游戏在运行,从而禁用一些功能,而mpv是为了检测屏幕混和然后使用不同的渲染策略。目的上就有所不同

@Blinue Blinue mentioned this issue Dec 23, 2021
10 tasks
@Blinue
Copy link
Owner

Blinue commented Dec 25, 2021

尝试了 ZwUpdateWnfStateData(和 dnd),在 win11 中没有作用

更新:最后成功调用了这个 API,调用之后任务栏右侧出现了月亮图标,表示已经进入专注模式,但设置里没有变,且经过测试无法屏蔽通知。

获取 __DDrawExclMode__ 后确实可以使 SHQueryUserNotificationState 返回 QUNS_RUNNING_D3D_FULL_SCREEN,但也没法屏蔽系统通知,可能对传统 Win32 有用

@Blinue
Copy link
Owner

Blinue commented Dec 25, 2021

这些是我实验用的代码,如果你要完善这个功能,希望开一个 pull request

FocusModeHack.h

#pragma once
#include "pch.h"
#include "Utils.h"

class FocusModeHack {
public:
	FocusModeHack();

	~FocusModeHack();

private:
	Utils::ScopedHandle _exclModeMutex;
};

FocusModeHack.cpp

#include "pch.h"
#include "FocusModeHack.h"
#include <winternl.h>
#include <shellapi.h>

typedef struct _WNF_STATE_NAME {
	ULONG Data[2];
} WNF_STATE_NAME;

typedef struct _WNF_STATE_NAME* PWNF_STATE_NAME;
typedef const struct _WNF_STATE_NAME* PCWNF_STATE_NAME;

typedef struct _WNF_TYPE_ID {
	GUID TypeId;
} WNF_TYPE_ID, * PWNF_TYPE_ID;

typedef const WNF_TYPE_ID* PCWNF_TYPE_ID;

typedef ULONG WNF_CHANGE_STAMP, * PWNF_CHANGE_STAMP;

enum class FocusAssistResult {
	not_supported = -2,
	failed = -1,
	off = 0,
	priority_only = 1,
	alarms_only = 2
};

typedef NTSTATUS(NTAPI* PNTQUERYWNFSTATEDATA)(
	_In_ PWNF_STATE_NAME StateName,
	_In_opt_ PWNF_TYPE_ID TypeId,
	_In_opt_ const VOID* ExplicitScope,
	_Out_ PWNF_CHANGE_STAMP ChangeStamp,
	_Out_writes_bytes_to_opt_(*BufferSize, *BufferSize) PVOID Buffer,
	_Inout_ PULONG BufferSize);

typedef NTSTATUS (NTAPI* ZwUpdateWnfStateData)(
	_In_ PCWNF_STATE_NAME StateName,
	_In_reads_bytes_opt_(Length) const VOID* Buffer,
	_In_opt_ ULONG Length,
	_In_opt_ PCWNF_TYPE_ID TypeId,
	_In_opt_ const PVOID ExplicitScope,
	_In_ WNF_CHANGE_STAMP MatchingChangeStamp,
	_In_ BOOL CheckStamp
);


// 模拟 D3D 独占全屏模式,以起到免打扰的效果
// SHQueryUserNotificationState 通常被用来检测是否有 D3D 游戏独占全屏,以确定是否应该向用户推送通知/弹窗
// 此函数内部使用名为 __DDrawExclMode__ 的 mutex 检测独占全屏,因此这里直接获取该 mutex 以模拟独占全屏
FocusModeHack::FocusModeHack() {
	// note: ntdll is guaranteed to be in the process address space.
	const auto h_ntdll = GetModuleHandle(L"ntdll");

	// get pointer to function
	const auto pNtQueryWnfStateData = PNTQUERYWNFSTATEDATA(GetProcAddress(h_ntdll, "NtQueryWnfStateData"));
	if (!pNtQueryWnfStateData) {
		OutputDebugString(L"fail");
		return ;
	}

	const auto pNtUpdateWnfStateData = ZwUpdateWnfStateData(GetProcAddress(h_ntdll, "NtUpdateWnfStateData"));
	if (!pNtUpdateWnfStateData) {
		OutputDebugString(L"fail");
		return;
	}

	// state name for active hours (Focus Assist)
	WNF_STATE_NAME WNF_SHEL_QUIETHOURS_ACTIVE_PROFILE_CHANGED{ 0xA3BF1C75, 0xD83063E };
	
	BYTE b[4] = { 0x02, 0x00, 0x00, 0x00 };
	if (NT_SUCCESS(pNtUpdateWnfStateData(
		&WNF_SHEL_QUIETHOURS_ACTIVE_PROFILE_CHANGED,
		b,
		4,
		0,
		0,
		0,
		0
	))) {
		OutputDebugString(L"fail");
	}

	// note: we won't use it but it's required
	WNF_CHANGE_STAMP change_stamp = { 0 };

	// on output buffer will tell us the status of Focus Assist
	DWORD buffer = 0;
	ULONG buffer_size = sizeof(buffer);
	
	if (NT_SUCCESS(pNtQueryWnfStateData(&WNF_SHEL_QUIETHOURS_ACTIVE_PROFILE_CHANGED, nullptr, nullptr, &change_stamp,
		&buffer, &buffer_size))) {
		if (buffer != (DWORD)FocusAssistResult::alarms_only) {
			OutputDebugString(L"fail");
		}
	}

	_exclModeMutex.reset(Utils::SafeHandle(
		OpenMutex(SYNCHRONIZE, FALSE,L"__DDrawExclMode__")));
	if (!_exclModeMutex) {
		SPDLOG_LOGGER_ERROR(logger, MakeWin32ErrorMsg("OpenMutex 失败"));
		return;
	}

	QUERY_USER_NOTIFICATION_STATE state;
	HRESULT hr = SHQueryUserNotificationState(&state);
	if (FAILED(hr)) {
		SPDLOG_LOGGER_ERROR(logger, MakeComErrorMsg("SHQueryUserNotificationState 失败", hr));
		return;
	}
	if (state != QUNS_ACCEPTS_NOTIFICATIONS) {
		SPDLOG_LOGGER_INFO(logger, "已处于免打扰状态");
		return;
	}

	DWORD result = WaitForSingleObject(_exclModeMutex.get(), 0);
	if (result != WAIT_OBJECT_0) {
		SPDLOG_LOGGER_ERROR(logger, "获取 __DDrawExclMode__ 失败");
		_exclModeMutex.reset();
		return;
	}

	hr = SHQueryUserNotificationState(&state);
	if (FAILED(hr)) {
		SPDLOG_LOGGER_ERROR(logger, MakeComErrorMsg("SHQueryUserNotificationState 失败", hr));
		ReleaseMutex(_exclModeMutex.get());
		_exclModeMutex.reset();
		return;
	}
	if (state != QUNS_RUNNING_D3D_FULL_SCREEN) {
		SPDLOG_LOGGER_INFO(logger, "进入 D3D 全屏模式失败");
		ReleaseMutex(_exclModeMutex.get());
		_exclModeMutex.reset();
		return;
	}

	SPDLOG_LOGGER_INFO(logger, "已进入 D3D 全屏模式");
}

FocusModeHack::~FocusModeHack() {
	if (_exclModeMutex) {
		ReleaseMutex(_exclModeMutex.get());
	}
}

@codehz
Copy link
Author

codehz commented Dec 25, 2021

(嘛,其实一开始我就是就是想让PowerToys的那个双击ctrl聚焦鼠标的功能自己禁用掉而已(毕竟ctrl在某些游戏里还是挺重要的一个功能(
用dwrite full screen的方法已经奏效了

@Blinue
Copy link
Owner

Blinue commented Dec 26, 2021

在找到办法之前这个功能暂且搁置

@codehz
Copy link
Author

codehz commented Dec 27, 2021

在找到办法之前这个功能暂且搁置

刚看了一下,目前release版本在全屏的时候(os: win11 dev),已经会自动启动专注模式了(关闭动画的情况下能看到在全屏瞬间出现月亮图标,并且有观察到在退出全屏后专注助手给出的全屏期间的通知提示。)
因此专注模式的这个问题不需要解决了

只要用ddraw的那个workaround干掉 PowerToys 的双击ctrl聚焦鼠标功能即可(原始需求)

@Blinue
Copy link
Owner

Blinue commented Dec 27, 2021

目前release版本在全屏的时候(os: win11 dev),已经会自动启动专注模式了

确实如此,我居然一直没有发现!

这个行为可以在设置里更改
image

(不确定win10是否有这个功能)

@Blinue
Copy link
Owner

Blinue commented Dec 27, 2021

模拟独占全屏的功能我会添加一个选项

@Blinue
Copy link
Owner

Blinue commented Dec 27, 2021

已实现这个功能 9328748

@Blinue Blinue changed the title 全屏时启动游戏模式(不确定是否可行) 缩放时模拟D3D独占全屏 Dec 28, 2021
@Blinue Blinue closed this as completed Dec 28, 2021
@Blinue Blinue moved this to Done in Magpie Feb 7, 2023
@Blinue Blinue added this to Magpie Feb 7, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
Archived in project
Development

No branches or pull requests

2 participants