diff --git a/MobileGL/MG_IntegrationTest/Scenarios/PrimitivesGeneratedNoXfbScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/PrimitivesGeneratedNoXfbScenario.cpp index 711a4bd5..3b43be66 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/PrimitivesGeneratedNoXfbScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/PrimitivesGeneratedNoXfbScenario.cpp @@ -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> stages, - bool withCaptureVarying) { + const char* captureVarying) { std::vector 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}, - {GL_TESS_CONTROL_SHADER, kTessControlSource}, - {GL_TESS_EVALUATION_SHADER, kTessEvalSource}}, - false); + return BuildProgram( + {{GL_VERTEX_SHADER, kTessVertexSource}, + {GL_TESS_CONTROL_SHADER, kTessControlSource}, + {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 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 diff --git a/MobileGL/MG_Test/SelfTest/PrimitivesGeneratedNoXfbProbeTest.cpp b/MobileGL/MG_Test/SelfTest/PrimitivesGeneratedNoXfbProbeTest.cpp index eb0f01ed..26818549 100644 --- a/MobileGL/MG_Test/SelfTest/PrimitivesGeneratedNoXfbProbeTest.cpp +++ b/MobileGL/MG_Test/SelfTest/PrimitivesGeneratedNoXfbProbeTest.cpp @@ -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 +#include + #include 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 + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + } + 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(); + 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(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); +}