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();