From 1350031368b489f6e3bac019d8f10102ff1b25fd Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 28 Aug 2026 04:35:40 -0400 Subject: [PATCH] [Fix] (DirectVulkan): count GL_PRIMITIVES_GENERATED for transform-feedback-inactive draws - a bring-up probe measures the silent stream query and reroutes such draws through the dedicated primitives-generated query or a clipping-statistics pool, reported from the Vulkan POST --- CMakeLists.txt | 1 + MobileGL/Config.h | 16 + MobileGL/ConfigLoader.cpp | 1 + .../MG_Backend/DirectVulkan/DirectVulkan.cpp | 11 +- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 312 +++++++++- .../DirectVulkan/Renderer/VulkanRenderer.h | 46 +- MobileGL/MG_Util/SelfTest/DriverPost.cpp | 281 +++++++++ .../PrimitivesGeneratedNoXfbProbe.cpp | 534 ++++++++++++++++++ .../SelfTest/PrimitivesGeneratedNoXfbProbe.h | 239 ++++++++ .../PrimitivesGeneratedNoXfbProbe.tesc | 24 + .../PrimitivesGeneratedNoXfbProbe.tese | 19 + .../PrimitivesGeneratedNoXfbProbe.vert | 22 + .../PrimitivesGeneratedNoXfbProbeSpv.h | 153 +++++ 13 files changed, 1626 insertions(+), 33 deletions(-) create mode 100644 MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp create mode 100644 MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h create mode 100644 MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.tesc create mode 100644 MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.tese create mode 100644 MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.vert create mode 100644 MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbeSpv.h diff --git a/CMakeLists.txt b/CMakeLists.txt index dd109a77..93fa2f2f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -314,6 +314,7 @@ set(SOURCE_FILES MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp MobileGL/MG_Util/SelfTest/DriverPost.cpp MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp + MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp diff --git a/MobileGL/Config.h b/MobileGL/Config.h index f76c4263..51d629b5 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -287,6 +287,22 @@ namespace MobileGL::MG_Config { // negative control that replays the corruption. Costs 2x the memory of the affected // formats where it engages, which is why Auto is probe-gated rather than always-on. QuirkOverride EsprytWidenPacked16Storage = QuirkOverride::Auto; + // MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE: DirectVulkan's GL_PRIMITIVES_GENERATED + // reroute for draws made while transform feedback is INACTIVE. The stream query + // (VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT primitivesNeeded) is defined to count + // them, but a Mali driver - and Mesa lavapipe - answers 0 unless a capture span is + // open, which is exactly the shape the CTS uses to measure the tessellator, so ~29 + // tessellation tests per tree size a capture buffer from the 0 and die on the + // zero-length map. Auto defers to a device probe at renderer bring-up + // (SelfTest::RunPrimitivesGeneratedNoXfbProbe), which measures two substitutes on + // the same capture-less draws and arms the best proven one: the dedicated + // VK_EXT_primitives_generated_query (exact semantics by definition; lavapipe passes + // it, rasterizer discard included), else a clipping-invocations pipeline-statistics + // pool (see the verdict vocabulary for its rasterizer-discard split). ForceOn pins + // the reroute structurally wherever a pool can exist (the arming-observable lane, + // immune to the probe's verdict moving), and ForceOff is the negative control that + // replays the driver's silence. + QuirkOverride MagmaPrimGenQueryReroute = QuirkOverride::Auto; }; extern FeaturesTable Features; } // namespace MobileGL::MG_Config diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 87bcb038..55e99d93 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -205,6 +205,7 @@ namespace MobileGL::MG_ConfigLoader { QueryEnvQuirkOverride("MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION"); features.EsprytWidenPacked16Storage = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE"); + features.MagmaPrimGenQueryReroute = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE"); } inline void InitBackendType() { diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp index 4bcf74f4..06dd0349 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp @@ -1206,6 +1206,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { SharedPtr end; // Kind::Occlusion - pool slots recorded between Begin/End; summed at result time. Vector occlusionSlots; + // Kind::XfbGenerated - reroute-pool slots for the span's XFB-INACTIVE + // draws, where the renderer's reroute is armed (the affected driver's + // stream query counts nothing without an open capture; see + // VulkanRenderer::BeginXfbQueryForDraw). Summed alongside the stream + // slots above, which keep the span's XFB-active draws. + Vector rerouteSlots; // Renderer generation the records were written under (see // g_rendererGeneration). A stale generation resolves as available // with a final zero result: the records' pool indices and frame @@ -1313,7 +1319,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (query->kind == VulkanTimerQuery::Kind::XfbWritten || query->kind == VulkanTimerQuery::Kind::XfbGenerated) { Uint64 primitives = 0; - if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots, + if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots, query->rerouteSlots, query->kind == VulkanTimerQuery::Kind::XfbGenerated, primitives)) { return false; @@ -1377,7 +1383,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { return; } pVulkanRenderer->StopXfbQueryCapture( - query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots); + query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots, + query->rerouteSlots); } BackendQueryHandle BeginOcclusionQuery() { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 310e0027..4727f6e8 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -28,6 +28,7 @@ #include "MG_Util/Converters/MGToVk/TextureEnumConverter.h" #include "MG_Util/Math/HalfFloat.h" #include "MG_Util/Metrics/TextureMetrics.h" +#include "MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h" #include "MG_Util/Texture/PixelStoreProcessor.h" #include #include @@ -3277,6 +3278,16 @@ void main() { vkDestroyQueryPool(m_device, m_xfbQueryPool, nullptr); m_xfbQueryPool = VK_NULL_HANDLE; } + if (m_primGenReroutePool != VK_NULL_HANDLE) { + vkDestroyQueryPool(m_device, m_primGenReroutePool, nullptr); + m_primGenReroutePool = VK_NULL_HANDLE; + } + m_primGenRerouteActiveSlots.clear(); + m_primGenRerouteSlotCursor = 0; + m_primGenRerouteSlotOpen = false; + // Not sticky across renderers: the next bring-up re-decides it (from the + // per-process probe memo, so it re-decides without re-probing). + m_primGenRerouteKind = MG_Util::SelfTest::PrimGenRerouteKind::None; m_bufferManager.Shutdown(); // Device is idle (vkDeviceWaitIdle above); query pools can be destroyed. @@ -11299,7 +11310,7 @@ void main() { VkCommandBuffer& commandBuffer = frame.commandBuffer; const Bool xfbActive = BeginXfbCaptureForDraw(frame); - BeginXfbQueryForDraw(commandBuffer); + BeginXfbQueryForDraw(commandBuffer, xfbActive); const Bool occlusionActive = BeginOcclusionForDraw(commandBuffer); vkCmdDraw(commandBuffer, payload.params.vertexCount, @@ -11385,23 +11396,70 @@ void main() { } s_vkResetQueryPool(m_device, m_xfbQueryPool, 0, kXfbQuerySlots); } + // The reroute pool, on the first GENERATED span that needs it. A creation + // failure disarms rather than failing the capture: the stream path still + // answers (with the driver's defect), which beats answering nothing. + if (kind == 1 && m_primGenRerouteKind != MG_Util::SelfTest::PrimGenRerouteKind::None && + m_primGenReroutePool == VK_NULL_HANDLE) { + VkQueryPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; + poolInfo.queryCount = kXfbQuerySlots; + if (m_primGenRerouteKind == MG_Util::SelfTest::PrimGenRerouteKind::PrimitivesGeneratedExt) { + // The query Vulkan defines for this GL target; counts vertex stream 0 + // when begun with plain vkCmdBeginQuery. + poolInfo.queryType = VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT; + } else { + poolInfo.queryType = VK_QUERY_TYPE_PIPELINE_STATISTICS; + // The clipping-stage INVOCATION counter: one per primitive reaching + // primitive clipping (GL's CLIPPING_INPUT_PRIMITIVES) - post-tess/GS, + // pre-clip, and per spec still counted under rasterizer discard, which + // is exactly the set GL_PRIMITIVES_GENERATED is defined over. The + // stage's OUTPUT count (CLIPPING_PRIMITIVES_BIT) would be wrong: + // clipping may drop or split primitives. + poolInfo.pipelineStatistics = VK_QUERY_PIPELINE_STATISTIC_CLIPPING_INVOCATIONS_BIT; + } + if (vkCreateQueryPool(m_device, &poolInfo, nullptr, &m_primGenReroutePool) != VK_SUCCESS) { + MGLOG_E_ONCE("StartXfbQueryCapture: reroute pool creation failed; the " + "PRIMITIVES_GENERATED reroute is disarmed and XFB-inactive draws keep " + "the stream query"); + m_primGenReroutePool = VK_NULL_HANDLE; + m_primGenRerouteKind = MG_Util::SelfTest::PrimGenRerouteKind::None; + } else { + s_vkResetQueryPool(m_device, m_primGenReroutePool, 0, kXfbQuerySlots); + } + } m_xfbQueryActiveSlots[kind].clear(); m_xfbQueryCaptureActive[kind] = true; + if (kind == 1) { + m_primGenRerouteActiveSlots.clear(); + } return true; } - void VulkanRenderer::StopXfbQueryCapture(Uint32 kind, Vector& outSlots) { + void VulkanRenderer::StopXfbQueryCapture(Uint32 kind, Vector& outSlots, + Vector& outRerouteSlots) { if (kind > 1) { return; } outSlots = Move(m_xfbQueryActiveSlots[kind]); m_xfbQueryActiveSlots[kind].clear(); m_xfbQueryCaptureActive[kind] = false; + outRerouteSlots.clear(); + if (kind == 1) { + outRerouteSlots = Move(m_primGenRerouteActiveSlots); + m_primGenRerouteActiveSlots.clear(); + } } - Bool VulkanRenderer::ResolveXfbQueryResult(const Vector& slots, Bool wantGenerated, Uint64& outPrimitives) { + Bool VulkanRenderer::ResolveXfbQueryResult(const Vector& slots, const Vector& rerouteSlots, + Bool wantGenerated, Uint64& outPrimitives) { outPrimitives = 0; - if (slots.empty() || m_xfbQueryPool == VK_NULL_HANDLE) { + const Bool haveStreamSlots = !slots.empty() && m_xfbQueryPool != VK_NULL_HANDLE; + // Reroute slots only ever accumulate the GENERATED target (see + // BeginXfbQueryForDraw); WRITTEN never opens one. + const Bool haveRerouteSlots = + wantGenerated && !rerouteSlots.empty() && m_primGenReroutePool != VK_NULL_HANDLE; + if (!haveStreamSlots && !haveRerouteSlots) { return true; } auto& frame = m_frameContext.GetCurrent(); @@ -11413,44 +11471,104 @@ void main() { return false; } } - for (const Uint32 slot : slots) { - Uint64 pair[2] = {0, 0}; // {primitivesWritten, primitivesNeeded} - const VkResult result = - vkGetQueryPoolResults(m_device, m_xfbQueryPool, slot, 1, sizeof(pair), pair, sizeof(pair), - VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT); - if (result == VK_SUCCESS) { - outPrimitives += pair[wantGenerated ? 1 : 0]; + if (haveStreamSlots) { + for (const Uint32 slot : slots) { + Uint64 pair[2] = {0, 0}; // {primitivesWritten, primitivesNeeded} + const VkResult result = + vkGetQueryPoolResults(m_device, m_xfbQueryPool, slot, 1, sizeof(pair), pair, sizeof(pair), + VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT); + if (result == VK_SUCCESS) { + outPrimitives += pair[wantGenerated ? 1 : 0]; + } + } + } + if (haveRerouteSlots) { + for (const Uint32 slot : rerouteSlots) { + // Both reroute pool kinds answer one 64-bit primitive count per slot. + Uint64 generated = 0; + const VkResult result = vkGetQueryPoolResults( + m_device, m_primGenReroutePool, slot, 1, sizeof(generated), &generated, + sizeof(generated), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT); + if (result == VK_SUCCESS) { + outPrimitives += generated; + } } } return true; } - void VulkanRenderer::BeginXfbQueryForDraw(VkCommandBuffer commandBuffer) { + void VulkanRenderer::BeginXfbQueryForDraw(VkCommandBuffer commandBuffer, Bool xfbActive) { m_xfbQuerySlotOpen = false; + m_primGenRerouteSlotOpen = false; if ((!m_xfbQueryCaptureActive[0] && !m_xfbQueryCaptureActive[1]) || m_xfbQueryPool == VK_NULL_HANDLE) { return; } - const Uint32 slot = m_xfbQuerySlotCursor; - m_xfbQuerySlotCursor = (m_xfbQuerySlotCursor + 1) % kXfbQuerySlots; - // Slots are never host-reset at read time (both GL targets may reference one - // slot); recycle them here instead. - s_vkResetQueryPool(m_device, m_xfbQueryPool, slot, 1); - s_vkCmdBeginQueryIndexedEXT(commandBuffer, m_xfbQueryPool, slot, 0, 0); - for (Uint32 kind = 0; kind < 2; ++kind) { - if (m_xfbQueryCaptureActive[kind]) { - m_xfbQueryActiveSlots[kind].push_back(slot); + // A draw made while the GL span is merely PAUSED is CPU-accounted by the + // frontend's paused-primitive counter, whose delta the GENERATED resolve adds + // (DirectVulkan.cpp); a reroute slot for it would count it twice. Only a + // draw with no capture AND no paused span is the stream query's silent case. + const Bool glSpanPausedDraw = MG_State::pGLContext != nullptr && + MG_State::pGLContext->IsTransformFeedbackActive() && + MG_State::pGLContext->IsTransformFeedbackPaused(); + const Bool rerouteGenerated = m_xfbQueryCaptureActive[1] && + m_primGenRerouteKind != MG_Util::SelfTest::PrimGenRerouteKind::None && + m_primGenReroutePool != VK_NULL_HANDLE && !xfbActive && + !glSpanPausedDraw; + // The stream slot stays for WRITTEN whatever the reroute does (with capture + // inactive its primitivesWritten is 0, which is the correct WRITTEN answer), + // and for GENERATED wherever this draw is not rerouted - so one GL query span + // may accumulate stream slots (XFB-active draws) and reroute slots + // (XFB-inactive draws) side by side. + const Bool wantStreamSlot = + m_xfbQueryCaptureActive[0] || (m_xfbQueryCaptureActive[1] && !rerouteGenerated); + if (wantStreamSlot) { + const Uint32 slot = m_xfbQuerySlotCursor; + m_xfbQuerySlotCursor = (m_xfbQuerySlotCursor + 1) % kXfbQuerySlots; + // Slots are never host-reset at read time (both GL targets may reference one + // slot); recycle them here instead. + s_vkResetQueryPool(m_device, m_xfbQueryPool, slot, 1); + s_vkCmdBeginQueryIndexedEXT(commandBuffer, m_xfbQueryPool, slot, 0, 0); + if (m_xfbQueryCaptureActive[0]) { + m_xfbQueryActiveSlots[0].push_back(slot); } + if (m_xfbQueryCaptureActive[1] && !rerouteGenerated) { + m_xfbQueryActiveSlots[1].push_back(slot); + } + m_xfbQuerySlotOpen = true; + m_xfbQueryOpenSlot = slot; + } + if (rerouteGenerated) { + // Latched at INFO on purpose: it is the pinned integration lane's arming + // observable (the shape UnlocatedIoBlockScenario asserts), and the builds + // CI runs compile INFO in. + MGLOG_I_ONCE("PRIMITIVES_GENERATED reroute engaged: an XFB-inactive draw accumulates " + "through the %s pool", + m_primGenRerouteKind == + MG_Util::SelfTest::PrimGenRerouteKind::PrimitivesGeneratedExt + ? "VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT" + : "clipping-invocations statistics"); + const Uint32 slot = m_primGenRerouteSlotCursor; + m_primGenRerouteSlotCursor = (m_primGenRerouteSlotCursor + 1) % kXfbQuerySlots; + // Same recycle-at-begin discipline as the stream pool. Both pool kinds + // are begun with plain vkCmdBeginQuery (a PRIMITIVES_GENERATED_EXT + // query begun this way counts vertex stream 0). + s_vkResetQueryPool(m_device, m_primGenReroutePool, slot, 1); + vkCmdBeginQuery(commandBuffer, m_primGenReroutePool, slot, 0); + m_primGenRerouteActiveSlots.push_back(slot); + m_primGenRerouteSlotOpen = true; + m_primGenRerouteOpenSlot = slot; } - m_xfbQuerySlotOpen = true; - m_xfbQueryOpenSlot = slot; } void VulkanRenderer::EndXfbQueryForDraw(VkCommandBuffer commandBuffer) { - if (!m_xfbQuerySlotOpen) { - return; + if (m_xfbQuerySlotOpen) { + s_vkCmdEndQueryIndexedEXT(commandBuffer, m_xfbQueryPool, m_xfbQueryOpenSlot, 0); + m_xfbQuerySlotOpen = false; + } + if (m_primGenRerouteSlotOpen) { + vkCmdEndQuery(commandBuffer, m_primGenReroutePool, m_primGenRerouteOpenSlot); + m_primGenRerouteSlotOpen = false; } - s_vkCmdEndQueryIndexedEXT(commandBuffer, m_xfbQueryPool, m_xfbQueryOpenSlot, 0); - m_xfbQuerySlotOpen = false; } Bool VulkanRenderer::BeginOcclusionForDraw(VkCommandBuffer commandBuffer) { @@ -11500,7 +11618,7 @@ void main() { VkCommandBuffer& commandBuffer = frame.commandBuffer; const Bool xfbActive = BeginXfbCaptureForDraw(frame); - BeginXfbQueryForDraw(commandBuffer); + BeginXfbQueryForDraw(commandBuffer, xfbActive); const Bool occlusionActive = BeginOcclusionForDraw(commandBuffer); vkCmdDrawIndexed(commandBuffer, payload.params.indexCount, @@ -13333,6 +13451,13 @@ void main() { // occlusion result still satisfies any-samples-style consumers. deviceFeatures.occlusionQueryPrecise = supportedDeviceFeatures.occlusionQueryPrecise; m_occlusionQueryPreciseEnabled = deviceFeatures.occlusionQueryPrecise == VK_TRUE; + m_tessellationShaderFeatureEnabled = deviceFeatures.tessellationShader == VK_TRUE; + // Backs the GL_PRIMITIVES_GENERATED reroute's statistics tier (see the + // m_primGenReroute* members): a VK_QUERY_TYPE_PIPELINE_STATISTICS pool may only + // be created with this feature enabled. Enabled wherever the device has it - the + // feature alone costs nothing; pools exist only where the reroute is armed. + deviceFeatures.pipelineStatisticsQuery = supportedDeviceFeatures.pipelineStatisticsQuery; + m_pipelineStatisticsQueryFeatureEnabled = deviceFeatures.pipelineStatisticsQuery == VK_TRUE; VkDeviceCreateInfo deviceCreateInfo{}; deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; @@ -13660,6 +13785,38 @@ void main() { MGLOG_I("Enabled optional device extension: %s", VK_EXT_TRANSFORM_FEEDBACK_EXTENSION_NAME); } } + // VK_EXT_primitives_generated_query - the query Vulkan defines for GL's + // GL_PRIMITIVES_GENERATED precisely because the stream query above needs no + // capture by spec but drivers disagree. Taken with BOTH the base feature and the + // rasterizer-discard feature or not at all: without the latter, a discarding draw + // inside the query is invalid usage, and GL applications toggle discard freely. + // Only the PRIMITIVES_GENERATED reroute consumes it (see ArmPrimGenReroute). + m_primitivesGeneratedQueryFeatureEnabled = false; + m_primitivesGeneratedQueryDiscardFeatureEnabled = false; + VkPhysicalDevicePrimitivesGeneratedQueryFeaturesEXT primitivesGeneratedQueryFeatures{}; + primitivesGeneratedQueryFeatures.sType = + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIMITIVES_GENERATED_QUERY_FEATURES_EXT; + if (IsExtensionSupported(availableExtensions, VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME) && + getPhysicalDeviceFeatures2 != nullptr) { + VkPhysicalDeviceFeatures2 featureQuery{}; + featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; + featureQuery.pNext = &primitivesGeneratedQueryFeatures; + getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery); + if (primitivesGeneratedQueryFeatures.primitivesGeneratedQuery == VK_TRUE && + primitivesGeneratedQueryFeatures.primitivesGeneratedQueryWithRasterizerDiscard == VK_TRUE) { + if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions, + VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME)) { + enabledDeviceExtensions.push_back(VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME); + } + primitivesGeneratedQueryFeatures.primitivesGeneratedQueryWithNonZeroStreams = VK_FALSE; + primitivesGeneratedQueryFeatures.pNext = const_cast(deviceCreateInfo.pNext); + deviceCreateInfo.pNext = &primitivesGeneratedQueryFeatures; + m_primitivesGeneratedQueryFeatureEnabled = true; + m_primitivesGeneratedQueryDiscardFeatureEnabled = true; + MGLOG_I("Enabled optional device extension: %s", + VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME); + } + } // VK_EXT_provoking_vertex. Two independent features live behind one extension: // provokingVertexLast -> flat varyings, gl_Layer/gl_ViewportIndex and // the input-assembler capture order. @@ -14009,6 +14166,105 @@ void main() { m_timerQuerySupported = m_timestampValidBits > 0 && m_timestampPeriodNs > 0.0f; MGLOG_I("Timer queries %s (timestampValidBits=%u, timestampPeriod=%f ns/tick)", m_timerQuerySupported ? "supported" : "not supported", m_timestampValidBits, m_timestampPeriodNs); + + // Last, because it records on m_graphicsQueue: decide the PRIMITIVES_GENERATED + // reroute for XFB-inactive draws. Nothing else has touched the queue yet. + ArmPrimGenReroute(); + } + + void VulkanRenderer::ArmPrimGenReroute() { + using namespace MG_Util::SelfTest; + m_primGenRerouteKind = PrimGenRerouteKind::None; + const MG_Config::QuirkOverride overrideSetting = MG_Config::Features.MagmaPrimGenQueryReroute; + // Without stream queries the GENERATED path never opens a slot at all, so + // there is nothing to reroute - whatever the override says. + if (!m_xfbQueriesSupported || !m_hostQueryResetEnabled) { + return; + } + const Bool primitivesGeneratedQueryUsable = + m_primitivesGeneratedQueryFeatureEnabled && m_primitivesGeneratedQueryDiscardFeatureEnabled; + PrimitivesGeneratedNoXfbVerdict verdict = PrimitivesGeneratedNoXfbVerdict::Inconclusive; + // The probe only matters under Auto (ForceOn bypasses the verdict, ForceOff + // never asks), and the answer is a device property - so it is memoized per + // process rather than re-paid on every renderer recreation. + if (overrideSetting == MG_Config::QuirkOverride::Auto) { + static const PrimitivesGeneratedNoXfbMeasurement s_measurement = [&]() { + PrimitivesGeneratedNoXfbProbeContext probeContext; + probeContext.device = m_device; + probeContext.queue = m_graphicsQueue; + probeContext.queueFamilyIndex = + static_cast(m_physicalDevice.queueFamilies.graphicsFamily); + probeContext.transformFeedbackQueriesUsable = m_xfbQueriesSupported; + probeContext.primitivesGeneratedQueryUsable = primitivesGeneratedQueryUsable; + probeContext.pipelineStatisticsEnabled = m_pipelineStatisticsQueryFeatureEnabled; + probeContext.tessellationEnabled = m_tessellationShaderFeatureEnabled; + auto& fns = probeContext.fns; + fns.vkCreateCommandPool = vkCreateCommandPool; + fns.vkDestroyCommandPool = vkDestroyCommandPool; + fns.vkAllocateCommandBuffers = vkAllocateCommandBuffers; + fns.vkBeginCommandBuffer = vkBeginCommandBuffer; + fns.vkEndCommandBuffer = vkEndCommandBuffer; + fns.vkCreateQueryPool = vkCreateQueryPool; + fns.vkDestroyQueryPool = vkDestroyQueryPool; + fns.vkCmdResetQueryPool = vkCmdResetQueryPool; + fns.vkCmdBeginQuery = vkCmdBeginQuery; + fns.vkCmdEndQuery = vkCmdEndQuery; + fns.vkCmdBeginQueryIndexedEXT = s_vkCmdBeginQueryIndexedEXT; + fns.vkCmdEndQueryIndexedEXT = s_vkCmdEndQueryIndexedEXT; + fns.vkCreateRenderPass = vkCreateRenderPass; + fns.vkDestroyRenderPass = vkDestroyRenderPass; + fns.vkCreateFramebuffer = vkCreateFramebuffer; + fns.vkDestroyFramebuffer = vkDestroyFramebuffer; + fns.vkCmdBeginRenderPass = vkCmdBeginRenderPass; + fns.vkCmdEndRenderPass = vkCmdEndRenderPass; + fns.vkCreateShaderModule = vkCreateShaderModule; + fns.vkDestroyShaderModule = vkDestroyShaderModule; + fns.vkCreatePipelineLayout = vkCreatePipelineLayout; + fns.vkDestroyPipelineLayout = vkDestroyPipelineLayout; + fns.vkCreateGraphicsPipelines = vkCreateGraphicsPipelines; + fns.vkDestroyPipeline = vkDestroyPipeline; + fns.vkCmdBindPipeline = vkCmdBindPipeline; + fns.vkCmdDraw = vkCmdDraw; + fns.vkCreateFence = vkCreateFence; + fns.vkDestroyFence = vkDestroyFence; + fns.vkQueueSubmit = vkQueueSubmit; + fns.vkWaitForFences = vkWaitForFences; + fns.vkGetQueryPoolResults = vkGetQueryPoolResults; + fns.vkDeviceWaitIdle = vkDeviceWaitIdle; + return RunPrimitivesGeneratedNoXfbProbe(probeContext); + }(); + verdict = EvaluatePrimitivesGeneratedNoXfbVerdict(s_measurement); + if (!s_measurement.ran) { + MGLOG_W("PRIMITIVES_GENERATED probe did not run (%s); XFB-inactive draws keep the " + "stream query", s_measurement.failureReason.c_str()); + } else { + const auto logShape = [](const char* name, + const MG_Util::SelfTest::PrimitivesGeneratedNoXfbShapeMeasurement& + shape) { + MGLOG_I("PRIMITIVES_GENERATED probe %s: drawn=%d stream=%llu/%llu pgq=%llu(%d) " + "stat=%llu(%d)", + name, shape.drawn ? 1 : 0, + static_cast(shape.streamGenerated), + static_cast(shape.expectedPrimitives), + static_cast(shape.primitivesGeneratedExt), + shape.primitivesGeneratedExtMeasured ? 1 : 0, + static_cast(shape.statisticsClippingInput), + shape.statisticsMeasured ? 1 : 0); + }; + logShape("triangles", s_measurement.trianglesPlain); + logShape("triangles+discard", s_measurement.trianglesDiscard); + logShape("patches+discard", s_measurement.patchesDiscard); + } + } + m_primGenRerouteKind = ChoosePrimitivesGeneratedReroute( + overrideSetting, verdict, primitivesGeneratedQueryUsable, m_pipelineStatisticsQueryFeatureEnabled); + if (m_primGenRerouteKind != PrimGenRerouteKind::None) { + MGLOG_I("PRIMITIVES_GENERATED for XFB-inactive draws will accumulate through a %s pool%s", + m_primGenRerouteKind == PrimGenRerouteKind::PrimitivesGeneratedExt + ? "VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT" + : "clipping-invocations pipeline-statistics", + overrideSetting == MG_Config::QuirkOverride::ForceOn ? " (forced on)" : ""); + } } void VulkanRenderer::CreateAllocator() { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 27333ca5..67657051 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -24,6 +24,7 @@ #include "MG_Util/Math/VectorTypes.h" #include #include +#include #include #include "../VkIncludes.h" @@ -717,15 +718,54 @@ namespace MobileGL::MG_Backend::DirectVulkan { Vector m_xfbQueryActiveSlots[2]; Bool m_xfbQuerySlotOpen = false; Uint32 m_xfbQueryOpenSlot = 0; + // GL_PRIMITIVES_GENERATED reroute for draws made while transform feedback is + // INACTIVE. The stream pool's primitivesNeeded is defined to count those draws + // too, but a Mali driver (and Mesa lavapipe) answers 0 unless a capture span + // is open (the CTS's tessellator-measuring shape). Where the bring-up probe + // finds that defect with a working control - or + // MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE forces it - such draws accumulate the + // GENERATED count through this pool instead, whose type the arming picks: + // VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT where the device hosts the dedicated + // query with its rasterizer-discard feature (exact semantics by definition - + // the extension exists because GL needs this count without a capture), else a + // VK_QUERY_TYPE_PIPELINE_STATISTICS pool over clipping-stage invocations (one + // per primitive reaching primitive clipping - after every vertex processing + // stage, before rasterizer discard - which is the same set). + // XFB-ACTIVE draws keep the stream slot (exact today, and WRITTEN needs it); + // paused-span draws keep the frontend's CPU accounting (see the paused counter + // in DirectVulkan.cpp) and never open a reroute slot, or they would count + // twice. One GL query span may therefore hold slots of both pools. + Bool m_pipelineStatisticsQueryFeatureEnabled = false; + // VK_EXT_primitives_generated_query: base feature, and the + // ...WithRasterizerDiscard feature without which a discarding draw inside the + // query is invalid usage (so the reroute never picks the dedicated pool on a + // base-only device - GL applications toggle discard freely). + Bool m_primitivesGeneratedQueryFeatureEnabled = false; + Bool m_primitivesGeneratedQueryDiscardFeatureEnabled = false; + // tessellationShader was enabled at device creation (it is taken whenever the + // device advertises it); gates the probe's PATCHES shape. + Bool m_tessellationShaderFeatureEnabled = false; + MG_Util::SelfTest::PrimGenRerouteKind m_primGenRerouteKind = + MG_Util::SelfTest::PrimGenRerouteKind::None; + VkQueryPool m_primGenReroutePool = VK_NULL_HANDLE; + Uint32 m_primGenRerouteSlotCursor = 0; + Vector m_primGenRerouteActiveSlots; + Bool m_primGenRerouteSlotOpen = false; + Uint32 m_primGenRerouteOpenSlot = 0; + // Runs the bring-up probe (memoized per process) and decides + // m_primGenRerouteKind. Called at the end of device creation: it records on + // m_graphicsQueue, which nothing else is using yet. + void ArmPrimGenReroute(); public: // kind: 0 = PRIMITIVES_WRITTEN, 1 = PRIMITIVES_GENERATED. Bool StartXfbQueryCapture(Uint32 kind); - void StopXfbQueryCapture(Uint32 kind, Vector& outSlots); - Bool ResolveXfbQueryResult(const Vector& slots, Bool wantGenerated, Uint64& outPrimitives); + void StopXfbQueryCapture(Uint32 kind, Vector& outSlots, Vector& outRerouteSlots); + Bool ResolveXfbQueryResult(const Vector& slots, const Vector& rerouteSlots, + Bool wantGenerated, Uint64& outPrimitives); private: - void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer); + void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer, Bool xfbActive); void EndXfbQueryForDraw(VkCommandBuffer commandBuffer); VkCommandPool m_commandPool = VK_NULL_HANDLE; diff --git a/MobileGL/MG_Util/SelfTest/DriverPost.cpp b/MobileGL/MG_Util/SelfTest/DriverPost.cpp index c7026f72..3d1fdac9 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPost.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverPost.cpp @@ -9,6 +9,7 @@ #include "DriverPost.h" #include "DriverPostIterationRPWitness.h" #include "DriverPostIterationRPWitnessSpv.h" +#include "PrimitivesGeneratedNoXfbProbe.h" #include "MG_Util/BackendLoaders/OpenGL/Loader.h" #include #include @@ -1555,6 +1556,282 @@ namespace MobileGL::MG_Util::SelfTest { disabledNote); } + // GL_PRIMITIVES_GENERATED for draws made with transform feedback INACTIVE. GL + // defines the query to count them; the DirectVulkan backend serves it from the + // stream query's primitivesNeeded, and an affected Mali driver answers 0 there + // unless a capture span is open - the exact shape the CTS uses to measure the + // tessellator (see PrimitivesGeneratedNoXfbProbe.h). One row: + // PASS - the stream query counts the capture-less draw exactly. + // WARN - it answers 0, and the CLIPPING_INPUT_PRIMITIVES statistics control + // on the same draw answers exactly right, so the renderer substitutes + // a pipeline-statistics pool for such draws (the same probe, run at + // renderer bring-up, is what arms it). + // FAIL (optional) - it answers 0 with no working substitute, or the probe + // could not reach a verdict; applications sizing capture buffers from + // the query get 0. + // Throwaway device on purpose, like every probe here: the row reports the + // driver, not the renderer's configuration - MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE + // steers the renderer, never this row. + void ProbeVulkanPrimitivesGeneratedNoXfb(ReportBuilder& builder, + PFN_vkGetInstanceProcAddr getInstanceProcAddr, + VkInstance instance, VkPhysicalDevice physicalDevice, + Uint32 graphicsQueueFamilyIndex, + const Vector& deviceExtensions, + const VkPhysicalDeviceFeatures& features, + PFN_vkGetPhysicalDeviceFeatures2 getFeatures2, + PFN_vkGetPhysicalDeviceProperties2 getProperties2) { + constexpr const char* RowName = "Primitives-generated query without capture"; + const auto fail = [&](String detail) { builder.FailOptional(RowName, Move(detail)); }; + + if (!HasVkExtension(deviceExtensions, VK_EXT_TRANSFORM_FEEDBACK_EXTENSION_NAME) || + getFeatures2 == nullptr || getProperties2 == nullptr) { + fail("VK_EXT_transform_feedback is unavailable, so the backend has no GPU counter for " + "GL_PRIMITIVES_GENERATED at all - with or without a capture"); + return; + } + VkPhysicalDeviceTransformFeedbackFeaturesEXT xfbFeatures{}; + xfbFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TRANSFORM_FEEDBACK_FEATURES_EXT; + VkPhysicalDeviceFeatures2 features2{}; + features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; + features2.pNext = &xfbFeatures; + getFeatures2(physicalDevice, &features2); + VkPhysicalDeviceTransformFeedbackPropertiesEXT xfbProperties{}; + xfbProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TRANSFORM_FEEDBACK_PROPERTIES_EXT; + VkPhysicalDeviceProperties2 properties2{}; + properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; + properties2.pNext = &xfbProperties; + getProperties2(physicalDevice, &properties2); + if (xfbFeatures.transformFeedback != VK_TRUE || xfbProperties.transformFeedbackQueries != VK_TRUE) { + fail("the device has VK_EXT_transform_feedback but no usable stream queries " + "(transformFeedbackQueries = false); GL_PRIMITIVES_GENERATED and " + "GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN queries answer 0"); + return; + } + + const auto vkCreateDeviceFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkCreateDevice")); + const auto vkDestroyDeviceFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkDestroyDevice")); + const auto vkGetDeviceQueueFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkGetDeviceQueue")); + if (vkCreateDeviceFn == nullptr || vkDestroyDeviceFn == nullptr || vkGetDeviceQueueFn == nullptr) { + fail("vkGetInstanceProcAddr could not resolve the device-creation entry points"); + return; + } + + const Float queuePriority = 1.0f; + VkDeviceQueueCreateInfo queueInfo{}; + queueInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + queueInfo.queueFamilyIndex = graphicsQueueFamilyIndex; + queueInfo.queueCount = 1; + queueInfo.pQueuePriorities = &queuePriority; + + // Only what the probe itself needs: the transform feedback feature (a + // stream-query pool may not be created without it), the two candidate + // substitutes' features, and tessellationShader for the PATCHES shape - + // each only where the device has it. The dedicated + // primitives-generated query is taken with BOTH its bits or not at all, + // mirroring the renderer (without the discard bit two of the three + // shapes may not run inside it). + VkPhysicalDeviceFeatures enabledFeatures{}; + enabledFeatures.pipelineStatisticsQuery = features.pipelineStatisticsQuery; + enabledFeatures.tessellationShader = features.tessellationShader; + VkPhysicalDeviceTransformFeedbackFeaturesEXT enabledXfbFeatures{}; + enabledXfbFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TRANSFORM_FEEDBACK_FEATURES_EXT; + enabledXfbFeatures.transformFeedback = VK_TRUE; + const char* enabledExtensions[2] = {VK_EXT_TRANSFORM_FEEDBACK_EXTENSION_NAME, nullptr}; + Uint32 enabledExtensionCount = 1; + + Bool primitivesGeneratedQueryUsable = false; + VkPhysicalDevicePrimitivesGeneratedQueryFeaturesEXT enabledPgqFeatures{}; + enabledPgqFeatures.sType = + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIMITIVES_GENERATED_QUERY_FEATURES_EXT; + if (HasVkExtension(deviceExtensions, VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME)) { + VkPhysicalDevicePrimitivesGeneratedQueryFeaturesEXT pgqQuery{}; + pgqQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIMITIVES_GENERATED_QUERY_FEATURES_EXT; + VkPhysicalDeviceFeatures2 pgqFeatures2{}; + pgqFeatures2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; + pgqFeatures2.pNext = &pgqQuery; + getFeatures2(physicalDevice, &pgqFeatures2); + if (pgqQuery.primitivesGeneratedQuery == VK_TRUE && + pgqQuery.primitivesGeneratedQueryWithRasterizerDiscard == VK_TRUE) { + primitivesGeneratedQueryUsable = true; + enabledPgqFeatures.primitivesGeneratedQuery = VK_TRUE; + enabledPgqFeatures.primitivesGeneratedQueryWithRasterizerDiscard = VK_TRUE; + enabledPgqFeatures.pNext = &enabledXfbFeatures; + enabledExtensions[enabledExtensionCount++] = + VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME; + } + } + + VkDeviceCreateInfo deviceInfo{}; + deviceInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + deviceInfo.pNext = primitivesGeneratedQueryUsable + ? static_cast(&enabledPgqFeatures) + : static_cast(&enabledXfbFeatures); + deviceInfo.queueCreateInfoCount = 1; + deviceInfo.pQueueCreateInfos = &queueInfo; + deviceInfo.enabledExtensionCount = enabledExtensionCount; + deviceInfo.ppEnabledExtensionNames = enabledExtensions; + deviceInfo.pEnabledFeatures = &enabledFeatures; + + VkDevice device = VK_NULL_HANDLE; + const VkResult createResult = vkCreateDeviceFn(physicalDevice, &deviceInfo, nullptr, &device); + if (createResult != VK_SUCCESS || device == VK_NULL_HANDLE) { + fail(format("vkCreateDevice failed (VkResult = {})", static_cast(createResult))); + return; + } + const ScopeGuard destroyDevice([&]() { vkDestroyDeviceFn(device, nullptr); }); + + VkQueue queue = VK_NULL_HANDLE; + vkGetDeviceQueueFn(device, graphicsQueueFamilyIndex, 0, &queue); + if (queue == VK_NULL_HANDLE) { + fail("vkGetDeviceQueue returned a null graphics queue"); + return; + } + + PrimitivesGeneratedNoXfbProbeContext probeContext; + probeContext.device = device; + probeContext.queue = queue; + probeContext.queueFamilyIndex = graphicsQueueFamilyIndex; + probeContext.transformFeedbackQueriesUsable = true; + probeContext.primitivesGeneratedQueryUsable = primitivesGeneratedQueryUsable; + probeContext.pipelineStatisticsEnabled = enabledFeatures.pipelineStatisticsQuery == VK_TRUE; + probeContext.tessellationEnabled = enabledFeatures.tessellationShader == VK_TRUE; + auto& fns = probeContext.fns; + const auto resolve = [&](const char* name) { return getInstanceProcAddr(instance, name); }; + fns.vkCreateCommandPool = reinterpret_cast(resolve("vkCreateCommandPool")); + fns.vkDestroyCommandPool = + reinterpret_cast(resolve("vkDestroyCommandPool")); + fns.vkAllocateCommandBuffers = + reinterpret_cast(resolve("vkAllocateCommandBuffers")); + fns.vkBeginCommandBuffer = + reinterpret_cast(resolve("vkBeginCommandBuffer")); + fns.vkEndCommandBuffer = reinterpret_cast(resolve("vkEndCommandBuffer")); + fns.vkCreateQueryPool = reinterpret_cast(resolve("vkCreateQueryPool")); + fns.vkDestroyQueryPool = reinterpret_cast(resolve("vkDestroyQueryPool")); + fns.vkCmdResetQueryPool = reinterpret_cast(resolve("vkCmdResetQueryPool")); + fns.vkCmdBeginQuery = reinterpret_cast(resolve("vkCmdBeginQuery")); + fns.vkCmdEndQuery = reinterpret_cast(resolve("vkCmdEndQuery")); + fns.vkCmdBeginQueryIndexedEXT = + reinterpret_cast(resolve("vkCmdBeginQueryIndexedEXT")); + fns.vkCmdEndQueryIndexedEXT = + reinterpret_cast(resolve("vkCmdEndQueryIndexedEXT")); + fns.vkCreateRenderPass = reinterpret_cast(resolve("vkCreateRenderPass")); + fns.vkDestroyRenderPass = + reinterpret_cast(resolve("vkDestroyRenderPass")); + fns.vkCreateFramebuffer = + reinterpret_cast(resolve("vkCreateFramebuffer")); + fns.vkDestroyFramebuffer = + reinterpret_cast(resolve("vkDestroyFramebuffer")); + fns.vkCmdBeginRenderPass = + reinterpret_cast(resolve("vkCmdBeginRenderPass")); + fns.vkCmdEndRenderPass = reinterpret_cast(resolve("vkCmdEndRenderPass")); + fns.vkCreateShaderModule = + reinterpret_cast(resolve("vkCreateShaderModule")); + fns.vkDestroyShaderModule = + reinterpret_cast(resolve("vkDestroyShaderModule")); + fns.vkCreatePipelineLayout = + reinterpret_cast(resolve("vkCreatePipelineLayout")); + fns.vkDestroyPipelineLayout = + reinterpret_cast(resolve("vkDestroyPipelineLayout")); + fns.vkCreateGraphicsPipelines = + reinterpret_cast(resolve("vkCreateGraphicsPipelines")); + fns.vkDestroyPipeline = reinterpret_cast(resolve("vkDestroyPipeline")); + fns.vkCmdBindPipeline = reinterpret_cast(resolve("vkCmdBindPipeline")); + fns.vkCmdDraw = reinterpret_cast(resolve("vkCmdDraw")); + fns.vkCreateFence = reinterpret_cast(resolve("vkCreateFence")); + fns.vkDestroyFence = reinterpret_cast(resolve("vkDestroyFence")); + fns.vkQueueSubmit = reinterpret_cast(resolve("vkQueueSubmit")); + fns.vkWaitForFences = reinterpret_cast(resolve("vkWaitForFences")); + fns.vkGetQueryPoolResults = + reinterpret_cast(resolve("vkGetQueryPoolResults")); + fns.vkDeviceWaitIdle = reinterpret_cast(resolve("vkDeviceWaitIdle")); + + const PrimitivesGeneratedNoXfbMeasurement measurement = + RunPrimitivesGeneratedNoXfbProbe(probeContext); + if (!measurement.ran) { + fail(format("the probe could not run ({}); the renderer's bring-up probe decides the " + "reroute independently", + measurement.failureReason)); + return; + } + + const auto shapeFacts = [](const char* name, + const PrimitivesGeneratedNoXfbShapeMeasurement& shape) { + if (!shape.drawn) { + return format("{} not drawn (no tessellationShader)", name); + } + String facts = format("{}: stream answered {} of {} expected", name, shape.streamGenerated, + shape.expectedPrimitives); + if (shape.primitivesGeneratedExtMeasured) { + facts += format(", dedicated query answered {}", shape.primitivesGeneratedExt); + } + if (shape.statisticsMeasured) { + facts += format(", statistics control answered {}", shape.statisticsClippingInput); + } + if (!shape.primitivesGeneratedExtMeasured && !shape.statisticsMeasured) { + facts += ", no control (neither VK_EXT_primitives_generated_query with its " + "discard feature nor pipelineStatisticsQuery is available)"; + } + return facts; + }; + const String facts = shapeFacts("triangles", measurement.trianglesPlain) + "; " + + shapeFacts("triangles under discard", measurement.trianglesDiscard) + + "; " + shapeFacts("patches under discard", measurement.patchesDiscard); + + switch (EvaluatePrimitivesGeneratedNoXfbVerdict(measurement)) { + case PrimitivesGeneratedNoXfbVerdict::StreamCounts: + builder.Pass(RowName, + "the stream query counts a draw made with no capture span open, as " + "VK_EXT_transform_feedback defines (" + + facts + ")"); + return; + case PrimitivesGeneratedNoXfbVerdict::PrimitivesGeneratedExtSubstitute: + builder.Warn(RowName, + "the stream query answers 0 for a draw made with no capture span open - " + "the shape the CTS measures the tessellator with - while a " + "VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT query around an identical replay answers " + "exactly right, rasterizer discard included, so the renderer " + "accumulates GL_PRIMITIVES_GENERATED for such draws through the " + "dedicated query instead (one extra query slot per XFB-inactive draw " + "inside a GENERATED span; " + + facts + ")"); + return; + case PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute: + builder.Warn(RowName, + "the stream query answers 0 for a draw made with no capture span open - " + "the shape the CTS measures the tessellator with - while a " + "clipping-invocations statistics query around an identical replay answers exactly " + "right, rasterizer discard included, so the renderer accumulates " + "GL_PRIMITIVES_GENERATED for such draws through a pipeline-statistics " + "pool instead (one extra query slot per XFB-inactive draw inside a " + "GENERATED span; " + + facts + ")"); + return; + case PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly: + fail("the stream query answers 0 for a draw made with no capture span open, and the " + "clipping-invocations statistics substitute counts the plain draw exactly but " + "reads 0 under rasterizer discard - so the renderer reroutes XFB-inactive draws " + "(repairing undiscarded queries at no cost to the rest) and the CTS's " + "tessellator-measuring shape, which needs the discarded count, remains broken " + "on this driver (" + + facts + ")"); + return; + case PrimitivesGeneratedNoXfbVerdict::Unfixable: + fail("the stream query answers 0 for a draw made with no capture span open and the " + "device offers no working statistics substitute; an application sizing a capture " + "buffer from GL_PRIMITIVES_GENERATED gets 0 (" + + facts + ")"); + return; + case PrimitivesGeneratedNoXfbVerdict::Inconclusive: + break; + } + fail("the probe reached no verdict - the answers fit neither the defect nor health, and " + "MobileGL declines to repair a driver it does not understand (" + + facts + ")"); + } + // Native iterationRP compute witness. This deliberately uses a separate // throwaway Vulkan device rather than the real renderer's queues, and it // treats MOBILEGL_MAGMA_DISABLE_SUBGROUP as irrelevant: the row reports what the @@ -2652,6 +2929,10 @@ namespace MobileGL::MG_Util::SelfTest { ProbeVulkanIterationRPWitness(builder, getInstanceProcAddr, instance, physicalDevice, computeQueueFamilyIndex, properties, subgroupPropertiesAvailable, subgroupProperties); + ProbeVulkanPrimitivesGeneratedNoXfb(builder, getInstanceProcAddr, instance, physicalDevice, + graphicsQueueFamilyIndex, deviceExtensions, features, + vkGetPhysicalDeviceFeatures2Fn, vkGetPhysicalDeviceProperties2Fn); + if (HasVkExtension(deviceExtensions, VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME)) { builder.Pass("VK_KHR_draw_indirect_count", "supported (count-buffer indirect draws run as single native " diff --git a/MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp b/MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp new file mode 100644 index 00000000..e3b85159 --- /dev/null +++ b/MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp @@ -0,0 +1,534 @@ +// MobileGL - MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.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 +// +// See the header for what is being measured and why. The plumbing here is shaped +// like the POST timestamp probe (DriverPost.cpp, ProbeVulkanTimerQuery): one +// throwaway command buffer, a bounded fence wait that deliberately leaks the +// device objects rather than idle-wait a hung GPU, and teardown on every path. + +#include "PrimitivesGeneratedNoXfbProbe.h" +#include "PrimitivesGeneratedNoXfbProbeSpv.h" + +namespace MobileGL::MG_Util::SelfTest { + namespace { + template + struct ProbeScopeGuard { + explicit ProbeScopeGuard(Callable callable) : onExit(Move(callable)) {} + ProbeScopeGuard(const ProbeScopeGuard&) = delete; + ProbeScopeGuard& operator=(const ProbeScopeGuard&) = delete; + ~ProbeScopeGuard() { onExit(); } + + private: + Callable onExit; + }; + + Bool AllRequiredFnsPresent(const PrimitivesGeneratedNoXfbProbeFns& fns) { + return fns.vkCreateCommandPool != nullptr && fns.vkDestroyCommandPool != nullptr && + fns.vkAllocateCommandBuffers != nullptr && fns.vkBeginCommandBuffer != nullptr && + fns.vkEndCommandBuffer != nullptr && fns.vkCreateQueryPool != nullptr && + fns.vkDestroyQueryPool != nullptr && fns.vkCmdResetQueryPool != nullptr && + fns.vkCmdBeginQuery != nullptr && fns.vkCmdEndQuery != nullptr && + fns.vkCmdBeginQueryIndexedEXT != nullptr && fns.vkCmdEndQueryIndexedEXT != nullptr && + fns.vkCreateRenderPass != nullptr && fns.vkDestroyRenderPass != nullptr && + fns.vkCreateFramebuffer != nullptr && fns.vkDestroyFramebuffer != nullptr && + fns.vkCmdBeginRenderPass != nullptr && fns.vkCmdEndRenderPass != nullptr && + fns.vkCreateShaderModule != nullptr && fns.vkDestroyShaderModule != nullptr && + fns.vkCreatePipelineLayout != nullptr && fns.vkDestroyPipelineLayout != nullptr && + fns.vkCreateGraphicsPipelines != nullptr && fns.vkDestroyPipeline != nullptr && + fns.vkCmdBindPipeline != nullptr && fns.vkCmdDraw != nullptr && + fns.vkCreateFence != nullptr && fns.vkDestroyFence != nullptr && + fns.vkQueueSubmit != nullptr && fns.vkWaitForFences != nullptr && + fns.vkGetQueryPoolResults != nullptr && fns.vkDeviceWaitIdle != nullptr; + } + } // namespace + + PrimitivesGeneratedNoXfbMeasurement RunPrimitivesGeneratedNoXfbProbe( + const PrimitivesGeneratedNoXfbProbeContext& context) { + PrimitivesGeneratedNoXfbMeasurement measurement; + const auto fail = [&](String reason) { + measurement.ran = false; + measurement.failureReason = Move(reason); + return measurement; + }; + + if (!context.transformFeedbackQueriesUsable) { + return fail("transform feedback stream queries are not usable on this device, so the " + "probe has no subject"); + } + if (context.device == VK_NULL_HANDLE || context.queue == VK_NULL_HANDLE) { + return fail("no device/queue was supplied"); + } + const PrimitivesGeneratedNoXfbProbeFns& fns = context.fns; + if (!AllRequiredFnsPresent(fns)) { + return fail("a required Vulkan entry point was not resolved"); + } + + VkDevice device = context.device; + + // Slot i of each pool belongs to shape i (0 = triangles plain, 1 = triangles + // under discard, 2 = patches under discard). Unused slots are reset either + // way; reset needs no feature and an unqueried reset slot is never read. + constexpr Uint32 kShapeSlots = 3; + const Bool drawPatches = context.tessellationEnabled; + const Bool measureStatistics = context.pipelineStatisticsEnabled; + // Only with BOTH feature bits: without ...WithRasterizerDiscard, a + // discarding draw inside the query is invalid usage + // (VUID-vkCmdDraw-primitivesGeneratedQueryWithRasterizerDiscard-06708), + // and two of the three shapes discard. + const Bool measurePrimitivesGeneratedExt = context.primitivesGeneratedQueryUsable; + + VkCommandPool commandPool = VK_NULL_HANDLE; + VkQueryPool streamQueryPool = VK_NULL_HANDLE; + VkQueryPool primitivesGeneratedQueryPool = VK_NULL_HANDLE; + VkQueryPool statisticsQueryPool = VK_NULL_HANDLE; + VkRenderPass renderPass = VK_NULL_HANDLE; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + VkShaderModule vertModule = VK_NULL_HANDLE; + VkShaderModule tescModule = VK_NULL_HANDLE; + VkShaderModule teseModule = VK_NULL_HANDLE; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkPipeline trianglePlainPipeline = VK_NULL_HANDLE; + VkPipeline triangleDiscardPipeline = VK_NULL_HANDLE; + VkPipeline patchDiscardPipeline = VK_NULL_HANDLE; + VkFence fence = VK_NULL_HANDLE; + Bool fenceWaitTimedOut = false; + + // Teardown on every path. When the fence wait timed out the submission may + // still be executing on a hung GPU: vkDeviceWaitIdle could block forever + // and destroying in-flight objects is undefined, so everything is + // deliberately leaked - a hung GPU must not hang the caller. + const ProbeScopeGuard teardown([&]() { + if (fenceWaitTimedOut) { + return; + } + fns.vkDeviceWaitIdle(device); + if (fence != VK_NULL_HANDLE) fns.vkDestroyFence(device, fence, nullptr); + if (trianglePlainPipeline != VK_NULL_HANDLE) + fns.vkDestroyPipeline(device, trianglePlainPipeline, nullptr); + if (triangleDiscardPipeline != VK_NULL_HANDLE) + fns.vkDestroyPipeline(device, triangleDiscardPipeline, nullptr); + if (patchDiscardPipeline != VK_NULL_HANDLE) + fns.vkDestroyPipeline(device, patchDiscardPipeline, nullptr); + if (pipelineLayout != VK_NULL_HANDLE) fns.vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + if (vertModule != VK_NULL_HANDLE) fns.vkDestroyShaderModule(device, vertModule, nullptr); + if (tescModule != VK_NULL_HANDLE) fns.vkDestroyShaderModule(device, tescModule, nullptr); + if (teseModule != VK_NULL_HANDLE) fns.vkDestroyShaderModule(device, teseModule, nullptr); + if (framebuffer != VK_NULL_HANDLE) fns.vkDestroyFramebuffer(device, framebuffer, nullptr); + if (renderPass != VK_NULL_HANDLE) fns.vkDestroyRenderPass(device, renderPass, nullptr); + if (statisticsQueryPool != VK_NULL_HANDLE) fns.vkDestroyQueryPool(device, statisticsQueryPool, nullptr); + if (primitivesGeneratedQueryPool != VK_NULL_HANDLE) + fns.vkDestroyQueryPool(device, primitivesGeneratedQueryPool, nullptr); + if (streamQueryPool != VK_NULL_HANDLE) fns.vkDestroyQueryPool(device, streamQueryPool, nullptr); + if (commandPool != VK_NULL_HANDLE) fns.vkDestroyCommandPool(device, commandPool, nullptr); + }); + + VkCommandPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + poolInfo.queueFamilyIndex = context.queueFamilyIndex; + if (fns.vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) { + return fail("vkCreateCommandPool failed"); + } + + VkCommandBufferAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + allocInfo.commandPool = commandPool; + allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocInfo.commandBufferCount = 1; + VkCommandBuffer commandBuffer = VK_NULL_HANDLE; + if (fns.vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer) != VK_SUCCESS) { + return fail("vkAllocateCommandBuffers failed"); + } + + VkQueryPoolCreateInfo streamPoolInfo{}; + streamPoolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; + streamPoolInfo.queryType = VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT; + streamPoolInfo.queryCount = kShapeSlots; + if (fns.vkCreateQueryPool(device, &streamPoolInfo, nullptr, &streamQueryPool) != VK_SUCCESS) { + return fail("vkCreateQueryPool(TRANSFORM_FEEDBACK_STREAM) failed"); + } + if (measurePrimitivesGeneratedExt) { + VkQueryPoolCreateInfo pgqPoolInfo{}; + pgqPoolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; + pgqPoolInfo.queryType = VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT; + pgqPoolInfo.queryCount = kShapeSlots; + if (fns.vkCreateQueryPool(device, &pgqPoolInfo, nullptr, &primitivesGeneratedQueryPool) != + VK_SUCCESS) { + return fail("vkCreateQueryPool(PRIMITIVES_GENERATED_EXT) failed"); + } + } + if (measureStatistics) { + VkQueryPoolCreateInfo statPoolInfo{}; + statPoolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; + statPoolInfo.queryType = VK_QUERY_TYPE_PIPELINE_STATISTICS; + statPoolInfo.queryCount = kShapeSlots; + // CLIPPING_INVOCATIONS counts the primitives PROCESSED BY (i.e. reaching) + // primitive clipping - GL's CLIPPING_INPUT_PRIMITIVES - which is the + // pre-clip, post-vertex-processing set PRIMITIVES_GENERATED is defined + // over. CLIPPING_PRIMITIVES (the stage's OUTPUT count) would be wrong: + // clipping may drop or split primitives. + statPoolInfo.pipelineStatistics = VK_QUERY_PIPELINE_STATISTIC_CLIPPING_INVOCATIONS_BIT; + if (fns.vkCreateQueryPool(device, &statPoolInfo, nullptr, &statisticsQueryPool) != VK_SUCCESS) { + return fail("vkCreateQueryPool(PIPELINE_STATISTICS) failed"); + } + } + + // Zero-attachment render pass + 1x1 framebuffer: the draw is discarded + // before rasterization, nothing is ever written, but vkCmdDraw needs a + // render pass instance to live in. + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + VkRenderPassCreateInfo renderPassInfo{}; + renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + renderPassInfo.subpassCount = 1; + renderPassInfo.pSubpasses = &subpass; + if (fns.vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) { + return fail("vkCreateRenderPass failed"); + } + VkFramebufferCreateInfo framebufferInfo{}; + framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + framebufferInfo.renderPass = renderPass; + framebufferInfo.width = 1; + framebufferInfo.height = 1; + framebufferInfo.layers = 1; + if (fns.vkCreateFramebuffer(device, &framebufferInfo, nullptr, &framebuffer) != VK_SUCCESS) { + return fail("vkCreateFramebuffer failed"); + } + + const auto makeModule = [&](const std::uint32_t* words, std::size_t wordCount, VkShaderModule& out) { + VkShaderModuleCreateInfo moduleInfo{}; + moduleInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + moduleInfo.codeSize = wordCount * sizeof(std::uint32_t); + moduleInfo.pCode = words; + return fns.vkCreateShaderModule(device, &moduleInfo, nullptr, &out) == VK_SUCCESS; + }; + if (!makeModule(kPrimitivesGeneratedNoXfbProbeVertSpv, kPrimitivesGeneratedNoXfbProbeVertSpvWordCount, + vertModule)) { + return fail("vkCreateShaderModule(vert) failed"); + } + if (drawPatches) { + if (!makeModule(kPrimitivesGeneratedNoXfbProbeTescSpv, kPrimitivesGeneratedNoXfbProbeTescSpvWordCount, + tescModule) || + !makeModule(kPrimitivesGeneratedNoXfbProbeTeseSpv, kPrimitivesGeneratedNoXfbProbeTeseSpvWordCount, + teseModule)) { + return fail("vkCreateShaderModule(tesc/tese) failed"); + } + } + + VkPipelineLayoutCreateInfo layoutInfo{}; + layoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + if (fns.vkCreatePipelineLayout(device, &layoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) { + return fail("vkCreatePipelineLayout failed"); + } + + // With rasterizerDiscardEnable the viewport and multisample state are + // ignored by the spec, but well-formed ones are supplied anyway: the probe + // must never be the thing that trips a picky driver. The discard-off + // variant rasterizes into the zero-attachment subpass, which writes + // nothing anywhere. + const auto makePipeline = [&](Bool tessellated, Bool rasterizerDiscard, VkPipeline& out) { + VkPipelineShaderStageCreateInfo stages[3] = {}; + Uint32 stageCount = 0; + stages[stageCount].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + stages[stageCount].stage = VK_SHADER_STAGE_VERTEX_BIT; + stages[stageCount].module = vertModule; + stages[stageCount].pName = "main"; + ++stageCount; + if (tessellated) { + stages[stageCount].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + stages[stageCount].stage = VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT; + stages[stageCount].module = tescModule; + stages[stageCount].pName = "main"; + ++stageCount; + stages[stageCount].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + stages[stageCount].stage = VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT; + stages[stageCount].module = teseModule; + stages[stageCount].pName = "main"; + ++stageCount; + } + + VkPipelineVertexInputStateCreateInfo vertexInput{}; + vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; + + VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; + inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; + inputAssembly.topology = + tessellated ? VK_PRIMITIVE_TOPOLOGY_PATCH_LIST : VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + + VkPipelineTessellationStateCreateInfo tessellation{}; + tessellation.sType = VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_STATE_CREATE_INFO; + tessellation.patchControlPoints = 1; + + VkViewport viewport{}; + viewport.width = 1.0f; + viewport.height = 1.0f; + viewport.maxDepth = 1.0f; + VkRect2D scissor{}; + scissor.extent.width = 1; + scissor.extent.height = 1; + VkPipelineViewportStateCreateInfo viewportState{}; + viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; + viewportState.viewportCount = 1; + viewportState.pViewports = &viewport; + viewportState.scissorCount = 1; + viewportState.pScissors = &scissor; + + VkPipelineRasterizationStateCreateInfo rasterization{}; + rasterization.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + rasterization.rasterizerDiscardEnable = rasterizerDiscard ? VK_TRUE : VK_FALSE; + rasterization.polygonMode = VK_POLYGON_MODE_FILL; + rasterization.cullMode = VK_CULL_MODE_NONE; + rasterization.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + rasterization.lineWidth = 1.0f; + + VkPipelineMultisampleStateCreateInfo multisample{}; + multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; + multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + + VkGraphicsPipelineCreateInfo pipelineInfo{}; + pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; + pipelineInfo.stageCount = stageCount; + pipelineInfo.pStages = stages; + pipelineInfo.pVertexInputState = &vertexInput; + pipelineInfo.pInputAssemblyState = &inputAssembly; + pipelineInfo.pTessellationState = tessellated ? &tessellation : nullptr; + pipelineInfo.pViewportState = &viewportState; + pipelineInfo.pRasterizationState = &rasterization; + pipelineInfo.pMultisampleState = &multisample; + pipelineInfo.layout = pipelineLayout; + pipelineInfo.renderPass = renderPass; + pipelineInfo.subpass = 0; + return fns.vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &out) == + VK_SUCCESS; + }; + if (!makePipeline(false, false, trianglePlainPipeline)) { + return fail("vkCreateGraphicsPipelines(triangles) failed"); + } + if (!makePipeline(false, true, triangleDiscardPipeline)) { + return fail("vkCreateGraphicsPipelines(triangles, discard) failed"); + } + if (drawPatches && !makePipeline(true, true, patchDiscardPipeline)) { + return fail("vkCreateGraphicsPipelines(patches, discard) failed"); + } + + VkCommandBufferBeginInfo beginInfo{}; + beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + if (fns.vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) { + return fail("vkBeginCommandBuffer failed"); + } + fns.vkCmdResetQueryPool(commandBuffer, streamQueryPool, 0, kShapeSlots); + if (measurePrimitivesGeneratedExt) { + fns.vkCmdResetQueryPool(commandBuffer, primitivesGeneratedQueryPool, 0, kShapeSlots); + } + if (measureStatistics) { + fns.vkCmdResetQueryPool(commandBuffer, statisticsQueryPool, 0, kShapeSlots); + } + + VkRenderPassBeginInfo renderPassBegin{}; + renderPassBegin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + renderPassBegin.renderPass = renderPass; + renderPassBegin.framebuffer = framebuffer; + renderPassBegin.renderArea.extent.width = 1; + renderPassBegin.renderArea.extent.height = 1; + fns.vkCmdBeginRenderPass(commandBuffer, &renderPassBegin, VK_SUBPASS_CONTENTS_INLINE); + + // Each query kind wraps ITS OWN replay of the shape's draw, never a shared + // one. Not pedantry - a co-active control CONTAMINATES the subject: + // measured on lavapipe, a dedicated primitives-generated query active + // around the same draw switches llvmpipe's primitive collection on, and + // the stream query on that draw then answers the exact count it answers 0 + // for when it is alone - which is how the renderer actually runs it. A + // probe that measured them together certified this driver healthy and + // repaired nothing. The replays are identical recordings of a + // deterministic draw, so the per-shape comparison loses nothing. + const auto recordShape = [&](Uint32 slot, VkPipeline pipeline, Uint32 vertexCount) { + fns.vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + // THE SUBJECT, alone: no vkCmdBeginTransformFeedbackEXT anywhere in + // this command buffer - the stream query wraps a draw with transform + // feedback inactive, exactly the CTS's tessellator-measuring shape. + fns.vkCmdBeginQueryIndexedEXT(commandBuffer, streamQueryPool, slot, 0, 0); + fns.vkCmdDraw(commandBuffer, vertexCount, 1, 0, 0); + fns.vkCmdEndQueryIndexedEXT(commandBuffer, streamQueryPool, slot, 0); + if (measurePrimitivesGeneratedExt) { + // Plain vkCmdBeginQuery: a PRIMITIVES_GENERATED_EXT query begun + // this way counts vertex stream 0, which is where every non-GS + // (and default-stream GS) primitive goes. + fns.vkCmdBeginQuery(commandBuffer, primitivesGeneratedQueryPool, slot, 0); + fns.vkCmdDraw(commandBuffer, vertexCount, 1, 0, 0); + fns.vkCmdEndQuery(commandBuffer, primitivesGeneratedQueryPool, slot); + } + if (measureStatistics) { + fns.vkCmdBeginQuery(commandBuffer, statisticsQueryPool, slot, 0); + fns.vkCmdDraw(commandBuffer, vertexCount, 1, 0, 0); + fns.vkCmdEndQuery(commandBuffer, statisticsQueryPool, slot); + } + }; + recordShape(0, trianglePlainPipeline, 3); // one rasterized triangle + recordShape(1, triangleDiscardPipeline, 3); // one discarded triangle + if (drawPatches) { + // one 1-vertex patch -> one tessellated, discarded triangle + recordShape(2, patchDiscardPipeline, 1); + } + fns.vkCmdEndRenderPass(commandBuffer); + if (fns.vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) { + return fail("vkEndCommandBuffer failed"); + } + + VkFenceCreateInfo fenceInfo{}; + fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + if (fns.vkCreateFence(device, &fenceInfo, nullptr, &fence) != VK_SUCCESS) { + return fail("vkCreateFence failed"); + } + VkSubmitInfo submitInfo{}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &commandBuffer; + if (fns.vkQueueSubmit(context.queue, 1, &submitInfo, fence) != VK_SUCCESS) { + return fail("vkQueueSubmit failed"); + } + constexpr Uint64 kFenceTimeoutNs = 5'000'000'000ull; // a probe must never hang its caller + if (fns.vkWaitForFences(device, 1, &fence, VK_TRUE, kFenceTimeoutNs) != VK_SUCCESS) { + fenceWaitTimedOut = true; // see the scope guard + return fail("the probe submission did not complete within 5 s"); + } + + const auto readShape = [&](Uint32 slot, Uint64 expected, PrimitivesGeneratedNoXfbShapeMeasurement& out) { + Uint64 streamPair[2] = {0, 0}; // {primitivesWritten, primitivesNeeded} + if (fns.vkGetQueryPoolResults(device, streamQueryPool, slot, 1, sizeof(streamPair), streamPair, + sizeof(streamPair), + VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT) != VK_SUCCESS) { + return false; + } + out.drawn = true; + out.expectedPrimitives = expected; + out.streamGenerated = streamPair[1]; + if (measurePrimitivesGeneratedExt) { + Uint64 generated = 0; + if (fns.vkGetQueryPoolResults(device, primitivesGeneratedQueryPool, slot, 1, sizeof(generated), + &generated, sizeof(generated), + VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT) == VK_SUCCESS) { + out.primitivesGeneratedExtMeasured = true; + out.primitivesGeneratedExt = generated; + } + } + if (measureStatistics) { + Uint64 clippingInput = 0; + if (fns.vkGetQueryPoolResults(device, statisticsQueryPool, slot, 1, sizeof(clippingInput), + &clippingInput, sizeof(clippingInput), + VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT) == VK_SUCCESS) { + out.statisticsMeasured = true; + out.statisticsClippingInput = clippingInput; + } + } + return true; + }; + if (!readShape(0, 1, measurement.trianglesPlain)) { + return fail("vkGetQueryPoolResults(triangles) failed"); + } + if (!readShape(1, 1, measurement.trianglesDiscard)) { + return fail("vkGetQueryPoolResults(triangles, discard) failed"); + } + if (drawPatches && !readShape(2, 1, measurement.patchesDiscard)) { + return fail("vkGetQueryPoolResults(patches, discard) failed"); + } + + measurement.ran = true; + return measurement; + } + + PrimitivesGeneratedNoXfbVerdict EvaluatePrimitivesGeneratedNoXfbVerdict( + const PrimitivesGeneratedNoXfbMeasurement& measurement) { + if (!measurement.ran || !measurement.trianglesPlain.drawn || !measurement.trianglesDiscard.drawn) { + return PrimitivesGeneratedNoXfbVerdict::Inconclusive; + } + const PrimitivesGeneratedNoXfbShapeMeasurement* shapes[3] = {&measurement.trianglesPlain, + &measurement.trianglesDiscard, + &measurement.patchesDiscard}; + Bool anyStreamSilent = false; + Bool allStreamExact = true; + Bool allPrimitivesGeneratedExtExact = true; + Bool allStatisticsExact = true; + for (const auto* shape : shapes) { + if (!shape->drawn) { + continue; + } + if (shape->streamGenerated == 0) { + anyStreamSilent = true; + } + if (shape->streamGenerated != shape->expectedPrimitives) { + allStreamExact = false; + // A nonzero wrong answer is neither the defect nor health: refuse + // a verdict rather than repair a driver the probe does not + // understand. + if (shape->streamGenerated != 0) { + return PrimitivesGeneratedNoXfbVerdict::Inconclusive; + } + } + if (!shape->primitivesGeneratedExtMeasured || + shape->primitivesGeneratedExt != shape->expectedPrimitives) { + allPrimitivesGeneratedExtExact = false; + } + if (!shape->statisticsMeasured || + shape->statisticsClippingInput != shape->expectedPrimitives) { + allStatisticsExact = false; + } + } + if (allStreamExact) { + return PrimitivesGeneratedNoXfbVerdict::StreamCounts; + } + // At this point at least one drawn shape answered exactly 0. + MOBILEGL_ASSERT(anyStreamSilent, "verdict fell through with no silent shape"); + if (allPrimitivesGeneratedExtExact) { + return PrimitivesGeneratedNoXfbVerdict::PrimitivesGeneratedExtSubstitute; + } + if (allStatisticsExact) { + return PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute; + } + const auto& plain = measurement.trianglesPlain; + const Bool plainStatisticsExact = + plain.statisticsMeasured && plain.statisticsClippingInput == plain.expectedPrimitives; + return plainStatisticsExact ? PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly + : PrimitivesGeneratedNoXfbVerdict::Unfixable; + } + + PrimGenRerouteKind ChoosePrimitivesGeneratedReroute(MG_Config::QuirkOverride overrideSetting, + PrimitivesGeneratedNoXfbVerdict verdict, + Bool primitivesGeneratedQueryUsable, + Bool pipelineStatisticsEnabled) { + switch (overrideSetting) { + case MG_Config::QuirkOverride::ForceOff: + return PrimGenRerouteKind::None; + case MG_Config::QuirkOverride::ForceOn: + // ForceOn bypasses the device verdict, never the structural checks: + // without a hostable pool there is nothing to route through. The + // dedicated query wins where both exist - its semantics are the GL + // target's by definition. + if (primitivesGeneratedQueryUsable) { + return PrimGenRerouteKind::PrimitivesGeneratedExt; + } + return pipelineStatisticsEnabled ? PrimGenRerouteKind::ClippingStatistics + : PrimGenRerouteKind::None; + case MG_Config::QuirkOverride::Auto: + break; + } + switch (verdict) { + case PrimitivesGeneratedNoXfbVerdict::PrimitivesGeneratedExtSubstitute: + return primitivesGeneratedQueryUsable ? PrimGenRerouteKind::PrimitivesGeneratedExt + : PrimGenRerouteKind::None; + case PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute: + case PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly: + return pipelineStatisticsEnabled ? PrimGenRerouteKind::ClippingStatistics + : PrimGenRerouteKind::None; + case PrimitivesGeneratedNoXfbVerdict::Inconclusive: + case PrimitivesGeneratedNoXfbVerdict::StreamCounts: + case PrimitivesGeneratedNoXfbVerdict::Unfixable: + break; + } + return PrimGenRerouteKind::None; + } +} // namespace MobileGL::MG_Util::SelfTest diff --git a/MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h b/MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h new file mode 100644 index 00000000..5b96e20c --- /dev/null +++ b/MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h @@ -0,0 +1,239 @@ +// MobileGL - MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.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 + +#pragma once +#include +#include + +namespace MobileGL::MG_Util::SelfTest { + // ============ PRIMITIVES GENERATED WITHOUT TRANSFORM FEEDBACK ============ + // + // GL_PRIMITIVES_GENERATED counts what the last vertex processing stage emits + // whether or not a transform feedback capture is active (GL 4.6 core 13.4), and + // the DirectVulkan backend serves it from the second result + // (primitivesNeeded) of a VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT pool + // slot wrapped around each draw. VK_EXT_transform_feedback defines that value + // as the primitives the vertex stream produced, capture or no capture - but a + // Mali driver (G1-Ultra, observed against the gl44/gl45/gl46 CTS) answers 0 + // for every draw made while no vkCmdBeginTransformFeedbackEXT span is open, + // while answering exactly right as soon as one is. The tessellation suites + // measure the tessellator by exactly that shape (rasterizer discard on, + // transform feedback INACTIVE, a PATCHES draw inside a GENERATED query; + // esextcTessellationShaderUtils.cpp, captureTessellationData), size their + // capture buffers from the answer, and die on the zero-byte buffer the 0 + // produces - about 29 tessellation tests per tree plus all 13 + // tessellation_shader.vertex bodies. + // + // THE PROBE draws three shapes through pipelines with no Xfb execution mode + // and no transform feedback begun, each inside its own stream-query slot: + // - one triangle, plainly (no rasterizer discard); + // - one triangle with rasterizer discard baked into the pipeline; + // - one PATCHES draw with discard, through a passthrough tessellation + // pipeline whose all-1 levels emit exactly one triangle (when the device + // has tessellationShader) - the CTS shape verbatim. + // Alongside each stream slot it measures the TWO candidate substitutes, each + // around ITS OWN identical replay of the shape's draw - never co-active with + // the subject, because a co-active control contaminates it: on lavapipe a + // dedicated primitives-generated query active around the same draw switches + // the driver's primitive collection on and the stream query then counts a + // draw it answers 0 for when alone, which is how the renderer actually runs + // it. The substitutes: + // - a VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT slot, where the device has + // VK_EXT_primitives_generated_query with BOTH primitivesGeneratedQuery and + // primitivesGeneratedQueryWithRasterizerDiscard (without the discard + // feature the spec forbids the query around a discarding draw at all - + // VUID-vkCmdDraw-...-06708 - and GL applications toggle discard freely, so + // a base-feature-only device cannot use this tier). The extension exists + // precisely because GL needs PRIMITIVES_GENERATED without a capture, so + // its semantics are exact by definition - what remains to prove is that + // the DRIVER's implementation is not silent in the same way its stream + // query is; + // - a VK_QUERY_TYPE_PIPELINE_STATISTICS slot counting CLIPPING_INVOCATIONS + // (when the device has pipelineStatisticsQuery): one invocation of the + // primitive clipping stage per primitive reaching it - GL's + // CLIPPING_INPUT_PRIMITIVES - which sits AFTER every vertex processing + // stage and, per spec, BEFORE rasterizer discard, so for an XFB-inactive + // draw it is definitionally the number PRIMITIVES_GENERATED must answer. + // + // THE CONTROL DISCIPLINE (DriverBugProbes.h): the substitute slots are the + // probe's controls, and the DISCARD dimension is measured separately because + // it is a real fault line, not paranoia: Mesa llvmpipe short-circuits its + // clipping statistics under rasterizer discard (reading 0 there while counting + // the identical undiscarded draw exactly) while its dedicated + // primitives-generated query counts both - measured 2026-08, and the reason + // the verdict ranks the dedicated query first. A substitute qualifies only by + // answering the exact expected count on every shape it is required to cover; + // a device where no substitute qualifies even for the plain shape gets none + // (the honest verdict is the current behaviour); anything that fits neither + // the defect nor health is INCONCLUSIVE and must never arm anything. The + // expected counts are exact (1 triangle per shape), not merely nonzero, so a + // driver that half-counts cannot arm a half-right repair. + + // What one drawn shape of the probe measured. + struct PrimitivesGeneratedNoXfbShapeMeasurement { + // The shape's draw was recorded and its query slots were read back. + Bool drawn = false; + // Primitives the draw is defined to emit (1 for every shape). + Uint64 expectedPrimitives = 0; + // The stream-query slot's primitivesNeeded answer - what the renderer's + // GL_PRIMITIVES_GENERATED path would have returned. + Uint64 streamGenerated = 0; + // Whether the dedicated primitives-generated slot ran (it needs the + // extension with both feature bits, see above). + Bool primitivesGeneratedExtMeasured = false; + // Its answer for the same draw. + Uint64 primitivesGeneratedExt = 0; + // Whether the statistics slot ran (it needs pipelineStatisticsQuery). + Bool statisticsMeasured = false; + // The clipping-stage invocation count for the same draw. + Uint64 statisticsClippingInput = 0; + }; + + struct PrimitivesGeneratedNoXfbMeasurement { + // The probe submitted and read back at least the two triangle shapes. + // False when any setup step failed; failureReason then names the step. + Bool ran = false; + String failureReason; + PrimitivesGeneratedNoXfbShapeMeasurement trianglesPlain; + PrimitivesGeneratedNoXfbShapeMeasurement trianglesDiscard; + // drawn = false when the device has no tessellationShader. + PrimitivesGeneratedNoXfbShapeMeasurement patchesDiscard; + }; + + // The device-level entry points the probe records with. Supplied by the caller + // because the two callers resolve them differently: the renderer passes its + // statically linked symbols (and its vkGetDeviceProcAddr-resolved EXT + // pointers), the driver POST passes vkGetInstanceProcAddr trampolines. + struct PrimitivesGeneratedNoXfbProbeFns { + PFN_vkCreateCommandPool vkCreateCommandPool = nullptr; + PFN_vkDestroyCommandPool vkDestroyCommandPool = nullptr; + PFN_vkAllocateCommandBuffers vkAllocateCommandBuffers = nullptr; + PFN_vkBeginCommandBuffer vkBeginCommandBuffer = nullptr; + PFN_vkEndCommandBuffer vkEndCommandBuffer = nullptr; + PFN_vkCreateQueryPool vkCreateQueryPool = nullptr; + PFN_vkDestroyQueryPool vkDestroyQueryPool = nullptr; + PFN_vkCmdResetQueryPool vkCmdResetQueryPool = nullptr; + PFN_vkCmdBeginQuery vkCmdBeginQuery = nullptr; + PFN_vkCmdEndQuery vkCmdEndQuery = nullptr; + PFN_vkCmdBeginQueryIndexedEXT vkCmdBeginQueryIndexedEXT = nullptr; + PFN_vkCmdEndQueryIndexedEXT vkCmdEndQueryIndexedEXT = nullptr; + PFN_vkCreateRenderPass vkCreateRenderPass = nullptr; + PFN_vkDestroyRenderPass vkDestroyRenderPass = nullptr; + PFN_vkCreateFramebuffer vkCreateFramebuffer = nullptr; + PFN_vkDestroyFramebuffer vkDestroyFramebuffer = nullptr; + PFN_vkCmdBeginRenderPass vkCmdBeginRenderPass = nullptr; + PFN_vkCmdEndRenderPass vkCmdEndRenderPass = nullptr; + PFN_vkCreateShaderModule vkCreateShaderModule = nullptr; + PFN_vkDestroyShaderModule vkDestroyShaderModule = nullptr; + PFN_vkCreatePipelineLayout vkCreatePipelineLayout = nullptr; + PFN_vkDestroyPipelineLayout vkDestroyPipelineLayout = nullptr; + PFN_vkCreateGraphicsPipelines vkCreateGraphicsPipelines = nullptr; + PFN_vkDestroyPipeline vkDestroyPipeline = nullptr; + PFN_vkCmdBindPipeline vkCmdBindPipeline = nullptr; + PFN_vkCmdDraw vkCmdDraw = nullptr; + PFN_vkCreateFence vkCreateFence = nullptr; + PFN_vkDestroyFence vkDestroyFence = nullptr; + PFN_vkQueueSubmit vkQueueSubmit = nullptr; + PFN_vkWaitForFences vkWaitForFences = nullptr; + PFN_vkGetQueryPoolResults vkGetQueryPoolResults = nullptr; + PFN_vkDeviceWaitIdle vkDeviceWaitIdle = nullptr; + }; + + struct PrimitivesGeneratedNoXfbProbeContext { + VkDevice device = VK_NULL_HANDLE; + VkQueue queue = VK_NULL_HANDLE; + Uint32 queueFamilyIndex = 0; + // The device was created with VK_EXT_transform_feedback, its + // transformFeedback feature, and advertises transformFeedbackQueries. + // Without this the probe has no subject and reports "did not run". + Bool transformFeedbackQueriesUsable = false; + // The device was created with VK_EXT_primitives_generated_query and BOTH + // its primitivesGeneratedQuery and ...WithRasterizerDiscard features; + // gates the dedicated-query control slots. + Bool primitivesGeneratedQueryUsable = false; + // The device was created with the pipelineStatisticsQuery feature; gates + // the statistics control slots. A probe with no control at all can still + // DETECT, but never qualifies a substitute. + Bool pipelineStatisticsEnabled = false; + // The device was created with the tessellationShader feature; gates the + // PATCHES shape. + Bool tessellationEnabled = false; + PrimitivesGeneratedNoXfbProbeFns fns; + }; + + // Records, submits and reads back the probe. Synchronous: waits on its own + // fence (bounded; on timeout it deliberately leaks its device objects rather + // than idle-wait a possibly hung GPU, mirroring the POST timestamp probe) and + // destroys everything it created. Never touches MG_State or renderer state - + // the caller only lends it a device and an otherwise idle queue. + PrimitivesGeneratedNoXfbMeasurement RunPrimitivesGeneratedNoXfbProbe( + const PrimitivesGeneratedNoXfbProbeContext& context); + + // The verdict vocabulary. Pure function of the measurement, split from the + // Vulkan plumbing so a unit test can pin every mapping with synthetic numbers. + enum class PrimitivesGeneratedNoXfbVerdict : Uint8 { + // The probe did not run, or answered something that is neither healthy nor + // the defect (a half-count, a nonzero-but-wrong stream answer). Must never + // arm the reroute and must never be reported as the bug. + Inconclusive, + // Every drawn shape's stream query answered its exact expected count: the + // driver counts XFB-inactive draws and the existing path is correct. + StreamCounts, + // The defect is present (a drawn shape's stream query answered exactly 0) + // and the dedicated primitives-generated query answered every drawn shape + // exactly, the rasterizer-discard shapes included: the substitution is + // proven whole through the query Vulkan defines for exactly this GL + // target. + PrimitivesGeneratedExtSubstitute, + // The defect is present, the dedicated query did not qualify (absent, or + // silent like the stream query), and the statistics control answered EVERY + // drawn shape exactly - discard shapes included: the substitution is + // proven whole through clipping statistics. + StatisticsSubstitute, + // The defect is present and the statistics control is exact on the PLAIN + // shape but silent or wrong under rasterizer discard (llvmpipe's discard + // short-circuit does this to its statistics - its dedicated query is what + // rescues it to the verdict above). Rerouting still repairs every + // undiscarded query and is never worse per draw - a rerouted draw would + // have answered 0 through the silent stream query - but the CTS shape + // stays broken and the report must say so. + StatisticsSubstitutePlainOnly, + // The defect is present and no substitute qualifies even for the plain + // shape: the honest verdict is the current behaviour. + Unfixable, + }; + + PrimitivesGeneratedNoXfbVerdict EvaluatePrimitivesGeneratedNoXfbVerdict( + const PrimitivesGeneratedNoXfbMeasurement& measurement); + + // Which query pool the renderer routes GL_PRIMITIVES_GENERATED accumulation + // for XFB-inactive draws through. + enum class PrimGenRerouteKind : Uint8 { + None, + // VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT (needs the extension with both + // feature bits - see the context flag). + PrimitivesGeneratedExt, + // VK_QUERY_TYPE_PIPELINE_STATISTICS over clipping invocations (needs + // pipelineStatisticsQuery). + ClippingStatistics, + }; + + // The arming decision. Pure, so the override mapping is unit-pinnable: + // - ForceOff never reroutes; + // - ForceOn bypasses the verdict but never the structural checks: it takes + // the dedicated query where the device can host it, the statistics pool + // where only that exists, and nothing where neither does; + // - Auto follows the verdict: the dedicated query on + // PrimitivesGeneratedExtSubstitute, the statistics pool on + // StatisticsSubstitute and StatisticsSubstitutePlainOnly (each already + // implies its feature-backed control), and nothing otherwise. + PrimGenRerouteKind ChoosePrimitivesGeneratedReroute(MG_Config::QuirkOverride overrideSetting, + PrimitivesGeneratedNoXfbVerdict verdict, + Bool primitivesGeneratedQueryUsable, + Bool pipelineStatisticsEnabled); +} // namespace MobileGL::MG_Util::SelfTest diff --git a/MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.tesc b/MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.tesc new file mode 100644 index 00000000..fc65c605 --- /dev/null +++ b/MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.tesc @@ -0,0 +1,24 @@ +// MobileGL - MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.tesc +// 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 +// +// Tessellation control stage of the PATCHES variant of the +// primitives-generated-without-transform-feedback probe. Every level is 1, so with +// the evaluation stage's triangles domain the tessellator emits exactly one +// triangle per patch - the expected count the probe checks the queries against. +#version 450 + +layout(vertices = 1) out; + +void main() { + gl_TessLevelOuter[0] = 1.0; + gl_TessLevelOuter[1] = 1.0; + gl_TessLevelOuter[2] = 1.0; + gl_TessLevelOuter[3] = 1.0; + gl_TessLevelInner[0] = 1.0; + gl_TessLevelInner[1] = 1.0; +} diff --git a/MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.tese b/MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.tese new file mode 100644 index 00000000..f346faf8 --- /dev/null +++ b/MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.tese @@ -0,0 +1,19 @@ +// MobileGL - MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.tese +// 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 +// +// Tessellation evaluation stage of the PATCHES variant of the +// primitives-generated-without-transform-feedback probe. Triangles domain: with the +// control stage's all-1 levels the tessellator emits exactly one triangle per +// patch. Like the vertex stage, it deliberately carries no Xfb execution mode. +#version 450 + +layout(triangles, equal_spacing, cw) in; + +void main() { + gl_Position = vec4(gl_TessCoord.xy * 2.0 - 1.0, 0.0, 1.0); +} diff --git a/MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.vert b/MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.vert new file mode 100644 index 00000000..8008ecaf --- /dev/null +++ b/MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.vert @@ -0,0 +1,22 @@ +// MobileGL - MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.vert +// 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 +// +// Vertex stage of the primitives-generated-without-transform-feedback probe +// (PrimitivesGeneratedNoXfbProbe.cpp), used by both triangle shapes (with and +// without rasterizer discard) and as the tessellation shapes' vertex stage. +// Deliberately carries NO Xfb execution mode: the probe's whole subject is what +// the transform-feedback stream query answers for a pipeline that captures +// nothing. Positions are distinct (a full-viewport triangle per three vertices) +// so no driver can excuse the primitive as degenerate before it reaches +// primitive assembly. +#version 450 + +void main() { + const vec2 corners[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + gl_Position = vec4(corners[gl_VertexIndex % 3], 0.0, 1.0); +} diff --git a/MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbeSpv.h b/MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbeSpv.h new file mode 100644 index 00000000..b3a2b522 --- /dev/null +++ b/MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbeSpv.h @@ -0,0 +1,153 @@ +// MobileGL - MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbeSpv.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 + +// Generated from PrimitivesGeneratedNoXfbProbe.{vert,tesc,tese} with: +// glslangValidator --target-env vulkan1.1 -V PrimitivesGeneratedNoXfbProbe. +// (SPIR-V words dumped little-endian, six per line.) +// +// Regenerate whenever a probe shader changes; nothing else in the probe depends on +// the exact binary. None of the modules carries an Xfb execution mode - that is the +// probe's subject, see PrimitivesGeneratedNoXfbProbe.cpp. + +#pragma once +#include +#include + +namespace MobileGL::MG_Util::SelfTest { + inline constexpr std::uint32_t kPrimitivesGeneratedNoXfbProbeVertSpv[] = { + 0x07230203, 0x00010300, 0x0008000b, 0x0000002a, 0x00000000, 0x00020011, + 0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, + 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0007000f, 0x00000000, + 0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x0000001a, 0x00030003, + 0x00000002, 0x000001c2, 0x00040005, 0x00000004, 0x6e69616d, 0x00000000, + 0x00060005, 0x0000000b, 0x505f6c67, 0x65567265, 0x78657472, 0x00000000, + 0x00060006, 0x0000000b, 0x00000000, 0x505f6c67, 0x7469736f, 0x006e6f69, + 0x00070006, 0x0000000b, 0x00000001, 0x505f6c67, 0x746e696f, 0x657a6953, + 0x00000000, 0x00070006, 0x0000000b, 0x00000002, 0x435f6c67, 0x4470696c, + 0x61747369, 0x0065636e, 0x00070006, 0x0000000b, 0x00000003, 0x435f6c67, + 0x446c6c75, 0x61747369, 0x0065636e, 0x00030005, 0x0000000d, 0x00000000, + 0x00060005, 0x0000001a, 0x565f6c67, 0x65747265, 0x646e4978, 0x00007865, + 0x00050005, 0x0000001f, 0x65646e69, 0x6c626178, 0x00000065, 0x00030047, + 0x0000000b, 0x00000002, 0x00050048, 0x0000000b, 0x00000000, 0x0000000b, + 0x00000000, 0x00050048, 0x0000000b, 0x00000001, 0x0000000b, 0x00000001, + 0x00050048, 0x0000000b, 0x00000002, 0x0000000b, 0x00000003, 0x00050048, + 0x0000000b, 0x00000003, 0x0000000b, 0x00000004, 0x00040047, 0x0000001a, + 0x0000000b, 0x0000002a, 0x00020013, 0x00000002, 0x00030021, 0x00000003, + 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, + 0x00000006, 0x00000004, 0x00040015, 0x00000008, 0x00000020, 0x00000000, + 0x0004002b, 0x00000008, 0x00000009, 0x00000001, 0x0004001c, 0x0000000a, + 0x00000006, 0x00000009, 0x0006001e, 0x0000000b, 0x00000007, 0x00000006, + 0x0000000a, 0x0000000a, 0x00040020, 0x0000000c, 0x00000003, 0x0000000b, + 0x0004003b, 0x0000000c, 0x0000000d, 0x00000003, 0x00040015, 0x0000000e, + 0x00000020, 0x00000001, 0x0004002b, 0x0000000e, 0x0000000f, 0x00000000, + 0x00040017, 0x00000010, 0x00000006, 0x00000002, 0x0004002b, 0x00000008, + 0x00000011, 0x00000003, 0x0004001c, 0x00000012, 0x00000010, 0x00000011, + 0x0004002b, 0x00000006, 0x00000013, 0xbf800000, 0x0005002c, 0x00000010, + 0x00000014, 0x00000013, 0x00000013, 0x0004002b, 0x00000006, 0x00000015, + 0x40400000, 0x0005002c, 0x00000010, 0x00000016, 0x00000015, 0x00000013, + 0x0005002c, 0x00000010, 0x00000017, 0x00000013, 0x00000015, 0x0006002c, + 0x00000012, 0x00000018, 0x00000014, 0x00000016, 0x00000017, 0x00040020, + 0x00000019, 0x00000001, 0x0000000e, 0x0004003b, 0x00000019, 0x0000001a, + 0x00000001, 0x0004002b, 0x0000000e, 0x0000001c, 0x00000003, 0x00040020, + 0x0000001e, 0x00000007, 0x00000012, 0x00040020, 0x00000020, 0x00000007, + 0x00000010, 0x0004002b, 0x00000006, 0x00000023, 0x00000000, 0x0004002b, + 0x00000006, 0x00000024, 0x3f800000, 0x00040020, 0x00000028, 0x00000003, + 0x00000007, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, + 0x000200f8, 0x00000005, 0x0004003b, 0x0000001e, 0x0000001f, 0x00000007, + 0x0004003d, 0x0000000e, 0x0000001b, 0x0000001a, 0x0005008b, 0x0000000e, + 0x0000001d, 0x0000001b, 0x0000001c, 0x0003003e, 0x0000001f, 0x00000018, + 0x00050041, 0x00000020, 0x00000021, 0x0000001f, 0x0000001d, 0x0004003d, + 0x00000010, 0x00000022, 0x00000021, 0x00050051, 0x00000006, 0x00000025, + 0x00000022, 0x00000000, 0x00050051, 0x00000006, 0x00000026, 0x00000022, + 0x00000001, 0x00070050, 0x00000007, 0x00000027, 0x00000025, 0x00000026, + 0x00000023, 0x00000024, 0x00050041, 0x00000028, 0x00000029, 0x0000000d, + 0x0000000f, 0x0003003e, 0x00000029, 0x00000027, 0x000100fd, 0x00010038, + }; + inline constexpr std::size_t kPrimitivesGeneratedNoXfbProbeVertSpvWordCount = sizeof(kPrimitivesGeneratedNoXfbProbeVertSpv) / sizeof(kPrimitivesGeneratedNoXfbProbeVertSpv[0]); + + inline constexpr std::uint32_t kPrimitivesGeneratedNoXfbProbeTescSpv[] = { + 0x07230203, 0x00010300, 0x0008000b, 0x0000001d, 0x00000000, 0x00020011, + 0x00000003, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, + 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0007000f, 0x00000001, + 0x00000004, 0x6e69616d, 0x00000000, 0x0000000b, 0x0000001a, 0x00040010, + 0x00000004, 0x0000001a, 0x00000001, 0x00030003, 0x00000002, 0x000001c2, + 0x00040005, 0x00000004, 0x6e69616d, 0x00000000, 0x00070005, 0x0000000b, + 0x545f6c67, 0x4c737365, 0x6c657665, 0x6574754f, 0x00000072, 0x00070005, + 0x0000001a, 0x545f6c67, 0x4c737365, 0x6c657665, 0x656e6e49, 0x00000072, + 0x00040047, 0x0000000b, 0x0000000b, 0x0000000b, 0x00030047, 0x0000000b, + 0x0000000f, 0x00040047, 0x0000001a, 0x0000000b, 0x0000000c, 0x00030047, + 0x0000001a, 0x0000000f, 0x00020013, 0x00000002, 0x00030021, 0x00000003, + 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040015, 0x00000007, + 0x00000020, 0x00000000, 0x0004002b, 0x00000007, 0x00000008, 0x00000004, + 0x0004001c, 0x00000009, 0x00000006, 0x00000008, 0x00040020, 0x0000000a, + 0x00000003, 0x00000009, 0x0004003b, 0x0000000a, 0x0000000b, 0x00000003, + 0x00040015, 0x0000000c, 0x00000020, 0x00000001, 0x0004002b, 0x0000000c, + 0x0000000d, 0x00000000, 0x0004002b, 0x00000006, 0x0000000e, 0x3f800000, + 0x00040020, 0x0000000f, 0x00000003, 0x00000006, 0x0004002b, 0x0000000c, + 0x00000011, 0x00000001, 0x0004002b, 0x0000000c, 0x00000013, 0x00000002, + 0x0004002b, 0x0000000c, 0x00000015, 0x00000003, 0x0004002b, 0x00000007, + 0x00000017, 0x00000002, 0x0004001c, 0x00000018, 0x00000006, 0x00000017, + 0x00040020, 0x00000019, 0x00000003, 0x00000018, 0x0004003b, 0x00000019, + 0x0000001a, 0x00000003, 0x00050036, 0x00000002, 0x00000004, 0x00000000, + 0x00000003, 0x000200f8, 0x00000005, 0x00050041, 0x0000000f, 0x00000010, + 0x0000000b, 0x0000000d, 0x0003003e, 0x00000010, 0x0000000e, 0x00050041, + 0x0000000f, 0x00000012, 0x0000000b, 0x00000011, 0x0003003e, 0x00000012, + 0x0000000e, 0x00050041, 0x0000000f, 0x00000014, 0x0000000b, 0x00000013, + 0x0003003e, 0x00000014, 0x0000000e, 0x00050041, 0x0000000f, 0x00000016, + 0x0000000b, 0x00000015, 0x0003003e, 0x00000016, 0x0000000e, 0x00050041, + 0x0000000f, 0x0000001b, 0x0000001a, 0x0000000d, 0x0003003e, 0x0000001b, + 0x0000000e, 0x00050041, 0x0000000f, 0x0000001c, 0x0000001a, 0x00000011, + 0x0003003e, 0x0000001c, 0x0000000e, 0x000100fd, 0x00010038, + }; + inline constexpr std::size_t kPrimitivesGeneratedNoXfbProbeTescSpvWordCount = sizeof(kPrimitivesGeneratedNoXfbProbeTescSpv) / sizeof(kPrimitivesGeneratedNoXfbProbeTescSpv[0]); + + inline constexpr std::uint32_t kPrimitivesGeneratedNoXfbProbeTeseSpv[] = { + 0x07230203, 0x00010300, 0x0008000b, 0x00000021, 0x00000000, 0x00020011, + 0x00000003, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, + 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0007000f, 0x00000002, + 0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x00000012, 0x00030010, + 0x00000004, 0x00000016, 0x00030010, 0x00000004, 0x00000001, 0x00030010, + 0x00000004, 0x00000004, 0x00030003, 0x00000002, 0x000001c2, 0x00040005, + 0x00000004, 0x6e69616d, 0x00000000, 0x00060005, 0x0000000b, 0x505f6c67, + 0x65567265, 0x78657472, 0x00000000, 0x00060006, 0x0000000b, 0x00000000, + 0x505f6c67, 0x7469736f, 0x006e6f69, 0x00070006, 0x0000000b, 0x00000001, + 0x505f6c67, 0x746e696f, 0x657a6953, 0x00000000, 0x00070006, 0x0000000b, + 0x00000002, 0x435f6c67, 0x4470696c, 0x61747369, 0x0065636e, 0x00070006, + 0x0000000b, 0x00000003, 0x435f6c67, 0x446c6c75, 0x61747369, 0x0065636e, + 0x00030005, 0x0000000d, 0x00000000, 0x00060005, 0x00000012, 0x545f6c67, + 0x43737365, 0x64726f6f, 0x00000000, 0x00030047, 0x0000000b, 0x00000002, + 0x00050048, 0x0000000b, 0x00000000, 0x0000000b, 0x00000000, 0x00050048, + 0x0000000b, 0x00000001, 0x0000000b, 0x00000001, 0x00050048, 0x0000000b, + 0x00000002, 0x0000000b, 0x00000003, 0x00050048, 0x0000000b, 0x00000003, + 0x0000000b, 0x00000004, 0x00040047, 0x00000012, 0x0000000b, 0x0000000d, + 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, + 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000004, + 0x00040015, 0x00000008, 0x00000020, 0x00000000, 0x0004002b, 0x00000008, + 0x00000009, 0x00000001, 0x0004001c, 0x0000000a, 0x00000006, 0x00000009, + 0x0006001e, 0x0000000b, 0x00000007, 0x00000006, 0x0000000a, 0x0000000a, + 0x00040020, 0x0000000c, 0x00000003, 0x0000000b, 0x0004003b, 0x0000000c, + 0x0000000d, 0x00000003, 0x00040015, 0x0000000e, 0x00000020, 0x00000001, + 0x0004002b, 0x0000000e, 0x0000000f, 0x00000000, 0x00040017, 0x00000010, + 0x00000006, 0x00000003, 0x00040020, 0x00000011, 0x00000001, 0x00000010, + 0x0004003b, 0x00000011, 0x00000012, 0x00000001, 0x00040017, 0x00000013, + 0x00000006, 0x00000002, 0x0004002b, 0x00000006, 0x00000016, 0x40000000, + 0x0004002b, 0x00000006, 0x00000018, 0x3f800000, 0x0004002b, 0x00000006, + 0x0000001b, 0x00000000, 0x00040020, 0x0000001f, 0x00000003, 0x00000007, + 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, + 0x00000005, 0x0004003d, 0x00000010, 0x00000014, 0x00000012, 0x0007004f, + 0x00000013, 0x00000015, 0x00000014, 0x00000014, 0x00000000, 0x00000001, + 0x0005008e, 0x00000013, 0x00000017, 0x00000015, 0x00000016, 0x00050050, + 0x00000013, 0x00000019, 0x00000018, 0x00000018, 0x00050083, 0x00000013, + 0x0000001a, 0x00000017, 0x00000019, 0x00050051, 0x00000006, 0x0000001c, + 0x0000001a, 0x00000000, 0x00050051, 0x00000006, 0x0000001d, 0x0000001a, + 0x00000001, 0x00070050, 0x00000007, 0x0000001e, 0x0000001c, 0x0000001d, + 0x0000001b, 0x00000018, 0x00050041, 0x0000001f, 0x00000020, 0x0000000d, + 0x0000000f, 0x0003003e, 0x00000020, 0x0000001e, 0x000100fd, 0x00010038, + }; + inline constexpr std::size_t kPrimitivesGeneratedNoXfbProbeTeseSpvWordCount = sizeof(kPrimitivesGeneratedNoXfbProbeTeseSpv) / sizeof(kPrimitivesGeneratedNoXfbProbeTeseSpv[0]); +} // namespace MobileGL::MG_Util::SelfTest