[Fix, Test] (MG_Backend/DirectGLES): backend framebuffer, renderbuffer and sampler twins release their driver ids - a framebuffer per readback leaked the driver into stale pixels

This commit is contained in:
2026-08-11 03:54:05 -04:00
parent fa0f6693d0
commit ef562ee9b5
6 changed files with 529 additions and 25 deletions
+15 -15
View File
@@ -1214,7 +1214,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (g_unitTextureSyncListValid && if (g_unitTextureSyncListValid &&
g_unitTextureSyncListContextId == keys.contextId && g_unitTextureSyncListContextId == keys.contextId &&
g_unitTextureSyncListMaxUnit == maxTouchedUnit && g_unitTextureSyncListMaxUnit == maxTouchedUnit &&
g_unitTextureSyncListContextGeneration == g_textureContextGeneration && g_unitTextureSyncListContextGeneration == g_backendContextGeneration &&
g_unitTextureSyncListEpoch == unitBindingsEpoch && g_unitTextureSyncListEpoch == unitBindingsEpoch &&
g_unitTextureSyncListSamplingGeneration == samplingGeneration && g_unitTextureSyncListSamplingGeneration == samplingGeneration &&
PairingsIntact(g_unitTextureSyncList)) { PairingsIntact(g_unitTextureSyncList)) {
@@ -1246,7 +1246,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
g_unitTextureSyncListContextId = keys.contextId; g_unitTextureSyncListContextId = keys.contextId;
g_unitTextureSyncListMaxUnit = maxTouchedUnit; g_unitTextureSyncListMaxUnit = maxTouchedUnit;
g_unitTextureSyncListContextGeneration = g_textureContextGeneration; g_unitTextureSyncListContextGeneration = g_backendContextGeneration;
g_unitTextureSyncListEpoch = unitBindingsEpoch; g_unitTextureSyncListEpoch = unitBindingsEpoch;
g_unitTextureSyncListSamplingGeneration = samplingGeneration; g_unitTextureSyncListSamplingGeneration = samplingGeneration;
g_unitTextureSyncListValid = true; g_unitTextureSyncListValid = true;
@@ -1274,7 +1274,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_fboTextureSyncListSlotVersion == fboSlotVersion && g_fboTextureSyncListSlotVersion == fboSlotVersion &&
g_fboTextureSyncListObjectVersion == fboObjectVersion && g_fboTextureSyncListObjectVersion == fboObjectVersion &&
g_fboTextureSyncListContextId == keys.contextId && g_fboTextureSyncListContextId == keys.contextId &&
g_fboTextureSyncListContextGeneration == g_textureContextGeneration && g_fboTextureSyncListContextGeneration == g_backendContextGeneration &&
PairingsIntact(g_fboTextureSyncList); PairingsIntact(g_fboTextureSyncList);
if (fboListValid) { if (fboListValid) {
for (const auto& entry : g_fboTextureSyncList) { for (const auto& entry : g_fboTextureSyncList) {
@@ -1302,7 +1302,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_fboTextureSyncListSlotVersion = fboSlotVersion; g_fboTextureSyncListSlotVersion = fboSlotVersion;
g_fboTextureSyncListObjectVersion = fboObjectVersion; g_fboTextureSyncListObjectVersion = fboObjectVersion;
g_fboTextureSyncListContextId = keys.contextId; g_fboTextureSyncListContextId = keys.contextId;
g_fboTextureSyncListContextGeneration = g_textureContextGeneration; g_fboTextureSyncListContextGeneration = g_backendContextGeneration;
} }
} else { } else {
g_fboTextureSyncListFbo = nullptr; g_fboTextureSyncListFbo = nullptr;
@@ -2423,7 +2423,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
static_cast<SizeT>(maxTouchedUnit + 1) * sizeof(SamplerImpl::g_boundSamplersCache[0]); static_cast<SizeT>(maxTouchedUnit + 1) * sizeof(SamplerImpl::g_boundSamplersCache[0]);
if (g_unitSamplerWalkValid && g_unitSamplerWalkContextId == keys.contextId && if (g_unitSamplerWalkValid && g_unitSamplerWalkContextId == keys.contextId &&
g_unitSamplerWalkEpoch == keys.unitBindingsEpoch && g_unitSamplerWalkMaxUnit == maxTouchedUnit && g_unitSamplerWalkEpoch == keys.unitBindingsEpoch && g_unitSamplerWalkMaxUnit == maxTouchedUnit &&
g_unitSamplerWalkContextGeneration == TextureImpl::g_textureContextGeneration && g_unitSamplerWalkContextGeneration == g_backendContextGeneration &&
std::memcmp(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes) == 0) { std::memcmp(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes) == 0) {
return; return;
} }
@@ -2444,7 +2444,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_unitSamplerWalkContextId = keys.contextId; g_unitSamplerWalkContextId = keys.contextId;
g_unitSamplerWalkEpoch = keys.unitBindingsEpoch; g_unitSamplerWalkEpoch = keys.unitBindingsEpoch;
g_unitSamplerWalkMaxUnit = maxTouchedUnit; g_unitSamplerWalkMaxUnit = maxTouchedUnit;
g_unitSamplerWalkContextGeneration = TextureImpl::g_textureContextGeneration; g_unitSamplerWalkContextGeneration = g_backendContextGeneration;
std::memcpy(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes); std::memcpy(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes);
g_unitSamplerWalkValid = true; g_unitSamplerWalkValid = true;
} }
@@ -2554,7 +2554,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
memo.programBackendStateVersion == memo.programBackendStateVersion ==
(currentProgram ? currentProgram->GetBackendStateVersion() : 0) && (currentProgram ? currentProgram->GetBackendStateVersion() : 0) &&
memo.programLinked == (currentProgram && currentProgram->GetLinkStatus()) && memo.programLinked == (currentProgram && currentProgram->GetLinkStatus()) &&
memo.contextGeneration == TextureImpl::g_textureContextGeneration; memo.contextGeneration == g_backendContextGeneration;
// Short-circuited: the shadow compare is only meaningful once the key (and with it the // Short-circuited: the shadow compare is only meaningful once the key (and with it the
// snapshotted row count) matches. // snapshotted row count) matches.
if (!keysMatch || std::memcmp(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(), if (!keysMatch || std::memcmp(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(),
@@ -2569,7 +2569,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
memo.programLifetimeId = currentProgram ? currentProgram->GetLifetimeId() : 0; memo.programLifetimeId = currentProgram ? currentProgram->GetLifetimeId() : 0;
memo.programBackendStateVersion = currentProgram ? currentProgram->GetBackendStateVersion() : 0; memo.programBackendStateVersion = currentProgram ? currentProgram->GetBackendStateVersion() : 0;
memo.programLinked = currentProgram && currentProgram->GetLinkStatus(); memo.programLinked = currentProgram && currentProgram->GetLinkStatus();
memo.contextGeneration = TextureImpl::g_textureContextGeneration; memo.contextGeneration = g_backendContextGeneration;
std::memcpy(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(), shadowBytes); std::memcpy(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(), shadowBytes);
memo.valid = true; memo.valid = true;
} }
@@ -2744,7 +2744,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
samplerPassMemo.unitBindingsEpoch == keys.unitBindingsEpoch && samplerPassMemo.unitBindingsEpoch == keys.unitBindingsEpoch &&
samplerPassMemo.samplingGeneration == keys.samplingGeneration && samplerPassMemo.samplingGeneration == keys.samplingGeneration &&
samplerPassMemo.backendStateVersion == programBackendStateVersion && samplerPassMemo.backendStateVersion == programBackendStateVersion &&
samplerPassMemo.textureContextGeneration == TextureImpl::g_textureContextGeneration; samplerPassMemo.textureContextGeneration == g_backendContextGeneration;
if (samplerPassClean) { if (samplerPassClean) {
for (Uint i = 0; i < samplerPassMemo.count; ++i) { for (Uint i = 0; i < samplerPassMemo.count; ++i) {
if (SamplerImpl::g_boundSamplersCache[samplerPassMemo.units[i]] != if (SamplerImpl::g_boundSamplersCache[samplerPassMemo.units[i]] !=
@@ -2843,7 +2843,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
samplerPassMemo.unitBindingsEpoch = keys.unitBindingsEpoch; samplerPassMemo.unitBindingsEpoch = keys.unitBindingsEpoch;
samplerPassMemo.samplingGeneration = keys.samplingGeneration; samplerPassMemo.samplingGeneration = keys.samplingGeneration;
samplerPassMemo.backendStateVersion = programBackendStateVersion; samplerPassMemo.backendStateVersion = programBackendStateVersion;
samplerPassMemo.textureContextGeneration = TextureImpl::g_textureContextGeneration; samplerPassMemo.textureContextGeneration = g_backendContextGeneration;
samplerPassMemo.valid = true; samplerPassMemo.valid = true;
} }
} }
@@ -3605,12 +3605,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false; return false;
} }
if (s_resolveContextGeneration != TextureImpl::g_textureContextGeneration) { if (s_resolveContextGeneration != g_backendContextGeneration) {
// The ids belonged to a dead context; the context reclaimed them with it. // The ids belonged to a dead context; the context reclaimed them with it.
s_resolveFramebuffer = 0; s_resolveFramebuffer = 0;
s_resolveRenderbuffer = 0; s_resolveRenderbuffer = 0;
s_resolveFormat = 0; s_resolveFormat = 0;
s_resolveContextGeneration = TextureImpl::g_textureContextGeneration; s_resolveContextGeneration = g_backendContextGeneration;
} }
if (s_resolveFramebuffer == 0) { if (s_resolveFramebuffer == 0) {
g_GLESFuncs.glGenFramebuffers(1, &s_resolveFramebuffer); g_GLESFuncs.glGenFramebuffers(1, &s_resolveFramebuffer);
@@ -3758,7 +3758,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
static Bool EnsureResources() { static Bool EnsureResources() {
if (s_contextGeneration != TextureImpl::g_textureContextGeneration) { if (s_contextGeneration != g_backendContextGeneration) {
// The ids belonged to a dead context; the context reclaimed them with it. // The ids belonged to a dead context; the context reclaimed them with it.
s_framebuffer = 0; s_framebuffer = 0;
s_texture = 0; s_texture = 0;
@@ -3769,7 +3769,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
s_depthProgram = 0; s_depthProgram = 0;
s_stencilProgram = 0; s_stencilProgram = 0;
s_programsFailed = false; s_programsFailed = false;
s_contextGeneration = TextureImpl::g_textureContextGeneration; s_contextGeneration = g_backendContextGeneration;
} }
if (s_programsFailed) { if (s_programsFailed) {
return false; return false;
@@ -7482,7 +7482,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
PixelStoreImpl::InvalidatePackStateCache(); PixelStoreImpl::InvalidatePackStateCache();
// Texture ids belong to the dying context; wrappers destroyed later must // Texture ids belong to the dying context; wrappers destroyed later must
// not glDeleteTextures a recycled name in a successor context. // not glDeleteTextures a recycled name in a successor context.
++TextureImpl::g_textureContextGeneration; ++g_backendContextGeneration;
g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release); g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release);
// Outstanding fence handles now refer to a dead context; treat them as // Outstanding fence handles now refer to a dead context; treat them as
// signaled from here on. // signaled from here on.
+71 -5
View File
@@ -33,6 +33,8 @@
#include <regex> #include <regex>
namespace MobileGL::MG_Backend::DirectGLES { namespace MobileGL::MG_Backend::DirectGLES {
Uint g_backendContextGeneration = 1;
constexpr Bool PREFER_MAP_BUFFER_RANGE_FOR_BUFFER_SYNC = false; constexpr Bool PREFER_MAP_BUFFER_RANGE_FOR_BUFFER_SYNC = false;
constexpr const char* BASE_INSTANCE_UNIFORM_NAME = "mg_BaseInstance"; constexpr const char* BASE_INSTANCE_UNIFORM_NAME = "mg_BaseInstance";
constexpr const char* DRAW_ID_UNIFORM_NAME = "mg_DrawID"; constexpr const char* DRAW_ID_UNIFORM_NAME = "mg_DrawID";
@@ -1646,7 +1648,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
g_GLESFuncs.glGenTextures(1, &m_backendTextureId); g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
m_contextGeneration = g_textureContextGeneration; m_contextGeneration = g_backendContextGeneration;
if (m_backendTextureId == 0) { if (m_backendTextureId == 0) {
MGLOG_E("Failed to generate texture object."); MGLOG_E("Failed to generate texture object.");
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
@@ -1673,7 +1675,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
} }
} }
if (m_contextGeneration == g_textureContextGeneration && g_GLESFuncs.glDeleteTextures) { if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteTextures) {
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId); g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
} }
m_backendTextureId = 0; m_backendTextureId = 0;
@@ -1712,7 +1714,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BackendTextureObject::RecreateBackendTexture() { void BackendTextureObject::RecreateBackendTexture() {
if (m_backendTextureId != 0) { if (m_backendTextureId != 0) {
ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId); ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId);
if (m_contextGeneration == g_textureContextGeneration) { if (m_contextGeneration == g_backendContextGeneration) {
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId); g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
} }
for (auto& unitCache : g_boundTexturesCache) { for (auto& unitCache : g_boundTexturesCache) {
@@ -1725,7 +1727,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
g_GLESFuncs.glGenTextures(1, &m_backendTextureId); g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
m_contextGeneration = g_textureContextGeneration; m_contextGeneration = g_backendContextGeneration;
if (m_backendTextureId == 0) { if (m_backendTextureId == 0) {
MGLOG_E("Failed to regenerate texture object."); MGLOG_E("Failed to regenerate texture object.");
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
@@ -3081,7 +3083,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
Uint g_activeTextureUnit = 0; Uint g_activeTextureUnit = 0;
Uint g_textureContextGeneration = 1;
Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>, Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS> MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
g_boundTexturesCache; g_boundTexturesCache;
@@ -3100,6 +3101,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_backendColorSlots[i] = GL_COLOR_ATTACHMENT0 + i; m_backendColorSlots[i] = GL_COLOR_ATTACHMENT0 + i;
} }
g_GLESFuncs.glGenFramebuffers(1, &m_backendFBOId); g_GLESFuncs.glGenFramebuffers(1, &m_backendFBOId);
m_contextGeneration = g_backendContextGeneration;
if (m_backendFBOId == 0) { if (m_backendFBOId == 0) {
MGLOG_E("Failed to generate framebuffer object."); MGLOG_E("Failed to generate framebuffer object.");
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
@@ -3108,6 +3110,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
} }
BackendFramebufferObject::~BackendFramebufferObject() {
if (InProcessTeardown()) {
return; // see InProcessTeardown(): the driver may be unloaded already
}
if (m_backendFBOId == 0) {
return;
}
// Scrub the binding shadow whether or not the id can still be deleted: a
// recycled name must never satisfy the shadow's dedup.
NoteFramebufferIdDeleted(m_backendFBOId);
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteFramebuffers) {
g_GLESFuncs.glDeleteFramebuffers(1, &m_backendFBOId);
}
m_backendFBOId = 0;
}
void BackendFramebufferObject::Bind(FramebufferTarget target) const { void BackendFramebufferObject::Bind(FramebufferTarget target) const {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
@@ -3163,6 +3181,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
return g_driverFBOBindings[idx]; return g_driverFBOBindings[idx];
} }
void NoteFramebufferIdDeleted(Uint id) {
if (id == 0) {
return;
}
for (SizeT idx = 0; idx < g_driverFBOBindings.size(); ++idx) {
if (g_driverFBOBindingKnown[idx] && g_driverFBOBindings[idx] == id) {
g_driverFBOBindings[idx] = 0; // glDeleteFramebuffers reverts a bound FBO to 0
}
}
}
void InvalidateFramebufferBindingCache() { void InvalidateFramebufferBindingCache() {
g_driverFBOBindings = {0, 0}; g_driverFBOBindings = {0, 0};
g_driverFBOBindingKnown = {false, false}; g_driverFBOBindingKnown = {false, false};
@@ -4570,6 +4599,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
g_GLESFuncs.glGenSamplers(1, &m_backendSamplerId); g_GLESFuncs.glGenSamplers(1, &m_backendSamplerId);
m_contextGeneration = g_backendContextGeneration;
if (m_backendSamplerId == 0) { if (m_backendSamplerId == 0) {
MGLOG_E("Failed to generate sampler object."); MGLOG_E("Failed to generate sampler object.");
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
@@ -4578,6 +4608,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
} }
BackendSamplerObject::~BackendSamplerObject() {
if (InProcessTeardown()) {
return; // see InProcessTeardown(): the driver may be unloaded already
}
if (m_backendSamplerId == 0) {
return;
}
// Scrub the unit shadow whether or not the id can still be deleted - the next
// twin can land on this heap address and would otherwise false-skip its Bind.
for (auto& boundSampler : g_boundSamplersCache) {
if (boundSampler == this) {
boundSampler = nullptr; // glDeleteSamplers unbinds from every unit
}
}
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteSamplers) {
g_GLESFuncs.glDeleteSamplers(1, &m_backendSamplerId);
}
m_backendSamplerId = 0;
}
void BackendSamplerObject::SyncToBackend( void BackendSamplerObject::SyncToBackend(
const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject) { const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject) {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
@@ -4693,12 +4743,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif #endif
g_GLESFuncs.glGenRenderbuffers(1, &m_backendRBOId); g_GLESFuncs.glGenRenderbuffers(1, &m_backendRBOId);
m_contextGeneration = g_backendContextGeneration;
if (m_backendRBOId == 0) { if (m_backendRBOId == 0) {
MGLOG_E("Failed to generate renderbuffer object."); MGLOG_E("Failed to generate renderbuffer object.");
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
} }
} }
BackendRenderbufferObject::~BackendRenderbufferObject() {
if (InProcessTeardown()) {
return; // see InProcessTeardown(): the driver may be unloaded already
}
if (m_backendRBOId == 0) {
return;
}
// No driver-level renderbuffer-binding shadow exists (Bind() always issues the
// call), so there is nothing to scrub here - only the id to release.
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteRenderbuffers) {
g_GLESFuncs.glDeleteRenderbuffers(1, &m_backendRBOId);
}
m_backendRBOId = 0;
}
void BackendRenderbufferObject::Bind() const { void BackendRenderbufferObject::Bind() const {
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND); ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
+34 -3
View File
@@ -36,6 +36,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool InProcessTeardown(); Bool InProcessTeardown();
void EnsureProcessTeardownSentinel(); void EnsureProcessTeardownSentinel();
// Generation of the backend ES context that owns the driver ids currently handed
// out. Bumped exactly once per DestroyEGLContext. Every backend twin that owns a
// driver name (texture, framebuffer, renderbuffer, sampler) stamps this at
// construction and compares it in its destructor: a twin outliving its context
// must NOT glDelete* its id, because a successor context may already have recycled
// that name and the delete would take out a live object of the new context.
extern Uint g_backendContextGeneration;
// Which optional pieces of state a draw needs synchronized before it is issued. // Which optional pieces of state a draw needs synchronized before it is issued.
// Index/indirect buffer syncs and the instancing-related work are skipped for // Index/indirect buffer syncs and the instancing-related work are skipped for
// draws that provably cannot read them. // draws that provably cannot read them.
@@ -657,15 +665,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS> MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
g_boundTexturesCache; g_boundTexturesCache;
extern Uint g_activeTextureUnit; extern Uint g_activeTextureUnit;
// Bumped when the backend ES context is destroyed; texture ids stamped with
// an older generation belong to a dead context and must not be deleted.
extern Uint g_textureContextGeneration;
} // namespace TextureImpl } // namespace TextureImpl
namespace FramebufferImpl { namespace FramebufferImpl {
class BackendFramebufferObject { class BackendFramebufferObject {
public: public:
BackendFramebufferObject(); BackendFramebufferObject();
// Deletes the driver framebuffer and scrubs the binding shadow. Without it every
// frontend glDeleteFramebuffers leaked one ES framebuffer for the process lifetime;
// an app that creates a framebuffer per readback (GL CTS packed_pixels does ~3300
// per case) walked the driver into hundreds of megabytes of dead framebuffers and
// out of the resources a later attachment needs.
~BackendFramebufferObject();
BackendFramebufferObject(const BackendFramebufferObject&) = delete;
BackendFramebufferObject& operator=(const BackendFramebufferObject&) = delete;
void SyncToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject, void SyncToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject,
FramebufferTarget asTarget); FramebufferTarget asTarget);
// Apply only this FBO's read buffer (glReadBuffer) to the backend. Split out so it can // Apply only this FBO's read buffer (glReadBuffer) to the backend. Split out so it can
@@ -680,6 +693,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
private: private:
Uint m_backendFBOId = 0; Uint m_backendFBOId = 0;
Uint m_contextGeneration = 0;
/* this will save buffers in its original form, /* this will save buffers in its original form,
reversion, absence or not consecutive are all allowed, as long as GL spec allows it reversion, absence or not consecutive are all allowed, as long as GL spec allows it
@@ -821,6 +835,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BindFramebufferId(GLenum fbTarget, Uint id); void BindFramebufferId(GLenum fbTarget, Uint id);
Uint CurrentFramebufferBinding(FramebufferTarget target); Uint CurrentFramebufferBinding(FramebufferTarget target);
void InvalidateFramebufferBindingCache(); void InvalidateFramebufferBindingCache();
// A driver framebuffer id is about to be deleted: ES reverts every target that
// currently binds it to 0, so the binding shadow has to follow or the next
// BindFramebufferId(0) would be deduped away and leave the deleted name bound.
void NoteFramebufferIdDeleted(Uint id);
} // namespace FramebufferImpl } // namespace FramebufferImpl
// Shared scratch framebuffers for the readback/copy/blit emulation paths, with a // Shared scratch framebuffers for the readback/copy/blit emulation paths, with a
@@ -1087,12 +1105,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
class BackendSamplerObject { class BackendSamplerObject {
public: public:
BackendSamplerObject(); BackendSamplerObject();
// Deletes the driver sampler and clears the units whose binding shadow still names
// this twin (a recycled heap address would otherwise false-skip a later Bind).
// Frontend glDeleteSamplers used to leak the backend id for the process lifetime.
~BackendSamplerObject();
BackendSamplerObject(const BackendSamplerObject&) = delete;
BackendSamplerObject& operator=(const BackendSamplerObject&) = delete;
void SyncToBackend(const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject); void SyncToBackend(const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject);
void Bind(Uint unit); void Bind(Uint unit);
Uint GetBackendSamplerId() const; Uint GetBackendSamplerId() const;
private: private:
Uint m_backendSamplerId = 0; Uint m_backendSamplerId = 0;
Uint m_contextGeneration = 0;
Bool m_isInitialized = false; Bool m_isInitialized = false;
SamplerParameters m_cacheSamplerParameters; SamplerParameters m_cacheSamplerParameters;
Uint16 m_syncedSamplerVersion = 0; Uint16 m_syncedSamplerVersion = 0;
@@ -1110,12 +1135,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
class BackendRenderbufferObject { class BackendRenderbufferObject {
public: public:
BackendRenderbufferObject(); BackendRenderbufferObject();
// Deletes the driver renderbuffer; frontend glDeleteRenderbuffers used to leak it
// (with its whole image allocation) for the process lifetime.
~BackendRenderbufferObject();
BackendRenderbufferObject(const BackendRenderbufferObject&) = delete;
BackendRenderbufferObject& operator=(const BackendRenderbufferObject&) = delete;
void SyncToBackend(const SharedPtr<MG_State::GLState::RenderbufferObject>& stateRBOObject); void SyncToBackend(const SharedPtr<MG_State::GLState::RenderbufferObject>& stateRBOObject);
Uint GetBackendRenderbufferId() const { return m_backendRBOId; } Uint GetBackendRenderbufferId() const { return m_backendRBOId; }
void Bind() const; void Bind() const;
private: private:
Uint m_backendRBOId = 0; Uint m_backendRBOId = 0;
Uint m_contextGeneration = 0;
Bool m_isInitialized = false; Bool m_isInitialized = false;
TextureInternalFormat m_cacheInternalFormat = TextureInternalFormat::Unknown; TextureInternalFormat m_cacheInternalFormat = TextureInternalFormat::Unknown;
Int m_cacheWidth = 0; Int m_cacheWidth = 0;
@@ -55,6 +55,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/ThreeChannelAttachmentScenario.cpp Scenarios/ThreeChannelAttachmentScenario.cpp
Scenarios/PipelineFailureScenario.cpp Scenarios/PipelineFailureScenario.cpp
Scenarios/AdvertisedLimitsScenario.cpp Scenarios/AdvertisedLimitsScenario.cpp
Scenarios/PixelStoreSweepScenario.cpp
) )
target_include_directories(MobileGLIntegrationTest PRIVATE target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -0,0 +1,249 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PixelStoreSweepScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - PIXEL-STORE MODES RESTORE, and FRAMEBUFFER CHURN STAYS EXACT.
//
// Both cases here replay the shape of KHR-GL3x.packed_pixels.varied_rectangle, the single
// heaviest polluter in the GL CTS: for each of 46 (pixel-store mode, value) pairs it uploads a
// gradient into a fresh texture, attaches that texture to a FRESH framebuffer, reads it back and
// deletes both - ~3300 texture+framebuffer pairs per test case.
//
// What that found: DirectGLES had no destructor for BackendFramebufferObject (nor for the
// renderbuffer and sampler twins), so every frontend glDeleteFramebuffers leaked one driver
// framebuffer for the process lifetime. On an Adreno 830 the CTS run walked the driver to 1.2 GB
// of dead objects, and from that point on EVERY readback through a freshly attached framebuffer
// came back with someone else's pixels - which is what made ~1,500 otherwise-correct cases fail
// depending only on how much ran before them. The unit-level pin for the missing destructors is
// MG_Test/SanityTest.cpp (DirectGLESBackendFramebuffer/Renderbuffer/Sampler); this file pins the
// end-to-end behaviour they protect.
//
// The mode sweep is the second half of the same story: 46 modes are set and reset per case, so a
// mode that fails to restore is indistinguishable from the leak in a full-batch CTS run. The
// assertion here is RESTORATION - after every single mode is set and put back, a readback at
// default state must be byte-identical to one taken before the sweep ever started.
//
// Backend-agnostic on purpose: both bugs this guards against are frontend/backend bookkeeping,
// and DirectVulkan is the built-in control.
#include <cstdint>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
// Small enough that the table's row lengths (10, 15) and image heights are all >= the
// image, which is the shape the CTS uses (its gradient is 7x3).
constexpr int kTexSize = 8;
// Every buffer handed to GL is this big regardless of the image size: with row length 15,
// two skipped rows/pixels and alignment 8 the driver strides well past the natural image
// extent, and a tight buffer would be an out-of-bounds access rather than a test. (It was:
// the first version of this scenario passed its assertions and then segfaulted at
// teardown, because glReadPixels had written past a 1 KiB destination.)
constexpr std::size_t kScratchBytes = 64 * 1024;
// Every pixel-store mode GL 4.0 has, so a reset provably covers the whole state and not
// just the subset a particular test happened to touch.
struct PixelStoreMode {
GLenum name;
GLint defaultValue;
};
const PixelStoreMode kAllModes[] = {
{GL_UNPACK_SWAP_BYTES, 0}, {GL_UNPACK_LSB_FIRST, 0}, {GL_UNPACK_ROW_LENGTH, 0},
{GL_UNPACK_IMAGE_HEIGHT, 0}, {GL_UNPACK_SKIP_ROWS, 0}, {GL_UNPACK_SKIP_PIXELS, 0},
{GL_UNPACK_SKIP_IMAGES, 0}, {GL_UNPACK_ALIGNMENT, 4}, {GL_PACK_SWAP_BYTES, 0},
{GL_PACK_LSB_FIRST, 0}, {GL_PACK_ROW_LENGTH, 0}, {GL_PACK_IMAGE_HEIGHT, 0},
{GL_PACK_SKIP_ROWS, 0}, {GL_PACK_SKIP_PIXELS, 0}, {GL_PACK_SKIP_IMAGES, 0},
{GL_PACK_ALIGNMENT, 4},
};
// The CTS table verbatim (glcPackedPixelsTests.cpp VariedRectangleTest::iterate): 32
// common cases plus the 14 core-only ones ES has no equivalent for and MobileGL therefore
// honours on the CPU. IMAGE_WIDTH_1/2 and IMAGE_HEIGHT_1/2 are the CTS's 10 and 15.
struct SweepCase {
GLenum mode;
GLint value;
};
const SweepCase kSweep[] = {
{GL_UNPACK_ROW_LENGTH, 0}, {GL_UNPACK_ROW_LENGTH, 10}, {GL_UNPACK_ROW_LENGTH, 15},
{GL_UNPACK_SKIP_ROWS, 0}, {GL_UNPACK_SKIP_ROWS, 1}, {GL_UNPACK_SKIP_ROWS, 2},
{GL_UNPACK_SKIP_PIXELS, 0}, {GL_UNPACK_SKIP_PIXELS, 1}, {GL_UNPACK_SKIP_PIXELS, 2},
{GL_UNPACK_ALIGNMENT, 1}, {GL_UNPACK_ALIGNMENT, 2}, {GL_UNPACK_ALIGNMENT, 4},
{GL_UNPACK_ALIGNMENT, 8}, {GL_UNPACK_IMAGE_HEIGHT, 0}, {GL_UNPACK_IMAGE_HEIGHT, 10},
{GL_UNPACK_IMAGE_HEIGHT, 15}, {GL_UNPACK_SKIP_IMAGES, 0}, {GL_UNPACK_SKIP_IMAGES, 1},
{GL_UNPACK_SKIP_IMAGES, 2}, {GL_PACK_ROW_LENGTH, 0}, {GL_PACK_ROW_LENGTH, 10},
{GL_PACK_ROW_LENGTH, 15}, {GL_PACK_SKIP_ROWS, 0}, {GL_PACK_SKIP_ROWS, 1},
{GL_PACK_SKIP_ROWS, 2}, {GL_PACK_SKIP_PIXELS, 0}, {GL_PACK_SKIP_PIXELS, 1},
{GL_PACK_SKIP_PIXELS, 2}, {GL_PACK_ALIGNMENT, 1}, {GL_PACK_ALIGNMENT, 2},
{GL_PACK_ALIGNMENT, 4}, {GL_PACK_ALIGNMENT, 8},
// core-only, no ES equivalent
{GL_UNPACK_SWAP_BYTES, GL_FALSE}, {GL_UNPACK_SWAP_BYTES, GL_TRUE},
{GL_UNPACK_LSB_FIRST, GL_FALSE}, {GL_UNPACK_LSB_FIRST, GL_TRUE},
{GL_PACK_SWAP_BYTES, GL_FALSE}, {GL_PACK_SWAP_BYTES, GL_TRUE},
{GL_PACK_LSB_FIRST, GL_FALSE}, {GL_PACK_LSB_FIRST, GL_TRUE},
{GL_PACK_IMAGE_HEIGHT, 0}, {GL_PACK_IMAGE_HEIGHT, 10},
{GL_PACK_IMAGE_HEIGHT, 15}, {GL_PACK_SKIP_IMAGES, 0},
{GL_PACK_SKIP_IMAGES, 1}, {GL_PACK_SKIP_IMAGES, 2},
};
std::size_t ImageBytes(int size) { return static_cast<std::size_t>(size) * size * 4; }
// Padded to kScratchBytes so it is safe to hand to an upload running under any of the
// sweep's stride/skip settings.
std::vector<std::uint8_t> MakeGradient(int size, unsigned seed) {
std::vector<std::uint8_t> pixels(kScratchBytes, 0);
for (int y = 0; y < size; ++y) {
for (int x = 0; x < size; ++x) {
const std::size_t base = (static_cast<std::size_t>(y) * size + x) * 4;
pixels[base + 0] = static_cast<std::uint8_t>((x * 11 + seed) & 0xFF);
pixels[base + 1] = static_cast<std::uint8_t>((y * 13 + seed) & 0xFF);
pixels[base + 2] = static_cast<std::uint8_t>((x * y + seed) & 0xFF);
pixels[base + 3] = 0xFF;
}
}
return pixels;
}
void ResetAllPixelStoreModes() {
for (const PixelStoreMode& mode : kAllModes) {
glPixelStorei(mode.name, mode.defaultValue);
}
}
// The one operation the CTS repeats: a fresh texture, a fresh framebuffer, one readback,
// both deleted. Returns the readback; `outStatus` carries the completeness answer so a
// caller can tell an incomplete framebuffer apart from wrong pixels.
std::vector<std::uint8_t> UploadAndReadBack(const std::vector<std::uint8_t>& source, int size,
GLenum* outStatus) {
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, size, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, source.data());
GLuint fbo = 0;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
*outStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
std::vector<std::uint8_t> read(kScratchBytes, 0);
if (*outStatus == GL_FRAMEBUFFER_COMPLETE) {
glReadPixels(0, 0, size, size, GL_RGBA, GL_UNSIGNED_BYTE, read.data());
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &fbo);
glBindTexture(GL_TEXTURE_2D, 0);
glDeleteTextures(1, &texture);
return read;
}
// Index of the first differing byte within the image, or `bytes` when they agree.
std::size_t FirstDifference(const std::vector<std::uint8_t>& a, const std::vector<std::uint8_t>& b,
std::size_t bytes) {
for (std::size_t i = 0; i < bytes; ++i) {
if (a[i] != b[i]) return i;
}
return bytes;
}
class PixelStoreSweepScenario : public ScenarioTest {};
class FramebufferChurnScenario : public ScenarioTest {};
} // namespace
// Every mode in the CTS table is set, exercised and put back; the readback at default state
// afterwards must be bit-identical to the one taken before the sweep. A mode that silently
// fails to restore corrupts every later case in the batch, which is exactly how the CTS
// failures presented (the FIRST sub-case, at default state, is what failed).
TEST_F(PixelStoreSweepScenario, DefaultStateSurvivesTheFullModeSweep) {
if (!Ready()) return;
ResetAllPixelStoreModes();
ASSERT_EQ(FirstGLError(), 0u) << "resetting the pixel-store modes must be legal on a GL 4.0 context";
const std::vector<std::uint8_t> gradient = MakeGradient(kTexSize, 0);
GLenum status = 0;
const std::vector<std::uint8_t> baseline = UploadAndReadBack(gradient, kTexSize, &status);
ASSERT_EQ(status, static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
ASSERT_EQ(FirstGLError(), 0u);
const std::vector<std::uint8_t> scratchSource(kScratchBytes, 0x5A);
for (const SweepCase& sweep : kSweep) {
glPixelStorei(sweep.mode, sweep.value);
ASSERT_EQ(FirstGLError(), 0u) << "glPixelStorei(0x" << std::hex << sweep.mode << std::dec << ", "
<< sweep.value << ") must be accepted";
// Exercise the mode: an upload and a readback that both run with it in force.
GLenum sweepStatus = 0;
(void)UploadAndReadBack(scratchSource, kTexSize, &sweepStatus);
ResetAllPixelStoreModes();
GLenum afterStatus = 0;
const std::vector<std::uint8_t> after = UploadAndReadBack(gradient, kTexSize, &afterStatus);
ASSERT_EQ(afterStatus, static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
const std::size_t diff = FirstDifference(baseline, after, ImageBytes(kTexSize));
ASSERT_EQ(diff, ImageBytes(kTexSize))
<< "default-state readback changed after setting and resetting 0x" << std::hex << sweep.mode
<< std::dec << " = " << sweep.value << "; first differing byte " << diff << " (baseline "
<< static_cast<int>(baseline[diff]) << ", now " << static_cast<int>(after[diff]) << ")";
}
// And the modes themselves must read back as the defaults the reset asked for.
for (const PixelStoreMode& mode : kAllModes) {
GLint value = -1;
glGetIntegerv(mode.name, &value);
EXPECT_EQ(value, mode.defaultValue)
<< "pixel-store mode 0x" << std::hex << mode.name << std::dec << " did not return to its default";
}
EXPECT_EQ(FirstGLError(), 0u);
}
// The leak regression. Each iteration is one complete CTS inner step, and every readback has
// to be exactly the gradient THIS iteration uploaded - never the previous one's. Before the
// missing destructors were added, the driver-side framebuffer count grew without bound here.
TEST_F(FramebufferChurnScenario, RepeatedFramebufferReadbackStaysExact) {
if (!Ready()) return;
ResetAllPixelStoreModes();
constexpr int kSize = 8;
constexpr int kIterations = 1024;
for (int i = 0; i < kIterations; ++i) {
// A distinct gradient per iteration: a stale attachment or a recycled driver name
// reads back the PREVIOUS iteration's image, which a constant fill could not tell
// apart from a correct read.
const std::vector<std::uint8_t> gradient = MakeGradient(kSize, static_cast<unsigned>(i * 7 + 1));
GLenum status = 0;
const std::vector<std::uint8_t> read = UploadAndReadBack(gradient, kSize, &status);
ASSERT_EQ(status, static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE)) << "iteration " << i;
const std::size_t diff = FirstDifference(gradient, read, ImageBytes(kSize));
ASSERT_EQ(diff, ImageBytes(kSize))
<< "iteration " << i << " read back a different image than it uploaded; first differing byte "
<< diff << " (uploaded " << static_cast<int>(gradient[diff]) << ", read "
<< static_cast<int>(read[diff]) << ")";
ASSERT_EQ(FirstGLError(), 0u) << "iteration " << i;
}
}
} // namespace MGITest
+159 -2
View File
@@ -1822,13 +1822,170 @@ TEST(DirectGLESBackendTexture, DestructorDeletesIdAndScrubsBindingCache) {
// A wrapper whose context died must NOT delete a foreign (recycled) name. // A wrapper whose context died must NOT delete a foreign (recycled) name.
{ {
auto backendTexture = MobileGL::MakeShared<TextureImpl::BackendTextureObject>(); auto backendTexture = MobileGL::MakeShared<TextureImpl::BackendTextureObject>();
++TextureImpl::g_textureContextGeneration; ++g_backendContextGeneration;
backendTexture.reset(); backendTexture.reset();
--TextureImpl::g_textureContextGeneration; // restore for later tests --g_backendContextGeneration; // restore for later tests
EXPECT_EQ(deleted.size(), 1u); EXPECT_EQ(deleted.size(), 1u);
} }
} }
// ---- DirectGLES backend twins release their driver ids --------------------------------------
// Framebuffers, renderbuffers and samplers had no destructor at all: every frontend object the
// application deleted leaked its ES twin for the whole process lifetime. An application that
// creates a framebuffer per readback (GL CTS packed_pixels.varied_rectangle makes ~3300 of them
// per case) walked the driver into a gigabyte of dead framebuffers, and past that point every
// readback through a freshly attached framebuffer came back with stale pixels.
namespace {
struct TwinDeletionSinks {
MobileGL::Vector<GLuint> framebuffers;
MobileGL::Vector<GLuint> renderbuffers;
MobileGL::Vector<GLuint> samplers;
};
TwinDeletionSinks* g_twinDeletionSinks = nullptr;
GLuint g_nextTwinDriverId = 900;
void TW_GenFramebuffers(GLsizei count, GLuint* ids) {
for (GLsizei i = 0; i < count; ++i) ids[i] = g_nextTwinDriverId++;
}
void TW_DeleteFramebuffers(GLsizei count, const GLuint* ids) {
if (!g_twinDeletionSinks) return;
for (GLsizei i = 0; i < count; ++i) g_twinDeletionSinks->framebuffers.push_back(ids[i]);
}
void TW_GenRenderbuffers(GLsizei count, GLuint* ids) {
for (GLsizei i = 0; i < count; ++i) ids[i] = g_nextTwinDriverId++;
}
void TW_DeleteRenderbuffers(GLsizei count, const GLuint* ids) {
if (!g_twinDeletionSinks) return;
for (GLsizei i = 0; i < count; ++i) g_twinDeletionSinks->renderbuffers.push_back(ids[i]);
}
void TW_GenSamplers(GLsizei count, GLuint* ids) {
for (GLsizei i = 0; i < count; ++i) ids[i] = g_nextTwinDriverId++;
}
void TW_DeleteSamplers(GLsizei count, const GLuint* ids) {
if (!g_twinDeletionSinks) return;
for (GLsizei i = 0; i < count; ++i) g_twinDeletionSinks->samplers.push_back(ids[i]);
}
void TW_BindFramebuffer(GLenum target, GLuint framebuffer) {
SG_Log("BindFramebuffer:" + std::to_string(target) + ":" + std::to_string(framebuffer));
}
void TW_BindSampler(GLuint, GLuint) {}
void TW_BindRenderbuffer(GLenum, GLuint) {}
// Installs a table that can create and destroy all three twin kinds, and unwinds it (plus the
// recording pointer) even when an assertion aborts the test body.
struct ScopedBackendTwinMocks {
ScopedBackendTwinMocks(): previousFunctions(MobileGL::MG_Backend::DirectGLES::g_GLESFuncs) {
MobileGL::MG_Backend::DirectGLES::FramebufferImpl::InvalidateFramebufferBindingCache();
MobileGL::MG_External::GLESFunctionsTable functions{};
functions.glGenFramebuffers = TW_GenFramebuffers;
functions.glDeleteFramebuffers = TW_DeleteFramebuffers;
functions.glBindFramebuffer = TW_BindFramebuffer;
functions.glGenRenderbuffers = TW_GenRenderbuffers;
functions.glDeleteRenderbuffers = TW_DeleteRenderbuffers;
functions.glBindRenderbuffer = TW_BindRenderbuffer;
functions.glGenSamplers = TW_GenSamplers;
functions.glDeleteSamplers = TW_DeleteSamplers;
functions.glBindSampler = TW_BindSampler;
functions.glGetError = SG_NoError;
MobileGL::MG_Backend::DirectGLES::SetGLESFuncsTable(functions);
g_twinDeletionSinks = &sinks;
g_stateGuardLog = &log;
}
~ScopedBackendTwinMocks() {
g_stateGuardLog = nullptr;
g_twinDeletionSinks = nullptr;
MobileGL::MG_Backend::DirectGLES::SetGLESFuncsTable(previousFunctions);
MobileGL::MG_Backend::DirectGLES::FramebufferImpl::InvalidateFramebufferBindingCache();
}
ScopedBackendTwinMocks(const ScopedBackendTwinMocks&) = delete;
ScopedBackendTwinMocks& operator=(const ScopedBackendTwinMocks&) = delete;
TwinDeletionSinks sinks;
StateGuardCallLog log;
MobileGL::MG_External::GLESFunctionsTable previousFunctions;
};
} // namespace
TEST(DirectGLESBackendFramebuffer, DestructorDeletesIdAndScrubsBindingShadow) {
using namespace MobileGL::MG_Backend::DirectGLES;
ScopedBackendTwinMocks mocks;
GLuint id = 0;
{
auto backendFBO = MobileGL::MakeShared<FramebufferImpl::BackendFramebufferObject>();
id = backendFBO->GetBackendFramebufferId();
ASSERT_NE(id, 0u);
backendFBO->Bind(MobileGL::FramebufferTarget::Draw);
ASSERT_EQ(FramebufferImpl::CurrentFramebufferBinding(MobileGL::FramebufferTarget::Draw), id);
}
ASSERT_EQ(mocks.sinks.framebuffers.size(), 1u);
EXPECT_EQ(mocks.sinks.framebuffers[0], id);
// ES reverts every target bound to a deleted framebuffer to 0. The shadow has to follow, or
// the next BindFramebufferId(0) is deduped away and the driver keeps the dead name bound.
EXPECT_EQ(FramebufferImpl::CurrentFramebufferBinding(MobileGL::FramebufferTarget::Draw), 0u);
// A twin whose context died must NOT delete a name a successor context may have recycled.
{
auto backendFBO = MobileGL::MakeShared<FramebufferImpl::BackendFramebufferObject>();
++g_backendContextGeneration;
backendFBO.reset();
--g_backendContextGeneration; // restore for later tests
EXPECT_EQ(mocks.sinks.framebuffers.size(), 1u);
}
}
TEST(DirectGLESBackendRenderbuffer, DestructorDeletesId) {
using namespace MobileGL::MG_Backend::DirectGLES;
ScopedBackendTwinMocks mocks;
GLuint id = 0;
{
auto backendRBO = MobileGL::MakeShared<RenderbufferImpl::BackendRenderbufferObject>();
id = backendRBO->GetBackendRenderbufferId();
ASSERT_NE(id, 0u);
}
ASSERT_EQ(mocks.sinks.renderbuffers.size(), 1u);
EXPECT_EQ(mocks.sinks.renderbuffers[0], id);
{
auto backendRBO = MobileGL::MakeShared<RenderbufferImpl::BackendRenderbufferObject>();
++g_backendContextGeneration;
backendRBO.reset();
--g_backendContextGeneration;
EXPECT_EQ(mocks.sinks.renderbuffers.size(), 1u);
}
}
TEST(DirectGLESBackendSampler, DestructorDeletesIdAndScrubsUnitCache) {
using namespace MobileGL::MG_Backend::DirectGLES;
ScopedBackendTwinMocks mocks;
GLuint id = 0;
{
auto backendSampler = MobileGL::MakeShared<SamplerImpl::BackendSamplerObject>();
id = backendSampler->GetBackendSamplerId();
ASSERT_NE(id, 0u);
backendSampler->Bind(3);
ASSERT_EQ(SamplerImpl::g_boundSamplersCache[3], backendSampler.get());
}
ASSERT_EQ(mocks.sinks.samplers.size(), 1u);
EXPECT_EQ(mocks.sinks.samplers[0], id);
// glDeleteSamplers unbinds from every unit, and the next twin can land on this heap
// address - a stale row would false-skip its Bind.
EXPECT_EQ(SamplerImpl::g_boundSamplersCache[3], nullptr);
{
auto backendSampler = MobileGL::MakeShared<SamplerImpl::BackendSamplerObject>();
++g_backendContextGeneration;
backendSampler.reset();
--g_backendContextGeneration;
EXPECT_EQ(mocks.sinks.samplers.size(), 1u);
}
}
TEST(DirectGLESStateGuards, DefaultFramebufferBindGoesThroughShadow) { TEST(DirectGLESStateGuards, DefaultFramebufferBindGoesThroughShadow) {
using namespace MobileGL::MG_Backend::DirectGLES; using namespace MobileGL::MG_Backend::DirectGLES;
ScopedStateGuardMocks mocks; ScopedStateGuardMocks mocks;