1 /* <lambda>null2 * Copyright 2023 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 androidx.graphics 18 19 import android.graphics.Bitmap 20 import android.graphics.BlendMode 21 import android.graphics.Canvas 22 import android.graphics.Color 23 import android.graphics.ColorSpace 24 import android.graphics.Matrix 25 import android.graphics.Outline 26 import android.graphics.Paint 27 import android.graphics.PixelFormat 28 import android.graphics.Rect 29 import android.graphics.RectF 30 import android.graphics.RenderNode 31 import android.hardware.HardwareBuffer 32 import android.os.Build 33 import android.os.Environment.DIRECTORY_PICTURES 34 import android.util.Half 35 import androidx.annotation.RequiresApi 36 import androidx.graphics.CanvasBufferedRenderer.RenderResult.Companion.SUCCESS 37 import androidx.graphics.CanvasBufferedRendererTests.TestHelper.Companion.hardwareBufferRendererTest 38 import androidx.graphics.CanvasBufferedRendererTests.TestHelper.Companion.record 39 import androidx.graphics.surface.SurfaceControlCompat 40 import androidx.hardware.DefaultFlags 41 import androidx.test.ext.junit.runners.AndroidJUnit4 42 import androidx.test.filters.LargeTest 43 import androidx.test.filters.SdkSuppress 44 import androidx.test.filters.SmallTest 45 import androidx.test.platform.app.InstrumentationRegistry 46 import java.io.File 47 import java.io.FileOutputStream 48 import java.io.IOException 49 import java.nio.ByteBuffer 50 import java.nio.ByteOrder 51 import java.util.concurrent.CountDownLatch 52 import java.util.concurrent.Executor 53 import java.util.concurrent.Executors 54 import java.util.concurrent.TimeUnit 55 import kotlin.math.abs 56 import kotlinx.coroutines.runBlocking 57 import org.junit.Assert.assertEquals 58 import org.junit.Assert.assertFalse 59 import org.junit.Assert.assertNotNull 60 import org.junit.Assert.assertThrows 61 import org.junit.Assert.assertTrue 62 import org.junit.Test 63 import org.junit.runner.RunWith 64 65 @RunWith(AndroidJUnit4::class) 66 @SmallTest 67 class CanvasBufferedRendererTests { 68 69 private val mExecutor = Executors.newSingleThreadExecutor() 70 71 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 72 @Test 73 fun testRenderAfterCloseReturnsError() = hardwareBufferRendererTest { renderer -> 74 renderer.close() 75 assertThrows(IllegalStateException::class.java) { 76 renderer.obtainRenderRequest().drawAsync(mExecutor) { _ -> /* NO-OP */ } 77 } 78 } 79 80 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 81 @Test 82 fun testIsClosed() = hardwareBufferRendererTest { renderer -> 83 assertFalse(renderer.isClosed) 84 renderer.close() 85 assertTrue(renderer.isClosed) 86 } 87 88 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 89 @Test 90 fun testMultipleClosesDoesNotCrash() = hardwareBufferRendererTest { renderer -> 91 renderer.close() 92 renderer.close() 93 renderer.close() 94 } 95 96 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 97 @Test 98 fun testPreservationDisabledClearsContents() = hardwareBufferRendererTest { renderer -> 99 val node = 100 RenderNode("content").apply { 101 setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT) 102 record { canvas -> canvas.drawColor(Color.BLUE) } 103 } 104 105 renderer.setContentRoot(node) 106 var latch = CountDownLatch(1) 107 var bitmap: Bitmap? = null 108 renderer.obtainRenderRequest().preserveContents(true).drawAsync(mExecutor) { result -> 109 assertEquals(SUCCESS, result.status) 110 result.fence?.awaitForever() 111 bitmap = Bitmap.wrapHardwareBuffer(result.hardwareBuffer, null) 112 latch.countDown() 113 } 114 115 assertTrue(latch.await(3000, TimeUnit.MILLISECONDS)) 116 assertNotNull(bitmap) 117 assertTrue(bitmap!!.copy(Bitmap.Config.ARGB_8888, false).isAllColor(Color.BLUE)) 118 119 node.record { canvas -> canvas.drawColor(Color.RED, BlendMode.DST_OVER) } 120 121 latch = CountDownLatch(1) 122 renderer.obtainRenderRequest().preserveContents(false).drawAsync(mExecutor) { result -> 123 assertEquals(SUCCESS, result.status) 124 result.fence?.awaitForever() 125 bitmap = Bitmap.wrapHardwareBuffer(result.hardwareBuffer, null) 126 latch.countDown() 127 } 128 129 assertTrue(latch.await(3000, TimeUnit.MILLISECONDS)) 130 131 assertNotNull(bitmap) 132 assertTrue(bitmap!!.copy(Bitmap.Config.ARGB_8888, false).isAllColor(Color.RED)) 133 } 134 135 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 136 @Test 137 fun testPreservationEnabledPreservesContents() = 138 repeat(20) { verifyPreservedBuffer(CanvasBufferedRenderer.DEFAULT_IMPL) } 139 140 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 141 @Test 142 fun testPreservationEnabledPreservesContentsWithRedrawStrategy() = 143 repeat(20) { verifyPreservedBuffer(CanvasBufferedRenderer.USE_V29_IMPL_WITH_REDRAW) } 144 145 @RequiresApi(Build.VERSION_CODES.Q) 146 private fun verifyPreservedBuffer( 147 impl: Int, 148 width: Int = TEST_WIDTH, 149 height: Int = TEST_HEIGHT 150 ) { 151 val bitmap = bufferPreservationTestHelper(impl, width, height, mExecutor) 152 assertNotNull(bitmap) 153 assertTrue(bitmap!!.copy(Bitmap.Config.ARGB_8888, false).isAllColor(Color.BLUE)) 154 } 155 156 /** Helper test method to save test bitmaps to disk to verify output for debugging purposes */ 157 private fun saveBitmap(bitmap: Bitmap, name: String) { 158 val filename = 159 InstrumentationRegistry.getInstrumentation() 160 .context 161 .getExternalFilesDir(DIRECTORY_PICTURES) 162 val testFile = File(filename!!.path + "/" + name) 163 try { 164 FileOutputStream(testFile).use { out -> 165 bitmap.compress(Bitmap.CompressFormat.PNG, 100, out) 166 } 167 } catch (e: IOException) { 168 e.printStackTrace() 169 } 170 } 171 172 @RequiresApi(Build.VERSION_CODES.Q) 173 private fun bufferPreservationTestHelper( 174 impl: Int, 175 width: Int, 176 height: Int, 177 executor: Executor 178 ): Bitmap? { 179 val hardwareBufferRenderer = 180 CanvasBufferedRenderer.Builder(width, height).setMaxBuffers(1).setImpl(impl).build() 181 182 hardwareBufferRenderer.use { renderer -> 183 val node = 184 RenderNode("content").apply { 185 setPosition(0, 0, width, height) 186 record { canvas -> 187 canvas.drawColor(Color.BLACK, BlendMode.CLEAR) 188 canvas.drawColor(Color.BLUE) 189 } 190 } 191 192 renderer.setContentRoot(node) 193 val firstRenderLatch = CountDownLatch(1) 194 var bitmap: Bitmap? = null 195 renderer.obtainRenderRequest().preserveContents(true).drawAsync(executor) { result -> 196 assertEquals(SUCCESS, result.status) 197 result.fence?.awaitForever() 198 bitmap = Bitmap.wrapHardwareBuffer(result.hardwareBuffer, null) 199 firstRenderLatch.countDown() 200 } 201 202 assertTrue(firstRenderLatch.await(3000, TimeUnit.MILLISECONDS)) 203 204 node.record { canvas -> canvas.drawColor(Color.RED, BlendMode.DST_OVER) } 205 206 val secondRenderLatch = CountDownLatch(1) 207 renderer.obtainRenderRequest().preserveContents(true).drawAsync(executor) { result -> 208 assertEquals(SUCCESS, result.status) 209 result.fence?.awaitForever() 210 bitmap = Bitmap.wrapHardwareBuffer(result.hardwareBuffer, null) 211 secondRenderLatch.countDown() 212 } 213 214 assertTrue(secondRenderLatch.await(3000, TimeUnit.MILLISECONDS)) 215 216 return bitmap 217 } 218 } 219 220 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 221 @Test 222 fun testHardwareBufferRender() = hardwareBufferRendererTest { renderer -> 223 val contentRoot = 224 RenderNode("content").apply { 225 setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT) 226 record { canvas -> canvas.drawColor(Color.BLUE) } 227 } 228 renderer.setContentRoot(contentRoot) 229 230 val colorSpace = ColorSpace.get(ColorSpace.Named.SRGB) 231 val latch = CountDownLatch(1) 232 var hardwareBuffer: HardwareBuffer? = null 233 renderer.obtainRenderRequest().setColorSpace(colorSpace).drawAsync(mExecutor) { renderResult 234 -> 235 renderResult.fence?.awaitForever() 236 hardwareBuffer = renderResult.hardwareBuffer 237 latch.countDown() 238 } 239 240 assertTrue(latch.await(3000, TimeUnit.MILLISECONDS)) 241 242 val bitmap = 243 Bitmap.wrapHardwareBuffer(hardwareBuffer!!, colorSpace)!!.copy( 244 Bitmap.Config.ARGB_8888, 245 false 246 ) 247 248 assertEquals(TEST_WIDTH, bitmap.width) 249 assertEquals(TEST_HEIGHT, bitmap.height) 250 assertEquals(0xFF0000FF.toInt(), bitmap.getPixel(0, 0)) 251 } 252 253 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 254 @Test 255 fun testDrawSync() = hardwareBufferRendererTest { renderer -> 256 val contentRoot = 257 RenderNode("content").apply { 258 setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT) 259 record { canvas -> canvas.drawColor(Color.BLUE) } 260 } 261 renderer.setContentRoot(contentRoot) 262 263 val colorSpace = ColorSpace.get(ColorSpace.Named.SRGB) 264 265 var renderResult: CanvasBufferedRenderer.RenderResult? 266 runBlocking { 267 renderResult = renderer.obtainRenderRequest().setColorSpace(colorSpace).draw() 268 } 269 assertNotNull(renderResult) 270 assertEquals(SUCCESS, renderResult!!.status) 271 val fence = renderResult?.fence 272 if (fence != null) { 273 // by default drawSync will automatically wait on the fence and close it leaving 274 // it in the invalid state 275 assertFalse(fence.isValid()) 276 } 277 278 val hardwareBuffer = renderResult!!.hardwareBuffer 279 280 val bitmap = 281 Bitmap.wrapHardwareBuffer(hardwareBuffer, colorSpace)!!.copy( 282 Bitmap.Config.ARGB_8888, 283 false 284 ) 285 286 assertEquals(TEST_WIDTH, bitmap.width) 287 assertEquals(TEST_HEIGHT, bitmap.height) 288 assertEquals(0xFF0000FF.toInt(), bitmap.getPixel(0, 0)) 289 } 290 291 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 292 @Test 293 fun testDrawSyncWithoutBlockingFence() = hardwareBufferRendererTest { renderer -> 294 val contentRoot = 295 RenderNode("content").apply { 296 setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT) 297 record { canvas -> canvas.drawColor(Color.BLUE) } 298 } 299 renderer.setContentRoot(contentRoot) 300 301 val colorSpace = ColorSpace.get(ColorSpace.Named.SRGB) 302 303 var renderResult: CanvasBufferedRenderer.RenderResult? 304 runBlocking { 305 renderResult = renderer.obtainRenderRequest().setColorSpace(colorSpace).draw(false) 306 } 307 assertNotNull(renderResult) 308 assertEquals(SUCCESS, renderResult!!.status) 309 renderResult?.fence?.let { fence -> 310 fence.awaitForever() 311 fence.close() 312 } 313 314 val hardwareBuffer = renderResult!!.hardwareBuffer 315 316 val bitmap = 317 Bitmap.wrapHardwareBuffer(hardwareBuffer, colorSpace)!!.copy( 318 Bitmap.Config.ARGB_8888, 319 false 320 ) 321 322 assertEquals(TEST_WIDTH, bitmap.width) 323 assertEquals(TEST_HEIGHT, bitmap.height) 324 assertEquals(0xFF0000FF.toInt(), bitmap.getPixel(0, 0)) 325 } 326 327 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 328 @Test 329 fun testContentsPreservedSRGB() = preservedContentsTest { bitmap -> 330 assertEquals(Color.RED, bitmap.getPixel(TEST_WIDTH / 2, TEST_HEIGHT / 4)) 331 assertEquals(Color.BLUE, bitmap.getPixel(TEST_WIDTH / 2, TEST_HEIGHT / 2 + TEST_HEIGHT / 4)) 332 } 333 334 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 335 @Test 336 fun testContentsPreservedF16() = 337 preservedContentsTest( 338 format = PixelFormat.RGBA_F16, 339 bitmapConfig = Bitmap.Config.RGBA_F16 340 ) { bitmap -> 341 val buffer = 342 ByteBuffer.allocateDirect(bitmap.allocationByteCount).apply { 343 bitmap.copyPixelsToBuffer(this) 344 rewind() 345 order(ByteOrder.LITTLE_ENDIAN) 346 } 347 val srcColorSpace = ColorSpace.get(ColorSpace.Named.SRGB) 348 val srcToDst = ColorSpace.connect(srcColorSpace, ColorSpace.get(ColorSpace.Named.SRGB)) 349 350 val expectedRed = srcToDst.transform(1.0f, 0.0f, 0.0f) 351 val expectedBlue = srcToDst.transform(0.0f, 0.0f, 1.0f) 352 353 TestHelper.assertEqualsRgba16f( 354 "TopMiddle", 355 bitmap, 356 TEST_WIDTH / 2, 357 TEST_HEIGHT / 4, 358 buffer, 359 expectedRed[0], 360 expectedRed[1], 361 expectedRed[2], 362 1.0f 363 ) 364 TestHelper.assertEqualsRgba16f( 365 "BottomMiddle", 366 bitmap, 367 TEST_WIDTH / 2, 368 TEST_HEIGHT / 2 + TEST_HEIGHT / 4, 369 buffer, 370 expectedBlue[0], 371 expectedBlue[1], 372 expectedBlue[2], 373 1.0f 374 ) 375 } 376 377 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 378 @Test 379 fun testContentsPreserved1010102() = 380 preservedContentsTest( 381 format = PixelFormat.RGBA_1010102, 382 bitmapConfig = Bitmap.Config.RGBA_1010102 383 ) { bitmap -> 384 assertEquals(Color.RED, bitmap.getPixel(TEST_WIDTH / 2, TEST_HEIGHT / 4)) 385 assertEquals( 386 Color.BLUE, 387 bitmap.getPixel(TEST_WIDTH / 2, TEST_HEIGHT / 2 + TEST_HEIGHT / 4) 388 ) 389 } 390 391 @RequiresApi(Build.VERSION_CODES.Q) 392 private fun preservedContentsTest( 393 format: Int = PixelFormat.RGBA_8888, 394 bitmapConfig: Bitmap.Config = Bitmap.Config.ARGB_8888, 395 block: (Bitmap) -> Unit 396 ) = 397 hardwareBufferRendererTest(format = format) { renderer -> 398 val contentRoot = 399 RenderNode("content").apply { 400 setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT) 401 record { canvas -> canvas.drawColor(Color.BLUE) } 402 } 403 renderer.setContentRoot(contentRoot) 404 val colorSpace = ColorSpace.get(ColorSpace.Named.SRGB) 405 val latch = CountDownLatch(1) 406 var hardwareBuffer: HardwareBuffer? 407 renderer 408 .obtainRenderRequest() 409 .setColorSpace(colorSpace) 410 .preserveContents(true) 411 .drawAsync(mExecutor) { renderResult -> 412 renderResult.fence?.awaitForever() 413 hardwareBuffer = renderResult.hardwareBuffer 414 latch.countDown() 415 } 416 417 assertTrue(latch.await(3000, TimeUnit.MILLISECONDS)) 418 419 val latch2 = CountDownLatch(1) 420 contentRoot.record { canvas -> 421 val paint = Paint().apply { color = Color.RED } 422 canvas.drawRect(0f, 0f, TEST_WIDTH.toFloat(), TEST_HEIGHT / 2f, paint) 423 } 424 renderer.setContentRoot(contentRoot) 425 426 hardwareBuffer = null 427 renderer 428 .obtainRenderRequest() 429 .setColorSpace(colorSpace) 430 .preserveContents(true) 431 .drawAsync(mExecutor) { renderResult -> 432 renderResult.fence?.awaitForever() 433 hardwareBuffer = renderResult.hardwareBuffer 434 latch2.countDown() 435 } 436 437 assertTrue(latch2.await(3000, TimeUnit.MILLISECONDS)) 438 439 val bitmap = 440 Bitmap.wrapHardwareBuffer(hardwareBuffer!!, colorSpace)!!.copy(bitmapConfig, false) 441 442 assertEquals(TEST_WIDTH, bitmap.width) 443 assertEquals(TEST_HEIGHT, bitmap.height) 444 block(bitmap) 445 } 446 447 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 448 @Test 449 fun testTransformRotate0TallWide() = 450 TestHelper.quadTest( 451 mExecutor, 452 width = TEST_WIDTH * 2, 453 height = TEST_HEIGHT, 454 transform = SurfaceControlCompat.BUFFER_TRANSFORM_IDENTITY 455 ) { bitmap -> 456 TestHelper.assertBitmapQuadColors( 457 bitmap, 458 topLeft = Color.RED, 459 topRight = Color.BLUE, 460 bottomRight = Color.YELLOW, 461 bottomLeft = Color.GREEN 462 ) 463 } 464 465 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 466 @Test 467 fun testTransformRotate0TallRect() = 468 TestHelper.quadTest( 469 mExecutor, 470 width = TEST_WIDTH, 471 height = TEST_HEIGHT * 2, 472 transform = SurfaceControlCompat.BUFFER_TRANSFORM_IDENTITY 473 ) { bitmap -> 474 TestHelper.assertBitmapQuadColors( 475 bitmap, 476 topLeft = Color.RED, 477 topRight = Color.BLUE, 478 bottomRight = Color.YELLOW, 479 bottomLeft = Color.GREEN 480 ) 481 } 482 483 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 484 @Test 485 fun testTransformRotate90WideRect() = 486 TestHelper.quadTest( 487 mExecutor, 488 width = TEST_WIDTH * 2, 489 height = TEST_HEIGHT, 490 transform = SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_90 491 ) { bitmap -> 492 TestHelper.assertBitmapQuadColors( 493 bitmap, 494 topLeft = Color.GREEN, 495 topRight = Color.RED, 496 bottomRight = Color.BLUE, 497 bottomLeft = Color.YELLOW 498 ) 499 } 500 501 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 502 @Test 503 fun testTransformRotate90TallRect() = 504 TestHelper.quadTest( 505 mExecutor, 506 width = TEST_WIDTH, 507 height = TEST_HEIGHT * 2, 508 transform = SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_90 509 ) { bitmap -> 510 TestHelper.assertBitmapQuadColors( 511 bitmap, 512 topLeft = Color.GREEN, 513 topRight = Color.RED, 514 bottomLeft = Color.YELLOW, 515 bottomRight = Color.BLUE 516 ) 517 } 518 519 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 520 @Test 521 fun testTransformRotate180WideRect() = 522 TestHelper.quadTest( 523 mExecutor, 524 width = TEST_WIDTH * 2, 525 height = TEST_HEIGHT, 526 transform = SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_180 527 ) { bitmap -> 528 TestHelper.assertBitmapQuadColors( 529 bitmap, 530 topLeft = Color.YELLOW, 531 topRight = Color.GREEN, 532 bottomLeft = Color.BLUE, 533 bottomRight = Color.RED 534 ) 535 } 536 537 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 538 @Test 539 fun testTransformRotate180TallRect() = 540 TestHelper.quadTest( 541 mExecutor, 542 width = TEST_WIDTH, 543 height = TEST_HEIGHT * 2, 544 transform = SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_180 545 ) { bitmap -> 546 TestHelper.assertBitmapQuadColors( 547 bitmap, 548 topLeft = Color.YELLOW, 549 topRight = Color.GREEN, 550 bottomLeft = Color.BLUE, 551 bottomRight = Color.RED 552 ) 553 } 554 555 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 556 @Test 557 fun testTransformRotate270WideRect() = 558 TestHelper.quadTest( 559 mExecutor, 560 width = TEST_WIDTH * 2, 561 height = TEST_HEIGHT, 562 transform = SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_270 563 ) { bitmap -> 564 TestHelper.assertBitmapQuadColors( 565 bitmap, 566 topLeft = Color.BLUE, 567 topRight = Color.YELLOW, 568 bottomRight = Color.GREEN, 569 bottomLeft = Color.RED 570 ) 571 } 572 573 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 574 @Test 575 fun testTransformRotate270TallRect() = 576 TestHelper.quadTest( 577 mExecutor, 578 width = TEST_WIDTH, 579 height = TEST_HEIGHT * 2, 580 transform = SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_270 581 ) { bitmap -> 582 TestHelper.assertBitmapQuadColors( 583 bitmap, 584 topLeft = Color.BLUE, 585 topRight = Color.YELLOW, 586 bottomRight = Color.GREEN, 587 bottomLeft = Color.RED 588 ) 589 } 590 591 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 592 @Test 593 fun testUnknownTransformThrows() = hardwareBufferRendererTest { renderer -> 594 val root = 595 RenderNode("content").apply { 596 setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT) 597 record { canvas -> 598 with(canvas) { 599 drawColor(Color.BLUE) 600 val paint = Paint().apply { color = Color.RED } 601 canvas.drawRect(0f, 0f, TEST_WIDTH / 2f, TEST_HEIGHT / 2f, paint) 602 } 603 } 604 } 605 renderer.setContentRoot(root) 606 607 val colorSpace = ColorSpace.get(ColorSpace.Named.SRGB) 608 val latch = CountDownLatch(1) 609 610 assertThrows(IllegalArgumentException::class.java) { 611 renderer 612 .obtainRenderRequest() 613 .setColorSpace(colorSpace) 614 .setBufferTransform(42) 615 .drawAsync(mExecutor) { renderResult -> 616 renderResult.fence?.awaitForever() 617 latch.countDown() 618 } 619 } 620 } 621 622 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 623 @Test 624 fun testColorSpaceDisplayP3() = 625 TestHelper.colorSpaceTest(mExecutor, ColorSpace.get(ColorSpace.Named.DISPLAY_P3)) 626 627 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 628 @Test 629 fun testColorSpaceProPhotoRGB() = 630 TestHelper.colorSpaceTest(mExecutor, ColorSpace.get(ColorSpace.Named.PRO_PHOTO_RGB)) 631 632 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 633 @Test 634 fun testColorSpaceAdobeRGB() = 635 TestHelper.colorSpaceTest(mExecutor, ColorSpace.get(ColorSpace.Named.ADOBE_RGB)) 636 637 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 638 @Test 639 fun testColorSpaceDciP3() = 640 TestHelper.colorSpaceTest(mExecutor, ColorSpace.get(ColorSpace.Named.DCI_P3)) 641 642 @RequiresApi(Build.VERSION_CODES.Q) 643 private fun spotShadowTest( 644 transform: Int = SurfaceControlCompat.BUFFER_TRANSFORM_IDENTITY, 645 ) = hardwareBufferRendererTest { renderer -> 646 val content = RenderNode("content") 647 val colorSpace = ColorSpace.get(ColorSpace.Named.SRGB) 648 renderer.apply { 649 setLightSourceAlpha(0.0f, 1.0f) 650 setLightSourceGeometry(TEST_WIDTH / 2f, 0f, 800.0f, 20.0f) 651 setContentRoot(content) 652 } 653 val childRect = Rect(25, 25, 65, 65) 654 content.setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT) 655 with(TestHelper.Companion) { 656 content.record { parentCanvas -> 657 val childNode = RenderNode("shadowCaster") 658 childNode.setPosition(childRect) 659 val outline = Outline() 660 outline.setRect(Rect(0, 0, childRect.width(), childRect.height())) 661 outline.alpha = 1f 662 childNode.setOutline(outline) 663 val childCanvas = childNode.beginRecording() 664 childCanvas.drawColor(Color.RED) 665 childNode.endRecording() 666 childNode.elevation = 20f 667 668 parentCanvas.drawColor(Color.WHITE) 669 parentCanvas.enableZ() 670 parentCanvas.drawRenderNode(childNode) 671 parentCanvas.disableZ() 672 } 673 } 674 675 val latch = CountDownLatch(1) 676 var renderStatus = CanvasBufferedRenderer.RenderResult.ERROR_UNKNOWN 677 var hardwareBuffer: HardwareBuffer? = null 678 679 renderer 680 .obtainRenderRequest() 681 .setColorSpace(colorSpace) 682 .setBufferTransform(transform) 683 .drawAsync(mExecutor) { renderResult -> 684 renderStatus = renderResult.status 685 renderResult.fence?.awaitForever() 686 hardwareBuffer = renderResult.hardwareBuffer 687 latch.countDown() 688 } 689 690 assertTrue(latch.await(3000, TimeUnit.MILLISECONDS)) 691 assertEquals(renderStatus, SUCCESS) 692 val bitmap = 693 Bitmap.wrapHardwareBuffer(hardwareBuffer!!, colorSpace)!!.copy( 694 Bitmap.Config.ARGB_8888, 695 false 696 ) 697 698 val rect = Rect(childRect.left, childRect.bottom, childRect.right, childRect.bottom + 10) 699 700 var result = 701 bitmap.verify(rect) { actual, _, _ -> verifyPixelWithThreshold(actual, Color.RED, 10) } 702 result = 703 result || 704 bitmap.verify(rect.applyBufferTransform(bitmap.width, bitmap.height, transform)) { 705 actual, 706 _, 707 _ -> 708 verifyPixelGrayScale(actual, 1) 709 } 710 711 assertTrue(result) 712 } 713 714 private fun Bitmap.verify(rect: Rect, block: (Int, Int, Int) -> Boolean): Boolean { 715 for (i in rect.left until rect.right) { 716 for (j in rect.top until rect.bottom) { 717 if (!block(getPixel(i, j), i, j)) { 718 return false 719 } 720 } 721 } 722 return true 723 } 724 725 /** @return True if close enough */ 726 private fun verifyPixelWithThreshold(color: Int, expectedColor: Int, threshold: Int): Boolean { 727 val diff = 728 (abs((Color.red(color) - Color.red(expectedColor)).toDouble()) + 729 abs((Color.green(color) - Color.green(expectedColor)).toDouble()) + 730 abs((Color.blue(color) - Color.blue(expectedColor)).toDouble())) 731 .toInt() 732 return diff <= threshold 733 } 734 735 /** 736 * @param threshold Per channel differences for R / G / B channel against the average of these 3 737 * channels. Should be less than 2 normally. 738 * @return True if the color is close enough to be a gray scale color. 739 */ 740 private fun verifyPixelGrayScale(color: Int, threshold: Int): Boolean { 741 var average = Color.red(color) + Color.green(color) + Color.blue(color) 742 average /= 3 743 return abs((Color.red(color) - average).toDouble()) <= threshold && 744 abs((Color.green(color) - average).toDouble()) <= threshold && 745 abs((Color.blue(color) - average).toDouble()) <= threshold 746 } 747 748 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 749 @Test 750 fun testSpotShadowSetup() = spotShadowTest() 751 752 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 753 @Test 754 fun testSpotShadowRotate90() = spotShadowTest(SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_90) 755 756 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 757 @Test 758 fun testSpotShadowRotate180() = spotShadowTest(SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_180) 759 760 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 761 @Test 762 fun testSpotShadowRotate270() = spotShadowTest(SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_270) 763 764 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 765 @Test 766 fun testRendererBlocksOnBufferRelease() { 767 val renderNode = 768 RenderNode("node").apply { 769 setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT) 770 val canvas = beginRecording() 771 canvas.drawColor(Color.RED) 772 endRecording() 773 } 774 val renderer = 775 CanvasBufferedRenderer.Builder(TEST_WIDTH, TEST_HEIGHT).setMaxBuffers(2).build().apply { 776 setContentRoot(renderNode) 777 } 778 779 val executor = Executors.newSingleThreadExecutor() 780 try { 781 val latch1 = CountDownLatch(1) 782 val latch2 = CountDownLatch(1) 783 val latch3 = CountDownLatch(1) 784 var hardwareBuffer: HardwareBuffer? = null 785 renderer.obtainRenderRequest().drawAsync(executor) { result -> 786 result.fence?.awaitForever() 787 result.fence?.close() 788 hardwareBuffer = result.hardwareBuffer 789 latch1.countDown() 790 } 791 792 assertTrue(latch1.await(1000, TimeUnit.MILLISECONDS)) 793 794 var canvas = renderNode.beginRecording() 795 canvas.drawColor(Color.BLUE) 796 renderNode.endRecording() 797 798 renderer.obtainRenderRequest().drawAsync(executor) { _ -> latch2.countDown() } 799 800 assertTrue(latch2.await(1000, TimeUnit.MILLISECONDS)) 801 802 canvas = renderNode.beginRecording() 803 canvas.drawColor(Color.GREEN) 804 renderNode.endRecording() 805 806 renderer.obtainRenderRequest().drawAsync(executor) { _ -> latch3.countDown() } 807 808 // The 3rd render request should be blocked until the buffer is released 809 assertFalse(latch3.await(1000, TimeUnit.MILLISECONDS)) 810 assertNotNull(hardwareBuffer) 811 renderer.releaseBuffer(hardwareBuffer!!, null) 812 assertTrue(latch3.await(1000, TimeUnit.MILLISECONDS)) 813 } finally { 814 renderer.close() 815 executor.shutdownNow() 816 } 817 } 818 819 @RequiresApi(Build.VERSION_CODES.Q) 820 private fun Rect.applyBufferTransform(width: Int, height: Int, transform: Int): Rect { 821 val rectF = RectF(this) 822 val matrix = Matrix() 823 when (transform) { 824 SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_90 -> { 825 matrix.apply { 826 setRotate(90f) 827 postTranslate(width.toFloat(), 0f) 828 } 829 } 830 SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_180 -> { 831 matrix.apply { 832 setRotate(180f) 833 postTranslate(width.toFloat(), height.toFloat()) 834 } 835 } 836 SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_270 -> { 837 matrix.apply { 838 setRotate(270f) 839 postTranslate(0f, width.toFloat()) 840 } 841 } 842 SurfaceControlCompat.BUFFER_TRANSFORM_IDENTITY -> { 843 matrix.reset() 844 } 845 else -> throw IllegalArgumentException("Invalid transform value") 846 } 847 matrix.mapRect(rectF) 848 return Rect( 849 rectF.left.toInt(), 850 rectF.top.toInt(), 851 rectF.right.toInt(), 852 rectF.bottom.toInt() 853 ) 854 } 855 856 // See b/295332012 857 @SdkSuppress( 858 minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, 859 maxSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE 860 ) 861 @Test 862 fun testHardwareBufferRendererV34SharedFileDescriptorMonitoring() { 863 fun createHardwareBufferRenderer( 864 sharedFdMonitor: SharedFileDescriptorMonitor 865 ): CanvasBufferedRendererV34 { 866 return CanvasBufferedRendererV34( 867 TEST_WIDTH, 868 TEST_HEIGHT, 869 HardwareBuffer.RGBA_8888, 870 DefaultFlags, 871 1, 872 sharedFdMonitor 873 ) 874 } 875 876 val sharedFdMonitor = CanvasBufferedRendererV34.obtainSharedFdMonitor() 877 // Monitor is only returned on devices running the vulkan hwui backend 878 if (sharedFdMonitor != null) { 879 val hbr1 = createHardwareBufferRenderer(sharedFdMonitor) 880 val hbr2 = createHardwareBufferRenderer(sharedFdMonitor) 881 val hbr3 = createHardwareBufferRenderer(sharedFdMonitor) 882 883 hbr1.close() 884 885 assertTrue(sharedFdMonitor.isMonitoring) 886 887 hbr2.close() 888 889 assertTrue(sharedFdMonitor.isMonitoring) 890 891 hbr3.close() 892 893 assertFalse(sharedFdMonitor.isMonitoring) 894 895 val sharedFdMonitor2 = CanvasBufferedRendererV34.obtainSharedFdMonitor()!! 896 val hbr4 = createHardwareBufferRenderer(sharedFdMonitor2) 897 898 assertTrue(sharedFdMonitor2.isMonitoring) 899 900 hbr4.close() 901 902 assertFalse(sharedFdMonitor2.isMonitoring) 903 } 904 } 905 906 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 907 @LargeTest 908 @Test 909 fun testFdCleanupAfterSeveralRenders() { 910 val hbr = CanvasBufferedRenderer.Builder(TEST_WIDTH, TEST_HEIGHT).setMaxBuffers(1).build() 911 val renderNode = RenderNode("node").apply { setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT) } 912 hbr.setContentRoot(renderNode) 913 val executor = Executors.newSingleThreadExecutor() 914 try { 915 for (i in 0 until 100000) { 916 val canvas = renderNode.beginRecording() 917 canvas.drawColor(Color.RED) 918 renderNode.endRecording() 919 920 val latch = CountDownLatch(1) 921 hbr.obtainRenderRequest().drawAsync(executor) { result -> 922 hbr.releaseBuffer(result.hardwareBuffer, result.fence) 923 latch.countDown() 924 } 925 latch.await() 926 } 927 } finally { 928 executor.shutdownNow() 929 hbr.close() 930 } 931 } 932 933 companion object { 934 const val TEST_WIDTH = 90 935 const val TEST_HEIGHT = 90 936 } 937 938 /** 939 * Helper class to move test methods that include APIs introduced in newer class levels. This is 940 * done in order to avoid NoClassFoundExceptions being thrown when the test is loaded on lower 941 * API levels even if there are corresponding @SdkSuppress annotations used in conjunction with 942 * the corresponding API version code. 943 */ 944 internal class TestHelper { 945 companion object { 946 947 @RequiresApi(Build.VERSION_CODES.Q) 948 fun quadTest( 949 executor: Executor, 950 width: Int = TEST_WIDTH, 951 height: Int = TEST_HEIGHT, 952 transform: Int = SurfaceControlCompat.BUFFER_TRANSFORM_IDENTITY, 953 colorSpace: ColorSpace = ColorSpace.get(ColorSpace.Named.SRGB), 954 format: Int = PixelFormat.RGBA_8888, 955 bitmapConfig: Bitmap.Config = Bitmap.Config.ARGB_8888, 956 block: (Bitmap) -> Unit, 957 ) { 958 val bufferWidth: Int 959 val bufferHeight: Int 960 if ( 961 transform == SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_90 || 962 transform == SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_270 963 ) { 964 bufferWidth = height 965 bufferHeight = width 966 } else { 967 bufferWidth = width 968 bufferHeight = height 969 } 970 hardwareBufferRendererTest( 971 width = bufferWidth, 972 height = bufferHeight, 973 format = format 974 ) { renderer -> 975 val root = 976 RenderNode("content").apply { 977 setPosition(0, 0, width, height) 978 record { canvas -> 979 val widthF = width.toFloat() 980 val heightF = height.toFloat() 981 val paint = Paint().apply { color = Color.RED } 982 canvas.drawRect(0f, 0f, widthF / 2f, heightF / 2f, paint) 983 paint.color = Color.BLUE 984 canvas.drawRect(widthF / 2f, 0f, widthF, heightF / 2f, paint) 985 paint.color = Color.GREEN 986 canvas.drawRect(0f, heightF / 2f, widthF / 2f, heightF, paint) 987 paint.color = Color.YELLOW 988 canvas.drawRect(widthF / 2f, heightF / 2f, widthF, heightF, paint) 989 } 990 } 991 renderer.setContentRoot(root) 992 993 val latch = CountDownLatch(1) 994 var hardwareBuffer: HardwareBuffer? = null 995 renderer 996 .obtainRenderRequest() 997 .setColorSpace(colorSpace) 998 .preserveContents(true) 999 .setBufferTransform(transform) 1000 .drawAsync(executor) { renderResult -> 1001 renderResult.fence?.awaitForever() 1002 hardwareBuffer = renderResult.hardwareBuffer 1003 latch.countDown() 1004 } 1005 1006 assertTrue(latch.await(3000, TimeUnit.MILLISECONDS)) 1007 1008 val bitmap = 1009 Bitmap.wrapHardwareBuffer(hardwareBuffer!!, colorSpace)!!.copy( 1010 bitmapConfig, 1011 false 1012 ) 1013 1014 assertEquals(bufferWidth, bitmap.width) 1015 assertEquals(bufferHeight, bitmap.height) 1016 1017 block(bitmap) 1018 } 1019 } 1020 1021 @RequiresApi(Build.VERSION_CODES.Q) 1022 fun colorSpaceTest(executor: Executor, dstColorSpace: ColorSpace) = 1023 quadTest( 1024 executor, 1025 format = PixelFormat.RGBA_F16, 1026 colorSpace = dstColorSpace, 1027 bitmapConfig = Bitmap.Config.RGBA_F16 1028 ) { bitmap -> 1029 val buffer = 1030 ByteBuffer.allocateDirect(bitmap.allocationByteCount).apply { 1031 bitmap.copyPixelsToBuffer(this) 1032 rewind() 1033 order(ByteOrder.LITTLE_ENDIAN) 1034 } 1035 val srcColorSpace = ColorSpace.get(ColorSpace.Named.SRGB) 1036 val srcToDst = ColorSpace.connect(srcColorSpace, dstColorSpace) 1037 1038 val expectedRed = srcToDst.transform(1.0f, 0.0f, 0.0f) 1039 val expectedBlue = srcToDst.transform(0.0f, 0.0f, 1.0f) 1040 val expectedGreen = srcToDst.transform(0.0f, 1.0f, 0.0f) 1041 val expectedYellow = srcToDst.transform(1.0f, 1.0f, 0.0f) 1042 1043 assertEqualsRgba16f( 1044 "TopLeft", 1045 bitmap, 1046 TEST_WIDTH / 4, 1047 TEST_HEIGHT / 4, 1048 buffer, 1049 expectedRed[0], 1050 expectedRed[1], 1051 expectedRed[2], 1052 1.0f 1053 ) 1054 1055 assertEqualsRgba16f( 1056 "TopRight", 1057 bitmap, 1058 (TEST_WIDTH * 3f / 4f).toInt(), 1059 TEST_HEIGHT / 4, 1060 buffer, 1061 expectedBlue[0], 1062 expectedBlue[1], 1063 expectedBlue[2], 1064 1.0f 1065 ) 1066 1067 assertEqualsRgba16f( 1068 "BottomLeft", 1069 bitmap, 1070 TEST_WIDTH / 4, 1071 (TEST_HEIGHT * 3f / 4f).toInt(), 1072 buffer, 1073 expectedGreen[0], 1074 expectedGreen[1], 1075 expectedGreen[2], 1076 1.0f 1077 ) 1078 assertEqualsRgba16f( 1079 "BottomRight", 1080 bitmap, 1081 (TEST_WIDTH * 3f / 4f).toInt(), 1082 (TEST_HEIGHT * 3f / 4f).toInt(), 1083 buffer, 1084 expectedYellow[0], 1085 expectedYellow[1], 1086 expectedYellow[2], 1087 1.0f 1088 ) 1089 } 1090 1091 @RequiresApi(Build.VERSION_CODES.Q) 1092 fun hardwareBufferRendererTest( 1093 width: Int = TEST_WIDTH, 1094 height: Int = TEST_HEIGHT, 1095 format: Int = HardwareBuffer.RGBA_8888, 1096 impl: Int = CanvasBufferedRenderer.DEFAULT_IMPL, 1097 block: (renderer: CanvasBufferedRenderer) -> Unit, 1098 ) { 1099 val usage = 1100 HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE or HardwareBuffer.USAGE_GPU_COLOR_OUTPUT 1101 if ( 1102 format != HardwareBuffer.RGBA_8888 && 1103 !HardwareBuffer.isSupported(width, height, format, 1, usage) 1104 ) { 1105 // Early out if the hardware configuration is not supported. 1106 // PixelFormat.RGBA_8888 should always be supported 1107 return 1108 } 1109 val renderer = 1110 CanvasBufferedRenderer.Builder(width, height) 1111 .setMaxBuffers(1) 1112 .setBufferFormat(format) 1113 .setUsageFlags(usage) 1114 .setImpl(impl) 1115 .build() 1116 try { 1117 block(renderer) 1118 } finally { 1119 renderer.close() 1120 } 1121 } 1122 1123 @RequiresApi(Build.VERSION_CODES.Q) 1124 inline fun RenderNode.record(block: (canvas: Canvas) -> Unit): RenderNode { 1125 block(beginRecording()) 1126 endRecording() 1127 return this 1128 } 1129 1130 @RequiresApi(Build.VERSION_CODES.Q) 1131 fun assertEqualsRgba16f( 1132 message: String, 1133 bitmap: Bitmap, 1134 x: Int, 1135 y: Int, 1136 dst: ByteBuffer, 1137 r: Float, 1138 g: Float, 1139 b: Float, 1140 a: Float, 1141 ) { 1142 val index = y * bitmap.rowBytes + (x shl 3) 1143 val cR = dst.getShort(index) 1144 val cG = dst.getShort(index + 2) 1145 val cB = dst.getShort(index + 4) 1146 val cA = dst.getShort(index + 6) 1147 assertEquals(message, r, Half.toFloat(cR), 0.01f) 1148 assertEquals(message, g, Half.toFloat(cG), 0.01f) 1149 assertEquals(message, b, Half.toFloat(cB), 0.01f) 1150 assertEquals(message, a, Half.toFloat(cA), 0.01f) 1151 } 1152 1153 fun assertBitmapQuadColors( 1154 bitmap: Bitmap, 1155 topLeft: Int, 1156 topRight: Int, 1157 bottomLeft: Int, 1158 bottomRight: Int, 1159 ) { 1160 val width = bitmap.width 1161 val height = bitmap.height 1162 1163 val topLeftStartX = 0 1164 val topLeftEndX = width / 2 - 2 1165 val topLeftStartY = 0 1166 val topLeftEndY = height / 2 - 2 1167 1168 val topRightStartX = width / 2 + 2 1169 val topRightEndX = width - 1 1170 val topRightStartY = 0 1171 val topRightEndY = height / 2 - 2 1172 1173 val bottomRightStartX = width / 2 + 2 1174 val bottomRightEndX = width - 1 1175 val bottomRightStartY = height / 2 + 2 1176 val bottomRightEndY = height - 1 1177 1178 val bottomLeftStartX = 0 1179 val bottomLeftEndX = width / 2 - 2 1180 val bottomLeftStartY = height / 2 + 2 1181 val bottomLeftEndY = height - 1 1182 1183 assertEquals(topLeft, bitmap.getPixel(topLeftStartX, topLeftStartY)) 1184 assertEquals(topLeft, bitmap.getPixel(topLeftEndX, topLeftStartY)) 1185 assertEquals(topLeft, bitmap.getPixel(topLeftEndX, topLeftEndY)) 1186 assertEquals(topLeft, bitmap.getPixel(topLeftStartX, topLeftEndY)) 1187 1188 assertEquals(topRight, bitmap.getPixel(topRightStartX, topRightStartY)) 1189 assertEquals(topRight, bitmap.getPixel(topRightEndX, topRightStartY)) 1190 assertEquals(topRight, bitmap.getPixel(topRightEndX, topRightEndY)) 1191 assertEquals(topRight, bitmap.getPixel(topRightStartX, topRightEndY)) 1192 1193 assertEquals(bottomRight, bitmap.getPixel(bottomRightStartX, bottomRightStartY)) 1194 assertEquals(bottomRight, bitmap.getPixel(bottomRightEndX, bottomRightStartY)) 1195 assertEquals(bottomRight, bitmap.getPixel(bottomRightEndX, bottomRightEndY)) 1196 assertEquals(bottomRight, bitmap.getPixel(bottomRightStartX, bottomRightEndY)) 1197 1198 assertEquals(bottomLeft, bitmap.getPixel(bottomLeftStartX, bottomLeftStartY)) 1199 assertEquals(bottomLeft, bitmap.getPixel(bottomLeftEndX, bottomLeftStartY)) 1200 assertEquals(bottomLeft, bitmap.getPixel(bottomLeftEndX, bottomLeftEndY)) 1201 assertEquals(bottomLeft, bitmap.getPixel(bottomLeftStartX, bottomLeftEndY)) 1202 } 1203 } 1204 } 1205 } 1206