[MG_Backend, MG_IntegrationTest] (P7): bound wire descriptor sets within long frames

This commit is contained in:
2026-09-23 01:35:20 -04:00
parent 254bfb013e
commit 9fd768d493
8 changed files with 171 additions and 6 deletions
@@ -674,6 +674,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
#if MOBILEGL_BUILD_DISAGGREGATED
Bool UniformManager::WireDescriptorSetBudgetReached(Uint32 frameIndex) const {
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "WireDescriptorSetBudgetReached invalid frame index");
return m_frames[frameIndex].allocatedSetsThisFrame >= kWireDescriptorSetBudget;
}
SizeT UniformManager::RewindWireDescriptorSets(Uint32 frameIndex) {
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "RewindWireDescriptorSets invalid frame index");
auto& frame = m_frames[frameIndex];
// The caller proved that the last graphics submit using these sets has
// retired and that no unsubmitted command buffer still references them.
// Keep the pools and sets: only the per-layout write cursors rewind.
// Image and buffer views stay alive until the regular frame boundary.
m_peakDescriptorSetsObserved = std::max(m_peakDescriptorSetsObserved, frame.peakAllocatedSetsThisFrame);
SizeT cachedSets = 0;
for (auto& entry : frame.descriptorSetCacheByLayout) {
cachedSets += entry.second.sets.size();
entry.second.cursor = 0;
}
frame.allocatedSetsThisFrame = 0;
frame.peakAllocatedSetsThisFrame = 0;
for (auto& entry : m_descriptorReuseMemo) entry.valid = false;
m_fastRebindMemo.valid = false;
m_lastBindValid = false;
const Uint32 touchedBindings =
std::min<Uint32>(m_samplerResolveMemoHighWater, static_cast<Uint32>(m_samplerResolveMemo.size()));
for (Uint32 binding = 0; binding < touchedBindings; ++binding) {
m_samplerResolveMemo[binding].valid = false;
m_samplerResolveMemo[binding].infoValid = false;
}
return cachedSets;
}
#endif
void UniformManager::OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout) {
SizeT purgedSets = 0;
for (auto& frame : m_frames) {
@@ -54,6 +54,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void Shutdown();
void BeginFrame(Uint32 frameIndex);
#if MOBILEGL_BUILD_DISAGGREGATED
// A present-less wire frame may run arbitrarily many draws. Before its
// descriptor cursor reaches this budget, the renderer retires the
// submission that last used the sets and rewinds every layout cursor.
static constexpr Uint32 kWireDescriptorSetBudget = 2048;
Bool WireDescriptorSetBudgetReached(Uint32 frameIndex) const;
SizeT RewindWireDescriptorSets(Uint32 frameIndex);
#endif
// A command buffer (re)began recording: descriptor bindings recorded into
// the previous buffer do not carry over, so drop the bind-dedup shadow.
void OnCommandBufferBoundary() { m_lastBindValid = false; }
@@ -7061,8 +7061,10 @@ void main() {
const DrawCmdParam& drawParams,
const IndexBufferView* pIndexBufferView) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith)
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
RewindWireDescriptorSetsIfDue();
return SetupWireDraw(frame, mode, aspects, drawParams, pIndexBufferView);
}
#endif
// Sync each sampled texture at most once across this whole draw: the layout
// probe loop, the post-transition loop, and ResolveSamplerDescriptor would
@@ -7682,6 +7684,7 @@ void main() {
void VulkanRenderer::DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
RewindWireDescriptorSetsIfDue();
DispatchWireCompute(numGroupsX, numGroupsY, numGroupsZ);
return;
}
@@ -13573,6 +13576,36 @@ void main() {
return frame.isCommandRecording || frame.hasCommandBufferRecorded;
}
#if MOBILEGL_BUILD_DISAGGREGATED
void VulkanRenderer::RewindWireDescriptorSetsIfDue() {
if (!m_uniformManager || m_frameContext.GetFrameCount() == 0) return;
const Uint32 frameIndex = m_frameContext.GetCurrentFrameIndex();
if (!m_uniformManager->WireDescriptorSetBudgetReached(frameIndex)) return;
// A set cannot be rewritten while a recorded or submitted command
// buffer might still read it. This wire-only boundary first submits
// the current recording, then waits for that exact submit index.
if (HasPendingRecordedWork() && !FlushPendingCommands())
MagmaWireFatal("descriptor-rewind-flush");
const Uint64 through = m_submitCounter;
if (through > m_completedSubmitCounter) {
// B4 has not yet made every old retirement site aggregate-safe.
// Queue idle proves even a submission a previous single-fence path
// prematurely removed from m_inFlightSubmits has finished; only
// then may any layout cursor be rewound and its sets rewritten.
if (vkQueueWaitIdle(m_graphicsQueue) != VK_SUCCESS)
MagmaWireFatal("descriptor-rewind-wait");
OnSubmitsCompletedUpTo(through);
}
if (HasPendingRecordedWork() || m_completedSubmitCounter < through)
MagmaWireFatal("descriptor-rewind-proof");
const SizeT cachedSets = m_uniformManager->RewindWireDescriptorSets(frameIndex);
MGLOG_I("Magma descriptor rewind: frame=%u retiredSubmit=%llu budget=%u cached=%zu",
frameIndex, static_cast<unsigned long long>(through),
UniformManager::kWireDescriptorSetBudget, cachedSets);
}
#endif
Bool VulkanRenderer::IsSubmitIndexComplete(Uint64 submitIndex) {
if (submitIndex <= m_completedSubmitCounter) {
return true;
@@ -628,6 +628,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<WireRetiredObjects> m_wireRetiredObjects;
Uint32 m_wirePreparationDepth = 0;
void DispatchWireCompute(GLuint x, GLuint y, GLuint z);
void RewindWireDescriptorSetsIfDue();
UniquePtr<RenderPassEntry> m_wireDrawPass;
Vector<VkImageView> m_wireDrawViews;
VkPipeline GetOrCreatePipelineWithInput(GLenum mode, const MagmaProgramSource& program,
+4 -3
View File
@@ -3469,13 +3469,14 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
mgl_itest_magma_arm_shape(${mglItestArm} reclaim)
mgl_itest_join_environment(MGL_ITEST_MAGMA_RECLAIM_ENVIRONMENT
${MGL_ITEST_MAGMA_RECLAIM_FLAVOUR_ENV} ${mglItestArmTransportEnv})
# Round 2 (ID-P7-43) adds the fourth: the reclaim's own memo hazard. It reads no gauge, but it
# is the same fixture, the same in-frame shape and the same two arms, so it rides this list.
# Round 2 (ID-P7-43) adds the fourth: the reclaim's own memo hazard. M3 adds the fifth:
# fixed-store descriptor churn in a long frame. Both use this private-log fixture and two arms.
foreach(reclaimCase
RespecifiesWithNoDrawBetweenKeepTheLiveStoreCountBounded
RespecifyAndDrawEachStoreInOneFrameStaysWithinTheDeferredBudget
ManySmallRespecifyAndDrawRoundsStayUnderTheStoreCountCeiling
ADrawAfterTheEarlyReclaimFollowsTheNewStoreNotTheMemoizedHandle)
ADrawAfterTheEarlyReclaimFollowsTheNewStoreNotTheMemoizedHandle
DescriptorSetsRewindInsideOneLongFrameAfterTheirSubmitRetires)
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 THREE CASES PIN, all inside ONE frame (no swap between the first and the last
// WHAT THE 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
@@ -43,8 +43,10 @@
// whose environment the lane cannot set and whose log this process cannot read
// (scripts/ci/spawn_lane_parity.py names the exception).
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
@@ -99,6 +101,7 @@ namespace MGITest {
constexpr std::size_t kAbaPinStoreBytes = static_cast<std::size_t>(kMiB);
constexpr std::size_t kAbaNewStoreBytes = 64 * 1024;
constexpr GLuint64 kAbaWaitNs = 2000000000ull; // 2 s: a 64x16 draw on lavapipe retires in ms
constexpr int kDescriptorDraws = 2049; // one past UniformManager's wire epoch budget
constexpr const char* kVertex = R"(#version 330 core
layout(location = 0) in vec2 aPos;
@@ -118,6 +121,14 @@ layout(std140) uniform Block { vec4 color; };
out vec4 fragColor;
void main() { fragColor = color; })";
constexpr const char* kStorageVertex = R"(#version 430 core
layout(location = 0) in vec2 aPos;
void main() { gl_Position = vec4(aPos, 0.0, 1.0); })";
constexpr const char* kStorageFragment = R"(#version 430 core
layout(std430, binding = 0) readonly buffer Colors { vec4 color[]; };
out vec4 fragColor;
void main() { fragColor = color[0]; })";
// Two triangles covering pixel columns [x0, x1) over the whole height, in NDC.
void WriteStripQuad(float* out, int x0, int x1) {
const float l = -1.0f + 2.0f * static_cast<float>(x0) / kWidth;
@@ -506,5 +517,62 @@ void main() { fragColor = color; })";
static_cast<int>(PipeStatsWindow::CounterOrAbsent(window, "wlivepk")));
}
// M3: a fixed SSBO store and one live layout, but 2049 distinct aligned
// windows force 2049 descriptor writes before a present. M2's buffer-store
// ceiling cannot help: no store is orphaned. The first 2048 sets must be
// retired by submit index before their per-layout cursor rewinds, and the
// last draw must read the last window rather than a set still in flight.
TEST_F(MagmaWireReclaimScenario, DescriptorSetsRewindInsideOneLongFrameAfterTheirSubmitRetires) {
if (!Ready() || IsSkipped()) return;
std::string error;
const GLuint program = CompileProgram(kStorageVertex, kStorageFragment, &error);
ASSERT_NE(program, 0u) << error;
const PipeStatsWindow::LogMark mark = SetupFrame();
ClearTo(0.0f, 0.0f, 0.0f, 0.0f);
std::vector<std::uint8_t> quad(kUndrawnStoreBytes, 0);
glBindBuffer(GL_ARRAY_BUFFER, m_buffer);
Respecify(quad, 0, kWidth);
GLint alignment = 0;
glGetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &alignment);
ASSERT_GT(alignment, 0);
const std::size_t stride = static_cast<std::size_t>(std::max(alignment, 16));
std::vector<std::uint8_t> bytes(stride * kDescriptorDraws, 0);
const Rgba8 last{60, 200, 90, 255};
for (int i = 0; i < kDescriptorDraws; ++i) {
const float color[4] = {i + 1 == kDescriptorDraws ? last.r / 255.0f : 200.0f / 255.0f,
i + 1 == kDescriptorDraws ? last.g / 255.0f : 60.0f / 255.0f,
i + 1 == kDescriptorDraws ? last.b / 255.0f : 30.0f / 255.0f, 1.0f};
std::memcpy(bytes.data() + stride * i, color, sizeof(color));
}
GLuint storage = 0;
glGenBuffers(1, &storage);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, storage);
glBufferData(GL_SHADER_STORAGE_BUFFER, static_cast<GLsizeiptr>(bytes.size()), bytes.data(), GL_STATIC_DRAW);
glUseProgram(program);
for (int i = 0; i < kDescriptorDraws; ++i) {
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, storage,
static_cast<GLintptr>(stride * i), 16);
glDrawArrays(GL_TRIANGLES, 0, 6);
}
const Image image = ReadPixels(kWidth, kHeight);
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
EXPECT_TRUE(Near(image.At(kWidth / 2, kHeight / 2), last))
<< "the last SSBO window was not read after descriptor recycling";
(void)CloseFrameAndReadServerWindow(mark);
const std::string serverLog = PipeStatsWindow::ReadServerLogSince(mark);
const std::string marker = "Magma descriptor rewind:";
const std::size_t rewind = serverLog.find(marker);
ASSERT_NE(rewind, std::string::npos) << "2049 distinct descriptor writes in one frame did not rewind";
const std::size_t cachedAt = serverLog.find(" cached=", rewind);
ASSERT_NE(cachedAt, std::string::npos) << serverLog.substr(rewind, 160);
const unsigned long long cached = std::strtoull(serverLog.c_str() + cachedAt + 8, nullptr, 10);
EXPECT_LE(cached, 2080ull) << "the fixed-layout cache grew beyond the 2048-set epoch: "
<< serverLog.substr(rewind, 160);
RecordProperty("descriptor_cached_at_rewind", static_cast<int>(cached));
glDeleteBuffers(1, &storage);
glDeleteProgram(program);
}
} // namespace
} // namespace MGITest
+19
View File
@@ -0,0 +1,19 @@
# P7 M3:长帧内 descriptor set 游标回卷(草稿,待门)
基线:集成树 `0e16ca27`。本包只在 disaggregated 构建的 Magma wire draw / compute 入口触发;pull 的语句与对象代码不变。设备 p7w6 在 M2 后已让 bsl-esc-menu 的 inproc / spawn 各三遍通过,但 M2 的 1,024 store 同步只是偶然给 descriptor 游标制造 drain,不能作为 descriptor set 自己的界。
## 机制
`UniformManager::AcquireDescriptorSet` 每次实际占用一个 layout 游标(从缓存取旧 set 或从 pool 分配新 set)都会增加当前 frame slot 的 `allocatedSetsThisFrame`。达到 2048 次时,下一笔 wire draw / dispatch 在任何新 descriptor 解析前调用 `RewindWireDescriptorSetsIfDue`:
1. 若当前 graphics command buffer 已录制,`FlushPendingCommands` 把它作为新的 `submitIndex` 提交。
2. 取此时的 `m_submitCounter` 为 `through`,等待 graphics queue idle 后才 `OnSubmitsCompletedUpTo(through)`;当前 B4 未落地,已有单 fence 退休站点可能从 tracker 中提前移除一条提交,单靠遍历 tracker 的 fence 不足以证明安全。队列 idle 覆盖这些已移除记录,并显式核对 `m_completedSubmitCounter >= through` 与没有尚未提交的当前录制。B4 全面修正聚合等待后可以再评估这笔保守等待的成本。
3. `UniformManager::RewindWireDescriptorSets` 将每个仍存活 layout 的缓存 cursor 置零,保留 pool 和 set 供改写,清空 descriptor-reuse / fast-rebind / bind-dedup / sampler-resolve memo,并重置本 epoch 的计数。image/buffer view 不在此销毁,仍由原 frame 生命周期管理。
因此一个固定活 layout 的长帧不会因 draw 次数持续分配新 descriptor set。多个 layout 的缓存仍保存各自历史高水位;layout 销毁时沿用 `OnDescriptorSetLayoutDestroyed` 的逐 pool 释放。该界不替代 program/layout 数量本身的生命周期管理。
## 负控与门
`MagmaWireReclaimScenario.DescriptorSetsRewindInsideOneLongFrameAfterTheirSubmitRetires` 用一个 SSBO store、一个 program/layout,在一帧内把 2049 个对齐窗口逐个绑定后 draw,M2 的 orphan-store 上限不会触发。最后一个窗口颜色必须正确;server 日志须给出 submit-index 退休后的回卷,回卷时缓存数不超过 2080。禁用入口的回卷调用应红(无回卷行),恢复后 split / spawn 两臂绿。之后跑 G1 `.text` / 符号恒等、普查、棘轮、parity、八车道、OpenRA 回放与 verify 构建。性能只记录。
当前状态:`~/w7/p7-m3` 的完整 `MobileGL` / `MobileGLServer` / `MobileGLIntegrationTest` 构建通过;新用例 split 与 spawn 2/2 绿(各约 1.2 s,server 行均为 `retiredSubmit=2 budget=2048 cached=2048`)。红一次:仅删 draw 入口回卷调用,三目标重建后两臂都在「2049 distinct descriptor writes in one frame did not rewind」处红,像素断言没有先抢红;恢复源码与三目标重建后两臂再绿。原始日志 `~/w7/logs/p7m3/{green-test,red-test,reclaim-test}.log`。最终源码改用 graphics queue idle 作完整提交证明后,完整目标重建并再次全绿:pull G1 `.text=0xa52203`、符号 +0/−0;fatal census 79、棘轮 173、parity 0、wire-declines 审计 53/53/0 与自测 16/16;split 构建 unit 2436、integration-split 304、spawn 220、tcp 223、magma 102/81/71、full-split 540;verify 构建 unit 2436、integration-verify 1152、integration-verify-split 1086;OpenRA DirectVulkan inproc 与 spawn 两臂均 ssim 1.000000、0 `unsound-serial-complete`。门日志在 `~/w7/logs/p7m3/` 与 `~/w7/logs/p7-gate-m3-final-retrace/`。集成者在 pipe 源码上的审查、落地及设备回归仍待做。
+2 -1
View File
@@ -87,7 +87,8 @@ MAGMA_SERVER_ENV_KNOB_NO_TCP = (".ShaderMip1.", ".ShaderMip2.", ".DepthMip.",
".Reclaim.MagmaWireReclaimScenario.RespecifiesWithNoDrawBetweenKeepTheLiveStoreCountBounded",
".Reclaim.MagmaWireReclaimScenario.RespecifyAndDrawEachStoreInOneFrameStaysWithinTheDeferredBudget",
".Reclaim.MagmaWireReclaimScenario.ManySmallRespecifyAndDrawRoundsStayUnderTheStoreCountCeiling",
".Reclaim.MagmaWireReclaimScenario.ADrawAfterTheEarlyReclaimFollowsTheNewStoreNotTheMemoizedHandle")
".Reclaim.MagmaWireReclaimScenario.ADrawAfterTheEarlyReclaimFollowsTheNewStoreNotTheMemoizedHandle",
".Reclaim.MagmaWireReclaimScenario.DescriptorSetsRewindInsideOneLongFrameAfterTheirSubmitRetires")
def lane_names(build_dir, label):