[Fix] (Pipe): share-group clocks fire dirty bits 11-14 for sibling-context changes

This commit is contained in:
2026-10-10 04:14:07 -04:00
parent 9a1d3062cb
commit 8ebb69d895
10 changed files with 517 additions and 4 deletions
+45
View File
@@ -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
@@ -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
@@ -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<const char*>(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<EGLDisplay>(m_display), static_cast<EGLConfig>(m_config),
static_cast<EGLContext>(m_context), contextAttribs);
return context == EGL_NO_CONTEXT ? nullptr : context;
}
bool HeadlessGL::MakeContextCurrent(void* context) {
if (!m_usable) return false;
EGLSurface surface = static_cast<EGLSurface>(m_surface);
return eglMakeCurrent(static_cast<EGLDisplay>(m_display), surface, surface,
static_cast<EGLContext>(context != nullptr ? context : m_context)) == EGL_TRUE;
}
void HeadlessGL::DestroySharingContext(void* context) {
if (!m_usable || context == nullptr) return;
MakeContextCurrent(nullptr);
eglDestroyContext(static_cast<EGLDisplay>(m_display), static_cast<EGLContext>(context));
}
void HeadlessGL::ShutDown() {
if (!m_usable) return;
EGLDisplay display = static_cast<EGLDisplay>(m_display);
@@ -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 -------------------------------------------
@@ -100,6 +100,7 @@ set(MGL_P8A_AMBIENT_CASES
ResidentIndexScenario.*
SampleMaskScopeScenario.*
SampleVariablesScenario.*
SharedContextChangeScenario.*
SnormAttachmentScenario.*
SpirvShaderBinaryScenario.SpecializedModulesLinkAndRenderWithTheirConstantsApplied
SsboArrayDynamicIndexScenario.*
@@ -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 <cstddef>
#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 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<unsigned char> green(static_cast<std::size_t>(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<GLenum>(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<GLenum>(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
+2
View File
@@ -1377,6 +1377,8 @@ namespace MobileGL::MG_State {
GLContext::GLContext(SharedPtr<ShareGroupState> shareGroup)
: m_shareGroup(shareGroup ? Move(shareGroup) : MakeShared<ShareGroupState>()) {
// 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
+20 -4
View File
@@ -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();
}
@@ -8,6 +8,7 @@
#pragma once
#include <Includes.h>
#include <atomic>
#include <mutex>
#include <MG_Util/Miscellany/IndexGenerator.h>
#include "BufferState/BufferObject.h"
@@ -74,7 +75,42 @@ namespace MobileGL::MG_State::GLState {
UniquePtr<BufferObject> TakeClientArrayBuffer();
void ReturnClientArrayBuffer(UniquePtr<BufferObject> 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<SizeT>(clock)].fetch_add(1, std::memory_order_release);
}
Uint64 ReadSharedClock(SharedClock clock) const {
return m_sharedClocks[static_cast<SizeT>(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<Uint64> m_sharedClocks[static_cast<SizeT>(SharedClock::Count)] = {};
std::atomic<Uint32> m_attachedContexts{0};
Uint64 m_token = 0;
UnorderedMap<Uint, SharedPtr<BufferObject>> m_bufferObjects;
IndexGenerator<Uint> m_bufferNames{1024, 1};
+161
View File
@@ -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<MG_State::GLState::ShareGroupState>();
m_a = MakeShared<GLContext>(group);
m_b = MakeShared<GLContext>(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<GLContext>& 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<SizeT>(bit)] << " did not fire on A: " << why;
}
MGPipeTracker m_tracker;
SharedPtr<GLContext> m_a;
SharedPtr<GLContext> m_b;
SharedPtr<GLContext> 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<TextureObjectBase*>(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