mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[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:
@@ -132,6 +132,12 @@ namespace MobileGL::MG_Config {
|
|||||||
Bool MagmaR11G11B10FFallback = false;
|
Bool MagmaR11G11B10FFallback = false;
|
||||||
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
|
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
|
||||||
Uint32 MagmaFramesInFlight = 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,
|
// MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
|
||||||
// resolves certain rendering bugs on ANGLE + llvmpipe.
|
// resolves certain rendering bugs on ANGLE + llvmpipe.
|
||||||
Bool EsprytAvoidSamplerMipmapMinFilter = false;
|
Bool EsprytAvoidSamplerMipmapMinFilter = false;
|
||||||
|
|||||||
@@ -177,6 +177,8 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
||||||
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
||||||
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
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 =
|
features.EsprytAvoidSamplerMipmapMinFilter =
|
||||||
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||||
features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
|
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,
|
Bool VulkanRenderer::SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
|
||||||
const DrawCmdParam& drawParams,
|
const DrawCmdParam& drawParams,
|
||||||
const IndexBufferView* pIndexBufferView) {
|
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
|
// Sync each sampled texture at most once across this whole draw: the layout
|
||||||
// probe loop, the post-transition loop, and ResolveSamplerDescriptor would
|
// probe loop, the post-transition loop, and ResolveSamplerDescriptor would
|
||||||
// otherwise each re-run the full SyncTexture path on the same textures.
|
// 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) {
|
void VulkanRenderer::DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) {
|
||||||
|
SplitOversizedRecording();
|
||||||
m_textureManager->CollectGarbage();
|
m_textureManager->CollectGarbage();
|
||||||
auto& frame = m_frameContext.GetCurrent();
|
auto& frame = m_frameContext.GetCurrent();
|
||||||
// The DISPATCH accessor: with a pipeline bound this is its compute stage program
|
// The DISPATCH accessor: with a pipeline bound this is its compute stage program
|
||||||
@@ -7034,6 +7037,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VulkanRenderer::DispatchComputeIndirect(GLintptr indirect) {
|
void VulkanRenderer::DispatchComputeIndirect(GLintptr indirect) {
|
||||||
|
SplitOversizedRecording();
|
||||||
m_textureManager->CollectGarbage();
|
m_textureManager->CollectGarbage();
|
||||||
auto& frame = m_frameContext.GetCurrent();
|
auto& frame = m_frameContext.GetCurrent();
|
||||||
// See DispatchCompute: the dispatch accessor, not the draw one.
|
// See DispatchCompute: the dispatch accessor, not the draw one.
|
||||||
@@ -12669,6 +12673,54 @@ void main() {
|
|||||||
return FlushPendingCommands();
|
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) {
|
Bool VulkanRenderer::WaitForSubmitIndex(Uint64 submitIndex, Uint64 timeoutNs, Bool flushIfPending) {
|
||||||
if (IsSubmitIndexComplete(submitIndex)) {
|
if (IsSubmitIndexComplete(submitIndex)) {
|
||||||
return true;
|
return true;
|
||||||
@@ -12707,6 +12759,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VulkanRenderer::OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) {
|
void VulkanRenderer::OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) {
|
||||||
|
m_drawsInRecording = 0;
|
||||||
// Dynamic state does not survive a command-buffer boundary.
|
// Dynamic state does not survive a command-buffer boundary.
|
||||||
ResetDynamicStateShadow();
|
ResetDynamicStateShadow();
|
||||||
InvalidateSetupDrawSnapshots();
|
InvalidateSetupDrawSnapshots();
|
||||||
|
|||||||
@@ -490,6 +490,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// only), so the presenting path's frames-in-flight pipelining is
|
// only), so the presenting path's frames-in-flight pipelining is
|
||||||
// untouched. Returns true when the drain ran.
|
// untouched. Returns true when the drain ran.
|
||||||
Bool TryDrainFrameTransients();
|
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<SubmitRecord> m_inFlightSubmits;
|
||||||
Vector<VkFence> m_freeSubmitFences;
|
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
|
// aging clocks, while present-less loops still cross a boundary every few
|
||||||
// iterations. Reset in Present.
|
// iterations. Reset in Present.
|
||||||
Uint32 m_drainsSinceLastPresent = 0;
|
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;
|
NativeWindowType m_window = 0;
|
||||||
void* m_platformDisplay = nullptr;
|
void* m_platformDisplay = nullptr;
|
||||||
|
|||||||
@@ -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_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_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_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_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_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` |
|
| `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` |
|
||||||
|
|||||||
Reference in New Issue
Block a user