[Perf] (MG_Pipe): a vertex array keeps its buffer and index bindings on the server, a small uniform change crosses as a span, and unchanged context values skip their record (rd12 split arms +5-10%).

This commit is contained in:
2026-10-07 14:58:21 -04:00
parent 114bd70122
commit c1497bb83e
20 changed files with 398 additions and 21 deletions
+1
View File
@@ -59,6 +59,7 @@ namespace MobileGL::MG_Pipe {
binding.Res = info.IndexResource; binding.Res = info.IndexResource;
binding.IndexSize = info.IndexSize; binding.IndexSize = info.IndexSize;
MGPipeRouteSetIndexBuffer(binding); MGPipeRouteSetIndexBuffer(binding);
MGPipeVertexInputEmitterInstance().NoteIndexBufferRoutedElsewhere(m_context);
m_indexBindingChanged = true; m_indexBindingChanged = true;
} }
+15 -3
View File
@@ -1422,6 +1422,7 @@ namespace MobileGL::MG_Pipe {
// refusal, and the refusal asserts (PipeApply.cpp's ResolveVertexElements), i.e. it // refusal, and the refusal asserts (PipeApply.cpp's ResolveVertexElements), i.e. it
// stops a verify build. // stops a verify build.
MGPipeVertexInputEmitter& emitter = MGPipeVertexInputEmitterInstance(); MGPipeVertexInputEmitter& emitter = MGPipeVertexInputEmitterInstance();
emitter.ForgetVao(lifetimeId);
const Bool published = emitter.RecordIsPublished(handle); const Bool published = emitter.RecordIsPublished(handle);
if (published) { if (published) {
MGPHandleOnly only{}; MGPHandleOnly only{};
@@ -3152,6 +3153,18 @@ namespace MobileGL::MG_Pipe {
// server-role-only fixture) - nothing is emitted and the fields keep being pulled, byte // server-role-only fixture) - nothing is emitted and the fields keep being pulled, byte
// for byte as before (G1). // for byte as before (G1).
Uint64 EmitContextValues(GLContext& ctx) { Uint64 EmitContextValues(GLContext& ctx) {
// P14: nothing the record reads has moved since the record whose hash the suppressor
// still holds was built (same context, same input generation): it would be dropped.
static MGPContextValues s_lastValues{};
static Uint64 s_lastHash = 0;
static Uint64 s_lastContextId = 0;
static Uint64 s_lastGeneration = 0;
auto& suppressor = MGPipeSetHashSuppressorInstance();
const Uint64 contextId = ctx.GetTextureContextId();
const Uint64 generation = ctx.GetContextValuesGeneration();
if (s_lastHash != 0 && contextId == s_lastContextId && generation == s_lastGeneration &&
suppressor.LastEmitted(MGPipeSuppressorSlot::SetContextValues) == s_lastHash)
return 0;
MGPContextValues values{}; MGPContextValues values{};
values.ActiveTextureUnit = static_cast<Uint32>(ctx.GetActiveTextureUnit()); values.ActiveTextureUnit = static_cast<Uint32>(ctx.GetActiveTextureUnit());
values.MaxTouchedTextureUnit = static_cast<Uint32>(ctx.GetMaxTouchedTextureUnit()); values.MaxTouchedTextureUnit = static_cast<Uint32>(ctx.GetMaxTouchedTextureUnit());
@@ -3174,9 +3187,8 @@ namespace MobileGL::MG_Pipe {
// is byte-equal to the record whose hash the suppressor still holds, the suppressor // 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, // would drop it: skip the hash. (An InvalidateAll changes what the suppressor holds,
// so a reset still re-emits.) // so a reset still re-emits.)
static MGPContextValues s_lastValues{}; s_lastContextId = contextId;
static Uint64 s_lastHash = 0; s_lastGeneration = generation;
auto& suppressor = MGPipeSetHashSuppressorInstance();
if (s_lastHash != 0 && std::memcmp(&values, &s_lastValues, sizeof(values)) == 0 && if (s_lastHash != 0 && std::memcmp(&values, &s_lastValues, sizeof(values)) == 0 &&
suppressor.LastEmitted(MGPipeSuppressorSlot::SetContextValues) == s_lastHash) suppressor.LastEmitted(MGPipeSuppressorSlot::SetContextValues) == s_lastHash)
return 0; return 0;
+36
View File
@@ -218,6 +218,37 @@ namespace MobileGL::MG_Pipe {
m_lastConstants = MGPGlobalConstants{}; m_lastConstants = MGPGlobalConstants{};
m_lastConstants.ShaderCso = cso; m_lastConstants.ShaderCso = cso;
m_lastConstants.Version = version; m_lastConstants.Version = version;
// P14: the server holds this program's previous image while the latch names it (every
// path that could drop or replace the applier's block clears the latch), so only the
// span that moved goes out - when it is small enough to be worth it.
const Uint8* image = static_cast<const Uint8*>(program->GetUBOData());
if (cso == m_constantsCso && m_constantsShadow.size() == size && size <= 0xffffu) {
Uint32 first = 0;
while (first < size && image[first] == m_constantsShadow[first]) ++first;
Uint32 last = size;
while (last > first && image[last - 1] == m_constantsShadow[last - 1]) --last;
// Nothing moved but the version: one byte carries the new version.
if (first == size) {
first = 0;
last = 1;
}
const Uint32 length = last - first;
if (length * 2 <= size) {
m_lastConstants.Range = (first << 16) | length;
m_lastConstants.Blob.Seg = kMGHostSpanSegNone;
m_lastConstants.Blob.Offset = reinterpret_cast<Uint64>(image + first);
m_lastConstants.Blob.Size = 0;
MGPipeRouteSetGlobalConstants(m_lastConstants, image + first, static_cast<Uint64>(length));
std::memcpy(m_constantsShadow.data() + first, image + first, length);
m_constantsVersion = version;
++m_constantSets;
++m_constantSpans;
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::CsoBlobBytes, length);
}
return bytes + sizeof(MGPGlobalConstants) + length;
}
}
// THE ONE BLOB RULE: Size 0 means "this record does not declare its blob" - which // THE ONE BLOB RULE: Size 0 means "this record does not declare its blob" - which
// is what a monolith emission is - and the bytes ride beside it as a companion // is what a monolith emission is - and the bytes ride beside it as a companion
// pointer. Offset carries the staging address for diagnostics only; nothing reads // pointer. Offset carries the staging address for diagnostics only; nothing reads
@@ -233,6 +264,7 @@ namespace MobileGL::MG_Pipe {
static_cast<Uint64>(size)); static_cast<Uint64>(size));
m_constantsCso = cso; m_constantsCso = cso;
m_constantsVersion = version; m_constantsVersion = version;
m_constantsShadow.assign(image, image + size);
++m_constantSets; ++m_constantSets;
if (MG_Util::PipeStats::Enabled()) { if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::CsoBlobBytes, size); MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::CsoBlobBytes, size);
@@ -705,6 +737,10 @@ namespace MobileGL::MG_Pipe {
MGPipeHandle m_dispatchCso = kMGPipeNullHandle; MGPipeHandle m_dispatchCso = kMGPipeNullHandle;
MGPipeHandle m_constantsCso = kMGPipeNullHandle; MGPipeHandle m_constantsCso = kMGPipeNullHandle;
Uint32 m_constantsVersion = kMGPipeGlobalConstantsNeverUploaded; Uint32 m_constantsVersion = kMGPipeGlobalConstantsNeverUploaded;
// The image last sent for m_constantsCso (whole or patched by spans): what the server
// holds while the latch names it.
Vector<Uint8> m_constantsShadow;
Uint64 m_constantSpans = 0;
Uint64 m_creates = 0; Uint64 m_creates = 0;
Uint64 m_binds = 0; Uint64 m_binds = 0;
@@ -103,6 +103,13 @@ namespace MobileGL::MG_Pipe {
return true; return true;
} }
// The server now holds the set `contentHash` names without this slot having emitted it
// (P14: bind_vertex_elements re-applies the array's kept set). Same 0 -> 1 remap as
// ShouldEmit, so the next ShouldEmit of the same hash answers false.
void Latch(MGPipeSuppressorSlot slot, Uint64 contentHash) {
m_lastEmitted[static_cast<SizeT>(slot)] = contentHash == 0 ? 1 : contentHash;
}
// A context change or a server reset: what the server has is no longer what this // A context change or a server reset: what the server has is no longer what this
// slot last emitted, so the next resolved set must go out whatever it hashes to. // slot last emitted, so the next resolved set must go out whatever it hashes to.
void Invalidate(MGPipeSuppressorSlot slot) { m_lastEmitted[static_cast<SizeT>(slot)] = 0; } void Invalidate(MGPipeSuppressorSlot slot) { m_lastEmitted[static_cast<SizeT>(slot)] = 0; }
+112 -7
View File
@@ -181,13 +181,26 @@ namespace MobileGL::MG_Pipe {
Uint64 bytes = 0; Uint64 bytes = 0;
const Bool configMoved = !latch.Published || latch.ConfigVersion != configVersion || const Bool configMoved = !latch.Published || latch.ConfigVersion != configVersion ||
latch.Gen != handle.Gen; latch.Gen != handle.Gen;
if (configMoved) bytes += EmitCreate(*vao, handle, latch, configVersion); if (configMoved) {
bytes += EmitCreate(*vao, handle, latch, configVersion);
// The applier drops the record's kept bindings at the same create.
memo.StoredBuffersValid = false;
memo.StoredIndexValid = false;
}
if (lifetimeId != m_boundLifetimeId || m_boundHandle != handle) { if (lifetimeId != m_boundLifetimeId || m_boundHandle != handle) {
MGPipeRouteBindVertexElements(HandleOnly(handle)); MGPipeRouteBindVertexElements(HandleOnly(handle));
++m_binds; ++m_binds;
bytes += sizeof(MGPHandleOnly); bytes += sizeof(MGPHandleOnly);
m_boundHandle = handle; m_boundHandle = handle;
m_boundLifetimeId = lifetimeId; m_boundLifetimeId = lifetimeId;
// P14: the bind re-applies whatever the record keeps (PipeApply.h,
// MGPipeVertexElementsRecord). The mirror below says what that is; where it does
// not know, the next set goes out whatever it hashes to.
auto& suppressor = MGPipeSetHashSuppressorInstance();
if (memo.StoredBuffersValid) suppressor.Latch(MGPipeSuppressorSlot::SetVertexBuffers, memo.StoredBuffersHash);
else suppressor.Invalidate(MGPipeSuppressorSlot::SetVertexBuffers);
m_serverIndexKnown = memo.StoredIndexValid;
m_serverIndexRes = memo.StoredIndexRes;
} }
return bytes; return bytes;
} }
@@ -287,6 +300,14 @@ namespace MobileGL::MG_Pipe {
m_lastBuffers.ContentHash = hash; m_lastBuffers.ContentHash = hash;
MGPipeRouteSetVertexBuffers(m_lastBuffers, m_entries.data()); MGPipeRouteSetVertexBuffers(m_lastBuffers, m_entries.data());
++m_bufferSets; ++m_bufferSets;
// The bound array now keeps this set (the applier's rule, mirrored): a draw-time
// client-array set is the array's too, but it names private buffers, so the mirror
// forgets rather than remembers it.
if (vao && vao->GetLifetimeId() == m_boundLifetimeId) {
VaoMemo& bound = MemoFor(*vao);
bound.StoredBuffersValid = ownedClientBuffers == nullptr && count <= kMGPipeVaoStoredVertexBuffers;
bound.StoredBuffersHash = hash;
}
return sizeof(MGPVertexBuffers) + static_cast<Uint64>(count) * sizeof(MGPVertexBuffer); return sizeof(MGPVertexBuffers) + static_cast<Uint64>(count) * sizeof(MGPVertexBuffer);
} }
@@ -316,11 +337,29 @@ namespace MobileGL::MG_Pipe {
MGPipeResourceTrackerInstance().NoteBoundAs(m_lastIndex.Res, BufferTarget::Index); MGPipeResourceTrackerInstance().NoteBoundAs(m_lastIndex.Res, BufferTarget::Index);
} }
} }
// P14: the server already holds this binding - re-applied by the bind from the
// array's kept one, or sent last time - so there is nothing to send.
if (m_serverIndexKnown && m_serverIndexRes == m_lastIndex.Res) return 0;
MGPipeRouteSetIndexBuffer(m_lastIndex); MGPipeRouteSetIndexBuffer(m_lastIndex);
++m_indexSets; ++m_indexSets;
m_serverIndexKnown = true;
m_serverIndexRes = m_lastIndex.Res;
if (vao && vao->GetLifetimeId() == m_boundLifetimeId) {
VaoMemo& bound = MemoFor(*vao);
bound.StoredIndexValid = true;
bound.StoredIndexRes = m_lastIndex.Res;
}
return sizeof(MGPIndexBuffer); return sizeof(MGPIndexBuffer);
} }
// A set_index_buffer this emitter did not send (OwnedDrawInputs' private element
// buffer): the server's binding and the bound array's kept one are no longer known.
void NoteIndexBufferRoutedElsewhere(GLContext& ctx) {
m_serverIndexKnown = false;
const auto& vao = ctx.GetBoundVertexArray();
if (vao && vao->GetLifetimeId() == m_boundLifetimeId) MemoFor(*vao).StoredIndexValid = false;
}
// ---- what a unit case reads. None of it costs a copy: the emitter builds INTO // ---- what a unit case reads. None of it costs a copy: the emitter builds INTO
// these and hands the applier the same pointers. ---- // these and hands the applier the same pointers. ----
const Array<MGPVertexAttribWire, kMGPipeMaxVertexAttribs>& LastAttributes() const { return m_attributes; } const Array<MGPVertexAttribWire, kMGPipeMaxVertexAttribs>& LastAttributes() const { return m_attributes; }
@@ -394,16 +433,19 @@ namespace MobileGL::MG_Pipe {
} }
m_boundHandle = kMGPipeNullHandle; m_boundHandle = kMGPipeNullHandle;
m_boundLifetimeId = 0; m_boundLifetimeId = 0;
m_serverIndexKnown = false;
} }
void ResetCounters() { m_creates = m_binds = m_bufferSets = m_indexSets = 0; } void ResetCounters() { m_creates = m_binds = m_bufferSets = m_indexSets = 0; }
private: private:
// P14: what EmitVertexElements / EmitVertexBuffers / EmitIndexBuffer resolve from a VAO, // 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 // one entry per live VAO keyed on its lifetime id (monotonic, never reused), erased at the
// allocator reset. A colliding VAO simply takes the entry over. It changes no record: the // VAO's death and re-initialised after an allocator reset. A direct-mapped table was tried
// same handles and entries are emitted, only found without the per-draw walk and lookups. // first and lost most entries to collisions: a Minecraft frame draws ~1,500 chunk arrays
static constexpr SizeT kVaoMemoEntries = 1024; // whose ids spread well past any fixed table. The handles and entries it keeps change no
// record; the kept-binding mirror below does decide what is sent, and is only ever
// believed when the server provably holds the same.
static constexpr SizeT kVaoMemoBuffers = 8; static constexpr SizeT kVaoMemoBuffers = 8;
struct VaoMemo { struct VaoMemo {
Uint64 LifetimeId = 0; Uint64 LifetimeId = 0;
@@ -418,11 +460,29 @@ namespace MobileGL::MG_Pipe {
Uint32 HashBaseInstance = 0; Uint32 HashBaseInstance = 0;
Uint64 Hash = 0; Uint64 Hash = 0;
Array<MGPVertexBuffer, kVaoMemoBuffers> Buffers{}; Array<MGPVertexBuffer, kVaoMemoBuffers> Buffers{};
// The mirror of what the applier's record for this array keeps (PipeApply.h's
// MGPipeVertexElementsRecord): valid only while the server provably keeps exactly
// this. A memo entry taken over by another array starts invalid, which only costs a
// re-send.
Bool StoredBuffersValid = false;
Uint64 StoredBuffersHash = 0;
Bool StoredIndexValid = false;
MGPipeHandle StoredIndexRes = kMGPipeNullHandle;
}; };
// What the server's index binding is, when known (P14).
Bool m_serverIndexKnown = false;
MGPipeHandle m_serverIndexRes = kMGPipeNullHandle;
// The returned reference stays valid until the next MemoFor of a DIFFERENT array (an
// insert may grow the table) or ForgetVao.
VaoMemo& MemoFor(const VertexArrayObject& vao) { VaoMemo& MemoFor(const VertexArrayObject& vao) {
const Uint64 lifetimeId = vao.GetLifetimeId(); const Uint64 lifetimeId = vao.GetLifetimeId();
const Uint64 epoch = MGPipeSlots().ResetEpoch(); const Uint64 epoch = MGPipeSlots().ResetEpoch();
VaoMemo& memo = m_vaoMemo[lifetimeId % kVaoMemoEntries]; if (m_vaoMemo.empty() || (m_vaoMemoUsed + 1) * 2 > m_vaoMemo.size()) GrowVaoMemo();
const SizeT mask = m_vaoMemo.size() - 1;
SizeT i = VaoMemoHome(lifetimeId, mask);
while (m_vaoMemo[i].LifetimeId != 0 && m_vaoMemo[i].LifetimeId != lifetimeId) i = (i + 1) & mask;
VaoMemo& memo = m_vaoMemo[i];
if (memo.LifetimeId == 0) ++m_vaoMemoUsed;
if (memo.LifetimeId != lifetimeId || memo.SlotEpoch != epoch) { if (memo.LifetimeId != lifetimeId || memo.SlotEpoch != epoch) {
memo = VaoMemo{}; memo = VaoMemo{};
memo.LifetimeId = lifetimeId; memo.LifetimeId = lifetimeId;
@@ -430,7 +490,52 @@ namespace MobileGL::MG_Pipe {
} }
return memo; return memo;
} }
Array<VaoMemo, kVaoMemoEntries> m_vaoMemo{}; static SizeT VaoMemoHome(Uint64 lifetimeId, SizeT mask) {
return static_cast<SizeT>((lifetimeId * 0x9E3779B97F4A7C15ull) >> 32) & mask;
}
void GrowVaoMemo() {
Vector<VaoMemo> old;
old.swap(m_vaoMemo);
m_vaoMemo.resize(old.empty() ? 1024 : old.size() * 2);
m_vaoMemoUsed = 0;
const SizeT mask = m_vaoMemo.size() - 1;
for (const VaoMemo& entry : old) {
if (entry.LifetimeId == 0) continue;
SizeT i = VaoMemoHome(entry.LifetimeId, mask);
while (m_vaoMemo[i].LifetimeId != 0) i = (i + 1) & mask;
m_vaoMemo[i] = entry;
++m_vaoMemoUsed;
}
}
Vector<VaoMemo> m_vaoMemo;
SizeT m_vaoMemoUsed = 0;
public:
// The VAO died (MGPipeEmitVertexElementsDestroyAndFree): its entry goes, by backward-shift
// deletion so every other entry stays reachable from its home.
void ForgetVao(Uint64 lifetimeId) {
if (m_vaoMemo.empty() || lifetimeId == 0) return;
const SizeT mask = m_vaoMemo.size() - 1;
SizeT i = VaoMemoHome(lifetimeId, mask);
while (m_vaoMemo[i].LifetimeId != lifetimeId) {
if (m_vaoMemo[i].LifetimeId == 0) return;
i = (i + 1) & mask;
}
SizeT hole = i;
for (SizeT j = (hole + 1) & mask; m_vaoMemo[j].LifetimeId != 0; j = (j + 1) & mask) {
const SizeT home = VaoMemoHome(m_vaoMemo[j].LifetimeId, mask);
// Move j into the hole when its home is not strictly inside (hole, j].
const Bool between = hole <= j ? (home > hole && home <= j) : (home > hole || home <= j);
if (!between) {
m_vaoMemo[hole] = m_vaoMemo[j];
hole = j;
}
}
m_vaoMemo[hole] = VaoMemo{};
--m_vaoMemoUsed;
}
private:
struct Latch { struct Latch {
// The PER-CONTEXT half: "has this emitter told THIS server about this handle's // The PER-CONTEXT half: "has this emitter told THIS server about this handle's
+7 -1
View File
@@ -1140,10 +1140,16 @@ namespace MobileGL::MG_Pipe {
MGP_ASSERT_POD(MGPProgramBindings, 32); MGP_ASSERT_POD(MGPProgramBindings, 32);
// Covers the DEFAULT UNIFORM BLOCK only (D6). // Covers the DEFAULT UNIFORM BLOCK only (D6).
//
// Range (P14, was Pad0 and the size did not move): 0 means the bytes are the whole block.
// Otherwise it is (offset << 16) | length, and the bytes are only that span of the block,
// written over the image the program's record already holds - which the client sends only
// while it knows the server holds its previous image for the same ShaderCso (the latch that
// suppresses an unchanged block). A Minecraft chunk draw moves one vec3 of a ~230-byte block.
struct MGPGlobalConstants { struct MGPGlobalConstants {
MGPipeHandle ShaderCso; MGPipeHandle ShaderCso;
Uint32 Version; Uint32 Version;
Uint32 Pad0; Uint32 Range;
MGPBlobRef Blob; MGPBlobRef Blob;
}; };
MGP_ASSERT_POD(MGPGlobalConstants, 40); MGP_ASSERT_POD(MGPGlobalConstants, 40);
+41 -2
View File
@@ -2951,6 +2951,10 @@ namespace MobileGL::MG_Pipe {
record.AttributeCount = desc.AttributeCount; record.AttributeCount = desc.AttributeCount;
record.BindingPointCount = desc.BindingPointCount; record.BindingPointCount = desc.BindingPointCount;
record.Live = true; record.Live = true;
// A new configuration starts without kept bindings (see the record): the client drops
// its mirror of them at the same create.
record.StoredBuffersValid = false;
record.StoredIndexValid = false;
// Serial 0 means "never created", so the first create of an identity lands on 1 and a // Serial 0 means "never created", so the first create of an identity lands on 1 and a
// backend twin that has synced nothing can never accidentally match a live record. // backend twin that has synced nothing can never accidentally match a live record.
++record.ContentSerial; ++record.ContentSerial;
@@ -2973,6 +2977,14 @@ namespace MobileGL::MG_Pipe {
const MGPipeVertexElementsRecord* record = ResolveVertexElements("bind_vertex_elements", handle.Handle); const MGPipeVertexElementsRecord* record = ResolveVertexElements("bind_vertex_elements", handle.Handle);
if (record == nullptr) return; if (record == nullptr) return;
MGPipeApplier().BoundVertexElements = handle.Handle; MGPipeApplier().BoundVertexElements = handle.Handle;
// The array's kept bindings come back with it, through the same two applies their
// records take, so every serial and field moves exactly as it would for the records.
// (The apply writes the same entries back over themselves: harmless, and no copy.)
if (record->StoredBuffersValid) {
const MGPVertexBuffers header = record->StoredBuffersHeader;
MGPipeApplySetVertexBuffers(header, record->StoredBuffers.data());
}
if (record->StoredIndexValid) MGPipeApplySetIndexBuffer(record->StoredIndex);
} }
void MGPipeApplyDeleteVertexElements(const MGPHandleOnly& handle) { void MGPipeApplyDeleteVertexElements(const MGPHandleOnly& handle) {
@@ -3043,6 +3055,15 @@ namespace MobileGL::MG_Pipe {
// would keep the previous shift. // would keep the previous shift.
MGPipeApplier().VertexFetchBaseInstance = hdr.BaseInstance; MGPipeApplier().VertexFetchBaseInstance = hdr.BaseInstance;
++MGPipeApplier().VertexBuffersSerial; ++MGPipeApplier().VertexBuffersSerial;
// Kept by the bound array (MGPipeVertexElementsRecord's P14 note). A re-apply from the
// bind writes back what it read, which is harmless.
if (MGPipeVertexElementsRecord* bound = FindVertexElements(MGPipeApplier().BoundVertexElements)) {
bound->StoredBuffersValid = hdr.Start == 0 && hdr.Count <= kMGPipeVaoStoredVertexBuffers;
if (bound->StoredBuffersValid) {
bound->StoredBuffersHeader = hdr;
for (Uint32 i = 0; i < hdr.Count; ++i) bound->StoredBuffers[i] = tail[i];
}
}
} }
void MGPipeApplySetIndexBuffer(const MGPIndexBuffer& record) { void MGPipeApplySetIndexBuffer(const MGPIndexBuffer& record) {
@@ -3058,6 +3079,11 @@ namespace MobileGL::MG_Pipe {
// configuration's, which is exactly what the backend's two separate compares need. // configuration's, which is exactly what the backend's two separate compares need.
MGPipeApplier().IndexBuffer = record; MGPipeApplier().IndexBuffer = record;
++MGPipeApplier().IndexBufferSerial; ++MGPipeApplier().IndexBufferSerial;
// Kept by the bound array (MGPipeVertexElementsRecord's P14 note).
if (MGPipeVertexElementsRecord* bound = FindVertexElements(MGPipeApplier().BoundVertexElements)) {
bound->StoredIndex = record;
bound->StoredIndexValid = true;
}
} }
void MGPipeDeriveRenderStateFields(PipeInputs& inputs) { void MGPipeDeriveRenderStateFields(PipeInputs& inputs) {
@@ -3949,10 +3975,21 @@ namespace MobileGL::MG_Pipe {
// bounded there - so this call can only ever allocate what the create already declared, // bounded there - so this call can only ever allocate what the create already declared,
// and the blob rule has a real field to cross-check against for once. // and the blob rule has a real field to cross-check against for once.
const Uint32 size = stored->Desc.GlobalUboSize; const Uint32 size = stored->Desc.GlobalUboSize;
// A span (MGPGlobalConstants::Range) lands on the block the record already holds.
const Uint32 spanOffset = record.Range >> 16;
const Uint32 spanLength = record.Range & 0xffffu;
const char* fault = nullptr; const char* fault = nullptr;
if (record.Version == ~Uint32{0}) { if (record.Version == ~Uint32{0}) {
fault = "the version is the backends' never-uploaded sentinel, which no record may carry"; fault = "the version is the backends' never-uploaded sentinel, which no record may carry";
} else if (record.Blob.Size != 0 && record.Blob.Size != size) { } else if (record.Range != 0 && (spanLength == 0 || spanOffset + spanLength > size)) {
fault = "the span runs outside the program's default uniform block";
} else if (record.Range != 0 && stored->GlobalConstants.size() != size) {
fault = "a span arrived for a block the record does not hold";
} else if (record.Range != 0 && record.Blob.Size != 0 && record.Blob.Size != spanLength) {
fault = "the declared blob length is not the span's length";
} else if (record.Range != 0 && bytes == nullptr) {
fault = "a span carries no bytes";
} else if (record.Range == 0 && record.Blob.Size != 0 && record.Blob.Size != size) {
fault = "the declared blob length is not the program's own default uniform block size"; fault = "the declared blob length is not the program's own default uniform block size";
} else if (size > kMGPipeMaxGlobalConstantsBytes) { } else if (size > kMGPipeMaxGlobalConstantsBytes) {
fault = "the program's default uniform block is larger than any program may declare"; fault = "the program's default uniform block is larger than any program may declare";
@@ -3968,7 +4005,9 @@ namespace MobileGL::MG_Pipe {
return; return;
} }
if (size == 0) { if (record.Range != 0) {
std::memcpy(stored->GlobalConstants.data() + spanOffset, bytes, spanLength);
} else if (size == 0) {
stored->GlobalConstants.clear(); stored->GlobalConstants.clear();
} else { } else {
const Uint8* image = static_cast<const Uint8*>(bytes); const Uint8* image = static_cast<const Uint8*>(bytes);
+24
View File
@@ -500,6 +500,11 @@ namespace MobileGL::MG_Pipe {
// The 32 is GL's MAX_VERTEX_ATTRIBS as MobileGL advertises it (kMGPipeMaxVertexAttribs, // The 32 is GL's MAX_VERTEX_ATTRIBS as MobileGL advertises it (kMGPipeMaxVertexAttribs,
// MGPipeTypes.h), which is also the bound the record's two declared counts are checked // MGPipeTypes.h), which is also the bound the record's two declared counts are checked
// against before the blob is unpacked. // against before the blob is unpacked.
// The widest vertex-buffer set a vertex-elements record keeps as its own (see below). A
// wider set is not kept, and the client, which mirrors this rule, then re-sends it after
// every bind of that array.
inline constexpr Uint32 kMGPipeVaoStoredVertexBuffers = 16;
struct MGPipeVertexElementsRecord { struct MGPipeVertexElementsRecord {
Uint32 Gen = 0; Uint32 Gen = 0;
Bool Live = false; Bool Live = false;
@@ -511,6 +516,25 @@ namespace MobileGL::MG_Pipe {
// including a RE-create on the same handle, which is how a configuration change // including a RE-create on the same handle, which is how a configuration change
// travels (the handle is minted per frontend VAO and Gen moves only on slot reuse). // travels (the handle is minted per frontend VAO and Gen moves only on slot reuse).
Uint64 ContentSerial = 0; Uint64 ContentSerial = 0;
// P14: THE ARRAY'S OWN BUFFER AND ELEMENT BINDINGS, as GL keeps them on the vertex array
// object. The last set_vertex_buffers (window starting at 0, at most
// kMGPipeVaoStoredVertexBuffers entries) and the last set_index_buffer applied while this
// record was bound are kept here, and bind_vertex_elements re-applies them, exactly as if
// the two records had arrived again - so a client switching between arrays whose
// bindings did not move sends the bind alone. create_vertex_elements on the handle drops
// both (the client drops its mirror at the same create), so a kept set never outlives the
// configuration it was sent for. The client's mirror (VertexInputEmit.h) only ever
// believes a kept set the server also keeps; where it is unsure it re-sends.
Bool StoredBuffersValid = false;
Bool StoredIndexValid = false;
MGPVertexBuffers StoredBuffersHeader{};
Array<MGPVertexBuffer, kMGPipeVaoStoredVertexBuffers> StoredBuffers{};
MGPIndexBuffer StoredIndex{};
// P14: a backend's own derivation from this record (Magma's resolved vertex input
// layout), opaque here. It lives and dies with the record - a re-created or recycled
// record starts without it - and the backend keys it on ContentSerial plus the inputs it
// read, so it is never believed for a configuration it was not built from.
mutable SharedPtr<void> BackendMemo;
}; };
// set_framebuffer_state's record, HELD PER FRAMEBUFFER OBJECT and indexed by the handle's // set_framebuffer_state's record, HELD PER FRAMEBUFFER OBJECT and indexed by the handle's
+1 -1
View File
@@ -159,7 +159,7 @@
F(Name) F(Binding) F(Name) F(Binding)
#define MGP_FIELDS_MGPGlobalConstants(F) \ #define MGP_FIELDS_MGPGlobalConstants(F) \
F(ShaderCso) F(Version) F(Blob) F(ShaderCso) F(Version) F(Range) F(Blob)
#define MGP_FIELDS_MGPAttribValue(F) \ #define MGP_FIELDS_MGPAttribValue(F) \
F(Location) F(ValueClass) F(FloatView) F(IntView) F(UintView) F(Location) F(ValueClass) F(FloatView) F(IntView) F(UintView)
+3 -1
View File
@@ -35,7 +35,9 @@ namespace MobileGL::MG_Pipe {
kMGPipeWireLayoutMembers, sizeof(kMGPipeWireLayoutMembers) / sizeof(WireLayoutMember)); kMGPipeWireLayoutMembers, sizeof(kMGPipeWireLayoutMembers) / sizeof(WireLayoutMember));
// Bump when a fixed-layout pipe carrier keeps its bytes but changes their meaning. The // Bump when a fixed-layout pipe carrier keeps its bytes but changes their meaning. The
// mutable-level extent carrier reuses BufOffset/BufSize only on named image respecifies. // mutable-level extent carrier reuses BufOffset/BufSize only on named image respecifies.
inline constexpr Uint32 kMGPipeResourceRespecifyExtentCarrierRevision = 1; // 2 (P14): bind_vertex_elements re-applies the array's kept vertex-buffer and index
// bindings (PipeApply.h, MGPipeVertexElementsRecord), which a revision-1 peer would not.
inline constexpr Uint32 kMGPipeResourceRespecifyExtentCarrierRevision = 2;
constexpr Uint64 WireCatalogueDigest() { constexpr Uint64 WireCatalogueDigest() {
Uint64 hash = 1469598103934665603ull; Uint64 hash = 1469598103934665603ull;
@@ -221,6 +221,7 @@ inline constexpr WireLayoutMember kMGPipeWireLayoutMembers[] = {
{"MGPGlobalConstants", 0, sizeof(MGPGlobalConstants)}, {"MGPGlobalConstants", 0, sizeof(MGPGlobalConstants)},
{"MGPGlobalConstants.ShaderCso", offsetof(MGPGlobalConstants, ShaderCso), sizeof(((MGPGlobalConstants*)nullptr)->ShaderCso)}, {"MGPGlobalConstants.ShaderCso", offsetof(MGPGlobalConstants, ShaderCso), sizeof(((MGPGlobalConstants*)nullptr)->ShaderCso)},
{"MGPGlobalConstants.Version", offsetof(MGPGlobalConstants, Version), sizeof(((MGPGlobalConstants*)nullptr)->Version)}, {"MGPGlobalConstants.Version", offsetof(MGPGlobalConstants, Version), sizeof(((MGPGlobalConstants*)nullptr)->Version)},
{"MGPGlobalConstants.Range", offsetof(MGPGlobalConstants, Range), sizeof(((MGPGlobalConstants*)nullptr)->Range)},
{"MGPGlobalConstants.Blob", offsetof(MGPGlobalConstants, Blob), sizeof(((MGPGlobalConstants*)nullptr)->Blob)}, {"MGPGlobalConstants.Blob", offsetof(MGPGlobalConstants, Blob), sizeof(((MGPGlobalConstants*)nullptr)->Blob)},
{"MGPAttribValue", 0, sizeof(MGPAttribValue)}, {"MGPAttribValue", 0, sizeof(MGPAttribValue)},
{"MGPAttribValue.Location", offsetof(MGPAttribValue, Location), sizeof(((MGPAttribValue*)nullptr)->Location)}, {"MGPAttribValue.Location", offsetof(MGPAttribValue, Location), sizeof(((MGPAttribValue*)nullptr)->Location)},
+12 -4
View File
@@ -673,10 +673,18 @@ namespace MobileGL::MG_Remote::Transport {
auto& x = *m_impl; auto& x = *m_impl;
if (!x.Client() || span.Offset > x.memory->StageBytes() || span.Size > x.memory->StageBytes() - span.Offset) if (!x.Client() || span.Offset > x.memory->StageBytes() || span.Size > x.memory->StageBytes() - span.Offset)
Corrupt("staged span outside window"); Corrupt("staged span outside window");
if (!x.stage.empty() && x.stage.back().Offset + x.stage.back().Size == span.Offset) // Adjacent once the producer's 8-byte allocation granule is counted (StageAllocate rounds
x.stage.back().Size += span.Size; // every blob up): one span then carries both, padding included, instead of one Stage
else // message per small blob - a Minecraft chunk draw stages a 12-byte uniform span.
x.stage.push_back(span); if (!x.stage.empty()) {
auto& back = x.stage.back();
const std::uint64_t end = back.Offset + back.Size;
if (span.Offset >= end && span.Offset - end < 8) {
back.Size = span.Offset + span.Size - back.Offset;
return;
}
}
x.stage.push_back(span);
} }
MobileGLResult StreamLink::Flush() { MobileGLResult StreamLink::Flush() {
auto& x = *m_impl; auto& x = *m_impl;
@@ -16,6 +16,7 @@ namespace MobileGL::MG_State::GLState {
const auto target = static_cast<SizeT>(GlobalBufferTargets[i]); const auto target = static_cast<SizeT>(GlobalBufferTargets[i]);
if (target < m_bindingSlotIndex.size()) m_bindingSlotIndex[target] = static_cast<Int16>(i); if (target < m_bindingSlotIndex.size()) m_bindingSlotIndex[target] = static_cast<Int16>(i);
} }
++m_contextValuesGeneration;
for (SizeT i = 0; i < m_touchedBindPointCount.size(); ++i) { for (SizeT i = 0; i < m_touchedBindPointCount.size(); ++i) {
m_touchedBindPointCount[i] = 0; m_touchedBindPointCount[i] = 0;
} }
@@ -51,9 +51,14 @@ namespace MobileGL::MG_State::GLState {
auto it = std::find(BufferBindPointTargets.begin(), BufferBindPointTargets.end(), target); auto it = std::find(BufferBindPointTargets.begin(), BufferBindPointTargets.end(), target);
if (it == BufferBindPointTargets.end()) return; if (it == BufferBindPointTargets.end()) return;
auto slot = std::distance(BufferBindPointTargets.begin(), it); auto slot = std::distance(BufferBindPointTargets.begin(), it);
if (static_cast<SizeT>(index) + 1 > m_touchedBindPointCount[slot]) if (static_cast<SizeT>(index) + 1 > m_touchedBindPointCount[slot]) {
m_touchedBindPointCount[slot] = static_cast<SizeT>(index) + 1; m_touchedBindPointCount[slot] = static_cast<SizeT>(index) + 1;
++m_contextValuesGeneration;
}
} }
// Moves whenever a touched binding-point count does: the buffer half of
// GLContext::GetContextValuesGeneration.
Uint64 GetContextValuesGeneration() const { return m_contextValuesGeneration; }
SizeT GetTouchedBindPointCount(const BufferTarget target) const { SizeT GetTouchedBindPointCount(const BufferTarget target) const {
auto it = std::find(BufferBindPointTargets.begin(), BufferBindPointTargets.end(), target); auto it = std::find(BufferBindPointTargets.begin(), BufferBindPointTargets.end(), target);
if (it == BufferBindPointTargets.end()) return 0; if (it == BufferBindPointTargets.end()) return 0;
@@ -113,5 +118,6 @@ namespace MobileGL::MG_State::GLState {
Array<Array<BindingSlotRange1D<BufferObject>, BufferBindingPointCount>, BufferBindPointTargets.size()> Array<Array<BindingSlotRange1D<BufferObject>, BufferBindingPointCount>, BufferBindPointTargets.size()>
m_bufferBindPointTargets; m_bufferBindPointTargets;
Array<SizeT, BufferBindPointTargets.size()> m_touchedBindPointCount; Array<SizeT, BufferBindPointTargets.size()> m_touchedBindPointCount;
Uint64 m_contextValuesGeneration = 0;
}; };
} // namespace MobileGL::MG_State::GLState } // namespace MobileGL::MG_State::GLState
+1
View File
@@ -1436,6 +1436,7 @@ namespace MobileGL::MG_State {
point.ClearRange(); point.ClearRange();
} }
} }
++m_transformFeedbackValuesGeneration;
m_transformFeedbackActive = object.active; m_transformFeedbackActive = object.active;
m_transformFeedbackPaused = object.paused; m_transformFeedbackPaused = object.paused;
m_transformFeedbackPrimitiveMode = object.primitiveMode; m_transformFeedbackPrimitiveMode = object.primitiveMode;
+16 -1
View File
@@ -437,6 +437,7 @@ namespace MobileGL {
// feedback object currently bound to GL_TRANSFORM_FEEDBACK; see the object // feedback object currently bound to GL_TRANSFORM_FEEDBACK; see the object
// block further down for how a bind swaps them. // block further down for how a bind swaps them.
void BeginTransformFeedback(GLenum primitiveMode, const SharedPtr<ProgramObject>& program) { void BeginTransformFeedback(GLenum primitiveMode, const SharedPtr<ProgramObject>& program) {
++m_transformFeedbackValuesGeneration;
m_transformFeedbackActive = true; m_transformFeedbackActive = true;
m_transformFeedbackPaused = false; m_transformFeedbackPaused = false;
m_transformFeedbackPrimitiveMode = primitiveMode; m_transformFeedbackPrimitiveMode = primitiveMode;
@@ -446,6 +447,7 @@ namespace MobileGL {
m_transformFeedbackInputPrimitives = 0; m_transformFeedbackInputPrimitives = 0;
} }
void EndTransformFeedback() { void EndTransformFeedback() {
++m_transformFeedbackValuesGeneration;
m_transformFeedbackActive = false; m_transformFeedbackActive = false;
m_transformFeedbackPaused = false; m_transformFeedbackPaused = false;
m_transformFeedbackProgram.reset(); m_transformFeedbackProgram.reset();
@@ -456,7 +458,10 @@ namespace MobileGL {
} }
Bool IsTransformFeedbackActive() const { return m_transformFeedbackActive; } Bool IsTransformFeedbackActive() const { return m_transformFeedbackActive; }
Bool IsTransformFeedbackPaused() const { return m_transformFeedbackPaused; } Bool IsTransformFeedbackPaused() const { return m_transformFeedbackPaused; }
void SetTransformFeedbackPaused(Bool paused) { m_transformFeedbackPaused = paused; } void SetTransformFeedbackPaused(Bool paused) {
++m_transformFeedbackValuesGeneration;
m_transformFeedbackPaused = paused;
}
GLenum GetTransformFeedbackPrimitiveMode() const { return m_transformFeedbackPrimitiveMode; } GLenum GetTransformFeedbackPrimitiveMode() const { return m_transformFeedbackPrimitiveMode; }
const SharedPtr<ProgramObject>& GetTransformFeedbackProgram() const { const SharedPtr<ProgramObject>& GetTransformFeedbackProgram() const {
return m_transformFeedbackProgram; return m_transformFeedbackProgram;
@@ -484,8 +489,17 @@ namespace MobileGL {
// Vertices already captured since BeginTransformFeedback (drives the // Vertices already captured since BeginTransformFeedback (drives the
// buffer-capacity clamp on the primitives-written accounting). // buffer-capacity clamp on the primitives-written accounting).
void AddTransformFeedbackCapturedVertices(Uint64 vertices) { void AddTransformFeedbackCapturedVertices(Uint64 vertices) {
++m_transformFeedbackValuesGeneration;
m_transformFeedbackCapturedVertices += vertices; m_transformFeedbackCapturedVertices += vertices;
} }
// P14: moves whenever any input of set_context_values does (the active texture
// unit, the two touched high-water marks, the transform feedback state of the bound
// object). Each half only ever counts up, so the sum moves exactly when one does;
// the emitter skips rebuilding the record while it holds.
Uint64 GetContextValuesGeneration() const {
return m_textureState.GetContextValuesGeneration() + m_bufferState.GetContextValuesGeneration() +
m_transformFeedbackValuesGeneration;
}
Uint64 GetTransformFeedbackCapturedVertices() const { return m_transformFeedbackCapturedVertices; } Uint64 GetTransformFeedbackCapturedVertices() const { return m_transformFeedbackCapturedVertices; }
// Raw assembled input primitives fed to the capture stage since Begin // Raw assembled input primitives fed to the capture stage since Begin
// (pre-clamp; drives the GS strip capture-order fixup at EndTF). // (pre-clamp; drives the GS strip capture-order fixup at EndTF).
@@ -655,6 +669,7 @@ namespace MobileGL {
Array<CurrentVertexAttributeValue, VertexArrayObject::MAX_VERTEX_ATTRIBS> m_currentVertexAttributes{}; Array<CurrentVertexAttributeValue, VertexArrayObject::MAX_VERTEX_ATTRIBS> m_currentVertexAttributes{};
Bool m_transformFeedbackActive = false; Bool m_transformFeedbackActive = false;
Bool m_transformFeedbackPaused = false; Bool m_transformFeedbackPaused = false;
Uint64 m_transformFeedbackValuesGeneration = 0;
GLenum m_transformFeedbackPrimitiveMode = GL_POINTS; GLenum m_transformFeedbackPrimitiveMode = GL_POINTS;
SharedPtr<ProgramObject> m_transformFeedbackProgram; SharedPtr<ProgramObject> m_transformFeedbackProgram;
Uint64 m_anyVertexAttribDefaultGeneration = 0; Uint64 m_anyVertexAttribDefaultGeneration = 0;
@@ -98,6 +98,7 @@ namespace MobileGL::MG_State::GLState {
} }
void TextureState::SetActiveTextureUnit(Int unit) { void TextureState::SetActiveTextureUnit(Int unit) {
if (unit != m_activeTextureUnit) ++m_contextValuesGeneration;
m_activeTextureUnit = unit; m_activeTextureUnit = unit;
} }
} // namespace MobileGL::MG_State::GLState } // namespace MobileGL::MG_State::GLState
@@ -84,6 +84,7 @@ namespace MobileGL::MG_State::GLState {
void NoteUnitTouched(Int unit, Bool bindingChanged = true) { void NoteUnitTouched(Int unit, Bool bindingChanged = true) {
if (unit > m_maxTouchedUnit && unit < MAX_TEXTURE_IMAGE_UNITS) { if (unit > m_maxTouchedUnit && unit < MAX_TEXTURE_IMAGE_UNITS) {
m_maxTouchedUnit = unit; m_maxTouchedUnit = unit;
++m_contextValuesGeneration;
// Push-on-mutation (MG_Pipe/PipeMutation.h): the high-water mark is a pushed // Push-on-mutation (MG_Pipe/PipeMutation.h): the high-water mark is a pushed
// PipeInputs field, and a bind reached from inside a verb - a backend binding // PipeInputs field, and a bind reached from inside a verb - a backend binding
// its own synthesised fallback texture - would otherwise leave the block // its own synthesised fallback texture - would otherwise leave the block
@@ -105,6 +106,9 @@ namespace MobileGL::MG_State::GLState {
} }
} }
Int GetMaxTouchedUnit() const { return m_maxTouchedUnit; } Int GetMaxTouchedUnit() const { return m_maxTouchedUnit; }
// Moves whenever the active unit or the touched high-water mark does: the texture half of
// GLContext::GetContextValuesGeneration.
Uint64 GetContextValuesGeneration() const { return m_contextValuesGeneration; }
// High-water mark of IMAGE units ever bound by glBindImageTexture, and it is a SECOND // High-water mark of IMAGE units ever bound by glBindImageTexture, and it is a SECOND
// mark rather than a widening of the one above because the two index different // mark rather than a widening of the one above because the two index different
// namespaces: GL 4.6 core 8.22's image units are their own array, and folding them // namespaces: GL 4.6 core 8.22's image units are their own array, and folding them
@@ -198,6 +202,7 @@ namespace MobileGL::MG_State::GLState {
Uint64 m_samplingResolutionGeneration = 0; Uint64 m_samplingResolutionGeneration = 0;
Int m_maxTouchedUnit = -1; Int m_maxTouchedUnit = -1;
Int m_activeTextureUnit = 0; Int m_activeTextureUnit = 0;
Uint64 m_contextValuesGeneration = 0;
Array<TextureUnit, MAX_TEXTURE_IMAGE_UNITS> m_textureUnits; Array<TextureUnit, MAX_TEXTURE_IMAGE_UNITS> m_textureUnits;
Array<ImageTextureBinding, MAX_TEXTURE_IMAGE_UNITS> m_imageTextureBindings; Array<ImageTextureBinding, MAX_TEXTURE_IMAGE_UNITS> m_imageTextureBindings;
// One default texture object (external name 0) per target, created with the context and // One default texture object (external name 0) per target, created with the context and
@@ -852,6 +852,59 @@ namespace {
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundVertexElements)); EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundVertexElements));
} }
// P14: a vertex-elements record keeps the last vertex-buffer set and index binding applied
// while it was bound, and bind_vertex_elements re-applies them through the same two applies
// - every field and both serials move as if the records had arrived again. A re-create of
// the handle drops what it kept (the client drops its mirror at the same create).
TEST(ResourceEmit, AVertexElementsRecordKeepsItsBindingsAndABindReappliesThem) {
ApplierGuard guard;
const MGPipeHandle resA{7, 3};
const MGPipeHandle resB{8, 1};
const MGPipeHandle csoA{2, 1};
const MGPipeHandle csoB{3, 1};
const ElementsBlob elementsA = MakeElements(csoA, 2, 1, true);
const ElementsBlob elementsB = MakeElements(csoB, 2, 1, true);
MGPipeApplyCreateVertexElements(elementsA.Desc, elementsA.Data());
MGPipeApplyCreateVertexElements(elementsB.Desc, elementsB.Data());
const auto setBuffers = [](MGPipeHandle res, Uint32 stride, Uint32 baseInstance) {
MGPVertexBuffers hdr{};
hdr.Count = 1;
hdr.BaseInstance = baseInstance;
MGPVertexBuffer entry{};
entry.Res = res;
entry.Stride = stride;
MGPipeApplySetVertexBuffers(hdr, &entry);
};
MGPipeApplyBindVertexElements(ElementsHandle(csoA));
setBuffers(resA, 12, 0);
MGPipeApplySetIndexBuffer(MGPIndexBuffer{resA, 0, 0, 0});
MGPipeApplyBindVertexElements(ElementsHandle(csoB));
setBuffers(resB, 20, 0);
MGPipeApplySetIndexBuffer(MGPIndexBuffer{resB, 0, 0, 0});
// Back to A with nothing sent but the bind: A's own bindings come back.
const Uint64 vertexSerial = MGPipeApplier().VertexBuffersSerial;
const Uint64 indexSerial = MGPipeApplier().IndexBufferSerial;
MGPipeApplyBindVertexElements(ElementsHandle(csoA));
EXPECT_EQ(MGPipeApplier().VertexBufferCount, 1u);
EXPECT_EQ(MGPipeApplier().VertexBuffers[0].Res, resA);
EXPECT_EQ(MGPipeApplier().VertexBuffers[0].Stride, 12u);
EXPECT_EQ(MGPipeApplier().IndexBuffer.Res, resA);
EXPECT_GT(MGPipeApplier().VertexBuffersSerial, vertexSerial) << "a re-applied set must move its serial";
EXPECT_GT(MGPipeApplier().IndexBufferSerial, indexSerial);
MGPipeApplyBindVertexElements(ElementsHandle(csoB));
EXPECT_EQ(MGPipeApplier().VertexBuffers[0].Res, resB);
EXPECT_EQ(MGPipeApplier().IndexBuffer.Res, resB);
// A re-create drops what A kept: binding it again leaves B's bindings in place.
MGPipeApplyCreateVertexElements(elementsA.Desc, elementsA.Data());
MGPipeApplyBindVertexElements(ElementsHandle(csoA));
EXPECT_EQ(MGPipeApplier().VertexBuffers[0].Res, resB);
EXPECT_EQ(MGPipeApplier().IndexBuffer.Res, resB);
EXPECT_EQ(MGPipeApplier().RefusedVertexInputCalls, 0u);
}
// C1's observable. A call that names a record this applier does not have is a DEFINED // C1's observable. A call that names a record this applier does not have is a DEFINED
// no-op - nothing stored, nothing dispatched, no serial moved - because the teardown order // no-op - nothing stored, nothing dispatched, no serial moved - because the teardown order
// makes exactly one such sequence legal (release the records, then every ~BufferObject // makes exactly one such sequence legal (release the records, then every ~BufferObject
@@ -557,6 +557,60 @@ namespace {
EXPECT_EQ(Emitter().CreateCount(), 3u); EXPECT_EQ(Emitter().CreateCount(), 3u);
} }
// P14: GL keeps a vertex array's buffer and element bindings ON THE ARRAY, and so does the
// applier's record (MGPipeVertexElementsRecord): a switch back to an array whose bindings did
// not move sends the bind alone - no set_vertex_buffers, no set_index_buffer. A binding that
// did move still goes out after the bind.
TEST(VertexInputEmit, ASwitchBetweenArraysWithUnchangedBindingsSendsOnlyTheBind) {
EmitterScope scope;
const SharedPtr<BufferObject> vertices = Ctx().CreateBufferObject(1);
vertices->Respecify(256, nullptr);
const SharedPtr<BufferObject> elements = Ctx().CreateBufferObject(2);
elements->Respecify(64, nullptr);
const SharedPtr<BufferObject> other = Ctx().CreateBufferObject(3);
other->Respecify(256, nullptr);
const auto configure = [&](Uint name, const SharedPtr<BufferObject>& buffer) {
const SharedPtr<VertexArrayObject> vao = MakeVao(name);
vao->SetAttributeFormat(0, 4, DataType::Float32, false, 16, 0, false);
vao->BindAttributeBuffer(0, buffer);
vao->EnableAttribute(0);
vao->GetIndexBufferBindingSlot().Bind(elements);
return vao;
};
const auto validate = [&] {
Uint64 bytes = Emitter().EmitVertexElements(Ctx());
bytes += Emitter().EmitVertexBuffers(Ctx(), 0);
bytes += Emitter().EmitIndexBuffer(Ctx());
return bytes;
};
const SharedPtr<VertexArrayObject> a = configure(1, vertices);
ASSERT_GT(validate(), 0u);
const SharedPtr<VertexArrayObject> b = configure(2, other);
ASSERT_GT(validate(), 0u);
const Uint64 sets = Emitter().VertexBufferSetCount();
const Uint64 indexSets = Emitter().IndexBufferSetCount();
for (int i = 0; i < 4; ++i) {
Ctx().BindVertexArray(1);
EXPECT_EQ(validate(), sizeof(MGPHandleOnly)) << "a switch to an unchanged array sent more than its bind";
Ctx().BindVertexArray(2);
EXPECT_EQ(validate(), sizeof(MGPHandleOnly));
}
EXPECT_EQ(Emitter().VertexBufferSetCount(), sets);
EXPECT_EQ(Emitter().IndexBufferSetCount(), indexSets);
// A moved binding is sent after the bind, and kept from then on.
Ctx().BindVertexArray(1);
a->BindAttributeBuffer(0, other);
EXPECT_GT(validate(), sizeof(MGPHandleOnly));
EXPECT_EQ(Emitter().VertexBufferSetCount(), sets + 1);
Ctx().BindVertexArray(2);
EXPECT_EQ(validate(), sizeof(MGPHandleOnly));
Ctx().BindVertexArray(1);
EXPECT_EQ(validate(), sizeof(MGPHandleOnly));
EXPECT_EQ(Emitter().VertexBufferSetCount(), sets + 1);
}
// ============================ C-1: the death path ============================ // ============================ C-1: the death path ============================
// //
// THE LEAK THIS RULES OUT, and why it is a client case rather than a backend one. Every // THE LEAK THIS RULES OUT, and why it is a client case rather than a backend one. Every