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:
Mathias Agopian
2019-08-08 15:42:08 -07:00
committed by Mathias Agopian
parent 0bd9515b3d
commit c65239aec2
6 changed files with 218 additions and 17 deletions

View File

@@ -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);

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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