[Fix] (MG_Impl, MG_State, DirectGLES): give the vertex-elements CSO a backend-neutral death path - the client minted every VAO's slot and only Espryt ever freed one, so under Magma each VAO leaked a slot and a 1.3 KB applier record for the life of the process

This commit is contained in:
2026-09-08 06:49:59 -04:00
parent 3e298c9ad1
commit 78ff014547
5 changed files with 161 additions and 12 deletions
+54
View File
@@ -14,6 +14,9 @@
// (CMakeLists.txt appends it to SOURCE_FILES there).
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/BufferState/BufferState.h>
// C-1: the vertex-elements CSO's death path raises the backend notice from here, between the
// applier's delete and the slot free, so that the whole order lives in one place.
#include <MG_State/GLState/StateObjectDeathNotice.h>
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Impl/Pipe/CsoCache.h>
#include <MG_Impl/Pipe/PipeFill.h>
@@ -762,6 +765,57 @@ namespace MobileGL::MG_Pipe {
return published;
}
Bool MGPipeEmitVertexElementsDestroyAndFree(Uint64 lifetimeId) {
// C-1. THE SAME SHAPE AS MGPipeEmitResourceDestroyAndFree ABOVE, and for the same
// reason: whatever mints a handle owns the death of that handle, and the mint for this
// kind is MGPipeVertexInputEmitter::EmitVertexElements - i.e. the client, on every
// backend. Espryt's StateObjectDeathOps notice used to be the only free, so under a
// backend that installs none the slot and the applier's record leaked per VAO, for
// ever. It is now the SECOND, redundant path (Managers.cpp's
// OnFrontendStateObjectDestroyed) and it must stay idempotent, which it is: the
// notice resolves through the same lifetimeId -> slot map this function frees, and
// MGPipeSlotAllocator::Free refuses a slot that is not live at that generation.
// May be the null handle: no slot is minted for a VAO that no draw ever validated with
// and no backend twin table ever looked up. That case still raises the notice below -
// see there.
const MGPipeHandle handle =
MGPipeSlots().FindByLifetimeId(MGPipeKind::VertexElementsCso, lifetimeId);
// ASKED, NOT ASSUMED. A slot is not evidence of a record: DirectGLES mints one from
// BackendSlotTable::GetOrCreate at every VAO sync, whether or not bit 8 asked this
// client to emit a create - the shipping 0x7f A/B control arm is exactly that
// configuration. delete_vertex_elements on a handle the applier has no record for is a
// refusal, and the refusal asserts (PipeApply.cpp's ResolveVertexElements), i.e. it
// stops a verify build.
MGPipeVertexInputEmitter& emitter = MGPipeVertexInputEmitterInstance();
const Bool published = emitter.RecordIsPublished(handle);
if (published) {
MGPHandleOnly only{};
only.Handle = handle;
only.Kind = static_cast<Uint32>(MGPipeKind::VertexElementsCso);
MGPipeApplyDeleteVertexElements(only);
emitter.NoteRecordDestroyed(handle);
}
// THE ORDER IS D-L's, WITH THE BACKEND NOTICE IN THE MIDDLE, and each of the three
// positions is load-bearing:
// * the applier's record is dropped FIRST, while nothing else can have re-handed the
// slot out, so a recycled slot cannot inherit a field;
// * the death notice is raised SECOND, because it resolves the handle through the
// allocator and a backend told after the Free below could no longer find its twin
// - which would move the leak from the client to the driver VAO. It is raised
// UNCONDITIONALLY, exactly as ~VertexArrayObject raised it before C-1: whether a
// slot exists is this client's business, and a consumer that records notices (the
// P2 e2 gate does) must not stop seeing this class announce itself;
// * the slot goes back LAST. Espryt's notice frees it too; that Free and this one
// are the same call on the same handle and the second is a no-op, because Free
// bumps no generation (the bump rides the next handout) and refuses a slot that is
// no longer live at this generation.
MG_State::GLState::NotifyStateObjectDestroyed(MGPipeKind::VertexElementsCso, lifetimeId);
if (!MGPipeHandleIsNull(handle)) MGPipeSlots().Free(MGPipeKind::VertexElementsCso, handle);
return published;
}
void MGPipeSetPoisonOmission(const char* verb, const char* field) {
if (verb == nullptr || field == nullptr) {
g_omission = PoisonOmission{};
+61 -1
View File
@@ -288,12 +288,61 @@ namespace MobileGL::MG_Pipe {
Uint64 VertexBufferSetCount() const { return m_bufferSets; }
Uint64 IndexBufferSetCount() const { return m_indexSets; }
// ---- C-1: "does the applier hold a record for exactly this handle?" ----
//
// The CSO's death path (MGPipeEmitVertexElementsDestroyAndFree) needs that answer and
// MUST NOT GUESS IT FROM THE SLOT. A VertexElementsCso slot can exist with no record
// behind it, because a backend that keys its twins on the handle mints the slot itself
// (DirectGLES' BackendSlotTable::GetOrCreate -> MGPipeSlots().Acquire) whether or not
// bit 8 ever asked this client to emit anything - which is exactly what a
// MOBILEGL_PIPE_PUSH=0x7f lane runs. delete_vertex_elements on such a handle is a
// REFUSED call, and the applier's resolver asserts on a refusal
// (PipeApply.cpp's ResolveVertexElements), i.e. a stop in a verify build.
//
// Kept OUT of Reset(), unlike the create/bind latch beside it, and for the mirror
// image of Reset()'s own reason: "a fresh context is a fresh server" is true of the
// per-context half of this table, and object RECORDS are precisely what
// MGPipeApplierReset does not clear (PipeApply.h's two halves). This half tracks those
// records, so it lives exactly as long as they do.
Bool RecordIsPublished(MGPipeHandle handle) const {
if (MGPipeHandleIsNull(handle)) return false;
const SizeT slot = handle.Slot;
if (slot >= m_latch.size()) return false;
const Latch& latch = m_latch[slot];
return latch.RecordLive && latch.RecordGen == handle.Gen;
}
// The record named by `handle` is gone from the applier. Also drops the bound-handle
// memo when it named it, so the client's idea of BoundVertexElements and the applier's
// (which MGPipeApplyDeleteVertexElements just cleared for the same handle) stay in
// step rather than diverging until the next bind happens to correct it.
void NoteRecordDestroyed(MGPipeHandle handle) {
if (MGPipeHandleIsNull(handle)) return;
const SizeT slot = handle.Slot;
if (slot < m_latch.size() && m_latch[slot].RecordGen == handle.Gen) {
m_latch[slot] = Latch{};
}
if (m_boundHandle == handle) {
m_boundHandle = kMGPipeNullHandle;
m_boundLifetimeId = 0;
}
}
// A fresh context is a fresh server: the applier's records are gone, so every latch
// this emitter holds describes objects the server no longer has. Called from the
// validate point's FreshlyPrimed arm beside MGPipeApplierReset and the suppressor's
// InvalidateAll, for the same reason they are.
//
// The PER-CONTEXT half only - see RecordIsPublished above for why RecordLive/RecordGen
// survive. Re-creating a configuration the applier already holds is a bounded
// over-fire (MGPipeApplyCreateVertexElements starts the record over); forgetting that
// it holds one at all would leak the record and its slot at the object's death.
void Reset() {
m_latch.clear();
for (Latch& latch : m_latch) {
latch.Published = false;
latch.Gen = 0;
latch.ConfigVersion = 0;
}
m_boundHandle = kMGPipeNullHandle;
m_boundLifetimeId = 0;
}
@@ -302,9 +351,16 @@ namespace MobileGL::MG_Pipe {
private:
struct Latch {
// The PER-CONTEXT half: "has this emitter told THIS server about this handle's
// configuration". Cleared by Reset() at every make-current.
Bool Published = false;
Uint32 Gen = 0;
Uint32 ConfigVersion = 0;
// The RECORD half: "does the applier hold a create_vertex_elements record at this
// slot, for this generation". Lives as long as the record does - see
// RecordIsPublished.
Bool RecordLive = false;
Uint32 RecordGen = 0;
};
static MGPHandleOnly HandleOnly(MGPipeHandle handle) {
@@ -345,6 +401,10 @@ namespace MobileGL::MG_Pipe {
latch.Published = true;
latch.Gen = handle.Gen;
latch.ConfigVersion = configVersion;
// THE ONE PRODUCER of the record half: a create that reached the applier is the
// only thing that makes delete_vertex_elements a legal call for this handle.
latch.RecordLive = true;
latch.RecordGen = handle.Gen;
return sizeof(MGPVertexElements) + kAttribBytes + kBindingBytes;
}