diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 7c4226f2..04c30537 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -6667,6 +6667,19 @@ namespace MobileGL::MG_Backend::DirectGLES { if (needsRegeneration) { MGLOG_D("Texture state changed significantly or not initialized, regenerating texture with ID: %u", m_backendTextureId); + // A REDEFINITION IN PLACE TAKES THE SAME GENERATION A RE-MINT TAKES (P4a fable + // seam F-3, the pre-handle half). Mutable driver storage is redefined on the + // SAME id below, so unlike RecreateBackendTexture nothing moves the FBO twins' + // memo: the frontend framebuffer versions do not see a texture's respecify + // and the id did not change. An attached texture whose format moved to one + // with the same carrier (GL_SRGB8 -> GL_SRGB8_ALPHA8 on a driver that widens + // the first) therefore kept the framebuffer's alpha-widening mask, and every + // draw into it stayed masked. The first definition is not a redefinition and + // bumps nothing; a redefinition that went through RecreateBackendTexture above + // has already bumped. + if (m_isInitialized && !m_backendStorageImmutable) { + ++FramebufferImpl::g_attachmentBackendIdGeneration; + } // Regenerate all mipmap levels GLenum glInternalFormat, glType, glFormat; @@ -12726,6 +12739,17 @@ namespace MobileGL::MG_Backend::DirectGLES { } #endif + // A RE-STORAGE IS A REDEFINITION ON THE SAME DRIVER ID (P4a fable seam F-3, the + // pre-handle half): glRenderbufferStorage below re-allocates behind the name the + // framebuffer twins already attached, the frontend's renderbuffer setters bump no + // version and no framebuffer version sees them, so without this the FBO memo kept + // the widening masks of the storage the renderbuffer was attached with. Same + // generation a texture re-mint takes, for the same reason; the first allocation is + // not a redefinition. + if (m_isInitialized) { + ++FramebufferImpl::g_attachmentBackendIdGeneration; + } + Bind(); // Allocate storage diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index d9406051..cd8aaa1d 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -1978,6 +1978,14 @@ namespace MobileGL::MG_Backend::DirectGLES { // the driver FBO would keep the deleted texture name attached forever. The // SyncCurrentFBO gate compares this generation (below) to re-enter the sync, // and each twin re-arms its per-attachment memo on a mismatch (SyncToBackend). + // + // AND WHENEVER AN ATTACHABLE OBJECT'S DRIVER STORAGE IS REDEFINED IN PLACE (P4a fable + // seam F-3): a mutable texture regenerated on the same id, a renderbuffer re-storaged + // on the same id. The id did not move, but the four cross-object masks SyncToBackend + // computes from the attachment's format did, and nothing else the FBO memo reads sees + // a respecify of an attached object. So "did I change something under an attachment + // point that no frontend version can tell the framebuffer about" is what this counts, + // and the re-mint is one case of it. extern Uint64 g_attachmentBackendIdGeneration; // What g_attachmentBackendIdGeneration was when SyncCurrentFBO last stamped each // target; part of the synced tuple above. diff --git a/MobileGL/MG_IntegrationTest/Harness/P4aSeamPeek.cpp b/MobileGL/MG_IntegrationTest/Harness/P4aSeamPeek.cpp index 355ec646..88315ca7 100644 --- a/MobileGL/MG_IntegrationTest/Harness/P4aSeamPeek.cpp +++ b/MobileGL/MG_IntegrationTest/Harness/P4aSeamPeek.cpp @@ -40,6 +40,13 @@ namespace MGITest { return true; } + bool PeekEsprytFramebufferHandleArmIsLive(bool* outLive) { + if (outLive == nullptr) return false; + if (!EsprytIsRunning()) return false; + *outLive = MGB::FramebufferSubsystemEnabled(); + return true; + } + bool PeekPipeShaderImageWindow(PipeShaderImageWindowPeek* out) { if (out == nullptr) return false; const MGP::MGPipeApplierState& applier = MGP::MGPipeApplier(); @@ -92,6 +99,7 @@ namespace MGITest { } #else bool PeekEsprytSamplerHandleArmIsLive(bool*) { return false; } + bool PeekEsprytFramebufferHandleArmIsLive(bool*) { return false; } bool PeekPipeShaderImageWindow(PipeShaderImageWindowPeek*) { return false; } bool PeekEsprytUnitSampler(unsigned, unsigned, EsprytUnitSamplerPeek*) { return false; } #endif diff --git a/MobileGL/MG_IntegrationTest/Harness/P4aSeamPeek.h b/MobileGL/MG_IntegrationTest/Harness/P4aSeamPeek.h index 8895f1db..9c5e5b1f 100644 --- a/MobileGL/MG_IntegrationTest/Harness/P4aSeamPeek.h +++ b/MobileGL/MG_IntegrationTest/Harness/P4aSeamPeek.h @@ -36,6 +36,12 @@ namespace MGITest { // other. Written only on true. bool PeekEsprytSamplerHandleArmIsLive(bool* outLive); + // The same question for the FRAMEBUFFER family (bit 9): true when Espryt consumes + // set_framebuffer_state in this process. The renderbuffer half of the F-3 case asserts only + // there - on the pre-handle arm a renderbuffer re-storaged while attached moves nothing the + // FBO memo reads (D-D2's documented hole, pre-P4a code), and the record is what closes it. + bool PeekEsprytFramebufferHandleArmIsLive(bool* outLive); + // ---- the applier's shader-image window, as last received ------------------------------ // // MGPipeApplierState::ShaderImageStart / ShaderImageCount / ShaderImagesSerial. Count is diff --git a/MobileGL/MG_IntegrationTest/Scenarios/P4aSeamAuditScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/P4aSeamAuditScenario.cpp index f56d9403..bab3e57f 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/P4aSeamAuditScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/P4aSeamAuditScenario.cpp @@ -18,9 +18,18 @@ // // F-3 set_framebuffer_state INLINES an attachment's format (D-C1) and a storage redefinition of // an ATTACHED texture or renderbuffer moved nothing bit 11 read: Espryt's handle arm then -// answered its alpha-widening / snorm-clamp / integer masks from the stale copy while the -// legacy arm re-read the frontend. Three cases, both directions, texture and renderbuffer. -// DirectGLES only: the masks are Espryt's substitution machinery. +// answered its alpha-widening / snorm-clamp / integer masks from the stale copy. AND THE +// PRE-HANDLE ARM WAS NOT FRESH EITHER, which these cases found on the 0x1ff / 0 / pull +// lanes: a redefinition that keeps the driver id (mutable texture storage regenerated in +// place, a renderbuffer re-storaged in place) moves neither the framebuffer's frontend +// versions nor the backend-id generation the FBO memo reads, so SyncToBackend never +// re-ran and the masks stayed on both arms. The texture half is fixed on both arms (an +// in-place regeneration now takes the same generation a re-mint takes); the renderbuffer +// half only on the handle arm, where the resource record carries the re-storage - on the +// pre-handle arm a renderbuffer's twin is only ever reached from inside the FBO walk the +// memo skips (D-D2's documented hole, pre-P4a code), so that case asserts on the handle +// arm and declines by name elsewhere. Three cases, both directions, texture and +// renderbuffer. DirectGLES only: the masks are Espryt's substitution machinery. // F-1 set_sampler_views is resolved for the PROGRAM IN USE and bit 12's shutter read no program // input, so a glUseProgram alone never re-emitted it; E's record epoch (the two set serials) // then kept the program-independent texture sync list from ever rebuilding, and a texture @@ -273,7 +282,9 @@ void main() { imageStore(i1, 0, imageLoad(i0, 0) + uvec4(2u, 0u, 0u, 0u)); } // alpha masked off so the stored alpha stays at the 1.0 a three-channel format implies. // Redefine the same attached texture as GL_SRGB8_ALPHA8 and the application owns alpha // again - the mask must clear. On the tree the audit read the record still said SRGB8, the - // handle arm kept masking, and the 0.25 this case draws never reached the storage. + // handle arm kept masking, and the 0.25 this case draws never reached the storage; on the + // pre-handle arm the twin regenerated the (mutable) storage on the same driver id, nothing + // the FBO memo reads moved, and the masks stayed the same way. TEST_F(P4aSeamAuditScenario, ATextureRespecifiedWhileAttachedReachesTheFramebufferRecord) { if (!Ready()) return; @@ -327,8 +338,8 @@ void main() { imageStore(i1, 0, imageLoad(i0, 0) + uvec4(2u, 0u, 0u, 0u)); } EXPECT_NEAR(pixel[1], 1.0f, 0.05f) << "the draw did not land at all"; EXPECT_NEAR(pixel[3], 0.25f, 0.02f) << "the draw's alpha never reached a four-channel attachment: the framebuffer record " - "still describes the three-channel storage the texture was attached with, so the " - "handle arm kept masking alpha off (F-3)"; + "(handle arm) or the FBO twin's memo (pre-handle arm) still describes the " + "three-channel storage the texture was attached with, so alpha stayed masked off (F-3)"; glBindFramebuffer(GL_FRAMEBUFFER, 0); glDeleteFramebuffers(1, &fbo); @@ -386,9 +397,27 @@ void main() { imageStore(i1, 0, imageLoad(i0, 0) + uvec4(2u, 0u, 0u, 0u)); } DrawQuad(); ReadPixelFloat(kSize / 2, kSize / 2, pixel); EXPECT_NEAR(pixel[1], 1.0f, 0.05f) << "the draw did not land at all"; - EXPECT_NEAR(pixel[3], 0.25f, 0.02f) - << "the draw's alpha never reached the four-channel renderbuffer: the framebuffer " - "record still describes the storage it was attached with (F-3)"; + // THE HANDLE ARM ONLY. On the pre-handle arm a renderbuffer's twin is reached only from + // inside the FBO walk, and nothing that walk's memo reads moves on glRenderbufferStorage + // - the frontend setters bump no version (D-D2), no framebuffer version sees them, and + // the twin that would bump the backend generation is exactly what the memo skips. That + // is pre-P4a code and D-D2's documented hole; the resource record is what closes it, + // so the verdict is taken where the record is consumed and declined by name elsewhere + // (measured: alpha 1.0 on the pull build and at 0x1ff / 0, the mask of the storage the + // renderbuffer was attached with). + bool framebufferArmLive = false; + if (PeekEsprytFramebufferHandleArmIsLive(&framebufferArmLive) && framebufferArmLive) { + EXPECT_NEAR(pixel[3], 0.25f, 0.02f) + << "the draw's alpha never reached the four-channel renderbuffer: the framebuffer " + "record still describes the storage it was attached with (F-3)"; + } else { + std::cout << "[ P4aSeamAudit ] renderbuffer re-storage verdict DECLINED on the pre-handle arm " + "(D-D2's documented hole: no frontend version and no backend generation moves on a " + "renderbuffer re-storage until the FBO walk the memo skips); alpha read " + << pixel[3] << std::endl; + RecordProperty("p4a_seam_white_box", "declined"); + RecordProperty("p4a_seam_white_box_reason", "renderbuffer re-storage: pre-handle arm (D-D2)"); + } glBindFramebuffer(GL_FRAMEBUFFER, 0); glDeleteFramebuffers(1, &fbo); @@ -461,8 +490,9 @@ void main() { imageStore(i1, 0, imageLoad(i0, 0) + uvec4(2u, 0u, 0u, 0u)); } ReadPixelFloat(kSize / 2, kSize / 2, pixel); EXPECT_NEAR(pixel[0], 1.0f, 0.05f) << "GL_DST_ALPHA read the stored alpha of a three-channel attachment and it was not " - "1.0: the framebuffer record still describes the four-channel storage the texture " - "was attached with, so the handle arm let the draw write alpha (F-3, mirror)"; + "1.0: the framebuffer record (handle arm) or the FBO twin's memo (pre-handle arm) " + "still describes the four-channel storage the texture was attached with, so the " + "draw was let write alpha (F-3, mirror)"; glBindFramebuffer(GL_FRAMEBUFFER, 0); glDeleteFramebuffers(1, &fbo);