Add an option to use different texture filtering

Add an option to GlMatrixTransformation to choose the OpenGL texture
minification filter.
When mipmaps are requested, mipmaps are generated with
`glGenerateMipmap()`.

PiperOrigin-RevId: 720629807
This commit is contained in:
dancho 2025-01-28 10:41:45 -08:00 committed by Copybara-Service
parent bb9b3bd660
commit bb37aad170
7 changed files with 174 additions and 5 deletions

View File

@ -30,6 +30,7 @@ import android.media.MediaCodec;
import android.media.MediaCrypto;
import android.media.MediaFormat;
import android.net.Uri;
import android.opengl.GLES20;
import android.view.Surface;
import androidx.annotation.IntDef;
import androidx.media3.common.util.UnstableApi;
@ -1693,6 +1694,40 @@ public final class C {
/** The first frame was rendered. */
@UnstableApi public static final int FIRST_FRAME_RENDERED = 3;
/**
* Texture filtering algorithm for minification.
*
* <p>Possible values are:
*
* <ul>
* <li>{@link #TEXTURE_MIN_FILTER_LINEAR}
* <li>{@link #TEXTURE_MIN_FILTER_LINEAR_MIPMAP_LINEAR}
* </ul>
*
* <p>The algorithms are ordered by increasing visual quality and computational cost.
*/
@UnstableApi
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({TEXTURE_MIN_FILTER_LINEAR, TEXTURE_MIN_FILTER_LINEAR_MIPMAP_LINEAR})
public @interface TextureMinFilter {}
/**
* Returns the weighted average of the four texture elements that are closest to the specified
* texture coordinates.
*/
@UnstableApi public static final int TEXTURE_MIN_FILTER_LINEAR = GLES20.GL_LINEAR;
/**
* Chooses the two mipmaps that most closely match the size of the pixel being textured and uses
* the {@link C#TEXTURE_MIN_FILTER_LINEAR} criterion (a weighted average of the texture elements
* that are closest to the specified texture coordinates) to produce a texture value from each
* mipmap. The final texture value is a weighted average of those two values.
*/
@UnstableApi
public static final int TEXTURE_MIN_FILTER_LINEAR_MIPMAP_LINEAR = GLES20.GL_LINEAR_MIPMAP_LINEAR;
/**
* @deprecated Use {@link Util#usToMs(long)}.
*/

View File

@ -15,12 +15,14 @@
*/
package androidx.media3.common.util;
import static androidx.media3.common.C.TEXTURE_MIN_FILTER_LINEAR_MIPMAP_LINEAR;
import static androidx.media3.common.util.Assertions.checkNotNull;
import android.content.Context;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import java.io.IOException;
import java.nio.Buffer;
import java.util.HashMap;
@ -186,6 +188,22 @@ public final class GlProgram {
checkNotNull(uniformByName.get(name)).setSamplerTexId(texId, texUnitIndex);
}
/**
* Sets a texture sampler type uniform.
*
* @param name The uniform's name.
* @param texId The texture identifier.
* @param texUnitIndex The texture unit index. Use a different index (0, 1, 2, ...) for each
* texture sampler in the program.
* @param texMinFilter The {@link C.TextureMinFilter}.
*/
public void setSamplerTexIdUniform(
String name, int texId, int texUnitIndex, @C.TextureMinFilter int texMinFilter) {
Uniform texUniform = checkNotNull(uniformByName.get(name));
texUniform.setSamplerTexId(texId, texUnitIndex);
texUniform.setTexMinFilter(texMinFilter);
}
/** Sets an {@code int} type uniform. */
public void setIntUniform(String name, int value) {
checkNotNull(uniformByName.get(name)).setInt(value);
@ -365,6 +383,7 @@ public final class GlProgram {
private int texIdValue;
private int texUnitIndex;
private @C.TextureMinFilter int texMinFilter;
private Uniform(String name, int location, int type) {
this.name = name;
@ -372,6 +391,7 @@ public final class GlProgram {
this.type = type;
this.floatValue = new float[16]; // Allocate 16 for mat4
this.intValue = new int[4]; // Allocate 4 for ivec4
this.texMinFilter = C.TEXTURE_MIN_FILTER_LINEAR;
}
/**
@ -386,6 +406,19 @@ public final class GlProgram {
this.texUnitIndex = texUnitIndex;
}
/**
* Configures {@link #bind(boolean)} to use the specified texture minification filter for this
* sampler uniform.
*
* <p>Only has effect for {@linkplain GLES20#GL_SAMPLER_2D internal texture} type. External
* texture sampling is controlled via the parameter passed to {@link #bind(boolean)}.
*
* @param texMinFilter The {@link C.TextureMinFilter}.
*/
public void setTexMinFilter(@C.TextureMinFilter int texMinFilter) {
this.texMinFilter = texMinFilter;
}
/** Configures {@link #bind(boolean)} to use the specified {@code int} {@code value}. */
public void setInt(int value) {
this.intValue[0] = value;
@ -476,6 +509,15 @@ public final class GlProgram {
type == GLES20.GL_SAMPLER_2D || !externalTexturesRequireNearestSampling
? GLES20.GL_LINEAR
: GLES20.GL_NEAREST);
if (type == GLES20.GL_SAMPLER_2D) {
if (texMinFilter == TEXTURE_MIN_FILTER_LINEAR_MIPMAP_LINEAR) {
GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D);
GlUtil.checkGlError();
}
GLES20.glTexParameteri(
GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, texMinFilter);
GlUtil.checkGlError();
}
GLES20.glUniform1i(location, texUnitIndex);
GlUtil.checkGlError();
break;

View File

@ -22,6 +22,8 @@ import static androidx.media3.test.utils.BitmapPixelTestUtil.createGlTextureFrom
import static androidx.media3.test.utils.BitmapPixelTestUtil.getBitmapAveragePixelAbsoluteDifferenceArgb8888;
import static androidx.media3.test.utils.BitmapPixelTestUtil.maybeSaveTestBitmap;
import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap;
import static androidx.media3.test.utils.TestUtil.PSNR_THRESHOLD;
import static androidx.media3.test.utils.TestUtil.assertBitmapsAreSimilar;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.common.truth.Truth.assertThat;
@ -71,6 +73,9 @@ public final class PresentationPixelTest {
"test-generated-goldens/sample_mp4_first_frame/electrical_colors/aspect_ratio_stretch_to_fit_narrow.png";
private static final String ASPECT_RATIO_STRETCH_TO_FIT_WIDE_PNG_ASSET_PATH =
"test-generated-goldens/sample_mp4_first_frame/electrical_colors/aspect_ratio_stretch_to_fit_wide.png";
private static final String HIGH_RESOLUTION_JPG_ASSET_PATH = "media/jpeg/ultraHDR.jpg";
private static final String DOWNSCALED_6X_PNG_ASSET_PATH =
"test-generated-goldens/PresentationPixelTest/ultraHDR_mipmap_512x680.png";
private final Context context = getApplicationContext();
@ -254,6 +259,29 @@ public final class PresentationPixelTest {
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
}
@Test
public void drawFrame_downscaleWithLinearMipmap_matchesGoldenFile() throws Exception {
Bitmap inputBitmap = readBitmap(HIGH_RESOLUTION_JPG_ASSET_PATH);
inputWidth = inputBitmap.getWidth();
inputHeight = inputBitmap.getHeight();
inputTexId = createGlTextureFromBitmap(inputBitmap);
presentationShaderProgram =
Presentation.createForWidthAndHeight(
inputWidth / 6, inputHeight / 6, Presentation.LAYOUT_SCALE_TO_FIT)
.copyWithTextureMinFilter(C.TEXTURE_MIN_FILTER_LINEAR_MIPMAP_LINEAR)
.toGlShaderProgram(context, /* useHdr= */ false);
Size outputSize = presentationShaderProgram.configure(inputWidth, inputHeight);
setupOutputTexture(outputSize.getWidth(), outputSize.getHeight());
Bitmap expectedBitmap = readBitmap(DOWNSCALED_6X_PNG_ASSET_PATH);
presentationShaderProgram.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
Bitmap actualBitmap =
createArgb8888BitmapFromFocusedGlFramebuffer(outputSize.getWidth(), outputSize.getHeight());
maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual", actualBitmap, /* path= */ null);
assertBitmapsAreSimilar(expectedBitmap, actualBitmap, PSNR_THRESHOLD);
}
private void setupOutputTexture(int outputWidth, int outputHeight) throws GlUtil.GlException {
int outputTexId =
GlUtil.createTexture(

View File

@ -21,6 +21,7 @@ import static androidx.media3.common.VideoFrameProcessor.INPUT_TYPE_BITMAP;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.effect.DefaultVideoFrameProcessor.WORKING_COLOR_SPACE_LINEAR;
import static java.lang.Math.max;
import android.content.Context;
import android.graphics.Gainmap;
@ -140,6 +141,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Matrix for storing an intermediate calculation result. */
private final float[] tempResultMatrix;
/** The texture minification filter to use when sampling from the input texture. */
private final @C.TextureMinFilter int textureMinFilter;
/**
* A polygon in the input space chosen such that no additional clipping is needed to keep vertices
* inside the NDC range when applying each of the {@link #matrixTransformations}.
@ -473,6 +477,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
tempResultMatrix = new float[16];
visiblePolygon = NDC_SQUARE;
gainmapTexId = C.INDEX_UNSET;
// When multiple matrix transformations are applied in a single shader program, use the highest
// quality resampling algorithm requested.
@C.TextureMinFilter int textureMinFilter = C.TEXTURE_MIN_FILTER_LINEAR;
for (int i = 0; i < matrixTransformations.size(); i++) {
textureMinFilter =
max(textureMinFilter, matrixTransformations.get(i).getGlTextureMinFilter());
}
this.textureMinFilter = textureMinFilter;
}
private static GlProgram createGlProgram(
@ -518,7 +531,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
try {
glProgram.use();
setGainmapSamplerAndUniforms();
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
glProgram.setSamplerTexIdUniform(
"uTexSampler", inputTexId, /* texUnitIndex= */ 0, textureMinFilter);
glProgram.setFloatsUniform("uTransformationMatrix", compositeTransformationMatrixArray);
glProgram.setFloatsUniformIfPresent("uRgbMatrix", compositeRgbMatrixArray);
glProgram.setBufferAttribute(

View File

@ -15,8 +15,11 @@
*/
package androidx.media3.effect;
import static androidx.media3.common.C.TEXTURE_MIN_FILTER_LINEAR;
import android.content.Context;
import android.opengl.Matrix;
import androidx.media3.common.C;
import androidx.media3.common.VideoFrameProcessingException;
import androidx.media3.common.util.Size;
import androidx.media3.common.util.UnstableApi;
@ -47,6 +50,14 @@ public interface GlMatrixTransformation extends GlEffect {
return new Size(inputWidth, inputHeight);
}
/**
* Returns the {@linkplain C.TextureMinFilter texture minification filter} to use for sampling the
* input texture when applying this matrix transformation.
*/
default @C.TextureMinFilter int getGlTextureMinFilter() {
return TEXTURE_MIN_FILTER_LINEAR;
}
/**
* Returns the 4x4 transformation {@link Matrix} to apply to the frame with the given timestamp.
*/

View File

@ -15,6 +15,8 @@
*/
package androidx.media3.effect;
import static androidx.media3.common.C.TEXTURE_MIN_FILTER_LINEAR;
import static androidx.media3.common.C.TEXTURE_MIN_FILTER_LINEAR_MIPMAP_LINEAR;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static java.lang.annotation.ElementType.TYPE_USE;
@ -125,7 +127,11 @@ public final class Presentation implements MatrixTransformation {
checkArgument(aspectRatio > 0, "aspect ratio " + aspectRatio + " must be positive");
checkLayout(layout);
return new Presentation(
/* width= */ C.LENGTH_UNSET, /* height= */ C.LENGTH_UNSET, aspectRatio, layout);
/* width= */ C.LENGTH_UNSET,
/* height= */ C.LENGTH_UNSET,
aspectRatio,
layout,
TEXTURE_MIN_FILTER_LINEAR);
}
/**
@ -138,7 +144,11 @@ public final class Presentation implements MatrixTransformation {
*/
public static Presentation createForHeight(int height) {
return new Presentation(
/* width= */ C.LENGTH_UNSET, height, ASPECT_RATIO_UNSET, LAYOUT_SCALE_TO_FIT);
/* width= */ C.LENGTH_UNSET,
height,
ASPECT_RATIO_UNSET,
LAYOUT_SCALE_TO_FIT,
TEXTURE_MIN_FILTER_LINEAR);
}
/**
@ -156,19 +166,25 @@ public final class Presentation implements MatrixTransformation {
checkArgument(width > 0, "width " + width + " must be positive");
checkArgument(height > 0, "height " + height + " must be positive");
checkLayout(layout);
return new Presentation(width, height, ASPECT_RATIO_UNSET, layout);
return new Presentation(width, height, ASPECT_RATIO_UNSET, layout, TEXTURE_MIN_FILTER_LINEAR);
}
private final int requestedWidthPixels;
private final int requestedHeightPixels;
private float requestedAspectRatio;
private final @Layout int layout;
private final @C.TextureMinFilter int textureMinFilter;
private float outputWidth;
private float outputHeight;
private @MonotonicNonNull Matrix transformationMatrix;
private Presentation(int width, int height, float aspectRatio, @Layout int layout) {
private Presentation(
int width,
int height,
float aspectRatio,
@Layout int layout,
@C.TextureMinFilter int textureMinFilter) {
checkArgument(
(aspectRatio == ASPECT_RATIO_UNSET) || (width == C.LENGTH_UNSET),
"width and aspect ratio should not both be set");
@ -177,12 +193,35 @@ public final class Presentation implements MatrixTransformation {
this.requestedHeightPixels = height;
this.requestedAspectRatio = aspectRatio;
this.layout = layout;
this.textureMinFilter = textureMinFilter;
outputWidth = C.LENGTH_UNSET;
outputHeight = C.LENGTH_UNSET;
transformationMatrix = new Matrix();
}
/**
* Returns a copy with the specified texture minification filter.
*
* @param textureMinFilter The {@link C.TextureMinFilter}.
*/
public Presentation copyWithTextureMinFilter(@C.TextureMinFilter int textureMinFilter) {
checkArgument(
textureMinFilter == TEXTURE_MIN_FILTER_LINEAR
|| textureMinFilter == TEXTURE_MIN_FILTER_LINEAR_MIPMAP_LINEAR);
return new Presentation(
requestedWidthPixels,
requestedHeightPixels,
requestedAspectRatio,
layout,
textureMinFilter);
}
@Override
public @C.TextureMinFilter int getGlTextureMinFilter() {
return textureMinFilter;
}
@Override
public Size configure(int inputWidth, int inputHeight) {
checkArgument(inputWidth > 0, "inputWidth must be positive");

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB