mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
[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:
@@ -59,6 +59,7 @@ namespace MobileGL::MG_Pipe {
|
||||
binding.Res = info.IndexResource;
|
||||
binding.IndexSize = info.IndexSize;
|
||||
MGPipeRouteSetIndexBuffer(binding);
|
||||
MGPipeVertexInputEmitterInstance().NoteIndexBufferRoutedElsewhere(m_context);
|
||||
m_indexBindingChanged = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1422,6 +1422,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// refusal, and the refusal asserts (PipeApply.cpp's ResolveVertexElements), i.e. it
|
||||
// stops a verify build.
|
||||
MGPipeVertexInputEmitter& emitter = MGPipeVertexInputEmitterInstance();
|
||||
emitter.ForgetVao(lifetimeId);
|
||||
const Bool published = emitter.RecordIsPublished(handle);
|
||||
if (published) {
|
||||
MGPHandleOnly only{};
|
||||
@@ -3152,6 +3153,18 @@ namespace MobileGL::MG_Pipe {
|
||||
// server-role-only fixture) - nothing is emitted and the fields keep being pulled, byte
|
||||
// for byte as before (G1).
|
||||
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{};
|
||||
values.ActiveTextureUnit = static_cast<Uint32>(ctx.GetActiveTextureUnit());
|
||||
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
|
||||
// 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();
|
||||
s_lastContextId = contextId;
|
||||
s_lastGeneration = generation;
|
||||
if (s_lastHash != 0 && std::memcmp(&values, &s_lastValues, sizeof(values)) == 0 &&
|
||||
suppressor.LastEmitted(MGPipeSuppressorSlot::SetContextValues) == s_lastHash)
|
||||
return 0;
|
||||
|
||||
@@ -218,6 +218,37 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastConstants = MGPGlobalConstants{};
|
||||
m_lastConstants.ShaderCso = cso;
|
||||
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
|
||||
// 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
|
||||
@@ -233,6 +264,7 @@ namespace MobileGL::MG_Pipe {
|
||||
static_cast<Uint64>(size));
|
||||
m_constantsCso = cso;
|
||||
m_constantsVersion = version;
|
||||
m_constantsShadow.assign(image, image + size);
|
||||
++m_constantSets;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::CsoBlobBytes, size);
|
||||
@@ -705,6 +737,10 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPipeHandle m_dispatchCso = kMGPipeNullHandle;
|
||||
MGPipeHandle m_constantsCso = kMGPipeNullHandle;
|
||||
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_binds = 0;
|
||||
|
||||
@@ -103,6 +103,13 @@ namespace MobileGL::MG_Pipe {
|
||||
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
|
||||
// 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; }
|
||||
|
||||
@@ -181,13 +181,26 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint64 bytes = 0;
|
||||
const Bool configMoved = !latch.Published || latch.ConfigVersion != configVersion ||
|
||||
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) {
|
||||
MGPipeRouteBindVertexElements(HandleOnly(handle));
|
||||
++m_binds;
|
||||
bytes += sizeof(MGPHandleOnly);
|
||||
m_boundHandle = handle;
|
||||
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;
|
||||
}
|
||||
@@ -287,6 +300,14 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastBuffers.ContentHash = hash;
|
||||
MGPipeRouteSetVertexBuffers(m_lastBuffers, m_entries.data());
|
||||
++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);
|
||||
}
|
||||
|
||||
@@ -316,11 +337,29 @@ namespace MobileGL::MG_Pipe {
|
||||
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);
|
||||
++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);
|
||||
}
|
||||
|
||||
// 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
|
||||
// these and hands the applier the same pointers. ----
|
||||
const Array<MGPVertexAttribWire, kMGPipeMaxVertexAttribs>& LastAttributes() const { return m_attributes; }
|
||||
@@ -394,16 +433,19 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
m_boundHandle = kMGPipeNullHandle;
|
||||
m_boundLifetimeId = 0;
|
||||
m_serverIndexKnown = false;
|
||||
}
|
||||
|
||||
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;
|
||||
// one entry per live VAO keyed on its lifetime id (monotonic, never reused), erased at the
|
||||
// VAO's death and re-initialised after an allocator reset. A direct-mapped table was tried
|
||||
// first and lost most entries to collisions: a Minecraft frame draws ~1,500 chunk arrays
|
||||
// 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;
|
||||
struct VaoMemo {
|
||||
Uint64 LifetimeId = 0;
|
||||
@@ -418,11 +460,29 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint32 HashBaseInstance = 0;
|
||||
Uint64 Hash = 0;
|
||||
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) {
|
||||
const Uint64 lifetimeId = vao.GetLifetimeId();
|
||||
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) {
|
||||
memo = VaoMemo{};
|
||||
memo.LifetimeId = lifetimeId;
|
||||
@@ -430,7 +490,52 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
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 {
|
||||
// The PER-CONTEXT half: "has this emitter told THIS server about this handle's
|
||||
|
||||
@@ -1140,10 +1140,16 @@ namespace MobileGL::MG_Pipe {
|
||||
MGP_ASSERT_POD(MGPProgramBindings, 32);
|
||||
|
||||
// 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 {
|
||||
MGPipeHandle ShaderCso;
|
||||
Uint32 Version;
|
||||
Uint32 Pad0;
|
||||
Uint32 Range;
|
||||
MGPBlobRef Blob;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPGlobalConstants, 40);
|
||||
|
||||
@@ -2951,6 +2951,10 @@ namespace MobileGL::MG_Pipe {
|
||||
record.AttributeCount = desc.AttributeCount;
|
||||
record.BindingPointCount = desc.BindingPointCount;
|
||||
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
|
||||
// backend twin that has synced nothing can never accidentally match a live record.
|
||||
++record.ContentSerial;
|
||||
@@ -2973,6 +2977,14 @@ namespace MobileGL::MG_Pipe {
|
||||
const MGPipeVertexElementsRecord* record = ResolveVertexElements("bind_vertex_elements", handle.Handle);
|
||||
if (record == nullptr) return;
|
||||
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) {
|
||||
@@ -3043,6 +3055,15 @@ namespace MobileGL::MG_Pipe {
|
||||
// would keep the previous shift.
|
||||
MGPipeApplier().VertexFetchBaseInstance = hdr.BaseInstance;
|
||||
++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) {
|
||||
@@ -3058,6 +3079,11 @@ namespace MobileGL::MG_Pipe {
|
||||
// configuration's, which is exactly what the backend's two separate compares need.
|
||||
MGPipeApplier().IndexBuffer = record;
|
||||
++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) {
|
||||
@@ -3949,10 +3975,21 @@ namespace MobileGL::MG_Pipe {
|
||||
// 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.
|
||||
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;
|
||||
if (record.Version == ~Uint32{0}) {
|
||||
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";
|
||||
} else if (size > kMGPipeMaxGlobalConstantsBytes) {
|
||||
fault = "the program's default uniform block is larger than any program may declare";
|
||||
@@ -3968,7 +4005,9 @@ namespace MobileGL::MG_Pipe {
|
||||
return;
|
||||
}
|
||||
|
||||
if (size == 0) {
|
||||
if (record.Range != 0) {
|
||||
std::memcpy(stored->GlobalConstants.data() + spanOffset, bytes, spanLength);
|
||||
} else if (size == 0) {
|
||||
stored->GlobalConstants.clear();
|
||||
} else {
|
||||
const Uint8* image = static_cast<const Uint8*>(bytes);
|
||||
|
||||
@@ -500,6 +500,11 @@ namespace MobileGL::MG_Pipe {
|
||||
// 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
|
||||
// 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 {
|
||||
Uint32 Gen = 0;
|
||||
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
|
||||
// travels (the handle is minted per frontend VAO and Gen moves only on slot reuse).
|
||||
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
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
F(Name) F(Binding)
|
||||
|
||||
#define MGP_FIELDS_MGPGlobalConstants(F) \
|
||||
F(ShaderCso) F(Version) F(Blob)
|
||||
F(ShaderCso) F(Version) F(Range) F(Blob)
|
||||
|
||||
#define MGP_FIELDS_MGPAttribValue(F) \
|
||||
F(Location) F(ValueClass) F(FloatView) F(IntView) F(UintView)
|
||||
|
||||
@@ -35,7 +35,9 @@ namespace MobileGL::MG_Pipe {
|
||||
kMGPipeWireLayoutMembers, sizeof(kMGPipeWireLayoutMembers) / sizeof(WireLayoutMember));
|
||||
// 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.
|
||||
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() {
|
||||
Uint64 hash = 1469598103934665603ull;
|
||||
|
||||
@@ -221,6 +221,7 @@ inline constexpr WireLayoutMember kMGPipeWireLayoutMembers[] = {
|
||||
{"MGPGlobalConstants", 0, sizeof(MGPGlobalConstants)},
|
||||
{"MGPGlobalConstants.ShaderCso", offsetof(MGPGlobalConstants, ShaderCso), sizeof(((MGPGlobalConstants*)nullptr)->ShaderCso)},
|
||||
{"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)},
|
||||
{"MGPAttribValue", 0, sizeof(MGPAttribValue)},
|
||||
{"MGPAttribValue.Location", offsetof(MGPAttribValue, Location), sizeof(((MGPAttribValue*)nullptr)->Location)},
|
||||
|
||||
@@ -673,10 +673,18 @@ namespace MobileGL::MG_Remote::Transport {
|
||||
auto& x = *m_impl;
|
||||
if (!x.Client() || span.Offset > x.memory->StageBytes() || span.Size > x.memory->StageBytes() - span.Offset)
|
||||
Corrupt("staged span outside window");
|
||||
if (!x.stage.empty() && x.stage.back().Offset + x.stage.back().Size == span.Offset)
|
||||
x.stage.back().Size += span.Size;
|
||||
else
|
||||
x.stage.push_back(span);
|
||||
// Adjacent once the producer's 8-byte allocation granule is counted (StageAllocate rounds
|
||||
// every blob up): one span then carries both, padding included, instead of one Stage
|
||||
// message per small blob - a Minecraft chunk draw stages a 12-byte uniform 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() {
|
||||
auto& x = *m_impl;
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
const auto target = static_cast<SizeT>(GlobalBufferTargets[i]);
|
||||
if (target < m_bindingSlotIndex.size()) m_bindingSlotIndex[target] = static_cast<Int16>(i);
|
||||
}
|
||||
++m_contextValuesGeneration;
|
||||
for (SizeT i = 0; i < m_touchedBindPointCount.size(); ++i) {
|
||||
m_touchedBindPointCount[i] = 0;
|
||||
}
|
||||
|
||||
@@ -51,9 +51,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
auto it = std::find(BufferBindPointTargets.begin(), BufferBindPointTargets.end(), target);
|
||||
if (it == BufferBindPointTargets.end()) return;
|
||||
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_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 {
|
||||
auto it = std::find(BufferBindPointTargets.begin(), BufferBindPointTargets.end(), target);
|
||||
if (it == BufferBindPointTargets.end()) return 0;
|
||||
@@ -113,5 +118,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
Array<Array<BindingSlotRange1D<BufferObject>, BufferBindingPointCount>, BufferBindPointTargets.size()>
|
||||
m_bufferBindPointTargets;
|
||||
Array<SizeT, BufferBindPointTargets.size()> m_touchedBindPointCount;
|
||||
Uint64 m_contextValuesGeneration = 0;
|
||||
};
|
||||
} // namespace MobileGL::MG_State::GLState
|
||||
|
||||
@@ -1436,6 +1436,7 @@ namespace MobileGL::MG_State {
|
||||
point.ClearRange();
|
||||
}
|
||||
}
|
||||
++m_transformFeedbackValuesGeneration;
|
||||
m_transformFeedbackActive = object.active;
|
||||
m_transformFeedbackPaused = object.paused;
|
||||
m_transformFeedbackPrimitiveMode = object.primitiveMode;
|
||||
|
||||
@@ -437,6 +437,7 @@ namespace MobileGL {
|
||||
// feedback object currently bound to GL_TRANSFORM_FEEDBACK; see the object
|
||||
// block further down for how a bind swaps them.
|
||||
void BeginTransformFeedback(GLenum primitiveMode, const SharedPtr<ProgramObject>& program) {
|
||||
++m_transformFeedbackValuesGeneration;
|
||||
m_transformFeedbackActive = true;
|
||||
m_transformFeedbackPaused = false;
|
||||
m_transformFeedbackPrimitiveMode = primitiveMode;
|
||||
@@ -446,6 +447,7 @@ namespace MobileGL {
|
||||
m_transformFeedbackInputPrimitives = 0;
|
||||
}
|
||||
void EndTransformFeedback() {
|
||||
++m_transformFeedbackValuesGeneration;
|
||||
m_transformFeedbackActive = false;
|
||||
m_transformFeedbackPaused = false;
|
||||
m_transformFeedbackProgram.reset();
|
||||
@@ -456,7 +458,10 @@ namespace MobileGL {
|
||||
}
|
||||
Bool IsTransformFeedbackActive() const { return m_transformFeedbackActive; }
|
||||
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; }
|
||||
const SharedPtr<ProgramObject>& GetTransformFeedbackProgram() const {
|
||||
return m_transformFeedbackProgram;
|
||||
@@ -484,8 +489,17 @@ namespace MobileGL {
|
||||
// Vertices already captured since BeginTransformFeedback (drives the
|
||||
// buffer-capacity clamp on the primitives-written accounting).
|
||||
void AddTransformFeedbackCapturedVertices(Uint64 vertices) {
|
||||
++m_transformFeedbackValuesGeneration;
|
||||
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; }
|
||||
// Raw assembled input primitives fed to the capture stage since Begin
|
||||
// (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{};
|
||||
Bool m_transformFeedbackActive = false;
|
||||
Bool m_transformFeedbackPaused = false;
|
||||
Uint64 m_transformFeedbackValuesGeneration = 0;
|
||||
GLenum m_transformFeedbackPrimitiveMode = GL_POINTS;
|
||||
SharedPtr<ProgramObject> m_transformFeedbackProgram;
|
||||
Uint64 m_anyVertexAttribDefaultGeneration = 0;
|
||||
|
||||
@@ -98,6 +98,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
void TextureState::SetActiveTextureUnit(Int unit) {
|
||||
if (unit != m_activeTextureUnit) ++m_contextValuesGeneration;
|
||||
m_activeTextureUnit = unit;
|
||||
}
|
||||
} // namespace MobileGL::MG_State::GLState
|
||||
|
||||
@@ -84,6 +84,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
void NoteUnitTouched(Int unit, Bool bindingChanged = true) {
|
||||
if (unit > m_maxTouchedUnit && unit < MAX_TEXTURE_IMAGE_UNITS) {
|
||||
m_maxTouchedUnit = unit;
|
||||
++m_contextValuesGeneration;
|
||||
// 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
|
||||
// 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; }
|
||||
// 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
|
||||
// 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
|
||||
@@ -198,6 +202,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
Uint64 m_samplingResolutionGeneration = 0;
|
||||
Int m_maxTouchedUnit = -1;
|
||||
Int m_activeTextureUnit = 0;
|
||||
Uint64 m_contextValuesGeneration = 0;
|
||||
Array<TextureUnit, MAX_TEXTURE_IMAGE_UNITS> m_textureUnits;
|
||||
Array<ImageTextureBinding, MAX_TEXTURE_IMAGE_UNITS> m_imageTextureBindings;
|
||||
// One default texture object (external name 0) per target, created with the context and
|
||||
|
||||
@@ -852,6 +852,59 @@ namespace {
|
||||
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
|
||||
// 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
|
||||
|
||||
@@ -557,6 +557,60 @@ namespace {
|
||||
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 ============================
|
||||
//
|
||||
// THE LEAK THIS RULES OUT, and why it is a client case rather than a backend one. Every
|
||||
|
||||
Reference in New Issue
Block a user