[MG_Backend, MG_Test] (Disaggregated): P5e fix1 - the by-handle attachment sync must not run on the monolith arm

- 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.<init> -> 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.
This commit is contained in:
2026-09-18 09:48:24 -04:00
parent 44f91c7405
commit 6662176733
3 changed files with 356 additions and 11 deletions
+136 -11
View File
@@ -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.<init> -> 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<TextureStorageType>((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<MG_State::GLState::ITextureObject>& 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.<init> -> 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<MG_State::GLState::ITextureObject> 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<TextureUploadTarget>(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<MG_State::GLState::RenderbufferObject> 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<FramebufferAttachmentType>(
@@ -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.<init> -> 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
@@ -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.<init> -> 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 <cstdint>
#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 {
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.<init> 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<GLubyte> zeros(static_cast<std::size_t>(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