mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[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:
@@ -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},
|
||||
{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<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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user