Bug 1830792 - [4/4] Flicker-resize the window on first fullscreen entry r=emilio

DWM doesn't update its cached nonclient region information when a window
changes its client area without changing its actual size.

This happens in Firefox when a maximized window becomes fullscreen. If
this happens, "flicker-resize" the window to force DWM to update.

Differential Revision: https://phabricator.services.mozilla.com/D176844
This commit is contained in:
Ray Kraesig 2023-05-03 14:24:35 +00:00
parent 98dc3ec133
commit 14431ef542
3 changed files with 182 additions and 8 deletions

View file

@ -15551,7 +15551,6 @@
type: RelaxedAtomicUint32
value: 6
mirror: always
#endif
# Whether to flush the Ole clipboard synchronously.
# Possible values are:
@ -15563,6 +15562,20 @@
value: 2
mirror: always
# Whether to apply a hack (adjusting the window height by -1px and back again)
# upon first entering fullscreen intended to work around a bug exhibited under
# on some Windows 11 machines under some configurations. (See bug 1763981.)
#
# Semantics:
# * 0: never
# * 1: always
# * 2: auto
- name: widget.windows.apply-dwm-resize-hack
type: RelaxedAtomicInt32
value: 2
mirror: always
#endif
# Whether to disable SwipeTracker (e.g. swipe-to-nav).
- name: widget.disable-swipe-tracker
type: bool

View file

@ -60,6 +60,7 @@
#include "mozilla/AppShutdown.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/Likely.h"
#include "mozilla/PreXULSkeletonUI.h"
#include "mozilla/Logging.h"
#include "mozilla/MathAlgorithms.h"
@ -2896,19 +2897,20 @@ bool nsWindow::UpdateNonClientMargins(bool aReflowWindow) {
mNonClientOffset.left = mHorResizeMargin;
mNonClientOffset.right = mHorResizeMargin;
} else if (sizeMode == nsSizeMode_Maximized) {
// On Windows 10+, we make the entire frame part of the client area.
// We leave the default frame sizes for left, right and bottom since
// Windows will automagically position the edges "offscreen" for maximized
// windows.
// On Windows 10+, we make the entire frame part of the client area. We
// leave the default frame sizes for left, right and bottom since Windows
// will automagically position the edges "offscreen" for maximized windows.
//
// On versions prior to Windows 10, we add padding to the widget to
// circumvent a bug in DwmDefWindowProc (see
// nsNativeThemeWin::GetWidgetPadding). We "undo" that padding in
// WM_NCCALCSIZE by adding the caption (as well as the sizing frame) to the
// client area.
//
// The padding is not needed on Win10+ because we handle window buttons
// non-natively in the theme. It also does not work on Win10+ -- it
// exposes a new issue where widget edges would sometimes appear to bleed
// into other displays (bug 1614218).
// non-natively in the theme. It also does not work on Win10+ -- it exposes
// a new issue where widget edges would sometimes appear to bleed into other
// displays (bug 1614218).
int verticalResize = 0;
if (IsWin10OrLater()) {
verticalResize =
@ -3647,7 +3649,153 @@ void nsWindow::CleanupFullscreenTransition() {
mTransitionWnd = nullptr;
}
void nsWindow::TryDwmResizeHack() {
// The "DWM resize hack", aka the "fullscreen resize hack", is a workaround
// for DWM's occasional and not-entirely-predictable failure to update its
// internal state when the client area of a window changes without changing
// the window size. The effect of this is that DWM will clip the content of
// the window to its former client area.
//
// It is not known under what circumstances the bug will trigger. Windows 11
// is known to be required, but many Windows 11 machines do not exhibit the
// issue. Even machines that _do_ exhibit it will sometimes not do so when
// apparently- irrelevant changes are made to the configuration. (See bug
// 1763981.)
//
// The bug is triggered by Firefox when a maximized window (which has window
// decorations) becomes fullscreen (which doesn't). To work around this, if we
// think it may occur, we "flicker-resize" the relevant window -- that is, we
// reduce its height by 1px, then restore it. This causes DWM to acquire the
// new client-area metrics.
//
// This is admittedly a sledgehammer where a screwdriver should suffice.
// ---------------------------------------------------------------------------
// Regardless of preferences or heuristics, only apply the hack if this is the
// first time we've entered fullscreen across the entire Firefox session.
// (Subsequent transitions to fullscreen, even with different windows, don't
// appear to induce the bug.)
{
// (main thread only; `atomic` not needed)
static bool sIsFirstFullscreenEntry = true;
bool isFirstFullscreenEntry = sIsFirstFullscreenEntry;
sIsFirstFullscreenEntry = false;
if (MOZ_LIKELY(!isFirstFullscreenEntry)) {
return;
}
MOZ_LOG(gWindowsLog, LogLevel::Verbose,
("%s: first fullscreen entry", __PRETTY_FUNCTION__));
}
// Check whether to try to apply the DWM resize hack, based on the override
// pref and/or some internal heuristics.
{
const auto hackApplicationHeuristics = [&]() -> bool {
// The bug has only been seen under Windows 11. (At time of writing, this
// is the latest version of Windows.)
if (!IsWin11OrLater()) {
return false;
}
KnowsCompositor const* const kc = mWindowRenderer->AsKnowsCompositor();
// This should never happen...
MOZ_ASSERT(kc);
// ... so if it does, we are in uncharted territory: don't apply the hack.
if (!kc) {
return false;
}
// The bug doesn't occur when we're using a separate compositor window
// (since the compositor window always comprises exactly its client area,
// with no non-client border).
if (kc->GetUseCompositorWnd()) {
return false;
}
// Otherwise, apply the hack.
return true;
};
// Figure out whether or not we should perform the hack, and -- arguably
// more importantly -- log that decision.
bool const shouldApplyHack = [&]() {
enum Reason : bool { Pref, Heuristics };
auto const msg = [&](bool decision, Reason reason) -> bool {
MOZ_LOG(gWindowsLog, LogLevel::Verbose,
("%s %s per %s", decision ? "applying" : "skipping",
"DWM resize hack", reason == Pref ? "pref" : "heuristics"));
return decision;
};
switch (StaticPrefs::widget_windows_apply_dwm_resize_hack()) {
case 0:
return msg(false, Pref);
case 1:
return msg(true, Pref);
default: // treat all other values as `auto`
return msg(hackApplicationHeuristics(), Heuristics);
}
}();
if (!shouldApplyHack) {
return;
}
}
// The DWM bug is believed to involve a race condition: some users have
// reported that setting a custom theme or adding unused command-line
// parameters sometimes causes the bug to vanish.
//
// Out of an abundance of caution, we therefore apply the hack in a later
// event, rather than inline.
NS_DispatchToMainThread(NS_NewRunnableFunction(
"nsWindow::TryFullscreenResizeHack", [self = RefPtr(this)]() {
HWND const hwnd = self->GetWindowHandle();
if (self->mFrameState->GetSizeMode() != nsSizeMode_Fullscreen) {
MOZ_LOG(gWindowsLog, mozilla::LogLevel::Info,
("DWM resize hack: window no longer fullscreen; aborting"));
return;
}
RECT origRect;
if (!::GetWindowRect(hwnd, &origRect)) {
MOZ_LOG(gWindowsLog, mozilla::LogLevel::Error,
("DWM resize hack: could not get window size?!"));
return;
}
LONG const x = origRect.left;
LONG const y = origRect.top;
LONG const width = origRect.right - origRect.left;
LONG const height = origRect.bottom - origRect.top;
MOZ_DIAGNOSTIC_ASSERT(!self->mIsPerformingDwmFlushHack);
auto const onExit =
MakeScopeExit([&, oldVal = self->mIsPerformingDwmFlushHack]() {
self->mIsPerformingDwmFlushHack = oldVal;
});
self->mIsPerformingDwmFlushHack = true;
MOZ_LOG(gWindowsLog, LogLevel::Debug,
("beginning DWM resize hack for HWND %08" PRIXPTR,
uintptr_t(hwnd)));
::MoveWindow(hwnd, x, y, width, height - 1, FALSE);
::MoveWindow(hwnd, x, y, width, height, TRUE);
MOZ_LOG(gWindowsLog, LogLevel::Debug,
("concluded DWM resize hack for HWND %08" PRIXPTR,
uintptr_t(hwnd)));
}));
}
void nsWindow::OnFullscreenChanged(nsSizeMode aOldSizeMode, bool aFullScreen) {
MOZ_ASSERT((aOldSizeMode != nsSizeMode_Fullscreen) == aFullScreen);
// HACK: Potentially flicker-resize the window, to force DWM to get the right
// client-area information.
if (aFullScreen) {
TryDwmResizeHack();
}
// Hide chrome and reposition window. Note this will also cache dimensions for
// restoration, so it should only be called once per fullscreen request.
//
@ -5098,6 +5246,17 @@ bool nsWindow::ProcessMessageInternal(UINT msg, WPARAM& wParam, LPARAM& lParam,
bool result = false; // call the default nsWindow proc
*aRetValue = 0;
// The DWM resize hack (see bug 1763981) causes us to process a number of
// messages, notably including some WM_WINDOWPOSCHANG{ING,ED} messages which
// would ordinarily result in a whole lot of internal state being updated.
//
// Since we're supposed to end in the same state we started in (and since the
// content shouldn't know about any of this nonsense), just discard any
// messages synchronously dispatched from within the hack.
if (MOZ_UNLIKELY(mIsPerformingDwmFlushHack)) {
return true;
}
// Glass hit testing w/custom transparent margins
LRESULT dwmHitResult;
if (mCustomNonClient &&

View file

@ -702,6 +702,7 @@ class nsWindow final : public nsBaseWidget {
uint32_t aOrientation = 90);
void OnFullscreenChanged(nsSizeMode aOldSizeMode, bool aFullScreen);
void TryDwmResizeHack();
static void OnCloakEvent(HWND aWnd, bool aCloaked);
void OnCloakChanged(bool aCloaked);
@ -776,6 +777,7 @@ class nsWindow final : public nsBaseWidget {
bool mIsShowingPreXULSkeletonUI = false;
bool mResizable = false;
bool mForMenupopupFrame = false;
bool mIsPerformingDwmFlushHack = false;
DWORD_PTR mOldStyle = 0;
DWORD_PTR mOldExStyle = 0;
nsNativeDragTarget* mNativeDragTarget = nullptr;