Skip to content

Add mouse-trail mod - #5442

Open
MCheng404 wants to merge 61 commits into
ramensoftware:mainfrom
MCheng404:mouse-trail-v3.3
Open

Add mouse-trail mod#5442
MCheng404 wants to merge 61 commits into
ramensoftware:mainfrom
MCheng404:mouse-trail-v3.3

Conversation

@MCheng404

@MCheng404 MCheng404 commented Sep 10, 2026

Copy link
Copy Markdown

A highly customizable mouse cursor trail with particle effects, multiple color modes, custom function trails, cursor color extraction, click effects, centripetal vortex, and particle physics system. Built on native D3D11 + HLSL + DirectComposition hardware acceleration, runs as a dedicated process with low CPU usage when idle.

Version: v3.3.1

Demo

Tapered Ribbon Trail

Tapered Trail

Shape Trail (Hearts / Stars / Hexagrams / Circles)

Shape Trail

Particle Effects

Particles

Click Effects (Starburst + Ripple)

Click Effects

Combined Effects (Gradient + Particles + Glow)

Combined Effects

Visual Effects

  • 10 trail shapes: Tapered ribbon, Dot Chain, Function Curve, Wave Curve, Shape Trail, Double Line, Dashed, Spiral, Lightning, Feather
  • 23 color modes: Single, Gradient (flowing, any number of colors), Rainbow, Warm, Cool, Neon, Velocity-reactive, Stripes, Fire, Aurora, Cursor Color Extract, Cursor Color Mix, Metallic, Cyberpunk, Pastel, Hue Rotate, Dual Pulse, Sparkle, Thermal Heatmap, Wave Interference, Spectrum Split, Grain Dither, Gradient Warp
  • 9 particle shapes with self-rotation and inter-particle repulsion: Circle, Star, Hexagram, Heart, Diamond, Triangle, Flower, Pentagon, Hexagon
  • Bezier smoothing (Catmull-Rom spline) for buttery-smooth curves
  • Motion blur via history frame overlay
  • Multi-layer glow with additive blending (trail/particles/ripples)
  • Head highlight focused at the cursor position
  • Trail shadow for depth perception
  • Speed-reactive width: trail widens when moving fast
  • Adaptive contrast: auto-brightens on dark backgrounds and darkens on bright backgrounds for always-visible trails
  • Click effects: starburst particle burst + expanding ripple ring
  • Particle fast-move interpolation: eliminates gaps when moving quickly (interval ≤10ms)
  • Centripetal vortex: curved mouse motion captures particles into orbiting vortex with 3D tilt and z-depth occlusion
  • Particle mass system: random mass affects size (cube-root), lifetime, inertia, and acceleration (a=F/m)
  • Newtonian particle gravity: F=G·m₁·m₂/r², a=F/m, with binary star / N-body system modes

v3.3.1 Changelog

Display Optimization

  • Additive blending for all glow layers (SrcAlpha + One)
  • Particle interpolation for fast cursor movement (size/quantity/lifetime/speed gradients)
  • Trigger/stop logic: 2-frame confirm, actual distance-based stop detection
  • Hard cut fadeout: clears all state to prevent residual rendering
  • Gradient system rewrite: any number of colors (max 16), OKLab perceptual interpolation, 256-color precomputed LUT, flowing animation
  • 5 new color modes with velocity-responsive algorithms
  • All 23 color modes optimized with multi-frequency dynamics and enhanced head-tail contrast
  • Bilingual (Chinese/English) code comments

Centripetal Vortex (Entertainment Feature)

  • Curvature-based detection: any curved motion triggers the vortex
  • Physical orbital model: angular momentum conservation, Kepler velocity gradient, spiral convergence
  • 3D orbit tilt with z-depth association and front-back occlusion sorting
  • Particles fly out centrifugally when motion stops

Particle Physics Mass System

  • Random particle mass (configurable range, default 0.5–2.0)
  • Mass affects: size (∝mass^(1/3)), lifetime, friction/inertia, acceleration (a=F/m)
  • Newtonian gravity between particles: F=G·m₁·m₂/r², a=F/m
  • Binary star / N-body system modes (2–10 massive bodies)
  • All features with independent toggles

Rendering Architecture

  • Native D3D11 + HLSL: all trail geometry rendered as vertex buffers with custom vertex/pixel shaders — no D2D1 intermediate paths
  • GPU gradient interpolation: gradient stops uploaded to a constant buffer, sampled per-pixel in the pixel shader with smooth or stepped interpolation
  • Instanced particle rendering: all particles drawn in a single DrawInstanced call with per-instance position, color, size, shape, and rotation
  • Signed distance field (SDF) particle shapes: all 9 shapes rendered with soft anti-aliased edges in one pixel shader, no per-shape geometry
  • Cylindrical lighting on trail ribbons via per-vertex v-coordinate (tube shading + soft edge fade)
  • Radial lighting on particles for volumetric sphere appearance
  • DirectComposition overlay with DXGI flip swap chain and premultiplied alpha for tear-free composition
  • Dual-thread design: UI thread handles window/message pump, render thread handles all D3D11/DComp work — mouse input never blocks
  • Device loss recovery: auto-rebuilds entire D3D/DComp stack on GPU TDR, driver update, or GPU switch
  • Display change handling: auto-resizes and repositions the overlay on WM_DISPLAYCHANGE
  • Background sampling thread: cursor color extraction and adaptive contrast sampling run on a separate low-priority thread to avoid render-thread stalls

Performance

  • Single Map per frame: all 7 trail layers (shadow, adaptive edge, 3 glow layers, body, head highlight) written into one vertex buffer with a single Map/Unmap
  • Particles: glow and normal layers merged into one instance buffer Map, single traversal of the particle list
  • Precomputed shape geometry: heart/star/hexagon/circle vertices cached statically, no per-frame trigonometry
  • Adaptive backoff: render thread sleeps 1ms when active, 16ms when idle
  • Vsync pacing: Present(1, 0) + DwmFlush() for consistent frame timing across refresh rates

Credits

Developed by MCheng404. Inspired by and based on the core overlay/smear architecture of Cursor Motion Blur by TheatriChris (MIT License).

Mod authorship

This mod was created by:

  • The submitter, with AI assistance
  • The submitter, without AI assistance
  • Claude
  • ChatGPT
  • Gemini
  • Another AI (please specify): Doubao
  • Other (please specify):

…very, display change handling, idle backoff, .gitattributes for binary assets
…st (not in upstream Windhawk), add upper bounds for tail_length/click_max_radius/click_duration
…ion reset, cursor color extraction excludes overlay, cached system metrics, refreshed game detection handles, code cleanup
…c cross-thread, hidden window init, vsync sync with DwmFlush, honest derivative credit in PR
…skip click effects in game, clear motion blur history on device loss
…remove SWP_SHOWWINDOW on display change, add WARP fallback in device recovery
…, 18 color modes, 10 trail shapes, 2.5D depth effects, optimized cursor color shift (6 modes), Bezier smoothing, motion blur
@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 Sep 10, 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.

@MCheng404

Copy link
Copy Markdown
Author

/ready-for-reviewer

@windhawk-reviewer

Copy link
Copy Markdown

@MCheng404 /ready-for-reviewer can't be applied here: this pull request hasn't been through an AI review yet. Comment /ai-review first.

See the pull request review process for details.

@MCheng404

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 Sep 10, 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 headline feature of v3 — the native D3D11/HLSL renderer — never executes: the shaders fail to compile at startup, so every frame silently falls back to the D2D1 path. That makes most of the ~700 lines added in v3.0–v3.3 dead code, and it hides several further bugs in that code. Please decide which renderer is canonical and re-test end to end before the next round.

1. The native D3D11 renderer never runs — shader entry-point mismatch. CompileShader hardcodes "main" as the D3DCompile entry point (line 1462), but every shader declares VSMain / PSMain (lines 1315, 1350, 1395, 1420). D3DCompile fails with "entrypoint not found", so InitNativeRendering() returns false on its very first call, g_pNativeVS stays nullptr, and bool useNative = g_pNativeVS != nullptr; (line 3438) is always false. Everything the README and PR description advertise as "native D3D11 + HLSL + instanced particles + 2.5D depth + GPU gradient interpolation" is unreachable — what actually runs is the D2D1 path.

static bool CompileShader(const char* source, const char* entry, const char* target, ID3DBlob** blob) {
    ...
    HRESULT hr = D3DCompile(source, strlen(source), nullptr, nullptr, nullptr, entry, target, ...);
}
...
if (!CompileShader(g_vsShader, "VSMain", "vs_4_0", &vsBlob)) return false;
if (!CompileShader(g_psShader, "PSMain", "ps_4_0", &psBlob)) return false;

Note that simply fixing this will regress visible behaviour, because NativeRenderFrame implements a strict subset of the D2D1 path. Turning the native path on drops: micro glow and enhanced glow (enable_glow, glow_intensity, enhanced_glow), trail shadow (enable_trail_shadow), head highlight (enable_head_highlight), the dot-chain's per-dot jitter and inner dot, shape-trail birth animation, the ripple's filled disc and inner ring, and the star/hexagram particle shapes (g_particlePS discards everything outside a circle, so particle_shape becomes a no-op). Items 2–6 below are additional bugs in that same path.

Given that the native renderer has clearly never executed on any machine, the lowest-risk option is to delete it (InitNativeRendering/ReleaseNativeRendering, the four HLSL strings, NativeRender*, UpdateConstantBuffer, the vertex/instance structs), drop -ld3dcompiler from @compilerOptions, and rewrite the README/PR text to describe the D2D1 renderer that actually ships. If you'd rather keep it, it needs to reach feature parity and be tested with useNative genuinely true.

2. The pixel shader's constant buffer is never bound. Every call site does VSSetConstantBuffers(0, 1, &g_pConstantBuffer) (lines 1650, 1724, 1868, 1988, 2038) and there is no PSSetConstantBuffers anywhere in the file. g_psShader reads gradient[] and gradientCount from b0 in the pixel stage, so it would see an unbound buffer (zeros) → gradientCount == 0gradColor = input.color. The trail vertices are all pushed with colour (1, 1, 1, fadeAlpha) (lines 1707–1710), so the entire trail would render white in every colour mode. Add g_pD3DContext->PSSetConstantBuffers(0, 1, &g_pConstantBuffer); alongside each VSSetConstantBuffers.

3. gradientCount is written as a float into an int field. data[72] = (float)gradCount; (line 1609) stores the bit pattern of 12.0f (0x41400000) where HLSL declares int gradientCount. The shader would read 1094713344, so float t = input.u * (gradientCount - 1) explodes and gradient[gradientCount - 1] indexes far out of bounds. Either write the integer bits (((int*)data)[72] = gradCount;) or declare the field as float in HLSL and compare accordingly.

4. The constant buffer is undersized and lightDir is written at the wrong offset. With HLSL packing rules, float2 lightDir cannot straddle the c0 boundary, so it lands at byte 16; float4 gradient[16] starts at 32, int gradientCount at 288, and float pad[3] occupies 304–352. The buffer is created with cbDesc.ByteWidth = 320 (line 1512), i.e. 32 bytes short of what the declaration requires. The CPU side writes lightDir at data[3]/data[4] (bytes 12/16), so lightDir.y reads uninitialised WRITE_DISCARD memory. lightDir and pad are unused by both shaders — the cleanest fix is to remove them from the cbuffer and size the buffer to what's actually used.

5. NativeRenderRipples ignores the virtual-screen origin. It takes int vX, int vY but never uses them: verts.push_back({r.pos.x + cosA * (radius + ringWidth), r.pos.y + sinA * (radius + ringWidth), ...}) (lines 2018–2022). g_ripples stores absolute cursor coordinates (g_ripples.push_back({pt, dwTime}), line 3118) while the overlay surface is virtual-screen-relative — the D2D1 path correctly subtracts them at line 3040. On any layout where the virtual-screen origin isn't (0,0) (a second monitor placed left of or above the primary), click ripples would draw at the wrong position, off by the full monitor offset.

6. NativeRenderRipples can overrun the vertex buffer, and joins all rings into one strip. g_pTrailVB is sized for 4096 VertexPosColor (line 1520). Each ripple emits (48 + 1) * 2 = 98 vertices and the total is never clamped before memcpy(mapped.pData, verts.data(), verts.size() * sizeof(VertexPosColor)) (line 2030), so more than 41 live ripples writes past the mapped buffer. With click_duration configurable up to 3000 ms that's reachable by a burst of clicks. Clamp it the way NativeRenderTrailShapes does (if (verts.size() > 4096) verts.resize(4096);, line 1975) — better, clamp the ripple count so you never emit a partial ring. Separately, all rings are drawn with a single TRIANGLESTRIP Draw() call, so consecutive ripples are connected by stray triangles; issue one draw per ring, or separate them with degenerate vertices.

7. Three of the ten advertised trail shapes do nothing. double (5), dashed (6) and feather (9) appear only in the per-vertex depth block of NativeRenderTrail (lines 1690–1705) — dead code today, and even when live it only varies a depth scalar, not the geometry. Worse, 5 and 6 use exactly the same depth expression as tapered (0). In the D2D1 path that actually runs, all three fall through to the generic ribbon branch (line 3596), so Double Line, Dashed and Feather render identically to Tapered Ribbon. Please either implement them or remove them from $options, the README and the PR description.

8. Roughly 150 lines of never-called shape-rendering code. ShapeInstance, g_pShapeVB, g_pShapeInstanceBuf, GenerateShapeVerts, InitShapeRendering, ReleaseShapeRendering and NativeRenderShapes (lines 1738–1898) have no callers anywhere — NativeRenderTrailShapes is the function actually wired up. NativeRenderShapes also still carries // TODO: 优化为按类型分组的实例缓冲 and, if it ever ran, would draw every instance once per shape type (DrawInstanced(counts[shapeType], count, ...) passes the full count in all four iterations). Please delete the whole block.

9. SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE) hides the trail from all screen capture. It's applied unconditionally at window creation (line 3935), so the trail never appears in screenshots, OBS recordings, or Teams/Zoom screen shares. That's a surprising, undocumented loss for a purely visual mod, and it's only needed by the two cursor-sampling colour modes. Scope it to those modes (set WDA_EXCLUDEFROMCAPTURE when color_mode is cursor_extract/cursor_mix, WDA_NONE otherwise), or make it an explicit setting and document it in the README. (It also requires Windows 10 2004+; the call silently fails on older builds, which quietly breaks the self-sampling protection there.)

10. Substantial overlap with the existing Cursor Motion Blur mod. This isn't just a shared theme — the architecture is recognisably the same mod: same @include windhawk.exe tool-mod shape, same std::deque<POINT> g_history + tapered-ribbon smear, the same five setting keys with the same defaults (trigger_velocity: 25, stop_velocity: 10, tail_length: 10, tail_offset_x: 6, tail_offset_y: 10), and a CheckForegroundFullscreen that reproduces cursor-motion-blur's Progman/WorkerW + GetClipCursor + GetCursorInfo(flags == 0) heuristic (lines 102–136). With trail_shape: tapered and effects off, this mod is a functional superset of that one. Windhawk's maintainer strongly prefers extending an existing mod over merging a near-duplicate, so please make the case explicitly: either propose these features as options on cursor-motion-blur / open a PR against TheatriChris's repo, or explain in the PR why a separate mod is the right call. The MIT-in/MIT-out licensing and the README credit are both correct — this is a catalog-scope question, not an attribution one.

11. The README has no screenshot or GIF. The demo GIFs added in 639a24b were removed again in 622042 / 07077ad, so the published README for an entirely visual mod shows nothing. Please add at least one demo GIF; allowed hosts are i.imgur.com and raw.githubusercontent.com (a branch-pinned raw.githubusercontent.com URL is fine — Windhawk mirrors it after merge). cursor-motion-blur is a good reference for what's expected.

Optional improvements

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

  • Two settings control the same thing. enable_cursor_color_shift (bool) and color_shift_mode (which already has an off option) both gate hue shifting, and they behave subtly differently: enable_cursor_color_shift: false skips ShiftToComplementary entirely, while color_shift_mode: off still applies the saturation/brightness floors (g_colorShiftSatBoost / g_colorShiftValBoost). Drop the boolean and let off be the off state.

  • The tool-mod boilerplate is reformatted rather than copied verbatim. Lines 4148–4287 are semantically identical to the wiki snippet, but re-wrapped to a wider column and with an extra Wh_Log(L"Tool mod host: %s", currentProcessPath); added. Keeping it byte-identical (as explorer-folder-hover-menu does) makes it trivial to diff when the snippet is updated.

  • ShiftToComplementary duplicates RGBtoHSV. v3.3 added RGBtoHSV (line 640), but ShiftToComplementary still inlines the identical conversion at lines 2594–2609. Call the helper. The name is also stale now that the function handles six shift modes — ShiftHue or similar would read better.

  • Unsynchronised access to the virtual-screen cache. UpdateVirtualScreenCache() writes g_virtX/Y/W/H from the UI thread inside the WM_DISPLAYCHANGE handler (line 3895) while RenderFrame reads them from the render thread (lines 3098, 3137). It's a benign-looking data race on aligned ints, but the render thread can observe a half-updated origin/size pair for a frame. Making them std::atomic<int> (or snapshotting all four under a small lock) removes the ambiguity.

  • PostMessage(g_overlayHwnd, WM_QUIT, 0, 0) (line 4129) — WM_QUIT isn't a window message. It happens to work because GetMessage returns FALSE when it retrieves it, but PostThreadMessage(threadId, WM_QUIT, 0, 0) (or posting WM_CLOSE and calling PostQuitMessage from the wndproc) is the documented way.

  • The overlay thread waits only 5 s for the render thread (line 3961), then closes the handle, destroys the window and unregisters the class while that thread may still be touching g_overlayHwnd and its D3D resources. It's harmless today only because Wh_ModUninit calls ExitProcess(0) immediately after. The render loop's exit is already bounded (the recovery path re-checks g_renderExitEvent on every iteration), so INFINITE would be both safer and simpler.

  • RegisterClass(&wc) return value is ignored (line 3919). Not a problem in a fresh dedicated process, but checking it would turn a silent CreateWindowEx failure into a useful log line.

  • #include <cstdint>uint32_t is used in Hash01 (line 2144) but only reaches the file transitively through <windows.h>.

  • g_virtH -= 1 (line 3890) silently clips the bottom row of the virtual screen. If the DWM composition-boundary issue it works around is still real, a short comment explaining the symptom would help; if it was only needed for the old UpdateLayeredWindow path, it can probably go.

  • Version numbering. The mod jumped 2.2 → 2.3 → 3.3 within this unmerged PR. For a first publication, a single starting version (1.0) is conventional and avoids implying a release history that users never saw.

Functionality notes

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

  • Present(1, 0) followed by DwmFlush() waits for two vsyncs per frame. Present(1, 0) on a composition swap chain already blocks until the next vertical blank (lines 3445, 3719); the extra DwmFlush() in the render loop (line 4074) adds a second wait, so the trail effectively runs at ~30 fps on a 60 Hz display — the opposite of the intended smoothness. Drop one of the two (keeping Present(1, 0) is the simpler choice).

  • trail_delay never fully converges. g_lagPos.x += (int)((pt.x - g_lagPos.x) * ease); (line 3081) truncates to int, so once the remaining distance drops below 1/ease the increment is 0 — at the default ease of ~0.12 the lagged head parks up to ~8 px away from the cursor and stays there. Keeping g_lagPos as float (rounding only when it's used) fixes it.

  • Hard-coded pixel sizes don't scale with DPI. Trail width (10.0f), inner width (6.0f), glow radii, the 2.8f head highlight, shape and particle sizes, and the ripple ring width are all raw pixels, so the whole effect renders at half its intended visual size on a 200 % display and mismatches across a mixed-DPI multi-monitor setup. Scaling these by the DPI of the monitor under the cursor (GetDpiForMonitor on MonitorFromPoint) would make it consistent.

  • CheckForegroundFullscreen treats "cursor hidden" as "game". Condition 4 (GetCursorInfo(&ci) && ci.flags == 0, line 2586) fires whenever the pointer is hidden, and Windows hides it while you type in many apps. Combined with the size match it only needs a maximised/borderless window, so typing in a full-screen editor or browser hides the overlay and clears the trail state. Requiring QUNS_RUNNING_D3D_FULL_SCREEN or the clip-cursor signal, and treating the hidden cursor only as a corroborating hint, would cut the false positives. (This heuristic is inherited from cursor-motion-blur, so it's a pre-existing behaviour rather than a regression.)

  • GetPixel on the screen DC 25×/s (line 2633) is a synchronous read-back from the desktop and one of the slower GDI calls; it runs continuously while cursor_extract / cursor_mix is selected. It's hard to avoid without a much heavier DXGI desktop-duplication path, so this is just an FYI — but sampling less often when the extracted colour is stable (it's already smoothed with a 0.22 lerp) would cost nothing visually.

  • Idle wake-up rate. The render loop falls back to a 16 ms wait when nothing is on screen (line 4077), so the dedicated process wakes ~60×/s indefinitely even if the mouse hasn't moved for hours, which blocks timer coalescing. Since the trail only starts once velocity exceeds trigger_velocity (25 px/frame by default), a second backoff tier — e.g. 16 ms for a few seconds after the last activity, then 32–50 ms — would be essentially invisible and noticeably kinder to battery.

  • Click detection polls GetAsyncKeyState once per frame (lines 3113–3114), so a click shorter than the current wait interval (up to 16 ms when idle) is missed entirely, and only the left and right buttons are handled — middle-click and the side buttons produce no effect. Worth mentioning in the README if it's intentional.


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 Sep 10, 2026
@MCheng404

Copy link
Copy Markdown
Author

/ai-review

@windhawk-reviewer

Copy link
Copy Markdown

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

- NativeRenderTrail: merge 7 layers into single Map (eliminate 6 extra Map/Unmap + vector allocations)
- NativeRenderParticles: merge glow+normal layers into single Map, single particle traversal
- NativeRenderTrailShapes: precompute shape vertices static cache (eliminate per-frame cos/sin/pow)
- Trail PS: tube lighting edge brightness 0.75->0.88, edge softness via v coordinate
- Particle PS: signed distance soft edges for all shapes, radial lighting min 0.45->0.6
- Particle opacity 0.65->0.80
- Adaptive contrast strength halved (0.7->0.35)
- New settings: edge_softness, enable_particle_glow, particle_glow_intensity
- Head highlight focused to trail head only
- Fix: glow layer no longer switches to gray on bright backgrounds (was causing gradual dimming)
@MCheng404

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 Sep 11, 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 tool-mod structure is right (dedicated windhawk.exe process, verbatim launcher boilerplate, no hooks in other processes), settings and code are fully in sync in both directions, teardown joins every thread it starts, no registry/system writes, and no network access. The items below are what I'd want fixed before merge.

1. The 4096-vertex trail buffer silently drops the main trail band at higher tail_length

g_pTrailVB is a fixed 4096-vertex buffer (line 1769). NativeRenderTrail writes up to 7 bands of smoothed.size() * 2 vertices each into it, and writeBand silently returns 0 when the next band doesn't fit (line 2012). The body band is written after the shadow and glow bands (lines 2034-2053), so it is among the first things dropped.

With Bezier smoothing on (default), smoothed.size() == 1 + (tail_length - 1) * 4, so at the default 6 enabled bands:

tail_length smoothed pts verts/band bands that fit result
10 (default) 37 74 all 6 fine
87 345 690 5 head highlight dropped
120 477 954 4 body band dropped — only shadow + glow render
200 (documented max) 797 1594 2 shadow + one glow layer only

So anywhere above ~120 the actual trail disappears and the user is left with a faint halo, with nothing in the log to explain it. The non-Bezier path grows at the same rate (two 2x subdivision passes), so it hits the same wall.

NativeRenderTrailShapes has the same problem from the other direction — it truncates with verts.resize(4096) (line 2144), and since the vector is built oldest-first, the newest shapes (the ones at the cursor) are the ones thrown away. At 150 shapes × 72 verts for heart/circle that's 10,800 vertices, so only ~56 shapes ever render, and super_performance_mode raising the cap to 400 makes it worse, not better.

Suggested fix: size the vertex buffer from the actual requirement instead of a magic 4096 — recreate it when needed > capacity (it's D3D11_USAGE_DYNAMIC, so this is just a release + CreateBuffer on the render thread). If you want to keep a hard cap, at least write the body band first so only the cosmetic layers get dropped, and clamp smoothed to a point count that provably fits.

2. The "Feather" trail shape isn't implemented

g_trailShape == 9 is set in LoadSettings (line 2693) and then never read anywhere in the file — no deformation in the smoothed pipeline (lines 3787-3794, which handle 2/3/7/8) and no branch in NativeRenderFrame (lines 2378-2427, which handle 1/5/6). Selecting Feather renders exactly the same as Tapered Ribbon, while the README promises "Random spiky depth texture". Either implement it or remove the option from $options and the README.

3. Two complete renderers, one of which is unreachable and behaves differently

useNative = g_pNativeVS != nullptr (line 4001), and when native rendering succeeds the function returns at line 4018. So roughly 1,500 lines — the D2D device/brushes, UpdateColorBrushes / ReleaseGradientBrushes / the whole GradData brush cache, CreateMeshFromQuadStrip*, CreateStarGeometry / CreateHexagramGeometry / CreateHeartGeometry, RenderParticles, RenderTrailShapes, RenderMotionBlur, RenderClickRipples, and the DotInfo / mesh branches at lines 4027-4278 — only execute if shader compilation fails, which on any D3D10+ GPU it won't.

Worse, if it did execute the mod would silently behave differently: the D2D particle path only knows shape types 2 and 3 (line 3474-3487), ignores particle_size_multiplier, enable_particle_spin, enable_particle_glow and particle_glow_intensity, and NativeRenderFrame's double/dashed handling has no D2D counterpart. A fallback that quietly turns off half the settings is worse than no fallback.

I'd drop the D2D path entirely: log and leave the overlay hidden if InitNativeRendering() fails. That also lets you drop -ld2d1, <d2d1_1.h>, the ID2D1Factory1/ID2D1Device/ID2D1DeviceContext creation, and the D2D1_COLOR_F usage for plain colour math. The only non-cosmetic use of D2D left is getting the RTV in RecreateSwapChain (lines 3355-3364), which you can do directly: g_pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&tex)CreateRenderTargetView.

4. Overlap with the existing cursor-motion-blur mod

This mod is a derivative of cursor-motion-blur — same @include windhawk.exe tool-mod shape, same setting keys (trigger_velocity, stop_velocity, tail_offset_x/y, tail_length), the same globals (g_overlayHwnd, g_threadHandle, g_history), and the same 0.75/0.25 subdivision + normal-offset ribbon construction. The credit in the README and the matching MIT licence are handled correctly, so that part is fine.

The catalog problem is that with trail_shape: tapered and particles off, this is Cursor Motion Blur — two mods doing the same visible thing is exactly the fragmentation the maintainer usually pushes back on. It's a large functional superset, so a separate mod is defensible, but that's a call for the maintainer rather than something to assume. Please say explicitly in the PR why this should ship as a separate mod rather than as options added to cursor-motion-blur (e.g. via a PR to TheatriChris), and consider dropping a note to the original author.

Optional improvements

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

  • Use WindhawkUtils::StringSetting instead of the 12 manual Wh_GetStringSetting + Wh_FreeStringSetting pairs in LoadSettings — it's RAII and can't leak on an early return. Also, Wh_GetStringSetting never returns NULL (it returns L"" on error/unset), so the if (str) guards at lines 2577, 2623, 2633, 2675, … are dead. Requires #include <windhawk_utils.h>.

  • TerminateThread (lines 4542 and 4680). Both are "last resort" paths, and since Wh_ModUninit calls ExitProcess(0) right after the blast radius is small — but killing the sampler thread and then calling DeleteCriticalSection(&g_sampleCS) (line 4687) on a section it may still own is undefined behaviour, and killing the render thread mid-D3D-call leaves the driver state inconsistent. The worst case is also a ~8 s hang in the Windhawk UI while disabling the mod (5 s + 2 s + 1 s). Both threads have short, bounded loop bodies, so a plain unbounded WaitForSingleObject(..., INFINITE) after signalling would be both simpler and safer.

  • surfaceDirty is dead on the native path. It's only assigned true inside the D2D branches (lines 4034, 4041, 4046, 4163, 4270, 4277), which never run once native rendering works — so the else { hideDelayCounter = 0; } branch at line 3988 and every || surfaceDirty test are unreachable. A side effect is that the surface is never cleared before the window is hidden, so the last drawn frame stays in the swap chain until the next present.

  • SetWindowPos(hwnd, HWND_TOPMOST, …, SWP_NOACTIVATE | SWP_NOZORDER) (line 4473): SWP_NOZORDER makes the HWND_TOPMOST argument a no-op. If the intent was to re-assert topmost after a display change, drop SWP_NOZORDER; if not, pass NULL as hWndInsertAfter to make that explicit.

  • RenderThreadProc early returns leak. The D3D11CreateDevice and CreateDeviceContext failure paths (lines 4584-4602) return without calling ReleaseAllRenderResources(), leaking whatever was already created. Cheap to fix and it makes the failure path match the recovery path.

  • Settings UI grouping. The # ===== 基础设置 ===== style comments in the settings block are YAML comments — Windhawk doesn't render them, so users see 65 ungrouped options in one flat list. Nested groups (particles.enable, particles.density, read as Wh_GetIntSetting(L"particles.density")) would give real sections in the UI.

  • Confusable setting names: particle_repel_distance / particle_inter_repel_force control inter-particle repulsion while particle_repel_radius / particle_repel_force control cursor repulsion. Something like particle_particle_repel_* vs cursor_repel_* (or nested groups, above) would be much easier to follow.

  • Missing cross-validation on velocities: trigger_velocity and stop_velocity are only clamped to > 0 (lines 2837-2840), with no upper bound and no check that stop < trigger. Setting stop above trigger makes the trail activate and immediately deactivate every frame.

  • Data race on the sampler results. g_cursorExtractedColor (line 3029) and g_bgLuminance (line 3120) are written on the sampler thread and read on the render thread (ComputeColors, UpdateConstantBuffer) with no synchronization. Visually benign, but it's still UB — either make them std::atomic<float> or publish them back through g_sampleCS the same way the inputs are passed in.

  • Idle wakeups. The render loop waits 16 ms when idle (line 4670), i.e. ~62 wakeups/second forever just to call GetCursorPos + GetAsyncKeyState. cursor-motion-blur does the same with a 10 ms timer so it's precedented, but you could block in GetMessage while idle and use raw input (RegisterRawInputDevices with RIDEV_INPUTSINK for HID_USAGE_GENERIC_MOUSE) to wake the loop only when the mouse actually moves or a button changes, then fall into the 1 ms active loop. Nice for laptops.

  • The tool-mod boilerplate has one extra line vs. the wiki snippetWh_Log(L"Tool mod host: %s", currentProcessPath); at line 4820. Keeping the snippet verbatim makes it easier to diff against future updates.

  • Code comments are Chinese-only. The user-facing side is done right (English defaults with :zh-CN localization throughout), but English comments would make the code much easier for the maintainer to review and for others to contribute to.

  • README particle list is stale: "Shapes: circle, star, hexagram, or random mix" under Particle System, but the mod now offers 9 shapes.

  • g_prevVelocity (line 3895) and the function-static lastVel (line 3686) track the same value; one of them can go.

  • CreateStarGeometry / CreateHexagramGeometry / CreateHeartGeometry dereference *geom without checking that CreatePathGeometry succeeded.

Functionality notes

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

  • The head highlight looks inverted. In writeBand, ratio is 0 at the head and 1 at the tail, and the headOnly mask is (ratio < 0.25f) ? 0.0f : ((ratio > 0.85f) ? 1.0f : (ratio - 0.25f) / 0.6f) (line 2019) — so the "head highlight" band is fully transparent at the head and opaque toward the tail, the opposite of what the setting ("Bright center dot at trail head") and the code comment ("头部高亮(只在头部显示)") describe. Multiplied by the gradient's own (1-ratio)^1.4 alpha it ends up peaking somewhere in the middle-rear of the ribbon. Inverting the mask (ratio > 0.25f ? 0.0f : 1.0f - ratio / 0.25f) looks like what was intended.

  • 2.5D only affects particles. Every trail, shape, ripple and dot-chain vertex is written with z = 0.0f, so enable_25d_effect / perspective_strength only do anything for particles. That matches the README, but the setting's own description ("Add per-vertex depth with perspective projection for a 2.5D look") reads as if it applies to the whole trail — worth saying "particles only" there.

  • Motion blur only applies to Tapered Ribbon. g_trailShape == 0 gates both recording (line 3808) and rendering (line 3525); with any other shape the setting silently does nothing. Worth a note in the description.

  • WDA_EXCLUDEFROMCAPTURE side effect. In Cursor Extract / Cursor Mix modes LoadSettings excludes the overlay from capture (line 2920) so it doesn't sample itself — but that also makes the trail invisible in the user's own screenshots and screen recordings, and in remote-desktop sessions. Also, WDA_EXCLUDEFROMCAPTURE only exists on Windows 10 2004+; on older builds the call fails and the mod will sample its own trail, producing a colour feedback loop. Worth documenting, and worth checking the return value so you can fall back (e.g. sampling further from the path, as SampleBackgroundLuminance already does with its 10 px normal offset).

  • Game detection has false positives. CheckForegroundFullscreen returns true when the foreground window covers a monitor and the cursor is hidden (line 2974). A fullscreen video player or an F11 browser that hides the pointer after a few idle seconds will therefore be treated as a game, clearing the trail and hiding the overlay until the 500 ms recheck. Probably acceptable, but it's a behaviour users may report as "the trail randomly stops working".

  • trail_delay never fully converges. g_lagPos.x += (int)((pt.x - g_lagPos.x) * ease) (line 3607) truncates to int, so once the difference is small enough that the product is < 1 the lag position sticks a pixel or two off the cursor permanently. Keeping g_lagPos as floats would fix it.

  • Only left and right buttons trigger click effects (lines 3639-3640) — middle and X buttons produce nothing. Fine if intentional, just noting it since the settings say "on mouse click".

  • super_performance_mode doesn't lift the real cap. It raises the particle/shape list limits and the O(n²) repulsion threshold, but the binding constraint on high settings is the 4096-vertex buffer (item 1 above), which it doesn't touch — so the "removes all performance limits" description over-promises.


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 Sep 11, 2026
@MCheng404

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 Sep 11, 2026
Display optimization:
- Additive blending for glow layers (trail/particles/ripples)
- Particle fast-move interpolation (interval<=10ms, eliminates gaps)
- Trigger/stop logic optimization (2-frame confirm, actual distance check)
- Hard cut fadeout fix (clear all state to prevent residual rendering)
- 5 new color modes: thermal heatmap, wave interference, spectrum split, grain dither, gradient warp
- Gradient system rewrite: any number of colors (max 16), OKLab perceptual interpolation, precomputed 256-color LUT, flowing gradient

Color mode algorithm optimization:
- All 23 modes optimized with velocity response, multi-frequency dynamics, enhanced head-tail contrast
- Bilingual code comments (Chinese/English)

Centripetal vortex (entertainment feature):
- Curvature-based detection (any curved motion triggers)
- Physical orbital model: angular momentum conservation, Kepler velocity gradient, spiral convergence
- 3D orbit tilt with z-depth association and front-back occlusion sorting
- Particles fly out centrifugally when motion stops

Particle physics mass system:
- Random particle mass (affects size via cube root, lifetime, inertia/friction, acceleration a=F/m)
- Newtonian gravity between particles: F=G*m1*m2/r^2, a=F/m
- Binary star / N-body system modes (2-10 massive bodies)
- All features with independent toggles
@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 Sep 12, 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.

- Separate particle random color offset from vortex brightness modulation
- Add vortexBrightness[3] field for centripetal/3D brightness effects
- colorOffset now preserved as particle's permanent random color variation
- Render: final color = base + colorOffset + vortexBrightness
@MCheng404

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 Sep 12, 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 overall shape is right: a dedicated-process tool mod with no hooks into other processes, the wiki launcher boilerplate, clean setting/code mapping (all 76 settings are read, with matching types), no persistent system changes, and a README with GIFs on an allowed host. The items below are what stood out.

1. Screen-capture exclusion is never applied at startup. SetWindowDisplayAffinity is only called from LoadSettings() (line 3292), behind if (g_overlayHwnd). The first LoadSettings() call happens in WhTool_ModInit (line 5364), before OverlayThreadProc creates the window — so g_overlayHwnd is still NULL and the call is skipped. After that, LoadSettings() only runs again when the user changes a setting (line 3960). So if color_mode is already cursor_extract or cursor_mix when the mod starts, the overlay is never excluded from capture, and ExtractCursorColor's GetPixel (line 3396) reads the trail's own pixels — a colour feedback loop until the user happens to touch the settings. The comment at line 5179 says the exclusion is handled in LoadSettings, so this looks like an oversight rather than intent. Apply the affinity right after CreateWindowEx succeeds (and keep the LoadSettings update for later changes).

2. Remove the two TerminateThread fallbacks. TerminateThread is called on the render thread (line 5211) and on the background sampler (line 5349). It doesn't unwind or release anything the thread holds, and here that has concrete consequences: TerminateThread(g_bgSampleThread) is immediately followed by DeleteCriticalSection(&g_sampleCS) (line 5356), which is undefined behaviour if the killed thread owned the section, and TerminateThread(g_renderThread) leaves the D3D11/DXGI device in an unknown state while the overlay thread goes on to DestroyWindow / UnregisterClass. Both waits already have generous timeouts (5 s + 2 s, and 3 s), and if a thread really is wedged inside a GPU call, killing it is strictly worse than doing nothing — Wh_ModUninit calls ExitProcess(0) a moment later, which tears down every thread cleanly from the OS's point of view. Just log the timeout and continue:

if (WaitForSingleObject(g_bgSampleThread, 3000) == WAIT_TIMEOUT) {
    Wh_Log(L"RenderThread: background sampler thread timed out, leaking handle");
} else {
    CloseHandle(g_bgSampleThread);
}
g_bgSampleThread = NULL;

(and skip the DeleteCriticalSection on that path, since the section may still be owned).

3. Unchecked GDI allocations in SampleBackgroundLuminance can crash the process. CreateDIBSection (line 3466) and CreateCompatibleDC (line 3467) are not checked. On failure pBits stays nullptr, and the sampling loop still does const BYTE* px = pSrc + (dy * w + dx) * 4; followed by px[2] (lines 3479-3481) — a guaranteed access violation that kills the tool process and takes the trail with it. GDI allocations do fail in practice under desktop-heap/handle pressure. Bail out early:

HBITMAP hBmp = CreateDIBSection(hdcScreen, &bmi, DIB_RGB_COLORS, &pBits, nullptr, 0);
HDC hdcMem = CreateCompatibleDC(hdcScreen);
if (!hBmp || !hdcMem || !pBits) {
    if (hBmp) DeleteObject(hBmp);
    if (hdcMem) DeleteDC(hdcMem);
    ReleaseDC(NULL, hdcScreen);
    return;
}

4. The English README is a subset of the Chinese one. English is the catalogue default, but the English half documents 18 colour modes, no centripetal vortex and no particle mass/gravity system, while the Chinese half documents 23 modes and all of those features. The @description / @description:zh-CN metadata also still says 18 modes. Please bring the English README and @description up to date so English-speaking users see the same feature list as Chinese-speaking ones.

5. The feature surface is very large, and parts of it don't currently work. 76 settings, 23 colour modes, 10 trail shapes, a runtime expression parser, a centripetal-vortex orbital model and an N-body Newtonian gravity simulator is a lot for a cursor trail, and three of the more exotic pieces are inert or broken in this build (see Functionality notes — the vortex's acceleration response is dead code, the double-ring click ripple never renders). Since the mod is AI-assisted, it's worth asking directly which of these you actually use and intend to maintain. The centripetal_* group (4 settings), the enable_particle_gravity / gravity_* group (4 settings) and super_performance_mode in particular look like good candidates to drop rather than ship — fewer, well-tested options are easier for users to discover and for you to keep working.

6. Please state explicitly how this differs from Cursor Motion Blur. You credit its overlay architecture (thanks for that, and the MIT-to-MIT licensing is consistent), but the default configuration here — tapered ribbon with glow, triggered above a velocity threshold, drawn by a topmost DComp overlay in a windhawk.exe tool process — is close to what that mod already does. The rest of this mod clearly goes well beyond it, so it reads as a separate mod rather than a duplicate, but Windhawk's policy is to extend an existing mod rather than ship a near-variant, so a sentence or two in the README/PR describing the differentiation will save the maintainer from having to derive it.

Optional improvements

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

  • Tool-mod boilerplate isn't verbatim. Line 5489 adds Wh_Log(L"Tool mod host: %s", currentProcessPath);, which isn't in the wiki snippet, and the whole block has been reflowed. The snippet is meant to be pasted unchanged so it can be diffed against the upstream version later. mods/explorer-folder-hover-menu.wh.cpp keeps it byte-for-byte.
  • Wh_GetStringSetting never returns NULL — it returns L"" when unset or on error — so every if (str) guard in LoadSettings is dead code. WindhawkUtils::StringSetting (RAII) is also nicer than the raw get + Wh_FreeStringSetting pairs; see taskbar-clock-customization.wh.cpp#L523.
  • Dead global: g_gradientWarp (line 1230) is assigned in ComputeColors case 22 (line 1624) and never read anywhere.
  • The D2D1 fallback renderer is ~1,200 lines of code that essentially never runs. RenderParticles, RenderTrailShapes, RenderMotionBlur, RenderClickRipples, the dot-chain/mesh path in RenderFrame, CreateMeshFromQuadStrip*, and the g_pStarGeom / g_pHexagramGeom / g_pHeartGeom geometries are only reachable if InitNativeRendering() fails outright (line 4670), and they produce a visibly different, much less featured result. Given you already fall back from hardware to WARP and accept feature level 10.0, deleting the D2D path would remove a large block of untested code and a couple of D2D-only globals with it.
  • SWP_NOZORDER makes HWND_TOPMOST a no-op in the WM_DISPLAYCHANGE handler (line 5142). Either drop SWP_NOZORDER or pass HWND_TOP to make the intent match the behaviour. (WS_EX_TOPMOST is already set on the window, so this is cosmetic.)
  • ParseHexColor(tok, g_gradColors[idx]) (line 938) falls back to whatever colour was previously at that index, so a typo in gradient_colors silently keeps a stale colour instead of a predictable default. Falling back to a fixed colour (or skipping the entry) would be less confusing.
  • ^ is left-associative in the expression compiler — OpPrec returns 4 for it (line 962) and the shunting-yard loop pops on >= (line 1038), so 2^3^2 evaluates as (2^3)^2. Surprising in a user-facing formula field; exponentiation is conventionally right-associative.
  • WS_EX_LAYERED + SetLayeredWindowAttributes(..., 255, LWA_ALPHA) (lines 5169/5178) alongside a DirectComposition target is unusual — DComp already provides per-pixel alpha, and WS_EX_NOREDIRECTIONBITMAP is the more common choice for a DComp overlay. Worth testing whether the layered style is actually needed; if it isn't, dropping it saves a composition pass.

Functionality notes

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

  • The second, delayed click ripple is discarded on the frame it is created. Lines 4019 and 4023 push {pt, dwTime + 120}, i.e. a start time in the future. Both the cleanup predicate (line 4038) and the render code (lines 2561, 3937) compute dwTime - r.startTime in unsigned DWORD arithmetic, so for the delayed ripple that expression underflows to ~4.29e9, which is > g_clickDuration — it is erased by the remove_if a dozen lines after it was pushed, and would be skipped by the renderer anyway. The advertised double-ring click effect therefore never appears. Either keep startTime = dwTime and add a separate delay field, or compare with signed arithmetic: (int32_t)(dwTime - r.startTime) > g_clickDuration.
  • The vortex's acceleration response is dead. g_prevVelocity = velocity; executes at line 4336, before the centripetal block computes float accel = velocity - g_prevVelocity; at line 4342 — so accel is always exactly 0 and accelBoost is always 0. That makes minVel a constant 3.0, and removes the acceleration terms from boostRate (line 4403) and vortexAngularVel (line 4408). Moving the g_prevVelocity update to after the vortex block fixes it; note the particle-spawn code at line 4244 reads g_prevVelocity correctly because it runs before the update.
  • Present(1, 0) followed by DwmFlush(). The render loop presents with a vsync interval (lines 4678 / 4956) and then calls DwmFlush() (line 5336). Both block on the composition clock, so each iteration can cost two refresh intervals — worth measuring the actual frame rate with one or the other, since the pacing from Present(1, 0) alone may already be what you want and the extra flush may be costing smoothness rather than adding it.
  • Inter-particle repulsion is O(n²) and enabled by default. enable_particle_interaction defaults to true, and the double loop at line 4527 runs for up to 500 particles (1200 with super_performance_mode) — roughly 125k (or 720k) distance computations per frame, on top of the gravity pass. It can't destabilise the shell since this is a dedicated process, but it's a real CPU cost for an effect most users won't consciously notice. Consider defaulting it off, or bucketing particles into a coarse grid so only nearby pairs are tested.
  • Adaptive contrast samples the trail itself. The capture exclusion is only applied for the two cursor-colour modes, so with enable_adaptive_contrast on in any other mode, SampleBackgroundLuminance's BitBlt (line 3471) reads back the overlay along with the background. The 10 px normal offset (OFFSET, line 3417) is measured against a ~10 px half-width trail plus glow layers that extend to 2.6× that width (line 2374), so the sample points land inside the glow. Either apply WDA_EXCLUDEFROMCAPTURE whenever the sampler is running, or widen OFFSET past the glow radius.
  • Everything is sized in physical pixels. Trail half-width (10.0f, lines 2309 and 4859), glow radii, the head-highlight radius, tail_offset_x/y, and the shape/particle/ripple sizes are raw pixels on a per-monitor-DPI-aware surface. The result is half as large relative to the UI on a 200 % display, and the trail visibly changes size as the cursor crosses between monitors with different scaling. Scaling these by the DPI of the monitor under the cursor (GetDpiForMonitor on MonitorFromPoint) would keep it consistent; positioning is already correct since you work in virtual-screen coordinates.
  • Fullscreen detection is broader than "DirectX game". CheckForegroundFullscreen returns true when a monitor-sized window is foreground and the cursor is hidden (lines 3346-3349), which also matches full-screen video playback, full-screen browser windows and slideshows. That may well be what you want, but the setting/README wording ("Auto-hide in fullscreen DirectX games") promises something narrower.


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. waiting-for-reviewer Ready for a human reviewer, and in the queue for one. and removed waiting-for-ai-review An AI review was requested and is being prepared. waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Sep 12, 2026
@m417z

m417z commented Sep 12, 2026

Copy link
Copy Markdown
Member

It's unclear whether the PR is ready. My best guess is that you commented /ready-for-reviewer, and then removed your comment. Please clarify.

@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 Sep 12, 2026
@MCheng404

Copy link
Copy Markdown
Author

/ready-for-reviewer

Not much else to improve, the code is pretty much perfect... except for this one bug I'm not sure how to fix. When users update their graphics drivers, the renderer stops working properly and they have to disable and re-enable the mod to recover. Shouldn't be a big deal though—it's not like people update their drivers every day, right?

@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 Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-for-reviewer Ready for a human reviewer, and in the queue for one.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants