mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 04:38:30 +09:00
[Refactor, Test] (MG_State, MG_Util): split the link into ProgramLinkTask (query surface) and a chained ProgramSpirvTask behind its own join gate
This commit is contained in:
@@ -296,6 +296,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.cpp
|
||||
|
||||
@@ -428,18 +428,28 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
// ---- past this point the link cannot fail any more ----
|
||||
MGLOG_D("ProgramObject %u: Starting SPIR-V generation", in.externalIndex);
|
||||
GenerateSpirv();
|
||||
|
||||
MGLOG_D("ProgramObject %u: Building global-UBO routing tables", in.externalIndex);
|
||||
{
|
||||
// TEMP-STAGE-PROBE: "spvc-routing" - the SPIRV-Cross session per SPIR-V module.
|
||||
const MG_Util::Debug::TempStageProbeScope tempStageProbeSpvcRouting(
|
||||
MG_Util::Debug::kTempStageProbeSpvcRouting);
|
||||
BuildGlobalUboRouting();
|
||||
// Everything left is SPIR-V work, and it belongs to phase B. Hand it what it needs
|
||||
// and stop: from the join's point of view this program is now fully linked.
|
||||
//
|
||||
// The TShaders move rather than copy - `attrib` borrowed them into the TProgram as
|
||||
// raw pointers and this node is now their owner of record, for as long as phase B
|
||||
// (which holds this node) needs the intermediates hanging off them.
|
||||
spirvHandoff.shaders = Move(attrib.shaders);
|
||||
spirvHandoff.shaderTypes.resize(in.shaders.size());
|
||||
for (SizeT i = 0; i < in.shaders.size(); i++) {
|
||||
spirvHandoff.shaderTypes[i] = MG_Util::ConvertShaderStageToGLEnum(in.shaders[i].stage);
|
||||
}
|
||||
MGLOG_D("ProgramObject %u: Binary generation finished (generatedSpirv size=%zu)", in.externalIndex,
|
||||
artifacts.generatedSpirv.size());
|
||||
// Copied, not referenced: `artifacts` is MOVED out of this node by the join, and
|
||||
// phase B runs after that. Measured at ~20 us per program, which is noise against the
|
||||
// ~450 ms phase B spends on the same program.
|
||||
spirvHandoff.reflection.program = artifacts.program;
|
||||
spirvHandoff.reflection.uniformLocations = artifacts.uniformLocations;
|
||||
spirvHandoff.reflection.uniformIndexInTProgram = artifacts.uniformIndexInTProgram;
|
||||
spirvHandoff.reflection.tProgramUniformIndexToGl = artifacts.tProgramUniformIndexToGl;
|
||||
spirvHandoff.reflection.maxUniformLocation = artifacts.maxUniformLocation;
|
||||
spirvHandoff.ready = true;
|
||||
MGLOG_D("ProgramObject %u: phase A done, %zu module(s) handed to the SPIR-V job", in.externalIndex,
|
||||
spirvHandoff.shaderTypes.size());
|
||||
}
|
||||
|
||||
Bool ProgramLinkTask::ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders) {
|
||||
@@ -888,206 +898,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProgramLinkTask::GenerateSpirv() {
|
||||
/* As we passed first stage compilation/linking,
|
||||
* we'll assume all the operations here should
|
||||
* pass. We may be able to employ some optimizations
|
||||
* here without the burden of error reporting.
|
||||
*/
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
MGLOG_D("ProgramObject %u: GenerateSpirv - start", in.externalIndex);
|
||||
|
||||
// The shaders were parsed once, in the link-compatible (relaxed Vulkan-rules)
|
||||
// configuration, and artifacts.program linked those parses - so artifacts.program IS
|
||||
// the program the backends consume. Generate SPIR-V straight from its
|
||||
// intermediates; the full re-parse + re-link that used to live here (one
|
||||
// glslang pass per shader per link) is gone.
|
||||
Vector<GLenum> shaderTypes(in.shaders.size());
|
||||
for (SizeT i = 0; i < in.shaders.size(); i++) {
|
||||
shaderTypes[i] = MG_Util::ConvertShaderStageToGLEnum(in.shaders[i].stage);
|
||||
}
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{
|
||||
.shaderTypes = shaderTypes,
|
||||
.program = *artifacts.program,
|
||||
};
|
||||
MGLOG_D("ProgramObject %u: GenerateSpirv - requesting SPIR-V binary from program", in.externalIndex);
|
||||
// TEMP-STAGE-PROBE: "spirv-gen" - GlslangToSpv for every stage of this program.
|
||||
auto binaryResult = [&] {
|
||||
const MG_Util::Debug::TempStageProbeScope tempStageProbeSpirvGen(
|
||||
MG_Util::Debug::kTempStageProbeSpirvGen);
|
||||
return ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
}();
|
||||
if (!binaryResult) {
|
||||
DeferLog(std::format("ProgramObject {}: GenerateSpirv - GetSpirvBinaryFromProgram failed",
|
||||
in.externalIndex));
|
||||
}
|
||||
MOBILEGL_ASSERT(binaryResult, "GetSpirvBinaryFromProgram failed");
|
||||
artifacts.generatedSpirv = Move(binaryResult.value());
|
||||
MGLOG_D("ProgramObject %u: GenerateSpirv - generated %zu SPIR-V modules", in.externalIndex,
|
||||
artifacts.generatedSpirv.size());
|
||||
|
||||
// Linked SPIR-V generated, sanitize and optimize it
|
||||
{
|
||||
// TEMP-STAGE-PROBE: "spirv-null" - the same Optimizer::Run with ZERO passes,
|
||||
// on the pre-optimize binary: pure BuildModule + serialize + IRContext
|
||||
// teardown. Its device/desktop share ratio against "spirv-opt" is the
|
||||
// allocator-pathology discriminator. Costs one extra plumbing round per
|
||||
// module; diagnostic build only.
|
||||
const MG_Util::Debug::TempStageProbeScope tempStageProbeSpirvNull(
|
||||
MG_Util::Debug::kTempStageProbeSpirvNull);
|
||||
for (auto& spv : artifacts.generatedSpirv) {
|
||||
Vector<uint32_t> nullOut;
|
||||
(void)ShaderCompiler::TempProbeNullOptimizeBinary(spv, nullOut);
|
||||
}
|
||||
}
|
||||
{
|
||||
// TEMP-STAGE-PROBE: "spirv-opt" - the spirv-tools optimizer run over every module.
|
||||
const MG_Util::Debug::TempStageProbeScope tempStageProbeSpirvOpt(
|
||||
MG_Util::Debug::kTempStageProbeSpirvOpt);
|
||||
for (auto& spv : artifacts.generatedSpirv) {
|
||||
auto success = ShaderCompiler::SanitizeAndOptimizeBinary(spv, spv);
|
||||
MOBILEGL_ASSERT(success, "SanitizeBinary failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProgramLinkTask::BuildGlobalUboRouting() {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
Vector<GLenum> shaderTypes(in.shaders.size());
|
||||
for (SizeT i = 0; i < in.shaders.size(); i++) {
|
||||
shaderTypes[i] = MG_Util::ConvertShaderStageToGLEnum(in.shaders[i].stage);
|
||||
}
|
||||
|
||||
artifacts.uniformSizesInBytes.clear();
|
||||
artifacts.uniformOffsets.clear();
|
||||
artifacts.globalUboScratch.clear();
|
||||
// kInvalidUniformOffset marks locations that end up without global-UBO backing
|
||||
// (e.g. the optimizer eliminated every use of the uniform); the fallback pass
|
||||
// below gives those locations tail storage so glUniform* always has a target.
|
||||
artifacts.uniformOffsets.resize(artifacts.maxUniformLocation + 1, ProgramObject::kInvalidUniformOffset);
|
||||
artifacts.uniformSizesInBytes.resize(artifacts.maxUniformLocation + 1, 0);
|
||||
for (SizeT i = 0; i < artifacts.generatedSpirv.size(); i++) {
|
||||
auto& spv = artifacts.generatedSpirv[i];
|
||||
|
||||
auto shaderType = shaderTypes[i];
|
||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - parsing SPIR-V meta data for module %zu "
|
||||
"(shaderType=%u, wordCount=%zu)",
|
||||
in.externalIndex, i, shaderType, spv.size());
|
||||
SpvcSession session(spv, SessionUsageBit::Reflection);
|
||||
auto result = session.ParseMetaData();
|
||||
if (result < 0) {
|
||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - SpvcSession::ParseMetaData failed for module %zu, "
|
||||
"err = %d%s",
|
||||
in.externalIndex, i, result,
|
||||
(result == SPVC_ERROR_INVALID_SPIRV ? ". Probably no global UBO?" : ""));
|
||||
continue;
|
||||
} else {
|
||||
auto& meta = session.GetMetadata();
|
||||
auto size = meta.globalUboSize;
|
||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - SPIR-V meta: uboSize=%zu plainUniformCount=%zu "
|
||||
"plainUniformOffsets=%zu",
|
||||
in.externalIndex, meta.globalUboSize, meta.plainUniformMemberSizesInBytes.size(),
|
||||
meta.plainUniformOffsetsInUBO.size());
|
||||
if (size == 0) {
|
||||
continue;
|
||||
}
|
||||
if (artifacts.globalUboScratch.size() < size) {
|
||||
artifacts.globalUboScratch.resize(size);
|
||||
}
|
||||
for (const auto& [name, offset] : meta.plainUniformOffsetsInUBO) {
|
||||
// SPIRV-Reflect leaf names never carry a "[0]" suffix; frontend
|
||||
// reflection keys arrays as "arr[0]" (GL naming), so retry with the
|
||||
// suffix before declaring the uniform unbacked.
|
||||
auto locationIt = artifacts.uniformLocations.find(name);
|
||||
if (locationIt == artifacts.uniformLocations.end()) {
|
||||
locationIt = artifacts.uniformLocations.find(name + "[0]");
|
||||
}
|
||||
if (locationIt == artifacts.uniformLocations.end()) {
|
||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' offset=%u but not found in "
|
||||
"uniformLocations",
|
||||
in.externalIndex, name.c_str(), offset);
|
||||
continue;
|
||||
}
|
||||
const Uint baseLocation = locationIt->second;
|
||||
if (!ProgramObject::IsValidUniformLocation(artifacts, static_cast<Int>(baseLocation))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Int uniformIndex = artifacts.uniformIndexInTProgram[baseLocation];
|
||||
const GLint arraySize = ProgramObject::GetUniformArraySizeByTIndex(artifacts, uniformIndex);
|
||||
SizeT memberSize = 0;
|
||||
const auto sizeIt = meta.plainUniformMemberSizesInBytes.find(name);
|
||||
if (sizeIt != meta.plainUniformMemberSizesInBytes.end()) {
|
||||
memberSize = sizeIt->second;
|
||||
}
|
||||
Uint arrayStride = 0;
|
||||
const auto strideIt = meta.plainUniformArrayStridesInUBO.find(name);
|
||||
if (strideIt != meta.plainUniformArrayStridesInUBO.end()) {
|
||||
arrayStride = strideIt->second;
|
||||
}
|
||||
|
||||
// Array uniforms span one location per element (see DoReflection);
|
||||
// give each element its real byte offset inside the UBO.
|
||||
const GLint elementCount = (arraySize > 1 && arrayStride == 0) ? 1 : std::max(arraySize, 1);
|
||||
for (GLint element = 0; element < elementCount; ++element) {
|
||||
const Uint location = baseLocation + static_cast<Uint>(element);
|
||||
if (location > artifacts.maxUniformLocation ||
|
||||
artifacts.uniformIndexInTProgram[location] != uniformIndex) {
|
||||
break;
|
||||
}
|
||||
artifacts.uniformOffsets[location] = offset + static_cast<Uint>(element) * arrayStride;
|
||||
const SizeT consumed = static_cast<SizeT>(element) * arrayStride;
|
||||
artifacts.uniformSizesInBytes[location] = memberSize > consumed ? memberSize - consumed : 0;
|
||||
}
|
||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' offset=%u stride=%u size=%zu assigned "
|
||||
"to locations %u..%u",
|
||||
in.externalIndex, name.c_str(), offset, arrayStride, memberSize, baseLocation,
|
||||
baseLocation + static_cast<Uint>(elementCount) - 1);
|
||||
}
|
||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - finished parsing module %zu metadata",
|
||||
in.externalIndex, i);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback pass: a linked program's active non-opaque uniforms must accept
|
||||
// glUniform*/glGetUniform* even when the optimized SPIR-V no longer contains
|
||||
// them (AggressiveDCE can remove a dead loop together with the only loads of a
|
||||
// uniform -- or the entire global UBO, leaving the scratch unallocated). Hand
|
||||
// such locations CPU-side storage at the (16-byte aligned) tail of the shadow
|
||||
// buffer; backends bind at least the SPIR-V-declared UBO range, and the GPU
|
||||
// never reads these bytes, so this only keeps the GL-visible state coherent.
|
||||
for (Uint location = 0; location <= artifacts.maxUniformLocation; ++location) {
|
||||
if (artifacts.uniformOffsets[location] != ProgramObject::kInvalidUniformOffset) continue;
|
||||
if (!ProgramObject::IsValidUniformLocation(artifacts, static_cast<Int>(location))) continue;
|
||||
const auto& uniform = artifacts.program->getUniform(artifacts.uniformIndexInTProgram[location]);
|
||||
const glslang::TType* type = uniform.getType();
|
||||
if (type != nullptr && type->isOpaque()) continue;
|
||||
if (uniform.index >= 0 && uniform.index < artifacts.program->getNumUniformBlocks() &&
|
||||
std::strstr(artifacts.program->getUniformBlock(uniform.index).name.c_str(),
|
||||
MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) == nullptr) {
|
||||
// Member of a named uniform block: not settable through glUniform*, so it
|
||||
// needs no global-UBO shadow storage.
|
||||
continue;
|
||||
}
|
||||
|
||||
// std140-style slot: the matrix upload paths write column vectors at
|
||||
// 16-byte strides, so a matrix slot must cover cols * 16 bytes.
|
||||
SizeT slotSize = MG_Util::GetGLTypeSize(uniform.glDefineType);
|
||||
if (type != nullptr && type->isMatrix()) {
|
||||
slotSize = static_cast<SizeT>(type->getMatrixCols()) * 16u;
|
||||
}
|
||||
slotSize = (slotSize + 15u) & ~static_cast<SizeT>(15u);
|
||||
const SizeT slotOffset = (artifacts.globalUboScratch.size() + 15u) & ~static_cast<SizeT>(15u);
|
||||
artifacts.globalUboScratch.resize(slotOffset + slotSize, 0);
|
||||
artifacts.uniformOffsets[location] = static_cast<Uint>(slotOffset);
|
||||
artifacts.uniformSizesInBytes[location] = slotSize;
|
||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' location %u has no UBO backing in the "
|
||||
"generated SPIR-V (optimized out?); allocated %zu fallback bytes at scratch offset %zu",
|
||||
in.externalIndex, uniform.name.c_str(), location, slotSize, slotOffset);
|
||||
}
|
||||
}
|
||||
|
||||
Bool ProgramLinkTask::ValidateFragmentOutputLocations() {
|
||||
if (!artifacts.program) return false;
|
||||
|
||||
|
||||
@@ -29,10 +29,16 @@ namespace MobileGL::MG_State::GLState {
|
||||
SharedPtr<const ShaderCompileTask> compiled;
|
||||
};
|
||||
|
||||
// The unit of asynchronous linking: one glLinkProgram's worth of pure CPU work - glslang
|
||||
// link + mapIO, SPIR-V generation and optimization, the GL-facing reflection surface, the
|
||||
// global-UBO routing tables, fragment-output validation and transform-feedback
|
||||
// resolution - with every input it needs snapshotted at enqueue.
|
||||
// PHASE A of one glLinkProgram: the half that decides what GL can be asked about the
|
||||
// program - glslang link + mapIO, the GL-facing reflection surface, fragment-output
|
||||
// validation and transform-feedback resolution - with every input it needs snapshotted at
|
||||
// enqueue.
|
||||
//
|
||||
// Every one of the eight ways a link can fail lives here, so once this node has published
|
||||
// through EnsureLinkJoined() the program's LINK_STATUS, info log and entire query surface
|
||||
// are FINAL and truthful. SPIR-V generation, spirv-opt and the global-UBO routing tables
|
||||
// moved to ProgramSpirvTask, which chains behind this node and is joined by only five
|
||||
// getters (see ProgramObject::EnsureSpirvJoined).
|
||||
//
|
||||
// Same ownership rule as ShaderCompileTask: the body reads nothing but `in` (all of it
|
||||
// owned or immutable) and writes nothing but `artifacts`. No GL call, no
|
||||
@@ -70,6 +76,37 @@ namespace MobileGL::MG_State::GLState {
|
||||
// Moved (never copied) into the ProgramObject by EnsureLinkJoined().
|
||||
ProgramObject::LinkArtifacts artifacts;
|
||||
|
||||
// ---- output: everything ProgramSpirvTask needs to run without this node's
|
||||
// artifacts, filled at the tail of a successful RunBody() ----
|
||||
//
|
||||
// THIS IS NOT `artifacts` AND MUST NOT BE MERGED INTO IT. The GL thread MOVES
|
||||
// `artifacts` out of this node at the join, and phase B runs on a worker afterwards -
|
||||
// so phase B may read `spirvHandoff` and `in` (neither is ever touched by the join)
|
||||
// and this node's JobState, and nothing else on it. Reading `artifacts` or
|
||||
// `diagnostics` from phase B would race the publish.
|
||||
struct SpirvHandoff {
|
||||
// MANDATORY, and the reason this struct exists at all: TProgram::addShader stores
|
||||
// a RAW TShader*, and for the one-shader-per-stage case getIntermediate() returns
|
||||
// the TShader's own intermediate rather than a copy. These used to die when
|
||||
// RunBody() returned, which was safe only because nothing called getIntermediate()
|
||||
// afterwards. GlslangToSpv does exactly that, so phase B has to own them.
|
||||
Vector<SharedPtr<glslang::TShader>> shaders;
|
||||
// GL enum per entry of `in.shaders`, in the same order (GetSpirvBinaryFromProgram
|
||||
// walks it to pick the intermediates).
|
||||
Vector<GLenum> shaderTypes;
|
||||
// The reflection slice BuildGlobalUboRouting consumes: {program, uniformLocations,
|
||||
// uniformIndexInTProgram, tProgramUniformIndexToGl, maxUniformLocation}. Carried
|
||||
// as a LinkArtifacts with only those five fields set, so the routing pass can keep
|
||||
// calling ProgramObject::IsValidUniformLocation / GetUniformArraySizeByTIndex
|
||||
// unchanged. The SharedPtr copy of `program` is also what keeps the TProgram alive
|
||||
// for phase B after the join has moved `artifacts` away.
|
||||
ProgramObject::LinkArtifacts reflection;
|
||||
|
||||
// The one flag phase B tests before doing anything: false means this link never
|
||||
// reached the tail of RunBody (it failed, or was cancelled mid-body).
|
||||
Bool ready = false;
|
||||
} spirvHandoff;
|
||||
|
||||
// Posts this job once every compile in `deps` is terminal - and not one moment
|
||||
// earlier, so the body never waits on anything (invariant I4: no job body may block
|
||||
// on another job, or the pool could deadlock with all its workers waiting on each
|
||||
@@ -96,8 +133,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
Bool ValidateFragmentOutputLocations();
|
||||
Bool ResolveTransformFeedbackVaryings();
|
||||
void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate);
|
||||
void GenerateSpirv();
|
||||
void BuildGlobalUboRouting();
|
||||
|
||||
// Worker-side MGLOG replacement: appended to diagnostics.logLines and replayed by the
|
||||
// join, on the GL thread, where a serial implementation would have printed it.
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "ProgramObject.h"
|
||||
#include "ProgramLinkTask.h"
|
||||
#include "ProgramSpirvTask.h"
|
||||
#include <atomic>
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
@@ -68,12 +69,50 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
Bool ProgramObject::IsPendingLinkTerminal() const { return m_pendingLink->IsTerminal(); }
|
||||
|
||||
Bool ProgramObject::IsPendingSpirvTerminal() const { return m_pendingSpirv->IsTerminal(); }
|
||||
|
||||
void ProgramObject::JoinPendingSpirv() const {
|
||||
MOBILEGL_ASSERT(!MG_Util::Async::ShaderCompilePool::IsPoolThread(),
|
||||
"ProgramObject::EnsureSpirvJoined() reached from a pool thread; a job body must never read "
|
||||
"GL-thread-owned objects");
|
||||
|
||||
// Move the node out FIRST, for the same reason JoinPendingLink does: everything below
|
||||
// runs GL-thread-only code that reads program state, and with m_pendingSpirv still set
|
||||
// that would re-enter this function.
|
||||
const SharedPtr<ProgramSpirvTask> pending = Move(m_pendingSpirv);
|
||||
m_pendingSpirv.reset();
|
||||
|
||||
pending->Wait();
|
||||
if (pending->IsComplete()) {
|
||||
m_spirv = Move(pending->artifacts);
|
||||
}
|
||||
// A node that settled as Cancelled published nothing, so m_spirv stays empty with
|
||||
// spirvStatus false: linked, queryable, not drawable. Nothing to repair.
|
||||
|
||||
// The THIRD version bump of this link (enqueue, phase-A publish, phase-B publish), and
|
||||
// it is mandatory for exactly the reason the phase-A one is (see JoinPendingLink): a
|
||||
// backend memo taken during the A->B window - when the program was already answering
|
||||
// as linked but had no SPIR-V and no uniform shadow - must not survive the arrival of
|
||||
// either. The memos at risk are keyed on (lifetimeId, backendStateVersion).
|
||||
BumpLinkObservableVersions();
|
||||
|
||||
MG_Util::Async::ApplyDeferredDiagnostics(*pending);
|
||||
}
|
||||
|
||||
void ProgramObject::CancelLink() {
|
||||
// Phase B first: it is chained behind phase A, so cancelling A would otherwise run A's
|
||||
// continuation and post a node this call is about to abandon anyway. Cancelling it up
|
||||
// front makes that continuation a no-op.
|
||||
//
|
||||
// Cooperative and non-blocking, both of them. A node that no worker has picked up
|
||||
// settles immediately; one that is running is flagged and settles when its body
|
||||
// returns, writing only into itself the whole time. Either way nothing waits, and each
|
||||
// node keeps its own inputs alive for as long as it needs them.
|
||||
if (m_pendingSpirv) {
|
||||
m_pendingSpirv->Cancel();
|
||||
m_pendingSpirv.reset();
|
||||
}
|
||||
if (!m_pendingLink) return;
|
||||
// Cooperative and non-blocking. A node that no worker has picked up settles
|
||||
// immediately; one that is running is flagged and settles when its body returns,
|
||||
// writing only into itself the whole time. Either way nothing waits, and the node
|
||||
// keeps its own inputs alive for as long as it needs them.
|
||||
m_pendingLink->Cancel();
|
||||
m_pendingLink.reset();
|
||||
}
|
||||
@@ -103,8 +142,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
// function has ever cleared, and its callers depend on that (they write infoLog
|
||||
// immediately AFTER calling here). Link()'s prologue does not use this - it assigns a
|
||||
// whole default-constructed block, where the ordering is explicit.
|
||||
// Phase-B output (generatedSpirv / uniformOffsets / globalUboScratch) is NOT cleared
|
||||
// here and is not in LinkArtifacts at all: the link body calls this on its own block,
|
||||
// where no phase-B output exists yet. The two GL-thread callers that also have to
|
||||
// discard phase-B output say so themselves (MarkLinkFailedByProgramBinary clears
|
||||
// m_spirv; Link()'s prologue assigns a fresh one).
|
||||
artifacts.program.reset();
|
||||
artifacts.generatedSpirv.clear();
|
||||
artifacts.uniformLocations.clear();
|
||||
artifacts.glUniformIndexToTProgram.clear();
|
||||
artifacts.tProgramUniformIndexToGl.clear();
|
||||
@@ -117,9 +160,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
artifacts.uniformBlockIndexByName.clear();
|
||||
artifacts.uniformBlockBinding.clear();
|
||||
artifacts.shaderStorageBlockBinding.clear();
|
||||
artifacts.uniformOffsets.clear();
|
||||
artifacts.uniformSizesInBytes.clear();
|
||||
artifacts.globalUboScratch.clear();
|
||||
artifacts.attribs.clear();
|
||||
artifacts.attribTypes.clear();
|
||||
artifacts.activeUniformCount = 0;
|
||||
@@ -238,6 +278,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
// is what every gated reader sees, so it has to be the complete "not linked" state -
|
||||
// including the fields ResetLinkArtifacts deliberately preserves for its own callers.
|
||||
m_artifacts = {};
|
||||
m_spirv = {};
|
||||
|
||||
// ---- GL-thread-owned mutations ----
|
||||
// Remove detached shaders first
|
||||
@@ -292,17 +333,33 @@ namespace MobileGL::MG_State::GLState {
|
||||
task->in.shaders.push_back({shader->GetShaderStage(), shader->GetShaderSourcePtr(), node});
|
||||
}
|
||||
|
||||
// Phase B of the same link: SPIR-V generation, spirv-opt and the global-UBO routing
|
||||
// tables. Created here, alongside phase A, so that from this instant the program has
|
||||
// BOTH pending nodes and every cancel site (this prologue, ~ProgramObject,
|
||||
// glProgramBinary's failure) drops both through the one CancelLink().
|
||||
auto spirvTask = MakeShared<ProgramSpirvTask>();
|
||||
m_pendingLink = task;
|
||||
m_pendingSpirv = spirvTask;
|
||||
|
||||
// Flag off - or glMaxShaderCompilerThreadsKHR(0), see AsyncShaderCompileActive():
|
||||
// byte-identical to the synchronous implementation. RunInline() executes the same
|
||||
// body on this thread and the join below publishes through the same code, so the two
|
||||
// modes differ only in WHICH thread ran RunBody().
|
||||
// bodies on this thread, in the same order, and the join below publishes through the
|
||||
// same code, so the two modes differ only in WHICH thread ran them.
|
||||
//
|
||||
// Deliberately NOT expressed as SubmitAfter here: its continuation posts to the pool,
|
||||
// and in this mode the pool is merely unused rather than stopped - the work would
|
||||
// silently move off-thread in the one mode whose whole contract is that it does not.
|
||||
if (!MG_Util::Async::AsyncShaderCompileActive()) {
|
||||
task->RunInline();
|
||||
EnsureLinkJoined();
|
||||
spirvTask->RunInlineAfter(task);
|
||||
EnsureSpirvJoined();
|
||||
return;
|
||||
}
|
||||
// The chain edge FIRST, while phase A is still Pending, so registering it is a plain
|
||||
// list append rather than an inline continuation on this thread. If SubmitAfter below
|
||||
// then fails to post phase A it cancels it, and that cancel fires this edge, which
|
||||
// cancels phase B - nothing is left stranded either way.
|
||||
spirvTask->SubmitAfter(task);
|
||||
task->SubmitAfter(deps);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
// ProgramLinkTask.h includes THIS header (it outputs a LinkArtifacts), so including it
|
||||
// back would be circular. The destructor is therefore out of line.
|
||||
class ProgramLinkTask;
|
||||
// Phase B of the same link: SPIR-V generation, spirv-opt and the global-UBO routing
|
||||
// tables. Chained behind the ProgramLinkTask, forward-declared for the same reason.
|
||||
class ProgramSpirvTask;
|
||||
|
||||
class ProgramObject {
|
||||
public:
|
||||
@@ -303,7 +306,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
// Sentinel for a uniform location without global-UBO backing storage (should not
|
||||
// survive linking: GenerateBinary falls back to tail-allocated scratch storage).
|
||||
static constexpr Uint kInvalidUniformOffset = ~0u;
|
||||
Uint GetUniformOffset(Uint location) const { return Artifacts().uniformOffsets[location]; }
|
||||
// PHASE B (joins the SPIR-V job; see EnsureSpirvJoined).
|
||||
Uint GetUniformOffset(Uint location) const { return Spirv().uniformOffsets[location]; }
|
||||
Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); }
|
||||
|
||||
Int GetAttributeLocation(const String& name) {
|
||||
@@ -381,9 +385,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
const String& GetActiveAttribName(Uint index) const {
|
||||
return NormalizeBuiltinPipeInputName(Artifacts().program->getPipeInput(static_cast<Int>(index)).name);
|
||||
}
|
||||
void* MapUBO() { return Artifacts().globalUboScratch.data(); }
|
||||
const void* GetUBOData() const { return Artifacts().globalUboScratch.data(); }
|
||||
Uint GetUBOSize() const { return static_cast<Uint>(Artifacts().globalUboScratch.size()); }
|
||||
// PHASE B, all three (see EnsureSpirvJoined): the shadow buffer's layout is decided
|
||||
// by the OPTIMIZED SPIR-V, so it does not exist until the SPIR-V job has settled.
|
||||
void* MapUBO() { return Spirv().globalUboScratch.data(); }
|
||||
const void* GetUBOData() const { return Spirv().globalUboScratch.data(); }
|
||||
Uint GetUBOSize() const { return static_cast<Uint>(Spirv().globalUboScratch.size()); }
|
||||
// Content version of the CPU-side global-UBO shadow: writers bump it so backends
|
||||
// can skip re-uploading an unchanged UBO on every draw. ~0u is reserved as the
|
||||
// backends' "never uploaded" sentinel, so skip over it on wrap.
|
||||
@@ -463,6 +469,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
CancelLink();
|
||||
BumpLinkObservableVersions();
|
||||
ResetLinkArtifacts(Artifacts());
|
||||
// ResetLinkArtifacts is a LinkArtifacts-only operation (the link body calls it on
|
||||
// its own block, where no phase-B output exists yet), so the phase-B half is
|
||||
// cleared here. CancelLink() above already dropped the pending SPIR-V job, so
|
||||
// this cannot be racing a publish.
|
||||
m_spirv = {};
|
||||
Artifacts().infoLog = "No program binary format is supported.";
|
||||
}
|
||||
Bool GetValidateStatus() const { return m_validateStatus; }
|
||||
@@ -571,8 +582,15 @@ namespace MobileGL::MG_State::GLState {
|
||||
return Artifacts().shaderStorageBlockBinding;
|
||||
}
|
||||
|
||||
Vector<Vector<unsigned>>& GetGeneratedSpirv() { return Artifacts().generatedSpirv; }
|
||||
const Vector<Vector<unsigned>>& GetGeneratedSpirv() const { return Artifacts().generatedSpirv; }
|
||||
// PHASE B (see EnsureSpirvJoined). Empty for a program whose SPIR-V job was
|
||||
// cancelled; GetSpirvStatus() below is how a backend tells that apart from a program
|
||||
// that never linked.
|
||||
Vector<Vector<unsigned>>& GetGeneratedSpirv() { return Spirv().generatedSpirv; }
|
||||
const Vector<Vector<unsigned>>& GetGeneratedSpirv() const { return Spirv().generatedSpirv; }
|
||||
// Whether phase B produced usable SPIR-V. Joins, like the four getters above: a
|
||||
// backend asks this exactly where it used to ask GetLinkStatus(), i.e. right before
|
||||
// it builds or draws with the program.
|
||||
Bool GetSpirvStatus() const { return Spirv().spirvStatus; }
|
||||
|
||||
// The linked glslang reflection itself, for the ONE consumer that needs resource
|
||||
// lists no typed getter above exposes: the GL program-interface query layer
|
||||
@@ -620,7 +638,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
// without going through the gate.
|
||||
struct LinkArtifacts {
|
||||
SharedPtr<glslang::TProgram> program;
|
||||
Vector<Vector<unsigned>> generatedSpirv;
|
||||
|
||||
// Attributes (Vertex in)
|
||||
Vector<String> attribs;
|
||||
@@ -664,11 +681,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
// SetShaderStorageBlockBinding for why this one is by name and not by index.
|
||||
UnorderedMap<String, Int> shaderStorageBlockBinding;
|
||||
|
||||
// Need to be reflected after linking of SPIR-V binary
|
||||
Vector<Uint> uniformOffsets;
|
||||
Vector<Uint> uniformSizesInBytes;
|
||||
Vector<Uint8> globalUboScratch;
|
||||
|
||||
Uint activeUniformCount = 0;
|
||||
Uint maxUniformLocation = 0;
|
||||
Int uniformNameMaxLength = 0;
|
||||
@@ -697,6 +709,35 @@ namespace MobileGL::MG_State::GLState {
|
||||
Uint32 xfbPackedStride = 0;
|
||||
};
|
||||
|
||||
// ---- everything phase B of a link produces, in one movable block ----
|
||||
//
|
||||
// The membership rule is the same mechanical one LinkArtifacts uses: this is exactly
|
||||
// what ProgramSpirvTask writes, which is what makes moving it THE publish. It is
|
||||
// deliberately NOT part of LinkArtifacts, and that separation is what routes the five
|
||||
// readers of SPIR-V-derived data through their own join gate by compiler rather than
|
||||
// by review - m_spirv is private and Spirv() is the only spelling that reaches it.
|
||||
//
|
||||
// Why these three and nothing else: `generatedSpirv` has no GL-thread reader at all
|
||||
// (every consumer is a backend draw/prepare path), and `uniformOffsets` +
|
||||
// `globalUboScratch` are the ONLY things glUniform*/glGetUniform* need that are
|
||||
// derived from the OPTIMIZED SPIR-V rather than from glslang reflection - spirv-opt
|
||||
// runs in place and can delete a uniform, or the whole global UBO, so the offsets
|
||||
// cannot be lifted out of glslang's reflection instead.
|
||||
struct SpirvArtifacts {
|
||||
Vector<Vector<unsigned>> generatedSpirv;
|
||||
// Byte offset of each uniform location inside globalUboScratch, or
|
||||
// kInvalidUniformOffset. Sized maxUniformLocation + 1 by the routing pass.
|
||||
Vector<Uint> uniformOffsets;
|
||||
Vector<Uint8> globalUboScratch;
|
||||
// False for a program whose SPIR-V was never produced (phase B cancelled at
|
||||
// teardown or by a relink) or whose optimizer run failed. GL has no way to
|
||||
// retract a LINK_STATUS it already reported true, so such a program stays
|
||||
// "linked" and every reflection answer it has given stays correct - it is simply
|
||||
// not drawable, which the backends already express through their link-status
|
||||
// gates.
|
||||
Bool spirvStatus = false;
|
||||
};
|
||||
|
||||
// ---- artifacts-only helpers, shared with ProgramLinkTask ----
|
||||
// Static and taking the block explicitly, because from stage 4 the link BODY needs
|
||||
// them while its artifacts still live on the job node, not on any ProgramObject. The
|
||||
@@ -736,9 +777,20 @@ namespace MobileGL::MG_State::GLState {
|
||||
// Blocks until a pending link has published its artifacts. Public because a few call
|
||||
// sites have to join without reading anything - see the explicit-join list (J1-J8) in
|
||||
// the P1 design. GL thread only.
|
||||
//
|
||||
// PHASE A ONLY. After this returns, LINK_STATUS and the whole GL query surface are
|
||||
// final and truthful, but the SPIR-V and the uniform shadow may still be in flight.
|
||||
void JoinLink() const { EnsureLinkJoined(); }
|
||||
|
||||
// Drops a link that is still in flight, without waiting for it. Called at the points
|
||||
// Both phases. The draw path uses this, and must: the backends sample lifetimeId /
|
||||
// backendStateVersion / the UBO content version OUTSIDE the gate, so a draw that
|
||||
// joined only phase A would sample a version, join phase B later inside the same draw
|
||||
// (through GetGeneratedSpirv), and memoize under a version the phase-B publish had
|
||||
// already superseded - the exact lost-invalidation hazard J1 exists to prevent.
|
||||
void JoinLinkAndSpirv() const { EnsureSpirvJoined(); }
|
||||
|
||||
// Drops BOTH phases of a link that is still in flight, without waiting for either.
|
||||
// Called at the points
|
||||
// where the pending link's result stops being the answer to "what did this program
|
||||
// link to": a re-link supersedes it, glProgramBinary must force LINK_STATUS false,
|
||||
// and a destroyed program has no observers left.
|
||||
@@ -757,7 +809,15 @@ namespace MobileGL::MG_State::GLState {
|
||||
// MUST NOT JOIN - this is what GL_COMPLETION_STATUS_KHR reads when the extension
|
||||
// surface lands. "No job at all" counts as complete: there is nothing outstanding to
|
||||
// wait for.
|
||||
Bool IsLinkComplete() const { return m_pendingLink == nullptr || IsPendingLinkTerminal(); }
|
||||
//
|
||||
// BOTH phases, deliberately: an application that polls GL_COMPLETION_STATUS_KHR and
|
||||
// then draws must not be told "done" while the SPIR-V is still being generated, or
|
||||
// the draw it was cleared for is the thing that blocks.
|
||||
Bool IsLinkComplete() const { return IsPhaseALinkComplete() && IsSpirvComplete(); }
|
||||
// Phase A alone, for the callers that only care about the query surface (and for the
|
||||
// tests that pin the two phases apart).
|
||||
Bool IsPhaseALinkComplete() const { return m_pendingLink == nullptr || IsPendingLinkTerminal(); }
|
||||
Bool IsSpirvComplete() const { return m_pendingSpirv == nullptr || IsPendingSpirvTerminal(); }
|
||||
|
||||
void SetTransformFeedbackVaryings(Vector<String>&& names, GLenum bufferMode) {
|
||||
m_requestedXfbVaryings = Move(names);
|
||||
@@ -824,6 +884,21 @@ namespace MobileGL::MG_State::GLState {
|
||||
// node's state goes through this out-of-line helper.
|
||||
Bool IsPendingLinkTerminal() const;
|
||||
|
||||
// ---- the second join gate: phase-B (SPIR-V) output only ----
|
||||
// Phase A FIRST, always. Two reasons: the phase-B publish replays the uniform writes
|
||||
// that were buffered during its window, and those need the phase-A reflection to
|
||||
// validate against; and a caller that reaches a phase-B getter without having settled
|
||||
// phase A would otherwise leave the link half-published.
|
||||
//
|
||||
// Same inline/out-of-line split as the phase-A gate, for the same reason: the five
|
||||
// getters behind this one include the per-draw uniform upload path.
|
||||
void EnsureSpirvJoined() const {
|
||||
if (m_pendingLink) JoinPendingLink();
|
||||
if (m_pendingSpirv) JoinPendingSpirv();
|
||||
}
|
||||
void JoinPendingSpirv() const;
|
||||
Bool IsPendingSpirvTerminal() const;
|
||||
|
||||
LinkArtifacts& Artifacts() {
|
||||
EnsureLinkJoined();
|
||||
return m_artifacts;
|
||||
@@ -832,6 +907,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
EnsureLinkJoined();
|
||||
return m_artifacts;
|
||||
}
|
||||
SpirvArtifacts& Spirv() {
|
||||
EnsureSpirvJoined();
|
||||
return m_spirv;
|
||||
}
|
||||
const SpirvArtifacts& Spirv() const {
|
||||
EnsureSpirvJoined();
|
||||
return m_spirv;
|
||||
}
|
||||
|
||||
// GL-thread-only companion to ResetLinkArtifacts (see its definition). Const because
|
||||
// the publish half of the join calls it; see the mutable counters below.
|
||||
@@ -899,10 +982,17 @@ namespace MobileGL::MG_State::GLState {
|
||||
// Mutable because publishing is a READ-side operation: a const getter has to be able
|
||||
// to settle an outstanding link before answering it.
|
||||
mutable LinkArtifacts m_artifacts;
|
||||
// Phase-B output. Same mutability argument as m_artifacts, reached only through
|
||||
// Spirv().
|
||||
mutable SpirvArtifacts m_spirv;
|
||||
|
||||
// The link job, from enqueue until the first observable read pulls its result. Null
|
||||
// means m_artifacts is already the answer - which is the state every reader outside
|
||||
// the pending window sees, and the whole reason the gate above is one branch.
|
||||
mutable SharedPtr<ProgramLinkTask> m_pendingLink;
|
||||
// The SPIR-V job, chained behind m_pendingLink. Null means m_spirv is already the
|
||||
// answer. A program can be in the window where m_pendingLink is already null (phase A
|
||||
// published, the query surface is live) while this is still set.
|
||||
mutable SharedPtr<ProgramSpirvTask> m_pendingSpirv;
|
||||
};
|
||||
} // namespace MobileGL::MG_State::GLState
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "ProgramSpirvTask.h"
|
||||
|
||||
#include <MG_State/GLState/ProgramState/ShaderCompileTask.h> // GlslangThreadAllocatorGuard
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <MG_Util/Debug/TempStageProbe.h> // TEMP-STAGE-PROBE
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
void ProgramSpirvTask::DeferLog(String line) { diagnostics.logLines.push_back(Move(line)); }
|
||||
|
||||
void ProgramSpirvTask::SubmitAfter(const SharedPtr<ProgramLinkTask>& phaseA) {
|
||||
MOBILEGL_ASSERT(phaseA != nullptr, "ProgramSpirvTask::SubmitAfter: the phase-A node is missing");
|
||||
m_phaseA = phaseA;
|
||||
|
||||
auto self = std::static_pointer_cast<ProgramSpirvTask>(shared_from_this());
|
||||
// ONE dependency, so no counter and no guard slot: the whole race
|
||||
// ProgramLinkTask::SubmitAfter's +1 exists to close (a dependency settling while the
|
||||
// remaining edges are still being registered) cannot arise with a single edge.
|
||||
//
|
||||
// Runs inline, right here, if phase A is already terminal.
|
||||
phaseA->OnTerminal([self, phaseA] {
|
||||
// "Dependency did not complete, publish nothing" - the same collapse
|
||||
// ProgramLinkTask::CompiledArtifacts() performs for an abandoned compile. Note
|
||||
// this reads the HANDOFF, never phaseA->artifacts: the GL thread may already be
|
||||
// moving those out (see the class comment).
|
||||
if (!phaseA->IsComplete() || !phaseA->spirvHandoff.ready) {
|
||||
self->Cancel();
|
||||
return;
|
||||
}
|
||||
// A cancel that landed before phase A settled (relink, glDeleteProgram, teardown).
|
||||
// Posting would only make a worker pick up a node that immediately falls out of
|
||||
// Run() again.
|
||||
if (self->IsCancellationRequested()) {
|
||||
self->Cancel();
|
||||
return;
|
||||
}
|
||||
// Non-throwing by construction, and it has to be: this is a JobNode continuation,
|
||||
// so on the pool side it runs inside an Asio handler. Post() contains its own
|
||||
// allocation failures, and the catch below CANCELS rather than swallowing - a
|
||||
// phase B that is never posted is a GL thread blocked forever in
|
||||
// EnsureSpirvJoined(), which is far worse than a program reported as not drawable.
|
||||
try {
|
||||
MG_Util::Async::ShaderCompilePool::Get().Post(self);
|
||||
} catch (...) {
|
||||
self->Cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ProgramSpirvTask::RunInlineAfter(const SharedPtr<ProgramLinkTask>& phaseA) {
|
||||
MOBILEGL_ASSERT(phaseA != nullptr, "ProgramSpirvTask::RunInlineAfter: the phase-A node is missing");
|
||||
MOBILEGL_ASSERT(phaseA->IsTerminal(),
|
||||
"ProgramSpirvTask::RunInlineAfter: phase A has not settled; the inline path must run the "
|
||||
"two bodies in order on the same thread");
|
||||
m_phaseA = phaseA;
|
||||
RunInline();
|
||||
}
|
||||
|
||||
// Pure CPU work only, on a pool worker (or on the GL thread in the inline mode).
|
||||
// Everything this reads is either owned by this node or published by a terminal phase A;
|
||||
// everything it writes is `artifacts` (and diagnostics). Same prohibitions as
|
||||
// ProgramLinkTask::RunBody - no GL/EGL call, no pActiveBackendObject read, no
|
||||
// pGLContext->RecordError().
|
||||
void ProgramSpirvTask::RunBody() {
|
||||
// glslang leaves this worker's TLS pool allocator pointing at the last arena it
|
||||
// touched; reset it on the way out so an unrelated later job cannot allocate out of a
|
||||
// pool that has since been freed. Declared FIRST so it is destroyed LAST - the phase-A
|
||||
// release below drops the TShaders (and their pools) and must happen inside it.
|
||||
const GlslangThreadAllocatorGuard glslangGuard;
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// Drop phase A - and with it the TShaders, the TProgram reference and phase A's whole
|
||||
// input snapshot - the moment this body is done, rather than at some later join. For a
|
||||
// pack load that is the difference between W glslang arenas alive and all of them.
|
||||
struct PhaseAReleaser {
|
||||
SharedPtr<ProgramLinkTask>& node;
|
||||
~PhaseAReleaser() { node.reset(); }
|
||||
} const phaseAReleaser{m_phaseA};
|
||||
|
||||
if (!m_phaseA) return;
|
||||
const ProgramLinkTask::SpirvHandoff& handoff = m_phaseA->spirvHandoff;
|
||||
const Uint externalIndex = m_phaseA->in.externalIndex;
|
||||
if (!handoff.ready || !handoff.reflection.program) {
|
||||
// Phase A did not reach its tail (it failed the link, or was cancelled mid-body).
|
||||
// Publish nothing; spirvStatus stays false.
|
||||
return;
|
||||
}
|
||||
|
||||
// TEMP-STAGE-PROBE: "spirvtask-total" (whole phase-B body, superset of spirv-gen /
|
||||
// spirv-null / spirv-opt / spvc-routing).
|
||||
const MG_Util::Debug::TempStageProbeScope tempStageProbeSpirvTask(
|
||||
MG_Util::Debug::kTempStageProbeSpirvTaskTotal);
|
||||
|
||||
MGLOG_D("ProgramObject %u: Starting SPIR-V generation", externalIndex);
|
||||
GenerateSpirv(handoff, externalIndex);
|
||||
|
||||
MGLOG_D("ProgramObject %u: Building global-UBO routing tables", externalIndex);
|
||||
{
|
||||
// TEMP-STAGE-PROBE: "spvc-routing" - the SPIRV-Cross session per SPIR-V module.
|
||||
const MG_Util::Debug::TempStageProbeScope tempStageProbeSpvcRouting(
|
||||
MG_Util::Debug::kTempStageProbeSpvcRouting);
|
||||
BuildGlobalUboRouting(handoff, externalIndex);
|
||||
}
|
||||
MGLOG_D("ProgramObject %u: Binary generation finished (generatedSpirv size=%zu)", externalIndex,
|
||||
artifacts.generatedSpirv.size());
|
||||
}
|
||||
|
||||
void ProgramSpirvTask::GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, const Uint externalIndex) {
|
||||
/* As we passed first stage compilation/linking,
|
||||
* we'll assume all the operations here should
|
||||
* pass. We may be able to employ some optimizations
|
||||
* here without the burden of error reporting.
|
||||
*/
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
MGLOG_D("ProgramObject %u: GenerateSpirv - start", externalIndex);
|
||||
|
||||
// The shaders were parsed once, in the link-compatible (relaxed Vulkan-rules)
|
||||
// configuration, and the handoff's program linked those parses - so it IS the program
|
||||
// the backends consume. Generate SPIR-V straight from its intermediates, which the
|
||||
// handoff's TShaders keep alive.
|
||||
ProgramBinaryAttrib binaryAttrib{
|
||||
.shaderTypes = handoff.shaderTypes,
|
||||
.program = *handoff.reflection.program,
|
||||
};
|
||||
MGLOG_D("ProgramObject %u: GenerateSpirv - requesting SPIR-V binary from program", externalIndex);
|
||||
// TEMP-STAGE-PROBE: "spirv-gen" - GlslangToSpv for every stage of this program.
|
||||
auto binaryResult = [&] {
|
||||
const MG_Util::Debug::TempStageProbeScope tempStageProbeSpirvGen(
|
||||
MG_Util::Debug::kTempStageProbeSpirvGen);
|
||||
return ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
}();
|
||||
if (!binaryResult) {
|
||||
DeferLog(std::format("ProgramObject {}: GenerateSpirv - GetSpirvBinaryFromProgram failed", externalIndex));
|
||||
MOBILEGL_ASSERT(binaryResult, "GetSpirvBinaryFromProgram failed");
|
||||
return; // spirvStatus stays false: linked, but not drawable.
|
||||
}
|
||||
artifacts.generatedSpirv = Move(binaryResult.value());
|
||||
MGLOG_D("ProgramObject %u: GenerateSpirv - generated %zu SPIR-V modules", externalIndex,
|
||||
artifacts.generatedSpirv.size());
|
||||
|
||||
// Linked SPIR-V generated, sanitize and optimize it
|
||||
{
|
||||
// TEMP-STAGE-PROBE: "spirv-null" - the same Optimizer::Run with ZERO passes,
|
||||
// on the pre-optimize binary: pure BuildModule + serialize + IRContext
|
||||
// teardown. Its device/desktop share ratio against "spirv-opt" is the
|
||||
// allocator-pathology discriminator. Costs one extra plumbing round per
|
||||
// module; diagnostic build only.
|
||||
const MG_Util::Debug::TempStageProbeScope tempStageProbeSpirvNull(
|
||||
MG_Util::Debug::kTempStageProbeSpirvNull);
|
||||
for (auto& spv : artifacts.generatedSpirv) {
|
||||
Vector<uint32_t> nullOut;
|
||||
(void)ShaderCompiler::TempProbeNullOptimizeBinary(spv, nullOut);
|
||||
}
|
||||
}
|
||||
Bool allOptimized = true;
|
||||
{
|
||||
// TEMP-STAGE-PROBE: "spirv-opt" - the spirv-tools optimizer run over every module.
|
||||
const MG_Util::Debug::TempStageProbeScope tempStageProbeSpirvOpt(
|
||||
MG_Util::Debug::kTempStageProbeSpirvOpt);
|
||||
for (auto& spv : artifacts.generatedSpirv) {
|
||||
auto success = ShaderCompiler::SanitizeAndOptimizeBinary(spv, spv);
|
||||
if (!success) {
|
||||
// The one genuine phase-B failure mode: one of the seven optimizer passes
|
||||
// reported failure, so `spv` is whatever the run left behind. A fordebug
|
||||
// build trips the assert below; a release build used to hand that binary
|
||||
// to the backend regardless. It no longer does - the program keeps its
|
||||
// (truthful) LINK_STATUS and its whole query surface, and the routing
|
||||
// tables below still give every settable uniform storage so glUniform*
|
||||
// and glGetUniform* keep working, but spirvStatus stays false and the
|
||||
// backends refuse to build or draw with it.
|
||||
allOptimized = false;
|
||||
DeferLog(std::format("ProgramObject {}: SanitizeAndOptimizeBinary failed; the program is linked "
|
||||
"and queryable but not drawable",
|
||||
externalIndex));
|
||||
}
|
||||
MOBILEGL_ASSERT(success, "SanitizeBinary failed");
|
||||
}
|
||||
}
|
||||
artifacts.spirvStatus = allOptimized;
|
||||
}
|
||||
|
||||
void ProgramSpirvTask::BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff,
|
||||
const Uint externalIndex) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
// The phase-A reflection slice this pass keys off. Carried in the handoff rather than
|
||||
// read off the phase-A node's artifacts, which the join has very likely already moved.
|
||||
const ProgramObject::LinkArtifacts& reflection = handoff.reflection;
|
||||
|
||||
artifacts.uniformOffsets.clear();
|
||||
artifacts.globalUboScratch.clear();
|
||||
// kInvalidUniformOffset marks locations that end up without global-UBO backing
|
||||
// (e.g. the optimizer eliminated every use of the uniform); the fallback pass
|
||||
// below gives those locations tail storage so glUniform* always has a target.
|
||||
artifacts.uniformOffsets.resize(reflection.maxUniformLocation + 1, ProgramObject::kInvalidUniformOffset);
|
||||
for (SizeT i = 0; i < artifacts.generatedSpirv.size(); i++) {
|
||||
auto& spv = artifacts.generatedSpirv[i];
|
||||
|
||||
auto shaderType = i < handoff.shaderTypes.size() ? handoff.shaderTypes[i] : GLenum{0};
|
||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - parsing SPIR-V meta data for module %zu "
|
||||
"(shaderType=%u, wordCount=%zu)",
|
||||
externalIndex, i, shaderType, spv.size());
|
||||
SpvcSession session(spv, SessionUsageBit::Reflection);
|
||||
auto result = session.ParseMetaData();
|
||||
if (result < 0) {
|
||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - SpvcSession::ParseMetaData failed for module %zu, "
|
||||
"err = %d%s",
|
||||
externalIndex, i, result,
|
||||
(result == SPVC_ERROR_INVALID_SPIRV ? ". Probably no global UBO?" : ""));
|
||||
continue;
|
||||
} else {
|
||||
auto& meta = session.GetMetadata();
|
||||
auto size = meta.globalUboSize;
|
||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - SPIR-V meta: uboSize=%zu plainUniformCount=%zu "
|
||||
"plainUniformOffsets=%zu",
|
||||
externalIndex, meta.globalUboSize, meta.plainUniformMemberSizesInBytes.size(),
|
||||
meta.plainUniformOffsetsInUBO.size());
|
||||
if (size == 0) {
|
||||
continue;
|
||||
}
|
||||
if (artifacts.globalUboScratch.size() < size) {
|
||||
artifacts.globalUboScratch.resize(size);
|
||||
}
|
||||
for (const auto& [name, offset] : meta.plainUniformOffsetsInUBO) {
|
||||
// SPIRV-Reflect leaf names never carry a "[0]" suffix; frontend
|
||||
// reflection keys arrays as "arr[0]" (GL naming), so retry with the
|
||||
// suffix before declaring the uniform unbacked.
|
||||
auto locationIt = reflection.uniformLocations.find(name);
|
||||
if (locationIt == reflection.uniformLocations.end()) {
|
||||
locationIt = reflection.uniformLocations.find(name + "[0]");
|
||||
}
|
||||
if (locationIt == reflection.uniformLocations.end()) {
|
||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' offset=%u but not found in "
|
||||
"uniformLocations",
|
||||
externalIndex, name.c_str(), offset);
|
||||
continue;
|
||||
}
|
||||
const Uint baseLocation = locationIt->second;
|
||||
if (!ProgramObject::IsValidUniformLocation(reflection, static_cast<Int>(baseLocation))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Int uniformIndex = reflection.uniformIndexInTProgram[baseLocation];
|
||||
const GLint arraySize = ProgramObject::GetUniformArraySizeByTIndex(reflection, uniformIndex);
|
||||
Uint arrayStride = 0;
|
||||
const auto strideIt = meta.plainUniformArrayStridesInUBO.find(name);
|
||||
if (strideIt != meta.plainUniformArrayStridesInUBO.end()) {
|
||||
arrayStride = strideIt->second;
|
||||
}
|
||||
|
||||
// Array uniforms span one location per element (see DoReflection);
|
||||
// give each element its real byte offset inside the UBO.
|
||||
const GLint elementCount = (arraySize > 1 && arrayStride == 0) ? 1 : std::max(arraySize, 1);
|
||||
for (GLint element = 0; element < elementCount; ++element) {
|
||||
const Uint location = baseLocation + static_cast<Uint>(element);
|
||||
if (location > reflection.maxUniformLocation ||
|
||||
reflection.uniformIndexInTProgram[location] != uniformIndex) {
|
||||
break;
|
||||
}
|
||||
artifacts.uniformOffsets[location] = offset + static_cast<Uint>(element) * arrayStride;
|
||||
}
|
||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' offset=%u stride=%u assigned "
|
||||
"to locations %u..%u",
|
||||
externalIndex, name.c_str(), offset, arrayStride, baseLocation,
|
||||
baseLocation + static_cast<Uint>(elementCount) - 1);
|
||||
}
|
||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - finished parsing module %zu metadata",
|
||||
externalIndex, i);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback pass: a linked program's active non-opaque uniforms must accept
|
||||
// glUniform*/glGetUniform* even when the optimized SPIR-V no longer contains
|
||||
// them (AggressiveDCE can remove a dead loop together with the only loads of a
|
||||
// uniform -- or the entire global UBO, leaving the scratch unallocated). Hand
|
||||
// such locations CPU-side storage at the (16-byte aligned) tail of the shadow
|
||||
// buffer; backends bind at least the SPIR-V-declared UBO range, and the GPU
|
||||
// never reads these bytes, so this only keeps the GL-visible state coherent.
|
||||
for (Uint location = 0; location <= reflection.maxUniformLocation; ++location) {
|
||||
if (artifacts.uniformOffsets[location] != ProgramObject::kInvalidUniformOffset) continue;
|
||||
if (!ProgramObject::IsValidUniformLocation(reflection, static_cast<Int>(location))) continue;
|
||||
const auto& uniform = reflection.program->getUniform(reflection.uniformIndexInTProgram[location]);
|
||||
const glslang::TType* type = uniform.getType();
|
||||
if (type != nullptr && type->isOpaque()) continue;
|
||||
if (uniform.index >= 0 && uniform.index < reflection.program->getNumUniformBlocks() &&
|
||||
std::strstr(reflection.program->getUniformBlock(uniform.index).name.c_str(),
|
||||
MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) == nullptr) {
|
||||
// Member of a named uniform block: not settable through glUniform*, so it
|
||||
// needs no global-UBO shadow storage.
|
||||
continue;
|
||||
}
|
||||
|
||||
// std140-style slot: the matrix upload paths write column vectors at
|
||||
// 16-byte strides, so a matrix slot must cover cols * 16 bytes.
|
||||
SizeT slotSize = MG_Util::GetGLTypeSize(uniform.glDefineType);
|
||||
if (type != nullptr && type->isMatrix()) {
|
||||
slotSize = static_cast<SizeT>(type->getMatrixCols()) * 16u;
|
||||
}
|
||||
slotSize = (slotSize + 15u) & ~static_cast<SizeT>(15u);
|
||||
const SizeT slotOffset = (artifacts.globalUboScratch.size() + 15u) & ~static_cast<SizeT>(15u);
|
||||
artifacts.globalUboScratch.resize(slotOffset + slotSize, 0);
|
||||
artifacts.uniformOffsets[location] = static_cast<Uint>(slotOffset);
|
||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' location %u has no UBO backing in the "
|
||||
"generated SPIR-V (optimized out?); allocated %zu fallback bytes at scratch offset %zu",
|
||||
externalIndex, uniform.name.c_str(), location, slotSize, slotOffset);
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_State::GLState
|
||||
@@ -0,0 +1,77 @@
|
||||
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include <MG_State/GLState/ProgramState/ProgramLinkTask.h>
|
||||
#include <MG_Util/Async/JobNode.h>
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
// PHASE B of one glLinkProgram: GlslangToSpv, spirv-opt, and the SPIRV-Cross pass that
|
||||
// builds the glUniform*-to-scratch routing tables. Chained behind exactly one
|
||||
// ProgramLinkTask and joined by exactly five ProgramObject getters (GetGeneratedSpirv,
|
||||
// GetUniformOffset, MapUBO, GetUBOData, GetUBOSize), so ~120 other getters and the whole
|
||||
// GL query surface stay on the phase-A gate and answer without waiting for any of this.
|
||||
//
|
||||
// ---- what this node may read, and what it may not ----
|
||||
// It holds the phase-A node by SharedPtr and reads `phaseA->spirvHandoff` plus
|
||||
// `phaseA->in`. It must NEVER read `phaseA->artifacts` or `phaseA->diagnostics`: the GL
|
||||
// thread MOVES the artifacts out of the node at the phase-A join and DRAINS the
|
||||
// diagnostics there, and both of those can happen while this body runs. The handoff exists
|
||||
// precisely so this node has a copy of everything it needs that the join does not touch.
|
||||
// (The general JobNode rule - a terminal node is immutable, so its outputs need no further
|
||||
// synchronization - covers everything except the two members the join consumes.)
|
||||
//
|
||||
// ---- lifetime ----
|
||||
// The handoff owns the Vector<SharedPtr<glslang::TShader>>, and that is mandatory rather
|
||||
// than tidy: glslang::TProgram stores raw TShader* and, for the one-shader-per-stage case,
|
||||
// BORROWS each stage's TIntermediate from its TShader. GlslangToSpv reads exactly those
|
||||
// intermediates. Before the split the shaders died when ProgramLinkTask::RunBody returned,
|
||||
// which was safe only because nothing called getIntermediate() afterwards.
|
||||
//
|
||||
// ---- failure ----
|
||||
// A cancel (relink, teardown, program destruction) or an optimizer failure publishes
|
||||
// spirvStatus = false rather than a half-built program. GL cannot retract a LINK_STATUS it
|
||||
// already reported true, so such a program stays linked and fully queryable; it is just
|
||||
// not drawable, which the backends express through their existing link-status gates.
|
||||
class ProgramSpirvTask final : public MG_Util::Async::JobNode {
|
||||
public:
|
||||
// ---- output: valid iff IsComplete(), immutable afterwards ----
|
||||
// Moved (never copied) into the ProgramObject by EnsureSpirvJoined().
|
||||
ProgramObject::SpirvArtifacts artifacts;
|
||||
|
||||
// Posts this job when `phaseA` goes terminal - and not one moment earlier, so the body
|
||||
// never waits on anything (invariant I4: no job body may block on another job). A
|
||||
// single dependency needs no counter, just the one continuation; it runs inline right
|
||||
// here if `phaseA` is already terminal, which is the same case
|
||||
// ProgramLinkTask::SubmitAfter already reasons about.
|
||||
//
|
||||
// GL thread only, and only after the caller has stored a SharedPtr to this node: the
|
||||
// continuation takes shared_from_this().
|
||||
void SubmitAfter(const SharedPtr<ProgramLinkTask>& phaseA);
|
||||
|
||||
// The async-off / glMaxShaderCompilerThreadsKHR(0) path: run the body on the calling
|
||||
// thread, right now, against an ALREADY-TERMINAL phase A. Deliberately not routed
|
||||
// through SubmitAfter, whose continuation would Post() to a pool that is merely
|
||||
// unused rather than stopped - that would move the work off-thread in the one mode
|
||||
// whose contract is "byte-identical to the synchronous implementation".
|
||||
void RunInlineAfter(const SharedPtr<ProgramLinkTask>& phaseA);
|
||||
|
||||
private:
|
||||
void RunBody() override;
|
||||
|
||||
void GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex);
|
||||
void BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex);
|
||||
|
||||
// Worker-side MGLOG replacement, replayed by the join on the GL thread. Same reason as
|
||||
// ProgramLinkTask::DeferLog.
|
||||
void DeferLog(String line);
|
||||
|
||||
SharedPtr<ProgramLinkTask> m_phaseA;
|
||||
};
|
||||
} // namespace MobileGL::MG_State::GLState
|
||||
@@ -117,11 +117,26 @@ void main() { fragColor = thisIdentifierWasNeverDeclared; }
|
||||
}
|
||||
|
||||
// The non-joining view of the program, i.e. what GL_COMPLETION_STATUS_KHR will report.
|
||||
// BOTH phases: a program whose SPIR-V job is still in flight is not finished, even though
|
||||
// its whole GL query surface already answers.
|
||||
Bool LinkIsSettled(const GLuint program) {
|
||||
const auto& object = MG_State::pGLContext->GetProgramObject(program);
|
||||
return object == nullptr || object->IsLinkComplete();
|
||||
}
|
||||
|
||||
// Phase A alone: the half that decides LINK_STATUS, the info log, and every reflection
|
||||
// query. This is what a read of LINK_STATUS is required to settle.
|
||||
Bool PhaseALinkIsSettled(const GLuint program) {
|
||||
const auto& object = MG_State::pGLContext->GetProgramObject(program);
|
||||
return object == nullptr || object->IsPhaseALinkComplete();
|
||||
}
|
||||
|
||||
// Phase B alone: SPIR-V + the uniform shadow's layout.
|
||||
Bool SpirvIsSettled(const GLuint program) {
|
||||
const auto& object = MG_State::pGLContext->GetProgramObject(program);
|
||||
return object == nullptr || object->IsSpirvComplete();
|
||||
}
|
||||
|
||||
// Enqueues `count` distinct heavy compiles without reading anything back, so the pool is
|
||||
// left with a real backlog for the caller to race against.
|
||||
Vector<GLuint> SaturatePool(const int count, Vector<String>& sourceStorage) {
|
||||
@@ -516,11 +531,63 @@ TEST_F(AsyncLinkTest, LinkProgramReturnsBeforeTheWorkIsDone) {
|
||||
|
||||
for (const GLuint program : programs) {
|
||||
EXPECT_EQ(QueryLinkStatus(program), GL_TRUE) << QueryProgramInfoLog(program);
|
||||
EXPECT_TRUE(LinkIsSettled(program)) << "reading LINK_STATUS must have joined";
|
||||
// PHASE A only. Reading LINK_STATUS settles the half that decides it, and no more -
|
||||
// the SPIR-V job may well still be running, which is the entire point of the split.
|
||||
EXPECT_TRUE(PhaseALinkIsSettled(program)) << "reading LINK_STATUS must have joined phase A";
|
||||
}
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The other half of the previous case, and the property the two-phase split exists for:
|
||||
// LINK_STATUS is answerable without the SPIR-V, so a run of LINK_STATUS reads over a
|
||||
// backlog must leave SPIR-V jobs outstanding rather than draining them one by one.
|
||||
TEST_F(AsyncLinkTest, ReadingLinkStatusDoesNotSettleTheSpirvJob) {
|
||||
const AsyncModeScope async(true);
|
||||
MG_Util::Async::ShaderCompilePool::Get().SetMaxConcurrency(1);
|
||||
constexpr int kPrograms = 24;
|
||||
|
||||
const GLuint vs = MakeShader(GL_VERTEX_SHADER, kVs);
|
||||
Vector<GLuint> programs;
|
||||
Vector<String> sources;
|
||||
for (int i = 0; i < kPrograms; ++i) {
|
||||
sources.push_back(MakeBulkySource(7900 + i));
|
||||
const char* text = sources.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);
|
||||
}
|
||||
|
||||
int spirvOutstanding = 0;
|
||||
for (int i = 0; i < kPrograms; ++i) {
|
||||
const GLuint program = programs[static_cast<SizeT>(i)];
|
||||
EXPECT_EQ(QueryLinkStatus(program), GL_TRUE) << QueryProgramInfoLog(program);
|
||||
EXPECT_TRUE(PhaseALinkIsSettled(program)) << "reading LINK_STATUS must have joined phase A";
|
||||
// Reflection has to answer here too, out of phase A and with no further join.
|
||||
const String uniformName = "uSeed" + std::to_string(7900 + i);
|
||||
EXPECT_GE(GetUniformLocation(program, uniformName.c_str()), 0) << uniformName;
|
||||
if (!SpirvIsSettled(program)) ++spirvOutstanding;
|
||||
}
|
||||
EXPECT_GT(spirvOutstanding, 0) << "the whole GL query surface was answered and yet every SPIR-V job had "
|
||||
"already been drained - the reads are joining phase B";
|
||||
|
||||
// And the SPIR-V gate really is a gate: touching it settles the job.
|
||||
for (const GLuint program : programs) {
|
||||
const auto& object = MG_State::pGLContext->GetProgramObject(program);
|
||||
ASSERT_NE(object, nullptr);
|
||||
EXPECT_GT(object->GetGeneratedSpirv().size(), 0u);
|
||||
EXPECT_TRUE(SpirvIsSettled(program));
|
||||
EXPECT_TRUE(LinkIsSettled(program));
|
||||
}
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
MG_Util::Async::ShaderCompilePool::Get().SetMaxConcurrency(
|
||||
MG_Util::Async::ShaderCompilePool::Get().GetThreadCount());
|
||||
}
|
||||
|
||||
// With the flag off, a link is finished by the time glLinkProgram returns. This is the guard
|
||||
// that keeps the default shippable.
|
||||
TEST_F(AsyncLinkTest, LinkIsFullySynchronousWithAsyncOff) {
|
||||
|
||||
@@ -239,7 +239,15 @@ TEST_F(ParallelShaderCompileTest, ProgramCompletionStatusReportsFalseWithoutJoin
|
||||
|
||||
for (const GLuint program : programs) {
|
||||
EXPECT_EQ(QueryLinkStatus(program), GL_TRUE);
|
||||
EXPECT_EQ(QueryProgramCompletion(program), GL_TRUE) << "GL_LINK_STATUS must have joined";
|
||||
// GL_COMPLETION_STATUS_KHR spans BOTH phases of a link, so reading GL_LINK_STATUS -
|
||||
// which is answered out of phase A - is no longer enough to turn it GL_TRUE. That is
|
||||
// deliberate: an application that polls completion and then draws must not be told
|
||||
// "done" while the SPIR-V is still being generated, or the draw it was cleared for is
|
||||
// the thing that blocks. Settling both phases is what makes the query true.
|
||||
const auto& object = MG_State::pGLContext->GetProgramObject(program);
|
||||
ASSERT_NE(object, nullptr);
|
||||
object->JoinLinkAndSpirv();
|
||||
EXPECT_EQ(QueryProgramCompletion(program), GL_TRUE) << "a full join must have settled both phases";
|
||||
}
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace MobileGL::MG_Util::Debug {
|
||||
kTempStageProbeSpvcRouting,
|
||||
kTempStageProbeCompileTaskTotal,
|
||||
kTempStageProbeLinkTaskTotal,
|
||||
kTempStageProbeSpirvTaskTotal,
|
||||
kTempStageProbeStageCount
|
||||
};
|
||||
|
||||
@@ -52,6 +53,7 @@ namespace MobileGL::MG_Util::Debug {
|
||||
"spvc-routing",
|
||||
"compiletask-total[superset]",
|
||||
"linktask-total[superset]",
|
||||
"spirvtask-total[superset]",
|
||||
};
|
||||
|
||||
inline std::atomic<unsigned long long> tempStageProbeMicros[kTempStageProbeStageCount] = {};
|
||||
|
||||
Reference in New Issue
Block a user