diff --git a/filament/src/Engine.cpp b/filament/src/Engine.cpp index 94b16fe3ff..7ec14f78bd 100644 --- a/filament/src/Engine.cpp +++ b/filament/src/Engine.cpp @@ -255,6 +255,7 @@ void FEngine::shutdown() { */ mPostProcessManager.terminate(driver); // free-up post-process manager resources + mResourceAllocator->terminate(); mDFG->terminate(); // free-up the DFG mRenderableManager.terminate(); // free-up all renderables mLightManager.terminate(); // free-up all lights @@ -338,6 +339,8 @@ void FEngine::prepare() { } void FEngine::gc() { + // Note: this runs in a Job + JobSystem& js = mJobSystem; auto parent = js.createJob(); auto em = std::ref(mEntityManager); diff --git a/filament/src/Renderer.cpp b/filament/src/Renderer.cpp index 4477f50f3a..df48c90e2a 100644 --- a/filament/src/Renderer.cpp +++ b/filament/src/Renderer.cpp @@ -488,7 +488,7 @@ bool FRenderer::beginFrame(FSwapChain* swapChain) { // latch the frame time std::chrono::duration time{ getUserTime() }; - float h = (float)time.count(); + float h = float(time.count()); float l = float(time.count() - h); mShaderUserTime = { h, l, 0, 0 }; @@ -523,6 +523,9 @@ void FRenderer::endFrame() { driver.endFrame(mFrameId); + // do this before engine.flush() + engine.getResourceAllocator().gc(); + // Run the component managers' GC in parallel // WARNING: while doing this we can't access any component manager auto& js = engine.getJobSystem(); @@ -534,7 +537,6 @@ void FRenderer::endFrame() { // make sure we're done with the gcs js.waitAndRelease(job); - #if EXTRA_TIMING_INFO if (UTILS_UNLIKELY(frameInfoManager.isLapRecordsEnabled())) { auto history = frameInfoManager.getHistory(); diff --git a/filament/src/fg/FrameGraph.cpp b/filament/src/fg/FrameGraph.cpp index 8a7e7f9a3d..89c1947f7b 100644 --- a/filament/src/fg/FrameGraph.cpp +++ b/filament/src/fg/FrameGraph.cpp @@ -417,8 +417,8 @@ void Resource::create(FrameGraph& fg) noexcept { effectiveUsage |= TextureUsage::SAMPLEABLE; samples = 1; // sampleable textures can't be multi-sampled } - texture = fg.getResourceAllocator().createTexture(desc.type, desc.levels, desc.format, - samples, desc.width, desc.height, desc.depth, effectiveUsage); + texture = fg.getResourceAllocator().createTexture(name, desc.type, desc.levels, + desc.format, samples, desc.width, desc.height, desc.depth, effectiveUsage); } } diff --git a/filament/src/fg/ResourceAllocator.cpp b/filament/src/fg/ResourceAllocator.cpp index aa98abf28b..e1ca43580c 100644 --- a/filament/src/fg/ResourceAllocator.cpp +++ b/filament/src/fg/ResourceAllocator.cpp @@ -18,16 +18,52 @@ #include "private/backend/DriverApi.h" +#include "details/Texture.h" + +#include + +using namespace utils; + namespace filament { using namespace backend; +using namespace details; namespace fg { +size_t ResourceAllocator::TextureKey::getSize() const noexcept { + size_t pixelCount = width * height * depth; + size_t size = pixelCount * FTexture::getFormatSize(format); + if (levels > 1) { + // if we have mip-maps we assume the full pyramid + size += size / 3; + } + size_t s = std::max(uint8_t(1), samples); + if (s > 1) { + // if we have MSAA, we assume 8 bit extra per pixel + size += pixelCount; + } + return size; +} + ResourceAllocator::ResourceAllocator(DriverApi& driverApi) noexcept : mBackend(driverApi) { } +ResourceAllocator::~ResourceAllocator() noexcept { + assert(!mTextureCache.size()); + assert(!mInUseTextures.size()); +} + +void ResourceAllocator::terminate() noexcept { + assert(!mInUseTextures.size()); + auto& textureCache = mTextureCache; + for (auto it = textureCache.begin(); it != textureCache.end();) { + mBackend.destroyTexture(it->second.handle); + it = textureCache.erase(it); + } +} + RenderTargetHandle ResourceAllocator::createRenderTarget( TargetBufferFlags targetBufferFlags, uint32_t width, uint32_t height, uint8_t samples, TargetBufferInfo color, TargetBufferInfo depth, @@ -40,14 +76,92 @@ void ResourceAllocator::destroyRenderTarget(RenderTargetHandle h) noexcept { return mBackend.destroyRenderTarget(h); } -TextureHandle ResourceAllocator::createTexture(SamplerType target, uint8_t levels, - TextureFormat format, uint8_t samples, uint32_t width, uint32_t height, - uint32_t depth, TextureUsage usage) noexcept { - return mBackend.createTexture(target, levels, format, samples, width, height, depth, usage); +backend::TextureHandle ResourceAllocator::createTexture(const char* name, + backend::SamplerType target, uint8_t levels, + backend::TextureFormat format, uint8_t samples, uint32_t width, uint32_t height, + uint32_t depth, backend::TextureUsage usage) noexcept { + // do we have a suitable texture in the cache? + TextureHandle handle; + if (mEnabled) { + auto& textureCache = mTextureCache; + const TextureKey key{ name, target, levels, format, samples, width, height, depth, usage }; + auto it = textureCache.find(key); + if (UTILS_LIKELY(it != textureCache.end())) { + // we do, move the entry to the in-use list, and remove from the cache + handle = it->second.handle; + mCacheSize -= it->second.size; + textureCache.erase(it); + } else { + // we don't, allocate a new texture and populate the in-use list + handle = mBackend.createTexture( + target, levels, format, samples, width, height, depth, usage); + } + mInUseTextures.emplace(handle, key); + } else { + handle = mBackend.createTexture( + target, levels, format, samples, width, height, depth, usage); + } + return handle; } void ResourceAllocator::destroyTexture(TextureHandle h) noexcept { - return mBackend.destroyTexture(h); + if (mEnabled) { + // find the texture in the in-use list (it must be there!) + auto it = mInUseTextures.find(h); + assert(it != mInUseTextures.end()); + + // move it to the cache + const TextureKey key = it->second; + uint32_t size = key.getSize(); + + mTextureCache.emplace(key, TextureCachePayload{ h, mAge, size }); + mCacheSize += size; + + // remove it from the in-use list + mInUseTextures.erase(it); + } else { + mBackend.destroyTexture(h); + } +} + +void ResourceAllocator::gc() noexcept { + // this is called regularly -- usually once per frame of each Renderer + + // increase our age + mAge++; + + // Remove entries that are older than a certain age + auto& textureCache = mTextureCache; + for (auto it = textureCache.begin(); it != textureCache.end();) { + if (mAge - it->second.age > CACHE_MAX_AGE) { + mBackend.destroyTexture(it->second.handle); + mCacheSize -= it->second.size; + //slog.d << "purging " << it->second.handle.getId() << io::endl; + it = textureCache.erase(it); + if (mCacheSize < CACHE_CAPACITY) { + // if we're not at capacity, only purge a single entry per gc, trying to + // avoid a burst of work. + break; + } + } else { + ++it; + } + } + + // TODO: maybe purge LRU entries if we have more than a certain number + // TODO: maybe purge LRU entries if the size of the cache is too large +} + +void ResourceAllocator::dump() const noexcept { + slog.d << "# entries=" << mTextureCache.size() << ", sz=" << mCacheSize / float(1u << 20u) + << " MiB" << io::endl; + for (auto const & it : mTextureCache) { + auto w = it.first.width; + auto h = it.first.height; + auto f = FTexture::getFormatSize(it.first.format); + slog.d << it.first.name << ": w=" << w << ", h=" << h << ", f=" << f << ", sz=" + << it.second.size / float(1u << 20u) << io::endl; + } } } // namespace fg diff --git a/filament/src/fg/ResourceAllocator.h b/filament/src/fg/ResourceAllocator.h index ee6043f832..f6920558af 100644 --- a/filament/src/fg/ResourceAllocator.h +++ b/filament/src/fg/ResourceAllocator.h @@ -23,7 +23,12 @@ #include "private/backend/DriverApiForward.h" +#include + +#include + #include +#include namespace filament { namespace fg { @@ -31,6 +36,9 @@ namespace fg { class ResourceAllocator { public: explicit ResourceAllocator(backend::DriverApi& driverApi) noexcept; + ~ResourceAllocator() noexcept; + + void terminate() noexcept; backend::RenderTargetHandle createRenderTarget( backend::TargetBufferFlags targetBufferFlags, @@ -43,23 +51,91 @@ public: void destroyRenderTarget(backend::RenderTargetHandle h) noexcept; - backend::TextureHandle createTexture( - backend::SamplerType target, + backend::TextureHandle createTexture(const char* name, backend::SamplerType target, uint8_t levels, - backend::TextureFormat format, - uint8_t samples, - uint32_t width, - uint32_t height, - uint32_t depth, - backend::TextureUsage usage) noexcept; + backend::TextureFormat format, uint8_t samples, uint32_t width, uint32_t height, + uint32_t depth, backend::TextureUsage usage) noexcept; void destroyTexture(backend::TextureHandle h) noexcept; + void gc() noexcept; + private: + // TODO: these should be settings of the engine + static constexpr size_t CACHE_CAPACITY = 64u << 20u; // 64 MiB + static constexpr size_t CACHE_MAX_AGE = 30u; // 64 MiB + + struct TextureKey { + const char* name; // doesn't participate in the hash + backend::SamplerType target; + uint8_t levels; + backend::TextureFormat format; + uint8_t samples; + uint32_t width; + uint32_t height; + uint32_t depth; + backend::TextureUsage usage; + + size_t getSize() const noexcept; + + bool operator==(const TextureKey& other) const noexcept { + return target == other.target && + levels == other.levels && + format == other.format && + samples == other.samples && + width == other.width && + height == other.height && + depth == other.depth && + usage == other.usage; + } + + friend size_t hash_value(TextureKey const& k) { + size_t seed = 0; + utils::hash::hash_combine(seed, k.target); + utils::hash::hash_combine(seed, k.levels); + utils::hash::hash_combine(seed, k.format); + utils::hash::hash_combine(seed, k.samples); + utils::hash::hash_combine(seed, k.width); + utils::hash::hash_combine(seed, k.height); + utils::hash::hash_combine(seed, k.depth); + utils::hash::hash_combine(seed, k.usage); + return seed; + } + }; + + struct TextureCachePayload { + backend::TextureHandle handle; + size_t age = 0; + uint32_t size = 0; + }; + + template + struct Hasher { + std::size_t operator()(T const& s) const noexcept { + return hash_value(s); + } + }; + + template + struct Hasher> { + std::size_t operator()(backend::Handle const& s) const noexcept { + std::hash::HandleId> hash{}; + return hash(s.getId()); + } + }; + + void dump() const noexcept; + backend::DriverApi& mBackend; + std::unordered_multimap> mTextureCache; + std::unordered_multimap> mInUseTextures; + size_t mAge = 0; + uint32_t mCacheSize = 0; + const bool mEnabled = true; }; }// namespace fg } // namespace filament + #endif //TNT_FILAMENT_FG_RESOURCEALLOCATOR_H diff --git a/libs/utils/include/utils/Hash.h b/libs/utils/include/utils/Hash.h index 3e2b7b16fa..1487afa090 100644 --- a/libs/utils/include/utils/Hash.h +++ b/libs/utils/include/utils/Hash.h @@ -52,6 +52,12 @@ struct MurmurHashFn { } }; +template +inline void hash_combine(size_t& seed, const T& v) { + std::hash hasher; + seed ^= hasher(v) + 0x9e3779b9 + (seed << 6u) + (seed >> 2u); +} + } // namespace hash } // namespace utils