//
// Copyright 2017 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// MemoryProgramCache: Stores compiled and linked programs in memory so they don't
//   always have to be re-compiled. Can be used in conjunction with the platform
//   layer to warm up the cache from disk.

// Include zlib first, otherwise FAR gets defined elsewhere.
#define USE_SYSTEM_ZLIB
#include "compression_utils_portable.h"

#include "libANGLE/MemoryProgramCache.h"

#include <GLSLANG/ShaderVars.h>
#include <anglebase/sha1.h>

#include "common/BinaryStream.h"
#include "common/angle_version_info.h"
#include "common/utilities.h"
#include "libANGLE/Context.h"
#include "libANGLE/Debug.h"
#include "libANGLE/Uniform.h"
#include "libANGLE/capture/FrameCapture.h"
#include "libANGLE/histogram_macros.h"
#include "libANGLE/renderer/ProgramImpl.h"
#include "platform/PlatformMethods.h"

namespace gl
{

namespace
{

// Limit decompressed programs to 10MB. If they're larger then this there is a good chance the data
// is not what we expect. This limits the amount of memory we will allocate based on a binary blob
// we believe is compressed data.
static constexpr size_t kMaxUncompressedProgramSize = 10 * 1024 * 1024;

void WriteProgramBindings(BinaryOutputStream *stream, const ProgramBindings &bindings)
{
    for (const auto &binding : bindings.getStableIterationMap())
    {
        stream->writeString(binding.first);
        stream->writeInt(binding.second);
    }
}

void WriteProgramAliasedBindings(BinaryOutputStream *stream, const ProgramAliasedBindings &bindings)
{
    for (const auto &binding : bindings.getStableIterationMap())
    {
        stream->writeString(binding.first);
        stream->writeInt(binding.second.location);
    }
}

}  // anonymous namespace

MemoryProgramCache::MemoryProgramCache(egl::BlobCache &blobCache) : mBlobCache(blobCache) {}

MemoryProgramCache::~MemoryProgramCache() {}

void MemoryProgramCache::ComputeHash(const Context *context,
                                     const Program *program,
                                     egl::BlobCache::Key *hashOut)
{
    // Compute the program hash. Start with the shader hashes.
    BinaryOutputStream hashStream;
    ShaderBitSet shaders;
    for (ShaderType shaderType : AllShaderTypes())
    {
        Shader *shader = program->getAttachedShader(shaderType);
        if (shader)
        {
            shaders.set(shaderType);
            shader->writeShaderKey(&hashStream);
        }
    }

    hashStream.writeInt(shaders.bits());

    // Add some ANGLE metadata and Context properties, such as version and back-end.
    hashStream.writeString(angle::GetANGLEShaderProgramVersion());
    hashStream.writeInt(angle::GetANGLESHVersion());
    hashStream.writeInt(context->getClientMajorVersion());
    hashStream.writeInt(context->getClientMinorVersion());
    hashStream.writeString(reinterpret_cast<const char *>(context->getString(GL_RENDERER)));

    // Hash pre-link program properties.
    WriteProgramBindings(&hashStream, program->getAttributeBindings());
    WriteProgramAliasedBindings(&hashStream, program->getUniformLocationBindings());
    WriteProgramAliasedBindings(&hashStream, program->getFragmentOutputLocations());
    WriteProgramAliasedBindings(&hashStream, program->getFragmentOutputIndexes());
    for (const std::string &transformFeedbackVaryingName :
         program->getState().getTransformFeedbackVaryingNames())
    {
        hashStream.writeString(transformFeedbackVaryingName);
    }
    hashStream.writeInt(program->getTransformFeedbackBufferMode());

    // Include the status of FrameCapture, which adds source strings to the binary
    hashStream.writeBool(context->getShareGroup()->getFrameCaptureShared()->enabled());

    // Call the secure SHA hashing function.
    const std::vector<uint8_t> &programKey = hashStream.getData();
    angle::base::SHA1HashBytes(programKey.data(), programKey.size(), hashOut->data());
}

angle::Result MemoryProgramCache::getProgram(const Context *context,
                                             Program *program,
                                             egl::BlobCache::Key *hashOut,
                                             egl::CacheGetResult *resultOut)
{
    *resultOut = egl::CacheGetResult::NotFound;

    // If caching is effectively disabled, don't bother calculating the hash.
    if (!mBlobCache.isCachingEnabled())
    {
        return angle::Result::Continue;
    }

    ComputeHash(context, program, hashOut);

    angle::MemoryBuffer uncompressedData;
    switch (mBlobCache.getAndDecompress(context->getScratchBuffer(), *hashOut,
                                        kMaxUncompressedProgramSize, &uncompressedData))
    {
        case egl::BlobCache::GetAndDecompressResult::NotFound:
            return angle::Result::Continue;

        case egl::BlobCache::GetAndDecompressResult::DecompressFailure:
            ANGLE_PERF_WARNING(context->getState().getDebug(), GL_DEBUG_SEVERITY_LOW,
                               "Error decompressing program binary data fetched from cache.");
            remove(*hashOut);
            // Consider this blob "not found".  As far as the rest of the code is considered,
            // corrupted cache might as well not have existed.
            return angle::Result::Continue;

        case egl::BlobCache::GetAndDecompressResult::GetSuccess:
            ANGLE_TRY(program->loadBinary(context, uncompressedData.data(),
                                          static_cast<int>(uncompressedData.size()), resultOut));

            // Result is either Success or Rejected
            ASSERT(*resultOut != egl::CacheGetResult::NotFound);

            // If cache load failed, evict the entry
            if (*resultOut == egl::CacheGetResult::Rejected)
            {
                ANGLE_PERF_WARNING(context->getState().getDebug(), GL_DEBUG_SEVERITY_LOW,
                                   "Failed to load program binary from cache.");
                remove(*hashOut);
            }

            return angle::Result::Continue;
    }

    UNREACHABLE();
    return angle::Result::Continue;
}

bool MemoryProgramCache::getAt(size_t index,
                               const egl::BlobCache::Key **hashOut,
                               egl::BlobCache::Value *programOut)
{
    return mBlobCache.getAt(index, hashOut, programOut);
}

void MemoryProgramCache::remove(const egl::BlobCache::Key &programHash)
{
    mBlobCache.remove(programHash);
}

angle::Result MemoryProgramCache::putProgram(const egl::BlobCache::Key &programHash,
                                             const Context *context,
                                             Program *program)
{
    // If caching is effectively disabled, don't bother serializing the program.
    if (!mBlobCache.isCachingEnabled())
    {
        return angle::Result::Continue;
    }

    angle::MemoryBuffer serializedProgram;
    ANGLE_TRY(program->serialize(context, &serializedProgram));

    angle::MemoryBuffer compressedData;
    if (!egl::CompressBlobCacheData(serializedProgram.size(), serializedProgram.data(),
                                    &compressedData))
    {
        ANGLE_PERF_WARNING(context->getState().getDebug(), GL_DEBUG_SEVERITY_LOW,
                           "Error compressing binary data.");
        return angle::Result::Continue;
    }

    {
        std::scoped_lock<std::mutex> lock(mBlobCache.getMutex());
        // TODO: http://anglebug.com/7568
        // This was a workaround for Chrome until it added support for EGL_ANDROID_blob_cache,
        // tracked by http://anglebug.com/2516. This issue has since been closed, but removing this
        // still causes a test failure.
        auto *platform = ANGLEPlatformCurrent();
        platform->cacheProgram(platform, programHash, compressedData.size(), compressedData.data());
    }

    mBlobCache.put(programHash, std::move(compressedData));
    return angle::Result::Continue;
}

angle::Result MemoryProgramCache::updateProgram(const Context *context, Program *program)
{
    egl::BlobCache::Key programHash;
    ComputeHash(context, program, &programHash);
    return putProgram(programHash, context, program);
}

bool MemoryProgramCache::putBinary(const egl::BlobCache::Key &programHash,
                                   const uint8_t *binary,
                                   size_t length)
{
    // Copy the binary.
    angle::MemoryBuffer newEntry;
    if (!newEntry.resize(length))
    {
        return false;
    }
    memcpy(newEntry.data(), binary, length);

    // Store the binary.
    mBlobCache.populate(programHash, std::move(newEntry));

    return true;
}

void MemoryProgramCache::clear()
{
    mBlobCache.clear();
}

void MemoryProgramCache::resize(size_t maxCacheSizeBytes)
{
    mBlobCache.resize(maxCacheSizeBytes);
}

size_t MemoryProgramCache::entryCount() const
{
    return mBlobCache.entryCount();
}

size_t MemoryProgramCache::trim(size_t limit)
{
    return mBlobCache.trim(limit);
}

size_t MemoryProgramCache::size() const
{
    return mBlobCache.size();
}

size_t MemoryProgramCache::maxSize() const
{
    return mBlobCache.maxSize();
}

}  // namespace gl
