diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp index 53fefc85..6ccace30 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp @@ -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(indirect), drawcount, + stride, indexed, counted, static_cast(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(countOffset), sizeof(count), &count)) return; drawcount = static_cast(std::min(count, static_cast(drawcount))); if (!drawcount) return; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index 6be8413b..66728a95 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -22,6 +22,7 @@ #include #endif #include +#include #include #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(std::chrono::duration_cast( + 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 diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h index 2c53ca21..7266b46a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h @@ -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(res.Gen) << 32) | res.Slot; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index f5ac2b47..765059be 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -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( + vkGetDeviceProcAddr(m_device, "vkCmdDrawIndirectCountKHR")); + if (s_vkCmdWireDrawIndirectCount == nullptr) { + s_vkCmdWireDrawIndirectCount = reinterpret_cast( + vkGetDeviceProcAddr(m_device, "vkCmdDrawIndirectCount")); + } +#endif s_vkCmdDrawMultiEXT = nullptr; s_vkCmdDrawMultiIndexedEXT = nullptr; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 08a2b871..08fd2e85 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -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 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 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; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclines.def b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclines.def index fc5b8149..e66756df 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclines.def +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclines.def @@ -104,3 +104,9 @@ MGL_WIRE_DECLINE(ShapeCreateImageFailed) // Appended by the B3 review round: the preserve-copy arm of a reallocation was the one exit in // SyncWireTextureShape still leaving no trace at all. MGL_WIRE_DECLINE(ShapePreserveCopyFailed) + +// ---- VulkanRenderer::DrawWireIndirectNative (P8-D), in statement order ----------------------- +MGL_WIRE_DECLINE(IndirectBufferUnbound) +MGL_WIRE_DECLINE(IndirectCommandRange) +MGL_WIRE_DECLINE(IndirectCountRange) +MGL_WIRE_DECLINE(IndirectIndexStore) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc index fd1cf1ec..5126c063 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc @@ -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(); @@ -310,8 +313,10 @@ const SizeT stride = static_cast(attr.Stride); SizeT count = stride ? 1 + (static_cast(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(count, static_cast(drawParams.firstVertex) + drawParams.vertexCount); if (!count) { MGL_WIRE_DECLINE_AT(VertexStreamCountZero, "the converted vertex stream would hold no elements"); @@ -601,6 +606,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(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(stride) * static_cast(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(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 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(elements.size - state.IndexBuffer.Offset); + vertexRange.vertexCount = static_cast(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(drawcount); + const Uint32 byteStride = static_cast(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(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(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(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(); diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 9929f305..0890d3be 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -4648,6 +4648,41 @@ if (MOBILEGL_BUILD_DISAGGREGATED) endforeach() 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 # ===================================================================================== diff --git a/MobileGL/MG_IntegrationTest/Harness/WireIndirectPeek.cpp b/MobileGL/MG_IntegrationTest/Harness/WireIndirectPeek.cpp new file mode 100644 index 00000000..d8b4ad5f --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Harness/WireIndirectPeek.cpp @@ -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 +#if MOBILEGL_PIPE_PUSH +#include +#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 diff --git a/MobileGL/MG_IntegrationTest/Harness/WireIndirectPeek.h b/MobileGL/MG_IntegrationTest/Harness/WireIndirectPeek.h new file mode 100644 index 00000000..57168456 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Harness/WireIndirectPeek.h @@ -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 diff --git a/MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDrawScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDrawScenario.cpp new file mode 100644 index 00000000..87a38fc3 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDrawScenario.cpp @@ -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 +#include +#include +#include + +#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 +#include + +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; + + 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 vertices(2 * kPad, 0.0f); + for (int strip = 0; strip < kStrips; ++strip) { + const float x0 = -1.0f + 0.5f * static_cast(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 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(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(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 + static void Fill(GLuint& name, GLenum target, const std::vector& data) { + if (name == 0) glGenBuffers(1, &name); + glBindBuffer(target, name); + glBufferData(target, static_cast(data.size() * sizeof(T)), data.data(), GL_DYNAMIC_COPY); + } + + // The compute write: `words` (already holding its poison) receives `data` from the GPU. + template + void ComputeWrite(GLuint words, const std::vector& 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(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 + 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 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(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 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(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{7, 2, 1}); + Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector{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{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{Arrays(2), Arrays(3)}); + ComputeWrite(m_indirect, std::vector{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{Elements(0), Elements(1)}); + ComputeWrite(m_indirect, std::vector{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{Elements(0), Elements(3), Elements(2)}); + Fill(m_parameter, GL_PARAMETER_BUFFER, std::vector{3}); + ComputeWrite(m_parameter, std::vector{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{Arrays(1), Arrays(2), Arrays(3)}); + Fill(m_parameter, GL_PARAMETER_BUFFER, std::vector{0}); + ComputeWrite(m_parameter, std::vector{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{Arrays(3), Arrays(2)}); + ComputeWrite(m_indirect, std::vector{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(16)); }); + ExpectStrips(image, {false, true, false, false}, "a second draw, no write between"); + ComputeWrite(m_indirect, std::vector{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 diff --git a/MobileGL/MG_Test/Util/PipeStatsTest.cpp b/MobileGL/MG_Test/Util/PipeStatsTest.cpp index 90e0d41d..9485b07a 100644 --- a/MobileGL/MG_Test/Util/PipeStatsTest.cpp +++ b/MobileGL/MG_Test/Util/PipeStatsTest.cpp @@ -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 diff --git a/MobileGL/MG_Util/Metrics/PipeStats.cpp b/MobileGL/MG_Util/Metrics/PipeStats.cpp index a683a1ae..629facd6 100644 --- a/MobileGL/MG_Util/Metrics/PipeStats.cpp +++ b/MobileGL/MG_Util/Metrics/PipeStats.cpp @@ -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(Gauge::WireStoresPeak)])); line += " wdefpk=" + std::to_string(Read(g_gauges[static_cast(Gauge::WireDeferredBytesPeak)])); line += " wdefsync=" + std::to_string(Read(g_gauges[static_cast(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(CallClass::WireIndirectNativeDraws)]); + line += " wixp=" + std::to_string(calls[static_cast(CallClass::WireIndirectCpuExpansions)]); + line += " wibar=" + std::to_string(calls[static_cast(CallClass::WireIndirectBarriers)]); + line += " whw=" + std::to_string(calls[static_cast(CallClass::WireHostWaits)]); + line += " whwi=" + std::to_string(calls[static_cast(CallClass::WireHostWaitsIndirect)]); + line += " whwus=" + std::to_string(calls[static_cast(CallClass::WireHostWaitMicros)]); #endif line += "] gates["; for (Uint32 i = 0; i < kGateCount; ++i) { diff --git a/MobileGL/MG_Util/Metrics/PipeStats.h b/MobileGL/MG_Util/Metrics/PipeStats.h index 9bd8c352..b7d0683a 100644 --- a/MobileGL/MG_Util/Metrics/PipeStats.h +++ b/MobileGL/MG_Util/Metrics/PipeStats.h @@ -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 }; diff --git a/docs/Disaggregated/notes/p8/D.md b/docs/Disaggregated/notes/p8/D.md new file mode 100644 index 00000000..d55e16e8 --- /dev/null +++ b/docs/Disaggregated/notes/p8/D.md @@ -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(保守,不影响正确性)。