Code layout¶
src/
main.cpp App start-up: logging, Qt app, theme, MainWindow
MainWindow.{h,cpp} Composes the UI (game bar, action row, table, tabs, menus, status bar)
WindowManagerController.* Qt-facing facade over the window logic; emits statusChanged()
config/AppConfig.* windowmanager.json (games, last target, per-game profiles)
core/Logger.* Thread-safe rolling file logger (app log + one log per game)
core/BuildInfo.h Window title / About text: version, build number, date (from build_stamp.h)
core/Utilities.* String / path / timestamp helpers
core/IniText.* Pure logic: read / edit ini text in place (helixsr.ini, Unreal config files)
window/WindowManager.* Window policy: scan, move, resize, fit, launch, kill (no OS calls; uses platform())
window/MonitorHelper.* Monitor indexes and geometry, read through platform()
window/WindowStabilizer.h Re-applies a window's rectangle when the game moves it (per-platform .cpp)
platform/PlatformBackend.* Interface to the OS (windows, monitors, processes, screen / window capture, opening URLs) and platform()
platform/win32/Win32Backend.cpp The Windows implementation of PlatformBackend
platform/win32/Win32WindowStabilizer.cpp WinEvent hook behind WindowStabilizer
platform/x11/X11Backend.cpp The Linux implementation: xcb + EWMH for windows, QScreen for monitors, /proc for processes.
Wine image paths go through the prefix's dosdevices (Proton's S: is the Steam folder);
a whole-monitor rectangle becomes _NET_WM_STATE_FULLSCREEN; snapshots read the
window (captureWindow), as XWayland's root has no pixels
platform/PollingWindowStabilizer.cpp WindowStabilizer for systems without a move hook (checks every 100 ms)
platform/unsupported/ Stand-ins for features a platform lacks (the downscaled view on Linux)
window/KillPlanner.* Pure logic: which processes belong to this session
window/GameWatch.h Diagnostic log of the running game's windows, the foreground and the mouse clip
(per-platform .cpp: platform/win32/Win32GameWatch.cpp, unsupported/ stand-in)
window/GameWatchModel.* Pure logic: what changed about a game window, the foreground / mouse lines
window/FocusSwitch.* Pure logic: who has the keyboard (game / app / other), what the focus hotkey does,
hotkey text <-> modifiers + virtual-key code
window/FitCalculator.* Pure logic: fit-to-monitor maths (Fit::compute) and the
launch-settle trigger (Fit::Trigger)
window/Supersample.* Pure logic: letterbox, render size, parking position
window/SteamMatch.* Pure logic: Steam AppID from a window class / pasted text, the launch URL, and which window is the target game
window/SteamLibrary.* Installed Steam games: libraryfolders.vdf / appmanifest parsing, tool filter, main-exe choice (pure, tested) and the folder scan
display/DisplayModel.* Pure logic: modes / orientation, scale steps, brightness percent, WMI instance match,
a game's display settings (JSON), what to change (plan) and how to undo it (Restore)
display/DisplayBackend.h Interface to the system's display settings, and displayBackend()
display/DisplaySession.* A game's display settings: applied before the launch, restored when the game closes
platform/win32/Win32Display.cpp DisplayBackend on Windows: DisplayConfig, ChangeDisplaySettingsEx,
SetDisplayConfig, WMI + DDC/CI brightness, mscms colour profiles
platform/unsupported/DisplayUnsupported.cpp DisplayBackend stand-in (Linux): supported() == false
gameconfig/GameConfigModel.* Pure logic: a game's config settings (JSON), an Unreal Engine 4 game's config layout,
the keys per file, JConfig's monitor_misc.bin fields
gameconfig/GameConfigFiles.* Writes them into the game's files (copy of each original as *.wm-original), restores
stats/StatsModel.* Pure logic: GPU counter instance names, GPU / CPU / frame-rate maths, the slow-game
warnings, the log line; Sampler turns two raw samples in a row into GameStats
stats/PictureInfo.* Pure logic: which scaler shows the game's picture (Magpie, downscaled view, none), from
what size to what size, scale and direction, what is in the way; the overlay's rows
stats/SessionReport.* Pure logic: report samples (CSV), setups ("Magpie FSR 1600 × 900 → 1920 × 1080"),
per-setup summaries, session / comparison JSON, sessions.csv, setups merged over
sessions, the sharpness score (RMS Sobel gradient)
stats/StatsBackend.h Interface to the game's resource use, the GPU setting, power plans / mode, and statsBackend()
stats/DriverModel.* Pure logic: installed driver per card, NVIDIA's version number, NVIDIA lookup ids / answer
stats/DriverCheck.* Asks NVIDIA's driver lookup for the newest driver (Qt Network)
platform/win32/Win32Stats.cpp StatsBackend on Windows: process times / memory, PDH GPU counters, DXGI adapters,
an ETW real-time session for present events, power status and plan
platform/unsupported/StatsUnsupported.cpp StatsBackend stand-in (Linux): supported() == false, no bar
helix/HelixRelease.* Pure logic: versions, GitHub release parsing, checksums, update decision
helix/HelixGame.* Pure logic: where a game's FSR 3.1 DLLs are, installed / replaced state
helix/HelixPaths.* Pure logic: on-disk layout, built-package marker, post-build leftovers
helix/HelixIni.* Pure logic: read / edit helixsr.ini (Sharpener, Sharpness, WaveSize), keeping the rest of the file
helix/HelixSystem.* Pure logic: GPU family from the name / SIMD width, can this PC run HelixSR (Verdict)
helix/HelixBridge.* Pure logic: HelixSR 2.x entry points: the .ps1's tool downloads, build / status /
bridge arguments, the bridge's JSON replies
helix/SystemProbe.* DXGI + Direct3D 12 probe of the adapters, free disk space, tar.exe
magpie/MagpieConfig.* Pure logic: Magpie's config.json: scaling modes, add / update / remove the app's
per-game profiles ("WindowManager: <game>"; 3D game mode off by default)
magpie/MagpieRelease.* Pure logic: newest Magpie release (x64 zip with a SHA-256 digest), <app>/magpie,
installed-version marker, what an update keeps (config, cache, logs)
magpie/MagpieProcess.* Running Magpie.exe processes (whose), WM_MAGPIE_QUIT, start in the tray, the game
window's facts, what Magpie scales now (its scaling window's properties) (Windows)
magpie/MagpieSetup.* The app's portable Magpie: download, verify, unpack, replace, first start
helix/HelixSetup.* Download, verify, unpack, tools, build (HelixSR's build script), clean up; per-game install
ui/Theme.* Dark palette + stylesheet; button roles and "active" state
ui/GameBar.* Game dropdown + Add / Start / Restart / Kill
ui/WindowTable.* Table of the target's windows (+ empty-state message)
ui/ControlPanels.* Tab pages: StatePanel, PositionPanel, PresetPanel, CapturePanel
ui/SnapshotCapture.* Screenshot / burst capture
ui/GameRegistry.* Saved-game helpers (names, entries, snapshot folders) and syncSteamGames (Steam libraries -> saved games)
ui/FitDialog.* "Fit window to monitor" popup (measurements + correction)
ui/ScaledView.* Downscaled view: overlay window + DWM thumbnail of the game
ui/InfoDialogs.* About dialog
ui/HelpDialog.* Help > Help topics: topic list + search over docs/help/*.md (embedded via the .qrc);
web links and Help > Online wiki open the wiki (wikiUrl)
ui/LogViewerDialog.* File > View logs...: app and per-game log viewer
ui/MagpiePanel.* Magpie tab: set up / update the app's copy of Magpie; the selected game's effect
ui/MagpieSession.* Magpie for the game being started: profile from its settled window, start, close
ui/HelixPanel.* HelixSR tab: set-up / update with a timer, list of confirmed games, run
ui/DisplayPanel.* Display tab: this PC's display settings, and the selected game's own
ui/GameConfigPanel.* Game config tab: render scale, resolution and JConfig window for the selected game
ui/FocusMonitor.* Reads the foreground every 250 ms: Focus: game / app / other (FocusSwitch::ownerOf),
and the foreground program's file name
ui/StatsMonitor.* Samples the game once a second whenever it runs (and Magpie.exe's GPU use); logs a
summary; statsUpdated() feeds the views
ui/SessionRecorder.* Records each game session into reports/<game>/ (CSV per second, JSON summary)
ui/PictureCompare.* Compare picture: the game's frame (PrintWindow) and Magpie's picture (screen copy) at
the same moment, a bilinear stretch, sharpness scores, the side-by-side PNG
ui/ReportCharts.* The Reports tab's QPainter charts (session timeline, setups bar table), ImageView
ui/ReportsPanel.* Reports tab: a game's sessions, the Session / Setups / Pictures views, Larger view
ui/GameOverlay.* Game info overlay: see-through panel in a corner of the game's monitor (click-through,
topmost above Magpie, left out of captures by default)
ui/MiniBar.* The floating bar the app minimizes to (frameless, topmost, takes no focus)
ui/GpuPanel.* GPU & Power tab: graphics cards and drivers (+ NVIDIA update check), the card in use,
Windows' per-exe graphics setting, power plan and power mode
tests/ QtTest suites (see below)
resources/ Icon, arrow SVGs used by the stylesheet, Windows .rc, helixsr_driver.py
How the pieces talk¶
buttons / table ──click──▶ WindowManagerController ──▶ WindowManager
▲ │
└────── refresh() ◀── statusChanged()
WindowManager / MonitorHelper ──▶ PlatformBackend (interface) ──▶ Win32Backend
- UI classes never call Win32 window APIs for the managed game. They call a
WindowManagerControllerslot, which runs the action, re-syncs the stabilizer and emitsstatusChanged(). MainWindow::refresh()is the single place that re-reads state: it updates the status bar, the monitor/stability buttons, the table, and callsrefresh()on each panel (which sets the green "active" buttons).WindowTableonly emitswindowChosenwhen the user selects a row, so refreshing the table cannot feed back into another refresh.
Adding a control¶
- A button on an existing tab: add it in that panel's constructor (
ControlPanels.cpp) withTheme::button(...)and connect it to a controller slot. If it shows a toggled state, set it in the panel'srefresh()withTheme::setActive. - A new tab: derive from
ControlPanel, implementrefresh()if needed, create it inMainWindow::buildUi()and add it to bothm_tabsandm_panels. The tab area sizes itself to the current page, so no height bookkeeping is needed. - A new window action: add a method to
WindowManager(it callsplatform(); a new OS primitive goes onPlatformBackendand in every backend), a slot onWindowManagerControllerusingfinish(ok, okMsg, failMsg), then connect it.
Styling¶
All colours live in ui/Theme.cpp. Widgets pick up variants through dynamic
properties rather than inline stylesheets:
role="accent"/role="danger"- emphasised buttons (Theme::button(text, parent, "danger"))active=true- a toggle that is currently on (Theme::setActive(button, on))
Spin boxes are styled in the same sheet; their arrows are the SVGs in resources/.
Fit to monitor¶
Start ──▶ controller.launchTarget() ──▶ armFitCheck() (Fit::Trigger.arm, 500 ms timer)
│
every tick: WindowManager::findPrimaryWindow() ─▶ Fit::Trigger.update()
│ (same hwnd + same rect for ~1.8 s)
▼
controller emits fitProposed(true) ──▶ MainWindow::showFitDialog()
(skipped for a game Magpie scales: MagpieSession::handles; by hand: fitProposed(false))
│
FitDialog ◀── measureFit(request) ──────────┤ recomputed on every option change
│
Apply ──▶ controller.applyFit(request) ──▶ WindowManager::applyFit()
Fit::computetakes the window rectangle, the client rectangle (screen coordinates), and the target rectangle, and returns the insets, the desired window rectangle and the correctiondx, dy, dw, dh. WithstripFramethe insets are zero. It has no Win32 dependency and is unit-tested (tst_fit).Fit::Triggerdecides when to propose: the window must exist, be usable and keep the same rectangle for the settle time; it fires once perarm()and times out after two minutes. Also unit-tested.WindowManager::findPrimaryWindowpicks "the game": visible, not minimized, at least 10,000 px^2, not a console/IME helper window; ready windows beat everything, then titled ones, then the largest.WindowManager::measureFitreadsGetWindowRect, the client rectangle viaGetClientRect+ClientToScreen, and the DWM extended frame bounds.applyFitremoves the frame (if requested), then measures and corrects up to three times, because some games move their window again right after a move, and finally re-measures to report whether the fit is exact.
Downscaled view¶
DownscalePanel ── plan() ──▶ Supersample::renderSize() how big the game window must be
│ Start
▼
ScaledView::start()
1. overlay: borderless, topmost, WS_EX_NOACTIVATE QWidget placed on the target monitor and pinned
there (its WM_WINDOWPOSCHANGING forces the monitor's rectangle: a monitor with another display
scale made Windows / Qt resize it, once to Qt's default 640x480)
2. game window: restore, popup style (no frame), size = render size,
parked at Supersample::parkingOrigin() (behind the overlay, or beyond the desktop edge)
3. DwmRegisterThumbnail(overlay, game) with the destination rectangle from
Supersample::letterbox(); source = client area only
4. SetForegroundWindow(game): keyboard / gamepad stay with the game
│ every 400 ms
▼
tick(): game gone -> stop(); restore if minimized; put the size back (Supersample::ReassertPolicy:
every time, but at most 5 times within 10 s, then follow the game until it is quiet);
update the thumbnail rectangle (the game's shape may change)
stop(): unregister thumbnail, delete overlay, restore the game's original style + rectangle
- The game window is switched to a popup style because Windows limits a normal window to about the virtual desktop size; a popup is not clamped, so it can be 2x-4x the monitor.
- The mirror is a DWM thumbnail, so there is no copy through this app's memory. GPU-rendered (flip-model) windows are mirrored in full even beyond the monitor edge; GDI windows are only painted inside the desktop, so only that part shows.
- Stop is available three ways because the overlay covers the screen: the panel button,
the global hotkey Ctrl+Alt+D (
RegisterHotKey+MainWindow::nativeEvent), and automatically when the game window disappears or the app closes. - Mouse forwarding. The overlay is a small
QWidgetsubclass whosenativeEventhands raw mouse messages (exact physical pixels, no DPI rounding) toScaledView::handleMouse. It maps the point withSupersample::mapPoint(overlay pixel -> game client pixel, using pixel centres: a 2x picture maps overlay pixel n to game pixel 2n+1) andPostMessages the same message to the game window with the mapped client coordinates. Wheel messages carry screen coordinates, so they are converted to the overlay's client space first and sent back as the game window's screen origin + mapped point. Points over the letterbox bars are swallowed; while a button is held the overlay has mouse capture and positions are clamped to the picture edge so a drag can leave the picture. The game keeps keyboard focus throughout. Games that read the mouse via raw input, DirectInput orGetCursorPoscannot see posted messages.
Display settings¶
DisplayPanel -- Apply / combos --> displayBackend() --> Win32Display
| (DisplayUnsupported on Linux)
| "While the game runs" --> AppConfig GameEntry::display (DisplayModel::GameSettings)
v
MainWindow::launchWhenFree --> DisplaySession::gameStarting(exe) --> controller.launchTarget()
1. DisplayModel::plan(mode only) -> setMode(temporary) (CDS_FULLSCREEN)
2. re-read, plan(the rest) -> setHdr / setScale / setBrightness
3. DisplayModel::undoFor + merge -> Restore, written to windowmanager.ini
every 2 s: WindowManager::isExeRunning
seen, then gone 3 ticks / never seen in 3 min -> restore()
restore(): resetMode (back to the stored mode), HDR, scale, brightness; also on a failed launch,
"Restore now", another game starting, the app closing, and recoverFromCrash() at start-up
DisplayBackendis separate fromPlatformBackend: that one is about the game's windows, this one about the PC's monitors. Monitors are named byDisplayModel::Monitor::id, the monitor's device path fromDISPLAYCONFIG_TARGET_DEVICE_NAME, which survives restarts (the\\.\DISPLAYnname does not).- Modes.
Monitor::modesis kept in landscape;orientedModesturns it into what is shown. The panel applies a mode withCDS_UPDATEREGISTRYand asks to keep it (15 s, then the old mode is set again); a game's mode usesCDS_FULLSCREEN(not stored) and is undone withChangeDisplaySettingsEx(NULL). - HDR.
DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO_2/SET_HDR_STATE(Windows 11 24H2), else the older advanced-colour calls. The structs are declared locally because the SDK only declares them for a newerNTDDI_VERSION. HDR is only switched on a display that reports HDR support: on 24H2 the old call would toggle Auto Color Management instead. - Scale. Undocumented
DisplayConfigGetDeviceInfo/SetDeviceInfotypes -3 / -4 on the source: steps relative to the recommended scale (DisplayModel::scaleFromRelative,kScaleSteps). The steps depend on the resolution, so a game's scale is applied after its mode, and restored after the mode is back. - Brightness. A built-in panel: WMI
root\WMIWmiMonitorBrightness/WmiSetBrightness, matched to the monitor byDisplayModel::wmiInstanceMatches. Otherwise DDC/CI (dxva2:GetMonitorBrightness); it is slow (tens of ms per command), so the slider sends at most every 250 ms. - Colour profiles.
ColorProfileGetDisplayList/GetDisplayDefault/SetDisplayDefaultAssociation(mscms, Windows 10 1809+), looked up withGetProcAddress; the SDR or HDR default by the HDR state. - Arrangement.
QueryDisplayConfig(QDC_DATABASE_CURRENT)gives the topology,SetDisplayConfig(SDC_APPLY | SDC_TOPOLOGY_*)sets it. Night light has no API: the button opensms-settings:nightlight. - The panel reads the monitors when the tab is shown, after each change and on Re-read, not in
refresh()(DDC/CI reads are slow).
Game stats and the game info overlay¶
StatsMonitor (QTimer 1 s, always on where statsBackend().supported())
-- pid --> thread pool: statsBackend().sample(pid) --> RawSample
pid: controller.activeWindowPid(), else WindowManager::runningPid(target) every 5 s
<-- queued -- Stats::Sampler::update(pid, raw, adapters) --> GameStats --> game log
statsUpdated --> GpuPanel, MainWindow::updatePicture
MainWindow::updatePicture (each sample)
Picture::Facts <-- Magpie::windowFacts(active window): drawn area, monitor + Hz, maximized / full screen
<-- Magpie::scaling(): FindWindow(Window_Magpie_967EB565-...), GetProp Magpie.SrcHWND / Src* / Dest*
<-- the game's Magpie setting, the app's Magpie running (MagpieSession::handles: starting)
<-- ScaledView render / shown size, FocusMonitor (game in front: else Magpie is paused)
<-- GameConfig::status(): render scale / resolution in the game's files (on game change, every 10 s)
Picture::evaluate --> Result (scaler, from, to, %, problems) --> status bar label, [Picture] log line on change
Picture::overlayLines(facts, result, GameStats) --> GameOverlay::display(title, lines, monitor rect)
Reports¶
MainWindow::updatePicture (each sample) --> SessionRecorder::add(exe, facts, result, stats)
first sample of a run: reports/<folderName(game)>/<yyyyMMdd-HHmmss>-samples.csv, header
each second with a game window: Report::Sample (setup = Report::setupName(scaler, state, from, to, window))
appended + flushed
game gone / another game / app closing: Report::summarize --> <stamp>.json, a row in reports/sessions.csv,
sessionSaved --> ReportsPanel::showGame (< 10 samples: CSV deleted)
ReportsPanel (refresh: tab shown, Refresh, a session or comparison saved)
reports/*/*.json --> Report::sessionFromJson / comparisonFromJson
Report::compareSetups(sessions) --> setup order (colours) and SetupChart (states left out)
a session picked --> its samples CSV --> SessionChart
PictureCompare::start(game, "Magpie FSR"): Magpie::scaling() must show the game's window
focusTarget() (Magpie draws live only while the game is in front), 2 s; captureStarting (MainWindow hides
itself, overlay opacity 0), 250 ms; BitBlt of Magpie's Dest rect, PrintWindow(PW_RENDERFULLCONTENT) of the
source window cut to Magpie's Src rect; captureDone
bilinear = source scaled to Dest size (Qt::SmoothTransformation); Report::sharpness of all three
PictureCompareImage::sideBySide: the 320 x 180 region with the most edge detail, 2x nearest-neighbour
reports/<game>/compare-<stamp>.png / -game.png / -bilinear.png / -scaled.png / .json
- Colours: the data-viz reference palette's dark steps, assigned by the game's setup order (first seen first),
never by rank; starting / paused / not scaling are grey (
ReportColors::isState). One value axis per plot: FPS and GPU % are two plots on one time axis. Hover: a crosshair and the second's values (SessionChart), a row's figures (SetupChart). - Sharpness: the RMS of the Sobel gradient of the luminance, not the mean: a blurred edge keeps the mean gradient (the same change over more pixels) but lowers the RMS. Compared at the same size (bilinear vs Magpie); the game's own frame is scored at its own size, for reference only.
-
Magpie's GPU:
StatsMonitorlooks up a running Magpie.exe every 3 s (the app's copy first) and passes its pid toSampler::update, which reads it from the same GPU engine counters (GameStats::magpieGpu). -
Overlay window:
Qt::Tool | FramelessWindowHint | WindowStaysOnTopHint | WindowDoesNotAcceptFocus | WindowTransparentForInput,WA_TranslucentBackground; rich-text table in a QLabel on a painted rounded panel. Placed withSetWindowPosin physical pixels (moved onto the game's monitor first so it takes that monitor's scale, then into the corner from its size there). Each update re-assertsHWND_TOPMOST: in full-screen mode Magpie puts its scaling window above all topmost windows when the focus changes.SetWindowDisplayAffinityWDA_EXCLUDEFROMCAPTUREkeeps it out of screen copies (the snapshot's BitBlt, recordings) unless In screenshots and recordings is ticked. Global hotkey Ctrl+Alt+I (id 5) toggles it. Magpie's overlap check (_CheckForegroundFor3DGameMode) only looks at the foreground window, which the overlay never is. -
CPU:
GetProcessTimesandGetSystemTimesdeltas between two samples (Stats::cpuPercent, 100 % = all cores). - GPU: the PDH wildcard counters
\GPU Engine(*)\Utilization Percentageand\GPU Process Memory(*)\Dedicated Usage. Instances are namedpid_<pid>_luid_0x<high>_0x<low>_phys_0[_eng_<n>_engtype_<type>](Stats::parseGpuInstance); the use is the busiest engine type summed per adapter, as in Task Manager, and the LUID names the adapter through DXGI. One collection costs tens of ms, hence the thread pool. - FPS: a real-time ETW session (
WindowManager-Frames, QPC timestamps, 1 s flush) with the DXGI provider (Present_Start42,PresentMultiplaneOverlay_Start55) and the D3D9 provider (Present_Start1), filtered to those ids. The callback keeps the target pid's timestamps;Stats::frameRatemeasures up to the newest present, because events arrive up to a second late. Starting the session needs administrator rights (or Performance Log Users):FpsSource::NoPermissionotherwise. A session left behind by a crash is stopped and restarted;MainWindow::closeEventstops it. - Power:
GetSystemPowerStatus,PowerGetActiveScheme/PowerReadFriendlyName, and for the Balanced plan the power mode fromPowerGetEffectiveOverlayScheme(looked up withGetProcAddress). - Graphics setting (GPU tab): the
REG_SZvalue named after the full exe path underHKCU\Software\Microsoft\DirectX\UserGpuPreferences, e.g.AppStatus=4096;GpuPreference=2;(Stats::gpuPreferenceFrom/withGpuPreferencechange only theGpuPreferenceentry). The card each choice means comes fromIDXGIFactory6::EnumAdapterByGpuPreference. DirectX reads it when the game starts. - Drivers: DXGI gives each adapter's PCI vendor / device id; the display class key
HKLM\SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\NNNNgivesMatchingDeviceId,DriverVersionandDriverDate(Driver::matchesDevice; re-read on everyadapters()call). NVIDIA's number is the last five digits (32.0.16.1742->617.42).DriverCheckfetcheslookupValueSearch.aspx?TypeID=2(series) andTypeID=3(products) once per run, picks the product's notebook or desktop series (a battery means notebook;Driver::findNvidiaIds), thenAjaxDriverService.php?func= DriverManualLookup(osID 57, DCH, WHQL) for the newest driver. - Power plans:
PowerEnumerate(ACCESS_SCHEME)lists every plan,PowerSetActiveSchemeswitches. The power mode isPowerGetEffectiveOverlayScheme/PowerSetActiveOverlayScheme(byGetProcAddress; Balanced plan only). - Floating bar:
MainWindow::changeEventsees the minimise and callsshowMiniBar()(hide this window, show theMiniBar);restoreFromMiniBar()reverses it.MiniBaris a top-levelQt::Tool | FramelessWindowHint | WindowStaysOnTopHint | WindowDoesNotAcceptFocuswindow withWA_ShowWithoutActivating, so it never takes the keyboard from the game; dragging usesQWindow::startSystemMove. It shows the game's name only (the stats are on the overlay). - Window list refresh:
WindowManager::refreshWindowsre-reads the target's windows without logging, keeps the active window while it exists (bestWindowotherwise) and says whether anything changed; the controller'srefreshWindowsslot emitsstatusChangedonly then. Called onGameWatch::windowsChanged,StatsMonitor::gameChanged, and every 3 s while this window is shown.scanWindowsForTarget(Scan) still picks the best window. The table marks the active row (bold, Status Active; green selection inTheme.cpp). - One instance (
main.cpp): aQLocalServernamed after the user and a hash of the install folder. A second start connects, sendsshow(afterAllowSetForegroundWindow) and exits; the first callsMainWindow::bringToFront. A copy in another folder (a test build) is a separate instance. - Off the game's monitor:
MainWindow::keepOffGameMonitorruns 400 ms after a controller status change, aGameWatch::windowsChangedor the downscaled view starting / stopping; if this window is on the game's monitor (gameMonitorIndex: the view's monitor while it runs, else the active window's) it moves to another one. - Release the mouse: the global hotkey Ctrl+Alt+M (
RegisterHotKey, like Ctrl+Alt+D) callsClipCursor(nullptr)andbringToFront; a game that holds the mouse lets go once it loses the foreground. - Game watch (diagnostics, Windows):
StatsMonitor::gameChanged(pid)startsGameWatch::watch(pid)(0 stops it). It installs out-of-context WinEvent hooks:EVENT_OBJECT_CREATE..HIDEandEVENT_OBJECT_LOCATIONCHANGEfor the game's process (top-level windows only, read 300 ms after the last report), andEVENT_SYSTEM_FOREGROUNDfor all processes;GetClipCursoris read every second.Watch::WindowTrackerturns the readings into change lines. The downscaled view logs its own layout (ScaledView layout:) fromupdateThumbnail, on change only. Stats::warningsFordecides the warnings; the bar logs each once per game run and asummaryLine10 s after the game is first seen and then every minute.
Game config¶
GameConfigPanel -- combos --> AppConfig GameEntry::gameConfig (GameConfig::Settings, JSON "GameConfig")
| Write now / Restore originals
v
MainWindow::launchWhenFree --> GameConfig::apply(exeForConfig(exe, childExePath), settings) (before the display
settings and the launch: the game reads its files at start-up)
apply / restore, per file of GameConfig::layoutFor(exe):
<file>.wm-original exists? -> the original; else the file itself (and nothing to do if the settings don't touch it)
entriesFor(file, settings, original) -> IniText::apply(current, entries, "=") -> write if changed (copy kept first)
monitor_misc.bin: readJConfig -> jconfigFor(settings, original) -> writeJConfig
- Layout.
layoutForexpects<root>/<Project>/Binaries/Win64|Win32/<exe>:DefaultEngine.iniandDefaultGameUserSettings.iniin<Project>/Config,WindowsEngine.iniin<root>/Engine/Config/Windows, JConfig'smonitor_misc.binnext to the exe. The tab is usable whenDefaultEngine.iniexists (loose config, as in Tekken 7's arcade builds: noSaved/folder, nothing packed). - Keys.
[SystemSettings]outranks the scalability groups and is the only placer.ScreenPercentagemay exceed 100 (sg.ResolutionQualityis capped at 100 by the engine, so it getsmin(scale, 100)).r.Upscale.Quality=3goes with any render scale. The resolution goes toResX/ResY,ResolutionSizeX/Y,r.setresand JConfig's DX resolution. - Edits in place. Only the managed keys change; a setting left at 0 takes the original's value (or removes the key the
original did not have), so the user's own edits elsewhere survive, and Restore originals is
apply(Settings{})plus deleting the copies.IniText::applyreturns the text untouched when nothing changes; otherwise trailing blank lines go. The encoding (UTF-8, UTF-8 with BOM, UTF-16 LE) and line endings stay. - JConfig (
monitor_misc.bin, 244 bytes = 61 little-endian int32): offset 60/64 window position, 68/72 window size, 76/80 DX resolution, read from the user's file and the game's first window (2026-10-20). Other values are kept; a file of another size is left alone. JConfig's own dialog reads the same file. .batgames:exeForConfiguseschildExePathwhen the script is not insideBinaries.
Focus switch¶
FocusMonitorpollsplatform().foregroundWindow()(GetForegroundWindow; X11_NET_ACTIVE_WINDOW) every 250 ms.FocusSwitch::ownerOfcompares the foreground pid with this app's, the active game window's (controller.activeWindowPid()) and whether the foreground process runs the target exe (read once per pid).MainWindow::updateFocusSwitchshows the result in the status bar and registers the hotkey (RegisterHotKey, id 3, defaultF1fromwindowmanager.inifocusHotkey) only while a game is found, so F1 is not taken from other programs (nor from this app's help) the rest of the time.WM_HOTKEY->switchFocus(): the game has it ->takeForeground(), elsereleaseForeground()+controller.focusTarget().takeForeground():bringToFront(); if Windows refused,AttachThreadInputto the foreground thread for theSetForegroundWindow; thenLockSetForegroundWindow(LSFW_LOCK)while any window of this process is in front. 0.5 s later the foreground is checked and logged. Tekken 7 + JConfig takes it back even so (reproduced with a test program that attaches to the foreground thread's input; disabling the game window withEnableWindowdid not stop it either), so thensetMouseOnly(true):WS_EX_NOACTIVATEon this window, so clicks reach the buttons without activating it and there is no tug of war. Off when the owner becomesNone(the game closed).- Window opacity (View > Window opacity,
opacity/opacityGameOnlyinwindowmanager.ini):MainWindow::applyOpacity->QWidget::setWindowOpacity(a layered window on Windows; needs a compositor on X11). With Only while a game runs it followsFocusMonitor(100 % while the owner isNone); called fromupdateFocusSwitch. - Terminate key (id 4, default
F2,killHotkey):FocusSwitch::DoublePress(2 s); the first press beeps (MessageBeep) and logs, the second callscontroller.killTarget()(Shutdown without the confirmation box, which cannot be answered while the game has the focus).
HelixSR¶
HelixPanel -- Set up / Update --> HelixSetup::installOrUpdate()
Checking GET api.github.com/.../releases -> Helix::latestRelease() (newest non-prerelease zip up to 2.x, kSupportedMajor)
Downloading zip -> SHA-256 against the asset digest (or SHA256SUMS.txt); mismatch = deleted
Extracting tar.exe -xf -> helixsr/HelixSR-<version>/
Tools Helix::setupDownloads(helixsr-setup.ps1) -> Python + numpy, DXC into %LOCALAPPDATA%\HelixSR
(each SHA-256 checked; skipped when there)
Building python -I setup/helixsr_setup.py <package> --dxc <dxc> --yes --no-fg (DLSS DLL, network; ~5-6 min)
then --status must say network=1
Cleaning Helix::buildLeftovers() + older packages deleted, markBuilt()
HelixPanel -- Install / Run --> HelixSetup::installIntoGame()
2.x: python -I setup/helixsr_bridge.py <package> install --game <game root> --way fsr (or remove)
1.x: python helixsr_driver.py <package> install|remove <game root>
HelixPanelrunsprobeSystem()+Helix::evaluate()at start-up, on Check this PC and before every set-up:Unsupportedstops it,Experimentaladds a warning to the confirmation. The rules live inHelixSystem.cpp(familyOfreads the name, or the SIMD width for generic names like "AMD Radeon(TM) Graphics": 32 lanes = RDNA, 64 = GCN / Vega).- The decisions are in the pure
helix/units (tst_helix);HelixSetuponly runs the steps and reports them (stateChanged,buildFinished,gameJobFinished). One job at a time. - A package counts as built only when
.windowmanager-builtexists in it, so a failed or cancelled build is never used and is removed. The set-up prompt (the DLSS download) is answered with--yesafter the user agrees inHelixPanel. - HelixSR 2.x: never
helixsr-setup.ps1. Its last step runshelixsr_install.py, which with-Yesinstalls HelixSR Scaler into every game it finds. The app does the steps before it itself:Helix::setupDownloadsreads the.ps1's$Downloadstable (URLs and SHA-256 change per release),HelixSetup::prepareToolsfetches and unpacks what is missing,buildNetworkrunshelixsr_setup.py(Helix::setupBuildArgs, always--no-fg),finishBuildchecks--status. Games go through HelixSR's JSON bridge (Helix::bridgeArgs, always--way fsr: the bridge's ownautopicks HelixSR Scaler);Helix::parseBridgeReplyreads its reply (protocolmust bekBridgeProtocol).Helix::hasBridge(package)picks the bridge or, for a 1.x package still on disk,resources/helixsr_driver.py, which imports HelixSR 1.x'shelixsr_installmodule and installs the FSR drop-in for one folder. HelixSR's files are never edited. - After an install or removal the result is judged from the files (
Helix::scanGame), not from the exit code.scanGamelooks foramd_fidelityfx_upscaler_dx12.dll/amd_fidelityfx_dx12.dll(not*.original.dll) under the game root (gameRootFor: the exe folder, or the game folder when the exe is in bin / x64 / Win64 / Binaries). "Installed" means the DLL containsHelixSR x.y.z; "replaced" means HelixSR's record and backup exist but the DLL is the game's again. - The HelixSR list shows every saved game; a game whose scan finds no FSR 3.1 DLL is disabled (greyed out, not
selectable).
helixConfirmedinwindowmanager.jsoncaches the last result, so usable games sort first. - Scans are asynchronous.
Helix::scanGameAsync(context, exe, done)runsscanGameon the global thread pool and callsdoneon the context's thread (dropped if the context is gone).HelixPanel::rebuildGamesshows the list at once ("checking...") and fills each row as its scan returns; a generation counter drops the results of an older list.HelixSetup::runGameJobmarks itself busy first, so nothing can overlap. - Installer self-test.
HelixSetup::testDriverbuilds a throw-away game underhelixsr/selftest, runs the installer to install and remove, and checks the DLL changed and was restored byte for byte. On success the installer's fingerprint (HelixSetup::installerFingerprint: the bridge script and protocol, orhelixsr_driver.py) is stored in the package's.windowmanager-builtmarker, so it is not repeated until the installer or the package changes. A bridge reply with another protocol fails the test. The 1.x helper exits with code 2 (driver-incompatible: ...) when HelixSR's functions no longer match (ImportError / AttributeError / TypeError / NameError); a failed test setsdriverOk() == false, whichcanInstall()and the panel's buttons respect. HelixIni::applyIniedits the existing text line by line (section and key lookup is case-insensitive), so comments, unknown keys and the file's line endings survive.
Process termination¶
WindowManager::killTarget() snapshots running processes and asks
KillPlanner::plan() which to terminate. The plan starts from the pids this app
launched or detected, the active window's pid, and processes whose image path is the
selected target; adds all their descendants; and adds loader processes only if they
started after the launch and are in that tree or orphaned. The ignore list and the
app's own pid are never included. Because KillPlanner has no Win32 dependency it is
unit-tested directly.
Tests¶
| Suite | Covers |
|---|---|
tst_utilities |
name sanitising, unique ids, string/path helpers, size formatting |
tst_killplanner |
session scoping: unrelated loaders spared, descendants, orphans, pid reuse, ignore list |
tst_gamewatch |
the watch's lines: first sight of a window (hidden helpers left out until shown), size / place / frame / visibility / title changes, unchanged and class-only changes not logged, closed windows, other programs' titles kept out of the foreground line, mouse clip vs. free |
tst_fit |
fit maths (framed, borderless, negative-origin monitor, overscan, applied plan is stable) and the settle trigger (fires once, restarts on move/new window, ignores unusable windows, times out) |
tst_supersample |
letterbox (side bars, top/bottom bars, never exceeds the area), render size per factor and shape, parking position (behind the overlay vs beyond the desktop, with a monitor above/next to it), mouse mapping (pixel centres, black bars ignored, clamping while dragging, never past the last pixel), putting the game window back (every focus change; a fast loop stopped, then tried again after 10 s) |
tst_steammatch |
AppID from steam_app_<id> classes and pasted text (store URL, run URL, bare number), AppID beats a Wine loader image, another Steam game never matches, path / exe-name fallback |
tst_windowmanager |
WindowManager and MonitorHelper against a fake PlatformBackend: which windows a scan keeps and their order, the quiet refresh (keeps a chosen window, follows a resize, a closed splash screen replaced by the game window), borderless fullscreen, centring on another monitor, missing monitors, toggle messages, kill scope |
tst_steamlibrary |
libraryfolders.vdf (current and old format, Windows paths), appmanifest fields / install state / escapes, tool filter, main-exe choice (redistributables, crash reporters, Unreal shipping exe), a fake Steam folder with two libraries, and the sync into the saved games (add, keep the user's name, AppID for a game added by hand, uninstalled vs. drive not connected, moved exe) |
tst_x11backend |
Linux only: the real X11 backend against an xterm (list, title, pid, move/resize, frame strip/restore, process info, terminate); skips itself without a display |
tst_appconfig |
save/load round-trip, clearing profiles, corrupt/missing files, legacy keys, the HelixSR flag |
tst_display |
mode lists (sizes, refresh rates, best mode, portrait), scale steps relative to the recommended one, brightness percent, WMI instance matching, a game's display settings (JSON, sanitising, description), choosing the monitor, the plan (only what differs and the display supports) and the undo record (first value wins, JSON); on Windows a read-only probe of this PC's displays is printed |
tst_stats |
GPU counter instance names, busiest engine and video memory per process, CPU percent, frame rate and 1 % low (late and out-of-order presents, stale), dedicated adapter, the warnings, short GPU names, reading / editing the graphics-setting value, the compact stats line, driver matching / dates / NVIDIA versions, |
| NVIDIA lookup ids (notebook vs desktop) and its answer, the log line, the sampler across samples and processes; on Windows a live sample of the test process is printed | |
tst_report |
sample CSV round trip (quoted setup, unknown values, header and broken lines skipped), setup names per state, segments per setup (first seen first, unknown values left out, peaks), session JSON / sessions.csv (nothing private), folder names, setups merged over sessions (weighted by time), comparison JSON, sharpness (hard edge beats a soft one, flat is 0) |
tst_picture |
scale percent (letterboxed: the smaller ratio) and direction; Magpie upscaling, shrinking a window bigger than its monitor (the Tekken case), not running / starting / maximized / paused / not scaling; the downscaled view before Magpie; no scaler (full screen, too big, no window); the overlay's rows (game files, render scale, monitor Hz, FPS, load, power, warnings) |
tst_gameconfig |
layout from the exe path (Win64 / Win32, native separators, not Unreal), the .bat game's exe, the keys per file (render scale above / below 100, resolution, left settings take the original or are removed), JConfig's fields (read / write / other bytes kept / wrong size), JSON and sanitising; against a fake Tekken folder: write (CRLF kept, copies only of changed files, nothing on a second run), a user's edit surviving, restore (originals back, copies gone), UTF-8 BOM kept, a game without config |
tst_focusswitch |
who has the focus (game, a second game process, app, other, no game), the toggle, labels, the press-twice rule of the terminate key, hotkey parsing (F-keys, letters, digits, named keys, modifiers, invalid) and text round trip |
tst_helix |
version parsing/ordering, release choice (no prereleases, the right zip), checksums, update decision, DLL version, game root, FSR DLL detection and installed / replaced state, async scan delivery, built-package marker and driver fingerprint, post-build leftovers, helixsr.ini parsing and in-place editing, GPU family and the can-this-PC-run-it verdict (a live probe of the test machine is printed too) |
The UI and the real OS backend (platform/win32/) are not unit-tested; they are checked by running the
app. New logic that can be written without Win32 or widgets (like KillPlanner) should
go in a Win32-free unit with a test.
Documentation¶
README.md What the app is for, getting started, link to the wiki
docs/index.md Wiki home page (MkDocs site, https://robertotorino.github.io/WindowManager/)
docs/wiki/*.md The user documentation in full
docs/development/*.md Building and testing, the docs site
docs/ARCHITECTURE.md, LINUX_PORT.md, HELIXSR_2.md, MAGPIE.md Development notes (also in the wiki)
docs/help/*.md In-app help: the highlights only, each page links to its wiki page;
embedded via resources/resources.qrc, excluded from the site
docs/assets/ Logo, favicon, extra.css (the blue theme), screenshots/ (placeholders)
overrides/ Theme overrides: the header (partials/header.html), no tabs on the home page and
the footer's link columns (main.html)
mkdocs.yml Site settings and navigation
.github/workflows/docs.yml Builds the site (--strict) and publishes it to GitHub Pages
See The docs site.