diff --git a/windows/runner/mpv/mpv_player.cpp b/windows/runner/mpv/mpv_player.cpp index 09c9879f..05ce95fb 100644 --- a/windows/runner/mpv/mpv_player.cpp +++ b/windows/runner/mpv/mpv_player.cpp @@ -13,6 +13,9 @@ struct InnerWindowSubclassState { HWND hwnd = nullptr; std::atomic forward_target{nullptr}; std::atomic active{false}; + std::atomic forwarded_pointer_token{0}; + std::atomic forwarded_pointer_position{0}; + std::mutex forwarded_pointer_mutex; UINT_PTR subclass_id = 0; // Guarded by g_inner_subclasses_mutex. bool installed = false; @@ -67,27 +70,93 @@ flutter::EncodableValue NodeToEncodableValue(const mpv_node* node) { // and consumes input over the video (WS_EX_TRANSPARENT hit-test skipping is // same-thread-only, and disabling the subtree makes the system drop the input // entirely instead of routing it to a sibling). Use the common-controls -// subclass chain with per-window reference data, and forward mouse/pointer -// input to the Flutter view. Pointer messages must be sent synchronously: -// Flutter calls GetPointerInfo while handling them, and Windows only retains -// that data for the current or forwarded message. -constexpr bool IsFlutterPointerMessage(UINT message) { +// subclass chain with per-window reference data. Mouse messages can be posted +// directly, but WM_POINTER metadata remains associated with the receiving +// window thread, so translate the primary contact to mouse input before +// forwarding it to Flutter. +constexpr bool IsForwardedPointerMessage(UINT message) { switch (message) { case WM_POINTERDOWN: case WM_POINTERUPDATE: case WM_POINTERUP: case WM_POINTERLEAVE: + case WM_POINTERCAPTURECHANGED: return true; default: return false; } } -static_assert(IsFlutterPointerMessage(WM_POINTERDOWN)); -static_assert(IsFlutterPointerMessage(WM_POINTERUPDATE)); -static_assert(IsFlutterPointerMessage(WM_POINTERUP)); -static_assert(IsFlutterPointerMessage(WM_POINTERLEAVE)); -static_assert(!IsFlutterPointerMessage(WM_MOUSEMOVE)); +static_assert(IsForwardedPointerMessage(WM_POINTERDOWN)); +static_assert(IsForwardedPointerMessage(WM_POINTERUPDATE)); +static_assert(IsForwardedPointerMessage(WM_POINTERUP)); +static_assert(IsForwardedPointerMessage(WM_POINTERLEAVE)); +static_assert(IsForwardedPointerMessage(WM_POINTERCAPTURECHANGED)); +static_assert(!IsForwardedPointerMessage(WM_MOUSEMOVE)); + +uint64_t PointerToken(WPARAM wparam) { return static_cast(GET_POINTERID_WPARAM(wparam)) + 1; } + +LPARAM PointerPositionInView(HWND view, LPARAM lparam) { + POINT point = {GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)}; + ::ScreenToClient(view, &point); + return MAKELPARAM(point.x, point.y); +} +void ReleaseForwardedPointer(InnerWindowSubclassState& state, HWND view) { + std::lock_guard lock(state.forwarded_pointer_mutex); + if (state.forwarded_pointer_token.exchange(0, std::memory_order_acq_rel) == 0 || !view) { + return; + } + const LPARAM position = state.forwarded_pointer_position.load(std::memory_order_acquire); + ::PostMessageW(view, WM_LBUTTONUP, 0, position); +} + +bool ForwardPointerAsMouse(InnerWindowSubclassState& state, HWND view, UINT message, WPARAM wparam, LPARAM lparam) { + if (!IsForwardedPointerMessage(message)) return false; + + std::lock_guard lock(state.forwarded_pointer_mutex); + if (!state.active.load(std::memory_order_acquire) || state.forward_target.load(std::memory_order_acquire) != view) { + return true; + } + const uint64_t pointer_token = PointerToken(wparam); + if (message == WM_POINTERDOWN) { + if (!IS_POINTER_PRIMARY_WPARAM(wparam) || !IS_POINTER_FIRSTBUTTON_WPARAM(wparam)) { + return true; + } + + uint64_t expected = 0; + if (!state.forwarded_pointer_token.compare_exchange_strong(expected, pointer_token)) { + return true; + } + + const LPARAM position = PointerPositionInView(view, lparam); + state.forwarded_pointer_position.store(position, std::memory_order_release); + ::PostMessageW(view, WM_LBUTTONDOWN, MK_LBUTTON, position); + return true; + } + + if (state.forwarded_pointer_token.load(std::memory_order_acquire) != pointer_token) { + return true; + } + + if (message == WM_POINTERUPDATE) { + const LPARAM position = PointerPositionInView(view, lparam); + state.forwarded_pointer_position.store(position, std::memory_order_release); + ::PostMessageW(view, WM_MOUSEMOVE, MK_LBUTTON, position); + return true; + } + + uint64_t expected = pointer_token; + if (!state.forwarded_pointer_token.compare_exchange_strong(expected, 0)) { + return true; + } + + const LPARAM position = message == WM_POINTERCAPTURECHANGED + ? state.forwarded_pointer_position.load(std::memory_order_acquire) + : PointerPositionInView(view, lparam); + state.forwarded_pointer_position.store(position, std::memory_order_release); + ::PostMessageW(view, WM_LBUTTONUP, 0, position); + return true; +} std::mutex g_inner_subclasses_mutex; std::unordered_map> g_inner_subclasses; @@ -111,11 +180,7 @@ LRESULT CALLBACK MpvInnerSubclassProc( const bool active = state->active.load(std::memory_order_acquire); HWND view = active ? state->forward_target.load(std::memory_order_acquire) : nullptr; - if (active && view && IsFlutterPointerMessage(message)) { - // WM_POINTER coordinates are already in screen space. SendMessage also - // preserves the message association required by GetPointerInfo in the - // Flutter view's window procedure. - ::SendMessageW(view, message, wparam, lparam); + if (active && view && ForwardPointerAsMouse(*state, view, message, wparam, lparam)) { return 0; } @@ -136,7 +201,8 @@ LRESULT CALLBACK MpvInnerSubclassProc( if (message == WM_NCDESTROY) { state->active.store(false, std::memory_order_release); - state->forward_target.store(nullptr, std::memory_order_release); + const HWND view = state->forward_target.exchange(nullptr, std::memory_order_acq_rel); + ReleaseForwardedPointer(*state, view); ::RemoveWindowSubclass(hwnd, MpvInnerSubclassProc, subclass_id); std::lock_guard lock(g_inner_subclasses_mutex); const auto it = g_inner_subclasses.find(hwnd); @@ -325,6 +391,7 @@ std::shared_ptr InstallMpvInnerSubclass(HWND inner, HW // A timed-out detach that never reached the window thread leaves the // helper-chain entry installed. Adopt that exact generation rather than // stacking a duplicate subclass or retaining a permanently inert entry. + retained->forwarded_pointer_token.store(0, std::memory_order_release); retained->forward_target.store(forward_target, std::memory_order_release); retained->active.store(true, std::memory_order_release); return retained; @@ -373,6 +440,7 @@ void DetachMpvInnerSubclassState(const std::shared_ptr // already holding this generation can still call DefSubclassProc, but can // no longer target a replacement Flutter/player generation. + HWND forward_target = nullptr; { std::lock_guard lock(g_inner_subclasses_mutex); const auto it = g_inner_subclasses.find(state->hwnd); @@ -380,9 +448,10 @@ void DetachMpvInnerSubclassState(const std::shared_ptr return; } state->active.store(false, std::memory_order_release); - state->forward_target.store(nullptr, std::memory_order_release); + forward_target = state->forward_target.exchange(nullptr, std::memory_order_acq_rel); state->removal_pending = true; } + ReleaseForwardedPointer(*state, forward_target); auto action = std::make_shared(); action->state = state; diff --git a/windows/runner/mpv/mpv_player_property_contract_test.cpp b/windows/runner/mpv/mpv_player_property_contract_test.cpp index c4475117..893c4671 100644 --- a/windows/runner/mpv/mpv_player_property_contract_test.cpp +++ b/windows/runner/mpv/mpv_player_property_contract_test.cpp @@ -1,3 +1,5 @@ +#include + #include #include #include @@ -45,6 +47,14 @@ void Check(bool condition, const char* message) { } std::atomic g_forwarded_mouse_messages{0}; +std::atomic g_forwarded_touch_moves{0}; +std::atomic g_forwarded_mouse_down_messages{0}; +std::atomic g_forwarded_mouse_up_messages{0}; +std::atomic g_forwarded_pointer_messages{0}; +std::atomic g_forwarded_mouse_down_position{0}; +std::atomic g_forwarded_mouse_up_position{0}; +std::atomic g_forwarded_mouse_down_flags{0}; +std::atomic g_forwarded_mouse_up_flags{0}; constexpr UINT kBlockWindowThreadMessage = WM_APP + 0x0505; std::atomic g_block_entered{nullptr}; std::atomic g_block_release{nullptr}; @@ -63,7 +73,27 @@ LRESULT CALLBACK CountingWindowProc(HWND hwnd, UINT message, WPARAM wparam, LPAR return 0; } if (message == WM_MOUSEMOVE) { - g_forwarded_mouse_messages.fetch_add(1, std::memory_order_relaxed); + if ((wparam & MK_LBUTTON) != 0) { + g_forwarded_touch_moves.fetch_add(1, std::memory_order_relaxed); + } else { + g_forwarded_mouse_messages.fetch_add(1, std::memory_order_relaxed); + } + return 0; + } + if (message == WM_LBUTTONDOWN) { + g_forwarded_mouse_down_position.store(lparam, std::memory_order_relaxed); + g_forwarded_mouse_down_flags.store(wparam, std::memory_order_relaxed); + g_forwarded_mouse_down_messages.fetch_add(1, std::memory_order_relaxed); + return 0; + } + if (message == WM_LBUTTONUP) { + g_forwarded_mouse_up_position.store(lparam, std::memory_order_relaxed); + g_forwarded_mouse_up_flags.store(wparam, std::memory_order_relaxed); + g_forwarded_mouse_up_messages.fetch_add(1, std::memory_order_relaxed); + return 0; + } + if (message >= WM_POINTERUPDATE && message <= WM_POINTERCAPTURECHANGED) { + g_forwarded_pointer_messages.fetch_add(1, std::memory_order_relaxed); return 0; } return ::DefWindowProcW(hwnd, message, wparam, lparam); @@ -174,10 +204,111 @@ void TestInnerSubclassOwnershipIsSerializedAndDetached() { } Check(g_forwarded_mouse_messages.load(std::memory_order_relaxed) == 1, "active generation must forward mouse input"); + constexpr UINT32 kPrimaryPointerId = 7; + constexpr UINT32 kSecondaryPointerId = 8; + constexpr UINT32 kCancelledPointerId = 9; + constexpr UINT32 kDetachedPointerId = 10; + constexpr UINT32 kDestroyedPointerId = 11; + constexpr UINT32 kPointerDownFlags = POINTER_MESSAGE_FLAG_NEW | POINTER_MESSAGE_FLAG_INRANGE | + POINTER_MESSAGE_FLAG_INCONTACT | POINTER_MESSAGE_FLAG_FIRSTBUTTON | + POINTER_MESSAGE_FLAG_PRIMARY; + constexpr UINT32 kPointerMoveFlags = POINTER_MESSAGE_FLAG_INRANGE | POINTER_MESSAGE_FLAG_INCONTACT | + POINTER_MESSAGE_FLAG_FIRSTBUTTON | POINTER_MESSAGE_FLAG_PRIMARY; + constexpr UINT32 kPointerUpFlags = POINTER_MESSAGE_FLAG_INRANGE | POINTER_MESSAGE_FLAG_PRIMARY; + + POINT down_position = {14, 18}; + ::ClientToScreen(windows.inner, &down_position); + POINT expected_down_position = down_position; + ::ScreenToClient(windows.target, &expected_down_position); + POINT up_position = {30, 36}; + ::ClientToScreen(windows.inner, &up_position); + POINT expected_up_position = up_position; + ::ScreenToClient(windows.target, &expected_up_position); + + ::SendMessageW( + windows.inner, WM_POINTERDOWN, MAKEWPARAM(kPrimaryPointerId, kPointerDownFlags), + MAKELPARAM(down_position.x, down_position.y)); + ::SendMessageW( + windows.inner, WM_POINTERUPDATE, MAKEWPARAM(kPrimaryPointerId, kPointerMoveFlags), + MAKELPARAM(up_position.x, up_position.y)); + ::SendMessageW( + windows.inner, WM_POINTERUP, MAKEWPARAM(kPrimaryPointerId, kPointerUpFlags), + MAKELPARAM(up_position.x, up_position.y)); + for (int attempt = 0; attempt < 100 && g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) < 1; ++attempt) { + ::Sleep(10); + } + + Check(g_forwarded_mouse_down_messages.load(std::memory_order_relaxed) == 1, "primary touch must press once"); + Check(g_forwarded_touch_moves.load(std::memory_order_relaxed) == 1, "primary touch movement must drag once"); + Check(g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) == 1, "primary touch must release once"); + Check( + g_forwarded_pointer_messages.load(std::memory_order_relaxed) == 0, "raw pointer messages must not reach Flutter"); + Check( + g_forwarded_mouse_down_flags.load(std::memory_order_relaxed) == MK_LBUTTON, + "touch down must hold the mouse button"); + Check(g_forwarded_mouse_up_flags.load(std::memory_order_relaxed) == 0, "touch up must release the mouse button"); + const LPARAM forwarded_down_position = g_forwarded_mouse_down_position.load(std::memory_order_relaxed); + Check( + GET_X_LPARAM(forwarded_down_position) == expected_down_position.x && + GET_Y_LPARAM(forwarded_down_position) == expected_down_position.y, + "touch down must use Flutter-view client coordinates"); + const LPARAM forwarded_up_position = g_forwarded_mouse_up_position.load(std::memory_order_relaxed); + Check( + GET_X_LPARAM(forwarded_up_position) == expected_up_position.x && + GET_Y_LPARAM(forwarded_up_position) == expected_up_position.y, + "touch up must use Flutter-view client coordinates"); + + const WPARAM secondary_down = MAKEWPARAM(kSecondaryPointerId, kPointerDownFlags & ~POINTER_MESSAGE_FLAG_PRIMARY); + const WPARAM secondary_up = MAKEWPARAM(kSecondaryPointerId, kPointerUpFlags & ~POINTER_MESSAGE_FLAG_PRIMARY); + ::SendMessageW(windows.inner, WM_POINTERDOWN, secondary_down, MAKELPARAM(down_position.x, down_position.y)); + ::SendMessageW(windows.inner, WM_POINTERUP, secondary_up, MAKELPARAM(up_position.x, up_position.y)); + ::Sleep(30); + Check( + g_forwarded_mouse_down_messages.load(std::memory_order_relaxed) == 1 && + g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) == 1, + "secondary touch must not synthesize another click"); + + ::SendMessageW( + windows.inner, WM_POINTERDOWN, MAKEWPARAM(kCancelledPointerId, kPointerDownFlags), + MAKELPARAM(down_position.x, down_position.y)); + ::SendMessageW( + windows.inner, WM_POINTERCAPTURECHANGED, MAKEWPARAM(kCancelledPointerId, kPointerUpFlags), + reinterpret_cast(windows.host)); + for (int attempt = 0; attempt < 100 && g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) < 2; ++attempt) { + ::Sleep(10); + } + Check( + g_forwarded_mouse_down_messages.load(std::memory_order_relaxed) == 2 && + g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) == 2, + "capture loss must release an active synthetic mouse press"); + + ::SendMessageW( + windows.inner, WM_POINTERDOWN, MAKEWPARAM(kDetachedPointerId, kPointerDownFlags), + MAKELPARAM(down_position.x, down_position.y)); + for (int attempt = 0; attempt < 100 && g_forwarded_mouse_down_messages.load(std::memory_order_relaxed) < 3; + ++attempt) { + ::Sleep(10); + } + MpvPlayerPropertyContractTestPeer::DetachInnerSubclass(player); Check( reinterpret_cast(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC)) == windows.inner_original, "detach must restore the original procedure before window destruction"); + for (int attempt = 0; attempt < 100 && g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) < 3; ++attempt) { + ::Sleep(10); + } + Check( + g_forwarded_mouse_down_messages.load(std::memory_order_relaxed) == 3 && + g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) == 3, + "detach must release an active synthetic mouse press"); + + ::SendMessageW( + windows.inner, WM_POINTERUP, MAKEWPARAM(kDetachedPointerId, kPointerUpFlags), + MAKELPARAM(up_position.x, up_position.y)); + ::Sleep(30); + Check( + g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) == 3, + "a late pointer up after detach must not release twice"); ::SendMessageW(windows.inner, WM_MOUSEMOVE, 0, MAKELPARAM(8, 9)); ::Sleep(30); @@ -201,9 +332,34 @@ void TestInnerSubclassOwnershipIsSerializedAndDetached() { reinterpret_cast(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC)) == windows.inner_original, "replacement detach must restore the original procedure"); + MpvPlayerPropertyContractTestPeer::ConfigureInnerSubclass(player, windows.host, windows.target); + MpvPlayerPropertyContractTestPeer::EnsureInnerSubclass(player); + const auto destroy_generation = reinterpret_cast(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC)); + Check( + destroy_generation && destroy_generation != windows.inner_original, + "destroy test must install a fresh subclass generation"); + + ::SendMessageW( + windows.inner, WM_POINTERDOWN, MAKEWPARAM(kDestroyedPointerId, kPointerDownFlags), + MAKELPARAM(down_position.x, down_position.y)); + ::SendMessageW(windows.inner, WM_CLOSE, 0, 0); + for (int attempt = 0; attempt < 100 && g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) < 4; ++attempt) { + ::Sleep(10); + } + Check(!::IsWindow(windows.inner), "destroy test must close the inner window"); + Check( + g_forwarded_mouse_down_messages.load(std::memory_order_relaxed) == 4 && + g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) == 4, + "window destruction must release an active synthetic mouse press"); + MpvPlayerPropertyContractTestPeer::ReleaseTestWindows(player); + ::PostThreadMessageW(windows.owner_thread, WM_QUIT, 0, 0); window_owner.join(); g_forwarded_mouse_messages.store(0, std::memory_order_relaxed); + g_forwarded_touch_moves.store(0, std::memory_order_relaxed); + g_forwarded_mouse_down_messages.store(0, std::memory_order_relaxed); + g_forwarded_mouse_up_messages.store(0, std::memory_order_relaxed); + g_forwarded_pointer_messages.store(0, std::memory_order_relaxed); } void TestTimedOutSubclassDetachCanBeAdopted() {