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

#ifndef skgpu_graphite_Resource_DEFINED
#define skgpu_graphite_Resource_DEFINED

#include "include/gpu/GpuTypes.h"
#include "include/private/base/SkMutex.h"
#include "src/gpu/GpuTypesPriv.h"
#include "src/gpu/graphite/GraphiteResourceKey.h"
#include "src/gpu/graphite/ResourceTypes.h"

#include <atomic>
#include <functional>
#include <string>
#include <string_view>

class SkMutex;
class SkTraceMemoryDump;

namespace skgpu::graphite {

class GlobalCache;
class ResourceCache;
class SharedContext;
class Texture;

/**
 * Base class for objects that can be kept in the ResourceCache and created or fetched by a
 * ResourceProvider. A ResourceProvider controls the creation of a Resource and determines how
 * its lifecycle is managed (e.g. is it cached, shareable, etc.). Once created, however, the
 * Resource's lifecycle is controlled by the provider's ResourceCache so that GPU resources can
 * be reused when ref counts reach 0; a Resource that has no more refs and does not belong to a
 * cache is simply deleted.
 *
 * Resource Threading Model
 * ========================
 *
 * A Resource is allowed to be used in multiple threads and be deleted from any thread safely.
 * Resource refs are broken into several categories: usage (public C++ references), command buffer
 * (representing active GPU work), and internal (managed by the ResourceCache). Non-cache refs can
 * be removed and reach zero from any thread and be safely returned to the ResourceCache. Unlike
 * Resource, ResourceCache and ResourceProvider are not thread safe and are only used by a single
 * thread.
 *
 * Active Threads:
 *   1. Context thread: this is the primary thread where Recordings are inserted and GPU work is
 *      scheduled.
 *   2. Cache thread: this is the thread that the ResourceCache is associated with. This can be the
 *      same as the context thread if the ResourceCache and ResourceProvider belong to the Context.
 *      However, a Recorder's ResourceCache/Provider can be used on an arbitrary single thread the
 *      client is using for that specific Recorder.
 *   3. Usage threads: once created on the cache thread, a Resource is not restricted to just the
 *      cache thread and context thread. The Resource can have references held on any other
 *      Recorder's thread if multiple work streams use the resource, or may end up held in other
 *      client threads naturally as part of their lifecycle management of Skia's API objects.
 *         - A Resource could be accessed by an arbitrary number of usage threads.
 *
 * Reference Types:
 *   1. Usage refs: a Resource starts with a single usage ref when created by the ResourceProvider,
 *      which is then given to the caller as an `sk_sp<R>`. Once it escapes the ResourceProvider,
 *      operations on the `sk_sp` can add more usage refs. Usage refs track the liveness of the
 *      Resource from C++ code external to the ResourceCache (either higher-level Skia/Graphite
 *      objects or client objects keeping it alive).
 *   2. Command buffer refs: every Resource used in a command buffer has command buffer refs added
 *      when referencing work (e.g. a Recording) is inserted into the Context. These refs are
 *      released when the command buffer's finish procs are called and it's known that the GPU-side
 *      operations are completely done with the Resources.
 *   3. Cache ref: A ResourceProvider can choose to register a Resource with its ResourceCache at
 *      creation time before returning the Resource. The ResourceCache holds a single ref on every
 *      Resource registered with it until the cache is shutdown.
 *   4. Return queue ref: At most one ref held while the Resource is being added to the cache's
 *      return queue and held until it's removed from the queue on the cache thread.

 *
 * Lifecycle:
 *   1. Initialization: all Resources are created via the ResourceProvider, which also coordinates
 *      with its ResourceCache. If an existing resource can't be used, a new one is created. The
 *      initialization phase is the period between when the provider creates the Resource object and
 *      when it's returned to the caller. The resource can be assigned a key and added to the cache
 *      at this point.
 *   2. Reusable: when all usage refs are removed, some Resources can become reusable and move into
 *      the return queue (on any thread). On the cache thread, the queue is regularly processed and
 *      resources that remain reusable are tracked appropriately within the cache.
 *         - Resources that are only modified via command buffer operations can be reusable when
 *           usage refs reach 0 because future modifications won't conflict with any outstanding
 *           work that is holding current command buffer refs.
 *         - Resources that can be modified directly (e.g. mapped buffers) are not reusable until
 *           there are no usage or command buffer refs to ensure modifications don't become visible
 *           to the GPU before intended.
 *   3. Purgeable: when all usage refs and command buffer refs are removed, a Resource is purgeable.
 *      Resources that weren't considered reusable while having outstanding command buffer refs are
 *      now also reusable. Purgeable resources are also added to the return queue so that the cache
 *      can decide whether or not to keep the resource alive (based on budget limits).
 *   4. Destruction: if a Resource has no refs at all, it can be destroyed. A purgeable resource
 *      that was not registered with a cache will never have a cache ref or return queue ref so is
 *      immediately destroyed. When a cache processes a purgeable resource from the return queue, it
 *      can choose to drop its cache ref (and return queue ref) so the resource will be destroyed.
 *      Purgeable resources that had been kept alive for reuse can also be dropped by the cache when
 *      it is being purged (for budget or idleness reasons).
 *   5. Shutdown: when a Context or Recorder are deleted, its ResourceCache is shutdown. The cache
 *      removes all of its cache refs and blocks any further changes to the return queue. This
 *      allows Resources to be destroyed ASAP when they become purgeable instead of going back to
 *      the shutdown cache. However, every Resource registered with the cache keeps the cache alive.
 *      Once every cached resource is destroyed, the cache itself will be destroyed.
 *
 * Ref Counting and State Transition Properties:
 *   - The cache ref can only be added during initialization (it is uniquely held and on the cache
 *     thread).
 *   - The cache ref can only be removed by the ResourceCache on the cache thread (purging
 *     operations and shutdown are all single-threaded functions on the cache thread).
 *   - The return queue ref can be added by any thread when the Resource transitions to reusable
 *     and/or purgeable.
 *   - The return queue ref cannot be added more than once; if the Resource goes through multiple
 *     transitions before being processed by the cache, the second transition does not need to be
 *     put into the queue.
 *   - The return queue ref cannot be added directly, but only as part of a usage/CB unref that
 *     transitions the resource to being reusable or purgeable. The return queue ref is added
 *     atomically with the other unref.
 *   - The return queue ref can only be removed by the resource cache on the cache thread, unless
 *     the cache was shutdown already. In that case the ref is removed by the adding thread. The
 *     cache will only remove the return queue ref *after* the resource is removed from the queue.
 *   - If the resource was not registered with a cache during initialization, its cache and return
 *     queue refs will always be zero.
 *   - Regular usage refs can only be added when there was prior usage ref held by the caller. This
 *     can happen on any thread.
 *   - Command buffer refs can only be added if the resource has a usage ref (that is held through
 *     the CB ref'ing operation). This can happen on any thread (although in practice just the
 *     context thread).
 *   - A command buffer ref cannot be promoted back to a usage ref.
 *   - Both command buffer and usage refs can be removed from any thread (although CB refs are
 *     likely to be unreffed on the context thread).
 *   - When the usage ref count reaches zero, it can only become non-zero via the cache thread by
 *     actions of its ResourceCache.
 *        - Actions predicated on usage refs being 0 on the cache thread are safe.
 *        - Such actions on other threads are not safe because the cache thread could process the
 *          return queue and reuse the resource, or skip the return queue entirely for shareable
 *          resources.
 *   - When the resource becomes purgeable, it can only become non-purgeable on the cache thread
 *     by action of the ResourceCache.
 *        - Since there are no other usage refs (beyond what the cache might re-add), no other
 *          thread can re-add a command buffer ref.
 *
 */
class Resource {
    enum class RefType {
        kUsage, // Counts controlled by `sk_sp` and tracks liveness from external C++ code.
        kCommandBuffer, // Incremented in Context::insertRecording, decremented by finish procs.
        kCache, // At most 1 ref, added in registerWithCache(), removed on cache shutdown or purge.
        kReturnQueue, // At most 1 ref, held while in the cache's return queue.
    };

public:
    Resource(const Resource&) = delete;
    Resource(Resource&&) = delete;
    Resource& operator=(const Resource&) = delete;
    Resource& operator=(Resource&&) = delete;

    // Adds a usage ref to the resource. Named ref so we can easily manage usage refs with sk_sp.
    void ref() const {
        // Only the cache should be able to add the first usage ref to a resource.
        this->addRef<RefType::kUsage>();
    }

    // Removes a usage ref from the resource
    void unref() const {
        this->removeRef<RefType::kUsage>();
    }

    // Adds a command buffer ref to the resource
    void refCommandBuffer() const {
        this->addRef<RefType::kCommandBuffer>();
    }

    // Removes a command buffer ref from the resource
    void unrefCommandBuffer() const {
        this->removeRef<RefType::kCommandBuffer>();
    }

    // Whether the resource is currently in use by the GPU: any resource that is used in a command
    // buffer is considered in use by the GPU.
    //
    // NOTE: This is currently only correct for textures, hence the name. Once the rest of the
    // resources use the command buffer ref instead of usage ref appropriately, this can be made
    // more generaic.
    bool isTextureBusyOnGPU() const {
        return (fRefs.load(std::memory_order_acquire) & RefMask(RefType::kCommandBuffer)) != 0;
    }

    Ownership ownership() const { return fOwnership; }

    Budgeted budgeted() const { return fBudgeted; }
    Shareable shareable() const { return fShareable; }
    const GraphiteResourceKey& key() const { return fKey; }

    // Retrieves the amount of GPU memory used by this resource in bytes. It is approximate since we
    // aren't aware of additional padding or copies made by the driver.
    size_t gpuMemorySize() const { return fGpuMemorySize; }

    class UniqueID {
    public:
        UniqueID() = default;

        explicit UniqueID(uint32_t id) : fID(id) {}

        uint32_t asUInt() const { return fID; }

        bool operator==(const UniqueID& other) const { return fID == other.fID; }
        bool operator!=(const UniqueID& other) const { return !(*this == other); }

    private:
        uint32_t fID = SK_InvalidUniqueID;
    };

    // Gets an id that is unique for this Resource object. It is static in that it does not change
    // when the content of the Resource object changes. This will never return 0.
    UniqueID uniqueID() const { return fUniqueID; }

    std::string getLabel() const { return fLabel; }

    // We allow the label on a Resource to change when used for a different function. For example
    // when reusing a scratch Texture we can change the label to match callers current use.
    void setLabel(std::string_view label) {
        if (fLabel == label) {
            return;
        }

        fLabel = label;

        if (!fLabel.empty()) {
            const std::string fullLabel = "Skia_" + fLabel;
            this->setBackendLabel(fullLabel.c_str());
        }
    }

    // Tests whether a object has been abandoned or released. All objects will be in this state
    // after their creating Context is destroyed or abandoned.
    //
    // @return true if the object has been released or abandoned,
    //         false otherwise.
    // TODO: As of now this function isn't really needed because in freeGpuData we are always
    // deleting this object. However, I want to implement all the purging logic first to make sure
    // we don't have a use case for calling internalDispose but not wanting to delete the actual
    // object yet.
    bool wasDestroyed() const { return fSharedContext == nullptr; }

    // Describes the type of gpu resource that is represented by the implementing
    // class (e.g. texture, buffer, etc).  This data is used for diagnostic
    // purposes by dumpMemoryStatistics().
    //
    // The value returned is expected to be long lived and will not be copied by the caller.
    virtual const char* getResourceType() const = 0;

    virtual const Texture* asTexture() const { return nullptr; }

#if defined(GPU_TEST_UTILS)
    bool testingShouldDeleteASAP() const { return fDeleteASAP == DeleteASAP::kYes; }
#endif

protected:
    Resource(const SharedContext*,
             Ownership,
             size_t gpuMemorySize,
             bool reusableRequiresPurgeable = false);
    virtual ~Resource();

    const SharedContext* sharedContext() const { return fSharedContext; }

    // Needs to be protected for DawnBuffer's emscripten prepareForReturnToCache
    void setDeleteASAP() { fDeleteASAP = DeleteASAP::kYes; }

private:
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // The following set of functions are only meant to be called by the [Global|Proxy]Cache. We
    // don't want them public general users of a Resource, but they also aren't purely internal.
    ///////////////////////////////////////////////////////////////////////////////////////////////
    friend class ProxyCache; // for setDeleteASAP and updateAccessTime
    friend GlobalCache; // for lastAccessTime and updateAccessTime

    enum class DeleteASAP : bool {
        kNo = false,
        kYes = true,
    };

    DeleteASAP shouldDeleteASAP() const { return fDeleteASAP; }

    // In the ResourceCache this is called whenever a Resource is moved into the purgeableQueue. It
    // may also be called by the ProxyCache and GlobalCache to track the time on Resources they are
    // holding on to.
    void updateAccessTime() { fLastAccess = skgpu::StdSteadyClock::now(); }
    skgpu::StdSteadyClock::time_point lastAccessTime() const { return fLastAccess; }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // The following set of functions are only meant to be called by the ResourceCache. We don't
    // want them public general users of a Resource, but they also aren't purely internal calls.
    ///////////////////////////////////////////////////////////////////////////////////////////////
    friend class ResourceCache;

    void setBudgeted(Budgeted budgeted) {
        SkASSERT(budgeted == Budgeted::kNo || fOwnership == Ownership::kOwned);
        fBudgeted = budgeted;
    }
    void setShareable(Shareable shareable) {
        SkASSERT(shareable == Shareable::kNo || fBudgeted == Budgeted::kYes);
        fShareable = shareable;
    }

    void setAvailableForReuse(bool avail) { fAvailableForReuse = avail; }
    bool isAvailableForReuse() const { return fAvailableForReuse; }

    uint32_t lastUseToken() const { return fLastUseToken; }
    void setLastUseToken(uint32_t token) { fLastUseToken = token; }

    void setNextInReturnQueue(Resource* next) {
        SkASSERT(this->hasReturnQueueRef());
        fNextInReturnQueue = next;
    }

    int* accessCacheIndex() const { return &fCacheArrayIndex; }
    const ResourceCache* cache() const { return fReturnCache.get(); }

    // If possible, queries the backend API to check the current allocation size of the gpu
    // resource and updates the tracked value. This is specifically useful for Vulkan backends which
    // use lazy allocated memory for "memoryless" resources. Ideally that memory should stay zero
    // throughout its usage, but certain usage patterns can trigger the device to commit real memory
    // to the resource. So this will allow us to have a more accurate tracking of our memory usage.
    void updateGpuMemorySize() { fGpuMemorySize = this->onUpdateGpuMemorySize(); }

    // Dumps memory usage information for this Resource to traceMemoryDump.
    void dumpMemoryStatistics(SkTraceMemoryDump* traceMemoryDump, bool inPurgeableQueue) const;

    /**
     * If the resource has a non-shareable key then this gives the resource subclass an opportunity
     * to prepare itself to re-enter the cache. The ResourceCache extends its privilege to take the
     * first UsageRef to this function via takeRef. If takeRef is called this resource will not
     * immediately enter the cache but will be re-reprocessed when the usage ref count again reaches
     * zero.
     *
     * Return true if takeRef() was invoked.
     */
    virtual bool prepareForReturnToCache(const std::function<void()>& takeRef) { return false; }

    // Adds a cache ref to the resource. May only be called once.
    void registerWithCache(sk_sp<ResourceCache>, const GraphiteResourceKey&, Budgeted, Shareable);

    // This version of ref allows adding a ref when the usage count is 0. This should only be called
    // from the ResourceCache.
    void initialUsageRef() const {
        this->addRef<RefType::kUsage, /*MustHaveUsageRefs=*/false>();
    }

    // Removes a cache ref from the resource. The unref here should only ever be called from the
    // ResourceCache and only in the Recorder/Context thread the ResourceCache is part of.
    void unrefCache() const {
        SkASSERT(fReturnCache);
        this->removeRef<RefType::kCache>();
    }

    // Removes the return queue ref that was held while the Resource was in the queue. This can only
    // be called by the ResourceCache on its thread. It should not be called after unrefCache().
    // It must only be called after the cache has removed the resource from its return queue.
    //
    // Returns {isReusable, isPurgeable} atomically based on the reference state when the return
    // queue ref was removed. `isReusable` is true if all refs affecting reusability were zero
    // when the queue ref was removed. `isPurgeable` is true if all usage and command buffer refs
    // were zero.
    //
    // If true is returned there are no other refs that could trigger a reusable or purgeable state
    // change. A resource that entered the return queue due to becoming reusable or purgeable only
    // happens if that ref count reached zero, so there should be no external ref holder (other than
    // the ResourceCache with its separate cache ref). However, unrefReturnQueue() is only called by
    // the ResourceCache so it won't be simultaneously handing out usage refs.
    //
    // If false is returned, it is possible for the resource to immediately become purgeable on
    // another thread but since this thread has released its return queue ref, the Resource will
    // simply go back in the next return queue.
    //
    // The cache should track the Resource based on this return value instead of re-checking the
    // ref counts as that would not be an atomic operation.
    std::tuple<bool, bool, Resource*> unrefReturnQueue() {
        // We must reset the fNextInReturnQueue value *before* removing the return queue ref, but we
        // need to return the old value to the ResourceCache so that it can continue iterating over
        // the linked list.
        Resource* next = fNextInReturnQueue;
        fNextInReturnQueue = nullptr;

        uint64_t origRefs = this->removeRef<RefType::kReturnQueue>();

        // Since we should always have a cache ref when this is called, the Resource will never be
        // transitioning to having zero refs, although if `true` is returned the cache may choose to
        // then drop its cache ref.
        SkASSERT((origRefs & RefMask(RefType::kCache)) != 0);
        // `fReusableRefMask` always includes the ReturnQueue ref mask, and since we just removed
        // the return ref value, `origRefs` also includes the the ReturnQueue ref mask bit. We have
        // to compare to the ref mask to detect the case when the actual reusable refs are all zero.
        // Since PurgeableMask() does not add the ReturnQueue ref mask, it *can* compare to zero.
        return {(origRefs & fReusableRefMask) == RefMask(RefType::kReturnQueue),
                (origRefs & PurgeableMask()) == 0,
                next};
    }

#if defined(SK_DEBUG) || defined(GPU_TEST_UTILS)
    bool hasCacheRef() const {
        return (fRefs.load(std::memory_order_acquire) & RefMask(RefType::kCache)) != 0;
    }

    bool hasReturnQueueRef() const {
        return (fRefs.load(std::memory_order_acquire) & RefMask(RefType::kReturnQueue)) != 0;
    }

    bool inReturnQueue() const {
        return this->hasReturnQueueRef() && SkToBool(fNextInReturnQueue);
    }

    bool isUsableAsScratch() const {
        // This is only called by the ResourceCache, so the state of the Resource's refs won't
        // be changed by another thread when isReusable is true.
        uint64_t origRefs = fRefs.load(std::memory_order_acquire) & ~RefMask(RefType::kReturnQueue);
        bool isReusable = (origRefs & fReusableRefMask) == 0;
        return fShareable == Shareable::kScratch || (fShareable == Shareable::kNo && isReusable);
    }

    bool isPurgeable() const {
        // This is only called by the ResourceCache on its thread; if the usage and CB ref counts
        // are 0, the ResourceCache is the only way in which they can become non-zero again.
        return (fRefs.load(std::memory_order_acquire) & PurgeableMask()) == 0;
    }

    bool isUniquelyHeld() const {
        // This intentionally checks that the cache ref and return queue refs are 0, so that fRefs
        // is compared to the value it is initialized with.
        return fRefs.load(std::memory_order_acquire) == RefIncrement(RefType::kUsage);
    }

    bool hasAnyRefs() const {
        // Because all ref counts are packed into the same atomic, when this load is actually 0
        // there are no other threads that can reach the object and add new refs (assuming a raw
        // pointer has never leaked).
        return fRefs.load(std::memory_order_acquire) != 0;
    }
#endif

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // The remaining calls are meant to be truely private (including virtuals for subclasses)
    ///////////////////////////////////////////////////////////////////////////////////////////////

    // Overridden to free GPU resources in the backend API.
    virtual void freeGpuData() = 0;

    // Overridden to call any release callbacks, if necessary
    virtual void invokeReleaseProc() {}

    // Overridden to set the label on the underlying GPU resource
    virtual void setBackendLabel(char const* label) {}

    // Overridden to add extra information to the memory dump.
    virtual void onDumpMemoryStatistics(SkTraceMemoryDump* traceMemoryDump,
                                        const char* dumpName) const {}


    // Overridden to calculate a more up-to-date size in bytes.
    virtual size_t onUpdateGpuMemorySize() { return fGpuMemorySize; }

    // Try to add the Resource to the cache's return queue for pending reuse.
    // This should only be called when there is a cache to return to, and the calling thread
    // successfully transitioned from no "return queue" ref to setting the return queue ref.
    //
    // Returns true if the cache accepted the Resource (in which case the set return ref should
    // remain set for the cache to remove). If false is returned, the caller should clear the
    // return queue ref (and possible dispose of the object).
    bool returnToCache() const;

    // Frees the object in the underlying 3D API *and* deletes the object itself.
    void internalDispose();

    // Resource tracks its different ref counts packed into a single atomic 64-bit value.
    // The bits are split into subfields:
    // commandBufferRefs:31 e.g. RefMask(kCB)
    // usageRefs:31              RefMask(kUsage)
    // returnQueueRef: 1         RefMask(kReturnQueue)
    // cacheRefs:1               RefMask(kCache)
    //
    // RefIncrement() and RefMask() help access specific ref type's values.
    static constexpr uint64_t RefIncrement(RefType refType) {
        switch (refType) {
            case RefType::kCommandBuffer: return (uint64_t) 1 << 33;
            case RefType::kUsage:         return (uint64_t) 1 << 2;
            case RefType::kCache:         return (uint64_t) 1 << 1;
            case RefType::kReturnQueue:   return (uint64_t) 1 << 0;
        }
        SkUNREACHABLE;
    }
    static inline constexpr uint64_t RefMask(RefType refType) {
        switch (refType) {
            case RefType::kCommandBuffer: return (((uint64_t)1 << 31) - 1) << 33;
            case RefType::kUsage:         return (((uint64_t)1 << 31) - 1) << 2;
            case RefType::kCache:         return 0b10;
            case RefType::kReturnQueue:   return 0b01;
        }
        SkUNREACHABLE;
    }
    static inline constexpr uint64_t PurgeableMask() {
        return RefMask(RefType::kUsage) | RefMask(RefType::kCommandBuffer);
    }

    template <RefType kType, bool MustHaveUsageRefs=true>
    void addRef() const {
        static_assert(kType != RefType::kReturnQueue, "return queue refs cannot be added directly");
        static constexpr uint64_t kRefIncrement = RefIncrement(kType);
        // No barrier required
        [[maybe_unused]] uint64_t origCnt =
                fRefs.fetch_add(kRefIncrement, std::memory_order_relaxed);
        // Require that there was an already held usage ref in order to add this new ref,
        // e.g. to add a command buffer ref, a usage ref must already be held; calling code can't
        // add usage refs if it wasn't explicitly handed out by the cache.
        SkASSERT(!MustHaveUsageRefs || (origCnt & RefMask(RefType::kUsage)) > 0);
        // And make sure that the specific type of ref did not overflow into another field
        SkASSERT((RefMask(kType) - (origCnt & RefMask(kType))) >= kRefIncrement);
    }

    template <RefType kType>
    uint64_t removeRef() const {
        static constexpr uint64_t kRefIncrement = RefIncrement(kType);

        uint64_t origRefs;
        if (kType == RefType::kCache || kType == RefType::kReturnQueue || !fReturnCache) {
            // Without a ResourceCache, or when it's a cache/return-queue unref, there is no
            // non-atomic work that has to happen so simply update the ref count. If the net ref
            // count reaches 0 we can safely delete the resource because no other thread will
            // increase the refs.
            origRefs = fRefs.fetch_sub(kRefIncrement, std::memory_order_acq_rel);
            SkASSERT((origRefs & RefMask(kType)) >= kRefIncrement); // had a ref to remove

            if (origRefs == kRefIncrement) {
                SkASSERT(!this->hasAnyRefs());
                Resource* mutableThis = const_cast<Resource*>(this);
                mutableThis->internalDispose();
            }
        } else {
            SkASSERT(kType == RefType::kCommandBuffer || kType == RefType::kUsage);
            // When removing a usage or CB ref and the resource is registered with the cache,
            // it may need to be returned to the cache. A resource can only be in the return queue
            // a single time and must remain alive until cache removes it from the queue. A CAS
            // loop is used to atomically decrement the ref and add the return queue ref.
            uint64_t nextRefs;
            bool needsReturn;
            do {
                origRefs = fRefs.load(std::memory_order_acquire);
                SkASSERT((origRefs & RefMask(kType)) >= kRefIncrement); // have a ref to remove

                // When unreffing a usage or command buffer ref, the Resource needs to return to
                // the queue when:
                //  - it's not already in the return queue (return queue ref is 0) AND
                //  - it's transitioning from non-reusable -> reusable OR non-purgeable -> purgeable
                //
                // Including RefMask(kReturnQueue) in the bitwise &'s before comparing to the
                // ref increment ensures that the return queue ref was 0 in origRefs.
                static constexpr uint64_t kPurgeableReturnMask = PurgeableMask() |
                                                                 RefMask(RefType::kReturnQueue);
                // fReusableRefMask should have added this bit added during construction.
                SkASSERT((fReusableRefMask & RefMask(RefType::kReturnQueue)) != 0);
                // This expression matches the above logic for returning because:
                //  - Both kPurgeableReturnMask and fReusableRefMask include the return queue bit,
                //    but kRefIncrement does not. The only way the comparisons can be true is if
                //    the return queue bit is unset.
                //  - When the resource is reusable only when purgeable, then both sides of the ||
                //    are identical because fReusableRefMask will equal kPurgeableReturnMask.
                //    And if the == returns true, we know origRefs was non-zero and nextRefs will
                //    be zero since it subtracts kRefIncrement.
                //  - When the resource is reusable when just the usage refs reach 0, the purgeable
                //    state transition works like before. But fReusableRefMask will mask out any
                //    non-zero bits in the command buffer subfield.
                //      - When kRefType==kUsage, the right-hand == will be true when origRefs had
                //        one usage ref left and nextRefs holds zero.
                //      - When kRefType==kCommandBuffer, the right-hand side of the || will always
                //        be false because kRefIncrement will hold bits outside of fReusableRefMask.
                //        This ensures that the non-reusable -> reusable transition occurs solely
                //        on removing a usage ref.
                SkASSERT((kRefIncrement & RefMask(RefType::kReturnQueue)) == 0);
                needsReturn = ((origRefs & kPurgeableReturnMask) == kRefIncrement) ||
                              ((origRefs & fReusableRefMask) == kRefIncrement);

                nextRefs = (origRefs - kRefIncrement) |
                           (needsReturn ? RefMask(RefType::kReturnQueue) : 0);
                // If origRefs already included a return queue ref, nextRefs hasn't changed that
                SkASSERT((origRefs & RefMask(RefType::kReturnQueue)) ==
                         (nextRefs & RefMask(RefType::kReturnQueue)) || needsReturn);
            } while (!fRefs.compare_exchange_weak(origRefs, nextRefs,
                                                  std::memory_order_release,
                                                  std::memory_order_relaxed));
            // NOTE: because RefMask(RefType::kReturnQueue) was included in the `needsReturn` check,
            // we know that it was unset in `origRefs`, and was added to `nextRefs`. The CAS ensures
            // that this was the thread that added the return queue ref if `needsReturn` is true
            // when the do-while loop exits.

            if (needsReturn && !this->returnToCache()) {
                // The cache rejected the resource, so we need to unset the "return queue" ref that
                // we added above, which may be the last ref keeping the object alive.
                SkASSERT(!fNextInReturnQueue);
                origRefs = this->removeRef<RefType::kReturnQueue>();
                // so do not access *this* after this point!
            }
            // else we weren't returning the resource yet, or the cache is maintaining the return
            // ref until the return queue has been drained.
        }

        return origRefs;
    }

    static constexpr size_t kInvalidGpuMemorySize = ~static_cast<size_t>(0);

    // See RefIncrement() for how the bits in this field are interpreted.
    mutable std::atomic<uint64_t> fRefs;

    // Depending on when the resource can be reused, there are two base values:
    // 1. RefMask(kUsage): reused while there is outstanding GPU work (CB ref count is ignored).
    // 2. RefMask(kUsage) | RefMask(kCB): cannot be reused until it is also purgeable.
    // To simplify logic in removeRef(), this value always includes RefMask(kReturnQueue).
    // See removeRef() for rationale.
    //
    // NOTE: Reusability is related but distinct from shareability. Shareability takes into account
    // external information about when and for how long the state of the resource must remain stable
    // We track when all resources become "reusable" again even if they were fully shareable because
    // that marks when the resource can also change its Shareable type.
    const uint64_t fReusableRefMask;

    // This is not ref'ed but internalDispose() will be called before the Gpu object is destroyed.
    // That call will set this to nullptr.
    const SharedContext* fSharedContext;

    const Ownership fOwnership;
    const UniqueID fUniqueID;

    // The resource key and return cache are both set at most once, during registerWithCache().
    /*const*/ GraphiteResourceKey  fKey;
    /*const*/ sk_sp<ResourceCache> fReturnCache;

    // Resources added to their return cache's queue are tracked in a lock-free thread-safe
    // singly-linked list whose head element is stored on the cache, and next elements are stored
    // inline in Resource. This can only be modified by the thread that set the return queue ref,
    // or by the thread that is removing said ref.
    //
    // A null value means the Resource is not in the return queue. A non-null value means it is in
    // the queue, although the ResourceCache assigns a special sentinel value for the tail address.
    Resource* fNextInReturnQueue = nullptr;

    // The remaining fields are mutable state that is only modified by the ResourceCache on the
    // cache's thread, guarded by `fReturnCache::fSingleOwner`.

    size_t fGpuMemorySize = kInvalidGpuMemorySize;

    // All resources created internally by Graphite that are held in the ResourceCache as shared or
    // available scratch resources are considered budgeted. Resources that back client-owned objects
    // (e.g. SkSurface or SkImage) and wrapper objects (e.g. BackendTexture) do not count against
    // cache limits and therefore should never be budgeted.
    Budgeted fBudgeted = Budgeted::kNo;
    // All resources start out as non-shareable (the strictest mode) and revert to non-shareable
    // when they are returned to the cache and have no more usage refs. An available resource can
    // be returned if its shareable type matches the request, or if it was non-shareable at which
    // point the resource is upgraded to the more permissive mode (until all shared usages are
    // dropped at which point it can be used for any purpose again).
    Shareable fShareable = Shareable::kNo;

    // This is only used by ProxyCache::purgeProxiesNotUsedSince which is called from
    // ResourceCache::purgeResourcesNotUsedSince. When kYes, this signals that the Resource
    // should've been purged based on its timestamp at some point regardless of what its
    // current timestamp may indicate (since the timestamp will be updated when the Resource
    // is returned to the ResourceCache).
    DeleteASAP fDeleteASAP = DeleteASAP::kNo;

    // Set to true when the resource is contained in its cache's `fResourceMap`, which allows it to
    // be returned from findAndRefResource().
    bool fAvailableForReuse = false;

    // An index into a heap when this resource is purgeable or an array when not. This is maintained
    // by the cache. Must be mutable to fit SkTDPQueue's access API.
    mutable int fCacheArrayIndex = -1;

    // This value reflects how recently this resource was accessed in the cache. This is maintained
    // by the cache. It defines a total order over resources, even if their fLastAccess times are
    // the same (i.e. returned at time points less than the system's granularity).
    uint32_t fLastUseToken;
    skgpu::StdSteadyClock::time_point fLastAccess;

    // String used to describe the current use of this Resource.
    std::string fLabel;
};

} // namespace skgpu::graphite

#endif // skgpu_graphite_Resource_DEFINED
