/*
 * Copyright 2022 Google LLC
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "src/gpu/graphite/Resource.h"

#include "include/core/SkTraceMemoryDump.h"
#include "src/gpu/graphite/ResourceCache.h"

namespace skgpu::graphite {

namespace {
uint32_t create_unique_id() {
    static std::atomic<uint32_t> nextID{1};
    uint32_t id;
    do {
        id = nextID.fetch_add(1, std::memory_order_relaxed);
    } while (id == SK_InvalidUniqueID);
    return id;
}
} // namespace anonymous

Resource::Resource(const SharedContext* sharedContext,
                   Ownership ownership,
                   size_t gpuMemorySize,
                   bool reusableRequiresPurgeable,
                   bool requiresPrepareForReturnToCache)
        : fRefs(RefIncrement(RefType::kUsage)) // Start with 1 usage ref and no others
        , fReusableRefMask(
            (reusableRequiresPurgeable ? PurgeableMask()
                                       : RefMask(RefType::kUsage)) | RefMask(RefType::kReturnQueue))
        , fSharedContext(sharedContext)
        , fUniqueID(create_unique_id())
        , fOwnership(ownership)
        , fRequiresPrepareForReturnToCache(requiresPrepareForReturnToCache)
        , fGpuMemorySize(gpuMemorySize) {
    // At initialization time, a Resource should not be considered budgeted because it does not yet
    // belong to a ResourceCache (which manages a budget). Wrapped resources and owned-but-uncached
    // resources will never be added to a cache and can therefore depend on this default value (as
    // opposed to a resource having its budget and shareable state set via registerWithCache()).
    SkASSERT(fBudgeted == Budgeted::kNo);
    SkASSERT(fShareable == Shareable::kNo);
    SkASSERT(this->isUniquelyHeld());
}

Resource::~Resource() {
    // The cache should have released or destroyed this resource.
    SkASSERT(this->wasDestroyed());
}

void Resource::registerWithCache(sk_sp<ResourceCache> returnCache,
                                 const GraphiteResourceKey& key,
                                 Budgeted initialBudgetedState,
                                 Shareable initialShareableState) {
    // ResourceCache should be registered before the Resource escapes the ResourceProvider, e.g. it
    // has a single usage ref and no others.
    SkASSERT(this->isUniquelyHeld());
    SkASSERT(!fReturnCache);
    SkASSERT(returnCache);

    fKey = key;
    fReturnCache = std::move(returnCache);

    this->addRef<RefType::kCache>();

    this->setBudgeted(initialBudgetedState);
    this->setShareable(initialShareableState);
}

bool Resource::returnToCache() const {
    // No resource should have been destroyed if there was still any sort of ref on it.
    SkASSERT(!this->wasDestroyed());

    // Not all resources are registered with the cache, but returnToCache() should only be called
    // when they have been registered.
    SkASSERT(fReturnCache);
    // In order to be returned, the Resource's "return queue" ref bit must be set. Its cache ref
    // may not be set if the cache has been shut down (but `fReturnCache` remains valid and just
    // returns false to reject the resource return).
    SkASSERT(this->hasReturnQueueRef());
    return fReturnCache->returnResource(const_cast<Resource*>(this));
}

void Resource::internalDispose() {
    SkASSERT(fSharedContext);
    this->invokeReleaseProc();
    this->freeGpuData();
    fSharedContext = nullptr;
    // TODO: If we ever support freeing all the backend objects without deleting the object, we'll
    // need to add a hasAnyRefs() check here.
    delete this;
}

void Resource::dumpMemoryStatistics(SkTraceMemoryDump* traceMemoryDump,
                                    bool inPurgeableQueue) const {
    if (this->ownership() == Ownership::kWrapped && !traceMemoryDump->shouldDumpWrappedObjects()) {
        return;
    }

    if (this->budgeted() == skgpu::Budgeted::kNo &&
        !traceMemoryDump->shouldDumpUnbudgetedObjects()) {
        return;
    }

    size_t size = this->gpuMemorySize();

    // Dump zero-sized objects (e.g. Samplers, pipelines, etc) per traceMemoryDump implementation.
    // Always dump memoryless textures.
    if (size == 0 && !traceMemoryDump->shouldDumpSizelessObjects() &&
        this->asTexture() == nullptr) {
        return;
    }

    SkString resourceName("skia/gpu_resources/resource_");
    resourceName.appendU32(this->uniqueID().asUInt());

    traceMemoryDump->dumpNumericValue(resourceName.c_str(), "size", "bytes", size);
    traceMemoryDump->dumpStringValue(resourceName.c_str(), "type", this->getResourceType());
    traceMemoryDump->dumpStringValue(resourceName.c_str(), "label", this->getLabel());
    if (inPurgeableQueue) {
        traceMemoryDump->dumpNumericValue(resourceName.c_str(), "purgeable_size", "bytes", size);
    }
    if (traceMemoryDump->shouldDumpWrappedObjects()) {
        traceMemoryDump->dumpWrappedState(resourceName.c_str(),
                                          this->ownership() == Ownership::kWrapped);
    }
    if (traceMemoryDump->shouldDumpUnbudgetedObjects()) {
        traceMemoryDump->dumpBudgetedState(resourceName.c_str(),
                                           this->budgeted() == skgpu::Budgeted::kYes);
    }

    this->onDumpMemoryStatistics(traceMemoryDump, resourceName.c_str());

    // TODO: implement this to report real gpu id backing the resource. Will be virtual implemented
    // by backend specific resource subclasses.
    //this->setMemoryBacking(traceMemoryDump, resourceName);
}

} // namespace skgpu::graphite
