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:
parent
eb01c3f440
commit
e5ef31b277
@ -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.
|
||||||
|
@ -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;
|
||||||
|
if (shouldInvalidateCache()) {
|
||||||
|
lastBitmap = bitmap;
|
||||||
try {
|
try {
|
||||||
lastBitmap = bitmap;
|
if (lastTextureId == C.INDEX_UNSET) {
|
||||||
if (lastTextureId != C.INDEX_UNSET) {
|
lastTextureId = GlUtil.generateTexture();
|
||||||
GlUtil.deleteTexture(lastTextureId);
|
|
||||||
}
|
}
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
lastBitmap =
|
if (lastBitmap == null
|
||||||
Bitmap.createBitmap(
|
|| lastBitmap.getWidth() != lastDrawable.getIntrinsicWidth()
|
||||||
lastDrawable.getIntrinsicWidth(),
|
|| lastBitmap.getHeight() != lastDrawable.getIntrinsicHeight()) {
|
||||||
lastDrawable.getIntrinsicHeight(),
|
lastBitmap =
|
||||||
Bitmap.Config.ARGB_8888);
|
Bitmap.createBitmap(
|
||||||
|
lastDrawable.getIntrinsicWidth(),
|
||||||
|
lastDrawable.getIntrinsicHeight(),
|
||||||
|
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);
|
||||||
|
@ -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));
|
||||||
lastBitmap =
|
if (lastBitmap == null
|
||||||
Bitmap.createBitmap(
|
|| lastBitmap.getWidth() != staticLayout.getWidth()
|
||||||
staticLayout.getWidth(), staticLayout.getHeight(), Bitmap.Config.ARGB_8888);
|
|| lastBitmap.getHeight() != staticLayout.getHeight()) {
|
||||||
|
lastBitmap =
|
||||||
|
Bitmap.createBitmap(
|
||||||
|
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);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user