mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[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:
@@ -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);
|
||||
|
||||
@@ -308,6 +308,7 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
|
||||
void MGPipeSlotAllocator::Reset() {
|
||||
++m_resetEpoch;
|
||||
for (KindState& state : m_kinds) {
|
||||
state.Slots.clear();
|
||||
state.FreeList.clear();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user