Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ are frozen history cited from code, not kept in step with it.
header by bare name (`"config.h"`, never a path); `build.bat`'s `WIND_INC` puts every folder on the
include path, and a new folder goes there (and in `CORE_SRC` if Wind.exe compiles it). `version.h`,
`resource.h` and `wind.rc` stay in `src/` itself. See 11.
- Engines behind `IMagnifierModel`, chosen by `model=` (applies live: both engines are always built): `hybrid` (Auto, default)
picks `render` or `transform` per session via the pure `src/engine/engine_pick.h`. `model=magnify` is
parsed as `hybrid`; the magnify engine is retired, do not revive it. See 03, 04, 05.
- Engines behind `IMagnifierModel`, chosen by `model=` (applies live: both engines are always built):
`transform` (default) or `render`, picked per session by the pure `src/engine/engine_pick.h`. Auto
(`hybrid`) was removed as an option (#508) and, like the retired `magnify`, parses as `transform`;
do not revive either. See 03, 04, 05.
- Profiles: `profiles\<Name>.ini` next to the live ini; the live ini is the session, the profile file
the saved state. See 08.
- Settings UI: schema-driven Svelte (`ui/src/settings-schema.js`); `HandleWebMessage` in
Expand Down Expand Up @@ -76,9 +77,9 @@ Chapter numbers refer to `docs/architecture/NN-*.md`.
thread at idle) and centres the view itself (`SetFullscreenMagnifierOffsetsDWMUpdated`). While DWM
centres, never write a same-level transform or warm pulse. Never use a public write to get the
composed pointer: it blocks 200-260 ms. `MagGetFullscreenTransform` cannot see DWM's centring.
In a native session the only window Wind draws is the Inspect crosshair. Mouse tracking = Wind
(`txNativeCursor=0`, the DEFAULT) is the pre-#369 sprite session (07): gate native-only paths on
`nativeSession_`.
In a native session the only window Wind draws is the Inspect crosshair. Native
(`txNativeCursor=1`) is the DEFAULT (#508); Mouse tracking = Wind (`txNativeCursor=0`) is the
pre-#369 sprite session (07): gate native-only paths on `nativeSession_`.
- The cursor grows with the zoom in every engine (#253). Do not restore constant size;
`cursorConstantSize=1` is the render-only opt-in. Test defaults on a wiped `%LOCALAPPDATA%\Wind`:
the dev ini differs from a clean install.
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ a game hides, clips or center-locks the cursor, and clicks pass through to the a
- Smooth sub-pixel zoom and pan.
- Clicks, hover and dragging keep working while zoomed.
- Tracks the mouse in games through raw input, without injecting into the game.
- Auto picks the best engine for each window and switches when you alt-tab.
- Zooms inside the compositor (Transform), so it stays smooth over heavy games; Render is there as an alternative.
- The real cursor shapes, sharp at every zoom level.
- HDR aware: the zoomed view matches the desktop's brightness.
- Follows the text caret as you type, and optionally the keyboard focus.
Expand Down Expand Up @@ -70,11 +70,11 @@ profile. Preferences > Show advanced settings shows the rest. Every setting is a

## Limits

- Magnifies the primary monitor unless `multiMonitor=1` is set.
- Magnifies the monitor the cursor is on at each zoom-in (`multiMonitor=0` keeps the primary).
- Games must run borderless or windowed. Switch exclusive-fullscreen games to borderless.
- The Start menu, taskbar and tray flyouts are not magnified by default.
- Protected video (Netflix and similar) shows normally in Auto, which switches to the transform
engine for it. Forcing the Render engine shows it black.
- Protected video (Netflix and similar) shows normally with the Transform engine (the default).
The Render engine shows it black.

## Development

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/01-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ These are commitments. Code that breaks one is a bug even if it works.
hides the cursor ([07](07-cursor.md)).
- **Tracking never moves the pointer.** Caret, focus and edge-mode views detach from the pointer;
the next real mouse move places the pointer in the view ([07](07-cursor.md)).
- **Scope.** Primary monitor by default (`multiMonitor=1` follows the cursor's monitor per
- **Scope.** The cursor's monitor by default (`multiMonitor=1` follows the cursor's monitor per
zoom-in); desktop, apps and borderless or windowed games. Exclusive fullscreen is out of scope.

## Three binaries
Expand Down
19 changes: 11 additions & 8 deletions docs/architecture/03-engines.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# 03. Engines and the hybrid pick

Wind has three engines: Auto (`hybrid`, the default), Render and Transform. Auto holds the other
two alive and picks one per zoom session with a pure, unit-tested predicate. This chapter covers
the model interface, what each engine is for, how the pick decides and how a mid-zoom handover
avoids a bare frame.
Wind has two engines: Transform (the default) and Render. Both stay alive, and a pure,
unit-tested predicate picks one per zoom session. Auto (`hybrid`), which picked per window type,
was removed as an option in #508: `model=hybrid` now reads as Transform, and the Auto rule below
(`ShouldPickTransform`, the per-window-type keys, `transformExclude`) is dormant code, unreachable
from Settings and the ini, kept until a follow-up deletes it. This chapter covers the model
interface, what each engine is for, how the pick decides and how a mid-zoom handover avoids a
bare frame.

## Where pixels can be scaled

Expand Down Expand Up @@ -33,8 +36,8 @@ whatever `TickState::model` points at:
| `idleTick()` | Every idle tick. The transform builds its Magnification context and cursor lens here and keeps them warm, see [05](05-transform-engine.md). |
| `retarget(MonitorTarget)` | Both engines, at activation (`multiMonitor` picks the monitor; a changed resolution is followed either way). Render returns false across adapters. |

`model` in `magnifier.ini` takes `hybrid`, `render` or `transform`. Anything else, including an old
`model=magnify`, reads as `hybrid`. Both engines are built at startup whatever it says (`mRender`,
`model` in `magnifier.ini` takes `transform` (the default) or `render`. Anything else, including an
old `model=hybrid` or `model=magnify`, reads as `transform`. Both engines are built at startup whatever it says (`mRender`,
`mTransform`), and `model` only decides the pick (`PickTransformFor`, `src/engine/engine_pick.h`): a change
applies on the config reload, at the next zoom-in or, while zoomed, through the instant switch. A
pinned Render still falls back to transform when the render device is lost or its overlay could not
Expand All @@ -43,9 +46,9 @@ and only then does an engine change relaunch Wind (Settings' Restart Wind button

| Model | What it is | For |
|---|---|---|
| `hybrid` ("Auto") | `TickState` holds a `RenderModel` (`mRender`) and a `TransformModel` (`mTransform`) and points `model` at one per session | The default |
| `hybrid` ("Auto", removed in #508) | `TickState` holds a `RenderModel` (`mRender`) and a `TransformModel` (`mTransform`) and points `model` at one per session | No longer reachable: reads as `transform` |
| `render` | DXGI Desktop Duplication + D3D11 onto a click-through, capture-excluded overlay (`src/render/render_engine.*`) | Sub-pixel pan, cursor in the same frame, shell coverage. [04](04-render-engine.md) |
| `transform` | The DWM fullscreen transform, no presents of its own (`src/transform/transform_model.cpp`) | Games, protected video and rotated outputs, plus the desktop when `desktopTransform=1` and the input transform is verified. [05](05-transform-engine.md) |
| `transform` | The DWM fullscreen transform, no presents of its own (`src/transform/transform_model.cpp`) | The default. Every session on the primary monitor; other monitors fall back to render. [05](05-transform-engine.md) |

If the transform half fails to initialize, Auto logs a warning and runs render only; every pick
site checks `t.mTransform`.
Expand Down
7 changes: 4 additions & 3 deletions docs/architecture/04-render-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,16 +207,17 @@ Measurements and rejected filters: [../COLOUR-FILTER-FINDINGS.md](../COLOUR-FILT
The magnified desktop is one full-screen triangle (no clear when a copy exists), then the outline,
then the cursor.

- **Scaling filter** (`renderScaling`, Settings: Render scaling, issue #456): Nearest uses the
plain pass with point sampling; xBR (the default) and NVIDIA Image Scaling are their own pixel
- **Scaling filter** (`renderScaling`, Settings: Render scaling, issue #456): Nearest (the default,
#508) uses the plain pass with point sampling; xBR and NVIDIA Image Scaling are their own pixel
shaders (`src/render/upscale_shaders.h`, `src/render/upscale_nis.h`, drawn by `UpscalePass`). They read texels
through `Tap()`, which tonemaps first, so they work in display space on SDR and HDR alike, and the
capture crop keeps a 4-texel margin for their taps. A shader that fails to build falls back to the
plain bilinear pass (logged once). Chosen in an A/B of eleven filters on 2026-10-12 (xBR 0.24 ms,
NIS 0.32 ms GPU per frame at 4K; RTX VSR and an ONNX model were choppy); the rest live on the
`proto/upscalers` branch, results in research/wind/2026-10-12-upscaler-findings.md.
`upscaleTiming=1` logs the magnify pass's GPU time every 2 s while zoomed.
`bilinear` is retired: an ini without `renderScaling` migrates (`bilinear=0` keeps Nearest).
`bilinear` is retired: an ini without `renderScaling` migrates (`bilinear=0` keeps Nearest, any
other `bilinear` gets xBR, neither key gets the Nearest default).

- The outline is **one** full-screen pass that colours the border band and discards the interior;
four separate quads dropped edges on some GPUs. It is inset 6 px because at non-integer DPI DWM
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/05-transform-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ Defences (wall arming in `RunTick`'s `ReadForegroundAndArmWall`, write clamp in

DWM magnifies with nearest neighbour unless something calls
`MagSetFullscreenUseBitmapSmoothing` (Magnification.dll, undocumented, resolved BY NAME).
`txSamplingMode`: 0 nearest (default), 1 smooth.
`txSamplingMode`: 0 nearest, 1 smooth (default since #508).

- Until 0.24.0 it was resolved by ordinal 1, which does not exist (the export ordinals start at
100), so Wind never set the filter: the image showed whatever state another process had left.
Expand Down
4 changes: 2 additions & 2 deletions docs/architecture/07-cursor.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ render-only opt-in for the old constant size. `cursorScaleWithZoom` is retired a
## Native cursor

Transform sessions use the pointer Windows Magnifier uses: the real pointer, drawn by DWM into the
magnified frame (Mouse tracking = Native; the default is Wind, below). It works at either sampling mode, because DWM
magnified frame (Mouse tracking = Native, the default since #508; Wind is below). It works at either sampling mode, because DWM
samples it like the content: Smooth transform scaling makes it sharp but it shimmers
slightly during zoom ramps, nearest keeps it pixelated and steady. Pure rules:
`src/transform/native_cursor.h` (tested).
Expand Down Expand Up @@ -179,7 +179,7 @@ slightly during zoom ramps, nearest keeps it pixelated and steady. Pure rules:
crosshair window. Games pay nothing extra for the native cursor: a zoomed full-screen window is
composed anyway, and at 1x the lens style is off.

### Wind mouse tracking (`txNativeCursor=0`, the default)
### Wind mouse tracking (`txNativeCursor=0`)

Settings > View > Mouse tracking picks Native (above) or Wind (#463), the transform session as it
was before the native cursor (#369), and the default since the owner chose it on 2026-10-14:
Expand Down
21 changes: 11 additions & 10 deletions docs/architecture/08-config-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ flowchart LR
as an initializer, so a missing or malformed key keeps the default; parsing never fails.

- Numeric values are clamped to tested ranges.
- `model` must be `render`, `transform` or `hybrid`; anything else becomes `hybrid`.
- `model` must be `render` or `transform`; anything else (the retired `hybrid` and `magnify` too)
becomes `transform`.
- Unsafe binds read as unbound (`src/input/keybind_rules.h`, [06](06-input.md)).
- `LoadConfig` is the I/O wrapper, excluded from the test build by `WIND_TESTS`.
- Structure: per line `SplitIniLine` (key and value), `ApplyEmptyableKey` (the exe lists and
Expand Down Expand Up @@ -157,7 +158,7 @@ single-instance mutex held, sets `Local\Wind_QuitRequest` and waits for the old
clean exit restores the OS cursor and releases `ClipCursor`.

**An empty profile file means factory defaults**: every profile key falls back to its `ParseConfig`
default. "New profile" writes a near-empty file (a comment plus `model=hybrid`) and switches to it.
default. "New profile" writes a near-empty file (a comment plus `model=transform`) and switches to it.
"Duplicate current" (`createProfile` with `fromCurrent=1`) instead writes `MakeProfileText(live)`: the
session as it stands, unsaved changes included, so `model` is unchanged (no restart) and the
outgoing profile is not mirrored into (`DoSwitchProfile(name, mirrorOutgoing=false)`).
Expand Down Expand Up @@ -197,21 +198,21 @@ Every key works in the ini whether or not Settings shows it. Keys hot-reload unl

| Keys | Meaning |
|---|---|
| `maxLevel` (12), `zoomInSpeed`, `zoomOutSpeed`, `zoomEaseOutMs`, `smoothZoom*` | Range and feel of the zoom |
| `maxLevel` (30), `zoomInSpeed` and `zoomOutSpeed` (1.5), `zoomEaseOutMs` (115), `smoothZoom*` | Range and feel of the zoom |
| `cursorSensitivity` (1.0), `panSpeed` (1.0), `panGlideMaxPx` (0) | Mouse and arrow-key pan speed; the pan glide's distance, which also sets its ease (#430, #434) |
| `gameCursorLagMs` (-1) | How late the view follows the pointer while a full-screen game hides it and draws its own cursor; -1 = one display frame, 0 = off (#443). Ini only |
| `mouseAlign` (0 centred, 1 within the edges), `mouseMarginPct` | Where the pointer sits while the view moves |
| `trackCaret` (1), `trackFocus` (0), `trackAlign`, `trackGlideMs` (200), `trackMarginPct` (15) | Follow the text caret and keyboard focus; `trackMarginPct` is the within-edges margin, % of the view on each side |
| `lockApps`, `warpLock` (0) | Games whose sessions pan from raw mouse motion; heuristics for unlisted games |
| `multiMonitor` (0) | 1 follows the cursor's monitor at each zoom-in |
| `multiMonitor` (1) | 1 follows the cursor's monitor at each zoom-in; 0 keeps the primary |

**Image and cursor.**

| Keys | Meaning |
|---|---|
| `txSamplingMode` (0) | Transform sampling: 0 nearest, 1 smooth (Settings: Transform scaling, formerly "High resolution cursor", [05](05-transform-engine.md)) |
| `txSamplingMode` (1) | Transform sampling: 0 nearest, 1 smooth (Settings: Transform scaling, formerly "High resolution cursor", [05](05-transform-engine.md)) |
| `edgeClip` (1, hot) | While a transform session is zoomed, clips the pointer 1 px inside the monitor's left and top edges so it cannot rest on the contested outermost pixel (cursor-shape flicker there). The right and bottom stay open so the cursor can slide off the screen, as at 1x. A tighter existing clip (game confine, Inspect) always wins; 0 turns it off. Rationale: `Config::edgeClip` in `src/config/config.h`, mechanics in `TransformModel::edgeClipManage` |
| `renderScaling` (1), `upscaleTiming` (0) | Render engine scaling: 0 nearest, 1 xBR, 2 NVIDIA Image Scaling (Settings: Render scaling, [04](04-render-engine.md)); `bilinear` is retired and only migrates (0 keeps nearest). `upscaleTiming=1` logs the filter's GPU time |
| `renderScaling` (0), `upscaleTiming` (0) | Render engine scaling: 0 nearest, 1 xBR, 2 NVIDIA Image Scaling (Settings: Render scaling, [04](04-render-engine.md)); `bilinear` is retired and only migrates (0 keeps nearest). `upscaleTiming=1` logs the filter's GPU time |
| `sharpness`, `brightness`, `hdrTonemap` (1) | Render engine image |
| `cursorConstantSize` (0), `cursorVisibility` (`auto`) | Render cursor size and when it is drawn |
| `colorWarmPct` (0), `colorDimPct` (100) | Warmth and brightness filter |
Expand All @@ -222,10 +223,10 @@ failed to init.

| Keys | Meaning |
|---|---|
| `model` | `hybrid` (Auto, default), `render` or `transform`; anything else reads as `hybrid` |
| `engineGame`, `engineAcrylic`, `engineDesktop`, `engineOther` | `auto`, `transform` or `render` per window category, Auto only |
| `model` | `transform` (default) or `render`; anything else, the retired `hybrid` (Auto) included, reads as `transform` |
| `engineGame`, `engineAcrylic`, `engineDesktop`, `engineOther` | Dormant since #508: read only by the removed Auto pick |
| `desktopTransform` (1) | Let Auto use the transform on the desktop when UIAccess is available |
| `transformExclude`, `renderExclude` | Exes that never get Transform, or never get Render |
| `transformExclude`, `renderExclude` | Dormant since #508: read only by the removed Auto pick |
| `vsync` (1), `dwmFlush` (0), `gameFpsCap`, `gpuPriority` (restart) | Render pacing and game coexistence |
| `zorderBand` (0, restart) | 16 covers the shell in the UIAccess build, at the cost of the Snipping Tool ([04](04-render-engine.md)) |

Expand All @@ -247,7 +248,7 @@ failed to init.
| `mpoGuard` (1), `mpoGuardLiftWall` (1), `mpoGuardTest` (0) | The MPO guard effect, whether it lifts the pan walls while plane-free, and a diagnostic that applies it on an MPO-off boot |
| `mpoNearestGuard` (1) | Allow nearest sampling on an MPO-on boot, relying on the guard; 0 turns nearest into smooth on such boots ([05](05-transform-engine.md)) |
| `txRestLevel` (1.0) | Level the transform rests at when idle; above 1.0 parks a hair off identity (costs a composed desktop) |
| `txNativeCursor` (0) | Mouse tracking (Settings > View, transform engine): 0 = Wind, Wind moves the view and draws its own pointer; 1 = Native, Windows' magnifier logic ([07](07-cursor.md#wind-mouse-tracking-txnativecursor0-the-default)); read at zoom-in |
| `txNativeCursor` (1) | Mouse tracking (Settings > View, transform engine): 0 = Wind, Wind moves the view and draws its own pointer; 1 = Native, Windows' magnifier logic ([07](07-cursor.md#wind-mouse-tracking-txnativecursor0-the-default)); read at zoom-in |
| `txTrace` (0) | 1 records a per-tick trace and writes `txtrace-<ms>.csv` to the log folder at each session end |
| `zoomTrace` (0) | 1 logs a zoom-in/zoom-out timeline per zoom (#310) |

Expand Down
7 changes: 4 additions & 3 deletions docs/architecture/09-settings-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ unless another setting has a value), `offIf` (kept in place but dimmed and inert
setting has a value: Release glide while Smooth transform scaling is on) and `tag` (a small label
after the name: Transform scaling is tagged Experimental). The Engine card's two Scaling rows
follow the engine: Transform scaling (`txSamplingMode`) unless it is Render, Render scaling
(`renderScaling`: Nearest, xBR, NVIDIA Image Scaling) unless it is Transform, so Auto shows both. Six groups: Hotkeys, Zoom,
(`renderScaling`: Nearest, xBR, NVIDIA Image Scaling) unless it is Transform. The Engine row offers
Transform and Render (Auto was removed in #508) and is a plain row, not an advanced one. Six groups: Hotkeys, Zoom,
View, then below a divider Preferences (with the Screen light card), Tray menu and About. Each group has a label, an
icon, a banner description and cards of rows; each row names its ini key, type, label, description
and default. `Settings.svelte` is the shell (title bar, sidebar, banner, save capsule, search), and
Expand All @@ -102,8 +103,8 @@ and default. `Settings.svelte` is the shell (title bar, sidebar, banner, save ca

- **Advanced rows** (`adv: true`) show inline while the global `showAdvanced` key is on
(Preferences > Show advanced settings). Search always finds them.
- **`showIf: {key, eq}`** hides a row unless another key has that value; the per-category engine
rows show only while `model` is `hybrid`.
- **`showIf: {key, eq}`** (or `ne`) hides a row unless another key has that value; each engine's
scaling row shows only for its engine.
- **Removed from the UI is not removed from the product.** Many keys have no row (quick zoom
setup, outline, `sharpness`, `multiMonitor`, the `tx*` knobs). The core still parses
them, and Open settings file is the way to edit them.
Expand Down
Loading
Loading