From 6662176733209461fda93ebce70c75c664245a4a Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 18 Sep 2026 09:48:24 -0400 Subject: [PATCH] [MG_Backend, MG_Test] (Disaggregated): P5e fix1 - the by-handle attachment sync must not run on the monolith arm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FramebufferImpl::SyncAttachmentSurface selects its STORAGE sync by MG_Config::Transport (ID-81 / CONTRACT-P5E §5.8) instead of routing both arms through tx2's by-handle seam. Under a transport it is unchanged (SyncMipmapsToBackendByHandle / RBO SyncToBackendByHandle); under Transport == Monolith it is the frontend sync it was at f6cfcbd3, driven by a monolith-glue attachment pointer the object-form caller passes and the handle-form caller leaves null. - The crash this repairs: the push-monolith build drives its framebuffer attachments from the record (P4a D-C2, gated on FramebufferSubsystemEnabled() alone), so glClear on an FBO holding a texture reached SyncMipmapsToBackendByHandle, which passes a NULL SharedPtr and relies on a record arm that `Transport != Monolith` selects. stateTextureObject->IsTextureView() then dereferenced null - the device SIGSEGV in Lightmap. -> clearColorTexture -> glClear. - The three texture sync bodies stop treating "a handle was noted" as "the record arm will be selected": RefuseNullFrontendTextureOffTheHandleArm is one shared check and a null frontend object on an arm that cannot serve it is Fatal{RoleViolation, "texture-handle-arm"} instead of a null dereference. It folds to the same constant in the pull build, so no call is added there. - The renderbuffer arm is arm-selected for the same reason: its by-handle form dereferences nothing but drops the application-visible GL_OUT_OF_MEMORY RecordError, which the push-monolith build still has an application to report to. - New lane entries (MonolithAttachmentClearScenario, three cases: a mutable texture attachment, an immutable one and a renderbuffer) and a DirectGLES.PushMonolithArm. registration that pins MOBILEGL_TRANSPORT=monolith and carries the integration-split label, so the gate covers BOTH runtime arms of the push build instead of only the inproc one (ID-107's coverage hole). - Gate: build rc=0, unit 2250/2250, integration-split 116/116 (113 + 3), integration-gpu 1294/1294 (137 DirectGLES SEGFAULTs before this commit), gens all rc=0. --- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 147 ++++++++++++-- MobileGL/MG_IntegrationTest/CMakeLists.txt | 40 ++++ .../MonolithAttachmentClearScenario.cpp | 180 ++++++++++++++++++ 3 files changed, 356 insertions(+), 11 deletions(-) create mode 100644 MobileGL/MG_IntegrationTest/Scenarios/MonolithAttachmentClearScenario.cpp diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 57d2f443..8aca1edc 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -6968,6 +6968,52 @@ namespace MobileGL::MG_Backend::DirectGLES { // pull build has no such member at all and this has to fold to a constant there (D-P: the pull // build's preprocessed text may not gain a call). #define MGB_TEXTURE_HANDLE_ARM_OFF(twin) (MG_Pipe::MGPipeHandleIsNull((twin).PushedSyncHandle())) + + // P5e (fix1, ID-81 / CONTRACT-P5E §5.8): THE NULL FRONTEND OBJECT IS A CONTRACT AND THIS + // IS WHERE IT IS CHECKED, ONCE, FOR ALL THREE SYNC BODIES. + // + // tx2's three sync bodies accept a null `stateTextureObject` because the RECORD answers + // every read they make - but only where the record arm is actually SELECTED. Two reads + // inside SyncMipmapsToBackend are gated on `Transport != Monolith` (the texture-VIEW test + // and RequireImageBindableStorage's re-dirty transition, §5.2), and every read in all + // three is gated on TextureResourceSubsystemEnabled() through `pushedStorage`. So a null + // object is servable exactly when BOTH hold, and the by-handle entries beside them + // (SyncTextureToBackendByHandle / SyncMipmapsToBackendByHandle) pass null on the strength + // of a NOTED HANDLE, which says nothing about either. + // + // The device found the gap: with MOBILEGL_TRANSPORT=monolith the push build still drives + // its framebuffer attachments from the record (P4a, D-C2), fb's SyncAttachmentSurface + // called the by-handle storage sync from that arm, and `stateTextureObject->IsTextureView()` + // dereferenced the null SharedPtr in Lightmap. -> clearColorTexture -> glClear. + // The arm selection is repaired at that call site; this refusal is what makes the shape + // UNREPEATABLE rather than merely repaired - a null frontend object reaching a body that + // can still read one aborts BY NAME instead of taking a SIGSEGV three frames deep. + // + // Returns true when the caller must decline (no handle noted: the caller's bug it always + // was, named by the caller's own log line), false when the record arm can serve the call, + // and never returns at all when a handle was noted on an arm that cannot serve it. + static Bool RefuseNullFrontendTextureOffTheHandleArm(const char* entry, + MG_Pipe::MGPipeHandle notedHandle) { + if (MG_Pipe::MGPipeHandleIsNull(notedHandle)) return true; +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith && + TextureResourceSubsystemEnabled()) { + return false; + } +#endif + MGLOG_F("MGPipe: Fatal{RoleViolation, \"texture-handle-arm\"} - %s was handed a NULL " + "frontend texture for handle {%u, %u} on an arm that cannot answer without " + "one. The by-handle entries pass null on the strength of a noted handle, and " + "the record arm they rely on is selected by Transport != Monolith AND the " + "texture-resource subsystem bit (CONTRACT-P5E §5.8, ID-81); neither holds " + "here, so the body below would read a frontend object that does not exist", + entry, notedHandle.Slot, notedHandle.Gen); + std::abort(); + } +// The guard the three sync bodies spell. In the PULL build it folds to the constant the +// pre-fix1 text folded to, so that build's preprocessed text gains no call (D-P). +#define MGB_TEXTURE_NULL_FRONTEND_REFUSED(twin, entry) \ + (RefuseNullFrontendTextureOffTheHandleArm((entry), (twin).PushedSyncHandle())) // P5e (tx2), CONTRACT-P5E §5.2: THE TEXTURE'S TARGET, from the descriptor whenever a record was // resolved. The two live `GetTarget()` reads left INSIDE the handle arm (scout G-S2-2, the // parameter sync and the built-in sampler sync) read Desc.Target here, through the same @@ -6984,6 +7030,7 @@ namespace MobileGL::MG_Backend::DirectGLES { ((rec) != nullptr ? static_cast((rec)->Desc.StorageKind) : (obj)->GetStorageType()) #else #define MGB_TEXTURE_HANDLE_ARM_OFF(twin) (true) +#define MGB_TEXTURE_NULL_FRONTEND_REFUSED(twin, entry) (true) #define MGB_TEXPARAM_TARGET(rec, obj) ((obj)->GetTarget()) #define MGB_TEXTURE_DIAG_NAME(rec, obj) ((obj)->GetExternalIndex()) #define MGB_TEXTURE_STORAGE_KIND(rec, obj) ((obj)->GetStorageType()) @@ -7351,8 +7398,12 @@ namespace MobileGL::MG_Backend::DirectGLES { void BackendTextureObject::SyncMipmapsToBackend( const SharedPtr& stateTextureObject) { // P5e (tx2): null IS the by-handle arm once a handle has been noted on this twin; - // see the twin note in SyncBuiltinSamplerToBackend. - if (!stateTextureObject && MGB_TEXTURE_HANDLE_ARM_OFF(*this)) { + // see the twin note in SyncBuiltinSamplerToBackend. P5e (fix1): "a handle was noted" + // is not on its own the statement that the record arm will be SELECTED - the view + // test below and RequireImageBindableStorage's transition are transport-gated - so + // the check is the one shared refusal and a noted handle on an arm that cannot serve + // it aborts by name rather than falling into the frontend reads. + if (!stateTextureObject && MGB_TEXTURE_NULL_FRONTEND_REFUSED(*this, "SyncMipmapsToBackend")) { MGLOG_E_ONCE("State texture object is null, cannot sync to backend."); return; } @@ -8939,8 +8990,10 @@ namespace MobileGL::MG_Backend::DirectGLES { // P5e (tx2): NULL IS THE BY-HANDLE ARM, not an error, once a handle has been noted on // this twin - the record and the built-in sampler's CSO answer everything below and // the frontend object is not consulted at all. Without a handle it is still the - // caller's bug it always was. - if (!stateTextureObject && MGB_TEXTURE_HANDLE_ARM_OFF(*this)) { + // caller's bug it always was. P5e (fix1): and with a handle noted on an arm that + // cannot serve it, it is Fatal{RoleViolation, "texture-handle-arm"} - see the + // refusal's own comment beside MGB_TEXTURE_HANDLE_ARM_OFF. + if (!stateTextureObject && MGB_TEXTURE_NULL_FRONTEND_REFUSED(*this, "SyncBuiltinSamplerToBackend")) { MGLOG_E_ONCE("State texture object is null, cannot sync to backend."); return; } @@ -9198,7 +9251,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // P5e (tx2): null IS the by-handle arm once a handle has been noted on this twin; // see the twin note in SyncBuiltinSamplerToBackend. - if (!stateTextureObject && MGB_TEXTURE_HANDLE_ARM_OFF(*this)) { + if (!stateTextureObject && MGB_TEXTURE_NULL_FRONTEND_REFUSED(*this, "SyncTextureParamsToBackend")) { MGLOG_E_ONCE("State texture object is null, cannot sync to backend."); return; } @@ -10012,8 +10065,27 @@ namespace MobileGL::MG_Backend::DirectGLES { // is deleted by the rekey rather than gated, which is what CONTRACT-P5E §4.4 asks for: // gating it would have left a client-allocator probe compiled into a path a run-ahead // apply reaches, and there is no longer anything for it to say. + // + // P5e (fix1, ID-81 / §5.8): THE STORAGE SYNC IS ARM-SELECTED AND THE MONOLITH ARM KEEPS + // ITS FRONTEND OBJECT. This function has TWO callers and they are on opposite arms: the + // handle form (SyncToBackendByHandle) has no frontend object and must not acquire one, + // while the OBJECT form (SyncToBackend, the push-monolith arm - P4a drives its + // attachments from the record too, gated on FramebufferSubsystemEnabled() alone, D-C2) + // is holding the frontend attachment the whole time. fb dropped the parameter for both, + // which routed the monolith arm into tx2's by-handle storage seam; that seam passes a + // NULL SharedPtr and relies on the record arm being selected, and under + // Transport == Monolith it is not - `stateTextureObject->IsTextureView()` then + // dereferenced null (the device's Lightmap. -> clearColorTexture -> glClear + // SIGSEGV). So the parameter comes back as MONOLITH GLUE, used for nothing but the + // storage sync: the attach SHAPE, the empty-point test and the twin adoption stay the + // record's on both arms, which is what keeps N-6's hole closed and the cross-checks + // deleted. It is a pointer and it is null on the handle arm, so "which arm am I on" is + // never inferred from it - MG_Config::Transport decides, as §5.8 requires, and a + // monolith arm that somehow arrives without one refuses by name instead of attaching a + // texture whose levels were never pushed. static Bool SyncAttachmentSurface(GLenum glFBOTarget, const MG_Pipe::MGPSurface& surface, - GLenum glBackendAttachment) { + GLenum glBackendAttachment, + const MG_State::GLState::FramebufferAttachmentObject* monolithAttachment) { if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone || MG_Pipe::MGPipeHandleIsNull(surface.Res)) { // An empty point: the caller detached it on exactly this test, so attaching @@ -10045,8 +10117,30 @@ namespace MobileGL::MG_Backend::DirectGLES { } // P5e (fb): the storage sync BY HANDLE (tx2's seam). The level shadow it reads // is the applier's staged texture store, not a frontend object, which is what - // let the object parameter go. - backendTextureObject->SyncMipmapsToBackendByHandle(surface.Res); + // let the object parameter go - UNDER A TRANSPORT. P5e (fix1, ID-81): the + // push-monolith arm keeps the frontend sync it had at f6cfcbd3, token for token, + // because tx2's record arm inside SyncMipmapsToBackend is itself selected by + // `Transport != Monolith` and cannot answer here. +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + backendTextureObject->SyncMipmapsToBackendByHandle(surface.Res); + } else +#endif + { + const SharedPtr monolithTexture = + (monolithAttachment != nullptr && monolithAttachment->IsTexture()) + ? monolithAttachment->GetTexture() + : nullptr; + if (!monolithTexture) { + MGLOG_E_ONCE("MGPipe: attachment record names texture {%u, %u} on the " + "push-monolith arm, where the storage sync needs the frontend " + "texture and this caller supplied none - refusing to attach a " + "texture whose levels were never pushed", + surface.Res.Slot, surface.Res.Gen); + return false; + } + backendTextureObject->SyncMipmapsToBackend(monolithTexture); + } const auto uploadTarget = static_cast(surface.UploadTarget); if (surface.Layered != 0) { g_GLESFuncs.glFramebufferTexture(glFBOTarget, glBackendAttachment, @@ -10103,7 +10197,33 @@ namespace MobileGL::MG_Backend::DirectGLES { // P5e (fb): the four allocation values were already record-supplied (D-D2); what // moves here is the LOOKUP - the handle the surface carries, never HandleOf on // an object the apply thread was handed. - backendRenderbufferObject->SyncToBackendByHandle(surface.Res); + // + // P5e (fix1, ID-81): arm-selected for the texture arm's reason, and here it is a + // BEHAVIOUR difference even though the by-handle body dereferences nothing: the + // object form reports a refused allocation to the application through the live + // GLContext (RecordError) and the by-handle form deliberately does not, because + // under a transport there is no application on this side to report it to. On the + // push-monolith arm there is, so the monolith arm keeps the object form. +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + backendRenderbufferObject->SyncToBackendByHandle(surface.Res); + } else +#endif + { + const SharedPtr monolithRenderbuffer = + (monolithAttachment != nullptr && monolithAttachment->IsRenderbuffer()) + ? monolithAttachment->GetRenderbuffer() + : nullptr; + if (!monolithRenderbuffer) { + MGLOG_E_ONCE("MGPipe: attachment record names renderbuffer {%u, %u} on the " + "push-monolith arm, where the storage allocation needs the " + "frontend renderbuffer and this caller supplied none - refusing " + "to attach a renderbuffer with no storage", + surface.Res.Slot, surface.Res.Gen); + return false; + } + backendRenderbufferObject->SyncToBackend(monolithRenderbuffer); + } backendRenderbufferObject->Bind(); g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER, backendRenderbufferObject->GetBackendRenderbufferId()); @@ -10756,7 +10876,8 @@ namespace MobileGL::MG_Backend::DirectGLES { } } else { attachmentSynced = - SyncAttachmentSurface(glFBOTarget, *pushedSurface, glBackendAttachment); + SyncAttachmentSurface(glFBOTarget, *pushedSurface, glBackendAttachment, + &attachmentObject); } } else { attachmentSynced = SyncAttachmentObject(glFBOTarget, attachmentObject, glBackendAttachment); @@ -11072,7 +11193,11 @@ namespace MobileGL::MG_Backend::DirectGLES { } return; } - SyncAttachmentSurface(glFBOTarget, surface, glBackendAttachment); + // No frontend attachment on this arm, by construction: the record IS the + // point set here (§5.4). Null is what selects nothing - the transport test + // inside is what selects the by-handle storage syncs. + SyncAttachmentSurface(glFBOTarget, surface, glBackendAttachment, + /*monolithAttachment=*/nullptr); }; for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) { applyPoint(static_cast( diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 2671b81c..f72d467a 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -72,6 +72,7 @@ add_executable(MobileGLIntegrationTest Scenarios/SampledSetStalenessScenario.cpp Scenarios/ThreeChannelAttachmentScenario.cpp Scenarios/SnormAttachmentScenario.cpp + Scenarios/MonolithAttachmentClearScenario.cpp Scenarios/PipelineFailureScenario.cpp Scenarios/AdvertisedLimitsScenario.cpp Scenarios/PixelStoreSweepScenario.cpp @@ -1938,6 +1939,45 @@ if (MOBILEGL_BUILD_DISAGGREGATED) ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}" ) + # ---- P5e fix1: THE PUSH-MONOLITH ARM, INSIDE THE GATED SET (ID-107) ---------------------- + # + # WHY A MONOLITH LANE CARRIES THE `integration-split` LABEL. That label does not mean "runs + # inproc" - it means "the set the disaggregation gate runs", which is exactly why it is + # registered inside this `if (MOBILEGL_BUILD_DISAGGREGATED)` and why the block above argues + # that a build without the option must match NOTHING under it. ID-81 gives the push build TWO + # server arms, selected at runtime by MG_Config::Transport, and until now only one of them had + # a gated entry: every Split block above exports MOBILEGL_TRANSPORT=inproc, and every + # split-only scenario skips itself the moment the runtime is not split (SplitRuntimeSkipReason + # in their SetUp), so the ambient monolith registration of those files runs zero cases. The + # arm the phone actually ships on was therefore ungated, and P5e landed a null dereference on + # it under 2250 green unit entries and 113 green split entries (ID-107: the game died in + # Lightmap. -> clearColorTexture -> glClear). A second arm needs a second lane, in the + # same set, or "the gate is green" keeps meaning "one of the two arms is green". + # + # MOBILEGL_TRANSPORT=monolith IS NAMED, for review finding N-6's reason exactly as the + # PersistentMapArm pair above names it: a ctest ENVIRONMENT property overrides the job + # environment, and without it a gate-level `MOBILEGL_TRANSPORT=inproc` export would quietly + # turn this block into a fourth copy of the split lane - the one failure mode a lane whose + # whole purpose is to be the OTHER arm cannot survive. + # + # The scenario is registered ambiently as well (its source is in the list above), so + # `DirectGLES.MonolithAttachmentClearScenario.*` and `DirectVulkan.…` still exist in + # integration-gpu; this block is the copy the gate can see, under a prefix that says which arm + # it pins. + mgl_itest_join_environment(MGL_ITEST_GLES_PUSH_MONOLITH_ENVIRONMENT + "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_TRANSPORT=monolith" + ${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV}) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectGLES.PushMonolithArm." + TEST_LIST MGL_PUSH_MONOLITH_ATTACHMENT_TESTS + TEST_FILTER "MonolithAttachmentClearScenario.*" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS "integration-gpu\;integration-split" + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_GLES_PUSH_MONOLITH_ENVIRONMENT}" + ) + # ---- P5b d1: the indexed / instanced / multi-draw / indirect draw family under inproc ---- # # One block per scenario, the precedent above. IndexedDrawFamilyScenario is d1's own lane diff --git a/MobileGL/MG_IntegrationTest/Scenarios/MonolithAttachmentClearScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/MonolithAttachmentClearScenario.cpp new file mode 100644 index 00000000..d141d74e --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/MonolithAttachmentClearScenario.cpp @@ -0,0 +1,180 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/MonolithAttachmentClearScenario.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 - CLEARING A FRAMEBUFFER THAT HOLDS AN ATTACHMENT, ON THE PUSH-MONOLITH ARM. +// +// THE HOLE THIS CLOSES (INTEGRATOR-DECISIONS-P5E ID-107). P5e landed with 2250 unit entries and +// 113 `integration-split` entries green, and the game then died at startup under +// MOBILEGL_TRANSPORT=monolith inside Lightmap. -> clearColorTexture -> glClear: +// SyncCurrentFBO -> BackendFramebufferObject::SyncToBackend(frontend FBO) -> +// SyncAttachmentSurface -> SyncMipmapsToBackend(null SharedPtr) -> SIGSEGV. Neither gated lane +// could see it, for two reasons that are one reason: +// +// * the gate runs `-L unit` and `-L integration-split`, and EVERY split entry sets +// MOBILEGL_TRANSPORT=inproc, which selects the by-handle framebuffer arm +// (FramebufferRecordArmIsMandatory) - the arm that never reaches the frontend overload at +// all; and +// * every P5-era scenario that DOES clear a texture-attached framebuffer is split-only by +// construction (F1WireScenario and friends call SplitRuntimeSkipReason() in SetUp and skip +// the moment the runtime is not split), so the ambient monolith registration of those files +// runs zero cases. +// +// So the arm the phone ships on - MOBILEGL_PIPE_PUSH compiled, transport monolith, framebuffer +// records produced and applied IN PROCESS (P4a D-C2), attachments resolved from those records - +// had no gated entry that attached anything to a framebuffer and cleared it. This file is that +// entry, and its registration block (MG_IntegrationTest/CMakeLists.txt, the +// `DirectGLES.PushMonolithArm.` prefix) pins the transport with a ctest ENVIRONMENT property for +// review finding N-6's reason: a job-level MOBILEGL_TRANSPORT=inproc must not be able to turn +// this lane into a second copy of the split one. +// +// WHAT EACH CASE IS FOR. The three attachment KINDS are the three arms of +// FramebufferImpl::SyncAttachmentSurface, and on the monolith arm each must drive its storage +// from the frontend object the walk is holding rather than from a by-handle seam whose record +// arm is selected by `Transport != Monolith` (CONTRACT-P5E §5.8 / ID-81): +// +// * a MUTABLE 2D texture (glTexImage2D, the Lightmap shape) - the texture arm, whose storage +// sync is the one that crashed; +// * an IMMUTABLE 2D texture (glTexStorage2D) - the same arm with the levels already allocated, +// so a regression that only shows up while levels are still being defined cannot hide the +// other half; +// * a RENDERBUFFER - the sibling arm, whose by-handle form does not dereference anything but +// does drop the application-visible GL_OUT_OF_MEMORY report, so it is arm-selected too. +// +// The assertion is the PIXEL, not "it did not crash": a clear that reaches a driver framebuffer +// with no attachment raises GL_INVALID_FRAMEBUFFER_OPERATION and writes nothing, which is the +// silent half of the same defect and is what an attach that was refused rather than crashed +// would produce. Both are failures here. +// +// Backend-agnostic on purpose. Nothing below is a DirectGLES fallback: "a clear of a complete +// framebuffer reaches its own attachment" is a property of GL, so both ambient registrations +// assert it and the scenario fails wherever it stops being true. + +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { + namespace { + + constexpr int kSize = 8; + + // 0.25 / 0.5 / 0.75 / 1.0 - the F1 lane's own clear values, so a reader comparing the two + // arms is comparing the same numbers. None of them is 0 or 1, so neither a cleared-to-zero + // texture nor an untouched one can pass by accident. + constexpr float kClearR = 0.25f; + constexpr float kClearG = 0.5f; + constexpr float kClearB = 0.75f; + constexpr GLubyte kExpected[4] = {64, 128, 191, 255}; + + class MonolithAttachmentClearScenario : public ScenarioTest { + protected: + GLuint fbo = 0; + GLuint texture = 0; + GLuint renderbuffer = 0; + + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + glGenFramebuffers(1, &fbo); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glDisable(GL_SCISSOR_TEST); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glViewport(0, 0, kSize, kSize); + } + + void TearDown() override { + if (!Ready()) return; + glBindFramebuffer(GL_FRAMEBUFFER, 0); + if (texture) glDeleteTextures(1, &texture); + if (renderbuffer) glDeleteRenderbuffers(1, &renderbuffer); + if (fbo) glDeleteFramebuffers(1, &fbo); + ScenarioTest::TearDown(); + } + + // Clear the bound framebuffer and read back the pixel the F1 lane reads. Separated + // from the attach so that a failure says which half broke. + void ClearAndExpectThePixel(const char* what) { + ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE)) + << what << ": the framebuffer never came up, so the clear below would prove nothing"; + glClearColor(kClearR, kClearG, kClearB, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + GLubyte pixel[4] = {0, 0, 0, 0}; + glReadPixels(2, 3, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); + // A clear issued against a driver framebuffer whose attachment point was never + // filled in raises this and writes nothing - the silent form of the defect. + EXPECT_EQ(FirstGLError(), 0u) << what << ": " << GLErrorName(FirstGLError()); + for (int i = 0; i < 4; ++i) { + EXPECT_NEAR(pixel[i], kExpected[i], 1) + << what << ": channel " << i << " of the attachment did not receive the clear"; + } + } + }; + + // THE DEVICE'S OWN SHAPE: a mutable texture whose level 0 is defined by glTexImage2D and + // then attached, which is what Lightmap. does before its clearColorTexture. The + // storage sync behind the attachment is the call that took the null frontend object. + TEST_F(MonolithAttachmentClearScenario, AClearOfAMutableTextureAttachmentReachesItsPixels) { + if (!Ready() || IsSkipped()) return; + + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + std::vector zeros(static_cast(kSize) * kSize * 4, 0); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kSize, kSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, zeros.data()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); + + ClearAndExpectThePixel("mutable 2D texture attachment"); + } + + // The same arm with immutable storage: the levels are allocated before the attach, so a + // sync that only misbehaves while a level is still being defined cannot account for a + // green here. + TEST_F(MonolithAttachmentClearScenario, AClearOfAnImmutableTextureAttachmentReachesItsPixels) { + if (!Ready() || IsSkipped()) return; + + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSize, kSize); + ASSERT_EQ(FirstGLError(), 0u) << "the driver refused the immutable storage itself"; + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); + + ClearAndExpectThePixel("immutable 2D texture attachment"); + } + + // The renderbuffer arm of the same function. It cannot crash the way the texture arm did + // - the by-handle allocation dereferences no frontend object - but it IS the other half of + // the arm selection, and a regression that routes it back through the by-handle form on + // the monolith arm silently drops the application's GL_OUT_OF_MEMORY report. + TEST_F(MonolithAttachmentClearScenario, AClearOfARenderbufferAttachmentReachesItsPixels) { + if (!Ready() || IsSkipped()) return; + + glGenRenderbuffers(1, &renderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kSize, kSize); + ASSERT_EQ(FirstGLError(), 0u) << "the driver refused the renderbuffer storage itself"; + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer); + + ClearAndExpectThePixel("renderbuffer attachment"); + } + + } // namespace +} // namespace MGITest