mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[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:
@@ -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
|
||||
Reference in New Issue
Block a user