Correct and revise OpenGL texture pooling (#2709)

This commit is contained in:
alasram
2024-08-29 14:13:26 -07:00
committed by GitHub
parent 290bf927f5
commit b850c257a3
79 changed files with 817 additions and 247 deletions

View File

@@ -404,6 +404,7 @@ list(APPEND INCLUDE_FILES
${PROJECT_SOURCE_DIR}/include/mbgl/util/instrumentation.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/util/interpolate.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/util/logging.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/util/lru_cache.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/util/noncopyable.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/util/platform.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/util/premultiply.hpp
@@ -1094,6 +1095,8 @@ if(MLN_WITH_OPENGL)
${PROJECT_SOURCE_DIR}/src/mbgl/gl/renderbuffer_resource.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/gl/renderbuffer_resource.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/gl/renderer_backend.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/gl/resource_pool.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/gl/resource_pool.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/gl/state.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/gl/texture.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/gl/texture.hpp

View File

@@ -830,6 +830,7 @@ MLN_CORE_HEADERS = [
"include/mbgl/util/instrumentation.hpp",
"include/mbgl/util/interpolate.hpp",
"include/mbgl/util/logging.hpp",
"include/mbgl/util/lru_cache.hpp",
"include/mbgl/util/monotonic_timer.hpp",
"include/mbgl/util/noncopyable.hpp",
"include/mbgl/util/platform.hpp",
@@ -886,6 +887,8 @@ MLN_OPENGL_SOURCE = [
"src/mbgl/gl/renderbuffer_resource.cpp",
"src/mbgl/gl/renderbuffer_resource.hpp",
"src/mbgl/gl/renderer_backend.cpp",
"src/mbgl/gl/resource_pool.cpp",
"src/mbgl/gl/resource_pool.hpp",
"src/mbgl/gl/state.hpp",
"src/mbgl/gl/texture.cpp",
"src/mbgl/gl/texture.hpp",

View File

@@ -59,8 +59,8 @@ public:
void unbind() noexcept;
private:
void createObject() noexcept;
void createStorage(const void* data = nullptr) noexcept;
void allocateTexture() noexcept;
void updateTextureData(const void* data = nullptr) noexcept;
private:
gl::Context& context;

View File

@@ -93,7 +93,7 @@ constexpr const char* tracyConstMemoryLabel = "Constant Buffer Memory";
#define MLN_TRACE_GL_CONTEXT() ((void)0)
#define MLN_TRACE_GL_ZONE(label) ((void)0)
#define MLN_TRACE_FUNC_GL() ((void)0)
#define MLN_END_FRAME() FrameMark;
#define MLN_END_FRAME() FrameMark
#endif // MLN_RENDER_BACKEND_OPENGL

View File

@@ -0,0 +1,60 @@
#pragma once
#include <list>
#include <stdexcept>
#include <unordered_map>
namespace mbgl {
// Simple non-thread-safe LRU cache
template <typename Item, typename Hash = std::hash<Item>>
class LRU {
public:
// Number of cached items
size_t size() const { return list.size(); }
// Check if cache is empty
bool empty() const { return list.empty(); }
// Access x
// If x is already cached, move it to the front (most recently used)
void touch(Item x) {
auto it = map.find(x);
if (it != map.end()) {
list.splice(list.begin(), list, it->second);
} else {
list.push_front(x);
map[std::move(x)] = list.begin();
}
}
// Evict the least recently used item
// If the cache is empty, throw an exception
Item evict() {
if (list.empty()) {
throw std::runtime_error("LRU::evict: empty cache");
}
Item x = std::move(list.back());
map.erase(x);
list.pop_back();
return x;
}
// Check if item exists in cache without changing its order
bool isHit(const Item& x) const { return map.find(std::move(x)) != map.end(); }
// remove item from cache if it exists
void remove(const Item& x) {
auto it = map.find(x);
if (it != map.end()) {
list.erase(it->second);
map.erase(it);
}
}
private:
std::list<Item> list;
std::unordered_map<Item, typename std::list<Item>::iterator, Hash> map;
};
} // namespace mbgl

View File

@@ -19,8 +19,8 @@
15,
1,
[
147712,
147712
164096,
164096
],
[
130,

View File

@@ -19,8 +19,8 @@
51,
1,
[
53024,
53024
53032,
53032
],
[
43734,

View File

@@ -19,8 +19,8 @@
17,
1,
[
125574,
125574
191110,
191110
],
[
454,

View File

@@ -19,8 +19,8 @@
7,
1,
[
32768,
32768
49152,
49152
],
[
46,

View File

@@ -19,8 +19,8 @@
8,
1,
[
32768,
32768
49152,
49152
],
[
46,

View File

@@ -19,8 +19,8 @@
9,
1,
[
22784,
22784
29184,
29184
],
[
34,

View File

@@ -19,8 +19,8 @@
5,
1,
[
22784,
22784
25088,
25088
],
[
22,

View File

@@ -19,8 +19,8 @@
7,
1,
[
32768,
32768
49152,
49152
],
[
94,

View File

@@ -19,8 +19,8 @@
9,
1,
[
33768,
33768
50702,
50702
],
[
34,

View File

@@ -19,8 +19,8 @@
9,
1,
[
22080,
22080
27776,
27776
],
[
34,

View File

@@ -19,8 +19,8 @@
7,
1,
[
32768,
32768
49152,
49152
],
[
34,

View File

@@ -19,8 +19,8 @@
5,
1,
[
32768,
32768
114688,
114688
],
[
22,

View File

@@ -19,8 +19,8 @@
7,
1,
[
32768,
32768
49152,
49152
],
[
34,

View File

@@ -19,8 +19,8 @@
5,
1,
[
65536,
65536
81920,
81920
],
[
22,

View File

@@ -19,8 +19,8 @@
9,
1,
[
21264,
21264
42528,
42528
],
[
34,

View File

@@ -19,8 +19,8 @@
17,
1,
[
163840,
163840
196608,
196608
],
[
118,

View File

@@ -19,8 +19,8 @@
7,
1,
[
32768,
32768
65536,
65536
],
[
82,

View File

@@ -19,8 +19,8 @@
9,
1,
[
24576,
24576
46656,
46656
],
[
34,

View File

@@ -19,8 +19,8 @@
7,
1,
[
32768,
32768
49152,
49152
],
[
46,

View File

@@ -19,8 +19,8 @@
32,
4,
[
158509,
158509
321286,
321286
],
[
712,

View File

@@ -19,8 +19,8 @@
60,
4,
[
131780,
131780
230615,
230615
],
[
102328,

View File

@@ -19,8 +19,8 @@
9,
1,
[
215688,
215688
281224,
281224
],
[
58,

View File

@@ -19,8 +19,8 @@
36,
4,
[
143344,
143344
250852,
250852
],
[
194056,

View File

@@ -19,8 +19,8 @@
52,
4,
[
296872,
296872
685542,
685542
],
[
79084,

View File

@@ -19,8 +19,8 @@
27,
1,
[
150144,
150144
215680,
215680
],
[
406,

View File

@@ -19,8 +19,8 @@
15,
1,
[
98816,
98816
164352,
164352
],
[
82,

View File

@@ -19,8 +19,8 @@
15,
1,
[
98816,
98816
164352,
164352
],
[
82,

View File

@@ -19,8 +19,8 @@
21,
1,
[
196608,
196608
262144,
262144
],
[
118,

View File

@@ -19,8 +19,8 @@
13,
1,
[
131072,
131072
196608,
196608
],
[
70,

View File

@@ -19,8 +19,8 @@
15,
1,
[
147712,
147712
164096,
164096
],
[
130,

View File

@@ -19,8 +19,8 @@
51,
1,
[
53024,
53024
53032,
53032
],
[
43734,

View File

@@ -19,8 +19,8 @@
17,
1,
[
125574,
125574
191110,
191110
],
[
454,

View File

@@ -19,8 +19,8 @@
7,
1,
[
32768,
32768
49152,
49152
],
[
46,

View File

@@ -19,8 +19,8 @@
8,
1,
[
32768,
32768
49152,
49152
],
[
46,

View File

@@ -19,8 +19,8 @@
9,
1,
[
22784,
22784
29184,
29184
],
[
34,

View File

@@ -19,8 +19,8 @@
5,
1,
[
22784,
22784
25088,
25088
],
[
22,

View File

@@ -19,8 +19,8 @@
7,
1,
[
32768,
32768
49152,
49152
],
[
94,

View File

@@ -19,8 +19,8 @@
9,
1,
[
33768,
33768
50702,
50702
],
[
34,

View File

@@ -19,8 +19,8 @@
9,
1,
[
22080,
22080
27776,
27776
],
[
34,

View File

@@ -19,8 +19,8 @@
7,
1,
[
32768,
32768
49152,
49152
],
[
34,

View File

@@ -19,8 +19,8 @@
5,
1,
[
32768,
32768
114688,
114688
],
[
22,

View File

@@ -19,8 +19,8 @@
7,
1,
[
32768,
32768
49152,
49152
],
[
34,

View File

@@ -19,8 +19,8 @@
5,
1,
[
65536,
65536
81920,
81920
],
[
22,

View File

@@ -19,8 +19,8 @@
9,
1,
[
21264,
21264
42528,
42528
],
[
34,

View File

@@ -19,8 +19,8 @@
17,
1,
[
163840,
163840
196608,
196608
],
[
118,

View File

@@ -19,8 +19,8 @@
7,
1,
[
32768,
32768
65536,
65536
],
[
82,

View File

@@ -19,8 +19,8 @@
9,
1,
[
24576,
24576
46656,
46656
],
[
34,

View File

@@ -19,8 +19,8 @@
7,
1,
[
32768,
32768
49152,
49152
],
[
46,

View File

@@ -19,8 +19,8 @@
7,
1,
[
32768,
32768
49152,
49152
],
[
46,

View File

@@ -19,8 +19,8 @@
32,
4,
[
158509,
158509
321286,
321286
],
[
712,

View File

@@ -19,8 +19,8 @@
60,
4,
[
131780,
131780
230615,
230615
],
[
102328,

View File

@@ -19,8 +19,8 @@
9,
1,
[
215688,
215688
281224,
281224
],
[
58,
@@ -32,4 +32,4 @@
]
]
]
}
}

View File

@@ -19,8 +19,8 @@
36,
4,
[
143344,
143344
250852,
250852
],
[
194056,

View File

@@ -19,8 +19,8 @@
52,
4,
[
296872,
296872
685542,
685542
],
[
79084,

View File

@@ -19,8 +19,8 @@
27,
1,
[
150144,
150144
215680,
215680
],
[
406,

View File

@@ -19,8 +19,8 @@
15,
1,
[
98816,
98816
164352,
164352
],
[
82,

View File

@@ -19,8 +19,8 @@
15,
1,
[
98816,
98816
164352,
164352
],
[
82,

View File

@@ -19,8 +19,8 @@
21,
1,
[
196608,
196608
262144,
262144
],
[
118,

View File

@@ -19,8 +19,8 @@
13,
1,
[
131072,
131072
196608,
196608
],
[
70,

View File

@@ -88,7 +88,7 @@ void Mailbox::push(std::unique_ptr<Message> message) {
}};
{
MLN_TRACE_ZONE(push_lock);
MLN_TRACE_ZONE(push lock);
std::lock_guard<std::mutex> pushingLock(pushingMutex);
if (closed) {
@@ -98,7 +98,7 @@ void Mailbox::push(std::unique_ptr<Message> message) {
bool wasEmpty = false;
{
MLN_TRACE_ZONE(queue_lock);
MLN_TRACE_ZONE(queue lock);
std::lock_guard<std::mutex> queueLock(queueMutex);
wasEmpty = queue.empty();
queue.push(std::move(message));
@@ -168,4 +168,4 @@ void Mailbox::scheduleToRecieve(const std::optional<util::SimpleIdentity>& tag)
}
}
} // namespace mbgl
} // namespace mbgl

View File

@@ -98,6 +98,8 @@ Context::Context(RendererBackend& backend_)
#if MLN_DRAWABLE_RENDERER
uboAllocator = std::make_unique<gl::UniformBufferAllocator>();
#endif
texturePool = std::make_unique<Texture2DPool>(this);
}
Context::~Context() noexcept {
@@ -105,6 +107,10 @@ Context::~Context() noexcept {
backend.getThreadPool().runRenderJobs(true /* closeQueue */);
reset();
// Delete all pooled resources while the context is still valid
texturePool.reset();
#if !defined(NDEBUG)
Log::Debug(Event::General, "Rendering Stats:\n" + stats.toString("\n"));
#endif
@@ -265,18 +271,14 @@ void Context::verifyProgramLinkage(ProgramID program_) {
throw std::runtime_error("program failed to link");
}
UniqueTexture Context::createUniqueTexture() {
UniqueTexture Context::createUniqueTexture(const Size& size,
gfx::TexturePixelType format,
gfx::TextureChannelDataType type) {
MLN_TRACE_FUNC();
if (pooledTextures.empty()) {
pooledTextures.resize(TextureMax);
MBGL_CHECK_ERROR(glGenTextures(TextureMax, pooledTextures.data()));
stats.numCreatedTextures += TextureMax;
}
assert(texturePool);
TextureID id = texturePool->alloc(size, format, type);
TextureID id = pooledTextures.back();
pooledTextures.pop_back();
stats.numActiveTextures++;
// NOLINTNEXTLINE(performance-move-const-arg)
return UniqueTexture{std::move(id), {this}};
}
@@ -306,27 +308,13 @@ std::unique_ptr<gfx::TextureResource> Context::createTextureResource(const Size
const gfx::TextureChannelDataType type) {
MLN_TRACE_FUNC();
auto obj = createUniqueTexture();
int textureByteSize = gl::TextureResource::getStorageSize(size, format, type);
stats.memTextures += textureByteSize;
std::unique_ptr<gfx::TextureResource> resource = std::make_unique<gl::TextureResource>(std::move(obj),
textureByteSize);
auto obj = createUniqueTexture(size, format, type);
std::unique_ptr<gfx::TextureResource> resource = std::make_unique<gl::TextureResource>(std::move(obj));
// Always use texture unit 0 for manipulating it.
activeTextureUnit = 0;
texture[0] = static_cast<gl::TextureResource&>(*resource).texture;
// Creates an empty texture with the specified size and format.
MBGL_CHECK_ERROR(glTexImage2D(GL_TEXTURE_2D,
0,
Enum<gfx::TexturePixelType>::sizedFor(format, type),
size.width,
size.height,
0,
Enum<gfx::TexturePixelType>::to(format),
Enum<gfx::TextureChannelDataType>::to(type),
nullptr));
// We are using clamp to edge here since OpenGL ES doesn't allow GL_REPEAT
// on NPOT textures. We use those when the pixelRatio isn't a power of two,
// e.g. on iPhone 6 Plus.
@@ -530,9 +518,10 @@ std::unique_ptr<gfx::DrawScopeResource> Context::createDrawScopeResource() {
void Context::reset() {
MLN_TRACE_FUNC();
std::copy(pooledTextures.begin(), pooledTextures.end(), std::back_inserter(abandonedTextures));
pooledTextures.resize(0);
performCleanup();
// Delete all unused pooled resources
texturePool->shrink();
}
#if MLN_DRAWABLE_RENDERER
@@ -883,10 +872,8 @@ void Context::performCleanup() {
binding.setDirty();
}
}
texturePool->release(id);
}
MBGL_CHECK_ERROR(glDeleteTextures(int(abandonedTextures.size()), abandonedTextures.data()));
stats.numCreatedTextures -= int(abandonedTextures.size());
assert(stats.numCreatedTextures >= 0);
abandonedTextures.clear();
}
@@ -923,6 +910,8 @@ void Context::reduceMemoryUsage() {
MLN_TRACE_FUNC_GL();
performCleanup();
assert(texturePool);
texturePool->shrink();
// Ensure that all pending actions are executed to ensure that they happen
// before the app goes to the background.
@@ -949,5 +938,10 @@ void Context::clearStencilBuffer(const int32_t bits) {
stats.stencilClears++;
}
Texture2DPool& Context::getTexturePool() {
assert(texturePool);
return *texturePool;
}
} // namespace gl
} // namespace mbgl

View File

@@ -10,6 +10,7 @@
#include <mbgl/gl/state.hpp>
#include <mbgl/gl/value.hpp>
#include <mbgl/gl/framebuffer.hpp>
#include <mbgl/gl/resource_pool.hpp>
#include <mbgl/gl/vertex_array.hpp>
#include <mbgl/gl/types.hpp>
#include <mbgl/platform/gl_functions.hpp>
@@ -29,7 +30,6 @@
namespace mbgl {
namespace gl {
constexpr size_t TextureMax = 64;
using ProcAddress = void (*)();
class RendererBackend;
@@ -58,7 +58,7 @@ public:
UniqueProgram createProgram(ShaderID vertexShader, ShaderID fragmentShader, const char* location0AttribName);
void verifyProgramLinkage(ProgramID);
void linkProgram(ProgramID);
UniqueTexture createUniqueTexture();
UniqueTexture createUniqueTexture(const Size& size, gfx::TexturePixelType format, gfx::TextureChannelDataType type);
Framebuffer createFramebuffer(const gfx::Renderbuffer<gfx::RenderbufferPixelType::RGBA>&,
const gfx::Renderbuffer<gfx::RenderbufferPixelType::DepthStencil>&);
@@ -93,18 +93,23 @@ public:
// Actually remove the objects we marked as abandoned with the above methods.
// Only call this while the OpenGL context is exclusive to this thread.
// Pooled textures are retained
void performCleanup() override;
// Called when the app receives a memory warning and before it goes to the background.
// Calls performCleanup and destroy all pooled resources
void reduceMemoryUsage() override;
// Drain pools and remove abandoned objects, in preparation for destroying the store.
// Only call this while the OpenGL context is exclusive to this thread.
void reset();
bool empty() const {
return pooledTextures.empty() && abandonedPrograms.empty() && abandonedShaders.empty() &&
abandonedBuffers.empty() && abandonedTextures.empty() && abandonedVertexArrays.empty() &&
abandonedFramebuffers.empty();
// Returns whether there are any objects that need to be cleaned up.
// If considerPool is true, it will also check if resources are still pooled.
bool empty(bool considerPool = false) const {
return abandonedPrograms.empty() && abandonedShaders.empty() && abandonedBuffers.empty() &&
abandonedTextures.empty() && abandonedVertexArrays.empty() && abandonedFramebuffers.empty() &&
(considerPool ? texturePool->empty() : true);
}
extension::Debugging* getDebuggingExtension() const { return debugging.get(); }
@@ -151,6 +156,8 @@ public:
void setDirtyState() override;
Texture2DPool& getTexturePool();
private:
RendererBackend& backend;
bool cleanupOnDestruction = true;
@@ -230,8 +237,6 @@ private:
friend detail::FramebufferDeleter;
friend detail::RenderbufferDeleter;
std::vector<TextureID> pooledTextures;
std::vector<ProgramID> abandonedPrograms;
std::vector<ShaderID> abandonedShaders;
std::vector<BufferID> abandonedBuffers;
@@ -240,6 +245,8 @@ private:
std::vector<FramebufferID> abandonedFramebuffers;
std::vector<RenderbufferID> abandonedRenderbuffers;
std::unique_ptr<Texture2DPool> texturePool;
public:
#if !defined(NDEBUG)
public:

View File

@@ -23,13 +23,10 @@ void BufferDeleter::operator()(BufferID id) const {
void TextureDeleter::operator()(TextureID id) const {
assert(context);
if (context->pooledTextures.size() >= TextureMax) {
context->abandonedTextures.push_back(id);
} else {
context->pooledTextures.push_back(id);
}
context->abandonedTextures.push_back(id);
context->renderingStats().numActiveTextures--;
assert(context->renderingStats().numActiveTextures >= 0);
assert(context->renderingStats().numActiveTextures < context->renderingStats().numCreatedTextures);
}
void VertexArrayDeleter::operator()(VertexArrayID id) const {

View File

@@ -0,0 +1,251 @@
#include <mbgl/gl/resource_pool.hpp>
#include <cassert>
#include <type_traits>
#include <mbgl/gl/context.hpp>
#include <mbgl/gl/defines.hpp>
#include <mbgl/gl/enum.hpp>
#include <mbgl/gl/texture_resource.hpp>
#include <mbgl/util/hash.hpp>
#include <mbgl/util/instrumentation.hpp>
#include <mbgl/util/logging.hpp>
namespace mbgl {
namespace gl {
using namespace platform;
namespace {
size_t storageSize(const Texture2DDesc& desc) {
return static_cast<size_t>(TextureResource::getStorageSize(desc.size, desc.pixelFormat, desc.channelType));
}
#if !defined(NDEBUG)
void logDebugMessage(const std::string& message) {
Log::Debug(Event::General, "Texture2DPool: " + message);
}
#else
void logDebugMessage(const std::string&) {}
#endif
} // namespace
std::size_t Texture2DDescHash::operator()(const Texture2DDesc& desc) const {
std::size_t seed = 0;
util::hash_combine(seed, desc.size.width);
util::hash_combine(seed, desc.size.height);
util::hash_combine(seed, std::underlying_type_t<gfx::TexturePixelType>(desc.pixelFormat));
util::hash_combine(seed, std::underlying_type_t<gfx::TextureChannelDataType>(desc.channelType));
return seed;
}
Texture2DPool::Texture2DPool(Context* context_, size_t maxPoolStorage_)
: context(context_),
maxPoolStorage(maxPoolStorage_) {
assert(context);
}
Texture2DPool::~Texture2DPool() {
assert(usedStorage() == 0);
assert(unusedStorage() == poolStorage);
shrink();
assert(usedStorage() == 0);
assert(unusedStorage() == 0);
assert(poolStorage == 0);
assert(lru_cache.empty());
assert(descriptions.empty());
assert(pool.empty());
logDebugMessage("total allocations " + util::toString(allocCount));
logDebugMessage("total reused allocations " + util::toString(reuseCount));
logDebugMessage("total textures released " + util::toString(releaseCount));
logDebugMessage("total textures released and deallocated " + util::toString(freedCount));
}
TextureID Texture2DPool::alloc(const Texture2DDesc& desc) {
MLN_TRACE_FUNC();
context->renderingStats().numActiveTextures++;
// Check if the texture is already allocated
auto set_it = pool.find(desc);
if (set_it != pool.end() && !set_it->second.free.empty()) {
// Reuse an existing texture
TextureID id = *set_it->second.free.begin();
set_it->second.free.erase(id);
set_it->second.used.insert(id);
assert(lru_cache.isHit(id));
lru_cache.remove(id);
++reuseCount;
return id;
}
// Allocate a new texture
++allocCount;
return allocateGLMemory(desc);
}
void Texture2DPool::release(TextureID id) {
MLN_TRACE_FUNC();
assert(isUsed(id));
auto desc = descriptions.at(id);
auto set_it = pool.find(desc);
set_it->second.used.erase(id);
set_it->second.free.insert(id);
lru_cache.touch(id);
// Evict old textures if necessary
evict();
++releaseCount;
}
TextureID Texture2DPool::allocateGLMemory(const Texture2DDesc& desc) {
MLN_TRACE_FUNC();
// Create handle
TextureID id = 0;
MBGL_CHECK_ERROR(glGenTextures(1, &id));
assert(id != 0);
// Allocate memory
// Bind to TU 0 and upload
context->activeTextureUnit = 0;
context->texture[0] = id;
MBGL_CHECK_ERROR(glTexImage2D(GL_TEXTURE_2D,
0,
Enum<gfx::TexturePixelType>::sizedFor(desc.pixelFormat, desc.channelType),
desc.size.width,
desc.size.height,
0,
Enum<gfx::TexturePixelType>::to(desc.pixelFormat),
Enum<gfx::TextureChannelDataType>::to(desc.channelType),
nullptr));
// Update stats
context->renderingStats().numCreatedTextures++;
// Add to descriptions
descriptions.emplace(id, desc);
// Add to pool
pool[desc].used.insert(id);
// Update poolStorage
auto storage = storageSize(desc);
poolStorage += storage;
context->renderingStats().memTextures += storage;
MLN_TRACE_ALLOC_TEXTURE(id, storage);
// Evict old textures if necessary
evict();
return id;
}
void Texture2DPool::freeAllocatedGLMemory(TextureID id) {
MLN_TRACE_FUNC();
// Remove from descriptions
auto desc_it = descriptions.find(id);
assert(desc_it != descriptions.end());
auto desc = desc_it->second;
descriptions.erase(desc_it);
// Remove from descriptions lru_cache if it exists
lru_cache.remove(id);
// Remove from pool
auto set_it = pool.find(desc);
assert(set_it != pool.end());
auto free_it = set_it->second.free.find(id);
assert(free_it != set_it->second.free.end());
set_it->second.free.erase(free_it);
if (set_it->second.free.empty() && set_it->second.used.empty()) {
pool.erase(set_it);
}
// Update poolStorage
auto storage = storageSize(desc);
poolStorage -= storage;
context->renderingStats().memTextures -= storage;
assert(context->renderingStats().memTextures >= 0);
MLN_TRACE_FREE_TEXTURE(id);
// Delete the texture
MBGL_CHECK_ERROR(glDeleteTextures(1, &id));
// Update stats
context->renderingStats().numCreatedTextures--;
++freedCount;
}
void Texture2DPool::evict() {
while (poolStorage > maxPoolStorage && !lru_cache.empty()) {
TextureID id = lru_cache.evict();
freeAllocatedGLMemory(id);
}
}
void Texture2DPool::shrink() {
MLN_TRACE_FUNC();
while (!lru_cache.empty()) {
TextureID id = lru_cache.evict();
freeAllocatedGLMemory(id);
}
}
bool Texture2DPool::isPooled(TextureID id) const {
return descriptions.find(id) != descriptions.end();
}
bool Texture2DPool::isUsed(TextureID id) const {
auto desc_it = descriptions.find(id);
if (desc_it == descriptions.end()) {
return false;
}
const auto& desc = desc_it->second;
const auto& used_set = pool.at(desc).used;
return used_set.find(id) != used_set.end();
}
bool Texture2DPool::isUnused(TextureID id) const {
auto desc_it = descriptions.find(id);
if (desc_it == descriptions.end()) {
return false;
}
const auto& desc = desc_it->second;
const auto& free_set = pool.at(desc).free;
return free_set.find(id) != free_set.end();
}
size_t Texture2DPool::usedStorage() const {
size_t total = 0;
for (const auto& set : pool) {
total += storageSize(set.first) * set.second.used.size();
}
return total;
}
size_t Texture2DPool::unusedStorage() const {
size_t total = 0;
for (const auto& set : pool) {
total += storageSize(set.first) * set.second.free.size();
}
return total;
}
const Texture2DDesc& Texture2DPool::desc(TextureID id) const {
assert(descriptions.find(id) != descriptions.end());
return descriptions.at(id);
}
size_t Texture2DPool::storage(TextureID id) const {
assert(descriptions.find(id) != descriptions.end());
return storageSize(descriptions.at(id));
}
} // namespace gl
} // namespace mbgl

View File

@@ -0,0 +1,118 @@
#pragma once
#include <cstddef>
#include <functional>
#include <unordered_map>
#include <unordered_set>
#include <mbgl/gfx/types.hpp>
#include <mbgl/gl/types.hpp>
#include <mbgl/util/lru_cache.hpp>
#include <mbgl/util/size.hpp>
namespace mbgl {
namespace gl {
class Context;
struct Texture2DDesc {
Size size;
gfx::TexturePixelType pixelFormat;
gfx::TextureChannelDataType channelType;
};
struct Texture2DDescHash {
std::size_t operator()(const Texture2DDesc& desc) const;
};
inline bool operator==(const Texture2DDesc& desc1, const Texture2DDesc& desc2) {
return desc1.size == desc2.size && desc1.pixelFormat == desc2.pixelFormat && desc1.channelType == desc2.channelType;
}
inline bool operator!=(const Texture2DDesc& desc1, const Texture2DDesc& desc2) {
return !(desc1 == desc2);
}
class Texture2DPool {
public:
// Create a texture pool with a maximum storage size maxPoolStorage in bytes
// If maxPoolStorage is 0, pooling will disabled
Texture2DPool(Context* context, size_t maxPoolStorage = 16 * 1024 * 1024);
// Deallocate all storage
~Texture2DPool();
// Allocate a texture with the given size, pixel format, and channel type
// If the texture is already allocated in the pool, the existing texture ID will be returned
TextureID alloc(const Size& size, gfx::TexturePixelType pixelFormat, gfx::TextureChannelDataType channelType) {
return alloc({size, pixelFormat, channelType});
}
TextureID alloc(const Texture2DDesc& desc);
// Release the texture but keep it in the pool
void release(TextureID id);
// Free all unused textures
void shrink();
// Get the description of a texture
const Texture2DDesc& desc(TextureID id) const;
// Get the storage size in byte of a texture
size_t storage(TextureID id) const;
// Get the storage size in byte of all textures being used
size_t usedStorage() const;
// Get the storage size in byte of all unused textures
size_t unusedStorage() const;
// Get the storage size in byte of all textures in the pool
size_t storage() const { return poolStorage; }
// Get the maximum storage size before starting to evict unused textures
size_t maxStorage() const { return maxPoolStorage; }
bool empty() const { return poolStorage == 0; }
// Check if a texture is pooled
bool isPooled(TextureID id) const;
// Check if a texture is pooled and being used
bool isUsed(TextureID id) const;
// Check if a texture is pooled but released
bool isUnused(TextureID id) const;
private:
struct TextureSet {
std::unordered_set<TextureID> free;
std::unordered_set<TextureID> used;
};
// Effectively glGenTextures and glTexImage2D
TextureID allocateGLMemory(const Texture2DDesc& desc);
// Effectively call glDeleteTextures
void freeAllocatedGLMemory(TextureID id);
// Evict the least recently used textures until the pool storage is below maxPoolStorage
void evict();
private:
Context* context = nullptr;
size_t maxPoolStorage = 0;
std::size_t poolStorage = 0;
std::unordered_map<Texture2DDesc, TextureSet, Texture2DDescHash> pool;
std::unordered_map<TextureID, Texture2DDesc> descriptions;
LRU<TextureID> lru_cache;
// stats
int64_t allocCount = 0; // total allocations
int64_t reuseCount = 0; // total reused allocations
int64_t releaseCount = 0; // total textures released
int64_t freedCount = 0; // total textures released and deallocated
};
} // namespace gl
} // namespace mbgl

View File

@@ -3,6 +3,7 @@
#include <mbgl/gl/defines.hpp>
#include <mbgl/gl/enum.hpp>
#include <mbgl/platform/gl_functions.hpp>
#include <mbgl/util/instrumentation.hpp>
#include <mbgl/gl/texture_resource.hpp>
#include <mbgl/gfx/texture.hpp>
@@ -70,46 +71,30 @@ size_t Texture2D::numChannels() const noexcept {
}
}
void Texture2D::createObject() noexcept {
void Texture2D::allocateTexture() noexcept {
MLN_TRACE_FUNC();
// Create a new texture object
assert(!textureResource);
auto obj = context.createUniqueTexture();
const auto storageSize = gl::TextureResource::getStorageSize(size, pixelFormat, channelType);
context.renderingStats().memTextures += storageSize;
auto obj = context.createUniqueTexture(size, pixelFormat, channelType);
// @TODO: TextureResource is still needed while we have legacy rendering pathways
textureResource = std::make_unique<gl::TextureResource>(std::move(obj), storageSize);
textureResource = std::make_unique<gl::TextureResource>(std::move(obj));
}
void Texture2D::createStorage(const void* data) noexcept {
assert(textureResource);
void Texture2D::updateTextureData(const void* data) noexcept {
MLN_TRACE_FUNC();
if (data) {
uploadSubRegion(data, size, 0, 0);
}
// Create backing storage for our texture object
using namespace platform;
// Bind to TU 0 and upload
context.activeTextureUnit = 0;
context.texture[0] = getTextureID();
context.pixelStoreUnpack = {1};
MBGL_CHECK_ERROR(glTexImage2D(GL_TEXTURE_2D,
0,
Enum<gfx::TexturePixelType>::sizedFor(pixelFormat, channelType),
size.width,
size.height,
0,
Enum<gfx::TexturePixelType>::to(pixelFormat),
Enum<gfx::TextureChannelDataType>::to(channelType),
data));
storageDirty = false;
updateSamplerConfiguration();
}
void Texture2D::create() noexcept {
if (!textureResource) {
createObject();
}
allocateTexture();
if (storageDirty) {
createStorage();
updateTextureData();
}
}
@@ -184,19 +169,10 @@ void Texture2D::upload(const void* pixelData, const Size& size_) noexcept {
if (!textureResource || storageDirty || size_ == Size{0, 0} || size_ != size) {
size = size_;
// Create the texture object if we don't already have one
if (!textureResource) {
createObject();
}
createStorage(pixelData);
} else {
if (pixelData) {
// Upload to existing memory
uploadSubRegion(pixelData, size, 0, 0);
}
// Create the texture object if we don't already have one or if storage is dirty
allocateTexture();
}
updateTextureData(pixelData);
}
void Texture2D::uploadSubRegion(const void* pixelData, const Size& size_, uint16_t xOffset, uint16_t yOffset) noexcept {

View File

@@ -32,18 +32,10 @@ static int channelStorageSize(gfx::TextureChannelDataType type) {
}
}
TextureResource::TextureResource(UniqueTexture&& texture_, int byteSize_)
: texture(std::move(texture_)),
byteSize(byteSize_) {
MLN_TRACE_ALLOC_TEXTURE(texture.get(), byteSize);
}
TextureResource::TextureResource(UniqueTexture&& texture_)
: texture(std::move(texture_)) {}
TextureResource::~TextureResource() noexcept {
auto& stats = texture.get_deleter().context->renderingStats();
MLN_TRACE_FREE_TEXTURE(texture.get());
stats.memTextures -= byteSize;
assert(stats.memTextures >= 0);
}
TextureResource::~TextureResource() noexcept {}
int TextureResource::getStorageSize(const Size& size, gfx::TexturePixelType format, gfx::TextureChannelDataType type) {
return size.width * size.height * channelCount(format) * channelStorageSize(type);

View File

@@ -8,7 +8,7 @@ namespace gl {
class TextureResource : public gfx::TextureResource {
public:
TextureResource(UniqueTexture&& texture_, int byteSize_);
TextureResource(UniqueTexture&& texture_);
~TextureResource() noexcept override;
static int getStorageSize(const Size& size, gfx::TexturePixelType format, gfx::TextureChannelDataType type);
@@ -18,7 +18,6 @@ public:
gfx::TextureMipMapType mipmap = gfx::TextureMipMapType::No;
gfx::TextureWrapType wrapX = gfx::TextureWrapType::Clamp;
gfx::TextureWrapType wrapY = gfx::TextureWrapType::Clamp;
int byteSize;
};
} // namespace gl

View File

@@ -76,12 +76,12 @@ std::unique_ptr<gfx::TextureResource> UploadPass::createTextureResource(const Si
const void* data,
gfx::TexturePixelType format,
gfx::TextureChannelDataType type) {
auto obj = commandEncoder.context.createUniqueTexture();
const int textureByteSize = gl::TextureResource::getStorageSize(size, format, type);
commandEncoder.context.renderingStats().memTextures += textureByteSize;
auto resource = std::make_unique<gl::TextureResource>(std::move(obj), textureByteSize);
MLN_TRACE_FUNC();
auto obj = commandEncoder.context.createUniqueTexture(size, format, type);
auto resource = std::make_unique<gl::TextureResource>(std::move(obj));
commandEncoder.context.pixelStoreUnpack = {1};
updateTextureResource(*resource, size, data, format, type);
updateTextureResourceSub(*resource, 0, 0, size, data, format, type);
// We are using clamp to edge here since OpenGL ES doesn't allow GL_REPEAT
// on NPOT textures. We use those when the pixelRatio isn't a power of two,
// e.g. on iPhone 6 Plus.
@@ -97,18 +97,9 @@ void UploadPass::updateTextureResource(gfx::TextureResource& resource,
const void* data,
gfx::TexturePixelType format,
gfx::TextureChannelDataType type) {
// Always use texture unit 0 for manipulating it.
commandEncoder.context.activeTextureUnit = 0;
commandEncoder.context.texture[0] = static_cast<gl::TextureResource&>(resource).texture;
MBGL_CHECK_ERROR(glTexImage2D(GL_TEXTURE_2D,
0,
Enum<gfx::TexturePixelType>::to(format),
size.width,
size.height,
0,
Enum<gfx::TexturePixelType>::to(format),
Enum<gfx::TextureChannelDataType>::to(type),
data));
MLN_TRACE_FUNC();
updateTextureResourceSub(resource, 0, 0, size, data, format, type);
}
void UploadPass::updateTextureResourceSub(gfx::TextureResource& resource,
@@ -118,9 +109,20 @@ void UploadPass::updateTextureResourceSub(gfx::TextureResource& resource,
const void* data,
gfx::TexturePixelType format,
gfx::TextureChannelDataType type) {
MLN_TRACE_FUNC();
auto& ctx = commandEncoder.context;
assert(ctx.getTexturePool().isUsed(static_cast<gl::TextureResource&>(resource).texture));
assert(ctx.getTexturePool().desc(static_cast<gl::TextureResource&>(resource).texture).channelType == type);
assert(ctx.getTexturePool().desc(static_cast<gl::TextureResource&>(resource).texture).pixelFormat == format);
assert(ctx.getTexturePool().desc(static_cast<gl::TextureResource&>(resource).texture).size.width >=
xOffset + size.width);
assert(ctx.getTexturePool().desc(static_cast<gl::TextureResource&>(resource).texture).size.height >=
yOffset + size.height);
// Always use texture unit 0 for manipulating it.
commandEncoder.context.activeTextureUnit = 0;
commandEncoder.context.texture[0] = static_cast<const gl::TextureResource&>(resource).texture;
ctx.activeTextureUnit = 0;
ctx.texture[0] = static_cast<const gl::TextureResource&>(resource).texture;
MBGL_CHECK_ERROR(glTexSubImage2D(GL_TEXTURE_2D,
0,
xOffset,

View File

@@ -111,6 +111,7 @@ add_library(
${PROJECT_SOURCE_DIR}/test/util/token.test.cpp
${PROJECT_SOURCE_DIR}/test/util/url.test.cpp
${PROJECT_SOURCE_DIR}/test/util/tile_server_options.test.cpp
${PROJECT_SOURCE_DIR}/test/util/lru_cache.test.cpp
)
# MapSnapshotter uses headless backend that is rendering image on a background thread. QT / macOS adaptation for headless backend creates
@@ -135,6 +136,7 @@ if(MLN_WITH_OPENGL)
${PROJECT_SOURCE_DIR}/test/gl/context.test.cpp
${PROJECT_SOURCE_DIR}/test/gl/gl_functions.test.cpp
${PROJECT_SOURCE_DIR}/test/gl/object.test.cpp
${PROJECT_SOURCE_DIR}/test/gl/resource_pool.test.cpp
${PROJECT_SOURCE_DIR}/test/renderer/backend_scope.test.cpp
${PROJECT_SOURCE_DIR}/test/util/offscreen_texture.test.cpp
)

View File

@@ -55,17 +55,22 @@ TEST(GLObject, Store) {
gl::Context context{backend};
EXPECT_TRUE(context.empty());
gl::UniqueTexture texture = context.createUniqueTexture();
gl::UniqueTexture texture = context.createUniqueTexture(
{8, 8}, gfx::TexturePixelType::RGBA, gfx::TextureChannelDataType::UnsignedByte);
EXPECT_NE(texture.get(), 0u);
texture.reset();
EXPECT_FALSE(context.empty());
EXPECT_FALSE(context.empty(false));
EXPECT_FALSE(context.empty(true));
context.performCleanup();
EXPECT_FALSE(context.empty());
EXPECT_TRUE(context.empty(false));
EXPECT_FALSE(context.empty(true));
context.reset();
EXPECT_TRUE(context.empty());
EXPECT_TRUE(context.empty(false));
EXPECT_TRUE(context.empty(true));
context.reset();
EXPECT_TRUE(context.empty());
EXPECT_TRUE(context.empty(false));
EXPECT_TRUE(context.empty(true));
}
#endif

View File

@@ -0,0 +1,123 @@
#if MLN_RENDER_BACKEND_OPENGL
#include <mbgl/test/util.hpp>
#include <unordered_set>
#include <mbgl/gfx/backend_scope.hpp>
#include <mbgl/gfx/types.hpp>
#include <mbgl/gl/headless_backend.hpp>
#include <mbgl/gl/context.hpp>
using namespace mbgl;
namespace {
constexpr auto oneMB = 1024 * 1024;
constexpr gl::Texture2DDesc makeDescOneMB() {
return {{1024, 1024}, gfx::TexturePixelType::Luminance, gfx::TextureChannelDataType::UnsignedByte};
}
} // namespace
TEST(ResourcePool, TexturePool) {
gl::HeadlessBackend backend{{32, 32}};
gfx::BackendScope scope{backend};
gl::Context context{backend};
EXPECT_TRUE(context.empty(true));
auto& pool = context.getTexturePool();
EXPECT_LE(oneMB, pool.maxStorage());
EXPECT_EQ(pool.usedStorage(), 0);
EXPECT_EQ(pool.unusedStorage(), 0);
EXPECT_EQ(pool.storage(), 0);
EXPECT_FALSE(pool.isUsed(3));
EXPECT_FALSE(pool.isUnused(3));
EXPECT_FALSE(pool.isPooled(3));
auto id = pool.alloc(makeDescOneMB());
EXPECT_EQ(pool.usedStorage(), oneMB);
EXPECT_EQ(pool.unusedStorage(), 0);
EXPECT_EQ(pool.storage(), oneMB);
EXPECT_TRUE(pool.isUsed(id));
EXPECT_FALSE(pool.isUnused(id));
EXPECT_TRUE(pool.isPooled(id));
pool.release(id);
EXPECT_EQ(pool.usedStorage(), 0u);
EXPECT_EQ(pool.unusedStorage(), oneMB);
EXPECT_EQ(pool.storage(), oneMB);
EXPECT_FALSE(pool.isUsed(id));
EXPECT_TRUE(pool.isUnused(id));
EXPECT_TRUE(pool.isPooled(id));
auto reused_id = pool.alloc(makeDescOneMB());
EXPECT_EQ(reused_id, id);
EXPECT_EQ(pool.usedStorage(), oneMB);
EXPECT_EQ(pool.unusedStorage(), 0);
EXPECT_EQ(pool.storage(), oneMB);
EXPECT_TRUE(pool.isUsed(id));
EXPECT_FALSE(pool.isUnused(id));
EXPECT_TRUE(pool.isPooled(id));
std::unordered_set<gl::TextureID> ids = {id};
while (pool.storage() <= pool.maxStorage()) {
auto new_id = pool.alloc(makeDescOneMB());
EXPECT_TRUE(ids.find(new_id) == ids.end());
ids.insert(new_id);
EXPECT_EQ(pool.usedStorage(), oneMB * ids.size());
EXPECT_EQ(pool.unusedStorage(), 0);
EXPECT_EQ(pool.storage(), pool.usedStorage());
EXPECT_TRUE(pool.isUsed(new_id));
EXPECT_FALSE(pool.isUnused(new_id));
EXPECT_TRUE(pool.isPooled(new_id));
}
EXPECT_LE(pool.maxStorage(), pool.usedStorage());
EXPECT_EQ(pool.usedStorage(), oneMB * ids.size());
EXPECT_EQ(pool.unusedStorage(), 0);
EXPECT_EQ(pool.storage(), pool.usedStorage());
EXPECT_TRUE(pool.isUsed(id));
EXPECT_FALSE(pool.isUnused(id));
EXPECT_TRUE(pool.isPooled(id));
pool.release(id);
ids.erase(id);
EXPECT_GE(pool.maxStorage(), pool.usedStorage());
EXPECT_EQ(pool.usedStorage(), oneMB * ids.size());
EXPECT_EQ(pool.unusedStorage(), 0);
EXPECT_EQ(pool.storage(), pool.usedStorage());
EXPECT_FALSE(pool.isUsed(id));
EXPECT_FALSE(pool.isUnused(id));
EXPECT_FALSE(pool.isPooled(id));
size_t used = pool.usedStorage();
size_t unused = 0;
while (!ids.empty()) {
auto i = *ids.begin();
pool.release(i);
ids.erase(i);
used -= oneMB;
unused += oneMB;
EXPECT_EQ(pool.usedStorage(), used);
EXPECT_EQ(pool.unusedStorage(), unused);
EXPECT_EQ(pool.storage(), used + unused);
EXPECT_FALSE(pool.isUsed(i));
EXPECT_TRUE(pool.isUnused(i));
EXPECT_TRUE(pool.isPooled(i));
}
context.reduceMemoryUsage();
context.renderingStats().numActiveTextures = 0;
EXPECT_EQ(pool.usedStorage(), 0);
EXPECT_EQ(pool.unusedStorage(), 0);
EXPECT_EQ(pool.storage(), 0);
EXPECT_FALSE(pool.isUsed(id));
EXPECT_FALSE(pool.isUnused(id));
EXPECT_FALSE(pool.isPooled(id));
EXPECT_TRUE(context.empty(true));
}
#endif

View File

@@ -0,0 +1,38 @@
#include <mbgl/util/lru_cache.hpp>
#include <gtest/gtest.h>
#include <stdexcept>
using namespace mbgl;
TEST(LRU, LRU) {
LRU<int> lru;
EXPECT_TRUE(lru.empty());
lru.touch(1);
lru.touch(2);
lru.touch(3);
EXPECT_FALSE(lru.empty());
EXPECT_EQ(lru.size(), 3);
lru.remove(2);
lru.remove(77);
EXPECT_EQ(lru.evict(), 1);
EXPECT_FALSE(lru.empty());
EXPECT_EQ(lru.size(), 1);
lru.touch(4);
EXPECT_EQ(lru.evict(), 3);
EXPECT_FALSE(lru.empty());
EXPECT_EQ(lru.size(), 1);
EXPECT_FALSE(lru.isHit(3));
EXPECT_FALSE(lru.isHit(33));
EXPECT_TRUE(lru.isHit(4));
EXPECT_EQ(lru.evict(), 4);
EXPECT_TRUE(lru.empty());
EXPECT_FALSE(lru.isHit(4));
EXPECT_THROW(lru.evict(), std::runtime_error);
}