[Merge] (P8): integrate package D - the Magma wire arm issues glDraw*Indirect / glMultiDraw*Indirect[Count] natively from the wire store (vkCmdDraw*Indirect[Count]) instead of a CPU expansion behind a GPU-idle wait, with an INDIRECT_COMMAND_READ barrier once per shader write; windr[] PipeStats counters and WireIndirectDrawScenario on both backends x every arm

This commit is contained in:
2026-09-30 01:28:20 -04:00
15 changed files with 826 additions and 8 deletions
@@ -370,16 +370,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
#endif // !MOBILEGL_BUILD_DISAGGREGATED
#if MOBILEGL_BUILD_DISAGGREGATED
// The verb's handles identify server stores. Readback orders GPU-produced
// command/count bytes before CPU tier expansion; never inspect a client binding.
// The verb's handles identify server stores; never inspect a client binding.
//
// P8-D: THE GPU READS THE WORDS (VulkanRenderer::DrawWireIndirectNative, WireDraw.inc), as
// on the monolith arm. The CPU expansion below is kept for the one shape the monolith arm
// also expands - a COUNT form the device cannot issue natively (no VK_KHR_draw_indirect_count,
// or maxdrawcount > 1 without multiDrawIndirect: VulkanRenderer.cpp's
// MultiDrawElementsIndirectCount) - where ReadWireBuffer orders GPU-produced command/count
// bytes before the expansion.
void DrawWireIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount,
GLsizei stride, Bool indexed, Bool counted = false, GLintptr countOffset = 0) {
if (drawcount <= 0) return;
if (drawcount <= 0 || countOffset < 0) return;
if (pVulkanRenderer->DrawWireIndirectNative(mode, type, reinterpret_cast<Uint64>(indirect), drawcount,
stride, indexed, counted, static_cast<Uint64>(countOffset)))
return;
auto& buffers = pVulkanRenderer->GetWireBufferManager();
const auto& state = MG_Pipe::MGPipeApplier();
// P8-D: this call reads its words on the CPU; the waits those reads take are its own.
struct ExpansionCount {
VkBufferManager& buffers;
const Uint64 waitsBefore = buffers.GetWireHostWaitCount();
~ExpansionCount() {
if (!MG_Util::PipeStats::Enabled()) return;
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectCpuExpansions, 1);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireHostWaitsIndirect,
buffers.GetWireHostWaitCount() - waitsBefore);
}
} expansionCount{buffers};
if (counted) {
Uint32 count = 0;
if (countOffset < 0 || !buffers.ReadWireBuffer(state.VerbIndirectParameterBuffer,
if (!buffers.ReadWireBuffer(state.VerbIndirectParameterBuffer,
static_cast<Uint64>(countOffset), sizeof(count), &count)) return;
drawcount = static_cast<GLsizei>(std::min<Uint32>(count, static_cast<Uint32>(drawcount)));
if (!drawcount) return;
@@ -22,6 +22,7 @@
#include <android/hardware_buffer.h>
#endif
#include <Config.h>
#include <chrono>
#include <cstdlib>
#endif
@@ -271,6 +272,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource->lastUseSerial = 0;
resource->lastUseSubmitIndex = 0; // M2 r2: the new store carries no submission yet (B3's stamp is per store)
resource->gpuWritesPending = false;
resource->indirectReadBarrierPending = false;
resource->stagedCoverage.clear();
++m_sliceEpochCounter;
if (resource->size == 0) return;
@@ -315,7 +317,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT | VK_ACCESS_HOST_WRITE_BIT;
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_HOST_BIT,
0, 1, &barrier, 0, nullptr, 0, nullptr);
if (!pVulkanRenderer->WaitForSubmitIndex(pVulkanRenderer->GetSyncPointSubmitIndex(), UINT64_MAX, true)) {
// P8-D: counted here, past the idle early-out, so `whw` is waits TAKEN, not calls.
++m_wireHostWaits;
const Bool timed = MG_Util::PipeStats::Enabled();
const auto waitStart = timed ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
const Bool waited =
pVulkanRenderer->WaitForSubmitIndex(pVulkanRenderer->GetSyncPointSubmitIndex(), UINT64_MAX, true);
if (timed) {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireHostWaits, 1);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireHostWaitMicros,
static_cast<Uint64>(std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - waitStart).count()));
}
if (!waited) {
return false;
}
resource.lastUseSerial = 0;
@@ -594,6 +608,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource->lastUseSerial = m_frameSerial;
resource->stagedCoverage.clear();
resource->gpuWritesPending = true;
resource->indirectReadBarrierPending = true;
++m_sliceEpochCounter;
// The existing client dirty-state protocol accepts one whole-store
// notification. The store remains on the GPU and exact later SubData
@@ -606,6 +621,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MG_Pipe::gMGPipeCallbacks.OnGpuWritten(res, 1, &range);
}
Bool VkBufferManager::TakeWireIndirectReadBarrier(MG_Pipe::MGPipeHandle res) {
auto* resource = FindWireBuffer(res);
if (!resource || !resource->indirectReadBarrierPending) return false;
resource->indirectReadBarrierPending = false;
return true;
}
// ---- P11 B2: T0 - THE CLIENT'S AHardwareBuffer AS A WIRE STORE -----------------------------
// The serial a T0 store is released under: its last use, or - when no GPU command has named it
@@ -125,6 +125,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool CopyWireBufferSubWordRangeToSlice(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size,
Uint32 frameIndex, const BufferSlice& dst, Uint64 dstSkip);
void MarkWireBufferGpuWritten(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size);
// P8-D: whether a shader write was marked on this store since the last indirect draw
// read it natively, clearing the mark. The caller records the INDIRECT_COMMAND_READ
// barrier before the draw when this answers true. Transfer writes need no mark: their own
// after-barriers already make them visible to MEMORY_READ at ALL_COMMANDS.
Bool TakeWireIndirectReadBarrier(MG_Pipe::MGPipeHandle res);
// P8-D: every WaitForWireBufferHostAccess that actually waited, over this manager's life -
// the attribution base a caller reads before and after its own ReadWireBuffer.
Uint64 GetWireHostWaitCount() const { return m_wireHostWaits; }
// Resource-op entry points. All run on the server apply owner.
void CreateWireBuffer(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc);
@@ -257,7 +265,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// P11 B2 (T0): `buffer` is the client's imported AHardwareBuffer. Its readback posts
// no bytes (the client reads its own pages once the wait below it is done).
Bool imported = false;
// P8-D: a shader write was marked (MarkWireBufferGpuWritten) and no indirect draw has
// recorded its INDIRECT_COMMAND_READ barrier since (TakeWireIndirectReadBarrier).
Bool indirectReadBarrierPending = false;
};
// P8-D: see GetWireHostWaitCount.
Uint64 m_wireHostWaits = 0;
static Uint64 WireBufferKey(MG_Pipe::MGPipeHandle res) {
return (static_cast<Uint64>(res.Gen) << 32) | res.Slot;
}
@@ -3582,6 +3582,9 @@ void main() {
m_device = VK_NULL_HANDLE;
}
s_vkCmdDrawIndexedIndirectCount = nullptr;
#if MOBILEGL_BUILD_DISAGGREGATED
s_vkCmdWireDrawIndirectCount = nullptr;
#endif
s_vkCmdDrawMultiEXT = nullptr;
s_vkCmdDrawMultiIndexedEXT = nullptr;
@@ -15933,6 +15936,16 @@ void main() {
MGLOG_W("VK_KHR_draw_indirect_count enabled but vkCmdDrawIndexedIndirectCount entry point is missing, will continue as if VK_KHR_draw_indirect_count is not supported!");
m_drawIndirectCountExtensionEnabled = false;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-D: the wire arm's glMultiDrawArraysIndirectCount. Null = that verb reads its count on
// the CPU (WireDraw.inc's DrawWireIndirectNative answers false for it).
s_vkCmdWireDrawIndirectCount = reinterpret_cast<PFNDrawIndexedIndirectCountFunc>(
vkGetDeviceProcAddr(m_device, "vkCmdDrawIndirectCountKHR"));
if (s_vkCmdWireDrawIndirectCount == nullptr) {
s_vkCmdWireDrawIndirectCount = reinterpret_cast<PFNDrawIndexedIndirectCountFunc>(
vkGetDeviceProcAddr(m_device, "vkCmdDrawIndirectCount"));
}
#endif
s_vkCmdDrawMultiEXT = nullptr;
s_vkCmdDrawMultiIndexedEXT = nullptr;
@@ -160,6 +160,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// GetSyncPointSubmitIndex(), which answers m_submitCounter when nothing is recorded -
// true for a fence taken at that instant, wrong for work about to be recorded.
Uint64 GetWireNextSubmitIndex() const { return m_submitCounter + 1; }
// P8-D: the wire arm's indirect draw families, issued as vkCmdDraw[Indexed]Indirect[Count]
// from the wire stores MGPipeApplier().VerbIndirectBuffer / VerbIndirectParameterBuffer
// name. `offset` / `countOffset` are byte offsets into those stores; `stride` 0 means
// tightly packed. FALSE = this device cannot issue the COUNT form natively (the monolith
// arm's own condition, see the definition in WireDraw.inc) and the caller must read the
// words on the CPU; TRUE = issued, or declined by name.
Bool DrawWireIndirectNative(GLenum mode, GLenum type, Uint64 offset, GLsizei drawcount, GLsizei stride,
Bool indexed, Bool counted, Uint64 countOffset);
// P11 B2 (T0): what importing a client's AHardwareBuffer as a wire buffer store needs from
// this device. False when the device did not take VK_ANDROID_external_memory_android_
// hardware_buffer at creation (not Android, not advertised, or a monolith device).
@@ -680,6 +688,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void CollectWireObjects(Uint64 completedSubmit, Bool all = false);
Vector<WireRetiredObjects> m_wireRetiredObjects;
Uint32 m_wirePreparationDepth = 0;
// P8-D: set only while DrawWireIndirectNative is inside SetupDraw, so SetupWireDraw can
// tell the wire arm's own indirect draw from a monolith indirect entry point reached under
// a transport (still the `buffer-legacy-arm` role violation).
Bool m_wireNativeIndirectDraw = false;
void DispatchWireCompute(GLuint x, GLuint y, GLuint z);
void RewindWireDescriptorSetsIfDue();
UniquePtr<RenderPassEntry> m_wireDrawPass;
@@ -921,6 +933,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDeviceSize countBufferOffset, Uint32 maxDrawCount,
Uint32 stride);
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr;
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-D: vkCmdDrawIndirectCount (same signature, same extension), for the wire arm's
// glMultiDrawArraysIndirectCount. The monolith arm never loads it: it reads that count on
// the CPU (DirectVulkan.cpp's MultiDrawArraysIndirectCount).
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdWireDrawIndirectCount = nullptr;
#endif
// VK_EXT_multi_draw entry points, loaded at device creation when the extension
// (and its multiDraw feature) is enabled; null otherwise.
static inline PFN_vkCmdDrawMultiEXT s_vkCmdDrawMultiEXT = nullptr;
@@ -110,3 +110,9 @@ MGL_WIRE_DECLINE(ShapePreserveCopyFailed)
// is the monolith's decline now. Counted once per refused glGenerateMipmap, whose chain is left
// as it was.
MGL_WIRE_DECLINE(MipmapShaderFormatOrShape)
// ---- VulkanRenderer::DrawWireIndirectNative (P8-D), in statement order -----------------------
MGL_WIRE_DECLINE(IndirectBufferUnbound)
MGL_WIRE_DECLINE(IndirectCommandRange)
MGL_WIRE_DECLINE(IndirectCountRange)
MGL_WIRE_DECLINE(IndirectIndexStore)
@@ -207,7 +207,10 @@
MGL_WIRE_DECLINE_AT(ProgramLinkOrSpirv, "program is not linked or carries no SPIR-V");
return false;
}
if (aspects & DrawSetupAspect::IndirectDrawBuffer)
// P8-D: the wire arm's own indirect draw (DrawWireIndirectNative) is the one caller that
// may pass this aspect; a monolith indirect entry point reached here is still the role
// violation.
if ((aspects & DrawSetupAspect::IndirectDrawBuffer) && !m_wireNativeIndirectDraw)
WireBufferLegacyFatal();
MakeXfbWritesVisible();
const auto* fbo = state.DrawFramebuffer();
@@ -317,8 +320,10 @@
const SizeT stride = static_cast<SizeT>(attr.Stride);
SizeT count = stride ? 1 + (static_cast<SizeT>(slice.size - offset) - elementSize) / stride : 1;
// Direct non-indexed draws bound the conversion; indexed/multi-draw may
// name arbitrary vertices, so preserve the entire valid source stream.
if (stride && !(aspects & DrawSetupAspect::IndexBuffer) && !resource.Divisor)
// name arbitrary vertices, so preserve the entire valid source stream. So may an
// indirect draw (P8-D): its vertex range is in GPU words nobody read.
if (stride && !(aspects & DrawSetupAspect::IndexBuffer) &&
!(aspects & DrawSetupAspect::IndirectDrawBuffer) && !resource.Divisor)
count = std::min<SizeT>(count, static_cast<SizeT>(drawParams.firstVertex) + drawParams.vertexCount);
if (!count) {
MGL_WIRE_DECLINE_AT(VertexStreamCountZero, "the converted vertex stream would hold no elements");
@@ -608,6 +613,136 @@
return true;
}
// ---- P8-D: THE WIRE ARM'S INDIRECT DRAWS READ THEIR WORDS ON THE GPU ----------------------
//
// The shape the monolith arm records (VulkanRenderer.cpp's MultiDraw{Elements,Arrays}Indirect
// and MultiDrawElementsIndirectCount), with the wire store's own VkBuffer in place of the
// frontend buffer's resident slice. Before this, every wire indirect draw read its commands
// through ReadWireBuffer and expanded them on the CPU, and a store a shader had written made
// that read a whole-GPU wait (WaitForWireBufferHostAccess) per draw.
//
// THE ONE FALSE: a COUNT form this device cannot issue - the monolith arm's own condition for
// vkCmdDrawIndexedIndirectCount (MultiDrawElementsIndirectCount), extended to the non-indexed
// twin the monolith never loads. Everything else is issued here or declined by name.
Bool VulkanRenderer::DrawWireIndirectNative(GLenum mode, GLenum type, Uint64 offset, GLsizei drawcount,
GLsizei stride, Bool indexed, Bool counted, Uint64 countOffset) {
if (drawcount <= 0) return true;
if (counted) {
const auto countEntry = indexed ? s_vkCmdDrawIndexedIndirectCount : s_vkCmdWireDrawIndirectCount;
if (!m_drawIndirectCountExtensionEnabled || countEntry == nullptr ||
(!m_multiDrawIndirectFeatureEnabled && drawcount != 1))
return false;
}
const auto& state = MG_Pipe::MGPipeApplier();
// GL's command sizes (GL 4.6 10.3.10), which are also Vulkan's.
const Uint64 commandSize = indexed ? 5 * sizeof(Uint32) : 4 * sizeof(Uint32);
if (stride == 0) stride = static_cast<GLsizei>(commandSize);
BufferSlice commands{};
if (MG_Pipe::MGPipeHandleIsNull(state.VerbIndirectBuffer) ||
!m_bufferManager.AcquireWireSlice(BufferKind::Indirect, state.VerbIndirectBuffer, commands)) {
MGL_WIRE_DECLINE_AT(IndirectBufferUnbound, "an indirect draw names no live indirect buffer store");
return true;
}
const Uint64 span = static_cast<Uint64>(stride) * static_cast<Uint64>(drawcount - 1) + commandSize;
// GL refuses a stride that is not a multiple of four (INVALID_VALUE), and Vulkan requires
// it for drawCount > 1; a peer's record that breaks either is declined, not issued.
if (stride < static_cast<GLsizei>(commandSize) || (stride & 3) != 0 || (offset & 3u) != 0 ||
offset > commands.size || span > commands.size - offset) {
MGL_WIRE_DECLINE_AT(IndirectCommandRange, "the indirect commands lie outside their store or are misaligned");
return true;
}
BufferSlice parameters{};
if (counted) {
if (MG_Pipe::MGPipeHandleIsNull(state.VerbIndirectParameterBuffer) ||
!m_bufferManager.AcquireWireSlice(BufferKind::Indirect, state.VerbIndirectParameterBuffer, parameters) ||
(countOffset & 3u) != 0 || countOffset > parameters.size || parameters.size - countOffset < sizeof(Uint32)) {
MGL_WIRE_DECLINE_AT(IndirectCountRange, "the indirect draw count lies outside its parameter store");
return true;
}
}
// The index view is the WHOLE bound element store past its binding offset, as the monolith
// arm binds it: the commands name their own firstIndex, which only the GPU reads.
IndexBufferView indices{};
DrawCmdParam vertexRange{};
vertexRange.instanceCount = 1;
Flags<DrawSetupAspect> aspects = DrawSetupAspect::IndirectDrawBuffer;
if (indexed) {
const SizeT width = MG_Util::GetGLTypeSize(type);
BufferSlice elements{};
if (width == 0 || MG_Pipe::MGPipeHandleIsNull(state.IndexBuffer.Res) ||
!m_bufferManager.AcquireWireSlice(BufferKind::Index, state.IndexBuffer.Res, elements) ||
state.IndexBuffer.Offset > elements.size) {
MGL_WIRE_DECLINE_AT(IndirectIndexStore, "an indexed indirect draw names no usable element store");
return true;
}
indices.indexType = type;
indices.indexByteOffset = 0;
indices.indexByteSize = static_cast<SizeT>(elements.size - state.IndexBuffer.Offset);
vertexRange.vertexCount = static_cast<Uint32>(indices.indexByteSize / width);
aspects |= DrawSetupAspect::IndexBuffer;
}
// A shader write the store was marked with (MarkWireBufferGpuWritten) is made visible to
// the indirect read here, once per write, so a program that omits GL_COMMAND_BARRIER_BIT
// is not left racing a read the old CPU path used to take after a whole-GPU wait. The
// application's own glMemoryBarrier is BuildMemoryBarrierForGlBarriers, as on the monolith
// arm; transfer writes carry their own MEMORY_READ after-barrier (VkBufferManager.cpp).
const Bool commandsWritten = m_bufferManager.TakeWireIndirectReadBarrier(state.VerbIndirectBuffer);
const Bool countWritten = counted && m_bufferManager.TakeWireIndirectReadBarrier(state.VerbIndirectParameterBuffer);
if (commandsWritten || countWritten) {
RetireWireDrawPass();
auto& barrierFrame = m_frameContext.GetCurrent();
if (!barrierFrame.isCommandRecording) m_frameContext.BeginCommandRecording();
if (VkRenderPassManager::GetActiveRenderPass() != nullptr)
VkRenderPassManager::EndRenderPass(barrierFrame.commandBuffer);
VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_INDIRECT_COMMAND_READ_BIT;
vkCmdPipelineBarrier(barrierFrame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr);
if (MG_Util::PipeStats::Enabled())
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectBarriers, 1);
}
auto& frame = m_frameContext.GetCurrent();
m_wireNativeIndirectDraw = true;
const Bool ready = SetupDraw(frame, mode, aspects, vertexRange, indexed ? &indices : nullptr);
m_wireNativeIndirectDraw = false;
if (!ready) return true;
MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__);
const VkCommandBuffer commandBuffer = frame.commandBuffer;
const VkDeviceSize first = commands.offset + offset;
const Uint32 count = static_cast<Uint32>(drawcount);
const Uint32 byteStride = static_cast<Uint32>(stride);
if (counted) {
const auto countEntry = indexed ? s_vkCmdDrawIndexedIndirectCount : s_vkCmdWireDrawIndirectCount;
countEntry(commandBuffer, commands.buffer, first, parameters.buffer, parameters.offset + countOffset,
count, byteStride);
} else if (count == 1 || (!m_multiDrawForceUnrollIndirect && m_multiDrawIndirectFeatureEnabled)) {
// maxDrawIndirectCount bounds one call; GL's drawcount does not.
const Uint32 limit = std::max<Uint32>(1u, m_physicalDevice.properties.limits.maxDrawIndirectCount);
for (Uint32 base = 0; base < count; base += limit) {
const Uint32 chunk = std::min(count - base, limit);
const VkDeviceSize at = first + static_cast<VkDeviceSize>(base) * byteStride;
if (indexed) vkCmdDrawIndexedIndirect(commandBuffer, commands.buffer, at, chunk, byteStride);
else vkCmdDrawIndirect(commandBuffer, commands.buffer, at, chunk, byteStride);
}
} else {
// No multiDrawIndirect: one indirect draw per command, still read by the GPU
// (the monolith arm's own fallback).
for (Uint32 i = 0; i < count; ++i) {
const VkDeviceSize at = first + static_cast<VkDeviceSize>(i) * byteStride;
if (indexed) vkCmdDrawIndexedIndirect(commandBuffer, commands.buffer, at, 1, 0);
else vkCmdDrawIndirect(commandBuffer, commands.buffer, at, 1, 0);
}
}
if (MG_Util::PipeStats::Enabled())
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectNativeDraws, 1);
return true;
}
void VulkanRenderer::DispatchWireCompute(GLuint x, GLuint y, GLuint z) {
RetireWireDrawPass();
auto& frame = m_frameContext.GetCurrent();
@@ -4761,6 +4761,41 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
include(${CMAKE_CURRENT_SOURCE_DIR}/P8Coverage.cmake)
endif()
# P8-D: the Magma wire arm issues indirect draws natively (docs/Disaggregated/notes/p8/D.md).
# Public GL, so the source joins the binary unguarded and the ambient `DirectGLES.` /
# `DirectVulkan.` monolith discovery runs it too. Split arms: DirectGLES through the registration
# macro (which also replays it on Magma's informational tier), Magma's gated tier by hand, all
# three transports - the counter halves read only on the inproc arm (WireIndirectPeek.h) and the
# pixel halves everywhere, so there is no arm-only property to declare.
target_sources(MobileGLIntegrationTest PRIVATE Harness/WireIndirectPeek.cpp Scenarios/WireIndirectDrawScenario.cpp)
if (MOBILEGL_BUILD_DISAGGREGATED)
mgl_itest_register_split_arms("WireIndirectDrawScenario.*" "")
set(MGL_ITEST_MAGMA_INDIRECT_FLAVOUR_ENV
"MOBILEGL_BACKEND_TYPE=DirectVulkan"
"MOBILEGL_IPC_ROLE_SPLIT_STATE=1" "MOBILEGL_IPC_STRICT_ERRORS=1"
"MOBILEGL_ITEST_REQUIRE_GPU=1" "MGITEST_SPLIT_LANE=1"
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_VULKAN_ENV})
foreach(mglItestArm IN LISTS MGL_ITEST_MAGMA_ARMS)
mgl_itest_magma_arm_shape(${mglItestArm} indirect)
mgl_itest_join_environment(MGL_ITEST_MAGMA_INDIRECT_ENVIRONMENT
${MGL_ITEST_MAGMA_INDIRECT_FLAVOUR_ENV} ${mglItestArmTransportEnv})
foreach(indirectCase
EveryIndirectFormIsIssuedNativelyFromItsStore
ComputeWrittenCommandsDrawWithoutAHostWait
ComputeWrittenCountWordIsTheOneTheGpuReads
ShaderWrittenCommandsWithoutAnApplicationBarrierAreOrdered)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.${mglItestArm}.Indirect."
TEST_FILTER "WireIndirectDrawScenario.${indirectCase}"
DISCOVERY_TIMEOUT 30
PROPERTIES LABELS "${mglItestMagmaLabels}"
TIMEOUT ${MGL_ITEST_TIMEOUT}
${mglItestArmProperties}
ENVIRONMENT "${MGL_ITEST_MAGMA_INDIRECT_ENVIRONMENT}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/magma-indirect${mglItestArmLogTag}-${indirectCase}.log")
endforeach()
endforeach()
endif()
# =====================================================================================
# P7 PACKAGE L: THE INFORMATIONAL MAGMA CENSUS - TIER 2 OF TWO
# =====================================================================================
@@ -0,0 +1,37 @@
// MobileGL - MobileGL/MG_IntegrationTest/Harness/WireIndirectPeek.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "WireIndirectPeek.h"
#if !defined(__ANDROID__)
#include <MG_Pipe/MGPipe.h>
#if MOBILEGL_PIPE_PUSH
#include <MG_Util/Metrics/PipeStats.h>
#define MGITEST_WIRE_INDIRECT_PEEK_LIVE 1
#endif
#endif
namespace MGITest {
#if defined(MGITEST_WIRE_INDIRECT_PEEK_LIVE)
bool PeekWireIndirectCounters(WireIndirectCounters* out) {
if (out == nullptr) return false;
namespace Stats = MobileGL::MG_Util::PipeStats;
if (!Stats::Enabled()) Stats::SetEnabledForTesting(true);
out->nativeDraws = Stats::TotalCalls(Stats::CallClass::WireIndirectNativeDraws);
out->cpuExpansions = Stats::TotalCalls(Stats::CallClass::WireIndirectCpuExpansions);
out->barriers = Stats::TotalCalls(Stats::CallClass::WireIndirectBarriers);
out->hostWaits = Stats::TotalCalls(Stats::CallClass::WireHostWaits);
out->indirectWaits = Stats::TotalCalls(Stats::CallClass::WireHostWaitsIndirect);
return true;
}
#else
bool PeekWireIndirectCounters(WireIndirectCounters*) { return false; }
#endif
} // namespace MGITest
@@ -0,0 +1,28 @@
// MobileGL - MobileGL/MG_IntegrationTest/Harness/WireIndirectPeek.h
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// P8-D: the Magma wire arm's indirect-draw counters (PipeStats `windr[...]`), read as run totals
// out of THIS process - P4aFinalFixPeek.cpp's shape. Process-wide means the SERVER's counts only
// where the server is this process: monolith and inproc, never spawn or tcp. The first call arms
// the counters (a case reads before and after its workload, so it must peek before it draws).
// False where the reading cannot be taken: a pull build, Android.
#pragma once
namespace MGITest {
struct WireIndirectCounters {
unsigned long long nativeDraws = 0; // wire-indirect-native-draws (`wind`)
unsigned long long cpuExpansions = 0; // wire-indirect-cpu-expansions (`wixp`)
unsigned long long barriers = 0; // wire-indirect-barriers (`wibar`)
unsigned long long hostWaits = 0; // wire-host-waits (`whw`)
unsigned long long indirectWaits = 0; // wire-host-waits-indirect (`whwi`)
};
bool PeekWireIndirectCounters(WireIndirectCounters* out);
} // namespace MGITest
@@ -0,0 +1,365 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDrawScenario.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// P8-D: THE MAGMA WIRE ARM ISSUES INDIRECT DRAWS FROM THE WIRE STORE, AS THE MONOLITH ARM DOES.
//
// Before P8-D every wire indirect draw read its commands (and its count word) through
// ReadWireBuffer and expanded them on the CPU, and a store a shader had written turned that read
// into a whole-GPU wait per draw (create-indirect: 321 expansions, 327 waits, ~4-8 s of waiting
// per replay on lavapipe). The cases pin both halves of the change:
// * PIXELS, on every arm and both backends where the backend's own semantics allow it - each
// indirect form draws exactly the strips its commands name, and GPU-written commands are the
// ones the draw sees (the poison the buffers start with names the OTHER strips);
// * THE SERVER'S OWN COUNTERS (Harness/WireIndirectPeek.h), where they can be read - Magma on
// the inproc arm, whose server is this process: every call issued natively, no CPU
// expansion, no host wait, and the INDIRECT_COMMAND_READ barrier exactly once per write.
//
// Four strips across the viewport, one per quarter; a command draws one strip. Green = drawn,
// red = the clear. The VBO opens with three padding vertices so a command that loses its `first`
// or `firstIndex` draws a degenerate triangle rather than the right strip.
#include <array>
#include <cstdint>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#include "../Harness/SplitRuntimePeek.h"
#include "../Harness/WireIndirectPeek.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glext.h>
namespace MGITest {
namespace {
constexpr int kStrips = 4;
constexpr std::uint32_t kPad = 3;
constexpr std::uint32_t kStripVertices = 6;
constexpr const char* kVertexSource = R"(#version 430 core
layout(location = 0) in vec2 aPos;
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
)";
constexpr const char* kFragmentSource = R"(#version 430 core
out vec4 oColor;
void main() { oColor = vec4(0.0, 1.0, 0.0, 1.0); }
)";
// Copies `source` into `words`, one word per invocation: the compute write under test.
constexpr const char* kCopySource = R"(#version 430 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Words { uint words[]; };
layout(std430, binding = 1) readonly buffer Source { uint source[]; };
void main() { words[gl_GlobalInvocationID.x] = source[gl_GlobalInvocationID.x]; }
)";
struct ArraysCommand {
std::uint32_t count, instanceCount, first, baseInstance;
};
struct ElementsCommand {
std::uint32_t count, instanceCount, firstIndex;
std::int32_t baseVertex;
std::uint32_t baseInstance;
};
// A padded elements command, for the stride case: 32 bytes, the tail unread.
struct PaddedElementsCommand {
ElementsCommand command;
std::uint32_t padding[3];
};
ArraysCommand Arrays(int strip) { return {kStripVertices, 1, kPad + kStripVertices * strip, 0}; }
ElementsCommand Elements(int strip) { return {kStripVertices, 1, kStripVertices * strip, 0, 0}; }
using Strips = std::array<bool, kStrips>;
class WireIndirectDrawScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_magma = Gl().BackendName() == "DirectVulkan";
m_transport = PeekSplitRuntime().transportName;
std::string error;
m_program = CompileProgram(kVertexSource, kFragmentSource, &error);
ASSERT_NE(m_program, 0u) << error;
m_copy = CompileCompute(kCopySource);
ASSERT_NE(m_copy, 0u);
std::vector<float> vertices(2 * kPad, 0.0f);
for (int strip = 0; strip < kStrips; ++strip) {
const float x0 = -1.0f + 0.5f * static_cast<float>(strip);
const float x1 = x0 + 0.5f;
const float quad[12] = {x0, -1.0f, x1, -1.0f, x1, 1.0f, x0, -1.0f, x1, 1.0f, x0, 1.0f};
vertices.insert(vertices.end(), quad, quad + 12);
}
std::vector<std::uint32_t> indices;
for (std::uint32_t i = 0; i < kStripVertices * kStrips; ++i) indices.push_back(kPad + i);
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(vertices.size() * sizeof(float)),
vertices.data(), GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
glGenBuffers(1, &m_ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, static_cast<GLsizeiptr>(indices.size() * sizeof(std::uint32_t)),
indices.data(), GL_STATIC_DRAW);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "scene setup left a GL error behind";
// The counters are armed by the first peek, so it has to come before any draw.
if (CountersReadable()) {
ASSERT_TRUE(PeekWireIndirectCounters(&m_before))
<< "Magma on the inproc arm must be able to read the wire arm's own counters";
}
}
void TearDown() override {
if (!Ready()) return;
glBindVertexArray(0);
for (GLuint* buffer : {&m_vbo, &m_ebo, &m_indirect, &m_parameter, &m_source}) {
if (*buffer != 0) glDeleteBuffers(1, buffer);
*buffer = 0;
}
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_program != 0) glDeleteProgram(m_program);
if (m_copy != 0) glDeleteProgram(m_copy);
}
static GLuint CompileCompute(const char* source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint ok = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
if (ok != GL_TRUE) {
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
glGetProgramiv(program, GL_LINK_STATUS, &ok);
if (ok != GL_TRUE) {
glDeleteProgram(program);
return 0;
}
return program;
}
template <typename T>
static void Fill(GLuint& name, GLenum target, const std::vector<T>& data) {
if (name == 0) glGenBuffers(1, &name);
glBindBuffer(target, name);
glBufferData(target, static_cast<GLsizeiptr>(data.size() * sizeof(T)), data.data(), GL_DYNAMIC_COPY);
}
// The compute write: `words` (already holding its poison) receives `data` from the GPU.
template <typename T>
void ComputeWrite(GLuint words, const std::vector<T>& data) {
Fill(m_source, GL_SHADER_STORAGE_BUFFER, data);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, words);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_source);
glUseProgram(m_copy);
glDispatchCompute(static_cast<GLuint>(data.size() * sizeof(T) / sizeof(std::uint32_t)), 1, 1);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
}
template <typename DrawFn>
Image Render(DrawFn&& draw) {
BindDefaultFramebuffer();
glViewport(0, 0, Gl().Width(), Gl().Height());
glDisable(GL_DEPTH_TEST);
ClearTo(1.0f, 0.0f, 0.0f, 1.0f);
glUseProgram(m_program);
glBindVertexArray(m_vao);
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, m_indirect);
draw();
return ReadPixels(Gl().Width(), Gl().Height());
}
static void ExpectStrips(const Image& image, const Strips& drawn, const std::string& what) {
for (int strip = 0; strip < kStrips; ++strip) {
const int x = image.Width() * (2 * strip + 1) / (2 * kStrips);
EXPECT_STREQ(image.ColorName(x, image.Height() / 2), drawn[strip] ? "green" : "red")
<< what << ": strip " << strip;
}
}
// Magma's server in THIS process: the inproc arm. Spawn and tcp servers are other
// processes, and the monolith arm never takes the wire path at all.
bool CountersReadable() const { return m_magma && m_transport == "inproc"; }
WireIndirectCounters Delta() const {
WireIndirectCounters now{};
EXPECT_TRUE(PeekWireIndirectCounters(&now));
return {now.nativeDraws - m_before.nativeDraws, now.cpuExpansions - m_before.cpuExpansions,
now.barriers - m_before.barriers, now.hostWaits - m_before.hostWaits,
now.indirectWaits - m_before.indirectWaits};
}
bool m_magma = false;
std::string m_transport;
WireIndirectCounters m_before{};
GLuint m_program = 0, m_copy = 0, m_vao = 0, m_vbo = 0, m_ebo = 0;
GLuint m_indirect = 0, m_parameter = 0, m_source = 0;
};
// Every indirect form from CPU-written commands. On Magma/inproc the server must have
// issued all six natively, expanded none, waited for nothing and recorded no barrier (no
// shader ever wrote these stores).
TEST_F(WireIndirectDrawScenario, EveryIndirectFormIsIssuedNativelyFromItsStore) {
if (!Ready() || IsSkipped()) return;
// Arrays commands at 0.., elements commands after them: the offsets are byte offsets
// into one store, as an application packs them.
const std::vector<ArraysCommand> arrays = {Arrays(0), Arrays(1), Arrays(3), Arrays(2)};
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, arrays);
Image image = Render([&] { glDrawArraysIndirect(GL_TRIANGLES, reinterpret_cast<const void*>(16)); });
ExpectStrips(image, {false, true, false, false}, "glDrawArraysIndirect at byte 16");
image = Render([&] { glMultiDrawArraysIndirect(GL_TRIANGLES, nullptr, 3, 0); });
ExpectStrips(image, {true, true, false, true}, "glMultiDrawArraysIndirect, 3 commands");
const std::vector<PaddedElementsCommand> padded = {{Elements(3), {}}, {Elements(0), {}}};
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, padded);
image = Render([&] {
glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr, 2, sizeof(PaddedElementsCommand));
});
ExpectStrips(image, {true, false, false, true}, "glMultiDrawElementsIndirect, stride 32");
image = Render([&] {
glDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT,
reinterpret_cast<const void*>(sizeof(PaddedElementsCommand)));
});
ExpectStrips(image, {true, false, false, false}, "glDrawElementsIndirect at byte 32");
// The count forms: three commands each, a count word that allows fewer.
Fill(m_parameter, GL_PARAMETER_BUFFER, std::vector<std::uint32_t>{7, 2, 1});
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector<ArraysCommand>{Arrays(0), Arrays(1), Arrays(2)});
image = Render([&] {
glMultiDrawArraysIndirectCount(GL_TRIANGLES, nullptr, 4, 3, 0);
});
ExpectStrips(image, {true, true, false, false}, "glMultiDrawArraysIndirectCount, count word 2 of 3");
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector<ElementsCommand>{Elements(3), Elements(2), Elements(1)});
image = Render([&] {
glMultiDrawElementsIndirectCount(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr, 8, 3, 0);
});
ExpectStrips(image, {false, false, false, true}, "glMultiDrawElementsIndirectCount, count word 1 of 3");
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
if (!CountersReadable()) return;
const WireIndirectCounters delta = Delta();
EXPECT_EQ(delta.nativeDraws, 6u) << "every indirect call must be issued as vkCmdDraw*Indirect[Count]";
EXPECT_EQ(delta.cpuExpansions, 0u) << "no indirect call may read its words on the CPU";
EXPECT_EQ(delta.indirectWaits, 0u) << "an indirect call took a host wait";
EXPECT_EQ(delta.hostWaits, 0u) << "a host wait was taken with no GPU write anywhere";
EXPECT_EQ(delta.barriers, 0u) << "no shader wrote these stores, so no barrier is due";
}
// Commands a compute shader wrote, behind the application's own GL_COMMAND_BARRIER_BIT.
// The buffers start with commands for the OTHER strips, so a read of stale words shows.
// On Magma/inproc: issued natively, and - the point of the package - NO host wait.
TEST_F(WireIndirectDrawScenario, ComputeWrittenCommandsDrawWithoutAHostWait) {
if (!Ready() || IsSkipped()) return;
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector<ArraysCommand>{Arrays(2), Arrays(3)});
ComputeWrite(m_indirect, std::vector<ArraysCommand>{Arrays(0), Arrays(1)});
glMemoryBarrier(GL_COMMAND_BARRIER_BIT);
Image image = Render([&] { glMultiDrawArraysIndirect(GL_TRIANGLES, nullptr, 2, 0); });
ExpectStrips(image, {true, true, false, false}, "glMultiDrawArraysIndirect of compute-written commands");
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector<ElementsCommand>{Elements(0), Elements(1)});
ComputeWrite(m_indirect, std::vector<ElementsCommand>{Elements(3), Elements(2)});
glMemoryBarrier(GL_COMMAND_BARRIER_BIT);
image = Render([&] { glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr, 2, 0); });
ExpectStrips(image, {false, false, true, true}, "glMultiDrawElementsIndirect of compute-written commands");
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
if (!CountersReadable()) return;
const WireIndirectCounters delta = Delta();
EXPECT_EQ(delta.nativeDraws, 2u) << "both draws must be issued natively";
EXPECT_EQ(delta.cpuExpansions, 0u) << "no indirect call may read its words on the CPU";
EXPECT_EQ(delta.indirectWaits, 0u)
<< "an indirect draw of shader-written commands waited for the GPU on the host";
EXPECT_EQ(delta.hostWaits, 0u) << "a host wait was taken";
EXPECT_EQ(delta.barriers, 2u) << "one INDIRECT_COMMAND_READ barrier per freshly written store";
}
// The COUNT word a compute shader wrote. Magma only: Espryt reads that word from a
// server shadow GPU writes do not update, on both arms (OPEN-QUESTIONS 15, P8-C's
// package), so its answer here is a known divergence rather than this package's claim.
TEST_F(WireIndirectDrawScenario, ComputeWrittenCountWordIsTheOneTheGpuReads) {
if (!Ready() || IsSkipped()) return;
if (!m_magma) {
GTEST_SKIP() << "Espryt reads the count word from its shadow on both arms (OQ15, P8-C)";
}
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector<ElementsCommand>{Elements(0), Elements(3), Elements(2)});
Fill(m_parameter, GL_PARAMETER_BUFFER, std::vector<std::uint32_t>{3});
ComputeWrite(m_parameter, std::vector<std::uint32_t>{1});
glMemoryBarrier(GL_COMMAND_BARRIER_BIT);
Image image = Render([&] {
glMultiDrawElementsIndirectCount(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr, 0, 3, 0);
});
ExpectStrips(image, {true, false, false, false}, "glMultiDrawElementsIndirectCount, shader-written count 1");
if (m_transport == "monolith") {
// DirectVulkan.cpp's monolith MultiDrawArraysIndirectCount reads this word from the
// frontend shadow without SyncGpuWrites: recorded for dev (ID-P8-3), not this lane's.
return;
}
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector<ArraysCommand>{Arrays(1), Arrays(2), Arrays(3)});
Fill(m_parameter, GL_PARAMETER_BUFFER, std::vector<std::uint32_t>{0});
ComputeWrite(m_parameter, std::vector<std::uint32_t>{2});
glMemoryBarrier(GL_COMMAND_BARRIER_BIT);
image = Render([&] { glMultiDrawArraysIndirectCount(GL_TRIANGLES, nullptr, 0, 3, 0); });
ExpectStrips(image, {false, true, true, false}, "glMultiDrawArraysIndirectCount, shader-written count 2");
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
if (!CountersReadable()) return;
const WireIndirectCounters delta = Delta();
EXPECT_EQ(delta.nativeDraws, 2u);
EXPECT_EQ(delta.cpuExpansions, 0u) << "a count word was read on the CPU";
EXPECT_EQ(delta.hostWaits, 0u) << "a host wait was taken";
EXPECT_EQ(delta.barriers, 2u) << "one barrier per draw whose count store a shader wrote";
}
// Flywheel's shape (create-indirect's trace: 322 cull-then-draw rounds, no glMemoryBarrier
// between them). GL leaves it undefined; the wire arm orders it anyway, once per write,
// because the CPU path it replaced did (by waiting). Magma's split arms only: the monolith
// arm records no dependency here (recorded for dev), and Espryt's is its driver's.
TEST_F(WireIndirectDrawScenario, ShaderWrittenCommandsWithoutAnApplicationBarrierAreOrdered) {
if (!Ready() || IsSkipped()) return;
if (!m_magma || m_transport == "monolith") {
GTEST_SKIP() << "the implicit INDIRECT_COMMAND_READ barrier is the Magma wire arm's (P8-D)";
}
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector<ArraysCommand>{Arrays(3), Arrays(2)});
ComputeWrite(m_indirect, std::vector<ArraysCommand>{Arrays(0), Arrays(1)});
Image image = Render([&] { glMultiDrawArraysIndirect(GL_TRIANGLES, nullptr, 2, 0); });
ExpectStrips(image, {true, true, false, false}, "the first draw after the write");
image = Render([&] { glDrawArraysIndirect(GL_TRIANGLES, reinterpret_cast<const void*>(16)); });
ExpectStrips(image, {false, true, false, false}, "a second draw, no write between");
ComputeWrite(m_indirect, std::vector<ArraysCommand>{Arrays(2), Arrays(3)});
image = Render([&] { glMultiDrawArraysIndirect(GL_TRIANGLES, nullptr, 2, 0); });
ExpectStrips(image, {false, false, true, true}, "the draw after a second write");
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
if (!CountersReadable()) return;
const WireIndirectCounters delta = Delta();
EXPECT_EQ(delta.nativeDraws, 3u);
EXPECT_EQ(delta.cpuExpansions, 0u);
EXPECT_EQ(delta.hostWaits, 0u) << "a host wait was taken";
EXPECT_EQ(delta.barriers, 2u) << "exactly one barrier per write: the second draw reads no new write";
}
} // namespace
} // namespace MGITest
+19
View File
@@ -574,4 +574,23 @@ namespace {
}
EXPECT_EQ(PS::FrameCount(), PS::SummaryFramePeriod());
}
#if MOBILEGL_PIPE_PUSH
// P8-D: the Magma wire arm's indirect-draw bracket. Its short names are what the create
// fixtures' measurement (notes/p8/D.md) reads off the server's line, and its long names are
// what Harness/WireIndirectPeek.cpp's readers expect in the JSON dump.
TEST_F(PipeStatsTest, WireIndirectCountersPrintInTheirOwnBracket) {
PS::AddCalls(PS::CallClass::WireIndirectNativeDraws, 3);
PS::AddCalls(PS::CallClass::WireIndirectCpuExpansions, 2);
PS::AddCalls(PS::CallClass::WireHostWaitMicros, 1500);
const String line = PS::FormatWindowLine();
EXPECT_NE(line.find("windr[wind=3 wixp=2 wibar=0 whw=0 whwi=0 whwus=1500]"), String::npos) << line;
EXPECT_STREQ(PS::NameOf(PS::CallClass::WireIndirectNativeDraws), "wire-indirect-native-draws");
EXPECT_STREQ(PS::NameOf(PS::CallClass::WireIndirectCpuExpansions), "wire-indirect-cpu-expansions");
EXPECT_STREQ(PS::NameOf(PS::CallClass::WireIndirectBarriers), "wire-indirect-barriers");
EXPECT_STREQ(PS::NameOf(PS::CallClass::WireHostWaits), "wire-host-waits");
EXPECT_STREQ(PS::NameOf(PS::CallClass::WireHostWaitsIndirect), "wire-host-waits-indirect");
EXPECT_STREQ(PS::NameOf(PS::CallClass::WireHostWaitMicros), "wire-host-wait-us");
}
#endif
} // namespace
+11
View File
@@ -238,6 +238,8 @@ namespace MobileGL::MG_Util::PipeStats {
"shader-image-emissions", "client-tex-upload-emissions", "tex-remint-pulls",
"resident-subdata-emissions",
"residual-pulls", "server-verb-boundaries", "wire-records",
"wire-indirect-native-draws", "wire-indirect-cpu-expansions", "wire-indirect-barriers",
"wire-host-waits", "wire-host-waits-indirect", "wire-host-wait-us",
#endif
};
const char* const kGateNames[kGateCount] = {
@@ -690,6 +692,15 @@ namespace MobileGL::MG_Util::PipeStats {
line += " wlivepk=" + std::to_string(Read(g_gauges[static_cast<Uint32>(Gauge::WireStoresPeak)]));
line += " wdefpk=" + std::to_string(Read(g_gauges[static_cast<Uint32>(Gauge::WireDeferredBytesPeak)]));
line += " wdefsync=" + std::to_string(Read(g_gauges[static_cast<Uint32>(Gauge::WireDeferredSyncs)]));
// P8-D: the Magma wire arm's indirect draws and its host-access waits, windowed like the
// other CallClass fields (per window, not per frame: the create fixtures hold one frame).
line += "] windr[wind=" +
std::to_string(calls[static_cast<Uint32>(CallClass::WireIndirectNativeDraws)]);
line += " wixp=" + std::to_string(calls[static_cast<Uint32>(CallClass::WireIndirectCpuExpansions)]);
line += " wibar=" + std::to_string(calls[static_cast<Uint32>(CallClass::WireIndirectBarriers)]);
line += " whw=" + std::to_string(calls[static_cast<Uint32>(CallClass::WireHostWaits)]);
line += " whwi=" + std::to_string(calls[static_cast<Uint32>(CallClass::WireHostWaitsIndirect)]);
line += " whwus=" + std::to_string(calls[static_cast<Uint32>(CallClass::WireHostWaitMicros)]);
#endif
line += "] gates[";
for (Uint32 i = 0; i < kGateCount; ++i) {
+13
View File
@@ -270,6 +270,19 @@ namespace MobileGL::MG_Util::PipeStats {
// that goes with it (FormatWindowLine prints the run total instead when the window holds
// no Present).
WireRecords,
// P8-D: the Magma wire arm's indirect draws, on the server (`windr[...]` on the line).
// `wind` - indirect draw calls issued as vkCmdDraw*Indirect[Count] from the wire store.
// `wixp` - indirect draw calls whose words were read on the CPU instead (the count word,
// or every command, through ReadWireBuffer).
// `wibar` - the INDIRECT_COMMAND_READ barriers recorded for a store marked GPU-written.
// `whw` / `whwus` - WaitForWireBufferHostAccess calls that actually waited, and their
// microseconds, from any caller; `whwi` - the subset an indirect read caused.
WireIndirectNativeDraws,
WireIndirectCpuExpansions,
WireIndirectBarriers,
WireHostWaits,
WireHostWaitsIndirect,
WireHostWaitMicros,
#endif
Count
};
+83
View File
@@ -0,0 +1,83 @@
# P8-D:Magma wire 臂原生 indirect draw
> 2026-09-29,分支 `p8/d`(基线 `8d3e2317`)。计划:[`PLAN-P8.md`](PLAN-P8.md) 包 D;审计 §4、§6 风险 3。主机 lavapipe,只动 Magma(Espryt 作对照)。
## 1. 结论
- wire 臂的 `glDraw*Indirect` / `glMultiDraw*Indirect[Count]` 改为从 wire store 自己的 `VkBuffer` 发 `vkCmdDraw[Indexed]Indirect[Count]`:`VulkanRenderer::DrawWireIndirectNative`(`MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc:620`)。
- `DrawWireIndirect`(`MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp:381`)先走原生;CPU 展开只留给设备发不出的 COUNT 形态(与 monolith 同条件,§3)。
- create-indirect × Magma × split:indirect 引起的 host 等待 321 → 0,等待时间 8.07 s → 0.013 s(剩下 6 次是应用回读,§2);SSIM 各臂不变。
- 新增 barrier:着色器写过的 store 在下一次 indirect 读前补一个 `INDIRECT_COMMAND_READ` barrier,每次写一次(`WireDraw.inc:686-698`)。create-indirect 的 322 次 cull→draw 里 320 次没有 `glMemoryBarrier`,旧 CPU 路径靠整 GPU 等待碰巧有序,原生路径靠这个 barrier。
## 2. 测量(主机,`MOBILEGL_PIPE_STATS=1 MOBILEGL_PIPE_STATS_PERIOD=1`,server 日志 `windr[...]` 求和)
两个 fixture 都只有 1 帧(frames=1),所以"每帧"= 每次重放。主机上同时有 7 个包在跑,毫秒只看量级。
| fixture | 臂 | draws | wind | wixp | wibar | whw | whwi | whwus | SSIM |
|---|---|---:|---:|---:|---:|---:|---:|---:|---|
| create-indirect | monolith(前后相同) | 13172 | 0 | 0 | 0 | 0 | 0 | 0 | 0.999956667 |
| create-indirect | inproc,改前 | 13172 | 0 | 321 | 0 | 327 | 321 | 8065617 | 0.999956667 |
| create-indirect | inproc,改后 | 13172 | 321 | 0 | 321 | 6 | 0 | 12540 | 0.999956667 |
| create-indirect | spawn,改前 | 13202 | 0 | 322 | 0 | 328 | 322 | 8360015 | 0.999956667 |
| create-indirect | spawn,改后 | 13202 | 322 | 0 | 322 | 6 | 0 | 15769 | 0.999956667 |
| create-instancing | 三臂,前后相同 | 14158 / 14194 | 0 | 0 | 0 | 0 | 0 | 0 | 0.999952470 |
- 字段:`wind` 原生 indirect 调用、`wixp` CPU 展开、`wibar` 补的 barrier、`whw` / `whwus` 真正等了的 `WaitForWireBufferHostAccess` 次数 / 微秒(`MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp:321`)、`whwi` 其中由 indirect 读引起的。定义在 `MobileGL/MG_Util/Metrics/PipeStats.h:280-285`,打印在 `MobileGL/MG_Util/Metrics/PipeStats.cpp:697`。
- 改后剩下的 6 次等待全部来自 `ReadbackWireBuffer`(`VkBufferManager.cpp:574`,临时探针取调用方地址 + `addr2line`,未提交):应用对 6 个 store 的回读,改前也是这 6 次(327 − 321)。
- 同一 trace(apitrace dump 到 target call):322 次 `glMultiDrawElementsIndirect`、2901 次 `glDispatchCompute`、9 次 `glMemoryBarrier`;前 320 次 draw 与其 cull dispatch 之间没有应用 barrier,只有最后 2 次前有 `GL_COMMAND_BARRIER_BIT`。每次 draw 前都有 compute 写它的命令 store,所以 `wibar` = draw 数。
- create-instancing 没有 indirect draw,也没有等待:它的 p99 差异不归 D。
## 3. monolith 何时在 CPU 上读 indirect 字(D 只在同样的场合保留 CPU 路径)
| monolith 场合 | 位置 | wire 臂(本包后) |
|---|---|---|
| 没绑 `GL_DRAW_INDIRECT_BUFFER`:命令在 client 内存 | `DirectVulkan.cpp:684`、`:825`、`:884` | client 在兼容档拒绝(`MobileGL/MG_Remote/Client/EmitTables.cpp:730` `CLIENT_COMMANDS`);store 为空则具名拒绝 `IndirectBufferUnbound` |
| `glMultiDrawArraysIndirectCount`:总在 CPU 读 count 字 | `DirectVulkan.cpp:720-757` | 原生 `vkCmdDrawIndirectCount`(`MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:15942` 加载,只在 disaggregated 构建) |
| `glMultiDrawElementsIndirectCount`:无 `VK_KHR_draw_indirect_count`,或 maxdrawcount > 1 且无 `multiDrawIndirect` → CPU 循环 | `VulkanRenderer.cpp:13421-13444` | 同条件(`WireDraw.inc:623-628`)返回 false,走原 CPU 展开(`DirectVulkan.cpp:390-413`,计入 `wixp`) |
| 无 `multiDrawIndirect` 或 stride 不是 4 的倍数:逐条原生 indirect,仍由 GPU 读 | `VulkanRenderer.cpp:13510`、`:13615` | 同(`WireDraw.inc:725-733`);stride 非 4 倍数在 GL 是 INVALID_VALUE,wire 上具名拒绝 `IndirectCommandRange` |
| client 顶点数组 + arrays indirect:CPU 读命令定上传范围,draw 仍原生 | `VulkanRenderer.cpp:13576` | 不可达:client 把 client 数组拷成自有 buffer |
| `GL_LINE_LOOP`:indirect 不改写,退化为 LINE_STRIP | `MobileGL/MG_Util/Converters/MGToVk/RenderStateEnumConverter.cpp:27` | 同 |
| uint8 索引无扩展 / 非固定 restart 索引 / 非对齐偏移:整段索引在 CPU 改写,命令仍 GPU 读 | 两臂都在各自的索引上传里 | `WireDraw.inc:391`,索引视图是整个 EBO(`WireDraw.inc:674`,同 monolith) |
| baseInstance / gl_DrawID | 原生(monolith 不查 `drawIndirectFirstInstance`) | 原生;旧 CPU 展开的第 3 档把 gl_DrawID 全给 0(`VulkanRenderer.cpp:13154`),原生反而更对 |
## 4. 设计
| 项 | 内容 |
|---|---|
| 入口 | `DrawWireIndirect`(`DirectVulkan.cpp:381`)→ `DrawWireIndirectNative`;false 才走 CPU 展开 |
| store | `AcquireWireSlice` 取 indirect / count / 元素 store(打 `lastUseSerial`,之后的 `glBufferSubData` 会排在这次读之后);范围、4 字节对齐、count 字越界都是具名拒绝(`MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclines.def:109-112`) |
| draw 准备 | 复用 `SetupDraw` → `SetupWireDraw`,带 `IndirectDrawBuffer`;该 aspect 原来是 `buffer-legacy-arm` Fatal,现只对 monolith 入口仍 Fatal(`WireDraw.inc:213`,`m_wireNativeIndirectDraw`);顶点流转换不再按 first+count 截断(`WireDraw.inc:319`) |
| 发射 | 同 monolith 的三档:COUNT 原生、`multiDrawIndirect` 一次发、否则逐条;另按 `maxDrawIndirectCount` 分块(`WireDraw.inc:712-733`) |
| barrier | `MarkWireBufferGpuWritten` 置 `indirectReadBarrierPending`(`VkBufferManager.cpp:611`),`TakeWireIndirectReadBarrier`(`VkBufferManager.cpp:624`)取走;有则结束 render pass 并记 `ALL_COMMANDS/MEMORY_WRITE → DRAW_INDIRECT/INDIRECT_COMMAND_READ`。transfer 写已有 `MEMORY_READ` 后置 barrier(`VkBufferManager.cpp:514`、`:1437`),不置位。应用自己的 `glMemoryBarrier(GL_COMMAND_BARRIER_BIT)` 仍走 `BuildMemoryBarrierForGlBarriers`(`VulkanRenderer.cpp:7873`),与 monolith 一样 |
| G1 | 全部在 `MOBILEGL_BUILD_DISAGGREGATED` / `MOBILEGL_PIPE_PUSH` 下;pull 构建 `added=0 removed=0` |
## 5. 门与 red-once
用例 `WireIndirectDrawScenario`(`MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDrawScenario.cpp:225`、`:274`、`:302`、`:340`),读数经 `MobileGL/MG_IntegrationTest/Harness/WireIndirectPeek.cpp:22`(进程内 PipeStats,只在 inproc 臂看得到 server)。登记:`MobileGL/MG_IntegrationTest/CMakeLists.txt:4651-4684`——Magma 门控三臂 `DirectVulkan.{Split,Spawn,Tcp}.Indirect.`、DirectGLES 三臂(经宏,含 Magma 信息层)、两后端 monolith(环境发现)。
| 用例 | 断言 | 两后端 / 各臂 |
|---|---|---|
| `EveryIndirectFormIsIssuedNativelyFromItsStore` | 6 种 indirect 形态像素;inproc Magma:wind=6、wixp=0、whw=0、wibar=0 | 两后端全臂 |
| `ComputeWrittenCommandsDrawWithoutAHostWait` | compute 写命令(初值毒化为别的条带)+ 应用 barrier,像素;inproc Magma:wind=2、wixp=0、whw=0、wibar=2 | 两后端全臂 |
| `ComputeWrittenCountWordIsTheOneTheGpuReads` | compute 写 count 字,像素;inproc Magma 计数 | Magma;Espryt 跳过(count 字读 server 影子,OQ15 / P8-C);Magma monolith 跳过 arrays-count 半(§6) |
| `ShaderWrittenCommandsWithoutAnApplicationBarrierAreOrdered` | Flywheel 形状(无应用 barrier);inproc Magma:每次写恰好 1 个 barrier(wibar=2 / 3 次 draw) | Magma split 三臂;monolith 与 Espryt 跳过 |
| red-once | 变异 | 载体 | 观察 |
|---|---|---|---|
| 1 | `DrawWireIndirectNative` 开头 `return false`(强制 CPU 路径) | 4 用例 × 3 个 inproc 登记(`.Split.Indirect.` 门控、`.Split.` 信息层、`.Split.Full.`) | 12 红;`ComputeWrittenCommandsDrawWithoutAHostWait`:nativeDraws 0、cpuExpansions 2、indirectWaits 2、hostWaits 2;spawn/tcp 只断言像素,保持绿(CPU 路径像素是对的) |
| 2 | 去掉 barrier(`if (false && ...)`) | 同上 | 3 个 compute 写用例 × 3 个 inproc 登记 = 9 红,全部是 `delta.barriers` 0 ≠ 2;**像素在 llvmpipe 上不红**(lavapipe 按序执行),spawn/tcp 绿 |
| 3 | 去掉 Magma monolith 的 arrays-count 跳过 | `DirectVulkan.WireIndirectDrawScenario.ComputeWrittenCountWordIsTheOneTheGpuReads` | 红:compute 写的 count=2,条带 1、2 未画(monolith 读到陈旧影子 count 0) |
| fixture 门 | 改前 = 旧 CPU 路径 | create-indirect × Magma × {inproc, spawn} | whwi 321 / 322 > 0(§2 改前行);改后 0 |
## 6. monolith 缺陷(记给 dev,ID-P8-3,本包不修)
| 缺陷 | 位置 | 证据 |
|---|---|---|
| Magma monolith `glMultiDrawArraysIndirectCount` 从前端影子读 count 字,不 `SyncGpuWrites`:着色器写的 count 读成旧值 | `DirectVulkan.cpp:749-757` | red-once 3(主机 lavapipe 红) |
| Magma monolith 对"compute 写 → indirect 读"没有任何依赖:不补 barrier,render pass 的外部依赖也不含 `COMPUTE_SHADER` / `DRAW_INDIRECT`(`MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp:1413-1419`)。create-indirect 的 320 次 cull→draw 不发 `glMemoryBarrier`(GL 未定义),真 GPU 上 cull 结果与 indirect 读竞争 | `VulkanRenderer.cpp:7727-7787`(dispatch 后无 barrier) | 推断:主机 lavapipe 按序执行,red 不出;可能与 ID-P7-4(红米上 create-indirect monolith 坏)有关,需设备验证 |
## 7. 找到但未做
- `DispatchComputeIndirect` 的 wire 臂同样经 `ReadWireBuffer` 在 CPU 读 group 数(`DirectVulkan.cpp:938-947`),GPU 写过则整 GPU 等待;create fixtures 里没有 `glDispatchComputeIndirect`,本包未改(monolith 是原生 `vkCmdDispatchIndirect`)。
- 补的 barrier 只覆盖 indirect 读;同一批 cull 写的实例 SSBO 被顶点着色器读时,两臂都没有 compute→vertex 的依赖(应用没发 barrier 时),不在 D 范围。
- 同一 store 既作 indirect 又作本 draw 的 SSBO 时,SetupWireDraw 解析描述符会再次置位,下一次 indirect draw 会多一个 barrier(保守,不影响正确性)。