fix(native): bound cross-platform lifecycle ownership

This commit is contained in:
edde746
2026-07-24 03:56:40 +02:00
parent 09656fa4d3
commit 9f2e050797
98 changed files with 12908 additions and 1599 deletions
+19 -1
View File
@@ -77,7 +77,7 @@ if(PLEZY_BUILD_MPV_PROPERTY_CONTRACT_TESTS)
target_compile_definitions(mpv_player_property_contract_test PRIVATE "NOMINMAX")
target_link_libraries(
mpv_player_property_contract_test
PRIVATE flutter "${MPV_LIB_DIR}/libmpv.dll.a" simdutf "user32.lib"
PRIVATE flutter_wrapper_plugin "${MPV_LIB_DIR}/libmpv.dll.a" simdutf "comctl32.lib" "user32.lib"
)
target_include_directories(
mpv_player_property_contract_test
@@ -101,3 +101,21 @@ if(PLEZY_BUILD_MPV_PROPERTY_CONTRACT_TESTS)
add_test(NAME mpv_property_result_contract_test COMMAND mpv_property_result_contract_test)
add_test(NAME mpv_player_property_contract_test COMMAND mpv_player_property_contract_test)
endif()
option(PLEZY_BUILD_DISPLAY_RECOVERY_TESTS
"Build the focused Windows display recovery transaction tests" OFF)
if(PLEZY_BUILD_DISPLAY_RECOVERY_TESTS)
enable_testing()
add_executable(display_mode_manager_test
"mpv/display_mode_manager.cpp"
"mpv/display_mode_manager_test.cpp"
)
apply_standard_settings(display_mode_manager_test)
target_compile_definitions(
display_mode_manager_test PRIVATE "NOMINMAX" "PLEZY_DISPLAY_MODE_MANAGER_TESTING"
)
target_link_libraries(display_mode_manager_test PRIVATE "advapi32.lib" "user32.lib")
add_test(NAME display_mode_manager_test COMMAND display_mode_manager_test)
endif()
+5
View File
@@ -3,6 +3,7 @@
#include <optional>
#include "flutter/generated_plugin_registrant.h"
#include "mpv/display_mode_manager.h"
#include "mpv/mpv_plugin.h"
// Registry key for window placement persistence
@@ -158,6 +159,10 @@ FlutterWindow::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam
}
switch (message) {
case WM_DISPLAYCHANGE:
// One bounded, serialized retry for a display that may have reconnected.
mpv::DisplayModeManager::RecoverIfNeeded();
break;
case WM_FONTCHANGE:
flutter_controller_->engine()->ReloadSystemFonts();
break;
+1 -1
View File
@@ -54,7 +54,7 @@ wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t* command
window.SetQuitOnClose(true);
// Recover display mode if a prior crash left it changed.
mpv::DisplayModeManager::RecoverIfNeeded(::GetAncestor(window.GetHandle(), GA_ROOT));
mpv::DisplayModeManager::RecoverIfNeeded();
::MSG msg;
while (::GetMessage(&msg, nullptr, 0, 0)) {
+487 -148
View File
@@ -2,19 +2,53 @@
#include <algorithm>
#include <cmath>
#include <mutex>
#include <vector>
#include "sdk_26100.h"
namespace mpv {
static const wchar_t* kRegistryPath = L"Software\\Plezy\\DisplayModeOverride";
static const wchar_t* kRegDeviceName = L"DeviceName";
static const wchar_t* kRegOriginalRefreshRate = L"OriginalRefreshRate";
static const wchar_t* kRegOriginalWidth = L"OriginalWidth";
static const wchar_t* kRegOriginalHeight = L"OriginalHeight";
static const wchar_t* kRegOriginalHDR = L"OriginalHDREnabled";
static const wchar_t* kRegModeChanged = L"ModeChanged";
static const wchar_t* kRegHDRChanged = L"HDRChanged";
namespace {
constexpr wchar_t kRegistryPath[] = L"Software\\Plezy\\DisplayModeOverride";
constexpr wchar_t kRegVersion[] = L"Version";
constexpr DWORD kRecoveryVersion = 1;
constexpr wchar_t kRegModeDeviceName[] = L"ModeDeviceName";
constexpr wchar_t kRegLegacyDeviceName[] = L"DeviceName";
constexpr wchar_t kRegHDRDeviceName[] = L"HDRDeviceName";
constexpr wchar_t kRegOriginalRefreshRate[] = L"OriginalRefreshRate";
constexpr wchar_t kRegOriginalWidth[] = L"OriginalWidth";
constexpr wchar_t kRegOriginalHeight[] = L"OriginalHeight";
constexpr wchar_t kRegOriginalHDR[] = L"OriginalHDREnabled";
constexpr wchar_t kRegModeChanged[] = L"ModeChanged";
constexpr wchar_t kRegHDRChanged[] = L"HDRChanged";
std::recursive_mutex g_display_override_mutex;
bool g_live_mode_recovery_record = false;
bool g_live_hdr_recovery_record = false;
bool g_recovery_in_progress = false;
class RecoveryRunGuard {
public:
RecoveryRunGuard() : acquired_(!g_recovery_in_progress) {
if (acquired_) g_recovery_in_progress = true;
}
~RecoveryRunGuard() {
if (acquired_) g_recovery_in_progress = false;
}
bool acquired() const { return acquired_; }
private:
bool acquired_;
};
bool PrepareModeRecoveryAtRegistry(const std::wstring& device_name, DWORD width, DWORD height, DWORD refresh_rate);
bool PrepareHDRRecoveryAtRegistry(const std::wstring& device_name, bool enabled);
bool CompleteRecoveryOperationAtRegistry(const wchar_t* marker);
} // namespace
DisplayModeManager::DisplayModeManager() {}
@@ -149,13 +183,27 @@ void DisplayModeManager::SaveOriginalMode(HWND window) {
}
bool DisplayModeManager::SetDisplayMode(HWND window, DWORD width, DWORD height, DWORD refresh_rate) {
std::wstring device_name = GetMonitorDeviceName(window);
std::lock_guard<std::recursive_mutex> transaction_lock(g_display_override_mutex);
const std::wstring device_name = GetMonitorDeviceName(window);
if (device_name.empty()) return false;
// Save original mode if not already saved.
if (!mode_changed_) {
SaveOriginalMode(window);
const bool mode_was_changed = mode_changed_;
if (!mode_changed_) SaveOriginalMode(window);
if (original_device_name_.empty() || original_devmode_.dmPelsWidth == 0 || original_devmode_.dmPelsHeight == 0 ||
original_devmode_.dmDisplayFrequency == 0) {
return false;
}
if (!PrepareModeRecoveryAtRegistry(
original_device_name_, original_devmode_.dmPelsWidth, original_devmode_.dmPelsHeight,
original_devmode_.dmDisplayFrequency)) {
return false;
}
// Mark the record live before calling Windows. ChangeDisplaySettingsExW can
// synchronously deliver WM_DISPLAYCHANGE; that event must not recover the
// override that is currently being applied.
g_live_mode_recovery_record = true;
DEVMODEW dm = {};
dm.dmSize = sizeof(dm);
@@ -187,37 +235,49 @@ bool DisplayModeManager::SetDisplayMode(HWND window, DWORD width, DWORD height,
// Standard path / fallback.
if (!changed) {
LONG rc = ChangeDisplaySettingsExW(device_name.c_str(), &dm, nullptr, CDS_FULLSCREEN, nullptr);
if (rc == DISP_CHANGE_SUCCESSFUL) changed = true;
const LONG rc = ChangeDisplaySettingsExW(device_name.c_str(), &dm, nullptr, CDS_FULLSCREEN, nullptr);
changed = rc == DISP_CHANGE_SUCCESSFUL;
}
if (changed) {
mode_changed_ = true;
WriteRecoveryState();
} else if (!mode_was_changed) {
// Do not discard an independently persisted HDR operation owned by
// another manager or retained from startup recovery.
CompleteRecoveryOperationAtRegistry(kRegModeChanged);
g_live_mode_recovery_record = false;
}
return changed;
}
bool DisplayModeManager::RestoreOriginalMode(HWND window) {
if (!mode_changed_ || original_device_name_.empty()) return false;
bool DisplayModeManager::RestoreOriginalMode(HWND) {
std::lock_guard<std::recursive_mutex> transaction_lock(g_display_override_mutex);
if (!mode_changed_) return false;
if (original_device_name_.empty()) {
g_live_mode_recovery_record = false;
return false;
}
original_devmode_.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS;
LONG rc =
ChangeDisplaySettingsExW(original_device_name_.c_str(), &original_devmode_, nullptr, CDS_FULLSCREEN, nullptr);
if (rc == DISP_CHANGE_SUCCESSFUL) {
mode_changed_ = false;
if (!hdr_changed_) ClearRecoveryState();
return true;
if (rc != DISP_CHANGE_SUCCESSFUL) {
// Fallback: restore registry defaults.
rc = ChangeDisplaySettingsExW(original_device_name_.c_str(), nullptr, nullptr, 0, nullptr);
}
if (rc != DISP_CHANGE_SUCCESSFUL) {
// The explicit owner has given up. Keep the durable marker, but release it
// so a later topology notification can restore a reconnected target.
g_live_mode_recovery_record = false;
return false;
}
// Fallback: restore registry defaults.
rc = ChangeDisplaySettingsExW(original_device_name_.c_str(), nullptr, nullptr, 0, nullptr);
mode_changed_ = (rc != DISP_CHANGE_SUCCESSFUL);
if (!mode_changed_ && !hdr_changed_) ClearRecoveryState();
return rc == DISP_CHANGE_SUCCESSFUL;
mode_changed_ = false;
CompleteRecoveryOperationAtRegistry(kRegModeChanged);
g_live_mode_recovery_record = false;
return true;
}
// --- HDR ---
@@ -297,26 +357,39 @@ void DisplayModeManager::SaveOriginalHDRState(HWND window) {
}
bool DisplayModeManager::SetHDREnabled(HWND window, bool enabled) {
std::wstring device_name = GetMonitorDeviceName(window);
std::lock_guard<std::recursive_mutex> transaction_lock(g_display_override_mutex);
const std::wstring device_name = GetMonitorDeviceName(window);
if (device_name.empty()) return false;
auto target_id = GetDisplayTargetId(device_name);
const auto target_id = GetDisplayTargetId(device_name);
if (!target_id) return false;
// Save original state if not already saved.
if (!hdr_changed_) {
SaveOriginalHDRState(window);
const bool hdr_was_changed = hdr_changed_;
if (!hdr_changed_) SaveOriginalHDRState(window);
if (original_hdr_device_name_.empty() ||
!PrepareHDRRecoveryAtRegistry(original_hdr_device_name_, original_hdr_enabled_)) {
return false;
}
// See SetDisplayMode: keep synchronous topology notifications from treating
// this process's just-persisted marker as crash recovery.
g_live_hdr_recovery_record = true;
// Save DEVMODEW before toggle — Windows changes display mode on HDR state change.
// Source: Kodi WIN32Util.cpp:1252-1257.
DEVMODEW pre_toggle_dm = {};
pre_toggle_dm.dmSize = sizeof(pre_toggle_dm);
EnumDisplaySettingsW(device_name.c_str(), ENUM_CURRENT_SETTINGS, &pre_toggle_dm);
// Toggle HDR.
LONG result = SetHDRStateForTarget(*target_id, enabled);
if (result != ERROR_SUCCESS) return false;
const LONG result = SetHDRStateForTarget(*target_id, enabled);
if (result != ERROR_SUCCESS) {
if (!hdr_was_changed) {
CompleteRecoveryOperationAtRegistry(kRegHDRChanged);
g_live_hdr_recovery_record = false;
}
return false;
}
// Restore DEVMODEW after toggle — Windows may have changed the display mode.
// Source: Kodi WIN32Util.cpp:1276-1288.
@@ -326,40 +399,48 @@ bool DisplayModeManager::SetHDREnabled(HWND window, bool enabled) {
}
hdr_changed_ = true;
WriteRecoveryState();
return true;
}
bool DisplayModeManager::RestoreOriginalHDRState(HWND window) {
if (!hdr_changed_ || original_hdr_device_name_.empty()) return false;
bool current = IsHDREnabled(window);
if (current == original_hdr_enabled_) {
hdr_changed_ = false;
if (!mode_changed_) ClearRecoveryState();
return true;
std::lock_guard<std::recursive_mutex> transaction_lock(g_display_override_mutex);
if (!hdr_changed_) return false;
if (original_hdr_device_name_.empty()) {
g_live_hdr_recovery_record = false;
return false;
}
// Need to actually toggle back.
auto target_id = GetDisplayTargetId(original_hdr_device_name_);
if (!target_id) return false;
const auto target_id = GetDisplayTargetId(original_hdr_device_name_);
if (!target_id) {
g_live_hdr_recovery_record = false;
return false;
}
// Save DEVMODEW before restore toggle.
DEVMODEW pre_toggle_dm = {};
pre_toggle_dm.dmSize = sizeof(pre_toggle_dm);
EnumDisplaySettingsW(original_hdr_device_name_.c_str(), ENUM_CURRENT_SETTINGS, &pre_toggle_dm);
if (IsHDREnabled(window) != original_hdr_enabled_) {
// The original target was resolved above even when the currently observed
// HDR state already matches, so a disconnected target cannot be mistaken
// for a successful restore.
LONG result = SetHDRStateForTarget(*target_id, original_hdr_enabled_);
if (result != ERROR_SUCCESS) return false;
// Save DEVMODEW before restore toggle.
DEVMODEW pre_toggle_dm = {};
pre_toggle_dm.dmSize = sizeof(pre_toggle_dm);
EnumDisplaySettingsW(original_hdr_device_name_.c_str(), ENUM_CURRENT_SETTINGS, &pre_toggle_dm);
// Restore DEVMODEW after toggle.
if (pre_toggle_dm.dmDisplayFrequency != 0) {
pre_toggle_dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS;
ChangeDisplaySettingsExW(original_hdr_device_name_.c_str(), &pre_toggle_dm, nullptr, CDS_FULLSCREEN, nullptr);
if (SetHDRStateForTarget(*target_id, original_hdr_enabled_) != ERROR_SUCCESS) {
g_live_hdr_recovery_record = false;
return false;
}
// Restore DEVMODEW after toggle.
if (pre_toggle_dm.dmDisplayFrequency != 0) {
pre_toggle_dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS;
ChangeDisplaySettingsExW(original_hdr_device_name_.c_str(), &pre_toggle_dm, nullptr, CDS_FULLSCREEN, nullptr);
}
}
hdr_changed_ = false;
if (!mode_changed_) ClearRecoveryState();
CompleteRecoveryOperationAtRegistry(kRegHDRChanged);
g_live_hdr_recovery_record = false;
return true;
}
@@ -385,152 +466,410 @@ LONG DisplayModeManager::SetHDRStateForTarget(const DisplayConfigId& target, boo
}
}
bool DisplayModeManager::WriteRegistryDWORD(const wchar_t* value_name, DWORD value) {
namespace {
bool WriteRegistryDWORD(const wchar_t* value_name, DWORD value) {
HKEY key;
if (RegCreateKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr) !=
ERROR_SUCCESS)
ERROR_SUCCESS) {
return false;
LONG result = RegSetValueExW(key, value_name, 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
}
const LONG result =
RegSetValueExW(key, value_name, 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
RegCloseKey(key);
return result == ERROR_SUCCESS;
}
bool DisplayModeManager::WriteRegistryString(const wchar_t* value_name, const std::wstring& value) {
bool WriteRegistryString(const wchar_t* value_name, const std::wstring& value) {
HKEY key;
if (RegCreateKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr) !=
ERROR_SUCCESS)
ERROR_SUCCESS) {
return false;
LONG result = RegSetValueExW(
}
const LONG result = RegSetValueExW(
key, value_name, 0, REG_SZ, reinterpret_cast<const BYTE*>(value.c_str()),
static_cast<DWORD>((value.size() + 1) * sizeof(wchar_t)));
RegCloseKey(key);
return result == ERROR_SUCCESS;
}
bool DisplayModeManager::ReadRegistryDWORD(const wchar_t* value_name, DWORD& value) {
bool ReadRegistryDWORD(const wchar_t* value_name, DWORD& value) {
HKEY key;
if (RegOpenKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, KEY_READ, &key) != ERROR_SUCCESS) return false;
DWORD size = sizeof(value);
DWORD type = 0;
LONG result = RegQueryValueExW(key, value_name, nullptr, &type, reinterpret_cast<BYTE*>(&value), &size);
const LONG result = RegQueryValueExW(key, value_name, nullptr, &type, reinterpret_cast<BYTE*>(&value), &size);
RegCloseKey(key);
return result == ERROR_SUCCESS && type == REG_DWORD;
return result == ERROR_SUCCESS && type == REG_DWORD && size == sizeof(value);
}
bool DisplayModeManager::ReadRegistryString(const wchar_t* value_name, std::wstring& value) {
bool ReadRegistryString(const wchar_t* value_name, std::wstring& value) {
HKEY key;
if (RegOpenKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, KEY_READ, &key) != ERROR_SUCCESS) return false;
DWORD size = 0;
DWORD type = 0;
RegQueryValueExW(key, value_name, nullptr, &type, nullptr, &size);
if (type != REG_SZ || size == 0) {
const LONG size_result = RegQueryValueExW(key, value_name, nullptr, &type, nullptr, &size);
if (size_result != ERROR_SUCCESS || type != REG_SZ || size == 0 || size % sizeof(wchar_t) != 0) {
RegCloseKey(key);
return false;
}
value.resize(size / sizeof(wchar_t));
LONG result = RegQueryValueExW(key, value_name, nullptr, nullptr, reinterpret_cast<BYTE*>(&value[0]), &size);
const LONG result = RegQueryValueExW(key, value_name, nullptr, nullptr, reinterpret_cast<BYTE*>(value.data()), &size);
RegCloseKey(key);
if (result != ERROR_SUCCESS) return false;
// Remove trailing null.
while (!value.empty() && value.back() == L'\0') value.pop_back();
return true;
return !value.empty();
}
bool DisplayModeManager::DeleteRegistryValue(const wchar_t* value_name) {
bool RecoveryRecordExists() {
HKEY key;
if (RegOpenKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, KEY_WRITE, &key) != ERROR_SUCCESS) return false;
RegDeleteValueW(key, value_name);
if (RegOpenKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, KEY_QUERY_VALUE, &key) != ERROR_SUCCESS) {
return false;
}
bool exists = false;
for (const wchar_t* value_name :
{kRegVersion, kRegModeDeviceName, kRegLegacyDeviceName, kRegHDRDeviceName, kRegOriginalRefreshRate,
kRegOriginalWidth, kRegOriginalHeight, kRegOriginalHDR, kRegModeChanged, kRegHDRChanged}) {
DWORD size = 0;
const LONG result = RegQueryValueExW(key, value_name, nullptr, nullptr, nullptr, &size);
if (result == ERROR_SUCCESS || result == ERROR_MORE_DATA) {
exists = true;
break;
}
}
RegCloseKey(key);
return exists;
}
} // namespace
class Win32DisplayRecoveryBackend final : public DisplayRecoveryBackend {
public:
bool RecordExists() const override { return RecoveryRecordExists(); }
bool ReadDWORD(const wchar_t* value_name, DWORD& value) override { return ReadRegistryDWORD(value_name, value); }
bool ReadString(const wchar_t* value_name, std::wstring& value) override {
return ReadRegistryString(value_name, value);
}
bool WriteDWORD(const wchar_t* value_name, DWORD value) override { return WriteRegistryDWORD(value_name, value); }
bool WriteString(const wchar_t* value_name, const std::wstring& value) override {
return WriteRegistryString(value_name, value);
}
bool IsDevicePresent(const std::wstring& device_name) const override {
DEVMODEW mode = {};
mode.dmSize = sizeof(mode);
return EnumDisplaySettingsW(device_name.c_str(), ENUM_CURRENT_SETTINGS, &mode) != FALSE;
}
bool RestoreMode(const std::wstring& device_name, DWORD width, DWORD height, DWORD refresh_rate) override {
DEVMODEW dm = {};
dm.dmSize = sizeof(dm);
dm.dmPelsWidth = width;
dm.dmPelsHeight = height;
dm.dmDisplayFrequency = refresh_rate;
dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY;
return ChangeDisplaySettingsExW(device_name.c_str(), &dm, nullptr, CDS_FULLSCREEN, nullptr) ==
DISP_CHANGE_SUCCESSFUL;
}
bool RestoreHDR(const std::wstring& device_name, bool enabled) override {
const auto target_id = DisplayModeManager::GetDisplayTargetId(device_name);
if (!target_id) return false;
DEVMODEW pre_toggle_mode = {};
pre_toggle_mode.dmSize = sizeof(pre_toggle_mode);
EnumDisplaySettingsW(device_name.c_str(), ENUM_CURRENT_SETTINGS, &pre_toggle_mode);
if (DisplayModeManager::SetHDRStateForTarget(*target_id, enabled) != ERROR_SUCCESS) return false;
if (pre_toggle_mode.dmDisplayFrequency != 0) {
pre_toggle_mode.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS;
if (ChangeDisplaySettingsExW(device_name.c_str(), &pre_toggle_mode, nullptr, CDS_FULLSCREEN, nullptr) !=
DISP_CHANGE_SUCCESSFUL) {
return false;
}
}
return true;
}
bool ClearMarker(const wchar_t* value_name) override { return WriteRegistryDWORD(value_name, 0); }
bool DeleteRecord() override {
HKEY key;
if (RegOpenKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, KEY_SET_VALUE, &key) != ERROR_SUCCESS) {
return !RecordExists();
}
bool deleted = true;
for (const wchar_t* value_name :
{kRegVersion, kRegModeDeviceName, kRegLegacyDeviceName, kRegHDRDeviceName, kRegOriginalRefreshRate,
kRegOriginalWidth, kRegOriginalHeight, kRegOriginalHDR, kRegModeChanged, kRegHDRChanged}) {
const LONG result = RegDeleteValueW(key, value_name);
deleted = deleted && (result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND);
}
RegCloseKey(key);
return deleted;
}
};
namespace {
bool ReadValidModeValues(
DisplayRecoveryBackend& backend, const wchar_t* device_value_name, std::wstring& device_name, DWORD& width,
DWORD& height, DWORD& refresh_rate) {
return backend.ReadString(device_value_name, device_name) && backend.ReadDWORD(kRegOriginalWidth, width) &&
width > 0 && backend.ReadDWORD(kRegOriginalHeight, height) && height > 0 &&
backend.ReadDWORD(kRegOriginalRefreshRate, refresh_rate) && refresh_rate > 0;
}
bool ReadValidHDRValues(
DisplayRecoveryBackend& backend, const wchar_t* device_value_name, std::wstring& device_name, DWORD& original_hdr) {
return backend.ReadString(device_value_name, device_name) && backend.ReadDWORD(kRegOriginalHDR, original_hdr) &&
original_hdr <= 1;
}
bool ReadValidMarkedMode(
DisplayRecoveryBackend& backend, std::wstring& device_name, DWORD& width, DWORD& height, DWORD& refresh_rate) {
DWORD version = 0;
DWORD marker = 0;
return backend.ReadDWORD(kRegVersion, version) && version == kRecoveryVersion &&
backend.ReadDWORD(kRegModeChanged, marker) && marker == 1 &&
ReadValidModeValues(backend, kRegModeDeviceName, device_name, width, height, refresh_rate);
}
bool ReadValidMarkedHDR(DisplayRecoveryBackend& backend, std::wstring& device_name, DWORD& original_hdr) {
DWORD version = 0;
DWORD marker = 0;
return backend.ReadDWORD(kRegVersion, version) && version == kRecoveryVersion &&
backend.ReadDWORD(kRegHDRChanged, marker) && marker == 1 &&
ReadValidHDRValues(backend, kRegHDRDeviceName, device_name, original_hdr);
}
bool DeleteRecordIfNoMarkedOperations(DisplayRecoveryBackend& backend) {
DWORD mode_marker = 0;
DWORD hdr_marker = 0;
if (!backend.ReadDWORD(kRegModeChanged, mode_marker) || !backend.ReadDWORD(kRegHDRChanged, hdr_marker) ||
mode_marker != 0 || hdr_marker != 0) {
// Missing, malformed, or active evidence is retained conservatively.
return false;
}
// Deletion is best effort after both operation markers are durably clear.
backend.DeleteRecord();
return true;
}
void DisplayModeManager::WriteRecoveryState() {
std::wstring device = mode_changed_ ? original_device_name_ : original_hdr_device_name_;
if (device.empty()) return;
WriteRegistryString(kRegDeviceName, device);
WriteRegistryDWORD(kRegModeChanged, mode_changed_ ? 1 : 0);
WriteRegistryDWORD(kRegHDRChanged, hdr_changed_ ? 1 : 0);
if (mode_changed_) {
WriteRegistryDWORD(kRegOriginalRefreshRate, original_devmode_.dmDisplayFrequency);
WriteRegistryDWORD(kRegOriginalWidth, original_devmode_.dmPelsWidth);
WriteRegistryDWORD(kRegOriginalHeight, original_devmode_.dmPelsHeight);
}
if (hdr_changed_) {
WriteRegistryDWORD(kRegOriginalHDR, original_hdr_enabled_ ? 1 : 0);
}
bool CompleteRecoveryOperation(DisplayRecoveryBackend& backend, const wchar_t* marker) {
if (!backend.ClearMarker(marker)) return false;
DeleteRecordIfNoMarkedOperations(backend);
return true;
}
void DisplayModeManager::ClearRecoveryState() {
// Delete the entire key.
RegDeleteKeyW(HKEY_CURRENT_USER, kRegistryPath);
}
bool PreserveValidModeSiblingOrClear(DisplayRecoveryBackend& backend) {
DWORD marker = 0;
if (!backend.ReadDWORD(kRegModeChanged, marker)) {
return backend.WriteDWORD(kRegModeChanged, 0);
}
if (marker == 0) return true;
bool DisplayModeManager::RecoverIfNeeded(HWND window) {
DWORD mode_changed = 0, hdr_changed = 0;
std::wstring device_name;
DWORD width = 0;
DWORD height = 0;
DWORD refresh_rate = 0;
if (marker == 1 && ReadValidMarkedMode(backend, device_name, width, height, refresh_rate)) {
return true;
}
return backend.ClearMarker(kRegModeChanged);
}
if (!ReadRegistryString(kRegDeviceName, device_name)) return false;
ReadRegistryDWORD(kRegModeChanged, mode_changed);
ReadRegistryDWORD(kRegHDRChanged, hdr_changed);
bool PreserveValidHDRSiblingOrClear(DisplayRecoveryBackend& backend) {
DWORD marker = 0;
if (!backend.ReadDWORD(kRegHDRChanged, marker)) {
return backend.WriteDWORD(kRegHDRChanged, 0);
}
if (marker == 0) return true;
if (!mode_changed && !hdr_changed) {
RegDeleteKeyW(HKEY_CURRENT_USER, kRegistryPath);
std::wstring device_name;
DWORD original_hdr = 0;
if (marker == 1 && ReadValidMarkedHDR(backend, device_name, original_hdr)) {
return true;
}
return backend.ClearMarker(kRegHDRChanged);
}
} // namespace
bool DisplayModeManager::PrepareModeRecovery(
DisplayRecoveryBackend& backend, const std::wstring& device_name, DWORD width, DWORD height, DWORD refresh_rate) {
if (device_name.empty() || width == 0 || height == 0 || refresh_rate == 0) return false;
if (!PreserveValidHDRSiblingOrClear(backend)) return false;
DWORD existing_width = 0;
DWORD existing_height = 0;
DWORD existing_refresh_rate = 0;
std::wstring existing_device_name;
if (ReadValidMarkedMode(backend, existing_device_name, existing_width, existing_height, existing_refresh_rate)) {
// A valid marked original is already protecting a live or failed
// operation. Reuse it only when this manager has the same original;
// replacing it would lose the only restoration point.
return existing_device_name == device_name && existing_width == width && existing_height == height &&
existing_refresh_rate == refresh_rate;
}
// Deactivate an incomplete old mode operation before replacing any
// originals. A crash anywhere before the final write is therefore a
// harmless pre-mutation prefix.
if (!backend.ClearMarker(kRegModeChanged)) return false;
return backend.WriteDWORD(kRegVersion, kRecoveryVersion) && backend.WriteString(kRegModeDeviceName, device_name) &&
backend.WriteDWORD(kRegOriginalWidth, width) && backend.WriteDWORD(kRegOriginalHeight, height) &&
backend.WriteDWORD(kRegOriginalRefreshRate, refresh_rate) && backend.WriteDWORD(kRegModeChanged, 1);
}
bool DisplayModeManager::PrepareHDRRecovery(
DisplayRecoveryBackend& backend, const std::wstring& device_name, bool enabled) {
if (device_name.empty()) return false;
if (!PreserveValidModeSiblingOrClear(backend)) return false;
DWORD existing_original = 0;
std::wstring existing_device_name;
if (ReadValidMarkedHDR(backend, existing_device_name, existing_original)) {
return existing_device_name == device_name && existing_original == (enabled ? 1u : 0u);
}
if (!backend.ClearMarker(kRegHDRChanged)) return false;
return backend.WriteDWORD(kRegVersion, kRecoveryVersion) && backend.WriteString(kRegHDRDeviceName, device_name) &&
backend.WriteDWORD(kRegOriginalHDR, enabled ? 1 : 0) && backend.WriteDWORD(kRegHDRChanged, 1);
}
namespace {
bool PrepareModeRecoveryAtRegistry(const std::wstring& device_name, DWORD width, DWORD height, DWORD refresh_rate) {
Win32DisplayRecoveryBackend backend;
return DisplayModeManager::PrepareModeRecovery(backend, device_name, width, height, refresh_rate);
}
bool PrepareHDRRecoveryAtRegistry(const std::wstring& device_name, bool enabled) {
Win32DisplayRecoveryBackend backend;
return DisplayModeManager::PrepareHDRRecovery(backend, device_name, enabled);
}
bool CompleteRecoveryOperationAtRegistry(const wchar_t* marker) {
Win32DisplayRecoveryBackend backend;
return CompleteRecoveryOperation(backend, marker);
}
bool RecoverRecord(DisplayRecoveryBackend& backend, bool mode_is_live, bool hdr_is_live) {
if (!backend.RecordExists()) return false;
DWORD version = 0;
const bool has_version = backend.ReadDWORD(kRegVersion, version);
const wchar_t* mode_device_value_name = kRegModeDeviceName;
const wchar_t* hdr_device_value_name = kRegHDRDeviceName;
if (has_version) {
if (version != kRecoveryVersion) {
backend.DeleteRecord();
return false;
}
} else {
// The released layout had no Version and shared one DeviceName between
// mode and HDR. Require that discriminator before interpreting any
// versionless values as recovery evidence.
std::wstring legacy_device_name;
if (!backend.ReadString(kRegLegacyDeviceName, legacy_device_name)) {
backend.DeleteRecord();
return false;
}
mode_device_value_name = kRegLegacyDeviceName;
hdr_device_value_name = kRegLegacyDeviceName;
}
DWORD mode_marker = 0;
const bool mode_marker_read = backend.ReadDWORD(kRegModeChanged, mode_marker);
std::wstring mode_device_name;
DWORD width = 0;
DWORD height = 0;
DWORD refresh_rate = 0;
const bool mode_requested =
mode_marker_read && mode_marker == 1 &&
ReadValidModeValues(backend, mode_device_value_name, mode_device_name, width, height, refresh_rate);
if (!mode_is_live && (!mode_marker_read || mode_marker > 1 || (mode_marker == 1 && !mode_requested))) {
// Malformation in one operation does not erase a valid or live sibling.
backend.ClearMarker(kRegModeChanged);
}
DWORD hdr_marker = 0;
const bool hdr_marker_read = backend.ReadDWORD(kRegHDRChanged, hdr_marker);
std::wstring hdr_device_name;
DWORD original_hdr = 0;
const bool hdr_requested = hdr_marker_read && hdr_marker == 1 &&
ReadValidHDRValues(backend, hdr_device_value_name, hdr_device_name, original_hdr);
if (!hdr_is_live && (!hdr_marker_read || hdr_marker > 1 || (hdr_marker == 1 && !hdr_requested))) {
backend.ClearMarker(kRegHDRChanged);
}
const bool recover_mode = mode_requested && !mode_is_live;
const bool recover_hdr = hdr_requested && !hdr_is_live;
if (!recover_mode && !recover_hdr) {
DeleteRecordIfNoMarkedOperations(backend);
return false;
}
bool recovered = false;
// Restore refresh rate / resolution.
if (mode_changed) {
DWORD width = 0, height = 0, refresh = 0;
ReadRegistryDWORD(kRegOriginalWidth, width);
ReadRegistryDWORD(kRegOriginalHeight, height);
ReadRegistryDWORD(kRegOriginalRefreshRate, refresh);
if (width > 0 && height > 0 && refresh > 0) {
DEVMODEW dm = {};
dm.dmSize = sizeof(dm);
dm.dmPelsWidth = width;
dm.dmPelsHeight = height;
dm.dmDisplayFrequency = refresh;
dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY;
LONG rc = ChangeDisplaySettingsExW(device_name.c_str(), &dm, nullptr, CDS_FULLSCREEN, nullptr);
if (rc == DISP_CHANGE_SUCCESSFUL) recovered = true;
bool completed = true;
if (recover_mode) {
if (backend.IsDevicePresent(mode_device_name) &&
backend.RestoreMode(mode_device_name, width, height, refresh_rate)) {
// A failed marker clear leaves an idempotent restoration for a later pass.
completed = CompleteRecoveryOperation(backend, kRegModeChanged) && completed;
} else {
completed = false;
}
}
// Restore HDR state.
if (hdr_changed) {
DWORD hdr_was_enabled = 0;
ReadRegistryDWORD(kRegOriginalHDR, hdr_was_enabled);
auto target_id = GetDisplayTargetId(device_name);
if (target_id) {
// Save DEVMODEW before toggle.
DEVMODEW pre_dm = {};
pre_dm.dmSize = sizeof(pre_dm);
EnumDisplaySettingsW(device_name.c_str(), ENUM_CURRENT_SETTINGS, &pre_dm);
LONG result = SetHDRStateForTarget(*target_id, hdr_was_enabled != 0);
if (result == ERROR_SUCCESS) {
recovered = true;
// Restore display mode after HDR toggle.
if (pre_dm.dmDisplayFrequency != 0) {
pre_dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS;
ChangeDisplaySettingsExW(device_name.c_str(), &pre_dm, nullptr, CDS_FULLSCREEN, nullptr);
}
}
if (recover_hdr) {
if (backend.IsDevicePresent(hdr_device_name) && backend.RestoreHDR(hdr_device_name, original_hdr != 0)) {
completed = CompleteRecoveryOperation(backend, kRegHDRChanged) && completed;
} else {
completed = false;
}
}
// Clean up registry regardless of success.
RegDeleteKeyW(HKEY_CURRENT_USER, kRegistryPath);
return recovered;
return completed;
}
} // namespace
bool DisplayModeManager::RecoverIfNeeded() {
std::lock_guard<std::recursive_mutex> transaction_lock(g_display_override_mutex);
RecoveryRunGuard run;
if (!run.acquired()) return false;
Win32DisplayRecoveryBackend backend;
return RecoverRecord(backend, g_live_mode_recovery_record, g_live_hdr_recovery_record);
}
bool DisplayModeManager::RecoverIfNeeded(DisplayRecoveryBackend& backend) {
std::lock_guard<std::recursive_mutex> transaction_lock(g_display_override_mutex);
RecoveryRunGuard run;
if (!run.acquired()) return false;
return RecoverRecord(backend, false, false);
}
#if defined(PLEZY_DISPLAY_MODE_MANAGER_TESTING)
bool DisplayModeManager::CompleteRecoveryOperationForTesting(DisplayRecoveryBackend& backend, bool mode) {
std::lock_guard<std::recursive_mutex> transaction_lock(g_display_override_mutex);
return CompleteRecoveryOperation(backend, mode ? kRegModeChanged : kRegHDRChanged);
}
bool DisplayModeManager::RecoverIfNeededForTesting(
DisplayRecoveryBackend& backend, bool mode_is_live, bool hdr_is_live) {
std::lock_guard<std::recursive_mutex> transaction_lock(g_display_override_mutex);
RecoveryRunGuard run;
if (!run.acquired()) return false;
return RecoverRecord(backend, mode_is_live, hdr_is_live);
}
#endif
} // namespace mpv
+36 -16
View File
@@ -3,7 +3,6 @@
#include <Windows.h>
#include <map>
#include <optional>
#include <string>
#include <vector>
@@ -22,6 +21,24 @@ struct DisplayConfigId {
UINT32 id;
};
// Windows-runner-internal boundary for deterministic crash-recovery tests.
// Production uses the Win32/registry implementation in display_mode_manager.cpp.
class DisplayRecoveryBackend {
public:
virtual ~DisplayRecoveryBackend() = default;
virtual bool RecordExists() const = 0;
virtual bool ReadDWORD(const wchar_t* value_name, DWORD& value) = 0;
virtual bool ReadString(const wchar_t* value_name, std::wstring& value) = 0;
virtual bool WriteDWORD(const wchar_t* value_name, DWORD value) = 0;
virtual bool WriteString(const wchar_t* value_name, const std::wstring& value) = 0;
virtual bool IsDevicePresent(const std::wstring& device_name) const = 0;
virtual bool RestoreMode(const std::wstring& device_name, DWORD width, DWORD height, DWORD refresh_rate) = 0;
virtual bool RestoreHDR(const std::wstring& device_name, bool enabled) = 0;
virtual bool ClearMarker(const wchar_t* value_name) = 0;
virtual bool DeleteRecord() = 0;
};
// Manages Windows display mode switching (refresh rate, HDR) for video playback.
// Pure Win32 utility — no mpv or Flutter dependency.
//
@@ -88,17 +105,27 @@ class DisplayModeManager {
// --- Crash recovery ---
// Write current override state to registry for crash recovery.
void WriteRecoveryState();
// Persist a complete original followed by its operation marker. These
// runner-internal seams make the crash ordering deterministic in tests.
static bool PrepareModeRecovery(
DisplayRecoveryBackend& backend, const std::wstring& device_name, DWORD width, DWORD height, DWORD refresh_rate);
static bool PrepareHDRRecovery(DisplayRecoveryBackend& backend, const std::wstring& device_name, bool enabled);
// Clear the recovery state from registry.
void ClearRecoveryState();
// Check for and recover from a prior crash that left display settings changed.
// Should be called early in app startup. Returns true if recovery was performed.
static bool RecoverIfNeeded(HWND window);
// Check for and recover from a prior crash that left display settings
// changed. Successful operation markers are cleared independently. Failed
// operations remain for the next startup or display-topology notification.
static bool RecoverIfNeeded();
static bool RecoverIfNeeded(DisplayRecoveryBackend& backend);
#if defined(PLEZY_DISPLAY_MODE_MANAGER_TESTING)
// Exercise persisted lifecycle exits and per-operation live ownership without
// touching a real display or registry.
static bool CompleteRecoveryOperationForTesting(DisplayRecoveryBackend& backend, bool mode);
static bool RecoverIfNeededForTesting(DisplayRecoveryBackend& backend, bool mode_is_live, bool hdr_is_live);
#endif
private:
friend class Win32DisplayRecoveryBackend;
// Get the GDI device name for the monitor containing the window.
static std::wstring GetMonitorDeviceName(HWND window);
@@ -115,13 +142,6 @@ class DisplayModeManager {
// Toggle HDR via DisplayConfig (version-dispatched).
static LONG SetHDRStateForTarget(const DisplayConfigId& target, bool enabled);
// Registry helpers for crash recovery.
static bool WriteRegistryDWORD(const wchar_t* value_name, DWORD value);
static bool WriteRegistryString(const wchar_t* value_name, const std::wstring& value);
static bool ReadRegistryDWORD(const wchar_t* value_name, DWORD& value);
static bool ReadRegistryString(const wchar_t* value_name, std::wstring& value);
static bool DeleteRegistryValue(const wchar_t* value_name);
// Stored original mode for restoration.
std::wstring original_device_name_;
DEVMODEW original_devmode_ = {};
@@ -0,0 +1,465 @@
#include "display_mode_manager.h"
#include <cstdlib>
#include <iostream>
#include <map>
#include <string>
#include <vector>
namespace mpv {
namespace {
constexpr wchar_t kVersion[] = L"Version";
constexpr wchar_t kModeDeviceName[] = L"ModeDeviceName";
constexpr wchar_t kLegacyDeviceName[] = L"DeviceName";
constexpr wchar_t kHDRDeviceName[] = L"HDRDeviceName";
constexpr wchar_t kOriginalRefreshRate[] = L"OriginalRefreshRate";
constexpr wchar_t kOriginalWidth[] = L"OriginalWidth";
constexpr wchar_t kOriginalHeight[] = L"OriginalHeight";
constexpr wchar_t kOriginalHDR[] = L"OriginalHDREnabled";
constexpr wchar_t kModeChanged[] = L"ModeChanged";
constexpr wchar_t kHDRChanged[] = L"HDRChanged";
constexpr wchar_t kModeDevice[] = L"\\\\.\\DISPLAY1";
constexpr wchar_t kHDRDevice[] = L"\\\\.\\DISPLAY2";
void Check(bool condition, const char* message) {
if (!condition) {
std::cerr << "display_mode_manager_test: " << message << '\n';
std::exit(1);
}
}
class FakeRecoveryBackend final : public DisplayRecoveryBackend {
public:
std::map<std::wstring, DWORD> dwords;
std::map<std::wstring, std::wstring> strings;
std::map<std::wstring, bool> device_present = {{kModeDevice, true}, {kHDRDevice, true}};
std::vector<std::wstring> events;
std::wstring expected_mode_device = kModeDevice;
std::wstring expected_hdr_device = kHDRDevice;
bool mode_restore_succeeds = true;
bool hdr_restore_succeeds = true;
bool mode_marker_clear_succeeds = true;
bool hdr_marker_clear_succeeds = true;
bool delete_succeeds = true;
bool final_mode_marker_write_succeeds = true;
bool final_hdr_marker_write_succeeds = true;
int delete_attempts = 0;
void SeedBoth() {
dwords[kVersion] = 1;
dwords[kModeChanged] = 1;
dwords[kHDRChanged] = 1;
dwords[kOriginalWidth] = 3840;
dwords[kOriginalHeight] = 2160;
dwords[kOriginalRefreshRate] = 60;
dwords[kOriginalHDR] = 0;
strings[kModeDeviceName] = kModeDevice;
strings[kHDRDeviceName] = kHDRDevice;
}
bool RecordExists() const override { return !dwords.empty() || !strings.empty(); }
bool ReadDWORD(const wchar_t* value_name, DWORD& value) override {
const auto it = dwords.find(value_name);
if (it == dwords.end()) return false;
value = it->second;
return true;
}
bool ReadString(const wchar_t* value_name, std::wstring& value) override {
const auto it = strings.find(value_name);
if (it == strings.end()) return false;
value = it->second;
return true;
}
bool WriteDWORD(const wchar_t* value_name, DWORD value) override {
const std::wstring name(value_name);
events.push_back(L"write:" + name + L"=" + std::to_wstring(value));
if ((name == kModeChanged && value == 1 && !final_mode_marker_write_succeeds) ||
(name == kHDRChanged && value == 1 && !final_hdr_marker_write_succeeds)) {
return false;
}
dwords[name] = value;
return true;
}
bool WriteString(const wchar_t* value_name, const std::wstring& value) override {
const std::wstring name(value_name);
events.push_back(L"write:" + name);
strings[name] = value;
return true;
}
bool IsDevicePresent(const std::wstring& device_name) const override {
const auto it = device_present.find(device_name);
return it != device_present.end() && it->second;
}
bool RestoreMode(const std::wstring& device_name, DWORD width, DWORD height, DWORD refresh_rate) override {
Check(device_name == expected_mode_device, "mode restore must use its persisted display");
Check(width == 3840 && height == 2160 && refresh_rate == 60, "mode restore must use persisted originals");
events.push_back(L"restore:mode");
return mode_restore_succeeds;
}
bool RestoreHDR(const std::wstring& device_name, bool enabled) override {
Check(device_name == expected_hdr_device, "HDR restore must use its independently persisted display");
Check(!enabled, "HDR restore must use the persisted original state");
events.push_back(L"restore:hdr");
return hdr_restore_succeeds;
}
bool ClearMarker(const wchar_t* value_name) override {
const std::wstring name(value_name);
events.push_back(L"clear:" + name);
const bool succeeds = name == kModeChanged ? mode_marker_clear_succeeds : hdr_marker_clear_succeeds;
if (succeeds) dwords[name] = 0;
return succeeds;
}
bool DeleteRecord() override {
++delete_attempts;
events.push_back(L"delete");
if (!delete_succeeds) return false;
dwords.clear();
strings.clear();
return true;
}
};
size_t EventIndex(const std::vector<std::wstring>& events, const std::wstring& event) {
for (size_t index = 0; index < events.size(); ++index) {
if (events[index] == event) return index;
}
return events.size();
}
bool ApplyModeAfterPreparing(FakeRecoveryBackend& backend) {
if (!DisplayModeManager::PrepareModeRecovery(backend, kModeDevice, 3840, 2160, 60)) {
return false;
}
backend.events.push_back(L"os:mode");
return true;
}
bool ApplyHDRAfterPreparing(FakeRecoveryBackend& backend) {
if (!DisplayModeManager::PrepareHDRRecovery(backend, kHDRDevice, false)) return false;
backend.events.push_back(L"os:hdr");
return true;
}
void TestMarkersArePersistedBeforeMutation() {
FakeRecoveryBackend mode;
Check(ApplyModeAfterPreparing(mode), "a complete mode recovery record must admit the OS mutation");
const size_t mode_marker = EventIndex(mode.events, L"write:ModeChanged=1");
const size_t mode_os = EventIndex(mode.events, L"os:mode");
Check(mode_marker < mode_os, "the mode marker must be durable before the OS mutation");
Check(
EventIndex(mode.events, L"write:ModeDeviceName") < mode_marker &&
EventIndex(mode.events, L"write:OriginalWidth=3840") < mode_marker &&
EventIndex(mode.events, L"write:OriginalHeight=2160") < mode_marker &&
EventIndex(mode.events, L"write:OriginalRefreshRate=60") < mode_marker,
"all mode originals must precede the operation marker");
FakeRecoveryBackend hdr;
Check(ApplyHDRAfterPreparing(hdr), "a complete HDR recovery record must admit the OS mutation");
const size_t hdr_marker = EventIndex(hdr.events, L"write:HDRChanged=1");
Check(
EventIndex(hdr.events, L"write:HDRDeviceName") < hdr_marker &&
EventIndex(hdr.events, L"write:OriginalHDREnabled=0") < hdr_marker &&
hdr_marker < EventIndex(hdr.events, L"os:hdr"),
"the HDR original and marker must be durable before the OS mutation");
FakeRecoveryBackend failed_marker;
failed_marker.final_mode_marker_write_succeeds = false;
Check(
!ApplyModeAfterPreparing(failed_marker),
"an OS mutation must not run when its final recovery marker cannot be persisted");
Check(
EventIndex(failed_marker.events, L"os:mode") == failed_marker.events.size(),
"a failed marker write must leave the display untouched");
}
void TestMalformedRecordIsIgnored() {
FakeRecoveryBackend backend;
backend.SeedBoth();
backend.dwords[kVersion] = 2;
backend.strings[kLegacyDeviceName] = kModeDevice;
Check(!DisplayModeManager::RecoverIfNeeded(backend), "an unknown recovery version must be ignored");
Check(
EventIndex(backend.events, L"restore:mode") == backend.events.size() &&
EventIndex(backend.events, L"restore:hdr") == backend.events.size(),
"a malformed record must not reach display APIs");
Check(backend.delete_attempts == 1 && !backend.RecordExists(), "a malformed record must be discarded");
}
void TestValidModeSurvivesMalformedHDR() {
FakeRecoveryBackend backend;
backend.SeedBoth();
backend.dwords[kOriginalHDR] = 2;
Check(
DisplayModeManager::RecoverIfNeeded(backend),
"malformed HDR evidence must not discard an independently valid mode restore");
Check(
EventIndex(backend.events, L"restore:mode") < backend.events.size() &&
EventIndex(backend.events, L"restore:hdr") == backend.events.size(),
"only the valid mode operation may reach a display API");
Check(
EventIndex(backend.events, L"clear:HDRChanged") < backend.events.size(),
"the malformed HDR operation must be discarded independently");
}
void TestValidHDRSurvivesMalformedMode() {
FakeRecoveryBackend backend;
backend.SeedBoth();
backend.dwords.erase(kOriginalHeight);
Check(
DisplayModeManager::RecoverIfNeeded(backend),
"malformed mode evidence must not discard an independently valid HDR restore");
Check(
EventIndex(backend.events, L"restore:mode") == backend.events.size() &&
EventIndex(backend.events, L"restore:hdr") < backend.events.size(),
"only the valid HDR operation may reach a display API");
Check(
EventIndex(backend.events, L"clear:ModeChanged") < backend.events.size(),
"the malformed mode operation must be discarded independently");
}
void TestPreparationClearsMalformedSibling() {
FakeRecoveryBackend mode;
mode.SeedBoth();
mode.dwords[kModeChanged] = 0;
mode.dwords[kOriginalHDR] = 2;
Check(ApplyModeAfterPreparing(mode), "a malformed HDR sibling must not block a new valid mode operation");
Check(mode.dwords[kHDRChanged] == 0, "mode preparation must not preserve malformed HDR evidence");
FakeRecoveryBackend hdr;
hdr.SeedBoth();
hdr.dwords[kHDRChanged] = 0;
hdr.dwords.erase(kOriginalHeight);
Check(ApplyHDRAfterPreparing(hdr), "a malformed mode sibling must not block a new valid HDR operation");
Check(hdr.dwords[kModeChanged] == 0, "HDR preparation must not preserve malformed mode evidence");
}
void TestModeAndHDRRestoreIndependently() {
FakeRecoveryBackend backend;
backend.SeedBoth();
backend.mode_restore_succeeds = false;
Check(!DisplayModeManager::RecoverIfNeeded(backend), "one failed restore must retain the record");
Check(
EventIndex(backend.events, L"restore:mode") < backend.events.size() &&
EventIndex(backend.events, L"restore:hdr") < backend.events.size(),
"mode failure must not prevent the independent HDR restore");
Check(backend.dwords[kModeChanged] == 1, "the failed mode marker must remain set");
Check(backend.dwords[kHDRChanged] == 0, "the successful HDR marker must be cleared");
backend.events.clear();
backend.mode_restore_succeeds = true;
Check(DisplayModeManager::RecoverIfNeeded(backend), "a later pass must finish the retained mode restore");
Check(
EventIndex(backend.events, L"restore:mode") < backend.events.size() &&
EventIndex(backend.events, L"restore:hdr") == backend.events.size(),
"a later pass must not repeat the completed HDR restore");
}
void TestFailedRestoreRemainsForTopologyRetry() {
FakeRecoveryBackend backend;
backend.SeedBoth();
backend.dwords[kHDRChanged] = 0;
backend.device_present[kModeDevice] = false;
Check(!DisplayModeManager::RecoverIfNeeded(backend), "an absent display must retain its marked restore");
Check(backend.dwords[kModeChanged] == 1, "an absent display must keep its operation marker");
Check(
EventIndex(backend.events, L"restore:mode") == backend.events.size(),
"an absent display must not call its restore API");
backend.events.clear();
backend.device_present[kModeDevice] = true;
Check(
DisplayModeManager::RecoverIfNeeded(backend), "a synchronous topology retry must restore a reconnected display");
Check(
EventIndex(backend.events, L"restore:mode") < backend.events.size(),
"the topology retry must attempt the retained restore");
}
void TestMarkerClearFailureRemainsRetryable() {
FakeRecoveryBackend backend;
backend.SeedBoth();
backend.dwords[kHDRChanged] = 0;
backend.mode_marker_clear_succeeds = false;
Check(!DisplayModeManager::RecoverIfNeeded(backend), "marker persistence is part of recovery completion");
Check(backend.dwords[kModeChanged] == 1, "a failed marker clear must retain idempotent recovery evidence");
backend.events.clear();
backend.mode_marker_clear_succeeds = true;
Check(DisplayModeManager::RecoverIfNeeded(backend), "a later pass must retry after marker persistence failure");
Check(
EventIndex(backend.events, L"restore:mode") < backend.events.size(),
"the retained marker must cause the restore to be retried");
}
void TestLifecycleCleanupPreservesPersistedSibling() {
FakeRecoveryBackend mode_completed;
mode_completed.SeedBoth();
Check(
DisplayModeManager::CompleteRecoveryOperationForTesting(mode_completed, true),
"successful mode cleanup must durably clear its own marker");
Check(
mode_completed.dwords[kModeChanged] == 0 && mode_completed.dwords[kHDRChanged] == 1,
"mode cleanup must preserve a persisted HDR sibling even without local HDR ownership");
Check(
mode_completed.dwords[kOriginalHDR] == 0 && mode_completed.strings[kHDRDeviceName] == kHDRDevice &&
mode_completed.delete_attempts == 0,
"mode cleanup must retain the HDR original and avoid deleting its record");
FakeRecoveryBackend hdr_failed_apply;
hdr_failed_apply.SeedBoth();
Check(
DisplayModeManager::CompleteRecoveryOperationForTesting(hdr_failed_apply, false),
"failed HDR apply cleanup must durably clear its own marker");
Check(
hdr_failed_apply.dwords[kHDRChanged] == 0 && hdr_failed_apply.dwords[kModeChanged] == 1,
"HDR cleanup must preserve a persisted mode sibling even without local mode ownership");
Check(
hdr_failed_apply.dwords[kOriginalWidth] == 3840 && hdr_failed_apply.strings[kModeDeviceName] == kModeDevice &&
hdr_failed_apply.delete_attempts == 0,
"HDR cleanup must retain the mode original and avoid deleting its record");
}
void TestReleasedLiveOperationRecoversAfterReconnect() {
FakeRecoveryBackend backend;
backend.SeedBoth();
backend.device_present[kModeDevice] = false;
Check(
!DisplayModeManager::RecoverIfNeededForTesting(backend, true, true),
"topology recovery must not take either genuinely live operation");
Check(backend.events.empty(), "live operations must not reach restore or persistence APIs");
Check(
!DisplayModeManager::RecoverIfNeededForTesting(backend, false, true),
"a released operation must remain marked while its target is absent");
Check(
backend.dwords[kModeChanged] == 1 && backend.dwords[kHDRChanged] == 1,
"an absent released mode and its live HDR sibling must both retain their markers");
backend.events.clear();
backend.device_present[kModeDevice] = true;
Check(
DisplayModeManager::RecoverIfNeededForTesting(backend, false, true),
"a topology retry must restore the released mode after reconnect");
Check(
EventIndex(backend.events, L"restore:mode") < backend.events.size() &&
EventIndex(backend.events, L"restore:hdr") == backend.events.size(),
"reconnect recovery must restore the released mode without stealing live HDR");
Check(
backend.dwords[kModeChanged] == 0 && backend.dwords[kHDRChanged] == 1 && backend.delete_attempts == 0,
"reconnect recovery must preserve the genuinely live sibling record");
}
void TestVersionlessModeRecoveryAndCleanup() {
FakeRecoveryBackend backend;
backend.dwords[kModeChanged] = 1;
backend.dwords[kHDRChanged] = 0;
backend.dwords[kOriginalWidth] = 3840;
backend.dwords[kOriginalHeight] = 2160;
backend.dwords[kOriginalRefreshRate] = 60;
backend.strings[kLegacyDeviceName] = kModeDevice;
Check(DisplayModeManager::RecoverIfNeeded(backend), "the released versionless mode layout must be recovered");
Check(
EventIndex(backend.events, L"restore:mode") < backend.events.size(),
"versionless mode recovery must use the backend restore");
Check(
backend.delete_attempts == 1 && !backend.RecordExists(),
"completed versionless mode recovery must clean the legacy DeviceName and record");
}
void TestVersionlessHDRRecoveryAndCleanup() {
FakeRecoveryBackend backend;
backend.dwords[kModeChanged] = 0;
backend.dwords[kHDRChanged] = 1;
backend.dwords[kOriginalHDR] = 0;
backend.strings[kLegacyDeviceName] = kHDRDevice;
Check(DisplayModeManager::RecoverIfNeeded(backend), "the released versionless HDR layout must be recovered");
Check(
EventIndex(backend.events, L"restore:hdr") < backend.events.size(),
"versionless HDR recovery must use the backend restore");
Check(
backend.delete_attempts == 1 && !backend.RecordExists(),
"completed versionless HDR recovery must clean the legacy DeviceName and record");
}
void TestVersionlessModeAndHDRUseSharedDevice() {
FakeRecoveryBackend backend;
backend.dwords[kModeChanged] = 1;
backend.dwords[kHDRChanged] = 1;
backend.dwords[kOriginalWidth] = 3840;
backend.dwords[kOriginalHeight] = 2160;
backend.dwords[kOriginalRefreshRate] = 60;
backend.dwords[kOriginalHDR] = 0;
backend.strings[kLegacyDeviceName] = kModeDevice;
backend.expected_hdr_device = kModeDevice;
Check(
DisplayModeManager::RecoverIfNeeded(backend),
"both versionless operations must recover from their shared DeviceName");
Check(
EventIndex(backend.events, L"restore:mode") < backend.events.size() &&
EventIndex(backend.events, L"restore:hdr") < backend.events.size(),
"the released shared-device layout must restore mode and HDR independently");
Check(
backend.delete_attempts == 1 && !backend.RecordExists(),
"shared versionless recovery must remove the legacy record after both markers clear");
}
void TestCleanupFailureDoesNotBlockNewOverride() {
FakeRecoveryBackend backend;
backend.SeedBoth();
backend.dwords[kHDRChanged] = 0;
backend.delete_succeeds = false;
Check(
DisplayModeManager::RecoverIfNeeded(backend),
"successful restoration must complete even when stale-value deletion fails");
Check(backend.dwords[kModeChanged] == 0, "the successful restore marker must be clear");
Check(backend.delete_attempts == 1, "completed recovery must make one best-effort cleanup attempt");
backend.events.clear();
Check(ApplyModeAfterPreparing(backend), "failed cleanup must not block persistence or admission of a fresh override");
Check(
backend.dwords[kModeChanged] == 1 &&
EventIndex(backend.events, L"write:ModeChanged=1") < EventIndex(backend.events, L"os:mode"),
"the fresh override must replace stale values with a pre-mutation marker");
}
} // namespace
} // namespace mpv
int main() {
mpv::TestMarkersArePersistedBeforeMutation();
mpv::TestMalformedRecordIsIgnored();
mpv::TestValidModeSurvivesMalformedHDR();
mpv::TestValidHDRSurvivesMalformedMode();
mpv::TestPreparationClearsMalformedSibling();
mpv::TestModeAndHDRRestoreIndependently();
mpv::TestFailedRestoreRemainsForTopologyRetry();
mpv::TestMarkerClearFailureRemainsRetryable();
mpv::TestLifecycleCleanupPreservesPersistedSibling();
mpv::TestReleasedLiveOperationRecoversAfterReconnect();
mpv::TestVersionlessModeRecoveryAndCleanup();
mpv::TestVersionlessHDRRecoveryAndCleanup();
mpv::TestVersionlessModeAndHDRUseSharedDevice();
mpv::TestCleanupFailureDoesNotBlockNewOverride();
std::cout << "display_mode_manager_test: PASS\n";
return 0;
}
+355 -42
View File
@@ -1,11 +1,26 @@
#include "mpv_player.h"
#include <commctrl.h>
#include <windowsx.h>
#include <unordered_map>
#include "sanitize_utf8.h"
namespace mpv {
struct InnerWindowSubclassState {
HWND hwnd = nullptr;
std::atomic<HWND> forward_target{nullptr};
std::atomic<bool> active{false};
UINT_PTR subclass_id = 0;
// Guarded by g_inner_subclasses_mutex.
bool installed = false;
// While true, the window thread may still remove this generation, so a
// replacement must not adopt it.
bool removal_pending = false;
};
namespace {
flutter::EncodableValue NodeToEncodableValue(const mpv_node* node) {
@@ -51,8 +66,8 @@ flutter::EncodableValue NodeToEncodableValue(const mpv_node* node) {
// DComp-mode input forwarding. mpv's inner window lives on mpv's own thread
// 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). Subclass the inner window
// (legal within one process, even across threads) and forward mouse and pointer
// 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.
@@ -74,24 +89,37 @@ static_assert(IsFlutterPointerMessage(WM_POINTERUP));
static_assert(IsFlutterPointerMessage(WM_POINTERLEAVE));
static_assert(!IsFlutterPointerMessage(WM_MOUSEMOVE));
WNDPROC g_mpv_inner_original_proc = nullptr;
HWND g_mpv_inner_hwnd = nullptr;
HWND g_forward_target_view = nullptr;
std::mutex g_inner_subclasses_mutex;
std::unordered_map<HWND, std::shared_ptr<InnerWindowSubclassState>> g_inner_subclasses;
std::atomic<uint64_t> g_next_inner_subclass_generation{1};
LRESULT CALLBACK MpvInnerSubclassProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
if (IsFlutterPointerMessage(message)) {
HWND view = g_forward_target_view;
if (view) {
// 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);
}
std::shared_ptr<InnerWindowSubclassState> FindInnerSubclassState(HWND hwnd, DWORD_PTR reference_data) {
std::lock_guard<std::mutex> lock(g_inner_subclasses_mutex);
const auto it = g_inner_subclasses.find(hwnd);
if (it == g_inner_subclasses.end() || reinterpret_cast<DWORD_PTR>(it->second.get()) != reference_data) {
return nullptr;
}
return it->second;
}
LRESULT CALLBACK MpvInnerSubclassProc(
HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam, UINT_PTR subclass_id, DWORD_PTR reference_data) {
const auto state = FindInnerSubclassState(hwnd, reference_data);
if (!state) {
return ::DefSubclassProc(hwnd, message, wparam, lparam);
}
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);
return 0;
}
if (message >= WM_MOUSEFIRST && message <= WM_MOUSELAST) {
HWND view = g_forward_target_view;
if (active && message >= WM_MOUSEFIRST && message <= WM_MOUSELAST) {
if (view) {
LPARAM forwarded = lparam;
if (message != WM_MOUSEWHEEL && message != WM_MOUSEHWHEEL) {
@@ -101,25 +129,287 @@ LRESULT CALLBACK MpvInnerSubclassProc(HWND hwnd, UINT message, WPARAM wparam, LP
::MapWindowPoints(hwnd, view, &pt, 1);
forwarded = MAKELPARAM(pt.x, pt.y);
}
::PostMessage(view, message, wparam, forwarded);
::PostMessageW(view, message, wparam, forwarded);
}
return 0;
}
return ::CallWindowProc(g_mpv_inner_original_proc, hwnd, message, wparam, lparam);
if (message == WM_NCDESTROY) {
state->active.store(false, std::memory_order_release);
state->forward_target.store(nullptr, std::memory_order_release);
::RemoveWindowSubclass(hwnd, MpvInnerSubclassProc, subclass_id);
std::lock_guard<std::mutex> lock(g_inner_subclasses_mutex);
const auto it = g_inner_subclasses.find(hwnd);
if (it != g_inner_subclasses.end() && it->second.get() == state.get()) {
g_inner_subclasses.erase(it);
}
}
return ::DefSubclassProc(hwnd, message, wparam, lparam);
}
// Subclass mpv's lazily-created inner window if it exists and isn't yet
// subclassed (or was recreated). Idempotent; callable from any thread in
// this process.
void EnsureMpvInnerSubclassed(HWND host) {
if (!host) {
constexpr UINT kSubclassOwnershipMessage = WM_APP + 0x0504;
enum class SubclassOwnershipActionPhase {
kPending,
kRunning,
kCompleted,
};
struct SubclassOwnershipAction {
std::shared_ptr<InnerWindowSubclassState> state;
bool install;
std::mutex mutex;
SubclassOwnershipActionPhase phase = SubclassOwnershipActionPhase::kPending;
bool cancelled = false;
bool success = false;
};
std::mutex g_subclass_actions_mutex;
std::unordered_map<UINT_PTR, std::shared_ptr<SubclassOwnershipAction>> g_subclass_actions;
std::atomic<UINT_PTR> g_next_subclass_action{1};
void ForgetInnerSubclassState(const std::shared_ptr<InnerWindowSubclassState>& state) {
std::lock_guard<std::mutex> lock(g_inner_subclasses_mutex);
const auto it = g_inner_subclasses.find(state->hwnd);
if (it != g_inner_subclasses.end() && it->second.get() == state.get()) {
g_inner_subclasses.erase(it);
}
}
bool ApplySubclassOwnershipAction(const SubclassOwnershipAction& action) {
if (action.install) {
return ::SetWindowSubclass(
action.state->hwnd, MpvInnerSubclassProc, action.state->subclass_id,
reinterpret_cast<DWORD_PTR>(action.state.get())) != FALSE;
}
std::lock_guard<std::mutex> lock(g_inner_subclasses_mutex);
const auto it = g_inner_subclasses.find(action.state->hwnd);
if (it == g_inner_subclasses.end() || it->second.get() != action.state.get()) {
return false;
}
const bool removed =
::RemoveWindowSubclass(action.state->hwnd, MpvInnerSubclassProc, action.state->subclass_id) != FALSE;
if (removed) {
g_inner_subclasses.erase(it);
} else {
action.state->removal_pending = false;
}
return removed;
}
void ExecuteSubclassOwnershipAction(const std::shared_ptr<SubclassOwnershipAction>& action) {
{
std::lock_guard<std::mutex> lock(action->mutex);
if (action->phase != SubclassOwnershipActionPhase::kPending) return;
if (action->cancelled) {
action->phase = SubclassOwnershipActionPhase::kCompleted;
if (action->install) {
ForgetInnerSubclassState(action->state);
}
return;
}
action->phase = SubclassOwnershipActionPhase::kRunning;
}
const bool applied = ApplySubclassOwnershipAction(*action);
bool cancelled_install = false;
{
std::lock_guard<std::mutex> lock(action->mutex);
cancelled_install = action->install && action->cancelled;
if (!cancelled_install) {
action->success = applied;
action->phase = SubclassOwnershipActionPhase::kCompleted;
}
}
if (!cancelled_install) {
if (action->install && !applied) {
ForgetInnerSubclassState(action->state);
}
return;
}
HWND inner = ::FindWindowExW(host, nullptr, nullptr, nullptr);
if (inner && inner != g_mpv_inner_hwnd) {
g_mpv_inner_hwnd = inner;
g_mpv_inner_original_proc = reinterpret_cast<WNDPROC>(
::SetWindowLongPtrW(inner, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(MpvInnerSubclassProc)));
// A timeout can race an action that the window thread has already begun.
// Remove a late install on that same thread before releasing the action's
// shared ownership of the reference data used by the subclass callback.
const bool detached = !applied || ::RemoveWindowSubclass(
action->state->hwnd, MpvInnerSubclassProc, action->state->subclass_id) != FALSE;
if (detached) {
ForgetInnerSubclassState(action->state);
}
std::lock_guard<std::mutex> lock(action->mutex);
action->success = false;
action->phase = SubclassOwnershipActionPhase::kCompleted;
}
LRESULT CALLBACK SubclassOwnershipHook(int code, WPARAM wparam, LPARAM lparam) {
if (code >= 0) {
const auto* message = reinterpret_cast<const CWPSTRUCT*>(lparam);
if (message && message->message == kSubclassOwnershipMessage) {
std::shared_ptr<SubclassOwnershipAction> action;
{
std::lock_guard<std::mutex> lock(g_subclass_actions_mutex);
const auto it = g_subclass_actions.find(static_cast<UINT_PTR>(message->wParam));
if (it != g_subclass_actions.end() && it->second->state->hwnd == message->hwnd) {
action = it->second;
}
}
if (action) {
ExecuteSubclassOwnershipAction(action);
}
}
}
return ::CallNextHookEx(nullptr, code, wparam, lparam);
}
bool RunSubclassOwnershipActionOnWindowThread(const std::shared_ptr<SubclassOwnershipAction>& action) {
DWORD window_thread = ::GetWindowThreadProcessId(action->state->hwnd, nullptr);
if (!window_thread) return false;
if (window_thread == ::GetCurrentThreadId()) {
ExecuteSubclassOwnershipAction(action);
std::lock_guard<std::mutex> lock(action->mutex);
return action->success;
}
HHOOK hook = ::SetWindowsHookExW(WH_CALLWNDPROC, SubclassOwnershipHook, nullptr, window_thread);
if (!hook) return false;
const UINT_PTR action_id = g_next_subclass_action.fetch_add(1, std::memory_order_relaxed);
{
std::lock_guard<std::mutex> lock(g_subclass_actions_mutex);
g_subclass_actions[action_id] = action;
}
DWORD_PTR message_result = 0;
::SendMessageTimeoutW(
action->state->hwnd, kSubclassOwnershipMessage, action_id, 0, SMTO_ABORTIFHUNG, 1000, &message_result);
bool success = false;
{
// Completion and cancellation use the same lock. If the callback won the
// race, its acknowledged result is authoritative. Otherwise it observes
// cancellation and cannot leave a late install referencing released data.
std::lock_guard<std::mutex> lock(action->mutex);
if (action->phase == SubclassOwnershipActionPhase::kCompleted) {
success = action->success;
} else {
action->cancelled = true;
}
}
{
std::lock_guard<std::mutex> lock(g_subclass_actions_mutex);
const auto it = g_subclass_actions.find(action_id);
if (it != g_subclass_actions.end() && it->second == action) {
g_subclass_actions.erase(it);
}
}
::UnhookWindowsHookEx(hook);
return success;
}
std::shared_ptr<InnerWindowSubclassState> InstallMpvInnerSubclass(HWND inner, HWND forward_target) {
if (!inner || !forward_target) return nullptr;
std::shared_ptr<InnerWindowSubclassState> state;
{
std::lock_guard<std::mutex> lock(g_inner_subclasses_mutex);
const auto existing = g_inner_subclasses.find(inner);
if (existing != g_inner_subclasses.end()) {
const auto& retained = existing->second;
if (!retained->installed || retained->active.load(std::memory_order_acquire) || retained->removal_pending) {
return nullptr;
}
// 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->forward_target.store(forward_target, std::memory_order_release);
retained->active.store(true, std::memory_order_release);
return retained;
}
state = std::make_shared<InnerWindowSubclassState>();
state->hwnd = inner;
state->forward_target.store(forward_target, std::memory_order_relaxed);
state->subclass_id = g_next_inner_subclass_generation.fetch_add(1, std::memory_order_relaxed);
// Publish the state before installing the helper-chain entry. The
// callback's reference data identifies this exact generation, so an old
// callback can never resolve a replacement generation that reuses HWND.
g_inner_subclasses[inner] = state;
}
auto action = std::make_shared<SubclassOwnershipAction>();
action->state = state;
action->install = true;
if (!RunSubclassOwnershipActionOnWindowThread(action)) {
bool action_never_started = false;
{
std::lock_guard<std::mutex> lock(action->mutex);
action_never_started = action->phase == SubclassOwnershipActionPhase::kPending;
}
if (action_never_started) {
ForgetInnerSubclassState(state);
}
return nullptr;
}
{
std::lock_guard<std::mutex> lock(g_inner_subclasses_mutex);
const auto it = g_inner_subclasses.find(inner);
if (it == g_inner_subclasses.end() || it->second.get() != state.get()) {
return nullptr;
}
state->installed = true;
state->active.store(true, std::memory_order_release);
}
return state;
}
void DetachMpvInnerSubclassState(const std::shared_ptr<InnerWindowSubclassState>& state) {
if (!state) return;
// Invalidate forwarding before removing the helper-chain entry. A callback
// already holding this generation can still call DefSubclassProc, but can
// no longer target a replacement Flutter/player generation.
{
std::lock_guard<std::mutex> lock(g_inner_subclasses_mutex);
const auto it = g_inner_subclasses.find(state->hwnd);
if (it == g_inner_subclasses.end() || it->second.get() != state.get()) {
return;
}
state->active.store(false, std::memory_order_release);
state->forward_target.store(nullptr, std::memory_order_release);
state->removal_pending = true;
}
auto action = std::make_shared<SubclassOwnershipAction>();
action->state = state;
action->install = false;
const bool detached = RunSubclassOwnershipActionOnWindowThread(action);
if (!detached) {
bool removal_not_applied = false;
{
std::lock_guard<std::mutex> lock(action->mutex);
removal_not_applied = action->phase == SubclassOwnershipActionPhase::kPending ||
(action->phase == SubclassOwnershipActionPhase::kCompleted && !action->success);
}
if (removal_not_applied) {
std::lock_guard<std::mutex> lock(g_inner_subclasses_mutex);
const auto it = g_inner_subclasses.find(state->hwnd);
if (it != g_inner_subclasses.end() && it->second.get() == state.get()) {
// RunSubclassOwnershipActionOnWindowThread has already unregistered
// the cancelled action and hook. The installed entry is now stable
// and can be reactivated by a replacement owner.
state->removal_pending = false;
}
}
}
if (!detached && !::IsWindow(state->hwnd)) {
// A destroyed HWND has already discarded its subclass chain, so no
// callback can retain the reference data even if dispatch was unavailable.
ForgetInnerSubclassState(state);
}
}
@@ -129,6 +419,29 @@ MpvPlayer::MpvPlayer(bool audio_only) : audio_only_(audio_only) {}
MpvPlayer::~MpvPlayer() { Dispose(); }
void MpvPlayer::EnsureMpvInnerSubclassed() {
if (!hwnd_ || !forward_target_view_) return;
HWND inner = ::FindWindowExW(hwnd_, nullptr, nullptr, nullptr);
if (!inner) return;
std::lock_guard<std::mutex> lock(inner_subclass_mutex_);
if (inner_subclass_ && inner_subclass_->hwnd == inner && inner_subclass_->active.load(std::memory_order_acquire)) {
inner_subclass_->forward_target.store(forward_target_view_, std::memory_order_release);
return;
}
DetachMpvInnerSubclassState(inner_subclass_);
inner_subclass_.reset();
inner_subclass_ = InstallMpvInnerSubclass(inner, forward_target_view_);
}
void MpvPlayer::DetachMpvInnerSubclass() {
std::lock_guard<std::mutex> lock(inner_subclass_mutex_);
DetachMpvInnerSubclassState(inner_subclass_);
inner_subclass_.reset();
}
bool MpvPlayer::Initialize(HWND view) {
if (mpv_) {
return true; // Already initialized.
@@ -166,7 +479,7 @@ bool MpvPlayer::Initialize(HWND view) {
mpv_ = nullptr;
return false;
}
g_forward_target_view = view;
forward_target_view_ = view;
// Set the wid option to embed mpv in our window.
int64_t wid = reinterpret_cast<int64_t>(hwnd_);
@@ -208,6 +521,8 @@ bool MpvPlayer::Initialize(HWND view) {
int err = mpv_initialize(mpv_);
if (err < 0) {
if (hwnd_) {
DetachMpvInnerSubclass();
forward_target_view_ = nullptr;
::DestroyWindow(hwnd_);
hwnd_ = nullptr;
}
@@ -244,17 +559,15 @@ void MpvPlayer::Dispose() {
auto* handle = mpv_;
mpv_ = nullptr;
// The input subclass must stop referencing this player generation before
// either the host HWND or the player object can be destroyed.
DetachMpvInnerSubclass();
forward_target_view_ = nullptr;
if (hwnd_) {
::ShowWindow(hwnd_, SW_HIDE);
::DestroyWindow(hwnd_);
hwnd_ = nullptr;
// The subclassed inner window died with hwnd_; clear the forwarding
// state. Only the owner of the window may do this: the audio-only core
// (which never has an hwnd_) must not wipe the video instance's state.
g_mpv_inner_hwnd = nullptr;
g_mpv_inner_original_proc = nullptr;
g_forward_target_view = nullptr;
}
if (handle) {
@@ -351,7 +664,7 @@ void MpvPlayer::SetRect(RECT rect, double device_pixel_ratio) {
// mpv creates its inner window lazily on its own thread; subclass it (and
// re-subclass if mpv ever recreates it) so mouse and pointer input over the
// video is forwarded to the Flutter view.
EnsureMpvInnerSubclassed(hwnd_);
EnsureMpvInnerSubclassed();
}
void MpvPlayer::SetVisible(bool visible) {
@@ -388,11 +701,11 @@ void MpvPlayer::LogRecovery(const std::string& text) {
SendEvent("log-message", data);
}
void MpvPlayer::TryAudioReload(const char* reason, int attempt) {
void MpvPlayer::TryAudioReload(const char* reason, int attempt, uint64_t request_generation) {
LogRecovery("issuing ao-reload (reason=" + std::string(reason) + ", attempt " + std::to_string(attempt) + ")");
const std::string reason_copy = reason;
CommandAsync({"ao-reload"}, [this, reason_copy, attempt](int error) {
audio_recovery_.CompleteReload();
CommandAsync({"ao-reload"}, [this, reason_copy, attempt, request_generation](int error) {
audio_recovery_.CompleteReload(request_generation);
LogRecovery(
"ao-reload completed (reason=" + reason_copy + ", attempt " + std::to_string(attempt) +
", error=" + std::to_string(error) + ")");
@@ -405,7 +718,7 @@ void MpvPlayer::MaybeRunAudioRecovery() {
return;
}
const char* reason = action.reason == plezy::mpv_common::AudioReloadReason::kResume ? "resume" : "null-fallback";
TryAudioReload(reason, action.attempt);
TryAudioReload(reason, action.attempt, action.request_generation);
if (action.exhausted) {
LogRecovery("audio recovery budget exhausted; waiting for device list change");
}
@@ -557,7 +870,7 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
// mpv's inner window exists by now (vo is configured); make sure the
// DComp-mode input forwarding subclass is installed. SetRect alone can
// miss it: the rect often settles before mpv creates the window.
EnsureMpvInnerSubclassed(hwnd_);
EnsureMpvInnerSubclassed();
SendEvent("playback-restart");
break;
}
+7 -1
View File
@@ -17,6 +17,7 @@
#include "../../../shared/mpv/mpv_player_common.h"
namespace mpv {
struct InnerWindowSubclassState;
// Wrapper for libmpv that handles initialization, commands, properties,
// and event dispatching.
@@ -96,12 +97,17 @@ class MpvPlayer {
void SendPropertyChange(const char* name, mpv_node* data);
void SendEvent(const std::string& name, const flutter::EncodableMap& data = {});
void MaybeRunAudioRecovery();
void TryAudioReload(const char* reason, int attempt);
void TryAudioReload(const char* reason, int attempt, uint64_t request_generation);
void LogRecovery(const std::string& text);
void EnsureMpvInnerSubclassed();
void DetachMpvInnerSubclass();
const bool audio_only_;
mpv_handle* mpv_ = nullptr;
HWND hwnd_ = nullptr;
HWND forward_target_view_ = nullptr;
std::mutex inner_subclass_mutex_;
std::shared_ptr<InnerWindowSubclassState> inner_subclass_;
std::thread event_thread_;
std::atomic<bool> running_{false};
@@ -1,5 +1,8 @@
#include <atomic>
#include <cstdlib>
#include <future>
#include <iostream>
#include <thread>
#include <utility>
#include "mpv_player.h"
@@ -11,6 +14,25 @@ class MpvPlayerPropertyContractTestPeer {
static void RegisterPendingPropertyWrite(MpvPlayer& player, MpvPlayer::StatusCallback callback) {
player.pending_requests_.RegisterStatus(std::move(callback));
}
static void RegisterPendingPropertyRead(MpvPlayer& player, MpvPlayer::GetPropertyCallback callback) {
player.pending_requests_.RegisterProperty(std::move(callback));
}
static void ConfigureInnerSubclass(MpvPlayer& player, HWND host, HWND target) {
player.hwnd_ = host;
player.forward_target_view_ = target;
}
static void EnsureInnerSubclass(MpvPlayer& player) { player.EnsureMpvInnerSubclassed(); }
static void DetachInnerSubclass(MpvPlayer& player) { player.DetachMpvInnerSubclass(); }
static const void* InnerSubclassIdentity(const MpvPlayer& player) { return player.inner_subclass_.get(); }
static void ReleaseTestWindows(MpvPlayer& player) {
player.DetachMpvInnerSubclass();
player.hwnd_ = nullptr;
player.forward_target_view_ = nullptr;
}
};
namespace {
@@ -22,6 +44,33 @@ void Check(bool condition, const char* message) {
}
}
std::atomic<int> g_forwarded_mouse_messages{0};
std::atomic<int> g_forwarded_pointer_messages{0};
constexpr UINT kBlockWindowThreadMessage = WM_APP + 0x0505;
std::atomic<HANDLE> g_block_entered{nullptr};
std::atomic<HANDLE> g_block_release{nullptr};
LRESULT CALLBACK CountingWindowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
if (message == kBlockWindowThreadMessage) {
const HANDLE entered = g_block_entered.load(std::memory_order_acquire);
const HANDLE release = g_block_release.load(std::memory_order_acquire);
if (entered && release) {
::SetEvent(entered);
::WaitForSingleObject(release, INFINITE);
}
return 0;
}
if (message == WM_POINTERUPDATE) {
g_forwarded_pointer_messages.fetch_add(1, std::memory_order_relaxed);
return 0;
}
if (message == WM_MOUSEMOVE) {
g_forwarded_mouse_messages.fetch_add(1, std::memory_order_relaxed);
return 0;
}
return ::DefWindowProcW(hwnd, message, wparam, lparam);
}
void TestUnavailablePropertyWriteFails() {
MpvPlayer player;
int callback_count = 0;
@@ -53,12 +102,271 @@ void TestPendingPropertyWriteFailsOnDispose() {
Check(callback_count == 1, "repeated dispose must not complete a property write twice");
}
void TestPendingRequestTypesRemainDistinctOnDispose() {
MpvPlayer player;
int write_count = 0;
int read_count = 0;
std::string read_value = "unexpected";
MpvPlayerPropertyContractTestPeer::RegisterPendingPropertyWrite(player, [&](int error) {
Check(error < 0, "cancelled property write must receive an error");
++write_count;
});
MpvPlayerPropertyContractTestPeer::RegisterPendingPropertyRead(player, [&](int error, const std::string& value) {
Check(error < 0, "cancelled property read must receive an error");
++read_count;
read_value = value;
});
player.Dispose();
Check(write_count == 1, "dispose must complete the typed write request exactly once");
Check(read_count == 1, "dispose must complete the typed read request exactly once");
Check(read_value.empty(), "cancelled property reads must not manufacture a value");
}
void TestInnerSubclassOwnershipIsSerializedAndDetached() {
struct TestWindows {
HWND target;
HWND host;
HWND inner;
WNDPROC inner_original;
DWORD owner_thread;
};
std::promise<TestWindows> windows_created;
auto windows_future = windows_created.get_future();
std::thread window_owner([&]() {
HWND target =
::CreateWindowExW(0, L"STATIC", L"", WS_OVERLAPPED, 0, 0, 100, 100, nullptr, nullptr, nullptr, nullptr);
HWND host = ::CreateWindowExW(0, L"STATIC", L"", WS_CHILD, 0, 0, 100, 100, target, nullptr, nullptr, nullptr);
HWND inner = ::CreateWindowExW(0, L"STATIC", L"", WS_CHILD, 0, 0, 100, 100, host, nullptr, nullptr, nullptr);
const auto target_original = reinterpret_cast<WNDPROC>(
::SetWindowLongPtrW(target, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(CountingWindowProc)));
const auto inner_original = reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(inner, GWLP_WNDPROC));
windows_created.set_value(TestWindows{target, host, inner, inner_original, ::GetCurrentThreadId()});
MSG message;
while (::GetMessageW(&message, nullptr, 0, 0) > 0) {
::TranslateMessage(&message);
::DispatchMessageW(&message);
}
::SetWindowLongPtrW(target, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(target_original));
::DestroyWindow(inner);
::DestroyWindow(host);
::DestroyWindow(target);
});
const TestWindows windows = windows_future.get();
Check(windows.target && windows.host && windows.inner, "test windows must be created");
MpvPlayer player;
MpvPlayerPropertyContractTestPeer::ConfigureInnerSubclass(player, windows.host, windows.target);
std::thread first([&]() { MpvPlayerPropertyContractTestPeer::EnsureInnerSubclass(player); });
std::thread second([&]() { MpvPlayerPropertyContractTestPeer::EnsureInnerSubclass(player); });
first.join();
second.join();
const auto installed = reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC));
Check(installed && installed != windows.inner_original, "exactly one subclass procedure must be installed");
::SendMessageW(windows.inner, WM_NULL, 0, 0);
::SendMessageW(windows.inner, WM_MOUSEMOVE, 0, MAKELPARAM(4, 7));
for (int attempt = 0; attempt < 100 && g_forwarded_mouse_messages.load(std::memory_order_relaxed) < 1; ++attempt) {
::Sleep(10);
}
Check(g_forwarded_mouse_messages.load(std::memory_order_relaxed) == 1, "active generation must forward mouse input");
MpvPlayerPropertyContractTestPeer::DetachInnerSubclass(player);
Check(
reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC)) == windows.inner_original,
"detach must restore the original procedure before window destruction");
::SendMessageW(windows.inner, WM_MOUSEMOVE, 0, MAKELPARAM(8, 9));
::Sleep(30);
Check(
g_forwarded_mouse_messages.load(std::memory_order_relaxed) == 1,
"a callback after detaching the old generation must be ignored");
MpvPlayerPropertyContractTestPeer::EnsureInnerSubclass(player);
const auto replacement = reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC));
Check(replacement && replacement != windows.inner_original, "a replacement generation must install cleanly");
::SendMessageW(windows.inner, WM_MOUSEMOVE, 0, MAKELPARAM(10, 11));
for (int attempt = 0; attempt < 100 && g_forwarded_mouse_messages.load(std::memory_order_relaxed) < 2; ++attempt) {
::Sleep(10);
}
Check(
g_forwarded_mouse_messages.load(std::memory_order_relaxed) == 2,
"replacement generation must own forwarding after installation");
MpvPlayerPropertyContractTestPeer::ReleaseTestWindows(player);
Check(
reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC)) == windows.inner_original,
"replacement detach must restore the original procedure");
::PostThreadMessageW(windows.owner_thread, WM_QUIT, 0, 0);
window_owner.join();
g_forwarded_mouse_messages.store(0, std::memory_order_relaxed);
}
void TestTimedOutSubclassDetachCanBeAdopted() {
struct TestWindows {
HWND target;
HWND host;
HWND inner;
WNDPROC inner_original;
DWORD owner_thread;
};
std::promise<TestWindows> windows_created;
auto windows_future = windows_created.get_future();
std::thread window_owner([&]() {
HWND target =
::CreateWindowExW(0, L"STATIC", L"", WS_OVERLAPPED, 0, 0, 100, 100, nullptr, nullptr, nullptr, nullptr);
HWND host = ::CreateWindowExW(0, L"STATIC", L"", WS_CHILD, 0, 0, 100, 100, target, nullptr, nullptr, nullptr);
HWND inner = ::CreateWindowExW(0, L"STATIC", L"", WS_CHILD, 0, 0, 100, 100, host, nullptr, nullptr, nullptr);
const auto target_original = reinterpret_cast<WNDPROC>(
::SetWindowLongPtrW(target, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(CountingWindowProc)));
const auto inner_original = reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(inner, GWLP_WNDPROC));
windows_created.set_value(TestWindows{target, host, inner, inner_original, ::GetCurrentThreadId()});
MSG message;
while (::GetMessageW(&message, nullptr, 0, 0) > 0) {
::TranslateMessage(&message);
::DispatchMessageW(&message);
}
::SetWindowLongPtrW(target, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(target_original));
::DestroyWindow(inner);
::DestroyWindow(host);
::DestroyWindow(target);
});
const TestWindows windows = windows_future.get();
Check(windows.target && windows.host && windows.inner, "detach-timeout test windows must be created");
const HANDLE block_entered = ::CreateEventW(nullptr, TRUE, FALSE, nullptr);
const HANDLE block_release = ::CreateEventW(nullptr, TRUE, FALSE, nullptr);
Check(block_entered && block_release, "detach-timeout synchronization events must be created");
g_block_entered.store(block_entered, std::memory_order_release);
g_block_release.store(block_release, std::memory_order_release);
g_forwarded_pointer_messages.store(0, std::memory_order_relaxed);
{
MpvPlayer original;
MpvPlayerPropertyContractTestPeer::ConfigureInnerSubclass(original, windows.host, windows.target);
MpvPlayerPropertyContractTestPeer::EnsureInnerSubclass(original);
Check(
reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC)) != windows.inner_original,
"the initial generation must be installed before forcing detach timeout");
const void* retained_generation = MpvPlayerPropertyContractTestPeer::InnerSubclassIdentity(original);
Check(retained_generation != nullptr, "the initial generation must have live state");
Check(
::PostMessageW(windows.target, kBlockWindowThreadMessage, 0, 0) != FALSE,
"the owner-thread blocking message must be posted");
Check(
::WaitForSingleObject(block_entered, 1000) == WAIT_OBJECT_0,
"the owner thread must enter the deterministic blocking message");
MpvPlayerPropertyContractTestPeer::DetachInnerSubclass(original);
MpvPlayer replacement;
MpvPlayerPropertyContractTestPeer::ConfigureInnerSubclass(replacement, windows.host, windows.target);
MpvPlayerPropertyContractTestPeer::EnsureInnerSubclass(replacement);
Check(
MpvPlayerPropertyContractTestPeer::InnerSubclassIdentity(replacement) == retained_generation,
"replacement must atomically adopt the retained generation");
Check(
reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC)) != windows.inner_original,
"replacement must adopt the retained installed generation without duplicate subclassing");
::SetEvent(block_release);
::SendMessageW(windows.inner, WM_POINTERUPDATE, 0, MAKELPARAM(12, 13));
Check(
g_forwarded_pointer_messages.load(std::memory_order_relaxed) == 1,
"the adopted generation must resume pointer forwarding");
MpvPlayerPropertyContractTestPeer::ReleaseTestWindows(replacement);
Check(
reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC)) == windows.inner_original,
"adopted generation cleanup must eventually restore the original procedure");
}
g_block_entered.store(nullptr, std::memory_order_release);
g_block_release.store(nullptr, std::memory_order_release);
::CloseHandle(block_entered);
::CloseHandle(block_release);
::PostThreadMessageW(windows.owner_thread, WM_QUIT, 0, 0);
window_owner.join();
g_forwarded_pointer_messages.store(0, std::memory_order_relaxed);
}
void TestTimedOutSubclassInstallCannotOutliveItsState() {
struct TestWindows {
HWND target;
HWND host;
HWND inner;
WNDPROC inner_original;
DWORD owner_thread;
};
std::promise<TestWindows> windows_created;
auto windows_future = windows_created.get_future();
std::promise<void> begin_dispatch;
auto begin_dispatch_future = begin_dispatch.get_future();
std::thread window_owner([&]() {
HWND target =
::CreateWindowExW(0, L"STATIC", L"", WS_OVERLAPPED, 0, 0, 100, 100, nullptr, nullptr, nullptr, nullptr);
HWND host = ::CreateWindowExW(0, L"STATIC", L"", WS_CHILD, 0, 0, 100, 100, target, nullptr, nullptr, nullptr);
HWND inner = ::CreateWindowExW(0, L"STATIC", L"", WS_CHILD, 0, 0, 100, 100, host, nullptr, nullptr, nullptr);
const auto inner_original = reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(inner, GWLP_WNDPROC));
windows_created.set_value(TestWindows{target, host, inner, inner_original, ::GetCurrentThreadId()});
// Keep the owning thread alive but unavailable long enough for
// SendMessageTimeoutW to cancel the cross-thread ownership action.
begin_dispatch_future.wait();
MSG message;
while (::GetMessageW(&message, nullptr, 0, 0) > 0) {
::TranslateMessage(&message);
::DispatchMessageW(&message);
}
::DestroyWindow(inner);
::DestroyWindow(host);
::DestroyWindow(target);
});
const TestWindows windows = windows_future.get();
Check(windows.target && windows.host && windows.inner, "timeout test windows must be created");
{
MpvPlayer player;
MpvPlayerPropertyContractTestPeer::ConfigureInnerSubclass(player, windows.host, windows.target);
MpvPlayerPropertyContractTestPeer::EnsureInnerSubclass(player);
MpvPlayerPropertyContractTestPeer::ReleaseTestWindows(player);
}
// The action and its subclass reference data have now left caller scope.
// Dispatching the timed-out message must neither install late nor touch the
// destroyed caller state.
begin_dispatch.set_value();
::SendMessageW(windows.inner, WM_NULL, 0, 0);
Check(
reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC)) == windows.inner_original,
"a timed-out action must remain cancelled after the window thread resumes");
::PostThreadMessageW(windows.owner_thread, WM_QUIT, 0, 0);
window_owner.join();
}
} // namespace
} // namespace mpv
int main() {
mpv::TestUnavailablePropertyWriteFails();
mpv::TestPendingPropertyWriteFailsOnDispose();
mpv::TestPendingRequestTypesRemainDistinctOnDispose();
mpv::TestInnerSubclassOwnershipIsSerializedAndDetached();
mpv::TestTimedOutSubclassDetachCanBeAdopted();
mpv::TestTimedOutSubclassInstallCannotOutliveItsState();
std::cout << "mpv_player_property_contract_test: PASS\n";
return 0;
}
+13 -8
View File
@@ -22,8 +22,8 @@ void MpvAudioPlayerPluginRegisterWithRegistrar(FlutterDesktopPluginRegistrarRef
namespace mpv {
namespace {
constexpr UINT kPlatformTaskMessage = WM_APP + 0x4D50;
constexpr UINT kAudioPlatformTaskMessage = WM_APP + 0x4D51;
constexpr UINT kPlatformTaskMessage = WM_APP + 0x04D0;
constexpr UINT kAudioPlatformTaskMessage = WM_APP + 0x04D1;
} // namespace
void MpvPlayerPlugin::RegisterWithRegistrar(
@@ -67,6 +67,7 @@ MpvPlayerPlugin::MpvPlayerPlugin(
}
MpvPlayerPlugin::~MpvPlayerPlugin() {
player_generation_.fetch_add(1, std::memory_order_acq_rel);
// Join the mpv event thread before draining: it enqueues platform tasks,
// and platform_tasks_/platform_tasks_mutex_ are destroyed before player_
// (reverse declaration order).
@@ -180,12 +181,14 @@ void MpvPlayerPlugin::HandleMethodCall(
// core is windowless, so it gets no view at all.
HWND view = audio_only_ ? nullptr : GetChildWindow();
const uint64_t generation = player_generation_.fetch_add(1, std::memory_order_acq_rel) + 1;
player_ = std::make_unique<MpvPlayer>(audio_only_);
bool success = player_->Initialize(view);
if (success) {
// Set up event callback.
player_->SetEventCallback([this](const flutter::EncodableValue& event) { SendEvent(event); });
player_->SetEventCallback(
[this, generation](const flutter::EncodableValue& event) { SendEvent(generation, event); });
if (!audio_only_) {
// Start hidden.
@@ -197,6 +200,7 @@ void MpvPlayerPlugin::HandleMethodCall(
result->Error("INIT_FAILED", "Failed to initialize MPV player");
}
} else if (method == "dispose") {
player_generation_.fetch_add(1, std::memory_order_acq_rel);
if (player_) {
player_->Dispose();
player_.reset();
@@ -524,12 +528,13 @@ void MpvPlayerPlugin::HandleMethodCall(
}
}
void MpvPlayerPlugin::SendEvent(const flutter::EncodableValue& event) {
void MpvPlayerPlugin::SendEvent(uint64_t player_generation, const flutter::EncodableValue& event) {
// mpv events arrive on the mpv event thread; Flutter channel APIs are
// platform-thread-only, so marshal onto the platform thread (the sink
// null-check then also runs on the same thread as onListen/onCancel).
PostToPlatformThread([this, event]() {
if (event_sink_) {
// platform-thread-only. Capture the player generation at receipt so queued
// property/event callbacks from a disposed player cannot publish into its
// replacement's stream.
PostToPlatformThread([this, player_generation, event]() {
if (player_generation_.load(std::memory_order_acquire) == player_generation && event_sink_) {
event_sink_->Success(event);
}
});
+4 -1
View File
@@ -8,6 +8,8 @@
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>
#include <atomic>
#include <cstdint>
#include <functional>
#include <memory>
#include <mutex>
@@ -44,7 +46,7 @@ class MpvPlayerPlugin : public flutter::Plugin {
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
void SendEvent(const flutter::EncodableValue& event);
void SendEvent(uint64_t player_generation, const flutter::EncodableValue& event);
void PostToPlatformThread(std::function<void()> task);
void DrainPlatformTasks();
@@ -64,6 +66,7 @@ class MpvPlayerPlugin : public flutter::Plugin {
std::unique_ptr<flutter::EventSink<flutter::EncodableValue>> event_sink_;
std::unique_ptr<MpvPlayer> player_;
std::atomic<uint64_t> player_generation_{0};
DisplayModeManager display_mode_manager_;
std::optional<int32_t> proc_id_;
std::mutex platform_tasks_mutex_;