mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Fix] (MG_State, MG_Impl, MG_Test): enforce strict GL 3.3 core rules only on contexts that explicitly request a core profile - texture deleted-name reservation keep and VAO-0 draws relax otherwise or under MOBILEGL_RELAXED_SEMANTICS, and GL_CONTEXT_PROFILE_MASK reports the requested profile
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -764,6 +764,20 @@ namespace MobileGL {
|
||||
ctx->MajorVersion > 3 || (ctx->MajorVersion == 3 && ctx->MinorVersion >= 1);
|
||||
}
|
||||
|
||||
Bool EGLContext::IsCurrentContextOpenGLCompatibilityProfile() const {
|
||||
const std::lock_guard<std::recursive_mutex> 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<std::recursive_mutex> lock(m_mutex);
|
||||
auto currentIt = m_threadCurrents.find(CurrentThreadKey());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "Core.h"
|
||||
#include "MG_State/GLState/RenderbufferState/RenderbufferObject.h"
|
||||
#include "MG_State/EGLState/Core.h"
|
||||
#include <Config.h>
|
||||
|
||||
namespace MobileGL::MG_State {
|
||||
void Init() {
|
||||
@@ -17,6 +18,11 @@ namespace MobileGL::MG_State {
|
||||
pEGLContext = MakeUnique<EGLState::EGLContext>();
|
||||
}
|
||||
|
||||
Bool IsRelaxedSemanticsActive() {
|
||||
return MG_Config::Features.RelaxedSemantics ||
|
||||
!(pEGLContext && pEGLContext->IsCurrentContextOpenGLCoreProfile());
|
||||
}
|
||||
|
||||
namespace GLState {
|
||||
// Error
|
||||
void GLContext::RecordError(ErrorCode code, UniquePtr<ErrorInfo> 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) {
|
||||
|
||||
@@ -252,5 +252,12 @@ namespace MobileGL {
|
||||
} // namespace GLState
|
||||
|
||||
extern UniquePtr<GLState::GLContext> 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
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -12,12 +12,14 @@
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <Config.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Backend/DirectGLES/Managers.h>
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||
#include <MG_Impl/GLImpl/Sampler/GL_Sampler.h>
|
||||
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
||||
#include <MG_State/EGLState/Core.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject.h>
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -9,5 +9,6 @@
|
||||
<string name="mobilegl_avoid_sampler_mipmap_min_filter_title">Avoid sampler mipmap minification filters</string>
|
||||
<string name="mobilegl_coherent_as_flush_title">Treat explicit-flush persistent maps as coherent</string>
|
||||
<string name="mobilegl_use_angle_title">Use ANGLE GLES libraries</string>
|
||||
<string name="mobilegl_relaxed_semantics_title">Relaxed GL semantics (legacy app leniencies)</string>
|
||||
<string name="mobilegl_default_backend" translatable="false">DirectGLES</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user