Keyboard Daemon: DRM/KMS Direct Scanout, Cairo Rendering, and evdev Parsing
The keyboard daemon (portal-keyboardd) is a standalone binary that renders a virtual touchscreen keyboard directly onto the Spaceboard’s eDP-1 panel and translates multi-touch input into synthetic keyboard and pointer events via /dev/uinput. Unlike the fusion engine and gesture recognizer described in Fusion Engine: Touch Tracking, Intent Classification, and Action Generation, this daemon owns the entire rendering surface — bypassing the compositor for pixel output while feeding synthesized keystrokes back into it through the virtual input devices covered in Virtual Input Devices: uinput Bridges to the Wayfire Compositor. The daemon operates two display paths: an active Wayland layer-shell path that renders via EGL with a Rive animation engine, and a preserved direct DRM/KMS path that scans out framebuffers with zero compositor involvement.
Sources: portal/input/src/bin/portal-keyboardd/main.rs#L1-L67, portal/input/Cargo.toml#L1-L51
Dual Display Architecture
Section titled “Dual Display Architecture”The daemon was ported from a C implementation that performed direct DRM/KMS modesetting. The Rust rewrite introduced a Wayland layer-shell path after discovering that the Snapdragon X Elite’s Mesa msm driver returns EGL_BAD_NATIVE_WINDOW for GBM window surfaces. Rather than discarding the DRM path, the architecture preserves both as selectable backends sharing identical fusion-engine, touch, and virtual-device pipelines.
flowchart TB
subgraph Init
M["main()"] -->|"RUNNING: AtomicBool"| WL["WaylandKeyboardDaemon::new()"]
end
subgraph "Active Path: Wayland Layer-Shell"
WL --> WLS["WaylandSurface.connect()"]
WL --> CR["cairo_render::render_keyboard()"]
WL --> EGL["WaylandEglContext::new()"]
WL --> RIV["RiveRenderer::new()"]
WL --> EVC["EvdevTouchCapture::discover_and_open()"]
end
subgraph "Legacy Path: Direct DRM/KMS (preserved, unused)"
KBD["KeyboardDaemon::new()"]
KBD --> DRM["DrmDevice::open('/dev/dri/card0')"]
KBD --> CR2["cairo_render::render_keyboard()"]
KBD --> EGL2["try_init_gles()"]
EGL2 -->|"msm driver?"| DUMB["DumbBuffer fallback"]
EGL2 -->|"else"| GBM["GBM + EGL + GLES"]
end
WLS -->|"zwlr_layer_surface_v1 OVERLAY"| LOOP["main_loop_iteration()"]
RIV --> LOOP
EVC --> LOOP
The active binary instantiates WaylandKeyboardDaemon, which connects to the Wayland display socket, creates a zwlr_layer_surface_v1 at the OVERLAY layer with namespace "keyboard" on the eDP-1 output, and waits for the compositor’s configure event before proceeding. The keyboard interactivity is set to None — no keyboard focus is claimed, because all synthesized keystrokes reach the compositor through /dev/uinput, not through the Wayland surface.
Sources: portal/input/src/bin/portal-keyboardd/main.rs#L40-L66, portal/input/src/bin/portal-keyboardd/app_wayland.rs#L111-L216, portal/input/src/bin/portal-keyboardd/wayland_surface.rs#L1-L50, portal/input/src/bin/portal-keyboardd/app/mod.rs#L1-L73
Display Path Comparison
Section titled “Display Path Comparison”| Aspect | Wayland Layer-Shell (Active) | Direct DRM/KMS (Legacy) |
|---|---|---|
| Surface creation | zwlr_layer_surface_v1 via SCTK |
drmModeSetCrtc on eDP/HDMI-A-1 |
| EGL platform | EGL_PLATFORM_WAYLAND_KHR |
EGL_PLATFORM_GBM_KHR or dumb buffer |
| Rendering engine | Rive (state machine + GL renderer) | GLES shader program (fullscreen quad + glow) or dumb buffer pixel copy |
| Vsync | Compositor frame callbacks | DRM PageFlipFlags::EVENT + poll(2) |
| Compositor bypass | No (cooperates with Wayfire) | Yes (direct scanout, needs DRM master) |
| msm driver support | Works | Falls back to dumb buffer (no glow) |
| Touch source | Direct evdev (wl_seat caps are 0) | Direct evdev |
Sources: portal/input/src/bin/portal-keyboardd/wayland_egl.rs#L1-L15, portal/input/src/bin/portal-keyboardd/app/mod.rs#L175-L223, portal/input/src/bin/portal-keyboardd/wayland_surface.rs#L107-L141
DRM/KMS Device Management
Section titled “DRM/KMS Device Management”The DrmDevice struct owns the DRM file descriptor and tracks the selected connector, CRTC, and display mode. Device bring-up follows the same three-phase sequence as the original C daemon: find connector → find CRTC → set mode.
Connector Discovery
Section titled “Connector Discovery”The discovery logic iterates all DRM connectors and selects one using a priority system. eDP connectors receive priority 2 (Spaceboard built-in panel); HDMI-A-1 connectors with interface_id == 1 receive priority 1 (Orange Pi external display). All other interfaces are rejected. Once a connector is selected, the preferred display mode is chosen by checking for ModeTypeFlags::PREFERRED, falling back to the first available mode if none is flagged.
Sources: portal/input/src/bin/portal-keyboardd/drm.rs#L177-L232
CRTC Binding
Section titled “CRTC Binding”CRTC discovery uses a two-strategy approach. First, the connector’s current encoder is queried — if it has an attached CRTC that exists, that CRTC is reused. If no current encoder is available, the code iterates all encoders listed by the connector and uses ResourceHandles::filter_crtcs to resolve each encoder’s possible_crtcs bitmask into concrete CRTC handles, returning the first that responds successfully to get_crtc.
Sources: portal/input/src/bin/portal-keyboardd/drm.rs#L240-L278
Page Flip Lifecycle
Section titled “Page Flip Lifecycle”Once the initial framebuffer is displayed via set_crtc, the render loop drives frame updates through page_flip:
sequenceDiagram
participant Main as Main Loop
participant DRM as DrmDevice
participant Kernel as DRM Kernel
participant GBM as GbmState
Main->>DRM: page_flip(next_fb)
DRM->>Kernel: drmModePageFlip(EVENT)
Main->>Kernel: poll(drm_fd, POLLIN)
Kernel-->>Main: page-flip completion
Main->>DRM: handle_events()
DRM-->>Main: flip_completed = true
Main->>GBM: release_previous()
Main->>GBM: rotate_buffers(next_bo)
The page-flip flow includes a DRM master recovery path: if page_flip returns EACCES, another client (typically Wayfire) has taken DRM master. The daemon re-acquires master via acquire_master_lock() and retries the flip. If recovery fails, the error propagates as a fatal DrmInit error.
Sources: portal/input/src/bin/portal-keyboardd/drm.rs#L307-L349, portal/input/src/bin/portal-keyboardd/drm.rs#L409-L458, portal/input/src/bin/portal-keyboardd/app/display.rs#L86-L168
Dumb Buffer Fallback
Section titled “Dumb Buffer Fallback”When the Mesa msm driver is detected (via /sys/class/drm/card0/device/driver symlink) or PORTAL_NO_EGL=1 is set, EGL/GBM initialization is skipped entirely. The daemon creates a DRM dumb buffer via drm_ffi::mode::dumbbuffer::create, maps it with mmap(PROT_READ | PROT_WRITE, MAP_SHARED), and registers it as a DRM framebuffer via drmModeAddFB. The upload_rgba method performs per-pixel RGBA→BGRA conversion (swapping R and B channels) with forced alpha 0xFF, since the dumb buffer path has no shader stage to handle color conversion.
Sources: portal/input/src/bin/portal-keyboardd/dumb_buffer.rs#L1-L135, portal/input/src/bin/portal-keyboardd/app/mod.rs#L175-L194
GBM Buffer Management
Section titled “GBM Buffer Management”The GbmState struct bridges EGL rendering and DRM scanout. It owns a gbm::Device<Card> wrapping the DRM file descriptor and a gbm::Surface<()> configured for XRGB8888 format with SCANOUT | RENDERING usage flags. After each eglSwapBuffers, the main loop calls lock_front_buffer() which locks the GBM front buffer, creates a DRM framebuffer via drmModeAddFB (or returns a cached one), and returns the framebuffer handle plus buffer object.
Buffer rotation follows the C daemon’s pattern: the current buffer object becomes the previous buffer object (released after the next page flip completes), while the newly locked buffer becomes current. A HashMap<u64, framebuffer::Handle> caches DRM framebuffers keyed by the GBM buffer handle’s raw u32 value, bounded in practice to ~3 entries due to double/triple buffering.
Sources: portal/input/src/bin/portal-keyboardd/gbm_surface.rs#L62-L127, portal/input/src/bin/portal-keyboardd/gbm_surface.rs#L149-L250
Cairo Keyboard Rendering
Section titled “Cairo Keyboard Rendering”The render_keyboard function produces a static 5-row QWERTY layout as RGBA pixel data plus a vector of KeyDef structs carrying pixel positions and Linux evdev keycodes for hit-testing. The function is called during initialization; in the Wayland path, only the key definitions are retained (pixels are discarded in favor of the Rive renderer). In the DRM path, the pixels are uploaded as an OpenGL ES texture or copied into the dumb buffer.
Layout Constants
Section titled “Layout Constants”| Constant | Value | Purpose |
|---|---|---|
KEYBOARD_WIDTH_PERCENT |
0.85 | Keyboard occupies 85% of screen width |
KEYBOARD_HEIGHT_PERCENT |
0.40 | Keyboard occupies 40% of screen height |
TOP_MARGIN_PERCENT |
0.05 | 5% top margin above keyboard |
GAP |
4.0 px | Inter-key gap (inset on all four sides) |
| Key background | #32323C |
Dark gray with slight blue tint |
| Keyboard region | #14141E |
Very dark blue-gray panel |
Each key is keyboard_width / 12 pixels wide and keyboard_height / 5 pixels tall. The layout organizes five rows: digits (12 keys, uniform width), Q-P row (10 keys, left-aligned), A-L row (9 keys, centered), Shift+Z-M+Backspace row (variable widths — Shift and Backspace at 1.5×), and a bottom row with Portal/Warp/Space/Vortex/Backspace at 1.2×/1.2×/4.8×/1.2×/1.6× widths.
Sources: portal/input/src/bin/portal-keyboardd/cairo_render.rs#L27-L86, portal/input/src/bin/portal-keyboardd/cairo_render.rs#L162-L458
Pixel Format Conversion
Section titled “Pixel Format Conversion”Cairo’s ImageSurface with Format::ARgb32 stores pixels in BGRA byte order on little-endian systems. The render_keyboard function performs a per-pixel conversion loop after flushing the surface, reading four bytes [B, G, R, A] and writing [R, G, B, A] to produce RGBA data suitable for glTexImage2D(GL_RGBA, GL_UNSIGNED_BYTE). The conversion accounts for stride differences between the Cairo surface stride and the tightly packed RGBA output.
Sources: portal/input/src/bin/portal-keyboardd/cairo_render.rs#L460-L490
Pango Text Layout
Section titled “Pango Text Layout”Key labels are rendered using pangocairo::functions::create_layout and show_layout. The font specification follows a "Sans {size}" or "Sans Bold {size}" pattern depending on the bold flag. Text is centered within each key cell by querying layout.pixel_size() and offsetting the draw position by half the text dimensions. Letter keys (Q-P, A-L, Z-M) use 18pt bold; digit keys use 16pt regular; special-function keys (Portal, Warp, Vortex) use 14pt with varying bold settings.
Sources: portal/input/src/bin/portal-keyboardd/cairo_render.rs#L116-L134
OpenGL ES Glow Renderer
Section titled “OpenGL ES Glow Renderer”The GlesRenderer (used only in the legacy DRM path) compiles a GLSL ES 3.00 shader program that renders the Cairo keyboard texture on a fullscreen quad with up to 10 concurrent per-key glow animations. The vertex shader passes through position and texture coordinates. The fragment shader samples the texture, then accumulates glow contributions from active slots.
Glow Envelope
Section titled “Glow Envelope”Each glow slot follows a three-phase temporal envelope: rise (0–150ms linear ramp to full intensity), hold (150–500ms at peak), decay (500–800ms linear fade to zero). Spatial falloff uses smoothstep on the key’s bounding box with a 6-pixel bloom expansion, combined with a center-based radial falloff (1.0 - normalized_distance * 0.3) that keeps glows brightest at the key center.
V-Flip Convention
Section titled “V-Flip Convention”The fullscreen quad vertices flip the V texture coordinate: Cairo row 0 (top of image) maps to V=0, while OpenGL’s bottom-left origin expects V=1 at the bottom. This is encoded directly in the vertex data so the fragment shader receives correctly oriented coordinates.
Sources: portal/input/src/bin/portal-keyboardd/gles.rs#L34-L104, portal/input/src/bin/portal-keyboardd/gles.rs#L110-L153, portal/input/src/bin/portal-keyboardd/gles.rs#L319-L399
Rive Renderer (Active Path)
Section titled “Rive Renderer (Active Path)”The Wayland daemon replaces the Cairo+GLES stack with a Rive state machine renderer. The keyboard.riv file is embedded at compile time via include_bytes! and loaded through a C++ FFI bridge (rive_bridge.cpp / rive_bridge.rs). The renderer constructs a dependency chain — RiveContext → RiveFile → RiveArtboard → RiveStateMachine — where each object is deliberately leaked via Box::leak to obtain 'static references. This avoids the undefined behavior of transmuting shorter-lived borrows, since the Rive object graph must live for the entire daemon lifetime.
Glow feedback in the Rive path uses a fixed-size [GlowSlot; 10] array with 0.8-second expiry. Each glow activation records a key index, an intent color constant (1=green for typing, 2=orange for pointing, 3=blue for gestures), and a monotonic start time. The state machine is advanced each frame by the delta time, and the artboard is drawn to the current GL framebuffer via the Rive GL renderer.
Sources: portal/input/src/bin/portal-keyboardd/rive_renderer.rs#L1-L171, portal/input/src/bin/portal-keyboardd/app_wayland.rs#L228-L325
evdev Touch Pipeline
Section titled “evdev Touch Pipeline”The original raw evdev parsing code in evdev.rs has been deprecated and removed in favor of a library-based three-stage pipeline. The tombstone module documents this migration explicitly. All touch functionality now lives in portal_input::touch::* and is shared between the keyboard daemon and the fusion engine.
flowchart LR
subgraph "Stage 1: Capture"
SC["EvdevDeviceScanner<br/>scan /dev/input/event*"] --> DS["Device Selection<br/>(vendor:product whitelist)"]
DS --> EC["EvdevTouchCapture<br/>open + O_NONBLOCK + fetch_events()"]
end
subgraph "Stage 2: Track"
EC -->|"RawEvdevEvent stream"| ST["SlotTracker<br/>Protocol B (ABS_MT_SLOT)"]
end
subgraph "Stage 3: Normalize"
ST -->|"on SYN_REPORT"| DN["DeviceNormalizer<br/>ioctl caps → [0,1]"]
DN --> TE["TouchEvent"]
end
Device Discovery and Whitelist
Section titled “Device Discovery and Whitelist”EvdevDeviceScanner probes /dev/input/event0 through event31, opening each device and checking for ABS_MT_SLOT support (the Protocol B requirement). Devices that don’t support MT-B are filtered out. The selection policy enforces a strict vendor:product whitelist with no generic fallback:
| Vendor | Product ID | Hardware |
|---|---|---|
ILITEK (0x222a) |
0x0001 |
Orange Pi touchscreen |
ELAN (0x04f3) |
0x4328 |
Spaceboard hid-over-i2c digitizer |
An explicit device path in DeviceSelectionConfig bypasses vendor:product matching, enabling testing with alternative hardware.
Sources: portal/input/src/touch/capture/mod.rs#L1-L60, portal/input/src/touch/capture/scanner.rs#L1-L162
Protocol B Slot Tracking
Section titled “Protocol B Slot Tracking”The SlotTracker implements Linux’s Multi-Touch Protocol Type B using a fixed-size [SlotState; MAX_SLOTS] array (10 slots, zero heap allocation on the hot path). Events are processed one at a time: EV_ABS events update slot state (ABS_MT_SLOT switches the active slot, ABS_MT_TRACKING_ID sets touch start/end/move, ABS_MT_POSITION_X/Y update coordinates, ABS_MT_TOUCH_MAJOR records contact area for palm rejection). A TouchEvent is emitted only on SYN_REPORT, when the build_touch_event method iterates all slots, normalizes coordinates via the injected TouchNormalizer, and computes the overall event phase from changed slots.
The tracking ID state machine handles four transitions: negative-to-positive (new touch, phase=Start), positive-to-negative (touch ended, phase=End), positive-to-positive with different ID (slot reuse, phase=Start), and positive-to-positive with same ID (position update, phase=Move).
Sources: portal/input/src/touch/tracker.rs#L1-L83, portal/input/src/touch/tracker.rs#L86-L133, portal/input/src/touch/tracker.rs#L135-L236
Coordinate Normalization
Section titled “Coordinate Normalization”DeviceNormalizer maps raw evdev coordinates to the [0.0, 1.0] range using device capabilities queried once via ioctl at construction time. The normalization formula is ((raw - min) / (max - min)).clamp(0.0, 1.0), with a guard against zero range. The same struct provides a denormalize method for reverse conversion, used by downstream consumers that need to map normalized coordinates back to device resolution.
Sources: portal/input/src/touch/normalizer.rs#L1-L63
Event Reading with Bounds and Rate Filtering
Section titled “Event Reading with Bounds and Rate Filtering”EvdevTouchCapture::read_events performs two filtering passes on the raw event stream. First, ABS_MT_POSITION_X/Y events are checked against device-reported bounds (min/max from ioctl) — out-of-bounds events are dropped with a tracing::debug! log. Second, a rate cap of 1000 events/second (MAX_EVENTS_PER_SEC) drops events whose timestamp is within MIN_INTER_EVENT_US (1000µs) of the last accepted event. Reading stops at the first SYN_REPORT boundary, producing a batch of events that represents one complete touch frame.
Sources: portal/input/src/touch/capture/backend.rs#L163-L250, portal/input/src/touch/capture/mod.rs#L39-L48
Channel-Based Fusion Pipeline
Section titled “Channel-Based Fusion Pipeline”Both display paths share the same concurrency architecture for touch-to-action processing. A sync_channel(64) pair — designated Channel A (main → worker) and Channel D (worker → main) — decouples the input thread from the rendering thread.
flowchart LR
subgraph "Main Loop Thread"
TC["EvdevTouchCapture<br/>read_events()"] --> TT["SlotTracker<br/>process_event()"]
TT --> ET["enrich_touch_event()<br/>hit-test + slot_keys"]
ET -->|"try_send"| CA["Channel A<br/>sync_channel(64)"]
CD["Channel D<br/>sync_channel(64)"] -->|"try_recv"| AD["ActionDispatcher"]
AD --> UK["UinputKeyboard"]
AD --> UP["UinputPointer"]
CD -->|"try_recv"| RIV["RiveRenderer<br/>set_key_glow()"]
end
subgraph "Fusion Worker Thread"
CA -->|"recv_timeout(100ms)"| FE["V21FusionEngine<br/>process_touch_event()"]
FE -->|"send"| CD
end
The EnrichedTouchEvent carries the normalized TouchEvent plus a slot_keys: Vec<(u32, Option<u16>)> mapping — each touch slot to the evdev keycode of the key it pressed (if hit-testing found a match). This mapping is computed on the main thread using the Cairo key definitions, before the event enters the channel, so the fusion engine receives pre-resolved key context without needing access to the layout geometry.
The fusion worker runs V21FusionEngine in a dedicated thread, processing each enriched event through intent classification and action generation. Results flow back on Channel D as FusionChannelResult containing classified intents and the original touch event (for glow source-event correlation). The main loop drains Channel D with try_recv in a non-blocking loop, dispatching intents to virtual devices and triggering glow animations.
Sources: portal/input/src/bin/portal-keyboardd/app_wayland.rs#L64-L100, portal/input/src/bin/portal-keyboardd/app_wayland.rs#L252-L367, portal/input/src/bin/portal-keyboardd/app/fusion_worker.rs#L1-L81
Action Dispatch
Section titled “Action Dispatch”The ActionDispatcher translates InputAction variants from the fusion engine into concrete uinput device calls. Each action maps to specific key sequences on the virtual keyboard and pointer:
| InputAction | uinput Effect |
|---|---|
KeyPress / KeyRelease |
keyboard.emit_key(code, pressed) |
PointerMove |
pointer.move_pointer(dx, dy) |
PointerClick |
pointer.click(button) |
PointerDrag |
pointer.press_button + move_pointer |
PointerScroll |
pointer.scroll(axis, amount) |
PointerZoom(factor > 1) |
Ctrl+Equal key combo |
SystemAction::Launcher |
Alt+F2 |
SystemAction::AppSwitcher |
Alt+Tab |
SystemAction::Overview |
Ctrl+KeyUp |
SystemAction::SpatialMode |
Ctrl+Alt+Right |
Key combos are emitted in the correct order: modifiers pressed first, then the target key pressed and released, then modifiers released in reverse order. Errors from individual device calls are logged but do not block subsequent actions.
Sources: portal/input/src/bin/portal-keyboardd/action_dispatch.rs#L42-L200
EGL Context Management
Section titled “EGL Context Management”Both display paths use libloading to dynamically load libEGL.so.1 rather than linking against it at build time. This accommodates GPU-specific EGL implementations (Mali, freedreno/turnip) that may not expose all extension entry points through static linking. The EGL initialization sequence:
- Load library and resolve
eglGetProcAddress,eglGetError,eglQueryString - Get platform display — GBM path uses
EGL_PLATFORM_GBM_KHR(0x31D7); Wayland path usesEGL_PLATFORM_WAYLAND_KHR(0x31D8). The Wayland path additionally loadslibwayland-egl.soforwl_egl_window_create - Choose config requesting GLES3 renderable, RGBA8888, WINDOW_BIT
- Create context with
EGL_CONTEXT_CLIENT_VERSION = 3 - Create window surface — GBM path uses
eglCreateWindowSurfacewith the GBM surface pointer; Wayland path useswl_egl_window_createto wrap thewl_surface, theneglCreateWindowSurface - Make current — EGL context is current on the main thread for all GL operations
The Wayland path resolves function pointers through eglGetProcAddress and uses a centralized egl_cast_fn<T> transmute helper to convert the *mut c_void return into typed function pointers, isolating the single unsafe transmute site.
Sources: portal/input/src/bin/portal-keyboardd/egl.rs#L1-L200, portal/input/src/bin/portal-keyboardd/wayland_egl.rs#L1-L200, portal/input/src/bin/portal-keyboardd/wayland_egl.rs#L117-L127
Wayland Layer-Shell Surface Wiring
Section titled “Wayland Layer-Shell Surface Wiring”The WaylandSurface struct owns the Wayland connection, event queue, and protocol state. It implements Dispatch for 11 Wayland protocol objects: WlRegistry, WlCompositor, WlOutput, ZxdgOutputManagerV1, ZxdgOutputV1, ZwlrLayerShellV1, ZwlrLayerSurfaceV1, WlSurface, WlSeat, WlTouch, and WlCallback.
The registry handler binds globals with version caps (wl_compositor ≤6, wl_output ≤4, zwlr_layer_shell_v1 ≤4, wl_seat ≤7). Output discovery uses zxdg_output_v1::get_name to match the eDP-1 target, falling back to description substring match, then to the first available output. The layer surface is configured with Anchor::all (top/bottom/left/right), exclusive_zone = -1 (overlay), and KeyboardInteractivity::None.
Although the surface binds wl_touch when the seat advertises touch capabilities, the daemon does not rely on Wayland touch events. Wayfire’s DRM backend doesn’t create a libinput backend, so wl_seat reports zero touch capabilities and wl_touch never fires. All touch input comes from the direct evdev pipeline.
Sources: portal/input/src/bin/portal-keyboardd/wayland_surface.rs#L146-L194, portal/input/src/bin/portal-keyboardd/wayland_surface.rs#L315-L370, portal/input/src/bin/portal-keyboardd/app_wayland.rs#L115-L119
Systemd Integration
Section titled “Systemd Integration”The daemon runs as the portal-input.service systemd unit under the portal user. The service waits for the Wayland socket at /run/portal/wayland-1 before starting, then grants /dev/uinput group ownership to portal. Security hardening includes NoNewPrivileges, ProtectSystem=strict, MemoryDenyWriteExecute, and an empty CapabilityBoundingSet — /dev/uinput access is granted through udev group ownership, not capabilities.
Sources: portal/systemd/portal-input.service#L1-L45
Key Type Definitions
Section titled “Key Type Definitions”The touch pipeline defines its types in portal_input::touch::types with a focus on fixed-size, stack-allocated structures for the hot path. TouchEvent contains a [TouchPoint; MAX_SLOTS] fixed array (not a Vec), changed_slots as a u16 bitmask, and primary_slot as a u8. RawEvdevEvent is Copy for efficient pass-by-value through the channel. DeviceCapabilities is Clone with pre-computed Option<i32> fields for touch-major bounds.
Sources: portal/input/src/touch/types.rs#L17-L184, portal/input/src/touch/types.rs#L260-L292
Further Reading
Section titled “Further Reading”- For the fusion engine’s intent classification and gesture recognition logic that processes the
TouchEventstream: Fusion Engine: Touch Tracking, Intent Classification, and Action Generation - For the uinput virtual device bridge that carries synthesized keystrokes to Wayfire: Virtual Input Devices: uinput Bridges to the Wayfire Compositor
- For the dual-display Wayland architecture that provides the
eDP-1output the keyboard surface binds to: Dual-Display Wayland Architecture: eDP-1 and Virtual GLASSES-1 Output - For systemd service management and network topology context: Systemd Service Management and Network Topology