1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.view.cts; 18 19 import static android.opengl.GLES20.GL_COLOR_BUFFER_BIT; 20 import static android.opengl.GLES20.GL_SCISSOR_TEST; 21 import static android.opengl.GLES20.glClear; 22 import static android.opengl.GLES20.glClearColor; 23 import static android.opengl.GLES20.glEnable; 24 import static android.opengl.GLES20.glScissor; 25 import static android.view.WindowInsets.Type.captionBar; 26 import static android.view.WindowInsets.Type.systemBars; 27 28 import static org.junit.Assert.assertEquals; 29 import static org.junit.Assert.assertNotNull; 30 import static org.junit.Assert.assertTrue; 31 import static org.junit.Assert.fail; 32 33 import android.Manifest; 34 import android.app.Instrumentation; 35 import android.graphics.Bitmap; 36 import android.graphics.Canvas; 37 import android.graphics.Color; 38 import android.graphics.ColorSpace; 39 import android.graphics.Matrix; 40 import android.graphics.Paint; 41 import android.graphics.PixelFormat; 42 import android.graphics.Point; 43 import android.graphics.Rect; 44 import android.graphics.RectF; 45 import android.graphics.SurfaceTexture; 46 import android.hardware.DataSpace; 47 import android.media.Image; 48 import android.media.ImageWriter; 49 import android.platform.test.annotations.AppModeSdkSandbox; 50 import android.util.Half; 51 import android.view.PixelCopy; 52 import android.view.Surface; 53 import android.view.SurfaceHolder; 54 import android.view.SurfaceView; 55 import android.view.TextureView; 56 import android.view.View; 57 import android.view.ViewGroup; 58 import android.view.Window; 59 import android.view.WindowInsets; 60 import android.view.cts.util.BitmapDumper; 61 62 import androidx.test.InstrumentationRegistry; 63 import androidx.test.filters.MediumTest; 64 import androidx.test.rule.ActivityTestRule; 65 66 import com.android.compatibility.common.util.AdoptShellPermissionsRule; 67 import com.android.compatibility.common.util.SynchronousPixelCopy; 68 import com.android.compatibility.common.util.WidgetTestUtils; 69 70 import org.junit.Assert; 71 import org.junit.Before; 72 import org.junit.Rule; 73 import org.junit.Test; 74 import org.junit.rules.TestName; 75 import org.junit.runner.RunWith; 76 77 import java.nio.ByteBuffer; 78 import java.nio.ByteOrder; 79 import java.util.concurrent.CountDownLatch; 80 import java.util.concurrent.TimeUnit; 81 import java.util.concurrent.TimeoutException; 82 83 import junitparams.JUnitParamsRunner; 84 import junitparams.Parameters; 85 86 @MediumTest 87 @RunWith(JUnitParamsRunner.class) 88 @AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).") 89 public class TextureViewTest { 90 91 static final int EGL_GL_COLORSPACE_SRGB_KHR = 0x3089; 92 static final int EGL_GL_COLORSPACE_LINEAR_KHR = 0x308A; 93 static final int EGL_GL_COLORSPACE_DISPLAY_P3_EXT = 0x3363; 94 static final int EGL_GL_COLORSPACE_DISPLAY_P3_LINEAR_EXT = 0x3362; 95 static final int EGL_GL_COLORSPACE_DISPLAY_P3_PASSTHROUGH_EXT = 0x3490; 96 static final int EGL_GL_COLORSPACE_SCRGB_EXT = 0x3351; 97 static final int EGL_GL_COLORSPACE_SCRGB_LINEAR_EXT = 0x3350; 98 99 private Instrumentation mInstrumentation; 100 101 @Before setup()102 public void setup() { 103 mInstrumentation = InstrumentationRegistry.getInstrumentation(); 104 assertNotNull(mInstrumentation); 105 } 106 107 @Rule(order = 0) 108 public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule( 109 androidx.test.platform.app.InstrumentationRegistry 110 .getInstrumentation().getUiAutomation(), 111 Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX); 112 113 @Rule(order = 1) 114 public ActivityTestRule<TextureViewCtsActivity> mActivityRule = 115 new ActivityTestRule<>(TextureViewCtsActivity.class, false, false); 116 117 @Rule(order = 1) 118 public ActivityTestRule<SDRTestActivity> mSDRActivityRule = 119 new ActivityTestRule<>(SDRTestActivity.class, false, false); 120 121 @Rule 122 public TestName mTestName = new TestName(); 123 124 @Test testFirstFrames()125 public void testFirstFrames() throws Throwable { 126 final TextureViewCtsActivity activity = mActivityRule.launchActivity(null); 127 activity.waitForEnterAnimationComplete(); 128 129 final Point center = new Point(); 130 final Window[] windowRet = new Window[1]; 131 mActivityRule.runOnUiThread(() -> { 132 View content = activity.findViewById(android.R.id.content); 133 int[] outLocation = new int[2]; 134 content.getLocationInWindow(outLocation); 135 center.x = outLocation[0] + (content.getWidth() / 2); 136 center.y = outLocation[1] + (content.getHeight() / 2); 137 windowRet[0] = activity.getWindow(); 138 }); 139 final Window window = windowRet[0]; 140 assertTrue(center.x > 0); 141 assertTrue(center.y > 0); 142 waitForColor(window, center, Color.WHITE); 143 activity.waitForSurface(); 144 activity.initGl(); 145 int updatedCount; 146 // If the caption bar is present, the surface update counts increase by 1 147 int extraSurfaceOffset = 148 window.getDecorView().getRootWindowInsets().getInsets(captionBar()).top == 0 149 ? 0 : 1; 150 updatedCount = activity.waitForSurfaceUpdateCount(0 + extraSurfaceOffset); 151 assertEquals(0 + extraSurfaceOffset, updatedCount); 152 activity.drawColor(Color.GREEN); 153 updatedCount = activity.waitForSurfaceUpdateCount(1 + extraSurfaceOffset); 154 assertEquals(1 + extraSurfaceOffset, updatedCount); 155 assertEquals(Color.WHITE, getPixel(window, center)); 156 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, 157 activity.findViewById(android.R.id.content), () -> activity.removeCover()); 158 159 int color = waitForChange(window, center, Color.WHITE); 160 assertEquals(Color.GREEN, color); 161 activity.drawColor(Color.BLUE); 162 updatedCount = activity.waitForSurfaceUpdateCount(2 + extraSurfaceOffset); 163 assertEquals(2 + extraSurfaceOffset, updatedCount); 164 color = waitForChange(window, center, color); 165 assertEquals(Color.BLUE, color); 166 } 167 168 @Test testScaling()169 public void testScaling() throws Throwable { 170 final TextureViewCtsActivity activity = mActivityRule.launchActivity(null); 171 activity.drawFrame(TextureViewTest::drawGlQuad); 172 final Bitmap bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888); 173 mActivityRule.runOnUiThread(() -> { 174 activity.getTextureView().getBitmap(bitmap); 175 }); 176 assertBitmapQuadColor(bitmap, 177 Color.RED, Color.GREEN, Color.BLUE, Color.BLACK); 178 } 179 180 @Test testRotateScale()181 public void testRotateScale() throws Throwable { 182 final TextureViewCtsActivity activity = mActivityRule.launchActivity(null); 183 final TextureView textureView = activity.getTextureView(); 184 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, activity.getTextureView(), null); 185 Matrix rotate = new Matrix(); 186 rotate.setRotate(180, textureView.getWidth() / 2, textureView.getHeight() / 2); 187 activity.drawFrame(rotate, TextureViewTest::drawGlQuad); 188 final Bitmap bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888); 189 mActivityRule.runOnUiThread(() -> { 190 activity.getTextureView().getBitmap(bitmap); 191 }); 192 // Verify the matrix did not rotate content of getTextureView.getBitmap(). 193 assertBitmapQuadColor(bitmap, 194 Color.RED, Color.GREEN, Color.BLUE, Color.BLACK); 195 196 // Remove cover and calculate TextureView position on the screen. 197 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, 198 activity.findViewById(android.R.id.content), () -> activity.removeCover()); 199 final Rect viewPos = new Rect(); 200 mActivityRule.runOnUiThread(() -> { 201 int[] outLocation = new int[2]; 202 textureView.getLocationInWindow(outLocation); 203 viewPos.left = outLocation[0]; 204 viewPos.top = outLocation[1]; 205 viewPos.right = viewPos.left + textureView.getWidth(); 206 viewPos.bottom = viewPos.top + textureView.getHeight(); 207 }); 208 209 // Capture the portion of the screen that contains the texture view only. 210 Window window = activity.getWindow(); 211 Bitmap screenshot = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); 212 int result = new SynchronousPixelCopy().request(window, viewPos, screenshot); 213 assertEquals("Copy request failed", PixelCopy.SUCCESS, result); 214 // Verify the matrix rotated the TextureView content drawn on the screen. 215 assertBitmapQuadColor(screenshot, 216 Color.BLACK, Color.BLUE, Color.GREEN, Color.RED); 217 } 218 219 @Test testTransformScale()220 public void testTransformScale() throws Throwable { 221 final TextureViewCtsActivity activity = mActivityRule.launchActivity(null); 222 final TextureView textureView = activity.getTextureView(); 223 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, activity.getTextureView(), null); 224 Matrix transform = new Matrix(); 225 final float translateY = 100.0f; 226 final float scaleY = 0.25f; 227 float[] values = {1, 0, 0, 0, scaleY, translateY, 0, 0, 1}; 228 transform.setValues(values); 229 activity.drawFrame(transform, TextureViewTest::drawGlQuad); 230 final Bitmap bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888); 231 mActivityRule.runOnUiThread(() -> { 232 activity.getTextureView().getBitmap(bitmap); 233 }); 234 // Verify the matrix did not affect the content of getTextureView.getBitmap(). 235 assertBitmapQuadColor(bitmap, 236 Color.RED, Color.GREEN, Color.BLUE, Color.BLACK); 237 238 // Remove cover and calculate TextureView position on the screen. 239 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, 240 activity.findViewById(android.R.id.content), () -> activity.removeCover()); 241 final Rect viewPos = new Rect(); 242 mActivityRule.runOnUiThread(() -> { 243 int[] outLocation = new int[2]; 244 textureView.getLocationInWindow(outLocation); 245 viewPos.left = outLocation[0]; 246 viewPos.top = outLocation[1]; 247 viewPos.right = viewPos.left + textureView.getWidth(); 248 viewPos.bottom = viewPos.top + textureView.getHeight(); 249 }); 250 251 // Capture the portion of the screen that contains the texture view only. 252 Window window = activity.getWindow(); 253 Bitmap screenshot = Bitmap.createBitmap(viewPos.width(), viewPos.height(), 254 Bitmap.Config.ARGB_8888); 255 int result = new SynchronousPixelCopy().request(window, viewPos, screenshot); 256 assertEquals("Copy request failed", PixelCopy.SUCCESS, result); 257 // Verify the matrix scaled and translated the TextureView content drawn on the screen. 258 // "texturePos" has SurfaceTexture position inside the TextureView. 259 final Rect texturePos = new Rect(0, (int) translateY, viewPos.width(), 260 (int) (viewPos.height() * scaleY + translateY)); 261 262 // Areas not covered by the texture are black, because FrameLayout background is set to 263 // Color.BLACK in TextureViewCtsActivity.onCreate. 264 assertEquals("above texture", Color.BLACK, 265 screenshot.getPixel(10, texturePos.top - 10)); 266 assertEquals("below texture", Color.BLACK, 267 screenshot.getPixel(10, texturePos.bottom + 10)); 268 assertEquals("top left", Color.RED, 269 screenshot.getPixel(texturePos.left + 10, texturePos.top + 10)); 270 assertEquals("top right", Color.GREEN, 271 screenshot.getPixel(texturePos.right - 10, texturePos.top + 10)); 272 assertEquals("Bottom left", Color.BLUE, 273 screenshot.getPixel(texturePos.left + 10, texturePos.bottom - 10)); 274 assertEquals("Bottom right", Color.BLACK, 275 screenshot.getPixel(texturePos.right - 10, texturePos.bottom - 10)); 276 } 277 278 // TODO(b/229173479): understand why DCI_P3 and BT2020 do not match with certain colors. 279 // TODO(b/230400473): Add in BT2020 and BT709 and BT601 once SurfaceFlinger reliably color 280 // converts. testDataSpaces()281 private static Object[] testDataSpaces() { 282 return new Integer[]{ 283 DataSpace.DATASPACE_SCRGB_LINEAR, 284 DataSpace.DATASPACE_SRGB, 285 DataSpace.DATASPACE_SCRGB, 286 DataSpace.DATASPACE_DISPLAY_P3, 287 DataSpace.DATASPACE_ADOBE_RGB, 288 DataSpace.DATASPACE_DCI_P3, 289 DataSpace.DATASPACE_SRGB_LINEAR 290 }; 291 } 292 293 @Test 294 @Parameters(method = "testDataSpaces") testSDRFromSurfaceViewAndTextureView(int dataSpace)295 public void testSDRFromSurfaceViewAndTextureView(int dataSpace) throws Throwable { 296 final int grayishYellow = 0xFFBABAB9; 297 long converted = Color.convert(grayishYellow, ColorSpace.getFromDataSpace(dataSpace)); 298 299 final SDRTestActivity activity = 300 mSDRActivityRule.launchActivity(/*startIntent*/ null); 301 activity.waitForEnterAnimationComplete(); 302 303 TextureView textureView = activity.getTextureView(); 304 // SurfaceView and TextureView dimensions are the same so we reuse variables 305 int width = textureView.getWidth(); 306 int height = textureView.getHeight(); 307 308 // paint surfaceView layer 309 SurfaceView surfaceView = activity.getSurfaceView(); 310 final CountDownLatch latch = new CountDownLatch(1); 311 surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() { 312 @Override 313 public void surfaceCreated(SurfaceHolder holder) { 314 ImageWriter writer = new ImageWriter 315 .Builder(holder.getSurface()) 316 .setHardwareBufferFormat(PixelFormat.RGBA_8888) 317 .setDataSpace(dataSpace) 318 .build(); 319 // spawn a thread here to iterate 10 times from image dequeue to queue 320 // so that we can be stalled until the first frame has been displayed. 321 new Thread(() -> { 322 Bitmap bitmap = null; 323 for (int i = 0; i < 10; i++) { 324 Image image = writer.dequeueInputImage(); 325 assertEquals(dataSpace, image.getDataSpace()); 326 Image.Plane plane = image.getPlanes()[0]; 327 // only make bitmap the first time to improve the performation 328 // if the bitmap is large. 329 if (bitmap == null) { 330 bitmap = Bitmap.createBitmap(plane.getRowStride() / 4, 331 image.getHeight(), 332 Bitmap.Config.ARGB_8888, true, 333 ColorSpace.getFromDataSpace(dataSpace)); 334 Canvas canvas = new Canvas(bitmap); 335 canvas.drawColor(converted); 336 } 337 bitmap.copyPixelsToBuffer(plane.getBuffer()); 338 writer.queueInputImage(image); 339 } 340 latch.countDown(); 341 }).start(); 342 } 343 344 @Override 345 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {} 346 347 @Override 348 public void surfaceDestroyed(SurfaceHolder holder) {} 349 }); 350 351 WidgetTestUtils.runOnMainAndDrawSync(mSDRActivityRule, surfaceView, () -> { 352 ((ViewGroup) surfaceView.getParent()).removeView(surfaceView); 353 activity.setContentView(surfaceView); 354 }); 355 356 assertTrue(latch.await(5, TimeUnit.SECONDS)); 357 358 Bitmap surfaceViewScreenshot = mInstrumentation 359 .getUiAutomation() 360 .takeScreenshot(activity.getWindow()); 361 362 WidgetTestUtils.runOnMainAndDrawSync(mSDRActivityRule, textureView, () -> { 363 ((ViewGroup) textureView.getParent()).removeView(textureView); 364 activity.setContentView(textureView); 365 }); 366 367 // paint textureView layer 368 SurfaceTexture surfaceTexture = textureView.getSurfaceTexture(); 369 Surface surface = new Surface(surfaceTexture); 370 assertTrue(surface.isValid()); 371 372 ImageWriter writer = new ImageWriter 373 .Builder(surface) 374 .setHardwareBufferFormat(PixelFormat.RGBA_8888) 375 .setDataSpace(dataSpace) 376 .build(); 377 Image image = writer.dequeueInputImage(); 378 assertEquals(dataSpace, image.getDataSpace()); 379 Image.Plane plane = image.getPlanes()[0]; 380 Bitmap bitmap = Bitmap.createBitmap(plane.getRowStride() / 4, image.getHeight(), 381 Bitmap.Config.ARGB_8888, true, ColorSpace.getFromDataSpace(dataSpace)); 382 Canvas canvas = new Canvas(bitmap); 383 canvas.drawColor(converted); 384 bitmap.copyPixelsToBuffer(plane.getBuffer()); 385 writer.queueInputImage(image); 386 387 final Bitmap textureViewScreenshot = 388 Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 389 // surfaceViewScreenshot colorspace depends on SF colormode selection, 390 // i.e., Display_P3 or sRGB, therefore, change textureViewScreenshot's bitmap 391 // colorspace to be aligned with it 392 textureViewScreenshot.setColorSpace(surfaceViewScreenshot.getColorSpace()); 393 394 WidgetTestUtils.runOnMainAndDrawSync( 395 mSDRActivityRule, textureView, () -> textureView.getBitmap(textureViewScreenshot)); 396 397 WindowInsets rootWindowInsets = activity.getWindow().getDecorView().getRootWindowInsets(); 398 399 int extraSurfaceTopOffset = rootWindowInsets.getInsets(systemBars()).top; 400 int extraSurfaceRightOffset = rootWindowInsets.getInsets(systemBars()).right; 401 int extraSurfaceBottomOffset = rootWindowInsets.getInsets(systemBars()).bottom; 402 int extraSurfaceLeftOffset = rootWindowInsets.getInsets(systemBars()).left; 403 404 // sample 5 pixels on the edge for bitmap comparison. 405 // TextureView and SurfaceView use different shaders, so compare these two with tolerance. 406 // TODO(b/229173479): These shaders shouldn't be very different. Figure out why we need 407 // this tolerance in the first place. 408 final int threshold = 3; 409 try { 410 assertPixelsAreSame(surfaceViewScreenshot.getPixel(width / 2, extraSurfaceTopOffset), 411 textureViewScreenshot.getPixel(width / 2, 0), threshold); 412 assertPixelsAreSame(surfaceViewScreenshot.getPixel(extraSurfaceLeftOffset, height / 2), 413 textureViewScreenshot.getPixel(0, height / 2), threshold); 414 assertPixelsAreSame(surfaceViewScreenshot.getPixel(width / 2, height / 2), 415 textureViewScreenshot.getPixel(width / 2, height / 2), threshold); 416 assertPixelsAreSame( 417 surfaceViewScreenshot.getPixel(width / 2, 418 height - 1 - extraSurfaceBottomOffset), 419 textureViewScreenshot.getPixel(width / 2, 420 height - 1), 421 threshold); 422 assertPixelsAreSame( 423 surfaceViewScreenshot.getPixel(width - 1 - extraSurfaceRightOffset, height / 2), 424 textureViewScreenshot.getPixel(width - 1, height / 2), threshold); 425 } catch (AssertionError err) { 426 BitmapDumper.dumpBitmap(textureViewScreenshot, 427 mTestName.getMethodName() + "_textureView", "TextureViewTest"); 428 BitmapDumper.dumpBitmap(surfaceViewScreenshot, 429 mTestName.getMethodName() + "_surfaceView", "TextureViewTest"); 430 throw err; 431 } 432 } 433 434 @Test testCropRect()435 public void testCropRect() throws Throwable { 436 final TextureViewCtsActivity activity = mActivityRule.launchActivity(/*startIntent*/ null); 437 activity.waitForSurface(); 438 final TextureView textureView = activity.getTextureView(); 439 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, textureView, () -> { 440 activity.removeCover(); 441 // This test is sensitive to GPU sampling precision, so cap the size of the textureview 442 // to a known small value that will not have float precision issues when being 443 // sampled, specifically when running on GPUs limited to fp16 precision 444 ViewGroup.LayoutParams params = textureView.getLayoutParams(); 445 params.width = 100; 446 params.height = 100; 447 textureView.setLayoutParams(params); 448 }); 449 int textureWidth = textureView.getWidth(); 450 int textureHeight = textureView.getHeight(); 451 assertEquals(100, textureWidth); 452 assertEquals(100, textureHeight); 453 SurfaceTexture surfaceTexture = textureView.getSurfaceTexture(); 454 Surface surface = new Surface(surfaceTexture); 455 assertTrue(surface.isValid()); 456 ImageWriter writer = ImageWriter.newInstance(surface, /*maxImages*/ 1); 457 Image image = writer.dequeueInputImage(); 458 assertEquals(100, image.getWidth()); 459 assertEquals(100, image.getHeight()); 460 Image.Plane plane = image.getPlanes()[0]; 461 Bitmap bitmap = Bitmap.createBitmap(plane.getRowStride() / 4, image.getHeight(), 462 Bitmap.Config.ARGB_8888); 463 Canvas canvas = new Canvas(bitmap); 464 Paint paint = new Paint(); 465 paint.setAntiAlias(false); 466 paint.setColor(Color.YELLOW); 467 canvas.drawRect(0f, 0f, textureWidth, textureHeight, paint); 468 paint.setColor(Color.BLACK); 469 canvas.drawRect(2f, 2f, textureWidth - 2f, textureHeight - 2f, paint); 470 471 image.setCropRect(new Rect(1, 1, textureWidth - 1, textureHeight - 1)); 472 bitmap.copyPixelsToBuffer(plane.getBuffer()); 473 writer.queueInputImage(image); 474 waitForDraw(textureView); 475 476 final Rect viewPos = new Rect(); 477 mActivityRule.runOnUiThread(() -> { 478 int[] outLocation = new int[2]; 479 textureView.getLocationInSurface(outLocation); 480 viewPos.left = outLocation[0]; 481 viewPos.top = outLocation[1]; 482 viewPos.right = viewPos.left + textureView.getWidth(); 483 viewPos.bottom = viewPos.top + textureView.getHeight(); 484 }); 485 SynchronousPixelCopy pixelCopy = new SynchronousPixelCopy(); 486 // Capture the portion of the screen that contains the texture view only. 487 Window window = activity.getWindow(); 488 bitmap = Bitmap.createBitmap(viewPos.width(), viewPos.height(), 489 Bitmap.Config.ARGB_8888); 490 int result = pixelCopy.request(window, viewPos, bitmap); 491 assertEquals("Copy request failed", PixelCopy.SUCCESS, result); 492 assertBitmapEdgeColor(bitmap, Color.YELLOW); 493 } 494 495 // TODO(b/220361081) replace with runOnMainAndDrawSync once we have the 496 // runOnMainAndDrawSync updated to use the registerFrameCommitCallback waitForDraw(final View view)497 private void waitForDraw(final View view) throws Throwable { 498 final CountDownLatch latch = new CountDownLatch(1); 499 mActivityRule.runOnUiThread(() -> { 500 view.getViewTreeObserver().registerFrameCommitCallback(latch::countDown); 501 view.invalidate(); 502 }); 503 assertTrue(latch.await(1, TimeUnit.SECONDS)); 504 } 505 pixelsAreSame(int ideal, int given, int threshold)506 private boolean pixelsAreSame(int ideal, int given, int threshold) { 507 int error = Math.abs(Color.red(ideal) - Color.red(given)); 508 error += Math.abs(Color.green(ideal) - Color.green(given)); 509 error += Math.abs(Color.blue(ideal) - Color.blue(given)); 510 return (error < threshold); 511 } 512 assertPixelsAreSame(int ideal, int given, int threshold)513 private void assertPixelsAreSame(int ideal, int given, int threshold) { 514 if (!pixelsAreSame(ideal, given, threshold)) { 515 fail("expected=" + Integer.toHexString(ideal) + ", actual=" 516 + Integer.toHexString(given)); 517 } 518 } 519 520 @Test testGetBitmap_8888_P3()521 public void testGetBitmap_8888_P3() throws Throwable { 522 testGetBitmap(EGL_GL_COLORSPACE_DISPLAY_P3_EXT, ColorSpace.get(ColorSpace.Named.DISPLAY_P3), 523 false, false, new FP16Compare(ColorSpace.Named.EXTENDED_SRGB)); 524 } 525 526 @Test testGetBitmap_8888_PassthroughP3()527 public void testGetBitmap_8888_PassthroughP3() throws Throwable { 528 testGetBitmap(EGL_GL_COLORSPACE_DISPLAY_P3_PASSTHROUGH_EXT, 529 ColorSpace.get(ColorSpace.Named.DISPLAY_P3), false, true, 530 new FP16Compare(ColorSpace.Named.EXTENDED_SRGB)); 531 } 532 533 @Test testGetBitmap_FP16_PassthroughP3()534 public void testGetBitmap_FP16_PassthroughP3() throws Throwable { 535 testGetBitmap(EGL_GL_COLORSPACE_DISPLAY_P3_PASSTHROUGH_EXT, 536 ColorSpace.get(ColorSpace.Named.DISPLAY_P3), true, true, 537 new FP16Compare(ColorSpace.Named.EXTENDED_SRGB)); 538 } 539 540 @Test testGetBitmap_FP16_LinearP3()541 public void testGetBitmap_FP16_LinearP3() throws Throwable { 542 ColorSpace.Rgb displayP3 = (ColorSpace.Rgb) ColorSpace.get(ColorSpace.Named.DISPLAY_P3); 543 ColorSpace.Rgb linearDisplayP3 = new ColorSpace.Rgb( 544 "Display P3 Linear", 545 displayP3.getTransform(), 546 displayP3.getWhitePoint(), 547 x -> x, 548 x -> x, 549 0.0f, 1.0f 550 ); 551 552 testGetBitmap(EGL_GL_COLORSPACE_DISPLAY_P3_LINEAR_EXT, linearDisplayP3, true, 553 true, new FP16Compare(ColorSpace.Named.EXTENDED_SRGB)); 554 } 555 556 @Test testGetBitmap_FP16_ExtendedSRGB()557 public void testGetBitmap_FP16_ExtendedSRGB() throws Throwable { 558 // isLinear is "true", because the spec says 559 // GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING is GL_LINEAR for EGL_GL_COLORSPACE_SCRGB_EXT. 560 // See https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_gl_colorspace_scrgb.txt. 561 testGetBitmap(EGL_GL_COLORSPACE_SCRGB_EXT, 562 ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB), true, 563 true, new FP16Compare(ColorSpace.Named.EXTENDED_SRGB)); 564 } 565 566 @Test testGetBitmap_FP16_LinearExtendedSRGB()567 public void testGetBitmap_FP16_LinearExtendedSRGB() throws Throwable { 568 testGetBitmap(EGL_GL_COLORSPACE_SCRGB_LINEAR_EXT, 569 ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB), true, 570 true, new FP16Compare(ColorSpace.Named.EXTENDED_SRGB)); 571 } 572 573 @Test testGet565Bitmap_SRGB()574 public void testGet565Bitmap_SRGB() throws Throwable { 575 testGetBitmap(EGL_GL_COLORSPACE_SRGB_KHR, ColorSpace.get(ColorSpace.Named.SRGB), 576 false, false, new SRGBCompare(Bitmap.Config.RGB_565)); 577 } 578 579 @Test testGetBitmap_SRGB()580 public void testGetBitmap_SRGB() throws Throwable { 581 testGetBitmap(EGL_GL_COLORSPACE_SRGB_KHR, ColorSpace.get(ColorSpace.Named.SRGB), 582 false, false, new SRGBCompare(Bitmap.Config.ARGB_8888)); 583 } 584 585 @Test testGetBitmap_SRGBLinear()586 public void testGetBitmap_SRGBLinear() throws Throwable { 587 testGetBitmap(EGL_GL_COLORSPACE_LINEAR_KHR, ColorSpace.get(ColorSpace.Named.LINEAR_SRGB), 588 false, true, new SRGBCompare(Bitmap.Config.ARGB_8888)); 589 } 590 591 /** 592 * Test that verifies TextureView is drawn with bilerp sampling, when the matrix is not 593 * an integer translate or identity. 594 */ 595 @Test testSamplingWithTransform()596 public void testSamplingWithTransform() throws Throwable { 597 final TextureViewCtsActivity activity = mActivityRule.launchActivity(null); 598 final TextureView textureView = activity.getTextureView(); 599 final int viewWidth = textureView.getWidth(); 600 final int viewHeight = textureView.getHeight(); 601 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, activity.getTextureView(), null); 602 // Remove cover and calculate TextureView position on the screen. 603 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, 604 activity.findViewById(android.R.id.content), () -> activity.removeCover()); 605 606 float[][] matrices = { 607 {1, 0, 0, 0, 1, 0, 0, 0, 1}, // identity matrix 608 {1, 0, 0, 0, 1, 10.3f, 0, 0, 1}, // translation matrix with a fractional offset 609 {1, 0, 0, 0, 0.75f, 0, 0, 0, 1}, // scaling matrix 610 {1, 0, 0, 0, 1, 10f, 0, 0, 1}, // translation matrix with an integer offset 611 {0, -1, viewWidth, 1, 0, 0, 0, 0, 1}, // 90 rotation matrix + integer translate X 612 {0, 1, 0, -1, 0, viewWidth, 0, 0, 1}, // 270 rotation matrix + integer translate Y 613 {-1, 0, viewWidth, 0, 1, 0, 0, 0, 1}, // H flip matrix + integer translate X 614 {1, 0, 0, 0, -1, viewHeight, 0, 0, 1}, // V flip matrix + integer translate Y 615 {-1, 0, viewWidth, 0, -1, viewHeight, 0, 0, 1}, // 180 rotation + integer translate X Y 616 {0, -1, viewWidth - 10.3f, 1, 0, 0, 0, 0, 1}, // 90 rotation matrix with a fractional 617 // offset 618 }; 619 boolean[] nearestSampling = { 620 true, // nearest sampling for identity 621 false, // bilerp sampling for fractional translate 622 false, // bilerp sampling for scaling 623 true, // nearest sampling for integer translate 624 true, // nearest sampling for 90 rotation with integer translate 625 true, // nearest sampling for 270 rotation with integer translate 626 true, // nearest sampling for H flip with integer translate 627 true, // nearest sampling for V flip with integer translate 628 true, // nearest sampling for 180 rotation with integer translate 629 false, // bilerp sampling for 90 rotation matrix with a fractional offset 630 }; 631 for (int i = 0; i < nearestSampling.length; i++) { 632 633 Matrix transform = new Matrix(); 634 transform.setValues(matrices[i]); 635 636 // Test draws a set of black & white alternating lines. 637 activity.drawFrame(transform, TextureViewTest::drawGlBlackWhiteLines); 638 639 final Rect viewPos = new Rect(); 640 mActivityRule.runOnUiThread(() -> { 641 int[] outLocation = new int[2]; 642 textureView.getLocationInWindow(outLocation); 643 viewPos.left = outLocation[0]; 644 viewPos.top = outLocation[1]; 645 viewPos.right = viewPos.left + textureView.getWidth(); 646 viewPos.bottom = viewPos.top + textureView.getHeight(); 647 }); 648 649 // Capture the portion of the screen that contains the texture view only. 650 Window window = activity.getWindow(); 651 Bitmap screenshot = Bitmap.createBitmap(viewPos.width(), viewPos.height(), 652 Bitmap.Config.ARGB_8888); 653 int result = new SynchronousPixelCopy().request(window, viewPos, screenshot); 654 assertEquals("Copy request failed", PixelCopy.SUCCESS, result); 655 656 // "texturePos" has SurfaceTexture position inside the TextureView. 657 RectF texturePosF = new RectF(0, 0, viewPos.width(), viewPos.height()); 658 transform.mapRect(texturePosF); 659 // Clip parts outside TextureView. 660 // Matrices are picked, so that the drawing area is not empty. 661 assertTrue("empty test area", 662 texturePosF.intersect(0, 0, viewPos.width(), viewPos.height())); 663 Rect texturePos = new Rect((int) Math.ceil(texturePosF.left), 664 (int) Math.ceil(texturePosF.top), (int) Math.floor(texturePosF.right), 665 (int) Math.floor(texturePosF.bottom)); 666 667 int[] pixels = new int[texturePos.width() * texturePos.height()]; 668 screenshot.getPixels(pixels, 0, texturePos.width(), texturePos.left, texturePos.top, 669 texturePos.width(), texturePos.height()); 670 671 boolean success = true; 672 int failPosition = 0; 673 if (nearestSampling[i]) { 674 // Check all pixels are either black or white. 675 for (int j = 0; j < pixels.length; j++) { 676 if (pixels[j] != Color.BLACK && pixels[j] != Color.WHITE) { 677 success = false; 678 failPosition = j; 679 break; 680 } 681 } 682 } else { 683 // Check that a third of pixels are not black nor white, because bilerp sampling 684 // changed pure black/white to a variety of gray intermediates. 685 int nonBlackWhitePixels = 0; 686 for (int j = 0; j < pixels.length; j++) { 687 if (pixels[j] != Color.BLACK && pixels[j] != Color.WHITE) { 688 nonBlackWhitePixels++; 689 } else { 690 failPosition = j; 691 } 692 } 693 if (nonBlackWhitePixels < pixels.length / 3) { 694 success = false; 695 } 696 } 697 assertTrue("Unexpected color at position " + failPosition + " = " 698 + Integer.toHexString(pixels[failPosition]) + " " + transform.toString(), 699 success); 700 } 701 } 702 703 interface CompareFunction { getConfig()704 Bitmap.Config getConfig(); getColorSpace()705 ColorSpace getColorSpace(); verify(float[] srcColor, ColorSpace srcColorSpace, Bitmap dstBitmap)706 void verify(float[] srcColor, ColorSpace srcColorSpace, Bitmap dstBitmap); 707 } 708 709 private class FP16Compare implements CompareFunction { 710 private ColorSpace mDstColorSpace; 711 FP16Compare(ColorSpace.Named namedCS)712 FP16Compare(ColorSpace.Named namedCS) { 713 mDstColorSpace = ColorSpace.get(namedCS); 714 } 715 getConfig()716 public Bitmap.Config getConfig() { 717 return Bitmap.Config.RGBA_F16; 718 } 719 getColorSpace()720 public ColorSpace getColorSpace() { 721 return mDstColorSpace; 722 } 723 verify(float[] srcColor, ColorSpace srcColorSpace, Bitmap dstBitmap)724 public void verify(float[] srcColor, ColorSpace srcColorSpace, Bitmap dstBitmap) { 725 // read pixels into buffer and compare using colorspace connector 726 ByteBuffer buffer = ByteBuffer.allocate(dstBitmap.getAllocationByteCount()); 727 buffer.order(ByteOrder.LITTLE_ENDIAN); 728 dstBitmap.copyPixelsToBuffer(buffer); 729 Half alpha = Half.valueOf(buffer.getShort(6)); 730 assertEquals(1.0f, alpha.floatValue(), 0.0f); 731 732 final ColorSpace dstSpace = getColorSpace(); 733 float[] expectedColor = ColorSpace.connect(srcColorSpace, dstSpace).transform(srcColor); 734 float[] outputColor = { 735 Half.valueOf(buffer.getShort(0)).floatValue(), 736 Half.valueOf(buffer.getShort(2)).floatValue(), 737 Half.valueOf(buffer.getShort(4)).floatValue() }; 738 739 assertEquals(expectedColor[0], outputColor[0], 0.01f); 740 assertEquals(expectedColor[1], outputColor[1], 0.01f); 741 assertEquals(expectedColor[2], outputColor[2], 0.01f); 742 } 743 } 744 745 private class SRGBCompare implements CompareFunction { 746 private Bitmap.Config mConfig; 747 SRGBCompare(Bitmap.Config config)748 SRGBCompare(Bitmap.Config config) { 749 mConfig = config; 750 } 751 getConfig()752 public Bitmap.Config getConfig() { 753 return mConfig; 754 } 755 getColorSpace()756 public ColorSpace getColorSpace() { 757 return ColorSpace.get(ColorSpace.Named.SRGB); 758 } 759 verify(float[] srcColor, ColorSpace srcColorSpace, Bitmap dstBitmap)760 public void verify(float[] srcColor, ColorSpace srcColorSpace, Bitmap dstBitmap) { 761 int color = dstBitmap.getPixel(0, 0); 762 assertEquals(1.0f, Color.alpha(color) / 255.0f, 0.0f); 763 assertEquals(srcColor[0], Color.red(color) / 255.0f, 0.01f); 764 assertEquals(srcColor[1], Color.green(color) / 255.0f, 0.01f); 765 assertEquals(srcColor[2], Color.blue(color) / 255.0f, 0.01f); 766 } 767 } 768 769 // isFramebufferLinear is true, when GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING is GL_LINEAR. 770 // It is false, when GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING is GL_SRGB. testGetBitmap(int eglColorSpace, ColorSpace colorSpace, boolean useHalfFloat, boolean isFramebufferLinear, CompareFunction compareFunction)771 private void testGetBitmap(int eglColorSpace, ColorSpace colorSpace, 772 boolean useHalfFloat, boolean isFramebufferLinear, 773 CompareFunction compareFunction) throws Throwable { 774 final TextureViewCtsActivity activity = mActivityRule.launchActivity(null); 775 activity.waitForSurface(); 776 777 try { 778 activity.initGl(eglColorSpace, useHalfFloat); 779 } catch (RuntimeException e) { 780 // failure to init GL with the right colorspace is not a TextureView failure as some 781 // devices may not support 16-bits or the colorspace extension 782 if (!activity.initGLExtensionUnsupported()) { 783 fail("Unable to initGL : " + e); 784 } 785 return; 786 } 787 788 final float[] inputColor = { 1.0f, 128 / 255.0f, 0.0f}; 789 790 int updatedCount; 791 updatedCount = activity.waitForSurfaceUpdateCount(0); 792 assertEquals(0, updatedCount); 793 activity.drawColor(inputColor[0], inputColor[1], inputColor[2], 1.0f); 794 updatedCount = activity.waitForSurfaceUpdateCount(1); 795 assertEquals(1, updatedCount); 796 797 final Bitmap bitmap = activity.getContents(compareFunction.getConfig(), 798 compareFunction.getColorSpace()); 799 800 // If GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING is GL_SRGB, then glClear will treat the input 801 // color as linear and write a converted sRGB color into the framebuffer. 802 if (isFramebufferLinear) { 803 compareFunction.verify(inputColor, colorSpace, bitmap); 804 } else { 805 ColorSpace.Connector connector; 806 connector = ColorSpace.connect(ColorSpace.get(ColorSpace.Named.LINEAR_SRGB), 807 ColorSpace.get(ColorSpace.Named.SRGB)); 808 float[] outputColor = connector.transform(inputColor); 809 compareFunction.verify(outputColor, colorSpace, bitmap); 810 } 811 } 812 drawGlQuad(int width, int height)813 private static void drawGlQuad(int width, int height) { 814 int cx = width / 2; 815 int cy = height / 2; 816 817 glEnable(GL_SCISSOR_TEST); 818 819 glScissor(0, cy, cx, height - cy); 820 clearColor(Color.RED); 821 822 glScissor(cx, cy, width - cx, height - cy); 823 clearColor(Color.GREEN); 824 825 glScissor(0, 0, cx, cy); 826 clearColor(Color.BLUE); 827 828 glScissor(cx, 0, width - cx, cy); 829 clearColor(Color.BLACK); 830 } 831 drawGlBlackWhiteLines(int width, int height)832 private static void drawGlBlackWhiteLines(int width, int height) { 833 final int lineHeight = 1; 834 glEnable(GL_SCISSOR_TEST); 835 for (int y = 0; y < height / lineHeight; y++) { 836 glScissor(0, lineHeight * y, width, lineHeight); 837 clearColor((y % 2 == 0) ? Color.BLACK : Color.WHITE); 838 } 839 } 840 clearColor(int color)841 private static void clearColor(int color) { 842 glClearColor(Color.red(color) / 255.0f, 843 Color.green(color) / 255.0f, 844 Color.blue(color) / 255.0f, 845 Color.alpha(color) / 255.0f); 846 glClear(GL_COLOR_BUFFER_BIT); 847 } 848 getPixel(Window window, Point point)849 private int getPixel(Window window, Point point) { 850 Bitmap screenshot = Bitmap.createBitmap(window.getDecorView().getWidth(), 851 window.getDecorView().getHeight(), Bitmap.Config.ARGB_8888); 852 int result = new SynchronousPixelCopy().request(window, screenshot); 853 assertEquals("Copy request failed", PixelCopy.SUCCESS, result); 854 int pixel = screenshot.getPixel(point.x, point.y); 855 screenshot.recycle(); 856 return pixel; 857 } 858 waitForColor(Window window, Point point, int color)859 private void waitForColor(Window window, Point point, int color) 860 throws InterruptedException, TimeoutException { 861 for (int i = 0; i < 20; i++) { 862 int pixel = getPixel(window, point); 863 if (pixel == color) { 864 return; 865 } 866 Thread.sleep(16); 867 } 868 throw new TimeoutException(); 869 } 870 waitForChange(Window window, Point point, int color)871 private int waitForChange(Window window, Point point, int color) 872 throws InterruptedException, TimeoutException { 873 for (int i = 0; i < 30; i++) { 874 int pixel = getPixel(window, point); 875 if (pixel != color) { 876 return pixel; 877 } 878 Thread.sleep(16); 879 } 880 throw new TimeoutException(); 881 } 882 assertBitmapQuadColor(Bitmap bitmap, int topLeft, int topRight, int bottomLeft, int bottomRight)883 private void assertBitmapQuadColor(Bitmap bitmap, 884 int topLeft, int topRight, int bottomLeft, int bottomRight) { 885 PixelCopyTest.assertBitmapQuadColor(mTestName.getMethodName(), "TextureViewTest", 886 bitmap, topLeft, topRight, bottomLeft, bottomRight); 887 } 888 assertBitmapEdgeColor(Bitmap bitmap, int edgeColor)889 private void assertBitmapEdgeColor(Bitmap bitmap, int edgeColor) { 890 // Just quickly sample a few pixels on the edge and assert 891 // they are edge color, then assert that just inside the edge is a different color 892 assertBitmapColor("Top edge", bitmap, edgeColor, bitmap.getWidth() / 2, 0); 893 assertBitmapNotColor("Top edge", bitmap, edgeColor, bitmap.getWidth() / 2, 2); 894 895 assertBitmapColor("Left edge", bitmap, edgeColor, 0, bitmap.getHeight() / 2); 896 assertBitmapNotColor("Left edge", bitmap, edgeColor, 2, bitmap.getHeight() / 2); 897 898 assertBitmapColor("Bottom edge", bitmap, edgeColor, 899 bitmap.getWidth() / 2, bitmap.getHeight() - 1); 900 assertBitmapNotColor("Bottom edge", bitmap, edgeColor, 901 bitmap.getWidth() / 2, bitmap.getHeight() - 3); 902 903 assertBitmapColor("Right edge", bitmap, edgeColor, 904 bitmap.getWidth() - 1, bitmap.getHeight() / 2); 905 assertBitmapNotColor("Right edge", bitmap, edgeColor, 906 bitmap.getWidth() - 3, bitmap.getHeight() / 2); 907 } 908 failBitmap(Bitmap bitmap, String message)909 private void failBitmap(Bitmap bitmap, String message) { 910 BitmapDumper.dumpBitmap(bitmap, mTestName.getMethodName(), "TextureViewTest"); 911 Assert.fail(message); 912 } 913 assertBitmapColor(String debug, Bitmap bitmap, int color, int x, int y)914 private void assertBitmapColor(String debug, Bitmap bitmap, int color, int x, int y) { 915 int pixel = bitmap.getPixel(x, y); 916 if (!pixelsAreSame(color, pixel, 10)) { 917 failBitmap(bitmap, debug + "; expected=" + Integer.toHexString(color) + ", actual=" 918 + Integer.toHexString(pixel)); 919 } 920 } 921 assertBitmapNotColor(String debug, Bitmap bitmap, int color, int x, int y)922 private void assertBitmapNotColor(String debug, Bitmap bitmap, int color, int x, int y) { 923 int pixel = bitmap.getPixel(x, y); 924 if (pixelsAreSame(color, pixel, 10)) { 925 failBitmap(bitmap, debug + "; actual=" + Integer.toHexString(pixel) 926 + " shouldn't have matched " + Integer.toHexString(color)); 927 } 928 } 929 } 930