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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ are frozen history cited from code, not kept in step with it.
- `build.bat`: `Wind.exe` + `WindTray.exe` (uiAccess=false). Wind refuses to start without UIAccess;
run a plain build with `WIND_ALLOW_NO_UIACCESS=1` (or `WIND_SELFTEST=1`).
- `build.bat test`: builds and runs the doctest binary; exit 0 = pass.
- `build.bat check`: compile-only pass over `src\*.cpp`, the tray sources and WindConfig's host.
- `build.bat check`: compile-only pass over Wind.exe's sources, the tray sources and WindConfig's host.
- `build.bat uiaccess`: the uiAccess=true build; works only signed and run from `C:\Program Files\Wind`.
- `build.bat config`: builds the Svelte UI (`ui/` -> `ui/dist/`) and `WindConfig.exe`.
- `build.bat tray`: `WindTray.exe` alone. It always uses the plain manifest; never make it uiAccess.
Expand All @@ -22,8 +22,13 @@ are frozen history cited from code, not kept in step with it.
## Stack and map
- Three binaries: `Wind.exe` (core, UIAccess when installed), `WindConfig.exe` (WebView2 settings),
`WindTray.exe` (tray flyout, never UIAccess). Settings travel only through `magnifier.ini`. See 01.
- `src/` is grouped by area (`core`, `engine`, `render`, `transform`, `color`, `input`, `cursor`,
`track`, `zoom`, `config`, `tray`, `platform`, `tray_app`, `config_ui`; map in 01). Include a Wind
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_pick.h`. `model=magnify` is
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.
- Profiles: `profiles\<Name>.ini` next to the live ini; the live ini is the session, the profile file
the saved state. See 08.
Expand All @@ -44,7 +49,7 @@ Chapter numbers refer to `docs/architecture/NN-*.md`.

**Magnification runtime (05)**
- Never call `MagInitialize`/`MagUninitialize` directly; use `wind::MagApiAcquire()`/`MagApiRelease()`
(`src/mag_host.*`). Independent pairs break each other: two cursors, or transform writes returning
(`src/transform/mag_host.*`). Independent pairs break each other: two cursors, or transform writes returning
FALSE. Keep every hold symmetric.
- The COMPOSED pointer (DWM drawing it into the magnified frame) taxes every cursor change any app
makes; a context alone does not (measured 2026-10-07). The transform engine builds the context +
Expand All @@ -60,13 +65,13 @@ Chapter numbers refer to `docs/architecture/NN-*.md`.
Never add `QS_RAWINPUT`/`QS_INPUT` to the mask.
- Tick counts depend on the refresh rate; derive new ones through `TicksAtHz`/`setTickRate`.
- A config reload rebuilds `ZoomController`; UI-only keys are stripped by `StripUiOnlyKeys` so they
never reload. Add new UI-only keys to `kUiOnlyKeys` (`src/profiles.h`) and
never reload. Add new UI-only keys to `kUiOnlyKeys` (`src/config/profiles.h`) and
`tests/fixtures/global_keys.txt` (the UI's `GLOBAL_KEYS` is tested against it).

**Cursor (07)**
- In a transform session the lock (`lockApps`, tells) applies only while the pointer is hidden
(`LockApplies`); gates read `t.lockEff`, not `t.detector.locked()`. See 07.
- Native cursor (the transform engine's only cursor, both sampling modes; `src/native_cursor.h`): DWM
- Native cursor (the transform engine's only cursor, both sampling modes; `src/transform/native_cursor.h`): DWM
draws the real pointer via Wind's cursor lens (a hidden `WC_MAGNIFIER` window, built on the tick
thread at idle) and centres the view itself (`SetFullscreenMagnifierOffsetsDWMUpdated`). While DWM
centres, never write a same-level transform or warm pulse. Never use a public write to get the
Expand Down Expand Up @@ -95,7 +100,7 @@ Chapter numbers refer to `docs/architecture/NN-*.md`.
- Bound keys are swallowed by LL hooks with balanced down/up; release swallowed keys on teardown.
- The keyboard hook is the authority for bound-key state; only the mouse hook skips Wind's own
injections (`kWindInjectTag`), the keyboard hook counts the Alt/Win mask key.
- Bind rules live in `src/keybind_rules.h` and `ui/src/lib/keybindRules.js`, both tested against
- Bind rules live in `src/input/keybind_rules.h` and `ui/src/lib/keybindRules.js`, both tested against
`tests/fixtures/keybind_cases.txt`: change both.
- LL hooks cannot block Raw Input, so bound keys still reach raw-input games. No driver-based fix.

Expand All @@ -105,7 +110,7 @@ Chapter numbers refer to `docs/architecture/NN-*.md`.
- Keep the 2 px right/bottom clamp and the 1-texel left/top floor in `ComputeMagTransform` (TDR and
grey-edge classes).
- MPO on + nearest sampling overflows a 16-bit driver field above ~9.3x at the far right unless the
MPO guard (`src/mpo_guard.h`, default on) keeps apps off planes while zoomed. Never turn the guard
MPO guard (`src/transform/mpo_guard.h`, default on) keeps apps off planes while zoomed. Never turn the guard
off with nearest on an MPO boot; walls come back only when it is off.
- `txWarmMode`/`txWarmHz`: every warm write is a full DWM re-render; composition-rate metrics miss
the pan-start hitch. Field-verify in a game.
Expand Down
4 changes: 2 additions & 2 deletions THIRD-PARTY-NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ Copyright (c) 2016-2026 Svelte contributors. The Svelte runtime is compiled into
which ships inside the installer. https://github.com/sveltejs/svelte

### xBR-lv2 shader (MIT)
Copyright (C) 2011-2022 Hyllian (sergiogdb@gmail.com). Ported to HLSL in `src/upscale_shaders.h`
Copyright (C) 2011-2022 Hyllian (sergiogdb@gmail.com). Ported to HLSL in `src/render/upscale_shaders.h`
(the render engine's xBR scaling) from the libretro slang-shaders
`edge-smoothing/xbr/shaders/xbr-lv2-standalone.slang`. https://github.com/libretro/slang-shaders

### NVIDIA Image Scaling 1.0.2 (MIT)
Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. Ported to a pixel shader
in `src/upscale_nis.h` (the render engine's NVIDIA Image Scaling) via agyild's mpv port.
in `src/render/upscale_nis.h` (the render engine's NVIDIA Image Scaling) via agyild's mpv port.
https://github.com/NVIDIAGameWorks/NVIDIAImageScaling

## Build and test only, not shipped
Expand Down
30 changes: 19 additions & 11 deletions build.bat
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ set "BUILD=%ROOT%build"
set "OBJ=%BUILD%\obj"
if not exist "%BUILD%" mkdir "%BUILD%"

rem --- Source folders ---------------------------------------------------------------
rem src\ is grouped by area (the source map in docs\architecture\01-overview.md). Wind headers are
rem included by bare file name ("config.h", never a path), so every compile gets every src folder on
rem its include path. A new folder goes in WIND_INC, and in CORE_SRC when it holds Wind.exe sources.
rem wind.rc, resource.h and version.h stay in src\ itself: the version readers expect them there.
set "WIND_INC=/I src /I src\core /I src\engine /I src\render /I src\transform /I src\color /I src\input /I src\cursor /I src\track /I src\zoom /I src\config /I src\tray /I src\platform /I src\tray_app /I src\config_ui"
set "CORE_SRC=src\core\*.cpp src\render\*.cpp src\transform\*.cpp src\color\*.cpp src\input\*.cpp src\cursor\*.cpp src\track\*.cpp src\zoom\*.cpp src\config\*.cpp src\tray\*.cpp"

if /i "%1"=="test" goto :test
if /i "%1"=="check" goto :check
if /i "%1"=="uiaccess" goto :uiaccess
Expand Down Expand Up @@ -65,9 +73,9 @@ if not exist "%CORE_OBJ%" mkdir "%CORE_OBJ%"
rem Compile the app-icon resource (rc.exe ships with the Windows SDK, on PATH via vcvars).
rc /nologo /fo "%CORE_OBJ%\wind.res" "%ROOT%src\wind.rc"
if errorlevel 1 (echo [build] rc.exe failed & exit /b 1)
cl /nologo /std:c++17 /EHsc /O2 /W4 /Zi /DUNICODE /D_UNICODE %~1 ^
cl /nologo /std:c++17 /EHsc /O2 /W4 /Zi /DUNICODE /D_UNICODE %~1 %WIND_INC% ^
/Fo"%CORE_OBJ%\\" /Fd"%CORE_OBJ%\\" ^
src\*.cpp "%CORE_OBJ%\wind.res" ^
%CORE_SRC% "%CORE_OBJ%\wind.res" ^
/Fe:"%BUILD%\Wind.exe" ^
/link Magnification.lib Dwmapi.lib user32.lib shell32.lib gdi32.lib Dbghelp.lib ^
d3d11.lib dxgi.lib dxguid.lib d3dcompiler.lib windowscodecs.lib ole32.lib oleaut32.lib uuid.lib advapi32.lib ^
Expand All @@ -87,9 +95,9 @@ if not exist "%OBJ%\tray" mkdir "%OBJ%\tray"
rc /nologo /fo "%OBJ%\tray\wind_tray.res" "%ROOT%src\tray_app\wind_tray.rc"
if errorlevel 1 (echo [build] rc.exe failed for WindTray & exit /b 1)
rem Own object dir (build\obj\tray) so the shared sources never overwrite Wind.exe's .obj files.
cl /nologo /std:c++17 /EHsc /O2 /W4 /Zi /DUNICODE /D_UNICODE ^
cl /nologo /std:c++17 /EHsc /O2 /W4 /Zi /DUNICODE /D_UNICODE %WIND_INC% ^
/Fo"%OBJ%\tray\\" /Fd"%OBJ%\tray\\" ^
src\tray_app\*.cpp src\tray_items.cpp src\profiles.cpp src\config.cpp src\logging.cpp src\ini_edit.cpp ^
src\tray_app\*.cpp src\tray\tray_items.cpp src\config\profiles.cpp src\config\config.cpp src\core\logging.cpp src\config\ini_edit.cpp ^
"%OBJ%\tray\wind_tray.res" ^
/Fe:"%BUILD%\WindTray.exe" ^
/link user32.lib shell32.lib gdi32.lib Dwmapi.lib Dbghelp.lib shlwapi.lib ole32.lib version.lib ^
Expand All @@ -116,9 +124,9 @@ rem Same app-icon resource as Wind.exe (rc.exe ships with the Windows SDK, on PA
rc /nologo /fo "%OBJ%\config\wind.res" "%ROOT%src\wind.rc"
if errorlevel 1 (echo [build] rc.exe failed & exit /b 1)
cl /nologo /std:c++17 /EHsc /O2 /W4 /DUNICODE /D_UNICODE ^
/I third_party\webview2\include ^
/I third_party\webview2\include %WIND_INC% ^
/Fo"%OBJ%\config\\" /Fd"%OBJ%\config\\" ^
src\config_ui\main.cpp src\ini_edit.cpp src\profiles.cpp src\config.cpp src\logging.cpp "%OBJ%\config\wind.res" ^
src\config_ui\main.cpp src\config\ini_edit.cpp src\config\profiles.cpp src\config\config.cpp src\core\logging.cpp "%OBJ%\config\wind.res" ^
/Fe:"%BUILD%\WindConfig.exe" ^
/link third_party\webview2\x64\WebView2LoaderStatic.lib ^
user32.lib shell32.lib shlwapi.lib ole32.lib version.lib advapi32.lib ntdll.lib Dwmapi.lib /SUBSYSTEM:WINDOWS
Expand All @@ -136,10 +144,10 @@ rem --- Test build (pure-logic sources only; no <windows.h>) -----------------
:test
rem /wd5285 silences a known doctest 2.4.11 header warning under MSVC /W4.
if not exist "%OBJ%\test" mkdir "%OBJ%\test"
cl /nologo /std:c++17 /EHsc /W4 /wd5285 /DWIND_TESTS /I third_party ^
cl /nologo /std:c++17 /EHsc /W4 /wd5285 /DWIND_TESTS /I third_party %WIND_INC% ^
/Fo"%OBJ%\test\\" /Fd"%OBJ%\test\\" ^
tests\*.cpp ^
src\transform.cpp src\zoom_controller.cpp src\config.cpp src\profiles.cpp src\cursor_mapper.cpp src\lock_detector.cpp src\cursor_lock.cpp src\crosshair.cpp src\ini_edit.cpp src\logging.cpp src\tray_items.cpp src\hitch_record.cpp src\render_params.cpp src\tray_app\tray_pin_logic.cpp ^
src\transform\transform.cpp src\zoom\zoom_controller.cpp src\config\config.cpp src\config\profiles.cpp src\cursor\cursor_mapper.cpp src\cursor\lock_detector.cpp src\cursor\cursor_lock.cpp src\cursor\crosshair.cpp src\config\ini_edit.cpp src\core\logging.cpp src\tray\tray_items.cpp src\core\hitch_record.cpp src\render\render_params.cpp src\tray_app\tray_pin_logic.cpp ^
/Fe:"%BUILD%\wind_tests.exe"
if errorlevel 1 exit /b 1
rem Runs from the repo root (the current directory): the tests find tests\fixtures from there.
Expand All @@ -149,13 +157,13 @@ exit /b %errorlevel%
rem --- Compile-only check (no link; verifies all sources compile) -----------
:check
for %%D in (core tray config) do if not exist "%OBJ%\check\%%D" mkdir "%OBJ%\check\%%D"
cl /nologo /std:c++17 /EHsc /W4 /DUNICODE /D_UNICODE /c /Fo"%OBJ%\check\core\\" src\*.cpp
cl /nologo /std:c++17 /EHsc /W4 /DUNICODE /D_UNICODE /c %WIND_INC% /Fo"%OBJ%\check\core\\" %CORE_SRC%
if errorlevel 1 exit /b 1
rem WindTray.exe sources (issue #291); own object dir, its main.cpp would overwrite Wind's main.obj.
cl /nologo /std:c++17 /EHsc /W4 /DUNICODE /D_UNICODE /c /Fo"%OBJ%\check\tray\\" src\tray_app\*.cpp
cl /nologo /std:c++17 /EHsc /W4 /DUNICODE /D_UNICODE /c %WIND_INC% /Fo"%OBJ%\check\tray\\" src\tray_app\*.cpp
if errorlevel 1 exit /b 1
rem WindConfig.exe's host (same flags as :config); own object dir for the same reason.
cl /nologo /std:c++17 /EHsc /W4 /DUNICODE /D_UNICODE /c /I third_party\webview2\include ^
cl /nologo /std:c++17 /EHsc /W4 /DUNICODE /D_UNICODE /c /I third_party\webview2\include %WIND_INC% ^
/Fo"%OBJ%\check\config\\" src\config_ui\main.cpp
exit /b %errorlevel%

Expand Down
2 changes: 1 addition & 1 deletion docs/COLOUR-FILTER-FINDINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ harness, not committed.)

Windows draws the pointer on a hardware cursor plane after composition, outside the DWM effect
(Night light does reach it). Wind swaps the standard pointers for tinted copies while idle
(`src/cursor_tint.*`): at warmth 100 the arrow read 246,76,0 in Desktop Duplication, size and
(`src/color/cursor_tint.*`): at warmth 100 the arrow read 246,76,0 in Desktop Duplication, size and
hotspot kept. Zoom-in swaps back in 1.1 ms. Idle restores always reload the real scheme, because
in-memory copies of the pointers are not identical to the scheme's own. A force-killed Wind leaves
the tint until its next start. Night-light apps that use the gamma ramp (f.lux, Iris) have the same
Expand Down
2 changes: 1 addition & 1 deletion docs/HITCH-FINDINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ smoothness, which is why the transform model stays the default for games.

Both engines are built at startup, so `model` and the per-category keys apply live on the config
reload (#455); no restart. The pick rules are in [03](architecture/03-engines.md) and
`src/engine_pick.h`.
`src/engine/engine_pick.h`.

| config | ini | behaviour today |
|---|---|---|
Expand Down
8 changes: 4 additions & 4 deletions docs/TRACKING-FINDINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ follows only the terminal caret there.
field's caret early.
- **Click quiet period, 1 s.** A click that opens a page moves focus somewhere the user never asked
to look, so caret and focus changes within 1 s of a mouse button are not followed. A fresh down of
a non-modifier key ends it early (#328, `src/typing_key.h`); key-ups and auto-repeat must not, or
a non-modifier key ends it early (#328, `src/input/typing_key.h`); key-ups and auto-repeat must not, or
releasing Ctrl after a Ctrl+click handed the view to the click's own caret.
- **Glide: critically damped spring, 200 ms** (A/B of 0, 25, 150, 200 ms and old ease versus
spring). The old exponential ease restarted on every keystroke; the spring carries its velocity,
Expand Down Expand Up @@ -78,7 +78,7 @@ follows only the terminal caret there.
- Outlook on the web reports the caret as one line after Enter, then as a rect that also covers the
blank lines above; the bottom stays on the caret line. Centring on it pushed the text lower with
every blank line.
- `src/caret_rect.h` learns the one-line height per focus and trims a rect taller than 1.4 lines to
- `src/track/caret_rect.h` learns the one-line height per focus and trims a rect taller than 1.4 lines to
one line at its bottom, but only when its top climbed more than half a line above the previous
line's top (headings and font changes grow downward and are learned instead). UIA and Win32 carets
are trimmed; Java carets are not.
Expand All @@ -92,7 +92,7 @@ follows only the terminal caret there.
made it follow again for a while. The focused element is xterm's `xterm-helper-textarea`, which
xterm moves onto the cursor cell on every cursor move, but Chromium's selection rect for it goes
stale for seconds (one typed line: ~60 textarea moves, 6 selection-rect changes).
- For that class (`IsXtermInput`, `src/caret_rect.h`) the textarea's own bounds are the caret
- For that class (`IsXtermInput`, `src/track/caret_rect.h`) the textarea's own bounds are the caret
(`xterm-input` in trackLog), and its bounds are left out of the focus identity, since they move
with every key.

Expand All @@ -112,6 +112,6 @@ about to wrap, the caret is reported for one keystroke at the right edge of the
space that hangs past the wrap: `2996,1944 1x49` -> `3375,1942 2x54` -> the next real position. Following it
put the view on the composer's buttons with the text off screen. The ghost sits on the same line but in a
different box (2 px higher, 5 px taller) and far to the right after a single key (380 px; a character is
8-30 px). `IsLineEndGhost` (src/caret_rect.h) skips a caret on the same line whose box changed by 2 px or
8-30 px). `IsLineEndGhost` (src/track/caret_rect.h) skips a caret on the same line whose box changed by 2 px or
more and that jumped right by more than 3 line heights; End and clicks keep the caret's box, so they are
followed. trackLog logs `caret skipped (line-end ghost)`.
Loading
Loading