mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
[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:
@@ -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
|
||||
Reference in New Issue
Block a user