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 // Android-specific extension. 160 private static final int EGL_RECORDABLE_ANDROID = 0x3142; 161 162 private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY; 163 private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT; 164 private EGLConfig mEGLConfig = null; 165 private int mGlVersion = -1; 166 167 168 /** 169 * Prepares EGL display and context. 170 * <p> 171 * Equivalent to EglCore(null, 0). 172 */ 173 public EglCore() { 174 this(null, 0); 175 } 176 177 /** 178 * Prepares EGL display and context. 179 * <p> 180 * @param sharedContext The context to share, or null if sharing is not desired. 181 * @param flags Configuration bit flags, e.g. FLAG_RECORDABLE. 182 */ 183 public EglCore(EGLContext sharedContext, int flags) { 184 if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) { 185 throw new RuntimeException("EGL already set up"); 186 } 187 188 if (sharedContext == null) { 189 sharedContext = EGL14.EGL_NO_CONTEXT; 190 } 191 192 mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); 193 if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { 194 throw new RuntimeException("unable to get EGL14 display"); 195 } 196 int[] version = new int[2]; 197 if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) { 198 mEGLDisplay = null; 199 throw new RuntimeException("unable to initialize EGL14"); 200 } 201 202 // Try to get a GLES3 context, if requested. 203 if ((flags & FLAG_TRY_GLES3) != 0) { 204 //Log.d(TAG, "Trying GLES 3"); 205 EGLConfig config = getConfig(flags, 3); 206 if (config != null) { 207 int[] attrib3_list = { 208 EGL14.EGL_CONTEXT_CLIENT_VERSION, 3, 209 EGL14.EGL_NONE 210 }; 211 EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext, 212 attrib3_list, 0); 213 214 if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) { 215 //Log.d(TAG, "Got GLES 3 config"); 216 mEGLConfig = config; 217 mEGLContext = context; 218 mGlVersion = 3; 219 } 220 } 221 } 222 if (mEGLContext == EGL14.EGL_NO_CONTEXT) { // GLES 2 only, or GLES 3 attempt failed 223 //Log.d(TAG, "Trying GLES 2"); 224 EGLConfig config = getConfig(flags, 2); 225 if (config == null) { 226 throw new RuntimeException("Unable to find a suitable EGLConfig"); 227 } 228 int[] attrib2_list = { 229 EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, 230 EGL14.EGL_NONE 231 }; 232 EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext, 233 attrib2_list, 0); 234 checkEglError("eglCreateContext"); 235 mEGLConfig = config; 236 mEGLContext = context; 237 mGlVersion = 2; 238 } 239 240 // Confirm with query. 241 int[] values = new int[1]; 242 EGL14.eglQueryContext(mEGLDisplay, mEGLContext, EGL14.EGL_CONTEXT_CLIENT_VERSION, 243 values, 0); 244 Log.d(TAG, "EGLContext created, client version " + values[0]); 245 } 246 247 /** 248 * Finds a suitable EGLConfig. 249 * 250 * @param flags Bit flags from constructor. 251 * @param version Must be 2 or 3. 252 */ 253 private EGLConfig getConfig(int flags, int version) { 254 int renderableType = EGL14.EGL_OPENGL_ES2_BIT; 255 if (version >= 3) { 256 renderableType |= EGLExt.EGL_OPENGL_ES3_BIT_KHR; 257 } 258 259 // The actual surface is generally RGBA or RGBX, so situationally omitting alpha 260 // doesn't really help. It can also lead to a huge performance hit on glReadPixels() 261 // when reading into a GL_RGBA buffer. 262 int[] attribList = { 263 EGL14.EGL_RED_SIZE, 8, 264 EGL14.EGL_GREEN_SIZE, 8, 265 EGL14.EGL_BLUE_SIZE, 8, 266 EGL14.EGL_ALPHA_SIZE, 8, 267 //EGL14.EGL_DEPTH_SIZE, 16, 268 //EGL14.EGL_STENCIL_SIZE, 8, 269 EGL14.EGL_RENDERABLE_TYPE, renderableType, 270 EGL14.EGL_NONE, 0, // placeholder for recordable [@-3] 271 EGL14.EGL_NONE 272 }; 273 if ((flags & FLAG_RECORDABLE) != 0) { 274 attribList[attribList.length - 3] = EGL_RECORDABLE_ANDROID; 275 attribList[attribList.length - 2] = 1; 276 } 277 EGLConfig[] configs = new EGLConfig[1]; 278 int[] numConfigs = new int[1]; 279 if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length, 280 numConfigs, 0)) { 281 Log.w(TAG, "unable to find RGB8888 / " + version + " EGLConfig"); 282 return null; 283 } 284 return configs[0]; 285 } 286 287 /** 288 * Discard all resources held by this class, notably the EGL context. 289 */ 290 public void release() { 291 if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) { 292 // Android is unusual in that it uses a reference-counted EGLDisplay. So for 293 // every eglInitialize() we need an eglTerminate(). 294 EGL14.eglDestroyContext(mEGLDisplay, mEGLContext); 295 EGL14.eglReleaseThread(); 296 EGL14.eglTerminate(mEGLDisplay); 297 } 298 299 mEGLDisplay = EGL14.EGL_NO_DISPLAY; 300 mEGLContext = EGL14.EGL_NO_CONTEXT; 301 mEGLConfig = null; 302 } 303 304 @Override 305 protected void finalize() throws Throwable { 306 try { 307 if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) { 308 // We're limited here -- finalizers don't run on the thread that holds 309 // the EGL state, so if a surface or context is still current on another 310 // thread we can't fully release it here. Exceptions thrown from here 311 // are quietly discarded. Complain in the log file. 312 Log.w(TAG, "WARNING: EglCore was not explicitly released -- state may be leaked"); 313 release(); 314 } 315 } finally { 316 super.finalize(); 317 } 318 } 319 320 /** 321 * Destroys the specified surface. Note the EGLSurface won't actually be destroyed if it's 322 * still current in a context. 323 */ 324 public void releaseSurface(EGLSurface eglSurface) { 325 EGL14.eglDestroySurface(mEGLDisplay, eglSurface); 326 } 327 328 /** 329 * Creates an EGL surface associated with a Surface. 330 * <p> 331 * If this is destined for MediaCodec, the EGLConfig should have the "recordable" attribute. 332 */ 333 public EGLSurface createWindowSurface(Object surface) { 334 if (!(surface instanceof Surface) && !(surface instanceof SurfaceTexture)) { 335 throw new RuntimeException("invalid surface: " + surface); 336 } 337 338 // Create a window surface, and attach it to the Surface we received. 339 int[] surfaceAttribs = { 340 EGL14.EGL_NONE 341 }; 342 EGLSurface eglSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, surface, 343 surfaceAttribs, 0); 344 checkEglError("eglCreateWindowSurface"); 345 if (eglSurface == null) { 346 throw new RuntimeException("surface was null"); 347 } 348 return eglSurface; 349 } 350 351 /** 352 * Creates an EGL surface associated with an offscreen buffer. 353 */ 354 public EGLSurface createOffscreenSurface(int width, int height) { 355 int[] surfaceAttribs = { 356 EGL14.EGL_WIDTH, width, 357 EGL14.EGL_HEIGHT, height, 358 EGL14.EGL_NONE 359 }; 360 EGLSurface eglSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, mEGLConfig, 361 surfaceAttribs, 0); 362 checkEglError("eglCreatePbufferSurface"); 363 if (eglSurface == null) { 364 throw new RuntimeException("surface was null"); 365 } 366 return eglSurface; 367 } 368 369 /** 370 * Makes our EGL context current, using the supplied surface for both "draw" and "read". 371 */ 372 public void makeCurrent(EGLSurface eglSurface) { 373 if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { 374 // called makeCurrent() before create? 375 Log.d(TAG, "NOTE: makeCurrent w/o display"); 376 } 377 if (!EGL14.eglMakeCurrent(mEGLDisplay, eglSurface, eglSurface, mEGLContext)) { 378 throw new RuntimeException("eglMakeCurrent failed"); 379 } 380 } 381 382 /** 383 * Makes our EGL context current, using the supplied "draw" and "read" surfaces. 384 */ 385 public void makeCurrent(EGLSurface drawSurface, EGLSurface readSurface) { 386 if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { 387 // called makeCurrent() before create? 388 Log.d(TAG, "NOTE: makeCurrent w/o display"); 389 } 390 if (!EGL14.eglMakeCurrent(mEGLDisplay, drawSurface, readSurface, mEGLContext)) { 391 throw new RuntimeException("eglMakeCurrent(draw,read) failed"); 392 } 393 } 394 395 /** 396 * Makes no context current. 397 */ 398 public void makeNothingCurrent() { 399 if (!EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, 400 EGL14.EGL_NO_CONTEXT)) { 401 throw new RuntimeException("eglMakeCurrent failed"); 402 } 403 } 404 405 /** 406 * Calls eglSwapBuffers. Use this to "publish" the current frame. 407 * 408 * @return false on failure 409 */ 410 public boolean swapBuffers(EGLSurface eglSurface) { 411 return EGL14.eglSwapBuffers(mEGLDisplay, eglSurface); 412 } 413 414 /** 415 * Sends the presentation time stamp to EGL. Time is expressed in nanoseconds. 416 */ 417 public void setPresentationTime(EGLSurface eglSurface, long nsecs) { 418 EGLExt.eglPresentationTimeANDROID(mEGLDisplay, eglSurface, nsecs); 419 } 420 421 /** 422 * Returns true if our context and the specified surface are current. 423 */ 424 public boolean isCurrent(EGLSurface eglSurface) { 425 return mEGLContext.equals(EGL14.eglGetCurrentContext()) && 426 eglSurface.equals(EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW)); 427 } 428 429 /** 430 * Performs a simple surface query. 431 */ 432 public int querySurface(EGLSurface eglSurface, int what) { 433 int[] value = new int[1]; 434 EGL14.eglQuerySurface(mEGLDisplay, eglSurface, what, value, 0); 435 return value[0]; 436 } 437 438 /** 439 * Returns the GLES version this context is configured for (2 or 3). 440 */ 441 public int getGlVersion() { 442 return mGlVersion; 443 } 444 445 /** 446 * Writes the current display, context, and surface to the log. 447 */ 448 public static void logCurrent(String msg) { 449 EGLDisplay display; 450 EGLContext context; 451 EGLSurface surface; 452 453 display = EGL14.eglGetCurrentDisplay(); 454 context = EGL14.eglGetCurrentContext(); 455 surface = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW); 456 Log.i(TAG, "Current EGL (" + msg + "): display=" + display + ", context=" + context + 457 ", surface=" + surface); 458 } 459 460 /** 461 * Checks for EGL errors. 462 */ 463 private void checkEglError(String msg) { 464 int error; 465 if ((error = EGL14.eglGetError()) != EGL14.EGL_SUCCESS) { 466 throw new RuntimeException(msg + ": EGL error: 0x" + Integer.toHexString(error)); 467 } 468 } 469 } 470 471 472 /** 473 * Common base class for EGL surfaces. 474 * <p> 475 * There can be multiple surfaces associated with a single context. 476 */ 477 private static class EglSurfaceBase { 478 // EglCore object we're associated with. It may be associated with multiple surfaces. 479 protected EglCore mEglCore; 480 481 private EGLSurface mEGLSurface = EGL14.EGL_NO_SURFACE; 482 private int mWidth = -1; 483 private int mHeight = -1; 484 485 protected EglSurfaceBase(EglCore eglCore) { 486 mEglCore = eglCore; 487 } 488 489 /** 490 * Creates a window surface. 491 * <p> 492 * @param surface May be a Surface or SurfaceTexture. 493 */ 494 public void createWindowSurface(Object surface) { 495 if (mEGLSurface != EGL14.EGL_NO_SURFACE) { 496 throw new IllegalStateException("surface already created"); 497 } 498 mEGLSurface = mEglCore.createWindowSurface(surface); 499 mWidth = mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH); 500 mHeight = mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT); 501 } 502 503 /** 504 * Creates an off-screen surface. 505 */ 506 public void createOffscreenSurface(int width, int height) { 507 if (mEGLSurface != EGL14.EGL_NO_SURFACE) { 508 throw new IllegalStateException("surface already created"); 509 } 510 mEGLSurface = mEglCore.createOffscreenSurface(width, height); 511 mWidth = width; 512 mHeight = height; 513 } 514 515 /** 516 * Returns the surface's width, in pixels. 517 */ 518 public int getWidth() { 519 return mWidth; 520 } 521 522 /** 523 * Returns the surface's height, in pixels. 524 */ 525 public int getHeight() { 526 return mHeight; 527 } 528 529 /** 530 * Release the EGL surface. 531 */ 532 public void releaseEglSurface() { 533 mEglCore.releaseSurface(mEGLSurface); 534 mEGLSurface = EGL14.EGL_NO_SURFACE; 535 mWidth = mHeight = -1; 536 } 537 538 /** 539 * Makes our EGL context and surface current. 540 */ 541 public void makeCurrent() { 542 mEglCore.makeCurrent(mEGLSurface); 543 } 544 545 /** 546 * Makes our EGL context and surface current for drawing, using the supplied surface 547 * for reading. 548 */ 549 public void makeCurrentReadFrom(EglSurfaceBase readSurface) { 550 mEglCore.makeCurrent(mEGLSurface, readSurface.mEGLSurface); 551 } 552 553 /** 554 * Calls eglSwapBuffers. Use this to "publish" the current frame. 555 * 556 * @return false on failure 557 */ 558 public boolean swapBuffers() { 559 boolean result = mEglCore.swapBuffers(mEGLSurface); 560 if (!result) { 561 Log.d(TAG, "WARNING: swapBuffers() failed"); 562 } 563 return result; 564 } 565 566 /** 567 * Sends the presentation time stamp to EGL. 568 * 569 * @param nsecs Timestamp, in nanoseconds. 570 */ 571 public void setPresentationTime(long nsecs) { 572 mEglCore.setPresentationTime(mEGLSurface, nsecs); 573 } 574 575 /** 576 * Saves the EGL surface to a file. 577 * <p> 578 * Expects that this object's EGL surface is current. 579 */ 580 public void saveFrame(File file) throws IOException { 581 if (!mEglCore.isCurrent(mEGLSurface)) { 582 throw new RuntimeException("Expected EGL context/surface is not current"); 583 } 584 585 // glReadPixels gives us a ByteBuffer filled with what is essentially big-endian RGBA 586 // data (i.e. a byte of red, followed by a byte of green...). We need an int[] filled 587 // with little-endian ARGB data to feed to Bitmap. 588 // 589 // If we implement this as a series of buf.get() calls, we can spend 2.5 seconds just 590 // copying data around for a 720p frame. It's better to do a bulk get() and then 591 // rearrange the data in memory. (For comparison, the PNG compress takes about 500ms 592 // for a trivial frame.) 593 // 594 // So... we set the ByteBuffer to little-endian, which should turn the bulk IntBuffer 595 // get() into a straight memcpy on most Android devices. Our ints will hold ABGR data. 596 // Swapping B and R gives us ARGB. 597 // 598 // Making this even more interesting is the upside-down nature of GL, which means 599 // our output will look upside-down relative to what appears on screen if the 600 // typical GL conventions are used. 601 602 String filename = file.toString(); 603 604 ByteBuffer buf = ByteBuffer.allocateDirect(mWidth * mHeight * 4); 605 buf.order(ByteOrder.LITTLE_ENDIAN); 606 GLES20.glReadPixels(0, 0, mWidth, mHeight, 607 GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf); 608 checkGlError("glReadPixels"); 609 buf.rewind(); 610 611 int pixelCount = mWidth * mHeight; 612 int[] colors = new int[pixelCount]; 613 buf.asIntBuffer().get(colors); 614 for (int i = 0; i < pixelCount; i++) { 615 int c = colors[i]; 616 colors[i] = (c & 0xff00ff00) | ((c & 0x00ff0000) >> 16) | ((c & 0x000000ff) << 16); 617 } 618 619 BufferedOutputStream bos = null; 620 try { 621 bos = new BufferedOutputStream(new FileOutputStream(filename)); 622 Bitmap bmp = Bitmap.createBitmap(colors, mWidth, mHeight, Bitmap.Config.ARGB_8888); 623 bmp.compress(Bitmap.CompressFormat.PNG, 90, bos); 624 bmp.recycle(); 625 } finally { 626 if (bos != null) bos.close(); 627 } 628 Log.d(TAG, "Saved " + mWidth + "x" + mHeight + " frame as '" + filename + "'"); 629 } 630 } 631 632 /** 633 * Off-screen EGL surface (pbuffer). 634 * <p> 635 * It's good practice to explicitly release() the surface, preferably from a "finally" block. 636 */ 637 private static class OffscreenSurface extends EglSurfaceBase { 638 /** 639 * Creates an off-screen surface with the specified width and height. 640 */ 641 public OffscreenSurface(EglCore eglCore, int width, int height) { 642 super(eglCore); 643 createOffscreenSurface(width, height); 644 } 645 646 /** 647 * Releases any resources associated with the surface. 648 */ 649 public void release() { 650 releaseEglSurface(); 651 } 652 } 653 } 654