mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +09:00
[Fix] (DirectVulkan, SelfTest): honour the primitives-generated probe's leak-on-timeout contract in both of its callers, count paused-span draws the frontend cannot price, refuse a substitute measured worse than the stream query, and derive the POST row's failure clause from the measurement
This commit is contained in:
@@ -1681,7 +1681,21 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
fail(format("vkCreateDevice failed (VkResult = {})", static_cast<Int>(createResult)));
|
||||
return;
|
||||
}
|
||||
const ScopeGuard destroyDevice([&]() { vkDestroyDeviceFn(device, nullptr); });
|
||||
// The probe's own teardown destroys (and idle-waits) everything it created -
|
||||
// EXCEPT when its bounded fence wait expires, where it deliberately leaks
|
||||
// every child object rather than touch a possibly hung GPU. This device must
|
||||
// then leak with them: vkDestroyDevice requires its children destroyed and its
|
||||
// queues idle, and on the driver that just missed a 5 s deadline the realistic
|
||||
// outcome is a block inside vkDestroyDevice - the POST hang the bound exists to
|
||||
// prevent. Same shape as the timestamp probe's guard above and the iterationRP
|
||||
// witness's below.
|
||||
Bool probeFenceWaitTimedOut = false;
|
||||
const ScopeGuard destroyDevice([&]() {
|
||||
if (probeFenceWaitTimedOut) {
|
||||
return;
|
||||
}
|
||||
vkDestroyDeviceFn(device, nullptr);
|
||||
});
|
||||
|
||||
VkQueue queue = VK_NULL_HANDLE;
|
||||
vkGetDeviceQueueFn(device, graphicsQueueFamilyIndex, 0, &queue);
|
||||
@@ -1750,6 +1764,8 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
|
||||
const PrimitivesGeneratedNoXfbMeasurement measurement =
|
||||
RunPrimitivesGeneratedNoXfbProbe(probeContext);
|
||||
// Before any return below: the guard above owns the device and must know.
|
||||
probeFenceWaitTimedOut = measurement.fenceWaitTimedOut;
|
||||
if (!measurement.ran) {
|
||||
fail(format("the probe could not run ({}); the renderer's bring-up probe decides the "
|
||||
"reroute independently",
|
||||
@@ -1780,6 +1796,52 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
shapeFacts("triangles under discard", measurement.trianglesDiscard) +
|
||||
"; " + shapeFacts("patches under discard", measurement.patchesDiscard);
|
||||
|
||||
const auto statisticsExactOn = [](const PrimitivesGeneratedNoXfbShapeMeasurement& shape) {
|
||||
return shape.statisticsMeasured && shape.statisticsClippingInput == shape.expectedPrimitives;
|
||||
};
|
||||
// What the PLAIN-ONLY verdict actually measured, named from the numbers rather
|
||||
// than assumed: the shape the substitute misses may be the tessellated one
|
||||
// alone, and a missed shape may read a wrong NONZERO count rather than 0. A
|
||||
// row that always blamed rasterizer discard would put a false statement about
|
||||
// the driver into the campaign's evidence artifact, contradicted by the facts
|
||||
// string printed right after it.
|
||||
const auto describeMissedStatisticsShapes = [&]() {
|
||||
String missed;
|
||||
const auto note = [&](const char* name,
|
||||
const PrimitivesGeneratedNoXfbShapeMeasurement& shape) {
|
||||
if (!shape.drawn || statisticsExactOn(shape)) {
|
||||
return;
|
||||
}
|
||||
if (!missed.empty()) {
|
||||
missed += " and ";
|
||||
}
|
||||
missed += name;
|
||||
missed += shape.statisticsMeasured
|
||||
? format(" (read {} of {} expected)", shape.statisticsClippingInput,
|
||||
shape.expectedPrimitives)
|
||||
: String(" (its statistics slot did not read back)");
|
||||
};
|
||||
note("the plain draw", measurement.trianglesPlain);
|
||||
note("triangles under rasterizer discard", measurement.trianglesDiscard);
|
||||
note("patches under rasterizer discard", measurement.patchesDiscard);
|
||||
return missed;
|
||||
};
|
||||
// The CTS's tessellator-measuring shape is a PATCHES draw under discard; say
|
||||
// whether THIS driver's substitute covers it instead of assuming it does not.
|
||||
const auto describeCtsShape = [&]() -> String {
|
||||
if (!measurement.patchesDiscard.drawn) {
|
||||
return "the CTS's tessellator-measuring shape (a PATCHES draw under discard) could "
|
||||
"not be measured here - this device has no tessellationShader - so whether "
|
||||
"the substitute covers it is unknown";
|
||||
}
|
||||
return statisticsExactOn(measurement.patchesDiscard)
|
||||
? "the CTS's tessellator-measuring shape (a PATCHES draw under discard) is "
|
||||
"NOT among them: the substitute answers it exactly, so those tests are "
|
||||
"repaired"
|
||||
: "the CTS's tessellator-measuring shape (a PATCHES draw under discard) is "
|
||||
"among them, so those tests stay broken on this driver";
|
||||
};
|
||||
|
||||
switch (EvaluatePrimitivesGeneratedNoXfbVerdict(measurement)) {
|
||||
case PrimitivesGeneratedNoXfbVerdict::StreamCounts:
|
||||
builder.Pass(RowName,
|
||||
@@ -1812,18 +1874,51 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
case PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly:
|
||||
fail("the stream query answers 0 for a draw made with no capture span open, and the "
|
||||
"clipping-invocations statistics substitute counts the plain draw exactly but "
|
||||
"reads 0 under rasterizer discard - so the renderer reroutes XFB-inactive draws "
|
||||
"(repairing undiscarded queries at no cost to the rest) and the CTS's "
|
||||
"tessellator-measuring shape, which needs the discarded count, remains broken "
|
||||
"on this driver (" +
|
||||
facts + ")");
|
||||
"misses " +
|
||||
describeMissedStatisticsShapes() +
|
||||
" - each of them a shape the stream query answered 0 for as well, so the renderer "
|
||||
"reroutes XFB-inactive draws (repairing every shape the substitute answers, at no "
|
||||
"cost to the rest, which is what the verdict requires); " +
|
||||
describeCtsShape() + " (" + facts + ")");
|
||||
return;
|
||||
case PrimitivesGeneratedNoXfbVerdict::Unfixable:
|
||||
case PrimitivesGeneratedNoXfbVerdict::Unfixable: {
|
||||
// Two ways to land here, and the report must not conflate them: no
|
||||
// substitute answers even the plain draw, or one does but it is WRONG on a
|
||||
// shape the stream query answers EXACTLY - arming it would trade a correct
|
||||
// answer for a wrong one, so MobileGL refuses (see the verdict's
|
||||
// domination rule).
|
||||
String downgradeShapes;
|
||||
const auto noteDowngrade = [&](const char* name,
|
||||
const PrimitivesGeneratedNoXfbShapeMeasurement& shape) {
|
||||
if (!shape.drawn || statisticsExactOn(shape) ||
|
||||
shape.streamGenerated != shape.expectedPrimitives) {
|
||||
return;
|
||||
}
|
||||
if (!downgradeShapes.empty()) {
|
||||
downgradeShapes += " and ";
|
||||
}
|
||||
downgradeShapes += name;
|
||||
};
|
||||
noteDowngrade("the plain draw", measurement.trianglesPlain);
|
||||
noteDowngrade("triangles under rasterizer discard", measurement.trianglesDiscard);
|
||||
noteDowngrade("patches under rasterizer discard", measurement.patchesDiscard);
|
||||
if (statisticsExactOn(measurement.trianglesPlain) && !downgradeShapes.empty()) {
|
||||
fail("the stream query answers 0 for a draw made with no capture span open, and the "
|
||||
"clipping-invocations statistics substitute repairs the plain draw but is wrong "
|
||||
"on " +
|
||||
downgradeShapes +
|
||||
", which the stream query answers exactly - rerouting every XFB-inactive draw "
|
||||
"would trade a correct count for a wrong one, so MobileGL arms nothing and the "
|
||||
"capture-less query keeps the driver's answers (" +
|
||||
facts + ")");
|
||||
return;
|
||||
}
|
||||
fail("the stream query answers 0 for a draw made with no capture span open and the "
|
||||
"device offers no working statistics substitute; an application sizing a capture "
|
||||
"buffer from GL_PRIMITIVES_GENERATED gets 0 (" +
|
||||
facts + ")");
|
||||
return;
|
||||
}
|
||||
case PrimitivesGeneratedNoXfbVerdict::Inconclusive:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -96,14 +96,17 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
VkPipeline triangleDiscardPipeline = VK_NULL_HANDLE;
|
||||
VkPipeline patchDiscardPipeline = VK_NULL_HANDLE;
|
||||
VkFence fence = VK_NULL_HANDLE;
|
||||
Bool fenceWaitTimedOut = false;
|
||||
|
||||
// Teardown on every path. When the fence wait timed out the submission may
|
||||
// still be executing on a hung GPU: vkDeviceWaitIdle could block forever
|
||||
// and destroying in-flight objects is undefined, so everything is
|
||||
// deliberately leaked - a hung GPU must not hang the caller.
|
||||
// deliberately leaked - a hung GPU must not hang the caller. The same flag
|
||||
// is returned in the measurement, because a caller that OWNS the device must
|
||||
// make the same choice for it (see the header): destroying a device whose
|
||||
// children are alive and whose queue may still be executing is the very hang
|
||||
// this bound exists to prevent.
|
||||
const ProbeScopeGuard teardown([&]() {
|
||||
if (fenceWaitTimedOut) {
|
||||
if (measurement.fenceWaitTimedOut) {
|
||||
return;
|
||||
}
|
||||
fns.vkDeviceWaitIdle(device);
|
||||
@@ -393,7 +396,9 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
}
|
||||
constexpr Uint64 kFenceTimeoutNs = 5'000'000'000ull; // a probe must never hang its caller
|
||||
if (fns.vkWaitForFences(device, 1, &fence, VK_TRUE, kFenceTimeoutNs) != VK_SUCCESS) {
|
||||
fenceWaitTimedOut = true; // see the scope guard
|
||||
// Set BEFORE failing: the scope guard reads it to skip every destroy, and
|
||||
// the caller reads it out of the measurement to skip destroying the device.
|
||||
measurement.fenceWaitTimedOut = true;
|
||||
return fail("the probe submission did not complete within 5 s");
|
||||
}
|
||||
|
||||
@@ -453,6 +458,15 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
Bool allStreamExact = true;
|
||||
Bool allPrimitivesGeneratedExtExact = true;
|
||||
Bool allStatisticsExact = true;
|
||||
// Whether the statistics substitute DOMINATES the stream query shape by shape:
|
||||
// every shape the statistics do not answer exactly must be one the stream query
|
||||
// answered 0 for anyway. Without this, a plain-shape-only substitute could be
|
||||
// armed on a device whose stream query was RIGHT on a shape the statistics get
|
||||
// wrong - and the renderer reroutes every XFB-inactive draw, so that shape would
|
||||
// be downgraded from correct to wrong. "Never worse per draw" is what makes
|
||||
// arming on an uncharacterised driver defensible; it has to be measured, not
|
||||
// assumed.
|
||||
Bool statisticsDominateStream = true;
|
||||
for (const auto* shape : shapes) {
|
||||
if (!shape->drawn) {
|
||||
continue;
|
||||
@@ -476,6 +490,11 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
if (!shape->statisticsMeasured ||
|
||||
shape->statisticsClippingInput != shape->expectedPrimitives) {
|
||||
allStatisticsExact = false;
|
||||
// Only a shape the stream query was silent on may be left behind by
|
||||
// the substitute; a shape it answered exactly must not be traded away.
|
||||
if (shape->streamGenerated == shape->expectedPrimitives) {
|
||||
statisticsDominateStream = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (allStreamExact) {
|
||||
@@ -492,8 +511,11 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
const auto& plain = measurement.trianglesPlain;
|
||||
const Bool plainStatisticsExact =
|
||||
plain.statisticsMeasured && plain.statisticsClippingInput == plain.expectedPrimitives;
|
||||
return plainStatisticsExact ? PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly
|
||||
: PrimitivesGeneratedNoXfbVerdict::Unfixable;
|
||||
// Both halves are required: the substitute must repair the plain shape, AND it
|
||||
// must not cost any shape an answer the stream query already had right.
|
||||
return (plainStatisticsExact && statisticsDominateStream)
|
||||
? PrimitivesGeneratedNoXfbVerdict::StatisticsSubstitutePlainOnly
|
||||
: PrimitivesGeneratedNoXfbVerdict::Unfixable;
|
||||
}
|
||||
|
||||
PrimGenRerouteKind ChoosePrimitivesGeneratedReroute(MG_Config::QuirkOverride overrideSetting,
|
||||
|
||||
@@ -102,6 +102,14 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
// The probe submitted and read back at least the two triangle shapes.
|
||||
// False when any setup step failed; failureReason then names the step.
|
||||
Bool ran = false;
|
||||
// The probe's bounded fence wait expired with the submission possibly
|
||||
// still executing. The probe then deliberately LEAKED every child object
|
||||
// it created (no vkDeviceWaitIdle, no destroys - a hung GPU must not hang
|
||||
// the caller), so a caller that owns the device MUST NOT destroy or
|
||||
// idle-wait it either: vkDestroyDevice with live children and in-flight
|
||||
// work is the exact hang the bound exists to prevent. The POST leaks its
|
||||
// throwaway device on this flag, mirroring its sibling probes.
|
||||
Bool fenceWaitTimedOut = false;
|
||||
String failureReason;
|
||||
PrimitivesGeneratedNoXfbShapeMeasurement trianglesPlain;
|
||||
PrimitivesGeneratedNoXfbShapeMeasurement trianglesDiscard;
|
||||
@@ -200,15 +208,22 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
// proven whole through clipping statistics.
|
||||
StatisticsSubstitute,
|
||||
// The defect is present and the statistics control is exact on the PLAIN
|
||||
// shape but silent or wrong under rasterizer discard (llvmpipe's discard
|
||||
// short-circuit does this to its statistics - its dedicated query is what
|
||||
// rescues it to the verdict above). Rerouting still repairs every
|
||||
// undiscarded query and is never worse per draw - a rerouted draw would
|
||||
// have answered 0 through the silent stream query - but the CTS shape
|
||||
// stays broken and the report must say so.
|
||||
// shape but not on every drawn shape (llvmpipe's discard short-circuit
|
||||
// does this to its statistics - its dedicated query is what rescues it to
|
||||
// the verdict above). This verdict additionally GUARANTEES domination:
|
||||
// every shape the statistics missed measured exactly 0 through the stream
|
||||
// query too, so rerouting is never worse per draw - it repairs every
|
||||
// shape the statistics answer exactly and leaves the rest at the 0 they
|
||||
// already read. A measurement where the stream was EXACT on a shape the
|
||||
// statistics missed does not qualify (rerouting would downgrade that
|
||||
// shape) and falls to Unfixable instead. The shapes the substitute
|
||||
// misses - the CTS's discarded shapes among them wherever they are the
|
||||
// missed ones - stay broken, and the report must say which.
|
||||
StatisticsSubstitutePlainOnly,
|
||||
// The defect is present and no substitute qualifies even for the plain
|
||||
// shape: the honest verdict is the current behaviour.
|
||||
// The defect is present and no substitute qualifies: none is exact
|
||||
// everywhere, and the plain-only fallback either misses the plain shape
|
||||
// or fails the domination rule above. The honest verdict is the current
|
||||
// behaviour.
|
||||
Unfixable,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user