[Perf] (Magma): a renamed wire store reuses its retired buffers once their submissions complete (FCL inproc apply thread 1.74 -> 1.61 ms/frame at cpuhunt).

This commit is contained in:
2026-10-08 15:26:24 -04:00
parent c8a5abe581
commit 500b63ea58
4 changed files with 82 additions and 7 deletions
@@ -239,6 +239,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource->indirectReadBarrierPending = false;
resource->stagedCoverage.clear();
DropWireShadow(*resource);
ReleaseRenameSpares(*resource);
if (resource->size == 0) return;
if (!CreateWireStoreBuffer(*resource)) {
@@ -279,6 +280,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource.buffer.Map() != nullptr;
}
void VkBufferManager::ReleaseRenameSpares(WireBufferResource& resource) {
for (auto& spare : resource.renameSpares)
DeferWireRelease(std::move(spare.buffer), spare.lastUseSerial != 0 ? spare.lastUseSerial : m_frameSerial);
resource.renameSpares.clear();
}
void VkBufferManager::DropWireShadow(WireBufferResource& resource) {
if (resource.shadow.empty()) return;
m_wireShadowBytes -= resource.shadow.size();
@@ -300,19 +307,45 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MG_Config::Features.MagmaWriteRename == MG_Config::QuirkOverride::ForceOff)
return false;
VkBufferObject old = std::move(resource.buffer);
if (!CreateWireStoreBuffer(resource) ||
!resource.buffer.Upload(resource.shadow.data(), static_cast<VkDeviceSize>(resource.size), 0)) {
resource.buffer.Destroy();
Bool reused = false;
for (SizeT i = 0; pVulkanRenderer != nullptr && i < resource.renameSpares.size(); ++i) {
if (!pVulkanRenderer->IsSubmitIndexComplete(resource.renameSpares[i].submitIndex)) continue;
resource.buffer = std::move(resource.renameSpares[i].buffer);
resource.renameSpares.erase(resource.renameSpares.begin() + static_cast<std::ptrdiff_t>(i));
reused = true;
break;
}
if (!reused) {
if (!CreateWireStoreBuffer(resource)) {
resource.buffer.Destroy();
resource.buffer = std::move(old);
return false;
}
++m_wireStoreCount;
}
if (!resource.buffer.Upload(resource.shadow.data(), static_cast<VkDeviceSize>(resource.size), 0)) {
// Keep the store on its old buffer; the new one goes the deferred way (it is idle).
DeferWireRelease(std::move(resource.buffer), 0);
resource.buffer = std::move(old);
return false;
}
// Busy means a GPU command recorded so far may name the old buffer: release it behind
// this frame's serial and the current sync point, whatever its own stamps said.
DeferWireRelease(std::move(old), m_frameSerial);
// Busy means a GPU command recorded so far may name the old buffer: it waits behind this
// frame's serial and the current sync point, whatever its own stamps said - as a spare
// for the next rename, or (past the bound) through DeferWireRelease.
constexpr SizeT kRenameSpares = 4;
if (resource.renameSpares.size() >= kRenameSpares) {
auto& oldest = resource.renameSpares.front();
DeferWireRelease(std::move(oldest.buffer), oldest.lastUseSerial);
resource.renameSpares.erase(resource.renameSpares.begin());
}
WireBufferResource::RenameSpare spare;
spare.buffer = std::move(old);
spare.submitIndex = pVulkanRenderer != nullptr ? pVulkanRenderer->GetSyncPointSubmitIndex() : 0;
spare.lastUseSerial = m_frameSerial;
resource.renameSpares.push_back(std::move(spare));
resource.lastUseSerial = 0;
resource.lastUseSubmitIndex = 0;
resource.gpuWritesPending = false;
++m_wireStoreCount;
++m_wireRenames;
NoteWireStorePeaks();
PublishWireReclaimGauges();
@@ -1154,6 +1187,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const auto found = m_wireBuffers.find(WireBufferKey(res));
if (found == m_wireBuffers.end()) return;
DropWireShadow(found->second);
ReleaseRenameSpares(found->second);
DeferWireRelease(std::move(found->second.buffer),
found->second.imported ? T0ReleaseSerial(found->second.lastUseSerial)
: found->second.lastUseSerial); // P11 B2, as Respecify
@@ -204,6 +204,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// WriteWireBuffer). Empty: no shadow (a large store, the budget was spent, or a GPU
// write, shader-writable bind or client mapping made the host copy unknowable).
Vector<Uint8> shadow;
// P15: buffers this store was renamed away from, kept for the next rename instead of
// allocating one (a UI store is renamed several times a frame). A spare is reused only
// once the submission that can name it has completed - DeferWireRelease's own rule -
// and its handle never died, so no handle-keyed memo can mistake it for another
// buffer. Bounded; the rest go through DeferWireRelease.
struct RenameSpare {
VkBufferObject buffer;
Uint64 submitIndex = 0;
Uint64 lastUseSerial = 0;
};
Vector<RenameSpare> renameSpares;
Bool gpuWritesPending = false;
// Only ranges actually submitted by resource_subdata are covered. No
// shadow is retained: flush cannot replay stale bytes over GPU writes.
@@ -220,6 +231,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 m_wireRenames = 0;
SizeT m_wireShadowBytes = 0;
void DropWireShadow(WireBufferResource& resource);
void ReleaseRenameSpares(WireBufferResource& resource);
Bool CreateWireStoreBuffer(WireBufferResource& resource);
Bool RenameBusyWireStore(WireBufferResource& resource);
static Uint64 WireBufferKey(MG_Pipe::MGPipeHandle res) {
@@ -220,6 +220,32 @@ void main() { o0 = vColor * uTint; }
Destroy(s);
}
// Two renames of ONE store inside one frame: the second must not reuse the buffer the first
// draw still reads (nothing is submitted yet, so a reused spare would be overwritten under it).
TEST_F(WriteRenameScenario, ThreeDrawsAroundTwoRenamesInOneFrameEachReadTheirOwnBytes) {
if (!Ready()) return;
std::vector<Vertex> v(4);
PutQuad(v, 0, 0.0f, 0.0f, 0.33f, 1.0f, 255, 0, 0); // left third, red
std::string error;
Scene s = MakeScene(v, &error);
ASSERT_NE(s.program, 0u) << error;
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
std::vector<Vertex> middle(4), right(4);
PutQuad(middle, 0, 0.33f, 0.0f, 0.66f, 1.0f, 0, 255, 0);
glBufferSubData(GL_ARRAY_BUFFER, 0, static_cast<GLsizeiptr>(middle.size() * sizeof(Vertex)), middle.data());
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
PutQuad(right, 0, 0.66f, 0.0f, 1.0f, 1.0f, 0, 0, 255);
glBufferSubData(GL_ARRAY_BUFFER, 0, static_cast<GLsizeiptr>(right.size() * sizeof(Vertex)), right.data());
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(Near(At(kSize / 6, kSize / 2), 255, 0, 0)) << "left: the first draw's bytes";
EXPECT_TRUE(Near(At(kSize / 2, kSize / 2), 0, 255, 0)) << "middle: after the first rename";
EXPECT_TRUE(Near(At(kSize * 5 / 6, kSize / 2), 0, 0, 255)) << "right: after the second rename";
Destroy(s);
}
TEST_F(WriteRenameScenario, ARenamedStoreKeepsDrawingRightInLaterFrames) {
if (!Ready()) return;
std::vector<Vertex> v(4);
@@ -31,6 +31,9 @@ case "${1:-status}" in
SER=$SER bash "$HERE/pin_clocks.sh" pin
"$0" status ;;
stop)
# Trace-replay packages a job started must not idle on the device after it (they hold GPU
# memory and a surface): every MobileGL plugin package is force-stopped at session end.
su_sh "for p in \$(pm list packages top.mobilegl.plugin | sed s/package://); do am force-stop \$p; done"
su_sh "if [ -f $MARK ]; then grep \"^svc \" $MARK | while read k s st; do [ \"\$st\" = running ] && setprop ctl.start \$s; done; else for s in $SERVICES; do setprop ctl.start \$s; done; fi"
sleep 3
bad=$(su_sh "if [ -f $MARK ]; then grep \"^svc \" $MARK | while read k s st; do [ \"\$st\" = running ] && [ \"\$(getprop init.svc.\$s)\" != running ] && echo \$s; done; fi" | tr -d '\r')