[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.
This commit is contained in:
2026-09-22 21:44:06 -04:00
parent b81a8827b5
commit c569c916c0
4 changed files with 62 additions and 12 deletions
+12
View File
@@ -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.
+10 -2
View File
@@ -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<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
static_cast<int>(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");
@@ -365,10 +365,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
uploaded = StagedWireRangeCopy(*resource, bytes, static_cast<SizeT>(offset),
static_cast<SizeT>(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<Uint64>(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
@@ -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);