From fc415aee762d086523e9b5a986a110b35af95023 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 30 Sep 2026 03:44:03 -0400 Subject: [PATCH 1/5] [Fix] (Magma, P8-SV): the wire arm's 3D native mip blit uses one layer with the depth in its z offsets, the depth/stencil mip decline gets its WireDeclineTally row (MipmapDepthStencilAspect), the depth shader mip refuses 1D/1D-array/3D view types before the chain grows, and glDispatchComputeIndirect is issued as vkCmdDispatchIndirect from the wire store (P8-D's barrier rule, wdsp counter, two named declines) --- .../MG_Backend/DirectVulkan/DirectVulkan.cpp | 10 +-- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 9 +++ .../DirectVulkan/Renderer/VulkanRenderer.h | 3 + .../DirectVulkan/Renderer/WireDeclines.def | 12 ++- .../DirectVulkan/Renderer/WireDepthMipmap.inc | 8 ++ .../DirectVulkan/Renderer/WireDraw.inc | 76 +++++++++++++++++++ .../DirectVulkan/Renderer/WireFramebuffer.inc | 55 ++++++++++---- MobileGL/MG_Test/Util/PipeStatsTest.cpp | 4 +- MobileGL/MG_Util/Metrics/PipeStats.cpp | 3 + MobileGL/MG_Util/Metrics/PipeStats.h | 4 + 10 files changed, 161 insertions(+), 23 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp index 6ccace30..61c1724c 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp @@ -937,12 +937,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { void DispatchComputeIndirect(GLintptr indirect) { #if MOBILEGL_BUILD_DISAGGREGATED + // P8-SV: THE GPU READS THE GROUP COUNTS on the wire arm too - VulkanRenderer's + // DispatchComputeIndirect takes its transport branch into DispatchWireComputeIndirect + // (WireDraw.inc), which issues vkCmdDispatchIndirect from the verb's store. The CPU read + // that was here (ReadWireBuffer, a whole-GPU wait once a shader had written the store) is gone. if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { - Uint32 groups[3]{}; - if (indirect >= 0 && pVulkanRenderer->GetWireBufferManager().ReadWireBuffer( - MG_Pipe::MGPipeApplier().VerbDispatchIndirectBuffer, - static_cast(indirect), sizeof(groups), groups)) - pVulkanRenderer->DispatchCompute(groups[0], groups[1], groups[2]); + pVulkanRenderer->DispatchComputeIndirect(indirect); return; } #endif diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 765059be..c92856ff 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -7789,6 +7789,15 @@ void main() { void VulkanRenderer::DispatchComputeIndirect(GLintptr indirect) { SplitOversizedRecording(); +#if MOBILEGL_BUILD_DISAGGREGATED + // P8-SV: the wire arm's own indirect dispatch, DispatchCompute's shape one call over. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + RewindWireDescriptorSetsIfDue(); + m_textureManager->CollectGarbage(); + DispatchWireComputeIndirect(indirect); + return; + } +#endif m_textureManager->CollectGarbage(); auto& frame = m_frameContext.GetCurrent(); // See DispatchCompute: the dispatch accessor, not the draw one. diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 08fd2e85..a6d09d54 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -693,6 +693,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { // a transport (still the `buffer-legacy-arm` role violation). Bool m_wireNativeIndirectDraw = false; void DispatchWireCompute(GLuint x, GLuint y, GLuint z); + // P8-SV: glDispatchComputeIndirect on the wire arm - vkCmdDispatchIndirect from the + // dispatch-indirect store, as the monolith arm issues it (WireDraw.inc). + void DispatchWireComputeIndirect(GLintptr offset); void RewindWireDescriptorSetsIfDue(); UniquePtr m_wireDrawPass; Vector m_wireDrawViews; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclines.def b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclines.def index 0fd20f7a..ab39a53d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclines.def +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclines.def @@ -58,7 +58,7 @@ MGL_WIRE_DECLINE(CurrentAttributePayload) MGL_WIRE_DECLINE(CurrentAttributeUpload) MGL_WIRE_DECLINE(BeginRenderPass) -// ---- DispatchWireCompute ---------------------------------------------------------------- +// ---- DispatchWireCompute (and DispatchWireComputeIndirect, P8-SV) ------------------------- MGL_WIRE_DECLINE(ComputeProgramLinkOrSpirv) MGL_WIRE_DECLINE(ComputeTextureResourcesUnprepared) MGL_WIRE_DECLINE(ComputePipelineUnavailable) @@ -116,3 +116,13 @@ MGL_WIRE_DECLINE(IndirectBufferUnbound) MGL_WIRE_DECLINE(IndirectCommandRange) MGL_WIRE_DECLINE(IndirectCountRange) MGL_WIRE_DECLINE(IndirectIndexStore) + +// ---- P8-SV ------------------------------------------------------------------------------- +// VulkanRenderer::GenerateWireMipmap: a combined depth/stencil (or stencil-only) chain, which no +// arm generates (CONTRACT-P7 §3.2). It logged but was never counted, so MGWIRE-DECLINES could +// not see it. Counted once per refused glGenerateMipmap, whose chain is left as it was. +MGL_WIRE_DECLINE(MipmapDepthStencilAspect) +// VulkanRenderer::DispatchWireComputeIndirect, in statement order: the dispatch-indirect store +// is missing, or the three group-count words lie outside it or are misaligned. +MGL_WIRE_DECLINE(DispatchIndirectBufferUnbound) +MGL_WIRE_DECLINE(DispatchIndirectRange) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthMipmap.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthMipmap.inc index 2d14a2ae..8f5cc86f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthMipmap.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthMipmap.inc @@ -64,6 +64,14 @@ if (source.samples != VK_SAMPLE_COUNT_1_BIT || destination.samples != VK_SAMPLE_COUNT_1_BIT) return false; if (source.is3D || destination.is3D) return false; + // P8-SV: THE PASS MAKES 2D VIEWS (makeView below), which a 1D, 1D-array or 3D image does + // not admit. A caller that fills viewType from the image gets a refusal here, and the + // wire arm's GenerateWireMipmap asks the same question before it grows the chain. + for (const VkImageViewType viewType : {source.viewType, destination.viewType}) { + if (viewType == VK_IMAGE_VIEW_TYPE_1D || viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY || + viewType == VK_IMAGE_VIEW_TYPE_3D) + return false; + } if (source.format != destination.format) return false; // DEPTH ONLY. A combined depth/stencil image can be sampled for its depth and // rendered as a depth attachment, but this pass writes gl_FragDepth and NOTHING diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc index de724113..e4e016aa 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc @@ -770,4 +770,80 @@ vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); vkCmdDispatch(frame.commandBuffer, x, y, z); } + + // ---- P8-SV: THE WIRE ARM'S INDIRECT DISPATCH READS ITS GROUP COUNTS ON THE GPU ---------- + // + // The monolith arm's shape (VulkanRenderer.cpp's DispatchComputeIndirect) with the wire + // store's own VkBuffer in place of the frontend buffer's resident slice. Before this the wire + // arm read the three words through ReadWireBuffer and dispatched them from the CPU, and a store + // a shader had written made that read a whole-GPU wait (WaitForWireBufferHostAccess) per call. + // There is no shape the device cannot issue, so there is no CPU path left: a record whose + // store or words are unusable is declined by name. + // + // The barrier is P8-D's draw rule (DrawWireIndirectNative): a store marked shader-written gets + // one MEMORY_WRITE -> INDIRECT_COMMAND_READ barrier before its next indirect read, whether or + // not the application issued GL_COMMAND_BARRIER_BIT; DRAW_INDIRECT is the stage that consumes + // VkDispatchIndirectCommand too. It is TAKEN before the bindings below, because binding this + // dispatch's own storage buffers marks them written, and a store that is both this dispatch's + // parameters and one of its outputs must keep that mark for the next reader. + void VulkanRenderer::DispatchWireComputeIndirect(GLintptr offset) { + RetireWireDrawPass(); + const auto& state = MG_Pipe::MGPipeApplier(); + constexpr Uint64 kCommandSize = 3 * sizeof(Uint32); // GL 4.6 10.3.10 / VkDispatchIndirectCommand + BufferSlice parameters{}; + if (MG_Pipe::MGPipeHandleIsNull(state.VerbDispatchIndirectBuffer) || + !m_bufferManager.AcquireWireSlice(BufferKind::Indirect, state.VerbDispatchIndirectBuffer, parameters)) { + MGL_WIRE_DECLINE_AT(DispatchIndirectBufferUnbound, "an indirect dispatch names no live dispatch-indirect store"); + return; + } + // GL refuses an offset that is negative or not a multiple of four (INVALID_VALUE) and a + // command past the end of the buffer (INVALID_OPERATION); a peer's record that breaks + // either is declined, not issued. + if (offset < 0 || (static_cast(offset) & 3u) != 0 || static_cast(offset) > parameters.size || + parameters.size - static_cast(offset) < kCommandSize) { + MGL_WIRE_DECLINE_AT(DispatchIndirectRange, "the dispatch group counts lie outside their store or are misaligned"); + return; + } + auto& frame = m_frameContext.GetCurrent(); + if (m_bufferManager.TakeWireIndirectReadBarrier(state.VerbDispatchIndirectBuffer)) { + if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording(); + if (VkRenderPassManager::GetActiveRenderPass() != nullptr) + VkRenderPassManager::EndRenderPass(frame.commandBuffer); + VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_INDIRECT_COMMAND_READ_BIT; + vkCmdPipelineBarrier(frame.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); + } + const auto program = WireProgramSource(state.DispatchProgram); + if (!program.GetLinkStatus() || !program.GetSpirvStatus()) { + MGL_WIRE_DECLINE_AT(ComputeProgramLinkOrSpirv, "the compute program is not linked or carries no SPIR-V"); + return; + } + const auto& programObj = m_programFactory->GetOrCreateProgram(program, {}); + if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) { + MGL_WIRE_DECLINE_AT(ComputeTextureResourcesUnprepared, "a sampled/storage image could not be made ready for the dispatch"); + return; + } + const auto pipeline = GetOrCreateComputePipeline(programObj); + if (!pipeline) { + MGL_WIRE_DECLINE_AT(ComputePipelineUnavailable, "no compute pipeline could be created for this dispatch"); + return; + } + // PrepareWireTextureResources may have flushed and begun a new recording. + auto& dispatchFrame = m_frameContext.GetCurrent(); + if (!dispatchFrame.isCommandRecording) m_frameContext.BeginCommandRecording(); + if (!m_uniformManager->BindProgramUniformBuffers(dispatchFrame.commandBuffer, program, programObj, + m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_COMPUTE)) { + MGL_WIRE_DECLINE_AT(ComputeUniformBufferBinding, "the dispatch's uniform/storage bindings could not be bound"); + return; + } + vkCmdBindPipeline(dispatchFrame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); + vkCmdDispatchIndirect(dispatchFrame.commandBuffer, parameters.buffer, + parameters.offset + static_cast(offset)); + if (MG_Util::PipeStats::Enabled()) + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectNativeDispatches, 1); + } #endif diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc index 9a772c02..3eb8b553 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc @@ -1394,12 +1394,16 @@ // release build both arms ship as. Nothing in GL promises a stencil mip either - the // pass writes gl_FragDepth and would leave the stencil aspect of the level it produced // undefined, which is worse than leaving the whole level alone and saying so. + // + // P8-SV: a NAMED decline (WireDeclines.def MipmapDepthStencilAspect), so MGWIRE-DECLINES + // counts it like every other refusal on this arm; the line keeps its words. if (resource->aspect != VK_IMAGE_ASPECT_COLOR_BIT && resource->aspect != VK_IMAGE_ASPECT_DEPTH_BIT) { - MGLOG_E_ONCE("DirectVulkan::GenerateWireMipmap: aspect 0x%x (a combined depth/stencil or " - "stencil-only texture, format %d) has no mip generation on either arm; " - "declining and leaving levels %u..%u as they are", - static_cast(resource->aspect), static_cast(resource->format), - base + 1, base + count - 1); + MGL_WIRE_DECLINE_AT(MipmapDepthStencilAspect, + "DirectVulkan::GenerateWireMipmap: aspect 0x%x (a combined depth/stencil or " + "stencil-only texture, format %d) has no mip generation on either arm; " + "declining and leaving levels %u..%u as they are", + static_cast(resource->aspect), static_cast(resource->format), + base + 1, base + count - 1); return; } // P8-F: WHICH ARM TAKES THIS CHAIN IS DECIDED BEFORE THE CHAIN IS GROWN, for the @@ -1417,18 +1421,23 @@ VK_FILTER_LINEAR : VK_FILTER_NEAREST; const Bool layersInRange = baseLayer < resource->arrayLayers && layerCount <= resource->arrayLayers - baseLayer; - // The depth arm's shape: the layer window, and what WireDepthMipmap.inc's pass refuses - // for an image this function hands it - a format that does not both sample and attach - // as depth (its other refusals, 3D / multisample / a default image, are never built here). - const auto depthPassFeatures = - VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT; - const Bool depthArmTakesShape = - layersInRange && (props.optimalTilingFeatures & depthPassFeatures) == depthPassFeatures; - // The colour arm's shape (the two shader shapes are explained at the colour arm below). - const Bool colour = resource->aspect == VK_IMAGE_ASPECT_COLOR_BIT; + // Both shader arms render a 2D view of one layer of one level, so a 1D, 1D-array or 3D + // image is a shape neither takes. const Bool planar = resource->viewType != VK_IMAGE_VIEW_TYPE_1D && resource->viewType != VK_IMAGE_VIEW_TYPE_1D_ARRAY && resource->viewType != VK_IMAGE_VIEW_TYPE_3D; + // The depth arm's shape: the layer window, and what WireDepthMipmap.inc's pass refuses + // for an image this function hands it (the view type is passed to it below) - a 1D, + // 1D-array or 3D view type, and a format that does not both sample and attach as depth. + // Its other refusals, multisample and a default image, are never built here. P8-SV: the + // view type half is new; before it a 1D depth chain reached the pass, which made a 2D + // view of the 1D image. + const auto depthPassFeatures = + VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT; + const Bool depthArmTakesShape = planar && + layersInRange && (props.optimalTilingFeatures & depthPassFeatures) == depthPassFeatures; + // The colour arm's shape (the two shader shapes are explained at the colour arm below). + const Bool colour = resource->aspect == VK_IMAGE_ASPECT_COLOR_BIT; // The shader arm samples through a sampler2D and writes a vec4, which is what // BlitWireColorImage's own float check enforces; naming it here rather than letting the // blit return false is what puts the reason in the decline line. @@ -1477,6 +1486,10 @@ source.image = resource->image; source.format = format; source.aspect = VK_IMAGE_ASPECT_DEPTH_BIT; + // P8-SV: the image's own view type, so the pass can refuse a shape it cannot + // render (it makes 2D views) instead of receiving the struct's 2D default. + source.viewType = resource->viewType; + source.is3D = resource->viewType == VK_IMAGE_VIEW_TYPE_3D; source.layout = resource->layout; source.trackedLayout = &resource->layout; source.levels = resource->mipLevels; @@ -1603,14 +1616,24 @@ if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer,resource->image,resource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,stage,VK_PIPELINE_STAGE_TRANSFER_BIT,access, VK_ACCESS_TRANSFER_WRITE_BIT,resource->aspect,0,resource->mipLevels)) MagmaWireFatal("mipmap-transition"); + // P8-SV: A 3D IMAGE HAS ONE ARRAY LAYER, AND ITS DEPTH IS THE Z EXTENT OF THE BLIT. + // ResolveWireTextureStorage answers `layerCount` in slices for Tex3D (Depth >> level, + // VkTextureManager.cpp) - the addressing attachments, descriptors and readback use - and + // passing that on as the blit's subresource layerCount names layers the image (created + // with arrayLayers = 1) does not have (VUID-vkCmdBlitImage-srcImage-00240, + // -srcSubresource-01707, -dstSubresource-01708). The monolith arm blits layer 0, count 1, + // with the z offsets below halving the depth level by level; so does this one now. + const Bool volume = resource->viewType == VK_IMAGE_VIEW_TYPE_3D; + const Uint32 blitBaseLayer = volume ? 0u : baseLayer; + const Uint32 blitLayerCount = volume ? 1u : layerCount; for (Uint32 level=base+1;levelimage,previous, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,VK_PIPELINE_STAGE_TRANSFER_BIT,VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,VK_ACCESS_TRANSFER_READ_BIT,resource->aspect,level-1,1); VkImageBlit blit{}; - blit.srcSubresource = {resource->aspect,level-1,baseLayer,layerCount}; - blit.dstSubresource = {resource->aspect,level,baseLayer,layerCount}; + blit.srcSubresource = {resource->aspect,level-1,blitBaseLayer,blitLayerCount}; + blit.dstSubresource = {resource->aspect,level,blitBaseLayer,blitLayerCount}; blit.srcOffsets[1] = {static_cast(std::max(resource->extent.width>>(level-1),1u)), static_cast(std::max(resource->extent.height>>(level-1),1u)), static_cast(std::max(resource->depth>>(level-1),1u))}; diff --git a/MobileGL/MG_Test/Util/PipeStatsTest.cpp b/MobileGL/MG_Test/Util/PipeStatsTest.cpp index 9485b07a..f48ccfcf 100644 --- a/MobileGL/MG_Test/Util/PipeStatsTest.cpp +++ b/MobileGL/MG_Test/Util/PipeStatsTest.cpp @@ -583,8 +583,10 @@ namespace { PS::AddCalls(PS::CallClass::WireIndirectNativeDraws, 3); PS::AddCalls(PS::CallClass::WireIndirectCpuExpansions, 2); PS::AddCalls(PS::CallClass::WireHostWaitMicros, 1500); + PS::AddCalls(PS::CallClass::WireIndirectNativeDispatches, 4); 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_NE(line.find("windr[wind=3 wixp=2 wibar=0 whw=0 whwi=0 whwus=1500 wdsp=4]"), String::npos) << line; + EXPECT_STREQ(PS::NameOf(PS::CallClass::WireIndirectNativeDispatches), "wire-indirect-native-dispatches"); 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"); diff --git a/MobileGL/MG_Util/Metrics/PipeStats.cpp b/MobileGL/MG_Util/Metrics/PipeStats.cpp index 629facd6..9e029406 100644 --- a/MobileGL/MG_Util/Metrics/PipeStats.cpp +++ b/MobileGL/MG_Util/Metrics/PipeStats.cpp @@ -240,6 +240,7 @@ namespace MobileGL::MG_Util::PipeStats { "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", + "wire-indirect-native-dispatches", #endif }; const char* const kGateNames[kGateCount] = { @@ -701,6 +702,8 @@ namespace MobileGL::MG_Util::PipeStats { 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)]); + // P8-SV: the indirect dispatches, on the same bracket (their barriers are in `wibar`). + line += " wdsp=" + std::to_string(calls[static_cast(CallClass::WireIndirectNativeDispatches)]); #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 b7d0683a..c0d3682d 100644 --- a/MobileGL/MG_Util/Metrics/PipeStats.h +++ b/MobileGL/MG_Util/Metrics/PipeStats.h @@ -283,6 +283,10 @@ namespace MobileGL::MG_Util::PipeStats { WireHostWaits, WireHostWaitsIndirect, WireHostWaitMicros, + // P8-SV: `wdsp` - glDispatchComputeIndirect calls issued as vkCmdDispatchIndirect from the + // wire store (VulkanRenderer::DispatchWireComputeIndirect). Their barriers count in `wibar` + // with the draws': one barrier per shader write, whichever indirect read consumes it. + WireIndirectNativeDispatches, #endif Count }; From 2edb2aa82acdb29ea0e9cc3e40001e731d343429 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 30 Sep 2026 03:44:03 -0400 Subject: [PATCH 2/5] [Test] (P8-SV): WireMipShapeScenario (3D chain on both backends x every arm, 1D depth decline on Magma split+spawn, the 3D chain under the validation layer inproc) and WireIndirectDispatchScenario (both backends x every arm, Magma gated .IndirectDispatch.), the MipmapDepthStencilAspect tally in F1's depth/stencil decline case, WireDeclinePeek, and the two parity rows --- MobileGL/MG_IntegrationTest/CMakeLists.txt | 74 +++++ .../Harness/WireDeclinePeek.cpp | 38 +++ .../Harness/WireDeclinePeek.h | 20 ++ .../Harness/WireIndirectPeek.cpp | 1 + .../Harness/WireIndirectPeek.h | 1 + .../Scenarios/F1WireScenario.cpp | 13 + .../WireIndirectDispatchScenario.cpp | 292 ++++++++++++++++++ .../Scenarios/WireMipShapeScenario.cpp | 220 +++++++++++++ scripts/ci/spawn_lane_parity.py | 11 +- 9 files changed, 668 insertions(+), 2 deletions(-) create mode 100644 MobileGL/MG_IntegrationTest/Harness/WireDeclinePeek.cpp create mode 100644 MobileGL/MG_IntegrationTest/Harness/WireDeclinePeek.h create mode 100644 MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDispatchScenario.cpp create mode 100644 MobileGL/MG_IntegrationTest/Scenarios/WireMipShapeScenario.cpp diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 830e57f3..96483d57 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -4863,6 +4863,80 @@ if (MOBILEGL_BUILD_DISAGGREGATED) endforeach() endif() +# P8-SV: Magma split residuals (docs/Disaggregated/notes/p8/SV.md) - the 3D native mip blit's layer +# count, the 1D depth mip decline, the depth/stencil mip decline's tally row (asserted inside +# F1WireScenario) and the wire arm's native glDispatchComputeIndirect. Public GL, so the sources join +# the binary unguarded and the ambient `DirectGLES.` / `DirectVulkan.` monolith discovery runs them +# too (the Espryt entries are the control). Split arms: DirectGLES through the registration macro +# (which also replays them on Magma's informational tier), Magma's gated tier by hand: +# `.Mip3D.` and `.IndirectDispatch.` - all three transports; the counter halves read only on the +# inproc arm (WireIndirectPeek.h, WireDeclinePeek.h), the pixel/grid halves everywhere. +# `.ShaderMipDepth1D.` - split + spawn: MGITEST_MAGMA_FORCE_SHADER_MIPMAP is read BY THE SERVER, +# so a tcp copy would run the default arm (its tail is in spawn_lane_parity.py's +# MAGMA_SERVER_ENV_KNOB_NO_TCP). +# `.Mip3DValidation.` - inproc only: the 3D chain under VK_LAYER_KHRONOS_validation, red on any +# vkCmdBlitImage VUID in the output. The server's Vulkan instance must be this process for its +# stdout to be the entry's and for the case to prove the layer loaded (/proc/self/maps), so it +# is in MAGMA_INPROC_ONLY. The lane that runs integration-magma-split in CI installs the layer. +target_sources(MobileGLIntegrationTest PRIVATE Harness/WireDeclinePeek.cpp Scenarios/WireMipShapeScenario.cpp + Scenarios/WireIndirectDispatchScenario.cpp) +if (MOBILEGL_BUILD_DISAGGREGATED) + mgl_itest_register_split_arms("WireMipShapeScenario.*" "") + mgl_itest_register_split_arms("WireIndirectDispatchScenario.*" "") + set(MGL_ITEST_MAGMA_SV_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}) + mgl_itest_join_environment(MGL_ITEST_MAGMA_SV_ENVIRONMENT + ${MGL_ITEST_MAGMA_SV_FLAVOUR_ENV} ${mglItestArmTransportEnv}) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectVulkan.${mglItestArm}.Mip3D." + TEST_FILTER "WireMipShapeScenario.VolumeChainIsGeneratedAlongItsDepth" + DISCOVERY_TIMEOUT 30 + PROPERTIES LABELS "${mglItestMagmaLabels}" + TIMEOUT ${MGL_ITEST_TIMEOUT} + ${mglItestArmProperties} + ENVIRONMENT "${MGL_ITEST_MAGMA_SV_ENVIRONMENT}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/magma-sv-mip3d${mglItestArmLogTag}.log") + foreach(dispatchCase + CpuWrittenGroupCountsAreReadByTheGpuAtTheirOffsets + ComputeWrittenGroupCountsDispatchWithoutAHostWait + ShaderWrittenGroupCountsWithoutAnApplicationBarrierAreOrdered) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectVulkan.${mglItestArm}.IndirectDispatch." + TEST_FILTER "WireIndirectDispatchScenario.${dispatchCase}" + DISCOVERY_TIMEOUT 30 + PROPERTIES LABELS "${mglItestMagmaLabels}" + TIMEOUT ${MGL_ITEST_TIMEOUT} + ${mglItestArmProperties} + ENVIRONMENT "${MGL_ITEST_MAGMA_SV_ENVIRONMENT}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/magma-sv-dispatch${mglItestArmLogTag}-${dispatchCase}.log") + endforeach() + if (NOT mglItestArm STREQUAL "Tcp") + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectVulkan.${mglItestArm}.ShaderMipDepth1D." + TEST_FILTER "WireMipShapeScenario.Depth1DChainWithoutANativeBlitDeclinesByName" + DISCOVERY_TIMEOUT 30 + PROPERTIES LABELS "${mglItestMagmaLabels}" + TIMEOUT ${MGL_ITEST_TIMEOUT} + ${mglItestArmProperties} + ENVIRONMENT "${MGL_ITEST_MAGMA_SV_ENVIRONMENT}\;MGITEST_MAGMA_FORCE_SHADER_MIPMAP=1\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/magma-sv-depth1d${mglItestArmLogTag}.log") + endif() + endforeach() + mgl_itest_magma_arm_shape(Split) + mgl_itest_join_environment(MGL_ITEST_MAGMA_SV_ENVIRONMENT + ${MGL_ITEST_MAGMA_SV_FLAVOUR_ENV} ${mglItestArmTransportEnv}) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectVulkan.Split.Mip3DValidation." + TEST_FILTER "WireMipShapeScenario.VolumeChainIsGeneratedAlongItsDepth" + DISCOVERY_TIMEOUT 30 + PROPERTIES LABELS "${mglItestMagmaLabels}" + TIMEOUT ${MGL_ITEST_TIMEOUT} + FAIL_REGULAR_EXPRESSION "VUID-vkCmdBlitImage-" + ENVIRONMENT "${MGL_ITEST_MAGMA_SV_ENVIRONMENT}\;VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation\;MGITEST_REQUIRE_VK_VALIDATION_LAYER=1\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/magma-sv-mip3d-validation.log") +endif() + # ===================================================================================== # P7 PACKAGE L: THE INFORMATIONAL MAGMA CENSUS - TIER 2 OF TWO # ===================================================================================== diff --git a/MobileGL/MG_IntegrationTest/Harness/WireDeclinePeek.cpp b/MobileGL/MG_IntegrationTest/Harness/WireDeclinePeek.cpp new file mode 100644 index 00000000..140cbaea --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Harness/WireDeclinePeek.cpp @@ -0,0 +1,38 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Harness/WireDeclinePeek.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 "WireDeclinePeek.h" + +#if !defined(__ANDROID__) +#include +#if MOBILEGL_BUILD_DISAGGREGATED +#include +#include +#define MGITEST_WIRE_DECLINE_PEEK_LIVE 1 +#endif +#endif + +namespace MGITest { + +#if defined(MGITEST_WIRE_DECLINE_PEEK_LIVE) + bool PeekWireDeclineCount(const char* row, unsigned long long* out) { + if (row == nullptr || out == nullptr) return false; + namespace Magma = MobileGL::MG_Backend::DirectVulkan; + for (MobileGL::SizeT i = 0; i < Magma::WireDeclineTally::kSiteCount; ++i) { + const auto site = static_cast(i); + if (std::strcmp(Magma::WireDeclineSiteName(site), row) != 0) continue; + *out = Magma::WireDeclineTally::Get(site); + return true; + } + return false; + } +#else + bool PeekWireDeclineCount(const char*, unsigned long long*) { return false; } +#endif + +} // namespace MGITest diff --git a/MobileGL/MG_IntegrationTest/Harness/WireDeclinePeek.h b/MobileGL/MG_IntegrationTest/Harness/WireDeclinePeek.h new file mode 100644 index 00000000..db806d1b --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Harness/WireDeclinePeek.h @@ -0,0 +1,20 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Harness/WireDeclinePeek.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-SV: one row of the Magma wire arm's decline tally (WireDeclines.def, the counts behind the +// server's MGWIRE-DECLINES lines), read as a run total out of THIS process - WireIndirectPeek.h's +// shape. Process-wide means the SERVER's counts only where the server is this process: the inproc +// arm, never spawn or tcp, and never the monolith arm, which takes no wire path. False where the +// reading cannot be taken: a pull build, Android, or a name that is not a row. +#pragma once + +namespace MGITest { + + bool PeekWireDeclineCount(const char* row, unsigned long long* out); + +} // namespace MGITest diff --git a/MobileGL/MG_IntegrationTest/Harness/WireIndirectPeek.cpp b/MobileGL/MG_IntegrationTest/Harness/WireIndirectPeek.cpp index d8b4ad5f..34a4edd8 100644 --- a/MobileGL/MG_IntegrationTest/Harness/WireIndirectPeek.cpp +++ b/MobileGL/MG_IntegrationTest/Harness/WireIndirectPeek.cpp @@ -28,6 +28,7 @@ namespace MGITest { out->barriers = Stats::TotalCalls(Stats::CallClass::WireIndirectBarriers); out->hostWaits = Stats::TotalCalls(Stats::CallClass::WireHostWaits); out->indirectWaits = Stats::TotalCalls(Stats::CallClass::WireHostWaitsIndirect); + out->nativeDispatches = Stats::TotalCalls(Stats::CallClass::WireIndirectNativeDispatches); return true; } #else diff --git a/MobileGL/MG_IntegrationTest/Harness/WireIndirectPeek.h b/MobileGL/MG_IntegrationTest/Harness/WireIndirectPeek.h index 57168456..010ce2ab 100644 --- a/MobileGL/MG_IntegrationTest/Harness/WireIndirectPeek.h +++ b/MobileGL/MG_IntegrationTest/Harness/WireIndirectPeek.h @@ -21,6 +21,7 @@ namespace MGITest { 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`) + unsigned long long nativeDispatches = 0; // P8-SV: wire-indirect-native-dispatches (`wdsp`) }; bool PeekWireIndirectCounters(WireIndirectCounters* out); diff --git a/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp index 160f8288..1a37cde0 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp @@ -3,6 +3,7 @@ #include "../Harness/SplitRuntimePeek.h" #include "../Harness/PipeStatsWindow.h" #include "../Harness/SplitLane.h" +#include "../Harness/WireDeclinePeek.h" #include #include #include @@ -659,6 +660,12 @@ TEST_F(F1WireScenario, GenerateMipmapDepthStencilDeclinesAndKeepsTheSession) { glDisable(GL_SCISSOR_TEST); glClearDepth(0.5); glClear(GL_DEPTH_BUFFER_BIT); + // P8-SV: the decline is a WireDeclines.def row now (MipmapDepthStencilAspect), so the server's + // MGWIRE-DECLINES dump sees it. Readable where the server is this process: the inproc arm. + // Red once (executed, reverted): the site back to a bare MGLOG_E_ONCE - the count stays 0. + unsigned long long declinesBefore = 0; + const bool counted = PeekSplitRuntime().transportName == "inproc" && + PeekWireDeclineCount("MipmapDepthStencilAspect", &declinesBefore); glGenerateMipmap(GL_TEXTURE_2D); EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.DepthStencilMip.error"; // The session is what the retired Fatal used to take with it. A readback is a round trip @@ -669,6 +676,12 @@ TEST_F(F1WireScenario, GenerateMipmapDepthStencilDeclinesAndKeepsTheSession) { glReadPixels(1, 1, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &level0); EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.DepthStencilMip.readback"; EXPECT_NEAR(level0, 0.5f, 0.002f) << "F1.DepthStencilMip.level0"; + if (counted) { + unsigned long long declinesAfter = 0; + ASSERT_TRUE(PeekWireDeclineCount("MipmapDepthStencilAspect", &declinesAfter)); + EXPECT_EQ(declinesAfter - declinesBefore, 1u) << "F1.DepthStencilMip.tally: one named decline per refused " + "glGenerateMipmap (WireDeclines.def MipmapDepthStencilAspect)"; + } glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); glDeleteTextures(1, &depthStencil); glBindTexture(GL_TEXTURE_2D, texture); diff --git a/MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDispatchScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDispatchScenario.cpp new file mode 100644 index 00000000..f7f20009 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDispatchScenario.cpp @@ -0,0 +1,292 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDispatchScenario.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-SV: THE MAGMA WIRE ARM ISSUES glDispatchComputeIndirect FROM THE WIRE STORE, AS THE MONOLITH +// ARM DOES (docs/Disaggregated/notes/p8/SV.md). +// +// Before P8-SV the wire arm read the three group counts through ReadWireBuffer and dispatched them +// from the CPU, so a dispatch buffer a shader had written cost a whole-GPU wait per call - the +// dispatch twin of what P8-D removed for indirect draws (WireIndirectDrawScenario.cpp). The cases +// pin both halves: +// * THE GRID, on every arm and both backends - each dispatch marks exactly the work groups its +// counts name, and GPU-written counts are the ones the dispatch sees (the buffer starts with +// OTHER counts, so a stale read shows as the wrong grid); +// * THE SERVER'S OWN COUNTERS (Harness/WireIndirectPeek.h), where they can be read - Magma on the +// inproc arm: every call issued as vkCmdDispatchIndirect (`wdsp`), no host wait (`whw`), and the +// INDIRECT_COMMAND_READ barrier exactly once per shader write (`wibar`). +// The counters are read BEFORE the grids are read back: that readback is a host read of a store a +// shader wrote, so it takes a host wait of its own, which is the application's and not the +// dispatch's. + +#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 { + + // Grids up to 4 x 4 x 2: one cell per work group. + constexpr std::uint32_t kGridX = 4, kGridY = 4, kGridZ = 2; + constexpr std::uint32_t kCells = kGridX * kGridY * kGridZ; + + // One invocation per work group, and each group marks its own cell. + constexpr const char* kMarkSource = R"(#version 430 core +layout(local_size_x = 1) in; +layout(std430, binding = 2) buffer Marks { uint marks[]; }; +void main() { + uvec3 g = gl_WorkGroupID; + atomicAdd(marks[(g.z * 4u + g.y) * 4u + g.x], 1u); +} +)"; + // 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]; } +)"; + + using Grid = std::array; + + class WireIndirectDispatchScenario : public ScenarioTest { + protected: + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + m_magma = Gl().BackendName() == "DirectVulkan"; + m_transport = PeekSplitRuntime().transportName; + m_mark = CompileCompute(kMarkSource); + ASSERT_NE(m_mark, 0u); + m_copy = CompileCompute(kCopySource); + ASSERT_NE(m_copy, 0u); + 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 dispatch. + 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; + glBindBuffer(GL_DISPATCH_INDIRECT_BUFFER, 0); + for (GLuint& buffer : m_marks) { + if (buffer != 0) glDeleteBuffers(1, &buffer); + } + for (GLuint* buffer : {&m_parameters, &m_source}) { + if (*buffer != 0) glDeleteBuffers(1, buffer); + *buffer = 0; + } + if (m_mark != 0) glDeleteProgram(m_mark); + 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; + } + + static void Fill(GLuint& name, GLenum target, const std::vector& words) { + if (name == 0) glGenBuffers(1, &name); + glBindBuffer(target, name); + glBufferData(target, static_cast(words.size() * sizeof(std::uint32_t)), words.data(), + GL_DYNAMIC_COPY); + } + + // The dispatch-indirect store, holding CPU-written counts. + void Parameters(const std::vector& words) { + Fill(m_parameters, GL_DISPATCH_INDIRECT_BUFFER, words); + } + + // The compute write: the parameter store (already holding its poison) receives `words`. + void ComputeWriteParameters(const std::vector& words) { + Fill(m_source, GL_SHADER_STORAGE_BUFFER, words); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_parameters); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_source); + glUseProgram(m_copy); + glDispatchCompute(static_cast(words.size()), 1, 1); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0); + } + + // One indirect dispatch of the mark program into a fresh, zeroed cell buffer. Returns + // the index of that buffer for ExpectGrid. + size_t DispatchIndirect(GLintptr offset) { + GLuint marks = 0; + Fill(marks, GL_SHADER_STORAGE_BUFFER, std::vector(kCells, 0u)); + m_marks.push_back(marks); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, marks); + glUseProgram(m_mark); + glBindBuffer(GL_DISPATCH_INDIRECT_BUFFER, m_parameters); + glDispatchComputeIndirect(offset); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, 0); + return m_marks.size() - 1; + } + + // A round trip that reads no buffer store, so the server has applied every dispatch + // above before the counters are read - and no host wait of its own is added. + static void Settle() { + BindDefaultFramebuffer(); + (void)ReadPixels(1, 1); + } + + void ExpectGrid(size_t which, const Grid& grid, const std::string& what) { + glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT); + std::vector cells(kCells, 0xDEADu); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_marks[which]); + glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, static_cast(kCells * sizeof(std::uint32_t)), + cells.data()); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << what << ": readback"; + size_t wrong = 0; + std::string first; + for (std::uint32_t z = 0; z < kGridZ; ++z) { + for (std::uint32_t y = 0; y < kGridY; ++y) { + for (std::uint32_t x = 0; x < kGridX; ++x) { + const bool inside = x < grid[0] && y < grid[1] && z < grid[2]; + const std::uint32_t got = cells[(z * kGridY + y) * kGridX + x]; + if (got != (inside ? 1u : 0u) && wrong++ == 0) { + first = "group (" + std::to_string(x) + "," + std::to_string(y) + "," + + std::to_string(z) + ") = " + std::to_string(got); + } + } + } + } + EXPECT_EQ(wrong, 0u) << what << ": expected exactly the " << grid[0] << "x" << grid[1] << "x" + << grid[2] << " grid marked once; first wrong " << first; + } + + // 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)); + WireIndirectCounters delta{}; + delta.nativeDraws = now.nativeDraws - m_before.nativeDraws; + delta.cpuExpansions = now.cpuExpansions - m_before.cpuExpansions; + delta.barriers = now.barriers - m_before.barriers; + delta.hostWaits = now.hostWaits - m_before.hostWaits; + delta.indirectWaits = now.indirectWaits - m_before.indirectWaits; + delta.nativeDispatches = now.nativeDispatches - m_before.nativeDispatches; + return delta; + } + + bool m_magma = false; + std::string m_transport; + WireIndirectCounters m_before{}; + GLuint m_mark = 0, m_copy = 0, m_parameters = 0, m_source = 0; + std::vector m_marks; + }; + + // CPU-written counts at two non-zero offsets of one store. On Magma/inproc both calls must + // be issued natively, with no host wait and no barrier (no shader wrote the store). + TEST_F(WireIndirectDispatchScenario, CpuWrittenGroupCountsAreReadByTheGpuAtTheirOffsets) { + if (!Ready() || IsSkipped()) return; + Parameters({1, 1, 1, 3, 2, 1, 4, 1, 2}); + const size_t first = DispatchIndirect(12); + const size_t second = DispatchIndirect(24); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); + Settle(); + if (CountersReadable()) { + const WireIndirectCounters delta = Delta(); + EXPECT_EQ(delta.nativeDispatches, 2u) << "every indirect dispatch must be vkCmdDispatchIndirect"; + EXPECT_EQ(delta.hostWaits, 0u) << "a host wait was taken with no GPU write to the counts"; + EXPECT_EQ(delta.barriers, 0u) << "no shader wrote this store, so no barrier is due"; + } + ExpectGrid(first, {3, 2, 1}, "glDispatchComputeIndirect at byte 12"); + ExpectGrid(second, {4, 1, 2}, "glDispatchComputeIndirect at byte 24"); + } + + // Counts a compute shader wrote, behind the application's own GL_COMMAND_BARRIER_BIT. The + // store starts with OTHER counts, so a stale read dispatches the wrong grid. On + // Magma/inproc: issued natively and - the point of the change - NO host wait. + TEST_F(WireIndirectDispatchScenario, ComputeWrittenGroupCountsDispatchWithoutAHostWait) { + if (!Ready() || IsSkipped()) return; + Parameters({1, 1, 1}); + ComputeWriteParameters({3, 2, 1}); + glMemoryBarrier(GL_COMMAND_BARRIER_BIT); + const size_t first = DispatchIndirect(0); + + Parameters({1, 1, 1, 1, 1, 1}); + ComputeWriteParameters({1, 1, 1, 2, 2, 2}); + glMemoryBarrier(GL_COMMAND_BARRIER_BIT); + const size_t second = DispatchIndirect(12); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); + Settle(); + if (CountersReadable()) { + const WireIndirectCounters delta = Delta(); + EXPECT_EQ(delta.nativeDispatches, 2u) << "both dispatches must be issued natively"; + EXPECT_EQ(delta.hostWaits, 0u) + << "an indirect dispatch of shader-written counts waited for the GPU on the host"; + EXPECT_EQ(delta.barriers, 2u) << "one INDIRECT_COMMAND_READ barrier per freshly written store"; + } + ExpectGrid(first, {3, 2, 1}, "compute-written counts at byte 0"); + ExpectGrid(second, {2, 2, 2}, "compute-written counts at byte 12"); + } + + // Flywheel's shape, one call over: the counts are written and dispatched with no + // glMemoryBarrier between (GL leaves it undefined). The wire arm orders it anyway, once per + // write, as P8-D does for draws. Magma's split arms only: the monolith arm records no + // dependency here (P8-D recorded that half for dev), and Espryt's is its driver's. + TEST_F(WireIndirectDispatchScenario, ShaderWrittenGroupCountsWithoutAnApplicationBarrierAreOrdered) { + 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, P8-SV)"; + } + Parameters({1, 1, 1}); + ComputeWriteParameters({3, 2, 1}); + const size_t first = DispatchIndirect(0); + const size_t again = DispatchIndirect(0); + ComputeWriteParameters({1, 2, 2}); + const size_t third = DispatchIndirect(0); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); + Settle(); + if (CountersReadable()) { + const WireIndirectCounters delta = Delta(); + EXPECT_EQ(delta.nativeDispatches, 3u); + EXPECT_EQ(delta.hostWaits, 0u) << "a host wait was taken"; + EXPECT_EQ(delta.barriers, 2u) << "exactly one barrier per write: the second dispatch reads no new write"; + } + ExpectGrid(first, {3, 2, 1}, "the first dispatch after the write"); + ExpectGrid(again, {3, 2, 1}, "a second dispatch, no write between"); + ExpectGrid(third, {1, 2, 2}, "the dispatch after a second write"); + } + + } // namespace +} // namespace MGITest diff --git a/MobileGL/MG_IntegrationTest/Scenarios/WireMipShapeScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/WireMipShapeScenario.cpp new file mode 100644 index 00000000..55bec3d7 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/WireMipShapeScenario.cpp @@ -0,0 +1,220 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/WireMipShapeScenario.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-SV: TWO glGenerateMipmap SHAPES THE MAGMA WIRE ARM GOT WRONG (docs/Disaggregated/notes/p8/SV.md). +// +// * A 3D chain through the NATIVE blit. The wire arm passed the level's depth as the blit's +// subresource layerCount on an image with one array layer (VUID-vkCmdBlitImage-srcImage-00240, +// -srcSubresource-01707, -dstSubresource-01708). Lavapipe blits a 3D image by its z offsets and +// ignores layerCount, so the PIXELS below are right with or without the fix on this host; what +// goes red on the host is the validation layer, which the `.Mip3DValidation.` entry runs this +// case under (inproc, so the server's Vulkan instance is this process and its stdout is ours). +// The pixel half runs on both backends and every arm, and is what a driver that honours +// layerCount would fail. +// * A 1D DEPTH chain with no native blit. The depth shader arm renders 2D views, which a 1D image +// does not admit; the wire arm handed it the chain anyway. It is now the named decline +// MipmapShaderFormatOrShape, decided before the chain grows, and the session carries on. +// Magma only, and reachable on this host only with MGITEST_MAGMA_FORCE_SHADER_MIPMAP (lavapipe +// blits depth natively), which the SERVER reads: split and spawn entries only. + +#include +#include +#include +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" +#include "../Harness/SplitLane.h" +#include "../Harness/SplitRuntimePeek.h" +#include "../Harness/WireDeclinePeek.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include + +namespace MGITest { + namespace { + + using Texel = std::array; + + // Is the Khronos validation layer loaded into THIS process? On the inproc arm the server's + // Vulkan instance lives here, so this is the proof that a run "under the layer" really was. + bool ValidationLayerMapped() { +#if defined(__linux__) + std::ifstream maps("/proc/self/maps"); + std::string line; + while (std::getline(maps, line)) { + if (line.find("VkLayer_khronos_validation") != std::string::npos) return true; + } +#endif + return false; + } + + class WireMipShapeScenario : public ScenarioTest { + protected: + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + m_magma = Gl().BackendName() == "DirectVulkan"; + m_transport = PeekSplitRuntime().transportName; + } + + // The Magma server's decline tally is readable where the server is this process. + bool DeclinesReadable() const { return m_magma && m_transport == "inproc"; } + + bool m_magma = false; + std::string m_transport; + }; + + // A 3D RGBA8 chain, 8x8x8 with its full storage, whose level-0 slices come in PAIRS of one + // colour: level 1's slice k is then pair k's colour exactly, and every level below is the + // average of two slices of the level above. A blit that loses the z axis (one slice only, + // or slices read as array layers of a 2D image) cannot produce those numbers. + TEST_F(WireMipShapeScenario, VolumeChainIsGeneratedAlongItsDepth) { + if (!Ready() || IsSkipped()) return; + constexpr int kSide = 8, kLevels = 4; + const std::array pairs = {Texel{200, 40, 80, 255}, Texel{40, 200, 120, 255}, + Texel{100, 100, 240, 255}, Texel{240, 160, 0, 255}}; + GLuint volume = 0; + glGenTextures(1, &volume); + glBindTexture(GL_TEXTURE_3D, volume); + glTexStorage3D(GL_TEXTURE_3D, kLevels, GL_RGBA8, kSide, kSide, kSide); + std::vector level0(static_cast(kSide * kSide * kSide * 4)); + for (int z = 0; z < kSide; ++z) { + const Texel& texel = pairs[static_cast(z / 2)]; + for (int at = 0; at < kSide * kSide; ++at) { + for (int c = 0; c < 4; ++c) + level0[static_cast((z * kSide * kSide + at) * 4 + c)] = static_cast(texel[c]); + } + } + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, kSide, kSide, kSide, GL_RGBA, GL_UNSIGNED_BYTE, level0.data()); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "3D texture setup"; + glGenerateMipmap(GL_TEXTURE_3D); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "glGenerateMipmap(GL_TEXTURE_3D)"; + + const auto average = [](const Texel& a, const Texel& b) { + return Texel{(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2, (a[3] + b[3]) / 2}; + }; + // expected[level][slice] + std::vector> expected(kLevels); + expected[1].assign(pairs.begin(), pairs.end()); + expected[2] = {average(pairs[0], pairs[1]), average(pairs[2], pairs[3])}; + expected[3] = {average(expected[2][0], expected[2][1])}; + glPixelStorei(GL_PACK_ALIGNMENT, 4); + for (int level = 1; level < kLevels; ++level) { + const int side = kSide >> level; + GLint width = 0, height = 0, depth = 0; + glGetTexLevelParameteriv(GL_TEXTURE_3D, level, GL_TEXTURE_WIDTH, &width); + glGetTexLevelParameteriv(GL_TEXTURE_3D, level, GL_TEXTURE_HEIGHT, &height); + glGetTexLevelParameteriv(GL_TEXTURE_3D, level, GL_TEXTURE_DEPTH, &depth); + EXPECT_EQ(width, side) << "level " << level; + EXPECT_EQ(height, side) << "level " << level; + EXPECT_EQ(depth, side) << "level " << level; + std::vector pixels(static_cast(side * side * side * 4), 0x5a); + glGetTexImage(GL_TEXTURE_3D, level, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "readback of level " << level; + for (int z = 0; z < side; ++z) { + const Texel& want = expected[static_cast(level)][static_cast(z)]; + size_t wrong = 0; + Texel first{}; + for (int at = 0; at < side * side; ++at) { + const size_t base = static_cast((z * side * side + at) * 4); + bool ok = true; + for (int c = 0; c < 4; ++c) { + const int got = pixels[base + static_cast(c)]; + if (got < want[c] - 2 || got > want[c] + 2) ok = false; + } + if (!ok && wrong++ == 0) { + for (int c = 0; c < 4; ++c) first[c] = pixels[base + static_cast(c)]; + } + } + EXPECT_EQ(wrong, 0u) << "level " << level << " slice " << z << ": expected (" << want[0] << "," + << want[1] << "," << want[2] << "," << want[3] << "), first wrong texel (" + << first[0] << "," << first[1] << "," << first[2] << "," << first[3] << ")"; + } + } + glBindTexture(GL_TEXTURE_3D, 0); + glDeleteTextures(1, &volume); + + // The `.Mip3DValidation.` entry: the blit above ran under the Khronos validation layer, + // whose VUID lines the entry's FAIL_REGULAR_EXPRESSION reads. Without the layer actually + // loaded, a clean output would prove nothing, so the entry requires it. + if (SplitLane::MarkerIsOne("MGITEST_REQUIRE_VK_VALIDATION_LAYER")) { + ASSERT_TRUE(m_magma && m_transport == "inproc") + << "the validation entry reads the server's layer output in-process: Magma, inproc"; + EXPECT_TRUE(ValidationLayerMapped()) + << "VK_LAYER_KHRONOS_validation is not loaded in this process, so the absence of " + "VUID lines in this entry's output says nothing"; + } + } + + // A 1D DEPTH chain with the native blit forced off: no mip arm renders a 1D image, so the + // wire arm declines by name (MipmapShaderFormatOrShape) before growing the chain. Pinned: + // no GL error, the session answering (two readback round trips), level 0 as uploaded, level + // 1 still the application's own sentinel - and, where the server is this process, exactly + // one MipmapShaderFormatOrShape decline counted. + TEST_F(WireMipShapeScenario, Depth1DChainWithoutANativeBlitDeclinesByName) { + if (!Ready() || IsSkipped()) return; + if (!m_magma) + GTEST_SKIP() << "Magma wire-arm decline (P8-SV): the knob and the depth shader arm it forces are " + "Magma's; Espryt (" << Gl().BackendName() << ") generates this chain itself"; + const std::string tier = SplitLane::MarkerValue("MGITEST_MAGMA_FORCE_SHADER_MIPMAP"); + if (tier.empty()) + GTEST_SKIP() << "MGITEST_MAGMA_FORCE_SHADER_MIPMAP is unset: this driver blits depth natively, " + "so the depth shader arm this case refuses is not entered"; + constexpr int kWidth = 16; + GLuint line = 0; + glGenTextures(1, &line); + glBindTexture(GL_TEXTURE_1D, line); + glTexStorage1D(GL_TEXTURE_1D, 5, GL_DEPTH_COMPONENT32F, kWidth); + const std::vector level0(kWidth, 0.25f), level1(kWidth / 2, 0.75f); + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + glTexSubImage1D(GL_TEXTURE_1D, 0, 0, kWidth, GL_DEPTH_COMPONENT, GL_FLOAT, level0.data()); + glTexSubImage1D(GL_TEXTURE_1D, 1, 0, kWidth / 2, GL_DEPTH_COMPONENT, GL_FLOAT, level1.data()); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "1D depth texture setup"; + unsigned long long declinesBefore = 0; + const bool counted = + DeclinesReadable() && PeekWireDeclineCount("MipmapShaderFormatOrShape", &declinesBefore); + glGenerateMipmap(GL_TEXTURE_1D); + EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "glGenerateMipmap(GL_TEXTURE_1D) on a depth chain"; + glPixelStorei(GL_PACK_ALIGNMENT, 4); + for (const int level : {0, 1}) { + const int width = kWidth >> level; + std::vector depths(static_cast(width), -1.0f); + glGetTexImage(GL_TEXTURE_1D, level, GL_DEPTH_COMPONENT, GL_FLOAT, depths.data()); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "readback of level " << level; + const GLfloat want = level == 0 ? 0.25f : 0.75f; + size_t wrong = 0; + GLfloat first = 0.0f; + for (const GLfloat depth : depths) { + if (depth < want - 0.0001f || depth > want + 0.0001f) { + if (wrong++ == 0) first = depth; + } + } + EXPECT_EQ(wrong, 0u) << "level " << level << " tier=" << tier << ": a declined mip must leave " + << "every level as the application wrote it (expected " << want + << ", first wrong " << first << ")"; + } + glBindTexture(GL_TEXTURE_1D, 0); + glDeleteTextures(1, &line); + if (counted) { + unsigned long long declinesAfter = 0; + ASSERT_TRUE(PeekWireDeclineCount("MipmapShaderFormatOrShape", &declinesAfter)); + EXPECT_EQ(declinesAfter - declinesBefore, 1u) + << "the 1D depth chain must be refused once, by name, before the chain grows"; + } + } + + } // namespace +} // namespace MGITest diff --git a/scripts/ci/spawn_lane_parity.py b/scripts/ci/spawn_lane_parity.py index f442a506..e1f4dc4e 100644 --- a/scripts/ci/spawn_lane_parity.py +++ b/scripts/ci/spawn_lane_parity.py @@ -64,7 +64,12 @@ FIXTURE_ENTRIES = {"TcpServer.Start", "TcpServer.Stop"} # answers unavailable, so a copy there checks pixels under a name claiming # the pools. The pixels do run on every arm: the Espryt entries and the # Magma informational and full-suite tiers. -MAGMA_INPROC_ONLY = ("MagmaRunAheadScenario.", "MagmaWireCacheScenario.", "DescriptorPoolGrowthScenario.") +# `.Mip3DValidation.` (P8-SV, a TAIL): WireMipShapeScenario's 3D chain under the Khronos validation +# layer, red on a vkCmdBlitImage VUID in the entry's own output and proving the +# layer loaded from /proc/self/maps - both need the server's Vulkan instance +# in this process. The knob-free `.Mip3D.` entries keep all three arms. +MAGMA_INPROC_ONLY = ("MagmaRunAheadScenario.", "MagmaWireCacheScenario.", "DescriptorPoolGrowthScenario.", + ".Mip3DValidation.") # P7 wave 2-B2: THE SANCTIONED ASYMMETRY THAT IS NOT INPROC-ONLY - these exist on split AND # spawn and cannot exist on tcp. Every MGITEST_MAGMA_FORCE_* knob is read BY THE SERVER @@ -140,7 +145,9 @@ MAGMA_SERVER_ENV_KNOB_NO_TCP = (".ShaderMip1.", ".ShaderMip2.", ".DepthMip.", f".PrimGenReroute.PrimitivesGeneratedNoXfbScenario.{case}" for case in PRIMGEN_REROUTE_CASES) + tuple( f".SplitRecording64.OversizedRecordingSplitScenario.{case}" for case in SPLIT_RECORDING_CASES) + ( # P8-F: the mip shader-shape decline, forced by MGITEST_MAGMA_FORCE_SHADER_MIPMAP (server-read). - ".ShaderMipDecline.",) + ".ShaderMipDecline.", + # P8-SV: the 1D depth chain's decline, forced by the same server-read knob. + ".ShaderMipDepth1D.",) # THE MIRROR SHAPE - entries that exist on tcp ALONE, by construction. Each tail must match at least From 394d2554874399d69edc8039a516a68d5f8a3d6b Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 30 Sep 2026 03:45:40 -0400 Subject: [PATCH 3/5] [Docs] (P8-SV): the package note - the four items with their before/after, the design, the gates, ten red-once rows, the monolith observation and what was found but not fixed --- docs/Disaggregated/notes/p8/SV.md | 63 +++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 docs/Disaggregated/notes/p8/SV.md diff --git a/docs/Disaggregated/notes/p8/SV.md b/docs/Disaggregated/notes/p8/SV.md new file mode 100644 index 00000000..27cfc5aa --- /dev/null +++ b/docs/Disaggregated/notes/p8/SV.md @@ -0,0 +1,63 @@ +# P8-SV:Magma split 残项 + +> 2026-09-30,分支 `p8/sv`(基线 `cbbaf77f`,第一波集成头)。计划:[`PLAN-P8.md`](PLAN-P8.md) 第二波 SV;来源:[`F.md`](F.md)「发现但未修」三条、[`D.md`](D.md) §7 第一条。主机 lavapipe,只动 Magma(Espryt 作对照)。 + +## 1. 结论 + +| 项 | 改动 | 位置 | 之前 | 之后 | +|---|---|---|---|---| +| 1 | 3D 原生 mip blit 的子资源 `layerCount` 改为 1,深度只走 z 偏移(逐级减半,原本就对) | `WireFramebuffer.inc:1626-1636` | `layerCount = Depth >> level`(`VkTextureManager.cpp:2738-2740` 的切片寻址)当层数,图像 `arrayLayers = 1`;验证层报 `VUID-vkCmdBlitImage-srcImage-00240`、`-srcSubresource-01707`、`-dstSubresource-01708` | 与 monolith 同形(`VulkanRenderer.cpp:12303`、`:12403`);验证层 0 条 | +| 2 | 深度模板 mip 拒绝补 `WireDeclineTally` 行 | `WireDeclines.def:124`;站点 `WireFramebuffer.inc:1401` | 只有 `MGLOG_E_ONCE`,`MGWIRE-DECLINES` 看不到 | `MGL_WIRE_DECLINE_AT(MipmapDepthStencilAspect, …)`,日志原文保留 | +| 3 | 深度着色器 mip 拒 1D / 1D 数组 / 3D | 生长前判定 `WireFramebuffer.inc:1426`、`:1437`;pass 自身 `WireDepthMipmap.inc:67-74`;调用方传 view 类型 `WireFramebuffer.inc:1491` | 只拒 `is3D`,调用方从不设置;1D 深度链进 pass,对 1D 图像建 2D view(`WireDepthMipmap.inc:244`) | 生长前具名拒绝 `MipmapShaderFormatOrShape`(`WireFramebuffer.inc:1465`);pass 的拒绝是漂移保护(`:1508-1513`) | +| 4 | wire 臂 `glDispatchComputeIndirect` 原生化 | `DispatchWireComputeIndirect`(`WireDraw.inc:789`),入口 `VulkanRenderer.cpp:7790-7797`,`DirectVulkan.cpp:938-945` | `ReadWireBuffer` 在 CPU 读 3 个字,shader 写过则整 GPU 等待 | `vkCmdDispatchIndirect`(`WireDraw.inc:844`);不再有 CPU 路径 | + +## 2. 设计 + +| 项 | 内容 | +|---|---| +| 1 | `ResolveWireTextureStorage` 不动:Tex3D 的 `layers` 是附件、描述符、回读共用的切片寻址;只在 blit 处按 `resource->viewType == 3D` 改用 `{层 0, 1 层}` | +| 2 | 行追加在 `WireDeclines.def` 末尾(只追加);宏同时计数与 `_ONCE` 日志 | +| 3 | 两层:生长前 `depthArmTakesShape` 加 `planar`(与颜色臂同一谓词,决定在 `GrowWireTextureMipChain` 之前,`WireFramebuffer.inc:1469`);pass 按 `WireImage::viewType` 再拒一次。monolith 调用方(`VulkanRenderer.cpp:4866`、`:4905`)不设 viewType,行为不变 | +| 4 | 形状同 monolith(`VulkanRenderer.cpp:7862`),store 取自 `VerbDispatchIndirectBuffer`(`AcquireWireSlice` 打 `lastUseSerial`) | +| 4 屏障 | P8-D 规则:store 被标 shader 写过(`VkBufferManager.cpp:603`)则首个 indirect 读前录一次 `ALL_COMMANDS/MEMORY_WRITE → DRAW_INDIRECT/INDIRECT_COMMAND_READ`(`WireDraw.inc:808`);在绑定本次 dispatch 的 SSBO **之前** 取标,否则本次 dispatch 若把参数 store 也当输出,会吃掉下一个读者的标 | +| 4 拒绝 | store 缺失 `DispatchIndirectBufferUnbound`、越界 / 未 4 对齐 / 负偏移 `DispatchIndirectRange`(`WireDeclines.def:127-128`);程序、纹理、管线、绑定四处拒绝复用 `DispatchWireCompute` 的行 | +| 4 计数 | 新 `wdsp`(`PipeStats.h:289`、`PipeStats.cpp:706`),屏障计入 D 的 `wibar`,等待仍是 `whw`;`WireIndirectCounters::nativeDispatches` 追加在结构末尾 | +| G1 | 全部在 `MOBILEGL_BUILD_DISAGGREGATED` / `MOBILEGL_PIPE_PUSH` 下 | + +## 3. 门 + +| 用例 | 断言 | 登记 | +|---|---|---| +| `WireMipShapeScenario.VolumeChainIsGeneratedAlongItsDepth`(`WireMipShapeScenario.cpp:82`) | 8³ RGBA8,层 0 按切片对着色;1..3 级每片等于上一级两片均值;`GL_TEXTURE_DEPTH` 逐级 | 两后端 monolith + 各 split 臂;Magma 门控 `.Mip3D.` 三臂 | +| 同上,`.Mip3DValidation.` | `VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation`,输出含 `VUID-vkCmdBlitImage-` 即红(`FAIL_REGULAR_EXPRESSION`);用例从 `/proc/self/maps` 证实层已加载(`WireMipShapeScenario.cpp:51`、`:156`) | 只 inproc(`MobileGL/MG_IntegrationTest/CMakeLists.txt:4931-4937`),`spawn_lane_parity.py:67-72` 的 `MAGMA_INPROC_ONLY`;CI 跑 `integration-magma-split` 的作业装了验证层(`.github/workflows/test.yml:1700`、`:1986`) | +| `WireMipShapeScenario.Depth1DChainWithoutANativeBlitDeclinesByName`(`WireMipShapeScenario.cpp:167`) | 1D `DEPTH_COMPONENT32F`,`MGITEST_MAGMA_FORCE_SHADER_MIPMAP=1`:无 GL 错误、两次回读、层 1 仍是哨兵;inproc 上 `MipmapShaderFormatOrShape` +1 | Magma `.ShaderMipDepth1D.` split + spawn(server 读旋钮),`spawn_lane_parity.py:150` | +| `F1WireScenario.GenerateMipmapDepthStencilDeclinesAndKeepsTheSession` | 原像素断言 + inproc 上 `MipmapDepthStencilAspect` +1(`F1WireScenario.cpp:663-683`,经 `WireDeclinePeek.cpp:23`) | 原登记不变 | +| `WireIndirectDispatchScenario` 三例(`WireIndirectDispatchScenario.cpp:219`、`:239`、`:267`) | 网格逐 work group 标记;inproc Magma:`wdsp` = 调用数、`whw` = 0、`wibar` = 写次数(读数在结果回读之前取) | 两后端各臂;Magma 门控 `.IndirectDispatch.` 三臂;第三例(无应用屏障)只 Magma split 臂 | + +## 4. red-once(全部执行并还原:拷贝比对 + touch) + +| # | 变异 | 载体 | 观察到的红 | +|---|---|---|---| +| R1 | blit 改回 `{baseLayer, layerCount}` | `.Mip3D.` 三臂、`.Mip3DValidation.`、两后端 monolith / 各臂 | `.Mip3DValidation.` 红:`Error regular expression found`,三条 VUID 各 6 次;像素用例全绿(lavapipe 3D blit 按 z 偏移、不看 layerCount) | +| R2 | 站点改回裸 `MGLOG_E_ONCE` | `F1WireScenario.GenerateMipmapDepthStencilDeclines*` 各臂 | inproc 两例红(`.Split.Fm.`、`.Split.Full.`):计数差 0 ≠ 1;spawn / tcp 绿 | +| R2b | 同上,静态 | `wire_declines_audit.py` | rc 1:`row MipmapDepthStencilAspect has NO SITE` | +| R3 | 生长前 `planar` 与 pass 的 view 类型拒绝都去掉 | `.ShaderMipDepth1D.` split、spawn | 两例红:层 1 被写成 0.25(哨兵 0.75),无拒绝计数;pass 在 1D 图像上建 2D view 仍产出像素 | +| R3b | 只去生长前判定 | 同上 | 绿:pass 拒绝兜住(`the depth pass refused level 1`,计数 1),漂移保护有效 | +| R4 | wire 分支恢复 `ReadWireBuffer` + CPU dispatch | `WireIndirectDispatchScenario` 各臂 | inproc 9 例红(3 例 × `.Split.IndirectDispatch.` / `.Split.` / `.Split.Full.`):`wdsp` 0、GPU 写过的 `whw` 2、`wibar` 0;spawn / tcp / monolith 绿(CPU 路径网格正确) | +| R5 | 去屏障(`if (false && …)`) | 同上 | inproc 6 例红(两个 shader 写用例 × 3 登记):`wibar` 0 ≠ 2;网格在 lavapipe 上不红(按序执行),spawn / tcp 绿 | +| R6 | `wdsp=` 改成 `wds=` | `PipeStatsTest.WireIndirectCountersPrintInTheirOwnBracket` | 红 | +| R7a | 删 `.ShaderMipDepth1D.` 例外行 | `spawn_lane_parity.py build-split` | rc 1:tcp 臂缺该例 | +| R7b | 删 `.Mip3DValidation.` 例外行 | 同上 | rc 1:spawn、tcp 两臂缺该例 | + +验证层后验:`.Split.IndirectDispatch.` 三例、`.Split.ShaderMipDepth1D.`、`.Split.Mip3D.`、`.Split.Fm.` 深度模板例在层下全绿,VUID 0 条。 + +## 5. monolith 观察(记给 dev / P13,本包不修) + +| 观察 | 位置 | 证据 | +|---|---|---| +| split 构建的 monolith 臂深度着色器 mip 同样不传 view 类型:设备无深度 BLIT_DST 时,1D 深度链会对 1D 图像建 2D view;Release 下 `MOBILEGL_ASSERT` 是空的 | `VulkanRenderer.cpp:4905`、`:12321` | 推断:lavapipe 原生 blit 深度,主机红不出;monolith 不读 `MGITEST_MAGMA_FORCE_SHADER_MIPMAP` | + +## 6. 发现但未修 + +- `mipmap-shader-format-or-shape` 的明细对深度形状也打印 `missing=COLOR_ATTACHMENT`(1D 深度的真实原因是 `view=0`);是 F 的 `MagmaWireShapeDetail` 口径,未改。 +- create 系列 fixture 没有 `glDispatchComputeIndirect`([`D.md`](D.md) §7),item 4 的真实负载收益未测。 From 982958ebb1d7588a618d2c9679b48b4cfbb8a4bb Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 30 Sep 2026 04:42:06 -0400 Subject: [PATCH 4/5] [Test] (P8-SV): rename WireIndirectDispatchScenario's dispatch store member m_parameters -> m_groupCounts - gen_pipe_dirty_surface.py reads writes to shutter member names across all of MobileGL, and the name made NEW_PATCH_STATE's shutter look written outside its roots (self-test negative control 16 stopped tripping) --- .../Scenarios/WireIndirectDispatchScenario.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDispatchScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDispatchScenario.cpp index f7f20009..c5c2f656 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDispatchScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDispatchScenario.cpp @@ -91,7 +91,7 @@ void main() { words[gl_GlobalInvocationID.x] = source[gl_GlobalInvocationID.x]; for (GLuint& buffer : m_marks) { if (buffer != 0) glDeleteBuffers(1, &buffer); } - for (GLuint* buffer : {&m_parameters, &m_source}) { + for (GLuint* buffer : {&m_groupCounts, &m_source}) { if (*buffer != 0) glDeleteBuffers(1, buffer); *buffer = 0; } @@ -130,13 +130,13 @@ void main() { words[gl_GlobalInvocationID.x] = source[gl_GlobalInvocationID.x]; // The dispatch-indirect store, holding CPU-written counts. void Parameters(const std::vector& words) { - Fill(m_parameters, GL_DISPATCH_INDIRECT_BUFFER, words); + Fill(m_groupCounts, GL_DISPATCH_INDIRECT_BUFFER, words); } // The compute write: the parameter store (already holding its poison) receives `words`. void ComputeWriteParameters(const std::vector& words) { Fill(m_source, GL_SHADER_STORAGE_BUFFER, words); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_parameters); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_groupCounts); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_source); glUseProgram(m_copy); glDispatchCompute(static_cast(words.size()), 1, 1); @@ -152,7 +152,7 @@ void main() { words[gl_GlobalInvocationID.x] = source[gl_GlobalInvocationID.x]; m_marks.push_back(marks); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, marks); glUseProgram(m_mark); - glBindBuffer(GL_DISPATCH_INDIRECT_BUFFER, m_parameters); + glBindBuffer(GL_DISPATCH_INDIRECT_BUFFER, m_groupCounts); glDispatchComputeIndirect(offset); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, 0); return m_marks.size() - 1; @@ -210,7 +210,7 @@ void main() { words[gl_GlobalInvocationID.x] = source[gl_GlobalInvocationID.x]; bool m_magma = false; std::string m_transport; WireIndirectCounters m_before{}; - GLuint m_mark = 0, m_copy = 0, m_parameters = 0, m_source = 0; + GLuint m_mark = 0, m_copy = 0, m_groupCounts = 0, m_source = 0; std::vector m_marks; }; From 4cfb29a5534f583646d8a966bf403c3ac768b0a3 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 30 Sep 2026 05:02:37 -0400 Subject: [PATCH 5/5] [Docs] (P8-SV): the note's gate table (G1 0/0, wire-declines 61/66, split_coverage, every lane, retrace 230/236) and the dirty-surface scan finding --- docs/Disaggregated/notes/p8/SV.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/Disaggregated/notes/p8/SV.md b/docs/Disaggregated/notes/p8/SV.md index 27cfc5aa..0eaad0ed 100644 --- a/docs/Disaggregated/notes/p8/SV.md +++ b/docs/Disaggregated/notes/p8/SV.md @@ -51,13 +51,25 @@ 验证层后验:`.Split.IndirectDispatch.` 三例、`.Split.ShaderMipDepth1D.`、`.Split.Mip3D.`、`.Split.Fm.` 深度模板例在层下全绿,VUID 0 条。 -## 5. monolith 观察(记给 dev / P13,本包不修) +## 5. 整套门(`~/w7/notes/p8/gate.sh`,树外) + +| 项 | 读数 | 相对第一波集成头(ID-P8-12) | +|---|---|---| +| G1 | 符号增 0 减 0,`.text` `a5ba13` | 不变 | +| census / ratchet / parity / 修订 | 78 / 20 / 45;172;0;5 | 不变 | +| wire-declines | 61 行、66 站点、0 未记日志 | +3 行(`MipmapDepthStencilAspect`、`DispatchIndirect{BufferUnbound,Range}`),+7 站点(新派发函数另复用 4 个 `Compute*` 行) | +| split_coverage | Espryt monolith 706 = 门控 617 + 豁免 89;Magma 706:门控 227、只在信息层 447、豁免 32;missing 0 | +5 例(2 mip + 3 派发),无新豁免行 | +| 车道 | unit 2721;split / spawn / tcp 740 / 654 / 666;Magma 277 / 253 / 220 / 673;gpu monolith / inproc 3709 / 3709,全部 100 % | +5 / +5 / +5;Magma +6 / +5 / +4 / +5;gpu +25 | +| retrace(`2edb2aa8`,只改测试的提交之前) | 230 / 236,只挂已知 6 例 iterationrp | 同第一波 | + +## 6. monolith 观察(记给 dev / P13,本包不修) | 观察 | 位置 | 证据 | |---|---|---| | split 构建的 monolith 臂深度着色器 mip 同样不传 view 类型:设备无深度 BLIT_DST 时,1D 深度链会对 1D 图像建 2D view;Release 下 `MOBILEGL_ASSERT` 是空的 | `VulkanRenderer.cpp:4905`、`:12321` | 推断:lavapipe 原生 blit 深度,主机红不出;monolith 不读 `MGITEST_MAGMA_FORCE_SHADER_MIPMAP` | -## 6. 发现但未修 +## 7. 发现但未修 - `mipmap-shader-format-or-shape` 的明细对深度形状也打印 `missing=COLOR_ATTACHMENT`(1D 深度的真实原因是 `view=0`);是 F 的 `MagmaWireShapeDetail` 口径,未改。 - create 系列 fixture 没有 `glDispatchComputeIndirect`([`D.md`](D.md) §7),item 4 的真实负载收益未测。 +- `gen_pipe_dirty_surface.py` 的「根外写者」扫描读整个 `MobileGL/`(`gen_pipe_dirty_surface.py:394`),集成测试源也在内:派发用例的 fixture 成员曾叫 `m_parameters`,让 NEW_PATCH_STATE 的 shutter 看起来在根外被写,自检否定对照 16(`gen_pipe_dirty_surface.py:1770-1778`)不再触发、门红。本包把成员改名 `m_groupCounts`;扫描口径未改,任何新测试 fixture 用到 GLContext 的成员名都会再撞上。