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

Commit b2bb2eda authored by Lais Andrade's avatar Lais Andrade
Browse files

Use composition size limit on repeating effects

Concatenate short repeating vibration patterns for PWLE and composed
primitives into a larger command to the vibrator HAL, making use of the
limits for compositions.

Add a check to find the best place to break down a large or repeating
PWLE waveform, preferably at amplitude zero, to avoid segmenting the HAL
commands on a high amplitude, causing a click effect.

Fix: 224930189
Test: VibrationThreadTest
Change-Id: Iac59855886596c98de583690356ef02bc67791df
parent f5debd0b
Loading
Loading
Loading
Loading
+9 −1
Original line number Diff line number Diff line
@@ -141,8 +141,16 @@ abstract class AbstractVibratorStep extends Step {
     */
    protected List<Step> nextSteps(long nextStartTime, long vibratorOffTimeout,
            int segmentsPlayed) {
        int nextSegmentIndex = segmentIndex + segmentsPlayed;
        int effectSize = effect.getSegments().size();
        int repeatIndex = effect.getRepeatIndex();
        if (nextSegmentIndex >= effectSize && repeatIndex >= 0) {
            // Count the loops that were played.
            int loopSize = effectSize - repeatIndex;
            nextSegmentIndex = repeatIndex + ((nextSegmentIndex - effectSize) % loopSize);
        }
        Step nextStep = conductor.nextVibrateStep(nextStartTime, controller, effect,
                segmentIndex + segmentsPlayed, vibratorOffTimeout);
                nextSegmentIndex, vibratorOffTimeout);
        return nextStep == null ? VibrationStepConductor.EMPTY_STEP_LIST : Arrays.asList(nextStep);
    }
}
+47 −12
Original line number Diff line number Diff line
@@ -32,6 +32,11 @@ import java.util.List;
 * {@link PrimitiveSegment} starting at the current index.
 */
final class ComposePrimitivesVibratorStep extends AbstractVibratorStep {
    /**
     * Default limit to the number of primitives in a composition, if none is defined by the HAL,
     * to prevent repeating effects from generating an infinite list.
     */
    private static final int DEFAULT_COMPOSITION_SIZE_LIMIT = 100;

    ComposePrimitivesVibratorStep(VibrationStepConductor conductor, long startTime,
            VibratorController controller, VibrationEffect.Composed effect, int index,
@@ -49,18 +54,8 @@ final class ComposePrimitivesVibratorStep extends AbstractVibratorStep {
            // Load the next PrimitiveSegments to create a single compose call to the vibrator,
            // limited to the vibrator composition maximum size.
            int limit = controller.getVibratorInfo().getCompositionSizeMax();
            int segmentCount = limit > 0
                    ? Math.min(effect.getSegments().size(), segmentIndex + limit)
                    : effect.getSegments().size();
            List<PrimitiveSegment> primitives = new ArrayList<>();
            for (int i = segmentIndex; i < segmentCount; i++) {
                VibrationEffectSegment segment = effect.getSegments().get(i);
                if (segment instanceof PrimitiveSegment) {
                    primitives.add((PrimitiveSegment) segment);
                } else {
                    break;
                }
            }
            List<PrimitiveSegment> primitives = unrollPrimitiveSegments(effect, segmentIndex,
                    limit > 0 ? limit : DEFAULT_COMPOSITION_SIZE_LIMIT);

            if (primitives.isEmpty()) {
                Slog.w(VibrationThread.TAG, "Ignoring wrong segment for a ComposePrimitivesStep: "
@@ -81,4 +76,44 @@ final class ComposePrimitivesVibratorStep extends AbstractVibratorStep {
            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
        }
    }

    /**
     * Get the primitive segments to be played by this step as a single composition, starting at
     * {@code startIndex} until:
     *
     * <ol>
     *     <li>There are no more segments in the effect;
     *     <li>The first non-primitive segment is found;
     *     <li>The given limit to the composition size is reached.
     * </ol>
     *
     * <p>If the effect is repeating then this method will generate the largest composition within
     * given limit.
     */
    private List<PrimitiveSegment> unrollPrimitiveSegments(VibrationEffect.Composed effect,
            int startIndex, int limit) {
        List<PrimitiveSegment> segments = new ArrayList<>(limit);
        int segmentCount = effect.getSegments().size();
        int repeatIndex = effect.getRepeatIndex();

        for (int i = startIndex; segments.size() < limit; i++) {
            if (i == segmentCount) {
                if (repeatIndex >= 0) {
                    i = repeatIndex;
                } else {
                    // Non-repeating effect, stop collecting primitives.
                    break;
                }
            }
            VibrationEffectSegment segment = effect.getSegments().get(i);
            if (segment instanceof PrimitiveSegment) {
                segments.add((PrimitiveSegment) segment);
            } else {
                // First non-primitive segment, stop collecting primitives.
                break;
            }
        }

        return segments;
    }
}
+91 −12
Original line number Diff line number Diff line
@@ -33,6 +33,11 @@ import java.util.List;
 * {@link StepSegment} or {@link RampSegment} starting at the current index.
 */
final class ComposePwleVibratorStep extends AbstractVibratorStep {
    /**
     * Default limit to the number of PWLE segments, if none is defined by the HAL, to prevent
     * repeating effects from generating an infinite list.
     */
    private static final int DEFAULT_PWLE_SIZE_LIMIT = 100;

    ComposePwleVibratorStep(VibrationStepConductor conductor, long startTime,
            VibratorController controller, VibrationEffect.Composed effect, int index,
@@ -50,18 +55,8 @@ final class ComposePwleVibratorStep extends AbstractVibratorStep {
            // Load the next RampSegments to create a single composePwle call to the vibrator,
            // limited to the vibrator PWLE maximum size.
            int limit = controller.getVibratorInfo().getPwleSizeMax();
            int segmentCount = limit > 0
                    ? Math.min(effect.getSegments().size(), segmentIndex + limit)
                    : effect.getSegments().size();
            List<RampSegment> pwles = new ArrayList<>();
            for (int i = segmentIndex; i < segmentCount; i++) {
                VibrationEffectSegment segment = effect.getSegments().get(i);
                if (segment instanceof RampSegment) {
                    pwles.add((RampSegment) segment);
                } else {
                    break;
                }
            }
            List<RampSegment> pwles = unrollRampSegments(effect, segmentIndex,
                    limit > 0 ? limit : DEFAULT_PWLE_SIZE_LIMIT);

            if (pwles.isEmpty()) {
                Slog.w(VibrationThread.TAG, "Ignoring wrong segment for a ComposePwleStep: "
@@ -81,4 +76,88 @@ final class ComposePwleVibratorStep extends AbstractVibratorStep {
            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
        }
    }

    /**
     * Get the ramp segments to be played by this step for a waveform, starting at
     * {@code startIndex} until:
     *
     * <ol>
     *     <li>There are no more segments in the effect;
     *     <li>The first non-ramp segment is found;
     *     <li>The given limit to the PWLE size is reached.
     * </ol>
     *
     * <p>If the effect is repeating then this method will generate the largest PWLE within given
     * limit. This will also optimize to end the list at a ramp to zero-amplitude, if possible, and
     * avoid braking down the effect in non-zero amplitude.
     */
    private List<RampSegment> unrollRampSegments(VibrationEffect.Composed effect, int startIndex,
            int limit) {
        List<RampSegment> segments = new ArrayList<>(limit);
        float bestBreakAmplitude = 1;
        int bestBreakPosition = limit; // Exclusive index.

        int segmentCount = effect.getSegments().size();
        int repeatIndex = effect.getRepeatIndex();

        // Loop once after reaching the limit to see if breaking it will really be necessary, then
        // apply the best break position found, otherwise return the full list as it fits the limit.
        for (int i = startIndex; segments.size() <= limit; i++) {
            if (i == segmentCount) {
                if (repeatIndex >= 0) {
                    i = repeatIndex;
                } else {
                    // Non-repeating effect, stop collecting ramps.
                    break;
                }
            }
            VibrationEffectSegment segment = effect.getSegments().get(i);
            if (segment instanceof RampSegment) {
                RampSegment rampSegment = (RampSegment) segment;
                segments.add(rampSegment);

                if (isBetterBreakPosition(segments, bestBreakAmplitude, limit)) {
                    // Mark this position as the best one so far to break a long waveform.
                    bestBreakAmplitude = rampSegment.getEndAmplitude();
                    bestBreakPosition = segments.size(); // Break after this ramp ends.
                }
            } else {
                // First non-ramp segment, stop collecting ramps.
                break;
            }
        }

        return segments.size() > limit
                // Remove excessive segments, using the best breaking position recorded.
                ? segments.subList(0, bestBreakPosition)
                // Return all collected ramp segments.
                : segments;
    }

    /**
     * Returns true if the current segment list represents a better break position for a PWLE,
     * given the current amplitude being used for breaking it at a smaller size and the size limit.
     */
    private boolean isBetterBreakPosition(List<RampSegment> segments,
            float currentBestBreakAmplitude, int limit) {
        RampSegment lastSegment = segments.get(segments.size() - 1);
        float breakAmplitudeCandidate = lastSegment.getEndAmplitude();
        int breakPositionCandidate = segments.size();

        if (breakPositionCandidate > limit) {
            // We're beyond limit, last break position found should be used.
            return false;
        }
        if (breakAmplitudeCandidate == 0) {
            // Breaking at amplitude zero at any position is always preferable.
            return true;
        }
        if (breakPositionCandidate < limit / 2) {
            // Avoid breaking at the first half of the allowed maximum size, even if amplitudes are
            // lower, to avoid creating PWLEs that are too small unless it's to break at zero.
            return false;
        }
        // Prefer lower amplitudes at a later position for breaking the PWLE in a more subtle way.
        return breakAmplitudeCandidate <= currentBestBreakAmplitude;
    }
}
+7 −4
Original line number Diff line number Diff line
@@ -33,6 +33,12 @@ import java.util.List;
 * and amplitude to simulate waveforms represented by a sequence of {@link StepSegment}.
 */
final class SetAmplitudeVibratorStep extends AbstractVibratorStep {
    /**
     * The repeating waveform keeps the vibrator ON all the time. Use a minimum duration to
     * prevent short patterns from turning the vibrator ON too frequently.
     */
    private static final int REPEATING_EFFECT_ON_DURATION = 5000; // 5s

    private long mNextOffTime;

    SetAmplitudeVibratorStep(VibrationStepConductor conductor, long startTime,
@@ -170,10 +176,7 @@ final class SetAmplitudeVibratorStep extends AbstractVibratorStep {
                repeatIndex = -1;
            }
            if (i == startIndex) {
                // The repeating waveform keeps the vibrator ON all the time. Use a minimum
                // of 1s duration to prevent short patterns from turning the vibrator ON too
                // frequently.
                return Math.max(timing, 1000);
                return Math.max(timing, REPEATING_EFFECT_ON_DURATION);
            }
        }
        if (i == segmentCount && effect.getRepeatIndex() < 0) {
+101 −22
Original line number Diff line number Diff line
@@ -276,7 +276,7 @@ public class VibrationThreadTest {
    }

    @Test
    public void vibrate_singleVibratorRepeatingShortAlwaysOnWaveform_turnsVibratorOnForASecond()
    public void vibrate_singleVibratorRepeatingShortAlwaysOnWaveform_turnsVibratorOnForLonger()
            throws Exception {
        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
        fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
@@ -293,10 +293,70 @@ public class VibrationThreadTest {

        verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_BY_USER);
        assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
        assertEquals(Arrays.asList(expectedOneShot(1000)),
        assertEquals(Arrays.asList(expectedOneShot(5000)),
                fakeVibrator.getEffectSegments(vibrationId));
    }

    @Test
    public void vibrate_singleVibratorRepeatingPwle_generatesLargestPwles() throws Exception {
        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
        fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS);
        fakeVibrator.setMinFrequency(100);
        fakeVibrator.setResonantFrequency(150);
        fakeVibrator.setFrequencyResolution(50);
        fakeVibrator.setMaxAmplitudes(1, 1, 1);
        fakeVibrator.setPwleSizeMax(10);

        long vibrationId = 1;
        VibrationEffect effect = VibrationEffect.startWaveform(targetAmplitude(1))
                // Very long segment so thread will be cancelled after first PWLE is triggered.
                .addTransition(Duration.ofMillis(100), targetFrequency(100))
                .build();
        VibrationEffect repeatingEffect = VibrationEffect.startComposition()
                .repeatEffectIndefinitely(effect)
                .compose();
        VibrationStepConductor conductor = startThreadAndDispatcher(vibrationId, repeatingEffect);

        assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibrationId).isEmpty(),
                TEST_TIMEOUT_MILLIS));
        conductor.notifyCancelled(Vibration.Status.CANCELLED_BY_USER, /* immediate= */ false);
        waitForCompletion();

        // PWLE size max was used to generate a single vibrate call with 10 segments.
        verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_BY_USER);
        assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
        assertEquals(10, fakeVibrator.getEffectSegments(vibrationId).size());
    }

    @Test
    public void vibrate_singleVibratorRepeatingPrimitives_generatesLargestComposition()
            throws Exception {
        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
        fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
        fakeVibrator.setSupportedPrimitives(VibrationEffect.Composition.PRIMITIVE_CLICK);
        fakeVibrator.setCompositionSizeMax(10);

        long vibrationId = 1;
        VibrationEffect effect = VibrationEffect.startComposition()
                // Very long delay so thread will be cancelled after first PWLE is triggered.
                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 100)
                .compose();
        VibrationEffect repeatingEffect = VibrationEffect.startComposition()
                .repeatEffectIndefinitely(effect)
                .compose();
        VibrationStepConductor conductor = startThreadAndDispatcher(vibrationId, repeatingEffect);

        assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibrationId).isEmpty(),
                TEST_TIMEOUT_MILLIS));
        conductor.notifyCancelled(Vibration.Status.CANCELLED_SUPERSEDED, /* immediate= */ false);
        waitForCompletion();

        // Composition size max was used to generate a single vibrate call with 10 primitives.
        verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_SUPERSEDED);
        assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
        assertEquals(10, fakeVibrator.getEffectSegments(vibrationId).size());
    }

    @Test
    public void vibrate_singleVibratorRepeatingLongAlwaysOnWaveform_turnsVibratorOnForACycle()
            throws Exception {
@@ -319,7 +379,7 @@ public class VibrationThreadTest {
                fakeVibrator.getEffectSegments(vibrationId));
    }


    @LargeTest
    @Test
    public void vibrate_singleVibratorRepeatingAlwaysOnWaveform_turnsVibratorBackOn()
            throws Exception {
@@ -329,22 +389,21 @@ public class VibrationThreadTest {
        long vibrationId = 1;
        int[] amplitudes = new int[]{1, 2};
        VibrationEffect effect = VibrationEffect.createWaveform(
                new long[]{900, 50}, amplitudes, 0);
                new long[]{4900, 50}, amplitudes, 0);
        VibrationStepConductor conductor = startThreadAndDispatcher(vibrationId, effect);

        assertTrue(waitUntil(() -> fakeVibrator.getAmplitudes().size() > 2 * amplitudes.length,
                1000 + TEST_TIMEOUT_MILLIS));
        assertTrue(waitUntil(() -> fakeVibrator.getEffectSegments(vibrationId).size() > 1,
                5000 + TEST_TIMEOUT_MILLIS));
        conductor.notifyCancelled(Vibration.Status.CANCELLED_BY_USER, /* immediate= */ false);
        waitForCompletion();

        verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_BY_USER);
        assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
        assertEquals(2, fakeVibrator.getEffectSegments(vibrationId).size());
        // First time turn vibrator ON for minimum of 1s.
        assertEquals(1000L, fakeVibrator.getEffectSegments(vibrationId).get(0).getDuration());
        // First time turn vibrator ON for minimum of 5s.
        assertEquals(5000L, fakeVibrator.getEffectSegments(vibrationId).get(0).getDuration());
        // Vibrator turns off in the middle of the second execution of first step, turn it back ON
        // for another 1s + remaining of 850ms.
        assertEquals(1850,
        // for another 5s + remaining of 850ms.
        assertEquals(4900 + 50 + 4900,
                fakeVibrator.getEffectSegments(vibrationId).get(1).getDuration(), /* delta= */ 20);
        // Set amplitudes for a cycle {1, 2}, start second loop then turn it back on to same value.
        assertEquals(expectedAmplitudes(1, 2, 1, 1),
@@ -530,12 +589,18 @@ public class VibrationThreadTest {

    @Test
    public void vibrate_singleVibratorComposedEffects_runsDifferentVibrations() throws Exception {
        mVibratorProviders.get(VIBRATOR_ID).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
        mVibratorProviders.get(VIBRATOR_ID).setSupportedPrimitives(
        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
        fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
        fakeVibrator.setSupportedPrimitives(
                VibrationEffect.Composition.PRIMITIVE_CLICK,
                VibrationEffect.Composition.PRIMITIVE_TICK);
        mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS,
                IVibrator.CAP_AMPLITUDE_CONTROL);
        fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS,
                IVibrator.CAP_COMPOSE_PWLE_EFFECTS, IVibrator.CAP_AMPLITUDE_CONTROL);
        fakeVibrator.setMinFrequency(100);
        fakeVibrator.setResonantFrequency(150);
        fakeVibrator.setFrequencyResolution(50);
        fakeVibrator.setMaxAmplitudes(
                0.5f /* 100Hz*/, 1 /* 150Hz */, 0.6f /* 200Hz */);

        long vibrationId = 1;
        VibrationEffect effect = VibrationEffect.startComposition()
@@ -543,7 +608,11 @@ public class VibrationThreadTest {
                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f)
                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f)
                .addEffect(VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
                .addOffDuration(Duration.ofMillis(100))
                .addEffect(VibrationEffect.startWaveform()
                        .addTransition(Duration.ofMillis(10),
                                targetAmplitude(1), targetFrequency(100))
                        .addTransition(Duration.ofMillis(20), targetFrequency(120))
                        .build())
                .addEffect(VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
                .compose();
        startThreadAndDispatcher(vibrationId, effect);
@@ -552,7 +621,7 @@ public class VibrationThreadTest {
        // Use first duration the vibrator is turned on since we cannot estimate the clicks.
        verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
        verify(mManagerHooks).noteVibratorOff(eq(UID));
        verify(mControllerCallbacks, times(4)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
        verify(mControllerCallbacks, times(5)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
        verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
        assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
        assertEquals(Arrays.asList(
@@ -560,6 +629,10 @@ public class VibrationThreadTest {
                expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0),
                expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f, 0),
                expectedPrebaked(VibrationEffect.EFFECT_CLICK),
                expectedRamp(/* startAmplitude= */ 0, /* endAmplitude= */ 0.5f,
                        /* startFrequencyHz= */ 150, /* endFrequencyHz= */ 100, /* duration= */ 10),
                expectedRamp(/* startAmplitude= */ 0.5f, /* endAmplitude= */ 0.7f,
                        /* startFrequencyHz= */ 100, /* endFrequencyHz= */ 120, /* duration= */ 20),
                expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
                mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
        assertEquals(expectedAmplitudes(100), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
@@ -605,30 +678,36 @@ public class VibrationThreadTest {
    }

    @Test
    public void vibrate_singleVibratorLargePwle_splitsVibratorComposeCalls() {
    public void vibrate_singleVibratorLargePwle_splitsComposeCallWhenAmplitudeIsLowest() {
        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
        fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS);
        fakeVibrator.setMinFrequency(100);
        fakeVibrator.setResonantFrequency(150);
        fakeVibrator.setFrequencyResolution(50);
        fakeVibrator.setMaxAmplitudes(1, 1, 1);
        fakeVibrator.setPwleSizeMax(2);
        fakeVibrator.setPwleSizeMax(3);

        long vibrationId = 1;
        VibrationEffect effect = VibrationEffect.startWaveform(targetAmplitude(1))
                .addSustain(Duration.ofMillis(10))
                .addTransition(Duration.ofMillis(20), targetAmplitude(0))
                // Waveform will be split here, after vibration goes to zero amplitude
                .addTransition(Duration.ZERO, targetAmplitude(0.8f), targetFrequency(100))
                .addSustain(Duration.ofMillis(30))
                .addTransition(Duration.ofMillis(40), targetAmplitude(0.6f), targetFrequency(200))
                // Waveform will be split here at lowest amplitude.
                .addTransition(Duration.ofMillis(40), targetAmplitude(0.7f), targetFrequency(200))
                .addTransition(Duration.ofMillis(40), targetAmplitude(0.6f), targetFrequency(200))
                .build();
        startThreadAndDispatcher(vibrationId, effect);
        waitForCompletion();

        verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
        // Vibrator compose called twice.
        verify(mControllerCallbacks, times(2)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
        assertEquals(4, fakeVibrator.getEffectSegments(vibrationId).size());

        // Vibrator compose called 3 times with 2 segments instead of 2 times with 3 segments.
        // Using best split points instead of max-packing PWLEs.
        verify(mControllerCallbacks, times(3)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
        assertEquals(6, fakeVibrator.getEffectSegments(vibrationId).size());
    }

    @Test