Skip to content

Add Glass Cursors mod - #5202

Open
fizixes wants to merge 6 commits into
ramensoftware:mainfrom
fizixes:add-glass-cursors
Open

Add Glass Cursors mod#5202
fizixes wants to merge 6 commits into
ramensoftware:mainfrom
fizixes:add-glass-cursors

Conversation

@fizixes

@fizixes fizixes commented Aug 24, 2026

Copy link
Copy Markdown

Adds Glass Cursors, a DPI-aware translucent cursor replacement with
configurable pointer styles, glass color, opacity, sizing, and animated
loading cursors.

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.

This mod was created by:

    • The submitter, without AI assistance
    • [ X] 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.

@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 24, 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.

@fizixes

fizixes commented Aug 24, 2026

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 24, 2026
@fizixes

fizixes commented Aug 24, 2026

Copy link
Copy Markdown
Author

/ai-review

@windhawk-reviewer

Copy link
Copy Markdown

@fizixes /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.


Nice, self-contained rasterizer, and the reversibility story (SPI_SETCURSORS on unload, nothing written to the registry) is the right approach. The main problems are architectural: the mod doesn't need to live inside Explorer, it can be unloaded while its worker thread is still running, and the animation loop hammers session-global state around the clock.

1. This should be a tool mod, not an explorer.exe injection. The mod installs zero function/symbol hooks and touches nothing Explorer-specific — it only calls SetSystemCursor and SystemParametersInfoW, both of which are session-global and work from any process. That's the textbook case for Mods as tools: Running mods in a dedicated process. Two concrete consequences today:

  • There can be more than one explorer.exe (the "Launch folder windows in a separate process" option, or Explorer being respawned for other reasons). Each instance loads its own copy of the mod, renders its own 62 cursor bitmaps, and starts its own 30 FPS SetSystemCursor loop — all writing the same global cursor slots and fighting each other.
  • A fault anywhere in ~1000 lines of hand-rolled rasterization takes the whole shell down with it.

Change @include to windhawk.exe, rename Wh_ModInit / Wh_ModUninit / Wh_ModSettingsChanged to the WhTool_* equivalents, and paste the launcher snippet from the wiki verbatim (don't refactor it). mods/mac-magnifying-cursor.wh.cpp is the closest precedent — it drives the same SetSystemCursor API for the same reason and is already a tool mod; mods/explorer-folder-hover-menu.wh.cpp has the boilerplate at the bottom of the file.

2. StopAnimation gives up on the worker thread after 2 seconds and unloads anyway — that's a crash. (L1359-L1377)

WaitForSingleObject(g_animationThread, 2000);   // <-- may return WAIT_TIMEOUT
CloseHandle(g_animationThread);
...
CloseHandle(g_animationStopEvent);
g_busyFrames.clear();

Windhawk unloads the mod with a single FreeLibrary as soon as Wh_ModUninit returns, so when the wait times out the thread keeps executing code in an image that is about to be unmapped — it crashes the host even though it never touches mod state, because its instruction pointer and return address live in the mod image. It also gets worse in three ways: g_animationStopEvent is closed underneath the thread (so its next WaitForSingleObject returns WAIT_FAILED and the loop never exits — or, worse, the handle value gets recycled and it waits on an unrelated object), g_busyFrames/g_workingFrames are cleared while the thread may be indexing them, and the globals themselves get unmapped.

The thread never blocks indefinitely (it only waits on the stop event with a short timeout), so there is no reason not to wait unconditionally:

WaitForSingleObject(g_animationThread, INFINITE);

3. The animation thread does 60 SetSystemCursor calls per second, forever, for the whole session — even when no busy cursor is on screen. (L1323-L1337) AnimationThreadProc unconditionally re-sets OCR_WAIT and OCR_APPSTARTING every g_animationDelay ms, and each of those goes through CreateCursorFromPixelsGetDC + CreateDIBSection + CreateBitmap + CreateIconIndirect + DeleteObject×2, then a SetSystemCursor call into win32k that mutates state shared by every process in the session. That is ~60 cursor objects created and destroyed per second, 24/7, for a cursor that is visible a fraction of a percent of the time.

Gate the loop on the busy cursor actually being displayed, and idle-poll cheaply otherwise. LoadImage(..., LR_SHARED) returns the current handle for a system cursor role, so you can compare it against GetCursorInfo — the same technique mac-magnifying-cursor uses:

bool BusyCursorVisible() {
    CURSORINFO ci = {sizeof(ci)};
    if (!GetCursorInfo(&ci) || !(ci.flags & CURSOR_SHOWING) || !ci.hCursor) {
        return false;
    }
    for (DWORD id : {CURSOR_WAIT, CURSOR_APPSTARTING}) {
        if (ci.hCursor == (HCURSOR)LoadImage(nullptr, MAKEINTRESOURCE(id),
                                             IMAGE_CURSOR, 0, 0, LR_SHARED)) {
            return true;
        }
    }
    return false;
}

then wait g_animationDelay while busy and ~100 ms while idle. While you're there, pre-create the 48 HCURSORs once in PrepareAnimationFrames and pass a CopyIcon of the frame to SetSystemCursor (which destroys the handle it's given) instead of rebuilding a DIB section and a mask bitmap on every frame.

It's also worth testing whether the animation can be handed to the system entirely: build an .ani (frames + rate/sequence chunks) in a temp file, LoadCursorFromFileW it, and SetSystemCursor that — if the animation survives, the whole thread and the global churn disappear.

4. Every settings change reloads the mod and re-renders all 62 cursors synchronously. Wh_ModSettingsChanged always sets *bReload = TRUE (L1397), so nudging the Red slider by one runs Wh_ModUninitSPI_SETCURSORS (a visible flash back to the default scheme, broadcast session-wide) → Wh_ModInitApplyStaticCursors() + PrepareAnimationFrames().

That init path is not cheap: 14 static roles + 24 busy frames + 24 working frames = 62 renders, each rasterizing an n = size * 4 surface of Pixel (4 doubles) with a per-pixel PointInPolygon scan over ~50 vertices. At the 96 px setting that's a 384×384 surface per render, and Wh_ModInit blocks until all 62 are done — which means it also delays Explorer's startup on every boot, not just on settings changes.

Two fixes:

  • Use the plain void Wh_ModSettingsChanged() variant and re-apply in place, skipping the SPI_SETCURSORS round-trip:
    void Wh_ModSettingsChanged() {
        StopAnimation();
        LoadSettings();
        ApplyStaticCursors();
        StartAnimation();
    }
  • Move PrepareAnimationFrames() off the init path — render the 48 animation frames on the animation thread itself (the !g_busyFrames.empty() guard in the loop already tolerates them not being ready yet), so only the 14 static cursors are rendered synchronously.

5. The README has no images. This is an entirely visual mod with four pointer styles, two hand styles, and configurable color/opacity/size — users have no way to tell what any of it looks like from the description. Please add at least one screenshot showing the cursor set (ideally a small grid of the roles, plus a GIF of the loading spinner). Only i.imgur.com and raw.githubusercontent.com are allowed as image hosts.

Optional improvements

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

  • WindhawkUtils::StringSetting instead of the raw get/free pair. (L1250-L1268) It's RAII, so no manual Wh_FreeStringSetting. Also, Wh_GetStringSetting never returns NULL — it returns L"" on error or when unset — so the if (pointerStyle) / if (handStyle) guards are dead code.
    WindhawkUtils::StringSetting pointerStyle =
        WindhawkUtils::StringSetting::make(L"PointerStyle");
    if (wcscmp(pointerStyle, L"cleanRounded") == 0) { ... }
  • Drop the Glass Cursors: prefix from the log line. (L1282) Wh_Log already prefixes the mod name and the function.
  • CreateBitmap(size, size, 1, 1, nullptr) leaves the mask contents undefined. (L1169) MSDN is explicit that with lpvBits == NULL the bitmap contents are undefined. The alpha channel of the 32-bpp color bitmap is what actually gets used here, so it works in practice, but any code path that falls back to the AND mask would render garbage. Either pass a zero-filled buffer (((size + 15) / 16) * 2 * size bytes) or PatBlt it, as net-toggle does.
  • Restore the cursors if the host process dies unexpectedly. If the process is terminated without Wh_ModUninit running, the glass cursors stay applied session-wide with no obvious way for the user to get them back. mac-magnifying-cursor guards against this with SetUnhandledExceptionFilter(RestoreCursorsOnCrash). This becomes more relevant once the mod runs in its own windhawk.exe.
  • StartAnimation()'s return value is ignored in Wh_ModInit (L1388) — either log the failure or make the function void.
  • Consider a single hex color setting instead of three 0-255 number settings. The repo convention for colors is a string like - color: "#212630" — see taskbar-count-badges or desktop-live-overlay. It's one field to edit instead of three, and it's what users are used to seeing in other mods.
  • The ## Author README section duplicates @author. Windhawk already shows the author from the metadata.

Functionality notes

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

  • ArtworkScale undoes the high-resolution rendering it's meant to complement. The README's selling point is that "geometry is drawn directly at the selected output resolution instead of scaling a 32 px bitmap" — but ScaleArtworkAroundHotspot (L986) then bilinearly resamples the already downsampled bitmap, by 68% at the default setting. So in the default configuration every cursor goes through exactly the blurry bitmap rescale the design is supposed to avoid. Fold the scale factor into the coordinate transform instead — P() already maps normalized coordinates to the supersampled canvas, so scaling around the hotspot there rasterizes the geometry at its final size and ScaleArtworkAroundHotspot can be deleted outright:
    Point P(double x, double y, int canvasSize) {
        const double k = g_artworkScale / 100.0;
        return {(hx + (x - hx) * k) * canvasSize, (hy + (y - hy) * k) * canvasSize};
    }
    (with hx/hy the per-role normalized hotspot).
  • A cursor set via SetSystemCursor is a single fixed-size bitmap, so mixed-DPI setups get one right size and one wrong one. ResolveCursorSize (L1205) samples SM_CXCURSOR and the desktop DC's LOGPIXELSX once at init — that's effectively the primary monitor's DPI. On a 150% laptop + 100% external display, the cursor will be visibly oversized or undersized on one of them, and Windows won't rescale it the way it does for the built-in multi-resolution .cur/.ani cursors. Nothing to fix in the code necessarily — but it's worth documenting in the README, since it's the likely reason someone would reach for the ArtworkScale and manual size overrides in the first place.
  • The size isn't re-resolved when the display configuration or the accessibility cursor size changes. If the user changes display scaling or the "Mouse pointer size" slider while the mod is loaded, the cursors keep the size picked at init until the mod is reloaded. Also note that the Settings > Mouse pointer page calls SPI_SETCURSORS itself, which silently reverts the mod's cursors until the next reload — reacting to WM_SETTINGCHANGE / SPI_SETCURSORS and re-applying would cover both.
  • Hotspots are truncated rather than rounded. hotX = static_cast<int>(size * 0.235) gives 7 at 32 px where the rendered tip sits at 7.52 — a consistent half-pixel bias up-left on the pointer, help and hand cursors. std::lround would centre it better.
  • PointInPolygon uses the even-odd rule, so any self-intersection in the generated outlines (most likely in the symmetricCurved notch, where the cubic crosses the symmetry axis) will punch a hole in the fill rather than filling solid. Worth eyeballing that style at 32 px specifically.


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 24, 2026
@fizixes

fizixes commented Aug 24, 2026

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 24, 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.


Good iteration — the tool-mod conversion, the unconditional thread join, the gated animation loop, the pre-created frames, the void WhTool_ModSettingsChanged(), the zero-filled mask and the folding of ArtworkScale into the coordinate transform all landed correctly. Three things are left.

1. If the animation can't start, the tool process exits and leaves the glass cursors applied with no way back. In the tool-mod pattern the mod's own thread is the only thing keeping windhawk.exe -tool-mod glass-cursors alive — EntryPoint_Hook makes the main thread ExitThread(0) immediately, so once the animation thread ends the last thread is gone and the process terminates. Two paths reach that state:

  • WhTool_ModInit only logs a StartAnimation() failure and still returns TRUE (L1414-L1417).
  • AnimationThreadProc returns 0 when PrepareAnimationFrames() fails (L1338-L1340) — one failed CreateDIBSection/CreateIconIndirect under GDI-handle pressure is enough.

Either way ApplyStaticCursors() has already replaced 14 session-global cursor slots, and WhTool_ModUninit never runs, so SPI_SETCURSORS is never issued. The user is left with the glass cursors for the rest of the session, and disabling the mod does not restore them — the launcher instance's Wh_ModUninit returns early via g_isToolModProcessLauncher.

Keep the thread alive when frames are unavailable, and fail loudly (restoring the scheme) if the thread itself can't be created. Note that PrepareAnimationFrames can also fail after pushing some frames, so the loop needs a guard — g_busyFrames[frame] would be out of bounds otherwise:

DWORD WINAPI AnimationThreadProc(LPVOID) {
    const bool framesReady = PrepareAnimationFrames() &&
                             g_busyFrames.size() == kSpinnerFrames &&
                             g_workingFrames.size() == kSpinnerFrames;
    if (!framesReady) {
        Wh_Log(L"Animation frames unavailable, static cursors only");
    }

    int frame = 0;
    while (WaitForSingleObject(g_animationStopEvent, 0) != WAIT_OBJECT_0) {
        const DWORD visibleRole = framesReady ? GetVisibleBusyCursorRole() : 0;
        ...
    }
    return 0;
}

BOOL WhTool_ModInit() {
    SetUnhandledExceptionFilter(RestoreCursorsOnCrash);
    LoadSettings();
    ApplyStaticCursors();
    if (!StartAnimation()) {
        RestoreWindowsCursorScheme();   // don't strand the user's cursors
        return FALSE;                   // boilerplate then ExitProcess(1)
    }
    return TRUE;
}

theme-toggler-tray is the minimal version of the same contract — its WhTool_ModInit returns the thread-creation result directly.

2. Anything that broadcasts SPI_SETCURSORS silently undoes the mod until it is reloaded. Applying a Windows theme, the Ease of Access "Mouse pointer size/style" page, and the classic Pointers control panel all call SystemParametersInfo(SPI_SETCURSORS, ...), which reloads the user's cursor scheme from the registry and wipes every SetSystemCursor the mod made. The mod has no message loop at all, so it never notices — from the user's point of view the mod just stops working, with no indication why, and the only fix is toggling it off and on.

The animation thread is a natural host for a message-only window:

// in the animation thread, before the poll loop
HWND hMsgWnd = CreateWindowExW(0, L"Static", nullptr, 0, 0, 0, 0, 0,
                               HWND_MESSAGE, nullptr, nullptr, nullptr);
// subclass it (WindhawkUtils::SetWindowSubclassFromAnyThread) and on
// WM_SETTINGCHANGE with wParam == SPI_SETCURSORS, re-run
// LoadSettings() + ApplyStaticCursors() + re-apply the current frames.

Two things to watch: destroy the window and unregister the subclass before the thread exits (so nothing from the mod image can run after WhTool_ModUninit), and debounce/guard the handler so the mod's own SPI_SETCURSORS in WhTool_ModUninit — and any notification triggered by its own SetSystemCursor calls — can't drive a re-apply loop. Re-resolving ResolveCursorSize() in the same handler also covers display-scaling and pointer-size changes, which the mod currently samples once at init.

3. The README still has no images. (L13-L36) This is an entirely visual mod with four pointer styles, two hand styles, and configurable colour, opacity and size — nobody can tell what any of that looks like from the text. Please add at least one screenshot of the cursor set (a small grid of the roles works well), ideally plus a GIF of the loading spinner so the animated part is visible too. Only i.imgur.com and raw.githubusercontent.com are allowed as image hosts.

Optional improvements

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

  • The tool-mod boilerplate has drifted from the wiki snippet. Four blank lines were stripped (after the sessionId check, after LocalFree(argv), after the swprintf_s call, and after the g_isToolModProcessLauncher check in Wh_ModUninit). It's cosmetic, but the snippet is meant to be a verbatim copy so it can be diffed against the wiki when it changes — worth restoring, and excluding that block from your formatter.
  • CURSOR_PERSON is applied outside the roles array for no apparent reason. (L1226-L1246) It goes through the exact same SetRenderedSystemCursor(RenderRole(...)) call as the other 13 — just make the array std::array<DWORD, 14> and drop the extra statement. (Is the split intentional, or an AI artifact?)
  • Include the headers for the wide-char CRT functions you use. (L91-L100) wcscmp and wcstol come in transitively via <windows.h>; <cwchar> declares both and makes the dependency explicit.
  • Pixel is four doubles (32 bytes/px). At the 96 px setting that's a 384×384 surface = 4.7 MB per render, 62 renders per settings change. float would quarter the memory traffic with no visible difference after the 4× downsample.
  • Consider one hex colour setting instead of three 0-255 numbers. The repo convention is a string like - glassColor: "#21262E" — see taskbar-count-badges. One field to edit instead of three, and it's what users see in other mods. (Raised last round; repeating only because it's a catalog-consistency thing, not because it's required.)

Functionality notes

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

  • OCR_PIN and OCR_PERSON get a centred hotspot on a symbol-only drawing. (L986-L1001, L1058-L1063) In Windows both of these are the normal arrow with a small pin/person badge, hotspot at the arrow tip. Rendering them as a standalone pin and a standalone bust means the user has no visual cue where the click actually lands. RenderHelp and the APPSTARTING case already do the right thing (arrow + badge) — the same treatment would fit here.
  • ResolveCursorSize measures DPI through a desktop DC. (L1160-L1170) GetSystemMetrics(SM_CXCURSOR) and GetDeviceCaps(hdc, LOGPIXELSX) both answer relative to the calling process's DPI-awareness context — which is now windhawk.exe's, not Explorer's, since the mod moved to a dedicated process. GetDpiForSystem() (and GetSystemMetricsForDpi(SM_CXCURSOR, GetDpiForSystem())) states the intent explicitly and skips the GetDC/ReleaseDC round-trip. Worth checking on a 150% display that the automatic size still picks 48 and not 32.
  • The animation could be handed to the system instead of driven at 30 Hz. The current loop is correct and cheap now that it's gated, but Windows can animate a cursor natively: build a RIFF ACON (anih + rate/seq + the frame icon chunks) in memory and hand it to CreateIconFromResourceEx(data, size, FALSE, 0x00030000, 0, 0, LR_DEFAULTCOLOR), then SetSystemCursor the result once. If that works, the thread, the GetCursorInfo polling and all the repeated session-global SetSystemCursor calls disappear.
  • PointInPolygon uses the even-odd rule, so any self-intersection in a generated outline punches a hole in the fill instead of filling solid. The symmetricCurved style is the likely candidate, since its notch cubic crosses the symmetry axis (L611-L621) — worth eyeballing that one at 32 px specifically.


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 24, 2026
@fizixes

fizixes commented Aug 24, 2026

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 24, 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.


Overall this is in good shape: the tool-mod boilerplate is a verbatim copy of the wiki snippet, the settings block and the code agree in both directions (every declared setting is read, every read name is declared, and every $options setting is a string read via StringSetting), hotspots line up with the rendered geometry, CreateIconIndirect is correctly fed straight (non-premultiplied) alpha, and unload joins the worker thread, destroys the window on its owning thread and restores the user's cursor scheme. One thing to fix:

The README has no screenshot or GIF. This mod is entirely artwork — four pointer silhouettes, two hand designs, two fill opacities, six artwork scales and an animated spinner — and there is no way for a user browsing windhawk.net to see any of it, or to pick between sharpRounded / cleanRounded / symmetricSharp / symmetricCurved without installing and cycling through them. Please add images to the README (only i.imgur.com and raw.githubusercontent.com are allowed hosts). At minimum a shot of the full cursor set, and ideally a small comparison strip of the four pointer styles plus a GIF of the busy spinner.

Optional improvements

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

  • Do the initial render on the worker thread instead of in Wh_ModInit. WhTool_ModInit currently runs LoadSettings() + ApplyStaticCursors() (14 roles, up to 384×384 supersampled each) inline, then StartAnimation() blocks up to 5 s waiting for the worker to signal readiness — all inside Wh_ModInit. Since the worker already exists and already knows how to build the whole set, having AnimationThreadProc call RebuildCursorSet() once before entering its loop would make init return immediately and remove the duplicated startup block: lines 1444-1449 in the thread proc are a copy of lines 1352-1360 in RebuildCursorSet.

  • ApplyStaticCursors() doesn't check the stop event. PrepareAnimationFrames() bails out between frames when g_animationStopEvent is signalled, but ApplyStaticCursors() doesn't, so WhTool_ModUninit can sit in WaitForSingleObject(g_animationThread, INFINITE) for the length of a full 14-cursor render at 96 px. Adding the same early-out to the role loop makes teardown bounded.

  • Cross-thread globals aren't synchronized. g_cursorWindow is written by the worker (set at line 1416, cleared at line 1499) and read by WhTool_ModSettingsChanged on the main thread; g_animationFramesReady and g_reapplyMessagePending are similar. The startup handoff is properly ordered by g_animationReadyEvent, but the shutdown clear isn't, so a settings change racing an unload is a data race. std::atomic<HWND> / std::atomic<bool> costs nothing here.

  • Drop the ERROR_CLASS_ALREADY_EXISTS tolerance (line 1406). Continuing when the class already exists means potentially reusing a class registered by a previous instance, whose lpfnWndProc points into an image that may no longer be mapped — that's the bug, not the fix. The matching if (classAtom) guard at line 1504 then also skips the UnregisterClass. It's unreachable today because the tool process always ExitProcesses on unload, but the guard invites the problem. Register unconditionally and treat failure as failure.

  • Chain and restore the unhandled exception filter. SetUnhandledExceptionFilter(RestoreCursorsOnCrash) (line 1615) discards the previous filter and never restores it. Keeping the returned pointer, calling it from RestoreCursorsOnCrash before returning, and restoring it in WhTool_ModUninit is a one-liner each. (The same shortcut exists in mac-magnifying-cursor, so this is a pre-existing pattern rather than something you invented — just not a great one.)

  • Include the Bootstrap Icons copyright line. The README credits the hand-index icon as "MIT-licensed", but MIT requires the copyright notice itself to travel with the derived work. A single line — Hand silhouette derived from Bootstrap Icons, Copyright (c) 2019-2024 The Bootstrap Authors, MIT License — in the README settles it.

  • The four RenderArrowShape* functions each redefine the same mirror lambda and repeat the same tip/notch axis math (lines 484-494, 531-542, 643-653, 711-721). Hoisting a single MakeMirror(tip, notch) helper, and a shared "build the left contour, mirror it, draw" driver, would cut roughly 150 lines of near-identical code and make the four styles read as the small control-point deltas they actually are.

  • Idle poll interval. The loop wakes every 100 ms forever just to call GetCursorInfo and compare two handles, even in a session where a busy cursor never appears. 300-500 ms would be plenty for "did a wait cursor just show up" and still let the 33 ms cadence kick in as soon as one is detected.

Functionality notes

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

  • Consider a native animated cursor instead of the 30 Hz SetSystemCursor loop. Windows animates .ani cursors itself with zero per-frame work from the mod — that's how the stock aero_working.ani "Working in background" cursor works. Building the 24 frames into a RIFF ANI in %TEMP%, loading it with LoadImage(..., LR_LOADFROMFILE) and handing that single handle to SetSystemCursor once would remove both the per-frame CopyIcon + SetSystemCursor churn (a system-wide USER operation, 30×/s, precisely while the machine is already busy) and the polling loop that drives it. It costs a temp file the mod has to clean up, so it's a trade-off rather than an obvious win — but worth a look.

  • No static fallback for the busy cursors. If PrepareAnimationFrames() fails, the log says "static cursors only", but CURSOR_WAIT and CURSOR_APPSTARTING are only ever set inside PrepareAnimationFrames — so on failure they stay as the stock Windows cursors while all 14 other roles are glass. A single SetRenderedSystemCursor(RenderRole(g_cursorSize, CURSOR_WAIT, 0), CURSOR_WAIT) (and the same for CURSOR_APPSTARTING) on the failure path would keep the set consistent.

  • OCR_NWPEN (32631, the "Handwriting" cursor) isn't replaced. It's part of the standard Windows cursor scheme alongside the 16 roles you do handle, so it shows up as the stock pen in the middle of an otherwise glass set. Easy to add if you want full coverage.

  • The default artwork scale makes the pointer noticeably smaller than stock. At ArtworkScale: 68, the arrow spans y 0.085→0.775 of n, i.e. ~47% of the cursor box, versus ~60% for the stock Windows arrow — about a quarter shorter. That may well be deliberate (glass shapes read heavier than a solid arrow), but if you want the mod to feel like a drop-in replacement out of the box, 90% is closer to parity.

  • Four pointer styles is a lot of surface for the differences involved. sharpRounded and cleanRounded differ by a handful of Bezier control points, as do symmetricSharp and symmetricCurved. Is the full set intentional, or leftover exploration? Two well-chosen styles would be easier to document with screenshots and easier for you to maintain.

  • Recovery if the tool process dies abnormally. RestoreCursorsOnCrash covers SEH crashes, but not TerminateProcess, a fail-fast (__fastfail, which bypasses the unhandled-exception filter), or a user killing the windhawk.exe tool process from Task Manager. In those cases the glass cursors persist until the user reapplies a cursor scheme or logs off. That's inherent to any SetSystemCursor-based mod and the effect is in-memory only, but a one-line README note ("if cursors get stuck, Settings → Accessibility → Mouse pointer, or sign out") would save some support questions.


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 24, 2026
@fizixes

fizixes commented Aug 24, 2026

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 24, 2026
@SoftwareExplorer6600

Copy link
Copy Markdown

Hey @fizixes is there a way we can implement the mica or mica alt effect under the cursor, that's one thing I wish we could do, maybe you can help me figure it out in this mod.

@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-reviewer Ready for a human reviewer, and in the queue for one. labels Aug 25, 2026
@windhawk-reviewer

Copy link
Copy Markdown

New commits were pushed, so this pull request left the human review queue and is back to waiting-for-author.

Comment /ai-review to get an AI review of the updated code, then /ready-for-reviewer to hand it over to a human reviewer again. See the pull request review process for details.

@fizixes

fizixes commented Aug 25, 2026

Copy link
Copy Markdown
Author

I noticed a bug in 0.20.0: does not work with this mod: macOS magnifying cursor or if you search by flag its mac-magnifying-cursor, created by Jaali, however they don't immediately conflict, the glass cursor plays the animation as it does scale up on rapid shaking movement, the cursor is there, until the last second where it starts shrinking back to normal (noticed this on DPI aware, not sure if it applies to fixed pixel sizes too), both mods were on their default settings. (note, may affect earlier variations as well, 0.20.0 is confirmed to be affected)

edit: may want to independently test and confirm windhawk 2.0 alpha support, while i use 1.7.3 on my main, in my VM i have a slightly modified version of this mod that I made changes to work with windhawk 2.0's alpha 3, it complained about lines 462 - 474, which that version of this mod i edited for my VM has omitted and it still works as intended, if you want i could send it over at your repo for this mod so you can review the modified version there (to be clear my host uses 0.20.0 on windhawk 1.7.3 or current release)

As it turns out, it's insanely computationally cheap, so I've added Mica, Acryllic, and Mica alt. It should work for 2.0 alpha 3 now, and also with the macOS magnifying cursor now, it retreats when another program wants to change the cursor. awaiting AI review now.

@fizixes

fizixes commented Aug 25, 2026

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 25, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Something went wrong while preparing the AI review, so there is nothing to post this time. This is a problem on the reviewer's side, not with this pull request.

Comment /ai-review to try again.

@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 25, 2026
@fizixes

fizixes commented Aug 25, 2026

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 25, 2026
@windhawk-reviewer

Copy link
Copy Markdown

This pull request has already had 3 AI reviews in the last 24 hours, which is the limit, so no review was posted this time.

Comment /ai-review again after 2026-08-25 18:30 UTC (in 24 minutes) to get another one.

@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 25, 2026
@m417z m417z 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 25, 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 lifecycle work from the previous rounds is holding up well: the worker thread owns all rendering and all global state, teardown joins unconditionally and destroys the window on its owning thread, the class registration no longer tolerates ERROR_CLASS_ALREADY_EXISTS, the exception filter is chained and restored, the tool-mod boilerplate is still a verbatim copy of the wiki snippet, and the settings block and the code agree in both directions. No global has a destructor that does anything but free heap, so nothing there needs [[clang::no_destroy]]. The new sampled-backdrop feature is the interesting part, and it brings the remaining issues.

1. The tool process never makes itself DPI-aware, so the backdrop is sampled from the wrong screen pixels on scaled displays. The worker thread reads screen coordinates from GetCursorInfo().ptScreenPos, the virtual-desktop bounds from SM_XVIRTUALSCREEN/SM_CXVIRTUALSCREEN (L2151-L2165), and then BitBlts out of GetDC(nullptr) at those coordinates (L2380) — all of which are answered in the calling thread's DPI context. windhawk.exe is not per-monitor-aware, which is exactly why every other tool mod that touches screen coordinates opts in explicitly: proportional-monitor-cursor ("so that all coordinates we deal with are real physical pixels"), cursor-motion-blur, and mac-magnifying-cursor.

Without it, on any non-100% display the screen DC is virtualized: the capture is read back from a rescaled copy of the desktop (soft/blurry material), and on a second monitor with a different scale factor the virtualization is anchored to the primary's scale, so the mod samples a region that isn't under the pointer at all. ResolveCursorSize()'s GetSystemMetrics(SM_CXCURSOR) (L2018) is virtualized the same way, so Automatic can pick 32 where the user's actual cursor metric is 48.

One call at the top of AnimationThreadProc, before anything queries a metric:

// DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 — all coordinates and metrics
// below must be real physical pixels.
if (HMODULE user32 = GetModuleHandleW(L"user32.dll")) {
    using SetThreadDpiAwarenessContext_t = HANDLE(WINAPI*)(HANDLE);
    if (auto setContext = (SetThreadDpiAwarenessContext_t)GetProcAddress(
            user32, "SetThreadDpiAwarenessContext")) {
        setContext((HANDLE)-4);
    }
}

Worth verifying side by side on a 150% display and on a mixed-DPI pair before and after — this is the kind of thing that looks fine on a single 100% monitor and is visibly wrong everywhere else.

2. The sampled styles run a screen readback and a session-global SetSystemCursor at the monitor's full refresh rate, and never fully idle. kMaxDynamicRefreshRate is 500 (L302), so on a 144/240 Hz display the worker wakes 144-240×/s (L3352-L3354) and, for every pixel of pointer movement (L2432), performs: BitBlt(... SRCCOPY | CAPTUREBLT) off the screen DC → 4 box-blur passes → a per-pixel material pass → CreateIconIndirectSetSystemCursorLoadImageW. At 96 px that's a 144×144 GPU→CPU readback plus ~110k pixel updates, 240 times a second, and SetSystemCursor is not a local operation — it replaces a cursor object that every process in the session references. Even parked on a completely static desktop the backoff only reaches 66 ms (L2205-L2215), so the mod never drops below ~15 captures/s for the life of the session, while the loop itself keeps spinning at refresh cadence. Four concrete changes:

  • Cap the sampled-material rate at ~60 Hz. The cursor is a ≤96 px bitmap that lags the pointer by a frame regardless; there is nothing above 60 Hz to see, and the cost is linear in the rate. kMaxDynamicRefreshRate = 60 would remove most of this on its own.
  • Let the stationary path reach the idle interval. Extend the backoff ladder past 66 ms up to kIdlePollInterval (350 ms) when nothing has changed for a few seconds — pointer movement is what needs low latency, and that resets the counter already.
  • Don't re-apply when the produced bitmap can't have changed. DynamicBackdropChanged short-circuits to true on any movement because Acrylic's noise is screen-space — but Mica and Mica Alt have noiseStrength == 0.00 (L2100-L2102), so moving across a flat-colored window produces a byte-identical cursor and the SetSystemCursor is pure waste. Gate that short-circuit on GetDynamicMaterialProfile().noiseStrength > 0.0 and let the pixel comparison decide otherwise.
  • Test whether CAPTUREBLT is needed. It is the expensive path (it forces layered content to be composited into the capture and is known to cause visible flicker in some configurations); under DWM the screen DC already contains layered windows, so plain SRCCOPY may give you the same image far more cheaply.

Also worth measuring before this merges: leave the mod running with the pointer moving for an hour and watch the USER/GDI object counts for windhawk.exe and the session. Each dynamic frame creates a cursor object and hands it to SetSystemCursor; at hundreds per second, any per-call retention that is invisible at 30 Hz during a busy spinner becomes a session-wide handle exhaustion problem.

3. IsForegroundWindowFullscreen() classifies any ordinary maximized window as fullscreen on common setups, which quietly disables cursor re-apply and self-healing. (L2716-L2755) The test is purely GetWindowRect(foreground) >= rcMonitor ± 2px. But the work area equals the monitor rect whenever the taskbar is auto-hidden, and on secondary monitors when "Show taskbar on all displays" is off — so a normal maximized Explorer or browser window passes. ShouldDeferCursorReapply() then returns true indefinitely, which means SPI_SETCURSORS, WM_THEMECHANGED and WM_DISPLAYCHANGE re-applies are deferred and the 350 ms slot-integrity check is skipped for as long as that window stays maximized. From the user's side: apply a theme with a maximized window on an auto-hide-taskbar setup and the glass cursors just disappear until something unmaximizes.

cursor-motion-blur needs the same decision and shows the shape: SHQueryUserNotificationState() == QUNS_RUNNING_D3D_FULL_SCREEN, or the rect test corroborated by a clipped cursor (GetClipCursor smaller than the virtual screen) or a hidden cursor. It also caches the result for 500 ms — worth copying, since you currently evaluate it twice per loop iteration at refresh cadence.

4. The README still has no images. (L13-L45) This is the fourth round and the fourth time asking, so I'll keep it short: the mod is entirely artwork — four pointer silhouettes, two hand designs, four material styles, six artwork scales and an animated spinner — and someone browsing windhawk.net has no way to see any of it, or to choose between sharpRounded / cleanRounded / symmetricSharp / symmetricCurved, or to tell Acrylic from Mica from Mica Alt, without installing the mod and cycling through every combination. Please add at least a shot of the full cursor set, and ideally a strip comparing the four pointer styles and the four materials plus a GIF of the spinner. Only i.imgur.com and raw.githubusercontent.com are allowed as image hosts.

Optional improvements

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

  • The loop queries the whole world twice per iteration. (L3305-L3340 and L3383-L3416) The pre-wait block does magnifier discovery + GetCursorInfo + IsForegroundWindowFullscreen purely to pick delay, then the post-wait block does all three again. Computing the next delay at the end of the iteration from the state you already gathered would halve it.

  • The magnifier interop is coupled to another mod's internal window class name. (L2907-L2913) L"WindhawkShakeCursorExclusiveOverlay" is an implementation detail of mac-magnifying-cursor, not an interface, and it's polled every 500 ms forever. The underlying problem is more general: anything else that calls SetSystemCursor will ping-pong with the 350 ms NormalCursorSlotChanged() self-heal, which re-applies 16 global cursor slots each time. A generic rule — if the normal slot changes to a handle we didn't install, back off for a few seconds instead of immediately re-applying — would cover the magnifier and whatever comes next, and you could keep the class-name check as a fast path.

  • CaptureDynamicBackdrop is a pass-through wrapper. (L2410-L2413) It only forwards to CaptureLiveBackdrop with the same arguments, and has exactly one caller. Worth collapsing unless a second capture backend is planned.

  • BlurStrength and BlurScale are inert for the Clear style, and multiply into one number for the others. ResolveDynamicBlurRadius is only reached from the dynamic path, so with GlassStyle: clear both settings do nothing and the descriptions don't say so. Either mention it in the $descriptions or fold them into a single radius setting.

  • The window class is registered with GetModuleHandleW(nullptr). (L3250-L3256) That's windhawk.exe's HINSTANCE, but lpfnWndProc lives in the mod image. It's harmless here because the tool process ExitProcesses on unload and the same handle is used to unregister — but the mod's own module handle is the correct one, and it's the habit you want if this ever runs somewhere that doesn't exit.

  • <climits> is unused. (L131) No *_MAX/*_MIN constant appears anywhere in the file.

Functionality notes

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

  • Anything the OS won't hand to a GDI screen capture comes back black. DRM/protected surfaces and windows using SetWindowDisplayAffinity (PlayReady video in Edge, some banking and password-manager windows) blit as black, and hardware-overlay video can too. Since ApplyDynamicGlassBackdrop forces the glass area opaque (L2693-L2694), the pointer becomes a black blob over that content rather than degrading to translucent glass. A sanity check on the captured tile — if it's uniformly black and the previous one wasn't, fall back to the Clear material for that frame — would keep the pointer readable.

  • Watch for self-capture feedback when Windows composites the cursor into the desktop. Normally the pointer is a hardware overlay and isn't in the screen DC, but Windows falls back to a software cursor in several situations (Magnifier, Accessibility color filters, some remote/mirrored sessions). If the capture picks up the previously installed glass cursor, each frame feeds its own output back in and the material will visibly smear. Worth testing with Color filters enabled specifically, since that's a one-toggle repro.

  • The backdrop is always one frame stale, by construction. The capture happens at the position the pointer had when the tick ran, and the OS then draws that bitmap wherever the pointer is now. During fast movement the opaque glass interior shows content from where the pointer was, which reads as a small frozen screenshot dragging along. There's no way around it with SetSystemCursor, but it's the main thing that will make the effect feel different from real Acrylic, and it argues for tuning toward the more tint-dominant Mica profiles by default.

  • Capping only on battery saver leaves laptops on battery at full rate. RefreshMaterialEnvironment checks SystemStatusFlag (L2141-L2143), so a laptop on battery with battery saver off still runs continuous screen readbacks at the panel's refresh rate. power.ACLineStatus == 0 is the broader signal.

  • OCR_NWPEN (32631, "Handwriting") is still not replaced. It's part of the standard scheme alongside the 16 roles you handle, so it shows up as the stock pen in the middle of an otherwise glass set. Raised last round; repeating only because it's a one-line addition if you want full coverage.


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 25, 2026
@fizixes

fizixes commented Aug 25, 2026

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 25, 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.


Good shape overall: the tool-mod boilerplate is a verbatim copy of the wiki snippet, @include windhawk.exe is the right scope, every declared setting is read with a matching API (no dead or undeclared settings), teardown signals the worker, joins it, destroys the window, unregisters the class and restores the user's cursor scheme with SPI_SETCURSORS (no persistent registry writes), and there's no meaningful overlap with the existing cursor mods (mac-magnifying-cursor hides cursors for a magnifier overlay, cursor-motion-blur draws a D2D trail overlay). The items below are mostly about cost and DPI.

1. The README has no screenshot or GIF.

The entire mod is artwork — four glass materials, four pointer silhouettes, two hand styles, six artwork sizes, an animated spinner — and there is currently no image at all, so a user browsing the catalog has no way to know what they'd be installing. Please add at least one image of the cursor set, and ideally a GIF of the animated busy/working spinner. Allowed image hosts are i.imgur.com and raw.githubusercontent.com; cursor-motion-blur.wh.cpp is a good example of a cursor mod with a demo GIF.

2. The sampled styles run a permanent monitor-refresh-rate loop, and they're the default.

GlassStyle defaults to acrylic, so out of the box AnimationThreadProc's wait interval comes from ResolveDynamicCaptureInterval() — the monitor's refresh rate, clamped to 24–500 Hz — for as long as the cursor is visible, including when the pointer is completely stationary and nothing on screen is changing. The comment above the clamp declines to back off on purpose, but the stationary backoff (ResolveStationaryBackdropCaptureInterval) only throttles the capture, not the wakeup. On a 165 Hz panel that's 165 wakeups/second, forever, each running IsForegroundWindowFullscreen() (GetForegroundWindowGetAncestorGetWindowRectMonitorFromWindowGetMonitorInfoW), GetCursorInfo, MonitorFromPoint and RefreshMaterialEnvironment. A permanent sub-16 ms timer keeps the CPU out of deep idle states, which is a measurable battery cost on laptops.

While the pointer moves it's a lot heavier: DynamicBackdropChanged returns true on any movement, so every frame does a BitBlt(..., SRCCOPY | CAPTUREBLT) off the screen DC, four box-blur passes, a CreateIconIndirect, and a SetSystemCursor. SetSystemCursor replaces a global slot for the whole desktop, so every process in the session sees the cursor object swapped 165×/second — and in a remote session (RDP/Parsec/VNC) each swap re-transmits the cursor shape to the client.

Concrete asks:

  • Default GlassStyle to clear, so the continuous screen-capture path is opt-in rather than what every user gets on install.
  • Cap the sampled update rate to roughly 30–60 Hz. A 32–48 px cursor over a backdrop that's already box-blurred won't visibly benefit from more, and it cuts the cost 3–5× on high-refresh monitors.
  • Back the wake cadence off when the pointer is stationary (e.g. clamp delay to ≥16 ms, or a plain 60 Hz GetCursorInfo poll) instead of running at panel refresh rate indefinitely — 60 Hz is plenty to notice that movement has started.

3. The worker thread never opts into per-monitor DPI awareness.

The mod's @description and README both lead with "DPI-aware", but AnimationThreadProc never calls SetThreadDpiAwarenessContext, so unless windhawk.exe already declares PerMonitorV2 (worth verifying), everything the mod measures goes through DPI virtualization on a scaled display:

  • ResolveCursorSize()'s Automatic branch reads GetSystemMetrics(SM_CXCURSOR), which returns the 96-DPI metric — so it picks a 32 px canvas on a 150% display where Windows is actually drawing 48 px cursors. That's exactly the case Automatic exists to handle.
  • The capture path mixes GetCursorInfo().ptScreenPos, SM_XVIRTUALSCREEN/SM_CXVIRTUALSCREEN and a GetDC(nullptr) BitBlt — in a virtualized thread the screen DC is a stretched copy, so on a mixed-DPI multi-monitor setup the sampled backdrop ends up misaligned and softened relative to where the cursor actually is.

Fix is one line at the top of the worker thread, as cursor-motion-blur.wh.cpp#L405 does:

DWORD WINAPI AnimationThreadProc(LPVOID) {
    SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
    ...

bt-battery-monitor.wh.cpp#L1676-L1679 does the same via GetProcAddress if you'd rather not hard-link it.

Optional improvements

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

  • Cached foreign HWND + hard-coded cross-mod class name. FindMacMagnifyingCursorWindow() hard-codes L"WindhawkShakeCursorExclusiveOverlay" — another mod's internal window class, which can be renamed at any time without warning — and the main loop then caches that HWND and re-validates it with IsWindow(). IsWindow is documented as unsafe for windows you didn't create: handles are recycled, so if the overlay is destroyed and an unrelated visible window inherits the handle value, IsMacMagnifyingCursorActive returns true and the mod pauses its cursor updates indefinitely. Cheapest fix is to re-run FindWindowW each time (you already re-discover every 500 ms) rather than trusting the cached handle, or to confirm the class with GetClassNameW before acting on it.
  • Unused #include <climits> — no INT_MAX/LONG_MAX/etc. appears in the file.
  • CaptureDynamicBackdrop is a one-line passthrough to CaptureLiveBackdrop with an identical signature. Either drop the wrapper or fold the two together.

Functionality notes

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

  • Rasterization cost on load and on artwork changes. RebuildCursorSet renders 14 static roles plus 48 spinner frames at 4× supersample, and ResolveRoleScaleNormalization renders a second full-canvas probe per role, roughly doubling it. FillGlassPolygon runs PointInPolygon and DistanceToPolygonEdge per pixel, each O(N) over a path that AppendCubic can subdivide up to 64 steps per curve — so at CursorSize: 96 (384² canvas) that's tens of millions of hypot calls per role. It's on a worker thread in a dedicated process so nothing hangs, but it's seconds of single-core CPU on load and again on every artwork-affecting settings change, and it's what bounds how long Wh_ModUninit blocks waiting for the thread. Two easy wins if you want them: derive the final artwork from the probe surface instead of rendering twice, and precompute a scanline edge table so the per-pixel inside/edge-distance test isn't O(path length).
  • Abnormal termination leaves the glass cursors installed. WhTool_ModUninit and the RestoreCursorsOnCrash filter cover the normal-unload and unhandled-exception paths, but a TerminateProcess (Task Manager, a hard service restart) skips both and the system cursor slots stay replaced until the user re-applies a scheme. There's no clean way to guard against that from user mode — mac-magnifying-cursor has the same exposure — but it'd be worth a line in the README telling users the recovery step (Settings → Mouse → re-apply the cursor scheme).
  • High contrast silently degrades sampled styles to a flat fill. When DynamicMaterialsAllowed() returns false, the sampled base raster (EvaluateGlassMaterial's early return for g_glassStyle != 0) is a flat tint at FillOpacity and nothing ever replaces it, so Acrylic/Mica look like plain translucent blocks rather than falling back to Clear's shaded treatment. Falling back to the Clear profile when materials are disallowed would look better; at minimum it's worth documenting.
  • IDC_PIN (32671) and IDC_PERSON (32672) aren't documented OCR_* values for SetSystemCursor. Failures are logged and skipped so nothing breaks, but worth confirming those two slots actually get replaced on your target builds rather than silently no-op'ing.
  • IsForegroundWindowFullscreen() classifies any monitor-covering foreground window as fullscreen, including an ordinary maximized window on a monitor with an auto-hidden taskbar. While that's true the mod suppresses both the deferred reapply and the cursor-slot self-heal, so a user who spends most of their time in such a window gets less self-healing than intended. Checking for the absence of WS_THICKFRAME/WS_CAPTION, or comparing against rcWork as well, would narrow 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 25, 2026
@SoftwareExplorer6600

SoftwareExplorer6600 commented Aug 26, 2026

Copy link
Copy Markdown

I will give a bit of feedback, I like the effects, they were implemented very closely with the original design of the effects, the last big toggle I think would be useful is maybe accent color for the loading wheel (you know the part thats visibly spinning) this should be for the working (loading wheel with cursor) and busy (just loading wheel) states. otherwise its very well implemented.

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.

3 participants