mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Fix] (Pipe): share-group clocks fire dirty bits 11-14 for sibling-context changes
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user