Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e78184bc authored by Harry Cutts's avatar Harry Cutts
Browse files

CursorInputMapper: share acceleration curves with touchpad

The new touchpad mapper implemented in Android 14 replaced our simple
cursor movement acceleration curves (where the acceleration factor
increased linearly with speed between minimum and maximum values) with
more sophisticated multi-segment curves. However, cursor movement using
mice remained on the old curves. For consistency and to improve pointing
accuracy, use the same curves for mice, too.

This is also a good opportunity to improve the documentation comments
and naming now that I've wrapped my head around the maths a bit better.

Bug: 315313622
Test: atest inputflinger_tests
Test: check pointer movement with a mouse, including changing the
      pointer speed setting and checking that the movement speed changes
Change-Id: Ifcf43f4de6017f06b66f37d5e03a13cc257d92d5
parent 1411cf8b
Loading
Loading
Loading
Loading
+49 −0
Original line number Diff line number Diff line
/*
 * Copyright 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#pragma once

#include <cstdint>
#include <vector>

namespace android {

/**
 * Describes a section of an acceleration curve as a function which outputs a scaling factor (gain)
 * for the pointer movement, given the speed of the mouse or finger (in mm/s):
 *
 *     gain(input_speed_mm_per_s) = baseGain + reciprocal / input_speed_mm_per_s
 */
struct AccelerationCurveSegment {
    /**
     * The maximum pointer speed at which this segment should apply, in mm/s. The last segment in a
     * curve should always set this to infinity.
     */
    double maxPointerSpeedMmPerS;
    /** The gain for this segment before the reciprocal is taken into account. */
    double baseGain;
    /** The reciprocal part of the formula, which should be divided by the input speed. */
    double reciprocal;
};

/**
 * Creates an acceleration curve for the given pointer sensitivity value. The sensitivity value
 * should be between -7 (for the lowest sensitivity) and 7, inclusive.
 */
std::vector<AccelerationCurveSegment> createAccelerationCurveForPointerSensitivity(
        int32_t sensitivity);

} // namespace android
+46 −9
Original line number Diff line number Diff line
@@ -16,7 +16,10 @@

#pragma once

#include <vector>

#include <android-base/stringprintf.h>
#include <input/AccelerationCurve.h>
#include <input/Input.h>
#include <input/VelocityTracker.h>
#include <utils/Timers.h>
@@ -86,12 +89,7 @@ struct VelocityControlParameters {
class VelocityControl {
public:
    VelocityControl();

    /* Gets the various parameters. */
    const VelocityControlParameters& getParameters() const;

    /* Sets the various parameters. */
    void setParameters(const VelocityControlParameters& parameters);
    virtual ~VelocityControl() {}

    /* Resets the current movement counters to zero.
     * This has the effect of nullifying any acceleration. */
@@ -101,16 +99,55 @@ public:
     * scaled / accelerated delta based on the current velocity. */
    void move(nsecs_t eventTime, float* deltaX, float* deltaY);

private:
protected:
    virtual void scaleDeltas(float* deltaX, float* deltaY) = 0;

    // If no movements are received within this amount of time,
    // we assume the movement has stopped and reset the movement counters.
    static const nsecs_t STOP_TIME = 500 * 1000000; // 500 ms

    VelocityControlParameters mParameters;

    nsecs_t mLastMovementTime;
    float mRawPositionX, mRawPositionY;
    VelocityTracker mVelocityTracker;
};

/**
 * Velocity control using a simple acceleration curve where the acceleration factor increases
 * linearly with movement speed, subject to minimum and maximum values.
 */
class SimpleVelocityControl : public VelocityControl {
public:
    /** Gets the various parameters. */
    const VelocityControlParameters& getParameters() const;

    /** Sets the various parameters. */
    void setParameters(const VelocityControlParameters& parameters);

protected:
    virtual void scaleDeltas(float* deltaX, float* deltaY) override;

private:
    VelocityControlParameters mParameters;
};

/** Velocity control using a curve made up of multiple reciprocal segments. */
class CurvedVelocityControl : public VelocityControl {
public:
    CurvedVelocityControl();

    /** Sets the curve to be used for acceleration. */
    void setCurve(const std::vector<AccelerationCurveSegment>& curve);

    void setAccelerationEnabled(bool enabled);

protected:
    virtual void scaleDeltas(float* deltaX, float* deltaY) override;

private:
    const AccelerationCurveSegment& segmentForSpeed(float speedMmPerS);

    bool mAccelerationEnabled = true;
    std::vector<AccelerationCurveSegment> mCurveSegments;
};

} // namespace android
+63 −0
Original line number Diff line number Diff line
/*
 * Copyright 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <input/AccelerationCurve.h>

#include <array>
#include <limits>

#include <log/log_main.h>

#define LOG_TAG "AccelerationCurve"

namespace android {

namespace {

// The last segment must have an infinite maximum speed, so that all speeds are covered.
constexpr std::array<AccelerationCurveSegment, 4> kSegments = {{
        {32.002, 3.19, 0},
        {52.83, 4.79, -51.254},
        {119.124, 7.28, -182.737},
        {std::numeric_limits<double>::infinity(), 15.04, -1107.556},
}};

static_assert(kSegments.back().maxPointerSpeedMmPerS == std::numeric_limits<double>::infinity());

constexpr std::array<double, 15> kSensitivityFactors = {1,  2,  4,  6,  7,  8,  9, 10,
                                                        11, 12, 13, 14, 16, 18, 20};

} // namespace

std::vector<AccelerationCurveSegment> createAccelerationCurveForPointerSensitivity(
        int32_t sensitivity) {
    LOG_ALWAYS_FATAL_IF(sensitivity < -7 || sensitivity > 7, "Invalid pointer sensitivity value");
    std::vector<AccelerationCurveSegment> output;
    output.reserve(kSegments.size());

    // The curves we want to produce for different sensitivity values are actually the same curve,
    // just scaled in the Y (gain) axis by a sensitivity factor and a couple of constants.
    double commonFactor = 0.64 * kSensitivityFactors[sensitivity + 7] / 10;
    for (AccelerationCurveSegment seg : kSegments) {
        output.push_back(AccelerationCurveSegment{seg.maxPointerSpeedMmPerS,
                                                  commonFactor * seg.baseGain,
                                                  commonFactor * seg.reciprocal});
    }

    return output;
}

} // namespace android
 No newline at end of file
+1 −0
Original line number Diff line number Diff line
@@ -175,6 +175,7 @@ cc_library {
    ],
    srcs: [
        "android/os/IInputFlinger.aidl",
        "AccelerationCurve.cpp",
        "Input.cpp",
        "InputDevice.cpp",
        "InputEventLabels.cpp",
+146 −64
Original line number Diff line number Diff line
@@ -15,7 +15,6 @@
 */

#define LOG_TAG "VelocityControl"
//#define LOG_NDEBUG 0

// Log debug messages about acceleration.
static constexpr bool DEBUG_ACCELERATION = false;
@@ -23,6 +22,7 @@ static constexpr bool DEBUG_ACCELERATION = false;
#include <math.h>
#include <limits.h>

#include <android-base/logging.h>
#include <input/VelocityControl.h>
#include <utils/BitSet.h>
#include <utils/Timers.h>
@@ -37,15 +37,6 @@ VelocityControl::VelocityControl() {
    reset();
}

const VelocityControlParameters& VelocityControl::getParameters() const{
    return mParameters;
}

void VelocityControl::setParameters(const VelocityControlParameters& parameters) {
    mParameters = parameters;
    reset();
}

void VelocityControl::reset() {
    mLastMovementTime = LLONG_MIN;
    mRawPositionX = 0;
@@ -54,12 +45,13 @@ void VelocityControl::reset() {
}

void VelocityControl::move(nsecs_t eventTime, float* deltaX, float* deltaY) {
    if ((deltaX && *deltaX) || (deltaY && *deltaY)) {
    if ((deltaX == nullptr || *deltaX == 0) && (deltaY == nullptr || *deltaY == 0)) {
        return;
    }
    if (eventTime >= mLastMovementTime + STOP_TIME) {
            if (DEBUG_ACCELERATION && mLastMovementTime != LLONG_MIN) {
                ALOGD("VelocityControl: stopped, last movement was %0.3fms ago",
        ALOGD_IF(DEBUG_ACCELERATION && mLastMovementTime != LLONG_MIN,
                 "VelocityControl: stopped, last movement was %0.3fms ago",
                 (eventTime - mLastMovementTime) * 0.000001f);
            }
        reset();
    }

@@ -70,15 +62,27 @@ void VelocityControl::move(nsecs_t eventTime, float* deltaX, float* deltaY) {
    if (deltaY) {
        mRawPositionY += *deltaY;
    }
        mVelocityTracker.addMovement(eventTime, /*pointerId=*/0, AMOTION_EVENT_AXIS_X,
                                     mRawPositionX);
        mVelocityTracker.addMovement(eventTime, /*pointerId=*/0, AMOTION_EVENT_AXIS_Y,
                                     mRawPositionY);
    mVelocityTracker.addMovement(eventTime, /*pointerId=*/0, AMOTION_EVENT_AXIS_X, mRawPositionX);
    mVelocityTracker.addMovement(eventTime, /*pointerId=*/0, AMOTION_EVENT_AXIS_Y, mRawPositionY);
    scaleDeltas(deltaX, deltaY);
}

// --- SimpleVelocityControl ---

const VelocityControlParameters& SimpleVelocityControl::getParameters() const {
    return mParameters;
}

void SimpleVelocityControl::setParameters(const VelocityControlParameters& parameters) {
    mParameters = parameters;
    reset();
}

void SimpleVelocityControl::scaleDeltas(float* deltaX, float* deltaY) {
    std::optional<float> vx = mVelocityTracker.getVelocity(AMOTION_EVENT_AXIS_X, 0);
    std::optional<float> vy = mVelocityTracker.getVelocity(AMOTION_EVENT_AXIS_Y, 0);
    float scale = mParameters.scale;
        if (vx && vy) {
    if (vx.has_value() && vy.has_value()) {
        float speed = hypotf(*vx, *vy) * scale;
        if (speed >= mParameters.highThreshold) {
            // Apply full acceleration above the high speed threshold.
@@ -86,33 +90,111 @@ void VelocityControl::move(nsecs_t eventTime, float* deltaX, float* deltaY) {
        } else if (speed > mParameters.lowThreshold) {
            // Linearly interpolate the acceleration to apply between the low and high
            // speed thresholds.
                scale *= 1 + (speed - mParameters.lowThreshold)
                        / (mParameters.highThreshold - mParameters.lowThreshold)
                        * (mParameters.acceleration - 1);
            scale *= 1 +
                    (speed - mParameters.lowThreshold) /
                            (mParameters.highThreshold - mParameters.lowThreshold) *
                            (mParameters.acceleration - 1);
        }

            if (DEBUG_ACCELERATION) {
                ALOGD("VelocityControl(%0.3f, %0.3f, %0.3f, %0.3f): "
        ALOGD_IF(DEBUG_ACCELERATION,
                 "SimpleVelocityControl(%0.3f, %0.3f, %0.3f, %0.3f): "
                 "vx=%0.3f, vy=%0.3f, speed=%0.3f, accel=%0.3f",
                 mParameters.scale, mParameters.lowThreshold, mParameters.highThreshold,
                 mParameters.acceleration, *vx, *vy, speed, scale / mParameters.scale);
            }

    } else {
            if (DEBUG_ACCELERATION) {
                ALOGD("VelocityControl(%0.3f, %0.3f, %0.3f, %0.3f): unknown velocity",
        ALOGD_IF(DEBUG_ACCELERATION,
                 "SimpleVelocityControl(%0.3f, %0.3f, %0.3f, %0.3f): unknown velocity",
                 mParameters.scale, mParameters.lowThreshold, mParameters.highThreshold,
                 mParameters.acceleration);
    }
        }

        if (deltaX) {
    if (deltaX != nullptr) {
        *deltaX *= scale;
    }
        if (deltaY) {
    if (deltaY != nullptr) {
        *deltaY *= scale;
    }
}

// --- CurvedVelocityControl ---

namespace {

/**
 * The resolution that we assume a mouse to have, in counts per inch.
 *
 * Mouse resolutions vary wildly, but 800 CPI is probably the most common. There should be enough
 * range in the available sensitivity settings to accommodate users of mice with other resolutions.
 */
constexpr int32_t MOUSE_CPI = 800;

float countsToMm(float counts) {
    return counts / MOUSE_CPI * 25.4;
}

} // namespace

CurvedVelocityControl::CurvedVelocityControl()
      : mCurveSegments(createAccelerationCurveForPointerSensitivity(0)) {}

void CurvedVelocityControl::setCurve(const std::vector<AccelerationCurveSegment>& curve) {
    mCurveSegments = curve;
}

void CurvedVelocityControl::setAccelerationEnabled(bool enabled) {
    mAccelerationEnabled = enabled;
}

void CurvedVelocityControl::scaleDeltas(float* deltaX, float* deltaY) {
    if (!mAccelerationEnabled) {
        ALOGD_IF(DEBUG_ACCELERATION, "CurvedVelocityControl: acceleration disabled");
        return;
    }

    std::optional<float> vx = mVelocityTracker.getVelocity(AMOTION_EVENT_AXIS_X, 0);
    std::optional<float> vy = mVelocityTracker.getVelocity(AMOTION_EVENT_AXIS_Y, 0);

    float ratio;
    if (vx.has_value() && vy.has_value()) {
        float vxMmPerS = countsToMm(*vx);
        float vyMmPerS = countsToMm(*vy);
        float speedMmPerS = sqrtf(vxMmPerS * vxMmPerS + vyMmPerS * vyMmPerS);

        const AccelerationCurveSegment& seg = segmentForSpeed(speedMmPerS);
        ratio = seg.baseGain + seg.reciprocal / speedMmPerS;
        ALOGD_IF(DEBUG_ACCELERATION,
                 "CurvedVelocityControl: velocities (%0.3f, %0.3f) → speed %0.3f → ratio %0.3f",
                 vxMmPerS, vyMmPerS, speedMmPerS, ratio);
    } else {
        // We don't have enough data to compute a velocity yet. This happens early in the movement,
        // when the speed is presumably low, so use the base gain of the first segment of the curve.
        // (This would behave oddly for curves with a reciprocal term on the first segment, but we
        // don't have any of those, and they'd be very strange at velocities close to zero anyway.)
        ratio = mCurveSegments[0].baseGain;
        ALOGD_IF(DEBUG_ACCELERATION,
                 "CurvedVelocityControl: unknown velocity, using base gain of first segment (%.3f)",
                 ratio);
    }

    if (deltaX != nullptr) {
        *deltaX *= ratio;
    }
    if (deltaY != nullptr) {
        *deltaY *= ratio;
    }
}

const AccelerationCurveSegment& CurvedVelocityControl::segmentForSpeed(float speedMmPerS) {
    for (const AccelerationCurveSegment& seg : mCurveSegments) {
        if (speedMmPerS <= seg.maxPointerSpeedMmPerS) {
            return seg;
        }
    }
    ALOGE("CurvedVelocityControl: No segment found for speed %.3f; last segment should always have "
          "a max speed of infinity.",
          speedMmPerS);
    return mCurveSegments.back();
}

} // namespace android
Loading