// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/common/gpu/client/webgraphicscontext3d_command_buffer_impl.h"

#include "third_party/khronos/GLES2/gl2.h"
#ifndef GL_GLEXT_PROTOTYPES
#define GL_GLEXT_PROTOTYPES 1
#endif
#include "third_party/khronos/GLES2/gl2ext.h"

#include <algorithm>
#include <map>

#include "base/atomicops.h"
#include "base/bind.h"
#include "base/command_line.h"
#include "base/debug/trace_event.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/message_loop/message_loop.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/histogram.h"
#include "content/common/gpu/client/gpu_channel_host.h"
#include "content/public/common/content_constants.h"
#include "content/public/common/content_switches.h"
#include "gpu/GLES2/gl2extchromium.h"
#include "gpu/command_buffer/client/gles2_cmd_helper.h"
#include "gpu/command_buffer/client/gles2_implementation.h"
#include "gpu/command_buffer/client/gles2_lib.h"
#include "gpu/command_buffer/client/gles2_trace_implementation.h"
#include "gpu/command_buffer/client/transfer_buffer.h"
#include "gpu/command_buffer/common/constants.h"
#include "gpu/command_buffer/common/gpu_memory_allocation.h"
#include "gpu/command_buffer/common/mailbox.h"
#include "gpu/skia_bindings/gl_bindings_skia_cmd_buffer.h"
#include "third_party/skia/include/core/SkTypes.h"

namespace content {

namespace {

static base::LazyInstance<base::Lock>::Leaky
    g_default_share_groups_lock = LAZY_INSTANCE_INITIALIZER;

typedef std::map<GpuChannelHost*,
    scoped_refptr<WebGraphicsContext3DCommandBufferImpl::ShareGroup> >
    ShareGroupMap;
static base::LazyInstance<ShareGroupMap> g_default_share_groups =
    LAZY_INSTANCE_INITIALIZER;

scoped_refptr<WebGraphicsContext3DCommandBufferImpl::ShareGroup>
    GetDefaultShareGroupForHost(GpuChannelHost* host) {
  base::AutoLock lock(g_default_share_groups_lock.Get());

  ShareGroupMap& share_groups = g_default_share_groups.Get();
  ShareGroupMap::iterator it = share_groups.find(host);
  if (it == share_groups.end()) {
    scoped_refptr<WebGraphicsContext3DCommandBufferImpl::ShareGroup> group =
        new WebGraphicsContext3DCommandBufferImpl::ShareGroup();
    share_groups[host] = group;
    return group;
  }
  return it->second;
}

// Singleton used to initialize and terminate the gles2 library.
class GLES2Initializer {
 public:
  GLES2Initializer() {
    gles2::Initialize();
  }

  ~GLES2Initializer() {
    gles2::Terminate();
  }

 private:
  DISALLOW_COPY_AND_ASSIGN(GLES2Initializer);
};

////////////////////////////////////////////////////////////////////////////////

base::LazyInstance<GLES2Initializer> g_gles2_initializer =
    LAZY_INSTANCE_INITIALIZER;

////////////////////////////////////////////////////////////////////////////////

} // namespace anonymous

WebGraphicsContext3DCommandBufferImpl::SharedMemoryLimits::SharedMemoryLimits()
    : command_buffer_size(kDefaultCommandBufferSize),
      start_transfer_buffer_size(kDefaultStartTransferBufferSize),
      min_transfer_buffer_size(kDefaultMinTransferBufferSize),
      max_transfer_buffer_size(kDefaultMaxTransferBufferSize),
      mapped_memory_reclaim_limit(gpu::gles2::GLES2Implementation::kNoLimit) {}

WebGraphicsContext3DCommandBufferImpl::ShareGroup::ShareGroup() {
}

WebGraphicsContext3DCommandBufferImpl::ShareGroup::~ShareGroup() {
  DCHECK(contexts_.empty());
}

WebGraphicsContext3DCommandBufferImpl::WebGraphicsContext3DCommandBufferImpl(
    int surface_id,
    const GURL& active_url,
    GpuChannelHost* host,
    const Attributes& attributes,
    bool lose_context_when_out_of_memory,
    const SharedMemoryLimits& limits,
    WebGraphicsContext3DCommandBufferImpl* share_context)
    : lose_context_when_out_of_memory_(lose_context_when_out_of_memory),
      attributes_(attributes),
      visible_(false),
      host_(host),
      surface_id_(surface_id),
      active_url_(active_url),
      gpu_preference_(attributes.preferDiscreteGPU ? gfx::PreferDiscreteGpu
                                                   : gfx::PreferIntegratedGpu),
      weak_ptr_factory_(this),
      mem_limits_(limits) {
  if (share_context) {
    DCHECK(!attributes_.shareResources);
    share_group_ = share_context->share_group_;
  } else {
    share_group_ = attributes_.shareResources
        ? GetDefaultShareGroupForHost(host)
        : scoped_refptr<WebGraphicsContext3DCommandBufferImpl::ShareGroup>(
            new ShareGroup());
  }
}

WebGraphicsContext3DCommandBufferImpl::
    ~WebGraphicsContext3DCommandBufferImpl() {
  if (real_gl_) {
    real_gl_->SetErrorMessageCallback(NULL);
  }

  Destroy();
}

bool WebGraphicsContext3DCommandBufferImpl::MaybeInitializeGL() {
  if (initialized_)
    return true;

  if (initialize_failed_)
    return false;

  TRACE_EVENT0("gpu", "WebGfxCtx3DCmdBfrImpl::MaybeInitializeGL");

  if (!CreateContext(surface_id_ != 0)) {
    Destroy();
    initialize_failed_ = true;
    return false;
  }

  if (gl_ && attributes_.webGL)
    gl_->EnableFeatureCHROMIUM("webgl_enable_glsl_webgl_validation");

  command_buffer_->SetChannelErrorCallback(
      base::Bind(&WebGraphicsContext3DCommandBufferImpl::OnGpuChannelLost,
                 weak_ptr_factory_.GetWeakPtr()));

  command_buffer_->SetOnConsoleMessageCallback(
      base::Bind(&WebGraphicsContext3DCommandBufferImpl::OnErrorMessage,
                 weak_ptr_factory_.GetWeakPtr()));

  real_gl_->SetErrorMessageCallback(getErrorMessageCallback());

  visible_ = true;
  initialized_ = true;
  return true;
}

bool WebGraphicsContext3DCommandBufferImpl::InitializeCommandBuffer(
    bool onscreen, WebGraphicsContext3DCommandBufferImpl* share_context) {
  if (!host_.get())
    return false;

  CommandBufferProxyImpl* share_group_command_buffer = NULL;

  if (share_context) {
    share_group_command_buffer = share_context->command_buffer_.get();
  }

  ::gpu::gles2::ContextCreationAttribHelper attribs_for_gles2;
  ConvertAttributes(attributes_, &attribs_for_gles2);
  attribs_for_gles2.lose_context_when_out_of_memory =
      lose_context_when_out_of_memory_;
  DCHECK(attribs_for_gles2.buffer_preserved);
  std::vector<int32> attribs;
  attribs_for_gles2.Serialize(&attribs);

  // Create a proxy to a command buffer in the GPU process.
  if (onscreen) {
    command_buffer_.reset(host_->CreateViewCommandBuffer(
        surface_id_,
        share_group_command_buffer,
        attribs,
        active_url_,
        gpu_preference_));
  } else {
    command_buffer_.reset(host_->CreateOffscreenCommandBuffer(
        gfx::Size(1, 1),
        share_group_command_buffer,
        attribs,
        active_url_,
        gpu_preference_));
  }

  if (!command_buffer_) {
    DLOG(ERROR) << "GpuChannelHost failed to create command buffer.";
    return false;
  }

  DVLOG_IF(1, gpu::error::IsError(command_buffer_->GetLastError()))
      << "Context dead on arrival. Last error: "
      << command_buffer_->GetLastError();
  // Initialize the command buffer.
  bool result = command_buffer_->Initialize();
  LOG_IF(ERROR, !result) << "CommandBufferProxy::Initialize failed.";
  return result;
}

bool WebGraphicsContext3DCommandBufferImpl::CreateContext(bool onscreen) {
  TRACE_EVENT0("gpu", "WebGfxCtx3DCmdBfrImpl::CreateContext");
  // Ensure the gles2 library is initialized first in a thread safe way.
  g_gles2_initializer.Get();

  scoped_refptr<gpu::gles2::ShareGroup> gles2_share_group;

  scoped_ptr<base::AutoLock> share_group_lock;
  bool add_to_share_group = false;
  if (!command_buffer_) {
    WebGraphicsContext3DCommandBufferImpl* share_context = NULL;

    share_group_lock.reset(new base::AutoLock(share_group_->lock()));
    share_context = share_group_->GetAnyContextLocked();

    if (!InitializeCommandBuffer(onscreen, share_context)) {
      LOG(ERROR) << "Failed to initialize command buffer.";
      return false;
    }

    if (share_context)
      gles2_share_group = share_context->GetImplementation()->share_group();

    add_to_share_group = true;
  }

  // Create the GLES2 helper, which writes the command buffer protocol.
  gles2_helper_.reset(new gpu::gles2::GLES2CmdHelper(command_buffer_.get()));
  if (!gles2_helper_->Initialize(mem_limits_.command_buffer_size)) {
    LOG(ERROR) << "Failed to initialize GLES2CmdHelper.";
    return false;
  }

  if (attributes_.noAutomaticFlushes)
    gles2_helper_->SetAutomaticFlushes(false);
  // Create a transfer buffer used to copy resources between the renderer
  // process and the GPU process.
  transfer_buffer_ .reset(new gpu::TransferBuffer(gles2_helper_.get()));

  DCHECK(host_.get());

  // Create the object exposing the OpenGL API.
  bool bind_generates_resources = false;
  real_gl_.reset(
      new gpu::gles2::GLES2Implementation(gles2_helper_.get(),
                                          gles2_share_group,
                                          transfer_buffer_.get(),
                                          bind_generates_resources,
                                          lose_context_when_out_of_memory_,
                                          command_buffer_.get()));
  setGLInterface(real_gl_.get());

  if (!real_gl_->Initialize(
      mem_limits_.start_transfer_buffer_size,
      mem_limits_.min_transfer_buffer_size,
      mem_limits_.max_transfer_buffer_size,
      mem_limits_.mapped_memory_reclaim_limit)) {
    LOG(ERROR) << "Failed to initialize GLES2Implementation.";
    return false;
  }

  if (add_to_share_group)
    share_group_->AddContextLocked(this);

  if (CommandLine::ForCurrentProcess()->HasSwitch(
      switches::kEnableGpuClientTracing)) {
    trace_gl_.reset(new gpu::gles2::GLES2TraceImplementation(GetGLInterface()));
    setGLInterface(trace_gl_.get());
  }
  return true;
}

bool WebGraphicsContext3DCommandBufferImpl::makeContextCurrent() {
  if (!MaybeInitializeGL()) {
    DLOG(ERROR) << "Failed to initialize context.";
    return false;
  }
  gles2::SetGLContext(GetGLInterface());
  if (gpu::error::IsError(command_buffer_->GetLastError())) {
    LOG(ERROR) << "Context dead on arrival. Last error: "
               << command_buffer_->GetLastError();
    return false;
  }

  return true;
}

void WebGraphicsContext3DCommandBufferImpl::Destroy() {
  share_group_->RemoveContext(this);

  gpu::gles2::GLES2Interface* gl = GetGLInterface();
  if (gl) {
    // First flush the context to ensure that any pending frees of resources
    // are completed. Otherwise, if this context is part of a share group,
    // those resources might leak. Also, any remaining side effects of commands
    // issued on this context might not be visible to other contexts in the
    // share group.
    gl->Flush();
    setGLInterface(NULL);
  }

  trace_gl_.reset();
  real_gl_.reset();
  transfer_buffer_.reset();
  gles2_helper_.reset();
  real_gl_.reset();

  if (command_buffer_) {
    if (host_.get())
      host_->DestroyCommandBuffer(command_buffer_.release());
    command_buffer_.reset();
  }

  host_ = NULL;
}

gpu::ContextSupport*
WebGraphicsContext3DCommandBufferImpl::GetContextSupport() {
  return real_gl_.get();
}

bool WebGraphicsContext3DCommandBufferImpl::isContextLost() {
  return initialize_failed_ ||
      (command_buffer_ && IsCommandBufferContextLost()) ||
      context_lost_reason_ != GL_NO_ERROR;
}

WGC3Denum WebGraphicsContext3DCommandBufferImpl::getGraphicsResetStatusARB() {
  if (IsCommandBufferContextLost() &&
      context_lost_reason_ == GL_NO_ERROR) {
    return GL_UNKNOWN_CONTEXT_RESET_ARB;
  }

  return context_lost_reason_;
}

bool WebGraphicsContext3DCommandBufferImpl::IsCommandBufferContextLost() {
  // If the channel shut down unexpectedly, let that supersede the
  // command buffer's state.
  if (host_.get() && host_->IsLost())
    return true;
  gpu::CommandBuffer::State state = command_buffer_->GetLastState();
  return state.error == gpu::error::kLostContext;
}

// static
WebGraphicsContext3DCommandBufferImpl*
WebGraphicsContext3DCommandBufferImpl::CreateOffscreenContext(
    GpuChannelHost* host,
    const WebGraphicsContext3D::Attributes& attributes,
    bool lose_context_when_out_of_memory,
    const GURL& active_url,
    const SharedMemoryLimits& limits,
    WebGraphicsContext3DCommandBufferImpl* share_context) {
  if (!host)
    return NULL;

  if (share_context && share_context->IsCommandBufferContextLost())
    return NULL;

  return new WebGraphicsContext3DCommandBufferImpl(
      0,
      active_url,
      host,
      attributes,
      lose_context_when_out_of_memory,
      limits,
      share_context);
}

namespace {

WGC3Denum convertReason(gpu::error::ContextLostReason reason) {
  switch (reason) {
  case gpu::error::kGuilty:
    return GL_GUILTY_CONTEXT_RESET_ARB;
  case gpu::error::kInnocent:
    return GL_INNOCENT_CONTEXT_RESET_ARB;
  case gpu::error::kUnknown:
    return GL_UNKNOWN_CONTEXT_RESET_ARB;
  }

  NOTREACHED();
  return GL_UNKNOWN_CONTEXT_RESET_ARB;
}

}  // anonymous namespace

void WebGraphicsContext3DCommandBufferImpl::OnGpuChannelLost() {
  context_lost_reason_ = convertReason(
      command_buffer_->GetLastState().context_lost_reason);
  if (context_lost_callback_) {
    context_lost_callback_->onContextLost();
  }

  share_group_->RemoveAllContexts();

  DCHECK(host_.get());
  {
    base::AutoLock lock(g_default_share_groups_lock.Get());
    g_default_share_groups.Get().erase(host_.get());
  }
}

}  // namespace content
