# How Native Scroll Inversion Works on Supported Devices in OpenLogi

> Discover how OpenLogi achieves native scroll inversion on supported devices. Learn about HID++ 2121 detection, user preference persistence, and direct firmware commands for a seamless experience.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: internals
- Published: 2026-09-03

---

**OpenLogi implements native scroll inversion by detecting the HID++ 0x2121 HiResWheel feature capability, persisting the user preference in per-device configuration, and writing the inversion flag directly to the firmware via the HiResWheel `set_wheel_mode` command.**

OpenLogi is an open-source Rust implementation of Logitech device drivers that supports hardware-level scroll direction customization. The **native scroll inversion** feature allows compatible mice to reverse scroll direction in firmware rather than software, eliminating input lag and ensuring consistent behavior across operating systems. This deep integration requires precise coordination between the HID++ protocol layer, configuration management, and the desktop UI.

## Capability Detection via HiResWheel Feature

Native scroll inversion depends on the **HID++ HiResWheel feature** (feature index 0x2121). When OpenLogi probes a connected device, the `HiResWheelFeature` struct queries the firmware capabilities to determine if hardware-level inversion is available.

### Reading the has_invert Flag

The capability detection occurs in [`crates/openlogi-hidpp/src/feature/hires_wheel.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/feature/hires_wheel.rs) within the `get_wheel_capabilities` function. This function returns a capability bitmask where the **has_invert** flag indicates whether the device supports native scroll direction toggling.

```rust
// From crates/openlogi-hidpp/src/feature/hires_wheel.rs (lines 51-58)
let caps = feature.get_wheel_capabilities().await?;
if caps.has_invert {
    // Device supports native scroll inversion
}

```

If `has_invert` is false, the UI hides the toggle to prevent user confusion, as the firmware cannot honor inversion requests.

## Configuration and Persistence

OpenLogi stores scroll preferences in a layered configuration system that supports both device-level defaults and per-link overrides for different connection types (USB, Bolt receiver, Unifying receiver).

### Device-Level Storage

The primary configuration flag lives in [`crates/openlogi-core/src/config/device.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config/device.rs) as the `invert_scroll` field on `DeviceConfig`. This boolean represents the user's preference for the specific device model.

```rust
// From crates/openlogi-core/src/config/device.rs (lines 49-56)
pub struct DeviceConfig {
    pub invert_scroll: bool,
    // ... other fields
}

```

### Effective Value Resolution

At runtime, OpenLogi resolves the final configuration by checking for per-route overrides first, then falling back to device-level settings. The method `DeviceConfig::effective_invert_scroll` handles this resolution logic in [`crates/openlogi-core/src/config/device.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config/device.rs) (lines 85-92).

```rust
// Retrieve effective setting for a specific connection route
let should_invert = config.effective_invert_scroll(&device_route_key);

```

This design allows users to invert scrolling on a USB connection while maintaining standard direction on a wireless receiver, or vice versa.

## UI Interaction and State Management

The desktop application provides a native toggle control that bridges user interaction with the configuration backend.

### Desktop Toggle Implementation

In [`crates/openlogi-desktop/src/app/detail.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/app/detail.rs) (lines 430-438), the UI constructs a toggle widget identified as `"invert-scroll-toggle"`. When the user interacts with this control, it triggers the state commit flow rather than modifying configuration directly.

```rust
// From crates/openlogi-desktop/src/app/detail.rs
Toggle::new("invert-scroll-toggle")
    .on_toggle(move |state| {
        state.commit_invert_scroll(!current_value);
    });

```

### Committing Changes

The `commit_invert_scroll` method in the desktop state handler performs two critical operations: persisting the new value to the configuration store and dispatching a command to the HID layer to update the physical device state immediately.

## Low-Level Device Communication

The actual hardware update travels through three architectural layers: the desktop state handler, the host HID abstraction, and finally the device-specific HID++ implementation.

### Writing the Wheel Mode

The host layer exposes `openlogi_hid::set_scroll_inversion`, defined in [`crates/openlogi-hid/src/host.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hid/src/host.rs) (lines 108-111). This function accepts a route identifier and the desired inversion boolean, forwarding the request to the appropriate device writer.

```rust
// From crates/openlogi-hid/src/host.rs
pub async fn set_scroll_inversion(
    route: &DeviceRoute,
    inverted: bool
) -> Result<(), Error> {
    // Forwards to device-specific implementation
}

```

### Command Structure and Bit Flags

The final command construction occurs in [`crates/openlogi-hidpp/src/feature/hires_wheel.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/feature/hires_wheel.rs) (lines 84-95) within `HiResWheelFeature::set_wheel_mode`. This function sets **bit 2** of the mode byte when the `inverted` parameter is true, following the HID++ specification for wheel mode configuration.

```rust
// From crates/openlogi-hidpp/src/feature/hires_wheel.rs
let mode = WheelMode {
    // Bit 2 controls inversion
    invert: inverted,
    ..current_mode
};
self.set_wheel_mode(mode).await?;

```

The device responds with a `WheelMode` structure reflecting the actual firmware state, allowing OpenLogi to confirm whether the inversion was successfully applied or rejected by the hardware.

## Implementation Example

The following Rust patterns demonstrate the complete flow from configuration query to hardware application:

```rust
// 1. Query the effective inversion setting for a specific connection
let invert = config.effective_invert_scroll(&device_route_key);

// 2. Toggle the setting from UI context and persist
state.commit_invert_scroll(!invert);

// 3. Write the configuration directly to the HID device
openlogi_hid::set_scroll_inversion(&route, true).await?;

```

## Summary

- **Capability Detection**: OpenLogi queries the **HiResWheel** feature's `has_invert` flag during device enumeration to determine hardware support.
- **Layered Configuration**: The `invert_scroll` preference supports both global device defaults and per-connection overrides via `effective_invert_scroll`.
- **UI Integration**: The desktop toggle in [`detail.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/detail.rs) calls `commit_invert_scroll()` to atomically persist and apply changes.
- **Hardware Protocol**: Inversion is applied by setting bit 2 of the wheel mode byte through `HiResWheelFeature::set_wheel_mode` in the HID++ layer.

## Frequently Asked Questions

### Which Logitech devices support native scroll inversion in OpenLogi?

Devices that expose the HID++ 0x2121 HiResWheel feature with the `has_invert` capability flag support native inversion. This typically includes modern MX Master series mice and other high-resolution wheel devices. OpenLogi automatically detects this capability during the initial `get_wheel_capabilities` call and hides the toggle for unsupported hardware.

### How does OpenLogi handle scroll inversion when switching between USB and wireless receivers?

The configuration system uses **per-route resolution** through `DeviceConfig::effective_invert_scroll`. If you enable inversion on the USB route but not on the wireless receiver route, OpenLogi maintains separate states for each connection type, querying the effective value based on the active `device_route_key` whenever the connection changes.

### Can I use native scroll inversion alongside high-resolution scrolling?

Yes. The `WheelMode` structure in [`hires_wheel.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/hires_wheel.rs) combines multiple flags into a single byte, where bit 2 controls inversion independently from high-resolution (HiRes) mode settings. When calling `set_wheel_mode`, OpenLogi preserves the existing resolution and ratchet settings while only modifying the inversion bit.

### What happens if the device rejects the inversion command?

When the HID++ layer sends `set_wheel_mode`, the firmware returns a `WheelMode` response reflecting the actual hardware state. If the device ignores or rejects the inversion request (for example, due to firmware restrictions), OpenLogi captures this response and updates the UI to reflect the true state rather than the requested state, ensuring the user interface remains synchronized with hardware behavior.