1 /* 2 * Copyright (C) 2021 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.mediav2.common.cts; 18 19 import static org.junit.Assert.assertTrue; 20 import static org.junit.Assume.assumeFalse; 21 22 import android.graphics.SurfaceTexture; 23 import android.opengl.EGL14; 24 import android.opengl.EGLConfig; 25 import android.opengl.EGLContext; 26 import android.opengl.EGLDisplay; 27 import android.opengl.EGLSurface; 28 import android.opengl.GLES20; 29 import android.util.Log; 30 import android.view.Surface; 31 32 33 /** 34 * Holds state associated with a Surface used for MediaCodec decoder output. 35 * <p> 36 * The (width,height) constructor for this class will prepare GL, create a SurfaceTexture, 37 * and then create a Surface for that SurfaceTexture. The Surface can be passed to 38 * MediaCodec.configure() to receive decoder output. When a frame arrives, we latch the 39 * texture with updateTexImage, then render the texture with GL to a pbuffer. 40 * <p> 41 * The no-arg constructor skips the GL preparation step and doesn't allocate a pbuffer. 42 * Instead, it just creates the Surface and SurfaceTexture, and when a frame arrives 43 * we just draw it on whatever surface is current. 44 * <p> 45 * By default, the Surface will be using a BufferQueue in asynchronous mode, so we 46 * can potentially drop frames. 47 */ 48 public class OutputSurface implements SurfaceTexture.OnFrameAvailableListener { 49 private static final String TAG = "OutputSurface"; 50 private static final boolean VERBOSE = false; 51 52 private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY; 53 private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT; 54 private EGLSurface mEGLSurface = EGL14.EGL_NO_SURFACE; 55 56 private SurfaceTexture mSurfaceTexture; 57 private Surface mSurface; 58 59 private Object mFrameSyncObject = new Object(); // guards mFrameAvailable 60 private boolean mFrameAvailable; 61 62 private TextureRender mTextureRender; 63 private int mEGLESVersion; 64 private boolean mEXTYuvTargetSupported = false; 65 66 /** 67 * Creates an OutputSurface backed by a pbuffer with the specified dimensions. The new 68 * EGL context and surface will be made current. Creates a Surface that can be passed 69 * to MediaCodec.configure(). 70 */ OutputSurface(int width, int height, boolean useHighBitDepth)71 public OutputSurface(int width, int height, boolean useHighBitDepth) { 72 this(width, height, useHighBitDepth, /* useYuvSampling */ false); 73 } 74 OutputSurface(int width, int height, boolean useHighBitDepth, boolean useYuvSampling)75 public OutputSurface(int width, int height, boolean useHighBitDepth, boolean useYuvSampling) { 76 if (width <= 0 || height <= 0) { 77 throw new IllegalArgumentException(); 78 } 79 80 eglSetup(width, height, useHighBitDepth, useYuvSampling); 81 makeCurrent(); 82 83 if (mEGLESVersion > 2) { 84 String extensionList = GLES20.glGetString(GLES20.GL_EXTENSIONS); 85 mEXTYuvTargetSupported = extensionList.contains("GL_EXT_YUV_target"); 86 } 87 88 setup(this, useYuvSampling); 89 } 90 91 /** 92 * Creates an OutputSurface using the current EGL context (rather than establishing a 93 * new one). Creates a Surface that can be passed to MediaCodec.configure(). 94 */ OutputSurface()95 public OutputSurface() { 96 setup(this, /* useYuvSampling */ false); 97 } 98 OutputSurface(final SurfaceTexture.OnFrameAvailableListener listener)99 public OutputSurface(final SurfaceTexture.OnFrameAvailableListener listener) { 100 setup(listener, /* useYuvSampling */ false); 101 } 102 103 /** 104 * Returns if the device support GL_EXT_YUV_target extension 105 */ getEXTYuvTargetSupported()106 public boolean getEXTYuvTargetSupported() { 107 return mEXTYuvTargetSupported; 108 } 109 110 /** 111 * Creates instances of TextureRender and SurfaceTexture, and a Surface associated 112 * with the SurfaceTexture. 113 */ setup(SurfaceTexture.OnFrameAvailableListener listener, boolean useYuvSampling)114 private void setup(SurfaceTexture.OnFrameAvailableListener listener, boolean useYuvSampling) { 115 assertTrue(EGL14.eglGetCurrentContext() != EGL14.EGL_NO_CONTEXT); 116 assertTrue(EGL14.eglGetCurrentDisplay() != EGL14.EGL_NO_DISPLAY); 117 assertTrue(EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW) != EGL14.EGL_NO_SURFACE); 118 assertTrue(EGL14.eglGetCurrentSurface(EGL14.EGL_READ) != EGL14.EGL_NO_SURFACE); 119 mTextureRender = new TextureRender(); 120 mTextureRender.setUseYuvSampling(mEXTYuvTargetSupported && useYuvSampling); 121 mTextureRender.surfaceCreated(); 122 123 // Even if we don't access the SurfaceTexture after the constructor returns, we 124 // still need to keep a reference to it. The Surface doesn't retain a reference 125 // at the Java level, so if we don't either then the object can get GCed, which 126 // causes the native finalizer to run. 127 if (VERBOSE) Log.d(TAG, "textureID=" + mTextureRender.getTextureId()); 128 mSurfaceTexture = new SurfaceTexture(mTextureRender.getTextureId()); 129 130 // This doesn't work if OutputSurface is created on the thread that CTS started for 131 // these test cases. 132 // 133 // The CTS-created thread has a Looper, and the SurfaceTexture constructor will 134 // create a Handler that uses it. The "frame available" message is delivered 135 // there, but since we're not a Looper-based thread we'll never see it. For 136 // this to do anything useful, OutputSurface must be created on a thread without 137 // a Looper, so that SurfaceTexture uses the main application Looper instead. 138 // 139 // Java language note: passing "this" out of a constructor is generally unwise, 140 // but we should be able to get away with it here. 141 mSurfaceTexture.setOnFrameAvailableListener(listener); 142 143 mSurface = new Surface(mSurfaceTexture); 144 } 145 146 /** 147 * Prepares EGL. We want a GLES 2.0 context and a surface that supports pbuffer. 148 */ eglSetup(int width, int height, boolean useHighBitDepth, boolean useYuvSampling)149 private void eglSetup(int width, int height, boolean useHighBitDepth, boolean useYuvSampling) { 150 mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); 151 if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { 152 throw new RuntimeException("unable to get EGL14 display"); 153 } 154 int[] version = new int[2]; 155 if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) { 156 mEGLDisplay = null; 157 throw new RuntimeException("unable to initialize EGL14"); 158 } 159 160 // Configure EGL for pbuffer and OpenGL ES 2.0. We want enough RGB bits 161 // to be able to tell if the frame is reasonable. 162 int eglColorSize = useHighBitDepth ? 10 : 8; 163 int eglAlphaSize = useHighBitDepth ? 2 : 0; 164 int[] attribList = { 165 EGL14.EGL_RED_SIZE, eglColorSize, 166 EGL14.EGL_GREEN_SIZE, eglColorSize, 167 EGL14.EGL_BLUE_SIZE, eglColorSize, 168 EGL14.EGL_ALPHA_SIZE, eglAlphaSize, 169 EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, 170 EGL14.EGL_SURFACE_TYPE, EGL14.EGL_PBUFFER_BIT, 171 EGL14.EGL_NONE 172 }; 173 EGLConfig[] configs = new EGLConfig[1]; 174 int[] numConfigs = new int[1]; 175 if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length, 176 numConfigs, 0) || numConfigs[0] == 0) { 177 String message = "Unable to find EGL config supporting renderable-type:ES2 " 178 + "surface-type:pbuffer r:" + eglColorSize + " g:" + eglColorSize 179 + " b:" + eglColorSize + " a:" + eglAlphaSize + "."; 180 // When eglChooseConfig fails for RGBA10102, skip high bit depth testing as it is not 181 // mandatory for devices to support this configuration. 182 assumeFalse(message + " Skipping the test for high bit depth case", useHighBitDepth); 183 throw new RuntimeException(message); 184 } 185 186 // Configure context for OpenGL ES 3.0/2.0. 187 mEGLESVersion = useYuvSampling ? 3 : 2; 188 do { 189 int[] attrib_list = { 190 EGL14.EGL_CONTEXT_CLIENT_VERSION, mEGLESVersion, 191 EGL14.EGL_NONE 192 }; 193 mEGLContext = EGL14.eglCreateContext(mEGLDisplay, configs[0], EGL14.EGL_NO_CONTEXT, 194 attrib_list, 0); 195 // if OpenGL ES 3.0 isn't supported, attempt to create OpenGL ES 2.0 context 196 if (mEGLContext == EGL14.EGL_NO_CONTEXT && useYuvSampling) { 197 mEGLESVersion--; 198 } else { 199 break; 200 } 201 } while (mEGLESVersion > 1); 202 203 checkEglError("eglCreateContext"); 204 if (mEGLContext == null) { 205 throw new RuntimeException("null context"); 206 } 207 208 // Create a pbuffer surface. By using this for output, we can use glReadPixels 209 // to test values in the output. 210 int[] surfaceAttribs = { 211 EGL14.EGL_WIDTH, width, 212 EGL14.EGL_HEIGHT, height, 213 EGL14.EGL_NONE 214 }; 215 mEGLSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, configs[0], surfaceAttribs, 0); 216 checkEglError("eglCreatePbufferSurface"); 217 if (mEGLSurface == null) { 218 throw new RuntimeException("surface was null"); 219 } 220 } 221 222 /** 223 * Discard all resources held by this class, notably the EGL context. 224 */ release()225 public void release() { 226 if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) { 227 EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface); 228 EGL14.eglDestroyContext(mEGLDisplay, mEGLContext); 229 EGL14.eglReleaseThread(); 230 EGL14.eglTerminate(mEGLDisplay); 231 } 232 233 mSurface.release(); 234 235 mSurfaceTexture.release(); 236 237 mEGLDisplay = EGL14.EGL_NO_DISPLAY; 238 mEGLContext = EGL14.EGL_NO_CONTEXT; 239 mEGLSurface = EGL14.EGL_NO_SURFACE; 240 241 mTextureRender = null; 242 mSurface = null; 243 mSurfaceTexture = null; 244 } 245 246 /** 247 * Makes our EGL context and surface current. 248 */ makeCurrent()249 public void makeCurrent() { 250 if (!EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) { 251 throw new RuntimeException("eglMakeCurrent failed"); 252 } 253 } 254 255 /** 256 * Returns the Surface that we draw onto. 257 */ getSurface()258 public Surface getSurface() { 259 return mSurface; 260 } 261 262 /** 263 * Replaces the fragment shader. 264 */ changeFragmentShader(String fragmentShader)265 public void changeFragmentShader(String fragmentShader) { 266 mTextureRender.changeFragmentShader(fragmentShader); 267 } 268 269 /** 270 * Latches the next buffer into the texture. Must be called from the thread that created 271 * the OutputSurface object, after the onFrameAvailable callback has signaled that new 272 * data is available. 273 */ awaitNewImage()274 public void awaitNewImage() { 275 final int timeOutMS = 2000; 276 277 synchronized (mFrameSyncObject) { 278 while (!mFrameAvailable) { 279 try { 280 // Wait for onFrameAvailable() to signal us. Use a timeout to avoid 281 // stalling the test if it doesn't arrive. 282 mFrameSyncObject.wait(timeOutMS); 283 if (!mFrameAvailable) { 284 // TODO: if "spurious wakeup", continue while loop 285 throw new RuntimeException("Surface frame wait timed out"); 286 } 287 } catch (InterruptedException ie) { 288 // shouldn't happen 289 throw new RuntimeException(ie); 290 } 291 } 292 mFrameAvailable = false; 293 } 294 295 // Latch the data. 296 mTextureRender.checkGlError("before updateTexImage"); 297 mSurfaceTexture.updateTexImage(); 298 } 299 300 /** 301 * Wait for new image to become available or until timeout, whichever comes first. 302 * @param timeoutMs 303 * @return true if new image is available. false for no new image until timeout. 304 */ checkForNewImage(int timeoutMs)305 public boolean checkForNewImage(int timeoutMs) { 306 synchronized (mFrameSyncObject) { 307 while (!mFrameAvailable) { 308 try { 309 // Wait for onFrameAvailable() to signal us. Use a timeout to avoid 310 // stalling the test if it doesn't arrive. 311 mFrameSyncObject.wait(timeoutMs); 312 if (!mFrameAvailable) { 313 return false; 314 } 315 } catch (InterruptedException ie) { 316 // shouldn't happen 317 throw new RuntimeException(ie); 318 } 319 } 320 mFrameAvailable = false; 321 } 322 323 // Latch the data. 324 mTextureRender.checkGlError("before updateTexImage"); 325 mSurfaceTexture.updateTexImage(); 326 return true; 327 } 328 329 /** 330 * Draws the data from SurfaceTexture onto the current EGL surface. 331 */ drawImage()332 public void drawImage() { 333 mTextureRender.drawFrame(mSurfaceTexture); 334 } 335 latchImage()336 public void latchImage() { 337 mTextureRender.checkGlError("before updateTexImage"); 338 mSurfaceTexture.updateTexImage(); 339 } 340 341 @Override onFrameAvailable(SurfaceTexture st)342 public void onFrameAvailable(SurfaceTexture st) { 343 if (VERBOSE) Log.d(TAG, "new frame available"); 344 synchronized (mFrameSyncObject) { 345 if (mFrameAvailable) { 346 throw new RuntimeException("mFrameAvailable already set, frame could be dropped"); 347 } 348 mFrameAvailable = true; 349 mFrameSyncObject.notifyAll(); 350 } 351 } 352 353 /** 354 * Checks for EGL errors. 355 */ checkEglError(String msg)356 private void checkEglError(String msg) { 357 int error = EGL14.eglGetError(); 358 if (error != EGL14.EGL_SUCCESS) { 359 throw new RuntimeException(msg + ": EGL error: 0x" + Integer.toHexString(error)); 360 } 361 } 362 } 363