[MG_Backend, MG_IntegrationTest] (P7): the wire watermark's sync point also fires above 1024 parked stores, so small orphans that never reach the byte budget are bounded by count

Measured on bsl-esc-menu-854 (spawn, lavapipe) with the byte watermark alone: the dead VkBuffers
at the end fell from 25,935 to 32, but one stretch with no submission still parked 12,498 stores
in 39.5 MB - under 64 MiB, so nothing forced a sync. A fixed ceiling
(VkBufferManager::kWireDeferredCountCeiling = 1024, not a knob) now rides the same
WaitForSubmitIndex call; MOBILEGL_IPC_WIRE_DEFERRED_MB=0 still turns both triggers off.
MagmaWireReclaimScenario gains the case: 3000 respecify-and-draw rounds of a 256-byte store in one
frame (750 KB, far below the lane's 8 MiB). Red before this commit on inproc and spawn (live peak
3001 vs a bound of 1027), green after (1025, two forced syncs).
This commit is contained in:
2026-09-22 21:46:08 -04:00
parent 9328bdc735
commit f0356dbe61
5 changed files with 71 additions and 7 deletions
+5 -3
View File
@@ -505,9 +505,11 @@ namespace MobileGL::MG_Config {
// 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.
// delivers one present for 1.3 M calls. The same sync point also fires above a fixed
// 1024 parked stores (VkBufferManager::kWireDeferredCountCeiling), because small
// orphans never reach a byte budget. 0 IS THE NEGATIVE CONTROL for both, not "unlimited
// by design": no forced sync, so a one-frame respecify-and-draw loop grows without bound
// and MagmaWireReclaimScenario's watermark cases must go red.
Uint32 WireDeferredMb = 64;
// MOBILEGL_IPC_SPIN_US: spin before parking on a doorbell, either direction.
Uint32 SpinUs = 50;
@@ -612,7 +612,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void VkBufferManager::EnforceWireDeferredWatermark() {
const Uint64 budget = static_cast<Uint64>(MG_Config::Ipc.WireDeferredMb) * 1024u * 1024u;
if (budget == 0 || m_deferredWireBytes <= budget) return;
// TWO TRIGGERS, ONE SWITCH. Bytes are what the knob names, but a VkBuffer costs per
// OBJECT as well as per byte, and small orphans never reach a byte budget: measured on
// bsl-esc-menu-854 (spawn, lavapipe), one stretch with no submission parked 12,498 stores
// in 39.5 MB against 64 MiB. So a fixed count ceiling rides the same sync point; the
// knob's 0 turns both off, which keeps it one negative control.
if (budget == 0) return;
if (m_deferredWireBytes <= budget && m_deferredWireReleases.size() <= kWireDeferredCountCeiling) 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
@@ -304,6 +304,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// 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();
// ...and the same sync point when more than this many stores are parked, whatever their
// bytes (see the definition for the measurement). Not a knob: it bounds an object count
// the byte budget cannot see, and MOBILEGL_IPC_WIRE_DEFERRED_MB=0 disables it too.
static constexpr SizeT kWireDeferredCountCeiling = 1024;
// 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
+3 -2
View File
@@ -3444,7 +3444,7 @@ endif()
# =====================================================================================
#
# ID-P7-27 / ID-P7-32: bsl-esc-menu-854 delivers one present per replay, and the split server
# held every orphaned VkBuffer until a frame boundary that never came. The two cases read the
# held every orphaned VkBuffer until a frame boundary that never came. The three cases read the
# server's own wbuf[] gauges (PipeStats.h) off its `MGPipe stats:` line, so the flavour pins the
# stats channel at one line per swap, and MOBILEGL_IPC_WIRE_DEFERRED_MB=8 keeps the watermark
# case at 48 MiB of traffic instead of 400.
@@ -3471,7 +3471,8 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
${MGL_ITEST_MAGMA_RECLAIM_FLAVOUR_ENV} ${mglItestArmTransportEnv})
foreach(reclaimCase
RespecifiesWithNoDrawBetweenKeepTheLiveStoreCountBounded
RespecifyAndDrawEachStoreInOneFrameStaysWithinTheDeferredBudget)
RespecifyAndDrawEachStoreInOneFrameStaysWithinTheDeferredBudget
ManySmallRespecifyAndDrawRoundsStayUnderTheStoreCountCeiling)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.${mglItestArm}.Reclaim."
TEST_FILTER "MagmaWireReclaimScenario.${reclaimCase}"
@@ -17,7 +17,7 @@
// split server held 25,923 dead stores against 28 live wire buffers and 41k mappings; on the
// Redmi it died in scudo at 1.2 GiB while the monolith passed at 807 MiB.
//
// WHAT THE TWO CASES PIN, both inside ONE frame (no swap between the first and the last
// WHAT THE THREE CASES PIN, all inside ONE frame (no swap between the first and the last
// respecify), because that is the only shape in which the bug existed:
// * RespecifiesWithNoDrawBetween... - the stores no GPU command ever named. They are dead the
// moment they are orphaned, so the live store count must stay a small multiple of the wire
@@ -27,6 +27,8 @@
// MOBILEGL_IPC_WIRE_DEFERRED_MB watermark's forced sync point. The parked bytes must stay
// within the budget plus one store, and every draw must still land on its own strip - a
// store destroyed while its draw was pending shows up as a wrong strip (or a dead lavapipe).
// * ManySmallRespecifyAndDrawRounds... - the same with stores too small to reach any byte
// budget; the fixed 1024-store ceiling on the same sync point is what bounds them.
//
// READ FROM THE SERVER, NOT THE PICTURE. The numbers are the wbuf[] gauges the server role's
// VkBufferManager publishes (PipeStats.h, Gauge::WireBuffers..WireDeferredSyncs): RUN MAXIMA,
@@ -77,6 +79,13 @@ namespace MGITest {
constexpr int kDrawnRespecifies = 48;
constexpr std::size_t kDrawnStoreBytes = static_cast<std::size_t>(kMiB);
// Case 3: many SMALL respecify-and-draw rounds - 3000 x 256 B is 750 KB, nowhere near
// the byte budget, so only the store-count ceiling (VkBufferManager::
// kWireDeferredCountCeiling, 1024) can bound them inside the frame.
constexpr int kSmallDrawnRespecifies = 3000;
constexpr std::size_t kSmallDrawnStoreBytes = 256;
constexpr long long kDeferredCountCeiling = 1024;
constexpr const char* kVertex = R"(#version 330 core
layout(location = 0) in vec2 aPos;
void main() { gl_Position = vec4(aPos, 0.0, 1.0); })";
@@ -308,5 +317,47 @@ void main() { fragColor = uColor; })";
<< ". " << window.line;
}
// --------------------------------------------------------------------------------------
// CASE 3. 3000 respecify-and-draw rounds of a 256-byte store in one frame. The byte
// budget never trips (750 KB against 8 MiB), which is the bsl-esc-menu-854 shape the
// count ceiling exists for: one stretch there parked 12,498 stores in 39.5 MB. Before M2
// every one of the 3000 was alive at the end of the loop.
TEST_F(MagmaWireReclaimScenario, ManySmallRespecifyAndDrawRoundsStayUnderTheStoreCountCeiling) {
if (!Ready() || IsSkipped()) return;
const PipeStatsWindow::LogMark mark = SetupFrame();
ClearTo(0.0f, 0.0f, 0.0f, 0.0f);
std::vector<std::uint8_t> bytes(kSmallDrawnStoreBytes, 0);
for (int i = 0; i < kSmallDrawnRespecifies; ++i) {
Respecify(bytes, 0, kWidth);
DrawStrip(i % 200);
}
const Image image = ReadPixels(kWidth, kHeight);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
const Rgba8 want = StripColor((kSmallDrawnRespecifies - 1) % 200);
EXPECT_TRUE(Near(image.At(kWidth / 2, kHeight / 2), want))
<< "read " << image.At(kWidth / 2, kHeight / 2) << ", wanted " << want
<< ": the last of " << kSmallDrawnRespecifies << " draws did not land on top";
const PipeStatsWindow::Window window = CloseFrameAndReadServerWindow(mark);
ASSERT_TRUE(window.found) << "no 'MGPipe stats:' window in the server log "
<< PipeStatsWindow::ServerLibraryLogPath();
const long long wireBuffers = PipeStatsWindow::CounterOrAbsent(window, "wbufs");
const long long livePeak = PipeStatsWindow::CounterOrAbsent(window, "wlivepk");
const long long syncs = PipeStatsWindow::CounterOrAbsent(window, "wdefsync");
ASSERT_GT(wireBuffers, 0) << "the server published no wbufs= gauge: " << window.line;
RecordProperty("wire_buffers", static_cast<int>(wireBuffers));
RecordProperty("wire_stores_peak", static_cast<int>(livePeak));
RecordProperty("wire_deferred_syncs", static_cast<int>(syncs));
// The ceiling is checked after the park that crosses it, so one store past it plus the
// records is the most the arm can hold.
const long long liveBound = wireBuffers + kDeferredCountCeiling + 2;
EXPECT_LE(livePeak, liveBound)
<< "the Magma server held " << livePeak << " VkBuffers at once after "
<< kSmallDrawnRespecifies << " small respecify-and-draw rounds in one frame; the "
"records plus the " << kDeferredCountCeiling << "-store ceiling allow " << liveBound
<< ". A count that tracks the rounds is the bsl-esc-menu-854 shape. " << window.line;
}
} // namespace
} // namespace MGITest