mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +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() {
|
void main() {
|
||||||
gl_Position = vec4(gl_TessCoord.xy * 2.0 - 1.0, 0.0, 1.0);
|
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 {
|
class PrimitivesGeneratedNoXfbScenario : public ScenarioTest {
|
||||||
@@ -145,8 +157,10 @@ void main() {
|
|||||||
ScenarioTest::TearDown();
|
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,
|
GLuint BuildProgram(std::initializer_list<std::pair<GLenum, const char*>> stages,
|
||||||
bool withCaptureVarying) {
|
const char* captureVarying) {
|
||||||
std::vector<GLuint> shaders;
|
std::vector<GLuint> shaders;
|
||||||
for (const auto& [type, source] : stages) {
|
for (const auto& [type, source] : stages) {
|
||||||
const GLuint shader = CompileShaderStage(type, source, &m_buildLog);
|
const GLuint shader = CompileShaderStage(type, source, &m_buildLog);
|
||||||
@@ -158,9 +172,8 @@ void main() {
|
|||||||
}
|
}
|
||||||
const GLuint program = glCreateProgram();
|
const GLuint program = glCreateProgram();
|
||||||
for (const GLuint shader : shaders) glAttachShader(program, shader);
|
for (const GLuint shader : shaders) glAttachShader(program, shader);
|
||||||
if (withCaptureVarying) {
|
if (captureVarying != nullptr) {
|
||||||
const char* varying = "vs_out_value";
|
glTransformFeedbackVaryings(program, 1, &captureVarying, GL_INTERLEAVED_ATTRIBS);
|
||||||
glTransformFeedbackVaryings(program, 1, &varying, GL_INTERLEAVED_ATTRIBS);
|
|
||||||
}
|
}
|
||||||
glLinkProgram(program);
|
glLinkProgram(program);
|
||||||
for (const GLuint shader : shaders) glDeleteShader(shader);
|
for (const GLuint shader : shaders) glDeleteShader(shader);
|
||||||
@@ -180,19 +193,41 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
GLuint BuildCaptureProgram() {
|
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;
|
GLint maxTessGenLevel = 0;
|
||||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||||
while (glGetError() != GL_NO_ERROR) {
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
}
|
}
|
||||||
if (maxTessGenLevel < 1) return 0;
|
if (maxTessGenLevel < 1) return 0;
|
||||||
return BuildProgram({{GL_VERTEX_SHADER, kTessVertexSource},
|
return BuildProgram(
|
||||||
|
{{GL_VERTEX_SHADER, kTessVertexSource},
|
||||||
{GL_TESS_CONTROL_SHADER, kTessControlSource},
|
{GL_TESS_CONTROL_SHADER, kTessControlSource},
|
||||||
{GL_TESS_EVALUATION_SHADER, kTessEvalSource}},
|
{GL_TESS_EVALUATION_SHADER,
|
||||||
false);
|
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.
|
// GENERATED query around `record()`, answered with GL_QUERY_RESULT.
|
||||||
@@ -240,6 +275,7 @@ void main() {
|
|||||||
|
|
||||||
GLuint m_vao = 0;
|
GLuint m_vao = 0;
|
||||||
GLuint m_queries[2] = {0, 0}; // [0]=written, [1]=generated
|
GLuint m_queries[2] = {0, 0}; // [0]=written, [1]=generated
|
||||||
|
GLuint m_captureBuffer = 0;
|
||||||
std::vector<GLuint> m_programs;
|
std::vector<GLuint> m_programs;
|
||||||
std::string m_buildLog;
|
std::string m_buildLog;
|
||||||
};
|
};
|
||||||
@@ -360,6 +396,113 @@ void main() {
|
|||||||
EXPECT_EQ(written, 1u) << "only the draw inside the span writes anything";
|
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 ONE CASE THAT CAN FAIL WHEN THE REROUTE SILENTLY STOPS BEING ARMED -
|
||||||
// the UnlocatedIoBlockScenario shape, for the same reason: every case above
|
// the UnlocatedIoBlockScenario shape, for the same reason: every case above
|
||||||
// is green here whether the reroute ran or not (that is the "two pools
|
// is green here whether the reroute ran or not (that is the "two pools
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
//
|
//
|
||||||
// The primitives-generated-without-transform-feedback probe's VERDICT and ARMING
|
// The primitives-generated-without-transform-feedback probe's VERDICT and ARMING
|
||||||
// logic, pinned over synthetic measurements. The Vulkan plumbing needs a GPU; the
|
// logic, pinned over synthetic measurements. Recording the probe for real needs a
|
||||||
// two pure functions are where the cheap mistakes live - a verdict that reads 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
|
// 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
|
// 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
|
// characterised is written down here as a fake measurement so the mapping cannot
|
||||||
@@ -20,21 +20,34 @@
|
|||||||
// discard (llvmpipe's discard short-circuit),
|
// discard (llvmpipe's discard short-circuit),
|
||||||
// - the same driver without the dedicated query - the statistics tiers,
|
// - the same driver without the dedicated query - the statistics tiers,
|
||||||
// - a device with the defect and no working substitute,
|
// - 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).
|
// - 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 <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
#include <MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h>
|
#include <MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h>
|
||||||
|
|
||||||
using MobileGL::Bool;
|
using MobileGL::Bool;
|
||||||
|
using MobileGL::Uint32;
|
||||||
using MobileGL::Uint64;
|
using MobileGL::Uint64;
|
||||||
using MobileGL::MG_Config::QuirkOverride;
|
using MobileGL::MG_Config::QuirkOverride;
|
||||||
using MobileGL::MG_Util::SelfTest::EvaluatePrimitivesGeneratedNoXfbVerdict;
|
using MobileGL::MG_Util::SelfTest::EvaluatePrimitivesGeneratedNoXfbVerdict;
|
||||||
using MobileGL::MG_Util::SelfTest::ChoosePrimitivesGeneratedReroute;
|
using MobileGL::MG_Util::SelfTest::ChoosePrimitivesGeneratedReroute;
|
||||||
using MobileGL::MG_Util::SelfTest::PrimGenRerouteKind;
|
using MobileGL::MG_Util::SelfTest::PrimGenRerouteKind;
|
||||||
using MobileGL::MG_Util::SelfTest::PrimitivesGeneratedNoXfbMeasurement;
|
using MobileGL::MG_Util::SelfTest::PrimitivesGeneratedNoXfbMeasurement;
|
||||||
|
using MobileGL::MG_Util::SelfTest::PrimitivesGeneratedNoXfbProbeContext;
|
||||||
using MobileGL::MG_Util::SelfTest::PrimitivesGeneratedNoXfbShapeMeasurement;
|
using MobileGL::MG_Util::SelfTest::PrimitivesGeneratedNoXfbShapeMeasurement;
|
||||||
using MobileGL::MG_Util::SelfTest::PrimitivesGeneratedNoXfbVerdict;
|
using MobileGL::MG_Util::SelfTest::PrimitivesGeneratedNoXfbVerdict;
|
||||||
|
using MobileGL::MG_Util::SelfTest::RunPrimitivesGeneratedNoXfbProbe;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
struct ShapeAnswers {
|
struct ShapeAnswers {
|
||||||
@@ -139,6 +152,31 @@ TEST(PrimitivesGeneratedNoXfbVerdictTest, StatisticsDeadUnderDiscardIsThePlainOn
|
|||||||
PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly);
|
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
|
// 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
|
// OVERCOUNTS the plain shape (as disqualifying as one that reads 0 - an exact
|
||||||
// match is what qualifies a substitute).
|
// match is what qualifies a substitute).
|
||||||
@@ -266,3 +304,243 @@ TEST(PrimitivesGeneratedNoXfbArmingTest, AutoFollowsExactlyTheSubstituteVerdicts
|
|||||||
QuirkOverride::Auto, PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute, false, false),
|
QuirkOverride::Auto, PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitute, false, false),
|
||||||
PrimGenRerouteKind::None);
|
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