// MobileGL - MobileGL/ConfigLoader.cpp // Copyright (c) 2025-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 #include "Config.h" #include #include #ifndef _WIN32 extern char** environ; #endif namespace MobileGL::MG_Config { // Zero/default-initialized at static-init time (all fields have constexpr-friendly // defaults), so it is safe to read even if MG_ConfigLoader::Init has not run yet. FeaturesTable Features; } // namespace MobileGL::MG_Config namespace MobileGL::MG_ConfigLoader { static UniquePtr> acceptedEnvVariablesMap; static Bool IsAcceptedPrefix(const String& key) { return (key.compare(0, 6, "LIBGL_") == 0 || key.compare(0, 9, "MOBILEGL_") == 0); } inline void InitializeAcceptedEnvVariables() { if (!acceptedEnvVariablesMap) { acceptedEnvVariablesMap = MakeUnique>(); } else { acceptedEnvVariablesMap->clear(); } char** envPtr = nullptr; #ifdef _WIN32 envPtr = _environ; #else // POSIX envPtr = ::environ; #endif if (envPtr == nullptr) return; for (char** env = envPtr; *env != nullptr; ++env) { String entry(*env); SizeT pos = entry.find('='); if (pos != String::npos) { String key = entry.substr(0, pos); String value = entry.substr(pos + 1); if (IsAcceptedPrefix(key)) { (*acceptedEnvVariablesMap)[key] = value; MGLOG_D("Config: Accepted env variable: %s=%s", key.c_str(), value.c_str()); } } } } inline void QueryEnvVariable(const String& key, String& outValue, const String& defaultValue) { auto it = acceptedEnvVariablesMap->find(key); if (it != acceptedEnvVariablesMap->end()) { outValue = it->second; } else { outValue = defaultValue; } } // Unified truthy rule for boolean feature env variables: set, non-empty, not "0", // and not "false" (case-insensitive). static Bool IsTruthyValue(const String& value) { if (value.empty() || value == "0") { return false; } String lowered = value; std::transform(lowered.begin(), lowered.end(), lowered.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return lowered != "false"; } inline Bool QueryEnvFlag(const String& key) { auto it = acceptedEnvVariablesMap->find(key); return it != acceptedEnvVariablesMap->end() && IsTruthyValue(it->second); } // Quirk overrides are tri-state: an unset variable keeps device auto-detection, a truthy // value forces the quirk on, anything else set ("0", "false", "") forces it off. inline MG_Config::QuirkOverride QueryEnvQuirkOverride(const String& key) { auto it = acceptedEnvVariablesMap->find(key); if (it == acceptedEnvVariablesMap->end()) { return MG_Config::QuirkOverride::Auto; } return IsTruthyValue(it->second) ? MG_Config::QuirkOverride::ForceOn : MG_Config::QuirkOverride::ForceOff; } // Multi-draw mode is a named-value preference: unset keeps Auto (best supported tier), // a recognized name selects that tier as the ceiling, anything else warns and keeps Auto. inline MG_Config::MultiDrawMode QueryEnvMultiDrawMode(const String& key) { auto it = acceptedEnvVariablesMap->find(key); if (it == acceptedEnvVariablesMap->end()) { return MG_Config::MultiDrawMode::Auto; } String lowered = it->second; std::transform(lowered.begin(), lowered.end(), lowered.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (lowered == "ext") return MG_Config::MultiDrawMode::Ext; if (lowered == "indirect") return MG_Config::MultiDrawMode::Indirect; if (lowered == "unroll") return MG_Config::MultiDrawMode::Unroll; if (lowered.empty() || lowered == "auto") return MG_Config::MultiDrawMode::Auto; MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected ext|indirect|unroll|auto, using auto", key.c_str(), it->second.c_str()); return MG_Config::MultiDrawMode::Auto; } // Same contract as QueryEnvMultiDrawMode, over the DirectGLES tier names. inline MG_Config::GLESMultiDrawMode QueryEnvGLESMultiDrawMode(const String& key) { auto it = acceptedEnvVariablesMap->find(key); if (it == acceptedEnvVariablesMap->end()) { return MG_Config::GLESMultiDrawMode::Auto; } String lowered = it->second; std::transform(lowered.begin(), lowered.end(), lowered.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (lowered == "ext") return MG_Config::GLESMultiDrawMode::Ext; if (lowered == "multiindirect") return MG_Config::GLESMultiDrawMode::MultiIndirect; if (lowered == "indirect") return MG_Config::GLESMultiDrawMode::Indirect; if (lowered == "basevertex") return MG_Config::GLESMultiDrawMode::BaseVertex; if (lowered == "drawelements") return MG_Config::GLESMultiDrawMode::DrawElements; if (lowered == "compute") return MG_Config::GLESMultiDrawMode::Compute; if (lowered.empty() || lowered == "auto") return MG_Config::GLESMultiDrawMode::Auto; MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected " "ext|multiindirect|indirect|basevertex|drawelements|compute|auto, using auto", key.c_str(), it->second.c_str()); return MG_Config::GLESMultiDrawMode::Auto; } inline Uint32 QueryEnvUint32(const String& key, Uint32 defaultValue, Uint32 minValue, Uint32 maxValue) { auto it = acceptedEnvVariablesMap->find(key); if (it == acceptedEnvVariablesMap->end()) { return defaultValue; } const String& value = it->second; char* parseEnd = nullptr; errno = 0; const unsigned long parsedValue = std::strtoul(value.c_str(), &parseEnd, 10); if (parseEnd == value.c_str() || *parseEnd != '\0' || errno == ERANGE || parsedValue < minValue || parsedValue > maxValue) { MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected an integer in range [%u, %u], " "using default %u", key.c_str(), value.c_str(), minValue, maxValue, defaultValue); return defaultValue; } return static_cast(parsedValue); } inline void InitFeatures() { auto& features = MG_Config::Features; features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY"); features.EnableSpirvValidation = QueryEnvFlag("MOBILEGL_ENABLE_SPIRV_VALIDATION"); features.UseAngle = QueryEnvFlag("MOBILEGL_USE_ANGLE"); #if defined(MOBILEGL_TRACE_ANGLE_VARIANTS) QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, ""); #endif features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP"); features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP"); features.FixIterationRPSubgroupScratch = QueryEnvQuirkOverride("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH"); features.IterationRPFixBarrier = QueryEnvFlag("MOBILEGL_ITERATIONRP_FIX_BARRIER"); features.DeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_DERIVE_NUM_SUBGROUPS"); features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64"); features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK"); features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64); features.AvoidSamplerMipmapMinFilter = QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER"); features.AvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_AVOID_EXPLICIT_LOD_BIAS"); features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH"); features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY"); features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING"); features.EsprytForceDepthStencilReadbackEmulation = QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION"); features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS"); features.MagmaDisableBlendedDepthWriteQuirk = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE"); features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS"); features.MagmaMultiDrawMode = QueryEnvMultiDrawMode("MOBILEGL_MAGMA_MULTIDRAW_MODE"); features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE"); features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE"); features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64); features.AsyncOptimisticShaderStatus = QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS"); features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE"); features.ViewportArrayEmulation = QueryEnvQuirkOverride("MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION"); } inline void InitBackendType() { String backendTypeStr; QueryEnvVariable("MOBILEGL_BACKEND_TYPE", backendTypeStr, "DirectGLES"); #define ENTRY(backendType) \ if (backendTypeStr == #backendType) { \ MG_Config::ActiveBackendType = BackendType::backendType; \ MGLOG_I("Config: Active backend type set to " #backendType); \ return; \ } ENTRY(DirectGLES) ENTRY(DirectVulkan) ENTRY(Unknown) MG_Config::ActiveBackendType = BackendType::Unknown; #undef ENTRY } void Init() { MGLOG_D("Loading configuration from environment variables..."); InitializeAcceptedEnvVariables(); InitBackendType(); InitFeatures(); // Destroy the map since we won't need it anymore acceptedEnvVariablesMap.reset(); } } // namespace MobileGL::MG_ConfigLoader