[Test] (DirectVulkan, SelfTest): pin the probe's fence-timeout teardown against a fake driver, the never-worse arming refusal, and paused-span patch/instanced counting against an unpaused control

This commit is contained in:
2026-08-28 06:19:41 -04:00
parent feea131d8b
commit 92dc41ebf9
2 changed files with 433 additions and 12 deletions
@@ -111,6 +111,18 @@ layout(triangles, equal_spacing, cw) in;
void main() {
gl_Position = vec4(gl_TessCoord.xy * 2.0 - 1.0, 0.0, 1.0);
}
)";
// The same tessellation pipeline with something to capture, so that
// glBeginTransformFeedback accepts it: the paused-span PATCHES case needs an
// open (but paused) capture span AND a tessellator in one program.
const char* const kTessEvalCaptureSource = R"(#version 430 core
layout(triangles, equal_spacing, cw) in;
out vec4 te_out_value;
void main() {
te_out_value = vec4(1.0);
gl_Position = vec4(gl_TessCoord.xy * 2.0 - 1.0, 0.0, 1.0);
}
)";
class PrimitivesGeneratedNoXfbScenario : public ScenarioTest {
@@ -145,8 +157,10 @@ void main() {
ScenarioTest::TearDown();
}
// captureVarying: the name to record with glTransformFeedbackVaryings, or
// nullptr for a program that can never open a capture span.
GLuint BuildProgram(std::initializer_list<std::pair<GLenum, const char*>> stages,
bool withCaptureVarying) {
const char* captureVarying) {
std::vector<GLuint> shaders;
for (const auto& [type, source] : stages) {
const GLuint shader = CompileShaderStage(type, source, &m_buildLog);
@@ -158,9 +172,8 @@ void main() {
}
const GLuint program = glCreateProgram();
for (const GLuint shader : shaders) glAttachShader(program, shader);
if (withCaptureVarying) {
const char* varying = "vs_out_value";
glTransformFeedbackVaryings(program, 1, &varying, GL_INTERLEAVED_ATTRIBS);
if (captureVarying != nullptr) {
glTransformFeedbackVaryings(program, 1, &captureVarying, GL_INTERLEAVED_ATTRIBS);
}
glLinkProgram(program);
for (const GLuint shader : shaders) glDeleteShader(shader);
@@ -180,19 +193,41 @@ void main() {
}
GLuint BuildCaptureProgram() {
return BuildProgram({{GL_VERTEX_SHADER, kVertexSource}}, true);
return BuildProgram({{GL_VERTEX_SHADER, kVertexSource}}, "vs_out_value");
}
GLuint BuildTessellationProgram() {
GLuint BuildTessellationProgram(bool withCaptureVarying = false) {
GLint maxTessGenLevel = 0;
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
while (glGetError() != GL_NO_ERROR) {
}
if (maxTessGenLevel < 1) return 0;
return BuildProgram({{GL_VERTEX_SHADER, kTessVertexSource},
return BuildProgram(
{{GL_VERTEX_SHADER, kTessVertexSource},
{GL_TESS_CONTROL_SHADER, kTessControlSource},
{GL_TESS_EVALUATION_SHADER, kTessEvalSource}},
false);
{GL_TESS_EVALUATION_SHADER,
withCaptureVarying ? kTessEvalCaptureSource : kTessEvalSource}},
withCaptureVarying ? "te_out_value" : nullptr);
}
// A capture span that is open but PAUSED. The pause closes the capture, so
// every draw inside it is XFB-inactive at the backend - the stream query's
// silent case - while the GL span stays active. `program` must be the one
// that is bound: GL requires the same program at resume.
void BeginPausedSpan() {
glGenBuffers(1, &m_captureBuffer);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_captureBuffer);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 64 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
glBeginTransformFeedback(GL_TRIANGLES);
glPauseTransformFeedback();
}
void EndPausedSpan() {
glResumeTransformFeedback();
glEndTransformFeedback();
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
if (m_captureBuffer != 0) glDeleteBuffers(1, &m_captureBuffer);
m_captureBuffer = 0;
}
// GENERATED query around `record()`, answered with GL_QUERY_RESULT.
@@ -240,6 +275,7 @@ void main() {
GLuint m_vao = 0;
GLuint m_queries[2] = {0, 0}; // [0]=written, [1]=generated
GLuint m_captureBuffer = 0;
std::vector<GLuint> m_programs;
std::string m_buildLog;
};
@@ -360,6 +396,113 @@ void main() {
EXPECT_EQ(written, 1u) << "only the draw inside the span writes anything";
}
// ===================== DRAWS INSIDE A PAUSED SPAN =====================
//
// glPauseTransformFeedback closes the capture without closing the span, so a
// draw made while paused is XFB-INACTIVE at the backend - the stream query is
// exactly as silent for it as for a draw with no span at all - while
// GL_PRIMITIVES_GENERATED must still count what the last vertex processing
// stage emitted (GL 4.6 core 13.4; the WRITTEN query is the one the pause
// silences). The frontend does keep a CPU counter for paused draws, but it can
// price only 3 of the ~15 draw entry points and answers 0 for GL_PATCHES, so
// these draws are the reroute's business like any other - and the trap on the
// other side is counting them TWICE, once in each accounting.
//
// Each case measures the SAME draw twice: once with no span open at all (the
// capability control - what this stack can count) and once inside the paused
// span, and requires the two to agree. That differential is what makes these
// cases falsifying rather than vacuous: a stack where no counter reaches a
// capture-less draw fails the control and skips, while a stack that counts the
// unpaused draw and answers 0 for the paused one - which is what excluding
// paused draws from the reroute produced - fails, instead of skipping into
// green.
// The draw the CPU counter CAN price: if the span both reroutes it and adds the
// CPU delta, this reads 2.
TEST_F(PrimitivesGeneratedNoXfbScenario, APausedSpanCountsACpuPricedDrawExactlyOnce) {
if (!Ready()) return;
if (AmbientQuirkFromEnvironment("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE") == AmbientQuirk::Off) {
GTEST_SKIP() << "the negative control replays the pre-probe accounting, whose paused "
"draws are CPU-counted on top of whatever the stream query says";
}
const GLuint program = BuildCaptureProgram();
ASSERT_NE(program, 0u) << BuildLog();
glUseProgram(program);
const GLuint unpaused = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 3); });
BeginPausedSpan();
const GLuint paused = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 3); });
EndPausedSpan();
EXPECT_EQ(DrainGLErrors(), 0u);
if (unpaused == 0u) {
GTEST_SKIP() << "no counter this backend can reach answers a capture-less draw on this "
"stack, so the paused half of the comparison proves nothing; the "
"bring-up probe measures the same hole and the POST row reports it";
}
EXPECT_EQ(unpaused, 1u) << "the control itself: one triangle is one primitive";
EXPECT_EQ(paused, unpaused)
<< "one triangle drawn while the capture span is paused is still one primitive "
"generated - counted once, by whichever accounting owns it, never by two of them "
"(a reroute slot AND the frontend's CPU paused counter reads 2)";
}
// The draw the CPU counter CANNOT price: GL_PATCHES, whose amplification is not
// knowable on the CPU (CountPrimitivesForDraw answers 0 for it by design) - and
// the CTS's tessellator-measuring shape. Excluding paused draws from the
// reroute left this counted by nothing at all on the affected device.
TEST_F(PrimitivesGeneratedNoXfbScenario, APausedSpanCountsATessellatedPatchExactlyOnce) {
if (!Ready()) return;
const GLuint program = BuildTessellationProgram(/*withCaptureVarying=*/true);
if (program == 0) {
GTEST_SKIP() << "no tessellation stages on this stack: " << BuildLog();
}
glUseProgram(program);
glPatchParameteri(GL_PATCH_VERTICES, 1);
const GLuint unpaused = QueryGenerated([]() { glDrawArrays(GL_PATCHES, 0, 1); });
BeginPausedSpan();
const GLuint paused = QueryGenerated([]() { glDrawArrays(GL_PATCHES, 0, 1); });
EndPausedSpan();
EXPECT_EQ(DrainGLErrors(), 0u);
if (unpaused == 0u) {
GTEST_SKIP() << "no counter this backend can reach answers a capture-less patch draw "
"on this stack, so the paused half proves nothing; the bring-up probe "
"measures the same hole and the POST row reports it";
}
EXPECT_EQ(unpaused, 1u)
<< "the control itself: a triangles-domain patch with every level 1 tessellates to "
"exactly one triangle";
EXPECT_EQ(paused, unpaused)
<< "pausing the capture does not stop the tessellator from generating that triangle, "
"and the frontend's CPU paused counter answers 0 for GL_PATCHES - so a paused "
"patch draw left out of the reroute is counted by nothing at all";
}
// The other half of the same hole: the instanced entry points never reach the
// frontend's paused accounting either, so a paused instanced draw excluded from
// the reroute is likewise counted by nothing.
TEST_F(PrimitivesGeneratedNoXfbScenario, APausedSpanCountsAnInstancedDrawExactlyOnce) {
if (!Ready()) return;
const GLuint program = BuildCaptureProgram();
ASSERT_NE(program, 0u) << BuildLog();
glUseProgram(program);
const GLuint unpaused =
QueryGenerated([]() { glDrawArraysInstanced(GL_TRIANGLES, 0, 3, 4); });
BeginPausedSpan();
const GLuint paused = QueryGenerated([]() { glDrawArraysInstanced(GL_TRIANGLES, 0, 3, 4); });
EndPausedSpan();
EXPECT_EQ(DrainGLErrors(), 0u);
if (unpaused == 0u) {
GTEST_SKIP() << "no counter this backend can reach answers a capture-less draw on this "
"stack, so the paused half proves nothing";
}
EXPECT_EQ(unpaused, 4u) << "the control itself: four instances of one triangle";
EXPECT_EQ(paused, unpaused)
<< "four instances generate four primitives whether or not the capture span is "
"paused, and no instanced entry point reaches the frontend's paused accounting";
}
// THE ONE CASE THAT CAN FAIL WHEN THE REROUTE SILENTLY STOPS BEING ARMED -
// the UnlocatedIoBlockScenario shape, for the same reason: every case above
// is green here whether the reroute ran or not (that is the "two pools
@@ -7,8 +7,8 @@
// End of Source File Header
//
// The primitives-generated-without-transform-feedback probe's VERDICT and ARMING
// logic, pinned over synthetic measurements. The Vulkan plumbing needs a GPU; the
// two pure functions are where the cheap mistakes live - a verdict that reads a
// logic, pinned over synthetic measurements. Recording the probe for real needs a
// GPU; the two pure functions are where the cheap mistakes live - a verdict that reads a
// half-broken driver as healthy, an override arm swapped so ForceOn disarms, a
// substitute ranked below a worse one - and every driver the campaign has
// characterised is written down here as a fake measurement so the mapping cannot
@@ -20,21 +20,34 @@
// discard (llvmpipe's discard short-circuit),
// - the same driver without the dedicated query - the statistics tiers,
// - a device with the defect and no working substitute,
// - a substitute that would be WORSE than the stream query on some shape (the
// never-worse rule the plain-only arm has to prove before it may arm),
// - and the refuse-to-guess shapes (half counts, missing mandatory shapes).
//
// The last section pins the probe's TEARDOWN CONTRACT instead, driving the real
// RunPrimitivesGeneratedNoXfbProbe against a fake Vulkan driver whose fence wait
// can be made to expire: no GPU is needed for that, only the entry points the
// probe is handed, and what it does on that path is what keeps a hung driver from
// hanging the POST.
#include <gtest/gtest.h>
#include <cstdint>
#include <MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h>
using MobileGL::Bool;
using MobileGL::Uint32;
using MobileGL::Uint64;
using MobileGL::MG_Config::QuirkOverride;
using MobileGL::MG_Util::SelfTest::EvaluatePrimitivesGeneratedNoXfbVerdict;
using MobileGL::MG_Util::SelfTest::ChoosePrimitivesGeneratedReroute;
using MobileGL::MG_Util::SelfTest::PrimGenRerouteKind;
using MobileGL::MG_Util::SelfTest::PrimitivesGeneratedNoXfbMeasurement;
using MobileGL::MG_Util::SelfTest::PrimitivesGeneratedNoXfbProbeContext;
using MobileGL::MG_Util::SelfTest::PrimitivesGeneratedNoXfbShapeMeasurement;
using MobileGL::MG_Util::SelfTest::PrimitivesGeneratedNoXfbVerdict;
using MobileGL::MG_Util::SelfTest::RunPrimitivesGeneratedNoXfbProbe;
namespace {
struct ShapeAnswers {
@@ -139,6 +152,31 @@ TEST(PrimitivesGeneratedNoXfbVerdictTest, StatisticsDeadUnderDiscardIsThePlainOn
PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly);
}
// THE DOMINATION RULE. The plain-only substitute is armed for EVERY XFB-inactive
// draw, so it may only be armed where it is never worse than what it replaces:
// each shape it gets wrong must be one the stream query already answered 0 for.
// Here the discarded triangle is one the stream query answers EXACTLY (a driver
// whose silence is selective) and whose statistics read 0 - rerouting would turn
// that correct 1 into a 0, so the honest verdict is that nothing may be armed.
TEST(PrimitivesGeneratedNoXfbVerdictTest, ASubstituteWorseThanTheStreamOnAnyShapeIsRefused) {
const auto measurement = Measurement(Shape({1, false, 0, true, 1}), Shape({1, false, 0, true, 0}),
Shape({0, false, 0, true, 0}));
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(measurement),
PrimitivesGeneratedNoXfbVerdict::Unfixable);
// The same shape with the statistics slot MISSING on the stream-exact shape is
// the same trade: an unmeasured control cannot be assumed to answer.
const auto unmeasured = Measurement(Shape({1, false, 0, true, 1}), Shape({1, false, 0, false, 0}),
Shape({0, false, 0, true, 0}));
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(unmeasured),
PrimitivesGeneratedNoXfbVerdict::Unfixable);
// ...while the same selective silence WITH a substitute that covers the shapes
// it must still qualifies: every shape the statistics miss read 0 anyway.
const auto dominating = Measurement(Shape({1, false, 0, true, 1}), Shape({0, false, 0, true, 1}),
Shape({0, false, 0, true, 0}));
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(dominating),
PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly);
}
// The defect with no substitute: no control, controls silent, or a control that
// OVERCOUNTS the plain shape (as disqualifying as one that reads 0 - an exact
// match is what qualifies a substitute).
@@ -266,3 +304,243 @@ TEST(PrimitivesGeneratedNoXfbArmingTest, AutoFollowsExactlyTheSubstituteVerdicts
QuirkOverride::Auto, PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute, false, false),
PrimGenRerouteKind::None);
}
// ===================== THE FENCE-TIMEOUT CONTRACT =====================
//
// A driver whose queue never signals the probe's fence inside 5 s is the one case
// where the probe must NOT clean up: the submission may still be executing, so
// vkDeviceWaitIdle can block forever and destroying in-flight objects is
// undefined. It therefore leaks everything it made and says so in the measurement
// (`fenceWaitTimedOut`), which is what lets its callers make the same choice for
// the object THEY own - the driver POST leaks its throwaway VkDevice instead of
// destroying it under live children (vkDestroyDevice would be the very hang the
// bound exists to prevent), and the renderer, whose device is the real one, must
// not idle-wait it either. Neither guard is reachable from a unit test - the POST
// probe lives in an anonymous namespace and the renderer needs a GPU - so this
// pins the contract they both key on, at the boundary where it is produced.
//
// The fake driver below is the whole Vulkan surface the probe touches, with a
// dialable fence-wait result and per-entry-point call counters.
namespace {
struct FakeDriverState {
VkResult fenceWaitResult = VK_SUCCESS;
Uint32 objectsCreated = 0;
Uint32 destroyCalls = 0;
Uint32 deviceWaitIdleCalls = 0;
Uint32 queueSubmitCalls = 0;
Uint64 streamGenerated = 1;
};
FakeDriverState g_fake;
template <typename Handle>
Handle FakeHandle() {
++g_fake.objectsCreated;
// One cast form for both handle flavours: a pointer on 64-bit builds, a
// uint64_t on 32-bit ones. The probe only ever compares against
// VK_NULL_HANDLE, so any distinct nonzero value will do.
return (Handle)(std::uintptr_t)(0x1000u + g_fake.objectsCreated * 0x10u);
}
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateCommandPool(VkDevice, const VkCommandPoolCreateInfo*,
const VkAllocationCallbacks*, VkCommandPool* out) {
*out = FakeHandle<VkCommandPool>();
return VK_SUCCESS;
}
VKAPI_ATTR void VKAPI_CALL FakeDestroyCommandPool(VkDevice, VkCommandPool, const VkAllocationCallbacks*) {
++g_fake.destroyCalls;
}
VKAPI_ATTR VkResult VKAPI_CALL FakeAllocateCommandBuffers(VkDevice, const VkCommandBufferAllocateInfo*,
VkCommandBuffer* out) {
*out = FakeHandle<VkCommandBuffer>();
return VK_SUCCESS;
}
VKAPI_ATTR VkResult VKAPI_CALL FakeBeginCommandBuffer(VkCommandBuffer, const VkCommandBufferBeginInfo*) {
return VK_SUCCESS;
}
VKAPI_ATTR VkResult VKAPI_CALL FakeEndCommandBuffer(VkCommandBuffer) { return VK_SUCCESS; }
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateQueryPool(VkDevice, const VkQueryPoolCreateInfo*,
const VkAllocationCallbacks*, VkQueryPool* out) {
*out = FakeHandle<VkQueryPool>();
return VK_SUCCESS;
}
VKAPI_ATTR void VKAPI_CALL FakeDestroyQueryPool(VkDevice, VkQueryPool, const VkAllocationCallbacks*) {
++g_fake.destroyCalls;
}
VKAPI_ATTR void VKAPI_CALL FakeCmdResetQueryPool(VkCommandBuffer, VkQueryPool, uint32_t, uint32_t) {}
VKAPI_ATTR void VKAPI_CALL FakeCmdBeginQuery(VkCommandBuffer, VkQueryPool, uint32_t, VkQueryControlFlags) {}
VKAPI_ATTR void VKAPI_CALL FakeCmdEndQuery(VkCommandBuffer, VkQueryPool, uint32_t) {}
VKAPI_ATTR void VKAPI_CALL FakeCmdBeginQueryIndexedEXT(VkCommandBuffer, VkQueryPool, uint32_t,
VkQueryControlFlags, uint32_t) {}
VKAPI_ATTR void VKAPI_CALL FakeCmdEndQueryIndexedEXT(VkCommandBuffer, VkQueryPool, uint32_t, uint32_t) {}
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateRenderPass(VkDevice, const VkRenderPassCreateInfo*,
const VkAllocationCallbacks*, VkRenderPass* out) {
*out = FakeHandle<VkRenderPass>();
return VK_SUCCESS;
}
VKAPI_ATTR void VKAPI_CALL FakeDestroyRenderPass(VkDevice, VkRenderPass, const VkAllocationCallbacks*) {
++g_fake.destroyCalls;
}
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateFramebuffer(VkDevice, const VkFramebufferCreateInfo*,
const VkAllocationCallbacks*, VkFramebuffer* out) {
*out = FakeHandle<VkFramebuffer>();
return VK_SUCCESS;
}
VKAPI_ATTR void VKAPI_CALL FakeDestroyFramebuffer(VkDevice, VkFramebuffer, const VkAllocationCallbacks*) {
++g_fake.destroyCalls;
}
VKAPI_ATTR void VKAPI_CALL FakeCmdBeginRenderPass(VkCommandBuffer, const VkRenderPassBeginInfo*,
VkSubpassContents) {}
VKAPI_ATTR void VKAPI_CALL FakeCmdEndRenderPass(VkCommandBuffer) {}
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateShaderModule(VkDevice, const VkShaderModuleCreateInfo*,
const VkAllocationCallbacks*, VkShaderModule* out) {
*out = FakeHandle<VkShaderModule>();
return VK_SUCCESS;
}
VKAPI_ATTR void VKAPI_CALL FakeDestroyShaderModule(VkDevice, VkShaderModule, const VkAllocationCallbacks*) {
++g_fake.destroyCalls;
}
VKAPI_ATTR VkResult VKAPI_CALL FakeCreatePipelineLayout(VkDevice, const VkPipelineLayoutCreateInfo*,
const VkAllocationCallbacks*,
VkPipelineLayout* out) {
*out = FakeHandle<VkPipelineLayout>();
return VK_SUCCESS;
}
VKAPI_ATTR void VKAPI_CALL FakeDestroyPipelineLayout(VkDevice, VkPipelineLayout,
const VkAllocationCallbacks*) {
++g_fake.destroyCalls;
}
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateGraphicsPipelines(VkDevice, VkPipelineCache, uint32_t count,
const VkGraphicsPipelineCreateInfo*,
const VkAllocationCallbacks*, VkPipeline* out) {
for (uint32_t i = 0; i < count; ++i) {
out[i] = FakeHandle<VkPipeline>();
}
return VK_SUCCESS;
}
VKAPI_ATTR void VKAPI_CALL FakeDestroyPipeline(VkDevice, VkPipeline, const VkAllocationCallbacks*) {
++g_fake.destroyCalls;
}
VKAPI_ATTR void VKAPI_CALL FakeCmdBindPipeline(VkCommandBuffer, VkPipelineBindPoint, VkPipeline) {}
VKAPI_ATTR void VKAPI_CALL FakeCmdDraw(VkCommandBuffer, uint32_t, uint32_t, uint32_t, uint32_t) {}
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateFence(VkDevice, const VkFenceCreateInfo*,
const VkAllocationCallbacks*, VkFence* out) {
*out = FakeHandle<VkFence>();
return VK_SUCCESS;
}
VKAPI_ATTR void VKAPI_CALL FakeDestroyFence(VkDevice, VkFence, const VkAllocationCallbacks*) {
++g_fake.destroyCalls;
}
VKAPI_ATTR VkResult VKAPI_CALL FakeQueueSubmit(VkQueue, uint32_t, const VkSubmitInfo*, VkFence) {
++g_fake.queueSubmitCalls;
return VK_SUCCESS;
}
VKAPI_ATTR VkResult VKAPI_CALL FakeWaitForFences(VkDevice, uint32_t, const VkFence*, VkBool32, uint64_t) {
return g_fake.fenceWaitResult;
}
VKAPI_ATTR VkResult VKAPI_CALL FakeGetQueryPoolResults(VkDevice, VkQueryPool, uint32_t, uint32_t,
size_t dataSize, void* data, VkDeviceSize,
VkQueryResultFlags) {
// The stream pool's {primitivesWritten, primitivesNeeded} pair; the probe
// reads primitivesNeeded, and this fake device counts capture-less draws.
if (data == nullptr || dataSize < 2 * sizeof(Uint64)) {
return VK_INCOMPLETE;
}
auto* pair = static_cast<Uint64*>(data);
pair[0] = 0;
pair[1] = g_fake.streamGenerated;
return VK_SUCCESS;
}
VKAPI_ATTR VkResult VKAPI_CALL FakeDeviceWaitIdle(VkDevice) {
++g_fake.deviceWaitIdleCalls;
return VK_SUCCESS;
}
PrimitivesGeneratedNoXfbProbeContext FakeProbeContext() {
g_fake = FakeDriverState{};
PrimitivesGeneratedNoXfbProbeContext context;
context.device = (VkDevice)(std::uintptr_t)0xD0D0;
context.queue = (VkQueue)(std::uintptr_t)0xC0C0;
context.transformFeedbackQueriesUsable = true;
// No controls and no tessellation: this fixture is about the teardown
// contract, and the fewer optional slots the fewer moving parts.
auto& fns = context.fns;
fns.vkCreateCommandPool = FakeCreateCommandPool;
fns.vkDestroyCommandPool = FakeDestroyCommandPool;
fns.vkAllocateCommandBuffers = FakeAllocateCommandBuffers;
fns.vkBeginCommandBuffer = FakeBeginCommandBuffer;
fns.vkEndCommandBuffer = FakeEndCommandBuffer;
fns.vkCreateQueryPool = FakeCreateQueryPool;
fns.vkDestroyQueryPool = FakeDestroyQueryPool;
fns.vkCmdResetQueryPool = FakeCmdResetQueryPool;
fns.vkCmdBeginQuery = FakeCmdBeginQuery;
fns.vkCmdEndQuery = FakeCmdEndQuery;
fns.vkCmdBeginQueryIndexedEXT = FakeCmdBeginQueryIndexedEXT;
fns.vkCmdEndQueryIndexedEXT = FakeCmdEndQueryIndexedEXT;
fns.vkCreateRenderPass = FakeCreateRenderPass;
fns.vkDestroyRenderPass = FakeDestroyRenderPass;
fns.vkCreateFramebuffer = FakeCreateFramebuffer;
fns.vkDestroyFramebuffer = FakeDestroyFramebuffer;
fns.vkCmdBeginRenderPass = FakeCmdBeginRenderPass;
fns.vkCmdEndRenderPass = FakeCmdEndRenderPass;
fns.vkCreateShaderModule = FakeCreateShaderModule;
fns.vkDestroyShaderModule = FakeDestroyShaderModule;
fns.vkCreatePipelineLayout = FakeCreatePipelineLayout;
fns.vkDestroyPipelineLayout = FakeDestroyPipelineLayout;
fns.vkCreateGraphicsPipelines = FakeCreateGraphicsPipelines;
fns.vkDestroyPipeline = FakeDestroyPipeline;
fns.vkCmdBindPipeline = FakeCmdBindPipeline;
fns.vkCmdDraw = FakeCmdDraw;
fns.vkCreateFence = FakeCreateFence;
fns.vkDestroyFence = FakeDestroyFence;
fns.vkQueueSubmit = FakeQueueSubmit;
fns.vkWaitForFences = FakeWaitForFences;
fns.vkGetQueryPoolResults = FakeGetQueryPoolResults;
fns.vkDeviceWaitIdle = FakeDeviceWaitIdle;
return context;
}
} // namespace
// The hung driver. Nothing the probe created may be destroyed, the device may not
// be idle-waited, and the measurement must SAY the wait timed out - a caller that
// owns the device reads that flag to leak it too, and `ran == false` alone cannot
// tell this apart from an ordinary setup failure (where teardown already ran and
// destroying the device is correct).
TEST(PrimitivesGeneratedNoXfbProbeTeardownTest, AFenceTimeoutLeaksEverythingAndReportsItself) {
PrimitivesGeneratedNoXfbProbeContext context = FakeProbeContext();
g_fake.fenceWaitResult = VK_TIMEOUT;
const PrimitivesGeneratedNoXfbMeasurement measurement = RunPrimitivesGeneratedNoXfbProbe(context);
EXPECT_FALSE(measurement.ran);
EXPECT_TRUE(measurement.fenceWaitTimedOut)
<< "without this flag the POST destroys its throwaway VkDevice while the probe's children "
"are alive and its submission may still be executing";
EXPECT_GT(g_fake.queueSubmitCalls, 0u) << "the timeout must be the SUBMITTED probe's, not a setup failure";
EXPECT_EQ(g_fake.destroyCalls, 0u)
<< "a probe that timed out must destroy nothing: the submission may still be executing";
EXPECT_EQ(g_fake.deviceWaitIdleCalls, 0u)
<< "vkDeviceWaitIdle on a queue that missed a 5 s deadline is the hang the bound exists to "
"prevent";
// The verdict must not read a timed-out probe as anything but "no verdict".
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(measurement),
PrimitivesGeneratedNoXfbVerdict::Inconclusive);
}
// The control: a driver that signals normally gets the ordinary teardown - idle
// wait, every object destroyed, no timeout flag - so the case above is testing the
// timeout branch and not a probe that never cleans up at all.
TEST(PrimitivesGeneratedNoXfbProbeTeardownTest, ASignalledFenceTearsDownNormally) {
PrimitivesGeneratedNoXfbProbeContext context = FakeProbeContext();
g_fake.fenceWaitResult = VK_SUCCESS;
g_fake.streamGenerated = 1; // healthy: the capture-less draws are counted
const PrimitivesGeneratedNoXfbMeasurement measurement = RunPrimitivesGeneratedNoXfbProbe(context);
EXPECT_TRUE(measurement.ran) << measurement.failureReason;
EXPECT_FALSE(measurement.fenceWaitTimedOut);
EXPECT_EQ(g_fake.deviceWaitIdleCalls, 1u);
EXPECT_GT(g_fake.destroyCalls, 0u);
EXPECT_EQ(EvaluatePrimitivesGeneratedNoXfbVerdict(measurement),
PrimitivesGeneratedNoXfbVerdict::StreamCounts);
}