diff --git a/CLAUDE.md b/CLAUDE.md index 89701ec9..1f69abed 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. diff --git a/docs/architecture/07-cursor.md b/docs/architecture/07-cursor.md index f95cd80d..67e44c83 100644 --- a/docs/architecture/07-cursor.md +++ b/docs/architecture/07-cursor.md @@ -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). @@ -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 diff --git a/docs/architecture/08-config-profiles.md b/docs/architecture/08-config-profiles.md index 4a333289..15885186 100644 --- a/docs/architecture/08-config-profiles.md +++ b/docs/architecture/08-config-profiles.md @@ -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\.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-.csv` to the log folder at each session end | | `zoomTrace` (0) | 1 logs a zoom-in/zoom-out timeline per zoom (#310) | diff --git a/src/config/config.cpp b/src/config/config.cpp index 6861d23b..fa8e44f0 100644 --- a/src/config/config.cpp +++ b/src/config/config.cpp @@ -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" diff --git a/src/config/config.h b/src/config/config.h index 6acddfb3..ec815fce 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -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). diff --git a/src/transform/native_cursor.h b/src/transform/native_cursor.h index f5604e55..fac26be5 100644 --- a/src/transform/native_cursor.h +++ b/src/transform/native_cursor.h @@ -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. diff --git a/src/transform/transform_model.h b/src/transform/transform_model.h index 22c7c159..b72297ab 100644 --- a/src/transform/transform_model.h +++ b/src/transform/transform_model.h @@ -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 diff --git a/tests/fixtures/config_defaults.txt b/tests/fixtures/config_defaults.txt index 5bce41b8..0d3ef82a 100644 --- a/tests/fixtures/config_defaults.txt +++ b/tests/fixtures/config_defaults.txt @@ -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 diff --git a/tests/test_config.cpp b/tests/test_config.cpp index 4a1c2878..ce63afd8 100644 --- a/tests/test_config.cpp +++ b/tests/test_config.cpp @@ -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)") { diff --git a/tests/test_config_model.cpp b/tests/test_config_model.cpp index 6cf37e98..ec418715 100644 --- a/tests/test_config_model.cpp +++ b/tests/test_config_model.cpp @@ -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"); diff --git a/tests/test_native_cursor.cpp b/tests/test_native_cursor.cpp index 7341d4ad..f8593ec4 100644 --- a/tests/test_native_cursor.cpp +++ b/tests/test_native_cursor.cpp @@ -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)); } diff --git a/ui/src/settings-schema.js b/ui/src/settings-schema.js index 803e2b3f..c36de620 100644 --- a/ui/src/settings-schema.js +++ b/ui/src/settings-schema.js @@ -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'], diff --git a/ui/tests/schema.spec.js b/ui/tests/schema.spec.js index f5bf1da4..434b6aac 100644 --- a/ui/tests/schema.spec.js +++ b/ui/tests/schema.spec.js @@ -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']);