[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

This commit is contained in:
2026-08-28 06:19:39 -04:00
parent 0ee3384b22
commit 1350031368
13 changed files with 1626 additions and 33 deletions
+1
View File
@@ -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
+16
View File
@@ -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
+1
View File
@@ -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() {
@@ -1206,6 +1206,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
SharedPtr<VkTimerQueryManager::TimestampRecord> end;
// Kind::Occlusion - pool slots recorded between Begin/End; summed at result time.
Vector<Uint32> 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<Uint32> 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() {
@@ -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 <Config.h>
#include <algorithm>
@@ -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<Uint32>& outSlots) {
void VulkanRenderer::StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots,
Vector<Uint32>& 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<Uint32>& slots, Bool wantGenerated, Uint64& outPrimitives) {
Bool VulkanRenderer::ResolveXfbQueryResult(const Vector<Uint32>& slots, const Vector<Uint32>& 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<void*>(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<Uint32>(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<unsigned long long>(shape.streamGenerated),
static_cast<unsigned long long>(shape.expectedPrimitives),
static_cast<unsigned long long>(shape.primitivesGeneratedExt),
shape.primitivesGeneratedExtMeasured ? 1 : 0,
static_cast<unsigned long long>(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() {
@@ -24,6 +24,7 @@
#include "MG_Util/Math/VectorTypes.h"
#include <Includes.h>
#include <MG_Backend/BackendObject.h>
#include <MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h>
#include <vk_mem_alloc.h>
#include "../VkIncludes.h"
@@ -717,15 +718,54 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<Uint32> 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<Uint32> 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<Uint32>& outSlots);
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, Bool wantGenerated, Uint64& outPrimitives);
void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots, Vector<Uint32>& outRerouteSlots);
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, const Vector<Uint32>& 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;
+281
View File
@@ -9,6 +9,7 @@
#include "DriverPost.h"
#include "DriverPostIterationRPWitness.h"
#include "DriverPostIterationRPWitnessSpv.h"
#include "PrimitivesGeneratedNoXfbProbe.h"
#include "MG_Util/BackendLoaders/OpenGL/Loader.h"
#include <Config.h>
#include <MGGitHash.h>
@@ -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<VkExtensionProperties>& 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<PFN_vkCreateDevice>(getInstanceProcAddr(instance, "vkCreateDevice"));
const auto vkDestroyDeviceFn =
reinterpret_cast<PFN_vkDestroyDevice>(getInstanceProcAddr(instance, "vkDestroyDevice"));
const auto vkGetDeviceQueueFn =
reinterpret_cast<PFN_vkGetDeviceQueue>(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<const void*>(&enabledPgqFeatures)
: static_cast<const void*>(&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<Int>(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<PFN_vkCreateCommandPool>(resolve("vkCreateCommandPool"));
fns.vkDestroyCommandPool =
reinterpret_cast<PFN_vkDestroyCommandPool>(resolve("vkDestroyCommandPool"));
fns.vkAllocateCommandBuffers =
reinterpret_cast<PFN_vkAllocateCommandBuffers>(resolve("vkAllocateCommandBuffers"));
fns.vkBeginCommandBuffer =
reinterpret_cast<PFN_vkBeginCommandBuffer>(resolve("vkBeginCommandBuffer"));
fns.vkEndCommandBuffer = reinterpret_cast<PFN_vkEndCommandBuffer>(resolve("vkEndCommandBuffer"));
fns.vkCreateQueryPool = reinterpret_cast<PFN_vkCreateQueryPool>(resolve("vkCreateQueryPool"));
fns.vkDestroyQueryPool = reinterpret_cast<PFN_vkDestroyQueryPool>(resolve("vkDestroyQueryPool"));
fns.vkCmdResetQueryPool = reinterpret_cast<PFN_vkCmdResetQueryPool>(resolve("vkCmdResetQueryPool"));
fns.vkCmdBeginQuery = reinterpret_cast<PFN_vkCmdBeginQuery>(resolve("vkCmdBeginQuery"));
fns.vkCmdEndQuery = reinterpret_cast<PFN_vkCmdEndQuery>(resolve("vkCmdEndQuery"));
fns.vkCmdBeginQueryIndexedEXT =
reinterpret_cast<PFN_vkCmdBeginQueryIndexedEXT>(resolve("vkCmdBeginQueryIndexedEXT"));
fns.vkCmdEndQueryIndexedEXT =
reinterpret_cast<PFN_vkCmdEndQueryIndexedEXT>(resolve("vkCmdEndQueryIndexedEXT"));
fns.vkCreateRenderPass = reinterpret_cast<PFN_vkCreateRenderPass>(resolve("vkCreateRenderPass"));
fns.vkDestroyRenderPass =
reinterpret_cast<PFN_vkDestroyRenderPass>(resolve("vkDestroyRenderPass"));
fns.vkCreateFramebuffer =
reinterpret_cast<PFN_vkCreateFramebuffer>(resolve("vkCreateFramebuffer"));
fns.vkDestroyFramebuffer =
reinterpret_cast<PFN_vkDestroyFramebuffer>(resolve("vkDestroyFramebuffer"));
fns.vkCmdBeginRenderPass =
reinterpret_cast<PFN_vkCmdBeginRenderPass>(resolve("vkCmdBeginRenderPass"));
fns.vkCmdEndRenderPass = reinterpret_cast<PFN_vkCmdEndRenderPass>(resolve("vkCmdEndRenderPass"));
fns.vkCreateShaderModule =
reinterpret_cast<PFN_vkCreateShaderModule>(resolve("vkCreateShaderModule"));
fns.vkDestroyShaderModule =
reinterpret_cast<PFN_vkDestroyShaderModule>(resolve("vkDestroyShaderModule"));
fns.vkCreatePipelineLayout =
reinterpret_cast<PFN_vkCreatePipelineLayout>(resolve("vkCreatePipelineLayout"));
fns.vkDestroyPipelineLayout =
reinterpret_cast<PFN_vkDestroyPipelineLayout>(resolve("vkDestroyPipelineLayout"));
fns.vkCreateGraphicsPipelines =
reinterpret_cast<PFN_vkCreateGraphicsPipelines>(resolve("vkCreateGraphicsPipelines"));
fns.vkDestroyPipeline = reinterpret_cast<PFN_vkDestroyPipeline>(resolve("vkDestroyPipeline"));
fns.vkCmdBindPipeline = reinterpret_cast<PFN_vkCmdBindPipeline>(resolve("vkCmdBindPipeline"));
fns.vkCmdDraw = reinterpret_cast<PFN_vkCmdDraw>(resolve("vkCmdDraw"));
fns.vkCreateFence = reinterpret_cast<PFN_vkCreateFence>(resolve("vkCreateFence"));
fns.vkDestroyFence = reinterpret_cast<PFN_vkDestroyFence>(resolve("vkDestroyFence"));
fns.vkQueueSubmit = reinterpret_cast<PFN_vkQueueSubmit>(resolve("vkQueueSubmit"));
fns.vkWaitForFences = reinterpret_cast<PFN_vkWaitForFences>(resolve("vkWaitForFences"));
fns.vkGetQueryPoolResults =
reinterpret_cast<PFN_vkGetQueryPoolResults>(resolve("vkGetQueryPoolResults"));
fns.vkDeviceWaitIdle = reinterpret_cast<PFN_vkDeviceWaitIdle>(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 "
@@ -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 <typename Callable>
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
@@ -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 <Config.h>
#include <Includes.h>
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
@@ -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;
}
@@ -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);
}
@@ -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);
}
@@ -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.<stage>
// (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 <cstddef>
#include <cstdint>
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