| /* |
| * Copyright 2021 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_PipelineDataCache_DEFINED |
| #define skgpu_graphite_PipelineDataCache_DEFINED |
| |
| #include "include/core/SkRefCnt.h" |
| #include "src/core/SkArenaAlloc.h" |
| #include "src/core/SkPipelineData.h" |
| |
| #include <unordered_map> |
| #include <vector> |
| |
| namespace skgpu::graphite { |
| |
| // Add a block of data to the cache and return a unique ID that corresponds to its |
| // contents. If an identical block of data is already in the cache, that unique ID is returned. |
| // A StorageT captures how the memory of the BaseT is managed in the cache |
| // A BaseT captures the datatype that is stored in the cache and must have: |
| // uint32_t hash() const; |
| // operator== |
| // static StorageT Make(const BaseT&, SkArenaAlloc*); |
| // |
| // Note: The baseT/storageT split is only required until the SkTextureDataBlock is also stored |
| // in an arena. |
| template<typename StorageT, typename BaseT> |
| class PipelineDataCache { |
| public: |
| static constexpr uint32_t kInvalidIndex = 0; |
| |
| PipelineDataCache() { |
| // kInvalidIndex is reserved |
| static_assert(kInvalidIndex == 0); |
| fDataBlocks.push_back({}); |
| fDataBlockIDs.insert({nullptr, Index()}); |
| } |
| |
| // TODO: For the uniform data version of this cache we should revisit the insert and Make APIs: |
| // 1. UniformData::Make requires knowing the data size up front, which involves two invocations |
| // of the UniformManager. Ideally, we could align uniforms on the fly into a dynamic buffer. |
| // 2. UniformData stores the offsets for each uniform, but these aren't needed after we've |
| // filled out the buffer. If we remember layout offsets, it should be stored per Combination |
| // or RenderStep that defines the uniform set. |
| // 3. UniformCache's ids are only fundamentally limited by the number of draws that can be |
| // recorded into a DrawPass, which means a very large recording with multiple passes could |
| // exceed uint32_t across all the passes. |
| // 4. The check to know if a UniformData is present in the cache is practically the same for |
| // checking if the data needs to be uploaded to the GPU, so UniformCache could remember the |
| // associated BufferBindInfos as well. |
| // 5. Because UniformCache only cares about the content byte hash/equality, and can memcpy to |
| // the GPU buffer, the cached data contents could all go into a shared byte array, instead of |
| // needing to extend SkRefCnt. |
| // 6. insert() as a name can imply that the value is always added, so we may want a better one. |
| // It can be a little less generic if UniformCache returns id and bind buffer info. On the |
| // other hand unordered_map::insert has the same semantics as this insert, so maybe it's fine |
| |
| // Simple wrapper around the returned index to keep all the uint32_ts straight |
| class Index { |
| public: |
| Index() : fIndex(kInvalidIndex) {} |
| explicit Index(uint32_t index) : fIndex(index) {} |
| |
| bool operator==(const Index& that) const { return fIndex == that.fIndex; } |
| bool operator!=(const Index& that) const { return !(*this == that); } |
| bool isValid() const { return fIndex != kInvalidIndex; } |
| uint32_t asUInt() const { return fIndex; } |
| private: |
| uint32_t fIndex; |
| }; |
| |
| Index insert(const BaseT& dataBlock) { |
| auto kv = fDataBlockIDs.find(const_cast<BaseT*>(&dataBlock)); |
| if (kv != fDataBlockIDs.end()) { |
| return kv->second; |
| } |
| |
| Index id(SkTo<uint32_t>(fDataBlocks.size())); |
| SkASSERT(id.isValid()); |
| |
| StorageT tmp(BaseT::Make(dataBlock, &fArena)); |
| fDataBlockIDs.insert({tmp.get(), id}); |
| fDataBlocks.push_back(std::move(tmp)); |
| this->validate(); |
| return id; |
| } |
| |
| const BaseT* lookup(Index uniqueID) { |
| SkASSERT(uniqueID.asUInt() < fDataBlocks.size()); |
| return fDataBlocks[uniqueID.asUInt()].get(); |
| } |
| |
| // The number of unique BaseT objects in the cache |
| size_t count() const { |
| SkASSERT(fDataBlocks.size() == fDataBlockIDs.size() && fDataBlocks.size() > 0); |
| return fDataBlocks.size() - 1; /* -1 to discount the invalidID's entry */ |
| } |
| |
| private: |
| struct Hash { |
| // This hash operator de-references and hashes the data contents |
| size_t operator()(const BaseT* dataBlock) const { |
| if (!dataBlock) { |
| return 0; |
| } |
| |
| return dataBlock->hash(); |
| } |
| }; |
| struct Eq { |
| // This equality operator de-references and compares the actual data contents |
| bool operator()(const BaseT* a, const BaseT* b) const { |
| if (!a || !b) { |
| return !a && !b; |
| } |
| |
| return *a == *b; |
| } |
| }; |
| |
| // Note: the unique IDs are only unique w/in a Recorder or a Recording _not_ globally |
| std::unordered_map<const BaseT*, Index, Hash, Eq> fDataBlockIDs; |
| std::vector<StorageT> fDataBlocks; |
| SkArenaAlloc fArena{0}; |
| |
| void validate() const { |
| #ifdef SK_DEBUG |
| for (size_t i = 0; i < fDataBlocks.size(); ++i) { |
| auto kv = fDataBlockIDs.find(fDataBlocks[i].get()); |
| SkASSERT(kv != fDataBlockIDs.end()); |
| SkASSERT(kv->first == fDataBlocks[i].get()); |
| SkASSERT(SkTo<uint32_t>(i) == kv->second.asUInt()); |
| } |
| #endif |
| } |
| }; |
| |
| // A UniformDataCache lives for the entire duration of a Recorder. As such it has a greater |
| // likelihood of overflowing a uint32_t index. |
| using UniformDataCache = PipelineDataCache<SkUniformDataBlockPassThrough, SkUniformDataBlock>; |
| |
| // A TextureDataCache only lives for a single Recording. When a Recording is snapped it is pulled |
| // off of the Recorder and goes with the Recording as a record of the required Textures and |
| // Samplers. |
| using TextureDataCache = PipelineDataCache<std::unique_ptr<SkTextureDataBlock>, SkTextureDataBlock>; |
| |
| } // namespace skgpu::graphite |
| |
| #endif // skgpu_graphite_PipelineDataCache_DEFINED |