[Fix] (Magma): a frame's recording is submitted every 16384 draws/dispatches and continues on a fresh command buffer, with at most frames-in-flight + 1 submissions outstanding - rd12's 981k-draw loading frame no longer grows one command buffer until Adreno's 16 KiB driver mappings exhaust vm.max_map_count (MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER, 0 = unbounded)

This commit is contained in:
2026-09-29 09:44:58 -04:00
parent 21c7e8cd63
commit d20c2d01ae
5 changed files with 75 additions and 0 deletions
+6
View File
@@ -132,6 +132,12 @@ namespace MobileGL::MG_Config {
Bool MagmaR11G11B10FFallback = false;
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
Uint32 MagmaFramesInFlight = 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
// freed; Adreno maps it into the process in 16 KiB chunks, so one ~1M-draw loading frame
// exhausts vm.max_map_count. See VulkanRenderer::SplitOversizedRecording.
Uint32 MagmaMaxDrawsPerCommandBuffer = 16384;
// MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
// resolves certain rendering bugs on ANGLE + llvmpipe.
Bool EsprytAvoidSamplerMipmapMinFilter = false;
+2
View File
@@ -177,6 +177,8 @@ namespace MobileGL::MG_ConfigLoader {
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
features.MagmaMaxDrawsPerCommandBuffer =
QueryEnvUint32("MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER", 16384, 0, 1u << 24);
features.EsprytAvoidSamplerMipmapMinFilter =
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
@@ -6381,6 +6381,8 @@ void main() {
Bool VulkanRenderer::SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
const DrawCmdParam& drawParams,
const IndexBufferView* pIndexBufferView) {
// First, before anything of this draw is recorded or resolved.
SplitOversizedRecording();
// Sync each sampled texture at most once across this whole draw: the layout
// probe loop, the post-transition loop, and ResolveSamplerDescriptor would
// otherwise each re-run the full SyncTexture path on the same textures.
@@ -6981,6 +6983,7 @@ void main() {
}
void VulkanRenderer::DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) {
SplitOversizedRecording();
m_textureManager->CollectGarbage();
auto& frame = m_frameContext.GetCurrent();
// The DISPATCH accessor: with a pipeline bound this is its compute stage program
@@ -7034,6 +7037,7 @@ void main() {
}
void VulkanRenderer::DispatchComputeIndirect(GLintptr indirect) {
SplitOversizedRecording();
m_textureManager->CollectGarbage();
auto& frame = m_frameContext.GetCurrent();
// See DispatchCompute: the dispatch accessor, not the draw one.
@@ -12669,6 +12673,54 @@ void main() {
return FlushPendingCommands();
}
void VulkanRenderer::SplitOversizedRecording() {
const Uint32 maxDraws = MG_Config::Features.MagmaMaxDrawsPerCommandBuffer;
if (maxDraws == 0) {
return;
}
// Counted before the check, so a buffer carries at most maxDraws calls before the one
// that splits it. OnFrameCommandRecordingBegan resets the count on every new recording.
if (++m_drawsInRecording <= maxDraws || !HasPendingRecordedWork()) {
return;
}
// Nothing of the current call is recorded yet, so the split falls between two GL calls,
// exactly where a glFlush would put it: the render pass ends with STORE, the next draw
// re-begins it with LOAD, and queries and transform feedback are bracketed per draw.
if (!FlushPendingCommands()) {
return;
}
// Fresh command buffer: the same reset every other mid-frame flush site applies.
m_lastSampledSetValid = false;
++m_oversizedRecordingSplits;
MGLOG_I_ONCE("DirectVulkan: a frame recorded more than %u draws into one command buffer; submitting it and "
"continuing on a fresh one (MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER, 0 = unbounded)",
maxDraws);
MGLOG_D("DirectVulkan: split an oversized recording (%llu so far, submit index %llu)",
static_cast<unsigned long long>(m_oversizedRecordingSplits),
static_cast<unsigned long long>(m_submitCounter));
// Splitting only bounds the driver's command memory if the submitted buffers are freed
// as fast as they are produced. A GPU that falls behind would park them all on the
// retired list, which is the same growth spread over more buffers - so hold the queue
// to frames-in-flight + 1 outstanding submissions, the steady state of a pipelined
// frame plus this split. Wait on the fence directly rather than through
// WaitForSubmitIndex: its opportunistic TryDrainFrameTransients may rewind the transient
// arena, and this runs at the start of a draw whose caller may already hold slices of it.
const SizeT maxOutstanding = static_cast<SizeT>(m_frameContext.GetFrameCount()) + 1;
while (m_inFlightSubmits.size() > maxOutstanding) {
const SubmitRecord oldest = m_inFlightSubmits.front();
if (oldest.fence == VK_NULL_HANDLE) {
break;
}
const VkResult waitResult = vkWaitForFences(m_device, 1, &oldest.fence, VK_TRUE, UINT64_MAX);
if (waitResult != VK_SUCCESS) {
MGLOG_E_ONCE("SplitOversizedRecording: vkWaitForFences returned %d", waitResult);
break;
}
OnSubmitsCompletedUpTo(oldest.submitIndex);
}
}
Bool VulkanRenderer::WaitForSubmitIndex(Uint64 submitIndex, Uint64 timeoutNs, Bool flushIfPending) {
if (IsSubmitIndexComplete(submitIndex)) {
return true;
@@ -12707,6 +12759,7 @@ void main() {
}
void VulkanRenderer::OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) {
m_drawsInRecording = 0;
// Dynamic state does not survive a command-buffer boundary.
ResetDynamicStateShadow();
InvalidateSetupDrawSnapshots();
@@ -490,6 +490,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// only), so the presenting path's frames-in-flight pipelining is
// untouched. Returns true when the drain ran.
Bool TryDrainFrameTransients();
// Bounded recording: called at the top of every GL draw and dispatch, before anything
// of that call is recorded. Once the open frame command buffer holds
// MagmaMaxDrawsPerCommandBuffer draws/dispatches, submits it (FlushPendingCommands) so
// the call continues on a fresh one, then waits until at most frames-in-flight + 1
// submissions are outstanding. A driver backs a command buffer with GPU memory it only
// returns when the buffer is freed (Adreno: one 16 KiB mapping per chunk), so a frame
// that is never split grows it without bound - rd12's loading frame records ~1M draws
// into one buffer and runs the process out of vm.max_map_count.
void SplitOversizedRecording();
Vector<SubmitRecord> m_inFlightSubmits;
Vector<VkFence> m_freeSubmitFences;
@@ -501,6 +510,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// aging clocks, while present-less loops still cross a boundary every few
// iterations. Reset in Present.
Uint32 m_drainsSinceLastPresent = 0;
// GL draws/dispatches recorded into the open frame command buffer; reset whenever a
// recording begins (OnFrameCommandRecordingBegan). See SplitOversizedRecording.
Uint32 m_drawsInRecording = 0;
Uint64 m_oversizedRecordingSplits = 0;
NativeWindowType m_window = 0;
void* m_platformDisplay = nullptr;
+1
View File
@@ -162,6 +162,7 @@ MobileGL supports runtime configuration via environment variables.
| `MOBILEGL_ADVERTISE_FP64` | Advertise `GL_ARB_gpu_shader_fp64`. GLSL `double`/`dvec`/`dmat` compile and run either way - they are narrowed to 32 bits - so this only changes whether an application is told it has 64-bit precision, which it does not. | `0`, `1` | `0` |
| `MOBILEGL_MAGMA_R11G11B10F_FALLBACK` | Use Magma's R11G11B10F format fallback. | `0`, `1` | `0` |
| `MOBILEGL_MAGMA_FRAMESINFLIGHT` | Set Magma frames in flight. | Integer `1`–`64` | `3` |
| `MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER` | Draws and dispatches Magma records into one command buffer before it submits the buffer mid-frame and continues on a fresh one. `0` never splits. | Integer `0`–`16777216` | `16384` |
| `MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER` | Avoid sampler mipmap minification filters. | `0`, `1` | `0` |
| `MOBILEGL_COHERENT_AS_FLUSH` | Treat persistent `GL_MAP_FLUSH_EXPLICIT_BIT` maps as coherent (app-compat for engines like Flywheel that never flush them). | `0`, `1` | `0` |
| `MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION` | Always emulate depth/stencil `glReadPixels`/`glGetTexImage` by shader sampling on Espryt, instead of using the driver's own depth/stencil readback where it has one. | `0`, `1` | `0` |