tkchin | 04dbb34 | 2016-08-08 03:10:07 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2016 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 | #import "RTCShader.h" |
| 12 | |
| 13 | #include <vector> |
| 14 | |
| 15 | #import "RTCShader+Private.h" |
| 16 | #import "WebRTC/RTCVideoFrame.h" |
| 17 | |
magjed | fb372f0 | 2016-08-10 07:58:29 -0700 | [diff] [blame] | 18 | #include "webrtc/base/optional.h" |
| 19 | #include "webrtc/common_video/rotation.h" |
| 20 | |
tkchin | 04dbb34 | 2016-08-08 03:10:07 -0700 | [diff] [blame] | 21 | // |kNumTextures| must not exceed 8, which is the limit in OpenGLES2. Two sets |
| 22 | // of 3 textures are used here, one for each of the Y, U and V planes. Having |
| 23 | // two sets alleviates CPU blockage in the event that the GPU is asked to render |
| 24 | // to a texture that is already in use. |
| 25 | static const GLsizei kNumTextureSets = 2; |
| 26 | static const GLsizei kNumTexturesPerSet = 3; |
| 27 | static const GLsizei kNumTextures = kNumTexturesPerSet * kNumTextureSets; |
| 28 | |
| 29 | // Fragment shader converts YUV values from input textures into a final RGB |
| 30 | // pixel. The conversion formula is from http://www.fourcc.org/fccyvrgb.php. |
| 31 | static const char kI420FragmentShaderSource[] = |
| 32 | SHADER_VERSION |
| 33 | "precision highp float;" |
| 34 | FRAGMENT_SHADER_IN " vec2 v_texcoord;\n" |
| 35 | "uniform lowp sampler2D s_textureY;\n" |
| 36 | "uniform lowp sampler2D s_textureU;\n" |
| 37 | "uniform lowp sampler2D s_textureV;\n" |
| 38 | FRAGMENT_SHADER_OUT |
| 39 | "void main() {\n" |
| 40 | " float y, u, v, r, g, b;\n" |
| 41 | " y = " FRAGMENT_SHADER_TEXTURE "(s_textureY, v_texcoord).r;\n" |
| 42 | " u = " FRAGMENT_SHADER_TEXTURE "(s_textureU, v_texcoord).r;\n" |
| 43 | " v = " FRAGMENT_SHADER_TEXTURE "(s_textureV, v_texcoord).r;\n" |
| 44 | " u = u - 0.5;\n" |
| 45 | " v = v - 0.5;\n" |
| 46 | " r = y + 1.403 * v;\n" |
| 47 | " g = y - 0.344 * u - 0.714 * v;\n" |
| 48 | " b = y + 1.770 * u;\n" |
| 49 | " " FRAGMENT_SHADER_COLOR " = vec4(r, g, b, 1.0);\n" |
| 50 | " }\n"; |
| 51 | |
| 52 | @implementation RTCI420Shader { |
| 53 | BOOL _hasUnpackRowLength; |
| 54 | GLint _currentTextureSet; |
| 55 | // Handles for OpenGL constructs. |
| 56 | GLuint _textures[kNumTextures]; |
| 57 | GLuint _i420Program; |
| 58 | GLuint _vertexArray; |
| 59 | GLuint _vertexBuffer; |
| 60 | GLint _ySampler; |
| 61 | GLint _uSampler; |
| 62 | GLint _vSampler; |
magjed | fb372f0 | 2016-08-10 07:58:29 -0700 | [diff] [blame] | 63 | // Store current rotation and only upload new vertex data when rotation |
| 64 | // changes. |
| 65 | rtc::Optional<webrtc::VideoRotation> _currentRotation; |
tkchin | 04dbb34 | 2016-08-08 03:10:07 -0700 | [diff] [blame] | 66 | // Used to create a non-padded plane for GPU upload when we receive padded |
| 67 | // frames. |
| 68 | std::vector<uint8_t> _planeBuffer; |
| 69 | } |
| 70 | |
| 71 | - (instancetype)initWithContext:(GlContextType *)context { |
| 72 | if (self = [super init]) { |
| 73 | #if TARGET_OS_IPHONE |
| 74 | _hasUnpackRowLength = (context.API == kEAGLRenderingAPIOpenGLES3); |
| 75 | #else |
| 76 | _hasUnpackRowLength = YES; |
| 77 | #endif |
| 78 | glPixelStorei(GL_UNPACK_ALIGNMENT, 1); |
| 79 | if (![self setupI420Program] || ![self setupTextures] || |
| 80 | !RTCSetupVerticesForProgram(_i420Program, &_vertexBuffer, &_vertexArray)) { |
| 81 | self = nil; |
| 82 | } |
| 83 | } |
| 84 | return self; |
| 85 | } |
| 86 | |
| 87 | - (void)dealloc { |
| 88 | glDeleteProgram(_i420Program); |
| 89 | glDeleteTextures(kNumTextures, _textures); |
| 90 | glDeleteBuffers(1, &_vertexBuffer); |
| 91 | glDeleteVertexArrays(1, &_vertexArray); |
| 92 | } |
| 93 | |
| 94 | - (BOOL)setupI420Program { |
| 95 | _i420Program = RTCCreateProgramFromFragmentSource(kI420FragmentShaderSource); |
| 96 | if (!_i420Program) { |
| 97 | return NO; |
| 98 | } |
| 99 | _ySampler = glGetUniformLocation(_i420Program, "s_textureY"); |
| 100 | _uSampler = glGetUniformLocation(_i420Program, "s_textureU"); |
| 101 | _vSampler = glGetUniformLocation(_i420Program, "s_textureV"); |
| 102 | |
| 103 | return (_ySampler >= 0 && _uSampler >= 0 && _vSampler >= 0); |
| 104 | } |
| 105 | |
| 106 | - (BOOL)setupTextures { |
| 107 | glGenTextures(kNumTextures, _textures); |
| 108 | // Set parameters for each of the textures we created. |
| 109 | for (GLsizei i = 0; i < kNumTextures; i++) { |
| 110 | glBindTexture(GL_TEXTURE_2D, _textures[i]); |
| 111 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); |
| 112 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); |
| 113 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); |
| 114 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); |
| 115 | } |
| 116 | return YES; |
| 117 | } |
| 118 | |
| 119 | - (BOOL)drawFrame:(RTCVideoFrame*)frame { |
| 120 | glUseProgram(_i420Program); |
| 121 | if (![self updateTextureDataForFrame:frame]) { |
| 122 | return NO; |
| 123 | } |
| 124 | #if !TARGET_OS_IPHONE |
| 125 | glBindVertexArray(_vertexArray); |
| 126 | #endif |
| 127 | glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer); |
magjed | fb372f0 | 2016-08-10 07:58:29 -0700 | [diff] [blame] | 128 | if (!_currentRotation || frame.rotation != *_currentRotation) { |
| 129 | _currentRotation = rtc::Optional<webrtc::VideoRotation>( |
| 130 | static_cast<webrtc::VideoRotation>(frame.rotation)); |
| 131 | RTCSetVertexData(*_currentRotation); |
| 132 | } |
tkchin | 04dbb34 | 2016-08-08 03:10:07 -0700 | [diff] [blame] | 133 | glDrawArrays(GL_TRIANGLE_FAN, 0, 4); |
| 134 | |
| 135 | return YES; |
| 136 | } |
| 137 | |
| 138 | - (void)uploadPlane:(const uint8_t *)plane |
| 139 | sampler:(GLint)sampler |
| 140 | offset:(GLint)offset |
| 141 | width:(size_t)width |
| 142 | height:(size_t)height |
| 143 | stride:(int32_t)stride { |
| 144 | glActiveTexture(static_cast<GLenum>(GL_TEXTURE0 + offset)); |
| 145 | glBindTexture(GL_TEXTURE_2D, _textures[offset]); |
| 146 | |
| 147 | // When setting texture sampler uniforms, the texture index is used not |
| 148 | // the texture handle. |
| 149 | glUniform1i(sampler, offset); |
| 150 | const uint8_t *uploadPlane = plane; |
| 151 | if ((size_t)stride != width) { |
| 152 | if (_hasUnpackRowLength) { |
| 153 | // GLES3 allows us to specify stride. |
| 154 | glPixelStorei(GL_UNPACK_ROW_LENGTH, stride); |
| 155 | glTexImage2D(GL_TEXTURE_2D, |
| 156 | 0, |
| 157 | RTC_PIXEL_FORMAT, |
| 158 | static_cast<GLsizei>(width), |
| 159 | static_cast<GLsizei>(height), |
| 160 | 0, |
| 161 | RTC_PIXEL_FORMAT, |
| 162 | GL_UNSIGNED_BYTE, |
| 163 | uploadPlane); |
| 164 | glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); |
| 165 | return; |
| 166 | } else { |
| 167 | // Make an unpadded copy and upload that instead. Quick profiling showed |
| 168 | // that this is faster than uploading row by row using glTexSubImage2D. |
| 169 | uint8_t *unpaddedPlane = _planeBuffer.data(); |
| 170 | for (size_t y = 0; y < height; ++y) { |
| 171 | memcpy(unpaddedPlane + y * width, plane + y * stride, width); |
| 172 | } |
| 173 | uploadPlane = unpaddedPlane; |
| 174 | } |
| 175 | } |
| 176 | glTexImage2D(GL_TEXTURE_2D, |
| 177 | 0, |
| 178 | RTC_PIXEL_FORMAT, |
| 179 | static_cast<GLsizei>(width), |
| 180 | static_cast<GLsizei>(height), |
| 181 | 0, |
| 182 | RTC_PIXEL_FORMAT, |
| 183 | GL_UNSIGNED_BYTE, |
| 184 | uploadPlane); |
| 185 | } |
| 186 | |
| 187 | - (BOOL)updateTextureDataForFrame:(RTCVideoFrame *)frame { |
| 188 | GLint textureOffset = _currentTextureSet * 3; |
| 189 | NSAssert(textureOffset + 3 <= kNumTextures, @"invalid offset"); |
| 190 | |
| 191 | if (frame.yPitch != static_cast<int32_t>(frame.width) || |
| 192 | frame.uPitch != static_cast<int32_t>(frame.chromaWidth) || |
| 193 | frame.vPitch != static_cast<int32_t>(frame.chromaWidth)) { |
| 194 | _planeBuffer.resize(frame.width * frame.height); |
| 195 | } |
| 196 | |
| 197 | [self uploadPlane:frame.yPlane |
| 198 | sampler:_ySampler |
| 199 | offset:textureOffset |
| 200 | width:frame.width |
| 201 | height:frame.height |
| 202 | stride:frame.yPitch]; |
| 203 | |
| 204 | [self uploadPlane:frame.uPlane |
| 205 | sampler:_uSampler |
| 206 | offset:textureOffset + 1 |
| 207 | width:frame.chromaWidth |
| 208 | height:frame.chromaHeight |
| 209 | stride:frame.uPitch]; |
| 210 | |
| 211 | [self uploadPlane:frame.vPlane |
| 212 | sampler:_vSampler |
| 213 | offset:textureOffset + 2 |
| 214 | width:frame.chromaWidth |
| 215 | height:frame.chromaHeight |
| 216 | stride:frame.vPitch]; |
| 217 | |
| 218 | _currentTextureSet = (_currentTextureSet + 1) % kNumTextureSets; |
| 219 | return YES; |
| 220 | } |
| 221 | |
| 222 | @end |