// 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 "gpu/command_buffer/service/gles2_external_framebuffer.h"

#include "gpu/command_buffer/service/feature_info.h"
#include "gpu/command_buffer/service/shared_image/shared_image_factory.h"
#include "gpu/command_buffer/service/shared_image/shared_image_representation.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gl/scoped_binders.h"
#include "ui/gl/scoped_restore_texture.h"

namespace gpu::gles2 {
namespace {
class ScopedRestoreRenderbuffer {
 public:
  explicit ScopedRestoreRenderbuffer(gl::GLApi* api) : api_(api) {
    api_->glGetIntegervFn(GL_RENDERBUFFER_BINDING, &renderbuffer_);
  }

  ~ScopedRestoreRenderbuffer() {
    api_->glBindRenderbufferEXTFn(GL_RENDERBUFFER, renderbuffer_);
  }

 private:
  const raw_ptr<gl::GLApi> api_;
  GLint renderbuffer_ = 0;
};

class ScopedRestoreWindowRectangles {
 public:
  explicit ScopedRestoreWindowRectangles(gl::GLApi* api) : api_(api) {
    api_->glGetIntegervFn(GL_WINDOW_RECTANGLE_MODE_EXT, &mode_);

    GLint num_windows = 0;
    api_->glGetIntegervFn(GL_NUM_WINDOW_RECTANGLES_EXT, &num_windows);

    windows_.resize(4 * num_windows);
    for (int i = 0; i < num_windows; ++i) {
      glGetIntegeri_v(GL_WINDOW_RECTANGLE_EXT, i, &windows_[i * 4]);
    }
  }

  ~ScopedRestoreWindowRectangles() {
    api_->glWindowRectanglesEXTFn(mode_, windows_.size() / 4, windows_.data());
  }

 private:
  const raw_ptr<gl::GLApi> api_;
  GLint mode_ = GL_EXCLUSIVE_EXT;
  std::vector<GLint> windows_;
};

class ScopedRestoreWriteMasks {
 public:
  explicit ScopedRestoreWriteMasks(gl::GLApi* api) : api_(api) {
    api_->glGetIntegervFn(GL_STENCIL_WRITEMASK, &stencil_front_mask_);
    api_->glGetIntegervFn(GL_STENCIL_BACK_WRITEMASK, &stencil_back_mask_);
    api_->glGetBooleanvFn(GL_DEPTH_WRITEMASK, &depth_mask_);
    api_->glGetBooleanvFn(GL_COLOR_WRITEMASK, color_mask_);
  }

  ~ScopedRestoreWriteMasks() {
    api_->glColorMaskFn(color_mask_[0], color_mask_[1], color_mask_[2],
                        color_mask_[3]);
    api_->glDepthMaskFn(depth_mask_);
    api_->glStencilMaskSeparateFn(GL_FRONT, stencil_front_mask_);
    api_->glStencilMaskSeparateFn(GL_BACK, stencil_back_mask_);
  }

 private:
  const raw_ptr<gl::GLApi> api_;
  GLboolean color_mask_[4] = {GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE};
  GLboolean depth_mask_ = GL_TRUE;
  GLint stencil_front_mask_ = 0xFF;
  GLint stencil_back_mask_ = 0xFF;
};

class ScopedRestoreClearValues {
 public:
  explicit ScopedRestoreClearValues(gl::GLApi* api) : api_(api) {
    api_->glGetFloatvFn(GL_COLOR_CLEAR_VALUE, clear_color_);
    api_->glGetFloatvFn(GL_DEPTH_CLEAR_VALUE, &clear_depth_);
    api_->glGetIntegervFn(GL_STENCIL_CLEAR_VALUE, &clear_stencil_);
  }
  ~ScopedRestoreClearValues() {
    api_->glClearColorFn(clear_color_[0], clear_color_[1], clear_color_[2],
                         clear_color_[3]);
    api_->glClearDepthFn(clear_depth_);
    api_->glClearStencilFn(clear_stencil_);
  }

 private:
  const raw_ptr<gl::GLApi> api_;
  GLfloat clear_color_[4] = {};
  GLfloat clear_depth_ = 0.0f;
  GLint clear_stencil_ = 0;
};

class ScopedRestoreFramebuffer {
 public:
  ScopedRestoreFramebuffer(gl::GLApi* api, bool supports_separate_fbo_bindings)
      : api_(api),
        supports_separate_fbo_bindings_(supports_separate_fbo_bindings) {
    if (supports_separate_fbo_bindings_) {
      api_->glGetIntegervFn(GL_DRAW_FRAMEBUFFER_BINDING, &draw_framebuffer_);
      api_->glGetIntegervFn(GL_READ_FRAMEBUFFER_BINDING, &read_framebuffer_);
    } else {
      api_->glGetIntegervFn(GL_FRAMEBUFFER_BINDING, &draw_framebuffer_);
    }
  }

  ~ScopedRestoreFramebuffer() {
    if (supports_separate_fbo_bindings_) {
      api_->glBindFramebufferEXTFn(GL_DRAW_FRAMEBUFFER, draw_framebuffer_);
      api_->glBindFramebufferEXTFn(GL_READ_FRAMEBUFFER, read_framebuffer_);
    } else {
      api_->glBindFramebufferEXTFn(GL_FRAMEBUFFER, draw_framebuffer_);
    }
  }

 private:
  const raw_ptr<gl::GLApi> api_;
  const bool supports_separate_fbo_bindings_;
  GLint draw_framebuffer_ = 0;
  GLint read_framebuffer_ = 0;
};
}  // namespace

class GLES2ExternalFramebuffer::Attachment {
 public:
  static std::unique_ptr<Attachment> CreateTexture(const gfx::Size& size,
                                                   GLenum format) {
    gl::GLApi* const api = gl::g_current_gl_context;
    gl::ScopedRestoreTexture scoped_restore(api, GL_TEXTURE_2D);

    // Don't use sized formats for textures
    GLenum texture_format;
    switch (format) {
      case GL_RGBA8:
        texture_format = GL_RGBA;
        break;
      case GL_RGB8:
        texture_format = GL_RGB;
        break;
      default:
        texture_format = GL_RGBA;
        NOTREACHED();
    }

    GLuint texture;
    api->glGenTexturesFn(1, &texture);
    api->glBindTextureFn(GL_TEXTURE_2D, texture);
    api->glTexImage2DFn(GL_TEXTURE_2D, 0, texture_format, size.width(),
                        size.height(), 0, texture_format, GL_UNSIGNED_BYTE,
                        nullptr);

    return std::make_unique<Attachment>(size, /*samples_count=*/0, format,
                                        /*texture=*/texture,
                                        /*renderbuffer=*/0);
  }

  static std::unique_ptr<Attachment> CreateRenderbuffer(const gfx::Size& size,
                                                        int samples_count,
                                                        GLenum format) {
    gl::GLApi* const api = gl::g_current_gl_context;
    ScopedRestoreRenderbuffer rb_restore(api);

    GLuint renderbuffer;
    api->glGenRenderbuffersEXTFn(1, &renderbuffer);
    api->glBindRenderbufferEXTFn(GL_RENDERBUFFER, renderbuffer);

    if (samples_count > 0) {
      api->glRenderbufferStorageMultisampleFn(
          GL_RENDERBUFFER, samples_count, format, size.width(), size.height());
    } else {
      api->glRenderbufferStorageEXTFn(GL_RENDERBUFFER, format, size.width(),
                                      size.height());
    }

    return std::make_unique<Attachment>(size, samples_count, format,
                                        /*texture=*/0,
                                        /*renderbuffer=*/renderbuffer);
  }

  Attachment(const gfx::Size& size,
             int samples_count,
             GLenum format,
             GLuint texture,
             GLuint renderbuffer)
      : size_(size),
        samples_count_(samples_count),
        format_(format),
        texture_(texture),
        renderbuffer_(renderbuffer) {
    DCHECK_NE(!!texture, !!renderbuffer);
    DCHECK(!size.IsEmpty());
    DCHECK(format);
  }

  Attachment(const Attachment&) = delete;
  Attachment(Attachment&&) = delete;

  Attachment& operator=(const Attachment&) = delete;
  Attachment& operator=(Attachment&&) = delete;

  ~Attachment() {
    // No need to do anything if context was lost.
    if (context_lost_)
      return;

    DCHECK_EQ(attach_point_, 0u);
    if (texture_)
      glDeleteTextures(1, &texture_);
    else if (renderbuffer_)
      glDeleteRenderbuffersEXT(1, &renderbuffer_);
  }

  void Attach(GLenum attachment) {
    DCHECK_EQ(attach_point_, 0u);
    attach_point_ = attachment;

    if (texture_)
      AttachImpl(attach_point_, /*is_texture=*/true, texture_);
    else
      AttachImpl(attach_point_, /*is_texture=*/false, renderbuffer_);
  }

  void Detach() {
    DCHECK_NE(attach_point_, 0u);

    if (texture_)
      AttachImpl(attach_point_, /*is_texture=*/true, 0);
    else
      AttachImpl(attach_point_, /*is_texture=*/false, 0);
    attach_point_ = 0;
  }

  bool NeedsResolve() { return samples_count_ > 0; }

  bool Compatible(const gfx::Size size, int samples_count, GLenum format) {
    return size_ == size && samples_count_ == samples_count &&
           format_ == format;
  }

  void OnContextLost() { context_lost_ = true; }

  GLenum format() const { return format_; }
  int samples_count() const { return samples_count_; }

 private:
  void AttachImpl(GLenum attachment, bool is_texture, GLuint object) {
    if (is_texture) {
      glFramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                                GL_TEXTURE_2D, object, 0);
    } else {
      if (attachment == GL_DEPTH_STENCIL_ATTACHMENT) {
        glFramebufferRenderbufferEXT(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
                                     GL_RENDERBUFFER, object);
        glFramebufferRenderbufferEXT(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT,
                                     GL_RENDERBUFFER, object);
      } else {
        glFramebufferRenderbufferEXT(GL_FRAMEBUFFER, attachment,
                                     GL_RENDERBUFFER, object);
      }
    }
  }

  GLenum attach_point_ = 0;
  bool context_lost_ = false;

  const gfx::Size size_;
  const int samples_count_;
  const GLenum format_;
  const GLuint texture_;
  const GLuint renderbuffer_;
};

GLES2ExternalFramebuffer::GLES2ExternalFramebuffer(
    bool passthrough,
    const FeatureInfo& feature_info,
    SharedImageRepresentationFactory* shared_image_representation_factory)
    : passthrough_(passthrough),
      shared_image_representation_factory_(
          shared_image_representation_factory) {
  const bool multisampled_framebuffers_supported =
      feature_info.feature_flags().chromium_framebuffer_multisample;
  const bool rgb8_supported = feature_info.feature_flags().oes_rgb8_rgba8;
  // The only available default render buffer formats in GLES2 have very
  // little precision.  Don't enable multisampling unless 8-bit render
  // buffer formats are available--instead fall back to 8-bit textures.

  if (multisampled_framebuffers_supported && rgb8_supported) {
    glGetIntegerv(GL_MAX_SAMPLES_EXT, &max_sample_count_);
  }

  packed_depth_stencil_ = feature_info.feature_flags().packed_depth24_stencil8;
  supports_separate_fbo_bindings_ = multisampled_framebuffers_supported ||
                                    feature_info.IsWebGL2OrES3Context();
  supports_window_rectangles_ =
      feature_info.feature_flags().ext_window_rectangles;

  glGenFramebuffersEXT(1, &fbo_);
}

GLES2ExternalFramebuffer::~GLES2ExternalFramebuffer() {
  DCHECK_EQ(fbo_, 0u);
  DCHECK(attachments_.empty());
}

void GLES2ExternalFramebuffer::Destroy(bool have_context) {
  if (!have_context) {
    for (auto& attachment : attachments_)
      attachment.second->OnContextLost();

    if (shared_image_representation_)
      shared_image_representation_->OnContextLost();
  } else {
    gl::GLApi* const api = gl::g_current_gl_context;
    ScopedRestoreFramebuffer scoped_fbo_reset(api,
                                              supports_separate_fbo_bindings_);
    api->glBindFramebufferEXTFn(GL_FRAMEBUFFER, fbo_);
    for (auto& attachment : attachments_)
      attachment.second->Detach();
  }

  scoped_access_.reset();
  shared_image_representation_.reset();

  attachments_.clear();

  if (have_context)
    glDeleteFramebuffersEXT(1, &fbo_);
  fbo_ = 0;
}

bool GLES2ExternalFramebuffer::AttachSharedImage(const Mailbox& mailbox,
                                                 int samples,
                                                 bool preserve,
                                                 bool need_depth,
                                                 bool need_stencil) {
  ResolveAndDetach();

  if (mailbox.IsZero())
    return true;

  if (passthrough_) {
    shared_image_representation_ =
        shared_image_representation_factory_->ProduceGLTexturePassthrough(
            mailbox);
  } else {
    shared_image_representation_ =
        shared_image_representation_factory_->ProduceGLTexture(mailbox);
  }

  if (!shared_image_representation_) {
    LOG(ERROR) << "Can't produce representation";
    return false;
  }

  if (!shared_image_representation_->format().is_single_plane() ||
      (shared_image_representation_->format() !=
           viz::SinglePlaneFormat::kRGBA_8888 &&
       shared_image_representation_->format() !=
           viz::SinglePlaneFormat::kRGBX_8888)) {
    LOG(ERROR) << "Unsupported format";
    return false;
  }

  scoped_access_ = shared_image_representation_->BeginScopedAccess(
      GL_SHARED_IMAGE_ACCESS_MODE_READWRITE_CHROMIUM,
      GLTextureImageRepresentationBase::AllowUnclearedAccess::kYes);

  if (!scoped_access_) {
    LOG(ERROR) << "Can't BeginAccess";
    return false;
  }

  samples = std::min(samples, max_sample_count_);
  const bool can_attach_directly = !samples && !preserve;

  GLenum clear_flags = 0;
  const auto& size = shared_image_representation_->size();

  gl::GLApi* const api = gl::g_current_gl_context;
  ScopedRestoreFramebuffer scoped_fbo_reset(api,
                                            supports_separate_fbo_bindings_);
  api->glBindFramebufferEXTFn(GL_FRAMEBUFFER, fbo_);

  if (can_attach_directly) {
    glFramebufferTexture2DEXT(
        GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
        shared_image_representation_->GetTextureBase()->service_id(), 0);
    if (!shared_image_representation_->IsCleared())
      clear_flags |= GL_COLOR_BUFFER_BIT;
  } else {
    const bool has_alpha = shared_image_representation_->format() ==
                           viz::SinglePlaneFormat::kRGBA_8888;
    if (UpdateAttachment(GL_COLOR_ATTACHMENT0, size, samples,
                         has_alpha ? GL_RGBA8 : GL_RGB8)) {
      clear_flags |= GL_COLOR_BUFFER_BIT;
    }
  }

  // If GL_DEPTH24_STENCIL8 is supported, we prefer it.
  if (packed_depth_stencil_) {
    if (UpdateAttachment(
            GL_DEPTH_STENCIL_ATTACHMENT, size, samples,
            (need_depth || need_stencil) ? GL_DEPTH24_STENCIL8 : GL_NONE)) {
      clear_flags |= GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT;
    }
  } else {
    if (UpdateAttachment(GL_DEPTH_ATTACHMENT, size, samples,
                         need_depth ? GL_DEPTH_COMPONENT16 : GL_NONE)) {
      clear_flags |= GL_DEPTH_BUFFER_BIT;
    }
    if (UpdateAttachment(GL_STENCIL_ATTACHMENT, size, samples,
                         need_stencil ? GL_STENCIL_INDEX8 : GL_NONE)) {
      clear_flags |= GL_STENCIL_BUFFER_BIT;
    }
  }

  GLenum status = api->glCheckFramebufferStatusEXTFn(GL_FRAMEBUFFER);
  LOG_IF(DFATAL, status != GL_FRAMEBUFFER_COMPLETE)
      << "Framebuffer incomplete: " << status;

  if (clear_flags) {
    gl::ScopedCapability scoped_scissor(GL_SCISSOR_TEST, GL_FALSE);

    absl::optional<ScopedRestoreWindowRectangles> window_rectangles_restore;
    if (supports_window_rectangles_) {
      window_rectangles_restore.emplace(api);
      api->glWindowRectanglesEXTFn(GL_EXCLUSIVE_EXT, 0, nullptr);
    }

    ScopedRestoreWriteMasks write_mask_restore(api);
    api->glColorMaskFn(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
    api->glDepthMaskFn(GL_TRUE);
    api->glStencilMaskSeparateFn(GL_FRONT, 0xFF);
    api->glStencilMaskSeparateFn(GL_BACK, 0xFF);

    ScopedRestoreClearValues clear_values_restore(api);
    api->glClearColorFn(0, 0, 0, 0);
    api->glClearDepthFn(0.0f);
    api->glClearStencilFn(0);

    api->glClearFn(clear_flags);

    // If we attached SharedImage directly and did clear color attachment, mark
    // it as cleared.
    if (attachments_.find(GL_COLOR_ATTACHMENT0) == attachments_.end() &&
        (clear_flags & GL_COLOR_BUFFER_BIT))
      shared_image_representation_->SetCleared();
  }

  return true;
}

bool GLES2ExternalFramebuffer::UpdateAttachment(GLenum attachment,
                                                const gfx::Size& size,
                                                int samples,
                                                GLenum format) {
  if (auto old_attachment = attachments_.find(attachment);
      old_attachment != attachments_.end()) {
    if (old_attachment->second->Compatible(size, samples, format))
      return false;
    old_attachment->second->Detach();
    attachments_.erase(attachment);
  }

  if (format) {
    attachments_[attachment] =
        CreateAttachment(attachment, size, samples, format);
    attachments_[attachment]->Attach(attachment);
    return true;
  }
  return false;
}

std::unique_ptr<GLES2ExternalFramebuffer::Attachment>
GLES2ExternalFramebuffer::CreateAttachment(GLenum attachment,
                                           const gfx::Size& size,
                                           int samples,
                                           GLenum format) {
  if (attachment == GL_COLOR_ATTACHMENT0 && samples == 0) {
    return Attachment::CreateTexture(size, format);
  }

  return Attachment::CreateRenderbuffer(size, samples, format);
}

void GLES2ExternalFramebuffer::ResolveAndDetach() {
  if (!scoped_access_) {
    DCHECK(!shared_image_representation_);
    return;
  }

  gl::GLApi* const api = gl::g_current_gl_context;
  ScopedRestoreFramebuffer scoped_fbo_reset(api,
                                            supports_separate_fbo_bindings_);

  if (auto color_attachment = attachments_.find(GL_COLOR_ATTACHMENT0);
      color_attachment != attachments_.end()) {
    const auto& size = shared_image_representation_->size();

    // If we have separate attachment, we need to blit/resolve it to shared
    // image.
    if (color_attachment->second->NeedsResolve()) {
      DCHECK(supports_separate_fbo_bindings_);

      gl::ScopedCapability scoped_scissor(GL_SCISSOR_TEST, GL_FALSE);
      ScopedRestoreWriteMasks write_mask_restore(api);
      api->glColorMaskFn(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

      absl::optional<ScopedRestoreWindowRectangles> window_rectangles_restore;
      if (supports_window_rectangles_) {
        window_rectangles_restore.emplace(api);
        api->glWindowRectanglesEXTFn(GL_EXCLUSIVE_EXT, 0, nullptr);
      }

      api->glBindFramebufferEXTFn(GL_READ_FRAMEBUFFER, fbo_);

      GLuint temp_fbo;
      api->glGenFramebuffersEXTFn(1, &temp_fbo);
      api->glBindFramebufferEXTFn(GL_DRAW_FRAMEBUFFER, temp_fbo);
      api->glFramebufferTexture2DEXTFn(
          GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
          shared_image_representation_->GetTextureBase()->service_id(), 0);

      api->glBlitFramebufferFn(0, 0, size.width(), size.height(), 0, 0,
                               size.width(), size.height(), GL_COLOR_BUFFER_BIT,
                               GL_NEAREST);

      api->glDeleteFramebuffersEXTFn(1, &temp_fbo);
    } else {
      api->glBindFramebufferEXTFn(GL_FRAMEBUFFER, fbo_);
      gl::ScopedRestoreTexture texture(api, GL_TEXTURE_2D);
      api->glBindTextureFn(
          GL_TEXTURE_2D,
          shared_image_representation_->GetTextureBase()->service_id());

      api->glCopyTexSubImage2DFn(GL_TEXTURE_2D, 0, 0, 0, 0, 0, size.width(),
                                 size.height());
    }
    // We did resolved to SharedImage, so we can mark it as cleared here.
    shared_image_representation_->SetCleared();
  } else {
    // Detach color attachment if we were attached directly.
    api->glBindFramebufferEXTFn(GL_FRAMEBUFFER, fbo_);
    api->glFramebufferTexture2DEXTFn(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                                     GL_TEXTURE_2D, 0, 0);
  }
  scoped_access_.reset();
  shared_image_representation_.reset();
}

GLuint GLES2ExternalFramebuffer::GetFramebufferId() const {
  return fbo_;
}

bool GLES2ExternalFramebuffer::IsSharedImageAttached() const {
  return !!scoped_access_;
}

gfx::Size GLES2ExternalFramebuffer::GetSize() const {
  DCHECK(IsSharedImageAttached());
  return shared_image_representation_->size();
}

GLenum GLES2ExternalFramebuffer::GetColorFormat() const {
  DCHECK(IsSharedImageAttached());
  auto it = attachments_.find(GL_COLOR_ATTACHMENT0);
  DCHECK(it != attachments_.end());
  return it->second->format();
}

GLenum GLES2ExternalFramebuffer::GetDepthFormat() const {
  DCHECK(IsSharedImageAttached());
  if (auto it = attachments_.find(GL_DEPTH_STENCIL_ATTACHMENT);
      it != attachments_.end()) {
    return it->second->format();
  }

  if (auto it = attachments_.find(GL_DEPTH_ATTACHMENT);
      it != attachments_.end()) {
    return it->second->format();
  }

  return GL_NONE;
}

GLenum GLES2ExternalFramebuffer::GetStencilFormat() const {
  DCHECK(IsSharedImageAttached());
  DCHECK(IsSharedImageAttached());
  if (auto it = attachments_.find(GL_DEPTH_STENCIL_ATTACHMENT);
      it != attachments_.end()) {
    return it->second->format();
  }

  if (auto it = attachments_.find(GL_STENCIL_ATTACHMENT);
      it != attachments_.end()) {
    return it->second->format();
  }

  return GL_NONE;
}

int GLES2ExternalFramebuffer::GetSamplesCount() const {
  DCHECK(IsSharedImageAttached());
  auto it = attachments_.find(GL_COLOR_ATTACHMENT0);
  DCHECK(it != attachments_.end());
  return it->second->samples_count();
}

bool GLES2ExternalFramebuffer::HasAlpha() const {
  DCHECK(IsSharedImageAttached());
  auto it = attachments_.find(GL_COLOR_ATTACHMENT0);
  DCHECK(it != attachments_.end());
  return it->second->format() == GL_RGBA8;
}

bool GLES2ExternalFramebuffer::HasDepth() const {
  return GetDepthFormat() != GL_NONE;
}

bool GLES2ExternalFramebuffer::HasStencil() const {
  return GetStencilFormat() != GL_NONE;
}

}  // namespace gpu::gles2