diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 0c2ec66d..04d08f33 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -61,6 +61,12 @@ namespace MobileGL::MG_Config { // per-draw glBufferSubData path instead of the persistent-mapped ring allocator // (negative control / driver-bug escape hatch). Bool DisableUboRing = false; + // MOBILEGL_RELAXED_SEMANTICS: relax strict core-profile rules (e.g. VAO-0 draws, + // texture-name reuse after delete) even on contexts that explicitly requested a core + // profile. Without it, relaxed semantics still apply to every context that did not + // explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1 + // version request. + Bool RelaxedSemantics = false; }; extern FeaturesTable Features; } // namespace MobileGL::MG_Config diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 8a110027..f231bbc3 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -122,6 +122,7 @@ namespace MobileGL::MG_ConfigLoader { features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH"); features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY"); features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING"); + features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS"); } inline void InitBackendType() { diff --git a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp index af5492cb..898c3253 100644 --- a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp +++ b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp @@ -67,7 +67,7 @@ namespace MobileGL::MG_Impl::GLImpl { } const auto& vao = MG_State::pGLContext->GetBoundVertexArray(); - if (MG_State::pEGLContext->IsCurrentContextOpenGLCoreProfile() && vao && vao->GetExternalIndex() == 0) { + if (vao && vao->GetExternalIndex() == 0 && !MG_State::IsRelaxedSemanticsActive()) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", functionName, diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index 65793186..869dd4a4 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -1734,7 +1734,11 @@ namespace MobileGL::MG_Impl::GLImpl { *params = 1024 * 1024; // TODO return; case GL_CONTEXT_PROFILE_MASK: - *params = GL_CONTEXT_CORE_PROFILE_BIT; + // Reports the requested context profile (EGL defaults 3.x contexts to core); + // MOBILEGL_RELAXED_SEMANTICS loosens behavior without changing the identity. + *params = MG_State::pEGLContext && MG_State::pEGLContext->IsCurrentContextOpenGLCompatibilityProfile() + ? GL_CONTEXT_COMPATIBILITY_PROFILE_BIT + : GL_CONTEXT_CORE_PROFILE_BIT; return; default: break; diff --git a/MobileGL/MG_State/EGLState/Core.cpp b/MobileGL/MG_State/EGLState/Core.cpp index b24bec5b..dcc1f6f4 100644 --- a/MobileGL/MG_State/EGLState/Core.cpp +++ b/MobileGL/MG_State/EGLState/Core.cpp @@ -764,6 +764,20 @@ namespace MobileGL { ctx->MajorVersion > 3 || (ctx->MajorVersion == 3 && ctx->MinorVersion >= 1); } + Bool EGLContext::IsCurrentContextOpenGLCompatibilityProfile() const { + const std::lock_guard lock(m_mutex); + auto currentIt = m_threadCurrents.find(CurrentThreadKey()); + if (currentIt == m_threadCurrents.end()) { + return false; + } + const auto* ctx = TryGetContext(currentIt->second.Context); + // Affirmative check: true only when the host explicitly requested the + // compatibility bit. Attrib-less contexts report false and thus read as core + // for GL_CONTEXT_PROFILE_MASK (EGL defaults 3.x contexts to the core profile). + return ctx && ctx->ClientAPI == EGL_OPENGL_API && + (ctx->OpenGLProfileMask & EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT); + } + EGLint EGLContext::GetCurrentContextFlags() const { const std::lock_guard lock(m_mutex); auto currentIt = m_threadCurrents.find(CurrentThreadKey()); diff --git a/MobileGL/MG_State/EGLState/Core.h b/MobileGL/MG_State/EGLState/Core.h index 8c42c813..41821186 100644 --- a/MobileGL/MG_State/EGLState/Core.h +++ b/MobileGL/MG_State/EGLState/Core.h @@ -59,6 +59,7 @@ namespace MobileGL { Bool ValidateContext(EGLContextHandle context) const; Bool ValidateContextOnDisplay(EGLDisplayHandle display, EGLContextHandle context) const; Bool IsCurrentContextOpenGLCoreProfile() const; + Bool IsCurrentContextOpenGLCompatibilityProfile() const; EGLint GetCurrentContextFlags() const; // Surface diff --git a/MobileGL/MG_State/GLState/Core.cpp b/MobileGL/MG_State/GLState/Core.cpp index 9fad6cce..6101ce9c 100644 --- a/MobileGL/MG_State/GLState/Core.cpp +++ b/MobileGL/MG_State/GLState/Core.cpp @@ -9,6 +9,7 @@ #include "Core.h" #include "MG_State/GLState/RenderbufferState/RenderbufferObject.h" #include "MG_State/EGLState/Core.h" +#include namespace MobileGL::MG_State { void Init() { @@ -17,6 +18,11 @@ namespace MobileGL::MG_State { pEGLContext = MakeUnique(); } + Bool IsRelaxedSemanticsActive() { + return MG_Config::Features.RelaxedSemantics || + !(pEGLContext && pEGLContext->IsCurrentContextOpenGLCoreProfile()); + } + namespace GLState { // Error void GLContext::RecordError(ErrorCode code, UniquePtr info) { @@ -235,7 +241,7 @@ namespace MobileGL::MG_State { } void GLContext::MarkTextureObjectForDeletion(Uint index) { - m_textureState.MarkTextureObjectForDeletion(index); + m_textureState.MarkTextureObjectForDeletion(index, IsRelaxedSemanticsActive()); } TextureUnit& GLContext::GetTextureUnitObject(Int unit) { diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index d3f20539..58369501 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -252,5 +252,12 @@ namespace MobileGL { } // namespace GLState extern UniquePtr pGLContext; + + // True when relaxed GL semantics apply. Strict core rules are enforced only when the + // current EGL context explicitly requested a core profile (core bit in + // EGL_CONTEXT_OPENGL_PROFILE_MASK, or a >=3.1 version request without the compatibility + // bit) and MOBILEGL_RELAXED_SEMANTICS is off; no current context, legacy version + // requests, and the compatibility bit all relax. + Bool IsRelaxedSemanticsActive(); } // namespace MG_State } // namespace MobileGL diff --git a/MobileGL/MG_State/GLState/TextureState/TextureState.cpp b/MobileGL/MG_State/GLState/TextureState/TextureState.cpp index c4847985..876c3134 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureState.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureState.cpp @@ -96,7 +96,7 @@ namespace MobileGL::MG_State::GLState { return textureObject; } - void TextureState::MarkTextureObjectForDeletion(Uint index) { + void TextureState::MarkTextureObjectForDeletion(Uint index, Bool keepUnboundReservation) { if (m_indexGenerator.IsValid(index)) { auto it = m_textureObjects.find(index); if (it != m_textureObjects.end()) { @@ -123,10 +123,15 @@ namespace MobileGL::MG_State::GLState { BumpTextureBindGeneration(); m_textureObjects.erase(index); m_indexGenerator.Delete(index); + } else if (!keepUnboundReservation) { + // GL 3.3 core 3.8.1 makes a deleted name unused again even when GenTextures only + // reserved it and no bind ever instantiated an object (so a later bind of it must + // fail), and the reservation has to return to the free list. + m_indexGenerator.Delete(index); } - // Compatibility: legacy Minecraft may delete a generated name before its first bind, - // then bind and populate that same name. Keep such a reservation alive; only a real - // texture object reaching deletion releases its index above. + // Relaxed semantics: legacy apps may delete a generated name before its first + // bind, then bind and populate that same name. Keep such a reservation alive there; + // a real texture object reaching deletion still releases its index above. } } diff --git a/MobileGL/MG_State/GLState/TextureState/TextureState.h b/MobileGL/MG_State/GLState/TextureState/TextureState.h index 6f6af3a2..be2b8260 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureState.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureState.h @@ -60,7 +60,7 @@ namespace MobileGL::MG_State::GLState { const ImageTextureBinding& GetImageTextureBinding(Int unit) const; Int GetActiveTextureUnit() const; void SetActiveTextureUnit(Int unit); - void MarkTextureObjectForDeletion(Uint index); + void MarkTextureObjectForDeletion(Uint index, Bool keepUnboundReservation); Bool ValidateName(Uint index) const; Bool ValidateTextureObject(Uint index) const; diff --git a/MobileGL/MG_Test/EGLState/EGLStateTest.cpp b/MobileGL/MG_Test/EGLState/EGLStateTest.cpp index 33135715..d53b08a9 100644 --- a/MobileGL/MG_Test/EGLState/EGLStateTest.cpp +++ b/MobileGL/MG_Test/EGLState/EGLStateTest.cpp @@ -102,3 +102,33 @@ TEST(EGLStateMakeCurrent, SameThreadRepeatedAttachReleaseDoesNotLeaveStaleOwner) EXPECT_TRUE(fixture->State.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); EXPECT_EQ(fixture->State.ConsumeError(), EGL_SUCCESS); } + +// The compatibility-profile accessor is affirmative-only (it backs GL_CONTEXT_PROFILE_MASK +// reporting): attrib-less contexts (profile mask 0) and released threads both answer "not +// compat" and therefore read as core-profile contexts. +TEST(EGLStateProfile, CompatibilityProfileRequiresExplicitCompatBit) { + auto fixture = CreateFixture(); + + EXPECT_FALSE(fixture->State.IsCurrentContextOpenGLCompatibilityProfile()); + + EXPECT_TRUE(fixture->State.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface, fixture->Context)); + EXPECT_FALSE(fixture->State.IsCurrentContextOpenGLCompatibilityProfile()); + + const EGLint compatAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, + 3, + EGL_CONTEXT_MINOR_VERSION, + 3, + EGL_CONTEXT_OPENGL_PROFILE_MASK, + EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT, + EGL_NONE}; + const auto compatContext = + fixture->State.CreateContext(fixture->Display, fixture->Config, EGL_NO_CONTEXT, compatAttribs); + ASSERT_NE(compatContext, EGL_NO_CONTEXT); + EXPECT_TRUE(fixture->State.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface, compatContext)); + EXPECT_TRUE(fixture->State.IsCurrentContextOpenGLCompatibilityProfile()); + EXPECT_FALSE(fixture->State.IsCurrentContextOpenGLCoreProfile()); + + EXPECT_TRUE(fixture->State.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); + EXPECT_FALSE(fixture->State.IsCurrentContextOpenGLCompatibilityProfile()); + EXPECT_EQ(fixture->State.ConsumeError(), EGL_SUCCESS); +} diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 476e749d..c16a07a7 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -12,12 +12,14 @@ #include "Includes.h" #include "Init.h" +#include #include #include #include #include #include #include +#include #include #include #include @@ -380,10 +382,106 @@ TEST_F(TextureTest, GenThenBindCreatesObjectForUnsizedPackedBgraSubImageUpload) EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } -// Legacy Minecraft reserves a texture name, deletes it before first bind, then reuses the same -// name for the atlas upload. Preserve that generated reservation so the later bind can instantiate -// the object and subsequent sub-image uploads target it instead of the default texture. -TEST_F(TextureTest, DeleteGeneratedReservationThenBindCreatesObjectForSubImageUpload) { +namespace { + // Strict core rules only apply when the current EGL context explicitly requested a core + // profile; the suite's default (no current context) runs with relaxed semantics. RAII so + // a failed ASSERT cannot leave the context current for the rest of the suite. + struct ScopedCoreProfileContext { + ScopedCoreProfileContext() { + auto& egl = *MG_State::pEGLContext; + m_display = egl.GetDisplay(EGL_DEFAULT_DISPLAY); + EXPECT_NE(m_display, EGL_NO_DISPLAY); + EXPECT_TRUE(egl.InitializeDisplay(m_display, nullptr, nullptr)); + EGLint configCount = 0; + EXPECT_TRUE(egl.ChooseConfig(m_display, nullptr, &m_config, 1, &configCount)); + const EGLint surfaceAttribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE}; + m_surface = egl.CreatePbufferSurface(m_display, m_config, surfaceAttribs); + EXPECT_NE(m_surface, EGL_NO_SURFACE); + const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, + 3, + EGL_CONTEXT_MINOR_VERSION, + 3, + EGL_CONTEXT_OPENGL_PROFILE_MASK, + EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, + EGL_NONE}; + m_context = egl.CreateContext(m_display, m_config, EGL_NO_CONTEXT, contextAttribs); + EXPECT_NE(m_context, EGL_NO_CONTEXT); + EXPECT_TRUE(egl.MakeCurrent(m_display, m_surface, m_surface, m_context)); + } + ~ScopedCoreProfileContext() { + auto& egl = *MG_State::pEGLContext; + egl.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if (m_context != EGL_NO_CONTEXT) egl.DestroyContext(m_display, m_context); + if (m_surface != EGL_NO_SURFACE) egl.DestroySurface(m_display, m_surface); + } + ScopedCoreProfileContext(const ScopedCoreProfileContext&) = delete; + ScopedCoreProfileContext& operator=(const ScopedCoreProfileContext&) = delete; + + private: + EGLDisplay m_display = EGL_NO_DISPLAY; + EGLConfig m_config = nullptr; + EGLSurface m_surface = EGL_NO_SURFACE; + MG_State::EGLState::EGLContext::EGLContextHandle m_context = EGL_NO_CONTEXT; + }; + + // MOBILEGL_RELAXED_SEMANTICS loosens strict core rules even on explicit core-profile + // contexts. RAII so a failed ASSERT cannot leak the flag into the rest of the suite. + struct ScopedRelaxedSemantics { + ScopedRelaxedSemantics(): m_previous(MG_Config::Features.RelaxedSemantics) { + MG_Config::Features.RelaxedSemantics = true; + } + ~ScopedRelaxedSemantics() { + MG_Config::Features.RelaxedSemantics = m_previous; + } + ScopedRelaxedSemantics(const ScopedRelaxedSemantics&) = delete; + ScopedRelaxedSemantics& operator=(const ScopedRelaxedSemantics&) = delete; + + private: + Bool m_previous; + }; +} // namespace + +// GL 3.3 core 3.8.1: on an explicit core-profile context, DeleteTextures makes the name unused +// again whether or not a bind ever instantiated an object, so the reservation must go back to +// the generator's free list rather than leaking, and binding the dead name afterwards must fail. +TEST_F(TextureTest, DeleteGeneratedButUnboundNameReleasesReservationAndBindFails) { + ScopedCoreProfileContext coreContext; + ASSERT_FALSE(MG_State::IsRelaxedSemanticsActive()); + + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + ASSERT_NE(texture, 0u); + ASSERT_TRUE(MG_State::pGLContext->ValidateTextureName(texture)); + ASSERT_FALSE(MG_State::pGLContext->ValidateTextureObject(texture)); + + MG_Impl::GLImpl::DeleteTextures(1, &texture); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureName(texture)); + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(texture)); + // IsTexture answers about a dead name without raising anything (GL 3.3 core 6.1.4). + EXPECT_EQ(MG_Impl::GLImpl::IsTexture(texture), GL_FALSE); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + ExpectSingleGlError(GL_INVALID_OPERATION); + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(texture)); + + // The freed reservation is recycled (the generator's free list is LIFO, so the very same + // name comes back) - a delete that skipped the release would hand out a fresh name here. + GLuint recycled = 0; + MG_Impl::GLImpl::GenTextures(1, &recycled); + EXPECT_EQ(recycled, texture); + EXPECT_TRUE(MG_State::pGLContext->ValidateTextureName(recycled)); +} + +// Relaxed semantics - the default whenever the context did not explicitly request a core +// profile: legacy Minecraft reserves a texture name, deletes it before first bind, then reuses +// the same name for the atlas upload. Preserve that generated reservation so the later bind can +// instantiate the object and subsequent sub-image uploads target it instead of the default +// texture. Explicit core contexts keep the strict delete semantics asserted above. +TEST_F(TextureTest, RelaxedDefaultDeleteGeneratedReservationThenBindCreatesObjectForSubImageUpload) { + ASSERT_TRUE(MG_State::IsRelaxedSemanticsActive()); + GLuint texture = 0; MG_Impl::GLImpl::GenTextures(1, &texture); ASSERT_NE(texture, 0u); @@ -424,6 +522,28 @@ TEST_F(TextureTest, DeleteGeneratedReservationThenBindCreatesObjectForSubImageUp EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +// MOBILEGL_RELAXED_SEMANTICS wins even on an explicit core-profile context: the deleted +// reservation survives and the name stays bindable. +TEST_F(TextureTest, RelaxedSemanticsOverrideKeepsDeletedReservationOnCoreProfileContext) { + ScopedCoreProfileContext coreContext; + ScopedRelaxedSemantics relaxedSemantics; + ASSERT_TRUE(MG_State::IsRelaxedSemanticsActive()); + + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + ASSERT_NE(texture, 0u); + MG_Impl::GLImpl::DeleteTextures(1, &texture); + EXPECT_TRUE(MG_State::pGLContext->ValidateTextureName(texture)); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + EXPECT_TRUE(MG_State::pGLContext->ValidateTextureObject(texture)); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); + MG_Impl::GLImpl::DeleteTextures(1, &texture); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + TEST_F(TextureTest, DeleteInstantiatedTextureInvalidatesNameUntilRegenerated) { GLuint textures[2] = {}; MG_Impl::GLImpl::GenTextures(2, textures); diff --git a/android-plugin/app/build.gradle.kts b/android-plugin/app/build.gradle.kts index 06f92f74..32c54220 100644 --- a/android-plugin/app/build.gradle.kts +++ b/android-plugin/app/build.gradle.kts @@ -87,6 +87,7 @@ val pluginRendererConfig = buildJsonValue { customizable("MOBILEGL_MAGMA_FRAMESINFLIGHT", "3", RendererConfig.MetaString("mobilegl_magma_frames_inflight_title")) toggleable("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER", "1", false, RendererConfig.MetaString("mobilegl_avoid_sampler_mipmap_min_filter_title")) toggleable("MOBILEGL_COHERENT_AS_FLUSH", "1", false, RendererConfig.MetaString("mobilegl_coherent_as_flush_title")) + toggleable("MOBILEGL_RELAXED_SEMANTICS", "1", false, RendererConfig.MetaString("mobilegl_relaxed_semantics_title")) toggleable("MOBILEGL_USE_ANGLE", "1", false, RendererConfig.MetaString("mobilegl_use_angle_title")) }, minMCVer = null, diff --git a/android-plugin/app/src/main/res/values/strings.xml b/android-plugin/app/src/main/res/values/strings.xml index dee03f1d..082044f3 100644 --- a/android-plugin/app/src/main/res/values/strings.xml +++ b/android-plugin/app/src/main/res/values/strings.xml @@ -9,5 +9,6 @@ Avoid sampler mipmap minification filters Treat explicit-flush persistent maps as coherent Use ANGLE GLES libraries + Relaxed GL semantics (legacy app leniencies) DirectGLES