Skip to content

Add Vector Screen Holder mod - #5426

Open
akilluminati47 wants to merge 2 commits into
ramensoftware:mainfrom
akilluminati47:vector-screen-holder
Open

Add Vector Screen Holder mod#5426
akilluminati47 wants to merge 2 commits into
ramensoftware:mainfrom
akilluminati47:vector-screen-holder

Conversation

@akilluminati47

@akilluminati47 akilluminati47 commented Sep 9, 2026

Copy link
Copy Markdown

Adds Vector Screen Holder, a tool mod that fills a display of your choosing with generative line art and holds the screen awake while it runs.

The use case it was built for: you're downloading something large, or running a long build, render or backup, and you need the machine not to sign you out or drop to idle. Put the Screen Holder on a side monitor next to your task monitor and walk away — the work keeps running, the screen stays awake, and progress is still readable from across the room.

What it does

  • Runs as a tool mod in its own dedicated windhawk.exe process, so it is never injected into other applications.
  • Everything is stroked geometry through Direct2D — no images, no video, no fixed resolution. The art is generated for whatever size the selected display actually is, so a 1080p side monitor and a 4K portrait panel each get correctly proportioned output.
  • While the overlay is up it calls SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED), cleared the moment it closes. It does not synthesise input.
  • The overlay sits above the wallpaper but below application windows, never steals focus on its own, and stays out of Alt+Tab.

Styles

Four, each with a wheel parameter (its character) and a right-click amount (how much information is on screen, five notches):

Style Parameter Amount
Flow field field turbulence ribbon packing density
Contours terrain relief 6–46 iso levels
Differential growth colony vigor 1–6 colonies
Harmonograph draw tempo 1–6 overlaid figures

Controls

Esc closes, left click cycles style, right click steps the amount, the wheel adjusts the parameter, and holding Space slides the hue. A configurable global hotkey toggles it, and it also listens on a named event so a desktop shortcut can drive it. The overlay is clean when it opens — labels appear only while you're actively changing something.

Notes on implementation

  • Scenes advance on a wall-clock step clock rather than per frame, so the frame-rate setting changes how smooth the motion is without changing how fast anything draws.
  • Contours interpolate between two sampled noise fields each frame, so movement stays continuous instead of stepping on the sampling interval. The noise grid is area-budgeted rather than tied to pixel count, keeping per-frame cost flat across resolutions.
  • Hue rotation is applied at stroke time rather than to the finished image, since Direct2D 1.0 has no hue-rotate effect. Styles that redraw each frame slide through colour wholesale; styles that accumulate lay a gradient through the artwork as the hue drifts.

Builds clean with the bundled toolchain at -O2 -Wall -Wextra.

Changelog

If this pull request updates an existing mod, describe the changes below:

  • New mod — not applicable.

Mod authorship

If this pull request introduces a new mod, please complete the section below.

This mod was created by:

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

Please select the options that best apply. Your selection does not affect the acceptance criteria, but it helps reviewers understand the context of the code and provide relevant feedback.

🤖 Generated with Claude Code

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

A Windhawk tool mod that fills a chosen display with generative line art and holds the screen awake while it runs. Everything is stroked geometry drawn through Direct2D, generated for whatever size the selected display is. Four styles, each with a wheel parameter and a right-click amount: flow field ribbons, topographic contours, differential growth, and a harmonograph. The overlay sits above the wallpaper but below application windows, never steals focus, and stays out of Alt+Tab. Scenes advance on a wall-clock step clock, so the frame rate changes how smooth the motion is without changing how fast anything draws; contours interpolate between sampled noise fields so their movement stays continuous.

Co-Authored-By: Claude AI <noreply@anthropic.com>
Co-Authored-By: Big-Pickle (opencode) <noreply@opencode.ai>
Windhawk 2.0 rejects a setting that uses $options unless its value is a
string or an array of strings, which failed the compatibility check against
2.0.0-alpha.3:

  Failed to parse settings: instance[9].amount must be a string or array of
  strings to use $options

The amount notch was an int (0-4) with numeric option keys. It is now stored
by name (minimal / sparse / balanced / dense / maximal) and mapped back to an
index on load, which also reads better in the settings UI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@akilluminati47

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 tool-mod choice is right, the DPI handling is right, and installing the low-level hook only while the overlay is up is a good instinct. The findings below are mostly about integration with Windhawk and about two behaviours the code doesn't actually deliver.

1. Scope: this is a standalone application, not a modification of an existing program. Windhawk mods change the behaviour of Windows programs; the "mods as tools" pattern exists for utilities that were shell mods and shouldn't be injected (theme scheduler, network indicator, virtual-desktop helper) — they still act on the shell/system. This mod hooks nothing, reads nothing from a host process, and modifies no program's behaviour: it is a ~2,600-line generative-art screensaver plus a keep-awake toggle, packaged as a mod. Expect the maintainer to question whether it belongs in the catalog at all rather than being shipped as a normal .exe. That's his call, not mine, so please make the case explicitly in the PR description.

Related: the keep-awake half duplicates mods/caffeine.wh.cpp (same SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED) toggle, also a windhawk.exe tool mod). If the art is the point, consider saying so and dropping/reducing the keep-awake feature rather than reimplementing an existing mod inside this one.

2. The overlay never actually sinks to the bottom of the z-order — its own WM_WINDOWPOSCHANGING handler blocks the call that would put it there. The handler unconditionally ORs SWP_NOZORDER into wpos->flags for every WM_WINDOWPOSCHANGING, starting from CreateWindowExW:

case WM_WINDOWPOSCHANGING: {
    WINDOWPOS* wpos = (WINDOWPOS*)lp;
    wpos->flags |= SWP_NOZORDER;   // line 1824
    return 0;
}

so the SetWindowPos(hwnd_, HWND_BOTTOM, ...) at line 1615 is neutralised too, and the window stays where creation left it — at the top of the non-topmost band, i.e. over your application windows on that monitor. That contradicts the central README promise ("sits above your wallpaper but below your windows"). Either exempt the initial sink with SWP_NOSENDCHANGING (which suppresses WM_WINDOWPOSCHANGING for that one call — see keep-rainmeter-always-bottom.wh.cpp#L49):

SetWindowPos(hwnd_, HWND_BOTTOM, 0, 0, 0, 0,
             SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOSENDCHANGING);

or, better, re-sink on every position change instead of freezing the z-order:

wpos->hwndInsertAfter = HWND_BOTTOM;
wpos->flags &= ~SWP_NOZORDER;

3. The WH_KEYBOARD_LL hook is installed on the render thread, so every frame stalls system-wide keyboard input. InstallKbdHook() (line 2101) runs on the worker thread, and an LL hook callback is only serviced while its installing thread is retrieving messages. That thread spends the rest of its time in Overlay::Render — for contours at maximal amount that's ~46 iso levels × ~18k marching-squares cells plus 46 CreatePathGeometry calls per frame, i.e. many milliseconds per frame on which every keystroke on the machine now waits. Worse, if a frame ever overruns LowLevelHooksTimeout (300 ms by default; a 4K contour frame on a busy machine — which is the mod's stated use case — can get there) Windows silently skips or drops the hook, and Space/Esc stop working until the mod is reloaded. Move the hook to a thread that does nothing but pump messages and PostThreadMessage to the render thread — lock-keys-notifier.wh.cpp#L1347 is exactly this pattern, with the reasoning in the comment above it. Alternatively drop the global hook: the overlay's own WndProc already handles Esc and Space when focused, and the configurable hotkey covers "from anywhere".

4. WhTool_ModUninit doesn't wait for the worker thread, and ExitProcess(0) then kills it mid-render. WhTool_ModInit closes the thread handle immediately (line 2463) and WhTool_ModUninit only sets g_running = false and posts WM_VSH_QUIT before returning — so Wh_ModUninit's ExitProcess(0) fires while the worker is almost certainly inside Direct2D/DXGI. ExitProcess terminates it wherever it is and then runs DLL_PROCESS_DETACH for every DLL; a thread killed while holding a D2D/DXGI/loader lock can hang that teardown, and a hung windhawk.exe keeps holding windhawk-tool-mod_vector-screen-holder, which makes the next enable bail out with "Tool mod already running". Keep the handle and join it, as always-on-top.wh.cpp#L462-L473 does:

void WhTool_ModUninit() {
    g_running = false;
    if (g_workerThreadId) {
        PostThreadMessageW(g_workerThreadId, WM_VSH_QUIT, 0, 0);
    }
    if (g_thread) {
        WaitForSingleObject(g_thread, INFINITE);
        CloseHandle(g_thread);
        g_thread = nullptr;
    }
}

This also makes the existing cleanup at the end of WorkerThread (overlays destroyed, hook removed, hotkey unregistered, factory released) actually run, instead of being dead code in practice.

5. The tool-mod boilerplate deviates from the wiki snippet. It should be a verbatim copy of the wiki code so it can be diffed at a glance; two pieces were changed:

  • The session-0 guard was deleted:
    DWORD sessionId;
    if (ProcessIdToSessionId(GetCurrentProcessId(), &sessionId) &&
        sessionId == 0) {
        return FALSE;
    }
  • The exclusion list was narrowed from -service / -service-start / -service-stop to -service only, so the short-lived windhawk.exe -service-start / -service-stop invocations become tool-mod launchers and spawn a throwaway process each (the mutex makes it exit again, but it shouldn't be started).

Please restore the snippet exactly as published (including the isExcluded naming).

6. The amount, parameter and style settings stop having any effect after the first interaction. ShowOverlays reads the persisted state with the setting only as a fallback:

int amount = ClampT(Wh_GetIntValue(L"state.amount", g_settings.amount), 0, kAmountCount - 1);
float param = ClampT(Wh_GetIntValue(L"state.param", g_settings.parameter * 10), 0, 1000) / 1000.0f;

Once the user has right-clicked or scrolled once, state.amount / state.param exist and the settings labelled "Starting amount notch" / "Starting value for the per-style parameter" are silently ignored forever — changing them in the settings UI does nothing. Let an explicit settings change win: in the WM_VSH_SETTINGS path, drop the saved state (or store the setting value you last applied alongside it and reset when it differs) before re-showing.

7. Null dereference if device-resource creation partially fails. CreateDeviceResources can return false with rt_ already created (when CreateCompatibleRenderTarget or CreateSolidColorBrush fails), and Render only tests rt_:

if (!rt_ && !CreateDeviceResources()) {   // line 1724
    return;
}
...
buf_->BeginDraw();                        // line 1745 — buf_ may be null

After a D2DERR_RECREATE_TARGET discard the next frame recreates rt_, fails on buf_, returns — and the frame after that skips creation entirely and dereferences the null buf_ (and brush_, via SetInk). Call DiscardDeviceResources() on every failure path in CreateDeviceResources, and test all three resources at the top of Render.

8. Display configuration changes aren't handled. Overlay::rect_ is captured once at creation and the render target is sized to it, but nothing handles WM_DISPLAYCHANGE or WM_DPICHANGED. Since the mod is designed to be left up for hours, plugging in a monitor, docking/undocking, or a resolution change leaves the overlay at the wrong size and position (and, with monitor: all, missing the new display entirely). Handling WM_DISPLAYCHANGE in the overlay WndProc by posting a "rebuild" message to the worker — tear the overlays down and re-run ShowOverlays' monitor selection — is enough.

9. The README documents an on-screen readout that doesn't exist. "The style name and its two readouts appear only while you are actively changing something, then fade away again" (and the same claim in the PR body) is implemented by:

static void Controller_Hud() {
    // Reserved for the on-screen readout; the overlay is deliberately clean
    // and the values are persisted, so nothing is drawn here yet.
}

Please either implement it or remove the claim from the README and delete the stub and its three call sites — as written, a user has no feedback at all about which style, amount or parameter value they just switched to.

10. Add a screenshot or GIF to the README. The mod is entirely visual and has four distinct styles with two axes of variation each; a still per style (or a short GIF) is the only way a user can tell what they're enabling. Only i.imgur.com and raw.githubusercontent.com are allowed as image hosts.

11. The toggle event is created with an over-broad DACL, in the wrong namespace. D:(A;;GA;;;WD) grants GENERIC_ALL — including DELETE, WRITE_DAC and WRITE_OWNER — to Everyone, on a Global\ object that is shared across sessions. All the documented shortcut needs is SetEvent, i.e. EVENT_MODIFY_STATE (0x0002), and the object should be session-scoped so a second logged-in user's shortcut doesn't toggle your overlay:

// EVENT_MODIFY_STATE only, and Local\ (per-session) rather than Global\.
ConvertStringSecurityDescriptorToSecurityDescriptorW(
    L"D:(A;;0x0002;;;WD)S:(ML;;NW;;;LW)", SDDL_REVISION_1, &psd, nullptr);

Given that the configurable global hotkey already covers "toggle from anywhere", it's also worth asking whether the named event, its SDDL, and the VBScript in the README are needed at all — dropping them removes this surface entirely.

Optional improvements

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

  • GetStringSetting (line 2221) reimplements WindhawkUtils::StringSetting; use the RAII wrapper instead of a manual get/free helper. The v ? v : L"" check is also unnecessary — Wh_GetStringSetting never returns NULL, it returns L"" when the value is unset or on error.
  • -lole32 and -lgdi32 in @compilerOptions appear unused — there are no COM (CoInitialize/CoCreateInstance) or GDI calls anywhere in the file.
  • Include the headers for what you use rather than relying on windows.h pulling them in: <cwctype> for towlower, <cstdlib> for _wtoi, <cstdio> for swprintf_s.
  • volatile bool g_running isn't a synchronisation primitive — use std::atomic<bool>. Same for g_workerThreadId, which the worker writes and WhTool_ModSettingsChanged / WhTool_ModUninit read from another thread.
  • SaveState calls three Wh_SetIntValues on every wheel notch (line 1937), so a fast scroll writes the mod's stored values dozens of times a second. Debouncing, or saving on hide/uninit, would be kinder.
  • Esc handled in WndProc (line 1848) calls Controller_Close()HideOverlays()delete on the very Overlay whose WndProc is executing. It happens to be benign because nothing touches self afterwards, but posting WM_VSH_CLOSE — exactly what the LL hook path already does — keeps destruction off the window's own stack frame.
  • g_spaceDown isn't reset in ShowOverlays/HideOverlays, so hiding the overlay while Space is held leaves it stuck at true and the hue ramps continuously on the next show until Space is pressed and released again.
  • SceneCtx::pal is set every frame but never read (every scene keeps its own pal_); SceneCtx::w/h are likewise only used by Render itself. Dead fields.
  • EnumMonProc communicates through the g_enumTarget global even though EnumDisplayMonitors already carries an LPARAM — just pass (LPARAM)&list and cast it back.
  • RegisterHotKey(nullptr, 0xC0DE, ...): for a thread hot key (hWnd == NULL) the documented id range is 0x00000xBFFF; 0xC0000xFFFF is reserved for GlobalAddAtom values used by shared DLLs.
  • fps, rotateSeconds and opacity are already clamped in LoadSettings, so the second ClampT at each use site (lines 1605, 1730, 2355) is redundant.
  • RegisterClassExW's return value is unchecked, the class is registered under GetModuleHandleW(nullptr) (windhawk.exe) even though lpfnWndProc lives in the mod image, and it's never unregistered. Harmless today because the process always exits via ExitProcess, but registering with the mod's own HINSTANCE and calling UnregisterClass on teardown is the convention.
  • kIID_ID2D1Factory doesn't need to be hand-rolled — mingw-w64's d2d1.h declares the interface with __CRT_UUID_DECL, so __uuidof(ID2D1Factory) works under Clang.

Functionality notes

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

  • CPU cost vs. the stated use case. Contours at maximal amount re-march ~46 levels over an ~18k-cell grid and rebuild up to 46 path geometries every frame, on top of resampling the whole noise field every 0.5 s — roughly a saturated core at the default 60 fps. The mod exists to babysit a long build/render/backup, so it is competing with the exact workload the user is waiting on. A default of 30 fps, and a line in the README about the cost, would fit the use case better.
  • rotateSeconds doubles as the per-scene hold duration (holdSecs in Render, line 1729): with rotation off a finished composition holds a hardcoded 8 s, with rotation on it holds for the rotation interval — two independent timers with the same period, which will race each other around the rotation boundary. A separate "hold" value (or just always 8 s) would be clearer.
  • Multi-display with monitor: all gives each overlay its own style/amount/param, but a click only affects the display that was clicked, and only g_overlays[0]'s values are persisted (line 2431). Either drive all overlays from one shared state or document that they're independent.
  • The wheel doesn't affect flow-field curves that were already traced. FlowScene::Step updates turns_ every frame, but AngleAt is only consulted in Spawn(), so turning the wheel mid-composition changes the field only for curves spawned afterwards — one picture ends up containing two different fields.
  • The friendly display name in the log is unlikely to be a model name. EnumDisplayDevicesW(mi.szDevice, 0, &dd, 0) returns the monitor's description, which is usually "Generic PnP Monitor" for every panel, not "DELL U2720Q" as the comment on line 1482 suggests. The resolution/position you already log is what actually lets a user identify a display; consider QueryDisplayConfig + DISPLAYCONFIG_TARGET_DEVICE_NAME if you want real names.
  • \\.\DISPLAYn numbering isn't guaranteed to match Windows Settings. It usually does, but Settings' "Identify" numbers come from the CCD/persisted display ordering, not from the GDI device name — the README states the equivalence more firmly than it holds.
  • Continuous scenes freeze during the fade-out, since Step isn't called in kPhaseOut (line 1747). Letting contours/growth keep advancing while artAlpha_ falls would keep the "never a static picture" property all the way through the transition.


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

1 participant