From c569c916c070739903be4d39374a45e38dc3bc83 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 22 Sep 2026 20:56:47 -0400 Subject: [PATCH] [MG_Backend] (P7): when the orphaned wire buffer stores still parked exceed MOBILEGL_IPC_WIRE_DEFERRED_MB (64 by default), the server takes the host-access sync point mid-frame and reclaims them all The defer-path sweep frees an orphan once its last submission has retired, but inside one long frame a store that was drawn from is named by recorded, unsubmitted work, and nothing submits it until the frame ends. The knob is read once through MG_Config::Ipc like its siblings; above it the park calls WaitForSubmitIndex(GetSyncPointSubmitIndex(), flush) - the call WaitForWireBufferHostAccess makes - and sweeps again, which empties the list because no entry can be tagged past the sync point just waited out. A sync point that cannot complete dies through the existing Fatal{ResourceUnavailable, "buffer-write-sync"} site, now one helper shared with the host write (fatal_census: 79 abort sites, unchanged). 0 is the negative control. --- MobileGL/Config.h | 12 ++++++ MobileGL/ConfigLoader.cpp | 12 +++++- .../DirectVulkan/Renderer/VkBufferManager.cpp | 37 +++++++++++++++---- .../DirectVulkan/Renderer/VkBufferManager.h | 13 ++++++- 4 files changed, 62 insertions(+), 12 deletions(-) diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 0075a232..2113fec4 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -497,6 +497,18 @@ namespace MobileGL::MG_Config { // stage chunk budget (MGPipeStageChunkBytes, a quarter of this), and a record type with // no cut is Fatal{RingOverrun, "SEG_STAGE"} rather than allowed to exceed it. Uint32 StageMb = 32; + // MOBILEGL_IPC_WIRE_DEFERRED_MB (P7 wave 4 M2, ID-P7-32): the SERVER's budget, in MiB, + // for orphaned wire buffer stores - the old VkBuffer every glBufferData that crosses the + // wire leaves behind - that a GPU command recorded but not yet retired may still name. + // Stores no command names are destroyed at once and stores whose last submission has + // retired are destroyed at the next park; this bounds the REST. When the parked bytes + // exceed it after a sweep, the server flushes what it has recorded and waits for it + // (WaitForWireBufferHostAccess's sync point, mid-frame), which retires every one. It is + // not a frame count because a frame is not bounded: a snapshot-exiting pbuffer replay + // delivers one present for 1.3 M calls. 0 IS THE NEGATIVE CONTROL, not "unlimited by + // design": no forced sync, so a one-frame respecify-and-draw loop grows without bound + // and MagmaWireReclaimScenario's watermark case must go red. + Uint32 WireDeferredMb = 64; // MOBILEGL_IPC_SPIN_US: spin before parking on a doorbell, either direction. Uint32 SpinUs = 50; // MOBILEGL_IPC_PERSISTENT_BLOCK_KB: block granularity of the persistent-map push. diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 3826c87e..2f898ebc 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -392,6 +392,9 @@ namespace MobileGL::MG_ConfigLoader { // 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); // 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. @@ -462,10 +465,10 @@ namespace MobileGL::MG_ConfigLoader { 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. - MGLOG_I("Config: IPC ring=%uMiB stage=%uMiB spin=%uus persistent-block=%uKiB " + MGLOG_I("Config: IPC ring=%uMiB stage=%uMiB wire-deferred=%uMiB spin=%uus persistent-block=%uKiB " "adopt-tier=%u verb-barrier=%u run-ahead=%u present-credit=%u strict=%d " "audit=%d role-split-state=%d affinity='%s'", - ipc.RingMb, ipc.StageMb, ipc.SpinUs, ipc.PersistentBlockKb, ipc.AdoptTier, + ipc.RingMb, ipc.StageMb, ipc.WireDeferredMb, ipc.SpinUs, ipc.PersistentBlockKb, ipc.AdoptTier, ipc.VerbBarrier, ipc.RunAhead, ipc.PresentCredit, static_cast(ipc.StrictErrors), static_cast(ipc.Audit), static_cast(ipc.RoleSplitState), ipc.ServerAffinity.c_str()); @@ -474,6 +477,11 @@ namespace MobileGL::MG_ConfigLoader { "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"); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index 3d4e71ed..be5e3b2d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -365,10 +365,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { uploaded = StagedWireRangeCopy(*resource, bytes, static_cast(offset), static_cast(size)); } - if (!uploaded && !WaitForWireBufferHostAccess(*resource)) { - MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-write-sync\"}"); - std::abort(); - } + if (!uploaded && !WaitForWireBufferHostAccess(*resource)) WireBufferSyncFatal("host-write"); } if (!uploaded) { uploaded = resource->buffer.Upload(bytes, size, offset); @@ -606,14 +603,38 @@ namespace MobileGL::MG_Backend::DirectVulkan { // pbuffer replay that snapshot-exits delivers ONE present for 1.3 M calls (ID-P7-32) - // so the only cadence that tracks the workload is the workload itself. SweepDeferredWireReleases(); + EnforceWireDeferredWatermark(); + } + + void VkBufferManager::EnforceWireDeferredWatermark() { + const Uint64 budget = static_cast(MG_Config::Ipc.WireDeferredMb) * 1024u * 1024u; + if (budget == 0 || m_deferredWireBytes <= budget) return; + // What is left after the sweep is named by work that is recorded but unsubmitted, or + // submitted but unretired. Every entry is tagged at or below the sync point taken HERE, + // so waiting it out - the host-access wait's own call, which flushes the recording first + // - proves all of them dead. A mid-frame flush is what WaitForWireBufferHostAccess and a + // glClientWaitSync already do to this command stream; it costs one submission per + // budget's worth of orphans, not one per glBufferData. + if (pVulkanRenderer == nullptr || + !pVulkanRenderer->WaitForSubmitIndex(pVulkanRenderer->GetSyncPointSubmitIndex(), UINT64_MAX, true)) { + WireBufferSyncFatal("deferred-watermark"); + } + SweepDeferredWireReleases(); + } + + void VkBufferManager::WireBufferSyncFatal(const char* site) { + MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-write-sync\"} {site=%s}", site); + std::abort(); } SizeT VkBufferManager::SweepDeferredWireReleases() { - if (m_deferredWireReleases.empty() || pVulkanRenderer == nullptr) return 0; + if (m_deferredWireReleases.empty()) return 0; SizeT dead = 0; - if (pVulkanRenderer->IsSubmitIndexComplete(pVulkanRenderer->GetSyncPointSubmitIndex())) { - // Idle: nothing recorded, every submission retired. Every parked store is dead, - // including one tagged for a batch that was abandoned instead of submitted. + if (pVulkanRenderer == nullptr || + pVulkanRenderer->IsSubmitIndexComplete(pVulkanRenderer->GetSyncPointSubmitIndex())) { + // Idle: nothing recorded, every submission retired - or no renderer, so no queue. + // Every parked store is dead, including one tagged for a batch that was abandoned + // instead of submitted. dead = m_deferredWireReleases.size(); } else { // The list is in park order and GetSyncPointSubmitIndex() never decreases, so the diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h index b923cf6f..f8e9ebcc 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h @@ -294,13 +294,22 @@ namespace MobileGL::MG_Backend::DirectVulkan { // running total has to stay exact as entries leave. Uint64 bytes = 0; }; - // Park a wire store, then sweep. `lastUseSerial` is the releasing resource's, captured - // before the caller resets it. + // Park a wire store, sweep, then hold the watermark. `lastUseSerial` is the releasing + // resource's, captured before the caller resets it. void DeferWireRelease(VkBufferObject&& buffer, Uint64 lastUseSerial); // Destroy every parked store proven dead by the rules above. Returns how many. SizeT SweepDeferredWireReleases(); + // MOBILEGL_IPC_WIRE_DEFERRED_MB (Config.h has the semantics): when the parked bytes + // still exceed the budget after a sweep, take the sync point WaitForWireBufferHostAccess + // takes - flush what is recorded, wait for it - and sweep again, which retires every + // parked store, because none can be tagged past the sync point it just waited out. + void EnforceWireDeferredWatermark(); // Teardown: the caller has proven the device idle (Shutdown / RecreateTransientArenas). void DestroyAllDeferredWireReleases(); + // THE one Fatal{ResourceUnavailable, "buffer-write-sync"} site (rule I: no second abort + // for the census to count), shared by the host write that cannot wait for the GPU and + // the watermark that cannot. `site` says which. + [[noreturn]] static void WireBufferSyncFatal(const char* site); #endif Bool InitializeTransientArenas(); static VkBufferUsageFlags GetVkBufferUsage(BufferKind kind);