diff --git a/MobileGL/MG_Impl/Pipe/Tracker.h b/MobileGL/MG_Impl/Pipe/Tracker.h index ecdd206a..e664198b 100644 --- a/MobileGL/MG_Impl/Pipe/Tracker.h +++ b/MobileGL/MG_Impl/Pipe/Tracker.h @@ -554,6 +554,8 @@ namespace MobileGL::MG_Pipe { // F-2 // shutter: content x params x bind generation x programImages // (lifetime id x link version x image unit version) + // bits 11-14 ALSO fire on the share group's twins of the aggregates they read, for + // a setter called through a sibling context (C8, after the dirty loop) // =========================================================================== // ---- the object-class bits 9..17 ---- @@ -752,6 +754,40 @@ namespace MobileGL::MG_Pipe { } m_lastMaxTextureUnit = maxTextureUnit; + // ---- bits 11..14 across contexts: the share clocks (C8) ---- + // The aggregates bits 11-14 read are this context's, and a texture, renderbuffer or + // sampler changed through a SIBLING context bumps only the sibling's - so with nothing + // done here A's walk after B's glTexParameteri / glTexImage2D / glRenderbufferStorage + // fired none of the four and A recorded the stale framebuffer, view and sampler sets. + // The group keeps a twin of each aggregate, and each bit fires here on exactly the + // twins of what its own shutter reads above. An exact compare rather than one more + // MGPipeMixShutter for vaoChanged's reason: the four are small neighbouring counters. + // A single-context group skips the clocks behind one relaxed load; the first walk + // after a second context joins may fire the four once, which is the safe direction. + // The program bits need nothing of this: a program is a group object too, but its + // versions are read off the object itself, which every context sees move. + const auto& group = *ctx.GetShareGroup(); + if (group.HasSeveralContexts()) { + using SharedClock = MG_State::GLState::ShareGroupState::SharedClock; + const SharedClocks clocks{group.ReadSharedClock(SharedClock::FramebufferAttachment), + group.ReadSharedClock(SharedClock::TextureContent), + group.ReadSharedClock(SharedClock::TextureParams), + group.ReadSharedClock(SharedClock::SamplingResolution)}; + const Bool content = clocks.TextureContent != m_lastSharedClocks.TextureContent; + const Bool params = clocks.TextureParams != m_lastSharedClocks.TextureParams; + if (clocks.FramebufferAttachment != m_lastSharedClocks.FramebufferAttachment) { + dirty |= MGPipeDirtyBit(MGPipeDirty::NewFramebuffer); + } + if (content || params) { + dirty |= MGPipeDirtyBit(MGPipeDirty::NewSamplerViews) | + MGPipeDirtyBit(MGPipeDirty::NewShaderImages); + } + if (params || clocks.SamplingResolution != m_lastSharedClocks.SamplingResolution) { + dirty |= MGPipeDirtyBit(MGPipeDirty::NewSamplers); + } + m_lastSharedClocks = clocks; + } + // ---- bit 2: the PACK half of the pixel store, BitwiseEqual ---- const PixelStoreParameters pack = ctx.GetPixelStoreParameters(false); if (!m_primed || std::memcmp(&pack, &m_pack, sizeof(pack)) != 0) { @@ -806,6 +842,7 @@ namespace MobileGL::MG_Pipe { m_lastVaoLifetime = 0; m_lastVaoConfig = 0; m_lastMaxTextureUnit = -1; + m_lastSharedClocks = SharedClocks{}; m_renderStateVersion.Reset(); m_pipelineStateVersion.Reset(); m_framebufferBind.Reset(); @@ -893,11 +930,19 @@ namespace MobileGL::MG_Pipe { Float Outer[4]; Float Inner[2]; }; + // The share group's four clocks as of this context's last walk (C8). + struct SharedClocks { + Uint64 FramebufferAttachment; + Uint64 TextureContent; + Uint64 TextureParams; + Uint64 SamplingResolution; + }; Uint64 m_lastPushed[kMGPipeDirtyCount]{}; Uint64 m_lastVaoLifetime = 0; Uint32 m_lastVaoConfig = 0; Int m_lastMaxTextureUnit = -1; + SharedClocks m_lastSharedClocks{}; MGPipeWidenedCounter m_renderStateVersion; MGPipeWidenedCounter m_pipelineStateVersion; // The draw framebuffer BINDING slot version, widened for the same reason: a Uint16 diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 08334833..ae3a0ae8 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -183,6 +183,8 @@ add_executable(MobileGLIntegrationTest Scenarios/TextureParamsWithoutASamplerViewScenario.cpp # P13 W3b: the GL-level successor of the borrowed-slot unit-memo probe. Scenarios/DsaUpdateKeepsTheBoundUnitScenario.cpp + # C8: a shared object changed through a sibling context reaches the other's next draw. + Scenarios/SharedContextChangeScenario.cpp Scenarios/TextureUploadShapeScenario.cpp Scenarios/ObjectSubsystemControlScenario.cpp Scenarios/P4aSeamAuditScenario.cpp diff --git a/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp b/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp index 6ca6343d..340e260a 100644 --- a/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp +++ b/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp @@ -158,6 +158,7 @@ namespace MGITest { void* display = nullptr; void* surface = nullptr; void* context = nullptr; + void* config = nullptr; std::string renderer; }; @@ -301,6 +302,7 @@ namespace MGITest { out.display = display; out.surface = surface; out.context = context; + out.config = config; out.renderer = reinterpret_cast(renderer); outReason.clear(); return 0; @@ -605,6 +607,7 @@ namespace MGITest { m_display = brought.display; m_surface = brought.surface; m_context = brought.context; + m_config = brought.config; m_width = kSurfaceWidth; m_height = kSurfaceHeight; m_renderer = std::move(brought.renderer); @@ -617,6 +620,27 @@ namespace MGITest { ++m_frameIndex; } + void* HeadlessGL::CreateSharingContext() { + if (!m_usable) return nullptr; + const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_NONE}; + EGLContext context = eglCreateContext(static_cast(m_display), static_cast(m_config), + static_cast(m_context), contextAttribs); + return context == EGL_NO_CONTEXT ? nullptr : context; + } + + bool HeadlessGL::MakeContextCurrent(void* context) { + if (!m_usable) return false; + EGLSurface surface = static_cast(m_surface); + return eglMakeCurrent(static_cast(m_display), surface, surface, + static_cast(context != nullptr ? context : m_context)) == EGL_TRUE; + } + + void HeadlessGL::DestroySharingContext(void* context) { + if (!m_usable || context == nullptr) return; + MakeContextCurrent(nullptr); + eglDestroyContext(static_cast(m_display), static_cast(context)); + } + void HeadlessGL::ShutDown() { if (!m_usable) return; EGLDisplay display = static_cast(m_display); diff --git a/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.h b/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.h index 2dffce9c..93c34813 100644 --- a/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.h +++ b/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.h @@ -139,6 +139,15 @@ namespace MGITest { // destructor with no driver left underneath. void ShutDown(); + // A second context in the harness context's share group, for the scenarios that change a + // shared object through one context and draw with it through the other. It is made current + // on the harness surface; nullptr where EGL refuses one. + void* CreateSharingContext(); + // nullptr makes the harness context current again. + bool MakeContextCurrent(void* context); + // Makes the harness context current, then destroys `context`. + void DestroySharingContext(void* context); + private: HeadlessGL(); HeadlessGL(const HeadlessGL&) = delete; @@ -156,6 +165,7 @@ namespace MGITest { void* m_display = nullptr; void* m_surface = nullptr; void* m_context = nullptr; + void* m_config = nullptr; }; // ---- the scenario vocabulary ------------------------------------------- diff --git a/MobileGL/MG_IntegrationTest/P8Coverage.cmake b/MobileGL/MG_IntegrationTest/P8Coverage.cmake index b636d2e9..d4792d9d 100644 --- a/MobileGL/MG_IntegrationTest/P8Coverage.cmake +++ b/MobileGL/MG_IntegrationTest/P8Coverage.cmake @@ -100,6 +100,7 @@ set(MGL_P8A_AMBIENT_CASES ResidentIndexScenario.* SampleMaskScopeScenario.* SampleVariablesScenario.* + SharedContextChangeScenario.* SnormAttachmentScenario.* SpirvShaderBinaryScenario.SpecializedModulesLinkAndRenderWithTheirConstantsApplied SsboArrayDynamicIndexScenario.* diff --git a/MobileGL/MG_IntegrationTest/Scenarios/SharedContextChangeScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/SharedContextChangeScenario.cpp new file mode 100644 index 00000000..4bd6b78a --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/SharedContextChangeScenario.cpp @@ -0,0 +1,216 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SharedContextChangeScenario.cpp +// Copyright (c) 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 SHARED OBJECT CHANGED THROUGH ONE CONTEXT IS WHAT THE OTHER CONTEXT DRAWS WITH. +// +// Textures and renderbuffers belong to the share group (GL 4.6 core 5.1), so a change made +// through context B is the object context A sees, once B has finished and A has re-bound it +// (5.3.3's rule 4). The dirty walk's framebuffer and unit-set bits (11-14) used to shutter only on +// aggregates the change bumps on the context it is made THROUGH: B's glTexParameteri or +// glRenderbufferStorage moved nothing A's walk reads, A's re-bind of the same object is redundant +// and moves nothing either, and A went on recording the view set and framebuffer state from before +// the change. The share group's clocks (C8) are what A's walk now reads beside its own. +// +// Neither context draws between the change and the switch back, so the monolith's one tracker is +// still latched on A when A draws again - the shape where no context switch can rescue it. + +#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 kFboSize = 32; + constexpr int kTexSize = 8; + constexpr int kSmallRenderbuffer = 8; + + constexpr const char* kQuadVertexSource = R"(#version 330 core +void main() { + vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0, + (gl_VertexID & 2) == 0 ? -1.0 : 1.0); + gl_Position = vec4(corner, 0.0, 1.0); +} +)"; + + // texelFetch: the point is WHETHER the texture is sampled, not how it is filtered. + constexpr const char* kSampleFragmentSource = R"(#version 330 core +uniform sampler2D u_tex; +out vec4 o_color; +void main() { + o_color = texelFetch(u_tex, ivec2(0, 0), 0); +} +)"; + + constexpr const char* kGreenFragmentSource = R"(#version 330 core +out vec4 o_color; +void main() { + o_color = vec4(0.0, 1.0, 0.0, 1.0); +} +)"; + + class SharedContextChangeScenario : public ScenarioTest { + protected: + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + glGenVertexArrays(1, &m_vao); + ASSERT_EQ(FirstGLError(), 0u) << "the scene setup raised a GL error"; + } + + void TearDown() override { + if (!Ready()) return; + if (m_sibling != nullptr) Gl().DestroySharingContext(m_sibling); + glBindVertexArray(0); + glUseProgram(0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, 0); + if (m_vao != 0) glDeleteVertexArrays(1, &m_vao); + ScenarioTest::TearDown(); + } + + // B, made current on the harness surface. Skips the case where EGL refuses one. + bool SwitchToSibling() { + if (m_sibling == nullptr) m_sibling = Gl().CreateSharingContext(); + if (m_sibling == nullptr) return false; + return Gl().MakeContextCurrent(m_sibling); + } + static bool SwitchBack() { return Gl().MakeContextCurrent(nullptr); } + + void DrawQuad(GLuint program) { + glBindVertexArray(m_vao); + glUseProgram(program); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBindVertexArray(0); + EXPECT_EQ(FirstGLError(), 0u) << "the draw raised a GL error"; + } + + GLuint m_vao = 0; + void* m_sibling = nullptr; + }; + + } // namespace + + // Bit 12: the view of an incomplete texture is null, and B's filter write completes it. + TEST_F(SharedContextChangeScenario, ATextureCompletedThroughASharingContextIsSampledByTheOther) { + if (!Ready() || IsSkipped()) return; + std::string error; + const unsigned int program = CompileProgram(kQuadVertexSource, kSampleFragmentSource, &error); + ASSERT_NE(program, 0u) << error; + ColorFbo target = MakeColorFbo(kFboSize, kFboSize); + ASSERT_NE(target.fbo, 0u) << "could not create the render target"; + + // ONE level and no glTexParameteri, so MIN_FILTER keeps GL_NEAREST_MIPMAP_LINEAR and the + // texture is mipmap-incomplete: sampling it reads (0,0,0,1). + GLuint texture = 0; + glGenTextures(1, &texture); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, texture); + std::vector green(static_cast(kTexSize * kTexSize * 4), 0); + for (std::size_t i = 0; i < green.size(); i += 4) { + green[i + 1] = 255; + green[i + 3] = 255; + } + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, green.data()); + glUseProgram(program); + glUniform1i(glGetUniformLocation(program, "u_tex"), 0); + ASSERT_EQ(FirstGLError(), 0u) << "defining the sampled texture raised a GL error"; + + BindFbo(target); + ClearTo(0.0f, 0.0f, 1.0f, 1.0f); + DrawQuad(program); + const Image incomplete = ReadPixels(kFboSize, kFboSize); + ASSERT_TRUE(RegionIsMostly(incomplete, 0, kFboSize - 1, 0, kFboSize - 1, "black", 0.0, + "an incomplete texture sampled through A")); + + // B completes the texture and finishes, and draws nothing. + if (!SwitchToSibling()) GTEST_SKIP() << "EGL did not make a sharing context current"; + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glFinish(); + ASSERT_EQ(FirstGLError(), 0u) << "completing the texture through B raised a GL error"; + + // A re-binds it (rule 4), which is the same binding and moves nothing on A. + ASSERT_TRUE(SwitchBack()); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, texture); + BindFbo(target); + ClearTo(0.0f, 0.0f, 1.0f, 1.0f); + DrawQuad(program); + const Image complete = ReadPixels(kFboSize, kFboSize); + EXPECT_TRUE(RegionIsMostly(complete, 0, kFboSize - 1, 0, kFboSize - 1, "green", 0.0, + "the same texture through A after B completed it")) + << "black means A still recorded the null view of the incomplete texture"; + + glBindTexture(GL_TEXTURE_2D, 0); + glDeleteTextures(1, &texture); + glDeleteProgram(program); + DestroyColorFbo(target); + } + + // Bit 11: set_framebuffer_state inlines the attachment's extent, and B resizes it. + TEST_F(SharedContextChangeScenario, ARenderbufferResizedThroughASharingContextIsDrawnWhole) { + if (!Ready() || IsSkipped()) return; + std::string error; + const unsigned int program = CompileProgram(kQuadVertexSource, kGreenFragmentSource, &error); + ASSERT_NE(program, 0u) << error; + + GLuint renderbuffer = 0; + glGenRenderbuffers(1, &renderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kSmallRenderbuffer, kSmallRenderbuffer); + GLuint fbo = 0; + glGenFramebuffers(1, &fbo); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer); + ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast(GL_FRAMEBUFFER_COMPLETE)); + glViewport(0, 0, kSmallRenderbuffer, kSmallRenderbuffer); + ClearTo(0.0f, 0.0f, 1.0f, 1.0f); + const Image small = ReadPixels(kSmallRenderbuffer, kSmallRenderbuffer); + ASSERT_TRUE(RegionIsMostly(small, 0, kSmallRenderbuffer - 1, 0, kSmallRenderbuffer - 1, "blue", 0.0, + "the small renderbuffer through A")); + + // B redefines the attached renderbuffer at four times the size, and draws nothing. + if (!SwitchToSibling()) GTEST_SKIP() << "EGL did not make a sharing context current"; + glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kFboSize, kFboSize); + glFinish(); + ASSERT_EQ(FirstGLError(), 0u) << "resizing the renderbuffer through B raised a GL error"; + + ASSERT_TRUE(SwitchBack()); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast(GL_FRAMEBUFFER_COMPLETE)); + glViewport(0, 0, kFboSize, kFboSize); + ClearTo(0.0f, 0.0f, 1.0f, 1.0f); + DrawQuad(program); + const Image whole = ReadPixels(kFboSize, kFboSize); + EXPECT_TRUE(RegionIsMostly(whole, 0, kFboSize - 1, 0, kFboSize - 1, "green", 0.0, + "the resized renderbuffer through A")) + << "a corner left uncovered means A still recorded the renderbuffer's old extent"; + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &fbo); + glDeleteRenderbuffers(1, &renderbuffer); + glDeleteProgram(program); + } + +} // namespace MGITest diff --git a/MobileGL/MG_State/GLState/Core.cpp b/MobileGL/MG_State/GLState/Core.cpp index 868fd568..52064b3c 100644 --- a/MobileGL/MG_State/GLState/Core.cpp +++ b/MobileGL/MG_State/GLState/Core.cpp @@ -1377,6 +1377,8 @@ namespace MobileGL::MG_State { GLContext::GLContext(SharedPtr shareGroup) : m_shareGroup(shareGroup ? Move(shareGroup) : MakeShared()) { + // A second context in the group makes the dirty walk read the share clocks. + m_shareGroup->NoteContextAttached(); // The default transform feedback object (name 0) exists from the start of the context // (GL 4.6 core 13.2.1), but nothing binds it, so nothing else would materialise it. // Materialising it here is what lets GetBoundTransformFeedbackLifetimeId() be a plain diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index 7d505b5b..657345e2 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -202,7 +202,10 @@ namespace MobileGL { Uint64 GetSamplingResolutionGeneration() const { return m_textureState.GetSamplingResolutionGeneration(); } - void BumpSamplingResolutionGeneration() { m_textureState.BumpSamplingResolutionGeneration(); } + void BumpSamplingResolutionGeneration() { + m_textureState.BumpSamplingResolutionGeneration(); + m_shareGroup->BumpSharedClock(ShareGroupState::SharedClock::SamplingResolution); + } // Never-reused id of this context, for backend memos keyed on the two counters // above: both restart at 0 in a new context, and a recreated context can land on // the old heap address. See TextureState::GetContextId. @@ -276,15 +279,28 @@ namespace MobileGL { Uint64 GetAnyVaoAttributeGeneration() const { return m_vertexArrayState.GetAnyAttributeGeneration(); } - void NoteFramebufferAttachmentChanged() { m_framebufferState.NoteAttachmentChanged(); } + // The framebuffer and the two texture aggregates (and the sampling resolution + // generation above) also bump the share group's twin: the objects behind them are + // the group's, and a sibling context's walk has to see the change too + // (ShareGroupState's share clocks, C8). + void NoteFramebufferAttachmentChanged() { + m_framebufferState.NoteAttachmentChanged(); + m_shareGroup->BumpSharedClock(ShareGroupState::SharedClock::FramebufferAttachment); + } Uint64 GetAnyFramebufferAttachmentGeneration() const { return m_framebufferState.GetAnyAttachmentGeneration(); } - void NoteTextureContentChanged() { m_textureState.NoteTextureContentChanged(); } + void NoteTextureContentChanged() { + m_textureState.NoteTextureContentChanged(); + m_shareGroup->BumpSharedClock(ShareGroupState::SharedClock::TextureContent); + } Uint64 GetAnyTextureContentGeneration() const { return m_textureState.GetAnyTextureContentGeneration(); } - void NoteTextureParamsChanged() { m_textureState.NoteTextureParamsChanged(); } + void NoteTextureParamsChanged() { + m_textureState.NoteTextureParamsChanged(); + m_shareGroup->BumpSharedClock(ShareGroupState::SharedClock::TextureParams); + } Uint64 GetAnyTextureParamsGeneration() const { return m_textureState.GetAnyTextureParamsGeneration(); } diff --git a/MobileGL/MG_State/GLState/ShareGroupState.h b/MobileGL/MG_State/GLState/ShareGroupState.h index 1082c48a..3b532eb2 100644 --- a/MobileGL/MG_State/GLState/ShareGroupState.h +++ b/MobileGL/MG_State/GLState/ShareGroupState.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include #include "BufferState/BufferObject.h" @@ -74,7 +75,42 @@ namespace MobileGL::MG_State::GLState { UniquePtr TakeClientArrayBuffer(); void ReturnClientArrayBuffer(UniquePtr buffer); + // ---- the share clocks (C8) ---- + // + // The dirty walk's framebuffer and texture shutters (bits 11-14) read per-CONTEXT + // aggregates, and a mutation bumps them on the context it is made through. Textures, + // renderbuffers and samplers live here, though, so a change made through context B moved + // nothing context A's walk reads and A went on recording the stale framebuffer, view and + // sampler sets. These are the same four aggregates kept once per group: the GLContext + // funnel that bumps a context's own counter bumps its twin here too, and the walk + // compares the twins beside its own. + // + // Release on the bump, which runs after the object write; acquire on the read, so a walk + // that sees the new value also sees the object it describes. + enum class SharedClock : Uint8 { + FramebufferAttachment, + TextureContent, + TextureParams, + SamplingResolution, + Count, + }; + void BumpSharedClock(SharedClock clock) { + m_sharedClocks[static_cast(clock)].fetch_add(1, std::memory_order_release); + } + Uint64 ReadSharedClock(SharedClock clock) const { + return m_sharedClocks[static_cast(clock)].load(std::memory_order_acquire); + } + // One relaxed load is the single-context walk's whole cost: with one context there is no + // other reader, and the clocks are not read at all. Sticky, never decremented, so no walk + // has to reason about a sibling that is mid-teardown. The BUMP is not skipped while the + // group has one context: the mutating thread may read a stale count, and a skipped bump + // is lost for good, where an unread clock costs nothing. + void NoteContextAttached() { m_attachedContexts.fetch_add(1, std::memory_order_relaxed); } + Bool HasSeveralContexts() const { return m_attachedContexts.load(std::memory_order_relaxed) > 1; } + private: + std::atomic m_sharedClocks[static_cast(SharedClock::Count)] = {}; + std::atomic m_attachedContexts{0}; Uint64 m_token = 0; UnorderedMap> m_bufferObjects; IndexGenerator m_bufferNames{1024, 1}; diff --git a/MobileGL/MG_Test/Pipe/TrackerTest.cpp b/MobileGL/MG_Test/Pipe/TrackerTest.cpp index 7e414d46..a4f482f5 100644 --- a/MobileGL/MG_Test/Pipe/TrackerTest.cpp +++ b/MobileGL/MG_Test/Pipe/TrackerTest.cpp @@ -1302,4 +1302,165 @@ namespace { MG_State::pGLContext = Move(parked); } + // =================================================================================== + // Bits 11-14 across the contexts of one share group (C8) + // =================================================================================== + // + // Textures, renderbuffers and samplers belong to the share group, but the aggregates + // bits 11-14 read are bumped on the context a change is made THROUGH. These change an + // object through context B and then walk context A with B never walking in between - + // what a second thread drawing into its own context does, and on one thread the shape + // that leaves the monolith's one tracker latched on A (no context switch reaches it). + class TrackerShareGroup : public ::testing::Test { + protected: + void SetUp() override { + m_previous = Move(MG_State::pGLContext); + const auto group = MakeShared(); + m_a = MakeShared(group); + m_b = MakeShared(group); + MakeLive(m_a); + } + void TearDown() override { MG_State::pGLContext = Move(m_previous); } + + // The live context is the one MGP_NOTE_AGGREGATE lands on. + static void MakeLive(const SharedPtr& ctx) { MG_State::pGLContext = ctx; } + + Uint32 WalkA() { + MakeLive(m_a); + return m_tracker.Update(*m_a, MGPipeVerbClass::kDraw); + } + // Prime A and prove the steady state, so whatever fires next is the change's. + void PrimeA() { + WalkA(); + ASSERT_EQ(WalkA(), 0u) << "the fixture did not reach a steady state"; + } + + static void ExpectFired(Uint32 dirty, MGPipeDirty bit, const char* why) { + EXPECT_NE(dirty & MGPipeDirtyBit(bit), 0u) + << kMGPipeDirtyNames[static_cast(bit)] << " did not fire on A: " << why; + } + + MGPipeTracker m_tracker; + SharedPtr m_a; + SharedPtr m_b; + SharedPtr m_previous; + }; + + TEST_F(TrackerShareGroup, OnlyASecondContextMakesTheWalkReadTheShareClocks) { + EXPECT_TRUE(m_a->GetShareGroup()->HasSeveralContexts()); + const GLContext solo; + EXPECT_FALSE(solo.GetShareGroup()->HasSeveralContexts()); + } + + // glTexParameteri through B on a texture A samples: completeness (bit 12's null view), + // the effective sampler (bit 13) and the image window (bit 14) all moved. + TEST_F(TrackerShareGroup, ATextureParameterThroughASiblingFiresTheUnitSetBits) { + const auto& tex = m_a->CreateTextureObject(1, TextureTarget::Texture2D); + ASSERT_TRUE(tex != nullptr); + ASSERT_EQ(m_b->GetTextureObject(1), tex) << "the premise is ONE texture seen by both contexts"; + PrimeA(); + + MakeLive(m_b); + tex->SetMaxLevel(4); + const Uint32 dirty = WalkA(); + ExpectFired(dirty, MGPipeDirty::NewSamplerViews, "a sibling's glTexParameteri moved completeness"); + ExpectFired(dirty, MGPipeDirty::NewSamplers, "a sibling's glTexParameteri moved the sampler state"); + ExpectFired(dirty, MGPipeDirty::NewShaderImages, "a sibling's glTexParameteri moved the image window"); + EXPECT_EQ(WalkA(), 0u) << "the share clocks fire forever"; + } + + TEST_F(TrackerShareGroup, ATextureUploadThroughASiblingFiresTheViewAndImageBits) { + const auto& tex = m_a->CreateTextureObject(1, TextureTarget::Texture2D); + ASSERT_TRUE(tex != nullptr); + PrimeA(); + + MakeLive(m_b); + static_cast(tex.get())->BumpContentVersion(); + const Uint32 dirty = WalkA(); + ExpectFired(dirty, MGPipeDirty::NewSamplerViews, "a sibling's upload can define the level a view needs"); + ExpectFired(dirty, MGPipeDirty::NewShaderImages, "a sibling's upload moved the image window's input"); + EXPECT_EQ(WalkA(), 0u); + } + + // F-3 across contexts: set_framebuffer_state inlines an attachment's format and extent, so + // B redefining the storage of a texture attached to A's framebuffer has to re-emit A's. + TEST_F(TrackerShareGroup, ATextureStorageDefinitionThroughASiblingFiresTheFramebufferBit) { + const auto& tex = m_a->CreateTextureObject(1, TextureTarget::Texture2D); + ASSERT_TRUE(tex != nullptr); + const auto& fbo = m_a->CreateFramebufferObject(1); + ASSERT_TRUE(fbo != nullptr); + fbo->AttachTexture(FramebufferAttachmentType::Color0, tex, TextureUploadTarget::Texture2D); + m_a->GetFramebufferBindingSlot(FramebufferTarget::Draw).Bind(fbo); + PrimeA(); + + MakeLive(m_b); + tex->SetInternalFormat(MobileGL::TextureInternalFormat::RGBA8); + const Uint32 dirty = WalkA(); + ExpectFired(dirty, MGPipeDirty::NewFramebuffer, "A's attachment was redefined through B (F-3)"); + ExpectFired(dirty, MGPipeDirty::NewSamplerViews, "the definition moved the params aggregate too"); + EXPECT_EQ(WalkA(), 0u); + } + + TEST_F(TrackerShareGroup, ARenderbufferStorageThroughASiblingFiresTheFramebufferBit) { + const auto& rbo = m_a->CreateRenderbufferObject(1); + ASSERT_TRUE(rbo != nullptr); + rbo->AllocateStorage(IntVec2{4, 4}); + const auto& fbo = m_a->CreateFramebufferObject(1); + ASSERT_TRUE(fbo != nullptr); + fbo->AttachRenderbuffer(FramebufferAttachmentType::Color0, rbo); + m_a->GetFramebufferBindingSlot(FramebufferTarget::Draw).Bind(fbo); + PrimeA(); + + MakeLive(m_b); + rbo->AllocateStorage(IntVec2{8, 8}); + const Uint32 dirty = WalkA(); + ExpectFired(dirty, MGPipeDirty::NewFramebuffer, "A's attached renderbuffer was resized through B"); + EXPECT_EQ(WalkA(), 0u); + } + + TEST_F(TrackerShareGroup, ASamplerParameterThroughASiblingFiresTheSamplerBit) { + const auto& sampler = m_a->CreateSamplerObject(1); + ASSERT_TRUE(sampler != nullptr); + PrimeA(); + + MakeLive(m_b); + sampler->SetWrapS(MobileGL::SamplerWrapMode::ClampToEdge); + const Uint32 dirty = WalkA(); + ExpectFired(dirty, MGPipeDirty::NewSamplers, "a sibling's glSamplerParameteri moved the CSO"); + EXPECT_EQ(WalkA(), 0u); + } + + // The split shape: one tracker per context, and B walks as well. B's own walk sees its own + // aggregate; A's sees the clock. + TEST_F(TrackerShareGroup, EachContextsOwnTrackerSeesTheSiblingsChange) { + const auto& tex = m_a->CreateTextureObject(1, TextureTarget::Texture2D); + ASSERT_TRUE(tex != nullptr); + MGPipeTracker trackerB; + PrimeA(); + MakeLive(m_b); + trackerB.Update(*m_b, MGPipeVerbClass::kDraw); + ASSERT_EQ(trackerB.Update(*m_b, MGPipeVerbClass::kDraw), 0u); + + tex->SetMaxLevel(4); + EXPECT_NE(trackerB.Update(*m_b, MGPipeVerbClass::kDraw) & MGPipeDirtyBit(MGPipeDirty::NewSamplers), 0u); + ExpectFired(WalkA(), MGPipeDirty::NewSamplers, "B's tracker saw the change and A's did not"); + EXPECT_EQ(WalkA(), 0u); + MakeLive(m_b); + EXPECT_EQ(trackerB.Update(*m_b, MGPipeVerbClass::kDraw), 0u) + << "B fired again with nothing moved: its own change reached it twice"; + } + + // The claim the share clocks rest on for the program bits: a program is a group object too, + // but bits 6-8 read its versions off the object itself, so a change through B already + // reaches A's walk with no clock at all. + TEST_F(TrackerShareGroup, AProgramChangeThroughASiblingNeedsNoShareClock) { + const Uint program = m_a->CreateProgram(); + m_a->UseProgram(program); + PrimeA(); + + MakeLive(m_b); + m_b->GetProgramObject(program)->MarkUBOContentDirty(); + ExpectFired(WalkA(), MGPipeDirty::NewGlobalConstants, "a sibling's glUniform* on the program in use"); + } + } // namespace