From 57115148c78d7a46ab665699ea6fb4580c24e382 Mon Sep 17 00:00:00 2001 From: Yelsin Sepulveda Date: Sun, 18 Jan 2026 11:09:55 -0500 Subject: [PATCH] quick update url Signed-off-by: Yelsin Sepulveda --- astro.config.mjs | 2 +- src/content.config.ts | 14 +- .../project/0.kwin-project-intro/index.md | 36 +++ .../project/1.kwin-project-week-1-2/index.md | 110 +++++++++ .../project/2.kwin-project-week-3-4/index.md | 221 ++++++++++++++++++ .../project/3.kwin-project-week-5-6/index.md | 183 +++++++++++++++ src/layouts/Projects.astro | 11 + src/pages/Projects.astro | 34 +++ 8 files changed, 609 insertions(+), 2 deletions(-) create mode 100644 src/content/project/0.kwin-project-intro/index.md create mode 100644 src/content/project/1.kwin-project-week-1-2/index.md create mode 100644 src/content/project/2.kwin-project-week-3-4/index.md create mode 100644 src/content/project/3.kwin-project-week-5-6/index.md create mode 100644 src/layouts/Projects.astro create mode 100644 src/pages/Projects.astro diff --git a/astro.config.mjs b/astro.config.mjs index 2b5fed2..3b8ec31 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -7,7 +7,7 @@ import react from '@astrojs/react'; // https://astro.build/config export default defineConfig({ - site: "https://blog.yorisoft.dev/", + site: "http://blog.yorisoft.dev/", vite: { plugins: [tailwindcss()], }, diff --git a/src/content.config.ts b/src/content.config.ts index 868073d..abb203c 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -12,6 +12,18 @@ const blog = defineCollection({ }), }); +const project = defineCollection({ + type: 'content', + schema: z.object({ + title: z.string(), + date: z.coerce.date(), + description: z.string(), + url: z.string(), + thumbnail: z.string(), + techList: z.string(), + }), +}); + export const collections = { - blog, + blog, project }; diff --git a/src/content/project/0.kwin-project-intro/index.md b/src/content/project/0.kwin-project-intro/index.md new file mode 100644 index 0000000..a748ca1 --- /dev/null +++ b/src/content/project/0.kwin-project-intro/index.md @@ -0,0 +1,36 @@ +--- +title: GSoC 2025 Project Blog Intro - Improving Game Controller Support in KWin +authors: + - yorisoft +date: 2025-05-12 +thumbnail: https://community.kde.org/images.community/thumb/a/af/Mascot_konqi-base-plasma.png/250px-Mascot_konqi-base-plasma.png +SPDX-License-Identifier: CC-BY-SA-4.0 +SPDX-FileCopyrightText: 2025 Yelsin Sepulveda +--- + +# Hello KDE Community! + +My name is Yelsin ['yorisoft'](https://invent.kde.org/yorisoft) Sepulveda. I'm an engineer with experience in DevOps, and Cloud Computing. I joined KDE as part of the GSoC application process early last month and have been contributing to a few projects ever since. Miraculously, my GSoC proposal has been selected! ❤️ *Hallelujah!* Which means over this summer **I'll be working on implementing game controller input recognition into KWin.** + +## About the Project + +Currently, applications directly manage controller input, leading to inconsistencies, the inability of the system to recognize controller input for power management, and unintentionally enabling/disabling "lizard mode" in certain controllers. This project proposes a solution to unify game controller input within KWin by capturing controller events, creating a virtual controller emulation layer, and ensuring proper routing of input to applications. This project aims to address the following issues: +- **System Power Management**: KWin lacks controller input recognition, preventing activity reporting and causing premature system sleep. +- **"Lizard Mode"**: When KWin opens a file descriptor for certain gaming controller devices (like the Steam Controller and Steam Deck Controller), this disables those controllers' lizard mode (keyboard and mouse input emulation), since the controller detects that a program is now handling input?even if KWin isn't actively using it. +- **Decentralized Input Handling**: Individual application input handling results in inconsistent input parsing and limited remapping capabilities. + +## Project Goals + +The primary goals of this project are to: +- Enable KWin to capture and process game controller input events. +- Implement a virtual controller emulation layer within KWin. +- Route physical controller input 1:1 to emulated devices, including haptics. +- Prevent system sleep during active controller use. +- Manage "lizard mode" for compatible controllers. +- Establish a foundation for future features: global remapping, haptics settings, and advanced Wayland protocols. + +## About Me + +I often spend my time surfing the internet learning new things, spending quality time with family and friends, or picking up new hobbies and skills-such as music! You could say I'm someone who likes to jump between multiple hobbies and interests. As of late, I'm learning a new [snare solo](https://youtu.be/q887A3B0tZQ?si=WNsSR2d4Me2TTU3o) and how to build an online brand. + +I started my career as a DevOps Engineer and SRE where I learned tools like Jenkins, Docker, and Terraform. I then transitioned to a Solutions Architect role where I worked with many different cloud technologies and helped other companies design their cloud architecture. I am relatively new to contributing to open-source projects but have been an avid user of Linux and open-source tools for over 4 years, and am committed to learning and growing in this community. Check me out on [GitHub](https://github.com/Yorisoft). diff --git a/src/content/project/1.kwin-project-week-1-2/index.md b/src/content/project/1.kwin-project-week-1-2/index.md new file mode 100644 index 0000000..2e2896e --- /dev/null +++ b/src/content/project/1.kwin-project-week-1-2/index.md @@ -0,0 +1,110 @@ +--- +title: "GSoC'25 Kwin Project Blog Post: Week 1-2" +authors: + - yorisoft +date: 2025-06-24 +thumbnail: /images/games.svg +SPDX-License-Identifier: CC-BY-SA-4.0 +SPDX-FileCopyrightText: 2025 Yelsin Sepulveda +--- + +These past few week’s my focus was on exploring input device detection and event handling mechanisms in Linux, with a particular emphasis on game controllers and their potential integration into KWin. +I also spent time reading through KWin’s input-related source code to understand how it currently manages devices, and began reviewing documentation for various Linux input subsystems—including `evdev`, HID, and `/dev/input/jsX` in order to evaluate which layer would provide the most reliable and straight forward support for integrating controller recognition. +The time was mostly spent learning how to use different libraries, tools and creating virtual controller prototype. + + +## Tools, Libraries, and Concepts Used + +### libevdev + +`libevdev` is a library for handling `evdev` devices. +It provides a higher-level interface over `/dev/input/event*` and abstracts much of the complexity of input event parsing. +> `evdev` is the generic input event interface. This is the preferred interface for userspace to consume user input, and all clients are encouraged to use it. +> +> -The kernel development community. + +`libevdev` can be used to: +- Detect physical game controllers. +- Read input events (e.g., joystick, buttons). +- Create virtual input device and write/forward events to it from physical game controller. + +### Useful functions: +- `libevdev_new()`, `libevdev_set_fd(int fd, struct libevdev **dev)`: for opening physical devices. +- `libevdev_next_event(struct libevdev *dev, unsigned int flags, struct input_event *ev)`: for polling events. +- `libevdev_get_id_*(const struct libevdev *dev)`: to query device meta data. + +### uinput (User Input Subsystem) +I used the Linux **uinput** subsystem to create a **virtual input device** that mirrors a physical controller input. +**uinput** is what allows us to make a virtual controller out of any evdev device by: +- Opening a file discriptor for the input device that will be emulate (i.e. have it input event forwarded). +- Forwarding the inputs from a `evdev` interface device to `/dev/uinput` (or `/dev/input/uinput`). +- **uinput** then creates a new node to expose the virtual device as a `evdev` interface device in `/dev/input/event*` + +From here the idea is that KWin or any other system component can treat the virtual controller as if it were an ordinary HID device. + +> uinput is a kernel module that makes it possible to emulate input devices from userspace. +> By writing to /dev/uinput (or /dev/input/uinput) device, a process can create a virtual input device with specific capabilities. +> Once this virtual device is created, the process can send events through it, that will be delivered to userspace and in-kernel consumers. +> +> -The kernel development community. + +### Useful functions: +- `libevdev_uinput_create_from_device(const struct libevdev *dev, int uinput_fd, struct libevdev_uinput **uinput_dev)`: + For creating a uinput device based on the given libevdev device. +- `libevdev_uinput_get_devnode (struct libevdev_uinput *uinput_dev)`: + Return the device node representing this uinput device. +- `libevdev_uinput_write_event (const struct libevdev_uinput *uinput_dev, unsigned int type, unsigned int code, int value)`: + Post an event through the uinput device. + + +Tools used: +- `libevdev-uinput.h` for management of `uinput` devices via `libevdev`. +- `/dev/uinput` opened with correct permissions. + - Ensuring the current user is in the `input` group. + - Verifying that the `uinput` kernel module is loaded (using `modprobe uinput`). Some distros (Ubuntu/Kubuntu) have it built in, not loaded as module, thus `modprobe uinput` command won't log anything. + - Opening `/dev/uinput` with `O_WRONLY | O_NONBLOCK` flags using `open()`, and ensuring no `EPERM` or `EACCES` errors were returned. + - Optional: Run program as sudo user. + +### force feedback detection/support + +Using `ioctl(fd, EVIOCGBIT(EV_FF, ...))` and tools like `fftest`, I examined: + +- How to query a device’s force feedback (FF) capabilities to figure out which effects are supported (e.g., rumble, sine wave). +- How to upload ff effects to physical game controller and test rumble motors. + - This was key to understanding haptic capability support on physical devices. + +> To enable force feedback, you have to: +> +> have your kernel configured with evdev and a driver that supports your device. +> +> make sure evdev module is loaded and /dev/input/event* device files are created. + + +### Testing & Validation +- Used `evtest` and `fftest`to test evdev devices and understand their capabilities - + `sudo evtest /dev/input/eventX`. +- Used those same tools to test virtual devices creating using uinput - +`sudo fftest dev/input/eventX`. uinput creates a node device in `dev/input/eventX` for the virtual input. +- Prototype logs validate that a virtual device can be created and events can properly be written to a that virtual device using `libevdev`. + +--- + +### **Takeaways** + +- Using `libevdev` and `libevdev-uinput` we can access physical controllers, create virtual controller and read/write low-level input events. +- Understanding of the permission requirements to open `/dev/input/*` and `/dev/uinput` (use `udev` rules or run as root). +- Tools to test: + - `evtest` and `fftest` (from `input-utils`) + - `udevadm info --name=/dev/input/eventX --attribute-walk` + - Shows the device hierarchy - how the device is connected to PC and any parent device it connects to. +- Built a minimal proof-of-concept C++ program that routes an evdev devices input 1:1 to a virtual controller (via uinput). +- Not all controllers support all force feedback types; some failed with `EINVAL` during upload. +- `libevdev` does not handle FF upload directly — this remains kernel-level and typically involves `ioctl()`. + + +### References and Documentation +- **[Linux Input Subsystem Documentation](https://www.kernel.org/doc/html/latest/input/index.html) (kernel-level overview of evdev, HID, uinput, etc.)** +- **[evdev interface documentation](https://www.kernel.org/doc/html/latest/input/event.html) (from the kernel source)** +- **[uinput](https://www.kernel.org/doc/html/latest/input/uinput.html): User-level input device emulation** +- **[Force Feedback programming on Linux](https://www.kernel.org/doc/html/latest/input/ff.html) (FF effect types and ioctl usage)** +- **[libevdev](https://www.freedesktop.org/software/libevdev/doc/latest/) (Userspace abstraction for evdev devices)** diff --git a/src/content/project/2.kwin-project-week-3-4/index.md b/src/content/project/2.kwin-project-week-3-4/index.md new file mode 100644 index 0000000..2ce50f0 --- /dev/null +++ b/src/content/project/2.kwin-project-week-3-4/index.md @@ -0,0 +1,221 @@ +--- +title: "GSoC'25 KWin Project Blog Post: Week 3-4" +discourse: yorisoft +authors: + - yorisoft +date: 2025-09-15 +thumbnail: /images/kwin_plugin_gamepad_architecture_diagram_3.png +SPDX-License-Identifier: CC-BY-SA-4.0 +SPDX-FileCopyrightText: 2025 Yelsin Sepulveda +--- + +# KWin::Plugin::GamePadManager + +Picking up from weeks 1+2 ( research + prototypes with libevdev/uinput ), these two past weeks were about moving from “research-only mode” to turning ideas into programming logic that lives inside KWin to: detect gaming controllers and their input events, keeps Plasma awake on controller activity, handles hot-plug and pre-existing connections on startup, and lays down the first mappings from controller input to keyboard/mouse actions without stepping on other apps utilizing the controllers. + +From the start my mentors and I have had a general idea of the features we wanted to add but weren't too sure how to implement them. After some thinking and experimenting they advised me to start off with a KWin::Plugin. This would allow us to start introducing the gaming controller functionalities to KWin while avoiding having to edit the core or guts of KWin. It would also be a great entry point for current and future game controller input objectives, allowing us to start small with a 1st party KWin plugin, build on it, and possibly integrate it into core functionality. + +When it comes to creating KWin plugins I had a few options: +- Scripts: Written in QML/JavaScript and used for automating window management, tiling, shortcuts, etc. +- Effects: Implement visual effects on windows, the desktop, or transitions. +- Core/Native: These are built into KWin itself and extend KWin’s internal functionality. + +Since the plugin needs low-level device access, such as monitoring `/dev/input/event*`, listening to `udev` hotplugs, opening fds, and reacting to `evdev` events the best choice was to go with Core / Native plugin. As opposed to Effect and Script plugins which aren’t designed to open devices or do long-running I/O, most simply just live inside the rendering/scripting layers. + + +I started off by searching for an example of how to build a KWin plugin so I could start learning how to build my own. Thankfully my mentor @zamundaaa provided me with some great examples: +- Example / Tutorial plugin located in `src/plugin/examples/plugin` +- Screenshots plugin located in `src/plugins` + +Between both of these examples and mentoring I was able to piece together the scaffolding ( essential parts ) of a KWin plugin and was able to put together the first version of this plugin, `gamepad` plugin, located in: `kwin/src/plugins/gamepad`. At this point the plugin is structured as follows: +``` +main.cpp // Entry point & Defines GamepadManagerFactory Class +metadata.json // Declares the plugin to KWin, define information about plugin +CMakeLists.txt // C++ Build/Installation/Logging wiring +gamepadManager.{cpp/h} // Plugin Logic: Defines GamepadManager Class +gamepad.{cpp/h} // Game Controller Object: Wrapper Class for Physical Controller +``` + +## Implementation notes + +### GamepadManagerFactory +`GamepadManagerFactory` Class serves simply as the entry point for the plugin. It's a factory class, or a class used to create other classes / object types. Like the examples, it inherits from `PluginFactory` and declares it as its interface as well as pointing to the `metadata.json` file for this plugin. It initializes the plugin through its `create()` function which returns a `GamepadManager`. + + +### GamepadManager +`GamepadManager` class serves as the central coordinator (the “brain” or “hub”) of the entire project. While creating this I took a lot of inspiration from `src/backend/drm/drm_backend.{cpp/h}`, which itself is responsible for handling drm/gpu devices. `GamepadManager` covers many responsibilities. It owns and manages all gamepad devices, handles discovery (startup enumeration, hot-plug), lifecycle (adding/removing), and communication (signals when pads are added/removed, or when their state changes). Overall its responsible for keeping track of the current set of controllers and their status. + +#### Detect hot-plug and pre-existing device detection: +For this part many of the DRM backend pattern were used. The first thing the manager class does on initialization is create two `QMetaObject::Connection`s that monitor the current KWin session for `devicePaused` and `deviceResumed` signals. This helps **track devices when Plasma goes in and out of sleep/suspend** which causes devices to be Paused and Resumed. It then enumerates over all event devices located in `/dev/input/event*` to **handle any pre-existing connections to game controllers**. If it discovers an event device it adds the gamepad ( start tracking it and its input ). +```cpp +// On init: +// Enumerate current input nodes to filter and add ONLY event nodes +QDir dir(QStringLiteral("/dev/input")); +const auto files = dir.entryList({QStringLiteral("event*")}, QDir::Files | QDir::Readable | QDir::System); +for (const QString &file : files) { + const QString path = dir.absoluteFilePath(file); + if (!isTracked(path)) { + addGamepad(path); + } +} +``` + + + +Finally using `udev` it monitors the subsystems and filter for only "input" subsystem events. It uses `QSocketNotifier` to produce signal notifications from `udev` events and creates a connections between that notifier and a memeber function, `handleUdevEvent`, that handles events coming from the `udev` monitor when an input device is detecetd. Some checks are performed to verify if the device is a gaming controller, such as expected input events and input event types. This include input events like `BTN_JOYSTICK` and `BTN_GAMEPAD`, which are commonly defined in gaming controllers. As well as checking for joystick or D-pad capabilities. If the checks pass the game controller is "added", or in other words, the device is wrapped in a `Gamepad` class, kept track of and its presence monitored. +```cpp +// setup udevMonitor +if (m_udevMonitor) { + m_udevMonitor->filterSubsystemDevType("input"); + const int fd = m_udevMonitor->fd(); + if (fd != -1) { + m_socketNotifier = std::make_unique(fd, QSocketNotifier::Read); + connect(m_socketNotifier.get(), &QSocketNotifier::activated, this, &GamepadManager::handleUdevEvent); + m_udevMonitor->enable(); + } +} +``` + + +### Gamepad +`Gamepad` is a wrapper class. It's purpose is to be tied to a physical controller. One `Gamepad` object per physical game controller. This enables quick access/reference to the device and allows for the physical controller to be treated like an object. This class is also responsible for device input handling, Plasma Idle refresh, and button to keyboard/mouse mappings. In the future things might get split up into seperate files but as it is, it handles a lot. As with the `GamepadManager`, this class takes a lot of inspiration from DRM backend patterns. + +#### Detect Input Events: +Once a gaming controller device is detected it gets wrapped in a `Gamepad` class object. Which in turn wraps the controller in a `libevdev` object pointer. This is the part that gives access to the controller through the `libevdev` API, making it easier to work with it and monitor its input events. Like `GamepadManager` the first thing this class does is use `QSocketNotifier` to produce notifications from the controllers `fd`, i.e monitor for input. It then creates a connections between that notifier and a member function, `handleEvdevEvent`, which **handles all incoming input events from that device**. +```cpp +libevdev *evdev = createEvDevice(); +if (evdev) { + m_evdev.reset(evdev); + + m_notifier = std::make_unique(m_fd, QSocketNotifier::Read, this); + connect(m_notifier.get(), &QSocketNotifier::activated, this, &Gamepad::handleEvdevEvent); + + qCDebug(KWIN_GAMEPAD) << "Connected to Gamepad ( new libevdev* ): " << libevdev_get_name(m_evdev.get()) << "at" << m_path; +} +``` + + +#### Plasma Idle Refresh On Controller Activity +With the ability to monitor for all input events from the device, the plugin then uses that information to know when to reset Plasma idle timer. For this `Gamepad` imports/includes `input.h` file and makes a call to `input()->simulateUserActivity()` when an input event is detected from the controller. This causes Plasma idle timer to be reset and **prevents the system from going into sleep/suspend mode while using only gaming controller**. +```cpp +// reset idle time +input()->simulateUserActivity(); +``` + + +#### Controller -> Keyboard & Mouse Mapping +`Gamepad` uses API function from `libevdev` to check for input events, identify the specific input event and map that to a keyboard or mouse input event. Using `libevdev_next_event()` it checks for the input event coming from that game controller. It then identifies the specific input event through its input event `type`, `code`, and `value`. To simulate a mouse and keyboard the `core/inputdevice.h` file is imported and used to declare `GenericInputDevice` which inherits from `InputDevice`. That `GenericInputDevice` effectively behaves like a virtual keyboard and mouse inside KWin’s input stack. + +When specific `libevdev` input event are identified, such as `EV_KEY` + `BTN_SOUTH` ( A button press ) OR `EV_KEY` + `BTN_EAST` ( B button press ), it call `InputDevice::sendKey()` to **simulate keyboard key press and inject the desired keys into KWin input pipeline**. In this case `Enter` for A ( `BTN_SOUTH` ) and `Escape` for B ( `BTN_EAST` ). To emulate mouse/pointer the plugin makes calls to `InputDevice::sendPointerButton()` for left and right mouse buttons, and `InputDevice::sendPointerMotionDelta()` for pointer movement. + +![architecture_diagram_0](/images/kwin_plugin_gamepad_architecture_diagram_0.png) +![architecture_diagram_1](/images/kwin_plugin_gamepad_architecture_diagram_1.png) +![architecture_diagram_2](/images/kwin_plugin_gamepad_architecture_diagram_2.png) +![architecture_diagram_3](/images/kwin_plugin_gamepad_architecture_diagram_3.png) + +Here is a list of all the buttons to keyboard/mouse mappings: +``` +Face Buttons +------------ +BTN_SOUTH → Enter (Qt::Key_Return) +BTN_EAST → Escape (Qt::Key_Escape) +BTN_NORTH +BTN_WEST + +Bumpers +------- +BTN_TL → Alt (Qt::Key_Alt) +BTN_TR → Tab (Qt::Key_Tab) + +Trigger Buttons +--------------- +ABS_Z → Mouse Left Click +ABS_RZ → Mouse Right Click + +D-Pad +----- +BTN_DPAD_LEFT → Arrow Left (Qt::Key_Left) +BTN_DPAD_RIGHT → Arrow Right (Qt::Key_Right) +BTN_DPAD_UP → Arrow Up (Qt::Key_Up) +BTN_DPAD_DOWN → Arrow Down (Qt::Key_Down) + +Analog Sticks +------------- +ABS_RX / ABS_RY → Pointer Motion + +Center Buttons +-------------- +BTN_SELECT → Show On-Screen Keyboard ( WIP ) +BTN_START → Meta/Super (Qt::Key_Meta) + +``` + +#### Prevent Stepping On Other Apps +It's essential that the plugin doesn't emulate keyboard and mouse for the gaming controller when another app is reading from it. Most likely in such cases the device is being used for something else and not being used to navigate the desktop. To achieve this the `GamepadManager` class creates an instance of `inotify` object, and adds a `watch device` to the `fd` of each game controller that’s added as a `Gamepad`. Whenever `inotify` produces a notification a function, `GamepadManager::handleFdAccess`, is called which increments a counter in `Gamepad`, `Gamepad::m_usageCount` by +1 if the event value is `IN_OPEN` or `Gamepad::m_usageCount` by -1 if the event value is `IN_CLOSE_WRITE | IN_CLOSE_NOWRITE`. The plugin will only attempt to emualte keyboard/mouse if `m_usageCount` is 0. This **prevents emulation of keyboard and mouse when other apps have the game controller opened / in use**. +```cpp +// Process all inotify events in the buffer +for (char *ptr = buffer; ptr < buffer + length;) { + struct inotify_event *event = reinterpret_cast(ptr); + + auto it = m_watchesToGamepads.find(event->wd); + if (it != m_watchesToGamepads.end()) { + Gamepad *pad = it.value(); + if (event->mask & IN_OPEN) { + pad->countUsage(+1); + } else if (event->mask & (IN_CLOSE_WRITE | IN_CLOSE_NOWRITE)) { + pad->countUsage(-1); + } + qCDebug(KWIN_GAMEPAD) << "Device" << pad->path() << "in use by:" << pad->usageCount() << " other apps"; + } + ptr += sizeof(struct inotify_event) + event->len; +} +``` + + +#### Opt-In +Many of the native plugins that ship with KWin are enabled by default but for our gaming controller plugin we will disable it by default and make it an opt-in option. This will allow users to start experimenting and benefiting from the plugin without risking the possibility of breaking current game controller input on their system. +```json +{ + "KPlugin": { + "Category": "Input", + "Description": "Enable KWin game controller input detection", + "EnabledByDefault": false, <---------- Not enabled by default. + "License": "GPL", + "Name": "gamepad" + }, + "X-KDE-ServiceTypes": ["KWin/Plugin"] +} +``` + + + +## Testing +- Controller awareness at startup and hot-plugging: tested in development session, KWin logs show the plugin picking up controllers in both scenarios, works as expected. +- Preventing sleep/suspend: tested in development session. Set suspend timer to 1min, repeatedly press A and B back and forth, and at 5min no suspend was initiated, works as expected. +- USB and Bluetooth connectivity support: tested in development session, KWin logs show plugin picking up on the controllers in both scenarios, works as expected. +- Mapping from controller to keyboard and mouse: tested in development session, all buttons are map to expected keyboard and mouse, works as expected. +- Backoff On Grab: tested in development session. Verified mapping work, started Steam app, verify mapping no longer enabled. + +Testing device: 8Bitdo Gaming Controller (USB/2.4h/Bluetooth) + + + +#### What’s next from here +- Integration into KWin Proper: Start pushing changes upstream for others to test. +- Map to Virtual Keyboard: Allow users to navigate over and get input from a virtual keyboard. Might open the way for logging in using only game controller. +- Test Cases: As per best practices when developing for KWin. +- KCM integration: A GUI option for users to toggle plugin ON/OFF. Ground work for more robust, user defined, button remapping. +- Use Config for Mapping: Using a config file to keep track of and read from all the button to keyboard/mouse button mapping. + + +#### Reference documentation: +- Example / Tutorial KWin Plugin: https://invent.kde.org/plasma/kwin/-/tree/master/examples/plugin +- Screenshots KWin Plugin: https://invent.kde.org/plasma/kwin/-/tree/33262fef1a6e4e3bcebc05181edbde2d9a72f38c/src/plugins/screenshot +- DRM Backend: https://invent.kde.org/yorisoft/kwin/-/tree/master/src/backends/drm/drm_backend.cpp +- Linux Input Subsystem (overview): https://www.kernel.org/doc/html/latest/input/index.html +- Libevdev Library: https://www.freedesktop.org/software/libevdev/doc/latest/ +- Udev Library: https://man7.org/linux/man-pages/man7/udev.7.html +- Inotify Library: https://man7.org/linux/man-pages/man7/inotify.7.html + +Checkout the source code here: +KWin Gamepad Plugin: https://invent.kde.org/yorisoft/kwin/-/tree/work/yorisoft/gamepad-plugin/src/plugins/gamepad diff --git a/src/content/project/3.kwin-project-week-5-6/index.md b/src/content/project/3.kwin-project-week-5-6/index.md new file mode 100644 index 0000000..0cb4faa --- /dev/null +++ b/src/content/project/3.kwin-project-week-5-6/index.md @@ -0,0 +1,183 @@ +--- +title: "GSoC'25 KWin Project Blog Post: Week 5-6" +discourse: yorisoft +authors: + - yorisoft +date: 2025-10-03 +thumbnail: /images/kwin_plugin_gamepad_kcm.png +SPDX-License-Identifier: CC-BY-SA-4.0 +SPDX-FileCopyrightText: 2025 Yelsin Sepulveda +--- + +It's been another few weeks of progress on the KWin GameController Plugin and I've got a lot to share! After spending the previous weeks setting up the foundation, I've progressed things forward by improving the logic a bit more, creating a few integration tests, integrating it into System Settings, and making sure it runs well on real hardware like the steamdeck. + +The primary change was splitting up `GameController` into two classes. The new one being `GenericInputDevice` which lives in `emulatedInputDevice.{cpp/h}`. This allowed me to separate the `GameController` logic responsible for emulating keyboard and mouse into it's own separate class. Now `GameController` wrapper class is just responsible for monitoring controller input, resetting idle timer on user activity, and logging. + +### GenericInputDevice + +`GenericInputDevice` is a class that inherits from `InputDevice` and is used to emulated Keyboard/Mouse in order to send those inputs through `KWins` input pipeline. The `input_events` come from `GameController` and get processed exactly like they were previously. Each `GameController` has access to an instance of `GenericInputDevice` to make its own calls. In the near future I plan on creating a static instance of this class for all `GameController` to access. + +```cpp +// Inside Gamecontroller construct + m_inputdevice = std::make_unique(); + KWin::input()->addInputDevice(m_inputdevice.get()); + +.. + +// GameController Event Handling Function +void GameController::handleEvdevEvent() +{ + input_event ev; + for (;;) { + const int rc = libevdev_next_event(m_evdev.get(), LIBEVDEV_READ_FLAG_NORMAL, &ev); + if (rc == 0) { + logEvent(&ev); + + input()->simulateUserActivity(); + + if (m_usageCount == 0 || isTestEnvironment) + m_inputdevice->emulateInputDevice(ev); + +.. + +// EmulatedInputDevice +void EmulatedInputDevice::emulateInputDevice(const input_event &ev) +{ + m_ev = ev; + if (ev.type == EV_KEY) { + qCDebug(KWIN_GAMECONTROLLER) << "Face button pressed: Simulating User Activity"; + evkeyMapping(); + } else if (m_ev.type == EV_ABS) { + qCDebug(KWIN_GAMECONTROLLER) << "Analog buttons pressed: Simulating User Activity"; + evabsMapping(); + } +} + +void EmulatedInputDevice::evkeyMapping() +{ + bool state = m_ev.value ? true : false; + std::chrono::microseconds time = std::chrono::seconds(m_ev.time.tv_sec) + std::chrono::microseconds(m_ev.time.tv_usec); + + switch (m_ev.code) { + case BTN_SOUTH: // A button → Enter + sendKeySequence(QKeySequence(Qt::Key_Return), state, time); + break; + case BTN_EAST: // B button → Escape + sendKeySequence(QKeySequence(Qt::Key_Escape), state, time); + break; + case BTN_NORTH: // X button → Virtual Keyboard + // TO-DO toggle Virtual Keyboard not working on my distro ( Kubuntu ) + EmulatedInputDevice::toggleVirtualKeyboard(QStringLiteral("forceActivate")); + case BTN_WEST: // Y button → Space + sendKeySequence(QKeySequence(Qt::Key_Space), state, time); + break; + case BTN_TL: // L button → Ctrl + sendKeySequence(QKeySequence(Qt::Key_Control), state, time); + break; + case BTN_TR: // R button → Alt + sendKeySequence(QKeySequence(Qt::Key_Alt), state, time); + break; + case BTN_START: // START button → Meta + sendKeySequence(QKeySequence(Qt::Key_Meta), state, time); + break; + case BTN_SELECT: // SELECT + break; + // Add more button mappings here as needed + default: + break; + } +} + +.. + + +``` + + +### Integration Test: Qt Test + +Part of the requirements for proposing significant contributions to `KWin` is creating integration test. This provides some assurance that things, like core functionality of the plugin, won't break so easily in the future as new code gets added. For testing KWin, uses the Qt Test Framework. Learning how to use the framework to create my own tests has been fairly simple and straightforward. Still, what exactly to test, and how to test it, was not so straightforward. + +I learned along the way that I'd be creating integration tests, instead of unit tests. The tests don't reference the plugins directly; instead, they test the effect of the plugins on the system overall. That meant that things which required an instance of the plugin to test were not possible in this case. That included testing hotplug capability, or the number of applications that the plugin thinks have opened an input device. Thankfully there were few very important functionalities that could be tested! + +Those include: + +```cpp +// Test system idle time reset. Prevents suspend +void testResetIdleTime(); + +// Test Controller To Keyboard Input Emulation +void testKeyboardMapping(); + +// Test Controller To Pointer/Mouse Input Emulation +void testPointerMapping(); +``` + +I took a lot of inspiration from the `buttonrebind_test.cpp`. + + +### System Settings KCM + +It was agreed upon early on that this plugin would be opt-in, giving the user to enable and disable it when they choose. For that I created a KDE Control Module or KCM. Or better put, I built on the existing Game Controller KCM :) I added a new UI element, a toggle, for users to enable and disable the plugin. On the backend, I added a `Q_PROPERTY`, *`pluginEnabled`*, which is responsible for checking the `kwinrc` Plugin configs, and writing to them, in order to manage the state of this plugin. This is what it currently looks like (subject to change): + +![game_controller_kcm](/images/kwin_plugin_gamepad_kcm.png) + + +### Handling Lizard Mode + +This was probably one of the most daunting parts of the project for me when I first started. I knew that steamOS had its own way of handling input coming from the Steam Deck controller which has nothing to do with KDE or Steam app. This is what allows the controller to work for navigating the device in game and desktop mode. It's what is refered to as "Lizard Mode". The controller -> keyboard/pointer rebinds that I implemented was based off of the rebinds of this Lizard mode. Ideally using a controller to navigate desktop feels/works the same across all devices on KDE. + +It's important that this new plugin not disrupt the current input system for the steamdeck. Originally I was warned that opening the fd for this device would cause Lizard mode to be disabled, which would mean I would have to either: + +A: Find a way to disable Lizard mode and implement it from scratch... + +B: Figure out what disabled Lizard mode on FD open and how to prevent / enable it as needed. + +or C: Just change the flag for opening the controller fd and everything works just fine :) + +Yup. After some testing and the smallest change I've had to make all project the Steam Deck controller was able to be detected by the plugin as well as its input detected! Even better than that, and not sure why I did not put this together before, Steam Deck already maps its input to keyboard/mouse. Duh. So this gamepad plugin doesn't need to worry about mapping and of Steam Deck input to just use it **prevent system sleep when activity from that controller is detected**. + +During my testing, I discovered that Steam Deck shows up on the system as 5 different controllers. Each having their own purpose, one to handle analog input (triggers, trackpads, sticks) another to handle face buttons & D-pad, another for keyboard, etc.. These are used by the system depending on the users needs. Again, this made life a lot easier. This are logs from `evtest` and `gamecontroller` plugin: + +![game_controller_steamdeck_testing_0](/images/kwin_plugin_gamepad_steamdeck_testing_0.png) + +![game_controller_steamdeck_testing_1](/images/kwin_plugin_gamepad_steamdeck_testing_1.png) + +![game_controller_steamdeck_testing_2](/images/kwin_plugin_gamepad_steamdeck_testing_2.png) + + +At the start of this project I had adopted a child. Some of you reading this post might have met my child. It's named ![Bug328987](https://bugs.kde.org/show_bug.cgi?id=328987). It had been drifting inside the KDE community some time, looking for someone to take care of it. But it never happened, and thus time just went on, and on. + +As some put it: + +> ![timonoj](https://bugs.kde.org/show_bug.cgi?id=328987#c39) +> Wow this is an ELEVEN (!) year old bug. +> +> ![WS](https://bugs.kde.org/show_bug.cgi?id=328987#c68) +> This issue is so old it can go to middle school. +> +> and my favorite +> +> ![Holmes](https://bugs.kde.org/show_bug.cgi?id=328987#c45) +> Is there any hope that this bug will be fixed before the heat death of the universe? + + +By the time I met Bug328987, it had been around for ≈12 years. But still! In the eyes of KDE, it was a young, bright eyed, workflow-breaking bug, like all the bugs out there, and it had potential to be fixed! After months of back and forth with mentors, living in KDE matrix server like it were my personal Discord server, and learning how to not do things in the code base - I'm proud to say gamecontroller plugin properly addresses Bug328987. Bringing to an end its more than a decade long journey. They grow up so fast. + + +#### What’s next from here + +- Integration into Kwin Proper: "Draft" label has been removed from MR and is ready for review. +- Final Fixes and Touch-up: Get Virtual Keyboard working, KCM toggle hot-plug, improve analog -> pointer emulation. + + +#### Reference documentation + +- buttonrebind_test: https://invent.kde.org/plasma/kwin/-/blob/master/autotests/integration/buttonrebind_test.cpp +- GameController KCM: https://invent.kde.org/plasma/plasma-desktop/-/tree/master/kcms/gamecontroller +- Qt Test: https://doc.qt.io/qt-6/qtest-overview.html +- Steam Controller Input: https://www.reddit.com/r/SteamController/wiki/index/ + +#### Other useful links + +KWin Gamepad Plugin: https://invent.kde.org/yorisoft/kwin/-/tree/work/yorisoft/gamepad-plugin/src/plugins/gamepad diff --git a/src/layouts/Projects.astro b/src/layouts/Projects.astro new file mode 100644 index 0000000..a2ec731 --- /dev/null +++ b/src/layouts/Projects.astro @@ -0,0 +1,11 @@ +--- +--- +
+
+ +
+
+ +
+
+ diff --git a/src/pages/Projects.astro b/src/pages/Projects.astro new file mode 100644 index 0000000..8468e65 --- /dev/null +++ b/src/pages/Projects.astro @@ -0,0 +1,34 @@ +--- +import Home from "../layouts/Projects.astro" +import BlogPostCard from "../components/BlogPostCard.astro" + +import { Label } from "@/components/ui/label" + +import { getCollection } from "astro:content"; +// This looks into src/content/blog/ +const projects = await getCollection('project'); +--- + + +
+do {'{'}
+  Yorisoft::things();
+{'};'}
+    
+ {projects.reverse().map((project) => { + const pubDate = project.data.date + .toISOString() + .split("T")[0]; + + return ( + + ) + })} +