Skip to content

Commit 7dc51b8

Browse files
Add a new external texture type to Android embedder (flutter#48803)
The `SurfaceProducer` interface exposes only exposes a Surface to render on, abstracting away the consumer side of the external texture. The `ImageReaderSurfaceProducer` implementation of this interface is included as well as a basic test of that code. Also, a small refactor so that `ImageTexture` and `ImageReaderSurfaceProducer` can use the same native C++ code in the engine. Subsequent CLs will need to address the following: - A SurfaceTextureSurfaceProducer (your eyes are probably bleeding from that name) implementation is needed so we can support GL based systems. - Update Platform Views to use this new SurfaceProducer type instead of the legacy types. - Deprecate SurfaceTexture and ImageTexture external texture types. Related issue [flutter#139702](flutter#139702)
1 parent 03c5f01 commit 7dc51b8

File tree

12 files changed

+469
-41
lines changed

12 files changed

+469
-41
lines changed

shell/platform/android/android_shell_holder_unittests.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class MockPlatformViewAndroidJNI : public PlatformViewAndroidJNI {
4848
(JavaLocalRef surface_texture),
4949
(override));
5050
MOCK_METHOD(JavaLocalRef,
51-
ImageTextureEntryAcquireLatestImage,
51+
ImageProducerTextureEntryAcquireLatestImage,
5252
(JavaLocalRef image_texture_entry),
5353
(override));
5454
MOCK_METHOD(JavaLocalRef,

shell/platform/android/image_external_texture.cc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,9 @@ JavaLocalRef ImageExternalTexture::AcquireLatestImage() {
7373
FML_CHECK(env != nullptr);
7474

7575
// ImageTextureEntry.acquireLatestImage.
76-
JavaLocalRef image_java = jni_facade_->ImageTextureEntryAcquireLatestImage(
77-
JavaLocalRef(image_texture_entry_));
76+
JavaLocalRef image_java =
77+
jni_facade_->ImageProducerTextureEntryAcquireLatestImage(
78+
JavaLocalRef(image_texture_entry_));
7879
return image_java;
7980
}
8081

shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -921,19 +921,19 @@ private native void nativeRegisterTexture(
921921
*/
922922
@UiThread
923923
public void registerImageTexture(
924-
long textureId, @NonNull TextureRegistry.ImageTextureEntry imageTextureEntry) {
924+
long textureId, @NonNull TextureRegistry.ImageConsumer imageTexture) {
925925
ensureRunningOnMainThread();
926926
ensureAttachedToNative();
927927
nativeRegisterImageTexture(
928928
nativeShellHolderId,
929929
textureId,
930-
new WeakReference<TextureRegistry.ImageTextureEntry>(imageTextureEntry));
930+
new WeakReference<TextureRegistry.ImageConsumer>(imageTexture));
931931
}
932932

933933
private native void nativeRegisterImageTexture(
934934
long nativeShellHolderId,
935935
long textureId,
936-
@NonNull WeakReference<TextureRegistry.ImageTextureEntry> imageTextureEntry);
936+
@NonNull WeakReference<TextureRegistry.ImageConsumer> imageTexture);
937937

938938
/**
939939
* Call this method to inform Flutter that a texture previously registered with {@link

shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java

Lines changed: 298 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66

77
import android.annotation.TargetApi;
88
import android.graphics.Bitmap;
9+
import android.graphics.ImageFormat;
910
import android.graphics.Rect;
1011
import android.graphics.SurfaceTexture;
12+
import android.hardware.HardwareBuffer;
1113
import android.hardware.SyncFence;
1214
import android.media.Image;
15+
import android.media.ImageReader;
1316
import android.os.Build;
1417
import android.os.Handler;
1518
import android.view.Surface;
@@ -151,6 +154,22 @@ private void clearDeadListeners() {
151154
}
152155

153156
// ------ START TextureRegistry IMPLEMENTATION -----
157+
158+
/**
159+
* Creates and returns a new external texture {@link SurfaceProducer} managed by the Flutter
160+
* engine that is also made available to Flutter code.
161+
*/
162+
@Override
163+
public SurfaceProducer createSurfaceProducer() {
164+
// TODO(matanl, johnmccutchan): Implement a SurfaceTexture version and switch on whether or
165+
// not impeller is enabled.
166+
final ImageReaderSurfaceProducer entry =
167+
new ImageReaderSurfaceProducer(nextTextureId.getAndIncrement());
168+
Log.v(TAG, "New SurfaceProducer ID: " + entry.id());
169+
registerImageTexture(isRenderingToImageViewCount, (TextureRegistry.ImageConsumer) entry);
170+
return entry;
171+
}
172+
154173
/**
155174
* Creates and returns a new {@link SurfaceTexture} managed by the Flutter engine that is also
156175
* made available to Flutter code.
@@ -182,7 +201,7 @@ public ImageTextureEntry createImageTexture() {
182201
final ImageTextureRegistryEntry entry =
183202
new ImageTextureRegistryEntry(nextTextureId.getAndIncrement());
184203
Log.v(TAG, "New ImageTextureEntry ID: " + entry.id());
185-
registerImageTexture(entry.id(), entry);
204+
registerImageTexture(entry.id(), (TextureRegistry.ImageConsumer) entry);
186205
return entry;
187206
}
188207

@@ -338,7 +357,277 @@ public void run() {
338357
}
339358

340359
@Keep
341-
final class ImageTextureRegistryEntry implements TextureRegistry.ImageTextureEntry {
360+
@TargetApi(29)
361+
final class ImageReaderSurfaceProducer
362+
implements TextureRegistry.SurfaceProducer, TextureRegistry.ImageConsumer {
363+
private static final String TAG = "ImageReaderSurfaceProducer";
364+
private static final int MAX_IMAGES = 4;
365+
366+
private final long id;
367+
368+
private boolean released;
369+
private boolean ignoringFence = false;
370+
371+
private int requestedWidth = 0;
372+
private int requestedHeight = 0;
373+
374+
private ImageReader activeReader;
375+
private ImageReader toBeClosedReader;
376+
private Image latestImage;
377+
378+
private final Handler onImageAvailableHandler = new Handler();
379+
private final ImageReader.OnImageAvailableListener onImageAvailableListener =
380+
new ImageReader.OnImageAvailableListener() {
381+
@Override
382+
public void onImageAvailable(ImageReader reader) {
383+
Image image = null;
384+
try {
385+
image = reader.acquireLatestImage();
386+
} catch (IllegalStateException e) {
387+
Log.e(TAG, "onImageAvailable acquireLatestImage failed: " + e.toString());
388+
}
389+
if (image == null) {
390+
return;
391+
}
392+
onImage(image);
393+
maybeCloseReader();
394+
}
395+
};
396+
397+
ImageReaderSurfaceProducer(long id) {
398+
this.id = id;
399+
}
400+
401+
@Override
402+
public long id() {
403+
return id;
404+
}
405+
406+
@Override
407+
public void release() {
408+
if (released) {
409+
return;
410+
}
411+
released = true;
412+
if (this.latestImage != null) {
413+
this.latestImage.close();
414+
this.latestImage = null;
415+
}
416+
if (this.toBeClosedReader != null) {
417+
this.toBeClosedReader.close();
418+
this.toBeClosedReader = null;
419+
}
420+
if (this.activeReader != null) {
421+
this.activeReader.close();
422+
this.activeReader = null;
423+
}
424+
unregisterTexture(id);
425+
}
426+
427+
@Override
428+
public void setSize(int width, int height) {
429+
if (requestedWidth == width && requestedHeight == height) {
430+
// No size change.
431+
return;
432+
}
433+
this.requestedHeight = height;
434+
this.requestedWidth = width;
435+
// Because the size was changed we will need to close the currently active reader.
436+
// Instead of closing it eagerly we wait until the a frame is produced at the new
437+
// size, ensuring that we don't render a blank frame in the app.
438+
maybeMarkReaderForClose();
439+
}
440+
441+
@Override
442+
public int getWidth() {
443+
return this.requestedWidth;
444+
}
445+
446+
@Override
447+
public int getHeight() {
448+
return this.requestedHeight;
449+
}
450+
451+
@Override
452+
public Surface getSurface() {
453+
maybeCreateReader();
454+
return activeReader.getSurface();
455+
}
456+
457+
@Override
458+
@TargetApi(29)
459+
public Image acquireLatestImage() {
460+
Image r;
461+
synchronized (this) {
462+
r = this.latestImage;
463+
this.latestImage = null;
464+
}
465+
maybeWaitOnFence(r);
466+
return r;
467+
}
468+
469+
private void maybeMarkReaderForClose() {
470+
synchronized (this) {
471+
if (this.toBeClosedReader != null) {
472+
// We only ever have two readers:
473+
// 1) The reader to be closed after the next image is produced.
474+
// 2) The reader being used to produce images.
475+
return;
476+
}
477+
this.toBeClosedReader = this.activeReader;
478+
this.activeReader = null;
479+
}
480+
}
481+
482+
private void maybeCloseReader() {
483+
if (this.toBeClosedReader == null) {
484+
return;
485+
}
486+
this.toBeClosedReader.close();
487+
this.toBeClosedReader = null;
488+
}
489+
490+
private void maybeCreateReader() {
491+
if (this.activeReader != null) {
492+
return;
493+
}
494+
this.activeReader = createImageReader();
495+
}
496+
497+
/** Invoked for each method that is available. */
498+
private void onImage(Image image) {
499+
if (released) {
500+
return;
501+
}
502+
Image toClose;
503+
synchronized (this) {
504+
toClose = this.latestImage;
505+
this.latestImage = image;
506+
}
507+
// Close the previously pushed buffer.
508+
if (toClose != null) {
509+
Log.e(TAG, "RawSurfaceTexture frame was not acquired in a timely manner.");
510+
toClose.close();
511+
}
512+
if (image != null) {
513+
// Mark that we have a new frame available. Eventually the raster thread will
514+
// call acquireLatestImage.
515+
markTextureFrameAvailable(id);
516+
}
517+
}
518+
519+
@TargetApi(33)
520+
private void waitOnFence(Image image) {
521+
try {
522+
SyncFence fence = image.getFence();
523+
boolean signaled = fence.awaitForever();
524+
if (!signaled) {
525+
Log.e(TAG, "acquireLatestImage image's fence was never signalled.");
526+
}
527+
} catch (IOException e) {
528+
// Drop.
529+
}
530+
}
531+
532+
private void maybeWaitOnFence(Image image) {
533+
if (image == null) {
534+
return;
535+
}
536+
if (ignoringFence) {
537+
return;
538+
}
539+
if (Build.VERSION.SDK_INT >= 33) {
540+
// The fence API is only available on Android >= 33.
541+
waitOnFence(image);
542+
return;
543+
}
544+
if (!ignoringFence) {
545+
// Log once per ImageTextureEntry.
546+
ignoringFence = true;
547+
Log.w(TAG, "ImageTextureEntry can't wait on the fence on Android < 33");
548+
}
549+
}
550+
551+
@Override
552+
protected void finalize() throws Throwable {
553+
try {
554+
if (released) {
555+
return;
556+
}
557+
if (latestImage != null) {
558+
// Be sure to finalize any cached image.
559+
latestImage.close();
560+
latestImage = null;
561+
}
562+
if (this.toBeClosedReader != null) {
563+
this.toBeClosedReader.close();
564+
}
565+
if (this.activeReader != null) {
566+
this.activeReader.close();
567+
}
568+
released = true;
569+
handler.post(new TextureFinalizerRunnable(id, flutterJNI));
570+
} finally {
571+
super.finalize();
572+
}
573+
}
574+
575+
@TargetApi(33)
576+
private ImageReader createImageReader33() {
577+
final ImageReader.Builder builder = new ImageReader.Builder(requestedWidth, requestedHeight);
578+
// Allow for double buffering.
579+
builder.setMaxImages(MAX_IMAGES);
580+
// Use PRIVATE image format so that we can support video decoding.
581+
// TODO(johnmccutchan): Should we always use PRIVATE here? It may impact our
582+
// ability to read back texture data. If we don't always want to use it, how do
583+
// we
584+
// decide when to use it or not? Perhaps PlatformViews can indicate if they may
585+
// contain
586+
// DRM'd content.
587+
// I need to investigate how PRIVATE impacts our ability to take screenshots or
588+
// capture
589+
// the output of Flutter application.
590+
builder.setImageFormat(ImageFormat.PRIVATE);
591+
// Hint that consumed images will only be read by GPU.
592+
builder.setUsage(HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
593+
final ImageReader reader = builder.build();
594+
reader.setOnImageAvailableListener(this.onImageAvailableListener, onImageAvailableHandler);
595+
return reader;
596+
}
597+
598+
@TargetApi(29)
599+
private ImageReader createImageReader29() {
600+
final ImageReader reader =
601+
ImageReader.newInstance(
602+
requestedWidth,
603+
requestedHeight,
604+
ImageFormat.PRIVATE,
605+
MAX_IMAGES,
606+
HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
607+
reader.setOnImageAvailableListener(this.onImageAvailableListener, onImageAvailableHandler);
608+
return reader;
609+
}
610+
611+
private ImageReader createImageReader() {
612+
if (Build.VERSION.SDK_INT >= 33) {
613+
return createImageReader33();
614+
} else if (Build.VERSION.SDK_INT >= 29) {
615+
return createImageReader29();
616+
}
617+
throw new UnsupportedOperationException(
618+
"ImageReaderPlatformViewRenderTarget requires API version 29+");
619+
}
620+
621+
@VisibleForTesting
622+
public void disableFenceForTest() {
623+
// Roboelectric's implementation of SyncFence is borked.
624+
ignoringFence = true;
625+
}
626+
}
627+
628+
@Keep
629+
final class ImageTextureRegistryEntry
630+
implements TextureRegistry.ImageTextureEntry, TextureRegistry.ImageConsumer {
342631
private static final String TAG = "ImageTextureRegistryEntry";
343632
private final long id;
344633
private boolean released;
@@ -408,6 +697,9 @@ private void maybeWaitOnFence(Image image) {
408697
if (image == null) {
409698
return;
410699
}
700+
if (ignoringFence) {
701+
return;
702+
}
411703
if (Build.VERSION.SDK_INT >= 33) {
412704
// The fence API is only available on Android >= 33.
413705
waitOnFence(image);
@@ -472,7 +764,8 @@ protected void finalize() throws Throwable {
472764
public void startRenderingToSurface(@NonNull Surface surface, boolean onlySwap) {
473765
if (!onlySwap) {
474766
// Stop rendering to the surface releases the associated native resources, which
475-
// causes a glitch when toggling between rendering to an image view (hybrid composition) and
767+
// causes a glitch when toggling between rendering to an image view (hybrid
768+
// composition) and
476769
// rendering directly to a Surface or Texture view. For more,
477770
// https://github.com/flutter/flutter/issues/95343
478771
stopRenderingToSurface();
@@ -645,8 +938,8 @@ private void registerTexture(long textureId, @NonNull SurfaceTextureWrapper text
645938
}
646939

647940
private void registerImageTexture(
648-
long textureId, @NonNull TextureRegistry.ImageTextureEntry textureEntry) {
649-
flutterJNI.registerImageTexture(textureId, textureEntry);
941+
long textureId, @NonNull TextureRegistry.ImageConsumer imageTexture) {
942+
flutterJNI.registerImageTexture(textureId, imageTexture);
650943
}
651944

652945
// TODO(mattcarroll): describe the native behavior that this invokes

0 commit comments

Comments
 (0)