mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Feat] (MG_State, MG_Impl): buffer non-opaque glUniform writes across the SPIR-V window and replay them at its publish
This commit is contained in:
@@ -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<char*>(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<char*>(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
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#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>
|
||||
@@ -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<Uint>(byteOffsetInUniform),
|
||||
.byteSize = static_cast<Uint>(byteSize),
|
||||
.dataOffset = static_cast<Uint>(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<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("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();
|
||||
|
||||
@@ -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<ProgramSpirvTask> 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<PendingUniformWrite> m_pendingUniformWrites;
|
||||
mutable Vector<Uint8> m_pendingUniformBytes;
|
||||
};
|
||||
} // namespace MobileGL::MG_State::GLState
|
||||
|
||||
Reference in New Issue
Block a user