diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp index af0ca449..606099ef 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp @@ -1120,10 +1120,9 @@ namespace MobileGL::MG_Impl::GLImpl { if (!programObject.IsUniformOpaqueAtLocation(location)) { MGLOG_D("%s: program = %d, location = %d, maxLocation = %d", __func__, programObject.GetExternalIndex(), location, programObject.GetMaxUniformLocation()); + // Everything up to and including the clamp is phase-A data (the uniform's GL type + // decides its size), so it is answered without joining anything. const SizeT size = programObject.GetUniformSizesInBytes(location); - const Uint offset = programObject.GetUniformOffset(location); - char* pUBO = static_cast(programObject.MapUBO()); - const SizeT uboSize = programObject.GetUBOSize(); SizeT writeSize = ItemCount * sizeof(T); if (size < writeSize) { // Metadata bug: degrade to a clamped copy instead of killing the process. @@ -1132,6 +1131,18 @@ namespace MobileGL::MG_Impl::GLImpl { __func__, programObject.GetExternalIndex(), location, ItemCount * sizeof(T), size); writeSize = size; } + // The uniform shadow's LAYOUT is phase-B data, so a write that lands while the + // SPIR-V job is still running is recorded and replayed at its publish instead of + // joining it. This is the hot path for a shaderpack that sets its uniforms + // immediately after glLinkProgram. BufferUniformWrite declines (and we fall + // through, joining) only past its size budget. + if (programObject.IsSpirvPending() && + programObject.BufferUniformWrite(location, byteOffsetInsideUniform, value, writeSize)) { + return; + } + const Uint offset = programObject.GetUniformOffset(location); + char* pUBO = static_cast(programObject.MapUBO()); + const SizeT uboSize = programObject.GetUBOSize(); if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset || offset + byteOffsetInsideUniform + writeSize > uboSize) { // Should not happen: linking gives every settable uniform backing diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp index e097b82f..4a54eda9 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp @@ -10,6 +10,7 @@ #include "ProgramLinkTask.h" #include "ProgramSpirvTask.h" #include +#include #include #include #include @@ -89,6 +90,10 @@ namespace MobileGL::MG_State::GLState { // A node that settled as Cancelled published nothing, so m_spirv stays empty with // spirvStatus false: linked, queryable, not drawable. Nothing to repair. + // Before the version bump, and before any caller can read the shadow: the writes the + // application made while the layout did not exist yet. + 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 @@ -99,6 +104,75 @@ namespace MobileGL::MG_State::GLState { 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(byteOffsetInUniform), + .byteSize = static_cast(byteSize), + .dataOffset = static_cast(dataOffset)}); + return true; + } + + 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 writes; + Vector 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(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("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 @@ -111,6 +185,12 @@ namespace MobileGL::MG_State::GLState { 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(); diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index bb0d1dfc..c18af57f 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -397,6 +397,25 @@ namespace MobileGL::MG_State::GLState { void MarkUBOContentDirty() const { if (++m_uboContentVersion == ~0u) m_uboContentVersion = 0; } + // ---- glUniform* inside the phase-A -> phase-B window ---- + // + // True while the program is fully linked and fully queryable but its uniform shadow's + // LAYOUT (which the optimized SPIR-V decides) does not exist yet. A non-opaque + // glUniform* write in that window is RECORDED rather than joined, and replayed into + // the shadow at the phase-B publish - so a pack that sets its uniforms immediately + // after glLinkProgram never waits for SPIR-V. + // + // Nothing can observe the difference: the only route to those bytes is glGetUniform* + // (and a draw), and both of those go through the phase-B gate, which replays first. + // The OPAQUE branch of glUniform* is deliberately not buffered - a sampler unit is + // phase-A state (uniformSamplerOrImageUnitIndex), so glUniform1i(samplerLoc, unit) + // right after a link stays a zero-join operation, which is exactly what Iris does. + Bool IsSpirvPending() const { return m_pendingSpirv != nullptr; } + // Records one write. Returns false if it declined to buffer - the caller must then + // perform the write directly (which joins). Declining is the pressure valve for an + // application that writes megabytes of uniforms into a single pending window. + Bool BufferUniformWrite(Uint location, SizeT byteOffsetInUniform, const void* source, SizeT byteSize); + Uint32 GetBackendStateVersion() const { return m_backendStateVersion; } // Bumped only by (re)linking — lets backends detect that every piece of // link-derived reflection (locations, block order, UBO layout) is stale. @@ -899,6 +918,25 @@ namespace MobileGL::MG_State::GLState { void JoinPendingSpirv() const; Bool IsPendingSpirvTerminal() const; + // One buffered non-opaque glUniform* write. `dataOffset` indexes m_pendingUniformBytes, + // which is one append-only blob rather than a per-record allocation. + struct PendingUniformWrite { + Uint location = 0; + Uint byteOffsetInUniform = 0; + Uint byteSize = 0; + Uint dataOffset = 0; + }; + // Replays the buffer into the freshly published shadow, in write order, and drains it. + // Each record re-does the bounds check and the bytes-equal dedupe the live write path + // performs, so "an identical write does not move the content version" survives the + // detour exactly - and a record that really does change bytes moves the version, which + // is what makes a backend re-upload the UBO it cached during the window. + void ReplayBufferedUniformWrites() const; + // Past this, BufferUniformWrite declines and the write joins instead. Sized so an + // ordinary pack load never reaches it (a pending window is one program's worth of + // uniforms) while a pathological writer cannot grow the heap without bound. + static constexpr SizeT kMaxBufferedUniformBytes = 4u << 20; + LinkArtifacts& Artifacts() { EnsureLinkJoined(); return m_artifacts; @@ -994,5 +1032,10 @@ namespace MobileGL::MG_State::GLState { // 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 m_pendingSpirv; + // glUniform* writes taken while m_pendingSpirv was set, in call order, plus their + // bytes. Drained by the phase-B publish and cleared by every cancel site (a relink's + // uniforms are not the previous link's uniforms). + mutable Vector m_pendingUniformWrites; + mutable Vector m_pendingUniformBytes; }; } // namespace MobileGL::MG_State::GLState