Skip to content

Add rounded corners mod for snapped/maximized windows - #5022

Open
leshaalexey wants to merge 13 commits into
ramensoftware:mainfrom
leshaalexey:main
Open

Add rounded corners mod for snapped/maximized windows#5022
leshaalexey wants to merge 13 commits into
ramensoftware:mainfrom
leshaalexey:main

Conversation

@leshaalexey

@leshaalexey leshaalexey commented Aug 5, 2026

Copy link
Copy Markdown

Keeps window corners rounded when a window is maximized or snapped, without touching the window state itself.

Addressed everything from the AI review, except where noted below.

1. SystemParametersInfo(SPI_SETDRAGFULLWINDOWS) — removed. Corners now update the next time each window's visuals are refreshed, as suggested.

2. Overlap with custom-corner-radius — see the note at the bottom.

3. Symbol resolution — the explorer.exe warmer, the hand-rolled RVA cache (ResolvedRvas, GetModuleIdentity, ResolveBySymbols, StoreRvas, LoadRvas, MatchesFilterList, RunningInDwm, the kKey* constants) and the @include explorer.exe line are all gone. The mod now uses WindhawkUtils::HookSymbols in dwm.exe only, with full decorated signatures. Symbol resolution inside dwm.exe works fine — the earlier failure was on my side.

Two of the radius getters are listed with several signature variants (const/non-const, private/public), because they differ between builds. Only CTopLevelWindow overloads are listed on purpose: those hooks pass this to IsMaximizedOrSnapped, so a same-named method on another class must not match. That was a real bug in the previous revision — wcsstr matching on undecorated names bound to same-named methods of a different class, and the filter was then reading a foreign this.

4. LoadLibraryW — removed, GetModuleHandle only, bail out if udwm.dll isn't loaded.

5. System DPI — no longer derived by hand. A forced radius is scaled through CTopLevelWindow::GetWindowData + CWindowData::ScaleForDpi, i.e. DWM's own per-window scaling. Both symbols are optional and the system DPI remains a fallback.

Regarding "verify that hooking GetDpiAdjustedFloatCornerRadius is needed at all": on 10.0.26100.8972 it is. Tracing shows GetEffectiveCornerStyle returning DWMWCP_ROUND and GetRadiusFromCornerStyle returning 8.0 for a maximized window — the squaring happens further down, and it's the ...ForCurrentStyle / ...DpiAdjusted... getters that return zero there. Only hooking the first two produced no rounding at all on this build.

6. Developer-only settingsdumpSymbols, noFilter, debugLog and forceRadius removed, along with LogThrottled. Forcing a radius is now automatic: it only happens when DWM returned zero for a surface IsMaximizedOrSnapped vouches for, so on builds where promoting the style is enough the hooks are no-ops. Two settings remain: corner style and fallback radius.

7. Screenshot — added to the README.

Also applied from the optional list: HasMultipleDwminitWarningsInLastMinute guard (taken from custom-corner-radius, which is why this mod is GPL-3.0 — credited in a comment at the top), DWMWCP_* constants from <dwmapi.h>, WindhawkUtils::StringSetting, <windhawk_utils.h>, radius clamped instead of silently substituted, Wh_Log used directly.

On the overlap with custom-corner-radius

Happy to move this there as an option if that's preferred — say the word and I'll close this PR and open one against that mod instead.

The reason I think it may work better standalone: the two mods answer different questions. custom-corner-radius changes the radius DWM already uses; this one restores rounding where Windows deliberately removes it, and every decision is gated on IsMaximizedOrSnapped rather than on a radius value. Folded in, it would mean a second, differently-scoped filter running through the same radius pipeline. But that's a judgement call about the catalog, not about the code, so I'll follow whatever you prefer.

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
  • The submitter, with AI assistance
  • 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.

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.
@windhawk-reviewer windhawk-reviewer Bot added the waiting-for-author The author's turn: request an AI review, or respond to one that was posted. label Aug 5, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Thanks for the pull request! This repository uses a two-stage review: an AI review that you run yourself, followed by a human review.

To get started, comment /ai-review. Once you're happy with the result, comment /ready-for-reviewer to hand it over to a human reviewer.

See the pull request review process for the full details.

@leshaalexey

Copy link
Copy Markdown
Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 5, 2026
@leshaalexey

Copy link
Copy Markdown
Author

/ready-for-reviewer

@windhawk-reviewer

Copy link
Copy Markdown

@leshaalexey /ready-for-reviewer can't be applied here: an AI review is being prepared, please wait for it to be posted.

@leshaalexey

Copy link
Copy Markdown
Author

/ready-for-reviewer

@windhawk-reviewer

Copy link
Copy Markdown

@leshaalexey /ready-for-reviewer can't be applied here: an AI review is being prepared, please wait for it to be posted.

@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


The idea is nice and the hook points are the right ones, but the symbol-resolution architecture and one system-wide side effect need to change before this can be merged.

1. Wh_ModSettingsChanged changes a system-wide user setting and never restores it

void Wh_ModSettingsChanged() {
    LoadSettings();
    SystemParametersInfoW(SPI_SETDRAGFULLWINDOWS, TRUE, nullptr, SPIF_SENDCHANGE);
}

SPI_SETDRAGFULLWINDOWS is a per-user, system-wide setting ("Show window contents while dragging"). A user who deliberately turned it off (Performance Options, remote sessions, accessibility) gets it silently switched back on every time they touch any of this mod's settings, and it is never restored on unload. That breaks Windhawk's core reversibility principle — a mod's effects must disappear when it's disabled, and it must not reconfigure things outside its own scope.

On top of that, SPIF_SENDCHANGE makes user32 broadcast WM_SETTINGCHANGE to every top-level window on the desktop. Issuing that broadcast from inside dwm.exe means a DWM thread blocks on unrelated (possibly hung) applications — a bad place for the compositor to be.

Please remove the call. If you need DWM to re-evaluate existing windows after a settings change, find a scoped way to do it; corners will in any case update the next time each window's visuals are refreshed.

2. Substantial overlap with custom-corner-radius

custom-corner-radius already hooks CTopLevelWindow::GetEffectiveCornerStyle, GetRadiusFromCornerStyle and GetFloatCornerRadiusForCurrentStyle in uDWM.dll inside dwm.exe — the exact same three functions this mod hooks, for the exact same purpose (overriding the effective corner style/radius). Two mods hooking the same functions in the same DLL will fight each other, and the README's compatibility list only mentions third-party tools, not the catalog mods it actually conflicts with.

The maintainer's consistent preference is to extend an existing mod with an option rather than merge a near-overlapping new mod. "Round maximized and snapped windows" fits naturally as a setting in custom-corner-radius (it already has the whole radius pipeline hooked, plus HWND recovery via CTopLevelWindow::GetWindowData, which would let the filter be done on the window rather than on an internal DWM predicate). Please consider opening a PR against that mod instead. If you believe it needs to stay separate, please explain in the PR why it can't be an option there.

3. Drop the explorer.exe "warmer" and the hand-rolled RVA cache — use WindhawkUtils::HookSymbols in dwm.exe

The README's premise ("dwm.exe runs under a restricted account and usually cannot download PDBs") doesn't match how the catalog's existing DWM mods work. All of them resolve uDWM symbols directly inside dwm.exe with HookSymbols, with dwm.exe as their only @include:

HookSymbols also does everything the custom cache is trying to do, and more:

  • it caches resolved addresses per binary version already (windhawk_utils.h: "Caches the result to avoid loading symbols each time"), so kCacheVersion / uDwmTimeDateStamp / uDwmSizeOfImage / the five rva* values are a reimplementation;
  • it consults Windhawk's online symbol cache first (WH_HOOK_SYMBOLS_OPTIONS::onlineCacheUrl), so in the common case no PDB is downloaded at all. Wh_FindFirstSymbol, which this mod uses directly, gets neither of those.

The current design also has a concrete user-visible failure mode. dwm.exe starts before explorer.exe, and the cache key is TimeDateStamp + SizeOfImage, so every cumulative update that touches uDWM.dll invalidates the cache. At the next boot dwm.exe finds no cache, and — if the mod's own premise holds and local resolution fails there — Wh_ModInit returns FALSE. Explorer then warms the cache, but nothing re-triggers dwm.exe; storage writes via Wh_SetIntValue don't cause a reload. The mod's own log message spells this out: "let explorer.exe warm the cache, then re-enable the mod". Requiring a manual re-enable after every Windows update isn't acceptable.

Finally, ResolveBySymbols matches with wcsstr on the undecorated name, which ignores the signature entirely — any overload or same-named symbol in another scope matches, and the last one in enumeration order wins. Since dwm.exe then blindly hooks that RVA, a mismatch crashes the compositor. The SYMBOL_HOOK form pins the full decorated signature, e.g.:

WindhawkUtils::SYMBOL_HOOK udwmDllHooks[] = {
    {
        {LR"(private: enum CORNER_STYLE __cdecl CTopLevelWindow::GetEffectiveCornerStyle(void))"},
        &GetEffectiveCornerStyle_orig,
        GetEffectiveCornerStyle_hook,
    },
    {
        {LR"(private: float __cdecl CTopLevelWindow::GetRadiusFromCornerStyle(void))"},
        &GetRadiusFromCornerStyle_orig,
        GetRadiusFromCornerStyle_hook,
    },
    // ...
};
if (!WindhawkUtils::HookSymbols(udwm, udwmDllHooks, ARRAYSIZE(udwmDllHooks))) { ... }

This also matters for IsMaximizedOrSnapped, which the mod calls directly as bool(void*). Nothing currently verifies that the resolved symbol actually has that signature; with a decorated symbol string it's verified at resolution time. (Windhawk Symbol Helper gives you the exact decorated names.)

Concretely: delete ResolvedRvas, GetModuleIdentity, ResolveBySymbols, StoreRvas, LoadRvas, MatchesFilterList, RunningInDwm, the kKey*/kCacheVersion constants and the @include explorer.exe line, and hook via HookSymbols in dwm.exe only. If you have actually observed symbol resolution failing in dwm.exe on your machine, that's worth reporting as a Windhawk issue rather than working around it inside a mod.

4. LoadLibraryW(L"uDWM.dll")

HMODULE hUDWM = GetModuleHandleW(L"uDWM.dll");
if (!hUDWM) {
    hUDWM = LoadLibraryW(L"uDWM.dll");
}

In explorer.exe, uDWM.dll is not loaded, so this always force-loads DWM's compositor library into Explorer, runs its DllMain, and never calls FreeLibrary — the module stays in Explorer even after the mod is unloaded, and the reference leaks again on each reload. Separately, uDWM.dll is not a KnownDLL, so the bare-name form searches the executable's directory first; that's low risk here given both targets live in System32, but it's an easy thing to get right.

Just do what the other DWM mods do — GetModuleHandleW only, and bail out if it isn't loaded (see custom-corner-radius.wh.cpp#L367-L371). Inside dwm.exe, uDWM.dll is always already loaded. If for some reason you do need an explicit load, use LoadLibraryExW(L"uDWM.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32).

5. GetDpiAdjustedFloatCornerRadius_hook uses the system DPI

UINT dpi = GetDpiForSystem();
if (!dpi) dpi = 96;
return g_settings.radius * (dpi / 96.0f);

GetDpiForSystem returns the system DPI, but this function exists precisely because the radius is per-window/per-monitor. On a mixed-DPI multi-monitor setup, a window on a 150% monitor gets the primary monitor's scale factor, so its forced corners come out visibly the wrong size.

Per the call tree documented in custom-corner-radius.wh.cpp#L373-L391, GetDpiAdjustedFloatCornerRadius calls GetRadiusFromCornerStyle and applies DWM's own DPI scaling to the result. So the clean fix is to force the radius only in the unscaled getter (GetRadiusFromCornerStyle_hook) and let DWM scale it, i.e. make this hook a plain pass-through (or drop it), rather than re-deriving a scale factor yourself.

6. Remove the developer-only settings

  • dumpSymbols — a diagnostics text box that enumerates and logs uDWM symbols. This is developer tooling in a user-facing settings page; Windhawk Symbol Helper covers this use case. It also only takes effect in Wh_ModInit on the non-DWM path, so changing it does nothing without a reload. Removing it also removes the ~30-line hand-rolled MatchesFilterList tokenizer.
  • noFilter — its own description says "For testing only" and that it "causes outlines around the Start menu and desktop switching". Shipping a setting whose documented behavior is known visual breakage isn't useful to users; please drop it (and the g_settings.noFilter branches).
  • debugLog — Windhawk already has a per-mod logging toggle, and Wh_Log compiles to a cheap if (logsEnabled) check that is off by default, so there's no reason for a second switch. See the note below about LogThrottled.

7. Add a screenshot to the README

This is a purely visual mod and the README has no image. A before/after screenshot (or a short GIF of snapping a window) makes a big difference both in the Windhawk mod browser and in review. Only i.imgur.com and raw.githubusercontent.com are accepted as image hosts.

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • LogThrottled is a custom wrapper around Wh_Log. Windhawk already prefixes the mod name and gates logging, so call Wh_Log directly and drop the wrapper along with the debugLog setting. (As a side note, the static ULONGLONG lastTick inside it is written from multiple DWM threads without synchronization.)
  • Use WindhawkUtils::SetFunctionHook() instead of raw Wh_SetFunctionHook with (void*)/(void**) casts — it's type-safe and catches prototype mismatches at compile time. Mostly moot if you switch to HookSymbols.
  • Wh_GetStringSetting never returns NULL (it returns L"" when unset or on error), so the style && / dump && null checks are dead. WindhawkUtils::StringSetting (RAII) is also preferable to the manual Wh_GetStringSetting + Wh_FreeStringSetting pairs:
    auto style = WindhawkUtils::StringSetting::make(L"roundStyle");
    g_settings.roundStyle = wcscmp(style, L"small") == 0 ? kCornerRoundSmall : kCornerRound;
  • kCornerSquare/kCornerRound/kCornerRoundSmall are the DWM_WINDOW_CORNER_PREFERENCE values — #include <dwmapi.h> and use DWMWCP_DONOTROUND / DWMWCP_ROUND / DWMWCP_ROUNDSMALL instead of redefining them, as custom-corner-radius does.
  • Include <windhawk_utils.h> rather than <windhawk_api.h> once you use the utils helpers.
  • if (radius < 1) radius = 8; silently turns a user-entered 0 into 8 rather than clamping to the nearest valid value. Either clamp to 1 or document that 0 isn't supported.
  • When IsMaximizedOrSnapped isn't found and noFilter is off, the mod installs all its hooks and then deliberately does nothing on every call. Returning FALSE from Wh_ModInit in that case is cleaner — Windhawk retries after each settings change anyway.
  • Same for the warmer path in explorer.exe: it returns TRUE and stays resident forever with nothing left to do. FALSE is the right answer once its job is done. (Moot if the warmer goes away.)

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • Consider adopting the safety guard from custom-corner-radius.wh.cpp#L327-L363 (HasMultipleDwminitWarningsInLastMinute): it refuses to load when the Application event log shows repeated recent Dwminit warnings. A bad hook in dwm.exe leaves the user with a crash-restart loop and a desktop that's hard to recover without Safe Mode, so a self-disarming check is worth having in any mod that hooks the compositor.
  • Once GetEffectiveCornerStyle_hook promotes square → round, DWM's own style→radius mapping produces a non-zero radius, so on those builds the forceRadius path never fires. The two mechanisms are largely alternatives rather than complements — it may be worth documenting which builds actually need forceRadius, or auto-detecting instead of exposing it as a setting.
  • Related to item 5: verify that hooking GetDpiAdjustedFloatCornerRadius is needed at all. Since it calls GetRadiusFromCornerStyle (already hooked), its value should already be non-zero by the time your hook sees it on current builds.
  • The README's compatibility section lists third-party patchers but not the Windhawk mods this directly conflicts with (custom-corner-radius, disable-rounded-corners). Worth mentioning if the mod stays separate.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 5, 2026
@leshaalexey

Copy link
Copy Markdown
Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 5, 2026
Added authorship section to mod readme.
@leshaalexey

Copy link
Copy Markdown
Author

/ai-review

@windhawk-reviewer

Copy link
Copy Markdown

@leshaalexey /ai-review can't be applied here: an AI review was already requested, please wait for it to be posted.

@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


The revision is a big improvement — the hand-rolled RVA cache, the developer-only settings, the custom logging and the LoadLibraryW call are all gone, the symbol strings are fully decorated and class-qualified (good catch on the cross-class this bug), and HookSymbols is called once for udwm.dll with a matching array name. The remaining items are mostly about where this code should live and about the README.

1. This is a strong candidate for an option in Custom Window Corner Radius rather than a separate mod.

You raised this yourself, so here's the reasoning from the catalog side. The two mods run in the same process, hook the same udwm.dll functions on the same class (CTopLevelWindow::GetEffectiveCornerStyle, GetRadiusFromCornerStyle, GetFloatCornerRadiusForCurrentStyle), capture the same helper (CTopLevelWindow::GetWindowData), and share the HasMultipleDwminitWarningsInLastMinute guard verbatim. The scaffolding is already the same code — see custom-corner-radius.wh.cpp#L393-L451 next to yours. What's genuinely new here is IsMaximizedOrSnapped as a gate plus the "promote when DWM said zero" direction, which is a handful of lines inside hooks that already exist there.

Note also that the README's compatibility warning is a symptom of the split, not an independent fact: Windhawk chains hooks, and the two mods are actually complementary (Custom Window Corner Radius replaces non-zero radii, yours replaces zero radii), so a user who wants both a custom radius and rounded maximized corners currently has no supported way to get it. As one mod with a "Keep corners rounded when maximized or snapped" option, that combination just works, and there's one place to maintain the udwm symbol list as builds change. Development for that mod happens at https://github.com/m417z/my-windhawk-mods.

2. README issues.

  • Remove the "Mod authorship" section. It's the pull request template's checklist ("If this pull request introduces a new mod, please complete the section below.") pasted into the mod README, which is the user-facing page on windhawk.net. That block belongs in the PR description instead — please move it there and delete it from the README. It also renders incorrectly as-is (the - - [ ] prefixes nest it as a sub-list, and several lines carry trailing whitespace).
  • Add a screenshot. The PR description says one was added, but the README has no image. This is a purely visual mod, so a before/after screenshot (or a GIF of maximizing a window) is important for users browsing the catalog. See custom-corner-radius.wh.cpp#L35. While you're there, consider also including the standard advanced-settings screenshot that accompanies the dwm.exe usage note (#L43) — https://i.imgur.com/LRhREtJ.png.
  • "disabling the mod restores the default look immediately" isn't accurate. Per your own description of the change, corners only revert the next time each window's visuals are refreshed, since the SPI_SETDRAGFULLWINDOWS nudge was removed. Please reword to say that the change takes effect the next time a window's visuals are refreshed (and that no system files or settings are modified), so users don't read a stale window as a bug.
Optional improvements

Minor polish — none of this affects users, so it's your call.

  • CTopLevelWindow::GetEffectiveCornerStyle is a mandatory hook here, so a build that renames or drops it makes Wh_ModInit fail and the radius-getter path never gets a chance. Custom Window Corner Radius marks the same symbol optional for that reason (#L422-L427). Since your mod covers both squaring mechanisms independently, marking it optional would let the other path still work.
  • Naming of the capture-only pointers is inconsistent: GetWindowData_Original and ScaleForDpi_Original carry an _Original suffix even though nothing is hooked, while IsMaximizedOrSnapped has no suffix at all. Custom Window Corner Radius uses a _Func suffix for capture-only symbols (#L114); picking one convention for all three would read better.
  • wcscmp is used in LoadSettings but <cwchar> isn't included — it currently works only transitively through windows.h.
  • The radius setting is silently clamped to 1–40, which the $description doesn't mention. Worth stating the accepted range there.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • The three radius hooks disagree about DPI scaling. GetDpiAdjustedFloatCornerRadius_Hook returns ScaledRadius(pThis), but GetRadiusFromCornerStyle_Hook and GetFloatCornerRadiusForCurrentStyle_Hook return the raw g_settings.radius. Per the call tree documented in Custom Window Corner Radius (#L373-L391), on older builds the DPI scaling is inlined into GetRadiusFromCornerStyle — meaning its return value is already scaled, and substituting an unscaled 8 there would come out visibly too small at 150%/200%. Worth verifying on a high-DPI setup which of the three actually fires on your build, and whether the fallback needs ScaledRadius too.
  • Promoting the corner style also changes the shadow style. GetEffectiveCornerStyle's result feeds GetShadowStyle as well as the radius — that's exactly why Custom Window Corner Radius promotes tooltips to get "border + shadow + clip" together (#L237-L246). Windows deliberately drops the drop shadow on maximized windows, so it's worth checking that a maximized window doesn't now get a shadow composited at the monitor edges, and that two side-by-side snapped windows don't get a shadow seam between them.
  • All four corners are rounded, including the ones users may not expect. For a maximized window the corners cut through to the wallpaper at the screen edges, and for two snapped windows the adjacent inner corners leave a small notch between them. That's the intended macOS-like look and there's no way to round selectively through this pipeline, but a sentence in the README setting the expectation (ideally next to the screenshot) would help.
  • roundStyle and radius can disagree. With roundStyle: small and the default radius: 8, a build that squares corners via the style path gets DWM's 4px small radius, while a build that zeroes the radius gets your 8px fallback. Either derive the fallback from the selected style, or say so in the $description.
  • Nothing forces a repaint on enable/disable/settings change, so already-composed windows keep their old corners until DWM refreshes their visuals. This matches Custom Window Corner Radius' behavior and removing the SPI_SETDRAGFULLWINDOWS nudge was the right call, so this is just an FYI for the README wording above.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 5, 2026
@leshaalexey

Copy link
Copy Markdown
Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 5, 2026
@leshaalexey

Copy link
Copy Markdown
Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 7, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


The symbol layer is in good shape now — decorated, class-qualified names, one HookSymbols call for udwm.dll, capture-only IsMaximizedOrSnapped used as the gate, no forced module load, and the Dwminit guard is a good addition. However, several items from the previous round are described as fixed in the pull request description but aren't present in the code at f090632 — the maximized/snapped gating and the DPI scaling are unchanged, and the README still carries the pull request template. Worth double-checking that everything you intended to push actually landed.

1. SetBorderParameters_Hook squares windows the mod never touched.

if (cornerRadius > 0.0f && !g_settings.roundMaximized &&
    CoversWorkArea(borderRect)) {
    cornerRadius = 0.0f;
}

There is no IsMaximizedOrSnapped gate here (understandably — pThis is a CWindowBorder) and no check that the incoming radius is one the mod produced. So any window whose border rect covers ≥95% of the work area loses its rounded border, including windows DWM was rounding perfectly well on its own: a window the user dragged/resized to roughly fill the screen is not maximized, so stock Windows rounds it — with this mod enabled it comes out square. The same applies to any large window whenever the taskbar is set to auto-hide, since rcWork then equals rcMonitor.

Enabling the mod makes those windows less rounded than the Windows default, which is the opposite of what the mod advertises. It also silently zeroes Custom Window Corner Radius' output for those windows if both mods are enabled.

2. roundMaximized: false doesn't actually skip maximized windows.

IsMaximizedOrSnapped returns true for both states, and the only code that separates them is SetBorderParameters_Hook. GetEffectiveCornerStyle_Hook and all three radius getters fire for maximized windows regardless of the setting.

Per the call tree documented in custom-corner-radius.wh.cpp#L373-L391, the border is only one of two consumers of the radius. The other — CTopLevelWindow3D::UpdateAnimatedResourcesResourceHelper::CreateRectangleGeometry — takes its value from GetDpiAdjustedFloatCornerRadius / GetRadiusFromCornerStyle, which are ungated here. With the default settings a maximized window therefore still gets a rounded clip geometry (and, through the promoted corner style, a rounded shadow style) while its border is forced square — a clip/border mismatch on every maximized window, rather than "Maximized windows are therefore skipped by default" as the README states.

On top of that, SetBorderParameters is registered as optional, so on any build where that symbol doesn't resolve, roundMaximized: false has no effect at all.

The fix for both items is the same: make the maximized test once, on the CTopLevelWindow side, and gate every hook on it. Custom Window Corner Radius shows how to recover the HWND from a CTopLevelWindow (#L109-L165 plus the prologue scan at #L465-L479); with the HWND in hand, IsZoomed(hwnd) is the exact answer for "maximized" — snapped windows don't carry WS_MAXIMIZE, and snap-to-top genuinely does maximize. A shared helper along the lines of

bool ShouldRound(void* pThis) {
    if (!IsMaximizedOrSnapped(pThis)) {
        return false;
    }
    if (g_settings.roundMaximized) {
        return true;
    }
    HWND hwnd = HwndFromTopLevelWindow(pThis);
    return hwnd && !IsZoomed(hwnd);
}

used by all four CTopLevelWindow hooks makes the setting mean the same thing everywhere, and lets SetBorderParameters_Hook / CoversWorkArea go away entirely. If you'd rather keep the border hook, it needs at minimum to only override a radius the mod itself produced.

3. GetDpiAdjustedFloatCornerRadius_Hook still scales with the system DPI.

UINT dpi = GetDpiForSystem();
float value = g_settings.radius * (dpi ? dpi / 96.0f : 1.0f);

The pull request description says this was changed to DWM's own per-window scaling via CTopLevelWindow::GetWindowData + CWindowData::ScaleForDpi, but neither symbol appears in the code at f090632 — it's still GetDpiForSystem(). This function exists precisely because the radius is per-window/per-monitor, so on a mixed-DPI setup a window on a 150% monitor gets the primary monitor's scale factor and its forced corners come out visibly the wrong size. If ScaleForDpi didn't work out, the HWND recovery from item 2 gives you GetDpiForWindow(hwnd), which is the per-window answer.

4. The pull request template is still pasted into the mod README.

README lines 68–85 are the "## Changelog" and "## Mod authorship" blocks from .github/pull_request_template.md. The README is the user-facing mod page on windhawk.net, so this ships to users as-is (and renders wrong — the - - [ ] prefixes nest it as a sub-list, and several lines carry trailing whitespace). No merged mod in the catalog contains either block. Please delete both from the README — the authorship checklist is already in the pull request description, which is where it belongs.

5. Still no screenshot in the README.

The description says one was added, but the README has no image. This is a purely visual mod, so a before/after shot (or a short GIF of snapping a window) matters a lot for users browsing the catalog — see custom-corner-radius.wh.cpp#L35. Only i.imgur.com and raw.githubusercontent.com are accepted image hosts. While you're there, the other dwm.exe mods pair the usage note with the standard advanced-settings screenshot https://i.imgur.com/LRhREtJ.png (#L43) — worth including.

6. "disabling the mod restores the default look immediately" is still inaccurate.

Nothing forces a repaint on enable, disable or settings change, so already-composed windows keep their old corners until DWM refreshes their visuals. Please reword to say the change takes effect the next time a window's visuals are refreshed, and that no system files or settings are modified — otherwise a stale window reads as a bug.

7. Consolidation into Custom Window Corner Radius — the recommendation stands.

You asked for a call on this, and it's ultimately the maintainer's, so this isn't something you need to act on unilaterally. For the record, the technical case: the two mods run in the same process, hook the same udwm.dll functions on the same class, and share HasMultipleDwminitWarningsInLastMinute verbatim. They're complementary — Custom Window Corner Radius replaces non-zero radii, this one replaces zero radii — yet as separate mods they can't be combined, and item 1 has this mod actively zeroing that mod's radius. Item 2's fix also needs the HWND-recovery machinery that already exists there. As one mod with a "Keep corners rounded when snapped" option the combination just works, and there's a single place to maintain the uDWM symbol list as builds change. Development for that mod happens at https://github.com/m417z/my-windhawk-mods.

Items 1–6 are worth fixing either way, since they carry over.

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • GetEffectiveCornerStyle and GetRadiusFromCornerStyle are both mandatory hooks, so a build that renames or drops either makes Wh_ModInit fail and the other mechanism never gets a chance. Custom Window Corner Radius marks GetEffectiveCornerStyle optional for exactly that reason (#L422-L427). Since this mod covers both squaring mechanisms independently, marking them optional and failing only if nothing resolved would degrade more gracefully.
  • Capture-only pointer naming: IsMaximizedOrSnapped has no suffix while the hooked originals use _Original, so at the call sites inside the hooks it isn't obvious that nothing is hooked there. Custom Window Corner Radius uses a _Func suffix for capture-only symbols (#L114).
  • wcscmp is used in LoadSettings but <cwchar> isn't included; it currently works only transitively through windows.h.
  • The radius setting is silently clamped to 1–40, which the $description doesn't mention. Worth stating the accepted range.
  • The fallback radius doesn't follow the corner-style choice: picking "Small, like a menu" still uses the default 8, while Windows uses 4 for small rounding (as your own $description notes). Deriving the fallback from roundStyle when the user hasn't changed it — or at least mentioning the pairing in the description — would avoid a mismatched result.
  • g_settings is written from Wh_ModSettingsChanged (arbitrary thread) while DWM's composition threads read it inside the hooks. It's a benign data race on scalars in practice, and it can only happen on a settings change, but std::atomic for the three fields would make it well-defined.
  • CoversWorkArea issues MonitorFromRect + GetMonitorInfoW — two win32k round-trips — on every border update from DWM's composition path. Cheap, but avoidable, and moot if items 1–2 are addressed as suggested.
  • GetModuleHandle(L"udwm.dll") works because UNICODE is defined, but the explicit GetModuleHandleW is the convention used elsewhere.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • Apps that explicitly ask for square corners get overridden. GetEffectiveCornerStyle returns DWMWCP_DONOTROUND both when Windows squares a snapped window and when the app itself set DWMWA_WINDOW_CORNER_PREFERENCE to DWMWCP_DONOTROUND. The hook can't tell the two apart, so such a window would render square when floating and round when snapped — the inverse of the intended effect, and potentially a clipping problem for windows with custom-shaped content. Worth testing with an app that opts out of rounding; the HWND recovery from item 2 would also let you read the app's own preference and leave it alone.
  • Promoting the corner style also changes the shadow style. GetEffectiveCornerStyle's result feeds GetShadowStyle as well as the radius — that's why Custom Window Corner Radius promotes tooltips to get "border + shadow + clip" together (#L237-L246). Windows deliberately drops the drop shadow on snapped/maximized windows, so it's worth confirming that two side-by-side snapped windows don't now get a shadow seam between them, and that a maximized window doesn't get one at the monitor edges.
  • The radius hooks disagree about DPI scaling. GetDpiAdjustedFloatCornerRadius_Hook scales, the other two return the raw g_settings.radius. That matches the new-build call tree, but on older builds the DPI scaling is inlined into GetRadiusFromCornerStyle (#L373-L391), so its return value is already scaled there and substituting an unscaled 8 comes out visibly too small at 150%/200%. Worth checking on a high-DPI machine which getter actually fires on which build.
  • All four corners round, including the inner ones. Two windows snapped side by side get a small notch where their adjacent corners meet. That's inherent to this pipeline and matches the look you're after, but a sentence in the README next to the screenshot would set expectations.
  • Nothing triggers a refresh on enable/disable/settings change. Removing the SPI_SETDRAGFULLWINDOWS nudge was the right call and this matches Custom Window Corner Radius' behavior — this is just an FYI backing the README wording in item 6.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 7, 2026
@leshaalexey

Copy link
Copy Markdown
Author

/ready-for-reviewer

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-reviewer Ready for a human reviewer, and in the queue for one. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 7, 2026
@m417z

m417z commented Aug 7, 2026

Copy link
Copy Markdown
Member

Thank you for the submission. I agree with point 7 of the last review.

I added it the custom-corner-radius mod here and credited you and your PR:
m417z/my-windhawk-mods@d1a9303

What do you think?

You can try the mod here:
https://github.com/m417z/my-windhawk-mods/blob/d1a9303a34c4580f5d5911d26d0fefb12fa5fff7/mods/custom-corner-radius.wh.cpp

@m417z m417z added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-reviewer Ready for a human reviewer, and in the queue for one. labels Aug 7, 2026
@leshaalexey

Copy link
Copy Markdown
Author

its good idea

Updated GitHub link and compiler options in the mod file. Removed unnecessary changelog and authorship sections.
@leshaalexey

Copy link
Copy Markdown
Author

/ready-for-reviewer

@windhawk-reviewer

Copy link
Copy Markdown

@leshaalexey /ready-for-reviewer can't be applied here: the most recent AI review covers f090632, but the current head of this pull request is cee1da2. Comment /ai-review to get a review of the current code.

@leshaalexey

Copy link
Copy Markdown
Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 8, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


Nice cleanup since the last revision — the symbol hooks are now proper decorated names against udwm.dll only, and the developer-only settings are gone. The remaining issues are mostly in the maximized/snapped split and in the region-thread lifecycle.

1. The maximized/snapped split is made at the wrong layer, and SetBorderParameters_Hook squares windows it shouldn't.

SetBorderParameters_Hook is the only place that distinguishes maximized from snapped, and it does so with a "≥95% of the work area" heuristic on the border rect, with no IsMaximizedOrSnapped gate (pThis here is a CWindowBorder, so the filter isn't available). Two consequences:

  • Any window whose border rect covers ≥95% of the work area in both dimensions gets cornerRadius forced to 0 — including ordinary windows that were never snapped or maximized. On a 1920×1080 display (work area 1920×1040), a window the user dragged out to 1900×1000 passes both checks, so it loses the rounded corners it would normally have while the mod is enabled. DWM's own full-screen surfaces (Task View, Alt+Tab) are in the same boat. This also contradicts the README's "Every replacement is gated on IsMaximizedOrSnapped".
  • Squaring the border doesn't actually leave maximized windows alone. The style/radius getters still fire for them (a maximized window is IsMaximizedOrSnapped), so the rounded clip geometry is still applied — per the pipeline documented in custom-corner-radius, CTopLevelWindow3D::UpdateAnimatedResources -> GetDpiAdjustedFloatCornerRadius -> ResourceHelper::CreateRectangleGeometry is a separate path from SetBorderParameters. With roundMaximized off you can therefore still get the intermittent rounded clipping the option is supposed to avoid, now framed by a square border.

The clean fix is to recover the HWND at the CTopLevelWindow level and gate on IsZoomed(hwnd) inside the existing getters, then drop CoversWorkArea and SetBorderParameters_Hook entirely. custom-corner-radius already does exactly this HWND recovery and you can reuse it verbatim: HwndFromTopLevelWindow plus the two capture-only symbols at L397 and L406.

2. UpdateWindowRegion never removes the region it applied, so regions outlive the mod.

if (!IsRoundableMaximizedWindow(hwnd)) {
    ForgetWindow(hwnd);
    return;
}

This drops the window from tracking without calling SetWindowRgn(hwnd, nullptr, TRUE). So: maximize an app (region applied, tracked) → restore it (IsZoomed false → forgotten, region left behind) → disable the mod → ClearAllWindowRegions() no longer knows about that window and the round-rect region stays on it permanently. That breaks the README's "disabling the mod restores the default look immediately" and Windhawk's reversibility principle. The fullscreen case is worse: a maximized window that goes fullscreen also fails IsRoundableMaximizedWindow (the EqualRect(&rect, &mi.rcMonitor) check), so the stale region keeps clipping a game's or player's corners.

The same leak happens when the table is full — RememberWindow silently does nothing when all 64 slots are taken, but SetWindowRgn already succeeded.

Fix: clear the region for tracked windows that stop qualifying, and use an std::unordered_set<HWND> so there's no cap:

if (!IsRoundableMaximizedWindow(hwnd)) {
    if (ForgetWindow(hwnd)) {   // returns true if it was tracked
        SetWindowRgn(hwnd, nullptr, TRUE);
    }
    return;
}

Worth noting in the README too — with roundMaximized on it is no longer true that "only the compositor's drawing changes"; the mod sets window regions on other processes' windows.

3. Unload can leave the region thread running inside a freed module — dwm.exe crash.

PostThreadMessage(g_regionThreadId, WM_QUIT, 0, 0);
WaitForSingleObject(g_regionThread, 2000);

Two independent ways this leaves a live thread behind:

  • PostThreadMessage's return value is ignored. If the region thread hasn't reached GetMessage yet, it has no message queue and the post fails with ERROR_INVALID_THREAD_ID — the quit request is simply lost. Wh_ModInit creates the thread and returns immediately, so a disable/reload shortly after enabling hits this.
  • Even when the post succeeds, the wait is bounded at 2000 ms and the return value is unchecked.

In either case Wh_ModUninit goes on to DeleteCriticalSection(&g_regionCs) (while the thread may be inside EnterCriticalSection) and returns, Windhawk FreeLibrarys the mod, and the thread is left executing unmapped mod code. In dwm.exe that means the desktop session restarts. The wait must be unconditional:

WaitForSingleObject(g_regionThread, INFINITE);

and the queue must be guaranteed to exist before Wh_ModInit returns. explorer-folder-hover-menu shows the pattern — force the queue with PeekMessageW(&msg, nullptr, WM_USER, WM_USER, PM_NOREMOVE), then SetEvent a ready event that the init path waits on. That mod also avoids posting WM_QUIT directly (L152-L155) in favour of a WM_APP message whose handler calls PostQuitMessage, since a posted WM_QUIT can be dropped by a modal loop.

4. The system-wide WinEvent hooks are installed even when the feature they serve is disabled, and they cover far more than needed.

The region thread and both SetWinEventHook calls run unconditionally in Wh_ModInit, but UpdateWindowRegion bails out immediately unless roundMaximized is on — which is off by default. So every user of this mod pays for two blanket, session-wide accessibility hooks for nothing. EVENT_OBJECT_DESTROY..EVENT_OBJECT_LOCATIONCHANGE is a wide range: it includes EVENT_OBJECT_LOCATIONCHANGE, EVENT_OBJECT_STATECHANGE, EVENT_OBJECT_FOCUS, EVENT_OBJECT_REORDER and the four EVENT_OBJECT_SELECTION* events, all of which fire constantly across every process and get marshalled into dwm.exe.

Start the thread and install the hooks only when roundMaximized is true (and tear them down from Wh_ModSettingsChanged when it's turned off), and register only the events you actually consume — separate SetWinEventHook calls for e.g. EVENT_OBJECT_LOCATIONCHANGE, EVENT_OBJECT_DESTROY and EVENT_SYSTEM_MINIMIZEEND instead of the two ranges.

5. The region is rebuilt and re-applied on every event, even when nothing changed.

UpdateWindowRegion always creates a fresh HRGN and calls SetWindowRgn(hwnd, region, TRUE). With the hooks above, a maximized window gets that on every EVENT_OBJECT_STATECHANGE / EVENT_OBJECT_REORDER / focus change, and bRedraw = TRUE forces a full non-client + client repaint each time — visible flicker plus GDI and composition churn. Cache the last applied rect and radius per tracked HWND and skip the call when they're unchanged.

6. GetDpiAdjustedFloatCornerRadius_Hook uses the system DPI, not the window's.

UINT dpi = GetDpiForSystem();

On a mixed-DPI multi-monitor setup this is wrong for every window that isn't on the primary-DPI monitor: a snapped window on a 150% display gets an 8px radius where the shell uses 12px, and vice versa. Once the HWND is available from item 1, use GetDpiForWindow(hwnd)UpdateWindowRegion already does the right thing.

7. The README has no screenshot.

The mod's entire effect is visual and there's no image in the readme block (the PR description says one was added, but the file at cee1da2 has none — it looks like it was dropped by the last commit). Please add a before/after screenshot. Both existing dwm.exe corner mods also include the advanced-settings screenshot in their "Important usage note" section — see disable-rounded-corners, which uses https://i.imgur.com/LRhREtJ.png; feel free to reuse it.

8. Overlap with Custom Window Corner Radius.

To put a number on the overlap you raised in the PR description: this mod targets the same process, hooks the same four uDWM functions with the same typedefs (GetEffectiveCornerStyle, GetRadiusFromCornerStyle, GetFloatCornerRadiusForCurrentStyle, CWindowBorder::SetBorderParameters), copies HasMultipleDwminitWarningsInLastMinute verbatim, and reuses the same Wh_ModInit skeleton. The functional delta is "promote DWMWCP_DONOTROUNDDWMWCP_ROUND when IsMaximizedOrSnapped", plus the optional window-region trick. That's a small, self-contained addition, and the two mods hooking the same functions in the same process is exactly the conflict your Compatibility section warns about.

Your argument that the two answer different questions is reasonable, but the maintainer's standing preference is to extend an existing mod over merging a near-neighbour, and here the shared surface is unusually large. My recommendation is to fold it into custom-corner-radius as a "Round snapped windows" option — the final call is the maintainer's, so this is worth confirming before investing more in the items above.

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • ClearAllWindowRegions calls SetWindowRgn on other processes' windows while holding g_regionCs. Copy the handles into a local vector, leave the critical section, then make the calls.
  • int free = -1; in RememberWindow shadows ::free. Switching to std::unordered_set<HWND> (item 2) removes the variable, the fixed cap and the linear scans in one go.
  • The radius setting's $description says it's "Only used on builds where DWM zeroes the corner radius instead of changing the corner style", but it also drives the roundMaximized region radius and GetDpiAdjustedFloatCornerRadius_Hook. Worth rewording.
  • The GPL credit comment names only HasMultipleDwminitWarningsInLastMinute, but the SetBorderParameters typedef/hook and the init skeleton come from the same mod. Broadening the credit line costs nothing.
  • Wh_ModSettingsChanged clears regions when roundMaximized is turned off but never applies them when it's turned on — already-maximized windows stay square until they happen to emit an event.
  • If CreateThread fails, the mod logs and carries on, so the feature silently doesn't work. If you adopt item 4 the thread only exists when the option is on, which makes a hard failure there reasonable.
  • LoadSettings writes g_settings from the Windhawk engine thread while the hooks read it from DWM's composition thread with no synchronization. They're aligned scalars so it's benign in practice, but std::atomic<int> / std::atomic<float> / std::atomic<bool> costs nothing here.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • Worth verifying that roundMaximized produces visible rounding at all. GetWindowRect on a maximized WS_THICKFRAME window includes the invisible resize border, so the rect overhangs the monitor on every side by roughly SM_CXSIZEFRAME + SM_CXPADDEDBORDER — about 8px at 100% DPI, i.e. the same order as the default radius. The comment above CreateRoundRectRgn notes that overhang but the code doesn't compensate for it, so the arcs should land off-screen and the visible corners at the monitor edge stay square. If that matches what you see, inset the region by the overhang (compare GetWindowRect against mi.rcMonitor, or use AdjustWindowRectExForDpi) before building it.
  • An app that deliberately opts out of rounding via DwmSetWindowAttribute(DWMWA_WINDOW_CORNER_PREFERENCE, DWMWCP_DONOTROUND) also reports DWMWCP_DONOTROUND from GetEffectiveCornerStyle, so snapping such a window will round it against the app's explicit wish. There's no way to tell that apart from "DWM squared it because it's snapped" at that hook, but it's worth a line in the README.
  • SetWindowRgn overwrites any region the app set for itself (custom-shaped windows), and ClearAllWindowRegions then sets nullptr — which destroys the app's shape rather than restoring it. Rare, but it's a case where "disabling the mod restores the previous state" doesn't hold.
  • Given that the region also clips the app's own content and forces a composition pass — both of which the README documents honestly — it's worth asking whether roundMaximized earns its complexity. Dropping it would remove items 2–5 and the -lgdi32 dependency along with it.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 8, 2026
@leshaalexey

Copy link
Copy Markdown
Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 8, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


The mod is well documented and the hook set is sound. The main concerns are the window-tracking mechanism, which is fragile in ways that produce exactly the flicker the README warns about, and the relationship to Custom Window Corner Radius.

1. This is very likely better as an option in Custom Window Corner Radius.

You raised this yourself, so here's the reasoning as input to that decision. The two mods hook the same four uDWM.dll functions (GetEffectiveCornerStyle, GetRadiusFromCornerStyle, GetFloatCornerRadiusForCurrentStyle, CWindowBorder::SetBorderParameters) in the same process, which is why your own Compatibility section has to tell users not to run them together. That's a concrete user-facing cost: anyone who wants a non-default radius and rounded snapped corners can't have both, and there's no way to fix that while they're separate mods. Folded in, the gating predicate becomes one extra condition in the existing RadiusForOriginal / GetEffectiveCornerStyle_Hook path rather than a second copy of the whole pipeline.

Worth noting too that the derivation is broader than the credit comment suggests — the symbol-hook list, the SetBorderParameters prototype, the Wh_ModInit structure and HasMultipleDwminitWarningsInLastMinute all come from custom-corner-radius.wh.cpp. The license matches (GPL-3.0), so there's no compliance problem — but if the mod does stay standalone, please widen the attribution comment to reflect that.

Ultimately a catalog call for the maintainer, not a code defect.

2. The g_currentWindow marker + 32-slot table is unreliable; ask Windows for the window state instead.

Three separate failure modes, all ending in the same visible symptom (a maximized window gets rounded and flickers):

  • Live entries get evicted. RememberWindowState allocates g_windowStateNext round-robin without checking whether that slot is still in use, and ForgetWindowState clears only maximized — it leaves window set, so destroyed windows keep occupying slots forever. In a session with more than 32 composed top-level windows (not unusual — this includes windows you never see), a live maximized window's entry gets overwritten, IsKnownMaximized returns false, and the getters force rounding onto it.
  • Recycled pointers inherit stale verdicts. Entries are never removed, so a CTopLevelWindow freed and reallocated at the same address picks up the previous window's maximized flag until the next border update corrects it.
  • The "a getter always runs right before SetBorderParameters for the same window" assumption doesn't hold on every build. Per the call tree documented in custom-corner-radius (L374-L391), old builds compute the radius inline inside UpdateWindowVisuals and pass it directly to SetBorderParameters — no getter call at all — while GetRadiusFromCornerStyle / GetDpiAdjustedFloatCornerRadius are also reached from CTopLevelWindow3D::UpdateAnimatedResources, which never calls SetBorderParameters. A marker left behind by that path is then consumed by an unrelated SetBorderParameters, attributing one window's rectangle to another. On top of that, all three hooks that set the marker plus SetBorderParameters itself are marked optional, so on a build where the resolution goes badly the table is simply never populated and maximized detection silently stops working.

The fix that removes all three at once: recover the HWND from the CTopLevelWindow and ask the system, e.g. IsZoomed(hwnd) (or GetWindowPlacement, as in center-new-windows.wh.cpp#L892). custom-corner-radius already implements exactly that lookup and you can lift it as-is — HwndFromTopLevelWindow at L155-L165, the two capture-only symbol hooks at L393-L410, and the offset recovery at L465-L478. ShouldRound then becomes:

bool ShouldRound(void* pThis) {
    if (!IsMaximizedOrSnapped(pThis)) {
        return false;
    }
    HWND hwnd = HwndFromTopLevelWindow(pThis);
    return hwnd && !IsZoomed(hwnd);
}

and g_windowState, g_stateCs, g_currentWindow, CoversWorkArea, RememberWindowState, ForgetWindowState and IsKnownMaximized all go away — roughly 80 lines, an exact answer instead of a 95%-of-work-area guess, and no cross-window attribution. SetBorderParameters_Hook reduces to the same IsZoomed check for its own window.

3. GetDpiAdjustedFloatCornerRadius_Hook scales by the system DPI.

UINT dpi = GetDpiForSystem();
float value = g_settings.radius * (dpi ? dpi / 96.0f : 1.0f);

On a mixed-DPI multi-monitor setup this is the wrong scale factor for any window not on the primary-DPI monitor — an 8px fallback renders as 8px on a 150% monitor when the surrounding geometry expects 12px. Use the window's own DPI: GetDpiForWindow(hwnd) once the HWND lookup from item 2 is in place, or record the dpi argument that SetBorderParameters already receives.

Related: the PR description says the forced radius is scaled through CTopLevelWindow::GetWindowData + CWindowData::ScaleForDpi, with system DPI only as a fallback. That code isn't in the head commit (4663799) — the last commit appears to have dropped it. Was that intentional?

4. The README has no screenshot.

The mod's entire purpose is a visible change, and the PR description says a screenshot was added, but the current README has no image. Please add a before/after (or a short GIF of snapping a window). Both dwm.exe mods in the catalog also include the advanced-settings screenshot next to the process-inclusion note — see custom-corner-radius.wh.cpp#L34-L43 — which is worth mirroring since users who skip that step get a mod that silently does nothing.

Optional improvements

Minor polish — none of this affects users, so it's your call. Items 1-3 are moot if you take the IsZoomed route above.

  • InitializeCriticalSection(&g_stateCs) runs before the udwm.dll and HookSymbols failure paths, but DeleteCriticalSection only ever runs in Wh_ModUninit. Either initialize it after those checks, or just use a global std::mutex + std::lock_guard — on Windows its destructor is a no-op, so there's nothing to initialize or delete and the early returns become safe by construction.

  • ForgetWindowState doesn't forget anything — it sets maximized = false and keeps the slot occupied. Either rename it (ClearMaximizedFlag) or actually clear window so the slot can be reused.

  • The captured original is named IsMaximizedOrSnapped, without the _Original suffix every other captured/hooked pointer in the file uses. At the call sites it reads like a real Win32 API rather than a uDWM internal.

  • LoadSettings silently clamps radius to 1-40. Worth stating the accepted range in the $description so a user who types 0 isn't surprised by a 1px radius.

  • SetBorderParameters is marked optional here but is mandatory in custom-corner-radius. If it doesn't resolve, maximized detection never engages and maximized windows get rounded (i.e. the flicker the README describes) with no indication why. Consider making it mandatory, or at least logging when an optional hook is missing so a bug report is diagnosable.

Functionality notes

Non-critical observations about the feature behavior itself.

  • One-update lag on newly maximized windows. The getters run before SetBorderParameters for a given update, so on the first update after a window becomes maximized there is no verdict yet: the getters force rounding, which reaches the geometry/clip path (UpdateAnimatedResourcesCreateRectangleGeometry), and SetBorderParameters then only takes back the border. So the README's "border and geometry both" is true from the second update onward, not the first. The IsZoomed check from item 2 makes it true immediately.

  • maximized && cornerRadius > 0.0f → 0 also removes rounding the mod never added. On any build (or with any other tool) where a maximized window legitimately arrives with a non-zero radius, this hook squares it. Probably not reachable on current Windows 11 builds, but it means the mod's failure mode is subtractive rather than neutral — worth gating on "we forced this radius" if you keep the current structure.

  • CoversWorkArea's 95% threshold is a heuristic. A window snapped to a zone that happens to cover ≥95% of the work area in both dimensions is classified as maximized and loses its rounding. Not likely with the standard Snap Layouts, but custom zone tools (FancyZones etc.) can produce it.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-for-author The author's turn: request an AI review, or respond to one that was posted.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants