Files
MobileGL/MobileGL/ConfigLoader.cpp
T

703 lines
40 KiB
C++

// 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
#if defined(__ANDROID__)
#include <sys/system_properties.h>
#endif
#include "Config.h"
// For kMGPipeSubsystemsMigratedAtP5e, the push build's PipePush default (the P2, P3a and P4a
// constants beside it are the phase-by-phase controls, not the default). Push-only, so the
// pull build's translation unit is unchanged.
#include <MG_Pipe/MGPipe.h>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
// std::strcmp: InitIpc's server-role check (P12) and the system configuration's.
#include <cstring>
#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;
#if MOBILEGL_BUILD_DISAGGREGATED
// Same contract, and for the same reason: MG_Backend::Init() reads Transport, and a
// build order that put it before MG_ConfigLoader::Init() must see Monolith rather than
// a torn enum. Defined only here - in a pull build Config.h makes Transport a constexpr
// and there is nothing to define.
TransportMode Transport = TransportMode::Monolith;
String TransportEndpoint;
Bool SplitTransportRequestedByConfig = false;
IpcTable Ipc;
#endif
} // namespace MobileGL::MG_Config
namespace MobileGL::MG_ConfigLoader {
static UniquePtr<UnorderedMap<String, String>> acceptedEnvVariablesMap;
static Bool IsAcceptedPrefix(const String& key) {
return (key.compare(0, 6, "LIBGL_") == 0 || key.compare(0, 9, "MOBILEGL_") == 0);
}
// ---- the system configuration -----------------------------------------------------------
//
// A library that is every process's GL implementation (installed as the system's EGL/GLX
// vendor) cannot rely on each process having been started with MOBILEGL_* in its environment:
// most are started by something that has never heard of it. So the settings a whole machine
// shares live in files, and the environment only overrides them:
//
// environment > /etc/mobilegl/backend (MOBILEGL_BACKEND_TYPE only) > client.conf > built-in
//
// client.conf is `KEY=value` lines; blank lines and lines starting with '#' are skipped, the
// key and the value are trimmed, and one pair of matching quotes around the value is removed.
// Only MOBILEGL_* / LIBGL_* keys are taken, exactly as from the environment. The backend file
// holds just the backend's name (the file the backend switch rewrites), and names it over a
// MOBILEGL_BACKEND_TYPE line in client.conf so that switch cannot be silently shadowed.
//
// MOBILEGL_CONFIG_FILE / MOBILEGL_BACKEND_FILE name other files; set to empty they turn the
// file off. The defaults exist only on desktop Linux: an Android app configures its process
// itself, and a server process (MOBILEGL_IPC_ROLE=server) never reads the clients' file.
namespace {
#if defined(__linux__) && !defined(__ANDROID__)
constexpr const char* kDefaultConfigFile = "/etc/mobilegl/client.conf";
constexpr const char* kDefaultBackendFile = "/etc/mobilegl/backend";
#else
constexpr const char* kDefaultConfigFile = "";
constexpr const char* kDefaultBackendFile = "";
#endif
String FilePathSetting(const char* variable, const char* fallback) {
const char* value = std::getenv(variable);
return value != nullptr ? String(value) : String(fallback);
}
String Trimmed(const String& text) {
const auto first = text.find_first_not_of(" \t\r\n");
if (first == String::npos) return String();
const auto last = text.find_last_not_of(" \t\r\n");
return text.substr(first, last - first + 1);
}
Bool IsServerProcess() {
const char* role = std::getenv("MOBILEGL_IPC_ROLE");
return role != nullptr && std::strcmp(role, "server") == 0;
}
// Reads `path` into `out` without replacing what `out` already holds. A missing file is
// not an error: the defaults are the absence of one.
void ReadConfigFile(const String& path, UnorderedMap<String, String>& out, Bool log) {
if (path.empty()) return;
std::FILE* file = std::fopen(path.c_str(), "r");
if (file == nullptr) return;
char buffer[4096];
Uint32 lineNumber = 0;
while (std::fgets(buffer, sizeof(buffer), file) != nullptr) {
++lineNumber;
const String line = Trimmed(buffer);
if (line.empty() || line[0] == '#') continue;
const auto equals = line.find('=');
const String key = equals == String::npos ? String() : Trimmed(line.substr(0, equals));
if (key.empty() || !IsAcceptedPrefix(key)) {
if (log) {
MGLOG_W("Config: %s:%u is not a MOBILEGL_*/LIBGL_* KEY=value line; ignored", path.c_str(),
lineNumber);
}
continue;
}
String value = Trimmed(line.substr(equals + 1));
if (value.size() >= 2 && (value.front() == '"' || value.front() == '\'') && value.back() == value.front()) {
value = value.substr(1, value.size() - 2);
}
out.emplace(key, value);
}
std::fclose(file);
}
void ReadBackendFile(const String& path, UnorderedMap<String, String>& out) {
if (path.empty()) return;
std::FILE* file = std::fopen(path.c_str(), "r");
if (file == nullptr) return;
char buffer[256];
const Bool read = std::fgets(buffer, sizeof(buffer), file) != nullptr;
std::fclose(file);
if (!read) return;
const String name = Trimmed(buffer);
if (!name.empty()) out.emplace("MOBILEGL_BACKEND_TYPE", name);
}
// Everything the system files say, the backend file ahead of client.conf (first emplace
// wins). Re-read on every call: callers are few (Init, and a handful of lookups before it),
// and a file that changed between two processes' starts must not be served stale.
void ReadSystemConfig(UnorderedMap<String, String>& out, Bool log) {
if (IsServerProcess()) return;
ReadBackendFile(FilePathSetting("MOBILEGL_BACKEND_FILE", kDefaultBackendFile), out);
ReadConfigFile(FilePathSetting("MOBILEGL_CONFIG_FILE", kDefaultConfigFile), out, log);
}
} // namespace
Bool LookupSetting(const char* key, String& outValue) {
if (const char* value = std::getenv(key)) {
outValue = value;
return true;
}
UnorderedMap<String, String> system;
ReadSystemConfig(system, false);
const auto it = system.find(key);
if (it == system.end()) return false;
outValue = it->second;
return true;
}
inline void InitializeAcceptedEnvVariables() {
if (!acceptedEnvVariablesMap) {
acceptedEnvVariablesMap = MakeUnique<UnorderedMap<String, String>>();
} 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());
}
}
}
// Below the environment: a key the environment set is never replaced.
UnorderedMap<String, String> system;
ReadSystemConfig(system, true);
for (const auto& [key, value] : system) {
if (acceptedEnvVariablesMap->emplace(key, value).second) {
MGLOG_D("Config: Accepted system setting: %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<char>(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<char>(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<char>(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<Uint32>(parsedValue);
}
// Same contract as QueryEnvUint32, over 64 bits and accepting an explicit 0x prefix: the
// one consumer is a subsystem BITMASK, and a bitmask written in decimal is unreadable.
// Decimal otherwise - never strtoull's base 0, whose "leading zero means octal" rule
// silently read MOBILEGL_PIPE_PUSH=010 as 8 - and a '-' anywhere is rejected rather than
// wrapped, which strtoull would otherwise do without complaint (-1 -> every bit set).
inline Uint64 QueryEnvUint64(const String& key, Uint64 defaultValue) {
auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) {
return defaultValue;
}
const String& value = it->second;
const char* text = value.c_str();
int base = 10;
if (value.size() > 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) {
text += 2;
base = 16;
}
char* parseEnd = nullptr;
errno = 0;
const bool negative = value.find('-') != String::npos;
const unsigned long long parsedValue = negative ? 0 : std::strtoull(text, &parseEnd, base);
if (negative || parseEnd == text || *parseEnd != '\0' || errno == ERANGE) {
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected a non-negative integer "
"(decimal, or 0x-prefixed hexadecimal), using default %llu",
key.c_str(), value.c_str(), static_cast<unsigned long long>(defaultValue));
return defaultValue;
}
return static_cast<Uint64>(parsedValue);
}
inline void InitFeatures() {
auto& features = MG_Config::Features;
features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
features.EsprytEnableTextureView = QueryEnvFlag("MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW");
features.EnableSpirvValidation = QueryEnvFlag("MOBILEGL_ENABLE_SPIRV_VALIDATION");
features.EsprytUseAngle = QueryEnvFlag("MOBILEGL_ESPRYT_USE_ANGLE");
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
#endif
features.MagmaDisableSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_SUBGROUP");
features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
features.MagmaFixIterationRPSubgroupScratch =
QueryEnvQuirkOverride("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
features.MagmaIterationRPFixBarrier = QueryEnvFlag("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER");
features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV");
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
features.MagmaMaxDrawsPerCommandBuffer =
QueryEnvUint32("MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER", 16384, 0, 1u << 24);
features.MagmaDescriptorTrimFrames = QueryEnvUint32("MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES", 120, 0, 1u << 20);
features.EsprytAvoidSamplerMipmapMinFilter =
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
features.EsprytUnlocatedIoBlocks = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS");
features.PointSizeDemotion = QueryEnvQuirkOverride("MOBILEGL_POINT_SIZE_DEMOTION");
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
features.EsprytDisableUboRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UBO_RING");
features.EsprytDisableUnpackRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UNPACK_RING");
features.EsprytDisableUploadRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING");
features.EsprytDisableInvalidateFlush = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH");
features.DisableLargeBufferAdoption = QueryEnvFlag("MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION");
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.MagmaDisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_MAGMA_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.EsprytViewportArrayEmulation =
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION");
features.EsprytWidenPacked16Storage =
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE");
features.MagmaPrimGenQueryReroute = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE");
// MGPipe. Nothing here needs adding to an allow-list: InitializeAcceptedEnvVariables
// accepts every MOBILEGL_ / LIBGL_ prefixed variable in the environment, so a name
// that starts with MOBILEGL_ is visible to these queries by construction.
// A push build with the knob unset runs every subsystem migrated so far, so the
// shipped path is the one the gates measure; MOBILEGL_PIPE_PUSH=0 in the
// environment is the all-subsystems-pull control that reproduces P1 exactly, and
// kMGPipeSubsystemsMigratedAtP3a (0x1ff) is the phase-by-phase control - P4a's four
// subsystems off, everything P3a landed still on.
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP5e);
features.PipeVerify = QueryEnvFlag("MOBILEGL_PIPE_VERIFY");
// Defaults ON: read as a tri-state so only an explicitly falsy value turns it off.
features.PipeVerifyFatal =
QueryEnvQuirkOverride("MOBILEGL_PIPE_VERIFY_FATAL") != MG_Config::QuirkOverride::ForceOff;
QueryEnvVariable("MOBILEGL_PIPE_VERIFY_CORRUPT", features.PipeVerifyCorrupt, "");
QueryEnvVariable("MOBILEGL_PIPE_POISON_OMIT", features.PipePoisonOmit, "");
features.PipeHandleAbaControl = QueryEnvFlag("MOBILEGL_PIPE_HANDLE_ABA_CONTROL");
features.PipeStats = QueryEnvFlag("MOBILEGL_PIPE_STATS");
// Defaults ON, so the flag has to be read as a tri-state rather than as a plain
// truthy check: unset must keep the memos, and only an explicitly falsy value may
// drop them.
features.PipeLegacyMemos =
QueryEnvQuirkOverride("MOBILEGL_PIPE_LEGACY_MEMOS") != MG_Config::QuirkOverride::ForceOff;
features.PipeTexelRetainMb = QueryEnvUint32("MOBILEGL_PIPE_TEXEL_RETAIN_MB", 0, 0, 4096);
features.PipeIndexMirrorMb = QueryEnvUint32("MOBILEGL_PIPE_INDEX_MIRROR_MB", 64, 0, 4096);
features.PipeStatsPeriod = QueryEnvUint32("MOBILEGL_PIPE_STATS_PERIOD", 120, 1, 1000000);
QueryEnvVariable("MOBILEGL_PIPE_STATS_FILE", features.PipeStatsFile, "");
}
inline void InitBackendType() {
String backendTypeStr;
QueryEnvVariable("MOBILEGL_BACKEND_TYPE", backendTypeStr, "DirectGLES");
#if defined(__ANDROID__)
// An Android app that hosts the library sets the environment itself, before the library
// loads; the debug property is how a developer picks the backend for such a process
// without rebuilding it (`adb shell setprop debug.mobilegl.backend DirectVulkan`).
char property[PROP_VALUE_MAX] = {};
if (__system_property_get("debug.mobilegl.backend", property) > 0) {
MGLOG_I("Config: debug.mobilegl.backend=%s overrides MOBILEGL_BACKEND_TYPE=%s", property,
backendTypeStr.c_str());
backendTypeStr = property;
}
#endif
#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
}
#if MOBILEGL_BUILD_DISAGGREGATED
// MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>
// (ARCHITECTURE.md:583). Shaped after InitBackendType above: an exact-name table, then
// one fallback that names what it did instead. The two prefixed forms are the only
// reason this is not literally that function's ENTRY macro.
//
// spawn / unix: / pipe: PARSE AND THEN REFUSE. They are P6's, and the refusal is NAMED
// rather than silent, because the failure this avoids is a P6 lane that set
// MOBILEGL_TRANSPORT=spawn, fell back to monolith, and went green on the wrong arm.
// The mode is left at Monolith so nothing half-initializes.
inline void InitTransport() {
String value;
QueryEnvVariable("MOBILEGL_TRANSPORT", value, "monolith");
String lowered = value;
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
MG_Config::TransportEndpoint.clear();
if (lowered.empty() || lowered == "monolith") {
MG_Config::Transport = MG_Config::TransportMode::Monolith;
return;
}
if (lowered == "inproc") {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
// P7 F1: see Config.h. Set beside the mode, on the two arms that actually resolve
// one, and NOT on the two that name a transport and then stay monolith - a process
// that was refused its transport brings no client half up either.
MG_Config::SplitTransportRequestedByConfig = true;
MGLOG_I("Config: MOBILEGL_TRANSPORT=inproc - the MGPipe record stream crosses a real "
"ring to an apply thread");
return;
}
// The three P6 forms. Recognised precisely, so the diagnostic can say "not yet"
// rather than "unknown", which are different bugs on the operator's side.
// P6 `sm`/`cp`: spawn is IMPLEMENTED. It launches a server process on a
// rendezvous of its own and connects to it - the two processes are
// independent, so this is `Dial = Connect` with a launcher attached
// rather than the fork-coupled shape ARCHITECTURE.md:488 described.
if (lowered == "spawn") {
MG_Config::Transport = MG_Config::TransportMode::Spawn;
MG_Config::SplitTransportRequestedByConfig = true; // P7 F1, as above.
// THE SAME SHAPE AS THE inproc LINE ABOVE, AND FOR THE SAME REASON.
// run_trace_case.cmake asserts on a distinctive sentence from this
// function rather than on `MOBILEGL_TRANSPORT=spawn` alone, because
// ConfigLoader logs an `Accepted env variable: KEY=VALUE` line for
// every MOBILEGL_* in EVERY build including the pull one - so the
// bare KEY=VALUE is satisfied by a monolith library that parsed
// nothing (review M-5). A spawn arm with no line of its own could
// only have been asserted with the inproc marker, which spawn never
// emits: the arm would have been red for a missing sentence rather
// than green for a working transport.
MGLOG_I("Config: MOBILEGL_TRANSPORT=spawn - the MGPipe record stream crosses a real "
"ring to an apply thread in ANOTHER PROCESS");
return;
}
if (lowered.compare(0, 5, "unix:") == 0 || lowered.compare(0, 5, "pipe:") == 0) {
// unix: and pipe: remain P6+ - connecting to an endpoint SOMEBODY
// ELSE is listening on needs the server lifecycle to be somebody
// else's too, which is P12's. Named, never a silent degrade.
MGLOG_E("Config: MOBILEGL_TRANSPORT='%s' names a transport P6 does not implement yet; "
"staying on monolith. This run is NOT a split run.",
value.c_str());
MG_Config::Transport = MG_Config::TransportMode::Monolith;
return;
}
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_TRANSPORT='%s'; expected "
"monolith|inproc|spawn|unix:<path>|pipe:<name>, using monolith",
value.c_str());
MG_Config::Transport = MG_Config::TransportMode::Monolith;
}
// The MOBILEGL_IPC_* family (Config.h IpcTable). Parsed unconditionally rather than only
// when Transport != Monolith: a knob that silently means nothing on one arm of an A/B is
// how an A/B stops being one, and the ranges below are the diagnostics.
inline void InitIpc() {
auto& ipc = MG_Config::Ipc;
QueryEnvVariable("MOBILEGL_IPC_SERVER_PATH", ipc.ServerPath, "");
QueryEnvVariable("MOBILEGL_IPC_CONTROL", ipc.Control, "fork");
QueryEnvVariable("MOBILEGL_IPC_DATA", ipc.Data, "auto");
// Both ring floors are 1 MiB, not 0: a ring caps ONE record at half its size, and
// the catalogue's largest fixed payload (MGPFramebufferState, 304 bytes) plus a
// create_shader_state archive already needs far more than a toy ring. The ceilings
// are sanity, not policy.
ipc.RingMb = QueryEnvUint32("MOBILEGL_IPC_RING_MB", 8, 1, 1024);
ipc.StageMb = QueryEnvUint32("MOBILEGL_IPC_STAGE_MB", 32, 1, 4096);
// P7 wave 4 M2 (Config.h has the semantics). 0 is admitted ON PURPOSE as the negative
// control: no forced sync, and MagmaWireReclaimScenario's watermark case must go red.
ipc.WireDeferredMb = QueryEnvUint32("MOBILEGL_IPC_WIRE_DEFERRED_MB", 64, 0, 65536);
ipc.SpinUs = QueryEnvUint32("MOBILEGL_IPC_SPIN_US", 50, 0, 1000000);
// PH-6 (ID-P7-2; Config.h has the semantics). 0 is NOT admitted: a server with no
// patience at all would forfeit a healthy run-ahead client the first time its ring
// filled between two of the client's drains, which is an ordinary backlog.
ipc.EventWaitMs = QueryEnvUint32("MOBILEGL_IPC_EVENT_WAIT_MS", 2000, 1, 600000);
// 0 is admitted ON PURPOSE and is the negative control of exit gate E3(a): it turns
// the persistent-map push OFF, and PersistentCoherentMapScenario must go red.
ipc.PersistentBlockKb = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_BLOCK_KB", 64, 0, 65536);
// Whole-range push is the 0 arm; with it on (default) only blocks whose
// xxHash64 changed since the last push are shipped.
ipc.PersistentHashSuppress = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS", 1, 0, 1);
ipc.BatchWaits = QueryEnvUint32("MOBILEGL_IPC_BATCH_WAITS", 1, 0, 1);
// The create window. DEFAULT 6, and it works because a link that declares RetainsReplies
// keeps a declared answer until its reader takes it - Config.h carries the three reasons it
// was inert before, the device numbers, and the sweep that put the default at 6 rather
// than 4 or 8 (the curve flattens at 6: 4->5 buys 1.92 s, 5->6 0.53 s, 6->8 nothing).
// Capped at 8: the retained set is bounded by the window's own depth, the store holds 24
// declared answers, and the device sweep showed the wall clock still falling at the old
// cap of 4 (see WireTables.cpp's kCreateWindowMax). WireTables.cpp's static_assert and
// StreamLink's crossing log are the checks on this number.
ipc.CreateWindow = QueryEnvUint32("MOBILEGL_IPC_CREATE_WINDOW", 10, 1, 12);
// P9 W1: the pack-buffer readback's A/B and red-once control (Config.h says both).
ipc.PboReadbackSync = QueryEnvUint32("MOBILEGL_IPC_PBO_READBACK_SYNC", 0, 0, 1);
// P10: fence polls answered locally; 0 is the A/B arm (Config.h).
ipc.PollEscalate = QueryEnvUint32("MOBILEGL_IPC_POLL_ESCALATE", 64, 0, 1000000);
// A lost session is replaced by a fresh one on the application's next create (Config.h).
ipc.RecoverAfterLoss = QueryEnvUint32("MOBILEGL_IPC_RECOVER", 1, 0, 1);
// The verify harness compares the pushed block against the applier per verb; a
// batched queue lets the comparer read a supplied field mid-apply, which is a
// torn read rather than a divergence. The batch is therefore off whenever the
// shadow comparer is armed.
if (MG_Config::Features.PipeVerify) ipc.BatchWaits = 0;
// UNSET is T0 with quiet fallbacks (the default since 2026-09-29); 0 asks for T0 by name, 1 is
// refused by name, 2 is T2 - settled at the handshake from the data plane (P11 A1/B2,
// MG_Remote/Transport/AdoptTier.h). An unparseable value is warned about and reads as unset.
ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", MG_Config::kAdoptTierUnset, 0, 2);
// P11 B2: the server's own T0 allow switch (Config.h).
ipc.AllowAdoptT0 = QueryEnvUint32("MOBILEGL_IPC_ALLOW_ADOPT_T0", 1, 0, 1);
ipc.VerbBarrier = QueryEnvUint32("MOBILEGL_IPC_VERB_BARRIER", 1, 0, 1);
// P5e (MG_Remote/CONTRACT-P5E.md §1). The wait rule's A/B, parsed here like every
// other IPC knob and armed only where the server publishes kCapRunAheadApply.
ipc.RunAhead = QueryEnvUint32("MOBILEGL_IPC_RUN_AHEAD", 1, 0, 1);
// ... and forced OFF by the verify harness for BatchWaits' reason, one step further:
// run-ahead's whole point is that the client stops filling gPipeInputs for an
// unbarriered record, and the comparator has nothing left to compare when it does.
if (MG_Config::Features.PipeVerify) ipc.RunAhead = 0;
// The present credit (ruling 4). 1 is one frame of overlap; 8 is the ceiling because a
// deeper queue buys nothing on a CPU-bound client and pays for it in latency. 0 is NOT
// admitted: a credit of zero would mean "publish no present at all".
ipc.PresentCredit = QueryEnvUint32("MOBILEGL_IPC_PRESENT_CREDIT", 1, 1, 8);
// CONTRACT-P6 D5b's reply bound and P7's cold-start budget (Config.h has the semantics).
// Both are floored at 100 ms: a zero bound would declare every live server silent.
ipc.ControlTimeoutMs = QueryEnvUint32("MOBILEGL_IPC_CONTROL_TIMEOUT_MS", 5000, 100, 600000);
ipc.ColdStartMs = QueryEnvUint32("MOBILEGL_IPC_COLD_START_MS", 20000, 100, 600000);
ipc.StrictErrors = QueryEnvFlag("MOBILEGL_IPC_STRICT_ERRORS");
ipc.Audit = QueryEnvFlag("MOBILEGL_IPC_AUDIT");
QueryEnvVariable("MOBILEGL_IPC_SERVER_AFFINITY", ipc.ServerAffinity, "auto");
// P5f f1: the dual-block rehearsal (P5F-WIRE-COMPLETENESS.md §4). Forced OFF by the
// verify harness: the comparator's entry compare and compare-at-read hook are built on
// there being ONE filled block (the hook pins itself to &gPipeInputs,
// PipeFill.cpp's MGPipeVerifyReadHook), which is exactly what the rehearsal splits.
ipc.RoleSplitState = QueryEnvFlag("MOBILEGL_IPC_ROLE_SPLIT_STATE");
if (MG_Config::Features.PipeVerify && ipc.RoleSplitState) {
MGLOG_W("Config: MOBILEGL_IPC_ROLE_SPLIT_STATE=1 is incompatible with "
"MOBILEGL_PIPE_VERIFY (the comparator owns the single fill block); "
"the dual-block rehearsal is OFF for this run");
ipc.RoleSplitState = false;
}
// P6 `dl` (CONTRACT-P6 5.3): MOBILEGL_IPC_RESPAWN IS A NAMED REFUSAL, NOT A NO-OP.
//
// The device-lost latch is deliberately one-way - a session whose server died has
// nothing to recover into, because every handle the client minted names an object in a
// process that no longer exists. Re-pushing the world onto a fresh server is the work
// this knob would turn on, and no stage has written it.
//
// Refused BY NAME rather than parsed and ignored, for the reason Config.h gives about
// the whole IPC family: an environment variable nothing consumes is indistinguishable
// from one that is consumed and does nothing, and an operator who set this one would
// otherwise conclude that recovery had been tried and had not helped.
{
String respawn;
QueryEnvVariable("MOBILEGL_IPC_RESPAWN", respawn, "");
if (!respawn.empty() && respawn != "0") {
MGLOG_E("Config: MOBILEGL_IPC_RESPAWN='%s' names a recovery NO STAGE HAS "
"IMPLEMENTED. The device-lost latch is one-way on purpose: every handle "
"this client minted names an object inside the server process, so a new "
"server would have to be re-pushed the entire world before a single verb "
"could land. This run will latch device-lost and stay there.",
respawn.c_str());
}
}
// P12 (on-screen server window): MOBILEGL_IPC_SURFACE = offscreen | server (Config.h has
// the semantics). An unknown value is named and read as the default, like every knob here.
{
String surface;
QueryEnvVariable("MOBILEGL_IPC_SURFACE", surface, "offscreen");
std::transform(surface.begin(), surface.end(), surface.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (surface.empty() || surface == "offscreen") {
ipc.Surface = MG_Config::IpcSurface::Offscreen;
} else if (surface == "server") {
ipc.Surface = MG_Config::IpcSurface::Server;
} else {
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_IPC_SURFACE='%s'; expected "
"offscreen|server, using offscreen",
surface.c_str());
ipc.Surface = MG_Config::IpcSurface::Offscreen;
}
// (Not said by a server process: a spawned server inherits the client's environment,
// parses this before RunSession sets its own transport, and has no client half anyway.)
const char* role = std::getenv("MOBILEGL_IPC_ROLE");
const Bool serverProcess = role != nullptr && std::strcmp(role, "server") == 0;
if (ipc.Surface == MG_Config::IpcSurface::Server && !serverProcess &&
MG_Config::Transport != MG_Config::TransportMode::Spawn) {
// Said, not silently honoured: there is no remote server here to own a window.
MGLOG_W("Config: MOBILEGL_IPC_SURFACE=server is IGNORED - it asks a remote server "
"(MOBILEGL_TRANSPORT=spawn, a fork or tcp:// control) to own the window surface, "
"and this run has none. Window surfaces stay the client's own");
}
}
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
// One line, on the arm where these numbers decide behaviour, because every one of
// them is a number a bug report has to quote.
// CREATE-WINDOW IS IN THIS LINE BECAUSE A RUN HAS TO SAY WHICH WINDOW IT USED. It was
// missing while the value was a number nobody varied, and the moment it became one that
// changes the wall clock by seconds (the P12 sweep: 28.3 s at 1 down to 10.8 s at 6) a
// log that does not name it makes every run ambiguous about its own configuration.
MGLOG_I("Config: IPC ring=%uMiB stage=%uMiB wire-deferred=%uMiB spin=%uus event-wait=%ums "
"persistent-block=%uKiB create-window=%u pbo-readback-sync=%u poll-escalate=%u "
"adopt-tier=%s allow-t0=%u verb-barrier=%u run-ahead=%u present-credit=%u control-timeout=%ums "
"cold-start=%ums strict=%d audit=%d role-split-state=%d affinity='%s' surface=%s",
ipc.RingMb, ipc.StageMb, ipc.WireDeferredMb, ipc.SpinUs, ipc.EventWaitMs,
ipc.PersistentBlockKb, ipc.CreateWindow, ipc.PboReadbackSync, ipc.PollEscalate,
ipc.AdoptTier == 0 ? "0" : ipc.AdoptTier == 1 ? "1" : ipc.AdoptTier == 2 ? "2" : "unset(T0)",
ipc.AllowAdoptT0,
ipc.VerbBarrier, ipc.RunAhead, ipc.PresentCredit, ipc.ControlTimeoutMs, ipc.ColdStartMs,
static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
static_cast<int>(ipc.RoleSplitState), ipc.ServerAffinity.c_str(),
MG_Config::ServerOwnedWindowSurfaces() ? "server" : "offscreen");
if (ipc.VerbBarrier == 0) {
MGLOG_W("Config: MOBILEGL_IPC_VERB_BARRIER=0 is the R-1 NEGATIVE CONTROL and is "
"expected to fail: the client still pulls 31 of 63 PipeInputs fields from a "
"live GLContext, so an unbarriered queue lets the server read future values");
}
if (ipc.WireDeferredMb == 0) {
MGLOG_W("Config: MOBILEGL_IPC_WIRE_DEFERRED_MB=0 is the M2 NEGATIVE CONTROL: the "
"server never forces a sync for orphaned wire buffer stores, so a long frame "
"that respecifies and draws holds every one of them until the frame ends");
}
if (ipc.PersistentBlockKb == 0) {
MGLOG_W("Config: MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 is the E3(a) NEGATIVE CONTROL: "
"the persistent-map push is OFF and a coherent-map scenario must go red");
}
}
#endif
void Init() {
MGLOG_D("Loading configuration from environment variables...");
InitializeAcceptedEnvVariables();
InitBackendType();
InitFeatures();
#if MOBILEGL_BUILD_DISAGGREGATED
// After InitFeatures, so the one line InitIpc logs is the last word on this run's
// configuration, and before the accepted-env map is destroyed just below.
InitTransport();
InitIpc();
#endif
// Destroy the map since we won't need it anymore
acceptedEnvVariablesMap.reset();
}
} // namespace MobileGL::MG_ConfigLoader