Merge "Use composition size limit on repeating effects" into tm-dev am: 48a15efcc7 am: 1a9e68638d

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/17474926

Change-Id: I07311446e1ec39e3a37f4c25150816a2dc956c1e
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Lais Andrade
2022-04-04 16:50:07 +00:00
committed by Automerger Merge Worker
5 changed files with 255 additions and 51 deletions

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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) {

View File

@@ -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