1 /* 2 * Copyright 2015 The WebRTC project authors. All Rights Reserved. 3 * 4 * Use of this source code is governed by a BSD-style license 5 * that can be found in the LICENSE file in the root of the source 6 * tree. An additional intellectual property rights grant can be found 7 * in the file PATENTS. All contributing project authors may 8 * be found in the AUTHORS file in the root of the source tree. 9 */ 10 11 package org.webrtc; 12 13 import static org.junit.Assert.assertEquals; 14 import static org.junit.Assert.assertTrue; 15 import static org.junit.Assert.fail; 16 17 import android.opengl.GLES20; 18 import androidx.test.filters.MediumTest; 19 import androidx.test.filters.SmallTest; 20 import java.nio.ByteBuffer; 21 import java.util.Random; 22 import org.junit.Test; 23 24 public class GlRectDrawerTest { 25 // Resolution of the test image. 26 private static final int WIDTH = 16; 27 private static final int HEIGHT = 16; 28 // Seed for random pixel creation. 29 private static final int SEED = 42; 30 // When comparing pixels, allow some slack for float arithmetic and integer rounding. 31 private static final float MAX_DIFF = 1.5f; 32 33 // clang-format off 34 private static final float[] IDENTITY_MATRIX = { 35 1, 0, 0, 0, 36 0, 1, 0, 0, 37 0, 0, 1, 0, 38 0, 0, 0, 1}; 39 // clang-format on 40 normalizedByte(byte b)41 private static float normalizedByte(byte b) { 42 return (b & 0xFF) / 255.0f; 43 } 44 saturatedConvert(float c)45 private static float saturatedConvert(float c) { 46 return 255.0f * Math.max(0, Math.min(c, 1)); 47 } 48 49 // Assert RGB ByteBuffers are pixel perfect identical. assertByteBufferEquals( int width, int height, ByteBuffer actual, ByteBuffer expected)50 private static void assertByteBufferEquals( 51 int width, int height, ByteBuffer actual, ByteBuffer expected) { 52 actual.rewind(); 53 expected.rewind(); 54 assertEquals(actual.remaining(), width * height * 3); 55 assertEquals(expected.remaining(), width * height * 3); 56 for (int y = 0; y < height; ++y) { 57 for (int x = 0; x < width; ++x) { 58 final int actualR = actual.get() & 0xFF; 59 final int actualG = actual.get() & 0xFF; 60 final int actualB = actual.get() & 0xFF; 61 final int expectedR = expected.get() & 0xFF; 62 final int expectedG = expected.get() & 0xFF; 63 final int expectedB = expected.get() & 0xFF; 64 if (actualR != expectedR || actualG != expectedG || actualB != expectedB) { 65 fail("ByteBuffers of size " + width + "x" + height + " not equal at position " 66 + "(" + x + ", " + y + "). Expected color (R,G,B): " 67 + "(" + expectedR + ", " + expectedG + ", " + expectedB + ")" 68 + " but was: " 69 + "(" + actualR + ", " + actualG + ", " + actualB + ")."); 70 } 71 } 72 } 73 } 74 75 // Convert RGBA ByteBuffer to RGB ByteBuffer. stripAlphaChannel(ByteBuffer rgbaBuffer)76 private static ByteBuffer stripAlphaChannel(ByteBuffer rgbaBuffer) { 77 rgbaBuffer.rewind(); 78 assertEquals(rgbaBuffer.remaining() % 4, 0); 79 final int numberOfPixels = rgbaBuffer.remaining() / 4; 80 final ByteBuffer rgbBuffer = ByteBuffer.allocateDirect(numberOfPixels * 3); 81 while (rgbaBuffer.hasRemaining()) { 82 // Copy RGB. 83 for (int channel = 0; channel < 3; ++channel) { 84 rgbBuffer.put(rgbaBuffer.get()); 85 } 86 // Drop alpha. 87 rgbaBuffer.get(); 88 } 89 return rgbBuffer; 90 } 91 92 // TODO(titovartem) make correct fix during webrtc:9175 93 @SuppressWarnings("ByteBufferBackingArray") 94 @Test 95 @SmallTest testRgbRendering()96 public void testRgbRendering() { 97 // Create EGL base with a pixel buffer as display output. 98 final EglBase eglBase = EglBase.create(null, EglBase.CONFIG_PIXEL_BUFFER); 99 eglBase.createPbufferSurface(WIDTH, HEIGHT); 100 eglBase.makeCurrent(); 101 102 // Create RGB byte buffer plane with random content. 103 final ByteBuffer rgbPlane = ByteBuffer.allocateDirect(WIDTH * HEIGHT * 3); 104 final Random random = new Random(SEED); 105 random.nextBytes(rgbPlane.array()); 106 107 // Upload the RGB byte buffer data as a texture. 108 final int rgbTexture = GlUtil.generateTexture(GLES20.GL_TEXTURE_2D); 109 GLES20.glActiveTexture(GLES20.GL_TEXTURE0); 110 GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, rgbTexture); 111 GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGB, WIDTH, HEIGHT, 0, GLES20.GL_RGB, 112 GLES20.GL_UNSIGNED_BYTE, rgbPlane); 113 GlUtil.checkNoGLES2Error("glTexImage2D"); 114 115 // Draw the RGB frame onto the pixel buffer. 116 final GlRectDrawer drawer = new GlRectDrawer(); 117 drawer.drawRgb(rgbTexture, IDENTITY_MATRIX, WIDTH, HEIGHT, 0 /* viewportX */, 0 /* viewportY */, 118 WIDTH, HEIGHT); 119 120 // Download the pixels in the pixel buffer as RGBA. Not all platforms support RGB, e.g. Nexus 9. 121 final ByteBuffer rgbaData = ByteBuffer.allocateDirect(WIDTH * HEIGHT * 4); 122 GLES20.glReadPixels(0, 0, WIDTH, HEIGHT, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, rgbaData); 123 GlUtil.checkNoGLES2Error("glReadPixels"); 124 125 // Assert rendered image is pixel perfect to source RGB. 126 assertByteBufferEquals(WIDTH, HEIGHT, stripAlphaChannel(rgbaData), rgbPlane); 127 128 drawer.release(); 129 GLES20.glDeleteTextures(1, new int[] {rgbTexture}, 0); 130 eglBase.release(); 131 } 132 133 // TODO(titovartem) make correct fix during webrtc:9175 134 @SuppressWarnings("ByteBufferBackingArray") 135 @Test 136 @SmallTest testYuvRendering()137 public void testYuvRendering() { 138 // Create EGL base with a pixel buffer as display output. 139 EglBase eglBase = EglBase.create(null, EglBase.CONFIG_PIXEL_BUFFER); 140 eglBase.createPbufferSurface(WIDTH, HEIGHT); 141 eglBase.makeCurrent(); 142 143 // Create YUV byte buffer planes with random content. 144 final ByteBuffer[] yuvPlanes = new ByteBuffer[3]; 145 final Random random = new Random(SEED); 146 for (int i = 0; i < 3; ++i) { 147 yuvPlanes[i] = ByteBuffer.allocateDirect(WIDTH * HEIGHT); 148 random.nextBytes(yuvPlanes[i].array()); 149 } 150 151 // Generate 3 texture ids for Y/U/V. 152 final int yuvTextures[] = new int[3]; 153 for (int i = 0; i < 3; i++) { 154 yuvTextures[i] = GlUtil.generateTexture(GLES20.GL_TEXTURE_2D); 155 } 156 157 // Upload the YUV byte buffer data as textures. 158 for (int i = 0; i < 3; ++i) { 159 GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i); 160 GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yuvTextures[i]); 161 GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, WIDTH, HEIGHT, 0, 162 GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, yuvPlanes[i]); 163 GlUtil.checkNoGLES2Error("glTexImage2D"); 164 } 165 166 // Draw the YUV frame onto the pixel buffer. 167 final GlRectDrawer drawer = new GlRectDrawer(); 168 drawer.drawYuv(yuvTextures, IDENTITY_MATRIX, WIDTH, HEIGHT, 0 /* viewportX */, 169 0 /* viewportY */, WIDTH, HEIGHT); 170 171 // Download the pixels in the pixel buffer as RGBA. Not all platforms support RGB, e.g. Nexus 9. 172 final ByteBuffer data = ByteBuffer.allocateDirect(WIDTH * HEIGHT * 4); 173 GLES20.glReadPixels(0, 0, WIDTH, HEIGHT, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, data); 174 GlUtil.checkNoGLES2Error("glReadPixels"); 175 176 // Compare the YUV data with the RGBA result. 177 for (int y = 0; y < HEIGHT; ++y) { 178 for (int x = 0; x < WIDTH; ++x) { 179 // YUV color space. Y in [0, 1], UV in [-0.5, 0.5]. The constants are taken from the YUV 180 // fragment shader code in GlGenericDrawer. 181 final float y_luma = normalizedByte(yuvPlanes[0].get()); 182 final float u_chroma = normalizedByte(yuvPlanes[1].get()); 183 final float v_chroma = normalizedByte(yuvPlanes[2].get()); 184 // Expected color in unrounded RGB [0.0f, 255.0f]. 185 final float expectedRed = 186 saturatedConvert(1.16438f * y_luma + 1.59603f * v_chroma - 0.874202f); 187 final float expectedGreen = saturatedConvert( 188 1.16438f * y_luma - 0.391762f * u_chroma - 0.812968f * v_chroma + 0.531668f); 189 final float expectedBlue = 190 saturatedConvert(1.16438f * y_luma + 2.01723f * u_chroma - 1.08563f); 191 192 // Actual color in RGB8888. 193 final int actualRed = data.get() & 0xFF; 194 final int actualGreen = data.get() & 0xFF; 195 final int actualBlue = data.get() & 0xFF; 196 final int actualAlpha = data.get() & 0xFF; 197 198 // Assert rendered image is close to pixel perfect from source YUV. 199 assertTrue(Math.abs(actualRed - expectedRed) < MAX_DIFF); 200 assertTrue(Math.abs(actualGreen - expectedGreen) < MAX_DIFF); 201 assertTrue(Math.abs(actualBlue - expectedBlue) < MAX_DIFF); 202 assertEquals(actualAlpha, 255); 203 } 204 } 205 206 drawer.release(); 207 GLES20.glDeleteTextures(3, yuvTextures, 0); 208 eglBase.release(); 209 } 210 211 /** 212 * The purpose here is to test GlRectDrawer.oesDraw(). Unfortunately, there is no easy way to 213 * create an OES texture, which is needed for input to oesDraw(). Most of the test is concerned 214 * with creating OES textures in the following way: 215 * - Create SurfaceTexture with help from SurfaceTextureHelper. 216 * - Create an EglBase with the SurfaceTexture as EGLSurface. 217 * - Upload RGB texture with known content. 218 * - Draw the RGB texture onto the EglBase with the SurfaceTexture as target. 219 * - Wait for an OES texture to be produced. 220 * The actual oesDraw() test is this: 221 * - Create an EglBase with a pixel buffer as target. 222 * - Render the OES texture onto the pixel buffer. 223 * - Read back the pixel buffer and compare it with the known RGB data. 224 */ 225 // TODO(titovartem) make correct fix during webrtc:9175 226 @SuppressWarnings("ByteBufferBackingArray") 227 @Test 228 @MediumTest 229 public void testOesRendering() throws InterruptedException { 230 /** 231 * Stub class to convert RGB ByteBuffers to OES textures by drawing onto a SurfaceTexture. 232 */ 233 class StubOesTextureProducer { 234 private final EglBase eglBase; 235 private final GlRectDrawer drawer; 236 private final int rgbTexture; 237 238 public StubOesTextureProducer(EglBase.Context sharedContext, 239 SurfaceTextureHelper surfaceTextureHelper, int width, int height) { 240 eglBase = EglBase.create(sharedContext, EglBase.CONFIG_PLAIN); 241 surfaceTextureHelper.setTextureSize(width, height); 242 eglBase.createSurface(surfaceTextureHelper.getSurfaceTexture()); 243 assertEquals(eglBase.surfaceWidth(), width); 244 assertEquals(eglBase.surfaceHeight(), height); 245 246 drawer = new GlRectDrawer(); 247 248 eglBase.makeCurrent(); 249 rgbTexture = GlUtil.generateTexture(GLES20.GL_TEXTURE_2D); 250 } 251 252 public void draw(ByteBuffer rgbPlane) { 253 eglBase.makeCurrent(); 254 255 // Upload RGB data to texture. 256 GLES20.glActiveTexture(GLES20.GL_TEXTURE0); 257 GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, rgbTexture); 258 GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGB, WIDTH, HEIGHT, 0, GLES20.GL_RGB, 259 GLES20.GL_UNSIGNED_BYTE, rgbPlane); 260 // Draw the RGB data onto the SurfaceTexture. 261 drawer.drawRgb(rgbTexture, IDENTITY_MATRIX, WIDTH, HEIGHT, 0 /* viewportX */, 262 0 /* viewportY */, WIDTH, HEIGHT); 263 eglBase.swapBuffers(); 264 } 265 266 public void release() { 267 eglBase.makeCurrent(); 268 drawer.release(); 269 GLES20.glDeleteTextures(1, new int[] {rgbTexture}, 0); 270 eglBase.release(); 271 } 272 } 273 274 // Create EGL base with a pixel buffer as display output. 275 final EglBase eglBase = EglBase.create(null, EglBase.CONFIG_PIXEL_BUFFER); 276 eglBase.createPbufferSurface(WIDTH, HEIGHT); 277 278 // Create resources for generating OES textures. 279 final SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create( 280 "SurfaceTextureHelper test" /* threadName */, eglBase.getEglBaseContext()); 281 final StubOesTextureProducer oesProducer = new StubOesTextureProducer( 282 eglBase.getEglBaseContext(), surfaceTextureHelper, WIDTH, HEIGHT); 283 final SurfaceTextureHelperTest.MockTextureListener listener = 284 new SurfaceTextureHelperTest.MockTextureListener(); 285 surfaceTextureHelper.startListening(listener); 286 287 // Create RGB byte buffer plane with random content. 288 final ByteBuffer rgbPlane = ByteBuffer.allocateDirect(WIDTH * HEIGHT * 3); 289 final Random random = new Random(SEED); 290 random.nextBytes(rgbPlane.array()); 291 292 // Draw the frame and block until an OES texture is delivered. 293 oesProducer.draw(rgbPlane); 294 final VideoFrame.TextureBuffer textureBuffer = listener.waitForTextureBuffer(); 295 296 // Real test starts here. 297 // Draw the OES texture on the pixel buffer. 298 eglBase.makeCurrent(); 299 final GlRectDrawer drawer = new GlRectDrawer(); 300 drawer.drawOes(textureBuffer.getTextureId(), 301 RendererCommon.convertMatrixFromAndroidGraphicsMatrix(textureBuffer.getTransformMatrix()), 302 WIDTH, HEIGHT, 0 /* viewportX */, 0 /* viewportY */, WIDTH, HEIGHT); 303 304 // Download the pixels in the pixel buffer as RGBA. Not all platforms support RGB, e.g. Nexus 9. 305 final ByteBuffer rgbaData = ByteBuffer.allocateDirect(WIDTH * HEIGHT * 4); 306 GLES20.glReadPixels(0, 0, WIDTH, HEIGHT, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, rgbaData); 307 GlUtil.checkNoGLES2Error("glReadPixels"); 308 309 // Assert rendered image is pixel perfect to source RGB. 310 assertByteBufferEquals(WIDTH, HEIGHT, stripAlphaChannel(rgbaData), rgbPlane); 311 312 drawer.release(); 313 textureBuffer.release(); 314 oesProducer.release(); 315 surfaceTextureHelper.dispose(); 316 eglBase.release(); 317 } 318 } 319