Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 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 android.opengl.GLES11Ext; |
| 14 | import android.opengl.GLES20; |
| 15 | import java.nio.ByteBuffer; |
| 16 | import java.nio.FloatBuffer; |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 17 | import org.webrtc.VideoFrame.I420Buffer; |
| 18 | import org.webrtc.VideoFrame.TextureBuffer; |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 19 | |
| 20 | /** |
Magnus Jedvert | 1d270f8 | 2018-04-16 16:28:29 +0200 | [diff] [blame] | 21 | * Class for converting OES textures to a YUV ByteBuffer. It can be constructed on any thread, but |
| 22 | * should only be operated from a single thread with an active EGL context. |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 23 | */ |
Sami Kalliomäki | 6bf70d2 | 2017-10-17 09:22:23 +0200 | [diff] [blame] | 24 | public class YuvConverter { |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 25 | // Vertex coordinates in Normalized Device Coordinates, i.e. |
| 26 | // (-1, -1) is bottom-left and (1, 1) is top-right. |
| 27 | private static final FloatBuffer DEVICE_RECTANGLE = GlUtil.createFloatBuffer(new float[] { |
| 28 | -1.0f, -1.0f, // Bottom left. |
| 29 | 1.0f, -1.0f, // Bottom right. |
| 30 | -1.0f, 1.0f, // Top left. |
| 31 | 1.0f, 1.0f, // Top right. |
| 32 | }); |
| 33 | |
| 34 | // Texture coordinates - (0, 0) is bottom-left and (1, 1) is top-right. |
| 35 | private static final FloatBuffer TEXTURE_RECTANGLE = GlUtil.createFloatBuffer(new float[] { |
| 36 | 0.0f, 0.0f, // Bottom left. |
| 37 | 1.0f, 0.0f, // Bottom right. |
| 38 | 0.0f, 1.0f, // Top left. |
| 39 | 1.0f, 1.0f // Top right. |
| 40 | }); |
| 41 | |
| 42 | // clang-format off |
| 43 | private static final String VERTEX_SHADER = |
| 44 | "varying vec2 interp_tc;\n" |
| 45 | + "attribute vec4 in_pos;\n" |
| 46 | + "attribute vec4 in_tc;\n" |
| 47 | + "\n" |
| 48 | + "uniform mat4 texMatrix;\n" |
| 49 | + "\n" |
| 50 | + "void main() {\n" |
| 51 | + " gl_Position = in_pos;\n" |
| 52 | + " interp_tc = (texMatrix * in_tc).xy;\n" |
| 53 | + "}\n"; |
| 54 | |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 55 | private static final String OES_FRAGMENT_SHADER = |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 56 | "#extension GL_OES_EGL_image_external : require\n" |
| 57 | + "precision mediump float;\n" |
| 58 | + "varying vec2 interp_tc;\n" |
| 59 | + "\n" |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 60 | + "uniform samplerExternalOES tex;\n" |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 61 | // Difference in texture coordinate corresponding to one |
| 62 | // sub-pixel in the x direction. |
| 63 | + "uniform vec2 xUnit;\n" |
| 64 | // Color conversion coefficients, including constant term |
| 65 | + "uniform vec4 coeffs;\n" |
| 66 | + "\n" |
| 67 | + "void main() {\n" |
| 68 | // Since the alpha read from the texture is always 1, this could |
| 69 | // be written as a mat4 x vec4 multiply. However, that seems to |
| 70 | // give a worse framerate, possibly because the additional |
| 71 | // multiplies by 1.0 consume resources. TODO(nisse): Could also |
| 72 | // try to do it as a vec3 x mat3x4, followed by an add in of a |
| 73 | // constant vector. |
| 74 | + " gl_FragColor.r = coeffs.a + dot(coeffs.rgb,\n" |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 75 | + " texture2D(tex, interp_tc - 1.5 * xUnit).rgb);\n" |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 76 | + " gl_FragColor.g = coeffs.a + dot(coeffs.rgb,\n" |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 77 | + " texture2D(tex, interp_tc - 0.5 * xUnit).rgb);\n" |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 78 | + " gl_FragColor.b = coeffs.a + dot(coeffs.rgb,\n" |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 79 | + " texture2D(tex, interp_tc + 0.5 * xUnit).rgb);\n" |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 80 | + " gl_FragColor.a = coeffs.a + dot(coeffs.rgb,\n" |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 81 | + " texture2D(tex, interp_tc + 1.5 * xUnit).rgb);\n" |
| 82 | + "}\n"; |
| 83 | |
| 84 | private static final String RGB_FRAGMENT_SHADER = |
| 85 | "precision mediump float;\n" |
| 86 | + "varying vec2 interp_tc;\n" |
| 87 | + "\n" |
Sami Kalliomäki | 033ea5f | 2017-10-18 09:16:22 +0200 | [diff] [blame] | 88 | + "uniform sampler2D tex;\n" |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 89 | // Difference in texture coordinate corresponding to one |
| 90 | // sub-pixel in the x direction. |
| 91 | + "uniform vec2 xUnit;\n" |
| 92 | // Color conversion coefficients, including constant term |
| 93 | + "uniform vec4 coeffs;\n" |
| 94 | + "\n" |
| 95 | + "void main() {\n" |
| 96 | // Since the alpha read from the texture is always 1, this could |
| 97 | // be written as a mat4 x vec4 multiply. However, that seems to |
| 98 | // give a worse framerate, possibly because the additional |
| 99 | // multiplies by 1.0 consume resources. TODO(nisse): Could also |
| 100 | // try to do it as a vec3 x mat3x4, followed by an add in of a |
| 101 | // constant vector. |
| 102 | + " gl_FragColor.r = coeffs.a + dot(coeffs.rgb,\n" |
| 103 | + " texture2D(tex, interp_tc - 1.5 * xUnit).rgb);\n" |
| 104 | + " gl_FragColor.g = coeffs.a + dot(coeffs.rgb,\n" |
| 105 | + " texture2D(tex, interp_tc - 0.5 * xUnit).rgb);\n" |
| 106 | + " gl_FragColor.b = coeffs.a + dot(coeffs.rgb,\n" |
| 107 | + " texture2D(tex, interp_tc + 0.5 * xUnit).rgb);\n" |
| 108 | + " gl_FragColor.a = coeffs.a + dot(coeffs.rgb,\n" |
| 109 | + " texture2D(tex, interp_tc + 1.5 * xUnit).rgb);\n" |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 110 | + "}\n"; |
| 111 | // clang-format on |
| 112 | |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 113 | private final ThreadUtils.ThreadChecker threadChecker = new ThreadUtils.ThreadChecker(); |
Magnus Jedvert | 1d270f8 | 2018-04-16 16:28:29 +0200 | [diff] [blame] | 114 | private final GlTextureFrameBuffer textureFrameBuffer = new GlTextureFrameBuffer(GLES20.GL_RGBA); |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 115 | private TextureBuffer.Type shaderTextureType; |
| 116 | private GlShader shader; |
| 117 | private int texMatrixLoc; |
| 118 | private int xUnitLoc; |
| 119 | private int coeffsLoc; |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 120 | private boolean released = false; |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 121 | |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 122 | /** |
| 123 | * This class should be constructed on a thread that has an active EGL context. |
| 124 | */ |
| 125 | public YuvConverter() { |
Magnus Jedvert | 1d270f8 | 2018-04-16 16:28:29 +0200 | [diff] [blame] | 126 | threadChecker.detachThread(); |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 127 | } |
| 128 | |
| 129 | /** Converts the texture buffer to I420. */ |
| 130 | public I420Buffer convert(TextureBuffer textureBuffer) { |
| 131 | final int width = textureBuffer.getWidth(); |
| 132 | final int height = textureBuffer.getHeight(); |
| 133 | |
| 134 | // SurfaceTextureHelper requires a stride that is divisible by 8. Round width up. |
| 135 | // See SurfaceTextureHelper for details on the size and format. |
| 136 | final int stride = ((width + 7) / 8) * 8; |
| 137 | final int uvHeight = (height + 1) / 2; |
| 138 | // Due to the layout used by SurfaceTextureHelper, vPos + stride * uvHeight would overrun the |
| 139 | // buffer. Add one row at the bottom to compensate for this. There will never be data in the |
| 140 | // extra row, but now other code does not have to deal with v stride * v height exceeding the |
| 141 | // buffer's capacity. |
| 142 | final int size = stride * (height + uvHeight + 1); |
Magnus Jedvert | 84d8ae5 | 2017-12-20 15:12:10 +0100 | [diff] [blame] | 143 | ByteBuffer buffer = JniCommon.nativeAllocateByteBuffer(size); |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 144 | convert(buffer, width, height, stride, textureBuffer.getTextureId(), |
| 145 | RendererCommon.convertMatrixFromAndroidGraphicsMatrix(textureBuffer.getTransformMatrix()), |
| 146 | textureBuffer.getType()); |
| 147 | |
| 148 | final int yPos = 0; |
| 149 | final int uPos = yPos + stride * height; |
| 150 | // Rows of U and V alternate in the buffer, so V data starts after the first row of U. |
| 151 | final int vPos = uPos + stride / 2; |
| 152 | |
| 153 | buffer.position(yPos); |
| 154 | buffer.limit(yPos + stride * height); |
| 155 | ByteBuffer dataY = buffer.slice(); |
| 156 | |
| 157 | buffer.position(uPos); |
| 158 | buffer.limit(uPos + stride * uvHeight); |
| 159 | ByteBuffer dataU = buffer.slice(); |
| 160 | |
| 161 | buffer.position(vPos); |
| 162 | buffer.limit(vPos + stride * uvHeight); |
| 163 | ByteBuffer dataV = buffer.slice(); |
| 164 | |
| 165 | // SurfaceTextureHelper uses the same stride for Y, U, and V data. |
| 166 | return JavaI420Buffer.wrap(width, height, dataY, stride, dataU, stride, dataV, stride, |
Magnus Jedvert | 84d8ae5 | 2017-12-20 15:12:10 +0100 | [diff] [blame] | 167 | () -> { JniCommon.nativeFreeByteBuffer(buffer); }); |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 168 | } |
| 169 | |
| 170 | /** Deprecated, use convert(TextureBuffer). */ |
| 171 | @Deprecated |
| 172 | void convert(ByteBuffer buf, int width, int height, int stride, int srcTextureId, |
| 173 | float[] transformMatrix) { |
| 174 | convert(buf, width, height, stride, srcTextureId, transformMatrix, TextureBuffer.Type.OES); |
| 175 | } |
| 176 | |
| 177 | private void initShader(TextureBuffer.Type textureType) { |
| 178 | if (shader != null) { |
| 179 | shader.release(); |
| 180 | } |
| 181 | |
| 182 | final String fragmentShader; |
| 183 | switch (textureType) { |
| 184 | case OES: |
| 185 | fragmentShader = OES_FRAGMENT_SHADER; |
| 186 | break; |
| 187 | case RGB: |
| 188 | fragmentShader = RGB_FRAGMENT_SHADER; |
| 189 | break; |
| 190 | default: |
| 191 | throw new IllegalArgumentException("Unsupported texture type."); |
| 192 | } |
| 193 | |
| 194 | shaderTextureType = textureType; |
| 195 | shader = new GlShader(VERTEX_SHADER, fragmentShader); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 196 | shader.useProgram(); |
| 197 | texMatrixLoc = shader.getUniformLocation("texMatrix"); |
| 198 | xUnitLoc = shader.getUniformLocation("xUnit"); |
| 199 | coeffsLoc = shader.getUniformLocation("coeffs"); |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 200 | GLES20.glUniform1i(shader.getUniformLocation("tex"), 0); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 201 | GlUtil.checkNoGLES2Error("Initialize fragment shader uniform values."); |
| 202 | // Initialize vertex shader attributes. |
| 203 | shader.setVertexAttribArray("in_pos", 2, DEVICE_RECTANGLE); |
| 204 | // If the width is not a multiple of 4 pixels, the texture |
| 205 | // will be scaled up slightly and clipped at the right border. |
| 206 | shader.setVertexAttribArray("in_tc", 2, TEXTURE_RECTANGLE); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 207 | } |
| 208 | |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 209 | private void convert(ByteBuffer buf, int width, int height, int stride, int srcTextureId, |
| 210 | float[] transformMatrix, TextureBuffer.Type textureType) { |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 211 | threadChecker.checkIsOnValidThread(); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 212 | if (released) { |
| 213 | throw new IllegalStateException("YuvConverter.convert called on released object"); |
| 214 | } |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 215 | if (textureType != shaderTextureType) { |
| 216 | initShader(textureType); |
| 217 | } |
Magnus Jedvert | c040dae | 2017-11-17 16:50:17 +0100 | [diff] [blame] | 218 | shader.useProgram(); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 219 | |
| 220 | // We draw into a buffer laid out like |
| 221 | // |
| 222 | // +---------+ |
| 223 | // | | |
| 224 | // | Y | |
| 225 | // | | |
| 226 | // | | |
| 227 | // +----+----+ |
| 228 | // | U | V | |
| 229 | // | | | |
| 230 | // +----+----+ |
| 231 | // |
| 232 | // In memory, we use the same stride for all of Y, U and V. The |
| 233 | // U data starts at offset |height| * |stride| from the Y data, |
| 234 | // and the V data starts at at offset |stride/2| from the U |
| 235 | // data, with rows of U and V data alternating. |
| 236 | // |
| 237 | // Now, it would have made sense to allocate a pixel buffer with |
| 238 | // a single byte per pixel (EGL10.EGL_COLOR_BUFFER_TYPE, |
| 239 | // EGL10.EGL_LUMINANCE_BUFFER,), but that seems to be |
| 240 | // unsupported by devices. So do the following hack: Allocate an |
| 241 | // RGBA buffer, of width |stride|/4. To render each of these |
| 242 | // large pixels, sample the texture at 4 different x coordinates |
| 243 | // and store the results in the four components. |
| 244 | // |
| 245 | // Since the V data needs to start on a boundary of such a |
| 246 | // larger pixel, it is not sufficient that |stride| is even, it |
| 247 | // has to be a multiple of 8 pixels. |
| 248 | |
| 249 | if (stride % 8 != 0) { |
| 250 | throw new IllegalArgumentException("Invalid stride, must be a multiple of 8"); |
| 251 | } |
| 252 | if (stride < width) { |
| 253 | throw new IllegalArgumentException("Invalid stride, must >= width"); |
| 254 | } |
| 255 | |
| 256 | int y_width = (width + 3) / 4; |
| 257 | int uv_width = (width + 7) / 8; |
| 258 | int uv_height = (height + 1) / 2; |
| 259 | int total_height = height + uv_height; |
| 260 | int size = stride * total_height; |
| 261 | |
| 262 | if (buf.capacity() < size) { |
| 263 | throw new IllegalArgumentException("YuvConverter.convert called with too small buffer"); |
| 264 | } |
| 265 | // Produce a frame buffer starting at top-left corner, not |
| 266 | // bottom-left. |
| 267 | transformMatrix = |
| 268 | RendererCommon.multiplyMatrices(transformMatrix, RendererCommon.verticalFlipMatrix()); |
| 269 | |
sakal | 2fcd2dd | 2017-01-18 03:21:10 -0800 | [diff] [blame] | 270 | final int frameBufferWidth = stride / 4; |
| 271 | final int frameBufferHeight = total_height; |
| 272 | textureFrameBuffer.setSize(frameBufferWidth, frameBufferHeight); |
| 273 | |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 274 | // Bind our framebuffer. |
sakal | 2fcd2dd | 2017-01-18 03:21:10 -0800 | [diff] [blame] | 275 | GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, textureFrameBuffer.getFrameBufferId()); |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 276 | GlUtil.checkNoGLES2Error("glBindFramebuffer"); |
| 277 | |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 278 | GLES20.glActiveTexture(GLES20.GL_TEXTURE0); |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 279 | GLES20.glBindTexture(textureType.getGlTarget(), srcTextureId); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 280 | GLES20.glUniformMatrix4fv(texMatrixLoc, 1, false, transformMatrix, 0); |
| 281 | |
| 282 | // Draw Y |
| 283 | GLES20.glViewport(0, 0, y_width, height); |
| 284 | // Matrix * (1;0;0;0) / width. Note that opengl uses column major order. |
| 285 | GLES20.glUniform2f(xUnitLoc, transformMatrix[0] / width, transformMatrix[1] / width); |
| 286 | // Y'UV444 to RGB888, see |
| 287 | // https://en.wikipedia.org/wiki/YUV#Y.27UV444_to_RGB888_conversion. |
| 288 | // We use the ITU-R coefficients for U and V */ |
Magnus Jedvert | 6507054 | 2018-06-14 12:23:01 +0200 | [diff] [blame^] | 289 | GLES20.glUniform4f(coeffsLoc, 0.2987856f, 0.5871095f, 0.1141049f, 0.0f); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 290 | GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); |
| 291 | |
| 292 | // Draw U |
| 293 | GLES20.glViewport(0, height, uv_width, uv_height); |
| 294 | // Matrix * (1;0;0;0) / (width / 2). Note that opengl uses column major order. |
| 295 | GLES20.glUniform2f( |
| 296 | xUnitLoc, 2.0f * transformMatrix[0] / width, 2.0f * transformMatrix[1] / width); |
Magnus Jedvert | 6507054 | 2018-06-14 12:23:01 +0200 | [diff] [blame^] | 297 | GLES20.glUniform4f(coeffsLoc, -0.168805420f, -0.3317003f, 0.5005057f, 0.5f); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 298 | GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); |
| 299 | |
| 300 | // Draw V |
| 301 | GLES20.glViewport(stride / 8, height, uv_width, uv_height); |
Magnus Jedvert | 6507054 | 2018-06-14 12:23:01 +0200 | [diff] [blame^] | 302 | GLES20.glUniform4f(coeffsLoc, 0.4997964f, -0.4184672f, -0.0813292f, 0.5f); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 303 | GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); |
| 304 | |
| 305 | GLES20.glReadPixels( |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 306 | 0, 0, frameBufferWidth, frameBufferHeight, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 307 | |
| 308 | GlUtil.checkNoGLES2Error("YuvConverter.convert"); |
| 309 | |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 310 | // Restore normal framebuffer. |
| 311 | GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); |
| 312 | GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); |
| 313 | |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 314 | // Unbind texture. Reportedly needed on some devices to get |
| 315 | // the texture updated from the camera. |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 316 | GLES20.glBindTexture(textureType.getGlTarget(), 0); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 317 | } |
| 318 | |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 319 | public void release() { |
| 320 | threadChecker.checkIsOnValidThread(); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 321 | released = true; |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 322 | if (shader != null) { |
| 323 | shader.release(); |
| 324 | } |
sakal | 2fcd2dd | 2017-01-18 03:21:10 -0800 | [diff] [blame] | 325 | textureFrameBuffer.release(); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 326 | } |
| 327 | } |