From 0b96a3efbfe13b5a53f67e5f8ba782178871e829 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 8 Oct 2026 19:49:05 -0400 Subject: [PATCH] MOBILEGL_FRAMES_IN_FLIGHT drives both backends and derives the split present credit (default 3 keeps credit 1; the Magma name stays as an alias). --- .github/scripts/validate-plugin-apks.sh | 2 +- MobileGL/Config.h | 26 ++++++++----- MobileGL/ConfigLoader.cpp | 18 ++++++++- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 19 ++++++++++ .../DirectVulkan/Renderer/VulkanRenderer.cpp | 2 +- MobileGL/MG_Test/Util/ConfigFileTest.cpp | 37 ++++++++++++++++--- android-plugin/app/build.gradle.kts | 2 +- .../app/src/main/AndroidManifest.xml | 4 +- .../app/src/main/res/values/strings.xml | 2 +- .../app/src/trace/AndroidManifest.xml | 2 +- .../design/09-build-switches-counters.md | 3 +- 11 files changed, 92 insertions(+), 25 deletions(-) diff --git a/.github/scripts/validate-plugin-apks.sh b/.github/scripts/validate-plugin-apks.sh index 52a975dd..7c495cd0 100644 --- a/.github/scripts/validate-plugin-apks.sh +++ b/.github/scripts/validate-plugin-apks.sh @@ -46,7 +46,7 @@ require 'DirectVulkan' "$plugin_resource_text" 'V2 DirectVulkan option' require 'key:MOBILEGL_DISABLE_TIMERQUERY' "$plugin_resource_text" 'V2 timer-query toggle' require 'key:MOBILEGL_MAGMA_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle' require 'key:MOBILEGL_MAGMA_R11G11B10F_FALLBACK' "$plugin_resource_text" 'V2 Magma format fallback toggle' -require 'key:MOBILEGL_MAGMA_FRAMESINFLIGHT' "$plugin_resource_text" 'V2 Magma frames-in-flight setting' +require 'key:MOBILEGL_FRAMES_IN_FLIGHT' "$plugin_resource_text" 'V2 frames-in-flight setting' require 'key:MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle' require 'key:MOBILEGL_COHERENT_AS_FLUSH' "$plugin_resource_text" 'V2 coherent-as-flush toggle' require 'key:MOBILEGL_ESPRYT_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle' diff --git a/MobileGL/Config.h b/MobileGL/Config.h index aab935b7..b1188987 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -163,8 +163,15 @@ namespace MobileGL::MG_Config { // draws refused by name), because a driver that accepts invalid SPIR-V may fault the GPU // into device loss. For diagnosing a driver that copes with a specific module only. Bool MagmaAllowInvalidSpirv = false; - // MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3. - Uint32 MagmaFramesInFlight = 3; + // MOBILEGL_FRAMES_IN_FLIGHT (P15; MOBILEGL_MAGMA_FRAMESINFLIGHT is its deprecated alias): + // how many frames may be queued ahead of the GPU, default 3, both backends. + // - Magma: its frame contexts (frame S+1 reuses S+1-N's), clamped to the surface. + // - Espryt: after presenting S the backend waits for S+1-N to complete (N = 1 is a full + // CPU/GPU sync; N >= 2 is what Android's own queueBuffer throttle already gives). + // - inproc / spawn / TCP: the present credit, how many presents the GL thread may run + // ahead of the server, is derived from it: max(1, N - 2), so the default 3 keeps the + // shipped credit of 1 and N = 4 is credit 2 (one more frame of latency). + Uint32 FramesInFlight = 3; // MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER: GL draws and dispatches Magma records into // one command buffer before it submits it and continues on a fresh one. 0 = unbounded. // Drivers back a command buffer with GPU memory that is only returned when the buffer is @@ -700,13 +707,14 @@ namespace MobileGL::MG_Config { // client-filled gPipeInputs block for every verb, and run-ahead is precisely the // arm that stops filling it. Uint32 RunAhead = 1; - // MOBILEGL_IPC_PRESENT_CREDIT (P5e, ruling 4 / ID-92): how many presents the client may - // have in flight before it waits for a swap to come back. 1 = the client publishes - // frame N+1's records while the server applies and swaps frame N - one frame of - // overlap, at most one frame of added latency - and the CREDIT, never the ring's bytes, - // is what paces a run-ahead client. 2 is a device MEASUREMENT arm: it buys no CPU on a - // client that is already CPU-bound and costs a frame of latency, which is why the - // default is 1 and not "as deep as the ring". + // The present credit (P5e, ruling 4 / ID-92): how many presents the client may have in + // flight before it waits for a swap to come back. 1 = the client publishes frame N+1's + // records while the server applies and swaps frame N - one frame of overlap, at most one + // frame of added latency - and the CREDIT, never the ring's bytes, is what paces a + // run-ahead client. Derived from MOBILEGL_FRAMES_IN_FLIGHT (max(1, N - 2)); the + // MOBILEGL_IPC_PRESENT_CREDIT override stays for experiments. Credit 2 is a real trade, + // not a measurement arm: on FCL inproc (cpuhunt, 2026-10-08) it brought Magma 309 -> 320 + // and Espryt 299 -> 319 fps, at one frame of added latency. Uint32 PresentCredit = 1; // MOBILEGL_IPC_WAIT_STATS (P15, diagnostic): every 600 presents, log how many times per // frame the run-ahead client waited, by record op (applied/reply), by quiesce reason and diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index f7594377..255bab7f 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -377,7 +377,14 @@ namespace MobileGL::MG_ConfigLoader { 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); + // P15: one backend-neutral knob; the Magma-only spelling stays as a deprecated alias. + if (acceptedEnvVariablesMap->count("MOBILEGL_FRAMES_IN_FLIGHT") != 0) { + features.FramesInFlight = QueryEnvUint32("MOBILEGL_FRAMES_IN_FLIGHT", 3, 1, 64); + } else { + features.FramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64); + if (acceptedEnvVariablesMap->count("MOBILEGL_MAGMA_FRAMESINFLIGHT") != 0) + MGLOG_W("Config: MOBILEGL_MAGMA_FRAMESINFLIGHT is deprecated; use MOBILEGL_FRAMES_IN_FLIGHT"); + } 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); @@ -611,7 +618,14 @@ namespace MobileGL::MG_ConfigLoader { // 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); + // P15: derived from MOBILEGL_FRAMES_IN_FLIGHT (Config.h states the mapping); the explicit + // knob stays as an override for experiments. + { + const Uint32 derived = std::max(1u, std::min(8u, MG_Config::Features.FramesInFlight > 2 + ? MG_Config::Features.FramesInFlight - 2 + : 1u)); + ipc.PresentCredit = QueryEnvUint32("MOBILEGL_IPC_PRESENT_CREDIT", derived, 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); diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 2a4303c3..1e4f8968 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -13713,8 +13713,10 @@ namespace MobileGL::MG_Backend::DirectGLES { // makes it reachable), then non-blocking-poll prior frames' fences AFTER to // advance the completed-frame watermark that gates buffer-pool recycling. const Bool canFence = IsBackendContextCurrentOnThisThread() && g_GLESFuncs.glFenceSync; + Uint64 presentedSerial = 0; if (canFence) { const Uint64 serial = g_currentFrameSerial.fetch_add(1, std::memory_order_relaxed) + 1; + presentedSerial = serial; FrameFence& slot = g_frameFenceRing[serial % kFrameFenceRingDepth]; if (slot.sync && slot.contextGeneration == g_syncContextGeneration) { // The slot's fence is a ring-depth of serials old and may be another context's: @@ -13772,6 +13774,23 @@ namespace MobileGL::MG_Backend::DirectGLES { // A frame boundary: the next frame's age is asked anew. t_ageAskedSurface = EGL_NO_SURFACE; + // P15: MOBILEGL_FRAMES_IN_FLIGHT, Espryt's half. Magma bounds the frames the GPU has + // queued with its frame contexts (frame S+1 reuses the context of frame S+1-N); this is + // the same bound on the ES driver: after presenting S, wait for S+1-N to complete. The + // platform's own queueBuffer throttle already waits for S-1, so only N = 1 is stricter + // than what Android does by itself; larger N is the same contract stated, not a change. + const Uint32 framesInFlight = MG_Config::Features.FramesInFlight; + if (presentedSerial != 0 && framesInFlight != 0 && + framesInFlight < static_cast(kFrameFenceRingDepth) && g_GLESFuncs.glClientWaitSync && + presentedSerial + 1 > framesInFlight) { + const Uint64 target = presentedSerial + 1 - framesInFlight; + const FrameFence& oldest = g_frameFenceRing[target % kFrameFenceRingDepth]; + if (oldest.sync && oldest.serial == target && oldest.contextGeneration == g_syncContextGeneration) { + constexpr Uint64 kFramesInFlightWaitNs = 1000ull * 1000 * 1000; + (void)g_GLESFuncs.glClientWaitSync(oldest.sync, GL_SYNC_FLUSH_COMMANDS_BIT, kFramesInFlightWaitNs); + } + } + if (canFence) PollFrameFences(); // After the watermark advanced: retire grown-away ring stores and record the diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index fad0c78b..e4ba9d7e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -2156,7 +2156,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { { // Desired depth comes from MOBILEGL_MAGMA_FRAMESINFLIGHT, parsed once by ConfigLoader // with a default of 3 when the variable is unset or invalid. - Uint32 requestedFramesInFlight = MG_Config::Features.MagmaFramesInFlight; + Uint32 requestedFramesInFlight = MG_Config::Features.FramesInFlight; MGLOG_I("MaxFramesInFlight: configured request=%u", requestedFramesInFlight); VkSurfaceCapabilitiesKHR surfaceCaps{}; diff --git a/MobileGL/MG_Test/Util/ConfigFileTest.cpp b/MobileGL/MG_Test/Util/ConfigFileTest.cpp index 221fdd73..6ed76f71 100644 --- a/MobileGL/MG_Test/Util/ConfigFileTest.cpp +++ b/MobileGL/MG_Test/Util/ConfigFileTest.cpp @@ -36,6 +36,7 @@ namespace { "MOBILEGL_CONFIG_FILE", "MOBILEGL_BACKEND_FILE", "MOBILEGL_BACKEND_TYPE", "MOBILEGL_MAGMA_FRAMESINFLIGHT", "MOBILEGL_TRANSPORT", "MOBILEGL_IPC_CONTROL", "MOBILEGL_IPC_DATA", "MOBILEGL_IPC_ROLE", "MOBILEGL_TEST_ONLY_KEY", + "MOBILEGL_FRAMES_IN_FLIGHT", "MOBILEGL_IPC_PRESENT_CREDIT", }; class ConfigFileTest : public ::testing::Test { @@ -84,7 +85,7 @@ namespace { TEST_F(ConfigFileTest, WithoutFilesOrEnvironmentTheBuiltInDefaultsHold) { Loader::Init(); EXPECT_EQ(Config::ActiveBackendType, BackendType::DirectGLES); - EXPECT_EQ(Config::Features.MagmaFramesInFlight, 3u); + EXPECT_EQ(Config::Features.FramesInFlight, 3u); EXPECT_EQ(Lookup("MOBILEGL_TRANSPORT"), ""); #if MOBILEGL_BUILD_DISAGGREGATED EXPECT_EQ(Config::Transport, Config::TransportMode::Monolith); @@ -92,6 +93,30 @@ namespace { #endif } + // P15: one frames-in-flight knob for both backends, the Magma spelling as its alias, and the + // split arms' present credit derived from it (max(1, N - 2), so the default keeps credit 1). + TEST_F(ConfigFileTest, FramesInFlightIsBackendNeutralAndDerivesThePresentCredit) { + Loader::Init(); + EXPECT_EQ(Config::Features.FramesInFlight, 3u); +#if MOBILEGL_BUILD_DISAGGREGATED + EXPECT_EQ(Config::Ipc.PresentCredit, 1u); +#endif + ::setenv("MOBILEGL_FRAMES_IN_FLIGHT", "4", 1); + ::setenv("MOBILEGL_MAGMA_FRAMESINFLIGHT", "6", 1); // the new name wins over the alias + Loader::Init(); + EXPECT_EQ(Config::Features.FramesInFlight, 4u); +#if MOBILEGL_BUILD_DISAGGREGATED + EXPECT_EQ(Config::Ipc.PresentCredit, 2u); + ::setenv("MOBILEGL_IPC_PRESENT_CREDIT", "1", 1); // the explicit override stays + Loader::Init(); + EXPECT_EQ(Config::Ipc.PresentCredit, 1u); + ::unsetenv("MOBILEGL_IPC_PRESENT_CREDIT"); +#endif + ::unsetenv("MOBILEGL_FRAMES_IN_FLIGHT"); + Loader::Init(); + EXPECT_EQ(Config::Features.FramesInFlight, 6u); // the alias alone still works + } + TEST_F(ConfigFileTest, TheFileFillsWhatTheEnvironmentLeavesUnset) { Write(conf_, "# a comment\n" "\n" @@ -103,7 +128,7 @@ namespace { "garbage line without equals\n" "MOBILEGL_TEST_ONLY_KEY=a=b\n"); Loader::Init(); - EXPECT_EQ(Config::Features.MagmaFramesInFlight, 5u); + EXPECT_EQ(Config::Features.FramesInFlight, 5u); EXPECT_EQ(Lookup("MOBILEGL_TRANSPORT"), "spawn"); EXPECT_EQ(Lookup("MOBILEGL_IPC_CONTROL"), "unix:@test-endpoint"); EXPECT_EQ(Lookup("MOBILEGL_IPC_DATA"), "shm"); @@ -121,7 +146,7 @@ namespace { ::setenv("MOBILEGL_MAGMA_FRAMESINFLIGHT", "7", 1); ::setenv("MOBILEGL_IPC_CONTROL", "unix:@from-env", 1); Loader::Init(); - EXPECT_EQ(Config::Features.MagmaFramesInFlight, 7u); + EXPECT_EQ(Config::Features.FramesInFlight, 7u); EXPECT_EQ(Lookup("MOBILEGL_IPC_CONTROL"), "unix:@from-env"); #if MOBILEGL_BUILD_DISAGGREGATED EXPECT_EQ(Config::Ipc.Control, "unix:@from-env"); @@ -156,12 +181,12 @@ namespace { ::setenv("MOBILEGL_CONFIG_FILE", "", 1); ::setenv("MOBILEGL_BACKEND_FILE", "", 1); Loader::Init(); - EXPECT_EQ(Config::Features.MagmaFramesInFlight, 3u); + EXPECT_EQ(Config::Features.FramesInFlight, 3u); EXPECT_EQ(Config::ActiveBackendType, BackendType::DirectGLES); ::setenv("MOBILEGL_CONFIG_FILE", (dir_ / "absent.conf").string().c_str(), 1); Loader::Init(); - EXPECT_EQ(Config::Features.MagmaFramesInFlight, 3u); + EXPECT_EQ(Config::Features.FramesInFlight, 3u); } TEST_F(ConfigFileTest, AServerProcessNeverReadsTheClientsFile) { @@ -169,7 +194,7 @@ namespace { Write(backend_, "DirectVulkan\n"); ::setenv("MOBILEGL_IPC_ROLE", "server", 1); Loader::Init(); - EXPECT_EQ(Config::Features.MagmaFramesInFlight, 3u); + EXPECT_EQ(Config::Features.FramesInFlight, 3u); EXPECT_EQ(Config::ActiveBackendType, BackendType::DirectGLES); EXPECT_EQ(Lookup("MOBILEGL_MAGMA_FRAMESINFLIGHT"), ""); } diff --git a/android-plugin/app/build.gradle.kts b/android-plugin/app/build.gradle.kts index abeac8dd..64811c88 100644 --- a/android-plugin/app/build.gradle.kts +++ b/android-plugin/app/build.gradle.kts @@ -105,7 +105,7 @@ val pluginRendererConfig = buildJsonValue { toggleable("MOBILEGL_DISABLE_TIMERQUERY", "1", false, RendererConfig.MetaString("mobilegl_disable_timerquery_title")) toggleable("MOBILEGL_MAGMA_DISABLE_SUBGROUP", "1", false, RendererConfig.MetaString("mobilegl_disable_subgroup_title")) toggleable("MOBILEGL_MAGMA_R11G11B10F_FALLBACK", "1", false, RendererConfig.MetaString("mobilegl_magma_r11g11b10f_fallback_title")) - customizable("MOBILEGL_MAGMA_FRAMESINFLIGHT", "3", RendererConfig.MetaString("mobilegl_magma_frames_inflight_title")) + customizable("MOBILEGL_FRAMES_IN_FLIGHT", "3", RendererConfig.MetaString("mobilegl_frames_inflight_title")) toggleable("MOBILEGL_ESPRYT_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")) diff --git a/android-plugin/app/src/main/AndroidManifest.xml b/android-plugin/app/src/main/AndroidManifest.xml index 3eabdf3d..5cde6c99 100644 --- a/android-plugin/app/src/main/AndroidManifest.xml +++ b/android-plugin/app/src/main/AndroidManifest.xml @@ -102,8 +102,8 @@ android:name="mobilegl_magma_r11g11b10f_fallback_title" android:resource="@string/mobilegl_magma_r11g11b10f_fallback_title" /> + android:name="mobilegl_frames_inflight_title" + android:resource="@string/mobilegl_frames_inflight_title" /> diff --git a/android-plugin/app/src/main/res/values/strings.xml b/android-plugin/app/src/main/res/values/strings.xml index 082044f3..7e4b4dfd 100644 --- a/android-plugin/app/src/main/res/values/strings.xml +++ b/android-plugin/app/src/main/res/values/strings.xml @@ -5,7 +5,7 @@ Disable GPU timer queries Disable Vulkan shader subgroups Use Magma R11G11B10F fallback - Magma frames in flight (1-64) + Frames in flight (1-64) Avoid sampler mipmap minification filters Treat explicit-flush persistent maps as coherent Use ANGLE GLES libraries diff --git a/android-plugin/app/src/trace/AndroidManifest.xml b/android-plugin/app/src/trace/AndroidManifest.xml index 803d3cab..e1ebe1e9 100644 --- a/android-plugin/app/src/trace/AndroidManifest.xml +++ b/android-plugin/app/src/trace/AndroidManifest.xml @@ -25,7 +25,7 @@ android:name="mobilegl_magma_r11g11b10f_fallback_title" tools:node="remove" />