Effect: Avoid allocating bitmaps and textures in Overlays.

In `TextOverlay` and `DrawableOverlay`, treat `Bitmap` as a buffer, where we
allocate it rarely and reuse it as long as possible before making a new one.

In `BitmapOverlay`, avoid allocating GL textures too often as well.

Strongly reduces allocations and memory usage growth (saving ~100-150 MB on 4k60fps
at high end), at the cost of more code complexity and low-end using 70MB more, on
1/1 comparisons.

PiperOrigin-RevId: 585990602
This commit is contained in:
huangdarwin 2023-11-28 08:41:46 -08:00 committed by Copybara-Service
parent eb01c3f440
commit e5ef31b277
4 changed files with 76 additions and 24 deletions

View File

@ -571,20 +571,15 @@ public final class GlUtil {
} }
/** /**
* Allocates a new texture, initialized with the {@link Bitmap bitmap} data. * Allocates a new texture, initialized with the {@link Bitmap bitmap} data and size.
*
* <p>The created texture will have the same size as the specified {@link Bitmap}.
* *
* @param bitmap The {@link Bitmap} for which the texture is created. * @param bitmap The {@link Bitmap} for which the texture is created.
* @return The texture identifier for the newly-allocated texture. * @return The texture identifier for the newly-allocated texture.
* @throws GlException If the texture allocation fails. * @throws GlException If the texture allocation fails.
*/ */
public static int createTexture(Bitmap bitmap) throws GlException { public static int createTexture(Bitmap bitmap) throws GlException {
assertValidTextureSize(bitmap.getWidth(), bitmap.getHeight());
int texId = generateTexture(); int texId = generateTexture();
bindTexture(GLES20.GL_TEXTURE_2D, texId); setTexture(texId, bitmap);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, bitmap, /* border= */ 0);
checkGlError();
return texId; return texId;
} }
@ -642,14 +637,22 @@ public final class GlUtil {
return texId; return texId;
} }
/** Returns a new GL texture identifier. */ /** Returns a new, unbound GL texture identifier. */
private static int generateTexture() throws GlException { public static int generateTexture() throws GlException {
int[] texId = new int[1]; int[] texId = new int[1];
GLES20.glGenTextures(/* n= */ 1, texId, /* offset= */ 0); GLES20.glGenTextures(/* n= */ 1, texId, /* offset= */ 0);
checkGlError(); checkGlError();
return texId[0]; return texId[0];
} }
/** Sets the {@code texId} to contain the {@link Bitmap bitmap} data and size. */
public static void setTexture(int texId, Bitmap bitmap) throws GlException {
assertValidTextureSize(bitmap.getWidth(), bitmap.getHeight());
bindTexture(GLES20.GL_TEXTURE_2D, texId);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, bitmap, /* border= */ 0);
checkGlError();
}
/** /**
* Binds the texture of the given type with default configuration of GL_LINEAR filtering and * Binds the texture of the given type with default configuration of GL_LINEAR filtering and
* GL_CLAMP_TO_EDGE wrapping. * GL_CLAMP_TO_EDGE wrapping.

View File

@ -44,6 +44,7 @@ public abstract class BitmapOverlay extends TextureOverlay {
private final float[] flipVerticallyMatrix; private final float[] flipVerticallyMatrix;
private int lastTextureId; private int lastTextureId;
private boolean hasUpdatedBitmapReference;
private @Nullable Bitmap lastBitmap; private @Nullable Bitmap lastBitmap;
/* package */ BitmapOverlay() { /* package */ BitmapOverlay() {
@ -74,16 +75,27 @@ public abstract class BitmapOverlay extends TextureOverlay {
return new Size(checkNotNull(lastBitmap).getWidth(), checkNotNull(lastBitmap).getHeight()); return new Size(checkNotNull(lastBitmap).getWidth(), checkNotNull(lastBitmap).getHeight());
} }
/**
* Returns whether the cached bitmap overlay should be updated using the latest {@linkplain
* #getBitmap bitmap}.
*/
protected boolean shouldInvalidateCache() {
// Bitmap#sameAs() is documented as a slow method. Therefore, only use a reference comparison by
// default, instead of the deeper comparison done in sameAs.
return hasUpdatedBitmapReference;
}
@Override @Override
public int getTextureId(long presentationTimeUs) throws VideoFrameProcessingException { public int getTextureId(long presentationTimeUs) throws VideoFrameProcessingException {
Bitmap bitmap = getBitmap(presentationTimeUs); Bitmap bitmap = getBitmap(presentationTimeUs);
if (bitmap != lastBitmap) { hasUpdatedBitmapReference = bitmap != lastBitmap;
try { if (shouldInvalidateCache()) {
lastBitmap = bitmap; lastBitmap = bitmap;
if (lastTextureId != C.INDEX_UNSET) { try {
GlUtil.deleteTexture(lastTextureId); if (lastTextureId == C.INDEX_UNSET) {
lastTextureId = GlUtil.generateTexture();
} }
lastTextureId = GlUtil.createTexture(checkNotNull(lastBitmap)); GlUtil.setTexture(lastTextureId, bitmap);
} catch (GlUtil.GlException e) { } catch (GlUtil.GlException e) {
throw new VideoFrameProcessingException(e); throw new VideoFrameProcessingException(e);
} }

View File

@ -19,6 +19,8 @@ import static androidx.media3.common.util.Assertions.checkNotNull;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -31,6 +33,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*/ */
@UnstableApi @UnstableApi
public abstract class DrawableOverlay extends BitmapOverlay { public abstract class DrawableOverlay extends BitmapOverlay {
private boolean hasUpdatedDrawable;
private @MonotonicNonNull Bitmap lastBitmap; private @MonotonicNonNull Bitmap lastBitmap;
private @MonotonicNonNull Drawable lastDrawable; private @MonotonicNonNull Drawable lastDrawable;
@ -44,19 +47,34 @@ public abstract class DrawableOverlay extends BitmapOverlay {
*/ */
public abstract Drawable getDrawable(long presentationTimeUs); public abstract Drawable getDrawable(long presentationTimeUs);
/**
* Returns whether the cached drawable overlay should be updated using the latest {@linkplain
* #getDrawable drawable}
*/
@Override
protected final boolean shouldInvalidateCache() {
return hasUpdatedDrawable;
}
@Override @Override
public Bitmap getBitmap(long presentationTimeUs) { public Bitmap getBitmap(long presentationTimeUs) {
Drawable overlayDrawable = getDrawable(presentationTimeUs); Drawable overlayDrawable = getDrawable(presentationTimeUs);
// TODO(b/227625365): Drawable doesn't implement the equals method, so investigate other methods // TODO(b/227625365): Drawable doesn't implement the equals method, so investigate other methods
// of detecting the need to redraw the bitmap. // of detecting the need to redraw the bitmap.
if (!overlayDrawable.equals(lastDrawable)) { hasUpdatedDrawable = !overlayDrawable.equals(lastDrawable);
if (shouldInvalidateCache()) {
lastDrawable = overlayDrawable; lastDrawable = overlayDrawable;
if (lastBitmap == null
|| lastBitmap.getWidth() != lastDrawable.getIntrinsicWidth()
|| lastBitmap.getHeight() != lastDrawable.getIntrinsicHeight()) {
lastBitmap = lastBitmap =
Bitmap.createBitmap( Bitmap.createBitmap(
lastDrawable.getIntrinsicWidth(), lastDrawable.getIntrinsicWidth(),
lastDrawable.getIntrinsicHeight(), lastDrawable.getIntrinsicHeight(),
Bitmap.Config.ARGB_8888); Bitmap.Config.ARGB_8888);
}
Canvas canvas = new Canvas(lastBitmap); Canvas canvas = new Canvas(lastBitmap);
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
lastDrawable.draw(canvas); lastDrawable.draw(canvas);
} }
return checkNotNull(lastBitmap); return checkNotNull(lastBitmap);

View File

@ -22,6 +22,8 @@ import static java.lang.Math.ceil;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.text.Layout; import android.text.Layout;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.StaticLayout; import android.text.StaticLayout;
@ -78,6 +80,8 @@ public abstract class TextOverlay extends BitmapOverlay {
public static final int TEXT_SIZE_PIXELS = 100; public static final int TEXT_SIZE_PIXELS = 100;
private boolean hasUpdatedText;
private @MonotonicNonNull Bitmap lastBitmap; private @MonotonicNonNull Bitmap lastBitmap;
private @MonotonicNonNull SpannableString lastText; private @MonotonicNonNull SpannableString lastText;
@ -88,19 +92,34 @@ public abstract class TextOverlay extends BitmapOverlay {
*/ */
public abstract SpannableString getText(long presentationTimeUs); public abstract SpannableString getText(long presentationTimeUs);
/**
* Returns whether the cached text overlay should be updated using the latest {@linkplain #getText
* text}
*/
@Override
protected final boolean shouldInvalidateCache() {
return hasUpdatedText;
}
@Override @Override
public Bitmap getBitmap(long presentationTimeUs) { public Bitmap getBitmap(long presentationTimeUs) {
SpannableString overlayText = getText(presentationTimeUs); SpannableString overlayText = getText(presentationTimeUs);
if (!overlayText.equals(lastText)) { hasUpdatedText = !overlayText.equals(lastText);
if (shouldInvalidateCache()) {
lastText = overlayText; lastText = overlayText;
TextPaint textPaint = new TextPaint(); TextPaint textPaint = new TextPaint();
textPaint.setTextSize(TEXT_SIZE_PIXELS); textPaint.setTextSize(TEXT_SIZE_PIXELS);
StaticLayout staticLayout = StaticLayout staticLayout =
createStaticLayout(overlayText, textPaint, getSpannedTextWidth(overlayText, textPaint)); createStaticLayout(overlayText, textPaint, getSpannedTextWidth(overlayText, textPaint));
if (lastBitmap == null
|| lastBitmap.getWidth() != staticLayout.getWidth()
|| lastBitmap.getHeight() != staticLayout.getHeight()) {
lastBitmap = lastBitmap =
Bitmap.createBitmap( Bitmap.createBitmap(
staticLayout.getWidth(), staticLayout.getHeight(), Bitmap.Config.ARGB_8888); staticLayout.getWidth(), staticLayout.getHeight(), Bitmap.Config.ARGB_8888);
}
Canvas canvas = new Canvas(checkNotNull(lastBitmap)); Canvas canvas = new Canvas(checkNotNull(lastBitmap));
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
staticLayout.draw(canvas); staticLayout.draw(canvas);
} }
return checkNotNull(lastBitmap); return checkNotNull(lastBitmap);