From daf2b44c71da79d261e74f15cee56079bc31bafc Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 6 Oct 2026 04:08:22 -0400 Subject: [PATCH] [Test] (P13 W3b): a GL-level successor for the borrowed-slot unit-memo probe --- MobileGL/MG_IntegrationTest/CMakeLists.txt | 12 +- MobileGL/MG_IntegrationTest/P8Coverage.cmake | 1 + .../DsaUpdateKeepsTheBoundUnitScenario.cpp | 189 ++++++++++++++++++ docs/ci/ci-cleanup-log.md | 7 +- scripts/data/split_coverage_exemptions.txt | 1 + 5 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 MobileGL/MG_IntegrationTest/Scenarios/DsaUpdateKeepsTheBoundUnitScenario.cpp diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index cc41469e..2c1c854c 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -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." diff --git a/MobileGL/MG_IntegrationTest/P8Coverage.cmake b/MobileGL/MG_IntegrationTest/P8Coverage.cmake index 4203ec46..1fc44624 100644 --- a/MobileGL/MG_IntegrationTest/P8Coverage.cmake +++ b/MobileGL/MG_IntegrationTest/P8Coverage.cmake @@ -42,6 +42,7 @@ set(MGL_P8A_AMBIENT_CASES DepthStencilReadbackAttachmentShapeScenario.PackedArrayLayerAttachmentReadsBackBothAspects DepthStencilReadbackScenario.* DoublePrecisionScenario.* + DsaUpdateKeepsTheBoundUnitScenario.* DualSourceBlendScenario.* EmptyScissorScenario.* F1WireScenario.ColorBlitToDefaultFromANonSampleableSourceDegrades diff --git a/MobileGL/MG_IntegrationTest/Scenarios/DsaUpdateKeepsTheBoundUnitScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/DsaUpdateKeepsTheBoundUnitScenario.cpp new file mode 100644 index 00000000..6f89bedc --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/DsaUpdateKeepsTheBoundUnitScenario.cpp @@ -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 +#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 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 Solid(int size, std::uint8_t r, std::uint8_t g, std::uint8_t b) { + std::vector texels(static_cast(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(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 diff --git a/docs/ci/ci-cleanup-log.md b/docs/ci/ci-cleanup-log.md index 6b0c2073..8fc5adf6 100644 --- a/docs/ci/ci-cleanup-log.md +++ b/docs/ci/ci-cleanup-log.md @@ -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, `.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.`: 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 diff --git a/scripts/data/split_coverage_exemptions.txt b/scripts/data/split_coverage_exemptions.txt index 69b23a46..c365a555 100644 --- a/scripts/data/split_coverage_exemptions.txt +++ b/scripts/data/split_coverage_exemptions.txt @@ -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