[Test] (P13 W3b): a GL-level successor for the borrowed-slot unit-memo probe

This commit is contained in:
2026-10-06 04:10:51 -04:00
parent e2c9098648
commit daf2b44c71
5 changed files with 202 additions and 8 deletions
+7 -5
View File
@@ -172,6 +172,8 @@ add_executable(MobileGLIntegrationTest
Scenarios/CsoContentAddressingScenario.cpp
Scenarios/ResourceSubsystemControlScenario.cpp
Scenarios/TextureParamsWithoutASamplerViewScenario.cpp
# P13 W3b: the GL-level successor of the borrowed-slot unit-memo probe.
Scenarios/DsaUpdateKeepsTheBoundUnitScenario.cpp
Scenarios/TextureUploadShapeScenario.cpp
Scenarios/ObjectSubsystemControlScenario.cpp
Scenarios/P4aSeamAuditScenario.cpp
@@ -1461,7 +1463,7 @@ gtest_discover_tests(MobileGLIntegrationTest
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_RESOURCE_SUBSYSTEM_ON_ENVIRONMENT}"
)
mgl_itest_add_refused_mask_control("DirectGLES.ResourceSubsystemControl.Off.RefusedAtStartup" "0x7f" "ResourceSubsystemControlScenario.*"
mgl_itest_add_refused_mask_control("DirectGLES.SubsystemMaskRefusal.ResourceSubsystemControlOff" "0x7f" "ResourceSubsystemControlScenario.*"
"${MGL_ITEST_GLES_RESOURCE_SUBSYSTEM_OFF_ENVIRONMENT}")
# G10's two counting entries, one per scenario that has a claim about the counter:
@@ -1536,7 +1538,7 @@ gtest_discover_tests(MobileGLIntegrationTest
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_ARENA_SUBSYSTEM_ON_ENVIRONMENT}"
)
mgl_itest_add_refused_mask_control("DirectGLES.ResourceSubsystemOff.RefusedAtStartup" "0x7f" "LargeArenaAdoptionScenario.*"
mgl_itest_add_refused_mask_control("DirectGLES.SubsystemMaskRefusal.ResourceSubsystemOff" "0x7f" "LargeArenaAdoptionScenario.*"
"${MGL_ITEST_GLES_ARENA_SUBSYSTEM_OFF_ENVIRONMENT}")
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.ArenaArm."
@@ -1646,11 +1648,11 @@ gtest_discover_tests(MobileGLIntegrationTest
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_OBJECT_SUBSYSTEM_ON_ENVIRONMENT}"
)
mgl_itest_add_refused_mask_control("DirectGLES.ObjectSubsystemControl.Off.RefusedAtStartup" "0x1ff" "ObjectSubsystemControlScenario.ClearingTheP4aBitsStopsTheEmissionsAndNotThePixels"
mgl_itest_add_refused_mask_control("DirectGLES.SubsystemMaskRefusal.ObjectSubsystemControlOff" "0x1ff" "ObjectSubsystemControlScenario.ClearingTheP4aBitsStopsTheEmissionsAndNotThePixels"
"${MGL_ITEST_GLES_OBJECT_SUBSYSTEM_OFF_ENVIRONMENT}")
mgl_itest_add_refused_mask_control("DirectGLES.ObjectSubsystemControl.Refused.RefusedAtStartup" "0x9ff" "ObjectSubsystemControlScenario.ASamplerBitWithoutTheTextureBitIsRefusedAndNamed"
mgl_itest_add_refused_mask_control("DirectGLES.SubsystemMaskRefusal.ObjectSubsystemControlRefused" "0x9ff" "ObjectSubsystemControlScenario.ASamplerBitWithoutTheTextureBitIsRefusedAndNamed"
"${MGL_ITEST_GLES_OBJECT_SUBSYSTEM_REFUSED_ENVIRONMENT}")
mgl_itest_add_refused_mask_control("DirectGLES.ObjectSubsystemControl.RefusedTexture.RefusedAtStartup" "0x5ff" "ObjectSubsystemControlScenario.ATextureBitWithoutTheSamplerBitIsRefusedAndNamed"
mgl_itest_add_refused_mask_control("DirectGLES.SubsystemMaskRefusal.ObjectSubsystemControlRefusedTexture" "0x5ff" "ObjectSubsystemControlScenario.ATextureBitWithoutTheSamplerBitIsRefusedAndNamed"
"${MGL_ITEST_GLES_OBJECT_SUBSYSTEM_REFUSED_TEXTURE_ENVIRONMENT}")
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.ObjectSubsystemControl.Consumer."
@@ -42,6 +42,7 @@ set(MGL_P8A_AMBIENT_CASES
DepthStencilReadbackAttachmentShapeScenario.PackedArrayLayerAttachmentReadsBackBothAspects
DepthStencilReadbackScenario.*
DoublePrecisionScenario.*
DsaUpdateKeepsTheBoundUnitScenario.*
DualSourceBlendScenario.*
EmptyScissorScenario.*
F1WireScenario.ColorBlitToDefaultFromANonSampleableSourceDegrades
@@ -0,0 +1,189 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DsaUpdateKeepsTheBoundUnitScenario.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 - A BY-NAME (DSA) UPDATE OF TEXTURE B DOES NOT RETARGET THE UNIT THAT HOLDS TEXTURE A.
//
// GL 4.6 core 8.1/8.5: glTextureSubImage2D / glTextureStorage2D name their texture directly and
// change no binding. So with A bound to unit 0, a by-name update of B must leave every later draw
// that samples unit 0 sampling A - A's texels and A's shape.
//
// The regression this guards (P13 W3b; it used to be the white-box SanityTest probe
// DirectGLESTextureSync.UnitMemoRefusesToDriveATwinFromAnotherTexture): Espryt's per-unit texture
// sync list on the monolith arm BORROWS a pointer into the frontend binding slot and replays it on
// the next draw. A by-name emulation that swapped the slot behind the binding accounting left the
// replay driving A's backend twin from B's frontend object, so A's storage was respecified with
// B's shape and its GPU-only contents were lost (Iris/BSL: the 16x16 lightmap blanked when a
// 2048x2048 shadow map was uploaded by name, and the HUD text discarded itself). The probe ran only
// at the P2 subsystem mask, whose legacy unit arm is gone; this asserts the same thing through
// public GL, on every lane and on both backends, at the shipping mask.
#include <cstdint>
#include <string>
#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 kInset = 2;
constexpr int kSmall = 4; // A: the lightmap
constexpr int kLarge = 32; // B: the shadow map
constexpr const char* kQuadVS = R"(#version 330 core
in vec2 aPos;
out vec2 vUv;
void main() {
vUv = aPos * 0.5 + 0.5;
gl_Position = vec4(aPos, 0.0, 1.0);
}
)";
// texelFetch at the texture's own last texel: a texture whose shape moved to B's would
// fetch a different texel, and one whose contents moved would fetch B's colour.
constexpr const char* kFetchFS = R"(#version 330 core
uniform sampler2D uTex;
out vec4 oColor;
void main() {
ivec2 size = textureSize(uTex, 0);
oColor = (size == ivec2(4, 4)) ? texelFetch(uTex, size - ivec2(1, 1), 0) : vec4(1.0, 0.0, 1.0, 1.0);
}
)";
struct Vertex {
float x, y;
};
std::vector<std::uint8_t> Solid(int size, std::uint8_t r, std::uint8_t g, std::uint8_t b) {
std::vector<std::uint8_t> texels(static_cast<std::size_t>(size) * size * 4);
for (std::size_t i = 0; i < texels.size(); i += 4) {
texels[i] = r;
texels[i + 1] = g;
texels[i + 2] = b;
texels[i + 3] = 255;
}
return texels;
}
class DsaUpdateKeepsTheBoundUnitScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
std::string error;
m_program = CompileProgram(kQuadVS, kFetchFS, &error);
ASSERT_NE(m_program, 0u) << error;
static const Vertex quad[6] = {{-1.0f, -1.0f}, {1.0f, -1.0f}, {1.0f, 1.0f},
{-1.0f, -1.0f}, {1.0f, 1.0f}, {-1.0f, 1.0f}};
glGenBuffers(1, &m_quad);
glBindBuffer(GL_ARRAY_BUFFER, m_quad);
glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);
glBindVertexArray(0);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the scene setup left a GL error behind";
}
void TearDown() override {
if (!Ready() || IsSkipped()) return;
glUseProgram(0);
glBindVertexArray(0);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_quad != 0) glDeleteBuffers(1, &m_quad);
if (m_program != 0) glDeleteProgram(m_program);
}
// The classic path for A, on purpose: it is BOUND to unit 0 and sampled through the
// binding, which is what puts it on the per-unit list the hazard lived in.
GLuint MakeBoundTexture(int size, std::uint8_t r, std::uint8_t g, std::uint8_t b) {
const auto texels = Solid(size, r, g, b);
GLuint texture = 0;
glGenTextures(1, &texture);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, size, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
return texture;
}
// Samples whatever unit 0 holds now - no bind here, the unit is the subject.
Image DrawSamplingUnitZero() {
BindDefaultFramebuffer();
glViewport(0, 0, Gl().Width(), Gl().Height());
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
glUseProgram(m_program);
glUniform1i(glGetUniformLocation(m_program, "uTex"), 0);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLES, 0, 6);
const Image image = ReadPixels(Gl().Width(), Gl().Height());
Gl().EndFrame();
return image;
}
::testing::AssertionResult WholeViewportIs(const Image& image, const char* expected,
const std::string& when) {
return RegionIsMostly(image, kInset, image.Width() - kInset, kInset,
image.Height() - kInset, expected, 0.0, when);
}
GLuint m_program = 0;
GLuint m_quad = 0;
GLuint m_vao = 0;
};
TEST_F(DsaUpdateKeepsTheBoundUnitScenario, AByNameUploadToAnotherTextureLeavesTheUnitsTextureIntact) {
if (!Ready()) return;
GLuint probe = 0;
glCreateTextures(GL_TEXTURE_2D, 1, &probe);
if (FirstGLError() != GLenum(GL_NO_ERROR) || probe == 0) {
GTEST_SKIP() << "glCreateTextures is not usable here; the case needs a by-name update";
}
glDeleteTextures(1, &probe);
// B first, then unbound: it exists but holds no unit.
const GLuint b = MakeBoundTexture(kSmall, 0, 0, 255);
glBindTexture(GL_TEXTURE_2D, 0);
// A on unit 0, sampled once so the unit's sync list is built and memoised.
const GLuint a = MakeBoundTexture(kSmall, 255, 0, 0);
EXPECT_TRUE(WholeViewportIs(DrawSamplingUnitZero(), "red", "A sampled through unit 0"));
// THE HAZARD: B respecified to a different shape and filled BY NAME while A stays bound.
const auto large = Solid(kLarge, 0, 255, 0);
// (glTextureStorage2D on a mutable texture is legal and makes it immutable at the new shape.)
glTextureStorage2D(b, 1, GL_RGBA8, kLarge, kLarge);
glTextureSubImage2D(b, 0, 0, 0, kLarge, kLarge, GL_RGBA, GL_UNSIGNED_BYTE, large.data());
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
EXPECT_TRUE(WholeViewportIs(DrawSamplingUnitZero(), "red",
"unit 0 after a by-name update of ANOTHER texture: it must still sample A "
"(magenta = A took B's shape, green = A took B's texels)"));
// And the update itself landed where it was addressed.
GLint bound = 0;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &bound);
EXPECT_EQ(static_cast<GLuint>(bound), a) << "a by-name update moved the unit's binding";
glBindTexture(GL_TEXTURE_2D, 0);
glDeleteTextures(1, &a);
glDeleteTextures(1, &b);
}
} // namespace
} // namespace MGITest
+4 -3
View File
@@ -14,9 +14,10 @@ them wrong.
| 2026-10-06 | `build explicit pull runtime control` (the pull `libMobileGL.so`, `-DMOBILEGL_PIPE_PUSH=OFF`) | Test / `build-linux-monolith-control` | P13 W2 retires the pull build: CMake defaults to push and nothing ships pull (ID-P13-1) | the same job builds the push monolith without MG_Remote (the FCL shape); its `runtime_mode_proof --mode monolith` is the zero-MG_Remote-symbol check |
| 2026-10-06 | `Negative control (pull library)` | Test / `retrace-split` (every leg) | its library was the pull build above | `Negative control (library without a transport)`: same script (renamed `retrace_transport_control.sh`) and evidence sentence, with the push library without MG_Remote; W1 ran both side by side on every leg |
| 2026-10-06 | `DirectGLES.HandleRecycle.Legacy.*`, `DirectVulkan.HandleRecycle.Legacy.*`, `DirectVulkan.HandleRecycle.AbaControl.*` | Test / integration-gpu (`integration-monolith-control`) | they ran `MOBILEGL_PIPE_PUSH=0`, the pre-handle arm, which P13 W3b removed with `MOBILEGL_PIPE_LEGACY_MEMOS` (ID-P13-3) | `HandleRecycle.Handles.*` and `DirectVulkan.HandleRecycle.AbaControlHandles.*` (the ABA control against the handle arm that ships) stay |
| 2026-10-06 | `DirectGLES.ResourceSubsystemControl.Off.*`, `DirectGLES.ResourceSubsystemOff.*`, `DirectGLES.ObjectSubsystemControl.{Off,Refused,RefusedTexture}.*` | Test / integration-gpu | they cleared subsystem bits (masks 0x7f / 0x1ff / 0x9ff / 0x5ff) to run legacy arms; since W3b bits 0-13 are fixed on (ID-P13-3) | one named-refusal control per lane, `<lane>.RefusedAtStartup`: the same scenario and mask must go red with `Fatal{PipeSubsystemsFixedOn}` naming that mask (`Harness/RefusedMaskControl.cmake`); the `On` lanes now run 0x3fff |
| 2026-10-06 | `SanityTest`: `DirectGLESSlotTable.{AnArmlessKnobCombinationStopsInsteadOfSkippingTheLane, TheArmlessCasesLeaveTheLogPathAndTheConfigAsTheyFoundThem, EglBringUpUnderTheArmlessKnobPairReturnsInsteadOfStopping}` | Test / unit | they pinned the `MOBILEGL_PIPE_LEGACY_MEMOS=0` + cleared-bit knob pair; the knob is gone and the mask is refused at startup | the RefusedAtStartup controls above |
| 2026-10-06 | `SanityTest`: `DirectGLESTextureSync.{UnitMemoRefusesToDriveATwinFromAnotherTexture, AnAttachmentOnlyTexturesParametersReachTheDriverWithNoSamplerView}` | Test / unit | white-box probes that only ever ran at the P2 mask (0x7f): the borrowed-slot unit memo and the pre-handle texture-parameter push; both arms are gone with the legacy memos, and at the shipping mask the fixture cannot produce the applier records the handle arm reads | `DirectGLESTextureSync`'s handle-arm ABA sibling (P5e tx2) and `TextureParamsWithoutASamplerViewScenario` (G9, every lane) |
| 2026-10-06 | `DirectGLES.ResourceSubsystemControl.Off.*`, `DirectGLES.ResourceSubsystemOff.*`, `DirectGLES.ObjectSubsystemControl.{Off,Refused,RefusedTexture}.*` | Test / integration-gpu | they cleared subsystem bits (masks 0x7f / 0x1ff / 0x9ff / 0x5ff) to run legacy arms; since W3b bits 0-13 are fixed on (ID-P13-3) | one named-refusal control per lane, `DirectGLES.SubsystemMaskRefusal.<Lane>`: the same scenario and mask must go red with `Fatal{PipeSubsystemsFixedOn}` naming that mask (`Harness/RefusedMaskControl.cmake`); the `On` lanes now run 0x3fff |
| 2026-10-06 | `SanityTest`: `DirectGLESSlotTable.{AnArmlessKnobCombinationStopsInsteadOfSkippingTheLane, TheArmlessCasesLeaveTheLogPathAndTheConfigAsTheyFoundThem, EglBringUpUnderTheArmlessKnobPairReturnsInsteadOfStopping}` | Test / unit | they pinned the `MOBILEGL_PIPE_LEGACY_MEMOS=0` + cleared-bit knob pair; the knob is gone and the mask is refused at startup | the SubsystemMaskRefusal controls above |
| 2026-10-06 | `SanityTest`: `DirectGLESTextureSync.UnitMemoRefusesToDriveATwinFromAnotherTexture` | Test / unit | **not a behaviour gap; the probe cannot run at the shipping mask in the unit lane.** The code it pins - the monolith arm's borrowed-slot unit list and its pairing check - is still live (`UnitTexturesByHandle()` is false on monolith), but at 0x3fff the twin sync below it reads applier records, and `MGPipeApplyResourceCreate` declines in every unit-lane process (`NoP4aConsumer`), so the probe observed nothing (red before W3b at 0x3fff too, green only at P2). | **replaced**, not just removed: `DsaUpdateKeepsTheBoundUnitScenario` asserts the same hazard through public GL at the shipping mask, every lane, both backends (A bound to unit 0, B respecified and filled by name, unit 0 must still sample A's texels and shape). The handle arm's ABA sibling (P5e tx2) stays in SanityTest. Goes away with the borrowed-slot list itself in W4c |
| 2026-10-06 | `SanityTest`: `DirectGLESTextureSync.AnAttachmentOnlyTexturesParametersReachTheDriverWithNoSamplerView` | Test / unit | **not a behaviour gap.** At 0x3fff the parameter push is record-addressed (`SyncTextureParamsToBackend` reads the texture's applier record; nothing in it consults a sampler view), but the unit lane cannot create that record (`NoP4aConsumer`, as above), so the probe's handle half never had a record to read; its legacy half pinned the pre-handle push that W3b removed | `TextureParamsWithoutASamplerViewScenario` (G9): its four cases take a WHITE-BOX reading of the driver-side parameters before any sample (`TakeTheWhiteBoxReadingBeforeAnySample`, ID-19), at the shipping mask on every lane - the same property, observed where records exist |
| 2026-10-06 | unit `RenderStateSpans.{ResidualTripWireIsSilentUntilTheApplierOwnsTheBytes, ResidualTripWireHoldsAcrossAVerbClassThatDoesNotPublishTheBlock, ResidualTripWireFiresNamingTheCapability}` and PeerLatchTest's `SetResidualValueStateBlobSize` row | Test / unit | P13 W3c deleted ResidualValueBlock and its applier trip wire (op 46 is a retired, declined row) | integration-verify's new `Negative control A2 (corrupted capability)`: the verify comparator must go red on a corrupted `IsCapabilityEnabled`; `PipeWireCodecTest.ResidualValueBlockCrossesAsItsOwnBlob` now pins that op 46 is declined; `TrackerTest...AClipDistanceEnableReArmsTheResidualBlock` now asserts the capability reaches the applier's block |
## Kept after investigation
@@ -21,6 +21,7 @@ HandleRecycleScenario.* mechanism pointer-key ABA / slot recycling inside one pr
CsoContentAddressingScenario.* mechanism toggles the process-local CSO content-addressing switch
ResourceSubsystemControlScenario.* mechanism toggles the process-local P3a push mask
ObjectSubsystemControlScenario.* mechanism toggles the process-local P4a push mask
DirectGLES:SubsystemMaskRefusal.* mechanism P13 W3b named-refusal controls: a startup refusal of a cleared subsystem bit (ConfigLoader), pinned to the monolith lane environment
PipeVerifyArmingScenario.* mechanism same-address-space verify comparator; its split arm is build-verify's integration-verify-split
PoisonOmissionScenario.* mechanism withheld-stamp oracle; its split arm is build-verify's VerifySplitPoisonOmitted entries
StorageBufferRegrowScenario.NStorageDefinitionsCostNMapPersistentRoundtripsNotOnePerDraw mechanism counts synchronous stats-slot roundtrips of direct persistent adoption