mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +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)) {
|
if (!programObject.IsUniformOpaqueAtLocation(location)) {
|
||||||
MGLOG_D("%s: program = %d, location = %d, maxLocation = %d", __func__, programObject.GetExternalIndex(),
|
MGLOG_D("%s: program = %d, location = %d, maxLocation = %d", __func__, programObject.GetExternalIndex(),
|
||||||
location, programObject.GetMaxUniformLocation());
|
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 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);
|
SizeT writeSize = ItemCount * sizeof(T);
|
||||||
if (size < writeSize) {
|
if (size < writeSize) {
|
||||||
// Metadata bug: degrade to a clamped copy instead of killing the process.
|
// 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);
|
__func__, programObject.GetExternalIndex(), location, ItemCount * sizeof(T), size);
|
||||||
writeSize = 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 ||
|
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||||
offset + byteOffsetInsideUniform + writeSize > uboSize) {
|
offset + byteOffsetInsideUniform + writeSize > uboSize) {
|
||||||
// Should not happen: linking gives every settable uniform backing
|
// Should not happen: linking gives every settable uniform backing
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "ProgramLinkTask.h"
|
#include "ProgramLinkTask.h"
|
||||||
#include "ProgramSpirvTask.h"
|
#include "ProgramSpirvTask.h"
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <cstring>
|
||||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
#include <MG_Util/ShaderTranspiler/CompileEnv.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
|
// A node that settled as Cancelled published nothing, so m_spirv stays empty with
|
||||||
// spirvStatus false: linked, queryable, not drawable. Nothing to repair.
|
// 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
|
// 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
|
// 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
|
// 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);
|
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() {
|
void ProgramObject::CancelLink() {
|
||||||
// Phase B first: it is chained behind phase A, so cancelling A would otherwise run A's
|
// 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
|
// 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) {
|
if (m_pendingSpirv) {
|
||||||
m_pendingSpirv->Cancel();
|
m_pendingSpirv->Cancel();
|
||||||
m_pendingSpirv.reset();
|
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;
|
if (!m_pendingLink) return;
|
||||||
m_pendingLink->Cancel();
|
m_pendingLink->Cancel();
|
||||||
|
|||||||
@@ -397,6 +397,25 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
void MarkUBOContentDirty() const {
|
void MarkUBOContentDirty() const {
|
||||||
if (++m_uboContentVersion == ~0u) m_uboContentVersion = 0;
|
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; }
|
Uint32 GetBackendStateVersion() const { return m_backendStateVersion; }
|
||||||
// Bumped only by (re)linking — lets backends detect that every piece of
|
// Bumped only by (re)linking — lets backends detect that every piece of
|
||||||
// link-derived reflection (locations, block order, UBO layout) is stale.
|
// link-derived reflection (locations, block order, UBO layout) is stale.
|
||||||
@@ -899,6 +918,25 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
void JoinPendingSpirv() const;
|
void JoinPendingSpirv() const;
|
||||||
Bool IsPendingSpirvTerminal() 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() {
|
LinkArtifacts& Artifacts() {
|
||||||
EnsureLinkJoined();
|
EnsureLinkJoined();
|
||||||
return m_artifacts;
|
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
|
// 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.
|
// published, the query surface is live) while this is still set.
|
||||||
mutable SharedPtr<ProgramSpirvTask> m_pendingSpirv;
|
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
|
} // namespace MobileGL::MG_State::GLState
|
||||||
|
|||||||
Reference in New Issue
Block a user