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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 438 public int getGlVersion() { 439 return mGlVersion; 440 } 441 442 /** 443 * Writes the current display, context, and surface to the log. 444 */ 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 */ 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 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 */ 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 */ 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 */ 515 public int getWidth() { 516 return mWidth; 517 } 518 519 /** 520 * Returns the surface's height, in pixels. 521 */ 522 public int getHeight() { 523 return mHeight; 524 } 525 526 /** 527 * Release the EGL surface. 528 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 646 public void release() { 647 releaseEglSurface(); 648 } 649 } 650 } 651