mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 04:38:30 +09:00
[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:
@@ -215,6 +215,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
PrgramImpl::g_backendProgramObjects.DestroyByLifetimeId(lifetimeId);
|
||||
break;
|
||||
case MG_Pipe::MGPipeKind::VertexElementsCso:
|
||||
// P3a C-1: this is now the SECOND path, not the only one. The client speaks the
|
||||
// whole death itself (MGPipeEmitVertexElementsDestroyAndFree: delete the
|
||||
// applier record, raise this notice, free the slot), because the slot is minted
|
||||
// client-side on every backend and a backend that installs no death ops - which
|
||||
// Magma deliberately does not - otherwise leaked the slot and the record per
|
||||
// VAO for the life of the process. What is left here is the one thing only this
|
||||
// side can do: drop the driver VAO the twin owns. It is raised while the handle
|
||||
// still resolves, so OnFrontendObjectDestroyed's shared free (which the other
|
||||
// five kinds still depend on) is simply the one that gets there first; the
|
||||
// client's own Free right after it is then a no-op, because Free refuses a slot
|
||||
// that is no longer live at that generation and the Gen bump rides the next
|
||||
// handout rather than the free. Double release, no corruption, no abort.
|
||||
VertexArrayImpl::g_backendVertexArrayObjects.DestroyByLifetimeId(lifetimeId);
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,22 @@ namespace MobileGL::MG_Pipe {
|
||||
// re-handed-out, or a backend object nobody releases.
|
||||
Bool MGPipeEmitResourceDestroyAndFree(MG_State::GLState::BufferObject& buffer);
|
||||
|
||||
// THE VERTEX-ELEMENTS CSO's DEATH, and it is BACKEND-NEUTRAL - which is the whole point.
|
||||
// Before this, the only thing that ever returned a VertexElementsCso slot was DirectGLES'
|
||||
// StateObjectDeathOps table; under any backend that installs none - DirectVulkan/Magma,
|
||||
// which keeps its own age-reclaimed identity table on purpose - every VAO ever created
|
||||
// held its slot and its ~1.3 KB applier record for the life of the process, on the shipped
|
||||
// 0x1ff mask, and past 65536 slots every create_vertex_elements became a permanent
|
||||
// Fatal{ProtocolCorruption}. The client mints the slot, so the client is where the death
|
||||
// has to be spoken from.
|
||||
//
|
||||
// Takes the lifetime id and not the object for StateObjectDeathNotice.h's reason: the last
|
||||
// SharedPtr has already dropped by the time this runs, and the lifetime id is what the
|
||||
// slot allocator resolves the handle from. Returns whether delete_vertex_elements went
|
||||
// out, i.e. whether the applier actually held a record - see the definition for why that
|
||||
// is asked rather than assumed.
|
||||
Bool MGPipeEmitVertexElementsDestroyAndFree(Uint64 lifetimeId);
|
||||
|
||||
void MGPipeEmitResourceCreate(MG_State::GLState::BufferObject& buffer);
|
||||
void MGPipeEmitResourceRespecify(MG_State::GLState::BufferObject& buffer);
|
||||
void MGPipeEmitResourceSubData(MG_State::GLState::BufferObject& buffer, SizeT offset, SizeT size);
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
|
||||
#include "VertexArrayObject.h"
|
||||
|
||||
#include <MG_State/GLState/StateObjectDeathNotice.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
|
||||
@@ -42,15 +40,24 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
VertexArrayObject::~VertexArrayObject() {
|
||||
// P2 step e2: ANNOUNCE the death instead of leaving the backend to discover it in a
|
||||
// garbage sweep. This is the last SharedPtr to this object dropping - not the
|
||||
// glDelete* that only marks the name and leaves a still-bound object very much
|
||||
// alive - so it is the exact moment the backend's twin, and the driver storage
|
||||
// that twin owns, stop being reachable. The notice carries the lifetime id
|
||||
// because the object no longer exists to be passed, and because the lifetime id
|
||||
// is what the client slot allocator resolves the handle from. No-op unless a
|
||||
// backend registered the ops (a pull build declares none at all).
|
||||
NotifyStateObjectDestroyed(MG_Pipe::MGPipeKind::VertexElementsCso, m_lifetimeId);
|
||||
// P2 step e2 / P3a C-1: ANNOUNCE the death instead of leaving the backend to discover
|
||||
// it in a garbage sweep, and RETURN THE CLIENT'S OWN SLOT while doing it. This is the
|
||||
// last SharedPtr to this object dropping - not the glDelete* that only marks the name
|
||||
// and leaves a still-bound object very much alive - so it is the exact moment the
|
||||
// backend's twin, the driver storage that twin owns, the applier's vertex-elements
|
||||
// record and the {slot, gen} that names all three stop being reachable.
|
||||
//
|
||||
// ALL FOUR OF THOSE GO THROUGH ONE HELPER, and it is the client's rather than a
|
||||
// backend's: the handle is minted by MGPipeVertexInputEmitter on every backend, so a
|
||||
// death path that only exists inside a backend's death-ops table is no path at all
|
||||
// under a backend that installs none - which is what DirectVulkan/Magma does on
|
||||
// purpose, and what made every VAO leak a slot and a ~1.3 KB applier record for the
|
||||
// life of the process on the shipped mask. The helper emits delete_vertex_elements,
|
||||
// raises the notice (a no-op unless a backend registered the ops) and frees the slot,
|
||||
// in that fixed order; MG_Pipe/PipeMutation.h and its definition say why each position
|
||||
// is where it is. The buffer's death has exactly this shape one file over
|
||||
// (BufferObject.cpp -> MGPipeEmitResourceDestroyAndFree).
|
||||
MG_Pipe::MGPipeEmitVertexElementsDestroyAndFree(m_lifetimeId);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
Reference in New Issue
Block a user