feat(win32): 添加鼠标锁定跟随模式,支持后台 TPS/FPS 视角控制#1232
Merged
MistEO merged 17 commits intoMaaXYZ:mainfrom Apr 2, 2026
Merged
Conversation
Contributor
There was a problem hiding this comment.
Hey - 我这边发现了 2 个问题,并给出了一些整体性的反馈:
- 有几个跨线程使用的状态标志(例如
mouse_lock_follow_active_、tracking_thread_started_for_lock_follow_以及rawinput_ensure_*这些布尔值)是普通的bool,但会在不同线程中读写(tracking 线程、action runner、析构函数);建议将它们改为std::atomic<bool>,或者在所有访问时统一使用tracking_state_mutex_进行保护,以避免数据竞争。 - tracking 线程现在在每次启动时都会调用
SetProcessDpiAwarenessContext/SetProcessDpiAwareness;这会在进程级别修改 DPI awareness,可能会让宿主应用措手不及——如果可以的话,建议把这部分集中为一次性的初始化(或者放到配置项 / 功能开关后面),而不是在每个控制器的 tracking 线程中都做一次。 MessageInput、tracking_thread_func和 RawInput 消息窗口之间的生命周期耦合依赖于 tracking 线程中的tracking_exit_加上destroy_rawinput_window();请仔细检查所有销毁路径(包括错误场景和inactive())最终是否都会在MessageInput被销毁之前 join tracking 线程,否则静态的RawInputWndProc可能会访问已经悬空的s_active_instance_指针。
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Several state flags used across threads (e.g., `mouse_lock_follow_active_`, `tracking_thread_started_for_lock_follow_`, and the `rawinput_ensure_*` booleans) are plain `bool` and read/written from different threads (tracking thread, action runner, destructor); consider making them `std::atomic<bool>` or consistently guarding all accesses with `tracking_state_mutex_` to avoid data races.
- The tracking thread now calls `SetProcessDpiAwarenessContext` / `SetProcessDpiAwareness` every time it starts; this alters DPI awareness process-wide and may surprise host applications—if possible, centralize this as a one-time initialization (or behind a config/feature flag) rather than doing it in a per-controller tracking thread.
- The lifetime coupling between `MessageInput`, `tracking_thread_func`, and the RawInput message window relies on `tracking_exit_` plus `destroy_rawinput_window()` in the tracking thread; double‑check that all destruction paths (including error cases and `inactive()`) ultimately join the tracking thread before `MessageInput` is destroyed, otherwise the static `RawInputWndProc` may see a dangling `s_active_instance_` target.
## Individual Comments
### Comment 1
<location path="source/MaaWin32ControlUnit/Input/MessageInput.cpp" line_range="491-496" />
<code_context>
+ // 如果失败可能是已经设置过(E_ACCESSDENIED),这是可以接受的
+ }
+ }
+ if (!ok) {
+ HMODULE shcore = LoadLibraryW(L"shcore.dll");
+ if (shcore) {
+ using FnAware = HRESULT(WINAPI*)(int);
+ auto fnAware = reinterpret_cast<FnAware>(GetProcAddress(shcore, "SetProcessDpiAwareness"));
+ if (fnAware) {
+ fnAware(2 /*PROCESS_PER_MONITOR_DPI_AWARE*/);
+ }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 考虑在使用完 SetProcessDpiAwareness 之后释放 shcore.dll 的模块句柄。
这里调用了 `LoadLibraryW(L"shcore.dll")`,但返回的 `HMODULE` 从未被释放。由于 `tracking_thread_func` 可能会运行多次,这会导致一个小且重复的资源泄漏问题。请在调用(或尝试调用)`SetProcessDpiAwareness` 之后增加 `FreeLibrary(shcore);`。
</issue_to_address>
### Comment 2
<location path="source/MaaWin32ControlUnit/Input/MessageInput.cpp" line_range="508-517" />
<code_context>
timeBeginPeriod(1);
- OnScopeLeave([this]() {
+ bool init_reported = false;
+ OnScopeLeave([&]() {
+ {
+ std::lock_guard lock(tracking_state_mutex_);
+ if (!init_reported) {
+ tracking_thread_init_done_ = true;
+ tracking_thread_init_ok_ = false;
+ }
+ rawinput_ensure_requested_ = false;
+ rawinput_ensure_done_ = true;
+ rawinput_ensure_ok_ = false;
+ }
+ tracking_state_cv_.notify_all();
close_target_process();
+ destroy_rawinput_window();
timeEndPeriod(1);
});
</code_context>
<issue_to_address>
**nitpick:** 在 scope guard 中按引用捕获会带来捕获过多变量的风险。
`tracking_thread_func` 里的 `OnScopeLeave([&] { ... })` 会按引用捕获所有局部变量,但实际上只需要 `this` 和 `init_reported`。请改为显式捕获列表(例如 `[this, &init_reported]`),以明确生命周期,并避免将来不小心按引用使用其他局部变量。
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据反馈改进后续的评审。
Original comment in English
Hey - I've found 2 issues, and left some high level feedback:
- Several state flags used across threads (e.g.,
mouse_lock_follow_active_,tracking_thread_started_for_lock_follow_, and therawinput_ensure_*booleans) are plainbooland read/written from different threads (tracking thread, action runner, destructor); consider making themstd::atomic<bool>or consistently guarding all accesses withtracking_state_mutex_to avoid data races. - The tracking thread now calls
SetProcessDpiAwarenessContext/SetProcessDpiAwarenessevery time it starts; this alters DPI awareness process-wide and may surprise host applications—if possible, centralize this as a one-time initialization (or behind a config/feature flag) rather than doing it in a per-controller tracking thread. - The lifetime coupling between
MessageInput,tracking_thread_func, and the RawInput message window relies ontracking_exit_plusdestroy_rawinput_window()in the tracking thread; double‑check that all destruction paths (including error cases andinactive()) ultimately join the tracking thread beforeMessageInputis destroyed, otherwise the staticRawInputWndProcmay see a danglings_active_instance_target.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Several state flags used across threads (e.g., `mouse_lock_follow_active_`, `tracking_thread_started_for_lock_follow_`, and the `rawinput_ensure_*` booleans) are plain `bool` and read/written from different threads (tracking thread, action runner, destructor); consider making them `std::atomic<bool>` or consistently guarding all accesses with `tracking_state_mutex_` to avoid data races.
- The tracking thread now calls `SetProcessDpiAwarenessContext` / `SetProcessDpiAwareness` every time it starts; this alters DPI awareness process-wide and may surprise host applications—if possible, centralize this as a one-time initialization (or behind a config/feature flag) rather than doing it in a per-controller tracking thread.
- The lifetime coupling between `MessageInput`, `tracking_thread_func`, and the RawInput message window relies on `tracking_exit_` plus `destroy_rawinput_window()` in the tracking thread; double‑check that all destruction paths (including error cases and `inactive()`) ultimately join the tracking thread before `MessageInput` is destroyed, otherwise the static `RawInputWndProc` may see a dangling `s_active_instance_` target.
## Individual Comments
### Comment 1
<location path="source/MaaWin32ControlUnit/Input/MessageInput.cpp" line_range="491-496" />
<code_context>
+ // 如果失败可能是已经设置过(E_ACCESSDENIED),这是可以接受的
+ }
+ }
+ if (!ok) {
+ HMODULE shcore = LoadLibraryW(L"shcore.dll");
+ if (shcore) {
+ using FnAware = HRESULT(WINAPI*)(int);
+ auto fnAware = reinterpret_cast<FnAware>(GetProcAddress(shcore, "SetProcessDpiAwareness"));
+ if (fnAware) {
+ fnAware(2 /*PROCESS_PER_MONITOR_DPI_AWARE*/);
+ }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider freeing the shcore.dll module handle after using SetProcessDpiAwareness.
`LoadLibraryW(L"shcore.dll")` is called here but the returned `HMODULE` is never released. Since `tracking_thread_func` may run multiple times, this can cause a small, repeated resource leak. Please call `FreeLibrary(shcore);` after `SetProcessDpiAwareness` has been invoked (or attempted).
</issue_to_address>
### Comment 2
<location path="source/MaaWin32ControlUnit/Input/MessageInput.cpp" line_range="508-517" />
<code_context>
timeBeginPeriod(1);
- OnScopeLeave([this]() {
+ bool init_reported = false;
+ OnScopeLeave([&]() {
+ {
+ std::lock_guard lock(tracking_state_mutex_);
+ if (!init_reported) {
+ tracking_thread_init_done_ = true;
+ tracking_thread_init_ok_ = false;
+ }
+ rawinput_ensure_requested_ = false;
+ rawinput_ensure_done_ = true;
+ rawinput_ensure_ok_ = false;
+ }
+ tracking_state_cv_.notify_all();
close_target_process();
+ destroy_rawinput_window();
timeEndPeriod(1);
});
</code_context>
<issue_to_address>
**nitpick:** Using a by-reference capture in the scope guard risks capturing more than necessary.
The `OnScopeLeave([&] { ... })` in `tracking_thread_func` captures all locals by reference, though only `this` and `init_reported` are needed. Please switch to an explicit capture list (e.g. `[this, &init_reported]`) to clarify lifetimes and avoid accidental future use of other locals by reference in this lambda.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Contributor
|
感觉接口过于特化了。如果真的需要做这个配置的话,考虑搞个通用的controller set attribute函数可能好点,或者直接复用现在的set option |
Member
|
ControlUnit 重构了下,有冲突了 #1235 |
Add a new action API (MaaControllerPostMouseLockFollow) to toggle a mode designed for TPS/FPS games that lock the mouse to their window in the background. When enabled, uses a three-layer architecture: 1. WH_MOUSE_LL hook: intercepts hardware mouse movement, accumulates processed delta for window/cursor synchronization 2. RawInput (WM_INPUT): receives raw device delta, sends counter-move via SendInput to prevent the game from sensing physical movement 3. Tracking thread (60fps): consumes accumulated delta, moves both window and cursor using absolute anchor positioning with NtSuspendProcess/NtResumeProcess for atomic updates Supports relative_move for intentional camera rotation (uses counter pending atomic to bypass the RawInput counter-move). Works with any MessageInput-based input method (PostMessage/SendMessage variants). Includes: C API, ControllerAgent action pipeline, Win32ControlUnitAPI interface, Python/NodeJS bindings, Agent client/server forwarding, and a standalone PoC (poc_mouse_lock_follow.cpp). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
At display scaling >100%, the window would continuously drift to the bottom-right corner due to coordinate system mismatch: - MSLLHOOKSTRUCT::pt always uses physical pixels - GetCursorPos/SetCursorPos behavior depends on DPI awareness context - SetThreadDpiAwarenessContext alone is unreliable in DLL contexts Fix with three approaches applied together: 1. Use GetPhysicalCursorPos in LL hook callback and activate function (always returns physical pixels regardless of DPI context) 2. Set process-level DPI awareness with 3-tier fallback in tracking thread (matching the validated PoC approach) 3. Temporarily set thread DPI awareness in activate/deactivate for SetWindowPos and other DPI-sensitive APIs Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
纯虚函数改为返回 false 的默认实现,避免不需要此功能的子类被迫实现空方法。 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
在析构函数和 deactivate_mouse_lock_follow 中使用 CAS 清除 s_active_instance_,确保钩子回调不会在对象销毁后访问悬空指针。 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
原先的单次 load + CAS 在 relative_move 并发递增 counter_pending_ 时 可能因值变化而失败,导致合成事件被误判为硬件事件并触发多余对冲。 改用 compare_exchange_weak 循环确保正确递减。 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
移除 tracking_thread_func 中的 SetProcessDpiAwarenessContext / SetProcessDpiAwareness / SetProcessDPIAware 三级回退,仅保留 SetThreadDpiAwarenessContext。进程级设置会影响宿主进程的所有线程, 线程级设置足以确保追踪线程的坐标系一致性。 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
SetWindowPos 后改为轮询检查窗口位置是否到位(最多 100ms), 在窗口快速响应时可提前退出,避免不必要的固定延迟。 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
tracking_exit_ 的重置已在 ensure_tracking_thread() 启动新线程前完成, deactivate 中 join 后再重置是多余的,且可能与析构函数中的 tracking_exit_ = true 产生竞态。 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
在中英文文档中补充说明 MaaControllerPostRelativeMove 在 Win32 平台 必须先开启 mouse-lock-follow 模式才能使用,否则调用将失败。 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…tation Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
仅使用线程级 SetThreadDpiAwarenessContext 在部分宿主进程中不可靠, 导致坐标系不匹配窗口飞到右下角。恢复进程级设置作为兜底,失败时静默跳过。 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…ostMouseLockFollow
b8f1b90 to
2e56336
Compare
Contributor
Author
|
现在这样怎么样 |
Contributor
Author
Contributor
|
这几天没空看( |
MistEO
reviewed
Mar 31, 2026
Align MessageInput with existing Win32 patterns so the mouse-lock-follow review fixes do not keep repeating process DPI initialization and remain easier to maintain. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概述
为 Win32 控制器添加鼠标锁定跟随模式(Mouse Lock Follow),专为 TPS/FPS 等在后台将鼠标锁定到窗口内的游戏设计。这是"实现终末地后台移动"计划的一部分,解决后台视角旋转的需求。
动机
终末地等 TPS 游戏在后台运行时会通过 RawInput 锁定鼠标到窗口中心,导致:
本 PR 通过"窗口跟随光标 + RawInput 对冲 + 合成事件标记"三管齐下,实现后台透明的视角控制。
实现方案
三层机制
1. LL Hook 拦截 + 窗口跟随(60fps 追踪线程)
WH_MOUSE_LL钩子拦截硬件鼠标移动,累加 pending deltaSetWindowPos移动窗口中心对齐光标SuspendProcess/ResumeProcess防止游戏在窗口移动期间竞争2. RawInput 对冲
HWND_MESSAGE隐藏窗口注册RIDEV_INPUTSINK鼠标 RawInputSendInput发送反向 delta 抵消3. 合成事件标记(
counter_pending_原子计数器)relative_move()调用前counter_pending_++DPI 感知
GetPhysicalCursorPos确保与MSLLHOOKSTRUCT::pt坐标系一致生命周期
s_active_instance_静态指针 + 安全清理防止 UAFAPI 变更
新增 C API:
新增 Python / Node.js 绑定:
post_mouse_lock_follow(enabled)行为变更:
relative_move()现在要求必须先开启 mouse-lock-follow 模式变更范围
MessageInput.cpp(+518 行)测试计划
relative_move()注入的 delta 不被对冲此 PR 是"实现终末地后台移动"计划的视角控制部分,与 feat/win32-background-managed-keys(后台托管按键)配合使用。
🤖 Generated with Claude Code
Summary by Sourcery
为后台 TPS/FPS 摄像机控制添加 Win32 鼠标锁定跟随模式,并通过控制器 API 和绑定将其暴露出来。
New Features:
MessageInput控制器中引入 Mouse Lock Follow 模式,以支持在鼠标锁定类游戏中基于后台参考系的鼠标移动和摄像机控制。relative_move操作中要求启用该模式。Enhancements:
Documentation:
MaaControllerPostRelativeMove之间的关系。Original summary in English
Summary by Sourcery
Add a Win32 mouse lock follow mode for background TPS/FPS camera control and expose it through the controller APIs and bindings.
New Features:
Enhancements:
Documentation: