mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,6 +11471,7 @@ void main() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (haveStreamSlots) {
|
||||
for (const Uint32 slot : slots) {
|
||||
Uint64 pair[2] = {0, 0}; // {primitivesWritten, primitivesNeeded}
|
||||
const VkResult result =
|
||||
@@ -11422,36 +11481,95 @@ void main() {
|
||||
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;
|
||||
}
|
||||
// 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);
|
||||
for (Uint32 kind = 0; kind < 2; ++kind) {
|
||||
if (m_xfbQueryCaptureActive[kind]) {
|
||||
m_xfbQueryActiveSlots[kind].push_back(slot);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::BeginOcclusionForDraw(VkCommandBuffer commandBuffer) {
|
||||
if (!m_occlusionCaptureActive || m_occlusionQueryPool == VK_NULL_HANDLE) {
|
||||
@@ -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;
|
||||
|
||||
@@ -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