// Copyright 2020 The Dawn Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #ifndef DAWNNATIVE_SUBRESOURCESTORAGE_H_ #define DAWNNATIVE_SUBRESOURCESTORAGE_H_ #include "common/Assert.h" #include "common/TypeTraits.h" #include "dawn_native/EnumMaskIterator.h" #include "dawn_native/Subresource.h" #include #include #include #include namespace dawn_native { // SubresourceStorage acts like a simple map from subresource (aspect, layer, level) to a // value of type T except that it tries to compress similar subresources so that algorithms // can act on a whole range of subresources at once if they have the same state. // // For example a very common case to optimize for is the tracking of the usage of texture // subresources inside a render pass: the vast majority of texture views will select the whole // texture while a small minority will select a sub-range. We want to optimize the common case // by setting and checking a single "usage" value when a full subresource is used but at the // same time allow per-subresource data when needed. // // Another example is barrier tracking per-subresource in the backends: it will often happen // that during texture upload each mip level will have a different "barrier state". However // when the texture is fully uploaded and after it is used for sampling (with a full view) for // the first time, the barrier state will likely be the same across all the subresources. // That's why some form of "recompression" of subresource state must be possibe. // // In order to keep the implementation details private and to avoid iterator-hell, this // container uses a more functional approach of calling a closure on the interesting ranges. // This is for example how to look at the state of all subresources. // // subresources.Iterate([](const SubresourceRange& range, const T& data) { // // Do something with the knowledge that all the subresources in `range` have value // // `data`. // }); // // SubresourceStorage internally tracks compression state per aspect and then per layer of each // aspect. This means that a 2-aspect texture can have the following compression state: // // - Aspect 0 is fully compressed. // - Aspect 1 is partially compressed: // - Aspect 1 layer 3 is decompressed. // - Aspect 1 layer 0-2 and 4-42 are compressed. // // A useful model to reason about SubresourceStorage is to represent is as a tree: // // - SubresourceStorage is the root. // |-> Nodes 1 deep represent each aspect. If an aspect is compressed, its node doesn't have // any children because the data is constant across all of the subtree. // |-> Nodes 2 deep represent layers (for uncompressed aspects). If a layer is compressed, // its node doesn't have any children because the data is constant across all of the // subtree. // |-> Nodes 3 deep represent individial mip levels (for uncompressed layers). // // The concept of recompression is the removal of all child nodes of a non-leaf node when the // data is constant across them. Decompression is the addition of child nodes to a leaf node // and copying of its data to all its children. // // The choice of having secondary compression for array layers is to optimize for the cases // where transfer operations are used to update specific layers of texture with render or // transfer operations, while the rest is untouched. It seems much less likely that there // would be operations that touch all Nth mips of a 2D array texture without touching the // others. // // There are several hot code paths that create new SubresourceStorage like the tracking of // resource usage per-pass. We don't want to allocate a container for the decompressed data // unless we have to because it would dramatically lower performance. Instead // SubresourceStorage contains an inline array that contains the per-aspect compressed data // and only allocates a per-subresource on aspect decompression. // // T must be a copyable type that supports equality comparison with ==. // // The implementation of functions in this file can have a lot of control flow and corner cases // so each modification should come with extensive tests and ensure 100% code coverage of the // modified functions. See instructions at // https://chromium.googlesource.com/chromium/src/+/master/docs/testing/code_coverage.md#local-coverage-script // to run the test with code coverage. A command line that worked in the past (with the right // GN args for the out/coverage directory in a Chromium checkout) is: // /* python tools/code_coverage/coverage.py dawn_unittests -b out/coverage -o out/report -c \ "out/coverage/dawn_unittests --gtest_filter=SubresourceStorage\*" -f \ third_party/dawn/src/dawn_native */ // // TODO(crbug.com/dawn/836): Make the recompression optional, the calling code should know // if recompression can happen or not in Update() and Merge() template class SubresourceStorage { public: static_assert(std::is_copy_assignable::value, "T must be copyable"); static_assert(HasEqualityOperator::value, "T requires bool operator == (T, T)"); // Creates the storage with the given "dimensions" and all subresources starting with the // initial value. SubresourceStorage(Aspect aspects, uint32_t arrayLayerCount, uint32_t mipLevelCount, T initialValue = {}); // Returns the data for a single subresource. Note that the reference returned might be the // same for multiple subresources. const T& Get(Aspect aspect, uint32_t arrayLayer, uint32_t mipLevel) const; // Given an iterateFunc that's a function or function-like objet that can be called with // arguments of type (const SubresourceRange& range, const T& data) and returns void, // calls it with aggregate ranges if possible, such that each subresource is part of // exactly one of the ranges iterateFunc is called with (and obviously data is the value // stored for that subresource). For example: // // subresources.Iterate([&](const SubresourceRange& range, const T& data) { // // Do something with range and data. // }); template void Iterate(F&& iterateFunc) const; // Given an updateFunc that's a function or function-like objet that can be called with // arguments of type (const SubresourceRange& range, T* data) and returns void, // calls it with ranges that in aggregate form `range` and pass for each of the // sub-ranges a pointer to modify the value for that sub-range. For example: // // subresources.Update(view->GetRange(), [](const SubresourceRange&, T* data) { // *data |= wgpu::TextureUsage::Stuff; // }); // // /!\ WARNING: updateFunc should never use range to compute the update to data otherwise // your code is likely to break when compression happens. Range should only be used for // side effects like using it to compute a Vulkan pipeline barrier. template void Update(const SubresourceRange& range, F&& updateFunc); // Given a mergeFunc that's a function or a function-like object that can be called with // arguments of type (const SubresourceRange& range, T* data, const U& otherData) and // returns void, calls it with ranges that in aggregate form the full resources and pass // for each of the sub-ranges a pointer to modify the value for that sub-range and the // corresponding value from other for that sub-range. For example: // // subresources.Merge(otherUsages, // [](const SubresourceRange&, T* data, const T& otherData) { // *data |= otherData; // }); // // /!\ WARNING: mergeFunc should never use range to compute the update to data otherwise // your code is likely to break when compression happens. Range should only be used for // side effects like using it to compute a Vulkan pipeline barrier. template void Merge(const SubresourceStorage& other, F&& mergeFunc); // Other operations to consider: // // - UpdateTo(Range, T) that updates the range to a constant value. // Methods to query the internal state of SubresourceStorage for testing. Aspect GetAspectsForTesting() const; uint32_t GetArrayLayerCountForTesting() const; uint32_t GetMipLevelCountForTesting() const; bool IsAspectCompressedForTesting(Aspect aspect) const; bool IsLayerCompressedForTesting(Aspect aspect, uint32_t layer) const; private: template friend class SubresourceStorage; void DecompressAspect(uint32_t aspectIndex); void RecompressAspect(uint32_t aspectIndex); void DecompressLayer(uint32_t aspectIndex, uint32_t layer); void RecompressLayer(uint32_t aspectIndex, uint32_t layer); SubresourceRange GetFullLayerRange(Aspect aspect, uint32_t layer) const; // LayerCompressed should never be called when the aspect is compressed otherwise it would // need to check that mLayerCompressed is not null before indexing it. bool& LayerCompressed(uint32_t aspectIndex, uint32_t layerIndex); bool LayerCompressed(uint32_t aspectIndex, uint32_t layerIndex) const; // Return references to the data for a compressed plane / layer or subresource. // Each variant should be called exactly under the correct compression level. T& DataInline(uint32_t aspectIndex); T& Data(uint32_t aspectIndex, uint32_t layer, uint32_t level = 0); const T& DataInline(uint32_t aspectIndex) const; const T& Data(uint32_t aspectIndex, uint32_t layer, uint32_t level = 0) const; Aspect mAspects; uint8_t mMipLevelCount; uint16_t mArrayLayerCount; // Invariant: if an aspect is marked compressed, then all it's layers are marked as // compressed. static constexpr size_t kMaxAspects = 2; std::array mAspectCompressed; std::array mInlineAspectData; // Indexed as mLayerCompressed[aspectIndex * mArrayLayerCount + layer]. std::unique_ptr mLayerCompressed; // Indexed as mData[(aspectIndex * mArrayLayerCount + layer) * mMipLevelCount + level]. // The data for a compressed aspect is stored in the slot for (aspect, 0, 0). Similarly // the data for a compressed layer of aspect if in the slot for (aspect, layer, 0). std::unique_ptr mData; }; template SubresourceStorage::SubresourceStorage(Aspect aspects, uint32_t arrayLayerCount, uint32_t mipLevelCount, T initialValue) : mAspects(aspects), mMipLevelCount(mipLevelCount), mArrayLayerCount(arrayLayerCount) { ASSERT(arrayLayerCount <= std::numeric_limits::max()); ASSERT(mipLevelCount <= std::numeric_limits::max()); uint32_t aspectCount = GetAspectCount(aspects); ASSERT(aspectCount <= kMaxAspects); for (uint32_t aspectIndex = 0; aspectIndex < aspectCount; aspectIndex++) { mAspectCompressed[aspectIndex] = true; DataInline(aspectIndex) = initialValue; } } template template void SubresourceStorage::Update(const SubresourceRange& range, F&& updateFunc) { bool fullLayers = range.baseMipLevel == 0 && range.levelCount == mMipLevelCount; bool fullAspects = range.baseArrayLayer == 0 && range.layerCount == mArrayLayerCount && fullLayers; for (Aspect aspect : IterateEnumMask(range.aspects)) { uint32_t aspectIndex = GetAspectIndex(aspect); // Call the updateFunc once for the whole aspect if possible or decompress and fallback // to per-layer handling. if (mAspectCompressed[aspectIndex]) { if (fullAspects) { SubresourceRange updateRange = SubresourceRange::MakeFull(aspect, mArrayLayerCount, mMipLevelCount); updateFunc(updateRange, &DataInline(aspectIndex)); continue; } DecompressAspect(aspectIndex); } uint32_t layerEnd = range.baseArrayLayer + range.layerCount; for (uint32_t layer = range.baseArrayLayer; layer < layerEnd; layer++) { // Call the updateFunc once for the whole layer if possible or decompress and // fallback to per-level handling. if (LayerCompressed(aspectIndex, layer)) { if (fullLayers) { SubresourceRange updateRange = GetFullLayerRange(aspect, layer); updateFunc(updateRange, &Data(aspectIndex, layer)); continue; } DecompressLayer(aspectIndex, layer); } // Worst case: call updateFunc per level. uint32_t levelEnd = range.baseMipLevel + range.levelCount; for (uint32_t level = range.baseMipLevel; level < levelEnd; level++) { SubresourceRange updateRange = SubresourceRange::MakeSingle(aspect, layer, level); updateFunc(updateRange, &Data(aspectIndex, layer, level)); } // If the range has fullLayers then it is likely we can recompress after the calls // to updateFunc (this branch is skipped if updateFunc was called for the whole // layer). if (fullLayers) { RecompressLayer(aspectIndex, layer); } } // If the range has fullAspects then it is likely we can recompress after the calls to // updateFunc (this branch is skipped if updateFunc was called for the whole aspect). if (fullAspects) { RecompressAspect(aspectIndex); } } } template template void SubresourceStorage::Merge(const SubresourceStorage& other, F&& mergeFunc) { ASSERT(mAspects == other.mAspects); ASSERT(mArrayLayerCount == other.mArrayLayerCount); ASSERT(mMipLevelCount == other.mMipLevelCount); for (Aspect aspect : IterateEnumMask(mAspects)) { uint32_t aspectIndex = GetAspectIndex(aspect); // If the other storage's aspect is compressed we don't need to decompress anything // in `this` and can just iterate through it, merging with `other`'s constant value for // the aspect. For code simplicity this can be done with a call to Update(). if (other.mAspectCompressed[aspectIndex]) { const U& otherData = other.DataInline(aspectIndex); Update(SubresourceRange::MakeFull(aspect, mArrayLayerCount, mMipLevelCount), [&](const SubresourceRange& subrange, T* data) { mergeFunc(subrange, data, otherData); }); continue; } // Other doesn't have the aspect compressed so we must do at least per-layer merging. if (mAspectCompressed[aspectIndex]) { DecompressAspect(aspectIndex); } for (uint32_t layer = 0; layer < mArrayLayerCount; layer++) { // Similarly to above, use a fast path if other's layer is compressed. if (other.LayerCompressed(aspectIndex, layer)) { const U& otherData = other.Data(aspectIndex, layer); Update(GetFullLayerRange(aspect, layer), [&](const SubresourceRange& subrange, T* data) { mergeFunc(subrange, data, otherData); }); continue; } // Sad case, other is decompressed for this layer, do per-level merging. if (LayerCompressed(aspectIndex, layer)) { DecompressLayer(aspectIndex, layer); } for (uint32_t level = 0; level < mMipLevelCount; level++) { SubresourceRange updateRange = SubresourceRange::MakeSingle(aspect, layer, level); mergeFunc(updateRange, &Data(aspectIndex, layer, level), other.Data(aspectIndex, layer, level)); } RecompressLayer(aspectIndex, layer); } RecompressAspect(aspectIndex); } } template template void SubresourceStorage::Iterate(F&& iterateFunc) const { for (Aspect aspect : IterateEnumMask(mAspects)) { uint32_t aspectIndex = GetAspectIndex(aspect); // Fastest path, call iterateFunc on the whole aspect at once. if (mAspectCompressed[aspectIndex]) { SubresourceRange range = SubresourceRange::MakeFull(aspect, mArrayLayerCount, mMipLevelCount); iterateFunc(range, DataInline(aspectIndex)); continue; } for (uint32_t layer = 0; layer < mArrayLayerCount; layer++) { // Fast path, call iterateFunc on the whole array layer at once. if (LayerCompressed(aspectIndex, layer)) { SubresourceRange range = GetFullLayerRange(aspect, layer); iterateFunc(range, Data(aspectIndex, layer)); continue; } // Slow path, call iterateFunc for each mip level. for (uint32_t level = 0; level < mMipLevelCount; level++) { SubresourceRange range = SubresourceRange::MakeSingle(aspect, layer, level); iterateFunc(range, Data(aspectIndex, layer, level)); } } } } template const T& SubresourceStorage::Get(Aspect aspect, uint32_t arrayLayer, uint32_t mipLevel) const { uint32_t aspectIndex = GetAspectIndex(aspect); ASSERT(aspectIndex < GetAspectCount(mAspects)); ASSERT(arrayLayer < mArrayLayerCount); ASSERT(mipLevel < mMipLevelCount); // Fastest path, the aspect is compressed! if (mAspectCompressed[aspectIndex]) { return DataInline(aspectIndex); } // Fast path, the array layer is compressed. if (LayerCompressed(aspectIndex, arrayLayer)) { return Data(aspectIndex, arrayLayer); } return Data(aspectIndex, arrayLayer, mipLevel); } template Aspect SubresourceStorage::GetAspectsForTesting() const { return mAspects; } template uint32_t SubresourceStorage::GetArrayLayerCountForTesting() const { return mArrayLayerCount; } template uint32_t SubresourceStorage::GetMipLevelCountForTesting() const { return mMipLevelCount; } template bool SubresourceStorage::IsAspectCompressedForTesting(Aspect aspect) const { return mAspectCompressed[GetAspectIndex(aspect)]; } template bool SubresourceStorage::IsLayerCompressedForTesting(Aspect aspect, uint32_t layer) const { return mAspectCompressed[GetAspectIndex(aspect)] || mLayerCompressed[GetAspectIndex(aspect) * mArrayLayerCount + layer]; } template void SubresourceStorage::DecompressAspect(uint32_t aspectIndex) { ASSERT(mAspectCompressed[aspectIndex]); const T& aspectData = DataInline(aspectIndex); mAspectCompressed[aspectIndex] = false; // Extra allocations are only needed when aspects are decompressed. Create them lazily. if (mData == nullptr) { ASSERT(mLayerCompressed == nullptr); uint32_t aspectCount = GetAspectCount(mAspects); mLayerCompressed = std::make_unique(aspectCount * mArrayLayerCount); mData = std::make_unique(aspectCount * mArrayLayerCount * mMipLevelCount); for (uint32_t layerIndex = 0; layerIndex < aspectCount * mArrayLayerCount; layerIndex++) { mLayerCompressed[layerIndex] = true; } } ASSERT(LayerCompressed(aspectIndex, 0)); for (uint32_t layer = 0; layer < mArrayLayerCount; layer++) { Data(aspectIndex, layer) = aspectData; ASSERT(LayerCompressed(aspectIndex, layer)); } } template void SubresourceStorage::RecompressAspect(uint32_t aspectIndex) { ASSERT(!mAspectCompressed[aspectIndex]); // All layers of the aspect must be compressed for the aspect to possibly recompress. for (uint32_t layer = 0; layer < mArrayLayerCount; layer++) { if (!LayerCompressed(aspectIndex, layer)) { return; } } T layer0Data = Data(aspectIndex, 0); for (uint32_t layer = 1; layer < mArrayLayerCount; layer++) { if (!(Data(aspectIndex, layer) == layer0Data)) { return; } } mAspectCompressed[aspectIndex] = true; DataInline(aspectIndex) = layer0Data; } template void SubresourceStorage::DecompressLayer(uint32_t aspectIndex, uint32_t layer) { ASSERT(LayerCompressed(aspectIndex, layer)); ASSERT(!mAspectCompressed[aspectIndex]); const T& layerData = Data(aspectIndex, layer); LayerCompressed(aspectIndex, layer) = false; // We assume that (aspect, layer, 0) is stored at the same place as (aspect, layer) which // allows starting the iteration at level 1. for (uint32_t level = 1; level < mMipLevelCount; level++) { Data(aspectIndex, layer, level) = layerData; } } template void SubresourceStorage::RecompressLayer(uint32_t aspectIndex, uint32_t layer) { ASSERT(!LayerCompressed(aspectIndex, layer)); ASSERT(!mAspectCompressed[aspectIndex]); const T& level0Data = Data(aspectIndex, layer, 0); for (uint32_t level = 1; level < mMipLevelCount; level++) { if (!(Data(aspectIndex, layer, level) == level0Data)) { return; } } LayerCompressed(aspectIndex, layer) = true; } template SubresourceRange SubresourceStorage::GetFullLayerRange(Aspect aspect, uint32_t layer) const { return {aspect, {layer, 1}, {0, mMipLevelCount}}; } template bool& SubresourceStorage::LayerCompressed(uint32_t aspectIndex, uint32_t layer) { ASSERT(!mAspectCompressed[aspectIndex]); return mLayerCompressed[aspectIndex * mArrayLayerCount + layer]; } template bool SubresourceStorage::LayerCompressed(uint32_t aspectIndex, uint32_t layer) const { ASSERT(!mAspectCompressed[aspectIndex]); return mLayerCompressed[aspectIndex * mArrayLayerCount + layer]; } template T& SubresourceStorage::DataInline(uint32_t aspectIndex) { ASSERT(mAspectCompressed[aspectIndex]); return mInlineAspectData[aspectIndex]; } template T& SubresourceStorage::Data(uint32_t aspectIndex, uint32_t layer, uint32_t level) { ASSERT(level == 0 || !LayerCompressed(aspectIndex, layer)); ASSERT(!mAspectCompressed[aspectIndex]); return mData[(aspectIndex * mArrayLayerCount + layer) * mMipLevelCount + level]; } template const T& SubresourceStorage::DataInline(uint32_t aspectIndex) const { ASSERT(mAspectCompressed[aspectIndex]); return mInlineAspectData[aspectIndex]; } template const T& SubresourceStorage::Data(uint32_t aspectIndex, uint32_t layer, uint32_t level) const { ASSERT(level == 0 || !LayerCompressed(aspectIndex, layer)); ASSERT(!mAspectCompressed[aspectIndex]); return mData[(aspectIndex * mArrayLayerCount + layer) * mMipLevelCount + level]; } } // namespace dawn_native #endif // DAWNNATIVE_SUBRESOURCESTORAGE_H_