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
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,14 @@ Chapter numbers refer to `docs/architecture/NN-*.md`.
**Cursor (07)**
- In a transform session the lock (`lockApps`, tells) applies only while the pointer is hidden
(`LockApplies`); gates read `t.lockEff`, not `t.detector.locked()`. See 07.
- Native cursor (the transform engine's only cursor, both sampling modes; `src/transform/native_cursor.h`): DWM
- Native cursor (Mouse tracking = Native, both sampling modes; `src/transform/native_cursor.h`): DWM
draws the real pointer via Wind's cursor lens (a hidden `WC_MAGNIFIER` window, built on the tick
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.
The only window Wind still draws in a transform session is the Inspect crosshair, except in the
ini-only A/B fallback `txNativeCursor=0` (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. Mouse tracking = Wind
(`txNativeCursor=0`, the DEFAULT) 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
7 changes: 4 additions & 3 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. It is the transform engine's only cursor. It works at either sampling mode, because DWM
magnified frame (Mouse tracking = Native; the default is Wind, 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 @@ -178,9 +178,10 @@ 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.

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

An ini-only A/B fallback (#463) to the transform cursor as it was before the native cursor (#369):
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:
Wind writes every view itself (no DWM centring, no cursor lens), hides the real pointer (blanker plus
`MagShowSystemCursor`) and paints its shape into the sprite window (`CursorSprite::refreshShape`,
from the blanker's copies of the standard cursors), placed at the lens point in desktop
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/08-config-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,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` (1) | 0 = the legacy sprite session, the pre-#369 transform cursor kept for A/B testing ([07](07-cursor.md#legacy-sprite-session-txnativecursor0)); read at zoom-in. A profile key: set it in `profiles\<Name>.ini` too, or the next launch restores the profile's value |
| `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 |
| `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
2 changes: 2 additions & 0 deletions src/config/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,8 @@ std::string DefaultIniText() {
"trackAlign=0\n"
"; mouseAlign: mouse pointer, 0=keep centred, 1=keep within the edges\n"
"mouseAlign=0\n"
"; txNativeCursor: mouse tracking (transform engine), 0=Wind (Wind moves the view and draws the pointer), 1=Native (Windows' magnifier logic)\n"
"txNativeCursor=0\n"
"; trackGlideMs: how long the view takes to glide to the caret/focus/pointer (ms)\n"
"trackGlideMs=200\n"
"; trackMarginPct: within-the-edges margin, percent of the view on each side\n"
Expand Down
10 changes: 5 additions & 5 deletions src/config/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -302,12 +302,12 @@ struct Config {
// is turned into smooth as before. The pan walls stay armed either way. Hot: read on every
// config load (EffectiveSamplingMode).
int mpoNearestGuard = 1;
// Native cursor (issue #369, src/transform/native_cursor.h): 1 (DEFAULT) = transform sessions use DWM's
// own magnified pointer and DWM's centring. 0 = the pre-#369 legacy sprite session, kept as an
// ini-only A/B fallback (issue #463): Wind hides the real pointer, draws its shape in a sprite
// welded to the view, and writes every view itself. Not in the Settings UI or the ini template.
// Mouse tracking (Settings > View, issues #369, #463; src/transform/native_cursor.h). Transform
// engine only. 0 (DEFAULT, owner pick 2026-10-14) = Wind: Wind writes every view and hides the
// real pointer, drawing its shape in a sprite welded to the view (the pre-#369 session).
// 1 = Native: DWM's own magnified pointer and DWM's centring, as Windows Magnifier does.
// Read at zoom-in (hot for the next session).
int txNativeCursor = 1;
int txNativeCursor = 0;
// Smooth-zoom ladder (issue #369, src/transform/zoom_ladder.h): with smooth sampling, zoom only through
// levels where DWM's per-frame scratch rounding predicts under 1 px of movement (the zoom shake:
// 11 px p95 -> 0.7 px measured at 10-25x). 0 = off (hot).
Expand Down
4 changes: 2 additions & 2 deletions src/transform/native_cursor.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
// Games pay nothing extra: a zoomed full-screen window is composed anyway, and at 1x the lens style
// is off (hardware pointer). The only window Wind still draws in a transform session is the Inspect
// crosshair.
// txNativeCursor=0 (ini only, issue #463) brings back the pre-#369 session for A/B testing: Wind's
// own cursor sprite, welded to a view Wind writes itself, no DWM centring.
// Settings > View > Mouse tracking (txNativeCursor, issue #463) picks this (Native) or the pre-#369
// session (Wind, the default): Wind's own cursor sprite, welded to a view Wind writes, no DWM centring.
namespace wind {

// Whether a transform session uses the native cursor. Decided once per session, at zoom-in.
Expand Down
2 changes: 1 addition & 1 deletion src/transform/transform_model.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ class TransformModel : public IMagnifierModel {
// middle-click: measured 17 spike frames per 14 clicks), and the lens is ON only while zoomed.
// The context is released at shutdown and when the model is swapped out.
bool magUp_ = false;
bool nativePref_ = true; // txNativeCursor (#463): the next session's kind
bool nativePref_ = false; // txNativeCursor (#463): the next session's kind
bool nativeSession_ = true; // latched at zoom-in; false = legacy sprite session
int lastSpriteX_ = INT_MIN, lastSpriteY_ = INT_MIN; // dedup the legacy sprite move
// Native cursor (issue #369). nativePrimed_: the context's one public write that makes DWM
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/config_defaults.txt
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ txRestLevel 1 1.01 1
launchQuiesce - - 1
txWarmHz 0 1000 12
mpoNearestGuard - - 1
txNativeCursor - - 1
txNativeCursor - - 0
txSmoothLadder - - 1
txRampMinStep 0 100 0
txRampMinFrom 1 - 12
Expand Down
6 changes: 3 additions & 3 deletions tests/test_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -439,9 +439,9 @@ TEST_CASE("the high-res/MPO option is atomic at restart (issue #242)") {
CHECK(ParseConfig("mpoNearestGuard=1\n").mpoNearestGuard == 1);
}

TEST_CASE("txNativeCursor defaults on and parses the legacy sprite fallback (issue #463)") {
CHECK(ParseConfig("").txNativeCursor == 1);
CHECK(ParseConfig("txNativeCursor=0\n").txNativeCursor == 0);
TEST_CASE("txNativeCursor (Mouse tracking) defaults to Wind and parses Native (issue #463)") {
CHECK(ParseConfig("").txNativeCursor == 0);
CHECK(ParseConfig("txNativeCursor=1\n").txNativeCursor == 1);
}

TEST_CASE("cursorBandAuto defaults on and parses off (issue #269)") {
Expand Down
2 changes: 1 addition & 1 deletion tests/test_config_model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ TEST_CASE("keys of the removed sprite cursor are ignored, and change nothing els
// The old sprite cursor, its tuning knobs and the experiments around it are gone; an ini that
// still carries them parses exactly like one without (unknown keys are skipped).
const Config d = ParseConfig("");
const Config c = ParseConfig("txNativeCursor=0\ncursorSprite=0\ntxDwmCentre=0\ntxFreeCursor=0\n"
const Config c = ParseConfig("cursorSprite=0\ntxDwmCentre=0\ntxFreeCursor=0\n"
"panelPointer=0\nspriteBand16=1\nspriteCapturable=1\ntxHookWrite=1\n"
"txIdleReleaseMs=0\ntxWriteHz=60\ntxMinOffsetPx=2\ntxLevelStep=5\ntxGrid=50\ntxPace=2\n"
"txWarmWindowMs=700\ntxWarmMaxLevel=8\ntxWarmLevelEps=0.001\ntxWobbleCage=1\n");
Expand Down
2 changes: 1 addition & 1 deletion tests/test_native_cursor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

using namespace wind;

TEST_CASE("native cursor: on by default, txNativeCursor=0 is the legacy sprite (#463)") {
TEST_CASE("native cursor: txNativeCursor=1 is Native, 0 is the Wind sprite session (#463)") {
CHECK(UseNativeCursor(1));
CHECK_FALSE(UseNativeCursor(0));
}
Expand Down
6 changes: 6 additions & 0 deletions ui/src/settings-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ export const groups = [
{ key: 'mouseAlign', type: 'select', label: 'Pointer position', keywords: ['mouse position', 'mouse', 'cursor', 'pointer', 'where', 'centred', 'centered', 'center', 'centre', 'middle', 'edge', 'edges', 'within the edges', 'align', 'alignment', 'lock to center', 'follow mode', 'placement'], desc: 'Where the pointer sits while the view moves', options: ['0', '1'], optionLabels: { '0': 'Centred', '1': 'Within the edges' }, def: '0' },
{ key: 'mouseMarginPct', type: 'slider', label: 'Edge margin', keywords: ['edge', 'edges', 'border', 'padding', 'distance', 'boundary', 'margin', 'pointer', 'mouse', 'percent', 'dead zone', 'how near'], desc: 'How close to the edge before the view moves', min: 0, max: 40, step: 1, def: 0, unit: '%',
showIf: { key: 'mouseAlign', eq: '1' } },
// Mouse tracking (txNativeCursor, #463): Native = Windows' own magnifier logic (DWM centres the
// view and draws the real pointer, #369); Wind = Wind writes every view and draws its own copy of
// the pointer (the pre-#369 session). Transform engine only; read at the next zoom-in.
{ key: 'txNativeCursor', type: 'select', label: 'Mouse tracking', keywords: ['tracking', 'mouse', 'pointer', 'cursor', 'native', 'windows', 'magnifier', 'follow', 'centring', 'centering', 'sprite', 'dwm', 'smooth', 'lag', 'wind'],
desc: 'Who follows the mouse while zoomed', options: ['1', '0'], optionLabels: { '1': 'Native', '0': 'Wind' }, def: '0',
showIf: { key: 'model', ne: 'render' } },
// Zoom lock detection (#221): games like DOOM pin the mouse to the screen centre, which would
// pin the zoom view there too. Listed apps get the view UNLOCKED from the pointer.
{ key: 'lockApps', type: 'applist', adv: true, label: 'Mouse-locked games', keywords: ['lock', 'locked', 'fps', 'shooter', 'first person', 'mouselook', 'mouse look', 'camera', 'game', 'exe', 'program', 'unlock', 'exception', 'apps', 'pinned', 'centre'],
Expand Down
2 changes: 1 addition & 1 deletion ui/tests/schema.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const OLD_BIND_KEYS = [
'panLeftVk', 'panLeftMods', 'panRightVk', 'panRightMods', 'panUpVk', 'panUpMods', 'panDownVk', 'panDownMods',
'hideCursorVk', 'hideCursorMods', 'cursorLockVk', 'cursorLockMods',
];
const NEW_KEYS = ['renderScaling', 'uiPalette', '__profiles', '__diagnostics', '__openIni', 'trayPerf', 'trayPinned', 'showAdvanced'];
const NEW_KEYS = ['renderScaling', 'txNativeCursor', 'uiPalette', '__profiles', '__diagnostics', '__openIni', 'trayPerf', 'trayPinned', 'showAdvanced'];

test('group ids, order and shape follow the spec', () => {
expect(groups.map((g) => g.id)).toEqual(['hotkeys', 'zoom', 'view', 'prefs', 'tray', 'about']);
Expand Down
Loading