[Perf] (MG_Pipe): a per-VAO memo keeps the emitters' handles and buffer set across draws, and an unchanged context-value record skips its hash (rd12 Espryt monolith +1%).

This commit is contained in:
2026-10-07 12:32:05 -04:00
parent d07ec0255b
commit 80dc9d45d9
4 changed files with 82 additions and 5 deletions
+13 -2
View File
@@ -3165,11 +3165,22 @@ namespace MobileGL::MG_Pipe {
values.TransformFeedbackGeneration = ctx.GetTransformFeedbackGeneration();
values.BoundTransformFeedbackLifetimeId = ctx.GetBoundTransformFeedbackLifetimeId();
values.TransformFeedbackCapturedVertices = ctx.GetTransformFeedbackCapturedVertices();
// P14: the record is rebuilt every draw and nearly always equal to the last one. When it
// is byte-equal to the record whose hash the suppressor still holds, the suppressor
// would drop it: skip the hash. (An InvalidateAll changes what the suppressor holds,
// so a reset still re-emits.)
static MGPContextValues s_lastValues{};
static Uint64 s_lastHash = 0;
auto& suppressor = MGPipeSetHashSuppressorInstance();
if (s_lastHash != 0 && std::memcmp(&values, &s_lastValues, sizeof(values)) == 0 &&
suppressor.LastEmitted(MGPipeSuppressorSlot::SetContextValues) == s_lastHash)
return 0;
// The whole record is the hash input, padding included - `values{}` zeroes it, so
// the pad bytes are defined and the hash is stable.
const Uint64 contentHash = XXH64(&values, sizeof(values), 0);
if (!MGPipeSetHashSuppressorInstance().ShouldEmit(MGPipeSuppressorSlot::SetContextValues,
contentHash)) {
s_lastValues = values;
s_lastHash = contentHash == 0 ? 1 : contentHash; // the suppressor's own latch value
if (!suppressor.ShouldEmit(MGPipeSuppressorSlot::SetContextValues, contentHash)) {
return 0;
}
MGPipeRouteSetContextValues(values);
+1
View File
@@ -308,6 +308,7 @@ namespace MobileGL::MG_Pipe {
}
void MGPipeSlotAllocator::Reset() {
++m_resetEpoch;
for (KindState& state : m_kinds) {
state.Slots.clear();
state.FreeList.clear();
+4
View File
@@ -120,6 +120,9 @@ namespace MobileGL::MG_Pipe {
// Context teardown / server reset / a unit test's fixture.
void Reset();
// Moves at every Reset(): a memo of handles minted by this allocator is good only while it
// holds (a handle lives as long as its object, and only a reset forgets that).
Uint64 ResetEpoch() const { return m_resetEpoch; }
private:
struct SlotState {
@@ -161,6 +164,7 @@ namespace MobileGL::MG_Pipe {
Uint32 BandLiveCount = 0;
};
Uint64 m_resetEpoch = 0;
KindState& StateOf(MGPipeKind kind);
const KindState& StateOf(MGPipeKind kind) const;
// The SlotState a (kind, slot) names, in whichever of the two vectors holds it, or
+64 -3
View File
@@ -168,7 +168,12 @@ namespace MobileGL::MG_Pipe {
const Uint64 lifetimeId = vao->GetLifetimeId();
const Uint32 configVersion = vao->GetConfigVersion();
const MGPipeHandle handle = MGPipeSlots().Acquire(MGPipeKind::VertexElementsCso, lifetimeId);
// The VAO's handle is the allocator's for the object's whole life (only an allocator
// reset forgets it), so it is resolved once per VAO rather than per draw (VaoMemo).
VaoMemo& memo = MemoFor(*vao);
if (MGPipeHandleIsNull(memo.ElementsHandle))
memo.ElementsHandle = MGPipeSlots().Acquire(MGPipeKind::VertexElementsCso, lifetimeId);
const MGPipeHandle handle = memo.ElementsHandle;
const SizeT slot = handle.Slot;
if (slot >= m_latch.size()) m_latch.resize(slot + 1);
Latch& latch = m_latch[slot];
@@ -200,7 +205,20 @@ namespace MobileGL::MG_Pipe {
const Array<MGPipeHandle, kMGPipeMaxVertexAttribs>* ownedClientBuffers = nullptr) {
const auto& vao = ctx.GetBoundVertexArray();
Uint32 count = 0;
if (vao) {
// The resolved set is a function of the VAO's configuration alone (buffers, strides,
// divisors and enables all move its version; its buffers' handles live as long as the
// VAO holds the buffers), so it is walked once per configuration rather than per draw -
// a Minecraft frame binds hundreds of chunk VAOs. Not for client-memory arrays, whose
// entries the draw itself supplies.
VaoMemo* memo = vao && ownedClientBuffers == nullptr ? &MemoFor(*vao) : nullptr;
if (memo != nullptr && memo->BuffersValid && memo->ConfigVersion == vao->GetConfigVersion()) {
count = memo->BufferCount;
for (SizeT i = 0; i < count; ++i) {
m_entries[i] = memo->Buffers[i];
MGPipeResourceTrackerInstance().NoteBoundAs(m_entries[i].Res, BufferTarget::Vertex);
}
} else if (vao) {
Bool memoizable = memo != nullptr;
for (SizeT i = 0; i < kAttribs; ++i) {
if (vao->GetAttribute(static_cast<Uint>(i)).Enabled) count = static_cast<Uint32>(i) + 1;
}
@@ -230,6 +248,13 @@ namespace MobileGL::MG_Pipe {
entry.Stride = static_cast<Uint32>(attrib.Stride);
entry.Divisor = static_cast<Uint32>(attrib.Divisor);
entry.BindingIndex = static_cast<Uint32>(i);
if (attrib.Enabled && !attrib.Buffer) memoizable = false;
}
if (memo != nullptr) {
memo->BuffersValid = memoizable && count <= kVaoMemoBuffers;
memo->ConfigVersion = vao->GetConfigVersion();
memo->BufferCount = count;
for (SizeT i = 0; i < count && i < kVaoMemoBuffers; ++i) memo->Buffers[i] = m_entries[i];
}
}
@@ -261,7 +286,13 @@ namespace MobileGL::MG_Pipe {
m_lastIndex = MGPIndexBuffer{};
if (vao) {
if (const auto& bound = vao->GetIndexBufferBindingSlot().GetBoundObject()) {
m_lastIndex.Res = MGPipeSlots().Acquire(MGPipeKind::Buffer, bound->GetLifetimeId());
// Per VAO, keyed on the element buffer's own lifetime id (VaoMemo).
VaoMemo& memo = MemoFor(*vao);
if (memo.IndexLifetimeId != bound->GetLifetimeId() || MGPipeHandleIsNull(memo.IndexHandle)) {
memo.IndexLifetimeId = bound->GetLifetimeId();
memo.IndexHandle = MGPipeSlots().Acquire(MGPipeKind::Buffer, memo.IndexLifetimeId);
}
m_lastIndex.Res = memo.IndexHandle;
// The INDEX|ELEMENT_ARRAY bind bits. ELEMENT_ARRAY's D-B7 reader
// (kCapNeedsHostIndexBytes) is RESERVED (P8-F) and was never built.
// Noted at every draw for RefreshBindMask's reason: an EBO defined through
@@ -353,6 +384,36 @@ namespace MobileGL::MG_Pipe {
void ResetCounters() { m_creates = m_binds = m_bufferSets = m_indexSets = 0; }
private:
// P14: what EmitVertexElements / EmitVertexBuffers / EmitIndexBuffer resolve from a VAO,
// direct-mapped on the VAO's lifetime id (monotonic, never reused) and dropped whole at an
// allocator reset. A colliding VAO simply takes the entry over. It changes no record: the
// same handles and entries are emitted, only found without the per-draw walk and lookups.
static constexpr SizeT kVaoMemoEntries = 1024;
static constexpr SizeT kVaoMemoBuffers = 8;
struct VaoMemo {
Uint64 LifetimeId = 0;
Uint64 SlotEpoch = 0;
MGPipeHandle ElementsHandle = kMGPipeNullHandle;
Uint64 IndexLifetimeId = 0;
MGPipeHandle IndexHandle = kMGPipeNullHandle;
Bool BuffersValid = false;
Uint32 ConfigVersion = 0;
Uint32 BufferCount = 0;
Array<MGPVertexBuffer, kVaoMemoBuffers> Buffers{};
};
VaoMemo& MemoFor(const VertexArrayObject& vao) {
const Uint64 lifetimeId = vao.GetLifetimeId();
const Uint64 epoch = MGPipeSlots().ResetEpoch();
VaoMemo& memo = m_vaoMemo[lifetimeId % kVaoMemoEntries];
if (memo.LifetimeId != lifetimeId || memo.SlotEpoch != epoch) {
memo = VaoMemo{};
memo.LifetimeId = lifetimeId;
memo.SlotEpoch = epoch;
}
return memo;
}
Array<VaoMemo, kVaoMemoEntries> m_vaoMemo{};
struct Latch {
// The PER-CONTEXT half: "has this emitter told THIS server about this handle's
// configuration". Cleared by Reset() at every make-current.