mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
[Test] (MG_Test): cover the cancelled-SPIR-V program, the buffered-write valve and drop, the delete-does-not-block bound and the zero-thread join
This commit is contained in:
@@ -28,6 +28,7 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -94,6 +95,9 @@ void main() {
|
||||
source += "uniform vec3 uTint" + n + ";\n";
|
||||
source += "uniform float uArr" + n + "[4];\n";
|
||||
source += "uniform float uSeed" + n + ";\n";
|
||||
// An OPAQUE uniform, so the window cases can pin the "glUniform1i(samplerLoc, unit)
|
||||
// right after a link is a zero-join operation" claim the split is built around.
|
||||
source += "uniform sampler2D uTex" + n + ";\n";
|
||||
source += "void main() {\n";
|
||||
source += " float acc = uSeed" + n + ";\n";
|
||||
for (int i = 0; i < 200; ++i) {
|
||||
@@ -102,7 +106,8 @@ void main() {
|
||||
source += " vec4 p = uModel" + n + " * vec4(vPos, 1.0);\n";
|
||||
source += " acc += p.x + p.y + p.z + p.w;\n";
|
||||
source += " acc += uArr" + n + "[0] + uArr" + n + "[1] + uArr" + n + "[2] + uArr" + n + "[3];\n";
|
||||
source += " fragColor = vec4(uTint" + n + " * acc, 1.0);\n";
|
||||
source += " vec4 t = texture(uTex" + n + ", vPos.xy);\n";
|
||||
source += " fragColor = vec4(uTint" + n + " * acc, 1.0) * t;\n";
|
||||
source += "}\n";
|
||||
return source;
|
||||
}
|
||||
@@ -277,10 +282,15 @@ void main() { fragColor = thisIdentifierWasNeverDeclared; }
|
||||
|
||||
const auto& object = Object(program);
|
||||
ASSERT_NE(object, nullptr);
|
||||
// Phase B settled (as cancelled) rather than being left in flight, so nothing can
|
||||
// block on it later, and it published nothing.
|
||||
EXPECT_TRUE(object->IsSpirvComplete());
|
||||
// GetSpirvStatus() FIRST, and the order is load-bearing rather than stylistic: phase B
|
||||
// is settled by a continuation that runs on whichever worker drove phase A terminal,
|
||||
// and JobNode::TryTransition releases waiters (notify_all) BEFORE it runs its
|
||||
// continuation list - so the GL thread can be back here with phase A published while
|
||||
// that one-line lambda has not run yet. GetSpirvStatus() goes through the phase-B
|
||||
// gate, which Waits; only after it has can IsSpirvComplete() be asserted without a
|
||||
// race. Asserting the other way round is a rare CI flake, not a red.
|
||||
EXPECT_FALSE(object->GetSpirvStatus());
|
||||
EXPECT_TRUE(object->IsSpirvComplete());
|
||||
EXPECT_TRUE(object->GetGeneratedSpirv().empty());
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
@@ -305,8 +315,123 @@ void main() { fragColor = vec4(1.0); }
|
||||
EXPECT_EQ(QueryLinkStatus(program), GL_FALSE) << "a fragment output past GL_MAX_DRAW_BUFFERS must fail the link";
|
||||
const auto& object = Object(program);
|
||||
ASSERT_NE(object, nullptr);
|
||||
EXPECT_TRUE(object->IsSpirvComplete());
|
||||
// GetSpirvStatus() before IsSpirvComplete(); see ALinkThatFailsNeverProducesSpirv.
|
||||
EXPECT_FALSE(object->GetSpirvStatus());
|
||||
EXPECT_TRUE(object->IsSpirvComplete());
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// Linked, but the SPIR-V never arrived
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
// The state the split invented and GL gives no way out of: phase A published LINK_STATUS
|
||||
// GL_TRUE, and phase B then settled CANCELLED rather than Complete - its body threw
|
||||
// (bad_alloc out of GlslangToSpv/spirv-opt under pack-load memory pressure), the pool failed
|
||||
// to enqueue it, or teardown cancelled it while it was queued. The shadow is then a
|
||||
// default-constructed SpirvArtifacts: empty uniformOffsets, null scratch, spirvStatus false -
|
||||
// while the whole phase-A query surface, IsValidUniformLocation() included, keeps answering.
|
||||
//
|
||||
// Every getter and every entry point on that surface must degrade, not fault. Before the
|
||||
// bounds check in GetUniformOffset this was a null dereference on the first glUniform* or
|
||||
// glGetUniform* the application made.
|
||||
//
|
||||
// The state is produced deterministically rather than by racing a cancel: after the
|
||||
// LINK_STATUS read has published phase A (m_pendingLink is null), CancelLink() can only reach
|
||||
// the SPIR-V job - which is exactly the shape StopAndDrain produces for a phase B queued
|
||||
// behind an already-complete phase A.
|
||||
TEST_F(AsyncSpirvPhaseTest, ALinkedProgramWhoseSpirvJobWasCancelledDegradesInsteadOfFaulting) {
|
||||
const AsyncModeScope async(true);
|
||||
const SingleWorkerScope oneWorker;
|
||||
constexpr int kPrograms = 12;
|
||||
constexpr int kFirst = 38000;
|
||||
|
||||
Vector<String> sources;
|
||||
const Vector<GLuint> programs = LinkBatch(kPrograms, kFirst, sources);
|
||||
|
||||
int cancelled = 0;
|
||||
for (int i = 0; i < kPrograms; ++i) {
|
||||
const GLuint program = programs[static_cast<SizeT>(i)];
|
||||
const String n = std::to_string(kFirst + i);
|
||||
ASSERT_EQ(QueryLinkStatus(program), GL_TRUE) << QueryProgramInfoLog(program);
|
||||
const auto& object = Object(program);
|
||||
ASSERT_NE(object, nullptr);
|
||||
if (object->IsSpirvComplete()) continue; // already published; not the state under test
|
||||
|
||||
object->CancelLink(); // phase A is published, so this reaches only the SPIR-V job
|
||||
++cancelled;
|
||||
|
||||
// ---- the contract: linked, fully queryable, not drawable ----
|
||||
EXPECT_TRUE(object->GetLinkStatus()) << "a cancelled phase B must not retract LINK_STATUS";
|
||||
EXPECT_EQ(QueryLinkStatus(program), GL_TRUE);
|
||||
EXPECT_FALSE(object->GetSpirvStatus());
|
||||
EXPECT_TRUE(object->IsSpirvComplete());
|
||||
// This is the expression both backends' bind gates evaluate.
|
||||
EXPECT_FALSE(object->GetLinkStatus() && object->GetSpirvStatus())
|
||||
<< "the backends must refuse to bind a program with no SPIR-V";
|
||||
EXPECT_TRUE(object->GetGeneratedSpirv().empty());
|
||||
EXPECT_EQ(object->GetUBOSize(), 0u);
|
||||
EXPECT_EQ(object->GetUBOData(), nullptr);
|
||||
|
||||
// ---- the reflection surface still answers ----
|
||||
const GLint locModel = GetUniformLocation(program, ("uModel" + n).c_str());
|
||||
const GLint locTint = GetUniformLocation(program, ("uTint" + n).c_str());
|
||||
const GLint locTex = GetUniformLocation(program, ("uTex" + n).c_str());
|
||||
ASSERT_GE(locModel, 0);
|
||||
ASSERT_GE(locTint, 0);
|
||||
ASSERT_GE(locTex, 0);
|
||||
EXPECT_TRUE(object->IsValidUniformLocation(locTint));
|
||||
|
||||
// ---- and every write/read path degrades ----
|
||||
// Direct getter first: kInvalidUniformOffset, not an out-of-bounds index.
|
||||
EXPECT_EQ(object->GetUniformOffset(static_cast<Uint>(locTint)),
|
||||
MG_State::GLState::ProgramObject::kInvalidUniformOffset);
|
||||
EXPECT_EQ(object->GetUniformOffset(object->GetMaxUniformLocation()),
|
||||
MG_State::GLState::ProgramObject::kInvalidUniformOffset);
|
||||
|
||||
const GLfloat tint[3] = {1.0f, 2.0f, 3.0f};
|
||||
GLfloat model[16] = {};
|
||||
for (int c = 0; c < 16; ++c) model[c] = static_cast<GLfloat>(c);
|
||||
ProgramUniform3fv(program, locTint, 1, tint); // dropped, not faulted
|
||||
ProgramUniformMatrix4fv(program, locModel, 1, GL_FALSE, model); // ditto
|
||||
UseProgram(program);
|
||||
Uniform3fv(locTint, 1, tint); // the glUseProgram + glUniform* entry, same verdict
|
||||
UseProgram(0);
|
||||
|
||||
// The opaque branch never touches phase B, so it keeps working in full.
|
||||
ProgramUniform1i(program, locTex, 3);
|
||||
GLint unit = -1;
|
||||
GetUniformiv(program, locTex, &unit);
|
||||
EXPECT_EQ(unit, 3) << "sampler units are phase-A state and must survive a lost phase B";
|
||||
|
||||
// Reads leave the caller's buffer alone rather than faulting.
|
||||
GLfloat readback[16] = {};
|
||||
for (int c = 0; c < 16; ++c) readback[c] = -1.0f;
|
||||
GetUniformfv(program, locModel, readback);
|
||||
for (int c = 0; c < 16; ++c) {
|
||||
EXPECT_FLOAT_EQ(readback[c], -1.0f) << "a program with no shadow must not write the query buffer";
|
||||
}
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
EXPECT_GT(cancelled, 0) << "no phase B was ever cancelled; this case proved nothing";
|
||||
}
|
||||
|
||||
// The mirror image: with async off the state is not reachable at all, because both bodies run
|
||||
// inline before glLinkProgram returns. Worth pinning - it is what makes the async-off mode a
|
||||
// usable fallback for a device where the state above would be a problem.
|
||||
TEST_F(AsyncSpirvPhaseTest, AsyncOffNeverProducesALinkedProgramWithoutSpirv) {
|
||||
const AsyncModeScope async(false);
|
||||
Vector<String> sources;
|
||||
const Vector<GLuint> programs = LinkBatch(4, 39000, sources);
|
||||
for (const GLuint program : programs) {
|
||||
const auto& object = Object(program);
|
||||
ASSERT_NE(object, nullptr);
|
||||
EXPECT_EQ(QueryLinkStatus(program), GL_TRUE) << QueryProgramInfoLog(program);
|
||||
EXPECT_TRUE(object->IsSpirvComplete()) << "nothing may be outstanding when async is off";
|
||||
EXPECT_TRUE(object->GetSpirvStatus());
|
||||
EXPECT_GT(object->GetUBOSize(), 0u);
|
||||
}
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
@@ -331,9 +456,11 @@ TEST_F(AsyncSpirvPhaseTest, UniformWritesInsideTheWindowReplayExactly) {
|
||||
GLint locModel = -1;
|
||||
GLint locTint = -1;
|
||||
GLint locArr = -1;
|
||||
GLint locTex = -1;
|
||||
GLfloat model[16] = {};
|
||||
GLfloat tint[3] = {};
|
||||
GLfloat arr1 = 0.0f;
|
||||
GLint texUnit = 0;
|
||||
Bool wasBuffered = false;
|
||||
};
|
||||
Vector<Expectation> expectations;
|
||||
@@ -352,23 +479,47 @@ TEST_F(AsyncSpirvPhaseTest, UniformWritesInsideTheWindowReplayExactly) {
|
||||
e.locModel = GetUniformLocation(e.program, ("uModel" + n).c_str());
|
||||
e.locTint = GetUniformLocation(e.program, ("uTint" + n).c_str());
|
||||
e.locArr = GetUniformLocation(e.program, ("uArr" + n + "[0]").c_str());
|
||||
e.locTex = GetUniformLocation(e.program, ("uTex" + n).c_str());
|
||||
ASSERT_GE(e.locModel, 0);
|
||||
ASSERT_GE(e.locTint, 0);
|
||||
ASSERT_GE(e.locArr, 0);
|
||||
ASSERT_GE(e.locTex, 0);
|
||||
|
||||
for (int c = 0; c < 16; ++c) e.model[c] = static_cast<GLfloat>(i) + static_cast<GLfloat>(c) * 0.25f;
|
||||
e.tint[0] = 0.125f * static_cast<GLfloat>(i);
|
||||
e.tint[1] = 0.25f * static_cast<GLfloat>(i);
|
||||
e.tint[2] = 0.5f * static_cast<GLfloat>(i);
|
||||
e.arr1 = 7.5f + static_cast<GLfloat>(i);
|
||||
e.texUnit = i % 8;
|
||||
|
||||
// An OPAQUE write inside the window. The design's load-bearing claim is that this is a
|
||||
// ZERO-JOIN operation - a sampler unit is phase-A state - and it is what Iris does
|
||||
// immediately after every glLinkProgram, so it is asserted rather than assumed.
|
||||
ProgramUniform1i(e.program, e.locTex, e.texUnit);
|
||||
if (e.wasBuffered) {
|
||||
EXPECT_FALSE(SpirvIsSettled(e.program))
|
||||
<< "glUniform1i on a sampler must not settle phase B (program " << e.program << ")";
|
||||
}
|
||||
|
||||
ProgramUniformMatrix4fv(e.program, e.locModel, 1, GL_FALSE, e.model);
|
||||
ProgramUniform3fv(e.program, e.locTint, 1, e.tint);
|
||||
// An element in the middle of an array, addressed by its own location.
|
||||
ProgramUniform1fv(e.program, e.locArr + 1, 1, &e.arr1);
|
||||
// Last write wins: overwrite the tint, so the replay's ordering is under test too.
|
||||
// Last write wins, and through the OTHER entry point for half the programs: the
|
||||
// Minecraft/Iris shape is glUseProgram + glUniform*, which reaches Uniform_State
|
||||
// through Uniformv_State/GetProgramForUniform rather than through the by-name form.
|
||||
e.tint[1] = 0.75f;
|
||||
ProgramUniform3fv(e.program, e.locTint, 1, e.tint);
|
||||
if ((i % 2) == 0) {
|
||||
ProgramUniform3fv(e.program, e.locTint, 1, e.tint);
|
||||
} else {
|
||||
UseProgram(e.program);
|
||||
Uniform3fv(e.locTint, 1, e.tint);
|
||||
UseProgram(0);
|
||||
}
|
||||
if (e.wasBuffered) {
|
||||
EXPECT_FALSE(SpirvIsSettled(e.program))
|
||||
<< "no glUniform* entry point may settle phase B (program " << e.program << ")";
|
||||
}
|
||||
|
||||
expectations.push_back(e);
|
||||
}
|
||||
@@ -389,6 +540,9 @@ TEST_F(AsyncSpirvPhaseTest, UniformWritesInsideTheWindowReplayExactly) {
|
||||
EXPECT_FLOAT_EQ(tint[c], e.tint[c]) << "program " << e.program << " tint component " << c;
|
||||
}
|
||||
EXPECT_FLOAT_EQ(arr1, e.arr1) << "program " << e.program << " array element 1";
|
||||
GLint unit = -1;
|
||||
GetUniformiv(e.program, e.locTex, &unit);
|
||||
EXPECT_EQ(unit, e.texUnit) << "program " << e.program << " sampler unit";
|
||||
// Reading them settled phase B, so the program is drawable now.
|
||||
const auto& object = Object(e.program);
|
||||
ASSERT_NE(object, nullptr);
|
||||
@@ -397,6 +551,67 @@ TEST_F(AsyncSpirvPhaseTest, UniformWritesInsideTheWindowReplayExactly) {
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The overflow valve. BufferUniformWrite declines past kMaxBufferedUniformBytes and the
|
||||
// caller falls through to the direct path - which JOINS, and therefore has to replay
|
||||
// everything already buffered BEFORE performing its own write, or last-write-wins breaks for
|
||||
// every uniform touched after the valve trips.
|
||||
TEST_F(AsyncSpirvPhaseTest, TheBufferedWriteValveFallsThroughToADirectWriteWithoutLosingOrder) {
|
||||
const AsyncModeScope async(true);
|
||||
const SingleWorkerScope oneWorker;
|
||||
constexpr int kPrograms = 8;
|
||||
constexpr int kFirst = 40000;
|
||||
// kMaxBufferedUniformBytes is 4 MiB and one mat4 write buffers 4 columns x 16 bytes, so
|
||||
// this many calls is comfortably past the valve.
|
||||
constexpr int kFloodWrites = 80000;
|
||||
|
||||
Vector<String> sources;
|
||||
const Vector<GLuint> programs = LinkBatch(kPrograms, kFirst, sources);
|
||||
|
||||
int flooded = 0;
|
||||
for (int i = 0; i < kPrograms; ++i) {
|
||||
const GLuint program = programs[static_cast<SizeT>(i)];
|
||||
const String n = std::to_string(kFirst + i);
|
||||
ASSERT_EQ(QueryLinkStatus(program), GL_TRUE) << QueryProgramInfoLog(program);
|
||||
if (SpirvIsSettled(program)) continue;
|
||||
++flooded;
|
||||
|
||||
const GLint locModel = GetUniformLocation(program, ("uModel" + n).c_str());
|
||||
const GLint locTint = GetUniformLocation(program, ("uTint" + n).c_str());
|
||||
ASSERT_GE(locModel, 0);
|
||||
ASSERT_GE(locTint, 0);
|
||||
|
||||
// A distinctive early value that must survive the valve: it is buffered, and the
|
||||
// fall-through write has to replay it before writing its own bytes.
|
||||
const GLfloat earlyTint[3] = {11.0f, 22.0f, 33.0f};
|
||||
ProgramUniform3fv(program, locTint, 1, earlyTint);
|
||||
|
||||
GLfloat model[16] = {};
|
||||
for (int w = 0; w < kFloodWrites; ++w) {
|
||||
for (int c = 0; c < 16; ++c) model[c] = static_cast<GLfloat>(w) + static_cast<GLfloat>(c);
|
||||
ProgramUniformMatrix4fv(program, locModel, 1, GL_FALSE, model);
|
||||
}
|
||||
|
||||
// Falling through joined, so the shadow is live and holds BOTH the buffered early
|
||||
// write and the last direct one.
|
||||
EXPECT_TRUE(SpirvIsSettled(program)) << "the valve must have fallen through to a joining write";
|
||||
GLfloat tintReadback[3] = {};
|
||||
GLfloat modelReadback[16] = {};
|
||||
GetUniformfv(program, locTint, tintReadback);
|
||||
GetUniformfv(program, locModel, modelReadback);
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
EXPECT_FLOAT_EQ(tintReadback[c], earlyTint[c])
|
||||
<< "the pre-valve buffered write was lost at component " << c;
|
||||
}
|
||||
for (int c = 0; c < 16; ++c) {
|
||||
EXPECT_FLOAT_EQ(modelReadback[c], static_cast<GLfloat>(kFloodWrites - 1) + static_cast<GLfloat>(c))
|
||||
<< "last-write-wins broke across the valve at component " << c;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_GT(flooded, 0) << "no program was ever observed inside the A->B window";
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The dedupe property the live write path has, preserved across the detour: replaying a record
|
||||
// that really changes bytes moves the UBO content version, and a bytes-identical write made
|
||||
// AFTER the replay does not move it. Both matter - the first is what makes a backend re-upload
|
||||
@@ -499,52 +714,151 @@ TEST_F(AsyncSpirvPhaseTest, RelinkingOverAPendingSpirvJobIsClean) {
|
||||
const SingleWorkerScope oneWorker;
|
||||
constexpr int kPrograms = 16;
|
||||
constexpr int kFirst = 35000;
|
||||
constexpr int kRelinkOffset = 500; // distinct uniform names for the second link
|
||||
|
||||
Vector<String> sources;
|
||||
const Vector<GLuint> programs = LinkBatch(kPrograms, kFirst, sources);
|
||||
// Built by hand rather than through LinkBatch, because the relink has to use DIFFERENT
|
||||
// source: relinking byte-identical source cannot tell the second link's artifacts from
|
||||
// the first's, so it would pass even if Link()'s prologue stopped resetting m_spirv.
|
||||
const GLuint vs = MakeShader(GL_VERTEX_SHADER, kVs);
|
||||
Vector<String> firstSources;
|
||||
Vector<String> secondSources;
|
||||
Vector<GLuint> programs;
|
||||
Vector<GLuint> fragmentShaders;
|
||||
for (int i = 0; i < kPrograms; ++i) {
|
||||
firstSources.push_back(MakeUniformSource(kFirst + i));
|
||||
const char* text = firstSources.back().c_str();
|
||||
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
||||
ShaderSource(fs, 1, &text, nullptr);
|
||||
CompileShader(fs);
|
||||
const GLuint program = CreateProgram();
|
||||
AttachShader(program, vs);
|
||||
AttachShader(program, fs);
|
||||
LinkProgram(program);
|
||||
programs.push_back(program);
|
||||
fragmentShaders.push_back(fs);
|
||||
}
|
||||
|
||||
int relinked = 0;
|
||||
for (int i = 0; i < kPrograms; ++i) {
|
||||
const GLuint program = programs[static_cast<SizeT>(i)];
|
||||
const String firstName = "uTint" + std::to_string(kFirst + i);
|
||||
ASSERT_EQ(QueryLinkStatus(program), GL_TRUE) << QueryProgramInfoLog(program);
|
||||
if (SpirvIsSettled(program)) continue;
|
||||
|
||||
// Relink while phase B is queued.
|
||||
// A write buffered against link #1, which the relink must DROP. If CancelLink stopped
|
||||
// clearing the buffers, these bytes would be replayed into link #2's shadow at
|
||||
// whatever offset its own routing tables assigned - silently overwriting a uniform
|
||||
// GL 4.6 core 7.6 requires a relink to have reset to zero.
|
||||
const GLint firstTint = GetUniformLocation(program, firstName.c_str());
|
||||
ASSERT_GE(firstTint, 0);
|
||||
const GLfloat poison[3] = {123.0f, 456.0f, 789.0f};
|
||||
ProgramUniform3fv(program, firstTint, 1, poison);
|
||||
ASSERT_FALSE(SpirvIsSettled(program)) << "the poison write should have been buffered, not applied";
|
||||
|
||||
// Relink, with different source, while phase B is queued.
|
||||
secondSources.push_back(MakeUniformSource(kFirst + kRelinkOffset + i));
|
||||
const char* secondText = secondSources.back().c_str();
|
||||
ShaderSource(fragmentShaders[static_cast<SizeT>(i)], 1, &secondText, nullptr);
|
||||
CompileShader(fragmentShaders[static_cast<SizeT>(i)]);
|
||||
LinkProgram(program);
|
||||
++relinked;
|
||||
|
||||
EXPECT_EQ(QueryLinkStatus(program), GL_TRUE) << QueryProgramInfoLog(program);
|
||||
// The artifacts really are the SECOND link's: the first link's uniform is gone.
|
||||
EXPECT_EQ(GetUniformLocation(program, firstName.c_str()), -1)
|
||||
<< "the relink is still answering out of the previous link's reflection";
|
||||
const String secondName = "uTint" + std::to_string(kFirst + kRelinkOffset + i);
|
||||
const GLint secondTint = GetUniformLocation(program, secondName.c_str());
|
||||
ASSERT_GE(secondTint, 0) << secondName;
|
||||
|
||||
const auto& object = Object(program);
|
||||
ASSERT_NE(object, nullptr);
|
||||
object->JoinLinkAndSpirv();
|
||||
EXPECT_TRUE(object->GetSpirvStatus()) << "the relink's own phase B must have produced SPIR-V";
|
||||
EXPECT_FALSE(object->GetGeneratedSpirv().empty());
|
||||
|
||||
// And the dropped buffer really was dropped: a freshly linked program's uniforms read
|
||||
// back as zero.
|
||||
GLfloat tint[3] = {-1.0f, -1.0f, -1.0f};
|
||||
GetUniformfv(program, secondTint, tint);
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
EXPECT_FLOAT_EQ(tint[c], 0.0f)
|
||||
<< "a uniform write buffered against the cancelled link leaked into the relink, component " << c;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_GT(relinked, 0) << "no relink ever landed inside the A->B window";
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// Destroying a program whose phase B is still queued must not wait for it, and must not leave
|
||||
// anything behind that a later join could block on.
|
||||
// Destroying a program whose phase B is still queued must ABANDON it, not wait for it. The
|
||||
// property is timed rather than asserted structurally, and self-calibrated: deleting a whole
|
||||
// batch of programs with outstanding SPIR-V jobs has to cost a small fraction of what draining
|
||||
// the same number of jobs costs. Replacing CancelLink's cooperative Cancel() with a Wait()
|
||||
// would make the two times equal - which is precisely the GL-thread stall the design forbids,
|
||||
// and which the previous shape of this case could not see.
|
||||
TEST_F(AsyncSpirvPhaseTest, DeletingAProgramWithAPendingSpirvJobDoesNotBlock) {
|
||||
const AsyncModeScope async(true);
|
||||
const SingleWorkerScope oneWorker;
|
||||
constexpr int kPrograms = 16;
|
||||
constexpr int kPrograms = 24;
|
||||
constexpr int kHalf = kPrograms / 2;
|
||||
constexpr int kFirst = 36000;
|
||||
|
||||
Vector<String> sources;
|
||||
const Vector<GLuint> programs = LinkBatch(kPrograms, kFirst, sources);
|
||||
|
||||
int deleted = 0;
|
||||
// Settle phase A for every program without touching phase B.
|
||||
for (int i = 0; i < kPrograms; ++i) {
|
||||
const GLuint program = programs[static_cast<SizeT>(i)];
|
||||
ASSERT_EQ(QueryLinkStatus(program), GL_TRUE) << QueryProgramInfoLog(program);
|
||||
if (!SpirvIsSettled(program)) ++deleted;
|
||||
DeleteProgram(program);
|
||||
ASSERT_EQ(QueryLinkStatus(programs[static_cast<SizeT>(i)]), GL_TRUE)
|
||||
<< QueryProgramInfoLog(programs[static_cast<SizeT>(i)]);
|
||||
}
|
||||
|
||||
EXPECT_GT(deleted, 0) << "no program was deleted inside the A->B window";
|
||||
int outstandingBeforeDelete = 0;
|
||||
for (int i = 0; i < kHalf; ++i) {
|
||||
const GLuint program = programs[static_cast<SizeT>(i)];
|
||||
if (!SpirvIsSettled(program)) ++outstandingBeforeDelete;
|
||||
// Buffered writes on the destruction path, so CancelLink's drop of
|
||||
// m_pendingUniformWrites/m_pendingUniformBytes is exercised rather than assumed.
|
||||
const String n = std::to_string(kFirst + i);
|
||||
const GLint locTint = GetUniformLocation(program, ("uTint" + n).c_str());
|
||||
if (locTint >= 0) {
|
||||
const GLfloat tint[3] = {1.0f, 2.0f, 3.0f};
|
||||
ProgramUniform3fv(program, locTint, 1, tint);
|
||||
}
|
||||
}
|
||||
|
||||
const auto deleteStart = std::chrono::steady_clock::now();
|
||||
for (int i = 0; i < kHalf; ++i) {
|
||||
DeleteProgram(programs[static_cast<SizeT>(i)]);
|
||||
}
|
||||
const auto deleteEnd = std::chrono::steady_clock::now();
|
||||
|
||||
// The calibration run: the same number of phase-B jobs, actually drained. Counted first,
|
||||
// so a machine that drained everything in the background cannot turn the bound below into
|
||||
// a comparison between two zeroes without saying so.
|
||||
int outstandingBeforeDrain = 0;
|
||||
for (int i = kHalf; i < kPrograms; ++i) {
|
||||
if (!SpirvIsSettled(programs[static_cast<SizeT>(i)])) ++outstandingBeforeDrain;
|
||||
}
|
||||
const auto drainStart = std::chrono::steady_clock::now();
|
||||
for (int i = kHalf; i < kPrograms; ++i) {
|
||||
const auto& object = Object(programs[static_cast<SizeT>(i)]);
|
||||
ASSERT_NE(object, nullptr);
|
||||
object->JoinLinkAndSpirv();
|
||||
}
|
||||
const auto drainEnd = std::chrono::steady_clock::now();
|
||||
|
||||
const auto deleteUs =
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(deleteEnd - deleteStart).count();
|
||||
const auto drainUs = std::chrono::duration_cast<std::chrono::microseconds>(drainEnd - drainStart).count();
|
||||
|
||||
EXPECT_GT(outstandingBeforeDelete, 0) << "no program was deleted inside the A->B window";
|
||||
EXPECT_GT(outstandingBeforeDrain, 0) << "nothing was left to drain, so the timing bound has no calibration";
|
||||
if (outstandingBeforeDelete > 0 && outstandingBeforeDrain > 0 && drainUs > 2000) {
|
||||
EXPECT_LT(deleteUs, drainUs / 4)
|
||||
<< "deleting " << kHalf << " programs with outstanding SPIR-V jobs took " << deleteUs
|
||||
<< " us against " << drainUs << " us to drain the same number - glDeleteProgram is waiting for them";
|
||||
}
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
@@ -556,12 +870,37 @@ TEST_F(AsyncSpirvPhaseTest, DeletingAProgramWithAPendingSpirvJobDoesNotBlock) {
|
||||
// implementation. The split runs two bodies instead of one, in order, on the calling thread -
|
||||
// and the artifacts it produces must equal what the asynchronous path produces.
|
||||
TEST_F(AsyncSpirvPhaseTest, AsyncOffAndAsyncOnProduceIdenticalSpirvAndShadow) {
|
||||
// The ASYNC arm runs FIRST, deliberately. Both arms must compile the same source text for
|
||||
// their SPIR-V to be comparable, and the first arm to run is the one that pays for the
|
||||
// cold path: it misses the per-context ShaderPreprocessCache and therefore executes
|
||||
// PreprocessShaderSource, the reserved-identifier scan and both lexical extractions. Run
|
||||
// the sync arm first and the async arm becomes a cache hit that never runs any of that on
|
||||
// a worker - which is exactly the half this case exists to compare.
|
||||
const SingleWorkerScope oneWorker;
|
||||
|
||||
Vector<Uint64> asyncDigest;
|
||||
Uint asyncUboSize = 0;
|
||||
Vector<Uint> asyncOffsets;
|
||||
Vector<Uint64> syncDigest;
|
||||
Uint syncUboSize = 0;
|
||||
Vector<Uint> syncOffsets;
|
||||
|
||||
const auto buildOnce = [&](const Bool async, Vector<Uint64>& digest, Uint& uboSize, Vector<Uint>& offsets) {
|
||||
const AsyncModeScope scope(async);
|
||||
// A witness that this arm really ran in the mode it claims: without it, any ambient
|
||||
// reason for AsyncShaderCompileActive() to be false degrades the case to sync-vs-sync
|
||||
// and it still passes.
|
||||
ASSERT_EQ(MG_Util::Async::AsyncShaderCompileActive(), async)
|
||||
<< "the arm did not run in the mode it was asked for";
|
||||
|
||||
// Give the single worker a backlog to chew on, so "was it actually asynchronous" is a
|
||||
// deterministic observation rather than a race with a fast pool.
|
||||
Vector<String> backlogSources;
|
||||
Vector<GLuint> backlog;
|
||||
if (async) {
|
||||
backlog = LinkBatch(6, 37500, backlogSources);
|
||||
}
|
||||
|
||||
const String source = MakeUniformSource(37000);
|
||||
const char* text = source.c_str();
|
||||
const GLuint vs = MakeShader(GL_VERTEX_SHADER, kVs);
|
||||
@@ -572,29 +911,35 @@ TEST_F(AsyncSpirvPhaseTest, AsyncOffAndAsyncOnProduceIdenticalSpirvAndShadow) {
|
||||
AttachShader(program, vs);
|
||||
AttachShader(program, fs);
|
||||
LinkProgram(program);
|
||||
ASSERT_EQ(QueryLinkStatus(program), GL_TRUE) << QueryProgramInfoLog(program);
|
||||
|
||||
const auto& object = Object(program);
|
||||
ASSERT_NE(object, nullptr);
|
||||
if (!async) {
|
||||
if (async) {
|
||||
// Nothing has been read yet, and the worker is busy with the backlog: the link
|
||||
// must genuinely still be outstanding.
|
||||
EXPECT_FALSE(object->IsLinkComplete()) << "the async arm settled before anything read it";
|
||||
} else {
|
||||
// The whole point of the mode: nothing is outstanding when glLinkProgram returns.
|
||||
EXPECT_TRUE(object->IsLinkComplete());
|
||||
}
|
||||
|
||||
ASSERT_EQ(QueryLinkStatus(program), GL_TRUE) << QueryProgramInfoLog(program);
|
||||
digest = SpirvDigest(program);
|
||||
uboSize = object->GetUBOSize();
|
||||
for (Uint location = 0; location <= object->GetMaxUniformLocation(); ++location) {
|
||||
offsets.push_back(object->GetUniformOffset(location));
|
||||
}
|
||||
EXPECT_TRUE(object->GetSpirvStatus());
|
||||
|
||||
for (const GLuint backlogProgram : backlog) {
|
||||
DeleteProgram(backlogProgram);
|
||||
}
|
||||
};
|
||||
|
||||
buildOnce(false, syncDigest, syncUboSize, syncOffsets);
|
||||
ASSERT_FALSE(syncDigest.empty());
|
||||
|
||||
Vector<Uint64> asyncDigest;
|
||||
Uint asyncUboSize = 0;
|
||||
Vector<Uint> asyncOffsets;
|
||||
buildOnce(true, asyncDigest, asyncUboSize, asyncOffsets);
|
||||
ASSERT_FALSE(asyncDigest.empty());
|
||||
|
||||
buildOnce(false, syncDigest, syncUboSize, syncOffsets);
|
||||
|
||||
EXPECT_EQ(asyncDigest, syncDigest) << "the two modes produced different SPIR-V";
|
||||
EXPECT_EQ(asyncUboSize, syncUboSize);
|
||||
|
||||
@@ -341,6 +341,54 @@ TEST_F(ParallelShaderCompileTest, ZeroCompilerThreadsJoinsEverythingAndCompilesI
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The same obligation, but for LINKS that are already in flight when the zero count arrives -
|
||||
// and specifically for BOTH phases of one. A link is two chained jobs now (ProgramLinkTask,
|
||||
// then ProgramSpirvTask), and GL_COMPLETION_STATUS_KHR spans both, so
|
||||
// ProgramState::JoinAllPendingWork has to settle both or this query reads GL_FALSE in the one
|
||||
// mode the extension says cannot have anything pending. The case above creates its program
|
||||
// AFTER the zero count, so it links inline and cannot see this; here the programs are linked
|
||||
// against a saturated pool BEFORE it.
|
||||
TEST_F(ParallelShaderCompileTest, ZeroCompilerThreadsJoinsPendingLinksAndTheirSpirvJobs) {
|
||||
const AsyncModeScope async(true);
|
||||
const CompilerThreadScope threads;
|
||||
MaxShaderCompilerThreadsKHR(1);
|
||||
|
||||
// A backlog first, so the links below cannot all drain before the zero count lands.
|
||||
Vector<String> sources;
|
||||
(void)EnqueueBacklog(24, 5000, sources);
|
||||
|
||||
Vector<GLuint> programs;
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
sources.push_back(MakeBulkySource(5100 + i));
|
||||
const char* text = sources.back().c_str();
|
||||
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
||||
ShaderSource(fs, 1, &text, nullptr);
|
||||
CompileShader(fs);
|
||||
const GLuint vs = MakeShader(GL_VERTEX_SHADER, kVs);
|
||||
CompileShader(vs); // this file's MakeShader only sources; it does not compile
|
||||
const GLuint program = CreateProgram();
|
||||
AttachShader(program, vs);
|
||||
AttachShader(program, fs);
|
||||
LinkProgram(program);
|
||||
programs.push_back(program);
|
||||
}
|
||||
|
||||
int outstanding = 0;
|
||||
for (const GLuint program : programs) {
|
||||
if (QueryProgramCompletion(program) == GL_FALSE) ++outstanding;
|
||||
}
|
||||
|
||||
MaxShaderCompilerThreadsKHR(0);
|
||||
|
||||
for (const GLuint program : programs) {
|
||||
EXPECT_EQ(QueryProgramCompletion(program), GL_TRUE)
|
||||
<< "glMaxShaderCompilerThreadsKHR(0) must leave neither link phase in flight";
|
||||
EXPECT_EQ(QueryLinkStatus(program), GL_TRUE);
|
||||
}
|
||||
EXPECT_GT(outstanding, 0) << "every link had drained before the zero count; this case proved nothing";
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// ...and a later NONZERO count is what lifts it. Nothing else does: not a new context, not a
|
||||
// join, not eglInitialize. That is the documented contract, so it gets an assertion.
|
||||
TEST_F(ParallelShaderCompileTest, NonzeroCompilerThreadsRestoresAsynchronousCompilation) {
|
||||
|
||||
Reference in New Issue
Block a user