|
5 | 5 | package io.flutter.embedding.engine.renderer; |
6 | 6 |
|
7 | 7 | import android.annotation.TargetApi; |
| 8 | +import android.content.ComponentCallbacks2; |
8 | 9 | import android.graphics.Bitmap; |
9 | 10 | import android.graphics.ImageFormat; |
10 | 11 | import android.graphics.Rect; |
@@ -361,17 +362,47 @@ public void run() { |
361 | 362 | final class ImageReaderSurfaceProducer |
362 | 363 | implements TextureRegistry.SurfaceProducer, TextureRegistry.ImageConsumer { |
363 | 364 | private static final String TAG = "ImageReaderSurfaceProducer"; |
364 | | - private static final int MAX_IMAGES = 4; |
| 365 | + private static final int MAX_IMAGES = 5; |
| 366 | + |
| 367 | + // Flip when debugging to see verbose logs. |
| 368 | + private static final boolean VERBOSE_LOGS = false; |
| 369 | + |
| 370 | + // We must always cleanup on memory pressure on Android 14 due to a bug in Android. |
| 371 | + // It is safe to do on all versions so we unconditionally have this set to true. |
| 372 | + private static final boolean CLEANUP_ON_MEMORY_PRESSURE = true; |
365 | 373 |
|
366 | 374 | private final long id; |
367 | 375 |
|
368 | 376 | private boolean released; |
369 | 377 | private boolean ignoringFence = false; |
370 | 378 |
|
371 | | - private int requestedWidth = 0; |
372 | | - private int requestedHeight = 0; |
373 | | - |
374 | 379 | /** Internal class: state held per image produced by image readers. */ |
| 380 | + private boolean trimOnMemoryPressure = CLEANUP_ON_MEMORY_PRESSURE; |
| 381 | + |
| 382 | + // The requested width and height are updated by setSize. |
| 383 | + private int requestedWidth = 1; |
| 384 | + private int requestedHeight = 1; |
| 385 | + // Whenever the requested width and height change we set this to be true so we |
| 386 | + // create a new ImageReader (inside getSurface) with the correct width and height. |
| 387 | + // We use this flag so that we lazily create the ImageReader only when a frame |
| 388 | + // will be produced at that size. |
| 389 | + private boolean createNewReader = true; |
| 390 | + |
| 391 | + // State held to track latency of various stages. |
| 392 | + private long lastDequeueTime = 0; |
| 393 | + private long lastQueueTime = 0; |
| 394 | + private long lastScheduleTime = 0; |
| 395 | + private int numTrims = 0; |
| 396 | + |
| 397 | + private Object lock = new Object(); |
| 398 | + // REQUIRED: The following fields must only be accessed when lock is held. |
| 399 | + private final ArrayDeque<PerImageReader> imageReaderQueue = new ArrayDeque<PerImageReader>(); |
| 400 | + private final HashMap<ImageReader, PerImageReader> perImageReaders = |
| 401 | + new HashMap<ImageReader, PerImageReader>(); |
| 402 | + private PerImage lastDequeuedImage = null; |
| 403 | + private PerImageReader lastReaderDequeuedFrom = null; |
| 404 | + |
| 405 | + /** Internal class: state held per Image produced by ImageReaders. */ |
375 | 406 | private class PerImage { |
376 | 407 | public final ImageReader reader; |
377 | 408 | public final Image image; |
@@ -414,9 +445,184 @@ public void onImageAvailable(ImageReader reader) { |
414 | 445 | return; |
415 | 446 | } |
416 | 447 | onImage(new PerImage(reader, image)); |
417 | | - } |
| 448 | + }; |
418 | 449 | }; |
419 | 450 |
|
| 451 | + PerImage dequeueImage() { |
| 452 | + if (imageQueue.size() == 0) { |
| 453 | + return null; |
| 454 | + } |
| 455 | + PerImage r = imageQueue.removeFirst(); |
| 456 | + return r; |
| 457 | + } |
| 458 | + |
| 459 | + /** returns true if we can prune this reader */ |
| 460 | + boolean canPrune() { |
| 461 | + return imageQueue.size() == 0 && lastReaderDequeuedFrom != this; |
| 462 | + } |
| 463 | + |
| 464 | + void close() { |
| 465 | + closed = true; |
| 466 | + if (VERBOSE_LOGS) { |
| 467 | + Log.i(TAG, "Closing reader=" + reader.hashCode()); |
| 468 | + } |
| 469 | + reader.close(); |
| 470 | + imageQueue.clear(); |
| 471 | + } |
| 472 | + |
| 473 | + double deltaMillis(long deltaNanos) { |
| 474 | + double ms = (double) deltaNanos / (double) 1000000.0; |
| 475 | + return ms; |
| 476 | + } |
| 477 | + |
| 478 | + PerImageReader getOrCreatePerImageReader(ImageReader reader) { |
| 479 | + PerImageReader r = perImageReaders.get(reader); |
| 480 | + if (r == null) { |
| 481 | + r = new PerImageReader(reader); |
| 482 | + perImageReaders.put(reader, r); |
| 483 | + imageReaderQueue.add(r); |
| 484 | + if (VERBOSE_LOGS) { |
| 485 | + Log.i(TAG, "imageReaderQueue#=" + imageReaderQueue.size()); |
| 486 | + } |
| 487 | + } |
| 488 | + return r; |
| 489 | + } |
| 490 | + |
| 491 | + void pruneImageReaderQueue() { |
| 492 | + boolean change = false; |
| 493 | + // Prune nodes from the head of the ImageReader queue. |
| 494 | + while (imageReaderQueue.size() > 1) { |
| 495 | + PerImageReader r = imageReaderQueue.peekFirst(); |
| 496 | + if (!r.canPrune()) { |
| 497 | + // No more ImageReaders can be pruned this round. |
| 498 | + break; |
| 499 | + } |
| 500 | + imageReaderQueue.removeFirst(); |
| 501 | + perImageReaders.remove(r.reader); |
| 502 | + r.close(); |
| 503 | + change = true; |
| 504 | + } |
| 505 | + if (change && VERBOSE_LOGS) { |
| 506 | + Log.i(TAG, "Pruned image reader queue length=" + imageReaderQueue.size()); |
| 507 | + } |
| 508 | + } |
| 509 | + |
| 510 | + void onImage(ImageReader reader, Image image) { |
| 511 | + PerImage queuedImage = null; |
| 512 | + synchronized (lock) { |
| 513 | + PerImageReader perReader = getOrCreatePerImageReader(reader); |
| 514 | + queuedImage = perReader.queueImage(image); |
| 515 | + } |
| 516 | + if (queuedImage == null) { |
| 517 | + // We got a late image. |
| 518 | + return; |
| 519 | + } |
| 520 | + if (VERBOSE_LOGS) { |
| 521 | + if (lastQueueTime != 0) { |
| 522 | + long now = System.nanoTime(); |
| 523 | + long queueDelta = now - lastQueueTime; |
| 524 | + Log.i( |
| 525 | + TAG, |
| 526 | + "" |
| 527 | + + reader.hashCode() |
| 528 | + + " enqueued image=" |
| 529 | + + queuedImage.image.hashCode() |
| 530 | + + " queueDelta=" |
| 531 | + + deltaMillis(queueDelta)); |
| 532 | + lastQueueTime = now; |
| 533 | + } else { |
| 534 | + lastQueueTime = System.nanoTime(); |
| 535 | + } |
| 536 | + } |
| 537 | + scheduleEngineFrame(); |
| 538 | + } |
| 539 | + |
| 540 | + public PerImageReader(ImageReader reader) { |
| 541 | + this.reader = reader; |
| 542 | + reader.setOnImageAvailableListener( |
| 543 | + onImageAvailableListener, new Handler(Looper.getMainLooper())); |
| 544 | + } |
| 545 | + |
| 546 | + PerImage queueImage(Image image) { |
| 547 | + if (closed) { |
| 548 | + return null; |
| 549 | + } |
| 550 | + PerImage perImage = new PerImage(image, System.nanoTime()); |
| 551 | + imageQueue.add(perImage); |
| 552 | + // If we fall too far behind we will skip some frames. |
| 553 | + while (imageQueue.size() > 2) { |
| 554 | + PerImage r = imageQueue.removeFirst(); |
| 555 | + if (VERBOSE_LOGS) { |
| 556 | + Log.i(TAG, "" + reader.hashCode() + " force closed image=" + r.image.hashCode()); |
| 557 | + } |
| 558 | + r.image.close(); |
| 559 | + } |
| 560 | + return perImage; |
| 561 | + } |
| 562 | + |
| 563 | + @Override |
| 564 | + public void onTrimMemory(int level) { |
| 565 | + if (!trimOnMemoryPressure) { |
| 566 | + return; |
| 567 | + } |
| 568 | + if (level < ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { |
| 569 | + return; |
| 570 | + } |
| 571 | + synchronized (lock) { |
| 572 | + numTrims++; |
| 573 | + } |
| 574 | + cleanup(); |
| 575 | + createNewReader = true; |
| 576 | + } |
| 577 | + |
| 578 | + private void cleanup() { |
| 579 | + synchronized (lock) { |
| 580 | + for (PerImageReader pir : perImageReaders.values()) { |
| 581 | + if (lastReaderDequeuedFrom == pir) { |
| 582 | + lastReaderDequeuedFrom = null; |
| 583 | + } |
| 584 | + pir.close(); |
| 585 | + } |
| 586 | + perImageReaders.clear(); |
| 587 | + if (lastDequeuedImage != null) { |
| 588 | + lastDequeuedImage.image.close(); |
| 589 | + lastDequeuedImage = null; |
| 590 | + } |
| 591 | + if (lastReaderDequeuedFrom != null) { |
| 592 | + lastReaderDequeuedFrom.close(); |
| 593 | + lastReaderDequeuedFrom = null; |
| 594 | + } |
| 595 | + imageReaderQueue.clear(); |
| 596 | + } |
| 597 | + } |
| 598 | + |
| 599 | + @TargetApi(API_LEVELS.API_33) |
| 600 | + private void waitOnFence(Image image) { |
| 601 | + try { |
| 602 | + SyncFence fence = image.getFence(); |
| 603 | + fence.awaitForever(); |
| 604 | + } catch (IOException e) { |
| 605 | + // Drop. |
| 606 | + } |
| 607 | + } |
| 608 | + |
| 609 | + private void maybeWaitOnFence(Image image) { |
| 610 | + if (image == null) { |
| 611 | + return; |
| 612 | + } |
| 613 | + if (ignoringFence) { |
| 614 | + return; |
| 615 | + } |
| 616 | + if (Build.VERSION.SDK_INT >= API_LEVELS.API_33) { |
| 617 | + // The fence API is only available on Android >= 33. |
| 618 | + waitOnFence(image); |
| 619 | + return; |
| 620 | + } |
| 621 | + // Log once per ImageTextureEntry. |
| 622 | + ignoringFence = true; |
| 623 | + Log.w(TAG, "ImageTextureEntry can't wait on the fence on Android < 33"); |
| 624 | + } |
| 625 | + |
420 | 626 | ImageReaderSurfaceProducer(long id) { |
421 | 627 | this.id = id; |
422 | 628 | } |
@@ -662,8 +868,28 @@ public void disableFenceForTest() { |
662 | 868 | } |
663 | 869 |
|
664 | 870 | @VisibleForTesting |
665 | | - public int readersToCloseSize() { |
666 | | - return readersToClose.size(); |
| 871 | + public int numImageReaders() { |
| 872 | + synchronized (lock) { |
| 873 | + return imageReaderQueue.size(); |
| 874 | + } |
| 875 | + } |
| 876 | + |
| 877 | + @VisibleForTesting |
| 878 | + public int numTrims() { |
| 879 | + synchronized (lock) { |
| 880 | + return numTrims; |
| 881 | + } |
| 882 | + } |
| 883 | + |
| 884 | + @VisibleForTesting |
| 885 | + public int numImages() { |
| 886 | + int r = 0; |
| 887 | + synchronized (lock) { |
| 888 | + for (PerImageReader reader : imageReaderQueue) { |
| 889 | + r += reader.imageQueue.size(); |
| 890 | + } |
| 891 | + } |
| 892 | + return r; |
667 | 893 | } |
668 | 894 | } |
669 | 895 |
|
|
0 commit comments