Implement a basic Texture cache for the framegraph
We're only caching textures (not render targets yet), and we're evicting cache entries older than 30 allocations. This should cut down on texture allocation / gl calls. The cache should get in a stable state very quickly (less than half second).
This commit is contained in:
committed by
Mathias Agopian
parent
0bd9515b3d
commit
c65239aec2
@@ -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);
|
||||
|
||||
@@ -488,7 +488,7 @@ bool FRenderer::beginFrame(FSwapChain* swapChain) {
|
||||
|
||||
// latch the frame time
|
||||
std::chrono::duration<double> 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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,16 +18,52 @@
|
||||
|
||||
#include "private/backend/DriverApi.h"
|
||||
|
||||
#include "details/Texture.h"
|
||||
|
||||
#include <utils/Log.h>
|
||||
|
||||
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
|
||||
|
||||
@@ -23,7 +23,12 @@
|
||||
|
||||
#include "private/backend/DriverApiForward.h"
|
||||
|
||||
#include <utils/Hash.h>
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
#include <stdint.h>
|
||||
#include <tsl/robin_map.h>
|
||||
|
||||
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<typename T>
|
||||
struct Hasher {
|
||||
std::size_t operator()(T const& s) const noexcept {
|
||||
return hash_value(s);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
struct Hasher<backend::Handle<T>> {
|
||||
std::size_t operator()(backend::Handle<T> const& s) const noexcept {
|
||||
std::hash<typename backend::Handle<T>::HandleId> hash{};
|
||||
return hash(s.getId());
|
||||
}
|
||||
};
|
||||
|
||||
void dump() const noexcept;
|
||||
|
||||
backend::DriverApi& mBackend;
|
||||
std::unordered_multimap<TextureKey, TextureCachePayload, Hasher<TextureKey>> mTextureCache;
|
||||
std::unordered_multimap<backend::TextureHandle, TextureKey, Hasher<backend::TextureHandle>> mInUseTextures;
|
||||
size_t mAge = 0;
|
||||
uint32_t mCacheSize = 0;
|
||||
const bool mEnabled = true;
|
||||
};
|
||||
|
||||
}// namespace fg
|
||||
} // namespace filament
|
||||
|
||||
|
||||
#endif //TNT_FILAMENT_FG_RESOURCEALLOCATOR_H
|
||||
|
||||
@@ -52,6 +52,12 @@ struct MurmurHashFn {
|
||||
}
|
||||
};
|
||||
|
||||
template<class T>
|
||||
inline void hash_combine(size_t& seed, const T& v) {
|
||||
std::hash<T> hasher;
|
||||
seed ^= hasher(v) + 0x9e3779b9 + (seed << 6u) + (seed >> 2u);
|
||||
}
|
||||
|
||||
} // namespace hash
|
||||
} // namespace utils
|
||||
|
||||
|
||||
Reference in New Issue
Block a user