// 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 <algorithm>
#include <cmath>
#include <string_view>
#include <utility>

#include "base/base64.h"
#include "base/compiler_specific.h"
#include "base/feature_list.h"
#include "base/memory_coordinator/memory_coordinator_features.h"
#include "base/memory_coordinator/traits.h"
#include "base/memory_coordinator/utils.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_view_util.h"
#include "ui/gl/gl_bindings.h"
#include "ui/gl/gl_display.h"
#include "ui/gl/gl_surface_egl.h"

namespace gpu {
namespace gles2 {

namespace {

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;
}

// 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;

// Blob cache function pointers can only be set once per EGLDisplay. Using a
// single bool is not technically correct, but it's acceptable since we only
// have one EGLDisplay in practice.
bool g_blob_cache_funcs_set = false;

constexpr base::MemoryConsumerTraits kPassthroughProgramCacheTraits(
    // Default capacity is small, under 10MB.
    base::MemoryConsumerTraits::EstimatedMemoryUsage::kSmall,
    // Evicting entries requires traversing the LRU cache.
    base::MemoryConsumerTraits::ReleaseMemoryCost::kRequiresTraversal,
    // Shaders can be re-compiled and re-linked if evicted.
    base::MemoryConsumerTraits::InformationRetention::kLossless,
    // Asynchronous since AsyncMemoryConsumerRegistration is used.
    base::MemoryConsumerTraits::ExecutionType::kAsynchronous,
    // Compiling and linking shaders is highly CPU intensive.
    base::MemoryConsumerTraits::RecreateMemoryCost::kExpensive);

}  // namespace

PassthroughProgramCache::PassthroughProgramCache(
    size_t max_cache_size_bytes,
    bool disable_gpu_shader_disk_cache)
    : ProgramCache(max_cache_size_bytes),
      disable_gpu_shader_disk_cache_(disable_gpu_shader_disk_cache),
      curr_size_bytes_(0),
      store_(ProgramLRUCache::NO_AUTO_EVICT),
      memory_consumer_registration_(
          "PassthroughProgramCache",
          kPassthroughProgramCacheTraits,
          this,
          base::AsyncMemoryConsumerRegistration::CheckUnregister::kDisabled),
      current_max_size_bytes_(max_cache_size_bytes) {
  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 && !g_blob_cache_funcs_set &&
      BlobCacheExtensionAvailable(gl_display)) {
    // Register the blob cache callbacks.
    eglSetBlobCacheFuncsANDROID(egl_display, BlobCacheSet, BlobCacheGet);
    g_blob_cache_funcs_set = true;
  }
}

PassthroughProgramCache::~PassthroughProgramCache() {
  // 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;
}

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();
}

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_);
  return TrimLocked(limit);
}

void PassthroughProgramCache::OnUpdateMemoryLimit() {
  base::AutoLock auto_lock(lock_);
  if (!base::FeatureList::IsEnabled(base::kStatefulMemoryPressure)) {
    return;
  }
  // To match previous behavior, the size must be 1/4 at 50% memory limit.
  double ratio = std::clamp(memory_limit_ratio(), 0.0, 1.0);
  size_t target_size = max_size_bytes() * std::pow(ratio, 2.0);
  current_max_size_bytes_ = std::max(curr_size_bytes_, target_size);
}

void PassthroughProgramCache::OnReleaseMemory() {
  base::AutoLock auto_lock(lock_);
  if (base::FeatureList::IsEnabled(base::kStatefulMemoryPressure)) {
    // To match previous behavior, the size must be 1/4 at 50% memory limit.
    double ratio = std::clamp(memory_limit_ratio(), 0.0, 1.0);
    size_t target_size = max_size_bytes() * std::pow(ratio, 2.0);
    current_max_size_bytes_ = target_size;
    TrimLocked(current_max_size_bytes_);
    return;
  }

  int limit = memory_limit();
  if (limit <= base::kCriticalMemoryPressureThreshold) {
    TrimLocked(0);
  } else if (limit <= base::kModerateMemoryPressureThreshold) {
    TrimLocked(max_size_bytes() / 4);
  }
}

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

void PassthroughProgramCache::Set(Key&& key,
                                  Value&& value,
                                  CacheProgramCallback callback) {
  base::AutoLock auto_lock(lock_);
  // If the value is so big it will never fit in the cache, throw it away.
  if (value.size() > GetCurrentMaxSizeBytes()) {
    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(GetCurrentMaxSizeBytes() >= value.size());

  TrimLocked(GetCurrentMaxSizeBytes() - value.size());

  // 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`.
  CacheProgramCallback callback_with_fallback =
      callback ? callback : cache_program_callback_;
  if (callback_with_fallback) {
    // Convert the key and binary to base-64 string form.
    std::string key_string_64 = base::Base64Encode(key);
    std::string value_string_64 = base::Base64Encode(value);
    callback_with_fallback.Run(key_string_64, value_string_64);
  }

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

size_t PassthroughProgramCache::Get(const Key& key,
                                    void* out_value,
                                    size_t value_size) {
  // Note that the |lock_| should be held during whole time ProgramCacheValue is
  // being accessed below.
  base::AutoLock auto_lock(lock_);

  ProgramLRUCache::iterator found = store_.Get(key);

  UMA_HISTOGRAM_BOOLEAN("Gpu.PassthroughProgramCacheLoadHitInCache",
                        found != store_.end());

  if (found == store_.end()) {
    return 0;
  }

  const PassthroughProgramCache::Value& entry_value = found->second.data();

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

  return entry_value.size();
}

EGLsizeiANDROID PassthroughProgramCache::BlobCacheGetImpl(
    const void* key,
    EGLsizeiANDROID key_size,
    void* value,
    EGLsizeiANDROID value_size) {
  if (key_size < 0) {
    return 0;
  }

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

  return Get(entry_key, value, value_size);
}

void PassthroughProgramCache::BlobCacheSetImpl(const void* key,
                                               EGLsizeiANDROID key_size,
                                               const void* value,
                                               EGLsizeiANDROID value_size) {
  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,
                                         UNSAFE_TODO(key_begin + key_size));

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

  // Pass a null callback to use the default cache_program_callback_
  Set(std::move(entry_key), std::move(entry_value), CacheProgramCallback());
}

size_t PassthroughProgramCache::TrimLocked(size_t limit) {
  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_;
}

size_t PassthroughProgramCache::GetCurrentMaxSizeBytes() const {
  if (base::FeatureList::IsEnabled(base::kStatefulMemoryPressure)) {
    return current_max_size_bytes_;
  }
  return max_size_bytes();
}

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

  g_program_cache->BlobCacheSetImpl(key, key_size, value, value_size);
}

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

  return g_program_cache->BlobCacheGetImpl(key, key_size, value, 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() {
  if (program_cache_) {
    program_cache_->curr_size_bytes_ -= program_blob_.size();
  }
}

PassthroughProgramCache::ProgramCacheValue::ProgramCacheValue(
    ProgramCacheValue&& other)
    : program_blob_(std::move(other.program_blob_)),
      program_cache_(std::exchange(other.program_cache_, nullptr)) {}

PassthroughProgramCache::ProgramCacheValue&
PassthroughProgramCache::ProgramCacheValue::operator=(
    ProgramCacheValue&& other) {
  program_blob_ = std::move(other.program_blob_);
  program_cache_ = std::exchange(other.program_cache_, nullptr);
  return *this;
}

}  // namespace gles2
}  // namespace gpu
