|  | // Copyright 2022 The Chromium Authors | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include "media/gpu/chromeos/gl_image_processor_backend.h" | 
|  |  | 
|  | #include "base/functional/callback_forward.h" | 
|  | #include "base/metrics/histogram_macros.h" | 
|  | #include "base/stl_util.h" | 
|  | #include "base/synchronization/waitable_event.h" | 
|  | #include "base/task/sequenced_task_runner.h" | 
|  | #include "base/task/thread_pool.h" | 
|  | #include "base/trace_event/trace_event.h" | 
|  | #include "components/viz/common/resources/shared_image_format.h" | 
|  | #include "media/base/format_utils.h" | 
|  | #include "media/gpu/chromeos/frame_resource.h" | 
|  | #include "media/gpu/chromeos/platform_video_frame_utils.h" | 
|  | #include "media/gpu/macros.h" | 
|  | #include "ui/gfx/buffer_types.h" | 
|  | #include "ui/gfx/geometry/size.h" | 
|  | #include "ui/gfx/gpu_memory_buffer_handle.h" | 
|  | #include "ui/gl/gl_bindings.h" | 
|  | #include "ui/gl/gl_context.h" | 
|  | #include "ui/gl/gl_enums.h" | 
|  | #include "ui/gl/gl_surface_egl.h" | 
|  | #include "ui/gl/gl_utils.h" | 
|  | #include "ui/gl/init/gl_factory.h" | 
|  | #include "ui/ozone/public/native_pixmap_gl_binding.h" | 
|  | #include "ui/ozone/public/ozone_platform.h" | 
|  | #include "ui/ozone/public/surface_factory_ozone.h" | 
|  |  | 
|  | namespace media { | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | #define ALIGN(x, y) (x + (y - 1)) & (~(y - 1)) | 
|  |  | 
|  | ui::GLOzone& GetCurrentGLOzone() { | 
|  | ui::OzonePlatform* platform = ui::OzonePlatform::GetInstance(); | 
|  | CHECK(platform); | 
|  | ui::SurfaceFactoryOzone* surface_factory = platform->GetSurfaceFactoryOzone(); | 
|  | CHECK(surface_factory); | 
|  | ui::GLOzone* gl_ozone = surface_factory->GetCurrentGLOzone(); | 
|  | CHECK(gl_ozone); | 
|  | return *gl_ozone; | 
|  | } | 
|  |  | 
|  | template <typename T> | 
|  | base::CheckedNumeric<T> GetNV12PlaneDimension(int dimension, int plane) { | 
|  | base::CheckedNumeric<T> dimension_scaled(base::strict_cast<T>(dimension)); | 
|  | if (plane == 0) { | 
|  | return dimension_scaled; | 
|  | } | 
|  | dimension_scaled += 1; | 
|  | dimension_scaled /= 2; | 
|  | return dimension_scaled; | 
|  | } | 
|  |  | 
|  | bool CreateAndAttachShader(GLuint program, | 
|  | GLenum type, | 
|  | const char* source, | 
|  | int size) { | 
|  | GLuint shader = glCreateShader(type); | 
|  | glShaderSource(shader, 1, &source, &size); | 
|  | glCompileShader(shader); | 
|  | int result = GL_FALSE; | 
|  | glGetShaderiv(shader, GL_COMPILE_STATUS, &result); | 
|  | if (!result) { | 
|  | char log[4096]; | 
|  | glGetShaderInfoLog(shader, sizeof(log), nullptr, log); | 
|  | LOG(ERROR) << log; | 
|  | return false; | 
|  | } | 
|  | glAttachShader(program, shader); | 
|  | glDeleteShader(shader); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | std::unique_ptr<ui::NativePixmapGLBinding> CreateAndBindImage( | 
|  | const FrameResource* frame, | 
|  | GLenum target, | 
|  | GLuint texture_id, | 
|  | bool should_split_planes, | 
|  | int plane) { | 
|  | CHECK(plane == 0 || plane == 1); | 
|  |  | 
|  | // Note: if this is changed to accept other formats, | 
|  | // GLImageProcessorBackend::InitializeTask() should be updated to ensure | 
|  | // GetCurrentGLOzone().CanImportNativePixmap() returns true for those formats. | 
|  | if (frame->format() != PIXEL_FORMAT_NV12) { | 
|  | LOG(ERROR) << "The frame's format is not NV12"; | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | // Create a native pixmap from the frame's memory buffer handle. Not using | 
|  | // CreateNativePixmapDmaBuf() because we should be using the visible size. | 
|  | gfx::GpuMemoryBufferHandle gpu_memory_buffer_handle = | 
|  | frame->CreateGpuMemoryBufferHandle(); | 
|  | if (gpu_memory_buffer_handle.is_null() || | 
|  | gpu_memory_buffer_handle.type != gfx::NATIVE_PIXMAP) { | 
|  | LOG(ERROR) << "Failed to create native GpuMemoryBufferHandle"; | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | if (!should_split_planes) { | 
|  | auto native_pixmap = base::MakeRefCounted<gfx::NativePixmapDmaBuf>( | 
|  | frame->coded_size(), viz::MultiPlaneFormat::kNV12, | 
|  | std::move(gpu_memory_buffer_handle).native_pixmap_handle()); | 
|  | DCHECK(native_pixmap->AreDmaBufFdsValid()); | 
|  |  | 
|  | // Import the NativePixmap into GL. | 
|  | return GetCurrentGLOzone().ImportNativePixmap( | 
|  | std::move(native_pixmap), gfx::BufferFormat::YUV_420_BIPLANAR, | 
|  | gfx::BufferPlane::DEFAULT, frame->coded_size(), gfx::ColorSpace(), | 
|  | target, texture_id); | 
|  | } | 
|  |  | 
|  | base::CheckedNumeric<int> uv_width(0); | 
|  | base::CheckedNumeric<int> uv_height(0); | 
|  |  | 
|  | if (plane == 1) { | 
|  | uv_width = GetNV12PlaneDimension<int>(frame->coded_size().width(), plane); | 
|  | uv_height = GetNV12PlaneDimension<int>(frame->coded_size().height(), plane); | 
|  |  | 
|  | if (!uv_width.IsValid() || !uv_height.IsValid()) { | 
|  | LOG(ERROR) << "Could not compute the UV plane's dimensions"; | 
|  | return nullptr; | 
|  | } | 
|  | } | 
|  |  | 
|  | const gfx::Size plane_size = | 
|  | plane ? gfx::Size(uv_width.ValueOrDie(), uv_height.ValueOrDie()) | 
|  | : frame->coded_size(); | 
|  |  | 
|  | const auto plane_format = | 
|  | plane ? viz::SinglePlaneFormat::kRG_88 : viz::SinglePlaneFormat::kR_8; | 
|  |  | 
|  | auto native_pixmap = base::MakeRefCounted<gfx::NativePixmapDmaBuf>( | 
|  | plane_size, plane_format, | 
|  | std::move(gpu_memory_buffer_handle).native_pixmap_handle()); | 
|  | DCHECK(native_pixmap->AreDmaBufFdsValid()); | 
|  |  | 
|  | // Import the NativePixmap into GL. | 
|  | return GetCurrentGLOzone().ImportNativePixmap( | 
|  | std::move(native_pixmap), | 
|  | viz::SinglePlaneSharedImageFormatToBufferFormat(plane_format), | 
|  | plane ? gfx::BufferPlane::UV : gfx::BufferPlane::Y, plane_size, | 
|  | gfx::ColorSpace(), target, texture_id); | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | GLImageProcessorBackend::GLImageProcessorBackend( | 
|  | const PortConfig& input_config, | 
|  | const PortConfig& output_config, | 
|  | OutputMode output_mode, | 
|  | ErrorCB error_cb) | 
|  | : ImageProcessorBackend( | 
|  | input_config, | 
|  | output_config, | 
|  | output_mode, | 
|  | std::move(error_cb), | 
|  | // Note: we use a single thread task runner because the GL context is | 
|  | // thread local, so we need to make sure we run the | 
|  | // GLImageProcessorBackend on the same thread always. | 
|  | base::ThreadPool::CreateSingleThreadTaskRunner( | 
|  | {base::TaskPriority::USER_VISIBLE})) {} | 
|  |  | 
|  | std::string GLImageProcessorBackend::type() const { | 
|  | return "GLImageProcessor"; | 
|  | } | 
|  |  | 
|  | bool GLImageProcessorBackend::IsSupported(const PortConfig& input_config, | 
|  | const PortConfig& output_config) { | 
|  | // Technically speaking GLIPBackend doesn't need Ozone but it | 
|  | // relies on it to initialize GL. | 
|  | if (!ui::OzonePlatform::IsInitialized()) { | 
|  | VLOGF(2) << "The GLImageProcessorBackend needs Ozone initialized."; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | if (input_config.fourcc.ToVideoPixelFormat() != PIXEL_FORMAT_NV12 || | 
|  | output_config.fourcc.ToVideoPixelFormat() != PIXEL_FORMAT_NV12) { | 
|  | VLOGF(2) << "The GLImageProcessorBackend only supports NV12."; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | if (!((input_config.fourcc == Fourcc(Fourcc::MM21) && | 
|  | output_config.fourcc == Fourcc(Fourcc::NV12)) || | 
|  | (input_config.fourcc == Fourcc(Fourcc::NV12) && | 
|  | output_config.fourcc == Fourcc(Fourcc::NV12)) || | 
|  | (input_config.fourcc == Fourcc(Fourcc::NM12) && | 
|  | output_config.fourcc == Fourcc(Fourcc::NM12)))) { | 
|  | VLOGF(2) << "The GLImageProcessor only supports MM21->NV12, NV12->NV12, " | 
|  | "and NM12->NM12."; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | if (input_config.visible_rect != output_config.visible_rect && | 
|  | input_config.fourcc == Fourcc(Fourcc::MM21)) { | 
|  | VLOGF(2) | 
|  | << "The GLImageProcessorBackend only supports scaling for NV12->NV12."; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | if (!gfx::Rect(input_config.size).Contains(input_config.visible_rect)) { | 
|  | VLOGF(2) << "The input frame size (" << input_config.size.ToString() | 
|  | << ") does not contain the input visible rect (" | 
|  | << input_config.visible_rect.ToString() << ")"; | 
|  | return false; | 
|  | } | 
|  | if (!gfx::Rect(output_config.size).Contains(output_config.visible_rect)) { | 
|  | VLOGF(2) << "The output frame size (" << output_config.size.ToString() | 
|  | << ") does not contain the output visible rect (" | 
|  | << output_config.visible_rect.ToString() << ")"; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | if (input_config.fourcc == Fourcc(Fourcc::MM21) && | 
|  | ((input_config.size.width() & (kTileWidth - 1)) || | 
|  | (input_config.size.height() & (kTileHeight - 1)))) { | 
|  | VLOGF(2) << "The input frame coded size (" << input_config.size.ToString() | 
|  | << ") is not aligned to the MM21 tile dimensions (" << kTileWidth | 
|  | << "x" << kTileHeight << ")."; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // static | 
|  | std::unique_ptr<ImageProcessorBackend> GLImageProcessorBackend::Create( | 
|  | const PortConfig& input_config, | 
|  | const PortConfig& output_config, | 
|  | OutputMode output_mode, | 
|  | ErrorCB error_cb) { | 
|  | DCHECK_EQ(output_mode, OutputMode::IMPORT); | 
|  |  | 
|  | if (!IsSupported(input_config, output_config)) { | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | auto image_processor = | 
|  | std::unique_ptr<GLImageProcessorBackend, | 
|  | std::default_delete<ImageProcessorBackend>>( | 
|  | new GLImageProcessorBackend(input_config, output_config, | 
|  | OutputMode::IMPORT, std::move(error_cb))); | 
|  |  | 
|  | // Initialize GLImageProcessorBackend on the |backend_task_runner_| so that | 
|  | // the GL context is bound to the right thread and all the shaders are | 
|  | // compiled before we start processing frames. base::Unretained is safe in | 
|  | // this circumstance because we block the thread on InitializeTask(), | 
|  | // preventing our local variables from being deallocated too soon. | 
|  | bool success = false; | 
|  | base::WaitableEvent done; | 
|  | image_processor->backend_task_runner_->PostTask( | 
|  | FROM_HERE, | 
|  | base::BindOnce(&GLImageProcessorBackend::InitializeTask, | 
|  | base::Unretained(image_processor.get()), | 
|  | base::Unretained(&done), base::Unretained(&success))); | 
|  | done.Wait(); | 
|  | if (!success) { | 
|  | return nullptr; | 
|  | } | 
|  | return std::move(image_processor); | 
|  | } | 
|  |  | 
|  | void GLImageProcessorBackend::InitializeTask(base::WaitableEvent* done, | 
|  | bool* success) { | 
|  | DCHECK_CALLED_ON_VALID_SEQUENCE(backend_sequence_checker_); | 
|  |  | 
|  | // InitializeTask() should only be called on a freshly constructed | 
|  | // GLImageProcessorBackend, so there shouldn't be any GL errors yet. | 
|  | CHECK(!got_unrecoverable_gl_error_); | 
|  |  | 
|  | // Create a driver-level GL context just for us. This is questionable because | 
|  | // work in this context will be competing with the context(s) used for | 
|  | // rasterization and compositing. However, it's a simple starting point. | 
|  | gl_surface_ = | 
|  | gl::init::CreateOffscreenGLSurface(gl::GetDefaultDisplay(), gfx::Size()); | 
|  | if (!gl_surface_) { | 
|  | LOG(ERROR) << "Could not create the offscreen EGL surface"; | 
|  | done->Signal(); | 
|  | return; | 
|  | } | 
|  | gl::GLContextAttribs attribs{}; | 
|  | attribs.can_skip_validation = true; | 
|  | attribs.context_priority = gl::ContextPriorityMedium; | 
|  | attribs.angle_context_virtualization_group_number = | 
|  | gl::AngleContextVirtualizationGroup::kGLImageProcessor; | 
|  | gl_context_ = gl::init::CreateGLContext(nullptr, gl_surface_.get(), attribs); | 
|  | if (!gl_context_) { | 
|  | LOG(ERROR) << "Could not create the GL context"; | 
|  | done->Signal(); | 
|  | return; | 
|  | } | 
|  | if (!gl_context_->MakeCurrent(gl_surface_.get())) { | 
|  | LOG(ERROR) << "Could not make the GL context current"; | 
|  | done->Signal(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | // CreateAndBindImage() will need to call | 
|  | // GetCurrentGLOzone().ImportNativePixmap() for NV12 frames, so we should | 
|  | // ensure that's supported. | 
|  | if (!GetCurrentGLOzone().CanImportNativePixmap( | 
|  | viz::MultiPlaneFormat::kNV12)) { | 
|  | LOG(ERROR) << "Importing NV12 buffers is not supported"; | 
|  | done->Signal(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Ensure we can use EGLImage objects as texture images and that we can use | 
|  | // the GL_TEXTURE_EXTERNAL_OES target. | 
|  | if (!gl_context_->HasExtension("GL_OES_EGL_image")) { | 
|  | LOG(ERROR) << "The context doesn't support GL_OES_EGL_image"; | 
|  | done->Signal(); | 
|  | return; | 
|  | } | 
|  | if (!gl_context_->HasExtension("GL_OES_EGL_image_external")) { | 
|  | LOG(ERROR) << "The context doesn't support GL_OES_EGL_image_external"; | 
|  | done->Signal(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Ensure the coded size and visible rectangle are reasonable. | 
|  | const gfx::Size input_coded_size = input_config_.size; | 
|  | const gfx::Size output_coded_size = output_config_.size; | 
|  | if (input_coded_size.IsEmpty() || output_coded_size.IsEmpty()) { | 
|  | LOG(ERROR) << "Either the input or output coded size is empty"; | 
|  | done->Signal(); | 
|  | return; | 
|  | } | 
|  | if (!VideoFrame::IsValidCodedSize(input_coded_size) || | 
|  | !VideoFrame::IsValidCodedSize(output_coded_size)) { | 
|  | LOG(ERROR) << "Either the input or output coded size is invalid"; | 
|  | done->Signal(); | 
|  | return; | 
|  | } | 
|  | if (input_config_.visible_rect.IsEmpty() || | 
|  | output_config_.visible_rect.IsEmpty()) { | 
|  | LOG(ERROR) << "Either the input or output visible rectangle is empty"; | 
|  | done->Signal(); | 
|  | return; | 
|  | } | 
|  | if (!gfx::Rect(input_coded_size).Contains(input_config_.visible_rect) || | 
|  | !gfx::Rect(output_coded_size).Contains(output_config_.visible_rect)) { | 
|  | LOG(ERROR) << "Either the input or output visible rectangle is invalid"; | 
|  | done->Signal(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Note: we use the coded size to import the frames later in | 
|  | // CreateAndBindImage(), so we need to check that size against | 
|  | // GL_MAX_TEXTURE_SIZE. | 
|  | GLint max_texture_size = 0; | 
|  | glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_texture_size); | 
|  | if (max_texture_size < base::strict_cast<GLint>(input_coded_size.width()) || | 
|  | max_texture_size < base::strict_cast<GLint>(input_coded_size.height()) || | 
|  | max_texture_size < base::strict_cast<GLint>(output_coded_size.width()) || | 
|  | max_texture_size < base::strict_cast<GLint>(output_coded_size.height())) { | 
|  | LOG(ERROR) << "Either the input or output coded size exceeds the maximum " | 
|  | "texture size"; | 
|  | done->Signal(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Create an output texture: this will be used as the color attachment for the | 
|  | // framebuffer and will be eventually attached to the output dma-buf. Since we | 
|  | // won't sample from it, we don't need to set parameters. | 
|  | glGenFramebuffersEXT(1, &fb_id_); | 
|  | CHECK_GT(fb_id_, 0u); | 
|  | glGenTextures(1, &dst_texture_id_); | 
|  | CHECK_GT(dst_texture_id_, 0u); | 
|  |  | 
|  | // These calculations are used to calculate vertices such that | 
|  | // regions that were meant to be cropped out would be clipped out | 
|  | // by GL naturally. GL's vertex space is from [-1 1] and everything outside | 
|  | // will be clipped out. | 
|  | // | 
|  | // To do this, the absolute coordinate space vertices of the texture: | 
|  | //    Example: |input_config_.visible_rect.x()| | 
|  | // | 
|  | // These absolute coordinate space vertices are converted to relative | 
|  | // space texture coordinates: | 
|  | //    Example: |input_left / input_width| | 
|  | const GLfloat input_width = | 
|  | base::checked_cast<GLfloat>(input_config_.visible_rect.width()); | 
|  | const GLfloat input_height = | 
|  | base::checked_cast<GLfloat>(input_config_.visible_rect.height()); | 
|  | const GLfloat input_coded_width = | 
|  | base::checked_cast<GLfloat>(input_coded_size.width()); | 
|  | const GLfloat input_coded_height = | 
|  | base::checked_cast<GLfloat>(input_coded_size.height()); | 
|  |  | 
|  | const GLfloat input_left = | 
|  | base::checked_cast<GLfloat>(input_config_.visible_rect.x()); | 
|  | const GLfloat input_top = | 
|  | base::checked_cast<GLfloat>(input_config_.visible_rect.y()); | 
|  |  | 
|  | const GLfloat normalized_left = input_left / input_width; | 
|  | const GLfloat normalized_top = input_top / input_height; | 
|  |  | 
|  | const GLfloat x_start = -1.0f - normalized_left * 2.0f; | 
|  |  | 
|  | const GLfloat x_end = 1.0f + (input_coded_width - input_width - input_left) / | 
|  | input_width * 2.0f; | 
|  |  | 
|  | const GLfloat y_start = -1.0f - normalized_top * 2.0f; | 
|  |  | 
|  | const GLfloat y_end = 1.0f + (input_coded_height - input_height - input_top) / | 
|  | input_height * 2.0f; | 
|  |  | 
|  | const GLfloat vertices[] = { | 
|  | // clang-format off | 
|  | x_end,   y_start, 0.0f, | 
|  | x_end,   y_end,   0.0f, | 
|  | x_start, y_start, 0.0f, | 
|  | x_start, y_end,   0.0f | 
|  | // clang-format on | 
|  | }; | 
|  |  | 
|  | glGenBuffersARB(1, &vbo_id_); | 
|  | CHECK_GT(vbo_id_, 0u); | 
|  | glBindBuffer(GL_ARRAY_BUFFER, vbo_id_); | 
|  | glBufferData(GL_ARRAY_BUFFER, 12 * sizeof(GLfloat), vertices, GL_STATIC_DRAW); | 
|  |  | 
|  | glGenVertexArraysOES(1, &vao_id_); | 
|  | CHECK_GT(vao_id_, 0u); | 
|  | glBindVertexArrayOES(vao_id_); | 
|  | glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr); | 
|  | glEnableVertexAttribArray(0); | 
|  |  | 
|  | // Create a vertex shader program which will be used for both scaling and | 
|  | // conversion shader programs. | 
|  | GLuint program = glCreateProgram(); | 
|  | constexpr GLchar kVertexShader[] = | 
|  | "#version 300 es\n" | 
|  | "layout(location = 0) in vec3 vertex_position;\n" | 
|  | "out vec2 texPos;\n" | 
|  | "void main() {\n" | 
|  | "  gl_Position = vec4(vertex_position, 1.0);\n" | 
|  | "  vec2 uvs[4];\n" | 
|  | "  uvs[0] = vec2(1.0, 0.0);\n" | 
|  | "  uvs[1] = vec2(1.0, 1.0);\n" | 
|  | "  uvs[2] = vec2(0.0, 0.0);\n" | 
|  | "  uvs[3] = vec2(0.0, 1.0);\n" | 
|  | "  texPos = uvs[gl_VertexID];\n" | 
|  | "}\n"; | 
|  | if (!CreateAndAttachShader(program, GL_VERTEX_SHADER, kVertexShader, | 
|  | sizeof(kVertexShader))) { | 
|  | LOG(ERROR) << "Could not compile the vertex shader"; | 
|  | done->Signal(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | const bool scaling = (input_config_.fourcc == Fourcc(Fourcc::NV12) && | 
|  | output_config_.fourcc == Fourcc(Fourcc::NV12)) || | 
|  | (input_config_.fourcc == Fourcc(Fourcc::NM12) && | 
|  | output_config_.fourcc == Fourcc(Fourcc::NM12)); | 
|  |  | 
|  | if (scaling) { | 
|  | // Creates a fragment shader program to do NV12 scaling. | 
|  | constexpr GLchar kFragmentShader[] = | 
|  | R"(#version 300 es | 
|  | precision mediump float; | 
|  | precision mediump int; | 
|  | uniform sampler2D videoTexture; | 
|  | in vec2 texPos; | 
|  | layout(location=0) out vec4 fragColour; | 
|  | void main() { | 
|  | fragColour = texture(videoTexture, texPos); | 
|  | })"; | 
|  | if (!CreateAndAttachShader(program, GL_FRAGMENT_SHADER, kFragmentShader, | 
|  | sizeof(kFragmentShader))) { | 
|  | LOG(ERROR) << "Could not compile the fragment shader"; | 
|  | done->Signal(); | 
|  | return; | 
|  | } | 
|  | } else { | 
|  | // The GL_EXT_YUV_target extension is needed for using a YUV texture (target | 
|  | // = GL_TEXTURE_EXTERNAL_OES) as a rendering target. | 
|  | CHECK_EQ(input_config_.fourcc, Fourcc(Fourcc::MM21)); | 
|  | if (!gl_context_->HasExtension("GL_EXT_YUV_target")) { | 
|  | LOG(ERROR) << "The context doesn't support GL_EXT_YUV_target"; | 
|  | done->Signal(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Creates a shader program to convert an MM21 buffer into an NV12 buffer. | 
|  | // Detiling fragment shader. Notice how we have to sample the Y and UV | 
|  | // channel separately. This is because the driver calculates UV coordinates | 
|  | // by simply dividing the Y coordinates by 2, but this results in subtle UV | 
|  | // plane artifacting, since we should really be dividing by 2 before | 
|  | // calculating the detiled coordinates. In practice, this second sample pass | 
|  | // usually hits the GPU's cache, so this doesn't influence DRAM bandwidth | 
|  | // too negatively. | 
|  | constexpr GLchar kFragmentShader[] = | 
|  | R"(#version 300 es | 
|  | #extension GL_EXT_YUV_target : require | 
|  | #pragma disable_alpha_to_coverage | 
|  | precision mediump float; | 
|  | precision mediump int; | 
|  | uniform __samplerExternal2DY2YEXT tex; | 
|  | const uvec2 kYTileDims = uvec2(16, 32); | 
|  | const uvec2 kUVTileDims = uvec2(8, 16); | 
|  | uniform uint width; | 
|  | uniform uint height; | 
|  | in vec2 texPos; | 
|  | layout(yuv) out vec4 fragColor; | 
|  | void main() { | 
|  | uvec2 iCoord = uvec2(gl_FragCoord.xy); | 
|  | uvec2 tileCoords = iCoord / kYTileDims; | 
|  | uint numTilesPerRow = width / kYTileDims.x; | 
|  | uint tileIdx = (tileCoords.y * numTilesPerRow) + tileCoords.x; | 
|  | uvec2 inTileCoord = iCoord % kYTileDims; | 
|  | uint offsetInTile = (inTileCoord.y * kYTileDims.x) + inTileCoord.x; | 
|  | highp uint linearIndex = tileIdx; | 
|  | linearIndex = linearIndex * kYTileDims.x; | 
|  | linearIndex = linearIndex * kYTileDims.y; | 
|  | linearIndex = linearIndex + offsetInTile; | 
|  | uint detiledY = linearIndex / width; | 
|  | uint detiledX = linearIndex % width; | 
|  | fragColor = vec4(0, 0, 0, 1); | 
|  | fragColor.r = texelFetch(tex, ivec2(detiledX, detiledY), 0).r; | 
|  | iCoord = iCoord / uint(2); | 
|  | tileCoords = iCoord / kUVTileDims; | 
|  | uint uvWidth = width / uint(2); | 
|  | numTilesPerRow = uvWidth / kUVTileDims.x; | 
|  | tileIdx = (tileCoords.y * numTilesPerRow) + tileCoords.x; | 
|  | inTileCoord = iCoord % kUVTileDims; | 
|  | offsetInTile = (inTileCoord.y * kUVTileDims.x) + inTileCoord.x; | 
|  | linearIndex = tileIdx; | 
|  | linearIndex = linearIndex * kUVTileDims.x; | 
|  | linearIndex = linearIndex * kUVTileDims.y; | 
|  | linearIndex = linearIndex + offsetInTile; | 
|  | detiledY = linearIndex / uvWidth; | 
|  | detiledX = linearIndex % uvWidth; | 
|  | detiledY = detiledY * uint(2); | 
|  | detiledX = detiledX * uint(2); | 
|  | fragColor.gb = texelFetch(tex, ivec2(detiledX, detiledY), 0).gb; | 
|  | })"; | 
|  | if (!CreateAndAttachShader(program, GL_FRAGMENT_SHADER, kFragmentShader, | 
|  | sizeof(kFragmentShader))) { | 
|  | LOG(ERROR) << "Could not compile the fragment shader"; | 
|  | done->Signal(); | 
|  | return; | 
|  | } | 
|  | } | 
|  |  | 
|  | glLinkProgram(program); | 
|  | glBindAttribLocation(program, 0, "vertex_position"); | 
|  |  | 
|  | GLint result = GL_FALSE; | 
|  | glGetProgramiv(program, GL_LINK_STATUS, &result); | 
|  | if (!result) { | 
|  | constexpr GLsizei kLogBufferSize = 4096; | 
|  | char log[kLogBufferSize]; | 
|  | glGetShaderInfoLog(program, kLogBufferSize, nullptr, log); | 
|  | LOG(ERROR) << "Could not link the GL program" << log; | 
|  | done->Signal(); | 
|  | return; | 
|  | } | 
|  | glUseProgram(program); | 
|  | glDeleteProgram(program); | 
|  |  | 
|  | // Create an input texture. This will be eventually attached to the input | 
|  | // dma-buf and we will sample from it, so we need to set some parameters. | 
|  | glGenTextures(1, &src_texture_id_); | 
|  | CHECK_GT(src_texture_id_, 0u); | 
|  | const auto gl_texture_target = | 
|  | scaling ? GL_TEXTURE_2D : GL_TEXTURE_EXTERNAL_OES; | 
|  | const auto gl_texture_filter = scaling ? GL_LINEAR : GL_NEAREST; | 
|  | glBindTexture(gl_texture_target, src_texture_id_); | 
|  | glTexParameteri(gl_texture_target, GL_TEXTURE_MIN_FILTER, gl_texture_filter); | 
|  | glTexParameteri(gl_texture_target, GL_TEXTURE_MAG_FILTER, gl_texture_filter); | 
|  | glTexParameteri(gl_texture_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); | 
|  | glTexParameteri(gl_texture_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); | 
|  |  | 
|  | if (!scaling) { | 
|  | glUniform1i(glGetUniformLocation(program, "tex"), 0); | 
|  | glUniform1ui(glGetUniformLocation(program, "width"), | 
|  | base::checked_cast<GLuint>( | 
|  | ALIGN(output_config_.visible_rect.width(), kTileWidth))); | 
|  | glUniform1ui(glGetUniformLocation(program, "height"), | 
|  | base::checked_cast<GLuint>( | 
|  | ALIGN(output_config_.visible_rect.height(), kTileHeight))); | 
|  | } | 
|  |  | 
|  | // Ensure the GLImageProcessorBackend is fully initialized by blocking until | 
|  | // all the commands above have completed. This should be okay because | 
|  | // initialization only happens once. | 
|  | glFinish(); | 
|  | GLenum last_gl_error = GL_NO_ERROR; | 
|  | bool gl_error_occurred = false; | 
|  | while ((last_gl_error = glGetError()) != GL_NO_ERROR) { | 
|  | if (last_gl_error == GL_OUT_OF_MEMORY || | 
|  | last_gl_error == GL_CONTEXT_LOST_KHR) { | 
|  | got_unrecoverable_gl_error_ = true; | 
|  | } | 
|  | gl_error_occurred = true; | 
|  | VLOGF(2) << "Got a GL error: " | 
|  | << gl::GLEnums::GetStringError(last_gl_error); | 
|  | } | 
|  | if (gl_error_occurred) { | 
|  | LOG(ERROR) | 
|  | << "Could not initialize the GL image processor due to GL errors"; | 
|  | done->Signal(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | VLOGF(1) << "Initialized a GLImageProcessorBackend: input coded size = " | 
|  | << input_coded_size.ToString() << ", input visible rectangle = " | 
|  | << input_config_.visible_rect.ToString() | 
|  | << ", output coded size = " << output_coded_size.ToString() | 
|  | << ", output visible rectangle = " | 
|  | << output_config_.visible_rect.ToString(); | 
|  | *success = true; | 
|  | done->Signal(); | 
|  | } | 
|  |  | 
|  | // Note that the ImageProcessor deletes the ImageProcessorBackend on the | 
|  | // |backend_task_runner_| so this should be thread-safe. | 
|  | // | 
|  | // TODO(b/339883058): do we need to explicitly call |gl_surface_|->Destroy()? | 
|  | GLImageProcessorBackend::~GLImageProcessorBackend() { | 
|  | DCHECK_CALLED_ON_VALID_SEQUENCE(backend_sequence_checker_); | 
|  |  | 
|  | if (!gl_surface_) { | 
|  | // If there's no surface, nothing else was created. | 
|  | CHECK(!gl_context_); | 
|  | return; | 
|  | } | 
|  | if (!gl_context_) { | 
|  | // If there's no context, nothing else was created. | 
|  | return; | 
|  | } | 
|  | // In case of an unrecoverable GL error, let's assume the GL state machine is | 
|  | // in an undefined state such that it's unsafe to issue further commands. | 
|  | if (got_unrecoverable_gl_error_) { | 
|  | return; | 
|  | } | 
|  | if (!gl_context_->MakeCurrent(gl_surface_.get())) { | 
|  | // If the context can't be made current, we shouldn't do anything else. | 
|  | return; | 
|  | } | 
|  | if (fb_id_) { | 
|  | glDeleteFramebuffersEXT(1, &fb_id_); | 
|  | } | 
|  | if (dst_texture_id_) { | 
|  | glDeleteTextures(1, &dst_texture_id_); | 
|  | } | 
|  | if (src_texture_id_) { | 
|  | glDeleteTextures(1, &src_texture_id_); | 
|  | } | 
|  | if (vao_id_) { | 
|  | glDeleteVertexArraysOES(1, &vao_id_); | 
|  | } | 
|  | if (vbo_id_) { | 
|  | glDeleteBuffersARB(1, &vbo_id_); | 
|  | } | 
|  | gl_context_->ReleaseCurrent(gl_surface_.get()); | 
|  | } | 
|  |  | 
|  | void GLImageProcessorBackend::ProcessFrame( | 
|  | scoped_refptr<FrameResource> input_frame, | 
|  | scoped_refptr<FrameResource> output_frame, | 
|  | FrameResourceReadyCB cb) { | 
|  | DCHECK_CALLED_ON_VALID_SEQUENCE(backend_sequence_checker_); | 
|  | TRACE_EVENT2("media", "GLImageProcessorBackend::ProcessFrame", "input_frame", | 
|  | input_frame->AsHumanReadableString(), "output_frame", | 
|  | output_frame->AsHumanReadableString()); | 
|  | SCOPED_UMA_HISTOGRAM_TIMER("GLImageProcessorBackend::Process"); | 
|  |  | 
|  | // In the case of unrecoverable GL errors, we assume it's unsafe to issue | 
|  | // further commands. | 
|  | if (got_unrecoverable_gl_error_) { | 
|  | VLOGF(2) << "Earlying out because an unrecoverable GL error was detected"; | 
|  | error_cb_.Run(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (!gl_context_->MakeCurrent(gl_surface_.get())) { | 
|  | LOG(ERROR) << "Could not make the GL context current"; | 
|  | error_cb_.Run(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Import the output buffer into GL. This involves creating an EGL image, | 
|  | // attaching it to |dst_texture_id_|, and making that texture the color | 
|  | // attachment of the framebuffer. Attaching the image of a | 
|  | // GL_TEXTURE_EXTERNAL_OES texture to the framebuffer is supported by the | 
|  | // GL_EXT_YUV_target extension. | 
|  | // | 
|  | // Note that calling glFramebufferTexture2DEXT() during InitializeTask() | 
|  | // didn't work: it generates a GL error. I guess this means the texture must | 
|  | // have a valid image prior to attaching it to the framebuffer. | 
|  |  | 
|  | const bool scaling = (input_config_.fourcc == Fourcc(Fourcc::NV12) && | 
|  | output_config_.fourcc == Fourcc(Fourcc::NV12)) || | 
|  | (input_config_.fourcc == Fourcc(Fourcc::NM12) && | 
|  | output_config_.fourcc == Fourcc(Fourcc::NM12)); | 
|  |  | 
|  | const int num_planes = scaling ? 2 : 1; | 
|  | const auto gl_texture_target = | 
|  | scaling ? GL_TEXTURE_2D : GL_TEXTURE_EXTERNAL_OES; | 
|  | for (int plane = 0; plane < num_planes; plane++) { | 
|  | base::CheckedNumeric<GLsizei> output_width = GetNV12PlaneDimension<GLsizei>( | 
|  | output_frame->visible_rect().width(), plane); | 
|  | base::CheckedNumeric<GLsizei> output_height = | 
|  | GetNV12PlaneDimension<GLsizei>(output_frame->visible_rect().height(), | 
|  | plane); | 
|  | if (!output_width.IsValid() || !output_height.IsValid()) { | 
|  | LOG(ERROR) << "Could not calculate the viewport dimensions"; | 
|  | error_cb_.Run(); | 
|  | return; | 
|  | } | 
|  | glViewport(0, 0, output_width.ValueOrDie(), output_height.ValueOrDie()); | 
|  |  | 
|  | glBindTexture(gl_texture_target, dst_texture_id_); | 
|  | auto output_image_binding = CreateAndBindImage( | 
|  | output_frame.get(), gl_texture_target, dst_texture_id_, | 
|  | /*should_split_planes=*/scaling, plane); | 
|  | if (!output_image_binding) { | 
|  | LOG(ERROR) << "Could not import the output buffer into GL"; | 
|  | error_cb_.Run(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | glBindFramebufferEXT(GL_FRAMEBUFFER, fb_id_); | 
|  | glFramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, | 
|  | gl_texture_target, dst_texture_id_, 0); | 
|  | if (glCheckFramebufferStatusEXT(GL_FRAMEBUFFER) != | 
|  | GL_FRAMEBUFFER_COMPLETE) { | 
|  | LOG(ERROR) << "The GL framebuffer is incomplete"; | 
|  | error_cb_.Run(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Import the input buffer into GL. This is done after importing the output | 
|  | // buffer so that binding so that the input texture remains as the texture | 
|  | // in unit 0 (otherwise, the sampler would be sampling out of the output | 
|  | // texture which wouldn't make sense). | 
|  | glBindTexture(gl_texture_target, src_texture_id_); | 
|  |  | 
|  | auto input_image_binding = CreateAndBindImage( | 
|  | input_frame.get(), gl_texture_target, src_texture_id_, | 
|  | /*should_split_planes=*/scaling, plane); | 
|  | if (!input_image_binding) { | 
|  | LOG(ERROR) << "Could not import the input buffer into GL"; | 
|  | error_cb_.Run(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | GLuint indices[4] = {0, 1, 2, 3}; | 
|  | glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_INT, indices); | 
|  | } | 
|  |  | 
|  | // glFlush() is not quite sufficient and will result in frames being output | 
|  | // out of order, so we use a full glFinish() call. | 
|  | // | 
|  | // TODO(bchoobineh): add proper synchronization that does not require blocking | 
|  | // the CPU. | 
|  | glFinish(); | 
|  |  | 
|  | // Check if any errors occurred. Note that we call glGetError() in a loop to | 
|  | // clear all error flags. | 
|  | GLenum last_gl_error = GL_NO_ERROR; | 
|  | bool gl_error_occurred = false; | 
|  | while ((last_gl_error = glGetError()) != GL_NO_ERROR) { | 
|  | if (last_gl_error == GL_OUT_OF_MEMORY || | 
|  | last_gl_error == GL_CONTEXT_LOST_KHR) { | 
|  | got_unrecoverable_gl_error_ = true; | 
|  | } | 
|  | gl_error_occurred = true; | 
|  | VLOGF(2) << "Got a GL error: " | 
|  | << gl::GLEnums::GetStringError(last_gl_error); | 
|  | } | 
|  | if (gl_error_occurred) { | 
|  | LOG(ERROR) << "Could not process a frame due to one or more GL errors"; | 
|  | error_cb_.Run(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | output_frame->set_timestamp(input_frame->timestamp()); | 
|  | std::move(cb).Run(std::move(output_frame)); | 
|  | return; | 
|  | } | 
|  |  | 
|  | }  // namespace media |