From c1497bb83e73522a36690087c4e67573d7b038f7 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 7 Oct 2026 14:58:21 -0400 Subject: [PATCH] [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%). --- MobileGL/MG_Impl/Pipe/OwnedDrawInputs.h | 1 + MobileGL/MG_Impl/Pipe/PipeFill.cpp | 18 ++- MobileGL/MG_Impl/Pipe/ProgramEmit.h | 36 ++++++ MobileGL/MG_Impl/Pipe/SetHashSuppressor.h | 7 ++ MobileGL/MG_Impl/Pipe/VertexInputEmit.h | 119 ++++++++++++++++-- MobileGL/MG_Pipe/MGPipeTypes.h | 8 +- MobileGL/MG_Pipe/PipeApply.cpp | 43 ++++++- MobileGL/MG_Pipe/PipeApply.h | 24 ++++ MobileGL/MG_Pipe/PipeFields.def | 2 +- MobileGL/MG_Pipe/PipeWireLayout.h | 4 +- MobileGL/MG_Pipe/generated/PipeWireLayout.inc | 1 + MobileGL/MG_Remote/Transport/StreamLink.cpp | 16 ++- .../GLState/BufferState/BufferState.cpp | 1 + .../GLState/BufferState/BufferState.h | 8 +- MobileGL/MG_State/GLState/Core.cpp | 1 + MobileGL/MG_State/GLState/Core.h | 17 ++- .../GLState/TextureState/TextureState.cpp | 1 + .../GLState/TextureState/TextureState.h | 5 + MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp | 53 ++++++++ MobileGL/MG_Test/Pipe/VertexInputEmitTest.cpp | 54 ++++++++ 20 files changed, 398 insertions(+), 21 deletions(-) diff --git a/MobileGL/MG_Impl/Pipe/OwnedDrawInputs.h b/MobileGL/MG_Impl/Pipe/OwnedDrawInputs.h index dd6783a6..774bad1c 100644 --- a/MobileGL/MG_Impl/Pipe/OwnedDrawInputs.h +++ b/MobileGL/MG_Impl/Pipe/OwnedDrawInputs.h @@ -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; } diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index 5c90fc24..29310fe7 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -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(ctx.GetActiveTextureUnit()); values.MaxTouchedTextureUnit = static_cast(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; diff --git a/MobileGL/MG_Impl/Pipe/ProgramEmit.h b/MobileGL/MG_Impl/Pipe/ProgramEmit.h index 3b668e7c..72146dfb 100644 --- a/MobileGL/MG_Impl/Pipe/ProgramEmit.h +++ b/MobileGL/MG_Impl/Pipe/ProgramEmit.h @@ -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(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(image + first); + m_lastConstants.Blob.Size = 0; + MGPipeRouteSetGlobalConstants(m_lastConstants, image + first, static_cast(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(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 m_constantsShadow; + Uint64 m_constantSpans = 0; Uint64 m_creates = 0; Uint64 m_binds = 0; diff --git a/MobileGL/MG_Impl/Pipe/SetHashSuppressor.h b/MobileGL/MG_Impl/Pipe/SetHashSuppressor.h index 80232799..ca365caa 100644 --- a/MobileGL/MG_Impl/Pipe/SetHashSuppressor.h +++ b/MobileGL/MG_Impl/Pipe/SetHashSuppressor.h @@ -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(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(slot)] = 0; } diff --git a/MobileGL/MG_Impl/Pipe/VertexInputEmit.h b/MobileGL/MG_Impl/Pipe/VertexInputEmit.h index e01fc656..d7ec0bdf 100644 --- a/MobileGL/MG_Impl/Pipe/VertexInputEmit.h +++ b/MobileGL/MG_Impl/Pipe/VertexInputEmit.h @@ -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(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& 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 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 m_vaoMemo{}; + static SizeT VaoMemoHome(Uint64 lifetimeId, SizeT mask) { + return static_cast((lifetimeId * 0x9E3779B97F4A7C15ull) >> 32) & mask; + } + void GrowVaoMemo() { + Vector 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 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 diff --git a/MobileGL/MG_Pipe/MGPipeTypes.h b/MobileGL/MG_Pipe/MGPipeTypes.h index 1246a071..30876c83 100644 --- a/MobileGL/MG_Pipe/MGPipeTypes.h +++ b/MobileGL/MG_Pipe/MGPipeTypes.h @@ -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); diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index 91d4c833..e8cc5829 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -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(bytes); diff --git a/MobileGL/MG_Pipe/PipeApply.h b/MobileGL/MG_Pipe/PipeApply.h index 06d5b749..88477dd3 100644 --- a/MobileGL/MG_Pipe/PipeApply.h +++ b/MobileGL/MG_Pipe/PipeApply.h @@ -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 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 BackendMemo; }; // set_framebuffer_state's record, HELD PER FRAMEBUFFER OBJECT and indexed by the handle's diff --git a/MobileGL/MG_Pipe/PipeFields.def b/MobileGL/MG_Pipe/PipeFields.def index a86de744..9c79ebe2 100644 --- a/MobileGL/MG_Pipe/PipeFields.def +++ b/MobileGL/MG_Pipe/PipeFields.def @@ -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) diff --git a/MobileGL/MG_Pipe/PipeWireLayout.h b/MobileGL/MG_Pipe/PipeWireLayout.h index 65dbb857..acd9d677 100644 --- a/MobileGL/MG_Pipe/PipeWireLayout.h +++ b/MobileGL/MG_Pipe/PipeWireLayout.h @@ -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; diff --git a/MobileGL/MG_Pipe/generated/PipeWireLayout.inc b/MobileGL/MG_Pipe/generated/PipeWireLayout.inc index 27e4cd9b..97e71585 100644 --- a/MobileGL/MG_Pipe/generated/PipeWireLayout.inc +++ b/MobileGL/MG_Pipe/generated/PipeWireLayout.inc @@ -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)}, diff --git a/MobileGL/MG_Remote/Transport/StreamLink.cpp b/MobileGL/MG_Remote/Transport/StreamLink.cpp index 866d8ab5..1f2f5b8b 100644 --- a/MobileGL/MG_Remote/Transport/StreamLink.cpp +++ b/MobileGL/MG_Remote/Transport/StreamLink.cpp @@ -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; diff --git a/MobileGL/MG_State/GLState/BufferState/BufferState.cpp b/MobileGL/MG_State/GLState/BufferState/BufferState.cpp index 8a39dcab..3ca9b372 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferState.cpp +++ b/MobileGL/MG_State/GLState/BufferState/BufferState.cpp @@ -16,6 +16,7 @@ namespace MobileGL::MG_State::GLState { const auto target = static_cast(GlobalBufferTargets[i]); if (target < m_bindingSlotIndex.size()) m_bindingSlotIndex[target] = static_cast(i); } + ++m_contextValuesGeneration; for (SizeT i = 0; i < m_touchedBindPointCount.size(); ++i) { m_touchedBindPointCount[i] = 0; } diff --git a/MobileGL/MG_State/GLState/BufferState/BufferState.h b/MobileGL/MG_State/GLState/BufferState/BufferState.h index c3450908..ad0171e6 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferState.h +++ b/MobileGL/MG_State/GLState/BufferState/BufferState.h @@ -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(index) + 1 > m_touchedBindPointCount[slot]) + if (static_cast(index) + 1 > m_touchedBindPointCount[slot]) { m_touchedBindPointCount[slot] = static_cast(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, BufferBindingPointCount>, BufferBindPointTargets.size()> m_bufferBindPointTargets; Array m_touchedBindPointCount; + Uint64 m_contextValuesGeneration = 0; }; } // namespace MobileGL::MG_State::GLState diff --git a/MobileGL/MG_State/GLState/Core.cpp b/MobileGL/MG_State/GLState/Core.cpp index 3695706b..f1b7c1e1 100644 --- a/MobileGL/MG_State/GLState/Core.cpp +++ b/MobileGL/MG_State/GLState/Core.cpp @@ -1436,6 +1436,7 @@ namespace MobileGL::MG_State { point.ClearRange(); } } + ++m_transformFeedbackValuesGeneration; m_transformFeedbackActive = object.active; m_transformFeedbackPaused = object.paused; m_transformFeedbackPrimitiveMode = object.primitiveMode; diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index 07f1457c..7d505b5b 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -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& 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& 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 m_currentVertexAttributes{}; Bool m_transformFeedbackActive = false; Bool m_transformFeedbackPaused = false; + Uint64 m_transformFeedbackValuesGeneration = 0; GLenum m_transformFeedbackPrimitiveMode = GL_POINTS; SharedPtr m_transformFeedbackProgram; Uint64 m_anyVertexAttribDefaultGeneration = 0; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureState.cpp b/MobileGL/MG_State/GLState/TextureState/TextureState.cpp index a3eceacc..adb20d14 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureState.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureState.cpp @@ -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 diff --git a/MobileGL/MG_State/GLState/TextureState/TextureState.h b/MobileGL/MG_State/GLState/TextureState/TextureState.h index e19c0f42..b3285b68 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureState.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureState.h @@ -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 m_textureUnits; Array m_imageTextureBindings; // One default texture object (external name 0) per target, created with the context and diff --git a/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp b/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp index ac9f386b..b6ad9d32 100644 --- a/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp @@ -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 diff --git a/MobileGL/MG_Test/Pipe/VertexInputEmitTest.cpp b/MobileGL/MG_Test/Pipe/VertexInputEmitTest.cpp index 232bc0fb..3a4a725e 100644 --- a/MobileGL/MG_Test/Pipe/VertexInputEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/VertexInputEmitTest.cpp @@ -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 vertices = Ctx().CreateBufferObject(1); + vertices->Respecify(256, nullptr); + const SharedPtr elements = Ctx().CreateBufferObject(2); + elements->Respecify(64, nullptr); + const SharedPtr other = Ctx().CreateBufferObject(3); + other->Respecify(256, nullptr); + const auto configure = [&](Uint name, const SharedPtr& buffer) { + const SharedPtr 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 a = configure(1, vertices); + ASSERT_GT(validate(), 0u); + const SharedPtr 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