mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +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:
@@ -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
|
||||
Reference in New Issue
Block a user