From 7bd286b345e7e9ee3771c0f12551fe1ded670e64 Mon Sep 17 00:00:00 2001 From: Alexey <112402687+leshaalexey@users.noreply.github.com> Date: Wed, 5 Aug 2026 15:34:55 +0300 Subject: [PATCH 01/13] Add rounded corners mod for snapped/maximized windows This mod enables rounded corners for snapped and maximized windows in DWM, mimicking macOS behavior. It includes settings for corner style, radius, and debug logging. --- mods/rounded-corners-when-snapped.wh.cpp | 474 +++++++++++++++++++++++ 1 file changed, 474 insertions(+) create mode 100644 mods/rounded-corners-when-snapped.wh.cpp diff --git a/mods/rounded-corners-when-snapped.wh.cpp b/mods/rounded-corners-when-snapped.wh.cpp new file mode 100644 index 0000000000..66c2937afe --- /dev/null +++ b/mods/rounded-corners-when-snapped.wh.cpp @@ -0,0 +1,474 @@ +// ==WindhawkMod== +// @id rounded-corners-when-snapped +// @name Rounded corners when snapped or maximized +// @description Makes DWM draw rounded corners for snapped and maximized windows (macOS-like), without changing the window state +// @version 2.1.0 +// @author Alexey +// @github https://github.com/leshaalexey +// @include dwm.exe +// @include explorer.exe +// @architecture x86-64 +// ==/WindhawkMod== + +// ==WindhawkModReadme== +/* +# Rounded corners when snapped or maximized + +Windows stays in charge of the window: it is really snapped / really maximized +(Snap Layouts, Snap Assist, Win+Arrow, the maximize button — all untouched). +Only the *drawing* changes, inside the compositor. + +Two functions of `uDWM.dll` are involved, both members of `CTopLevelWindow`: + +* `GetEffectiveCornerStyle` decides the corner style of a composed surface and + returns `1` (square) for maximized and snapped windows. +* `IsMaximizedOrSnapped` is DWM's own answer for the very same surface. + +The hook combines them: when the style came out square *and* DWM says this +surface is a maximized or snapped window, it returns `2` (round) instead. + +Some builds don't change the style at all — they keep the rounded style and +zero the *radius* instead. So the three radius getters of `CTopLevelWindow` +(`GetRadiusFromCornerStyle`, `GetFloatCornerRadiusForCurrentStyle`, +`GetDpiAdjustedFloatCornerRadius`) are hooked as well, and a radius of zero is +replaced — but only for surfaces `IsMaximizedOrSnapped` vouches for. + +Everything else — the Start menu backdrop, the virtual-desktop switch +animation, drag previews, fullscreen windows — is left exactly as DWM wanted +it, so there are no stray outlines. + +## Setup + +**1. Let Windhawk into `dwm.exe`** — it is on the built-in list of critical +system processes and mods targeting it fail silently: + +* Windhawk → **Settings** → **Advanced settings** → **More advanced settings** +* **Process inclusion list** → add `%systemroot%\system32\dwm.exe` + +**2. Keep `explorer.exe` targeted (default)** — `dwm.exe` runs under a +restricted account and usually cannot download PDBs, so explorer resolves the +addresses once and caches them as RVAs for it. + +## Requirements + +* Windows 11, x64. +* No other DWM corner patcher running (ExplorerPatcher, StartAllBack, + Win11DisableRoundedCorners). +*/ +// ==/WindhawkModReadme== + +// ==WindhawkModSettings== +/* +- roundStyle: round + $name: Corner style + $options: + - round: Normal rounding (like an unsnapped window) + - small: Small rounding (like a menu) +- noFilter: false + $name: Round everything DWM wanted square + $description: >- + Ignores IsMaximizedOrSnapped. Rounds internal surfaces too, which causes + outlines around the Start menu and desktop switching. For testing only. +- forceRadius: true + $name: Force a non-zero radius + $description: >- + Needed on builds where DWM zeroes the corner radius for maximized windows + instead of changing the corner style. Filtered by IsMaximizedOrSnapped just + like the style, so it no longer touches other surfaces. +- radius: 8 + $name: Radius in pixels + $description: Only used when the option above is on. Windows 11 default is 8. +- dumpSymbols: "" + $name: Dump symbols containing + $description: >- + Diagnostics. A comma-separated list of substrings, for example + "Hwnd, Maximiz, CornerStyle" - explorer.exe then lists every matching + uDWM.dll symbol in the log. Leave empty normally. +- debugLog: false + $name: Diagnostic logging +*/ +// ==/WindhawkModSettings== + +#include +#include + +// ---------------------------------------------------------------- constants + +// uDWM CORNER_STYLE +constexpr int kCornerSquare = 1; +constexpr int kCornerRound = 2; +constexpr int kCornerRoundSmall = 3; + +constexpr int kCacheVersion = 4; + +constexpr PCWSTR kKeyCacheVer = L"cacheVersion"; +constexpr PCWSTR kKeyStamp = L"uDwmTimeDateStamp"; +constexpr PCWSTR kKeySize = L"uDwmSizeOfImage"; +constexpr PCWSTR kKeyRvaCornerStyle = L"rvaGetEffectiveCornerStyle"; +constexpr PCWSTR kKeyRvaMaximized = L"rvaIsMaximizedOrSnapped"; +constexpr PCWSTR kKeyRvaRadiusStyle = L"rvaGetRadiusFromCornerStyle"; +constexpr PCWSTR kKeyRvaRadiusCurrent = L"rvaGetFloatCornerRadiusForCurrentStyle"; +constexpr PCWSTR kKeyRvaRadiusDpi = L"rvaGetDpiAdjustedFloatCornerRadius"; + +// ----------------------------------------------------------------- settings + +struct Settings { + int roundStyle; + bool noFilter; + bool forceRadius; + float radius; + bool debugLog; +} g_settings; + +// -------------------------------------------------------------------- hooks + +using GetEffectiveCornerStyle_t = int(__fastcall*)(void* pThis); +using IsMaximizedOrSnapped_t = bool(__fastcall*)(void* pThis); +using FloatGetter_t = float(__fastcall*)(void* pThis); + +GetEffectiveCornerStyle_t GetEffectiveCornerStyle_orig; +IsMaximizedOrSnapped_t IsMaximizedOrSnapped; +FloatGetter_t GetRadiusFromCornerStyle_orig; +FloatGetter_t GetFloatCornerRadiusForCurrentStyle_orig; +FloatGetter_t GetDpiAdjustedFloatCornerRadius_orig; + +static void LogThrottled(PCWSTR message) { + static ULONGLONG lastTick; + + if (!g_settings.debugLog) { + return; + } + ULONGLONG now = GetTickCount64(); + if (now - lastTick < 1000) { + return; + } + lastTick = now; + Wh_Log(L"%s", message); +} + +int __fastcall GetEffectiveCornerStyle_hook(void* pThis) { + int style = GetEffectiveCornerStyle_orig(pThis); + + if (style != kCornerSquare) { + return style; + } + + if (!g_settings.noFilter) { + if (!IsMaximizedOrSnapped || !IsMaximizedOrSnapped(pThis)) { + return style; + } + } + + LogThrottled(L"rounding a maximized/snapped window"); + return g_settings.roundStyle; +} + +// All three radius getters are CTopLevelWindow methods, so the very same +// `this` can be asked whether this surface is a maximized/snapped window. +static bool SurfaceWantsRounding(void* pThis) { + if (!g_settings.forceRadius) { + return false; + } + if (g_settings.noFilter) { + return true; + } + return IsMaximizedOrSnapped && IsMaximizedOrSnapped(pThis); +} + +static float FixRadius(void* pThis, float value) { + if (value > 0.01f || !SurfaceWantsRounding(pThis)) { + return value; + } + LogThrottled(L"forcing radius on a maximized/snapped window"); + return g_settings.radius; +} + +float __fastcall GetRadiusFromCornerStyle_hook(void* pThis) { + return FixRadius(pThis, GetRadiusFromCornerStyle_orig(pThis)); +} + +float __fastcall GetFloatCornerRadiusForCurrentStyle_hook(void* pThis) { + return FixRadius(pThis, GetFloatCornerRadiusForCurrentStyle_orig(pThis)); +} + +float __fastcall GetDpiAdjustedFloatCornerRadius_hook(void* pThis) { + float value = GetDpiAdjustedFloatCornerRadius_orig(pThis); + if (value > 0.01f || !SurfaceWantsRounding(pThis)) { + return value; + } + UINT dpi = GetDpiForSystem(); + if (!dpi) dpi = 96; + return g_settings.radius * (dpi / 96.0f); +} + +// ------------------------------------------------------- symbols and cache + +struct ResolvedRvas { + int cornerStyle; + int isMaximized; + int radiusStyle; + int radiusCurrent; + int radiusDpi; +}; + +static bool GetModuleIdentity(HMODULE module, int* stamp, int* size) { + auto dos = reinterpret_cast(module); + if (!dos || dos->e_magic != IMAGE_DOS_SIGNATURE) { + return false; + } + auto nt = reinterpret_cast( + reinterpret_cast(module) + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) { + return false; + } + *stamp = static_cast(nt->FileHeader.TimeDateStamp); + *size = static_cast(nt->OptionalHeader.SizeOfImage); + return true; +} + +// filter is a comma-separated list of substrings; empty means "no match". +static bool MatchesFilterList(PCWSTR text, PCWSTR filter) { + if (!text || !filter || !*filter) { + return false; + } + + while (*filter) { + while (*filter == L' ' || *filter == L',') { + filter++; + } + WCHAR token[64]; + int n = 0; + while (*filter && *filter != L',' && n < ARRAYSIZE(token) - 1) { + token[n++] = *filter++; + } + while (n > 0 && token[n - 1] == L' ') { + n--; + } + token[n] = L'\0'; + if (n && wcsstr(text, token)) { + return true; + } + while (*filter && *filter != L',') { + filter++; + } + } + return false; +} + +static bool ResolveBySymbols(HMODULE module, ResolvedRvas* out, + PCWSTR dumpFilter) { + WH_FIND_SYMBOL_OPTIONS options = {sizeof(options)}; + WH_FIND_SYMBOL symbol = {}; + + HANDLE find = Wh_FindFirstSymbol(module, &options, &symbol); + if (!find) { + return false; + } + + auto base = reinterpret_cast(module); + *out = {}; + + do { + if (!symbol.symbol || !symbol.address) { + continue; + } + int rva = static_cast(reinterpret_cast(symbol.address) - base); + + if (MatchesFilterList(symbol.symbol, dumpFilter)) { + Wh_Log(L"[%08X] %s", rva, symbol.symbol); + } + + if (wcsstr(symbol.symbol, L"CTopLevelWindow::GetEffectiveCornerStyle")) { + out->cornerStyle = rva; + } else if (wcsstr(symbol.symbol, L"CTopLevelWindow::IsMaximizedOrSnapped")) { + out->isMaximized = rva; + } else if (wcsstr(symbol.symbol, L"CTopLevelWindow::GetRadiusFromCornerStyle")) { + out->radiusStyle = rva; + } else if (wcsstr(symbol.symbol, + L"CTopLevelWindow::GetFloatCornerRadiusForCurrentStyle")) { + out->radiusCurrent = rva; + } else if (wcsstr(symbol.symbol, + L"CTopLevelWindow::GetDpiAdjustedFloatCornerRadius")) { + out->radiusDpi = rva; + } + } while (Wh_FindNextSymbol(find, &symbol)); + + Wh_FindCloseSymbol(find); + return out->cornerStyle != 0; +} + +static void StoreRvas(int stamp, int size, const ResolvedRvas& rvas) { + Wh_SetIntValue(kKeyRvaCornerStyle, rvas.cornerStyle); + Wh_SetIntValue(kKeyRvaMaximized, rvas.isMaximized); + Wh_SetIntValue(kKeyRvaRadiusStyle, rvas.radiusStyle); + Wh_SetIntValue(kKeyRvaRadiusCurrent, rvas.radiusCurrent); + Wh_SetIntValue(kKeyRvaRadiusDpi, rvas.radiusDpi); + // Identity last: it validates everything above. + Wh_SetIntValue(kKeyStamp, stamp); + Wh_SetIntValue(kKeySize, size); + Wh_SetIntValue(kKeyCacheVer, kCacheVersion); +} + +static bool LoadRvas(int stamp, int size, ResolvedRvas* out) { + if (Wh_GetIntValue(kKeyCacheVer, 0) != kCacheVersion || + Wh_GetIntValue(kKeyStamp, 0) != stamp || + Wh_GetIntValue(kKeySize, 0) != size) { + return false; + } + out->cornerStyle = Wh_GetIntValue(kKeyRvaCornerStyle, 0); + out->isMaximized = Wh_GetIntValue(kKeyRvaMaximized, 0); + out->radiusStyle = Wh_GetIntValue(kKeyRvaRadiusStyle, 0); + out->radiusCurrent = Wh_GetIntValue(kKeyRvaRadiusCurrent, 0); + out->radiusDpi = Wh_GetIntValue(kKeyRvaRadiusDpi, 0); + return out->cornerStyle != 0; +} + +// ---------------------------------------------------------------- settings + +static void LoadSettings() { + PCWSTR style = Wh_GetStringSetting(L"roundStyle"); + g_settings.roundStyle = + (style && wcscmp(style, L"small") == 0) ? kCornerRoundSmall : kCornerRound; + Wh_FreeStringSetting(style); + + g_settings.noFilter = Wh_GetIntSetting(L"noFilter") != 0; + g_settings.forceRadius = Wh_GetIntSetting(L"forceRadius") != 0; + g_settings.debugLog = Wh_GetIntSetting(L"debugLog") != 0; + + int radius = Wh_GetIntSetting(L"radius"); + if (radius < 1) radius = 8; + if (radius > 60) radius = 60; + g_settings.radius = static_cast(radius); +} + +// -------------------------------------------------------------- entrypoints + +static bool RunningInDwm() { + WCHAR path[MAX_PATH]{}; + if (!GetModuleFileNameW(nullptr, path, ARRAYSIZE(path))) { + return false; + } + PCWSTR name = wcsrchr(path, L'\\'); + name = name ? name + 1 : path; + return _wcsicmp(name, L"dwm.exe") == 0; +} + +BOOL Wh_ModInit() { + LoadSettings(); + + HMODULE hUDWM = GetModuleHandleW(L"uDWM.dll"); + if (!hUDWM) { + hUDWM = LoadLibraryW(L"uDWM.dll"); + } + if (!hUDWM) { + Wh_Log(L"uDWM.dll not available"); + return FALSE; + } + + int stamp = 0, size = 0; + if (!GetModuleIdentity(hUDWM, &stamp, &size)) { + Wh_Log(L"Bad uDWM.dll headers"); + return FALSE; + } + + // --- Warmer mode: resolve and cache the addresses for dwm.exe. + if (!RunningInDwm()) { + PCWSTR dump = Wh_GetStringSetting(L"dumpSymbols"); + bool wantDump = dump && *dump; + + ResolvedRvas cached{}; + if (!wantDump && LoadRvas(stamp, size, &cached)) { + Wh_Log(L"warmer: RVAs already cached for this uDWM build"); + Wh_FreeStringSetting(dump); + return TRUE; + } + + ResolvedRvas rvas{}; + bool ok = ResolveBySymbols(hUDWM, &rvas, dump); + Wh_FreeStringSetting(dump); + + if (!ok) { + Wh_Log(L"warmer: failed to enumerate uDWM.dll symbols here too"); + return TRUE; + } + + StoreRvas(stamp, size, rvas); + Wh_Log(L"warmer: stored RVAs style=%08X maximized=%08X", rvas.cornerStyle, + rvas.isMaximized); + return TRUE; + } + + // --- dwm.exe: cache first, own enumeration as a fallback. + ResolvedRvas rvas{}; + if (LoadRvas(stamp, size, &rvas)) { + Wh_Log(L"dwm: using cached RVAs"); + } else if (ResolveBySymbols(hUDWM, &rvas, nullptr)) { + Wh_Log(L"dwm: resolved symbols locally"); + StoreRvas(stamp, size, rvas); + } else { + Wh_Log(L"dwm: no cached RVAs and no symbols - let explorer.exe warm " + L"the cache, then re-enable the mod"); + return FALSE; + } + + auto base = reinterpret_cast(hUDWM); + + if (rvas.isMaximized) { + IsMaximizedOrSnapped = + reinterpret_cast(base + rvas.isMaximized); + } else if (!g_settings.noFilter) { + Wh_Log(L"dwm: CTopLevelWindow::IsMaximizedOrSnapped not found - " + L"staying passive to avoid rounding internal surfaces"); + } + + Wh_Log(L"dwm: RVAs style=%08X maximized=%08X radius=%08X/%08X/%08X", + rvas.cornerStyle, rvas.isMaximized, rvas.radiusStyle, + rvas.radiusCurrent, rvas.radiusDpi); + + int hooked = 0; + + if (rvas.cornerStyle && + Wh_SetFunctionHook(base + rvas.cornerStyle, + (void*)GetEffectiveCornerStyle_hook, + (void**)&GetEffectiveCornerStyle_orig)) { + hooked++; + } + // Always hooked; whether they change anything is decided per call, so the + // radius setting can be toggled without recompiling. + if (rvas.radiusStyle && + Wh_SetFunctionHook(base + rvas.radiusStyle, + (void*)GetRadiusFromCornerStyle_hook, + (void**)&GetRadiusFromCornerStyle_orig)) { + hooked++; + } + if (rvas.radiusCurrent && + Wh_SetFunctionHook(base + rvas.radiusCurrent, + (void*)GetFloatCornerRadiusForCurrentStyle_hook, + (void**)&GetFloatCornerRadiusForCurrentStyle_orig)) { + hooked++; + } + if (rvas.radiusDpi && + Wh_SetFunctionHook(base + rvas.radiusDpi, + (void*)GetDpiAdjustedFloatCornerRadius_hook, + (void**)&GetDpiAdjustedFloatCornerRadius_orig)) { + hooked++; + } + + if (!hooked) { + Wh_Log(L"dwm: nothing hooked"); + return FALSE; + } + + Wh_Log(L"dwm: hooked %d function(s), filter=%s", hooked, + IsMaximizedOrSnapped ? L"IsMaximizedOrSnapped" : L"none"); + return TRUE; +} + +void Wh_ModSettingsChanged() { + LoadSettings(); + SystemParametersInfoW(SPI_SETDRAGFULLWINDOWS, TRUE, nullptr, SPIF_SENDCHANGE); +} + +void Wh_ModUninit() { + Wh_Log(L"Unloaded"); +} From 64698889f5135ee99f6536b42dd4439684702b83 Mon Sep 17 00:00:00 2001 From: Alexey <112402687+leshaalexey@users.noreply.github.com> Date: Wed, 5 Aug 2026 15:55:47 +0300 Subject: [PATCH 02/13] Update mod authorship section in README --- mods/rounded-corners-when-snapped.wh.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mods/rounded-corners-when-snapped.wh.cpp b/mods/rounded-corners-when-snapped.wh.cpp index 66c2937afe..71d99abdc3 100644 --- a/mods/rounded-corners-when-snapped.wh.cpp +++ b/mods/rounded-corners-when-snapped.wh.cpp @@ -54,6 +54,13 @@ addresses once and caches them as RVAs for it. * Windows 11, x64. * No other DWM corner patcher running (ExplorerPatcher, StartAllBack, Win11DisableRoundedCorners). + +## Mod authorship + +This mod was created by: + +- [x] The submitter, with AI assistance +- [x] Claude */ // ==/WindhawkModReadme== From 24b0bfa7f3fc1478178eb95794907a4469921bb4 Mon Sep 17 00:00:00 2001 From: Alexey <112402687+leshaalexey@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:17:11 +0300 Subject: [PATCH 03/13] Update mod description and version for clarity --- mods/rounded-corners-when-snapped.wh.cpp | 601 ++++++++--------------- 1 file changed, 213 insertions(+), 388 deletions(-) diff --git a/mods/rounded-corners-when-snapped.wh.cpp b/mods/rounded-corners-when-snapped.wh.cpp index 71d99abdc3..971f857121 100644 --- a/mods/rounded-corners-when-snapped.wh.cpp +++ b/mods/rounded-corners-when-snapped.wh.cpp @@ -1,66 +1,61 @@ // ==WindhawkMod== // @id rounded-corners-when-snapped // @name Rounded corners when snapped or maximized -// @description Makes DWM draw rounded corners for snapped and maximized windows (macOS-like), without changing the window state -// @version 2.1.0 -// @author Alexey +// @description Keeps window corners rounded when a window is snapped or maximized, without changing the window state +// @version 1.0.0 +// @author Alexey Lavrinenko // @github https://github.com/leshaalexey +// @license GPL-3.0 // @include dwm.exe -// @include explorer.exe // @architecture x86-64 +// @compilerOptions -lwevtapi // ==/WindhawkMod== +// HasMultipleDwminitWarningsInLastMinute() is taken from the Custom Window +// Corner Radius mod by m417z, which is published under the GNU General Public +// License v3.0, so this mod carries the same license. + // ==WindhawkModReadme== /* # Rounded corners when snapped or maximized -Windows stays in charge of the window: it is really snapped / really maximized -(Snap Layouts, Snap Assist, Win+Arrow, the maximize button — all untouched). -Only the *drawing* changes, inside the compositor. - -Two functions of `uDWM.dll` are involved, both members of `CTopLevelWindow`: - -* `GetEffectiveCornerStyle` decides the corner style of a composed surface and - returns `1` (square) for maximized and snapped windows. -* `IsMaximizedOrSnapped` is DWM's own answer for the very same surface. - -The hook combines them: when the style came out square *and* DWM says this -surface is a maximized or snapped window, it returns `2` (round) instead. - -Some builds don't change the style at all — they keep the rounded style and -zero the *radius* instead. So the three radius getters of `CTopLevelWindow` -(`GetRadiusFromCornerStyle`, `GetFloatCornerRadiusForCurrentStyle`, -`GetDpiAdjustedFloatCornerRadius`) are hooked as well, and a radius of zero is -replaced — but only for surfaces `IsMaximizedOrSnapped` vouches for. - -Everything else — the Start menu backdrop, the virtual-desktop switch -animation, drag previews, fullscreen windows — is left exactly as DWM wanted -it, so there are no stray outlines. +Windows 11 squares off a window's corners as soon as it is maximized or +snapped. This mod keeps them rounded, the way macOS does. -## Setup +The window state is untouched: it is really snapped, really maximized, and +Snap Layouts, Snap Assist, Win+Arrow and the maximize button all behave exactly +as before. Only the compositor's drawing changes, and disabling the mod +restores the default look immediately. -**1. Let Windhawk into `dwm.exe`** — it is on the built-in list of critical -system processes and mods targeting it fail silently: +## ⚠ Important usage note ⚠ -* Windhawk → **Settings** → **Advanced settings** → **More advanced settings** -* **Process inclusion list** → add `%systemroot%\system32\dwm.exe` +This mod needs to hook into `dwm.exe` to work. Please navigate to Windhawk's +Settings > Advanced settings > More advanced settings > Process inclusion list, +and make sure that `dwm.exe` is in the list. -**2. Keep `explorer.exe` targeted (default)** — `dwm.exe` runs under a -restricted account and usually cannot download PDBs, so explorer resolves the -addresses once and caches them as RVAs for it. +## How it works -## Requirements +Windows are squared off in one of two ways, depending on the build: either the +corner style comes out as `DWMWCP_DONOTROUND`, or the style stays rounded and +the corner radius is zeroed further down the pipeline. The mod covers both, by +hooking `CTopLevelWindow::GetEffectiveCornerStyle` and the corner radius +getters of the same class. -* Windows 11, x64. -* No other DWM corner patcher running (ExplorerPatcher, StartAllBack, - Win11DisableRoundedCorners). +Every replacement is gated on `CTopLevelWindow::IsMaximizedOrSnapped` — DWM's +own answer about the very surface being composed. The Start menu backdrop, the +virtual-desktop switch animation, drag previews and fullscreen windows answer +`false` and are left exactly as DWM wanted them, so no stray outlines appear. -## Mod authorship +A forced radius is scaled through `CWindowData::ScaleForDpi`, the same way DWM +scales its own, so it comes out the right size on every monitor of a mixed-DPI +setup. -This mod was created by: +## Compatibility -- [x] The submitter, with AI assistance -- [x] Claude +Other mods and tools that patch DWM corners hook the same functions and will +fight with this one. Don't run it together with **Custom Window Corner +Radius**, **Disable rounded corners in Windows 11**, ExplorerPatcher's rounded +corner option, StartAllBack or Win11DisableRoundedCorners. */ // ==/WindhawkModReadme== @@ -68,414 +63,244 @@ This mod was created by: /* - roundStyle: round $name: Corner style - $options: - - round: Normal rounding (like an unsnapped window) - - small: Small rounding (like a menu) -- noFilter: false - $name: Round everything DWM wanted square - $description: >- - Ignores IsMaximizedOrSnapped. Rounds internal surfaces too, which causes - outlines around the Start menu and desktop switching. For testing only. -- forceRadius: true - $name: Force a non-zero radius $description: >- - Needed on builds where DWM zeroes the corner radius for maximized windows - instead of changing the corner style. Filtered by IsMaximizedOrSnapped just - like the style, so it no longer touches other surfaces. + Which rounding to apply. "Normal" matches an ordinary unsnapped window. + $options: + - round: Normal + - small: Small, like a menu - radius: 8 - $name: Radius in pixels - $description: Only used when the option above is on. Windows 11 default is 8. -- dumpSymbols: "" - $name: Dump symbols containing + $name: Fallback radius $description: >- - Diagnostics. A comma-separated list of substrings, for example - "Hwnd, Maximiz, CornerStyle" - explorer.exe then lists every matching - uDWM.dll symbol in the log. Leave empty normally. -- debugLog: false - $name: Diagnostic logging + Only used on builds where DWM zeroes the corner radius instead of changing + the corner style. Windows 11 uses 8 pixels for normal rounding and 4 for + small rounding. */ // ==/WindhawkModSettings== -#include -#include - -// ---------------------------------------------------------------- constants - -// uDWM CORNER_STYLE -constexpr int kCornerSquare = 1; -constexpr int kCornerRound = 2; -constexpr int kCornerRoundSmall = 3; - -constexpr int kCacheVersion = 4; +#include -constexpr PCWSTR kKeyCacheVer = L"cacheVersion"; -constexpr PCWSTR kKeyStamp = L"uDwmTimeDateStamp"; -constexpr PCWSTR kKeySize = L"uDwmSizeOfImage"; -constexpr PCWSTR kKeyRvaCornerStyle = L"rvaGetEffectiveCornerStyle"; -constexpr PCWSTR kKeyRvaMaximized = L"rvaIsMaximizedOrSnapped"; -constexpr PCWSTR kKeyRvaRadiusStyle = L"rvaGetRadiusFromCornerStyle"; -constexpr PCWSTR kKeyRvaRadiusCurrent = L"rvaGetFloatCornerRadiusForCurrentStyle"; -constexpr PCWSTR kKeyRvaRadiusDpi = L"rvaGetDpiAdjustedFloatCornerRadius"; +#include +#include -// ----------------------------------------------------------------- settings - -struct Settings { +struct { int roundStyle; - bool noFilter; - bool forceRadius; float radius; - bool debugLog; } g_settings; -// -------------------------------------------------------------------- hooks - -using GetEffectiveCornerStyle_t = int(__fastcall*)(void* pThis); -using IsMaximizedOrSnapped_t = bool(__fastcall*)(void* pThis); -using FloatGetter_t = float(__fastcall*)(void* pThis); - -GetEffectiveCornerStyle_t GetEffectiveCornerStyle_orig; +// Captured, not hooked: DWM's own verdict about the surface being composed. +using IsMaximizedOrSnapped_t = bool(WINAPI*)(void* pThis); IsMaximizedOrSnapped_t IsMaximizedOrSnapped; -FloatGetter_t GetRadiusFromCornerStyle_orig; -FloatGetter_t GetFloatCornerRadiusForCurrentStyle_orig; -FloatGetter_t GetDpiAdjustedFloatCornerRadius_orig; - -static void LogThrottled(PCWSTR message) { - static ULONGLONG lastTick; - - if (!g_settings.debugLog) { - return; - } - ULONGLONG now = GetTickCount64(); - if (now - lastTick < 1000) { - return; - } - lastTick = now; - Wh_Log(L"%s", message); -} -int __fastcall GetEffectiveCornerStyle_hook(void* pThis) { - int style = GetEffectiveCornerStyle_orig(pThis); +// Captured, not hooked: DWM's own per-window DPI scaling, so a forced radius +// comes out the right size on every monitor. +using GetWindowData_t = void*(WINAPI*)(void* pThis); +GetWindowData_t GetWindowData_Original; - if (style != kCornerSquare) { - return style; - } +using ScaleForDpi_t = unsigned int(WINAPI*)(void* pThis, unsigned int value); +ScaleForDpi_t ScaleForDpi_Original; - if (!g_settings.noFilter) { - if (!IsMaximizedOrSnapped || !IsMaximizedOrSnapped(pThis)) { - return style; +float ScaledRadius(void* pThis) { + if (GetWindowData_Original && ScaleForDpi_Original) { + if (void* data = GetWindowData_Original(pThis)) { + return static_cast(ScaleForDpi_Original( + data, static_cast(g_settings.radius))); } } - - LogThrottled(L"rounding a maximized/snapped window"); - return g_settings.roundStyle; -} - -// All three radius getters are CTopLevelWindow methods, so the very same -// `this` can be asked whether this surface is a maximized/snapped window. -static bool SurfaceWantsRounding(void* pThis) { - if (!g_settings.forceRadius) { - return false; - } - if (g_settings.noFilter) { - return true; - } - return IsMaximizedOrSnapped && IsMaximizedOrSnapped(pThis); -} - -static float FixRadius(void* pThis, float value) { - if (value > 0.01f || !SurfaceWantsRounding(pThis)) { - return value; - } - LogThrottled(L"forcing radius on a maximized/snapped window"); - return g_settings.radius; -} - -float __fastcall GetRadiusFromCornerStyle_hook(void* pThis) { - return FixRadius(pThis, GetRadiusFromCornerStyle_orig(pThis)); -} - -float __fastcall GetFloatCornerRadiusForCurrentStyle_hook(void* pThis) { - return FixRadius(pThis, GetFloatCornerRadiusForCurrentStyle_orig(pThis)); -} - -float __fastcall GetDpiAdjustedFloatCornerRadius_hook(void* pThis) { - float value = GetDpiAdjustedFloatCornerRadius_orig(pThis); - if (value > 0.01f || !SurfaceWantsRounding(pThis)) { - return value; - } + // Fall back to the system DPI, which is only wrong on mixed-DPI setups. UINT dpi = GetDpiForSystem(); - if (!dpi) dpi = 96; - return g_settings.radius * (dpi / 96.0f); + return g_settings.radius * (dpi ? dpi / 96.0f : 1.0f); } -// ------------------------------------------------------- symbols and cache - -struct ResolvedRvas { - int cornerStyle; - int isMaximized; - int radiusStyle; - int radiusCurrent; - int radiusDpi; -}; - -static bool GetModuleIdentity(HMODULE module, int* stamp, int* size) { - auto dos = reinterpret_cast(module); - if (!dos || dos->e_magic != IMAGE_DOS_SIGNATURE) { - return false; - } - auto nt = reinterpret_cast( - reinterpret_cast(module) + dos->e_lfanew); - if (nt->Signature != IMAGE_NT_SIGNATURE) { - return false; +// Builds that square a maximized window by reporting a "don't round" style. +using GetEffectiveCornerStyle_t = int(WINAPI*)(void* pThis); +GetEffectiveCornerStyle_t GetEffectiveCornerStyle_Original; +int WINAPI GetEffectiveCornerStyle_Hook(void* pThis) { + int orig = GetEffectiveCornerStyle_Original(pThis); + if (orig == DWMWCP_DONOTROUND && IsMaximizedOrSnapped(pThis)) { + Wh_Log(L"> DONOTROUND -> %d", g_settings.roundStyle); + return g_settings.roundStyle; } - *stamp = static_cast(nt->FileHeader.TimeDateStamp); - *size = static_cast(nt->OptionalHeader.SizeOfImage); - return true; + return orig; } -// filter is a comma-separated list of substrings; empty means "no match". -static bool MatchesFilterList(PCWSTR text, PCWSTR filter) { - if (!text || !filter || !*filter) { - return false; +// Builds that keep the rounded style and zero the radius instead. Which getter +// does the zeroing differs between builds, and a downstream getter may already +// see a value replaced upstream, so every replacement is guarded by "the radius +// is still zero". +using RadiusGetter_t = float(WINAPI*)(void* pThis); + +RadiusGetter_t GetRadiusFromCornerStyle_Original; +float WINAPI GetRadiusFromCornerStyle_Hook(void* pThis) { + float orig = GetRadiusFromCornerStyle_Original(pThis); + if (orig <= 0.0f && IsMaximizedOrSnapped(pThis)) { + Wh_Log(L"> radius 0 -> %f", g_settings.radius); + return g_settings.radius; } - - while (*filter) { - while (*filter == L' ' || *filter == L',') { - filter++; - } - WCHAR token[64]; - int n = 0; - while (*filter && *filter != L',' && n < ARRAYSIZE(token) - 1) { - token[n++] = *filter++; - } - while (n > 0 && token[n - 1] == L' ') { - n--; - } - token[n] = L'\0'; - if (n && wcsstr(text, token)) { - return true; - } - while (*filter && *filter != L',') { - filter++; - } - } - return false; + return orig; } -static bool ResolveBySymbols(HMODULE module, ResolvedRvas* out, - PCWSTR dumpFilter) { - WH_FIND_SYMBOL_OPTIONS options = {sizeof(options)}; - WH_FIND_SYMBOL symbol = {}; - - HANDLE find = Wh_FindFirstSymbol(module, &options, &symbol); - if (!find) { - return false; +RadiusGetter_t GetFloatCornerRadiusForCurrentStyle_Original; +float WINAPI GetFloatCornerRadiusForCurrentStyle_Hook(void* pThis) { + float orig = GetFloatCornerRadiusForCurrentStyle_Original(pThis); + if (orig <= 0.0f && IsMaximizedOrSnapped(pThis)) { + Wh_Log(L"> current style radius 0 -> %f", g_settings.radius); + return g_settings.radius; } - - auto base = reinterpret_cast(module); - *out = {}; - - do { - if (!symbol.symbol || !symbol.address) { - continue; - } - int rva = static_cast(reinterpret_cast(symbol.address) - base); - - if (MatchesFilterList(symbol.symbol, dumpFilter)) { - Wh_Log(L"[%08X] %s", rva, symbol.symbol); - } - - if (wcsstr(symbol.symbol, L"CTopLevelWindow::GetEffectiveCornerStyle")) { - out->cornerStyle = rva; - } else if (wcsstr(symbol.symbol, L"CTopLevelWindow::IsMaximizedOrSnapped")) { - out->isMaximized = rva; - } else if (wcsstr(symbol.symbol, L"CTopLevelWindow::GetRadiusFromCornerStyle")) { - out->radiusStyle = rva; - } else if (wcsstr(symbol.symbol, - L"CTopLevelWindow::GetFloatCornerRadiusForCurrentStyle")) { - out->radiusCurrent = rva; - } else if (wcsstr(symbol.symbol, - L"CTopLevelWindow::GetDpiAdjustedFloatCornerRadius")) { - out->radiusDpi = rva; - } - } while (Wh_FindNextSymbol(find, &symbol)); - - Wh_FindCloseSymbol(find); - return out->cornerStyle != 0; -} - -static void StoreRvas(int stamp, int size, const ResolvedRvas& rvas) { - Wh_SetIntValue(kKeyRvaCornerStyle, rvas.cornerStyle); - Wh_SetIntValue(kKeyRvaMaximized, rvas.isMaximized); - Wh_SetIntValue(kKeyRvaRadiusStyle, rvas.radiusStyle); - Wh_SetIntValue(kKeyRvaRadiusCurrent, rvas.radiusCurrent); - Wh_SetIntValue(kKeyRvaRadiusDpi, rvas.radiusDpi); - // Identity last: it validates everything above. - Wh_SetIntValue(kKeyStamp, stamp); - Wh_SetIntValue(kKeySize, size); - Wh_SetIntValue(kKeyCacheVer, kCacheVersion); + return orig; } -static bool LoadRvas(int stamp, int size, ResolvedRvas* out) { - if (Wh_GetIntValue(kKeyCacheVer, 0) != kCacheVersion || - Wh_GetIntValue(kKeyStamp, 0) != stamp || - Wh_GetIntValue(kKeySize, 0) != size) { - return false; +// Returns an already DPI-scaled value, so the replacement has to be scaled too. +RadiusGetter_t GetDpiAdjustedFloatCornerRadius_Original; +float WINAPI GetDpiAdjustedFloatCornerRadius_Hook(void* pThis) { + float orig = GetDpiAdjustedFloatCornerRadius_Original(pThis); + if (orig <= 0.0f && IsMaximizedOrSnapped(pThis)) { + float scaled = ScaledRadius(pThis); + Wh_Log(L"> dpi adjusted radius 0 -> %f", scaled); + return scaled; } - out->cornerStyle = Wh_GetIntValue(kKeyRvaCornerStyle, 0); - out->isMaximized = Wh_GetIntValue(kKeyRvaMaximized, 0); - out->radiusStyle = Wh_GetIntValue(kKeyRvaRadiusStyle, 0); - out->radiusCurrent = Wh_GetIntValue(kKeyRvaRadiusCurrent, 0); - out->radiusDpi = Wh_GetIntValue(kKeyRvaRadiusDpi, 0); - return out->cornerStyle != 0; + return orig; } -// ---------------------------------------------------------------- settings - -static void LoadSettings() { - PCWSTR style = Wh_GetStringSetting(L"roundStyle"); +void LoadSettings() { + WindhawkUtils::StringSetting style = + WindhawkUtils::StringSetting::make(L"roundStyle"); g_settings.roundStyle = - (style && wcscmp(style, L"small") == 0) ? kCornerRoundSmall : kCornerRound; - Wh_FreeStringSetting(style); - - g_settings.noFilter = Wh_GetIntSetting(L"noFilter") != 0; - g_settings.forceRadius = Wh_GetIntSetting(L"forceRadius") != 0; - g_settings.debugLog = Wh_GetIntSetting(L"debugLog") != 0; + wcscmp(style.get(), L"small") == 0 ? DWMWCP_ROUNDSMALL : DWMWCP_ROUND; int radius = Wh_GetIntSetting(L"radius"); - if (radius < 1) radius = 8; - if (radius > 60) radius = 60; + if (radius < 1) { + radius = 1; + } else if (radius > 40) { + radius = 40; + } g_settings.radius = static_cast(radius); } -// -------------------------------------------------------------- entrypoints - -static bool RunningInDwm() { - WCHAR path[MAX_PATH]{}; - if (!GetModuleFileNameW(nullptr, path, ARRAYSIZE(path))) { +// Returns true if at least two Dwminit warnings (Level=3) were logged in the +// Application event log within the last 60 seconds. DWM logs warnings here when +// it crashes and is restarted by the session manager, so repeated warnings are +// a strong signal that something in the desktop pipeline is unstable, and a mod +// hooking the compositor should stay out of the way. +bool HasMultipleDwminitWarningsInLastMinute() { + const WCHAR* queryPath = L"Application"; + const WCHAR* query = + L"*[System[Provider[@Name='Dwminit'] and (Level=3) and " + L"TimeCreated[timediff(@SystemTime) <= 60000]]]"; + + EVT_HANDLE queryHandle = + EvtQuery(nullptr, queryPath, query, EvtQueryChannelPath); + if (!queryHandle) { + Wh_Log(L"EvtQuery failed with error: %u", GetLastError()); return false; } - PCWSTR name = wcsrchr(path, L'\\'); - name = name ? name + 1 : path; - return _wcsicmp(name, L"dwm.exe") == 0; -} - -BOOL Wh_ModInit() { - LoadSettings(); - HMODULE hUDWM = GetModuleHandleW(L"uDWM.dll"); - if (!hUDWM) { - hUDWM = LoadLibraryW(L"uDWM.dll"); - } - if (!hUDWM) { - Wh_Log(L"uDWM.dll not available"); - return FALSE; + EVT_HANDLE events[2] = {}; + DWORD returned = 0; + constexpr DWORD kTimeout = 1000; + BOOL ok = + EvtNext(queryHandle, ARRAYSIZE(events), events, kTimeout, 0, &returned); + if (!ok && GetLastError() != ERROR_NO_MORE_ITEMS) { + Wh_Log(L"EvtNext failed with error: %u", GetLastError()); } - - int stamp = 0, size = 0; - if (!GetModuleIdentity(hUDWM, &stamp, &size)) { - Wh_Log(L"Bad uDWM.dll headers"); - return FALSE; + for (DWORD i = 0; i < returned; i++) { + EvtClose(events[i]); } - // --- Warmer mode: resolve and cache the addresses for dwm.exe. - if (!RunningInDwm()) { - PCWSTR dump = Wh_GetStringSetting(L"dumpSymbols"); - bool wantDump = dump && *dump; - - ResolvedRvas cached{}; - if (!wantDump && LoadRvas(stamp, size, &cached)) { - Wh_Log(L"warmer: RVAs already cached for this uDWM build"); - Wh_FreeStringSetting(dump); - return TRUE; - } - - ResolvedRvas rvas{}; - bool ok = ResolveBySymbols(hUDWM, &rvas, dump); - Wh_FreeStringSetting(dump); - - if (!ok) { - Wh_Log(L"warmer: failed to enumerate uDWM.dll symbols here too"); - return TRUE; - } + EvtClose(queryHandle); + return ok && returned >= ARRAYSIZE(events); +} - StoreRvas(stamp, size, rvas); - Wh_Log(L"warmer: stored RVAs style=%08X maximized=%08X", rvas.cornerStyle, - rvas.isMaximized); - return TRUE; - } +BOOL Wh_ModInit() { + Wh_Log(L">"); - // --- dwm.exe: cache first, own enumeration as a fallback. - ResolvedRvas rvas{}; - if (LoadRvas(stamp, size, &rvas)) { - Wh_Log(L"dwm: using cached RVAs"); - } else if (ResolveBySymbols(hUDWM, &rvas, nullptr)) { - Wh_Log(L"dwm: resolved symbols locally"); - StoreRvas(stamp, size, rvas); - } else { - Wh_Log(L"dwm: no cached RVAs and no symbols - let explorer.exe warm " - L"the cache, then re-enable the mod"); + if (HasMultipleDwminitWarningsInLastMinute()) { + Wh_Log(L"Refusing to load: multiple recent Dwminit warnings"); return FALSE; } - auto base = reinterpret_cast(hUDWM); - - if (rvas.isMaximized) { - IsMaximizedOrSnapped = - reinterpret_cast(base + rvas.isMaximized); - } else if (!g_settings.noFilter) { - Wh_Log(L"dwm: CTopLevelWindow::IsMaximizedOrSnapped not found - " - L"staying passive to avoid rounding internal surfaces"); - } - - Wh_Log(L"dwm: RVAs style=%08X maximized=%08X radius=%08X/%08X/%08X", - rvas.cornerStyle, rvas.isMaximized, rvas.radiusStyle, - rvas.radiusCurrent, rvas.radiusDpi); - - int hooked = 0; + LoadSettings(); - if (rvas.cornerStyle && - Wh_SetFunctionHook(base + rvas.cornerStyle, - (void*)GetEffectiveCornerStyle_hook, - (void**)&GetEffectiveCornerStyle_orig)) { - hooked++; - } - // Always hooked; whether they change anything is decided per call, so the - // radius setting can be toggled without recompiling. - if (rvas.radiusStyle && - Wh_SetFunctionHook(base + rvas.radiusStyle, - (void*)GetRadiusFromCornerStyle_hook, - (void**)&GetRadiusFromCornerStyle_orig)) { - hooked++; - } - if (rvas.radiusCurrent && - Wh_SetFunctionHook(base + rvas.radiusCurrent, - (void*)GetFloatCornerRadiusForCurrentStyle_hook, - (void**)&GetFloatCornerRadiusForCurrentStyle_orig)) { - hooked++; - } - if (rvas.radiusDpi && - Wh_SetFunctionHook(base + rvas.radiusDpi, - (void*)GetDpiAdjustedFloatCornerRadius_hook, - (void**)&GetDpiAdjustedFloatCornerRadius_orig)) { - hooked++; + HMODULE udwm = GetModuleHandle(L"udwm.dll"); + if (!udwm) { + Wh_Log(L"udwm.dll isn't loaded"); + return FALSE; } - if (!hooked) { - Wh_Log(L"dwm: nothing hooked"); + WindhawkUtils::SYMBOL_HOOK udwmDllHooks[] = { + // The filter. Without it there's no way to tell app windows from + // internal DWM surfaces, and rounding those leaves visible outlines, so + // this one is mandatory. + { + {LR"(public: bool __cdecl CTopLevelWindow::IsMaximizedOrSnapped(void)const )"}, + &IsMaximizedOrSnapped, + nullptr, // Capture only. + }, + { + {LR"(private: enum CORNER_STYLE __cdecl CTopLevelWindow::GetEffectiveCornerStyle(void))"}, + &GetEffectiveCornerStyle_Original, + GetEffectiveCornerStyle_Hook, + }, + { + {LR"(private: float __cdecl CTopLevelWindow::GetRadiusFromCornerStyle(void))"}, + &GetRadiusFromCornerStyle_Original, + GetRadiusFromCornerStyle_Hook, + }, + // The next two come in const and non-const, private and public flavors + // depending on the build, and are missing entirely in older ones. Only + // CTopLevelWindow overloads are listed on purpose: the hooks pass + // `this` to IsMaximizedOrSnapped, so a same-named method on another + // class must not match. + { + { + LR"(private: float __cdecl CTopLevelWindow::GetFloatCornerRadiusForCurrentStyle(void))", + LR"(private: float __cdecl CTopLevelWindow::GetFloatCornerRadiusForCurrentStyle(void)const )", + LR"(public: float __cdecl CTopLevelWindow::GetFloatCornerRadiusForCurrentStyle(void))", + LR"(public: float __cdecl CTopLevelWindow::GetFloatCornerRadiusForCurrentStyle(void)const )", + }, + &GetFloatCornerRadiusForCurrentStyle_Original, + GetFloatCornerRadiusForCurrentStyle_Hook, + true, // Optional. + }, + { + { + LR"(private: float __cdecl CTopLevelWindow::GetDpiAdjustedFloatCornerRadius(void))", + LR"(private: float __cdecl CTopLevelWindow::GetDpiAdjustedFloatCornerRadius(void)const )", + LR"(public: float __cdecl CTopLevelWindow::GetDpiAdjustedFloatCornerRadius(void))", + LR"(public: float __cdecl CTopLevelWindow::GetDpiAdjustedFloatCornerRadius(void)const )", + }, + &GetDpiAdjustedFloatCornerRadius_Original, + GetDpiAdjustedFloatCornerRadius_Hook, + true, // Optional. + }, + // Used only to scale a forced radius the way DWM would. + { + {LR"(public: class CWindowData * __cdecl CTopLevelWindow::GetWindowData(void)const )"}, + &GetWindowData_Original, + nullptr, // Capture only. + true, // Optional - falls back to the system DPI. + }, + { + {LR"(public: unsigned int __cdecl CWindowData::ScaleForDpi(unsigned int)const )"}, + &ScaleForDpi_Original, + nullptr, // Capture only. + true, // Optional - falls back to the system DPI. + }, + }; + + if (!HookSymbols(udwm, udwmDllHooks, ARRAYSIZE(udwmDllHooks))) { + Wh_Log(L"HookSymbols failed"); return FALSE; } - Wh_Log(L"dwm: hooked %d function(s), filter=%s", hooked, - IsMaximizedOrSnapped ? L"IsMaximizedOrSnapped" : L"none"); return TRUE; } void Wh_ModSettingsChanged() { + Wh_Log(L">"); + LoadSettings(); - SystemParametersInfoW(SPI_SETDRAGFULLWINDOWS, TRUE, nullptr, SPIF_SENDCHANGE); } void Wh_ModUninit() { - Wh_Log(L"Unloaded"); + Wh_Log(L">"); } From e0a97cf0150403d8173e695cf4eab867fdca9e39 Mon Sep 17 00:00:00 2001 From: Alexey <112402687+leshaalexey@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:24:44 +0300 Subject: [PATCH 04/13] Update mod readme with authorship information Added authorship section to mod readme. --- mods/rounded-corners-when-snapped.wh.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/mods/rounded-corners-when-snapped.wh.cpp b/mods/rounded-corners-when-snapped.wh.cpp index 971f857121..04fbe96945 100644 --- a/mods/rounded-corners-when-snapped.wh.cpp +++ b/mods/rounded-corners-when-snapped.wh.cpp @@ -56,6 +56,20 @@ Other mods and tools that patch DWM corners hook the same functions and will fight with this one. Don't run it together with **Custom Window Corner Radius**, **Disable rounded corners in Windows 11**, ExplorerPatcher's rounded corner option, StartAllBack or Win11DisableRoundedCorners. + +## Mod authorship + +If this pull request introduces a new mod, please complete the section below. + +This mod was created by: + +- - [ ] The submitter, without AI assistance +- - [x] The submitter, with AI assistance +- - [x] Claude +- - [ ] ChatGPT +- - [ ] Gemini +- - [ ] Another AI (please specify): +- - [ ] Other (please specify): */ // ==/WindhawkModReadme== From ebd6f9dde1aacaf3234b30692462a2a531233178 Mon Sep 17 00:00:00 2001 From: Alexey <112402687+leshaalexey@users.noreply.github.com> Date: Wed, 5 Aug 2026 17:17:57 +0300 Subject: [PATCH 05/13] Update mod name and description for clarity --- mods/rounded-corners-when-snapped.wh.cpp | 170 +++++++++++++---------- 1 file changed, 97 insertions(+), 73 deletions(-) diff --git a/mods/rounded-corners-when-snapped.wh.cpp b/mods/rounded-corners-when-snapped.wh.cpp index 04fbe96945..85bb711496 100644 --- a/mods/rounded-corners-when-snapped.wh.cpp +++ b/mods/rounded-corners-when-snapped.wh.cpp @@ -1,10 +1,10 @@ // ==WindhawkMod== // @id rounded-corners-when-snapped -// @name Rounded corners when snapped or maximized -// @description Keeps window corners rounded when a window is snapped or maximized, without changing the window state +// @name Rounded corners for snapped windows +// @description Keeps window corners rounded when a window is snapped, without changing the window state // @version 1.0.0 // @author Alexey Lavrinenko -// @github https://github.com/leshaalexey +// @github https://github.com/YOUR-GITHUB-USERNAME // @license GPL-3.0 // @include dwm.exe // @architecture x86-64 @@ -17,15 +17,14 @@ // ==WindhawkModReadme== /* -# Rounded corners when snapped or maximized +# Rounded corners for snapped windows -Windows 11 squares off a window's corners as soon as it is maximized or -snapped. This mod keeps them rounded, the way macOS does. +Windows 11 squares off a window's corners as soon as it is snapped. This mod +keeps them rounded. -The window state is untouched: it is really snapped, really maximized, and -Snap Layouts, Snap Assist, Win+Arrow and the maximize button all behave exactly -as before. Only the compositor's drawing changes, and disabling the mod -restores the default look immediately. +The window state is untouched: it is really snapped, and Snap Layouts, Snap +Assist and Win+Arrow behave exactly as before. Only the compositor's drawing +changes, and disabling the mod restores the default look immediately. ## ⚠ Important usage note ⚠ @@ -33,22 +32,31 @@ This mod needs to hook into `dwm.exe` to work. Please navigate to Windhawk's Settings > Advanced settings > More advanced settings > Process inclusion list, and make sure that `dwm.exe` is in the list. -## How it works +## Why maximized windows are left alone + +A window maximized over the whole screen is presented through *direct flip*: +its buffer goes to the display without DWM composing the frame, which is also +why Windows doesn't round it in the first place. Rounded corners drawn for such +a window only show up while something forces composition — the Start menu, a +notification, Alt+Tab — and disappear again the moment the overlay goes away. +In apps that draw their own frame (browsers, Electron apps) that reads as +corners flickering between round and square. -Windows are squared off in one of two ways, depending on the build: either the -corner style comes out as `DWMWCP_DONOTROUND`, or the style stays rounded and -the corner radius is zeroed further down the pipeline. The mod covers both, by -hooking `CTopLevelWindow::GetEffectiveCornerStyle` and the corner radius -getters of the same class. +Snapped windows are composed normally, so their corners stay rounded at all +times. Maximized windows are therefore skipped by default. The behaviour can be +turned on with the *Also round maximized windows* option, with the caveat +above — it looks fine in apps with a standard window frame. -Every replacement is gated on `CTopLevelWindow::IsMaximizedOrSnapped` — DWM's -own answer about the very surface being composed. The Start menu backdrop, the -virtual-desktop switch animation, drag previews and fullscreen windows answer -`false` and are left exactly as DWM wanted them, so no stray outlines appear. +## How it works -A forced radius is scaled through `CWindowData::ScaleForDpi`, the same way DWM -scales its own, so it comes out the right size on every monitor of a mixed-DPI -setup. +Two `CTopLevelWindow` methods of `uDWM.dll` are involved: +`GetEffectiveCornerStyle`, which decides the corner style of a composed +surface, and `IsMaximizedOrSnapped`, which is DWM's own answer about that very +surface. Depending on the build, a snapped window is squared off either through +the style or by zeroing the corner radius, so the radius getters of the same +class are hooked as well. Every replacement is gated on `IsMaximizedOrSnapped`, +so the Start menu backdrop, the virtual-desktop switch animation, drag previews +and fullscreen windows are left exactly as DWM wanted them. ## Compatibility @@ -56,29 +64,20 @@ Other mods and tools that patch DWM corners hook the same functions and will fight with this one. Don't run it together with **Custom Window Corner Radius**, **Disable rounded corners in Windows 11**, ExplorerPatcher's rounded corner option, StartAllBack or Win11DisableRoundedCorners. - -## Mod authorship - -If this pull request introduces a new mod, please complete the section below. - -This mod was created by: - -- - [ ] The submitter, without AI assistance -- - [x] The submitter, with AI assistance -- - [x] Claude -- - [ ] ChatGPT -- - [ ] Gemini -- - [ ] Another AI (please specify): -- - [ ] Other (please specify): */ // ==/WindhawkModReadme== // ==WindhawkModSettings== /* +- roundMaximized: false + $name: Also round maximized windows + $description: >- + Maximized windows are presented without DWM composing them, so their + rounded corners only appear while the Start menu, a notification or Alt+Tab + is on screen. Fine with a standard window frame, flickers in apps that draw + their own. - roundStyle: round $name: Corner style - $description: >- - Which rounding to apply. "Normal" matches an ordinary unsnapped window. $options: - round: Normal - small: Small, like a menu @@ -97,6 +96,7 @@ This mod was created by: #include struct { + bool roundMaximized; int roundStyle; float radius; } g_settings; @@ -105,27 +105,24 @@ struct { using IsMaximizedOrSnapped_t = bool(WINAPI*)(void* pThis); IsMaximizedOrSnapped_t IsMaximizedOrSnapped; -// Captured, not hooked: DWM's own per-window DPI scaling, so a forced radius -// comes out the right size on every monitor. -using GetWindowData_t = void*(WINAPI*)(void* pThis); -GetWindowData_t GetWindowData_Original; +// True for a window filling its monitor's work area, as opposed to one snapped +// to part of it. +bool CoversWorkArea(const RECT& rect) { + MONITORINFO mi{sizeof(MONITORINFO)}; + if (!GetMonitorInfoW(MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST), + &mi)) { + return false; + } -using ScaleForDpi_t = unsigned int(WINAPI*)(void* pThis, unsigned int value); -ScaleForDpi_t ScaleForDpi_Original; + LONG width = rect.right - rect.left; + LONG height = rect.bottom - rect.top; + LONG workWidth = mi.rcWork.right - mi.rcWork.left; + LONG workHeight = mi.rcWork.bottom - mi.rcWork.top; -float ScaledRadius(void* pThis) { - if (GetWindowData_Original && ScaleForDpi_Original) { - if (void* data = GetWindowData_Original(pThis)) { - return static_cast(ScaleForDpi_Original( - data, static_cast(g_settings.radius))); - } - } - // Fall back to the system DPI, which is only wrong on mixed-DPI setups. - UINT dpi = GetDpiForSystem(); - return g_settings.radius * (dpi ? dpi / 96.0f : 1.0f); + return width * 100 >= workWidth * 95 && height * 100 >= workHeight * 95; } -// Builds that square a maximized window by reporting a "don't round" style. +// Builds that square a snapped window by reporting a "don't round" style. using GetEffectiveCornerStyle_t = int(WINAPI*)(void* pThis); GetEffectiveCornerStyle_t GetEffectiveCornerStyle_Original; int WINAPI GetEffectiveCornerStyle_Hook(void* pThis) { @@ -163,16 +160,47 @@ float WINAPI GetFloatCornerRadiusForCurrentStyle_Hook(void* pThis) { return orig; } -// Returns an already DPI-scaled value, so the replacement has to be scaled too. +// Already DPI-scaled, hence the separate hook. The unscaled getter above feeds +// it on current builds, so it only fires where that isn't the case. RadiusGetter_t GetDpiAdjustedFloatCornerRadius_Original; float WINAPI GetDpiAdjustedFloatCornerRadius_Hook(void* pThis) { float orig = GetDpiAdjustedFloatCornerRadius_Original(pThis); - if (orig <= 0.0f && IsMaximizedOrSnapped(pThis)) { - float scaled = ScaledRadius(pThis); - Wh_Log(L"> dpi adjusted radius 0 -> %f", scaled); - return scaled; + if (orig > 0.0f || !IsMaximizedOrSnapped(pThis)) { + return orig; } - return orig; + + UINT dpi = GetDpiForSystem(); + float value = g_settings.radius * (dpi ? dpi / 96.0f : 1.0f); + Wh_Log(L"> dpi adjusted radius 0 -> %f", value); + return value; +} + +// The window border is where the rounding actually becomes visible, and the +// only place with a rectangle to tell a snapped window from a maximized one. +using SetBorderParameters_t = long(WINAPI*)(void* pThis, + const RECT& borderRect, + float cornerRadius, + int dpi, + const void* color, + int borderStyle, + int shadowStyle); +SetBorderParameters_t SetBorderParameters_Original; +long WINAPI SetBorderParameters_Hook(void* pThis, + const RECT& borderRect, + float cornerRadius, + int dpi, + const void* color, + int borderStyle, + int shadowStyle) { + if (cornerRadius > 0.0f && !g_settings.roundMaximized && + CoversWorkArea(borderRect)) { + // Maximized: DWM presents it without composing, so a rounded border + // would only show while something else is drawn on top. + Wh_Log(L"> maximized, leaving the border square"); + cornerRadius = 0.0f; + } + return SetBorderParameters_Original(pThis, borderRect, cornerRadius, dpi, + color, borderStyle, shadowStyle); } void LoadSettings() { @@ -181,6 +209,8 @@ void LoadSettings() { g_settings.roundStyle = wcscmp(style.get(), L"small") == 0 ? DWMWCP_ROUNDSMALL : DWMWCP_ROUND; + g_settings.roundMaximized = Wh_GetIntSetting(L"roundMaximized") != 0; + int radius = Wh_GetIntSetting(L"radius"); if (radius < 1) { radius = 1; @@ -286,18 +316,12 @@ BOOL Wh_ModInit() { GetDpiAdjustedFloatCornerRadius_Hook, true, // Optional. }, - // Used only to scale a forced radius the way DWM would. + // Keeps maximized windows square unless the user asks otherwise. { - {LR"(public: class CWindowData * __cdecl CTopLevelWindow::GetWindowData(void)const )"}, - &GetWindowData_Original, - nullptr, // Capture only. - true, // Optional - falls back to the system DPI. - }, - { - {LR"(public: unsigned int __cdecl CWindowData::ScaleForDpi(unsigned int)const )"}, - &ScaleForDpi_Original, - nullptr, // Capture only. - true, // Optional - falls back to the system DPI. + {LR"(public: long __cdecl CWindowBorder::SetBorderParameters(struct tagRECT const &,float,int,struct _D3DCOLORVALUE const &,enum CWindowBorder::BorderStyle,enum CWindowBorder::ShadowStyle))"}, + &SetBorderParameters_Original, + SetBorderParameters_Hook, + true, // Optional. }, }; From f18a8e588e7b0c4e6041aab6964649285f9ca308 Mon Sep 17 00:00:00 2001 From: Alexey <112402687+leshaalexey@users.noreply.github.com> Date: Wed, 5 Aug 2026 17:20:10 +0300 Subject: [PATCH 06/13] Revise mod authorship details in README Updated mod authorship section to indicate AI assistance. --- mods/rounded-corners-when-snapped.wh.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/mods/rounded-corners-when-snapped.wh.cpp b/mods/rounded-corners-when-snapped.wh.cpp index 85bb711496..88da3b317d 100644 --- a/mods/rounded-corners-when-snapped.wh.cpp +++ b/mods/rounded-corners-when-snapped.wh.cpp @@ -64,6 +64,20 @@ Other mods and tools that patch DWM corners hook the same functions and will fight with this one. Don't run it together with **Custom Window Corner Radius**, **Disable rounded corners in Windows 11**, ExplorerPatcher's rounded corner option, StartAllBack or Win11DisableRoundedCorners. + +## Mod authorship + +If this pull request introduces a new mod, please complete the section below. + +This mod was created by: + +- - [ ] The submitter, without AI assistance +- - [x] The submitter, with AI assistance +- - [x] Claude +- - [ ] ChatGPT +- - [ ] Gemini +- - [ ] Another AI (please specify): +- - [ ] Other (please specify): */ // ==/WindhawkModReadme== From 341eb1e158c61eec088cc9423434c5d32da7611c Mon Sep 17 00:00:00 2001 From: Alexey <112402687+leshaalexey@users.noreply.github.com> Date: Wed, 5 Aug 2026 17:37:59 +0300 Subject: [PATCH 07/13] Update GitHub link in metadata --- mods/rounded-corners-when-snapped.wh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mods/rounded-corners-when-snapped.wh.cpp b/mods/rounded-corners-when-snapped.wh.cpp index 88da3b317d..969ad63ba9 100644 --- a/mods/rounded-corners-when-snapped.wh.cpp +++ b/mods/rounded-corners-when-snapped.wh.cpp @@ -4,7 +4,7 @@ // @description Keeps window corners rounded when a window is snapped, without changing the window state // @version 1.0.0 // @author Alexey Lavrinenko -// @github https://github.com/YOUR-GITHUB-USERNAME +// @github https://github.com/leshaalexey // @license GPL-3.0 // @include dwm.exe // @architecture x86-64 From 214b4d696ca17f5322b88e5072261df2f0bfebfb Mon Sep 17 00:00:00 2001 From: Alexey <112402687+leshaalexey@users.noreply.github.com> Date: Wed, 5 Aug 2026 17:43:42 +0300 Subject: [PATCH 08/13] Modify mod authorship section Updated authorship section to reflect AI assistance. --- mods/rounded-corners-when-snapped.wh.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mods/rounded-corners-when-snapped.wh.cpp b/mods/rounded-corners-when-snapped.wh.cpp index 969ad63ba9..83dc1ca669 100644 --- a/mods/rounded-corners-when-snapped.wh.cpp +++ b/mods/rounded-corners-when-snapped.wh.cpp @@ -66,11 +66,11 @@ Radius**, **Disable rounded corners in Windows 11**, ExplorerPatcher's rounded corner option, StartAllBack or Win11DisableRoundedCorners. ## Mod authorship - + If this pull request introduces a new mod, please complete the section below. - + This mod was created by: - + - - [ ] The submitter, without AI assistance - - [x] The submitter, with AI assistance - - [x] Claude From 7420047149e754e4dbd2b20f7a7ad3a083d7f168 Mon Sep 17 00:00:00 2001 From: Alexey <112402687+leshaalexey@users.noreply.github.com> Date: Wed, 5 Aug 2026 17:47:03 +0300 Subject: [PATCH 09/13] Add changelog and authorship sections to mod file Added changelog section for mod updates and authorship. --- mods/rounded-corners-when-snapped.wh.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mods/rounded-corners-when-snapped.wh.cpp b/mods/rounded-corners-when-snapped.wh.cpp index 83dc1ca669..58354e627e 100644 --- a/mods/rounded-corners-when-snapped.wh.cpp +++ b/mods/rounded-corners-when-snapped.wh.cpp @@ -65,6 +65,14 @@ fight with this one. Don't run it together with **Custom Window Corner Radius**, **Disable rounded corners in Windows 11**, ExplorerPatcher's rounded corner option, StartAllBack or Win11DisableRoundedCorners. +## Changelog + +If this pull request updates an existing mod, describe the changes below: + +* Changelog item 1... +* Changelog item 2... + + ## Mod authorship If this pull request introduces a new mod, please complete the section below. From f090632dafbfc8d0fef672d35af353dad7a97678 Mon Sep 17 00:00:00 2001 From: Alexey <112402687+leshaalexey@users.noreply.github.com> Date: Wed, 5 Aug 2026 18:03:45 +0300 Subject: [PATCH 10/13] Update rounded-corners-when-snapped.wh.cpp --- mods/rounded-corners-when-snapped.wh.cpp | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/mods/rounded-corners-when-snapped.wh.cpp b/mods/rounded-corners-when-snapped.wh.cpp index 58354e627e..84e6ea22d0 100644 --- a/mods/rounded-corners-when-snapped.wh.cpp +++ b/mods/rounded-corners-when-snapped.wh.cpp @@ -66,19 +66,15 @@ Radius**, **Disable rounded corners in Windows 11**, ExplorerPatcher's rounded corner option, StartAllBack or Win11DisableRoundedCorners. ## Changelog - -If this pull request updates an existing mod, describe the changes below: - -* Changelog item 1... -* Changelog item 2... - - + +This pull request introduces a new mod. + ## Mod authorship - + If this pull request introduces a new mod, please complete the section below. - + This mod was created by: - + - - [ ] The submitter, without AI assistance - - [x] The submitter, with AI assistance - - [x] Claude @@ -86,6 +82,7 @@ This mod was created by: - - [ ] Gemini - - [ ] Another AI (please specify): - - [ ] Other (please specify): +Please select the options that best apply. Your selection does not affect the acceptance criteria, but it helps reviewers understand the context of the code and provide relevant feedback. */ // ==/WindhawkModReadme== From 5b072b6ccb7de76fd4a2af5d922e283e0589410f Mon Sep 17 00:00:00 2001 From: Alexey <112402687+leshaalexey@users.noreply.github.com> Date: Sat, 8 Aug 2026 10:42:35 +0300 Subject: [PATCH 11/13] Modify GitHub link and clean up comments Updated GitHub link and compiler options in the mod file. Removed unnecessary changelog and authorship sections. --- mods/rounded-corners-when-snapped.wh.cpp | 218 ++++++++++++++++++++--- 1 file changed, 193 insertions(+), 25 deletions(-) diff --git a/mods/rounded-corners-when-snapped.wh.cpp b/mods/rounded-corners-when-snapped.wh.cpp index 84e6ea22d0..69f4ff47cf 100644 --- a/mods/rounded-corners-when-snapped.wh.cpp +++ b/mods/rounded-corners-when-snapped.wh.cpp @@ -4,11 +4,11 @@ // @description Keeps window corners rounded when a window is snapped, without changing the window state // @version 1.0.0 // @author Alexey Lavrinenko -// @github https://github.com/leshaalexey +// @github https://github.com/YOUR-GITHUB-USERNAME // @license GPL-3.0 // @include dwm.exe // @architecture x86-64 -// @compilerOptions -lwevtapi +// @compilerOptions -lgdi32 -lwevtapi // ==/WindhawkMod== // HasMultipleDwminitWarningsInLastMinute() is taken from the Custom Window @@ -64,25 +64,6 @@ Other mods and tools that patch DWM corners hook the same functions and will fight with this one. Don't run it together with **Custom Window Corner Radius**, **Disable rounded corners in Windows 11**, ExplorerPatcher's rounded corner option, StartAllBack or Win11DisableRoundedCorners. - -## Changelog - -This pull request introduces a new mod. - -## Mod authorship - -If this pull request introduces a new mod, please complete the section below. - -This mod was created by: - -- - [ ] The submitter, without AI assistance -- - [x] The submitter, with AI assistance -- - [x] Claude -- - [ ] ChatGPT -- - [ ] Gemini -- - [ ] Another AI (please specify): -- - [ ] Other (please specify): -Please select the options that best apply. Your selection does not affect the acceptance criteria, but it helps reviewers understand the context of the code and provide relevant feedback. */ // ==/WindhawkModReadme== @@ -91,10 +72,11 @@ Please select the options that best apply. Your selection does not affect the ac - roundMaximized: false $name: Also round maximized windows $description: >- - Maximized windows are presented without DWM composing them, so their - rounded corners only appear while the Start menu, a notification or Alt+Tab - is on screen. Fine with a standard window frame, flickers in apps that draw - their own. + A maximized window is normally presented without DWM composing it, which is + why Windows squares its corners. To round it, the mod gives the window a + rounded region, which brings it back into composition and clips the app's + own content. That costs one composition pass for that window - a little more + GPU work and battery use while a window is maximized. - roundStyle: round $name: Corner style $options: @@ -222,6 +204,171 @@ long WINAPI SetBorderParameters_Hook(void* pThis, color, borderStyle, shadowStyle); } +// --------------------------------------------------------------------------- +// Maximized windows: a window with a region is no longer a plain rectangle, so +// it stops being a direct flip candidate and DWM composes it again - which is +// what makes the rounding above visible. The region also clips the app's own +// content, so apps that paint their own frame can't fill the corner. +// +// The cost is one composition pass for that window, which is exactly what +// Windows avoids by squaring the corners, so this is opt-in. +// --------------------------------------------------------------------------- + +HANDLE g_regionThread; +DWORD g_regionThreadId; +HWINEVENTHOOK g_locationHook; +HWINEVENTHOOK g_stateHook; + +CRITICAL_SECTION g_regionCs; +constexpr int kMaxTrackedWindows = 64; +HWND g_regionedWindows[kMaxTrackedWindows]; + +void RememberWindow(HWND hwnd) { + EnterCriticalSection(&g_regionCs); + int free = -1; + for (int i = 0; i < kMaxTrackedWindows; i++) { + if (g_regionedWindows[i] == hwnd) { + free = -1; + break; + } + if (!g_regionedWindows[i] && free < 0) { + free = i; + } + } + if (free >= 0) { + g_regionedWindows[free] = hwnd; + } + LeaveCriticalSection(&g_regionCs); +} + +void ForgetWindow(HWND hwnd) { + EnterCriticalSection(&g_regionCs); + for (int i = 0; i < kMaxTrackedWindows; i++) { + if (g_regionedWindows[i] == hwnd) { + g_regionedWindows[i] = nullptr; + } + } + LeaveCriticalSection(&g_regionCs); +} + +bool IsRoundableMaximizedWindow(HWND hwnd) { + if (!hwnd || !IsWindow(hwnd) || !IsWindowVisible(hwnd) || !IsZoomed(hwnd)) { + return false; + } + + LONG style = GetWindowLongW(hwnd, GWL_STYLE); + if ((style & WS_CHILD) || !(style & (WS_CAPTION | WS_THICKFRAME))) { + return false; + } + if (GetWindowLongW(hwnd, GWL_EXSTYLE) & WS_EX_TOOLWINDOW) { + return false; + } + + // Leave true fullscreen alone - games and video players. + RECT rect; + MONITORINFO mi{sizeof(MONITORINFO)}; + if (GetWindowRect(hwnd, &rect) && + GetMonitorInfoW(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), + &mi) && + EqualRect(&rect, &mi.rcMonitor)) { + return false; + } + return true; +} + +void UpdateWindowRegion(HWND hwnd) { + if (!g_settings.roundMaximized) { + return; + } + + if (!IsRoundableMaximizedWindow(hwnd)) { + ForgetWindow(hwnd); + return; + } + + RECT rect; + if (!GetWindowRect(hwnd, &rect)) { + return; + } + + int width = rect.right - rect.left; + int height = rect.bottom - rect.top; + if (width <= 0 || height <= 0) { + return; + } + + UINT dpi = GetDpiForWindow(hwnd); + if (!dpi) { + dpi = 96; + } + int radius = static_cast(g_settings.radius * dpi / 96.0f); + + // The region is in window coordinates, and a maximized window overhangs the + // work area by its border width on every side. + HRGN region = CreateRoundRectRgn(0, 0, width + 1, height + 1, radius * 2, + radius * 2); + if (!region) { + return; + } + + if (SetWindowRgn(hwnd, region, TRUE)) { + RememberWindow(hwnd); // SetWindowRgn took ownership. + } else { + DeleteObject(region); + } +} + +void CALLBACK WinEventProc(HWINEVENTHOOK, + DWORD event, + HWND hwnd, + LONG idObject, + LONG idChild, + DWORD, + DWORD) { + if (idObject != OBJID_WINDOW || idChild != CHILDID_SELF) { + return; + } + if (event == EVENT_OBJECT_DESTROY) { + ForgetWindow(hwnd); + return; + } + UpdateWindowRegion(hwnd); +} + +DWORD WINAPI RegionThreadProc(LPVOID) { + g_stateHook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, + EVENT_SYSTEM_MINIMIZEEND, nullptr, + WinEventProc, 0, 0, WINEVENT_OUTOFCONTEXT); + g_locationHook = SetWinEventHook(EVENT_OBJECT_DESTROY, + EVENT_OBJECT_LOCATIONCHANGE, nullptr, + WinEventProc, 0, 0, WINEVENT_OUTOFCONTEXT); + + MSG msg; + while (GetMessage(&msg, nullptr, 0, 0) > 0) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + if (g_stateHook) { + UnhookWinEvent(g_stateHook); + } + if (g_locationHook) { + UnhookWinEvent(g_locationHook); + } + return 0; +} + +void ClearAllWindowRegions() { + EnterCriticalSection(&g_regionCs); + for (int i = 0; i < kMaxTrackedWindows; i++) { + if (g_regionedWindows[i] && IsWindow(g_regionedWindows[i])) { + SetWindowRgn(g_regionedWindows[i], nullptr, TRUE); + } + g_regionedWindows[i] = nullptr; + } + LeaveCriticalSection(&g_regionCs); +} + void LoadSettings() { WindhawkUtils::StringSetting style = WindhawkUtils::StringSetting::make(L"roundStyle"); @@ -349,6 +496,13 @@ BOOL Wh_ModInit() { return FALSE; } + InitializeCriticalSection(&g_regionCs); + g_regionThread = + CreateThread(nullptr, 0, RegionThreadProc, nullptr, 0, &g_regionThreadId); + if (!g_regionThread) { + Wh_Log(L"Failed to start the region thread"); + } + return TRUE; } @@ -356,8 +510,22 @@ void Wh_ModSettingsChanged() { Wh_Log(L">"); LoadSettings(); + + if (!g_settings.roundMaximized) { + ClearAllWindowRegions(); + } } void Wh_ModUninit() { Wh_Log(L">"); + + if (g_regionThread) { + PostThreadMessage(g_regionThreadId, WM_QUIT, 0, 0); + WaitForSingleObject(g_regionThread, 2000); + CloseHandle(g_regionThread); + g_regionThread = nullptr; + } + + ClearAllWindowRegions(); + DeleteCriticalSection(&g_regionCs); } From cee1da2bb96c143ce01c891d8ff20dc41bcf475a Mon Sep 17 00:00:00 2001 From: Alexey <112402687+leshaalexey@users.noreply.github.com> Date: Sat, 8 Aug 2026 10:43:01 +0300 Subject: [PATCH 12/13] Update GitHub link in metadata --- mods/rounded-corners-when-snapped.wh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mods/rounded-corners-when-snapped.wh.cpp b/mods/rounded-corners-when-snapped.wh.cpp index 69f4ff47cf..79fa53c2d3 100644 --- a/mods/rounded-corners-when-snapped.wh.cpp +++ b/mods/rounded-corners-when-snapped.wh.cpp @@ -4,7 +4,7 @@ // @description Keeps window corners rounded when a window is snapped, without changing the window state // @version 1.0.0 // @author Alexey Lavrinenko -// @github https://github.com/YOUR-GITHUB-USERNAME +// @github https://github.com/leshaalexey // @license GPL-3.0 // @include dwm.exe // @architecture x86-64 From 46637999c61f423d6f56216dfe569270bfc8b287 Mon Sep 17 00:00:00 2001 From: Alexey <112402687+leshaalexey@users.noreply.github.com> Date: Sat, 8 Aug 2026 11:15:13 +0300 Subject: [PATCH 13/13] Refactor window corner rounding logic and settings --- mods/rounded-corners-when-snapped.wh.cpp | 359 +++++++++-------------- 1 file changed, 139 insertions(+), 220 deletions(-) diff --git a/mods/rounded-corners-when-snapped.wh.cpp b/mods/rounded-corners-when-snapped.wh.cpp index 79fa53c2d3..34b36e82a9 100644 --- a/mods/rounded-corners-when-snapped.wh.cpp +++ b/mods/rounded-corners-when-snapped.wh.cpp @@ -8,7 +8,7 @@ // @license GPL-3.0 // @include dwm.exe // @architecture x86-64 -// @compilerOptions -lgdi32 -lwevtapi +// @compilerOptions -lwevtapi // ==/WindhawkMod== // HasMultipleDwminitWarningsInLastMinute() is taken from the Custom Window @@ -32,20 +32,19 @@ This mod needs to hook into `dwm.exe` to work. Please navigate to Windhawk's Settings > Advanced settings > More advanced settings > Process inclusion list, and make sure that `dwm.exe` is in the list. -## Why maximized windows are left alone +## Maximized windows are deliberately left alone -A window maximized over the whole screen is presented through *direct flip*: -its buffer goes to the display without DWM composing the frame, which is also -why Windows doesn't round it in the first place. Rounded corners drawn for such -a window only show up while something forces composition — the Start menu, a -notification, Alt+Tab — and disappear again the moment the overlay goes away. -In apps that draw their own frame (browsers, Electron apps) that reads as -corners flickering between round and square. +A window maximized over the whole screen is handed straight to the display +through direct flip: its buffer is scanned out without DWM composing a frame +for it. Corners drawn for such a window are simply not shown, and they appear +for as long as something forces composition — the Start menu, a notification, +Alt+Tab — which reads as corners flickering. -Snapped windows are composed normally, so their corners stay rounded at all -times. Maximized windows are therefore skipped by default. The behaviour can be -turned on with the *Also round maximized windows* option, with the caveat -above — it looks fine in apps with a standard window frame. +Getting them to stay would mean pulling the window back into composition for +every frame, which costs exactly the GPU work Windows avoids here. That isn't a +reasonable trade for two corners, so a maximized window keeps its square +corners — border and geometry both, so nothing flickers when an overlay opens. +Unsnap or restore it and the rounding is back. ## How it works @@ -58,6 +57,11 @@ class are hooked as well. Every replacement is gated on `IsMaximizedOrSnapped`, so the Start menu backdrop, the virtual-desktop switch animation, drag previews and fullscreen windows are left exactly as DWM wanted them. +`IsMaximizedOrSnapped` covers both states at once, so telling a snapped window +from a maximized one needs a rectangle. The only place that has one is +`CWindowBorder::SetBorderParameters`, so the verdict is recorded there and +reused by the getters on the next update. + ## Compatibility Other mods and tools that patch DWM corners hook the same functions and will @@ -69,14 +73,6 @@ corner option, StartAllBack or Win11DisableRoundedCorners. // ==WindhawkModSettings== /* -- roundMaximized: false - $name: Also round maximized windows - $description: >- - A maximized window is normally presented without DWM composing it, which is - why Windows squares its corners. To round it, the mod gives the window a - rounded region, which brings it back into composition and clips the app's - own content. That costs one composition pass for that window - a little more - GPU work and battery use while a window is maximized. - roundStyle: round $name: Corner style $options: @@ -97,15 +93,84 @@ corner option, StartAllBack or Win11DisableRoundedCorners. #include struct { - bool roundMaximized; int roundStyle; float radius; } g_settings; -// Captured, not hooked: DWM's own verdict about the surface being composed. +// Captured, not hooked: DWM's own verdict about the surface being composed. It +// answers "maximized or snapped" as one, hence the bookkeeping below. using IsMaximizedOrSnapped_t = bool(WINAPI*)(void* pThis); IsMaximizedOrSnapped_t IsMaximizedOrSnapped; +// --------------------------------------------------------------------------- +// Telling a snapped window from a maximized one. +// +// None of the corner functions get a rectangle, and DWM's own predicate lumps +// both states together. CWindowBorder::SetBorderParameters does get one, and it +// runs right after the corner functions for the same window, so the verdict is +// recorded there and read back on the next update of that window. +// --------------------------------------------------------------------------- + +constexpr int kMaxTrackedWindows = 32; + +CRITICAL_SECTION g_stateCs; +struct { + void* window; + bool maximized; +} g_windowState[kMaxTrackedWindows]; +int g_windowStateNext; + +// The window whose visuals are currently being built on this thread. +thread_local void* g_currentWindow; + +bool IsKnownMaximized(void* window) { + bool maximized = false; + + EnterCriticalSection(&g_stateCs); + for (int i = 0; i < kMaxTrackedWindows; i++) { + if (g_windowState[i].window == window) { + maximized = g_windowState[i].maximized; + break; + } + } + LeaveCriticalSection(&g_stateCs); + + return maximized; +} + +// Drops a remembered verdict. Only touches windows that are already tracked, so +// querying an unrelated surface doesn't evict a real entry. +void ForgetWindowState(void* window) { + EnterCriticalSection(&g_stateCs); + for (int i = 0; i < kMaxTrackedWindows; i++) { + if (g_windowState[i].window == window) { + g_windowState[i].maximized = false; + break; + } + } + LeaveCriticalSection(&g_stateCs); +} + +void RememberWindowState(void* window, bool maximized) { + EnterCriticalSection(&g_stateCs); + + int slot = -1; + for (int i = 0; i < kMaxTrackedWindows; i++) { + if (g_windowState[i].window == window) { + slot = i; + break; + } + } + if (slot < 0) { + slot = g_windowStateNext; + g_windowStateNext = (g_windowStateNext + 1) % kMaxTrackedWindows; + g_windowState[slot].window = window; + } + g_windowState[slot].maximized = maximized; + + LeaveCriticalSection(&g_stateCs); +} + // True for a window filling its monitor's work area, as opposed to one snapped // to part of it. bool CoversWorkArea(const RECT& rect) { @@ -123,12 +188,27 @@ bool CoversWorkArea(const RECT& rect) { return width * 100 >= workWidth * 95 && height * 100 >= workHeight * 95; } +// True for a surface this mod should round: DWM considers it snapped or +// maximized, and it isn't one of the maximized ones. +bool ShouldRound(void* pThis) { + if (!IsMaximizedOrSnapped(pThis)) { + // Restored or never snapped - DWM rounds it on its own, and any earlier + // "maximized" verdict is stale now. Clearing it here is what keeps a + // window from staying square after it leaves the maximized state. + ForgetWindowState(pThis); + return false; + } + return !IsKnownMaximized(pThis); +} + // Builds that square a snapped window by reporting a "don't round" style. using GetEffectiveCornerStyle_t = int(WINAPI*)(void* pThis); GetEffectiveCornerStyle_t GetEffectiveCornerStyle_Original; int WINAPI GetEffectiveCornerStyle_Hook(void* pThis) { int orig = GetEffectiveCornerStyle_Original(pThis); - if (orig == DWMWCP_DONOTROUND && IsMaximizedOrSnapped(pThis)) { + g_currentWindow = pThis; + + if (orig == DWMWCP_DONOTROUND && ShouldRound(pThis)) { Wh_Log(L"> DONOTROUND -> %d", g_settings.roundStyle); return g_settings.roundStyle; } @@ -144,7 +224,9 @@ using RadiusGetter_t = float(WINAPI*)(void* pThis); RadiusGetter_t GetRadiusFromCornerStyle_Original; float WINAPI GetRadiusFromCornerStyle_Hook(void* pThis) { float orig = GetRadiusFromCornerStyle_Original(pThis); - if (orig <= 0.0f && IsMaximizedOrSnapped(pThis)) { + g_currentWindow = pThis; + + if (orig <= 0.0f && ShouldRound(pThis)) { Wh_Log(L"> radius 0 -> %f", g_settings.radius); return g_settings.radius; } @@ -154,7 +236,9 @@ float WINAPI GetRadiusFromCornerStyle_Hook(void* pThis) { RadiusGetter_t GetFloatCornerRadiusForCurrentStyle_Original; float WINAPI GetFloatCornerRadiusForCurrentStyle_Hook(void* pThis) { float orig = GetFloatCornerRadiusForCurrentStyle_Original(pThis); - if (orig <= 0.0f && IsMaximizedOrSnapped(pThis)) { + g_currentWindow = pThis; + + if (orig <= 0.0f && ShouldRound(pThis)) { Wh_Log(L"> current style radius 0 -> %f", g_settings.radius); return g_settings.radius; } @@ -166,7 +250,9 @@ float WINAPI GetFloatCornerRadiusForCurrentStyle_Hook(void* pThis) { RadiusGetter_t GetDpiAdjustedFloatCornerRadius_Original; float WINAPI GetDpiAdjustedFloatCornerRadius_Hook(void* pThis) { float orig = GetDpiAdjustedFloatCornerRadius_Original(pThis); - if (orig > 0.0f || !IsMaximizedOrSnapped(pThis)) { + g_currentWindow = pThis; + + if (orig > 0.0f || !ShouldRound(pThis)) { return orig; } @@ -176,8 +262,10 @@ float WINAPI GetDpiAdjustedFloatCornerRadius_Hook(void* pThis) { return value; } -// The window border is where the rounding actually becomes visible, and the -// only place with a rectangle to tell a snapped window from a maximized one. +// The only function in the chain that knows the window's rectangle. It runs +// right after the getters above for the same window, so this is where a +// maximized window is recognised - and where a rounding that slipped through +// before that was known is taken back. using SetBorderParameters_t = long(WINAPI*)(void* pThis, const RECT& borderRect, float cornerRadius, @@ -193,180 +281,31 @@ long WINAPI SetBorderParameters_Hook(void* pThis, const void* color, int borderStyle, int shadowStyle) { - if (cornerRadius > 0.0f && !g_settings.roundMaximized && - CoversWorkArea(borderRect)) { - // Maximized: DWM presents it without composing, so a rounded border - // would only show while something else is drawn on top. - Wh_Log(L"> maximized, leaving the border square"); - cornerRadius = 0.0f; - } - return SetBorderParameters_Original(pThis, borderRect, cornerRadius, dpi, - color, borderStyle, shadowStyle); -} - -// --------------------------------------------------------------------------- -// Maximized windows: a window with a region is no longer a plain rectangle, so -// it stops being a direct flip candidate and DWM composes it again - which is -// what makes the rounding above visible. The region also clips the app's own -// content, so apps that paint their own frame can't fill the corner. -// -// The cost is one composition pass for that window, which is exactly what -// Windows avoids by squaring the corners, so this is opt-in. -// --------------------------------------------------------------------------- - -HANDLE g_regionThread; -DWORD g_regionThreadId; -HWINEVENTHOOK g_locationHook; -HWINEVENTHOOK g_stateHook; - -CRITICAL_SECTION g_regionCs; -constexpr int kMaxTrackedWindows = 64; -HWND g_regionedWindows[kMaxTrackedWindows]; - -void RememberWindow(HWND hwnd) { - EnterCriticalSection(&g_regionCs); - int free = -1; - for (int i = 0; i < kMaxTrackedWindows; i++) { - if (g_regionedWindows[i] == hwnd) { - free = -1; - break; - } - if (!g_regionedWindows[i] && free < 0) { - free = i; - } - } - if (free >= 0) { - g_regionedWindows[free] = hwnd; - } - LeaveCriticalSection(&g_regionCs); -} - -void ForgetWindow(HWND hwnd) { - EnterCriticalSection(&g_regionCs); - for (int i = 0; i < kMaxTrackedWindows; i++) { - if (g_regionedWindows[i] == hwnd) { - g_regionedWindows[i] = nullptr; - } - } - LeaveCriticalSection(&g_regionCs); -} - -bool IsRoundableMaximizedWindow(HWND hwnd) { - if (!hwnd || !IsWindow(hwnd) || !IsWindowVisible(hwnd) || !IsZoomed(hwnd)) { - return false; - } - - LONG style = GetWindowLongW(hwnd, GWL_STYLE); - if ((style & WS_CHILD) || !(style & (WS_CAPTION | WS_THICKFRAME))) { - return false; - } - if (GetWindowLongW(hwnd, GWL_EXSTYLE) & WS_EX_TOOLWINDOW) { - return false; - } - - // Leave true fullscreen alone - games and video players. - RECT rect; - MONITORINFO mi{sizeof(MONITORINFO)}; - if (GetWindowRect(hwnd, &rect) && - GetMonitorInfoW(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), - &mi) && - EqualRect(&rect, &mi.rcMonitor)) { - return false; - } - return true; -} - -void UpdateWindowRegion(HWND hwnd) { - if (!g_settings.roundMaximized) { - return; - } - - if (!IsRoundableMaximizedWindow(hwnd)) { - ForgetWindow(hwnd); - return; - } - - RECT rect; - if (!GetWindowRect(hwnd, &rect)) { - return; - } - - int width = rect.right - rect.left; - int height = rect.bottom - rect.top; - if (width <= 0 || height <= 0) { - return; - } - - UINT dpi = GetDpiForWindow(hwnd); - if (!dpi) { - dpi = 96; - } - int radius = static_cast(g_settings.radius * dpi / 96.0f); - - // The region is in window coordinates, and a maximized window overhangs the - // work area by its border width on every side. - HRGN region = CreateRoundRectRgn(0, 0, width + 1, height + 1, radius * 2, - radius * 2); - if (!region) { - return; - } - - if (SetWindowRgn(hwnd, region, TRUE)) { - RememberWindow(hwnd); // SetWindowRgn took ownership. - } else { - DeleteObject(region); - } -} + // Consume the marker: a later border update may belong to another window, + // and attributing its rectangle to this one is how a window ends up with a + // verdict that isn't its own. + void* window = g_currentWindow; + g_currentWindow = nullptr; -void CALLBACK WinEventProc(HWINEVENTHOOK, - DWORD event, - HWND hwnd, - LONG idObject, - LONG idChild, - DWORD, - DWORD) { - if (idObject != OBJID_WINDOW || idChild != CHILDID_SELF) { - return; - } - if (event == EVENT_OBJECT_DESTROY) { - ForgetWindow(hwnd); - return; - } - UpdateWindowRegion(hwnd); -} + // The rectangle alone isn't enough - during the restore animation a window + // briefly still covers the work area while DWM already considers it + // restored, and squaring it there is what left windows square afterwards. + bool maximized = window && CoversWorkArea(borderRect) && + IsMaximizedOrSnapped(window); -DWORD WINAPI RegionThreadProc(LPVOID) { - g_stateHook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, - EVENT_SYSTEM_MINIMIZEEND, nullptr, - WinEventProc, 0, 0, WINEVENT_OUTOFCONTEXT); - g_locationHook = SetWinEventHook(EVENT_OBJECT_DESTROY, - EVENT_OBJECT_LOCATIONCHANGE, nullptr, - WinEventProc, 0, 0, WINEVENT_OUTOFCONTEXT); - - MSG msg; - while (GetMessage(&msg, nullptr, 0, 0) > 0) { - TranslateMessage(&msg); - DispatchMessage(&msg); + if (window) { + RememberWindowState(window, maximized); } - if (g_stateHook) { - UnhookWinEvent(g_stateHook); - } - if (g_locationHook) { - UnhookWinEvent(g_locationHook); + if (maximized && cornerRadius > 0.0f) { + // Direct flip means a rounded border here would only be shown while + // something else is composed on top, i.e. it would flicker. + Wh_Log(L"> maximized, leaving the border square"); + cornerRadius = 0.0f; } - return 0; -} -void ClearAllWindowRegions() { - EnterCriticalSection(&g_regionCs); - for (int i = 0; i < kMaxTrackedWindows; i++) { - if (g_regionedWindows[i] && IsWindow(g_regionedWindows[i])) { - SetWindowRgn(g_regionedWindows[i], nullptr, TRUE); - } - g_regionedWindows[i] = nullptr; - } - LeaveCriticalSection(&g_regionCs); + return SetBorderParameters_Original(pThis, borderRect, cornerRadius, dpi, + color, borderStyle, shadowStyle); } void LoadSettings() { @@ -375,8 +314,6 @@ void LoadSettings() { g_settings.roundStyle = wcscmp(style.get(), L"small") == 0 ? DWMWCP_ROUNDSMALL : DWMWCP_ROUND; - g_settings.roundMaximized = Wh_GetIntSetting(L"roundMaximized") != 0; - int radius = Wh_GetIntSetting(L"radius"); if (radius < 1) { radius = 1; @@ -429,6 +366,7 @@ BOOL Wh_ModInit() { } LoadSettings(); + InitializeCriticalSection(&g_stateCs); HMODULE udwm = GetModuleHandle(L"udwm.dll"); if (!udwm) { @@ -482,7 +420,7 @@ BOOL Wh_ModInit() { GetDpiAdjustedFloatCornerRadius_Hook, true, // Optional. }, - // Keeps maximized windows square unless the user asks otherwise. + // Recognises maximized windows and keeps their border square. { {LR"(public: long __cdecl CWindowBorder::SetBorderParameters(struct tagRECT const &,float,int,struct _D3DCOLORVALUE const &,enum CWindowBorder::BorderStyle,enum CWindowBorder::ShadowStyle))"}, &SetBorderParameters_Original, @@ -496,13 +434,6 @@ BOOL Wh_ModInit() { return FALSE; } - InitializeCriticalSection(&g_regionCs); - g_regionThread = - CreateThread(nullptr, 0, RegionThreadProc, nullptr, 0, &g_regionThreadId); - if (!g_regionThread) { - Wh_Log(L"Failed to start the region thread"); - } - return TRUE; } @@ -510,22 +441,10 @@ void Wh_ModSettingsChanged() { Wh_Log(L">"); LoadSettings(); - - if (!g_settings.roundMaximized) { - ClearAllWindowRegions(); - } } void Wh_ModUninit() { Wh_Log(L">"); - if (g_regionThread) { - PostThreadMessage(g_regionThreadId, WM_QUIT, 0, 0); - WaitForSingleObject(g_regionThread, 2000); - CloseHandle(g_regionThread); - g_regionThread = nullptr; - } - - ClearAllWindowRegions(); - DeleteCriticalSection(&g_regionCs); + DeleteCriticalSection(&g_stateCs); }