blob: 4ca3a9921d1849534b8762e9fd47566d0a0f5d01 [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.opengl.cts;
import android.graphics.Bitmap;
import android.graphics.SurfaceTexture;
import android.opengl.EGL14;
import android.opengl.EGLConfig;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
import android.opengl.EGLExt;
import android.opengl.EGLSurface;
import android.opengl.GLES20;
import android.opengl.GLES30;
import android.test.AndroidTestCase;
import android.util.Log;
import android.view.Surface;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
/**
* Test some GLES framebuffer stuff.
*/
public class FramebufferTest extends AndroidTestCase {
private static final String TAG = "FramebufferTest";
/**
* Tests very basic glBlitFramebuffer() features by copying from one offscreen framebuffer
* to another.
* <p>
* Requires GLES3.
*/
public void testBlitFramebuffer() throws Throwable {
final int WIDTH = 640;
final int HEIGHT = 480;
final int BYTES_PER_PIXEL = 4;
final int TEST_RED = 255;
final int TEST_GREEN = 0;
final int TEST_BLUE = 127;
final int TEST_ALPHA = 255;
final byte expectedBytes[] = new byte[] {
(byte) TEST_RED, (byte) TEST_GREEN, (byte) TEST_BLUE, (byte) TEST_ALPHA
};
EglCore eglCore = null;
OffscreenSurface surface1 = null;
OffscreenSurface surface2 = null;
try {
eglCore = new EglCore(null, EglCore.FLAG_TRY_GLES3);
if (eglCore.getGlVersion() < 3) {
Log.d(TAG, "GLES3 not available, skipping test");
return;
}
// Create two surfaces, and clear surface1
surface1 = new OffscreenSurface(eglCore, WIDTH, HEIGHT);
surface2 = new OffscreenSurface(eglCore, WIDTH, HEIGHT);
surface1.makeCurrent();
GLES30.glClearColor(TEST_RED / 255.0f, TEST_GREEN / 255.0f, TEST_BLUE / 255.0f,
TEST_ALPHA / 255.0f);
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
checkGlError("glClear");
// Set surface2 as "draw", surface1 as "read", and blit.
surface2.makeCurrentReadFrom(surface1);
GLES30.glBlitFramebuffer(0, 0, WIDTH, HEIGHT, 0, 0, WIDTH, HEIGHT,
GLES30.GL_COLOR_BUFFER_BIT, GLES30.GL_NEAREST);
checkGlError("glBlitFramebuffer");
ByteBuffer pixelBuf = ByteBuffer.allocateDirect(WIDTH * HEIGHT * BYTES_PER_PIXEL);
pixelBuf.order(ByteOrder.LITTLE_ENDIAN);
byte testBytes[] = new byte[4];
// Confirm that surface1 has the color by testing a pixel from the center.
surface1.makeCurrent();
pixelBuf.clear();
GLES30.glReadPixels(0, 0, WIDTH, HEIGHT, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE,
pixelBuf);
checkGlError("glReadPixels");
pixelBuf.position((WIDTH * (HEIGHT / 2) + (WIDTH / 2)) * BYTES_PER_PIXEL);
pixelBuf.get(testBytes, 0, 4);
Log.v(TAG, "testBytes1 = " + Arrays.toString(testBytes));
assertTrue(Arrays.equals(testBytes, expectedBytes));
// Confirm that surface2 has the color.
surface2.makeCurrent();
pixelBuf.clear();
GLES30.glReadPixels(0, 0, WIDTH, HEIGHT, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE,
pixelBuf);
checkGlError("glReadPixels");
pixelBuf.position((WIDTH * (HEIGHT / 2) + (WIDTH / 2)) * BYTES_PER_PIXEL);
pixelBuf.get(testBytes, 0, 4);
Log.v(TAG, "testBytes2 = " + Arrays.toString(testBytes));
assertTrue(Arrays.equals(testBytes, expectedBytes));
} finally {
if (surface1 != null) {
surface1.release();
}
if (surface2 != null) {
surface2.release();
}
if (eglCore != null) {
eglCore.release();
}
}
}
/**
* Checks to see if a GLES error has been raised.
*/
private static void checkGlError(String op) {
int error = GLES20.glGetError();
if (error != GLES20.GL_NO_ERROR) {
String msg = op + ": glError 0x" + Integer.toHexString(error);
Log.e(TAG, msg);
throw new RuntimeException(msg);
}
}
/**
* Core EGL state (display, context, config).
*/
private static final class EglCore {
/**
* Constructor flag: surface must be recordable. This discourages EGL from using a
* pixel format that cannot be converted efficiently to something usable by the video
* encoder.
*/
public static final int FLAG_RECORDABLE = 0x01;
/**
* Constructor flag: ask for GLES3, fall back to GLES2 if not available. Without this
* flag, GLES2 is used.
*/
public static final int FLAG_TRY_GLES3 = 0x02;
// Android-specific extension.
private static final int EGL_RECORDABLE_ANDROID = 0x3142;
private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY;
private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT;
private EGLConfig mEGLConfig = null;
private int mGlVersion = -1;
/**
* Prepares EGL display and context.
* <p>
* Equivalent to EglCore(null, 0).
*/
public EglCore() {
this(null, 0);
}
/**
* Prepares EGL display and context.
* <p>
* @param sharedContext The context to share, or null if sharing is not desired.
* @param flags Configuration bit flags, e.g. FLAG_RECORDABLE.
*/
public EglCore(EGLContext sharedContext, int flags) {
if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
throw new RuntimeException("EGL already set up");
}
if (sharedContext == null) {
sharedContext = EGL14.EGL_NO_CONTEXT;
}
mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
throw new RuntimeException("unable to get EGL14 display");
}
int[] version = new int[2];
if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
mEGLDisplay = null;
throw new RuntimeException("unable to initialize EGL14");
}
// Try to get a GLES3 context, if requested.
if ((flags & FLAG_TRY_GLES3) != 0) {
//Log.d(TAG, "Trying GLES 3");
EGLConfig config = getConfig(flags, 3);
if (config != null) {
int[] attrib3_list = {
EGL14.EGL_CONTEXT_CLIENT_VERSION, 3,
EGL14.EGL_NONE
};
EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext,
attrib3_list, 0);
if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) {
//Log.d(TAG, "Got GLES 3 config");
mEGLConfig = config;
mEGLContext = context;
mGlVersion = 3;
}
}
}
if (mEGLContext == EGL14.EGL_NO_CONTEXT) { // GLES 2 only, or GLES 3 attempt failed
//Log.d(TAG, "Trying GLES 2");
EGLConfig config = getConfig(flags, 2);
if (config == null) {
throw new RuntimeException("Unable to find a suitable EGLConfig");
}
int[] attrib2_list = {
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
EGL14.EGL_NONE
};
EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext,
attrib2_list, 0);
checkEglError("eglCreateContext");
mEGLConfig = config;
mEGLContext = context;
mGlVersion = 2;
}
// Confirm with query.
int[] values = new int[1];
EGL14.eglQueryContext(mEGLDisplay, mEGLContext, EGL14.EGL_CONTEXT_CLIENT_VERSION,
values, 0);
Log.d(TAG, "EGLContext created, client version " + values[0]);
}
/**
* Finds a suitable EGLConfig.
*
* @param flags Bit flags from constructor.
* @param version Must be 2 or 3.
*/
private EGLConfig getConfig(int flags, int version) {
int renderableType = EGL14.EGL_OPENGL_ES2_BIT;
if (version >= 3) {
renderableType |= EGLExt.EGL_OPENGL_ES3_BIT_KHR;
}
// The actual surface is generally RGBA or RGBX, so situationally omitting alpha
// doesn't really help. It can also lead to a huge performance hit on glReadPixels()
// when reading into a GL_RGBA buffer.
int[] attribList = {
EGL14.EGL_RED_SIZE, 8,
EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_ALPHA_SIZE, 8,
//EGL14.EGL_DEPTH_SIZE, 16,
//EGL14.EGL_STENCIL_SIZE, 8,
EGL14.EGL_RENDERABLE_TYPE, renderableType,
EGL14.EGL_NONE, 0, // placeholder for recordable [@-3]
EGL14.EGL_NONE
};
if ((flags & FLAG_RECORDABLE) != 0) {
attribList[attribList.length - 3] = EGL_RECORDABLE_ANDROID;
attribList[attribList.length - 2] = 1;
}
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length,
numConfigs, 0)) {
Log.w(TAG, "unable to find RGB8888 / " + version + " EGLConfig");
return null;
}
return configs[0];
}
/**
* Discard all resources held by this class, notably the EGL context.
*/
public void release() {
if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
// Android is unusual in that it uses a reference-counted EGLDisplay. So for
// every eglInitialize() we need an eglTerminate().
EGL14.eglDestroyContext(mEGLDisplay, mEGLContext);
EGL14.eglReleaseThread();
EGL14.eglTerminate(mEGLDisplay);
}
mEGLDisplay = EGL14.EGL_NO_DISPLAY;
mEGLContext = EGL14.EGL_NO_CONTEXT;
mEGLConfig = null;
}
@Override
protected void finalize() throws Throwable {
try {
if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
// We're limited here -- finalizers don't run on the thread that holds
// the EGL state, so if a surface or context is still current on another
// thread we can't fully release it here. Exceptions thrown from here
// are quietly discarded. Complain in the log file.
Log.w(TAG, "WARNING: EglCore was not explicitly released -- state may be leaked");
release();
}
} finally {
super.finalize();
}
}
/**
* Destroys the specified surface. Note the EGLSurface won't actually be destroyed if it's
* still current in a context.
*/
public void releaseSurface(EGLSurface eglSurface) {
EGL14.eglDestroySurface(mEGLDisplay, eglSurface);
}
/**
* Creates an EGL surface associated with a Surface.
* <p>
* If this is destined for MediaCodec, the EGLConfig should have the "recordable" attribute.
*/
public EGLSurface createWindowSurface(Object surface) {
if (!(surface instanceof Surface) && !(surface instanceof SurfaceTexture)) {
throw new RuntimeException("invalid surface: " + surface);
}
// Create a window surface, and attach it to the Surface we received.
int[] surfaceAttribs = {
EGL14.EGL_NONE
};
EGLSurface eglSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, surface,
surfaceAttribs, 0);
checkEglError("eglCreateWindowSurface");
if (eglSurface == null) {
throw new RuntimeException("surface was null");
}
return eglSurface;
}
/**
* Creates an EGL surface associated with an offscreen buffer.
*/
public EGLSurface createOffscreenSurface(int width, int height) {
int[] surfaceAttribs = {
EGL14.EGL_WIDTH, width,
EGL14.EGL_HEIGHT, height,
EGL14.EGL_NONE
};
EGLSurface eglSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, mEGLConfig,
surfaceAttribs, 0);
checkEglError("eglCreatePbufferSurface");
if (eglSurface == null) {
throw new RuntimeException("surface was null");
}
return eglSurface;
}
/**
* Makes our EGL context current, using the supplied surface for both "draw" and "read".
*/
public void makeCurrent(EGLSurface eglSurface) {
if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
// called makeCurrent() before create?
Log.d(TAG, "NOTE: makeCurrent w/o display");
}
if (!EGL14.eglMakeCurrent(mEGLDisplay, eglSurface, eglSurface, mEGLContext)) {
throw new RuntimeException("eglMakeCurrent failed");
}
}
/**
* Makes our EGL context current, using the supplied "draw" and "read" surfaces.
*/
public void makeCurrent(EGLSurface drawSurface, EGLSurface readSurface) {
if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
// called makeCurrent() before create?
Log.d(TAG, "NOTE: makeCurrent w/o display");
}
if (!EGL14.eglMakeCurrent(mEGLDisplay, drawSurface, readSurface, mEGLContext)) {
throw new RuntimeException("eglMakeCurrent(draw,read) failed");
}
}
/**
* Makes no context current.
*/
public void makeNothingCurrent() {
if (!EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
EGL14.EGL_NO_CONTEXT)) {
throw new RuntimeException("eglMakeCurrent failed");
}
}
/**
* Calls eglSwapBuffers. Use this to "publish" the current frame.
*
* @return false on failure
*/
public boolean swapBuffers(EGLSurface eglSurface) {
return EGL14.eglSwapBuffers(mEGLDisplay, eglSurface);
}
/**
* Sends the presentation time stamp to EGL. Time is expressed in nanoseconds.
*/
public void setPresentationTime(EGLSurface eglSurface, long nsecs) {
EGLExt.eglPresentationTimeANDROID(mEGLDisplay, eglSurface, nsecs);
}
/**
* Returns true if our context and the specified surface are current.
*/
public boolean isCurrent(EGLSurface eglSurface) {
return mEGLContext.equals(EGL14.eglGetCurrentContext()) &&
eglSurface.equals(EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW));
}
/**
* Performs a simple surface query.
*/
public int querySurface(EGLSurface eglSurface, int what) {
int[] value = new int[1];
EGL14.eglQuerySurface(mEGLDisplay, eglSurface, what, value, 0);
return value[0];
}
/**
* Returns the GLES version this context is configured for (2 or 3).
*/
public int getGlVersion() {
return mGlVersion;
}
/**
* Writes the current display, context, and surface to the log.
*/
public static void logCurrent(String msg) {
EGLDisplay display;
EGLContext context;
EGLSurface surface;
display = EGL14.eglGetCurrentDisplay();
context = EGL14.eglGetCurrentContext();
surface = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW);
Log.i(TAG, "Current EGL (" + msg + "): display=" + display + ", context=" + context +
", surface=" + surface);
}
/**
* Checks for EGL errors.
*/
private void checkEglError(String msg) {
int error;
if ((error = EGL14.eglGetError()) != EGL14.EGL_SUCCESS) {
throw new RuntimeException(msg + ": EGL error: 0x" + Integer.toHexString(error));
}
}
}
/**
* Common base class for EGL surfaces.
* <p>
* There can be multiple surfaces associated with a single context.
*/
private static class EglSurfaceBase {
// EglCore object we're associated with. It may be associated with multiple surfaces.
protected EglCore mEglCore;
private EGLSurface mEGLSurface = EGL14.EGL_NO_SURFACE;
private int mWidth = -1;
private int mHeight = -1;
protected EglSurfaceBase(EglCore eglCore) {
mEglCore = eglCore;
}
/**
* Creates a window surface.
* <p>
* @param surface May be a Surface or SurfaceTexture.
*/
public void createWindowSurface(Object surface) {
if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
throw new IllegalStateException("surface already created");
}
mEGLSurface = mEglCore.createWindowSurface(surface);
mWidth = mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH);
mHeight = mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT);
}
/**
* Creates an off-screen surface.
*/
public void createOffscreenSurface(int width, int height) {
if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
throw new IllegalStateException("surface already created");
}
mEGLSurface = mEglCore.createOffscreenSurface(width, height);
mWidth = width;
mHeight = height;
}
/**
* Returns the surface's width, in pixels.
*/
public int getWidth() {
return mWidth;
}
/**
* Returns the surface's height, in pixels.
*/
public int getHeight() {
return mHeight;
}
/**
* Release the EGL surface.
*/
public void releaseEglSurface() {
mEglCore.releaseSurface(mEGLSurface);
mEGLSurface = EGL14.EGL_NO_SURFACE;
mWidth = mHeight = -1;
}
/**
* Makes our EGL context and surface current.
*/
public void makeCurrent() {
mEglCore.makeCurrent(mEGLSurface);
}
/**
* Makes our EGL context and surface current for drawing, using the supplied surface
* for reading.
*/
public void makeCurrentReadFrom(EglSurfaceBase readSurface) {
mEglCore.makeCurrent(mEGLSurface, readSurface.mEGLSurface);
}
/**
* Calls eglSwapBuffers. Use this to "publish" the current frame.
*
* @return false on failure
*/
public boolean swapBuffers() {
boolean result = mEglCore.swapBuffers(mEGLSurface);
if (!result) {
Log.d(TAG, "WARNING: swapBuffers() failed");
}
return result;
}
/**
* Sends the presentation time stamp to EGL.
*
* @param nsecs Timestamp, in nanoseconds.
*/
public void setPresentationTime(long nsecs) {
mEglCore.setPresentationTime(mEGLSurface, nsecs);
}
/**
* Saves the EGL surface to a file.
* <p>
* Expects that this object's EGL surface is current.
*/
public void saveFrame(File file) throws IOException {
if (!mEglCore.isCurrent(mEGLSurface)) {
throw new RuntimeException("Expected EGL context/surface is not current");
}
// glReadPixels gives us a ByteBuffer filled with what is essentially big-endian RGBA
// data (i.e. a byte of red, followed by a byte of green...). We need an int[] filled
// with little-endian ARGB data to feed to Bitmap.
//
// If we implement this as a series of buf.get() calls, we can spend 2.5 seconds just
// copying data around for a 720p frame. It's better to do a bulk get() and then
// rearrange the data in memory. (For comparison, the PNG compress takes about 500ms
// for a trivial frame.)
//
// So... we set the ByteBuffer to little-endian, which should turn the bulk IntBuffer
// get() into a straight memcpy on most Android devices. Our ints will hold ABGR data.
// Swapping B and R gives us ARGB.
//
// Making this even more interesting is the upside-down nature of GL, which means
// our output will look upside-down relative to what appears on screen if the
// typical GL conventions are used.
String filename = file.toString();
ByteBuffer buf = ByteBuffer.allocateDirect(mWidth * mHeight * 4);
buf.order(ByteOrder.LITTLE_ENDIAN);
GLES20.glReadPixels(0, 0, mWidth, mHeight,
GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf);
checkGlError("glReadPixels");
buf.rewind();
int pixelCount = mWidth * mHeight;
int[] colors = new int[pixelCount];
buf.asIntBuffer().get(colors);
for (int i = 0; i < pixelCount; i++) {
int c = colors[i];
colors[i] = (c & 0xff00ff00) | ((c & 0x00ff0000) >> 16) | ((c & 0x000000ff) << 16);
}
BufferedOutputStream bos = null;
try {
bos = new BufferedOutputStream(new FileOutputStream(filename));
Bitmap bmp = Bitmap.createBitmap(colors, mWidth, mHeight, Bitmap.Config.ARGB_8888);
bmp.compress(Bitmap.CompressFormat.PNG, 90, bos);
bmp.recycle();
} finally {
if (bos != null) bos.close();
}
Log.d(TAG, "Saved " + mWidth + "x" + mHeight + " frame as '" + filename + "'");
}
}
/**
* Off-screen EGL surface (pbuffer).
* <p>
* It's good practice to explicitly release() the surface, preferably from a "finally" block.
*/
private static class OffscreenSurface extends EglSurfaceBase {
/**
* Creates an off-screen surface with the specified width and height.
*/
public OffscreenSurface(EglCore eglCore, int width, int height) {
super(eglCore);
createOffscreenSurface(width, height);
}
/**
* Releases any resources associated with the surface.
*/
public void release() {
releaseEglSurface();
}
}
}