From 06726c52d555d9b938c90d498c1012d828d795f0 Mon Sep 17 00:00:00 2001 From: Max <112043822+Maxaubert@users.noreply.github.com> Date: Sat, 24 Oct 2026 05:06:27 +0200 Subject: [PATCH] feat(config,ui,tray): owner-tuned defaults; Transform-only engine, Auto removed (#508) New defaults: Max zoom 30x, zoom speed 1.5x, Release glide 115 ms, Mouse tracking Native, Transform scaling Smooth, multiMonitor on, Render scaling Nearest, engine Transform. Hotkeys stay unbound, Inspect mode stays off. Auto (hybrid) is gone from Settings and the tray dropdown; model=hybrid, magnify and unknown values parse as transform everywhere (core, UI session, profile diff, tray). The per-window engine rows and the Auto-only template keys are dropped; the core's hybrid pick is dormant until a follow-up removes it. The Engine row is a plain row now, so its section keeps two rows. The retired bilinear key migrates only when present, so a missing key gets the new Nearest default. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BzgY8XJqqqLhU8BfSfdnvF --- CLAUDE.md | 13 ++--- README.md | 8 +-- docs/architecture/01-overview.md | 2 +- docs/architecture/03-engines.md | 19 ++++--- docs/architecture/04-render-engine.md | 7 +-- docs/architecture/05-transform-engine.md | 2 +- docs/architecture/07-cursor.md | 4 +- docs/architecture/08-config-profiles.md | 21 ++++---- docs/architecture/09-settings-ui.md | 7 +-- docs/architecture/README.md | 6 +-- src/config/config.cpp | 66 +++++++----------------- src/config/config.h | 54 +++++++++---------- src/config/profile_switch.h | 6 +-- src/config/profiles.cpp | 5 +- src/config_ui/main.cpp | 6 +-- src/tray_app/flyout_model.h | 8 +-- src/tray_app/flyout_test.cpp | 4 +- src/tray_app/flyout_tools.h | 13 ++--- tests/fixtures/config_defaults.txt | 20 +++---- tests/test_config.cpp | 39 +++++++------- tests/test_config_defaults.cpp | 2 +- tests/test_config_model.cpp | 18 ++++--- tests/test_flyout_tools.cpp | 30 +++++------ tests/test_profiles.cpp | 14 ++--- ui/src/controls/EngineRow.svelte | 2 +- ui/src/controls/Preview.svelte | 4 +- ui/src/session.js | 14 ++--- ui/src/settings-schema.js | 48 ++++++----------- ui/tests/a11y.spec.js | 2 +- ui/tests/interaction.spec.js | 2 +- ui/tests/prefs.spec.js | 4 +- ui/tests/responsive.spec.js | 2 +- ui/tests/scaling-notice.spec.js | 2 +- ui/tests/schema.spec.js | 33 ++++++------ ui/tests/search.spec.js | 9 ++-- ui/tests/session.spec.js | 22 ++++---- ui/tests/settings.spec.js | 36 +++++++------ ui/tests/tray.spec.js | 2 +- 38 files changed, 270 insertions(+), 286 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9aace15b..3f504ea8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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\.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 @@ -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. diff --git a/README.md b/README.md index 6b78b63a..ce9f750f 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 diff --git a/docs/architecture/01-overview.md b/docs/architecture/01-overview.md index 91605afd..84060b9c 100644 --- a/docs/architecture/01-overview.md +++ b/docs/architecture/01-overview.md @@ -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 diff --git a/docs/architecture/03-engines.md b/docs/architecture/03-engines.md index b92f393c..8f1a5717 100644 --- a/docs/architecture/03-engines.md +++ b/docs/architecture/03-engines.md @@ -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 @@ -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 @@ -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`. diff --git a/docs/architecture/04-render-engine.md b/docs/architecture/04-render-engine.md index d826e4f3..8ca2d7b0 100644 --- a/docs/architecture/04-render-engine.md +++ b/docs/architecture/04-render-engine.md @@ -207,8 +207,8 @@ 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 @@ -216,7 +216,8 @@ then the cursor. 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 diff --git a/docs/architecture/05-transform-engine.md b/docs/architecture/05-transform-engine.md index 4964e87b..a3eeb5de 100644 --- a/docs/architecture/05-transform-engine.md +++ b/docs/architecture/05-transform-engine.md @@ -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. diff --git a/docs/architecture/07-cursor.md b/docs/architecture/07-cursor.md index 9b6df0db..6b1338e7 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 (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). @@ -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: diff --git a/docs/architecture/08-config-profiles.md b/docs/architecture/08-config-profiles.md index 5aa7bcec..45f01452 100644 --- a/docs/architecture/08-config-profiles.md +++ b/docs/architecture/08-config-profiles.md @@ -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 @@ -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)`). @@ -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 | @@ -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)) | @@ -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-.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/docs/architecture/09-settings-ui.md b/docs/architecture/09-settings-ui.md index 154f8b95..22933389 100644 --- a/docs/architecture/09-settings-ui.md +++ b/docs/architecture/09-settings-ui.md @@ -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 @@ -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. diff --git a/docs/architecture/README.md b/docs/architecture/README.md index f8017ae5..11fe9c1c 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -5,7 +5,7 @@ Developer documentation for Wind. The code is authoritative; specs and findings ## The system at a glance One paced tick loop in `Wind.exe` reads input from hooks and Raw Input, turns it into a view with -pure mapper logic, and hands the view to one of two engines. Auto picks the engine per session. +pure mapper logic, and hands the view to one of two engines: Transform (the default) or Render. ```mermaid flowchart LR @@ -33,8 +33,8 @@ flowchart LR TX --> DWM[DWM compositor] --> SCREEN ``` -There are three engines (Auto, Render, Transform); Auto is the default and switches between the -other two. +There are two engines, Transform (the default) and Render. Auto, which switched between them per +window, was removed as an option in #508 ([03](03-engines.md)). ## Chapters diff --git a/src/config/config.cpp b/src/config/config.cpp index fa8e44f0..78929ed2 100644 --- a/src/config/config.cpp +++ b/src/config/config.cpp @@ -182,8 +182,8 @@ static void ClampConfig(Config& c, bool haveRenderScaling) { if (c.gameCursorLagMs < -1) c.gameCursorLagMs = -1; if (c.gameCursorLagMs > 100) c.gameCursorLagMs = 100; c.sharpness = clampd(c.sharpness, 0.0, 1.0); - if (!haveRenderScaling) c.renderScaling = c.bilinear == 0 ? 0 : 1; // bilinear retired (#456) - if (c.renderScaling < 0 || c.renderScaling > 2) c.renderScaling = 1; + if (!haveRenderScaling && c.bilinear >= 0) c.renderScaling = c.bilinear == 0 ? 0 : 1; // bilinear retired (#456) + if (c.renderScaling < 0 || c.renderScaling > 2) c.renderScaling = 0; c.brightness = clampd(c.brightness, 0.5, 1.5); c.colorWarmPct = (int)clampd(c.colorWarmPct, 0, 100); c.colorDimPct = (int)clampd(c.colorDimPct, 1, 100); @@ -216,12 +216,11 @@ static void ClampConfig(Config& c, bool haveRenderScaling) { if (c.txEdgeMargin < 0.0) c.txEdgeMargin = 0.0; if (c.txEdgeMargin > 8.0) c.txEdgeMargin = 8.0; // beyond this the lost border is the bug c.outlineIdleSeconds = clampd(c.outlineIdleSeconds, 0.5, 60.0); - // "transform" is a first-class model again (revived for issue #148: the compositor-internal - // zoom that stays smooth over heavy games); anything unknown falls back to hybrid, the - // product default ("Auto" in the UI) - same fallback as a missing key (struct default). - // "magnify" (the retired Windows Magnifier driver) is unknown too, so an old ini that still - // says so runs Auto. - if (c.model != "render" && c.model != "transform" && c.model != "hybrid") c.model = "hybrid"; + // Two engines: "transform" (the product default, the compositor-internal zoom revived for + // issue #148) and "render". Anything else falls back to transform, the same as a missing key + // (struct default): the retired "hybrid" (Auto, removed as an option in #508) and "magnify" + // (the retired Windows Magnifier driver), so an old ini that still says either runs Transform. + if (c.model != "render" && c.model != "transform") c.model = "transform"; // (The old transform/hybrid maxLevel<=12 clamp is GONE: the "TDR territory above 12x" was // root-caused 2026-07-26 to NVIDIA's 16-bit MPO plane-programming overflow - see issue // #148 - which the mapper's MPO-aware pan wall now guards at ANY level, so maxLevel is one @@ -526,10 +525,10 @@ std::string DefaultIniText() { "panLeftVk=0\npanLeftMods=0\npanRightVk=0\npanRightMods=0\n" "panUpVk=0\npanUpMods=0\npanDownVk=0\npanDownMods=0\npanSpeed=1.0\n" "; maxLevel: how far you can zoom (does not affect zoom speed)\n" - "maxLevel=12.0\n" - "; zoomInSpeed/zoomOutSpeed: zoom rate multipliers (1.0=default, 2.0=twice as fast, 0.5=half);\n" + "maxLevel=30.0\n" + "; zoomInSpeed/zoomOutSpeed: zoom rate multipliers (1.5=default, 3.0=twice as fast, 0.75=half);\n" "; speed is independent of maxLevel\n" - "zoomInSpeed=1.0\nzoomOutSpeed=1.0\n" + "zoomInSpeed=1.5\nzoomOutSpeed=1.5\n" "; smoothZoom: 0=linear constant speed; 1=zoom-IN soft-starts, easing up to linear (shipped on)\n" "smoothZoom=1\n" "; smoothZoomAccel: ease-in depth - zoom-in starts at zoomInSpeed/this and climbs to\n" @@ -577,9 +576,9 @@ std::string DefaultIniText() { "; cursorVisibility: auto=hide our cursor when the focused app hides its own (games);\n" "; always=always draw it; never=never draw it\n" "cursorVisibility=auto\n" - "; renderScaling: render engine filter, 0=nearest (crisp pixels), 1=xBR (smooth, default),\n" + "; renderScaling: render engine filter, 0=nearest (crisp pixels, default), 1=xBR (smooth),\n" "; 2=NVIDIA Image Scaling (smooth and sharpened)\n" - "renderScaling=1\n" + "renderScaling=0\n" "; sharpness: 0=off; 0.1-1.0 sharpens the magnified image (crisper text/detail)\n" "sharpness=0.0\n" "; zorderBand: z-band of the render engine's overlay and the Inspect crosshair. 0=ordinary\n" @@ -601,8 +600,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" + "; txNativeCursor: mouse tracking (transform engine), 0=Wind (Wind moves the view and draws the pointer), 1=Native (Windows' magnifier logic, default)\n" + "txNativeCursor=1\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" @@ -616,37 +615,10 @@ std::string DefaultIniText() { "colorDimPct=100\n" "; hdrTonemap: 1=HDR10->SDR tonemap when Windows HDR is on (no-op on SDR); 0=off\n" "hdrTonemap=1\n" - "; model: hybrid = Auto (default): picks render or transform per zoom-in (games get\n" - "; the compositor-internal transform, everything else the GPU overlay).\n" - "; render = GPU capture+overlay (high fidelity). transform = DWM fullscreen\n" - "; transform only. Anything else runs hybrid. Restart to switch.\n" - "model=hybrid\n" - "; transformExclude (Auto/hybrid only): exe names that must never get the transform\n" - "; engine even when fullscreen+borderless. Fullscreen browser video looks exactly\n" - "; like a game to the foreground test but wants the render engine. Comma-separated,\n" - "; case-insensitive, exact name match. Empty = exclude nothing.\n" - "transformExclude=zen.exe,firefox.exe,chrome.exe,msedge.exe,brave.exe,opera.exe,opera_gx.exe,vivaldi.exe\n" - "; renderExclude (Auto/hybrid only): exe names that must never get the RENDER engine.\n" - "; For DRM/protected apps (Netflix, Apple TV): Desktop Duplication captures those as\n" - "; BLACK, so render shows nothing. Wind detects most of them automatically via the\n" - "; window's display affinity; this is the escape hatch for the ones it misses.\n" - "renderExclude=\n" - "; engineGame / engineAcrylic / engineDesktop / engineOther (Auto/hybrid only):\n" - "; which engine to use per window type. auto|transform|render; auto = Wind decides\n" - "; exactly as it always has, so leaving these alone changes nothing.\n" - "; game = borderless and covering the monitor (games, F11 video)\n" - "; acrylic = the window declares a DWM backdrop (Mica/acrylic/tabbed). Only\n" - "; windows that OPT IN are detectable, so this is a subset of what\n" - "; looks blurred on screen - third-party blur is invisible to it.\n" - "; desktop = the shell desktop (Win+D)\n" - "; other = everything else\n" - "; Two rules always win over these: protected/DRM content never gets render, and\n" - "; transformExclude apps never get transform (except when protected, where black\n" - "; video is the worse failure).\n" - "engineGame=auto\n" - "engineAcrylic=auto\n" - "engineDesktop=auto\n" - "engineOther=auto\n" + "; model: transform = DWM fullscreen transform (default; the compositor zooms, smooth\n" + "; over heavy games). render = GPU capture+overlay. Anything else runs transform.\n" + "; Applies live.\n" + "model=transform\n" "; noSwallowApps: programs where Wind stops intercepting its keyboard binds.\n" "; Watching the keyboard makes Windows hand us every keystroke and WAIT before it\n" "; delivers anything else - including mouse movement to the game. Holding a key\n" @@ -660,7 +632,7 @@ std::string DefaultIniText() { "; manages this list (Keybinds -> Release keys in these apps).\n" "noSwallowApps=\n" "; multiMonitor: 1=magnify whichever monitor the cursor is on at zoom-in; 0=primary only\n" - "multiMonitor=0\n" + "multiMonitor=1\n" "; cropCapture (opt-in): 0=always copy all changed regions (cache never stale, default);\n" "; 1=on a full-screen repaint (games) copy only the magnified region (cuts 4K HDR GPU\n" "; copy ~zoom^2) but screen edges can briefly show a previous window after a switch.\n" diff --git a/src/config/config.h b/src/config/config.h index 4ccf5137..34139d6c 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -62,12 +62,12 @@ struct Config { // uses the same bit layout as the zoom combos. int hideCursorVk = 0; int hideCursorMods = 0; - double maxLevel = 12.0; // how FAR you can zoom (does not affect zoom SPEED) + double maxLevel = 30.0; // how FAR you can zoom (does not affect zoom SPEED) // --- Zoom experience (see docs/specs/2026-05-26-configurable-zoom-design.md) --- // Per-direction rate multipliers (1.0 = default speed); apply in BOTH linear and smooth modes. // Speed is independent of maxLevel (a fixed doublings/sec base inside ZoomController). - double zoomInSpeed = 1.0; // 0.25-4.0 - double zoomOutSpeed = 1.0; // 0.25-4.0 + double zoomInSpeed = 1.5; // 0.25-4.0 + double zoomOutSpeed = 1.5; // 0.25-4.0 int zoomTrace = 0; // 1 = log a zoom-in/zoom-out timeline per zoom (#310, diagnostics) // Hitch recorder (#361): log every long zoomed frame with its classified cause, plus a // per-minute summary. On by default: the record costs ~0.5 us per tick and logs nothing on a @@ -84,9 +84,9 @@ struct Config { double smoothZoomRamp = 0.6; // Release ease-out (2026-08-28, hot): the applied zoom rate glides to a stop over roughly // 3x this time constant instead of freezing the instant the button lifts (the square-wave - // stop read as harsh). ~45ms tau = ~150ms felt glide. 0 = off (the old dead stop). - // Not applied with the high resolution cursor (txSamplingMode=1), which stops on release (#427). - int zoomEaseOutMs = 45; + // stop read as harsh). ~45ms tau = ~150ms felt glide; the default 115 (owner pick, #508) is a + // longer coast. 0 = off (the old dead stop). + int zoomEaseOutMs = 115; // Present sync while zoomed (render engine): 1 = vsync (Present sync-interval 1, locked to // the display refresh); 0 = no vsync (Present 0), with the loop paced by the timer instead. int vsync = 1; @@ -98,14 +98,15 @@ struct Config { int diagnostics = 0; // 1 = log frame-timing as "diag" lines in wind-core.log // --- Model selection ---------------------------------------------------- - // Which magnification model runs. "hybrid" (DEFAULT, "Auto" in the UI) constructs render + - // transform and picks per zoom-in (engine_pick.h). "render" = the DXGI capture + D3D11 - // overlay. "transform" = the DWM fullscreen-transform model (MagSetFullscreenTransform, no - // Magnify.exe) - revived for issue #148: it magnifies inside the compositor with zero app - // presents, the only path that stays smooth while a heavy game renders. Missing or unknown - // values fall back to "hybrid" (the product default; the UI schema and new-profile seeding - // agree - keep all three in sync). Applied at launch (restart to switch; not hot-swapped). - std::string model = "hybrid"; + // Which magnification model runs. "transform" (DEFAULT) = the DWM fullscreen-transform model + // (MagSetFullscreenTransform, no Magnify.exe) - revived for issue #148: it magnifies inside + // the compositor with zero app presents, the only path that stays smooth while a heavy game + // renders. "render" = the DXGI capture + D3D11 overlay. Missing or unknown values, including + // the retired "hybrid" (Auto, removed as an option in #508) and "magnify", fall back to + // "transform" (the UI schema, the tray dropdown and new-profile seeding agree - keep them in + // sync). Applied live (engine_pick.h). + std::string model = "transform"; + // DORMANT since #508 (Auto removed; read by the hybrid pick only, kept until that code goes). // Auto/hybrid exclusion list: exe names (comma-separated, case-insensitive) that must NEVER // get the transform engine, even when they are fullscreen and borderless. Fullscreen browser // video looks exactly like a game to the foreground test, but it wants the render engine (a @@ -209,7 +210,7 @@ struct Config { // to come and go between builds; it is re-applied per magnification context. KNOWN // INTERACTION: the 1px warm pulse (txWarmMode=1) puts the view 1px off-true on rest ticks; // nearest masks that as sub-block noise, smoothing can render it as visible shaking. - int txSamplingMode = 0; + int txSamplingMode = 1; // MPO buster (issue #191, hot): 1 (default) = during transform GAME sessions on MPO-ENABLED // machines, show a fullscreen alpha-1 click-through ghost that demotes the game off its // hardware overlay plane - off the plane there is no 16-bit translation field to overflow, @@ -304,11 +305,11 @@ struct Config { // config load (EffectiveSamplingMode). int mpoNearestGuard = 1; // 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. + // engine only. 0 = 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 (DEFAULT, owner pick 2026-10-24, + // #508) = 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 = 0; + int txNativeCursor = 1; // 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). @@ -418,10 +419,11 @@ struct Config { // CURSOR_SHOWING flag, which our own MagShowSystemCursor hide does NOT affect); // "always" = always draw it; "never" = never draw it. std::string cursorVisibility = "auto"; - int bilinear = 1; // RETIRED (issue #456): read only to migrate to renderScaling - // Render engine scaling filter (issue #456, upscale_shaders.h): 0 nearest, 1 xBR, 2 NVIDIA Image - // Scaling. An ini without the key migrates from `bilinear` (0 keeps nearest, else xBR). - int renderScaling = 1; + int bilinear = -1; // RETIRED (issue #456): read only to migrate; -1 = not in the ini + // Render engine scaling filter (issue #456, upscale_shaders.h): 0 nearest (DEFAULT, #508), 1 xBR, + // 2 NVIDIA Image Scaling. An ini without the key migrates from `bilinear` when it has that + // (0 keeps nearest, else xBR). + int renderScaling = 0; int upscaleTiming = 0; // 1 = log the magnify pass's GPU time every 2 s while zoomed // Adaptive sharpening of the magnified image (counters upscale blur; crisps text/detail). // 0 = off (cheapest, single tap). 0.1-1.0 = strength. Folded into the magnify pass (no extra pass). @@ -455,9 +457,9 @@ struct Config { // on SDR it's a no-op (plain BGRA8 passthrough), so it's safe on by default. Set 0 to // force the legacy BGRA8 capture even on HDR. Applied at startup + on HDR toggle. int hdrTonemap = 1; - // Multi-monitor: 0 (the shipped default) = primary monitor only; 1 = on each zoom-in, - // magnify whichever monitor the cursor is on. Hot-reloadable (applies on the next zoom-in). - int multiMonitor = 0; + // Multi-monitor: 0 = primary monitor only; 1 (DEFAULT, #508) = on each zoom-in, magnify + // whichever monitor the cursor is on. Hot-reloadable (applies on the next zoom-in). + int multiMonitor = 1; // Capture optimization (opt-in). 0 (default) = always copy all changed regions, so the cached // desktop copy is never stale. 1 = on a near-full repaint (a game redrawing the whole screen), // copy only the magnified source region (cuts the GPU copy ~zoom^2 at 4K HDR). Caveat: with 1, diff --git a/src/config/profile_switch.h b/src/config/profile_switch.h index 9d0f4d0f..bf299f24 100644 --- a/src/config/profile_switch.h +++ b/src/config/profile_switch.h @@ -34,7 +34,7 @@ inline INT_PTR LaunchWindExe() { } // A relaunch for a model change failed: keep "ini model == running model" by writing the old model -// back (an empty one means the key was unset, which reads as hybrid) and dropping session.keep. +// back (an empty one means the key was unset, which reads as transform) and dropping session.keep. // Re-reads the live ini, since another surface may have written since; `fallbackLive` (may be // null) is used when that read fails. inline void RevertModelAfterFailedRelaunch(const std::wstring& ini, const std::string& oldModel, @@ -46,7 +46,7 @@ inline void RevertModelAfterFailedRelaunch(const std::wstring& ini, const std::s if (!fallbackLive) return; cur = *fallbackLive; } - WriteTextFileAtomic(ini, UpdateIniText(cur, "model", oldModel.empty() ? "hybrid" : oldModel)); + WriteTextFileAtomic(ini, UpdateIniText(cur, "model", oldModel.empty() ? "transform" : oldModel)); } enum class ProfileSwitchError { @@ -110,7 +110,7 @@ inline ProfileSwitchResult SwitchProfile(const std::wstring& ini, const std::wst r.error = ProfileSwitchError::WriteFailed; return r; } - // ParseConfig canonicalizes (unknown or retired models -> hybrid), so every surface agrees on + // ParseConfig canonicalizes (unknown or retired models -> transform), so every surface agrees on // whether the model changed. r.oldModel = ParseConfig(oldLive).model; r.newModel = ParseConfig(newLive).model; diff --git a/src/config/profiles.cpp b/src/config/profiles.cpp index d907a3d3..b4b7b5af 100644 --- a/src/config/profiles.cpp +++ b/src/config/profiles.cpp @@ -99,8 +99,11 @@ bool SessionDiffers(const std::string& liveText, const std::string& profileText) // Issue #456: renderScaling replaced bilinear; a side without it reads as ParseConfig migrates it. for (auto* m : { &a, &b }) { auto bl = m->find("bilinear"); - if (!m->count("renderScaling") && bl != m->end() && bl->second == "0") (*m)["renderScaling"] = "0"; + if (!m->count("renderScaling") && bl != m->end()) (*m)["renderScaling"] = bl->second == "0" ? "0" : "1"; if (bl != m->end()) m->erase("bilinear"); + // #508: a retired or unknown engine (hybrid = Auto, magnify) runs Transform, as ParseConfig reads it. + auto mo = m->find("model"); + if (mo != m->end() && mo->second != "render") mo->second = "transform"; } // A key one side lacks reads as the built-in default (the first-run template carries each // templated key's default), so a tray drag that wrote an explicit default back (Warmth 0 -> 40 diff --git a/src/config_ui/main.cpp b/src/config_ui/main.cpp index a9028f9d..3a1fed8c 100644 --- a/src/config_ui/main.cpp +++ b/src/config_ui/main.cpp @@ -203,7 +203,7 @@ static std::string SessionPayload(const std::string& live) { if (WindRunning() && wind::ReadTextFileOk(wind::RunningModelPath(), running)) { while (!running.empty() && (running.back() == '\r' || running.back() == '\n' || running.back() == ' ')) running.pop_back(); // "live": both engines are up and any engine applies without a restart (no Restart button). - if (running != "render" && running != "transform" && running != "hybrid" && running != "live") running.clear(); + if (running != "render" && running != "transform" && running != "live") running.clear(); } return "\"values\":" + JsonObjectOf(values) + ",\"saved\":" + JsonObjectOf(saved) + (running.empty() ? std::string() : ",\"runningModel\":\"" + running + "\"") + @@ -436,7 +436,7 @@ static void OnExportDiagnostics() { static void OnCreateProfile(ICoreWebView2* wv, const std::string& j) { // Factory defaults by design (spec): absent keys fall back to built-in defaults. `model` is // seeded EXPLICITLY so a fresh profile file always names its engine (the - // core's own missing-key default is hybrid, the same as the UI schema's "Auto"). Globals (onboarded=1, uiPalette, showAdvanced) carry over in MakeLiveText. + // core's own missing-key default is transform, the same as the UI schema's). Globals (onboarded=1, uiPalette, showAdvanced) carry over in MakeLiveText. // fromCurrent ("Duplicate current"): the new profile is the live session as it stands, unsaved // changes included, built here from the live text so `model` (and everything else) is exactly // what is running: no restart, no write-back by the page, and the outgoing profile is NOT @@ -452,7 +452,7 @@ static void OnCreateProfile(ICoreWebView2* wv, const std::string& j) { if (!wind::EnsureProfilesSeeded(IniPath())) err = "Could not set up profiles (the config file may be locked); try again in a moment"; std::string seed = "; Wind profile. Keys absent here fall back to Wind's built-in defaults.\n" - "model=hybrid\n"; + "model=transform\n"; if (err.empty() && fromCurrent) { std::string live; if (!ReadIni(live)) err = "Could not read the config file"; diff --git a/src/tray_app/flyout_model.h b/src/tray_app/flyout_model.h index 3fe499ec..3fb8c037 100644 --- a/src/tray_app/flyout_model.h +++ b/src/tray_app/flyout_model.h @@ -203,12 +203,12 @@ inline const SliderSpec* SliderSpecs(int* count) { static const SliderSpec k[] = { { "colorWarmPct", L"Warmth", 0, 100, 0, ValueFmt::Percent, "warm", 5 }, { "colorDimPct", L"Brightness", 1, 100, 100, ValueFmt::Percent, "bright", 1 }, - { "maxLevel", L"Max zoom", 2, 30, 12, ValueFmt::TimesInt, "maxz", 1 }, - { "zoomInSpeed", L"Zoom-in speed", 0.25, 4, 1, ValueFmt::Times2, "zin", 0.05 }, - { "zoomOutSpeed", L"Zoom-out speed", 0.25, 4, 1, ValueFmt::Times2, "zout", 0.05 }, + { "maxLevel", L"Max zoom", 2, 30, 30, ValueFmt::TimesInt, "maxz", 1 }, + { "zoomInSpeed", L"Zoom-in speed", 0.25, 4, 1.5, ValueFmt::Times2, "zin", 0.05 }, + { "zoomOutSpeed", L"Zoom-out speed", 0.25, 4, 1.5, ValueFmt::Times2, "zout", 0.05 }, { "panSpeed", L"Arrow key speed", 0.25, 4, 1, ValueFmt::Times2, "pan", 0.05 }, { "panGlideMaxPx", L"Pan glide", 0, 200, 0, ValueFmt::Pixels, "smooth", 5 }, - { "zoomEaseOutMs", L"Release glide", 0, 300, 45, ValueFmt::Millis, "glide", 5 }, + { "zoomEaseOutMs", L"Release glide", 0, 300, 115, ValueFmt::Millis, "glide", 5 }, }; if (count) *count = (int)(sizeof(k) / sizeof(k[0])); return k; diff --git a/src/tray_app/flyout_test.cpp b/src/tray_app/flyout_test.cpp index 96141dde..1afb4568 100644 --- a/src/tray_app/flyout_test.cpp +++ b/src/tray_app/flyout_test.cpp @@ -6,7 +6,7 @@ // --hover K[:I] hover state: toggle:1, engine, settings, quit, profile // --tools adds the #315 engine dropdown under the toggle group // --toggles a,b,c the enabled toggle keys, in order (trackCaret, trackFocus, keepEdges, engine) -// --model M the main engine shown on the dropdown: hybrid (Auto), render, transform +// --model M the main engine shown on the dropdown: transform, render // --engine-open the dropdown drawn open (chevron up, ring) // --engine-list renders the engine list popup instead of the flyout (the active row is --model) // Fake data: zoom 7.4x at 144 fps, Warmth 40%, Brightness 72%, all three toggles shown (text cursor @@ -29,7 +29,7 @@ int RunRenderTest(const wchar_t*) { int palette = 0; Flyout::Hit hover; bool tools = false, engineList = false, engineOpen = false; - std::string toggles, model = "hybrid"; + std::string toggles, model = "transform"; auto narrow = [](const wchar_t* c) { std::string o; for (; *c; ++c) o.push_back((char)*c); return o; }; for (int i = 1; i < argc; ++i) { const std::wstring a = argv[i]; diff --git a/src/tray_app/flyout_tools.h b/src/tray_app/flyout_tools.h index d0d9999a..e7a51e30 100644 --- a/src/tray_app/flyout_tools.h +++ b/src/tray_app/flyout_tools.h @@ -16,17 +16,18 @@ namespace wind { namespace Flyout { inline constexpr const char* kEngineKey = "model"; // The options, in the order and with the labels of the Settings row (ui/src/settings-schema.js: -// hybrid = Auto, render, transform). The core reads the retired model=magnify as Auto. -inline constexpr int kEngineCount = 3; +// transform, render). Auto (hybrid) was removed in #508; the core reads it, like the retired +// model=magnify, as Transform. +inline constexpr int kEngineCount = 2; inline const char* EngineValue(int i) { - switch (i) { case 1: return "render"; case 2: return "transform"; default: return "hybrid"; } + switch (i) { case 1: return "render"; default: return "transform"; } } inline const wchar_t* EngineLabel(int i) { - switch (i) { case 1: return L"Render"; case 2: return L"Transform"; default: return L"Auto"; } + switch (i) { case 1: return L"Render"; default: return L"Transform"; } } -// The option a `model` value means. Missing or unknown reads as Auto, exactly like the core's config -// parse (src/config/config.cpp falls back to "hybrid"). +// The option a `model` value means. Missing or unknown reads as Transform, exactly like the core's +// config parse (src/config/config.cpp falls back to "transform"). inline int EngineIndex(const std::string& model) { const std::string m = TrimWs(model); for (int i = 0; i < kEngineCount; ++i) if (m == EngineValue(i)) return i; diff --git a/tests/fixtures/config_defaults.txt b/tests/fixtures/config_defaults.txt index 0d3ef82a..ec8a9038 100644 --- a/tests/fixtures/config_defaults.txt +++ b/tests/fixtures/config_defaults.txt @@ -48,13 +48,13 @@ zoomWheelMods - - 0 panKeysOn - - 1 hideCursorOn - - 1 cursorLockOn - - 1 -maxLevel 1 30 12 -zoomInSpeed 0.25 4 1 -zoomOutSpeed 0.25 4 1 +maxLevel 1 30 30 +zoomInSpeed 0.25 4 1.5 +zoomOutSpeed 0.25 4 1.5 smoothZoom - - 1 smoothZoomAccel 1 8 3 smoothZoomRamp 0.1 3 0.6 -zoomEaseOutMs 0 300 45 +zoomEaseOutMs 0 300 115 vsync - - 1 dwmFlush - - 0 diagnostics - - 0 @@ -64,12 +64,12 @@ gameCursorLagMs -1 100 -1 cursorConstantSize - - 0 trayPinned 0 1 0 cursorVisibility - - auto -model - - hybrid +model - - transform fastPan - - 1 smoothPan - - 0 magInputTransform - - 1 -bilinear - - 1 -renderScaling - - 1 +bilinear - - -1 +renderScaling - - 0 upscaleTiming - - 0 sharpness 0 1 0 zorderBand - - 0 @@ -77,7 +77,7 @@ brightness 0.5 1.5 1 colorWarmPct 0 100 0 colorDimPct 1 100 100 hdrTonemap - - 1 -multiMonitor - - 0 +multiMonitor - - 1 cropCapture - - 0 lowGpuPriority - - 0 gpuPriority -1 1 0 @@ -96,14 +96,14 @@ mouseMarginPct 0 40 0 trackLog - - 0 ixDecimate 1 16 4 mpoBuster - - 1 -txSamplingMode - - 0 +txSamplingMode - - 1 txWarmMode 0 1 1 txTrace - - 0 txRestLevel 1 1.01 1 launchQuiesce - - 1 txWarmHz 0 1000 12 mpoNearestGuard - - 1 -txNativeCursor - - 0 +txNativeCursor - - 1 txSmoothLadder - - 1 txRampMinStep 0 100 0 txRampMinFrom 1 - 12 diff --git a/tests/test_config.cpp b/tests/test_config.cpp index ce63afd8..65f425a1 100644 --- a/tests/test_config.cpp +++ b/tests/test_config.cpp @@ -7,7 +7,7 @@ TEST_CASE("defaults when text is empty") { Config c = ParseConfig(""); CHECK(c.zoomInButton == 0); // shipped unbound (onboarding captures it) CHECK(c.zoomOutButton == 0); // shipped unbound - CHECK(c.maxLevel == doctest::Approx(12.0)); + CHECK(c.maxLevel == doctest::Approx(30.0)); // owner pick (#508) CHECK(c.diagnostics == 0); // Ramp step cap ships ON (issue #219): uncapped, ~15% of high-level zoom-ins stall 35-43ms // then snap 1.2-1.9 levels; capped at 2.5%/tick every measured ramp was even. 0 would @@ -43,15 +43,16 @@ TEST_CASE("parses renderer knobs") { CHECK(c.cursorConstantSize == 1); CHECK(c.bilinear == 1); } -// Issue #456: bilinear is retired. An ini that never had renderScaling keeps nearest when it chose -// bilinear=0, and every other one gets the new default, xBR; an explicit renderScaling wins. +// Issue #456: bilinear is retired. An ini that never had renderScaling keeps what its bilinear chose +// (0 nearest, else xBR); one with neither key gets the default, nearest (#508); an explicit +// renderScaling wins. TEST_CASE("renderScaling migrates from the retired bilinear key") { CHECK(ParseConfig("bilinear=0\n").renderScaling == 0); CHECK(ParseConfig("bilinear=1\n").renderScaling == 1); - CHECK(ParseConfig("").renderScaling == 1); + CHECK(ParseConfig("").renderScaling == 0); CHECK(ParseConfig("bilinear=0\nrenderScaling=2\n").renderScaling == 2); CHECK(ParseConfig("renderScaling=1\nbilinear=0\n").renderScaling == 1); // order does not matter - CHECK(ParseConfig("renderScaling=7\n").renderScaling == 1); // out of range: default + CHECK(ParseConfig("renderScaling=7\n").renderScaling == 0); // out of range: default CHECK(ParseConfig("upscaleTiming=1\n").upscaleTiming == 1); } // Issue #253: every ini written before 0.6.3 carries an explicit cursorScaleWithZoom=0 from the @@ -66,7 +67,7 @@ TEST_CASE("renderer knobs have sane defaults") { Config c = ParseConfig(""); CHECK(c.cursorSensitivity == doctest::Approx(1.0)); CHECK(c.cursorConstantSize == 0); // cursor grows with the zoom, like transform (issue #253) - CHECK(c.renderScaling == 1); // xBR (issue #456) + CHECK(c.renderScaling == 0); // nearest (issues #456, #508) CHECK(c.upscaleTiming == 0); CHECK(c.sharpness == doctest::Approx(0.0)); // off by default CHECK(c.panGlideMaxPx == 0); @@ -77,11 +78,12 @@ TEST_CASE("renderer knobs have sane defaults") { CHECK(c.cursorVisibility == "auto"); // follow the focused app by default CHECK(c.vsync == 1); // vsync on by default CHECK(c.dwmFlush == 0); // plain vsync pacing by default (fewer stutters) - CHECK(c.multiMonitor == 0); // shipped primary-only (follow-cursor opt-in) + CHECK(c.multiMonitor == 1); // follows the cursor's monitor (#508) CHECK(c.cropCapture == 0); // shipped off: always copy all changed regions (no edge staleness) CHECK(c.smoothZoom == 1); // shipped on (eased-in zoom) - CHECK(c.zoomInSpeed == doctest::Approx(1.0)); - CHECK(c.zoomOutSpeed == doctest::Approx(1.0)); + CHECK(c.zoomInSpeed == doctest::Approx(1.5)); // owner pick (#508) + CHECK(c.zoomOutSpeed == doctest::Approx(1.5)); + CHECK(c.zoomEaseOutMs == 115); CHECK(c.smoothZoomAccel == doctest::Approx(3.0)); CHECK(c.smoothZoomRamp == doctest::Approx(0.6)); } @@ -172,11 +174,11 @@ TEST_CASE("parses overrides and ignores comments/blank lines") { CHECK(c.maxLevel == doctest::Approx(12.5)); CHECK(c.zoomInButton == 1); CHECK(c.cursorSensitivity == doctest::Approx(0.5)); - CHECK(c.zoomInSpeed == doctest::Approx(1.0)); // untouched default + CHECK(c.zoomInSpeed == doctest::Approx(1.5)); // untouched default } TEST_CASE("malformed lines are ignored, keep defaults") { Config c = ParseConfig("garbage line\nmaxLevel\n=5\n"); - CHECK(c.maxLevel == doctest::Approx(12.0)); + CHECK(c.maxLevel == doctest::Approx(30.0)); } TEST_CASE("numeric fields are clamped to documented ranges") { // maxLevel < 1 would invert ZoomController's clamp and disable zoom; must clamp up to 1.0. @@ -439,9 +441,9 @@ TEST_CASE("the high-res/MPO option is atomic at restart (issue #242)") { CHECK(ParseConfig("mpoNearestGuard=1\n").mpoNearestGuard == 1); } -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("txNativeCursor (Mouse tracking) defaults to Native and parses Wind (issues #463, #508)") { + CHECK(ParseConfig("").txNativeCursor == 1); + CHECK(ParseConfig("txNativeCursor=0\n").txNativeCursor == 0); } TEST_CASE("cursorBandAuto defaults on and parses off (issue #269)") { @@ -638,6 +640,7 @@ TEST_CASE("the first-run ini carries the new keys and parses to the struct defau } TEST_CASE("txSamplingMode folds the nearest aliases 2..4 to 0 (review 2026-10-09 #23)") { + CHECK(ParseConfig("").txSamplingMode == 1); // Smooth by default (#508) CHECK(ParseConfig("txSamplingMode=0\n").txSamplingMode == 0); CHECK(ParseConfig("txSamplingMode=1\n").txSamplingMode == 1); CHECK(ParseConfig("txSamplingMode=-1\n").txSamplingMode == -1); // leave DWM alone stays valid @@ -649,10 +652,10 @@ TEST_CASE("txSamplingMode folds the nearest aliases 2..4 to 0 (review 2026-10-09 } TEST_CASE("ParseConfig rejects NaN, infinity, hex and trailing junk, and keeps the default (review 2026-10-09 #72)") { - CHECK(ParseConfig("maxLevel=nan\n").maxLevel == doctest::Approx(12.0)); - CHECK(ParseConfig("maxLevel=inf\n").maxLevel == doctest::Approx(12.0)); - CHECK(ParseConfig("maxLevel=0x10\n").maxLevel == doctest::Approx(12.0)); - CHECK(ParseConfig("maxLevel=8abc\n").maxLevel == doctest::Approx(12.0)); + CHECK(ParseConfig("maxLevel=nan\n").maxLevel == doctest::Approx(30.0)); + CHECK(ParseConfig("maxLevel=inf\n").maxLevel == doctest::Approx(30.0)); + CHECK(ParseConfig("maxLevel=0x10\n").maxLevel == doctest::Approx(30.0)); + CHECK(ParseConfig("maxLevel=8abc\n").maxLevel == doctest::Approx(30.0)); CHECK(ParseConfig("txMaxStepPct=0x10\n").txMaxStepPct == 25); CHECK(ParseConfig("txMaxStepPct=7junk\n").txMaxStepPct == 25); CHECK(ParseConfig("maxLevel=8\n").maxLevel == doctest::Approx(8.0)); diff --git a/tests/test_config_defaults.cpp b/tests/test_config_defaults.cpp index 70244220..06c628a3 100644 --- a/tests/test_config_defaults.cpp +++ b/tests/test_config_defaults.cpp @@ -173,7 +173,7 @@ TEST_CASE("every first-run ini template key parses to its struct default (issue CHECK(SameValue(kv.second, want.text)); // and the literal itself, not a clamp of it ++compared; } - CHECK(compared > 90); + CHECK(compared > 85); for (const auto& kv : kIntendedDifferences) CHECK(seen.count(kv.first) == 1); } diff --git a/tests/test_config_model.cpp b/tests/test_config_model.cpp index ec418715..f67f9b13 100644 --- a/tests/test_config_model.cpp +++ b/tests/test_config_model.cpp @@ -3,19 +3,21 @@ using namespace wind; -TEST_CASE("model defaults to hybrid (the product default, 'Auto' in the UI)") { +TEST_CASE("model defaults to transform (the product default, #508)") { Config c = ParseConfig(""); - CHECK(c.model == "hybrid"); + CHECK(c.model == "transform"); } -TEST_CASE("model=magnify (the retired engine) and unknown models read as hybrid") { - CHECK(ParseConfig("model=magnify\n").model == "hybrid"); - CHECK(ParseConfig("model=bogus\n").model == "hybrid"); +TEST_CASE("the retired engines (hybrid = Auto, magnify) and unknown models read as transform") { + CHECK(ParseConfig("model=hybrid\n").model == "transform"); + CHECK(ParseConfig("model=magnify\n").model == "transform"); + CHECK(ParseConfig("model=bogus\n").model == "transform"); + CHECK(ParseConfig("model=render\n").model == "render"); } TEST_CASE("an old ini with retired keys still loads") { Config c = ParseConfig("model=magnify\nmagnifyStep=25\ntxKeepAliveMaxLevel=0\nmaxLevel=6\n"); - CHECK(c.model == "hybrid"); + CHECK(c.model == "transform"); CHECK(c.maxLevel == doctest::Approx(6.0)); } @@ -92,8 +94,8 @@ TEST_CASE("keys of the removed sprite cursor are ignored, and change nothing els CHECK(c.model == d.model); } -TEST_CASE("unknown model value falls back to hybrid, same as a missing key") { - CHECK(ParseConfig("model=bogus\n").model == "hybrid"); +TEST_CASE("unknown model value falls back to transform, same as a missing key") { + CHECK(ParseConfig("model=bogus\n").model == "transform"); // The three-way default (core struct, unknown-value fallback, UI schema def) must never // fork again: an ini without a model key runs the same engine the UI displays. CHECK(ParseConfig("model=bogus\n").model == ParseConfig("").model); diff --git a/tests/test_flyout_tools.cpp b/tests/test_flyout_tools.cpp index 7346eb0e..7fca0a00 100644 --- a/tests/test_flyout_tools.cpp +++ b/tests/test_flyout_tools.cpp @@ -13,14 +13,12 @@ using namespace wind::Flyout; // ---------------------------------------------------------------- engine dropdown TEST_CASE("engine: the options are the Settings main-engine row, same order and labels") { - CHECK(kEngineCount == 3); + CHECK(kEngineCount == 2); CHECK(std::string(kEngineKey) == "model"); - CHECK(std::string(EngineValue(0)) == "hybrid"); + CHECK(std::string(EngineValue(0)) == "transform"); CHECK(std::string(EngineValue(1)) == "render"); - CHECK(std::string(EngineValue(2)) == "transform"); - CHECK(std::wstring(EngineLabel(0)) == L"Auto"); + CHECK(std::wstring(EngineLabel(0)) == L"Transform"); CHECK(std::wstring(EngineLabel(1)) == L"Render"); - CHECK(std::wstring(EngineLabel(2)) == L"Transform"); } TEST_CASE("engine: every option value is one the core accepts as is") { @@ -30,19 +28,19 @@ TEST_CASE("engine: every option value is one the core accepts as is") { } } -TEST_CASE("engine: a model value maps to its option, unknown reads as Auto like the core") { - CHECK(EngineIndex("hybrid") == 0); +TEST_CASE("engine: a model value maps to its option, unknown reads as Transform like the core") { + CHECK(EngineIndex("transform") == 0); CHECK(EngineIndex("render") == 1); - CHECK(EngineIndex("transform") == 2); - CHECK(EngineIndex("magnify") == 0); // the retired engine reads as Auto - CHECK(EngineIndex(" transform ") == 2); + CHECK(EngineIndex("hybrid") == 0); // the retired Auto reads as Transform (#508) + CHECK(EngineIndex("magnify") == 0); // so does the retired magnify engine + CHECK(EngineIndex(" render ") == 1); CHECK(EngineIndex("") == 0); CHECK(EngineIndex("junk") == 0); } TEST_CASE("engine: only a different pick changes anything (and so restarts Wind)") { - CHECK_FALSE(EnginePickChanges(2, 2)); - CHECK(EnginePickChanges(0, 2)); + CHECK_FALSE(EnginePickChanges(1, 1)); + CHECK(EnginePickChanges(0, 1)); CHECK(EnginePickChanges(3, 0)); CHECK_FALSE(EnginePickChanges(0, -1)); CHECK_FALSE(EnginePickChanges(0, kEngineCount)); @@ -64,10 +62,10 @@ TEST_CASE("view: the engine item is the dropdown, not a toggle segment, and show CHECK_FALSE(IsEngineKey("trackCaret")); } -TEST_CASE("view: a missing or unknown model reads Auto, and the dropdown is never disabled by it") { - CHECK(ViewWith("engine").engine.value == L"Auto"); - CHECK(ViewWith("engine", { { "model", "junk" } }).engine.value == L"Auto"); - CHECK(ViewWith("engine", { { "model", "magnify" } }).engine.value == L"Auto"); +TEST_CASE("view: a missing or unknown model reads Transform, and the dropdown is never disabled by it") { + CHECK(ViewWith("engine").engine.value == L"Transform"); + CHECK(ViewWith("engine", { { "model", "junk" } }).engine.value == L"Transform"); + CHECK(ViewWith("engine", { { "model", "hybrid" } }).engine.value == L"Transform"); CHECK(ViewWith("engine", { { "model", "render" } }).engine.value == L"Render"); CHECK(ViewWith("engine", { { "model", "junk" } }).hasEngine); } diff --git a/tests/test_profiles.cpp b/tests/test_profiles.cpp index fe25dc76..8751ba65 100644 --- a/tests/test_profiles.cpp +++ b/tests/test_profiles.cpp @@ -113,7 +113,7 @@ TEST_CASE("MakeLiveText: empty profile means factory defaults (profile keys drop CHECK(v["onboarded"] == "1"); // global survives CHECK(v["profile"] == "Fresh"); Config c = ParseConfig(live); - CHECK(c.maxLevel == doctest::Approx(12.0)); // built-in default + CHECK(c.maxLevel == doctest::Approx(30.0)); // built-in default CHECK(c.zoomInVk == 0); } @@ -270,13 +270,15 @@ TEST_CASE("profile names: a device name is reserved with any extension, and the } TEST_CASE("SessionDiffers: UI keys outside the first-run template read as their built-in default (review #70, #76)") { - CHECK_FALSE(SessionDiffers("zoomEaseOutMs=45\n", "maxLevel=12\n")); - CHECK(SessionDiffers("zoomEaseOutMs=100\n", "maxLevel=12\n")); - CHECK_FALSE(SessionDiffers("txSamplingMode=0\n", "")); - CHECK(SessionDiffers("txSamplingMode=1\n", "")); + CHECK_FALSE(SessionDiffers("zoomEaseOutMs=115\n", "maxLevel=30\n")); + CHECK(SessionDiffers("zoomEaseOutMs=100\n", "maxLevel=30\n")); + CHECK_FALSE(SessionDiffers("txSamplingMode=1\n", "")); + CHECK(SessionDiffers("txSamplingMode=0\n", "")); CHECK_FALSE(SessionDiffers("lockApps=\n", "")); CHECK(SessionDiffers("lockApps=game.exe\n", "")); - // The core's missing-key engine is hybrid, the template's too: no special case for `model`. + // The core's missing-key engine is transform, the template's too; the retired hybrid reads as it. + CHECK_FALSE(SessionDiffers("model=transform\n", "")); CHECK_FALSE(SessionDiffers("model=hybrid\n", "")); + CHECK_FALSE(SessionDiffers("model=hybrid\n", "model=transform\n")); CHECK(SessionDiffers("model=render\n", "")); } diff --git a/ui/src/controls/EngineRow.svelte b/ui/src/controls/EngineRow.svelte index 0d651d6f..30e6b5f8 100644 --- a/ui/src/controls/EngineRow.svelte +++ b/ui/src/controls/EngineRow.svelte @@ -4,7 +4,7 @@ // until it restarts: once the chosen value differs from it, an inline "Restart Wind" appears // beside the select. `running` empty = unknown, no button. import Select from './Select.svelte'; - let { value = 'hybrid', options = [], labels = null, running = '', onChange = () => {}, + let { value = 'transform', options = [], labels = null, running = '', onChange = () => {}, onRestart = () => {}, disabled = false, labelledby, describedby } = $props(); const pending = $derived(!!running && running !== 'live' && value !== running); diff --git a/ui/src/controls/Preview.svelte b/ui/src/controls/Preview.svelte index 63855782..c4f17b6f 100644 --- a/ui/src/controls/Preview.svelte +++ b/ui/src/controls/Preview.svelte @@ -15,7 +15,7 @@ const group = groups.find((g) => g.id === q.get('group')) || groups[0]; const calls = (window.__calls = []); - let values = $state({ model: q.get('model') || 'hybrid', zoomInVk: '33', zoomInButton: '2', noSwallowApps: 'netflix.exe' }); + let values = $state({ model: q.get('model') || 'transform', zoomInVk: '33', zoomInButton: '2', noSwallowApps: 'netflix.exe' }); for (const r of groups.flatMap(groupRows)) { if (!r.key.startsWith('__') && !(r.key in values)) values[r.key] = r.def ?? ''; for (const k of bindKeys(r)) if (!(k in values)) values[k] = '0'; @@ -23,7 +23,7 @@ const visible = (r) => !r.showIf || (r.showIf.ne !== undefined ? String(values[r.showIf.key]) !== String(r.showIf.ne) : String(values[r.showIf.key]) === String(r.showIf.eq)); const set = (key, val) => { values[key] = val; calls.push([key, val]); }; const extra = { - runningModel: 'hybrid', version: '0.22.5', + runningModel: 'transform', version: '0.22.5', onRestart: () => calls.push('restart'), onAction: (a) => calls.push(['action', a]), onRepo: () => calls.push('repo'), pick: async () => 'RDR2.exe', diff --git a/ui/src/session.js b/ui/src/session.js index cbf38740..bfacb343 100644 --- a/ui/src/session.js +++ b/ui/src/session.js @@ -34,14 +34,16 @@ function schemaDefaults() { } // Issue #456: renderScaling replaced bilinear. An ini (or profile) without it reads the way the core -// migrates it (ParseConfig): bilinear=0 keeps Nearest, anything else is the xBR default. -// A model the core does not know (the retired model=magnify) runs Auto (ClampConfig), so it reads -// as hybrid here too, or the Engine select shows blank and the Auto-only rows hide. -const MODELS = new Set(['render', 'transform', 'hybrid']); +// migrates it (ParseConfig): bilinear=0 keeps Nearest, any other bilinear value xBR, and no bilinear +// either the Nearest default. +// A model the core does not know (the retired hybrid and magnify) runs Transform (ClampConfig), so +// it reads as transform here too, or the Engine select shows blank. +const MODELS = new Set(['render', 'transform']); function migrate(m) { let out = m; - if (m.renderScaling === undefined && String(m.bilinear) === '0') out = { ...out, renderScaling: '0' }; - if (m.model !== undefined && !MODELS.has(String(m.model).trim())) out = { ...out, model: 'hybrid' }; + if (m.renderScaling === undefined && m.bilinear !== undefined) + out = { ...out, renderScaling: String(m.bilinear) === '0' ? '0' : '1' }; + if (m.model !== undefined && !MODELS.has(String(m.model).trim())) out = { ...out, model: 'transform' }; return out; } diff --git a/ui/src/settings-schema.js b/ui/src/settings-schema.js index c36de620..4de7c76a 100644 --- a/ui/src/settings-schema.js +++ b/ui/src/settings-schema.js @@ -21,10 +21,6 @@ // Copy rules: one short plain line, no punctuation or symbols, no references, no toggle labels starting with "Enable", // no description that restates its label, no product names. -const hybrid = { key: 'model', eq: 'hybrid' }; -const engineOpts = ['auto', 'transform', 'render']; -const engineLabels = { auto: 'Auto', transform: 'Transform', render: 'Render' }; - export const groups = [ { id: 'hotkeys', label: 'Hotkeys', icon: 'hotkeys', desc: 'Keys that control zooming', @@ -63,10 +59,10 @@ export const groups = [ desc: 'How far and how fast to zoom', cards: [ { caption: 'Level and speed', rows: [ - { key: 'maxLevel', type: 'slider', label: 'Max zoom', keywords: ['maximum', 'limit', 'highest', 'magnification', 'level', 'cap', 'zoom level', 'factor', 'times', 'upper'], desc: 'The highest zoom level', min: 2, max: 30, step: 1, def: 12.0, unit: 'times' }, - { key: 'zoomInSpeed', type: 'slider', label: 'Zoom-in speed', keywords: ['faster', 'slower', 'rate', 'magnify speed', 'zoom rate', 'acceleration', 'velocity', 'how fast'], desc: 'How fast the view zooms in', min: 0.25, max: 4, step: 0.05, def: 1.0, unit: 'times' }, - { key: 'zoomOutSpeed', type: 'slider', label: 'Zoom-out speed', keywords: ['faster', 'slower', 'rate', 'zoom rate', 'velocity', 'how fast'], desc: 'How fast the view zooms out', min: 0.25, max: 4, step: 0.05, def: 1.0, unit: 'times' }, - { key: 'zoomEaseOutMs', type: 'slider', label: 'Release glide', keywords: ['inertia', 'coast', 'momentum', 'ease out', 'easing', 'smooth stop', 'slow down', 'deceleration', 'stop', 'let go', 'release', 'delay', 'milliseconds'], desc: 'How long the zoom coasts after you let go', min: 0, max: 300, step: 5, def: 45, unit: 'ms' }, + { key: 'maxLevel', type: 'slider', label: 'Max zoom', keywords: ['maximum', 'limit', 'highest', 'magnification', 'level', 'cap', 'zoom level', 'factor', 'times', 'upper'], desc: 'The highest zoom level', min: 2, max: 30, step: 1, def: 30.0, unit: 'times' }, + { key: 'zoomInSpeed', type: 'slider', label: 'Zoom-in speed', keywords: ['faster', 'slower', 'rate', 'magnify speed', 'zoom rate', 'acceleration', 'velocity', 'how fast'], desc: 'How fast the view zooms in', min: 0.25, max: 4, step: 0.05, def: 1.5, unit: 'times' }, + { key: 'zoomOutSpeed', type: 'slider', label: 'Zoom-out speed', keywords: ['faster', 'slower', 'rate', 'zoom rate', 'velocity', 'how fast'], desc: 'How fast the view zooms out', min: 0.25, max: 4, step: 0.05, def: 1.5, unit: 'times' }, + { key: 'zoomEaseOutMs', type: 'slider', label: 'Release glide', keywords: ['inertia', 'coast', 'momentum', 'ease out', 'easing', 'smooth stop', 'slow down', 'deceleration', 'stop', 'let go', 'release', 'delay', 'milliseconds'], desc: 'How long the zoom coasts after you let go', min: 0, max: 300, step: 5, def: 115, unit: 'ms' }, ] }, { caption: 'Easing', rows: [ // Smooth zoom is always on (core default 1); its shape sliders are advanced. @@ -74,37 +70,23 @@ export const groups = [ { key: 'smoothZoomRamp', type: 'slider', adv: true, label: 'Soft start length', keywords: ['ease', 'duration', 'time', 'ramp', 'seconds', 'how long', 'smooth', 'smoothing', 'animation', 'curve'], desc: 'How long the soft start lasts', min: 0.1, max: 3, step: 0.1, def: 0.6, unit: 'seconds' }, ] }, { caption: 'Engine', rows: [ - { key: 'model', type: 'engine', adv: true, label: 'Engine', keywords: ['renderer', 'render', 'mode', 'transform', 'auto', 'hybrid', 'model', 'backend', 'method', 'magnifier', 'gpu', 'capture', 'dwm', 'restart', 'compositor'], desc: 'How the screen is zoomed', - options: ['hybrid', 'render', 'transform'], - optionLabels: { hybrid: 'Auto', render: 'Render', transform: 'Transform' }, - def: 'hybrid' }, - // SCALING: how each engine enlarges the screen, under the engine it belongs to (Auto uses - // both, so it shows both). Not advanced, so the card stays reachable without them. + // Not advanced since #508: with Auto gone the card would otherwise show one scaling row alone. + { key: 'model', type: 'engine', label: 'Engine', keywords: ['renderer', 'render', 'mode', 'transform', 'model', 'backend', 'method', 'magnifier', 'gpu', 'capture', 'dwm', 'restart', 'compositor'], desc: 'How the screen is zoomed', + options: ['transform', 'render'], + optionLabels: { transform: 'Transform', render: 'Render' }, + def: 'transform' }, + // SCALING: how each engine enlarges the screen, under the engine it belongs to. Not advanced, so the card stays reachable without them. // Transform: txSamplingMode, DWM's own filter (0 nearest / 1 smooth = Lanczos, the former High // resolution cursor toggle, #227). It applies live (#369) and turning Smooth on asks first (#441). { key: 'txSamplingMode', type: 'select', label: 'Transform scaling', tag: 'Experimental', keywords: ['scaling', 'upscaling', 'filter', 'sharp', 'crisp', 'smooth', 'blurry', 'pixelated', 'hidpi', 'sampling', 'quality', 'nearest', 'lanczos', 'bitmap smoothing', 'high resolution cursor', 'mpo', 'overlay', 'cursor', 'pointer', 'resolution', '4k', 'antialiasing'], - desc: 'Smooth also sharpens the pointer at high zoom', options: ['0', '1'], optionLabels: { '0': 'Nearest', '1': 'Smooth' }, def: '0', + desc: 'Smooth also sharpens the pointer at high zoom', options: ['0', '1'], optionLabels: { '0': 'Nearest', '1': 'Smooth' }, def: '1', showIf: { key: 'model', ne: 'render' } }, - // Render: renderScaling (issue #456), the engine's scaling filter: nearest, xBR (default) or - // NVIDIA Image Scaling, picked in the 2026-10-12 upscaler A/B. Applies on the config reload. + // Render: renderScaling (issue #456), the engine's scaling filter: nearest (default, #508), xBR + // or NVIDIA Image Scaling, picked in the 2026-10-12 upscaler A/B. Applies on the config reload. { key: 'renderScaling', type: 'select', label: 'Render scaling', keywords: ['scaling', 'upscaling', 'filter', 'sharp', 'crisp', 'smooth', 'blurry', 'pixelated', 'sampling', 'quality', 'nearest', 'xbr', 'nis', 'nvidia', 'image scaling', 'bilinear', 'point', 'resolution', 'antialiasing'], desc: 'How the image is enlarged', options: ['0', '1', '2'], - optionLabels: { '0': 'Nearest', '1': 'xBR', '2': 'NVIDIA Image Scaling' }, def: '1', + optionLabels: { '0': 'Nearest', '1': 'xBR', '2': 'NVIDIA Image Scaling' }, def: '0', showIf: { key: 'model', ne: 'transform' } }, - // PER-WINDOW-TYPE ENGINE (2026-08-24). Every row defaults to Auto so an untouched install - // behaves as before. They only apply when the engine above is Auto, so showIf hides them - // when a single engine is pinned. ("Never use Render for" left Settings in #318; the - // renderExclude ini key and the core's own detection stay.) - { key: 'engineGame', type: 'select', adv: true, label: 'Engine for games', keywords: ['renderer', 'render', 'transform', 'auto', 'fullscreen', 'borderless', 'video games', 'gaming', 'mode', 'per window', 'per app'], desc: 'Full screen and borderless games', - options: engineOpts, optionLabels: engineLabels, def: 'auto', showIf: hybrid }, - { key: 'engineAcrylic', type: 'select', adv: true, label: 'Engine for blurred windows', keywords: ['renderer', 'render', 'transform', 'auto', 'acrylic', 'mica', 'transparent', 'translucent', 'glass', 'blur', 'mode', 'per window'], - desc: 'Windows with blurred see through backgrounds', - options: engineOpts, optionLabels: engineLabels, def: 'auto', showIf: hybrid }, - { key: 'engineDesktop', type: 'select', adv: true, label: 'Engine for the desktop', keywords: ['renderer', 'render', 'transform', 'auto', 'taskbar', 'explorer', 'file windows', 'shell', 'mode', 'per window'], - desc: 'The desktop taskbar and file windows', - options: engineOpts, optionLabels: engineLabels, def: 'auto', showIf: hybrid }, - { key: 'engineOther', type: 'select', adv: true, label: 'Engine for other windows', keywords: ['renderer', 'render', 'transform', 'auto', 'apps', 'programs', 'rest', 'everything else', 'default', 'mode', 'per window'], desc: 'All other windows', - options: engineOpts, optionLabels: engineLabels, def: 'auto', showIf: hybrid }, ] }, ] }, @@ -124,7 +106,7 @@ export const groups = [ // 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', + desc: 'Who follows the mouse while zoomed', options: ['1', '0'], optionLabels: { '1': 'Native', '0': 'Wind' }, def: '1', 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. diff --git a/ui/tests/a11y.spec.js b/ui/tests/a11y.spec.js index 4e626281..ad7d6ad4 100644 --- a/ui/tests/a11y.spec.js +++ b/ui/tests/a11y.spec.js @@ -13,7 +13,7 @@ test.beforeEach(async ({ page }) => { window.__msgs = []; // Built on the first getConfig so a test's own init script is already set. let live, saved; - const init = () => { if (live) return; live = { maxLevel: '12', zoomInVk: '33', zoomInButton: '2', cursorLockVk: '113', model: 'hybrid', + const init = () => { if (live) return; live = { maxLevel: '12', zoomInVk: '33', zoomInButton: '2', cursorLockVk: '113', model: 'transform', uiTheme: 'dark', trackCaret: '1', noSwallowApps: 'netflix.exe', showAdvanced: '1' }; saved = { ...live }; }; const listeners = new Set(); diff --git a/ui/tests/interaction.spec.js b/ui/tests/interaction.spec.js index 5ab7d5d2..8372e7cf 100644 --- a/ui/tests/interaction.spec.js +++ b/ui/tests/interaction.spec.js @@ -4,7 +4,7 @@ import { test, expect } from '@playwright/test'; // switches off under prefers-reduced-motion. test.beforeEach(async ({ page }) => { await page.addInitScript(() => { - const live = { maxLevel: '12', model: 'hybrid', uiTheme: 'dark' }; + const live = { maxLevel: '12', model: 'transform', uiTheme: 'dark' }; const saved = { ...live }; const listeners = new Set(); const send = (data) => listeners.forEach((fn) => fn({ data })); diff --git a/ui/tests/prefs.spec.js b/ui/tests/prefs.spec.js index 401631e7..086e08b2 100644 --- a/ui/tests/prefs.spec.js +++ b/ui/tests/prefs.spec.js @@ -11,7 +11,7 @@ test.beforeEach(async ({ page }) => { window.__msgs = []; const init = () => { if (window.__live) return; - window.__live = { maxLevel: '12', model: 'hybrid', uiTheme: 'dark', ...(window.__cfgExtra || {}) }; + window.__live = { maxLevel: '12', model: 'transform', uiTheme: 'dark', ...(window.__cfgExtra || {}) }; window.__saved = { ...window.__live }; window.__profiles = { names: window.__names || ['Default', 'Gaming'], active: 'Default' }; }; @@ -42,7 +42,7 @@ test.beforeEach(async ({ page }) => { window.__profiles.names.push(msg.name); window.__profiles.active = msg.name; // fromCurrent: the host builds the profile from the live session, unsaved changes included. if (msg.fromCurrent === '1') window.__saved = { ...window.__live }; - else { window.__live = { model: 'hybrid', uiTheme: window.__live.uiTheme }; window.__saved = { ...window.__live }; } + else { window.__live = { model: 'transform', uiTheme: window.__live.uiTheme }; window.__saved = { ...window.__live }; } reply(); } else if (msg.type === 'deleteProfile') { window.__profiles.names = window.__profiles.names.filter((n) => n !== msg.name); diff --git a/ui/tests/responsive.spec.js b/ui/tests/responsive.spec.js index a04a67c4..3c90fd5d 100644 --- a/ui/tests/responsive.spec.js +++ b/ui/tests/responsive.spec.js @@ -8,7 +8,7 @@ const boot = (extra = {}) => async (page) => { await page.addInitScript((extra) => { const listeners = new Set(); const live = { maxLevel: '12', zoomInVk: '33', zoomInButton: '2', zoomOutVk: '114', cursorLockVk: '113', - model: 'hybrid', uiTheme: 'dark', showAdvanced: '1', ...extra }; + model: 'transform', uiTheme: 'dark', showAdvanced: '1', ...extra }; window.chrome = { webview: { addEventListener: (_e, fn) => listeners.add(fn), postMessage: (msg) => { diff --git a/ui/tests/scaling-notice.spec.js b/ui/tests/scaling-notice.spec.js index 414b0d3d..21a4589c 100644 --- a/ui/tests/scaling-notice.spec.js +++ b/ui/tests/scaling-notice.spec.js @@ -6,7 +6,7 @@ test.beforeEach(async ({ page }) => { await page.addInitScript(() => { window.__skipSplash = true; window.__msgs = []; - window.__live = { maxLevel: '12', model: 'hybrid', uiTheme: 'dark' }; + window.__live = { maxLevel: '12', model: 'transform', txSamplingMode: '0', uiTheme: 'dark' }; window.__saved = { ...window.__live }; const listeners = new Set(); const send = (data) => listeners.forEach((fn) => fn({ data })); diff --git a/ui/tests/schema.spec.js b/ui/tests/schema.spec.js index 434b6aac..08ada069 100644 --- a/ui/tests/schema.spec.js +++ b/ui/tests/schema.spec.js @@ -2,10 +2,11 @@ import { test, expect } from '@playwright/test'; import { readFileSync, readdirSync } from 'node:fs'; import { groups, allRows, groupRows, bindKeys } from '../src/settings-schema.js'; -// Row keys of the #303 schema. Every one must still appear exactly once after #318, except the two -// REMOVED rows: the scroll-wheel row (the wheel is a Zoom in / Zoom out binding now) and "Never use -// Render for" (renderExclude stays in the ini and the core, not in Settings). -const REMOVED = ['__zoomWheel', 'renderExclude']; +// Row keys of the #303 schema. Every one must still appear exactly once after #318, except the +// REMOVED rows: the scroll-wheel row (the wheel is a Zoom in / Zoom out binding now), "Never use +// Render for" (renderExclude stays in the ini and the core, not in Settings) and the per-window +// engine rows, which only applied to the removed Auto engine (#508). +const REMOVED = ['__zoomWheel', 'renderExclude', 'engineGame', 'engineAcrylic', 'engineDesktop', 'engineOther']; const OLD_KEYS = [ '__zoomIn', '__zoomOut', '__zoomWheel', '__pan', // was four rows (Pan left/right/up/down); their ini keys live in OLD_BIND_KEYS 'noSwallowApps', 'maxLevel', 'zoomInSpeed', 'zoomOutSpeed', 'panSpeed', 'smoothZoomAccel', @@ -57,7 +58,7 @@ test('a captioned section has two or more rows, and the advanced rows are the on if (base.length) expect(base.length, g.id + ' / ' + c.caption + ' (advanced off)').toBeGreaterThanOrEqual(2); } expect(allRows.filter((r) => r.adv).map((r) => r.key).sort()).toEqual( - ['engineAcrylic', 'engineDesktop', 'engineGame', 'engineOther', 'lockApps', 'model', 'noSwallowApps', 'smoothZoomAccel', 'smoothZoomRamp'].sort()); + ['lockApps', 'noSwallowApps', 'smoothZoomAccel', 'smoothZoomRamp'].sort()); // Troubleshooting is never advanced, and the extra keys carry their switch key. for (const k of ['diagnostics', '__diagnostics', '__openIni']) expect(allRows.find((r) => r.key === k).adv).toBeUndefined(); expect(Object.fromEntries(allRows.filter((r) => r.onKey).map((r) => [r.key, r.onKey]))).toEqual( @@ -111,10 +112,12 @@ test('rows land in the groups the spec names', () => { expect(where('__about')).toBe('about'); }); -test('per-window engines show only when the engine is Auto', () => { - for (const k of ['engineGame', 'engineAcrylic', 'engineDesktop', 'engineOther']) - expect(allRows.find((r) => r.key === k).showIf).toEqual({ key: 'model', eq: 'hybrid' }); - expect(allRows.find((r) => r.key === 'model').type).toBe('engine'); +test('the engine row offers Transform (the default) and Render only; Auto is gone (#508)', () => { + const m = allRows.find((r) => r.key === 'model'); + expect(m.type).toBe('engine'); + expect(m.options).toEqual(['transform', 'render']); + expect(m.def).toBe('transform'); + expect(JSON.stringify(allRows)).not.toMatch(/hybrid/); expect(allRows.find((r) => r.key === 'mouseMarginPct').showIf).toEqual({ key: 'mouseAlign', eq: '1' }); // the margin only matters when the pointer is kept within the edges }); @@ -149,10 +152,10 @@ test('hotkeys page: one box per binding, "or" between them, and the old keybind // slider: end-cap thumb and value readout await page.goto('/controls.html?group=zoom'); const sl = ctl(page, 'maxLevel').locator('input[type=range]'); - await expect(sl).toHaveAttribute('aria-valuetext', '12 times'); + await expect(sl).toHaveAttribute('aria-valuetext', '30 times'); // the default (#508) + await sl.fill('12'); + await expect(ctl(page, 'maxLevel').locator('.val')).toHaveText('12x'); expect(parseFloat(await css(sl, '--pct'))).toBeCloseTo(35.71, 1); // 12x on the 2-30x range (#429) - await sl.fill('30'); - await expect(ctl(page, 'maxLevel').locator('.val')).toHaveText('30x'); }); test('toggle and select drive their row value', async ({ page }) => { @@ -165,15 +168,13 @@ test('toggle and select drive their row value', async ({ page }) => { await expect(ctl(page, 'trackAlign').locator('select')).toHaveValue('1'); }); -test('engine row offers Restart Wind and per-window rows follow Auto; the share-keys app list manages apps', async ({ page }) => { +test('engine row offers Restart Wind; the share-keys app list manages apps', async ({ page }) => { await page.goto('/controls.html?group=zoom'); - await expect(ctl(page, 'engineGame')).toBeVisible(); await expect(page.getByRole('button', { name: 'Restart Wind' })).toHaveCount(0); await ctl(page, 'model').locator('select').selectOption('render'); await expect(page.getByRole('button', { name: 'Restart Wind' })).toBeVisible(); - await expect(ctl(page, 'engineGame')).toHaveCount(0); await page.getByRole('button', { name: 'Restart Wind' }).click(); - await ctl(page, 'model').locator('select').selectOption('hybrid'); + await ctl(page, 'model').locator('select').selectOption('transform'); await expect(page.getByRole('button', { name: 'Restart Wind' })).toHaveCount(0); await page.goto('/controls.html?group=hotkeys'); const apps = ctl(page, 'noSwallowApps'); diff --git a/ui/tests/search.spec.js b/ui/tests/search.spec.js index 7fa8cd5c..a4b3dab4 100644 --- a/ui/tests/search.spec.js +++ b/ui/tests/search.spec.js @@ -10,7 +10,7 @@ test.beforeEach(async ({ page }) => { window.__msgs = []; const listeners = new Set(); const send = (data) => listeners.forEach((fn) => fn({ data })); - window.__live = { maxLevel: '12', model: 'hybrid', uiTheme: 'dark' }; + window.__live = { maxLevel: '12', model: 'transform', uiTheme: 'dark' }; window.chrome = { webview: { addEventListener: (_e, fn) => listeners.add(fn), postMessage: (msg) => { @@ -118,7 +118,7 @@ test('meta keywords reach rows without their label words, incl. the centred-curs expect(keys('night light')[0]).toBe('colorWarmPct'); expect(keys('blue light')[0]).toBe('colorWarmPct'); expect(keys('dim')).toContain('colorDimPct'); - expect(keys('renderer')).toEqual(expect.arrayContaining(['model', 'engineGame'])); + expect(keys('renderer')).toEqual(expect.arrayContaining(['model'])); expect(keys('freeze')).toContain('__cursorLock'); expect(keys('crosshair')).toContain('__cursorLock'); expect(keys('arrows')).toContain('__pan'); @@ -138,10 +138,9 @@ test('every searchable row has meta keywords, and no keyword is empty or has an test('advanced rows are searchable and say so; removed and hero rows are not indexed', () => { const engine = search(groups, 'engine'); - expect(engine.find((h) => h.key === 'model').adv).toBe(true); - expect(engine.find((h) => h.key === 'engineGame').adv).toBe(true); + expect(engine.find((h) => h.key === 'model').adv).toBeFalsy(); // a plain row since #508 expect(search(groups, 'maximum').every((h) => !h.adv)).toBe(true); - expect(keys('advanced')).toEqual(expect.arrayContaining(['model', 'noSwallowApps', 'lockApps', 'smoothZoomAccel'])); + expect(keys('advanced')).toEqual(expect.arrayContaining(['noSwallowApps', 'lockApps', 'smoothZoomAccel'])); expect(keys('advanced')).not.toContain('maxLevel'); expect(keys('render')).not.toContain('renderExclude'); expect(keys('logo')).not.toContain('__about'); diff --git a/ui/tests/session.spec.js b/ui/tests/session.spec.js index 2fd7d168..8b172d0c 100644 --- a/ui/tests/session.spec.js +++ b/ui/tests/session.spec.js @@ -7,7 +7,7 @@ import { changedKeys, fill, GLOBAL_KEYS } from '../src/session.js'; test.beforeEach(async ({ page }) => { await page.addInitScript(() => { window.__msgs = []; - window.__live = { maxLevel: '12', zoomInVk: '33', zoomInButton: '2', model: 'hybrid', uiTheme: 'dark', ...(window.__liveExtra || {}) }; + window.__live = { maxLevel: '12', zoomInVk: '33', zoomInButton: '2', model: 'transform', uiTheme: 'dark', ...(window.__liveExtra || {}) }; window.__saved = { ...window.__live }; window.__profiles = { names: ['Default', 'Gaming'], active: 'Default' }; const listeners = new Set(); @@ -60,10 +60,10 @@ test('session helpers: global keys never count, defaults fill both sides', () => expect(changedKeys(f.values, f.saved)).toEqual(['maxLevel']); }); -test('session helpers: an unknown model (the retired magnify) reads as hybrid, like the core', () => { - const f = fill({ values: { model: 'magnify' }, saved: { model: 'magnify' } }); - expect(f.values.model).toBe('hybrid'); - expect(f.saved.model).toBe('hybrid'); +test('session helpers: an unknown model (the retired hybrid and magnify) reads as transform, like the core', () => { + const f = fill({ values: { model: 'magnify' }, saved: { model: 'hybrid' } }); + expect(f.values.model).toBe('transform'); + expect(f.saved.model).toBe('transform'); expect(changedKeys(f.values, f.saved)).toEqual([]); expect(fill({ values: { model: 'transform' } }).values.model).toBe('transform'); expect(fill({ values: { model: 'render' } }).values.model).toBe('render'); @@ -266,8 +266,8 @@ test('engine change writes the ini, then Restart Wind relaunches; a failed relau await page.evaluate(() => window.__hostSend({ type: 'restartFailed' })); await expect(page.getByRole('dialog', { name: "Couldn't restart Wind" })).toBeVisible(); await page.getByRole('button', { name: 'Close' }).last().click(); - await expect(page.locator('[data-key="model"] select')).toHaveValue('hybrid'); - expect(await page.evaluate(() => window.__live.model)).toBe('hybrid'); + await expect(page.locator('[data-key="model"] select')).toHaveValue('transform'); + expect(await page.evaluate(() => window.__live.model)).toBe('transform'); }); test('a Wind with both engines up switches engine live: no Restart Wind button', async ({ page }) => { @@ -276,7 +276,7 @@ test('a Wind with both engines up switches engine live: no Restart Wind button', await go(page, 'prefs'); await page.locator('[data-key="showAdvanced"]').getByRole('switch').check({ force: true }); // the engine row is advanced await go(page, 'zoom'); - for (const m of ['render', 'transform', 'hybrid']) { + for (const m of ['render', 'transform']) { await page.locator('[data-key="model"] select').selectOption(m); expect(await page.evaluate(() => window.__live.model)).toBe(m); await expect(page.getByRole('button', { name: 'Restart Wind' })).toHaveCount(0); @@ -284,6 +284,7 @@ test('a Wind with both engines up switches engine live: no Restart Wind button', }); test('Smooth transform scaling applies live: no MPO registry write, no restart prompt (#369)', async ({ page }) => { + await page.addInitScript(() => { window.__live.txSamplingMode = '0'; window.__saved.txSamplingMode = '0'; }); await page.goto('/'); await go(page, 'zoom'); const sel = page.locator('[data-key="txSamplingMode"] select'); @@ -298,7 +299,10 @@ test('Smooth transform scaling applies live: no MPO registry write, no restart p }); test('an ini from before renderScaling keeps Nearest when it had bilinear=0 (#456)', async ({ page }) => { - await page.addInitScript(() => { window.__live.bilinear = '0'; window.__saved.bilinear = '0'; }); + await page.addInitScript(() => { + window.__live.bilinear = '0'; window.__saved.bilinear = '0'; + window.__live.model = 'render'; window.__saved.model = 'render'; // the row shows for the render engine + }); await page.goto('/'); await go(page, 'zoom'); await expect(page.locator('[data-key="renderScaling"] select')).toHaveValue('0'); diff --git a/ui/tests/settings.spec.js b/ui/tests/settings.spec.js index 60a3b45c..1c267ab4 100644 --- a/ui/tests/settings.spec.js +++ b/ui/tests/settings.spec.js @@ -10,7 +10,7 @@ test.beforeEach(async ({ page }) => { const init = () => { if (window.__live) return; window.__live = { maxLevel: '12', zoomInVk: '33', zoomInButton: '2', zoomOutVk: '114', - cursorLockVk: '113', model: 'hybrid', uiTheme: 'dark', ...(window.__cfgExtra || {}) }; + cursorLockVk: '113', model: 'transform', uiTheme: 'dark', ...(window.__cfgExtra || {}) }; window.__saved = { ...window.__live }; }; const listeners = new Set(); @@ -67,7 +67,7 @@ test('pages show their sections; a section with nothing to show is dropped', asy await expect(page.locator('main .card')).toHaveCount(2); // Zoom, Extra keys await expect(page.locator('main .cap')).toHaveText(['Zoom', 'Extra keys']); await go(page, 'zoom'); - await expect(page.locator('main .cap')).toHaveText(['Level and speed', 'Engine']); // Easing is advanced; Engine keeps its scaling rows + await expect(page.locator('main .cap')).toHaveText(['Level and speed', 'Engine']); // Easing is advanced; Engine keeps its row and scaling await go(page, 'view'); await expect(page.locator('main .cap')).toHaveText(['Movement', 'Typing and focus']); await go(page, 'prefs'); @@ -90,7 +90,6 @@ test('copy: the row names and descriptions follow the table, and "Never use Rend await go(page, 'zoom'); await expect(page.locator('main .cap')).toHaveText(['Level and speed', 'Easing', 'Engine']); await expect(page.locator('[data-key="smoothZoomAccel"] .label')).toHaveText('Soft start'); - await expect(page.locator('[data-key="engineGame"] .desc')).toHaveText('Full screen and borderless games'); await expect(page.getByText('Never use Render for')).toHaveCount(0); await expect(key(page, 'renderExclude')).toHaveCount(0); }); @@ -119,7 +118,8 @@ test('Show advanced settings reveals the advanced rows inline, with no marker, a await page.goto('/'); await expect(key(page, 'noSwallowApps')).toHaveCount(0); await go(page, 'zoom'); - for (const k of ['model', 'engineGame', 'smoothZoomAccel', 'smoothZoomRamp']) await expect(key(page, k)).toHaveCount(0); + await expect(key(page, 'model')).toBeVisible(); // the engine row is a plain row since #508 + for (const k of ['smoothZoomAccel', 'smoothZoomRamp']) await expect(key(page, k)).toHaveCount(0); await go(page, 'view'); await expect(key(page, 'lockApps')).toHaveCount(0); await expect(key(page, 'panGlideMaxPx')).toBeVisible(); // Pan glide is a plain row (#430) @@ -132,7 +132,7 @@ test('Show advanced settings reveals the advanced rows inline, with no marker, a expect((await sent(page, 'setConfig')).some((m) => m.key === 'showAdvanced' && String(m.value) === '1')).toBe(true); await expect(page.locator('.capsule')).toHaveCount(0); await go(page, 'zoom'); - for (const k of ['model', 'engineGame', 'smoothZoomAccel', 'smoothZoomRamp']) await expect(key(page, k)).toBeVisible(); + for (const k of ['model', 'smoothZoomAccel', 'smoothZoomRamp']) await expect(key(page, k)).toBeVisible(); await expect(page.locator('main .cap')).toHaveText(['Level and speed', 'Easing', 'Engine']); await go(page, 'view'); await expect(key(page, 'lockApps')).toBeVisible(); @@ -154,16 +154,19 @@ test('an advanced switch left on in the ini shows the advanced rows at launch', await expect(key(page, 'showAdvanced').getByRole('switch')).toBeChecked(); }); -test('Zoom engine: the engine row, per-window rows follow Auto, the share-keys app list writes through the session', async ({ page }) => { +test('Zoom engine: Transform and Render only, each with its scaling row; the share-keys app list writes through the session', async ({ page }) => { await page.addInitScript(() => { window.__cfgExtra = { showAdvanced: '1' }; }); await page.goto('/'); await go(page, 'zoom'); + await expect(key(page, 'model').locator('option')).toHaveText(['Transform', 'Render']); + await expect(key(page, 'txSamplingMode')).toBeVisible(); + await expect(key(page, 'renderScaling')).toHaveCount(0); await expect(page.getByRole('button', { name: 'Restart Wind' })).toHaveCount(0); await key(page, 'model').locator('select').selectOption('render'); await expect(page.getByRole('button', { name: 'Restart Wind' })).toBeVisible(); - await expect(key(page, 'engineGame')).toHaveCount(0); - await key(page, 'model').locator('select').selectOption('hybrid'); - await expect(key(page, 'engineGame')).toBeVisible(); + await expect(key(page, 'renderScaling')).toBeVisible(); + await expect(key(page, 'txSamplingMode')).toHaveCount(0); + await key(page, 'model').locator('select').selectOption('transform'); await go(page, 'hotkeys'); const apps = key(page, 'noSwallowApps'); await apps.getByRole('button', { name: /Manage/ }).click(); @@ -190,12 +193,14 @@ test('an app list with a hand-edited duplicate still opens, listing it once (#49 expect(errors).toEqual([]); }); -test('a legacy model=magnify shows Auto with its per-window rows (#495)', async ({ page }) => { - await page.addInitScript(() => { window.__cfgExtra = { showAdvanced: '1', model: 'magnify' }; }); - await page.goto('/'); - await go(page, 'zoom'); - await expect(key(page, 'model').locator('select')).toHaveValue('hybrid'); - await expect(key(page, 'engineGame')).toBeVisible(); +test('a legacy model=magnify or hybrid shows Transform (#495, #508)', async ({ page }) => { + for (const legacy of ['magnify', 'hybrid']) { + await page.addInitScript((m) => { window.__cfgExtra = { showAdvanced: '1', model: m }; }, legacy); + await page.goto('/'); + await go(page, 'zoom'); + await expect(key(page, 'model').locator('select')).toHaveValue('transform'); + await expect(page.locator('.capsule')).toHaveCount(0); + } }); // ---- other pages ----------------------------------------------------------------------------------- @@ -209,6 +214,7 @@ test('View: the edge margin shows only when the pointer is kept within the edges }); test('Smooth transform scaling is tagged Experimental and keeps the release glide (#439)', async ({ page }) => { + await page.addInitScript(() => { window.__cfgExtra = { txSamplingMode: '0' }; }); await page.goto('/'); await go(page, 'zoom'); await expect(key(page, 'txSamplingMode').locator('.tag')).toHaveText('Experimental'); diff --git a/ui/tests/tray.spec.js b/ui/tests/tray.spec.js index 4bae0e8e..c82e99d7 100644 --- a/ui/tests/tray.spec.js +++ b/ui/tests/tray.spec.js @@ -9,7 +9,7 @@ test.beforeEach(async ({ page }) => { window.__msgs = []; const init = () => { if (window.__live) return; - window.__live = { maxLevel: '12', model: 'hybrid', uiTheme: 'dark', ...(window.__cfgExtra || {}) }; + window.__live = { maxLevel: '12', model: 'transform', uiTheme: 'dark', ...(window.__cfgExtra || {}) }; window.__saved = { ...window.__live }; }; const listeners = new Set();