mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
651 lines
36 KiB
C++
651 lines
36 KiB
C++
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramObject.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 "ProgramObject.h"
|
|
#include "ProgramLinkTask.h"
|
|
#include "ProgramSpirvTask.h"
|
|
#include <atomic>
|
|
#include <cstring>
|
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
|
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
|
|
|
const char* kDefaultFragmentShaderSource = R"(#version 460 core
|
|
layout(location = 0) out vec4 FragColor;
|
|
void main() {}
|
|
)";
|
|
|
|
|
|
namespace MobileGL::MG_State::GLState {
|
|
static std::atomic<Uint64> s_nextProgramLifetimeId = 1;
|
|
|
|
Uint64 ProgramObject::AllocateLifetimeId() {
|
|
return s_nextProgramLifetimeId.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
|
|
ProgramObject::~ProgramObject() { CancelLink(); }
|
|
|
|
// EnsureLinkJoined() is defined inline in ProgramObject.h (see the comment there for
|
|
// why: ~1200 call sites, no LTO). Only its blocking half lives here.
|
|
|
|
void ProgramObject::JoinPendingLink() const {
|
|
MOBILEGL_ASSERT(!MG_Util::Async::ShaderCompilePool::IsPoolThread(),
|
|
"ProgramObject::EnsureLinkJoined() reached from a pool thread; a job body must never read "
|
|
"GL-thread-owned objects");
|
|
|
|
// Move the node out FIRST. The publish below runs GL-thread-only code that reads
|
|
// link output through Artifacts() (ApplyDeferredDiagnostics can reach
|
|
// pGLContext->RecordError, and a future reader might not be so careful), and with
|
|
// m_pendingLink still set that would re-enter this function.
|
|
const SharedPtr<ProgramLinkTask> pending = Move(m_pendingLink);
|
|
m_pendingLink.reset();
|
|
|
|
pending->Wait();
|
|
if (pending->IsComplete()) {
|
|
// ONE move, not thirty cross-thread field assignments: the artifacts block is
|
|
// exactly what a link produces, so moving it IS the publish.
|
|
m_artifacts = Move(pending->artifacts);
|
|
// The second bump. The first one happened at ENQUEUE so every backend memo read
|
|
// "stale" for the whole pending window; this one invalidates anything a backend
|
|
// may have cached DURING that window, when m_artifacts still held the previous
|
|
// link's output. Without it a memo taken mid-window would survive the publish
|
|
// and describe a program that no longer exists.
|
|
BumpLinkObservableVersions();
|
|
}
|
|
// A node that settled as Cancelled published nothing, and m_artifacts still holds
|
|
// what Link()'s prologue left there: cleared, LINK_STATUS false, no info log. That is
|
|
// the correct answer for a link that was superseded or abandoned, and it is why no
|
|
// caller of CancelLink() has to repair anything afterwards.
|
|
|
|
// Worker-side log lines and any deferred GL error are raised HERE, on the GL thread,
|
|
// at the first join of the job that produced them - which is where a serial
|
|
// implementation would have produced them.
|
|
MG_Util::Async::ApplyDeferredDiagnostics(*pending);
|
|
}
|
|
|
|
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.
|
|
|
|
// Order matters, and it is the GL order. The shadow arrives zero-filled; the shaders'
|
|
// declared uniform initializers are what it should actually start from, and only then
|
|
// do the application's own writes - the ones it made while the layout did not exist
|
|
// yet - land on top. Seeding after the replay would clobber them.
|
|
ApplyUniformInitialValues();
|
|
ReplayBufferedUniformWrites();
|
|
|
|
// 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);
|
|
}
|
|
|
|
Bool ProgramObject::BufferUniformWrite(const Uint location, const SizeT byteOffsetInUniform, const void* source,
|
|
const SizeT byteSize) {
|
|
if (source == nullptr || byteSize == 0) return true; // nothing to record, nothing to join for
|
|
if (m_pendingUniformBytes.size() + byteSize > kMaxBufferedUniformBytes) {
|
|
// Pressure valve: stop growing and let the caller take the join. Say so once per
|
|
// program, because the interesting fact is WHICH program did it.
|
|
MGLOG_D("ProgramObject %u: buffered uniform writes exceeded %zu bytes during the SPIR-V window; the "
|
|
"write joins instead",
|
|
m_externalIndex, kMaxBufferedUniformBytes);
|
|
return false;
|
|
}
|
|
const SizeT dataOffset = m_pendingUniformBytes.size();
|
|
m_pendingUniformBytes.resize(dataOffset + byteSize);
|
|
std::memcpy(m_pendingUniformBytes.data() + dataOffset, source, byteSize);
|
|
m_pendingUniformWrites.push_back(PendingUniformWrite{.location = location,
|
|
.byteOffsetInUniform =
|
|
static_cast<Uint>(byteOffsetInUniform),
|
|
.byteSize = static_cast<Uint>(byteSize),
|
|
.dataOffset = static_cast<Uint>(dataOffset)});
|
|
return true;
|
|
}
|
|
|
|
// "uniform vec3 v = vec3(10, 20, 30);" - legal desktop GLSL since 1.20, and the value is
|
|
// what the uniform reads until glUniform* replaces it (and again after every relink).
|
|
// MobileGL parses with Vulkan-relaxed rules, which sweep default-block uniforms into
|
|
// MGL_GLOBAL_UBO; a block member cannot carry an initializer in SPIR-V, so glslang hands
|
|
// the folded constants over as a side-channel (TIntermediate::getUniformInitializers) and
|
|
// this is where they are honoured. Without it every such uniform silently read zero -
|
|
// which is what half of KHR-GL43.shader_storage_buffer_object was actually failing on.
|
|
//
|
|
// Writes go straight into the shadow rather than through glUniform*: this runs INSIDE the
|
|
// phase-B publish, so re-entering the join gate is not available, and the location space
|
|
// reflection assigns (one location per array element) is all that is needed.
|
|
void ProgramObject::ApplyUniformInitialValues() const {
|
|
// Through the phase-A gate, not off m_artifacts directly: phase B can be joined by a
|
|
// caller that has not read anything phase A publishes yet, and reading the raw field
|
|
// there would find the PREVIOUS link's block (or an empty one) and drop every
|
|
// initializer without a trace. Artifacts() is a no-op once phase A is in.
|
|
const auto& initializers = Artifacts().uniformInitialValues;
|
|
if (initializers.empty()) return;
|
|
if (m_spirv.globalUboScratch.empty() || m_spirv.uniformOffsets.empty()) {
|
|
// Phase B published no shadow (cancelled, or superseded by a relink). The program
|
|
// is not drawable; there is nowhere for these to land.
|
|
return;
|
|
}
|
|
|
|
Uint8* const scratch = m_spirv.globalUboScratch.data();
|
|
const SizeT uboSize = m_spirv.globalUboScratch.size();
|
|
|
|
for (const auto& init : initializers) {
|
|
// Scalars per array ELEMENT. A matrix element carries cols * rows of them, laid
|
|
// out column by column - which is also the order glslang folded them in.
|
|
const Int columns = init.matrixCols;
|
|
const Int rows = init.matrixRows;
|
|
const Int componentsPerElement = columns > 0 ? columns * rows : init.vectorSize;
|
|
const Int elements = init.arraySize;
|
|
if (componentsPerElement <= 0 || elements <= 0) continue;
|
|
|
|
// EbtDouble belongs with the floats now, not with the skipped types: every 64-bit
|
|
// float in a shader is narrowed to 32 bits before the module reaches a backend
|
|
// (ShaderTranspiler::DemoteFloat64Pass), so a `uniform double d = 1.5;` has exactly
|
|
// the 32-bit shadow encoding a `uniform float` does - and glslang already folded its
|
|
// value into floatValues, which is a vector<double> either way. Leaving it out meant
|
|
// the initializer was silently dropped and the uniform came up zero.
|
|
const Bool isFloat = init.basicType == glslang::EbtFloat ||
|
|
init.basicType == glslang::EbtFloat16 ||
|
|
init.basicType == glslang::EbtDouble;
|
|
const Bool isInt = init.basicType == glslang::EbtInt || init.basicType == glslang::EbtUint ||
|
|
init.basicType == glslang::EbtBool;
|
|
// Anything else (64-bit integers) has no 32-bit shadow encoding here, and a
|
|
// half-written uniform is worse than an untouched one.
|
|
if (!isFloat && !isInt) continue;
|
|
const SizeT provided = isFloat ? init.floatValues.size() : init.intValues.size();
|
|
if (provided < static_cast<SizeT>(componentsPerElement) * static_cast<SizeT>(elements)) continue;
|
|
|
|
const Int baseLocation = GetUniformLocation(init.name);
|
|
if (baseLocation < 0) continue; // optimized away, or not a default-block uniform
|
|
|
|
for (Int element = 0; element < elements; ++element) {
|
|
const Int location = baseLocation + element;
|
|
if (element > 0 && !UniformLocationsAliasSameUniform(baseLocation, location)) break;
|
|
if (!IsValidUniformLocation(location)) break;
|
|
const Uint offset = GetUniformOffset(static_cast<Uint>(location));
|
|
if (offset == kInvalidUniformOffset) continue;
|
|
|
|
// std140 pads every column of a float matrix out to a vec4, so the columns of
|
|
// a mat3 are 16 bytes apart even though each carries 12. The slot's own span
|
|
// states the stride the rest of the pipeline agreed on rather than guessing it.
|
|
const SizeT slotSpan = GetUniformStorageSpanInBytes(static_cast<Uint>(location));
|
|
const SizeT columnStride =
|
|
columns > 0 ? slotSpan / static_cast<SizeT>(columns) : slotSpan;
|
|
const Int componentsPerColumn = columns > 0 ? rows : componentsPerElement;
|
|
const Int columnCount = columns > 0 ? columns : 1;
|
|
|
|
for (Int column = 0; column < columnCount; ++column) {
|
|
const SizeT byteOffset = static_cast<SizeT>(offset) + static_cast<SizeT>(column) * columnStride;
|
|
const SizeT writeSize = static_cast<SizeT>(componentsPerColumn) * sizeof(Uint32);
|
|
if (byteOffset + writeSize > uboSize) break;
|
|
const SizeT firstComponent = static_cast<SizeT>(element) * componentsPerElement +
|
|
static_cast<SizeT>(column) * componentsPerColumn;
|
|
for (Int component = 0; component < componentsPerColumn; ++component) {
|
|
const SizeT source = firstComponent + static_cast<SizeT>(component);
|
|
Uint8* const destination = scratch + byteOffset + component * sizeof(Uint32);
|
|
if (isFloat) {
|
|
const Float value = static_cast<Float>(init.floatValues[source]);
|
|
std::memcpy(destination, &value, sizeof(value));
|
|
} else {
|
|
const Int32 value = static_cast<Int32>(init.intValues[source]);
|
|
std::memcpy(destination, &value, sizeof(value));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
MarkUBOContentDirty();
|
|
}
|
|
|
|
void ProgramObject::ReplayBufferedUniformWrites() const {
|
|
if (m_pendingUniformWrites.empty()) {
|
|
m_pendingUniformBytes.clear();
|
|
return;
|
|
}
|
|
|
|
// Drain into locals first: MarkUBOContentDirty below is a plain counter bump, but a
|
|
// future reader of this function should not be able to observe a half-drained buffer.
|
|
Vector<PendingUniformWrite> writes;
|
|
Vector<Uint8> bytes;
|
|
writes.swap(m_pendingUniformWrites);
|
|
bytes.swap(m_pendingUniformBytes);
|
|
|
|
if (m_spirv.globalUboScratch.empty() || m_spirv.uniformOffsets.empty()) {
|
|
// Phase B produced nothing (cancelled at teardown, or a relink superseded it).
|
|
// The program is not drawable, so there is nowhere for these to land and nothing
|
|
// that could observe them.
|
|
MGLOG_D("ProgramObject %u: dropping %zu buffered uniform write(s); the SPIR-V job published no shadow",
|
|
m_externalIndex, writes.size());
|
|
return;
|
|
}
|
|
|
|
Uint8* const scratch = m_spirv.globalUboScratch.data();
|
|
const SizeT uboSize = m_spirv.globalUboScratch.size();
|
|
for (const PendingUniformWrite& write : writes) {
|
|
if (write.location >= m_spirv.uniformOffsets.size()) continue;
|
|
const Uint offset = m_spirv.uniformOffsets[write.location];
|
|
if (offset == kInvalidUniformOffset ||
|
|
static_cast<SizeT>(offset) + write.byteOffsetInUniform + write.byteSize > uboSize) {
|
|
// Same verdict the live write path reaches for a uniform without backing
|
|
// storage: log and drop, rather than fault.
|
|
MGLOG_E_ONCE("ProgramObject %u: buffered uniform write at location %u has no backing storage "
|
|
"(offset=%u size=%u uboSize=%zu); dropping write",
|
|
m_externalIndex, write.location, offset, write.byteSize, uboSize);
|
|
continue;
|
|
}
|
|
Uint8* const destination = scratch + offset + write.byteOffsetInUniform;
|
|
const Uint8* const sourceBytes = bytes.data() + write.dataOffset;
|
|
// The same bytes-equal dedupe the live path applies, per record and in order, so
|
|
// the "an identical write does not move the content version" property survives
|
|
// the detour byte for byte.
|
|
if (std::memcmp(destination, sourceBytes, write.byteSize) == 0) continue;
|
|
std::memcpy(destination, sourceBytes, write.byteSize);
|
|
MarkUBOContentDirty();
|
|
}
|
|
}
|
|
|
|
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();
|
|
// Buffered writes belong to the link that is being abandoned. A relink resets
|
|
// every uniform to its initial value anyway (GL 4.6 core 7.6), and the other two
|
|
// callers are destruction and glProgramBinary's mandated failure, so there is
|
|
// nothing left that could want them.
|
|
m_pendingUniformWrites.clear();
|
|
m_pendingUniformBytes.clear();
|
|
}
|
|
if (!m_pendingLink) return;
|
|
m_pendingLink->Cancel();
|
|
m_pendingLink.reset();
|
|
}
|
|
|
|
void ProgramObject::BumpLinkObservableVersions() const {
|
|
// Relinking regenerates the SPIR-V, so any backend-cached state keyed on
|
|
// m_backendStateVersion (e.g. the content-hash memo) must be invalidated,
|
|
// along with every link-derived backend cache (m_linkVersion) and the
|
|
// last-uploaded-UBO gate (a relink resets uniforms to their initial values,
|
|
// and that reset must reach the GPU). GL-THREAD ONLY: bumped once per link
|
|
// in Link()'s prologue and at the publish, and by glProgramBinary's mandated
|
|
// failure - never from the link body, which runs on a pool worker: a
|
|
// non-atomic ++ there against the draw path's reads would be exactly the
|
|
// lost-invalidation memo hazard.
|
|
++m_backendStateVersion;
|
|
++m_linkVersion;
|
|
MarkUBOContentDirty();
|
|
}
|
|
|
|
void ProgramObject::ResetLinkArtifacts(LinkArtifacts& artifacts) {
|
|
// Worker-safe pure clear: touches LinkArtifacts only, which is why the link body can
|
|
// call it on its own block. The link-observable version bumps live in
|
|
// BumpLinkObservableVersions() on the GL thread.
|
|
|
|
// Deliberately NOT `artifacts = {}`: infoLog, linkedFragDataLocation/Index and the
|
|
// geometry strip-capture pair live in LinkArtifacts but are not part of what this
|
|
// 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.uniformLocations.clear();
|
|
artifacts.glUniformIndexToTProgram.clear();
|
|
artifacts.tProgramUniformIndexToGl.clear();
|
|
artifacts.glBlockIndexToTProgram.clear();
|
|
artifacts.tProgramBlockIndexToGl.clear();
|
|
artifacts.linkedExplicitUniformLocations.clear();
|
|
artifacts.uniformInitialValues.clear();
|
|
artifacts.uniformIndexInTProgram.clear();
|
|
// GL resets every uniform to its initial value at link, so nothing is "written since
|
|
// link" any more - and the locations these bits index no longer mean anything either.
|
|
artifacts.writtenUniformLocationBits.clear();
|
|
artifacts.writtenUniformIndexBits.clear();
|
|
artifacts.writtenUniformIndices.clear();
|
|
artifacts.uniformSamplerOrImageUnitIndex.clear();
|
|
artifacts.explicitOpaqueUniformBindings.clear();
|
|
artifacts.uniformBlockIndexByName.clear();
|
|
artifacts.uniformBlockBinding.clear();
|
|
artifacts.shaderStorageBlockBinding.clear();
|
|
// Cleared with it: the seed above is re-derived from the newly attached shaders on every
|
|
// link, so a stale set would otherwise default a block the new sources do declare a
|
|
// binding for.
|
|
artifacts.storageBlocksWithoutBinding.clear();
|
|
artifacts.attribs.clear();
|
|
artifacts.attribTypes.clear();
|
|
artifacts.activeUniformCount = 0;
|
|
artifacts.maxUniformLocation = 0;
|
|
artifacts.uniformNameMaxLength = 0;
|
|
artifacts.attribInNameMaxLength = 0;
|
|
artifacts.uniformBlockNameMaxLength = 0;
|
|
artifacts.xfbVaryings.clear();
|
|
artifacts.xfbInterfaceNames.clear();
|
|
artifacts.xfbStrides.clear();
|
|
artifacts.xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
|
artifacts.xfbVaryingNameMaxLength = 0;
|
|
artifacts.xfbNeedsScatteredCapture = false;
|
|
artifacts.xfbPackedStride = 0;
|
|
artifacts.gsInputPrimitive = GL_NONE;
|
|
artifacts.linkStatus = false;
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ProgramObject::ShaderIsAttached(const SharedPtr<ShaderObject>& shader) {
|
|
MGLOG_D("ProgramObject %u: ShaderIsAttached check for shader %p", m_externalIndex, shader.get());
|
|
auto it = std::find_if(m_shaders.begin(), m_shaders.end(),
|
|
[shader](const SharedPtr<ShaderObject>& s) { return s.get() == shader.get(); });
|
|
bool attached = it != m_shaders.end();
|
|
MGLOG_D("ProgramObject %u: ShaderIsAttached -> %s", m_externalIndex, attached ? "true" : "false");
|
|
return attached;
|
|
}
|
|
|
|
// NO CancelLink here, nor in DetachShader below. Both only edit the attach lists, which
|
|
// a pending link does not read - it snapshotted (stage, source, compile node) per shader
|
|
// at enqueue and is isolated from every later mutation. GL agrees: attaching or detaching
|
|
// takes effect at the NEXT link and leaves the current LINK_STATUS alone, so cancelling
|
|
// would make `glLinkProgram; glAttachShader; glGetProgramiv(LINK_STATUS)` report FALSE
|
|
// for a link that succeeded - and would break glCreateShaderProgramv outright, since that
|
|
// is specified as link-then-detach and would discard its own link before anyone read it.
|
|
bool ProgramObject::AttachShader(const SharedPtr<ShaderObject>& shader) {
|
|
MGLOG_D("ProgramObject %u: AttachShader called for shader %p", m_externalIndex, shader.get());
|
|
if (ShaderIsAttached(shader)) {
|
|
MGLOG_D("ProgramObject %u: AttachShader - shader already attached, skipping", m_externalIndex);
|
|
return false;
|
|
}
|
|
m_shaders.emplace_back(shader);
|
|
MGLOG_D("ProgramObject %u: AttachShader - attached successfully, total shaders now %zu", m_externalIndex,
|
|
m_shaders.size());
|
|
return true;
|
|
}
|
|
|
|
bool ProgramObject::AttachShaderWithPinnedLinkInput(const LinkedShaderRef& ref) {
|
|
if (!AttachShader(ref.shader)) {
|
|
return false;
|
|
}
|
|
m_pinnedLinkInputs[ref.shader.get()] = ref;
|
|
return true;
|
|
}
|
|
|
|
SizeT ProgramObject::DetachShader(const SharedPtr<ShaderObject>& shader) {
|
|
MGLOG_D("DetachShader called for shader %p from ProgramObject %u", shader.get(), m_externalIndex);
|
|
if (!ShaderIsAttached(shader)) {
|
|
MGLOG_D("Shader %p is not attached to ProgramObject %u, cannot detach.", shader.get(), m_externalIndex);
|
|
return 0;
|
|
}
|
|
m_detachedShaders.push_back(shader);
|
|
MGLOG_D("Shader %p marked for detachment from ProgramObject %u", shader.get(), m_externalIndex);
|
|
return 1;
|
|
}
|
|
|
|
SizeT ProgramObject::RemoveShader(const SharedPtr<ShaderObject>& shader) {
|
|
MGLOG_D("ProgramObject %u: RemoveShader called for shader %p", m_externalIndex, shader.get());
|
|
auto count =
|
|
std::erase_if(m_shaders, [shader](const SharedPtr<ShaderObject>& s) { return s.get() == shader.get(); });
|
|
|
|
MGLOG_D("ProgramObject %u: RemoveShader - removed %zu shader(s), remaining %zu", m_externalIndex, count,
|
|
m_shaders.size());
|
|
return count;
|
|
}
|
|
|
|
void ProgramObject::AddDefaultFragmentShaderIfMissing() {
|
|
Bool needsDefaultFS = false;
|
|
for (const auto& shader : m_shaders) {
|
|
auto stage = shader->GetShaderStage();
|
|
if (stage == ShaderStage::Vertex) {
|
|
needsDefaultFS = true;
|
|
continue;
|
|
}
|
|
if (stage == ShaderStage::Fragment) {
|
|
needsDefaultFS = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!needsDefaultFS) return;
|
|
|
|
MGLOG_D("ProgramObject %u: No fragment shader attached, adding default fragment shader.", m_externalIndex);
|
|
SharedPtr<ShaderObject> defaultFS = MakeShared<ShaderObject>(ShaderStage::Fragment, 0);
|
|
defaultFS->SetShaderSource(kDefaultFragmentShaderSource);
|
|
defaultFS->Compile(); // TODO: use a global default FS object.
|
|
auto status = defaultFS->GetCompileStatus();
|
|
if (!status) {
|
|
MGLOG_E_ONCE("ProgramObject %u: Failed to compile default fragment shader. InfoLog:\n%s", m_externalIndex,
|
|
defaultFS->GetInfoLog().c_str());
|
|
return;
|
|
}
|
|
m_shaders.push_back(defaultFS);
|
|
MGLOG_D("ProgramObject %u: Default fragment shader added.", m_externalIndex);
|
|
}
|
|
|
|
void ProgramObject::Link(Bool addDefaultFSIfMissingForRenderingPipelineProgram) {
|
|
MGLOG_D("ProgramObject %u: Link start, shaders to link: %zu", m_externalIndex, m_shaders.size());
|
|
// The last link wins. A link still in flight is computing an answer this call is
|
|
// about to replace, and nothing has observed it yet (an observation would have
|
|
// joined), so it is dropped where it stands - no wait.
|
|
CancelLink();
|
|
|
|
// Bumped at ENQUEUE, not at publish, and that ordering is the whole invalidation
|
|
// story: from this instant every backend memo keyed on m_backendStateVersion /
|
|
// m_linkVersion reads "stale", so nothing can keep using the PREVIOUS link's
|
|
// reflection while the new one is still being computed. (The publish bumps a second
|
|
// time, for anything cached during the pending window itself.)
|
|
++m_backendStateVersion;
|
|
BumpLinkObservableVersions();
|
|
// A whole-struct reset, unlike ResetLinkArtifacts(): during the pending window this
|
|
// 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
|
|
for (const auto& detachedShader : m_detachedShaders) {
|
|
RemoveShader(detachedShader);
|
|
}
|
|
m_detachedShaders.clear();
|
|
|
|
if (addDefaultFSIfMissingForRenderingPipelineProgram) {
|
|
AddDefaultFragmentShaderIfMissing();
|
|
}
|
|
if (m_shaders.empty()) {
|
|
// This IS the last link now, and it consumed nothing.
|
|
m_linkedShaderSnapshot.clear();
|
|
m_artifacts.infoLog = "No shader objects are attached to program.";
|
|
MGLOG_E("ProgramObject %u: Link failed - no shader objects attached.", m_externalIndex);
|
|
return;
|
|
}
|
|
|
|
std::sort(m_shaders.begin(), m_shaders.end(),
|
|
[](const SharedPtr<ShaderObject>& a, const SharedPtr<ShaderObject>& b) {
|
|
return a->GetShaderStage() < b->GetShaderStage();
|
|
});
|
|
|
|
// ---- end of the GL-thread prologue: everything below is the snapshot ----
|
|
// Everything above mutates GL-thread-owned state (the attach lists, the version
|
|
// counters, the default-FS fixup) and must stay on the calling thread. Everything
|
|
// below is a pure function of what is copied into `in`, which is what lets the body
|
|
// run on a worker. Nothing here reads compile OUTPUT - taking the nodes without
|
|
// joining them is exactly what makes glLinkProgram not block on glCompileShader.
|
|
auto task = MakeShared<ProgramLinkTask>();
|
|
task->in.externalIndex = m_externalIndex;
|
|
task->in.env = MG_Util::ShaderTranspiler::GetCurrentCompileEnv();
|
|
task->in.enableSpirvValidation = MG_Config::Features.EnableSpirvValidation;
|
|
task->in.explicitAttribLocations = m_explicitAttribLocations;
|
|
task->in.explicitFragDataLocation = m_explicitFragDataLocation;
|
|
task->in.explicitFragDataIndex = m_explicitFragDataIndex;
|
|
task->in.requestedXfbVaryings = m_requestedXfbVaryings;
|
|
task->in.requestedXfbBufferMode = m_requestedXfbBufferMode;
|
|
task->in.maxFragmentOutputColorNumber = m_maxFragmentOutputColorNumber;
|
|
|
|
Vector<SharedPtr<ShaderCompileTask>> deps;
|
|
deps.reserve(m_shaders.size());
|
|
task->in.shaders.reserve(m_shaders.size());
|
|
m_linkedShaderSnapshot.clear();
|
|
m_linkedShaderSnapshot.reserve(m_shaders.size());
|
|
for (const auto& shader : m_shaders) {
|
|
// A pipeline composite pins the (source, node) each stage program's LAST link
|
|
// consumed (AttachShaderWithPinnedLinkInput); an ordinary program takes the
|
|
// shader's current ones. Without the pin a post-link recompile would leak a
|
|
// shader the stage program never linked into the composite.
|
|
SharedPtr<const String> sourcePtr = shader->GetShaderSourcePtr();
|
|
SharedPtr<ShaderCompileTask> node = shader->CompiledNodeForLink();
|
|
if (const auto pinned = m_pinnedLinkInputs.find(shader.get()); pinned != m_pinnedLinkInputs.end()) {
|
|
sourcePtr = pinned->second.source;
|
|
node = pinned->second.node;
|
|
}
|
|
if (node) {
|
|
// This link is now an observer of that node's result, and the ShaderObject is
|
|
// no longer the only route to it: without the marker, the ordinary
|
|
// link-then-detach-then-delete teardown would cancel a compile this link is
|
|
// waiting on and turn a successful link into GL_FALSE.
|
|
node->MarkLinkReferenced();
|
|
if (!node->IsTerminal()) deps.push_back(node);
|
|
}
|
|
task->in.shaders.push_back({shader->GetShaderStage(), sourcePtr, node});
|
|
// What "as last linked" will mean for this program from now on - the pipeline
|
|
// composite cache rebuilds from exactly this set (GetProgramForDraw).
|
|
m_linkedShaderSnapshot.push_back({shader, sourcePtr, 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
|
|
// 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();
|
|
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);
|
|
}
|
|
|
|
|
|
void ProgramObject::MarkAsDeleted() {
|
|
MGLOG_D("ProgramObject %u: MarkAsDeleted called (was %s)", m_externalIndex,
|
|
m_deleteStatus ? "deleted" : "not deleted");
|
|
m_deleteStatus = true;
|
|
MGLOG_D("ProgramObject %u: MarkAsDeleted - now marked deleted", m_externalIndex);
|
|
}
|
|
|
|
Vector<SharedPtr<ShaderObject>>& ProgramObject::GetAttachedShaders() {
|
|
MGLOG_D("ProgramObject %u: GetAttachedShaders called, returning %zu shaders", m_externalIndex,
|
|
m_shaders.size());
|
|
return m_shaders;
|
|
}
|
|
|
|
const Vector<SharedPtr<ShaderObject>>& ProgramObject::GetAttachedShaders() const {
|
|
return m_shaders;
|
|
}
|
|
|
|
|
|
void ProgramObject::SetExplicitVertexInLocation(Uint index, const char* name) {
|
|
MGLOG_D("ProgramObject %u: SetExplicitVertexInLocation called name='%s' index=%u", m_externalIndex, name,
|
|
index);
|
|
m_explicitAttribLocations[name] = index;
|
|
MGLOG_D("ProgramObject %u: SetExplicitVertexInLocation - stored explicit location for '%s' -> %u",
|
|
m_externalIndex, name, index);
|
|
}
|
|
|
|
void ProgramObject::SetExplicitFragmentOutLocation(Uint index, const char* name) {
|
|
MGLOG_D("ProgramObject %u: SetExplicitFragmentOutLocation called name='%s' index=%u", m_externalIndex, name,
|
|
index);
|
|
m_explicitFragDataLocation[name] = index;
|
|
MGLOG_D("ProgramObject %u: SetExplicitFragmentOutLocation - stored explicit location for '%s' -> %u",
|
|
m_externalIndex, name, index);
|
|
}
|
|
|
|
void ProgramObject::SetExplicitFragmentOutIndex(Uint colorIndex, const char* name) {
|
|
m_explicitFragDataIndex[name] = colorIndex;
|
|
MGLOG_D("ProgramObject %u: SetExplicitFragmentOutIndex - stored color index for '%s' -> %u", m_externalIndex,
|
|
name, colorIndex);
|
|
}
|
|
|
|
|
|
Int ProgramObject::GetFragmentDataLocation(const char* name) {
|
|
// Answered from the OWNED pipe-output snapshot, not from Artifacts().program. The live
|
|
// TProgram is null on a translation-cache L1 hit - that is the entire point of the memo
|
|
// - and it is also null for any program that never linked. The old `if
|
|
// (!Artifacts().program) return -1` guard silently produced the never-linked answer for
|
|
// a perfectly good cached program, so glGetFragDataLocation returned -1 for every
|
|
// fragment output of it. The empty snapshot gives the never-linked case the same -1
|
|
// without needing the guard at all.
|
|
if (!name) return -1;
|
|
|
|
const auto explicitLocation = Artifacts().linkedFragDataLocation.find(name);
|
|
for (const PipeOutputReflection& output : Artifacts().pipeOutputReflection) {
|
|
if (output.name != name) continue;
|
|
if (explicitLocation != Artifacts().linkedFragDataLocation.end()) {
|
|
return static_cast<Int>(explicitLocation->second);
|
|
}
|
|
return output.location;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
Int ProgramObject::GetFragmentDataIndex(const char* name) {
|
|
// Only an active user-defined fragment output has an index; reuse the location lookup to test
|
|
// that. The color index defaults to 0 unless glBindFragDataLocationIndexed bound it to 1.
|
|
// (Shader-side layout(index = ...) qualifiers are not reflected here, only API bindings.)
|
|
if (GetFragmentDataLocation(name) < 0) return -1;
|
|
const auto it = Artifacts().linkedFragDataIndex.find(name);
|
|
return it != Artifacts().linkedFragDataIndex.end() ? static_cast<Int>(it->second) : 0;
|
|
}
|
|
} // namespace MobileGL::MG_State::GLState
|