xref: /aosp_15_r20/cts/tests/tests/opengl/src/android/opengl/cts/FramebufferTest.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.opengl.cts;
18 
19 import android.graphics.Bitmap;
20 import android.graphics.SurfaceTexture;
21 import android.opengl.EGL14;
22 import android.opengl.EGLConfig;
23 import android.opengl.EGLContext;
24 import android.opengl.EGLDisplay;
25 import android.opengl.EGLExt;
26 import android.opengl.EGLSurface;
27 import android.opengl.GLES20;
28 import android.opengl.GLES30;
29 import android.test.AndroidTestCase;
30 import android.util.Log;
31 import android.view.Surface;
32 
33 import java.io.BufferedOutputStream;
34 import java.io.File;
35 import java.io.FileOutputStream;
36 import java.io.IOException;
37 import java.nio.ByteBuffer;
38 import java.nio.ByteOrder;
39 import java.util.Arrays;
40 
41 /**
42  * Test some GLES framebuffer stuff.
43  */
44 public class FramebufferTest extends AndroidTestCase {
45     private static final String TAG = "FramebufferTest";
46 
47 
48     /**
49      * Tests very basic glBlitFramebuffer() features by copying from one offscreen framebuffer
50      * to another.
51      * <p>
52      * Requires GLES3.
53      */
testBlitFramebuffer()54     public void testBlitFramebuffer() throws Throwable {
55         final int WIDTH = 640;
56         final int HEIGHT = 480;
57         final int BYTES_PER_PIXEL = 4;
58         final int TEST_RED = 255;
59         final int TEST_GREEN = 0;
60         final int TEST_BLUE = 127;
61         final int TEST_ALPHA = 255;
62         final byte expectedBytes[] = new byte[] {
63                 (byte) TEST_RED, (byte) TEST_GREEN, (byte) TEST_BLUE, (byte) TEST_ALPHA
64         };
65         EglCore eglCore = null;
66         OffscreenSurface surface1 = null;
67         OffscreenSurface surface2 = null;
68 
69         try {
70             eglCore = new EglCore(null, EglCore.FLAG_TRY_GLES3);
71             if (eglCore.getGlVersion() < 3) {
72                 Log.d(TAG, "GLES3 not available, skipping test");
73                 return;
74             }
75 
76             // Create two surfaces, and clear surface1
77             surface1 = new OffscreenSurface(eglCore, WIDTH, HEIGHT);
78             surface2 = new OffscreenSurface(eglCore, WIDTH, HEIGHT);
79             surface1.makeCurrent();
80             GLES30.glClearColor(TEST_RED / 255.0f, TEST_GREEN / 255.0f, TEST_BLUE / 255.0f,
81                     TEST_ALPHA / 255.0f);
82             GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
83             checkGlError("glClear");
84 
85             // Set surface2 as "draw", surface1 as "read", and blit.
86             surface2.makeCurrentReadFrom(surface1);
87             GLES30.glBlitFramebuffer(0, 0, WIDTH, HEIGHT, 0, 0, WIDTH, HEIGHT,
88                     GLES30.GL_COLOR_BUFFER_BIT, GLES30.GL_NEAREST);
89             checkGlError("glBlitFramebuffer");
90 
91             ByteBuffer pixelBuf = ByteBuffer.allocateDirect(WIDTH * HEIGHT * BYTES_PER_PIXEL);
92             pixelBuf.order(ByteOrder.LITTLE_ENDIAN);
93             byte testBytes[] = new byte[4];
94 
95             // Confirm that surface1 has the color by testing a pixel from the center.
96             surface1.makeCurrent();
97             pixelBuf.clear();
98             GLES30.glReadPixels(0, 0, WIDTH, HEIGHT, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE,
99                     pixelBuf);
100             checkGlError("glReadPixels");
101             pixelBuf.position((WIDTH * (HEIGHT / 2) + (WIDTH / 2)) * BYTES_PER_PIXEL);
102             pixelBuf.get(testBytes, 0, 4);
103             Log.v(TAG, "testBytes1 = " + Arrays.toString(testBytes));
104             assertTrue(Arrays.equals(testBytes, expectedBytes));
105 
106             // Confirm that surface2 has the color.
107             surface2.makeCurrent();
108             pixelBuf.clear();
109             GLES30.glReadPixels(0, 0, WIDTH, HEIGHT, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE,
110                     pixelBuf);
111             checkGlError("glReadPixels");
112             pixelBuf.position((WIDTH * (HEIGHT / 2) + (WIDTH / 2)) * BYTES_PER_PIXEL);
113             pixelBuf.get(testBytes, 0, 4);
114             Log.v(TAG, "testBytes2 = " + Arrays.toString(testBytes));
115             assertTrue(Arrays.equals(testBytes, expectedBytes));
116         } finally {
117             if (surface1 != null) {
118                 surface1.release();
119             }
120             if (surface2 != null) {
121                 surface2.release();
122             }
123             if (eglCore != null) {
124                 eglCore.release();
125             }
126         }
127     }
128 
129     /**
130      * Checks to see if a GLES error has been raised.
131      */
checkGlError(String op)132     private static void checkGlError(String op) {
133         int error = GLES20.glGetError();
134         if (error != GLES20.GL_NO_ERROR) {
135             String msg = op + ": glError 0x" + Integer.toHexString(error);
136             Log.e(TAG, msg);
137             throw new RuntimeException(msg);
138         }
139     }
140 
141 
142     /**
143      * Core EGL state (display, context, config).
144      */
145     private static final class EglCore {
146         /**
147          * Constructor flag: surface must be recordable.  This discourages EGL from using a
148          * pixel format that cannot be converted efficiently to something usable by the video
149          * encoder.
150          */
151         public static final int FLAG_RECORDABLE = 0x01;
152 
153         /**
154          * Constructor flag: ask for GLES3, fall back to GLES2 if not available.  Without this
155          * flag, GLES2 is used.
156          */
157         public static final int FLAG_TRY_GLES3 = 0x02;
158 
159         private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY;
160         private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT;
161         private EGLConfig mEGLConfig = null;
162         private int mGlVersion = -1;
163 
164 
165         /**
166          * Prepares EGL display and context.
167          * <p>
168          * Equivalent to EglCore(null, 0).
169          */
EglCore()170         public EglCore() {
171             this(null, 0);
172         }
173 
174         /**
175          * Prepares EGL display and context.
176          * <p>
177          * @param sharedContext The context to share, or null if sharing is not desired.
178          * @param flags Configuration bit flags, e.g. FLAG_RECORDABLE.
179          */
EglCore(EGLContext sharedContext, int flags)180         public EglCore(EGLContext sharedContext, int flags) {
181             if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
182                 throw new RuntimeException("EGL already set up");
183             }
184 
185             if (sharedContext == null) {
186                 sharedContext = EGL14.EGL_NO_CONTEXT;
187             }
188 
189             mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
190             if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
191                 throw new RuntimeException("unable to get EGL14 display");
192             }
193             int[] version = new int[2];
194             if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
195                 mEGLDisplay = null;
196                 throw new RuntimeException("unable to initialize EGL14");
197             }
198 
199             // Try to get a GLES3 context, if requested.
200             if ((flags & FLAG_TRY_GLES3) != 0) {
201                 //Log.d(TAG, "Trying GLES 3");
202                 EGLConfig config = getConfig(flags, 3);
203                 if (config != null) {
204                     int[] attrib3_list = {
205                             EGL14.EGL_CONTEXT_CLIENT_VERSION, 3,
206                             EGL14.EGL_NONE
207                     };
208                     EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext,
209                             attrib3_list, 0);
210 
211                     if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) {
212                         //Log.d(TAG, "Got GLES 3 config");
213                         mEGLConfig = config;
214                         mEGLContext = context;
215                         mGlVersion = 3;
216                     }
217                 }
218             }
219             if (mEGLContext == EGL14.EGL_NO_CONTEXT) {  // GLES 2 only, or GLES 3 attempt failed
220                 //Log.d(TAG, "Trying GLES 2");
221                 EGLConfig config = getConfig(flags, 2);
222                 if (config == null) {
223                     throw new RuntimeException("Unable to find a suitable EGLConfig");
224                 }
225                 int[] attrib2_list = {
226                         EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
227                         EGL14.EGL_NONE
228                 };
229                 EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext,
230                         attrib2_list, 0);
231                 checkEglError("eglCreateContext");
232                 mEGLConfig = config;
233                 mEGLContext = context;
234                 mGlVersion = 2;
235             }
236 
237             // Confirm with query.
238             int[] values = new int[1];
239             EGL14.eglQueryContext(mEGLDisplay, mEGLContext, EGL14.EGL_CONTEXT_CLIENT_VERSION,
240                     values, 0);
241             Log.d(TAG, "EGLContext created, client version " + values[0]);
242         }
243 
244         /**
245          * Finds a suitable EGLConfig.
246          *
247          * @param flags Bit flags from constructor.
248          * @param version Must be 2 or 3.
249          */
getConfig(int flags, int version)250         private EGLConfig getConfig(int flags, int version) {
251             int renderableType = EGL14.EGL_OPENGL_ES2_BIT;
252             if (version >= 3) {
253                 renderableType |= EGLExt.EGL_OPENGL_ES3_BIT_KHR;
254             }
255 
256             // The actual surface is generally RGBA or RGBX, so situationally omitting alpha
257             // doesn't really help.  It can also lead to a huge performance hit on glReadPixels()
258             // when reading into a GL_RGBA buffer.
259             int[] attribList = {
260                     EGL14.EGL_RED_SIZE, 8,
261                     EGL14.EGL_GREEN_SIZE, 8,
262                     EGL14.EGL_BLUE_SIZE, 8,
263                     EGL14.EGL_ALPHA_SIZE, 8,
264                     //EGL14.EGL_DEPTH_SIZE, 16,
265                     //EGL14.EGL_STENCIL_SIZE, 8,
266                     EGL14.EGL_RENDERABLE_TYPE, renderableType,
267                     EGL14.EGL_NONE, 0,      // placeholder for recordable [@-3]
268                     EGL14.EGL_NONE
269             };
270             if ((flags & FLAG_RECORDABLE) != 0) {
271                 attribList[attribList.length - 3] = EGLExt.EGL_RECORDABLE_ANDROID;
272                 attribList[attribList.length - 2] = 1;
273             }
274             EGLConfig[] configs = new EGLConfig[1];
275             int[] numConfigs = new int[1];
276             if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length,
277                     numConfigs, 0)) {
278                 Log.w(TAG, "unable to find RGB8888 / " + version + " EGLConfig");
279                 return null;
280             }
281             return configs[0];
282         }
283 
284         /**
285          * Discard all resources held by this class, notably the EGL context.
286          */
release()287         public void release() {
288             if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
289                 // Android is unusual in that it uses a reference-counted EGLDisplay.  So for
290                 // every eglInitialize() we need an eglTerminate().
291                 EGL14.eglDestroyContext(mEGLDisplay, mEGLContext);
292                 EGL14.eglReleaseThread();
293                 EGL14.eglTerminate(mEGLDisplay);
294             }
295 
296             mEGLDisplay = EGL14.EGL_NO_DISPLAY;
297             mEGLContext = EGL14.EGL_NO_CONTEXT;
298             mEGLConfig = null;
299         }
300 
301         @Override
finalize()302         protected void finalize() throws Throwable {
303             try {
304                 if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
305                     // We're limited here -- finalizers don't run on the thread that holds
306                     // the EGL state, so if a surface or context is still current on another
307                     // thread we can't fully release it here.  Exceptions thrown from here
308                     // are quietly discarded.  Complain in the log file.
309                     Log.w(TAG, "WARNING: EglCore was not explicitly released -- state may be leaked");
310                     release();
311                 }
312             } finally {
313                 super.finalize();
314             }
315         }
316 
317         /**
318          * Destroys the specified surface.  Note the EGLSurface won't actually be destroyed if it's
319          * still current in a context.
320          */
releaseSurface(EGLSurface eglSurface)321         public void releaseSurface(EGLSurface eglSurface) {
322             EGL14.eglDestroySurface(mEGLDisplay, eglSurface);
323         }
324 
325         /**
326          * Creates an EGL surface associated with a Surface.
327          * <p>
328          * If this is destined for MediaCodec, the EGLConfig should have the "recordable" attribute.
329          */
createWindowSurface(Object surface)330         public EGLSurface createWindowSurface(Object surface) {
331             if (!(surface instanceof Surface) && !(surface instanceof SurfaceTexture)) {
332                 throw new RuntimeException("invalid surface: " + surface);
333             }
334 
335             // Create a window surface, and attach it to the Surface we received.
336             int[] surfaceAttribs = {
337                     EGL14.EGL_NONE
338             };
339             EGLSurface eglSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, surface,
340                     surfaceAttribs, 0);
341             checkEglError("eglCreateWindowSurface");
342             if (eglSurface == null) {
343                 throw new RuntimeException("surface was null");
344             }
345             return eglSurface;
346         }
347 
348         /**
349          * Creates an EGL surface associated with an offscreen buffer.
350          */
createOffscreenSurface(int width, int height)351         public EGLSurface createOffscreenSurface(int width, int height) {
352             int[] surfaceAttribs = {
353                     EGL14.EGL_WIDTH, width,
354                     EGL14.EGL_HEIGHT, height,
355                     EGL14.EGL_NONE
356             };
357             EGLSurface eglSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, mEGLConfig,
358                     surfaceAttribs, 0);
359             checkEglError("eglCreatePbufferSurface");
360             if (eglSurface == null) {
361                 throw new RuntimeException("surface was null");
362             }
363             return eglSurface;
364         }
365 
366         /**
367          * Makes our EGL context current, using the supplied surface for both "draw" and "read".
368          */
makeCurrent(EGLSurface eglSurface)369         public void makeCurrent(EGLSurface eglSurface) {
370             if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
371                 // called makeCurrent() before create?
372                 Log.d(TAG, "NOTE: makeCurrent w/o display");
373             }
374             if (!EGL14.eglMakeCurrent(mEGLDisplay, eglSurface, eglSurface, mEGLContext)) {
375                 throw new RuntimeException("eglMakeCurrent failed");
376             }
377         }
378 
379         /**
380          * Makes our EGL context current, using the supplied "draw" and "read" surfaces.
381          */
makeCurrent(EGLSurface drawSurface, EGLSurface readSurface)382         public void makeCurrent(EGLSurface drawSurface, EGLSurface readSurface) {
383             if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
384                 // called makeCurrent() before create?
385                 Log.d(TAG, "NOTE: makeCurrent w/o display");
386             }
387             if (!EGL14.eglMakeCurrent(mEGLDisplay, drawSurface, readSurface, mEGLContext)) {
388                 throw new RuntimeException("eglMakeCurrent(draw,read) failed");
389             }
390         }
391 
392         /**
393          * Makes no context current.
394          */
makeNothingCurrent()395         public void makeNothingCurrent() {
396             if (!EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
397                     EGL14.EGL_NO_CONTEXT)) {
398                 throw new RuntimeException("eglMakeCurrent failed");
399             }
400         }
401 
402         /**
403          * Calls eglSwapBuffers.  Use this to "publish" the current frame.
404          *
405          * @return false on failure
406          */
swapBuffers(EGLSurface eglSurface)407         public boolean swapBuffers(EGLSurface eglSurface) {
408             return EGL14.eglSwapBuffers(mEGLDisplay, eglSurface);
409         }
410 
411         /**
412          * Sends the presentation time stamp to EGL.  Time is expressed in nanoseconds.
413          */
setPresentationTime(EGLSurface eglSurface, long nsecs)414         public void setPresentationTime(EGLSurface eglSurface, long nsecs) {
415             EGLExt.eglPresentationTimeANDROID(mEGLDisplay, eglSurface, nsecs);
416         }
417 
418         /**
419          * Returns true if our context and the specified surface are current.
420          */
isCurrent(EGLSurface eglSurface)421         public boolean isCurrent(EGLSurface eglSurface) {
422             return mEGLContext.equals(EGL14.eglGetCurrentContext()) &&
423                     eglSurface.equals(EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW));
424         }
425 
426         /**
427          * Performs a simple surface query.
428          */
querySurface(EGLSurface eglSurface, int what)429         public int querySurface(EGLSurface eglSurface, int what) {
430             int[] value = new int[1];
431             EGL14.eglQuerySurface(mEGLDisplay, eglSurface, what, value, 0);
432             return value[0];
433         }
434 
435         /**
436          * Returns the GLES version this context is configured for (2 or 3).
437          */
getGlVersion()438         public int getGlVersion() {
439             return mGlVersion;
440         }
441 
442         /**
443          * Writes the current display, context, and surface to the log.
444          */
logCurrent(String msg)445         public static void logCurrent(String msg) {
446             EGLDisplay display;
447             EGLContext context;
448             EGLSurface surface;
449 
450             display = EGL14.eglGetCurrentDisplay();
451             context = EGL14.eglGetCurrentContext();
452             surface = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW);
453             Log.i(TAG, "Current EGL (" + msg + "): display=" + display + ", context=" + context +
454                     ", surface=" + surface);
455         }
456 
457         /**
458          * Checks for EGL errors.
459          */
checkEglError(String msg)460         private void checkEglError(String msg) {
461             int error;
462             if ((error = EGL14.eglGetError()) != EGL14.EGL_SUCCESS) {
463                 throw new RuntimeException(msg + ": EGL error: 0x" + Integer.toHexString(error));
464             }
465         }
466     }
467 
468 
469     /**
470      * Common base class for EGL surfaces.
471      * <p>
472      * There can be multiple surfaces associated with a single context.
473      */
474     private static class EglSurfaceBase {
475         // EglCore object we're associated with.  It may be associated with multiple surfaces.
476         protected EglCore mEglCore;
477 
478         private EGLSurface mEGLSurface = EGL14.EGL_NO_SURFACE;
479         private int mWidth = -1;
480         private int mHeight = -1;
481 
EglSurfaceBase(EglCore eglCore)482         protected EglSurfaceBase(EglCore eglCore) {
483             mEglCore = eglCore;
484         }
485 
486         /**
487          * Creates a window surface.
488          * <p>
489          * @param surface May be a Surface or SurfaceTexture.
490          */
createWindowSurface(Object surface)491         public void createWindowSurface(Object surface) {
492             if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
493                 throw new IllegalStateException("surface already created");
494             }
495             mEGLSurface = mEglCore.createWindowSurface(surface);
496             mWidth = mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH);
497             mHeight = mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT);
498         }
499 
500         /**
501          * Creates an off-screen surface.
502          */
createOffscreenSurface(int width, int height)503         public void createOffscreenSurface(int width, int height) {
504             if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
505                 throw new IllegalStateException("surface already created");
506             }
507             mEGLSurface = mEglCore.createOffscreenSurface(width, height);
508             mWidth = width;
509             mHeight = height;
510         }
511 
512         /**
513          * Returns the surface's width, in pixels.
514          */
getWidth()515         public int getWidth() {
516             return mWidth;
517         }
518 
519         /**
520          * Returns the surface's height, in pixels.
521          */
getHeight()522         public int getHeight() {
523             return mHeight;
524         }
525 
526         /**
527          * Release the EGL surface.
528          */
releaseEglSurface()529         public void releaseEglSurface() {
530             mEglCore.releaseSurface(mEGLSurface);
531             mEGLSurface = EGL14.EGL_NO_SURFACE;
532             mWidth = mHeight = -1;
533         }
534 
535         /**
536          * Makes our EGL context and surface current.
537          */
makeCurrent()538         public void makeCurrent() {
539             mEglCore.makeCurrent(mEGLSurface);
540         }
541 
542         /**
543          * Makes our EGL context and surface current for drawing, using the supplied surface
544          * for reading.
545          */
makeCurrentReadFrom(EglSurfaceBase readSurface)546         public void makeCurrentReadFrom(EglSurfaceBase readSurface) {
547             mEglCore.makeCurrent(mEGLSurface, readSurface.mEGLSurface);
548         }
549 
550         /**
551          * Calls eglSwapBuffers.  Use this to "publish" the current frame.
552          *
553          * @return false on failure
554          */
swapBuffers()555         public boolean swapBuffers() {
556             boolean result = mEglCore.swapBuffers(mEGLSurface);
557             if (!result) {
558                 Log.d(TAG, "WARNING: swapBuffers() failed");
559             }
560             return result;
561         }
562 
563         /**
564          * Sends the presentation time stamp to EGL.
565          *
566          * @param nsecs Timestamp, in nanoseconds.
567          */
setPresentationTime(long nsecs)568         public void setPresentationTime(long nsecs) {
569             mEglCore.setPresentationTime(mEGLSurface, nsecs);
570         }
571 
572         /**
573          * Saves the EGL surface to a file.
574          * <p>
575          * Expects that this object's EGL surface is current.
576          */
saveFrame(File file)577         public void saveFrame(File file) throws IOException {
578             if (!mEglCore.isCurrent(mEGLSurface)) {
579                 throw new RuntimeException("Expected EGL context/surface is not current");
580             }
581 
582             // glReadPixels gives us a ByteBuffer filled with what is essentially big-endian RGBA
583             // data (i.e. a byte of red, followed by a byte of green...).  We need an int[] filled
584             // with little-endian ARGB data to feed to Bitmap.
585             //
586             // If we implement this as a series of buf.get() calls, we can spend 2.5 seconds just
587             // copying data around for a 720p frame.  It's better to do a bulk get() and then
588             // rearrange the data in memory.  (For comparison, the PNG compress takes about 500ms
589             // for a trivial frame.)
590             //
591             // So... we set the ByteBuffer to little-endian, which should turn the bulk IntBuffer
592             // get() into a straight memcpy on most Android devices.  Our ints will hold ABGR data.
593             // Swapping B and R gives us ARGB.
594             //
595             // Making this even more interesting is the upside-down nature of GL, which means
596             // our output will look upside-down relative to what appears on screen if the
597             // typical GL conventions are used.
598 
599             String filename = file.toString();
600 
601             ByteBuffer buf = ByteBuffer.allocateDirect(mWidth * mHeight * 4);
602             buf.order(ByteOrder.LITTLE_ENDIAN);
603             GLES20.glReadPixels(0, 0, mWidth, mHeight,
604                     GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf);
605             checkGlError("glReadPixels");
606             buf.rewind();
607 
608             int pixelCount = mWidth * mHeight;
609             int[] colors = new int[pixelCount];
610             buf.asIntBuffer().get(colors);
611             for (int i = 0; i < pixelCount; i++) {
612                 int c = colors[i];
613                 colors[i] = (c & 0xff00ff00) | ((c & 0x00ff0000) >> 16) | ((c & 0x000000ff) << 16);
614             }
615 
616             BufferedOutputStream bos = null;
617             try {
618                 bos = new BufferedOutputStream(new FileOutputStream(filename));
619                 Bitmap bmp = Bitmap.createBitmap(colors, mWidth, mHeight, Bitmap.Config.ARGB_8888);
620                 bmp.compress(Bitmap.CompressFormat.PNG, 90, bos);
621                 bmp.recycle();
622             } finally {
623                 if (bos != null) bos.close();
624             }
625             Log.d(TAG, "Saved " + mWidth + "x" + mHeight + " frame as '" + filename + "'");
626         }
627     }
628 
629     /**
630      * Off-screen EGL surface (pbuffer).
631      * <p>
632      * It's good practice to explicitly release() the surface, preferably from a "finally" block.
633      */
634     private static class OffscreenSurface extends EglSurfaceBase {
635         /**
636          * Creates an off-screen surface with the specified width and height.
637          */
OffscreenSurface(EglCore eglCore, int width, int height)638         public OffscreenSurface(EglCore eglCore, int width, int height) {
639             super(eglCore);
640             createOffscreenSurface(width, height);
641         }
642 
643         /**
644          * Releases any resources associated with the surface.
645          */
release()646         public void release() {
647             releaseEglSurface();
648         }
649     }
650 }
651