Intra-frame Trail: a KWin effect that redraws the cursor at every position sampled during the previous frame. Modern mouses report far more positions per frame than the compositor draws, so a moving pointer leaves a trail of the copies it would otherwise skip; drawing them improves the pointer's visual smoothness and click accuracy. C++ and OpenGL, built against kwin's effect API. Needs an OpenGL compositor.
Not just for extreme 8000Hz mice: Even with a standard 250Hz office mouse on a 60Hz display, it quadruples the perceived cursor density, turning choppy, nauseating cursor jumping into smooth, readable motion.
The following is a demonstration video; the right side shows the view with this KWin effect enabled.
trail-demo.mp4
The human visual system has a relatively low temporal resolution for tracking fast-moving objects; instead, spatial information strongly dominates our perception of motion. When a cursor moves rapidly across a screen, it normally renders as a series of disconnected, discrete jumps.
Therefore, even on modern displays with high refresh rates like 120Hz or above, embedding more spatial information about the cursor's trajectory within a single frame provides crucial visual context. By drawing the intermediate positions that the mouse hardware reports between frames, this effect bridges the visual gaps. This dramatically enhances the brain's perception of smooth, continuous motion and greatly improves your subconscious ability to track and predict the cursor's exact location.
KWin 6.7.x on Plasma 6, with an OpenGL-capable compositor: the effect draws with OpenGL.
The source only uses the effect API as it exists throughout the 6.7 series, so any 6.7 point release builds it. CMakeLists.txt asks for KWin without a version, and the pin comes from KWin instead: config-kwin.h embeds the full version in the plugin IID (org.kde.kwin.EffectPluginFactory6.7.5), which KWin compares with its own before instantiating anything. The KWin development headers must therefore be the exact same version as the running KWin, and the effect has to be rebuilt after every KWin upgrade -- a 6.7.0 build does not load on 6.7.5. See "Pitfalls".
There is no distribution package yet, so the effect is compiled from source and installed system-wide. KWin loads the plugin from Qt's plugin directories, which on most distributions means the install prefix has to be /usr.
The effect itself only uses the KWin effect API, but KWin's CMake package requires its own full development set: Qt 6 Core/Gui/Widgets/DBus/Quick, KF6 Config/CoreAddons/ WindowSystem, libepoxy, libdrm, Vulkan and Wayland-server. CMake refuses to configure until every one of them is present, so install the development packages even though most of them look unrelated to a cursor effect.
Ubuntu/Debian (Plasma 6). Use a release that ships KWin 6.7.x, such as Debian forky/sid or KDE neon; Debian trixie ships 6.3, which this effect does not load on:
sudo apt install cmake extra-cmake-modules build-essential \
qt6-base-dev qt6-declarative-dev \
libkf6config-dev libkf6coreaddons-dev libkf6windowsystem-dev \
kwin-dev libepoxy-dev libdrm-dev libvulkan-dev libwayland-dev
Arch Linux. The KWin headers and CMake files are inside the kwin package; there is no separate -devel package:
sudo pacman -S --needed base-devel cmake extra-cmake-modules \
kwin qt6-base qt6-declarative \
kconfig kcoreaddons kwindowsystem \
libepoxy libdrm vulkan-headers vulkan-icd-loader wayland
Fedora:
sudo dnf install cmake gcc-c++ extra-cmake-modules \
qt6-qtbase-devel qt6-qtdeclarative-devel \
kf6-kconfig-devel kf6-kcoreaddons-devel kf6-kwindowsystem-devel \
kwin-devel libepoxy-devel libdrm-devel \
vulkan-loader-devel wayland-devel
Install the KWin development package from the same repository as the running Plasma, so that its version matches the running KWin exactly: a 6.7.4 header set cannot build a plugin for 6.7.5 (see "Pitfalls").
The install prefix must be /usr, the same prefix as the system Qt. KWin searches the Qt plugin directories, which do not include /usr/local, so a build configured without a prefix installs to a location KWin never scans and the effect stays invisible in System Settings.
# 1. get the source
git clone https://github.com/rainac1/kwin-effect-intra-frame-trail.git
cd kwin-effect-intra-frame-trail
# 2. configure (the prefix must be /usr)
cmake -B build -S . -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release
# 3. build
cmake --build build -j"$(nproc)"
# 4. install into the system (needs root)
sudo cmake --install build
Installation places the compiled .so in Qt's KWin effect directory, for example /usr/lib64/qt6/plugins/kwin/effects/plugins/trail.so on Fedora or /usr/lib/x86_64-linux-gnu/qt6/plugins/kwin/effects/plugins/trail.so on Debian/Ubuntu. The plugin metadata (src/metadata.json) is embedded in the .so; no separate JSON or desktop file is installed.
AUTOMOC cannot see the file name inside the KWIN_EFFECT_FACTORY macro, so when you edit src/metadata.json the embedded metadata is not regenerated on its own:
rm -rf build/src/trail_autogen
Installing does not enable the effect. Switch it on in System Settings:
- Open System Settings.
- Go to Window Management -> Desktop Effects.
- Find "Intra-frame Trail" in the list and tick its checkbox.
- Click Apply.
Apply loads the effect into the running compositor (it calls loadEffect over D-Bus), so no logout is needed. The same can be done without the GUI:
kwriteconfig6 --file kwinrc --group Plugins --key trailEnabled true
gdbus call --session --dest org.kde.KWin --object-path /Effects \
--method org.kde.kwin.Effects.loadEffect trail
qdbus org.kde.KWin /KWin reconfigure is not part of this and does not help: it re-reads
KWin's own settings (options and window rules) but never calls the effect loader, so it
neither rescans the plugin directories nor loads the effect. Enabling is the Apply button
or the loadEffect call above.
To ask the running compositor what it sees:
gdbus call --session --dest org.kde.KWin --object-path /Effects \
--method org.kde.kwin.Effects.isEffectSupported trail
gdbus call --session --dest org.kde.KWin --object-path /Effects \
--method org.kde.kwin.Effects.isEffectLoaded trail
Installing a new build does not change what a running KWin executes. Qt's plugin loader
caches a plugin by path, so unloadEffect + loadEffect re-creates the effect from the
same mapping instead of reading the file again. A rebuilt .so therefore only runs in a
new compositor process: log out and back in after sudo cmake --install build.
qdbus org.kde.KWin /KWin reconfigure does not reset that cache and does not reload the
plugin; it never calls the effect loader at all.
-
Plasma 5 and Plasma 6 are incompatible.
Plasma 6 moved KWin to Qt 6 and rebuilt its internals, so a C++ effect written for Plasma 5 does not load on Plasma 6, and the other way round. Build the branch that matches your Plasma major version.
-
KWin has no stable C++ plugin ABI.
KWin ships no long-term stable C++ plugin ABI. Even within Plasma 6 a point release can change an internal API and break an existing effect, or crash KWin. On top of the ABI, this effect carries a hard version guard: the factory IID contains KWin's full version (org.kde.kwin.EffectPluginFactory6.7.5), and KWin refuses to instantiate a plugin built against a different one. After a KWin update the symptom is usually just "the effect disappeared", because the mismatch is logged at debug level. Rebuild against the matching KWin development package (kwin-devel, kwin-dev on Debian/Ubuntu). This is also why C++ effects normally have to be rebuilt after each system update.
-
The install prefix trap.
A build configured without
-DCMAKE_INSTALL_PREFIX=/usrdefaults to /usr/local, and KWin's plugin search covers the system Qt plugin directories under /usr, not /usr/local.sudo cmake --install buildsucceeds but the effect never appears in Desktop Effects. Always pass -DCMAKE_INSTALL_PREFIX=/usr. -
The compositor keeps the .so it started with.
Rebuilding and installing changes the file on disk, not what the running KWin executes. Log out and back in; see "Apply a rebuild".
qdbus org.kde.KWin /KWin reconfiguredoes not help -- it never reaches the effect loader.
~/.config/kwinrc:
[Plugins]
trailEnabled=true
[Effect-trail]
Enabled=true
MaxSamples=256
SampleStride=1
The trail is always exactly one frame long: the effect draws every sample the pointer produced since the previous frame. MaxSamples caps how many cursors are drawn per frame. SampleStride keeps one out of every N samples and drops the rest before they are buffered, so the drawn cursors end up N times further apart while the trail still covers that same frame. N=1 keeps every sample. KWin re-reads the [Effect-trail] settings when kwinrc changes; the [Plugins] trailEnabled switch is only read at session start (docs/USAGE.md).
src/trailsample.{h,cpp} sample ring buffer, cursor geometry, sampling window
src/traileffect.{h,cpp} Effect + InputEventSpy, damage tracking, GL drawing
src/main.cpp plugin factory
src/metadata.json embedded plugin metadata
docs/USAGE.md discovery, configuration, troubleshooting
docs/DESIGN.md design and API research
The plugin id is the file name, trail.so, not a field in metadata.json.
The trail is color-managed: it is converted from sRGB into the output's color description, so it follows the display brightness, night light and HDR settings the same way the composited pointer does, in both SDR and HDR modes.
The effect keeps one repaint scheduled while a trail is on screen. The trail is drawn on top of the scene, so it only disappears when a later frame repaints the region it was drawn into; while the pointer moves, the motion events schedule those frames anyway, but once it stops nothing else produces damage and the last copy would stay behind as a bright edge around the pointer.
Screenshots and screencasts will not show the trail. TRAIL_KWIN_SELFCHECK=1 makes the effect read back its own pixels instead and log how many it changed.
Every output gets the trail, including on multi-monitor and mixed-scaling desks: each output has its own sample buffer and damage, so one monitor being painted first no longer consumes the samples of the others.
Shake Cursor magnifies the pointer on fast back-and-forth motion, which is exactly the motion that reveals a trail:
kwriteconfig6 --file kwinrc --group Plugins --key shakecursorEnabled false
Set Adaptive Sync to Automatic or Off rather than Always. The trail is not drawn while a frame is presented in adaptive sync mode, and on Always that is every frame; on Automatic it is only the windows that ask for it, so the trail is there everywhere else. What VRR does is drop the display's refresh rate to the content's, and drop it at once - a 60 fps video on a 120 Hz display takes it from 120 Hz to 60 Hz. The trail is what makes that drop obvious, because it is drawn at the lowered rate and steps visibly with it, so the effect stands back until the display is at its own rate again. The pointer stutters in that situation with the effect disabled as well, so the effect is not the cause.
Never run kwin_wayland --replace or restart the display manager in your session; that is how the desktop goes black and windows are lost. To try a freshly built plugin safely, start a nested KWin instead (docs/USAGE.md, "Nested session").
rainac1 rainandcap@gmail.com -- design, implementation and packaging.
The KWin effect API and the headers this plugin builds against are the work of the KDE community and are used here, not claimed. Third-party reference checkouts (KWin sources and headers, kept out of the repository) keep their own copyright notices.
Copyright (C) 2026 rainac1 rainandcap@gmail.com
GPL-3.0-only. The effect includes KWin headers and links libkwin; both are GPL-2.0-or-later, whose "or later" allows the combination to be used under GPL-3.0. Qt6 is available under GPL-3.0 as well, so the whole work is licensed as GPL-3.0-only. The full text is in LICENSE. The grant is version 3 only, not "or any later version": the "or later" wording inside LICENSE belongs to the FSF's sample notice for applying the license, not to this project.
