Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(3125)

Unified Diff: chrome/android/junit/src/org/chromium/chrome/browser/compositor/CompositorSurfaceManagerTest.java

Issue 2201483002: Improve transition between opaque and translucent compositor views. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: now without VR-breaking badness Created 3 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « chrome/android/java_sources.gni ('k') | content/browser/renderer_host/compositor_impl_android.cc » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: chrome/android/junit/src/org/chromium/chrome/browser/compositor/CompositorSurfaceManagerTest.java
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/compositor/CompositorSurfaceManagerTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/compositor/CompositorSurfaceManagerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..4ed9c7cbde5251c82acd5d8f46f651910898db02
--- /dev/null
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/compositor/CompositorSurfaceManagerTest.java
@@ -0,0 +1,440 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.compositor;
+
+import android.app.Activity;
+import android.graphics.PixelFormat;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.widget.FrameLayout;
+
+import static org.hamcrest.Matchers.lessThan;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowLooper;
+import org.robolectric.shadows.ShadowSurfaceView;
+
+import org.chromium.base.test.util.Feature;
+import org.chromium.testing.local.LocalRobolectricTestRunner;
+
+import java.util.Set;
+
+/**
+ * Unit tests for the CompositorSurfaceManager.
+ */
+@RunWith(LocalRobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class CompositorSurfaceManagerTest {
+ @Mock
+ private SurfaceHolder.Callback mCallback;
+
+ private CompositorSurfaceManager mManager;
+
+ private FrameLayout mLayout;
+
+ /**
+ * Implementation of a SurfaceView shadow that provides additional functionality for controlling
+ * the state of the underlying (fake) Surface.
+ */
+ @Implements(SurfaceView.class)
+ public static class MyShadowSurfaceView extends ShadowSurfaceView {
+ private final MyFakeSurfaceHolder mHolder = new MyFakeSurfaceHolder();
+
+ /**
+ * Robolectric's FakeSurfaceHolder doesn't keep track of the format, etc.
+ */
+ public static class MyFakeSurfaceHolder extends ShadowSurfaceView.FakeSurfaceHolder {
+ /**
+ * Fake surface that lets us control whether it's valid or not.
+ */
+ public static class MyFakeSurface extends Surface {
+ public boolean valid = false;
+
+ @Override
+ public boolean isValid() {
+ return valid;
+ }
+ }
+
+ private int mFormat = PixelFormat.UNKNOWN;
+ private final MyFakeSurface mSurface = new MyFakeSurface();
+
+ @Implementation
+ public void setFormat(int format) {
+ mFormat = format;
+ }
+
+ public int getFormat() {
+ return mFormat;
+ }
+
+ // Return a surface that we can control if it's valid or not.
+ @Override
+ public Surface getSurface() {
+ return getFakeSurface();
+ }
+
+ public MyFakeSurface getFakeSurface() {
+ return mSurface;
+ }
+ }
+
+ public MyShadowSurfaceView() {}
+
+ @Implementation
+ public SurfaceHolder getHolder() {
+ return getMyFakeSurfaceHolder();
+ }
+
+ @Override
+ public FakeSurfaceHolder getFakeSurfaceHolder() {
+ return getMyFakeSurfaceHolder();
+ }
+
+ public MyFakeSurfaceHolder getMyFakeSurfaceHolder() {
+ return mHolder;
+ }
+ }
+
+ @Before
+ public void beforeTest() {
+ MockitoAnnotations.initMocks(this);
+ Activity activity = Robolectric.buildActivity(Activity.class).setup().get();
+ mLayout = new FrameLayout(activity);
+ mManager = new CompositorSurfaceManager(mLayout, mCallback);
+ }
+
+ private void runDelayedTasks() {
+ ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
+ }
+
+ /**
+ * Return the callback for |view|, or null. Will get mad if there's more than one.
+ */
+ private SurfaceHolder.Callback callbackFor(SurfaceView view) {
+ MyShadowSurfaceView viewShadow = (MyShadowSurfaceView) Shadows.shadowOf(view);
+ ShadowSurfaceView.FakeSurfaceHolder viewHolder = viewShadow.getFakeSurfaceHolder();
+ Set<SurfaceHolder.Callback> callbacks = viewHolder.getCallbacks();
+ // Zero or one is okay.
+ assertThat(callbacks.size(), lessThan(2));
+
+ if (callbacks.size() == 1) return callbacks.iterator().next();
+
+ return null;
+ }
+
+ private MyShadowSurfaceView.MyFakeSurfaceHolder fakeHolderFor(SurfaceView view) {
+ MyShadowSurfaceView viewShadow = (MyShadowSurfaceView) Shadows.shadowOf(view);
+ return viewShadow.getMyFakeSurfaceHolder();
+ }
+
+ private void setSurfaceValid(SurfaceView view, boolean valid) {
+ fakeHolderFor(view).getFakeSurface().valid = valid;
+ }
+
+ /**
+ * Find and return the SurfaceView with format |format|.
+ */
+ private SurfaceView findSurface(int format) {
+ final int childCount = mLayout.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final SurfaceView child = (SurfaceView) mLayout.getChildAt(i);
+ if (fakeHolderFor(child).getFormat() == format) return child;
+ }
+
+ return null;
+ }
+
+ /**
+ * Request the pixel format |format|, and return the SurfaceView for it if it's attached. You
+ * are responsible for sending surfaceCreated / Changed to |mManager| if you want it to think
+ * that Android has provided the Surface.
+ */
+ private SurfaceView requestSurface(int format) {
+ mManager.requestSurface(format);
+ runDelayedTasks();
+
+ return findSurface(format);
+ }
+
+ /**
+ * Request format |format|, and send created / changed callbacks to |mManager| as if Android
+ * had provided the underlying Surface.
+ */
+ private SurfaceView requestThenCreateSurface(int format) {
+ SurfaceView view = requestSurface(format);
+ setSurfaceValid(view, true);
+ callbackFor(view).surfaceCreated(view.getHolder());
+ final int actualFormat =
+ (format == PixelFormat.OPAQUE) ? PixelFormat.RGB_565 : PixelFormat.RGBA_8888;
+ final int width = 320;
+ final int height = 240;
+ callbackFor(view).surfaceChanged(view.getHolder(), actualFormat, width, height);
+
+ return view;
+ }
+
+ @Test
+ @Feature("Compositor")
+ @Config(shadows = {MyShadowSurfaceView.class})
+ public void testRequestOpaqueSurface() {
+ // Request a SurfaceView, and test in detail that it worked.
+ SurfaceView opaque = requestSurface(PixelFormat.OPAQUE);
+ verify(mCallback, times(0)).surfaceCreated(ArgumentMatchers.<SurfaceHolder>any());
+ verify(mCallback, times(0))
+ .surfaceChanged(
+ ArgumentMatchers.<SurfaceHolder>any(), anyInt(), anyInt(), anyInt());
+ verify(mCallback, times(0)).surfaceDestroyed(ArgumentMatchers.<SurfaceHolder>any());
+
+ // Check that there's an opaque SurfaceView .
+ assertEquals(1, mLayout.getChildCount());
+ assertTrue(fakeHolderFor(opaque).getFormat() == PixelFormat.OPAQUE);
+
+ // Verify that we are notified when the surface is created.
+ callbackFor(opaque).surfaceCreated(opaque.getHolder());
+ verify(mCallback, times(1)).surfaceCreated(opaque.getHolder());
+ verify(mCallback, times(0)).surfaceDestroyed(ArgumentMatchers.<SurfaceHolder>any());
+
+ // Verify that we are notified when the surface is changed.
+ final int format = PixelFormat.RGB_565;
+ final int width = 320;
+ final int height = 240;
+ callbackFor(opaque).surfaceChanged(opaque.getHolder(), format, width, height);
+ verify(mCallback, times(1)).surfaceCreated(opaque.getHolder());
+ verify(mCallback, times(1)).surfaceChanged(opaque.getHolder(), format, width, height);
+ verify(mCallback, times(0)).surfaceDestroyed(ArgumentMatchers.<SurfaceHolder>any());
+
+ // Verify that we are notified when the surface is destroyed.
+ callbackFor(opaque).surfaceDestroyed(opaque.getHolder());
+ verify(mCallback, times(1)).surfaceCreated(opaque.getHolder());
+ verify(mCallback, times(1)).surfaceChanged(opaque.getHolder(), format, width, height);
+ verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder());
+ }
+
+ @Test
+ @Feature("Compositor")
+ @Config(shadows = {MyShadowSurfaceView.class})
+ public void testRequestOpaqueThenTranslucentSurface() {
+ // Request opaque then translucent.
+ SurfaceView opaque = requestThenCreateSurface(PixelFormat.OPAQUE);
+ SurfaceView translucent = requestThenCreateSurface(PixelFormat.TRANSLUCENT);
+
+ // Verify that we received a destroy for |opaque| and created / changed for |translucent|.
+ verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder());
+ verify(mCallback, times(1)).surfaceCreated(translucent.getHolder());
+ verify(mCallback, times(1))
+ .surfaceChanged(eq(translucent.getHolder()), anyInt(), anyInt(), anyInt());
+
+ // Both views should be present.
+ assertEquals(2, mLayout.getChildCount());
+
+ // Only the translucent surface should be left. Note that the old view is still valid.
+ mManager.doneWithUnownedSurface();
+ runDelayedTasks();
+ assertEquals(1, mLayout.getChildCount());
+ assertNotNull(findSurface(PixelFormat.TRANSLUCENT));
+ }
+
+ @Test
+ @Feature("Compositor")
+ @Config(shadows = {MyShadowSurfaceView.class})
+ public void testRequestSameSurface() {
+ // Request an opaque surface, get it, then request it again. Verify that we get synthetic
+ // create / destroy callbacks.
+ SurfaceView opaque = requestThenCreateSurface(PixelFormat.OPAQUE);
+ verify(mCallback, times(1)).surfaceCreated(opaque.getHolder());
+ verify(mCallback, times(1))
+ .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyInt());
+ verify(mCallback, times(0)).surfaceDestroyed(opaque.getHolder());
+
+ // Surface is curerntly valid. Request again. We should get back a destroy and create.
+ assertEquals(opaque, requestSurface(PixelFormat.OPAQUE));
+ verify(mCallback, times(2)).surfaceCreated(opaque.getHolder());
+ verify(mCallback, times(2))
+ .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyInt());
+ verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder());
+ assertEquals(1, mLayout.getChildCount());
+ }
+
+ @Test
+ @Feature("Compositor")
+ @Config(shadows = {MyShadowSurfaceView.class})
+ public void testRequestSameSurfaceBeforeReady() {
+ // Request an opaque surface, then request it again before the first one shows up.
+ SurfaceView opaque = requestSurface(PixelFormat.OPAQUE);
+ verify(mCallback, times(0)).surfaceCreated(opaque.getHolder());
+ verify(mCallback, times(0))
+ .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyInt());
+ verify(mCallback, times(0)).surfaceDestroyed(opaque.getHolder());
+
+ // Request again. We shouldn't get any callbacks, since the surface is still pending.
+ assertEquals(opaque, requestSurface(PixelFormat.OPAQUE));
+ verify(mCallback, times(0)).surfaceCreated(opaque.getHolder());
+ verify(mCallback, times(0))
+ .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyInt());
+ verify(mCallback, times(0)).surfaceDestroyed(opaque.getHolder());
+
+ // Only the opaque view should be attached.
+ assertEquals(1, mLayout.getChildCount());
+
+ // When the surface is created, we should get notified created / changed, but not destroyed.
+ callbackFor(opaque).surfaceCreated(opaque.getHolder());
+ verify(mCallback, times(1)).surfaceCreated(opaque.getHolder());
+
+ callbackFor(opaque).surfaceChanged(opaque.getHolder(), PixelFormat.RGB_565, 320, 240);
+ verify(mCallback, times(1))
+ .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyInt());
+ verify(mCallback, times(0)).surfaceDestroyed(opaque.getHolder());
+ }
+
+ @Test
+ @Feature("Compositor")
+ @Config(shadows = {MyShadowSurfaceView.class})
+ public void testRequestDifferentSurfacesBeforeReady() {
+ // Request an opaque surface, then request the translucent one before the it one shows up.
+ SurfaceView opaque = requestSurface(PixelFormat.OPAQUE);
+ verify(mCallback, times(0)).surfaceCreated(opaque.getHolder());
+ verify(mCallback, times(0))
+ .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyInt());
+ verify(mCallback, times(0)).surfaceDestroyed(opaque.getHolder());
+
+ // Request translucent. We should get no callbacks, but both views should be attached.
+ SurfaceView translucent = requestSurface(PixelFormat.TRANSLUCENT);
+ verify(mCallback, times(0)).surfaceCreated(opaque.getHolder());
+ verify(mCallback, times(0)).surfaceCreated(translucent.getHolder());
+ assertEquals(2, mLayout.getChildCount());
+
+ // If the opaque surface arrives, we shouldn't hear about it. It should be detached, since
+ // we've requested the other one.
+ callbackFor(opaque).surfaceCreated(opaque.getHolder());
+ runDelayedTasks();
+ assertEquals(1, mLayout.getChildCount());
+ verify(mCallback, times(0)).surfaceCreated(opaque.getHolder());
+ verify(mCallback, times(0)).surfaceCreated(translucent.getHolder());
+ verify(mCallback, times(0)).surfaceDestroyed(opaque.getHolder());
+
+ // When we create the translucent surface, we should be notified.
+ callbackFor(translucent).surfaceCreated(translucent.getHolder());
+ verify(mCallback, times(0)).surfaceCreated(opaque.getHolder());
+ verify(mCallback, times(1)).surfaceCreated(translucent.getHolder());
+ }
+
+ @Test
+ @Feature("Compositor")
+ @Config(shadows = {MyShadowSurfaceView.class})
+ public void testPendingSurfaceChangedCallback() {
+ // Request an opaque surface, and request it again between 'created' and 'changed'. We
+ // should get a synthetic 'created', but a real 'changed' callback.
+ SurfaceView opaque = requestSurface(PixelFormat.OPAQUE);
+ callbackFor(opaque).surfaceCreated(opaque.getHolder());
+ runDelayedTasks();
+
+ // Sanity check.
+ verify(mCallback, times(1)).surfaceCreated(opaque.getHolder());
+ verify(mCallback, times(0))
+ .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyInt());
+
+ // Re-request while 'changed' is still pending. We should get a synthetic 'destroyed' and
+ // synthetic 'created'.
+ assertEquals(opaque, requestSurface(PixelFormat.OPAQUE));
+ verify(mCallback, times(2)).surfaceCreated(opaque.getHolder());
+ verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder());
+ verify(mCallback, times(0))
+ .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyInt());
+
+ // Send 'changed', and expect that we'll receive it.
+ callbackFor(opaque).surfaceChanged(opaque.getHolder(), PixelFormat.RGB_565, 320, 240);
+ verify(mCallback, times(1))
+ .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyInt());
+ }
+
+ @Test
+ @Feature("Compositor")
+ @Config(shadows = {MyShadowSurfaceView.class})
+ public void testJellyBeanWorkaround() {
+ // See if recreateSurfaceForJellyBean destroys / re-creates the surface.
+ // should get a synthetic 'created', but a real 'changed' callback.
+ SurfaceView opaque = requestThenCreateSurface(PixelFormat.OPAQUE);
+ verify(mCallback, times(1)).surfaceCreated(opaque.getHolder());
+ assertEquals(1, mLayout.getChildCount());
+
+ // We should be notified that the surface was destroyed via synthetic callback, and the
+ // surface should be detached.
+ mManager.recreateSurfaceForJellyBean();
+ verify(mCallback, times(1)).surfaceCreated(opaque.getHolder());
+ verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder());
+ assertEquals(0, mLayout.getChildCount());
+
+ // When the surface really is destroyed, it should be re-attached. We should not be
+ // notified again, though.
+ callbackFor(opaque).surfaceDestroyed(opaque.getHolder());
+ verify(mCallback, times(1)).surfaceCreated(opaque.getHolder());
+ verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder());
+ assertEquals(1, mLayout.getChildCount());
+
+ // When the surface is re-created, we should be notified.
+ callbackFor(opaque).surfaceCreated(opaque.getHolder());
+ verify(mCallback, times(2)).surfaceCreated(opaque.getHolder());
+ verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder());
+ assertEquals(1, mLayout.getChildCount());
+ }
+
+ @Test
+ @Feature("Compositor")
+ @Config(shadows = {MyShadowSurfaceView.class})
+ public void testRequestSurfaceDuringDestruction() {
+ // If we re-request a surface while we're tearing it down, it should be re-attached and
+ // given back to us once the destruction completes.
+ SurfaceView opaque = requestThenCreateSurface(PixelFormat.OPAQUE);
+ SurfaceView translucent = requestThenCreateSurface(PixelFormat.TRANSLUCENT);
+ mManager.doneWithUnownedSurface();
+
+ // The transparent surface should be attached, and the opaque one detached.
+ assertEquals(1, mLayout.getChildCount());
+ assertNotNull(findSurface(PixelFormat.TRANSLUCENT));
+
+ // Re-request the opaque surface. Nothing should happen until it's destroyed. It should
+ // not be re-attached, since that is also deferred until destruction.
+ assertEquals(null, requestSurface(PixelFormat.OPAQUE));
+ assertEquals(1, mLayout.getChildCount());
+ assertNotNull(findSurface(PixelFormat.TRANSLUCENT));
+
+ // When the opaque surface is destroyed, then it should be re-attached. No callbacks shoud
+ // have arrived yet, except for initial creation and (synthetic) destroyed when we got the
+ // translucent surface.
+ callbackFor(opaque).surfaceDestroyed(opaque.getHolder());
+ assertEquals(2, mLayout.getChildCount());
+ verify(mCallback, times(1)).surfaceCreated(opaque.getHolder());
+ verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder());
+ verify(mCallback, times(0)).surfaceDestroyed(translucent.getHolder());
+
+ // When the opaque surface becomes available, we'll get the synthetic destroy for the
+ // translucent one that we lost ownership of, and the real create for the opaque one.
+ callbackFor(opaque).surfaceCreated(opaque.getHolder());
+ assertEquals(2, mLayout.getChildCount());
+ verify(mCallback, times(2)).surfaceCreated(opaque.getHolder());
+ verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder());
+ verify(mCallback, times(1)).surfaceDestroyed(translucent.getHolder());
+ }
+}
« no previous file with comments | « chrome/android/java_sources.gni ('k') | content/browser/renderer_host/compositor_impl_android.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698