mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
[Fix] (DirectGLES): overhaul readback/copy/blit driver-state handling with shadow-backed RAII guards - pixel-PACK/UNPACK PBO binding caches resting at 0 (the old bind-then-query 'restore' left the user PBO bound forever, capturing later client-memory readbacks), a PACK pixel-store shadow replacing per-readback glGetIntegerv syncs, a driver FBO-binding shadow behind all scoped binders (per-instance prev slots, nest-safe), scratch-FBO attachment shadows that detach cross-aspect residue exactly when present (depth CopyTex* attachments used to wedge later GetTexImage color reads and vice versa), scissor guards around emulation blits (app scissor clipped depth copies), stale-driver-error drains before single-shot glGetError consumers (fallbacks silently dropped readbacks / GenerateMipmap raised phantom app errors in production builds), ClearBufferiv missing BindCurrentFBO(Draw), cube-face glBindTexture INVALID_ENUM cache poisoning, per-slice GL_PACK_IMAGE_HEIGHT/SKIP_IMAGES semantics on 3D GetTexImage, backend texture ids deleted on wrapper destruction with cache/scratch-FBO scrubs (ids used to leak for the context lifetime and dangling cache pointers could false-skip binds), and context-death/MakeCurrent invalidation for all new shadows; regression tests drive the shadows against a recording mock GLES table
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -267,16 +267,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint g_boundArrayBufferId = 0;
|
||||
Bool g_boundArrayBufferKnown = false;
|
||||
|
||||
// Driver-level GL_PIXEL_PACK/UNPACK_BUFFER binding shadows (see
|
||||
// Managers.h). Resting state between operations is 0; scopes in the
|
||||
// readback/upload paths bind what they need through the cache and
|
||||
// return to 0, so a stale user PBO can never capture a later
|
||||
// readback that meant to target client memory.
|
||||
Uint g_boundPixelPackBufferId = 0;
|
||||
Bool g_boundPixelPackBufferKnown = false;
|
||||
Uint g_boundPixelUnpackBufferId = 0;
|
||||
Bool g_boundPixelUnpackBufferKnown = false;
|
||||
|
||||
// Bumped whenever the backend ES context is destroyed; resources with
|
||||
// an older generation hold ids from a dead context.
|
||||
Uint g_bufferContextGeneration = 1;
|
||||
|
||||
// Defined next to the indexed-binding shadow below; forward-declared so
|
||||
// every glDeleteBuffers site in this namespace can scrub stale shadow
|
||||
// entries (GL resets a deleted buffer's indexed bindings to 0, and a
|
||||
// recycled name matching a stale shadow entry would otherwise
|
||||
// false-skip the rebind).
|
||||
void ScrubIndexedBufferBindingShadowForId(Uint id);
|
||||
// entries (GL resets a deleted buffer's bindings - indexed and pixel
|
||||
// pack/unpack alike - to 0, and a recycled name matching a stale shadow
|
||||
// entry would otherwise false-skip the rebind).
|
||||
void ScrubBufferBindingShadowsForId(Uint id);
|
||||
|
||||
// Resources whose owning BufferObject died; ids deleted at the next
|
||||
// sync point with a current ES context.
|
||||
@@ -318,10 +328,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (g_boundArrayBufferKnown && g_boundArrayBufferId == r.id) {
|
||||
InvalidateArrayBufferBindingCache();
|
||||
}
|
||||
// Pooling keeps the id alive (and thus any driver binding of it);
|
||||
// drop to unknown rather than claiming the post-delete 0 state.
|
||||
if ((g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == r.id) ||
|
||||
(g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == r.id)) {
|
||||
InvalidatePixelBufferBindingCaches();
|
||||
}
|
||||
const std::lock_guard<std::mutex> lock(g_poolMutex);
|
||||
auto& bucket = g_bufferPool[r.storageSize];
|
||||
if (bucket.size() >= kMaxEntriesPerBucket || g_pooledBytes + r.storageSize > kMaxPoolBytes) {
|
||||
ScrubIndexedBufferBindingShadowForId(r.id);
|
||||
ScrubBufferBindingShadowsForId(r.id);
|
||||
g_GLESFuncs.glDeleteBuffers(1, &r.id); // over budget: don't pool
|
||||
r.id = 0;
|
||||
return;
|
||||
@@ -502,7 +518,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Need a fresh id: glBufferStorage fails on a buffer that already has
|
||||
// immutable storage, and any prior mutable store is replaced anyway.
|
||||
if (resource->id != 0) {
|
||||
ScrubIndexedBufferBindingShadowForId(resource->id);
|
||||
ScrubBufferBindingShadowsForId(resource->id);
|
||||
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
|
||||
resource->id = 0;
|
||||
}
|
||||
@@ -630,7 +646,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) {
|
||||
InvalidateArrayBufferBindingCache();
|
||||
}
|
||||
ScrubIndexedBufferBindingShadowForId(glesResource->id);
|
||||
ScrubBufferBindingShadowsForId(glesResource->id);
|
||||
g_GLESFuncs.glDeleteBuffers(1, &glesResource->id);
|
||||
glesResource->id = 0;
|
||||
}
|
||||
@@ -668,6 +684,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void OnBackendContextDestroyed() {
|
||||
UnregisterBufferBackendOps();
|
||||
++g_bufferContextGeneration;
|
||||
InvalidateArrayBufferBindingCache();
|
||||
InvalidateIndexedBufferBindingCache();
|
||||
InvalidatePixelBufferBindingCaches();
|
||||
// The global-UBO ring's id and persistent map died with the context;
|
||||
// drop the handles (no GL) and let the next draw recreate the ring.
|
||||
ResetUboRingForNewContext();
|
||||
@@ -694,7 +713,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) {
|
||||
InvalidateArrayBufferBindingCache();
|
||||
}
|
||||
ScrubIndexedBufferBindingShadowForId(glesResource->id);
|
||||
ScrubBufferBindingShadowsForId(glesResource->id);
|
||||
g_GLESFuncs.glDeleteBuffers(1, &glesResource->id);
|
||||
glesResource->id = 0;
|
||||
}
|
||||
@@ -819,6 +838,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_boundArrayBufferKnown = false;
|
||||
}
|
||||
|
||||
void BindPixelPackBufferId(Uint id) {
|
||||
if (g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == id) {
|
||||
return;
|
||||
}
|
||||
g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, id);
|
||||
g_boundPixelPackBufferId = id;
|
||||
g_boundPixelPackBufferKnown = true;
|
||||
}
|
||||
|
||||
void BindPixelUnpackBufferId(Uint id) {
|
||||
if (g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == id) {
|
||||
return;
|
||||
}
|
||||
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, id);
|
||||
g_boundPixelUnpackBufferId = id;
|
||||
g_boundPixelUnpackBufferKnown = true;
|
||||
}
|
||||
|
||||
void InvalidatePixelBufferBindingCaches() {
|
||||
g_boundPixelPackBufferId = 0;
|
||||
g_boundPixelPackBufferKnown = false;
|
||||
g_boundPixelUnpackBufferId = 0;
|
||||
g_boundPixelUnpackBufferKnown = false;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Shadow of the GL indexed buffer bindings so redundant glBindBufferBase/Range
|
||||
// (same index + id + range) are skipped. isBase distinguishes a whole-buffer
|
||||
@@ -840,12 +884,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// glDeleteBuffers resets the deleted buffer's bindings (indexed ones
|
||||
// included) to 0 in the current context; mirror that in the shadow, or a
|
||||
// later buffer recycling the same name with a matching range would
|
||||
// false-skip its rebind. Default IndexedBufferBinding{} == base(0) ==
|
||||
// the post-delete GL state.
|
||||
void ScrubIndexedBufferBindingShadowForId(Uint id) {
|
||||
// glDeleteBuffers resets the deleted buffer's bindings (indexed and
|
||||
// pixel pack/unpack ones included) to 0 in the current context; mirror
|
||||
// that in the shadows, or a later buffer recycling the same name with a
|
||||
// matching shadow entry would false-skip its rebind. Default
|
||||
// IndexedBufferBinding{} == base(0) == the post-delete GL state.
|
||||
void ScrubBufferBindingShadowsForId(Uint id) {
|
||||
if (id == 0) return;
|
||||
for (auto& binding : g_indexedUBOBindings) {
|
||||
if (binding.id == id) binding = {};
|
||||
@@ -853,6 +897,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
for (auto& binding : g_indexedSSBOBindings) {
|
||||
if (binding.id == id) binding = {};
|
||||
}
|
||||
if (g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == id) {
|
||||
g_boundPixelPackBufferId = 0;
|
||||
}
|
||||
if (g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == id) {
|
||||
g_boundPixelUnpackBufferId = 0;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -897,7 +947,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
auto& bucket = g_bufferPool[oldestKey];
|
||||
PooledBuffer& e = bucket[oldestIdx];
|
||||
if (e.contextGeneration == g_bufferContextGeneration && e.id != 0) {
|
||||
ScrubIndexedBufferBindingShadowForId(e.id);
|
||||
ScrubBufferBindingShadowsForId(e.id);
|
||||
g_GLESFuncs.glDeleteBuffers(1, &e.id);
|
||||
}
|
||||
g_pooledBytes -= e.size;
|
||||
@@ -1064,7 +1114,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const Bool staleContext = entry.contextGeneration != g_bufferContextGeneration;
|
||||
if (!staleContext && entry.retireSerial > completed) continue;
|
||||
if (!staleContext && entry.id != 0) {
|
||||
ScrubIndexedBufferBindingShadowForId(entry.id);
|
||||
ScrubBufferBindingShadowsForId(entry.id);
|
||||
g_GLESFuncs.glDeleteBuffers(1, &entry.id);
|
||||
}
|
||||
g_retiredUboRings[i] = g_retiredUboRings.back();
|
||||
@@ -1324,6 +1374,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
|
||||
m_contextGeneration = g_textureContextGeneration;
|
||||
if (m_backendTextureId == 0) {
|
||||
MGLOG_E("Failed to generate texture object.");
|
||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||
@@ -1332,6 +1383,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
BackendTextureObject::~BackendTextureObject() {
|
||||
if (m_backendTextureId == 0) {
|
||||
return;
|
||||
}
|
||||
// Scrub every driver-state shadow that could false-skip when the name
|
||||
// or this heap address is recycled - regardless of whether the id can
|
||||
// still be deleted.
|
||||
ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId);
|
||||
for (auto& unitCache : g_boundTexturesCache) {
|
||||
for (auto& boundTexture : unitCache) {
|
||||
if (boundTexture == this) {
|
||||
boundTexture = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m_contextGeneration == g_textureContextGeneration && g_GLESFuncs.glDeleteTextures) {
|
||||
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
||||
}
|
||||
m_backendTextureId = 0;
|
||||
}
|
||||
|
||||
void BackendTextureObject::Bind(GLenum target, Uint unit) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
@@ -1364,7 +1436,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
void BackendTextureObject::RecreateBackendTexture() {
|
||||
if (m_backendTextureId != 0) {
|
||||
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
||||
ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId);
|
||||
if (m_contextGeneration == g_textureContextGeneration) {
|
||||
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
||||
}
|
||||
for (auto& unitCache : g_boundTexturesCache) {
|
||||
for (auto& boundTexture : unitCache) {
|
||||
if (boundTexture == this) {
|
||||
@@ -1375,6 +1450,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
|
||||
m_contextGeneration = g_textureContextGeneration;
|
||||
if (m_backendTextureId == 0) {
|
||||
MGLOG_E("Failed to regenerate texture object.");
|
||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||
@@ -1391,7 +1467,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// glGetIntegerv - that query forces a driver pipeline sync and, because texture
|
||||
// uploads run it per dirty texture per frame, it dominated the DirectGLES draw
|
||||
// path. The backend unpack state is set ONLY by MobileGL's own save/restore
|
||||
// helpers (this class, TempPixelStoreParameterSync, the R32F copy path), all of
|
||||
// helpers (this class and, historically, the R32F copy path), all of
|
||||
// which restore to the resting default, so the shadow stays accurate; a one-time
|
||||
// forced sync pins the backend to that known default up front. Apply() is
|
||||
// compare-and-set, so the (now redundant) glPixelStorei calls also usually no-op.
|
||||
@@ -2365,6 +2441,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
Uint g_activeTextureUnit = 0;
|
||||
Uint g_textureContextGeneration = 1;
|
||||
Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
|
||||
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
||||
g_boundTexturesCache;
|
||||
@@ -2390,9 +2467,59 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (target == FramebufferTarget::Read)
|
||||
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, m_backendFBOId);
|
||||
BindFramebufferId(GL_READ_FRAMEBUFFER, m_backendFBOId);
|
||||
else
|
||||
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_backendFBOId);
|
||||
BindFramebufferId(GL_DRAW_FRAMEBUFFER, m_backendFBOId);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Driver-level framebuffer-binding shadow (see Managers.h). Indexed by
|
||||
// FramebufferTarget {Draw, Read}.
|
||||
Array<Uint, SizeT(FramebufferTarget::FramebufferTargetCount)> g_driverFBOBindings = {0, 0};
|
||||
Array<Bool, SizeT(FramebufferTarget::FramebufferTargetCount)> g_driverFBOBindingKnown = {false, false};
|
||||
} // namespace
|
||||
|
||||
void BindFramebufferId(GLenum fbTarget, Uint id) {
|
||||
const Bool bindsDraw = fbTarget == GL_DRAW_FRAMEBUFFER || fbTarget == GL_FRAMEBUFFER;
|
||||
const Bool bindsRead = fbTarget == GL_READ_FRAMEBUFFER || fbTarget == GL_FRAMEBUFFER;
|
||||
const SizeT drawIdx = SizeT(FramebufferTarget::Draw);
|
||||
const SizeT readIdx = SizeT(FramebufferTarget::Read);
|
||||
const Bool drawMatches =
|
||||
!bindsDraw || (g_driverFBOBindingKnown[drawIdx] && g_driverFBOBindings[drawIdx] == id);
|
||||
const Bool readMatches =
|
||||
!bindsRead || (g_driverFBOBindingKnown[readIdx] && g_driverFBOBindings[readIdx] == id);
|
||||
if (drawMatches && readMatches) {
|
||||
return;
|
||||
}
|
||||
g_GLESFuncs.glBindFramebuffer(fbTarget, id);
|
||||
if (bindsDraw) {
|
||||
g_driverFBOBindings[drawIdx] = id;
|
||||
g_driverFBOBindingKnown[drawIdx] = true;
|
||||
}
|
||||
if (bindsRead) {
|
||||
g_driverFBOBindings[readIdx] = id;
|
||||
g_driverFBOBindingKnown[readIdx] = true;
|
||||
}
|
||||
}
|
||||
|
||||
Uint CurrentFramebufferBinding(FramebufferTarget target) {
|
||||
const SizeT idx = SizeT(target);
|
||||
if (!g_driverFBOBindingKnown[idx]) {
|
||||
// Cold path: pin the shadow from the driver once (init probes and
|
||||
// pre-shadow code bind raw but restore what they found).
|
||||
GLint binding = 0;
|
||||
g_GLESFuncs.glGetIntegerv(
|
||||
target == FramebufferTarget::Read ? GL_READ_FRAMEBUFFER_BINDING : GL_DRAW_FRAMEBUFFER_BINDING,
|
||||
&binding);
|
||||
g_driverFBOBindings[idx] = static_cast<Uint>(binding);
|
||||
g_driverFBOBindingKnown[idx] = true;
|
||||
}
|
||||
return g_driverFBOBindings[idx];
|
||||
}
|
||||
|
||||
void InvalidateFramebufferBindingCache() {
|
||||
g_driverFBOBindings = {0, 0};
|
||||
g_driverFBOBindingKnown = {false, false};
|
||||
}
|
||||
|
||||
void BackendFramebufferObject::InvalidateSyncedState() {
|
||||
@@ -2446,7 +2573,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (glTextureTarget == GL_UNKNOWN_MGL) {
|
||||
glTextureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(textureObject->GetTarget());
|
||||
}
|
||||
backendTextureObject->Bind(glTextureTarget);
|
||||
// glBindTexture rejects cube-face enums (INVALID_ENUM with no
|
||||
// bind, while Bind() would still record the cube-map cache slot
|
||||
// as bound): bind via the owning cube target; the attach below
|
||||
// keeps the face target.
|
||||
const Bool isCubeFace = glTextureTarget >= GL_TEXTURE_CUBE_MAP_POSITIVE_X &&
|
||||
glTextureTarget <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z;
|
||||
backendTextureObject->Bind(isCubeFace ? GL_TEXTURE_CUBE_MAP : glTextureTarget);
|
||||
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
|
||||
backendTextureObject->GetBackendTextureId(),
|
||||
static_cast<GLint>(attachmentObject.GetTextureLevel()));
|
||||
@@ -2746,6 +2879,295 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_fboSyncedObjects = {};
|
||||
} // namespace FramebufferImpl
|
||||
|
||||
namespace ScratchFBOImpl {
|
||||
namespace {
|
||||
ScratchFramebuffer g_tempFramebuffer;
|
||||
ScratchFramebuffer g_blitReadFramebuffer;
|
||||
ScratchFramebuffer g_blitDrawFramebuffer;
|
||||
Uint g_completeTinyFBOId = 0;
|
||||
Uint g_completeTinyRBOId = 0;
|
||||
|
||||
// Detach every point the shadow no longer vouches for. Used when the
|
||||
// shadow is unknown (context reset, texture id deleted while attached).
|
||||
void ScrubAllAttachments(ScratchFramebuffer& fb, GLenum fbTarget) {
|
||||
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
||||
fb.colorTex = 0;
|
||||
fb.colorTarget = 0;
|
||||
fb.colorLevel = 0;
|
||||
fb.colorLayer = -1;
|
||||
fb.depthTex = 0;
|
||||
fb.depthTarget = 0;
|
||||
fb.depthLevel = 0;
|
||||
fb.depthHasStencil = false;
|
||||
fb.attachmentsKnown = true;
|
||||
}
|
||||
|
||||
void PrepareForUse(ScratchFramebuffer& fb, GLenum fbTarget) {
|
||||
if (!fb.attachmentsKnown) {
|
||||
ScrubAllAttachments(fb, fbTarget);
|
||||
}
|
||||
}
|
||||
|
||||
// The post-attach glGetError probe below must not misread an error some
|
||||
// earlier operation left queued; drain before attaching (rare path -
|
||||
// only runs when the attachment actually changes).
|
||||
void DrainPendingGLErrors() {
|
||||
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
}
|
||||
|
||||
// Record the color point as detached when the shadow said something was
|
||||
// there; the actual detach call is the caller's (it may be replaced by
|
||||
// the new attach directly when the point is being overwritten).
|
||||
void RecordNoColor(ScratchFramebuffer& fb) {
|
||||
fb.colorTex = 0;
|
||||
fb.colorTarget = 0;
|
||||
fb.colorLevel = 0;
|
||||
fb.colorLayer = -1;
|
||||
}
|
||||
|
||||
void RecordNoDepth(ScratchFramebuffer& fb) {
|
||||
fb.depthTex = 0;
|
||||
fb.depthTarget = 0;
|
||||
fb.depthLevel = 0;
|
||||
fb.depthHasStencil = false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ScratchFramebuffer& TempFramebuffer() {
|
||||
return g_tempFramebuffer;
|
||||
}
|
||||
ScratchFramebuffer& BlitReadFramebuffer() {
|
||||
return g_blitReadFramebuffer;
|
||||
}
|
||||
ScratchFramebuffer& BlitDrawFramebuffer() {
|
||||
return g_blitDrawFramebuffer;
|
||||
}
|
||||
|
||||
Uint EnsureId(ScratchFramebuffer& fb) {
|
||||
if (fb.id == 0) {
|
||||
g_GLESFuncs.glGenFramebuffers(1, &fb.id);
|
||||
// A fresh FBO has nothing attached and COLOR_ATTACHMENT0 read/draw
|
||||
// buffers (the ES defaults for a non-default framebuffer).
|
||||
fb.attachmentsKnown = true;
|
||||
RecordNoColor(fb);
|
||||
RecordNoDepth(fb);
|
||||
fb.readBuffer = GL_COLOR_ATTACHMENT0;
|
||||
fb.drawBuffer = GL_COLOR_ATTACHMENT0;
|
||||
}
|
||||
return fb.id;
|
||||
}
|
||||
|
||||
void EnsureColorAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget,
|
||||
GLint level) {
|
||||
PrepareForUse(fb, fbTarget);
|
||||
if (fb.depthTex != 0) {
|
||||
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
||||
RecordNoDepth(fb);
|
||||
}
|
||||
if (fb.colorTex == tex && fb.colorTarget == texTarget && fb.colorLevel == level && fb.colorLayer < 0) {
|
||||
return;
|
||||
}
|
||||
if (fb.colorTex != 0) {
|
||||
// Detach first: if the new attach fails, the point must read as
|
||||
// missing (incomplete FBO), not silently keep the old texture.
|
||||
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
}
|
||||
DrainPendingGLErrors();
|
||||
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, texTarget, tex, level);
|
||||
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
||||
RecordNoColor(fb);
|
||||
return;
|
||||
}
|
||||
fb.colorTex = tex;
|
||||
fb.colorTarget = texTarget;
|
||||
fb.colorLevel = level;
|
||||
fb.colorLayer = -1;
|
||||
}
|
||||
|
||||
void EnsureColorAttachmentLayer(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLint level, GLint layer) {
|
||||
PrepareForUse(fb, fbTarget);
|
||||
if (fb.depthTex != 0) {
|
||||
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
||||
RecordNoDepth(fb);
|
||||
}
|
||||
if (fb.colorTex == tex && fb.colorTarget == 0 && fb.colorLevel == level && fb.colorLayer == layer) {
|
||||
return;
|
||||
}
|
||||
if (fb.colorTex != 0) {
|
||||
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
}
|
||||
DrainPendingGLErrors();
|
||||
g_GLESFuncs.glFramebufferTextureLayer(fbTarget, GL_COLOR_ATTACHMENT0, tex, level, layer);
|
||||
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
||||
RecordNoColor(fb);
|
||||
return;
|
||||
}
|
||||
fb.colorTex = tex;
|
||||
fb.colorTarget = 0;
|
||||
fb.colorLevel = level;
|
||||
fb.colorLayer = layer;
|
||||
}
|
||||
|
||||
void EnsureDepthAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level,
|
||||
Bool withStencil) {
|
||||
PrepareForUse(fb, fbTarget);
|
||||
if (fb.colorTex != 0) {
|
||||
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
RecordNoColor(fb);
|
||||
}
|
||||
if (fb.depthTex == tex && fb.depthTarget == texTarget && fb.depthLevel == level &&
|
||||
fb.depthHasStencil == withStencil) {
|
||||
return;
|
||||
}
|
||||
if (fb.depthTex != 0) {
|
||||
// One call clears both depth and stencil points regardless of how
|
||||
// the previous attachment was made.
|
||||
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
||||
}
|
||||
DrainPendingGLErrors();
|
||||
g_GLESFuncs.glFramebufferTexture2D(fbTarget,
|
||||
withStencil ? GL_DEPTH_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT,
|
||||
texTarget, tex, level);
|
||||
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
||||
RecordNoDepth(fb);
|
||||
return;
|
||||
}
|
||||
fb.depthTex = tex;
|
||||
fb.depthTarget = texTarget;
|
||||
fb.depthLevel = level;
|
||||
fb.depthHasStencil = withStencil;
|
||||
}
|
||||
|
||||
void EnsureNoColorAttachment(ScratchFramebuffer& fb, GLenum fbTarget) {
|
||||
PrepareForUse(fb, fbTarget);
|
||||
if (fb.colorTex != 0) {
|
||||
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
RecordNoColor(fb);
|
||||
}
|
||||
}
|
||||
|
||||
void EnsureNoDepthAttachment(ScratchFramebuffer& fb, GLenum fbTarget) {
|
||||
PrepareForUse(fb, fbTarget);
|
||||
if (fb.depthTex != 0) {
|
||||
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
||||
RecordNoDepth(fb);
|
||||
}
|
||||
}
|
||||
|
||||
void EnsureReadBuffer(ScratchFramebuffer& fb, GLenum readBuffer) {
|
||||
if (fb.readBuffer == readBuffer) {
|
||||
return;
|
||||
}
|
||||
g_GLESFuncs.glReadBuffer(readBuffer);
|
||||
fb.readBuffer = readBuffer;
|
||||
}
|
||||
|
||||
void EnsureDrawBuffer(ScratchFramebuffer& fb, GLenum drawBuffer) {
|
||||
if (fb.drawBuffer == drawBuffer) {
|
||||
return;
|
||||
}
|
||||
g_GLESFuncs.glDrawBuffers(1, &drawBuffer);
|
||||
fb.drawBuffer = drawBuffer;
|
||||
}
|
||||
|
||||
Uint EnsureCompleteTinyFramebufferId() {
|
||||
if (g_completeTinyFBOId != 0) {
|
||||
return g_completeTinyFBOId;
|
||||
}
|
||||
// One-time creation: the renderbuffer binding is context state with no
|
||||
// shadow, so save/restore it by query here (cold path only).
|
||||
GLint prevRenderbuffer = 0;
|
||||
g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRenderbuffer);
|
||||
g_GLESFuncs.glGenFramebuffers(1, &g_completeTinyFBOId);
|
||||
g_GLESFuncs.glGenRenderbuffers(1, &g_completeTinyRBOId);
|
||||
FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, g_completeTinyFBOId);
|
||||
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, g_completeTinyRBOId);
|
||||
g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1);
|
||||
g_GLESFuncs.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
|
||||
g_completeTinyRBOId);
|
||||
const GLenum drawBuffer = GL_COLOR_ATTACHMENT0;
|
||||
g_GLESFuncs.glDrawBuffers(1, &drawBuffer);
|
||||
g_GLESFuncs.glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE,
|
||||
"Scratch 1x1 framebuffer is incomplete.");
|
||||
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<Uint>(prevRenderbuffer));
|
||||
return g_completeTinyFBOId;
|
||||
}
|
||||
|
||||
void NoteTextureIdDeleted(Uint textureId) {
|
||||
if (textureId == 0) {
|
||||
return;
|
||||
}
|
||||
for (ScratchFramebuffer* fb : {&g_tempFramebuffer, &g_blitReadFramebuffer, &g_blitDrawFramebuffer}) {
|
||||
if (fb->colorTex == textureId || fb->depthTex == textureId) {
|
||||
fb->attachmentsKnown = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnBackendContextDestroyed() {
|
||||
g_tempFramebuffer = {};
|
||||
g_blitReadFramebuffer = {};
|
||||
g_blitDrawFramebuffer = {};
|
||||
g_completeTinyFBOId = 0;
|
||||
g_completeTinyRBOId = 0;
|
||||
}
|
||||
} // namespace ScratchFBOImpl
|
||||
|
||||
namespace PixelStoreImpl {
|
||||
namespace {
|
||||
PackState g_packState;
|
||||
Bool g_packStateKnown = false;
|
||||
|
||||
void PinPackState(const PackState& value) {
|
||||
g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, value.Alignment);
|
||||
g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, value.RowLength);
|
||||
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, value.SkipRows);
|
||||
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, value.SkipPixels);
|
||||
g_packState = value;
|
||||
g_packStateKnown = true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void ApplyPackState(const PackState& desired) {
|
||||
if (!g_packStateKnown) {
|
||||
PinPackState(desired);
|
||||
return;
|
||||
}
|
||||
if (desired.Alignment != g_packState.Alignment) {
|
||||
g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, desired.Alignment);
|
||||
g_packState.Alignment = desired.Alignment;
|
||||
}
|
||||
if (desired.RowLength != g_packState.RowLength) {
|
||||
g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, desired.RowLength);
|
||||
g_packState.RowLength = desired.RowLength;
|
||||
}
|
||||
if (desired.SkipRows != g_packState.SkipRows) {
|
||||
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, desired.SkipRows);
|
||||
g_packState.SkipRows = desired.SkipRows;
|
||||
}
|
||||
if (desired.SkipPixels != g_packState.SkipPixels) {
|
||||
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, desired.SkipPixels);
|
||||
g_packState.SkipPixels = desired.SkipPixels;
|
||||
}
|
||||
}
|
||||
|
||||
PackState CurrentPackState() {
|
||||
if (!g_packStateKnown) {
|
||||
// Fresh/unknown context: pin to the GL defaults (what a new context
|
||||
// starts with; writing them makes the shadow authoritative either way).
|
||||
PinPackState(PackState{});
|
||||
}
|
||||
return g_packState;
|
||||
}
|
||||
|
||||
void InvalidatePackStateCache() {
|
||||
g_packStateKnown = false;
|
||||
}
|
||||
} // namespace PixelStoreImpl
|
||||
|
||||
namespace PrgramImpl {
|
||||
Uint32 g_snormFallbackClampOutputMask = 0;
|
||||
Uint32 g_unormFallbackClampOutputMask = 0;
|
||||
|
||||
@@ -178,6 +178,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// glBindBuffer with a redundant-bind cache for GL_ARRAY_BUFFER.
|
||||
void BindBufferId(GLenum target, Uint id);
|
||||
void InvalidateArrayBufferBindingCache();
|
||||
// Redundant-bind caches for the driver-level GL_PIXEL_PACK/UNPACK_BUFFER
|
||||
// bindings. Every backend readback (glReadPixels / pack-PBO map) and pixel
|
||||
// upload site routes its binding through these so the shadow always matches
|
||||
// the driver; the resting state between operations is 0, which keeps any
|
||||
// path that implicitly assumes "no PBO bound" correct. Scrubbed when a
|
||||
// buffer id is deleted/pooled (GL resets a deleted buffer's bindings to 0,
|
||||
// and a recycled name matching the shadow would false-skip the rebind) and
|
||||
// invalidated on MakeCurrent (context may reset).
|
||||
void BindPixelPackBufferId(Uint id);
|
||||
void BindPixelUnpackBufferId(Uint id);
|
||||
void InvalidatePixelBufferBindingCaches();
|
||||
// Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on
|
||||
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the
|
||||
// (id, range) already at that index matches, like the array-buffer/texture/
|
||||
@@ -332,6 +343,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
class BackendTextureObject {
|
||||
public:
|
||||
BackendTextureObject();
|
||||
// Deletes the GL texture (frontend glDeleteTextures used to leak every
|
||||
// backend id for the context lifetime) and scrubs the binding/scratch-FBO
|
||||
// shadows so a recycled name or heap address cannot false-skip a rebind.
|
||||
~BackendTextureObject();
|
||||
BackendTextureObject(const BackendTextureObject&) = delete;
|
||||
BackendTextureObject& operator=(const BackendTextureObject&) = delete;
|
||||
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
@@ -343,6 +360,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void RecreateBackendTexture();
|
||||
|
||||
Uint m_backendTextureId = 0;
|
||||
// ES context generation the id was created under; a dtor running after
|
||||
// that context died must not delete a foreign (recycled) name.
|
||||
Uint m_contextGeneration = 0;
|
||||
Bool m_isInitialized = false;
|
||||
Bool m_imageBindableStorageRequired = false;
|
||||
Bool m_backendStorageImmutable = false;
|
||||
@@ -367,6 +387,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
||||
g_boundTexturesCache;
|
||||
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 FramebufferImpl {
|
||||
@@ -418,8 +441,99 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedObjectVersions;
|
||||
extern Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
|
||||
g_fboSyncedObjects;
|
||||
|
||||
// Driver-level READ/DRAW framebuffer-binding shadow. Every backend
|
||||
// glBindFramebuffer routes through BindFramebufferId so scoped helpers can
|
||||
// save/restore the current binding without a glGetIntegerv round-trip (that
|
||||
// query forces a driver pipeline sync) and so redundant rebinds no-op.
|
||||
// Starts unknown; the first CurrentFramebufferBinding() query pins it from
|
||||
// the driver once. Invalidated on MakeCurrent (context may reset).
|
||||
// GL_FRAMEBUFFER binds both targets.
|
||||
void BindFramebufferId(GLenum fbTarget, Uint id);
|
||||
Uint CurrentFramebufferBinding(FramebufferTarget target);
|
||||
void InvalidateFramebufferBindingCache();
|
||||
} // namespace FramebufferImpl
|
||||
|
||||
// Shared scratch framebuffers for the readback/copy/blit emulation paths, with a
|
||||
// driver-side attachment shadow: repeated uses skip redundant detach/attach GL
|
||||
// calls, and an attachment left by one use (e.g. a depth copy's DEPTH_STENCIL
|
||||
// texture) is detached exactly when a later use of another aspect would
|
||||
// otherwise inherit it (stale cross-aspect attachments made the shared temp FBO
|
||||
// incomplete and silently degraded later readbacks).
|
||||
namespace ScratchFBOImpl {
|
||||
struct ScratchFramebuffer {
|
||||
Uint id = 0;
|
||||
// false => attachment state unknown; scrub every point on next use.
|
||||
// A fresh FBO starts with nothing attached, so creation sets it true.
|
||||
Bool attachmentsKnown = false;
|
||||
Uint colorTex = 0;
|
||||
GLenum colorTarget = 0;
|
||||
GLint colorLevel = 0;
|
||||
GLint colorLayer = -1; // >= 0 => attached via glFramebufferTextureLayer
|
||||
Uint depthTex = 0;
|
||||
GLenum depthTarget = 0;
|
||||
GLint depthLevel = 0;
|
||||
Bool depthHasStencil = false;
|
||||
// Per-FBO read/draw buffer state (0 = unknown, set on first use).
|
||||
GLenum readBuffer = 0;
|
||||
GLenum drawBuffer = 0;
|
||||
};
|
||||
ScratchFramebuffer& TempFramebuffer(); // GetTexImage READ / CopyTex*Image2D depth DRAW
|
||||
ScratchFramebuffer& BlitReadFramebuffer(); // texture-to-texture blit source
|
||||
ScratchFramebuffer& BlitDrawFramebuffer(); // texture-to-texture blit destination
|
||||
// Returns the GL id, generating it if needed (requires a current ES context).
|
||||
Uint EnsureId(ScratchFramebuffer& fb);
|
||||
// The fb must currently be bound at fbTarget (glReadBuffer/glDrawBuffers
|
||||
// target the READ/DRAW binding respectively). Each Ensure* performs the
|
||||
// minimal detach/attach set and keeps the shadow in sync; a failed attach
|
||||
// records the point as detached so the completeness check fails instead of
|
||||
// silently reading a stale attachment.
|
||||
void EnsureColorAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level);
|
||||
void EnsureColorAttachmentLayer(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLint level, GLint layer);
|
||||
void EnsureDepthAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level,
|
||||
Bool withStencil);
|
||||
void EnsureNoColorAttachment(ScratchFramebuffer& fb, GLenum fbTarget);
|
||||
void EnsureNoDepthAttachment(ScratchFramebuffer& fb, GLenum fbTarget);
|
||||
void EnsureReadBuffer(ScratchFramebuffer& fb, GLenum readBuffer);
|
||||
void EnsureDrawBuffer(ScratchFramebuffer& fb, GLenum drawBuffer);
|
||||
// A 1x1 RGBA8-renderbuffer-complete FBO (GenerateMipmap needs a complete
|
||||
// binding while respecifying texture storage). Attachment is set once at
|
||||
// creation and never changes.
|
||||
Uint EnsureCompleteTinyFramebufferId();
|
||||
// A backend texture id is being deleted or respecified: a scratch FBO still
|
||||
// referencing it would hold a dangling attachment (ES only auto-detaches
|
||||
// from the *bound* framebuffer), and a recycled name could false-skip a
|
||||
// re-attach; force a full scrub on next use.
|
||||
void NoteTextureIdDeleted(Uint textureId);
|
||||
// The ES context (and the scratch FBO ids with it) is going away.
|
||||
void OnBackendContextDestroyed();
|
||||
} // namespace ScratchFBOImpl
|
||||
|
||||
// Driver-level GL_PACK_* pixel-store shadow, the readback-side sibling of the
|
||||
// upload path's ScopedDefaultUnpackState (Managers.cpp): the backend PACK state
|
||||
// is written ONLY through ApplyPackState, so scoped helpers can save/restore it
|
||||
// from the shadow instead of glGetIntegerv (which forces a driver pipeline
|
||||
// sync), and redundant glPixelStorei calls no-op. The first Apply/Current call
|
||||
// pins the driver to the shadow by writing all fields once. Invalidated on
|
||||
// MakeCurrent (context may reset). PACK_IMAGE_HEIGHT/SKIP_IMAGES/SWAP_BYTES/
|
||||
// LSB_FIRST have no ES equivalents; readbacks honor them on the CPU from the
|
||||
// frontend context state instead.
|
||||
namespace PixelStoreImpl {
|
||||
struct PackState {
|
||||
GLint Alignment = 4;
|
||||
GLint RowLength = 0;
|
||||
GLint SkipRows = 0;
|
||||
GLint SkipPixels = 0;
|
||||
Bool operator==(const PackState& o) const {
|
||||
return Alignment == o.Alignment && RowLength == o.RowLength && SkipRows == o.SkipRows &&
|
||||
SkipPixels == o.SkipPixels;
|
||||
}
|
||||
};
|
||||
void ApplyPackState(const PackState& desired);
|
||||
PackState CurrentPackState();
|
||||
void InvalidatePackStateCache();
|
||||
} // namespace PixelStoreImpl
|
||||
|
||||
// Image uniforms take their unit from the layout(binding=N) qualifier baked into
|
||||
// the transpiled ESSL; unlike samplers they must not (and in ES cannot) be
|
||||
// assigned through glUniform1i.
|
||||
|
||||
@@ -1431,3 +1431,288 @@ TEST(RenderStateSanity, PrimitiveRestartIndexStoresAndReadsBack) {
|
||||
|
||||
MG_State::pGLContext.reset();
|
||||
}
|
||||
|
||||
|
||||
// ---- DirectGLES readback driver-state shadows ----------------------------------------------------
|
||||
// Regression coverage for the readback-path state-leak overhaul: the pixel-PBO
|
||||
// binding cache, the framebuffer-binding shadow, the PACK pixel-store shadow and
|
||||
// the scratch-FBO attachment shadow must (a) leave the driver in the documented
|
||||
// resting state, (b) skip redundant GL calls, and (c) scrub correctly on
|
||||
// deletion. All drive the real Managers.cpp implementations against a recording
|
||||
// mock GLES table.
|
||||
namespace {
|
||||
struct StateGuardCallLog {
|
||||
MobileGL::Vector<MobileGL::String> calls;
|
||||
|
||||
MobileGL::SizeT Count(const MobileGL::String& prefix) const {
|
||||
MobileGL::SizeT n = 0;
|
||||
for (const auto& c : calls) {
|
||||
if (c.compare(0, prefix.size(), prefix) == 0) ++n;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
};
|
||||
|
||||
StateGuardCallLog* g_stateGuardLog = nullptr;
|
||||
GLuint g_nextStateGuardFBOId = 201;
|
||||
|
||||
void SG_Log(MobileGL::String entry) {
|
||||
if (g_stateGuardLog) g_stateGuardLog->calls.push_back(MobileGL::Move(entry));
|
||||
}
|
||||
void SG_BindBuffer(GLenum target, GLuint buffer) {
|
||||
SG_Log("BindBuffer:" + std::to_string(target) + ":" + std::to_string(buffer));
|
||||
}
|
||||
void SG_BindFramebuffer(GLenum target, GLuint framebuffer) {
|
||||
SG_Log("BindFramebuffer:" + std::to_string(target) + ":" + std::to_string(framebuffer));
|
||||
}
|
||||
void SG_GetIntegerv(GLenum pname, GLint* data) {
|
||||
SG_Log("GetIntegerv:" + std::to_string(pname));
|
||||
if (data) *data = 0;
|
||||
}
|
||||
void SG_PixelStorei(GLenum pname, GLint param) {
|
||||
SG_Log("PixelStorei:" + std::to_string(pname) + ":" + std::to_string(param));
|
||||
}
|
||||
void SG_GenFramebuffers(GLsizei count, GLuint* framebuffers) {
|
||||
for (GLsizei i = 0; i < count; ++i) framebuffers[i] = g_nextStateGuardFBOId++;
|
||||
}
|
||||
void SG_FramebufferTexture2D(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) {
|
||||
SG_Log("FramebufferTexture2D:" + std::to_string(target) + ":" + std::to_string(attachment) + ":" +
|
||||
std::to_string(textarget) + ":" + std::to_string(texture) + ":" + std::to_string(level));
|
||||
}
|
||||
void SG_FramebufferTextureLayer(GLenum target, GLenum attachment, GLuint texture, GLint level, GLint layer) {
|
||||
SG_Log("FramebufferTextureLayer:" + std::to_string(target) + ":" + std::to_string(attachment) + ":" +
|
||||
std::to_string(texture) + ":" + std::to_string(level) + ":" + std::to_string(layer));
|
||||
}
|
||||
void SG_ReadBuffer(GLenum src) {
|
||||
SG_Log("ReadBuffer:" + std::to_string(src));
|
||||
}
|
||||
void SG_DrawBuffers(GLsizei n, const GLenum* bufs) {
|
||||
SG_Log("DrawBuffers:" + std::to_string(n) + ":" + std::to_string(n > 0 && bufs ? bufs[0] : 0));
|
||||
}
|
||||
GLenum SG_NoError() {
|
||||
return GL_NO_ERROR;
|
||||
}
|
||||
|
||||
// Installs the recording table and resets every readback driver-state shadow on
|
||||
// both ends, so these tests cannot bleed into (or inherit from) other tests.
|
||||
struct ScopedStateGuardMocks {
|
||||
ScopedStateGuardMocks(): previousFunctions(MobileGL::MG_Backend::DirectGLES::g_GLESFuncs) {
|
||||
ResetShadows();
|
||||
MobileGL::MG_External::GLESFunctionsTable functions{};
|
||||
functions.glBindBuffer = SG_BindBuffer;
|
||||
functions.glBindFramebuffer = SG_BindFramebuffer;
|
||||
functions.glGetIntegerv = SG_GetIntegerv;
|
||||
functions.glPixelStorei = SG_PixelStorei;
|
||||
functions.glGenFramebuffers = SG_GenFramebuffers;
|
||||
functions.glFramebufferTexture2D = SG_FramebufferTexture2D;
|
||||
functions.glFramebufferTextureLayer = SG_FramebufferTextureLayer;
|
||||
functions.glReadBuffer = SG_ReadBuffer;
|
||||
functions.glDrawBuffers = SG_DrawBuffers;
|
||||
functions.glGetError = SG_NoError;
|
||||
MobileGL::MG_Backend::DirectGLES::SetGLESFuncsTable(functions);
|
||||
g_stateGuardLog = &log;
|
||||
}
|
||||
|
||||
~ScopedStateGuardMocks() {
|
||||
g_stateGuardLog = nullptr;
|
||||
MobileGL::MG_Backend::DirectGLES::SetGLESFuncsTable(previousFunctions);
|
||||
ResetShadows();
|
||||
}
|
||||
|
||||
ScopedStateGuardMocks(const ScopedStateGuardMocks&) = delete;
|
||||
ScopedStateGuardMocks& operator=(const ScopedStateGuardMocks&) = delete;
|
||||
|
||||
static void ResetShadows() {
|
||||
MobileGL::MG_Backend::DirectGLES::BufferImpl::InvalidatePixelBufferBindingCaches();
|
||||
MobileGL::MG_Backend::DirectGLES::FramebufferImpl::InvalidateFramebufferBindingCache();
|
||||
MobileGL::MG_Backend::DirectGLES::PixelStoreImpl::InvalidatePackStateCache();
|
||||
MobileGL::MG_Backend::DirectGLES::ScratchFBOImpl::OnBackendContextDestroyed();
|
||||
}
|
||||
|
||||
StateGuardCallLog log;
|
||||
MobileGL::MG_External::GLESFunctionsTable previousFunctions;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST(DirectGLESStateGuards, PixelPackBindingCacheSkipsRedundantBindsAndRestsAtZero) {
|
||||
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||
ScopedStateGuardMocks mocks;
|
||||
|
||||
BufferImpl::BindPixelPackBufferId(5);
|
||||
EXPECT_EQ(mocks.log.Count("BindBuffer:"), 1u);
|
||||
BufferImpl::BindPixelPackBufferId(5); // redundant: must not reach the driver
|
||||
EXPECT_EQ(mocks.log.Count("BindBuffer:"), 1u);
|
||||
BufferImpl::BindPixelPackBufferId(0); // scope exit: resting state
|
||||
EXPECT_EQ(mocks.log.Count("BindBuffer:"), 2u);
|
||||
BufferImpl::BindPixelPackBufferId(0);
|
||||
EXPECT_EQ(mocks.log.Count("BindBuffer:"), 2u);
|
||||
|
||||
// After invalidation (MakeCurrent / context reset) the first bind must reach
|
||||
// the driver again even for the same value.
|
||||
BufferImpl::InvalidatePixelBufferBindingCaches();
|
||||
BufferImpl::BindPixelPackBufferId(0);
|
||||
EXPECT_EQ(mocks.log.Count("BindBuffer:"), 3u);
|
||||
}
|
||||
|
||||
TEST(DirectGLESStateGuards, FramebufferBindingShadowPinsOnceThenSkips) {
|
||||
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||
ScopedStateGuardMocks mocks;
|
||||
|
||||
// Cold path: one driver query pins the shadow; further reads are free.
|
||||
(void)FramebufferImpl::CurrentFramebufferBinding(MobileGL::FramebufferTarget::Read);
|
||||
EXPECT_EQ(mocks.log.Count("GetIntegerv:"), 1u);
|
||||
(void)FramebufferImpl::CurrentFramebufferBinding(MobileGL::FramebufferTarget::Read);
|
||||
EXPECT_EQ(mocks.log.Count("GetIntegerv:"), 1u);
|
||||
|
||||
FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, 7);
|
||||
EXPECT_EQ(mocks.log.Count("BindFramebuffer:"), 1u);
|
||||
FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, 7);
|
||||
EXPECT_EQ(mocks.log.Count("BindFramebuffer:"), 1u);
|
||||
// GL_FRAMEBUFFER touches both targets; DRAW is still unknown so it must bind.
|
||||
FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, 7);
|
||||
EXPECT_EQ(mocks.log.Count("BindFramebuffer:"), 2u);
|
||||
// Both halves now match: no further calls for either single target.
|
||||
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, 7);
|
||||
FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, 7);
|
||||
FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, 7);
|
||||
EXPECT_EQ(mocks.log.Count("BindFramebuffer:"), 2u);
|
||||
EXPECT_EQ(FramebufferImpl::CurrentFramebufferBinding(MobileGL::FramebufferTarget::Draw), 7u);
|
||||
EXPECT_EQ(mocks.log.Count("GetIntegerv:"), 1u); // shadow answered, no new query
|
||||
}
|
||||
|
||||
TEST(DirectGLESStateGuards, PackStateShadowAppliesMinimalDeltas) {
|
||||
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||
ScopedStateGuardMocks mocks;
|
||||
|
||||
// First application pins all four parameters.
|
||||
PixelStoreImpl::ApplyPackState(PixelStoreImpl::PackState{4, 0, 0, 0});
|
||||
EXPECT_EQ(mocks.log.Count("PixelStorei:"), 4u);
|
||||
// Identical state: zero driver calls.
|
||||
PixelStoreImpl::ApplyPackState(PixelStoreImpl::PackState{4, 0, 0, 0});
|
||||
EXPECT_EQ(mocks.log.Count("PixelStorei:"), 4u);
|
||||
// One field changed: exactly one driver call.
|
||||
PixelStoreImpl::ApplyPackState(PixelStoreImpl::PackState{1, 0, 0, 0});
|
||||
EXPECT_EQ(mocks.log.Count("PixelStorei:"), 5u);
|
||||
|
||||
const auto current = PixelStoreImpl::CurrentPackState();
|
||||
EXPECT_EQ(current.Alignment, 1);
|
||||
EXPECT_EQ(current.RowLength, 0);
|
||||
EXPECT_EQ(current.SkipRows, 0);
|
||||
EXPECT_EQ(current.SkipPixels, 0);
|
||||
}
|
||||
|
||||
TEST(DirectGLESStateGuards, ScratchFBODetachesCrossAspectResidue) {
|
||||
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||
ScopedStateGuardMocks mocks;
|
||||
|
||||
auto& fb = ScratchFBOImpl::TempFramebuffer();
|
||||
EXPECT_NE(ScratchFBOImpl::EnsureId(fb), 0u);
|
||||
|
||||
// A depth copy leaves a DEPTH_STENCIL attachment (the pre-fix code never
|
||||
// detached it, wedging every later color readback through this FBO).
|
||||
ScratchFBOImpl::EnsureDepthAttachment2D(fb, GL_DRAW_FRAMEBUFFER, 11, GL_TEXTURE_2D, 0, /*withStencil=*/true);
|
||||
const MobileGL::String dsAttach = "FramebufferTexture2D:" + std::to_string(GL_DRAW_FRAMEBUFFER) + ":" +
|
||||
std::to_string(GL_DEPTH_STENCIL_ATTACHMENT);
|
||||
EXPECT_EQ(mocks.log.Count(dsAttach), 1u);
|
||||
|
||||
// The next color use must detach the stale depth-stencil attachment exactly once.
|
||||
mocks.log.calls.clear();
|
||||
ScratchFBOImpl::EnsureColorAttachment2D(fb, GL_READ_FRAMEBUFFER, 22, GL_TEXTURE_2D, 0);
|
||||
const MobileGL::String dsDetach = "FramebufferTexture2D:" + std::to_string(GL_READ_FRAMEBUFFER) + ":" +
|
||||
std::to_string(GL_DEPTH_STENCIL_ATTACHMENT) + ":" +
|
||||
std::to_string(GL_TEXTURE_2D) + ":0:0";
|
||||
const MobileGL::String colorAttach = "FramebufferTexture2D:" + std::to_string(GL_READ_FRAMEBUFFER) + ":" +
|
||||
std::to_string(GL_COLOR_ATTACHMENT0) + ":" +
|
||||
std::to_string(GL_TEXTURE_2D) + ":22:0";
|
||||
EXPECT_EQ(mocks.log.Count(dsDetach), 1u);
|
||||
EXPECT_EQ(mocks.log.Count(colorAttach), 1u);
|
||||
|
||||
// Back-to-back identical color use: no driver traffic at all.
|
||||
mocks.log.calls.clear();
|
||||
ScratchFBOImpl::EnsureColorAttachment2D(fb, GL_READ_FRAMEBUFFER, 22, GL_TEXTURE_2D, 0);
|
||||
EXPECT_EQ(mocks.log.Count("FramebufferTexture2D:"), 0u);
|
||||
}
|
||||
|
||||
TEST(DirectGLESStateGuards, ScratchFBOTextureDeletionForcesFullScrub) {
|
||||
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||
ScopedStateGuardMocks mocks;
|
||||
|
||||
auto& fb = ScratchFBOImpl::TempFramebuffer();
|
||||
ScratchFBOImpl::EnsureId(fb);
|
||||
ScratchFBOImpl::EnsureColorAttachment2D(fb, GL_READ_FRAMEBUFFER, 22, GL_TEXTURE_2D, 0);
|
||||
|
||||
// The attached texture id dies: the shadow can no longer vouch for the FBO
|
||||
// (ES does not auto-detach from unbound FBOs, and the name may be recycled),
|
||||
// so the next use must scrub and re-attach instead of skipping.
|
||||
ScratchFBOImpl::NoteTextureIdDeleted(22);
|
||||
mocks.log.calls.clear();
|
||||
ScratchFBOImpl::EnsureColorAttachment2D(fb, GL_READ_FRAMEBUFFER, 22, GL_TEXTURE_2D, 0);
|
||||
EXPECT_GE(mocks.log.Count("FramebufferTexture2D:"), 2u); // scrub (color + depth) ...
|
||||
const MobileGL::String colorAttach = "FramebufferTexture2D:" + std::to_string(GL_READ_FRAMEBUFFER) + ":" +
|
||||
std::to_string(GL_COLOR_ATTACHMENT0) + ":" +
|
||||
std::to_string(GL_TEXTURE_2D) + ":22:0";
|
||||
EXPECT_EQ(mocks.log.Count(colorAttach), 1u); // ... then the real re-attach
|
||||
}
|
||||
|
||||
TEST(DirectGLESStateGuards, ScratchFBOReadDrawBufferStateCached) {
|
||||
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||
ScopedStateGuardMocks mocks;
|
||||
|
||||
auto& fb = ScratchFBOImpl::BlitReadFramebuffer();
|
||||
ScratchFBOImpl::EnsureId(fb);
|
||||
|
||||
// Fresh FBOs default to COLOR_ATTACHMENT0 for both buffers: no call needed.
|
||||
ScratchFBOImpl::EnsureReadBuffer(fb, GL_COLOR_ATTACHMENT0);
|
||||
EXPECT_EQ(mocks.log.Count("ReadBuffer:"), 0u);
|
||||
// Depth blits want GL_NONE; the transition costs one call, repeats are free.
|
||||
ScratchFBOImpl::EnsureReadBuffer(fb, GL_NONE);
|
||||
ScratchFBOImpl::EnsureReadBuffer(fb, GL_NONE);
|
||||
EXPECT_EQ(mocks.log.Count("ReadBuffer:"), 1u);
|
||||
ScratchFBOImpl::EnsureDrawBuffer(fb, GL_NONE);
|
||||
ScratchFBOImpl::EnsureDrawBuffer(fb, GL_NONE);
|
||||
EXPECT_EQ(mocks.log.Count("DrawBuffers:"), 1u);
|
||||
}
|
||||
|
||||
namespace {
|
||||
MobileGL::Vector<GLuint>* g_deletedTextureIds = nullptr;
|
||||
|
||||
void SG_DeleteTextures(GLsizei count, const GLuint* textures) {
|
||||
if (!g_deletedTextureIds) return;
|
||||
for (GLsizei i = 0; i < count; ++i) g_deletedTextureIds->push_back(textures[i]);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(DirectGLESBackendTexture, DestructorDeletesIdAndScrubsBindingCache) {
|
||||
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||
ScopedDirectGLESTextureBindings scoped; // installs glGenTextures/glBindTexture mocks + resets caches
|
||||
MobileGL::Vector<GLuint> deleted;
|
||||
g_deletedTextureIds = &deleted;
|
||||
auto functions = g_GLESFuncs;
|
||||
functions.glDeleteTextures = SG_DeleteTextures;
|
||||
SetGLESFuncsTable(functions);
|
||||
|
||||
const auto texture2DSlot = static_cast<MobileGL::SizeT>(MobileGL::TextureTarget::Texture2D);
|
||||
GLuint id = 0;
|
||||
{
|
||||
auto backendTexture = MobileGL::MakeShared<TextureImpl::BackendTextureObject>();
|
||||
id = backendTexture->GetBackendTextureId();
|
||||
ASSERT_NE(id, 0u);
|
||||
backendTexture->Bind(GL_TEXTURE_2D, 0);
|
||||
ASSERT_EQ(TextureImpl::g_boundTexturesCache[0][texture2DSlot], backendTexture.get());
|
||||
}
|
||||
// Frontend glDeleteTextures used to leak the backend id forever and leave the
|
||||
// cache pointer dangling (heap-address reuse then false-skips a later Bind).
|
||||
ASSERT_EQ(deleted.size(), 1u);
|
||||
EXPECT_EQ(deleted[0], id);
|
||||
EXPECT_EQ(TextureImpl::g_boundTexturesCache[0][texture2DSlot], nullptr);
|
||||
|
||||
// A wrapper whose context died must NOT delete a foreign (recycled) name.
|
||||
{
|
||||
auto backendTexture = MobileGL::MakeShared<TextureImpl::BackendTextureObject>();
|
||||
++TextureImpl::g_textureContextGeneration;
|
||||
backendTexture.reset();
|
||||
--TextureImpl::g_textureContextGeneration; // restore for later tests
|
||||
EXPECT_EQ(deleted.size(), 1u);
|
||||
}
|
||||
g_deletedTextureIds = nullptr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user