// Copyright 2017 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/passthrough_program_cache.h"

#include <stddef.h>

#include "base/base64.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_piece.h"
#include "ui/gl/gl_bindings.h"

#if defined(USE_EGL)
#include "ui/gl/gl_display.h"
#include "ui/gl/gl_surface_egl.h"
#endif  // defined(USE_EGL)

namespace gpu {
namespace gles2 {

namespace {

#if defined(USE_EGL)
bool BlobCacheExtensionAvailable(gl::GLDisplayEGL* gl_display) {
  // The display should be initialized if the extension is available.
  return gl_display->ext->b_EGL_ANDROID_blob_cache;
}
#endif  // defined(USE_EGL)

// EGL_ANDROID_blob_cache doesn't give user pointer to the callbacks so we are
// forced to have this be global.
PassthroughProgramCache* g_program_cache = nullptr;

}  // namespace

PassthroughProgramCache::PassthroughProgramCache(
    size_t max_cache_size_bytes,
    bool disable_gpu_shader_disk_cache,
    ValueAddedHook* value_added_hook)
    : ProgramCache(max_cache_size_bytes),
      disable_gpu_shader_disk_cache_(disable_gpu_shader_disk_cache),
      curr_size_bytes_(0),
      store_(ProgramLRUCache::NO_AUTO_EVICT),
      value_added_hook_(value_added_hook) {
#if defined(USE_EGL)
  gl::GLDisplayEGL* gl_display = gl::GLSurfaceEGL::GetGLDisplayEGL();
  EGLDisplay egl_display = gl_display->GetDisplay();

  DCHECK(!g_program_cache);
  g_program_cache = this;

  // display is EGL_NO_DISPLAY during unittests.
  if (egl_display != EGL_NO_DISPLAY &&
      BlobCacheExtensionAvailable(gl_display)) {
    // Register the blob cache callbacks.
    eglSetBlobCacheFuncsANDROID(egl_display, BlobCacheSet, BlobCacheGet);
  }
#endif  // defined(USE_EGL)
}

PassthroughProgramCache::~PassthroughProgramCache() {
#if defined(USE_EGL)
  // Clear up the blob cache callbacks.  Note that this not allowed by the
  // EGL_ANDROID_blob_cache spec, so we just set the pointer to this object to
  // nullptr as a workaround.  The callbacks don't work with this pointer
  // missing.
  g_program_cache = nullptr;
#endif  // defined(USE_EGL)
}

void PassthroughProgramCache::ClearBackend() {
  base::AutoLock auto_lock(lock_);
  store_.Clear();
  DCHECK_EQ(0U, curr_size_bytes_);
}

ProgramCache::ProgramLoadResult PassthroughProgramCache::LoadLinkedProgram(
    GLuint program,
    Shader* shader_a,
    Shader* shader_b,
    const LocationMap* bind_attrib_location_map,
    const std::vector<std::string>& transform_feedback_varyings,
    GLenum transform_feedback_buffer_mode,
    DecoderClient* client) {
  NOTREACHED();
  return PROGRAM_LOAD_FAILURE;
}

void PassthroughProgramCache::SaveLinkedProgram(
    GLuint program,
    const Shader* shader_a,
    const Shader* shader_b,
    const LocationMap* bind_attrib_location_map,
    const std::vector<std::string>& transform_feedback_varyings,
    GLenum transform_feedback_buffer_mode,
    DecoderClient* client) {
  NOTREACHED();
}

void PassthroughProgramCache::LoadProgram(const std::string& key,
                                          const std::string& program) {
  base::AutoLock auto_lock(lock_);
  if (!CacheEnabled()) {
    return;
  }

  std::string key_decoded;
  std::string program_decoded;
  base::Base64Decode(key, &key_decoded);
  base::Base64Decode(program, &program_decoded);

  Key entry_key(key_decoded.begin(), key_decoded.end());
  Value entry_value(program_decoded.begin(), program_decoded.end());

  store_.Put(entry_key, ProgramCacheValue(std::move(entry_value), this));
}

size_t PassthroughProgramCache::Trim(size_t limit) {
  base::AutoLock auto_lock(lock_);
  size_t initial_size = curr_size_bytes_;
  while (curr_size_bytes_ > limit) {
    DCHECK(!store_.empty());
    store_.Erase(store_.rbegin());
  }
  return initial_size - curr_size_bytes_;
}

bool PassthroughProgramCache::CacheEnabled() const {
  return !disable_gpu_shader_disk_cache_;
}

void PassthroughProgramCache::Set(Key&& key, Value&& value) {
  {
    base::AutoLock auto_lock(lock_);
    // If the value is so big it will never fit in the cache, throw it away.
    if (value.size() > max_size_bytes()) {
      return;
    }

    // Evict any cached program with the same key in favor of the least recently
    // accessed.
    ProgramLRUCache::iterator existing = store_.Peek(key);
    if (existing != store_.end()) {
      store_.Erase(existing);
    }

    // If the cache is overflowing, remove some old entries.
    DCHECK(max_size_bytes() >= value.size());
  }
  Trim(max_size_bytes() - value.size());
  {
    base::AutoLock auto_lock(lock_);
    // If callback is set, notify that there was a new/updated blob entry so it
    // can be stored in disk.  Note that this is done before the Put() call as
    // that consumes `value`.
    if (cache_program_callback_) {
      // Convert the key and binary to string form.
      base::StringPiece key_string(reinterpret_cast<const char*>(key.data()),
                                   key.size());
      base::StringPiece value_string(
          reinterpret_cast<const char*>(value.data()), value.size());
      std::string key_string_64;
      std::string value_string_64;
      base::Base64Encode(key_string, &key_string_64);
      base::Base64Encode(value_string, &value_string_64);
      cache_program_callback_.Run(key_string_64, value_string_64);
    }

    if (value_added_hook_) {
      value_added_hook_->OnValueAddedToCache(key, value);
    }

    store_.Put(key, ProgramCacheValue(std::move(value), this));
  }
}

const PassthroughProgramCache::ProgramCacheValue* PassthroughProgramCache::Get(
    const Key& key) {
  base::AutoLock auto_lock(lock_);
  ProgramLRUCache::iterator found = store_.Get(key);
  return found == store_.end() ? nullptr : &found->second;
}

void PassthroughProgramCache::BlobCacheSet(const void* key,
                                           EGLsizeiANDROID key_size,
                                           const void* value,
                                           EGLsizeiANDROID value_size) {
  if (!g_program_cache)
    return;

  if (key_size < 0 || value_size < 0)
    return;

  const uint8_t* key_begin = reinterpret_cast<const uint8_t*>(key);
  PassthroughProgramCache::Key entry_key(key_begin, key_begin + key_size);

  const uint8_t* value_begin = reinterpret_cast<const uint8_t*>(value);
  PassthroughProgramCache::Value entry_value(value_begin,
                                             value_begin + value_size);

  g_program_cache->Set(std::move(entry_key), std::move(entry_value));
}

EGLsizeiANDROID PassthroughProgramCache::BlobCacheGet(
    const void* key,
    EGLsizeiANDROID key_size,
    void* value,
    EGLsizeiANDROID value_size) {
  if (!g_program_cache)
    return 0;

  if (key_size < 0)
    return 0;

  const uint8_t* key_begin = reinterpret_cast<const uint8_t*>(key);
  PassthroughProgramCache::Key entry_key(key_begin, key_begin + key_size);

  const PassthroughProgramCache::ProgramCacheValue* cacheValue =
      g_program_cache->Get(std::move(entry_key));

  if (!cacheValue)
    return 0;

  const PassthroughProgramCache::Value& entry_value = cacheValue->data();

  if (value_size > 0) {
    if (static_cast<size_t>(value_size) >= entry_value.size())
      memcpy(value, entry_value.data(), entry_value.size());
  }

  return entry_value.size();
}

PassthroughProgramCache::ProgramCacheValue::ProgramCacheValue(
    PassthroughProgramCache::Value&& program_blob,
    PassthroughProgramCache* program_cache)
    : program_blob_(std::move(program_blob)), program_cache_(program_cache) {
  program_cache_->curr_size_bytes_ += program_blob_.size();
}

PassthroughProgramCache::ProgramCacheValue::~ProgramCacheValue() {
  program_cache_->curr_size_bytes_ -= program_blob_.size();
}

PassthroughProgramCache::ProgramCacheValue::ProgramCacheValue(
    ProgramCacheValue&& other) = default;

PassthroughProgramCache::ProgramCacheValue&
PassthroughProgramCache::ProgramCacheValue::operator=(
    ProgramCacheValue&& other) = default;

}  // namespace gles2
}  // namespace gpu